diff --git a/application/editormode.cpp b/application/editormode.cpp index 51b8e082..6d52174b 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1506,6 +1506,8 @@ bool editor_mode::update() background->PushClipRect(ImVec2(editor_ui::view_min().x, editor_ui::view_min().y), ImVec2(editor_ui::view_max().x, editor_ui::view_max().y), true); draw_orthophoto(); draw_nmt_pending(); + area_update(); + draw_download_area(); // --- ImGuizmo: in-viewport transform gizmo for the selected node --- render_gizmo(); @@ -2288,6 +2290,11 @@ void editor_mode::load_orthophoto_settings() } m_orthophoto.settings(config); m_orthophoto_origin_edit = {config.north, config.east}; + // the area the imagery and the terrain model are downloaded in, none in a scenery which has none + m_orthophoto_area_mark = scene::Layers.empty() ? std::vector{} : scene::Layers.marked(scene::layer_handle{1}, "//$a"); + m_orthophoto.area(editor_orthophoto::read_area(m_orthophoto_area_mark)); + if (false == m_orthophoto.area().empty()) + WriteLog(format("Editor: the download area of the scenery, %zu points, %d tiles of the orthophoto", m_orthophoto.area().size(), m_orthophoto.stats().area_tiles)); } void editor_mode::save_orthophoto_settings() @@ -2384,7 +2391,7 @@ void editor_mode::render_orthophoto_ui() }; auto const stats = m_orthophoto.stats(); - if (stats.failed > 0) + if (stats.failed > 0 || stats.area_failed > 0) { ImGui::Text(STR_C("Tiles: %d shown, %d loading, %d failed"), stats.resident, stats.loading, stats.failed); ImGui::SameLine(); @@ -2392,6 +2399,10 @@ void editor_mode::render_orthophoto_ui() m_orthophoto.retry_failed(); } + ImGui::Separator(); + render_download_area_ui(stats); + ImGui::Separator(); + if (ImGui::TreeNode(STR_C("More settings"))) { if (ImGui::Checkbox(STR_C("Fit to terrain"), &config.drape)) @@ -3055,6 +3066,11 @@ bool editor_mode::save() m_orthophoto_mark = map; scene::Layers.mark(scene::layer_handle{1}, "//$o", {map}); } + if (auto const area{editor_orthophoto::area_lines(m_orthophoto.area())}; false == m_orthophoto_scenery.empty() && area != m_orthophoto_area_mark) + { + m_orthophoto_area_mark = area; + scene::Layers.mark(scene::layer_handle{1}, "//$a", area); + } auto const terrainadded{false == rootstatements.empty() || false == statements.empty()}; auto const controls{turntable_controls(rootstatements)}; auto const noses{nose_controls(rootstatements)}; @@ -6181,6 +6197,8 @@ void editor_mode::enter() EditorModeFlag = true; load_orthophoto_settings(); + m_orthophoto.area_paused(m_area_paused); + m_nmt.prefetch_paused(m_area_paused); Application.set_cursor(GLFW_CURSOR_NORMAL); } @@ -6211,6 +6229,9 @@ void editor_mode::exit() m_fill_last.clear(); m_orthophoto.cancel_pending(); m_orthophoto.detach_scene(); // the layer is an editor aid, keep it out of the other modes + // the download of the area held while the scenery is driven, it goes on once the editor is back + m_orthophoto.area_paused(true); + m_nmt.prefetch_paused(true); scene::Layers.show_all(); // likewise for hidden scenery layers, the other modes get the scenery as it was defined // drop selection so a stale/dangling node pointer isn't used on the next editor session @@ -6484,6 +6505,12 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in // an open list or menu takes this Esc for itself, the selection behind it stays if (ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel)) break; + // the download area being drawn given up, the one before stays + if (is_press(Action) && m_area_drawing.active) + { + area_finish(false); + break; + } if (is_press(Action)) { road_cancel(); @@ -6526,6 +6553,13 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in break; case GLFW_KEY_BACKSPACE: + // the download area: the last point taken back + if (is_press(Action) && m_area_drawing.active) + { + if (false == m_area_drawing.points.empty()) + m_area_drawing.points.pop_back(); + break; + } // area fill: remove the last outline point if (is_press(Action) && ui()->mode() == nodebank_panel::FILL && !m_fill_points.empty()) m_fill_points.pop_back(); @@ -6540,6 +6574,13 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in case GLFW_KEY_ENTER: case GLFW_KEY_KP_ENTER: + // the download area closed, from three points on + if (is_press(Action) && m_area_drawing.active) + { + if (m_area_drawing.points.size() >= 3) + area_finish(true); + break; + } if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && m_lay.active) lay_enter(); if (is_press(Action) && m_route.trace.active && route_active()) @@ -6600,6 +6641,17 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const return; } + // the download area drawn on the map takes the left button while it's drawn + if (m_area_drawing.active && Button == GLFW_MOUSE_BUTTON_LEFT) + { + if (is_press(Action)) + area_click(Global.shiftState); + else + m_area_drawing.dragged = -1; + m_input.mouse.button(Button, Action); + return; + } + // the chunk, spacing and water tools of the terrain work on a click (Shift: the other way round) if (terrain_clicking() && Button == GLFW_MOUSE_BUTTON_LEFT) { diff --git a/application/editormode.h b/application/editormode.h index 6cfa504f..fd99b1b4 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -471,6 +471,20 @@ class editor_mode : public application_mode, private editor_track::observer, pri // the chunks waiting for the terrain model and the tiles of the imagery asked for outlined where they will be, so it's seen // what's on its way void draw_nmt_pending() const; + // the download area: a polygon drawn on the map, the boundary of what the orthophoto and the terrain model download in the + // background, none of them asking for anything outside it (application/editormodenmt.cpp) + void area_start(); + // Keep: the area drawn taken, else the one before stays + void area_finish(bool const Keep); + // a press of the left button while drawing: a point added, or the one under the cursor taken to drag, Shift: removed + void area_click(bool const Shift); + void area_update(); + // a point of the area (x, z) where it's drawn: over the map of the top view, on the ground in the 3d view + glm::dvec3 area_point(glm::dvec2 const &Point) const; + void draw_download_area() const; + void render_download_area_ui(editor_orthophoto::statistics const &Stats); + // the chunk of the terrain (x, z) touches the area, or there's none + bool in_download_area(glm::dvec2 const &Low, glm::dvec2 const &High) const; // over the 3d view while the terrain panel isn't seen void render_nmt_banner(); bool nmt_busy() const; @@ -712,6 +726,16 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::size_t m_ortho_waiting{0}; // squares of the terrain model asked for and not in yet, the same std::string m_orthophoto_scenery; // scenery whose origin is loaded std::string m_orthophoto_mark; + std::vector m_orthophoto_area_mark; // the lines of the download area as the scenery keeps them + // the download area being drawn on the map: its points in the world (x, z), the one dragged + struct area_drawing + { + bool active{false}; + std::vector points; + int dragged{-1}; + } m_area_drawing; + std::vector m_nmt_prefetched; // the download area the terrain model was given + bool m_area_paused{false}; // the download of the area held by the button; out of the editor it's held anyway glm::dvec2 m_orthophoto_origin_edit{0.0}; // origin being typed in (northing, easting); applied when editing ends // hierarchy management diff --git a/application/editormodenmt.cpp b/application/editormodenmt.cpp index 9e9cb618..7aa8182b 100644 --- a/application/editormodenmt.cpp +++ b/application/editormodenmt.cpp @@ -33,6 +33,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include namespace { @@ -511,7 +512,12 @@ std::vector editor_mode::nmt_keys_around() const auto const first{-(m_terrain_add_count - 1) / 2}; for (int dz = 0; dz < m_terrain_add_count; ++dz) for (int dx = 0; dx < m_terrain_add_count; ++dx) - result.push_back({centre.first + first + dx, centre.second + first + dz}); + { + heightmap::chunk_key const key{centre.first + first + dx, centre.second + first + dz}; + // none outside the download area + if (auto const low{heightmap::chunk_corner(key)}; in_download_area(low, low + glm::dvec2{heightmap::chunk_size})) + result.push_back(key); + } return result; } @@ -529,7 +535,7 @@ std::vector editor_mode::nmt_keys_along_tracks() const auto const low{heightmap::chunk_corner(key)}; // the chunk reaches within the band of the track glm::dvec2 const nearest{std::clamp(place.position.x, low.x, low.x + heightmap::chunk_size), std::clamp(place.position.z, low.y, low.y + heightmap::chunk_size)}; - if (glm::distance(nearest, glm::dvec2{place.position.x, place.position.z}) <= m_nmt_state.band) + if (glm::distance(nearest, glm::dvec2{place.position.x, place.position.z}) <= m_nmt_state.band && in_download_area(low, low + glm::dvec2{heightmap::chunk_size})) keys.insert(key); } } @@ -890,6 +896,12 @@ std::size_t editor_mode::nmt_shape_tracks(std::vector const *Only) void editor_mode::nmt_update() { auto &state{m_nmt_state}; + // the squares of the model in the download area to the disk cache, where the scenery is placed and its terrain is there + if (auto const wanted{m_nmt.placed() && m_streamer.active() ? m_orthophoto.area() : std::vector{}}; wanted != m_nmt_prefetched) + { + m_nmt_prefetched = wanted; + m_nmt.prefetch(wanted); + } nmt_offer(); nmt_follow_tracks(); if (state.stage != nmt_state::automatic::idle) @@ -1007,6 +1019,9 @@ void editor_mode::nmt_grow() if (false == laying && false == m_streamer.active()) return; auto const at{m_extend.active ? m_extend.mouse : m_lay.active ? m_lay.mouse : glm::dvec3{Camera.Pos}}; + // nothing asked for outside the download area + if (false == in_download_area({at.x - 100.0, at.z - 100.0}, {at.x + 100.0, at.z + 100.0})) + return; // the model under it, for the heights of the points laid where there's no terrain yet, as over the base map m_nmt.request({at.x - 100.0, at.z - 100.0}, {at.x + 100.0, at.z + 100.0}); if (m_streamer.resident() == 0) @@ -1023,7 +1038,8 @@ void editor_mode::nmt_grow() auto const low{heightmap::chunk_corner(key)}; glm::dvec2 const nearest{std::clamp(cursor.x, low.x, low.x + heightmap::chunk_size), std::clamp(cursor.y, low.y, low.y + heightmap::chunk_size)}; auto const distance{glm::distance(nearest, cursor)}; - if (distance <= grow_reach && false == m_streamer.exists(key) && false == state.given_up.contains(key) && std::find(state.pending.begin(), state.pending.end(), key) == state.pending.end()) + if (distance <= grow_reach && false == m_streamer.exists(key) && false == state.given_up.contains(key) && std::find(state.pending.begin(), state.pending.end(), key) == state.pending.end() && + in_download_area(low, low + glm::dvec2{heightmap::chunk_size})) missing.emplace_back(distance, key); } if (missing.empty()) @@ -1359,6 +1375,202 @@ void editor_mode::draw_nmt_pending() const square(low, high, 90, 170, 255, STR("Terrain model loading")); } +// --------------------------------------------------------------------------------------------- +// the download area + +void editor_mode::area_start() +{ + auto &drawing{m_area_drawing}; + drawing = {}; + drawing.active = true; + // the area there is taken up to be changed + for (auto const &point : m_orthophoto.area()) + drawing.points.push_back(m_orthophoto.to_world(point)); + // drawn on the map of the top view + if (false == m_orthophoto.enabled()) + m_orthophoto.enabled(true); + if (false == Global.EditorOrtho) + toggle_ortho(); + WriteLog("Editor: drawing the download area"); +} + +void editor_mode::area_finish(bool const Keep) +{ + auto &drawing{m_area_drawing}; + if (Keep && drawing.points.size() >= 3) + { + std::vector polygon; + for (auto const &point : drawing.points) + polygon.push_back(m_orthophoto.to_puwg(point)); + m_orthophoto.area(std::move(polygon)); + WriteLog(format("Editor: the download area of %zu points, %d tiles of the orthophoto", drawing.points.size(), m_orthophoto.stats().area_tiles)); + } + drawing = {}; +} + +glm::dvec3 editor_mode::area_point(glm::dvec2 const &Point) const +{ + double height{Camera.Pos.y - 2.0}; + if (false == Global.EditorOrtho && false == m_streamer.height_at(Point.x, Point.y, height) && false == nmt_ground(Point.x, Point.y, height)) + height = 0.0; + return {Point.x, height, Point.y}; +} + +void editor_mode::area_click(bool const Shift) +{ + auto &drawing{m_area_drawing}; + screen_projection const projection; + // the point under the cursor, within 10 pixels + int nearest{-1}; + auto best{100.0f}; + for (std::size_t i = 0; i < drawing.points.size(); ++i) + if (auto const distance{projection.mouse_distance2(area_point(drawing.points[i]))}; distance < best) + { + best = distance; + nearest = static_cast(i); + } + if (nearest >= 0) + { + if (Shift) + drawing.points.erase(drawing.points.begin() + nearest); + else + drawing.dragged = nearest; + return; + } + if (Shift) + return; + auto const cursor{std::as_const(*this).cursor_ground()}; + drawing.points.emplace_back(cursor.x, cursor.z); +} + +void editor_mode::area_update() +{ + auto &drawing{m_area_drawing}; + if (false == drawing.active || drawing.dragged < 0) + return; + if (false == ImGui::IsMouseDown(0) || drawing.dragged >= static_cast(drawing.points.size())) + { + drawing.dragged = -1; + return; + } + auto const cursor{std::as_const(*this).cursor_ground()}; + drawing.points[drawing.dragged] = {cursor.x, cursor.z}; +} + +void editor_mode::draw_download_area() const +{ + auto const &drawing{m_area_drawing}; + std::vector points; + if (drawing.active) + points = drawing.points; + else + for (auto const &point : m_orthophoto.area()) + points.push_back(m_orthophoto.to_world(point)); + // shown while it's drawn, with the settings of the orthophoto open, and over the map of the top view + if (points.empty() || (false == drawing.active && false == m_orthophoto_window && false == Global.EditorOrthoMap)) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList()}; + // the tiles of the area as they are: in the disk cache, failed; the ones waiting left clear + if (false == drawing.active && m_orthophoto_window) + for (auto const &square : m_orthophoto.area_squares()) + { + if (square.state == 0) + continue; + std::array corners; + auto seen{true}; + glm::dvec2 const at[]{square.low, {square.high.x, square.low.y}, square.high, {square.low.x, square.high.y}}; + for (int i = 0; i < 4 && seen; ++i) + seen = projection.project(area_point(at[i]), corners[i]); + if (seen) + drawlist->AddQuadFilled(corners[0], corners[1], corners[2], corners[3], square.state == 1 ? IM_COL32(90, 220, 120, 45) : IM_COL32(240, 80, 70, 80)); + } + auto const edge{IM_COL32(80, 220, 255, 230)}; + auto const closing{IM_COL32(80, 220, 255, drawing.active ? 110 : 230)}; + // the edge back to the first point from three points on + auto const edges{points.size() >= 3 ? points.size() : points.size() - 1}; + for (std::size_t i = 0; i < edges; ++i) + { + auto const last{i + 1 == points.size()}; + projection.line(drawlist, area_point(points[i]), area_point(points[(i + 1) % points.size()]), last ? closing : edge, 2.0f); + } + if (false == drawing.active) + return; + // the points, the one under the cursor larger, and the line on to the cursor + ImVec2 screen; + for (auto const &point : points) + if (projection.project(area_point(point), screen)) + drawlist->AddCircleFilled(screen, projection.mouse_distance2(area_point(point)) < 100.0f ? 6.0f : 4.0f, edge); + if (false == points.empty() && drawing.dragged < 0 && false == ImGui::GetIO().WantCaptureMouse) + { + auto const cursor{std::as_const(*this).cursor_ground()}; + projection.line(drawlist, area_point(points.back()), area_point({cursor.x, cursor.z}), closing, 1.5f); + } +} + +bool editor_mode::in_download_area(glm::dvec2 const &Low, glm::dvec2 const &High) const +{ + auto const &area{m_orthophoto.area()}; + if (area.empty()) + return true; + // the square in PUWG: the easting grows to the west of the scenery, the northing with z + auto const &config{m_orthophoto.settings()}; + return geometry::square_touches(area, config.east - High.x, config.north + Low.y, High.x - Low.x); +} + +void editor_mode::render_download_area_ui(editor_orthophoto::statistics const &Stats) +{ + auto &drawing{m_area_drawing}; + ImGui::TextUnformatted(STR_C("Download area")); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("A polygon drawn on the map: what the settings ask for inside it, the orthophoto and the terrain model,\n" + "goes to the disk cache in the background, and nothing outside it is downloaded, also around the camera")); + if (drawing.active) + { + ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f), "%s", STR_C("Click the points of the area on the map. Enter closes it, Backspace takes the last point back, Esc gives up;\n" + "a point dragged moves, Shift+click removes it")); + ImGui::Text(STR_C("%zu points"), drawing.points.size()); + ImGui::SameLine(); + if (drawing.points.size() >= 3 && ImGui::Button(STR_C("Done"))) + area_finish(true); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Cancel"))) + area_finish(false); + return; + } + auto const &area{m_orthophoto.area()}; + if (ImGui::Button(area.empty() ? STR_C("Draw on the map") : STR_C("Draw again"))) + area_start(); + if (area.empty()) + { + ImGui::SameLine(); + ImGui::TextDisabled("%s", STR_C("none: the tiles around the camera, wherever it is")); + return; + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Remove"))) + m_orthophoto.area({}); + ImGui::Text(STR_C("Area: %d of %d tiles in the cache, %d failed"), Stats.area_cached, Stats.area_tiles, Stats.area_failed); + ImGui::SameLine(); + if (ImGui::SmallButton(m_area_paused ? STR_C("Resume") : STR_C("Pause"))) + { + m_area_paused = false == m_area_paused; + m_orthophoto.area_paused(m_area_paused); + m_nmt.prefetch_paused(m_area_paused); + } + if (auto const terrain{m_nmt.prefetched()}; terrain.total > 0) + { + ImGui::Text(STR_C("Terrain model: %d of %d squares in the cache, %d failed"), terrain.cached, terrain.total, terrain.failed); + // the ones there are only looked up again + if (terrain.failed > 0) + { + ImGui::SameLine(); + if (ImGui::SmallButton((std::string{STR_C("Retry")} + "##terrainarea").c_str())) + m_nmt.prefetch(m_nmt_prefetched); + } + } +} + void editor_mode::render_nmt_banner() { auto const &state{m_nmt_state}; diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index c4bc00d7..431f25a7 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -124,6 +124,8 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c // terrainturn the textures of the palette entry turned // terrainwater [size] material and repeat of a body of water; terrainwaterdefault // orthophoto [in scene 0|1] the imagery layer on, placed and fitted as given +// orthoarea ... | clear the download area through the points of the world, as drawn on the map; orthoareapause 0|1 +// orthohires 0|1 the 4K tiles on or off; orthostats logs the tiles, the area and the requests made // join [radius] [variant] [transitions 0|1] the free ends at the points joined, as a drag from one to the other // layto from the free end at the first point towards the second, as a drag in the layout // layclicks ... clicks of the lay tool at the points, a free end within 3 m joined diff --git a/application/editormodeselftestterrain.cpp b/application/editormodeselftestterrain.cpp index e3ff2889..55287a2a 100644 --- a/application/editormodeselftestterrain.cpp +++ b/application/editormodeselftestterrain.cpp @@ -73,6 +73,46 @@ editor_mode::selftest_result editor_mode::selftest_terrain(std::string const &co WriteLog(format("SELFTEST orthophoto at %.0f %.0f, radius %d, %s%s", config.north, config.east, config.radius, config.drape ? "fitted" : "flat", config.in_scene ? ", in the scene" : "")); return selftest_result::handled; } + if (command == "orthoarea") + { + // the download area through the points of the world (x, z), as drawn on the map; clear: none + std::vector polygon; + if (words.str().find(" clear") == std::string::npos) + for (glm::dvec2 point{0.0}; words >> point.x >> point.y;) + polygon.push_back(m_orthophoto.to_puwg(point)); + m_orthophoto.area(polygon); + WriteLog(format("SELFTEST orthoarea: %zu points, %d tiles", m_orthophoto.area().size(), m_orthophoto.stats().area_tiles)); + return selftest_result::handled; + } + if (command == "orthoareapause") + { + int paused{1}; + words >> paused; + m_area_paused = paused != 0; + m_orthophoto.area_paused(m_area_paused); + m_nmt.prefetch_paused(m_area_paused); + WriteLog(format("SELFTEST orthoareapause %d", paused)); + return selftest_result::handled; + } + if (command == "orthohires") + { + int hires{1}; + words >> hires; + auto config{m_orthophoto.settings()}; + config.hires = hires != 0; + m_orthophoto.settings(config); + WriteLog(format("SELFTEST orthohires %d", hires)); + return selftest_result::handled; + } + if (command == "orthostats") + { + auto const stats{m_orthophoto.stats()}; + auto const terrain{m_nmt.prefetched()}; + WriteLog(format("SELFTEST orthostats: shown %d, loading %d, failed %d; area %d of %d in the cache, %d failed%s; requests %d, uploads %d, stale %d; terrain area %d of %d, %d failed", + stats.resident, stats.loading, stats.failed, stats.area_cached, stats.area_tiles, stats.area_failed, stats.area_paused ? ", paused" : "", stats.requests, stats.uploads, stats.stale, terrain.cached, + terrain.total, terrain.failed)); + return selftest_result::handled; + } if (command == "nmtfit") { // the heights of the terrain model fitted to the tracks, as the button of the terrain panel does diff --git a/editor/editorGeometry.hpp b/editor/editorGeometry.hpp index 6c2e6365..fedb3ff8 100644 --- a/editor/editorGeometry.hpp +++ b/editor/editorGeometry.hpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include #include @@ -240,4 +241,67 @@ inline glm::dvec3 level(glm::dvec3 const &Vector) return glm::length(flat) > 1e-9 ? glm::normalize(flat) : glm::dvec3{0.0, 0.0, 1.0}; } +// a point inside the polygon, by the number of its edges a ray along +x from it crosses +inline bool point_in_polygon(std::vector const &Polygon, glm::dvec2 const &Point) +{ + bool result{false}; + for (std::size_t i = 0, j = Polygon.size() - 1; i < Polygon.size(); j = i++) + { + auto const &a{Polygon[i]}; + auto const &b{Polygon[j]}; + if ((a.y > Point.y) != (b.y > Point.y) && Point.x < a.x + (b.x - a.x) * (Point.y - a.y) / (b.y - a.y)) + result = !result; + } + return result; +} + +// the square of the side from (West, South), the axes the polygon's, touches the polygon: a corner of it inside the polygon, a +// vertex of the polygon inside it, or an edge crossing an edge. A side of 0: the point inside the polygon +inline bool square_touches(std::vector const &Polygon, double const West, double const South, double const Size) +{ + if (Polygon.size() < 3) + return false; + glm::dvec2 const corners[]{{West, South}, {West + Size, South}, {West + Size, South + Size}, {West, South + Size}}; + for (auto const &corner : corners) + if (point_in_polygon(Polygon, corner)) + return true; + if (Size <= 0.0) + return false; + for (auto const &point : Polygon) + if (point.x >= West && point.x <= West + Size && point.y >= South && point.y <= South + Size) + return true; + // two segments crossing, by the sides each one's ends lie on of the other + auto const side = [](glm::dvec2 const &A, glm::dvec2 const &B, glm::dvec2 const &P) { return (B.x - A.x) * (P.y - A.y) - (B.y - A.y) * (P.x - A.x); }; + auto const crossing = [&](glm::dvec2 const &A, glm::dvec2 const &B, glm::dvec2 const &C, glm::dvec2 const &D) { + return ((side(A, B, C) > 0.0) != (side(A, B, D) > 0.0)) && ((side(C, D, A) > 0.0) != (side(C, D, B) > 0.0)); + }; + for (std::size_t i = 0, j = Polygon.size() - 1; i < Polygon.size(); j = i++) + for (int k = 0; k < 4; ++k) + if (crossing(Polygon[j], Polygon[i], corners[k], corners[(k + 1) % 4])) + return true; + return false; +} + +// the squares of a grid of the size, from 0 along both axes, the polygon touches, by their indices along the axes +inline std::vector> squares_in(std::vector const &Polygon, double const Size) +{ + std::vector> result; + if (Polygon.size() < 3 || Size <= 0.0) + return result; + glm::dvec2 low{Polygon.front()}; + glm::dvec2 high{Polygon.front()}; + for (auto const &point : Polygon) + { + low = glm::min(low, point); + high = glm::max(high, point); + } + auto const first{glm::ivec2{static_cast(std::floor(low.x / Size)), static_cast(std::floor(low.y / Size))}}; + auto const last{glm::ivec2{static_cast(std::floor(high.x / Size)), static_cast(std::floor(high.y / Size))}}; + for (int y = first.y; y <= last.y; ++y) + for (int x = first.x; x <= last.x; ++x) + if (square_touches(Polygon, x * Size, y * Size, Size)) + result.emplace_back(x, y); + return result; +} + } // namespace geometry diff --git a/editor/editorNmt.cpp b/editor/editorNmt.cpp index a71e0e2a..b57f7e90 100644 --- a/editor/editorNmt.cpp +++ b/editor/editorNmt.cpp @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "editor/editorNmt.hpp" #include "editor/editorHttp.hpp" +#include "editor/editorGeometry.hpp" #include "utilities/Logs.h" #include "utilities/translation.h" @@ -262,15 +263,60 @@ bool editor_nmt::request(glm::dvec2 const &Min, glm::dvec2 const &Max) m_queued.insert(square); m_queue.push_back(square); } - if (false == wanted.empty() && m_workers.empty()) - for (int i = 0; i < worker_count; ++i) - m_workers.emplace_back([this]() { work(); }); + if (false == wanted.empty()) + start_workers(); } if (false == wanted.empty()) m_wake.notify_all(); return all; } +void editor_nmt::start_workers() +{ + if (m_workers.empty()) + for (int i = 0; i < worker_count; ++i) + m_workers.emplace_back([this]() { work(); }); +} + +std::string editor_nmt::file_of(key const &Key) const +{ + return std::string{cache_folder} + std::to_string(Key.first) + "_" + std::to_string(Key.second) + ".tif"; +} + +void editor_nmt::prefetch(std::vector const &Polygon) +{ + std::vector keys; + if (can_download()) + for (auto const &square : geometry::squares_in(Polygon, square_size)) + keys.emplace_back(square); + { + std::lock_guard lock{m_mutex}; + m_prefetch = std::move(keys); + m_prefetch_next = 0; + m_prefetch_cached.clear(); + m_prefetch_failed.clear(); + ++m_prefetch_generation; + if (false == m_prefetch.empty()) + start_workers(); + } + m_wake.notify_all(); +} + +void editor_nmt::prefetch_paused(bool const Paused) +{ + { + std::lock_guard lock{m_mutex}; + m_prefetch_paused = Paused; + } + m_wake.notify_all(); +} + +editor_nmt::prefetch_state editor_nmt::prefetched() const +{ + std::lock_guard lock{m_mutex}; + return {static_cast(m_prefetch_cached.size()), static_cast(m_prefetch.size()), static_cast(m_prefetch_failed.size())}; +} + std::size_t editor_nmt::waiting() const { std::lock_guard lock{m_mutex}; @@ -338,13 +384,37 @@ void editor_nmt::work() while (true) { key square; + bool prefetching{false}; + std::uint32_t generation{0}; { std::unique_lock lock{m_mutex}; - m_wake.wait(lock, [this]() { return m_stop || false == m_queue.empty(); }); + auto const prefetchable = [this]() { return false == m_prefetch_paused && m_prefetch_next < m_prefetch.size(); }; + m_wake.wait(lock, [&]() { return m_stop || false == m_queue.empty() || prefetchable(); }); if (m_stop) return; - square = m_queue.front(); - m_queue.pop_front(); + if (false == m_queue.empty()) + { + square = m_queue.front(); + m_queue.pop_front(); + } + else + { + // a square of the download area, while none is asked for + square = m_prefetch[m_prefetch_next++]; + prefetching = true; + generation = m_prefetch_generation; + } + } + if (prefetching) + { + // put into the disk cache only, the file there not read + std::error_code fault; + std::string error; + auto const ok{fs::exists(file_of(square), fault) || false == fetch(square, error)->failed}; + std::lock_guard lock{m_mutex}; + if (generation == m_prefetch_generation) + (ok ? m_prefetch_cached : m_prefetch_failed).insert(square); + continue; } std::string error; auto fetched{fetch(square, error)}; @@ -363,7 +433,7 @@ std::shared_ptr editor_nmt::fetch(key const &Key, std: auto result{std::make_shared()}; std::error_code fault; fs::create_directories(cache_folder, fault); - auto const file{std::string{cache_folder} + std::to_string(Key.first) + "_" + std::to_string(Key.second) + ".tif"}; + auto const file{file_of(Key)}; std::vector data; if (std::ifstream cached{file, std::ios::binary}; cached.is_open()) { diff --git a/editor/editorNmt.hpp b/editor/editorNmt.hpp index 9d8fe330..108bf4a1 100644 --- a/editor/editorNmt.hpp +++ b/editor/editorNmt.hpp @@ -59,6 +59,18 @@ class editor_nmt void clear(); // whether this build can fetch from the service at all static bool can_download(); + // the squares touching the polygon of PUWG 1992 (easting, northing) put into the disk cache in the background, while no + // square is asked for; none of them kept in the memory. Empty: none + void prefetch(std::vector const &Polygon); + void prefetch_paused(bool const Paused); + // of the squares of the polygon: in the disk cache, all of them, failed + struct prefetch_state + { + int cached{0}; + int total{0}; + int failed{0}; + }; + prefetch_state prefetched() const; // side of a square of the grid, m static double constexpr square_size{500.0}; @@ -93,4 +105,13 @@ class editor_nmt double m_north{0.0}; double m_east{0.0}; bool m_stop{false}; + // the squares of the download area for the disk cache + std::vector m_prefetch; + std::size_t m_prefetch_next{0}; + std::set m_prefetch_cached; + std::set m_prefetch_failed; + std::uint32_t m_prefetch_generation{0}; + bool m_prefetch_paused{false}; + void start_workers(); // under the lock + std::string file_of(key const &Key) const; }; diff --git a/editor/editorOrthophoto.cpp b/editor/editorOrthophoto.cpp index b8118a91..cbaad9d1 100644 --- a/editor/editorOrthophoto.cpp +++ b/editor/editorOrthophoto.cpp @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "editor/editorOrthophoto.hpp" #include "editor/editorHttp.hpp" +#include "editor/editorGeometry.hpp" #include "rendering/renderer.h" #include "scene/scene.h" @@ -30,6 +31,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include @@ -43,7 +45,10 @@ constexpr int probe_pixels{64}; // coverage test of the high-resolution pro constexpr int map_pixels{1024}; // a side of a tile of the map, whatever its size in metres constexpr int map_stamp{32}; // rows under a tile of the map the service writes its credit into, cut off constexpr char const *map_service{"https://mapy.geoportal.gov.pl/wss/ext/OSM/BaseMap/service"}; -constexpr int worker_count{3}; // concurrent requests; the service is slow but shouldn't be hammered +constexpr int worker_count{8}; // concurrent requests; the service is slow but shouldn't be hammered +// of the workers at most that many on the tiles around the camera, each holding an image decoded (a 4K one is 64 MB); the rest only +// download the tiles of the area to the disk cache +constexpr std::size_t camera_workers{3}; constexpr std::size_t upload_budget{16u << 20}; // bytes of texture data uploaded per frame (always at least one tile) constexpr std::size_t vertex_budget{60000}; // ImGui uses 16-bit indices and not every backend supports vertex offsets constexpr float near_w{0.5f}; @@ -182,6 +187,9 @@ void write_file(fs::path const &Path, std::vector const &Data) fs::remove(temporary, ec); } +// the requests made of the services, the disk cache left out +std::atomic requests_made{0}; + // cached file if present, otherwise downloads (with a few retries, the service drops connections and answers 404 now and then) bool fetch(http_client &Client, std::string const &Url, fs::path const &Path, std::vector &Out, std::string &Error, stop_check const &Stop) { @@ -190,6 +198,7 @@ bool fetch(http_client &Client, std::string const &Url, fs::path const &Path, st for (int attempt = 0; attempt < 4; ++attempt) { + ++requests_made; if (Stop()) { Error = "cancelled"; @@ -261,8 +270,20 @@ bool is_blank(image const &Image) return true; } +// a file of no content standing for a place the standard imagery came blank for twice: none there, not asked for again +fs::path blank_file(tile_request const &Request) +{ + return cache_file(Request, false, std::to_string(standard_pixels) + ".blank"); +} + bool load_standard(http_client &Client, tile_request const &Request, image &Out, std::string &Error, stop_check const &Stop) { + std::error_code ec; + if (fs::exists(blank_file(Request), ec)) + { + Out = image{}; + return true; + } std::vector data; auto const path{cache_file(Request, false, std::to_string(standard_pixels) + ".jpg")}; for (int attempt = 0; attempt < 2; ++attempt) @@ -271,14 +292,42 @@ bool load_standard(http_client &Client, tile_request const &Request, image &Out, return false; if (!is_blank(Out)) return true; - std::error_code ec; fs::remove(path, ec); } + write_file(blank_file(Request), {}); return true; } -// returns false on failure (to be retried later); Empty is set when the place has no imagery -bool load_tile(http_client &Client, tile_request const &Request, image &Out, bool &Empty, std::string &Error, stop_check const &Stop) +// the files of a tile the settings ask for put into the disk cache, downloaded where missing; of the images only the probe of the +// high-resolution product and a standard one just downloaded are decoded, to tell where there's imagery. false on failure +bool prefetch_tile(http_client &Client, tile_request const &Request, std::string &Error, stop_check const &Stop) +{ + std::error_code ec; + if (false == fs::exists(cache_file(Request, false, std::to_string(standard_pixels) + ".jpg"), ec) && false == fs::exists(blank_file(Request), ec)) + { + image standard; + if (!load_standard(Client, Request, standard, Error, Stop)) + return false; + } + if (false == Request.hires) + return true; + std::vector data; + image probe; + if (!fetch(Client, wms_url(Request, true, probe_pixels, true), cache_file(Request, true, "probe.png"), data, Error, Stop) || !decode(data, probe, Error)) + return false; + std::size_t covered{0}; + for (std::size_t i = 3; i < probe.rgba.size(); i += 4) + covered += (probe.rgba[i] != 0); + if (covered == 0) + return true; + auto const png{covered < probe.rgba.size() / 4}; + auto const path{cache_file(Request, true, std::to_string(hires_pixels) + (png ? ".png" : ".jpg"))}; + return fs::exists(path, ec) || fetch(Client, wms_url(Request, true, hires_pixels, png), path, data, Error, Stop); +} + +// returns false on failure (to be retried later); Empty is set when the place has no imagery. Superseded: the tile wants another image +// now, the large one downloaded isn't decoded for nothing +bool load_tile(http_client &Client, tile_request const &Request, image &Out, bool &Empty, std::string &Error, stop_check const &Stop, stop_check const &Superseded) { Empty = false; if (Request.hires && Request.level >= hires_pixels) @@ -293,14 +342,20 @@ bool load_tile(http_client &Client, tile_request const &Request, image &Out, boo covered += (probe.rgba[i] != 0); std::size_t const total = probe.rgba.size() / 4; + auto const superseded = [&]() { + if (false == Superseded()) + return false; + Error = "superseded"; + return true; + }; if (covered == total) { - return fetch(Client, wms_url(Request, true, hires_pixels, false), cache_file(Request, true, std::to_string(hires_pixels) + ".jpg"), data, Error, Stop) && decode(data, Out, Error); + return fetch(Client, wms_url(Request, true, hires_pixels, false), cache_file(Request, true, std::to_string(hires_pixels) + ".jpg"), data, Error, Stop) && false == superseded() && decode(data, Out, Error); } if (covered > 0) { // edge of the covered area: transparent png, holes filled from the standard imagery - if (!fetch(Client, wms_url(Request, true, hires_pixels, true), cache_file(Request, true, std::to_string(hires_pixels) + ".png"), data, Error, Stop) || !decode(data, Out, Error)) + if (!fetch(Client, wms_url(Request, true, hires_pixels, true), cache_file(Request, true, std::to_string(hires_pixels) + ".png"), data, Error, Stop) || superseded() || !decode(data, Out, Error)) return false; image standard; std::string ignored; @@ -569,6 +624,7 @@ void editor_orthophoto::enabled(bool State) if (State == m_enabled) return; m_enabled = State; + restart_prefetch(); if (m_enabled) { start_workers(); @@ -608,6 +664,95 @@ void editor_orthophoto::settings(config const &Config) release_tile(entry.second); m_tiles.clear(); ++m_generation; + // the area in the cache once more, of the other product + restart_prefetch(); +} + +void editor_orthophoto::area(std::vector Polygon) +{ + if (Polygon.size() < 3) + Polygon.clear(); + m_area = std::move(Polygon); + m_area_tiles.clear(); + for (auto const &key : geometry::squares_in(m_area, tile_size)) + if (in_service_area(key.first, key.second)) + m_area_tiles.insert(key); + if (false == m_area.empty()) + { + // the jobs for the tiles outside it forgotten, the tiles shown let go by the next update + std::lock_guard lock(m_mutex); + m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.map == 0 && 0 == m_area_tiles.count(Job.key); }), m_queue.end()); + } + restart_prefetch(); +} + +bool editor_orthophoto::in_area(double const East, double const North) const +{ + return m_area.empty() || geometry::square_touches(m_area, East, North, 0.0); +} + +void editor_orthophoto::area_paused(bool const Paused) +{ + { + std::lock_guard lock(m_mutex); + m_prefetch_paused = Paused; + } + m_wakeup.notify_all(); +} + +void editor_orthophoto::restart_prefetch() +{ + std::vector keys; + if (m_enabled && can_download()) + keys.assign(m_area_tiles.begin(), m_area_tiles.end()); + auto const centre{m_camera_tile}; + auto const distance = [&](tile_key const &Key) { + auto const dx{static_cast(Key.first) - centre.first}; + auto const dy{static_cast(Key.second) - centre.second}; + return dx * dx + dy * dy; + }; + std::sort(keys.begin(), keys.end(), [&](tile_key const &A, tile_key const &B) { return distance(A) < distance(B); }); + { + std::lock_guard lock(m_mutex); + m_prefetch = std::move(keys); + m_prefetch_next = 0; + m_prefetch_failed.clear(); + m_prefetch_cached.clear(); + m_prefetch_source = source{m_config.year, m_config.hires}; + ++m_prefetch_generation; + } + m_wakeup.notify_all(); +} + +std::vector editor_orthophoto::area_lines(std::vector const &Polygon) +{ + std::vector lines; + if (Polygon.size() < 3) + return lines; + lines.emplace_back("# the area the editor downloads the orthophoto and the terrain model in, PUWG 1992 easting northing"); + for (auto const &point : Polygon) + { + char text[64]; + std::snprintf(text, sizeof(text), "point %.2f %.2f", point.x, point.y); + lines.emplace_back(text); + } + return lines; +} + +std::vector editor_orthophoto::read_area(std::vector const &Lines) +{ + std::vector polygon; + for (auto const &line : Lines) + { + std::istringstream words{line}; + std::string keyword; + glm::dvec2 point{0.0}; + if (words >> keyword && keyword == "point" && words >> point.x >> point.y) + polygon.push_back(point); + } + if (polygon.size() < 3) + polygon.clear(); + return polygon; } void editor_orthophoto::refit() @@ -690,11 +835,29 @@ void editor_orthophoto::retry_failed() entry.second.failures = 0; entry.second.retry_at = 0.0; } + // the tiles of the area which failed taken again + { + std::lock_guard lock(m_mutex); + for (auto const &key : m_prefetch_failed) + m_prefetch.push_back(key); + m_prefetch_failed.clear(); + } + m_wakeup.notify_all(); } editor_orthophoto::statistics editor_orthophoto::stats() const { statistics out; + out.area_tiles = static_cast(m_area_tiles.size()); + out.requests = requests_made.load(); + out.uploads = m_uploads; + out.stale = m_stale; + { + std::lock_guard lock(m_mutex); + out.area_cached = static_cast(m_prefetch_cached.size()); + out.area_failed = static_cast(m_prefetch_failed.size()); + out.area_paused = m_prefetch_paused; + } auto const count = [&](tile const &t) { if (t.level > 0 && !t.empty && t.slot.texture != null_handle) ++out.resident; @@ -816,6 +979,13 @@ std::size_t editor_orthophoto::apply(result &Result, double Now) } return 0; } + // an image the tile no longer wants, as a 4K one it asked for before the camera moved on, isn't sent to the graphics card: the + // one wanted now is on its way. A tile showing nothing yet takes it meanwhile + if (false == current && Result.map == 0 && t.level > 0) + { + ++m_stale; + return 0; + } t.failures = 0; t.level = Result.level; @@ -837,6 +1007,7 @@ std::size_t editor_orthophoto::apply(result &Result, double Now) } // a shape already in the scene shows the new content right away, it refers to the same texture GfxRenderer->Texture(t.slot.texture).update_from_memory(Result.width, Result.height, Result.pixels.data()); + ++m_uploads; return Result.pixels.size(); } @@ -867,6 +1038,18 @@ std::vector> editor_orthophoto::waiting() cons return result; } +std::vector editor_orthophoto::area_squares() const +{ + std::vector result; + std::lock_guard lock(m_mutex); + for (auto const &key : m_area_tiles) + { + glm::dvec2 const corner = tile_corner(key); + result.push_back({corner - glm::dvec2{tile_size}, corner, m_prefetch_cached.count(key) > 0 ? 1 : m_prefetch_failed.count(key) > 0 ? 2 : 0}); + } + return result; +} + bool editor_orthophoto::sampled_height(tile_key const &Own, double const X, double const Z, float &Height) const { auto const key{camera_tile({X, 0.0, Z})}; @@ -1234,6 +1417,9 @@ void editor_orthophoto::update(glm::dvec3 const &Camera) std::lock_guard lock(m_mutex); if (m_done.empty()) break; + // a 4K tile only in a frame of its own: two in one frame would stall it twice as long + if (uploaded > 0 && m_done.front().pixels.size() > upload_budget) + break; done = std::move(m_done.front()); m_done.pop_front(); } @@ -1301,6 +1487,16 @@ void editor_orthophoto::update(glm::dvec3 const &Camera) for (int dx = -radius; dx <= radius; ++dx) { tile_key const key{centre.first + dx, centre.second + dy}; + // outside the download area: nothing asked for, nothing shown + if (false == m_area.empty() && 0 == m_area_tiles.count(key)) + { + if (auto const found{m_tiles.find(key)}; found != m_tiles.end()) + { + release_tile(found->second); + m_tiles.erase(found); + } + continue; + } tile &t = m_tiles[key]; if (!in_service_area(key.first, key.second)) t.empty = true; @@ -1397,29 +1593,71 @@ void editor_orthophoto::worker() for (;;) { job current; + // a tile of the area to put into the disk cache, where no tile around the camera waits + std::optional prefetch; + source prefetchsource; + std::uint32_t prefetchgeneration{0}; { std::unique_lock lock(m_mutex); - m_wakeup.wait(lock, [this]() { return m_stop.load() || !m_queue.empty(); }); + // the nearest tile first, of the ones no other worker is on; none while enough workers are on tiles around the camera + auto const nearest = [&]() { + auto best = m_queue.end(); + if (m_running.size() >= camera_workers) + return best; + long long bestdistance = LLONG_MAX; + for (auto it = m_queue.begin(); it != m_queue.end(); ++it) + { + if (m_running.count({it->map, it->key}) > 0) + continue; + // a tile of the map is that many tiles of the imagery a side + long long const scale = 1ll << it->map; + long long const dx = it->key.first * scale + scale / 2 - m_focus.first; + long long const dy = it->key.second * scale + scale / 2 - m_focus.second; + long long const d = dx * dx + dy * dy; + if (d < bestdistance) + { + bestdistance = d; + best = it; + } + } + return best; + }; + auto const prefetching = [&]() { return false == m_prefetch_paused && m_prefetch_next < m_prefetch.size(); }; + m_wakeup.wait(lock, [&]() { return m_stop.load() || nearest() != m_queue.end() || prefetching(); }); if (m_stop) return; - // nearest tile first - auto best = m_queue.begin(); - long long bestdistance = LLONG_MAX; - for (auto it = m_queue.begin(); it != m_queue.end(); ++it) + if (auto const best{nearest()}; best != m_queue.end()) { - // a tile of the map is that many tiles of the imagery a side - long long const scale = 1ll << it->map; - long long const dx = it->key.first * scale + scale / 2 - m_focus.first; - long long const dy = it->key.second * scale + scale / 2 - m_focus.second; - long long const d = dx * dx + dy * dy; - if (d < bestdistance) - { - bestdistance = d; - best = it; - } + current = *best; + m_queue.erase(best); + m_running.insert({current.map, current.key}); } - current = *best; - m_queue.erase(best); + else + { + prefetch = m_prefetch[m_prefetch_next++]; + prefetchsource = m_prefetch_source; + prefetchgeneration = m_prefetch_generation; + } + } + + if (prefetch.has_value()) + { + std::string error; + tile_request const request{prefetch->first, prefetch->second, standard_pixels, prefetchsource.year, prefetchsource.hires}; + auto const ok{prefetch_tile(client, request, error, stopping)}; + if (m_stop) + return; + if (!ok) + ErrorLog("Editor orthophoto: tile " + std::to_string(prefetch->second) + "_" + std::to_string(prefetch->first) + " of the download area failed: " + error); + std::lock_guard lock(m_mutex); + if (prefetchgeneration == m_prefetch_generation) + { + if (ok) + m_prefetch_cached.insert(*prefetch); + else + m_prefetch_failed.insert(*prefetch); + } + continue; } result out; @@ -1432,10 +1670,15 @@ void editor_orthophoto::worker() image picture; std::string error; tile_request const request{current.key.first, current.key.second, current.level, current.src.year, current.src.hires}; - out.ok = current.map > 0 ? load_map(client, current.map, current.key.first, current.key.second, picture, error, stopping) : load_tile(client, request, picture, out.empty, error, stopping); + // a job of the same tile queued since: the tile wants another image now + stop_check const superseded = [&]() { + std::lock_guard lock(m_mutex); + return std::any_of(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.map == current.map && Job.key == current.key; }); + }; + out.ok = current.map > 0 ? load_map(client, current.map, current.key.first, current.key.second, picture, error, stopping) : load_tile(client, request, picture, out.empty, error, stopping, superseded); if (m_stop) return; - if (!out.ok) + if (!out.ok && error != "superseded") ErrorLog(std::string("Editor orthophoto: ") + (current.map > 0 ? "map " + std::to_string(current.map) + " " : std::string{}) + "tile " + std::to_string(current.key.second) + "_" + std::to_string(current.key.first) + " failed: " + error); if (out.ok && current.alpha < 255) for (std::size_t i = 3; i < picture.rgba.size(); i += 4) @@ -1444,8 +1687,13 @@ void editor_orthophoto::worker() out.height = picture.height; out.pixels = std::move(picture.rgba); - std::lock_guard lock(m_mutex); - m_done.push_back(std::move(out)); + { + std::lock_guard lock(m_mutex); + m_done.push_back(std::move(out)); + m_running.erase({current.map, current.key}); + } + // a job of the same tile may have waited for this one + m_wakeup.notify_all(); } } diff --git a/editor/editorOrthophoto.hpp b/editor/editorOrthophoto.hpp index c0fbf15a..ed579701 100644 --- a/editor/editorOrthophoto.hpp +++ b/editor/editorOrthophoto.hpp @@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include #include @@ -78,6 +79,14 @@ class editor_orthophoto int resident{0}; // tiles with imagery on screen int loading{0}; // tiles waiting for or being processed by a worker int failed{0}; // tiles whose last attempt failed + // the download area: its tiles, the ones in the disk cache in every product the settings ask for, the ones which failed + int area_tiles{0}; + int area_cached{0}; + int area_failed{0}; + bool area_paused{false}; + int requests{0}; // asked of the services since the start, the disk cache left out + int uploads{0}; // images of tiles sent to the graphics card + int stale{0}; // images which came when the tile no longer wanted them, dropped }; static constexpr double tile_size{256.0}; // metres per tile side @@ -102,6 +111,28 @@ class editor_orthophoto // the heights the ground will have where there's none yet, as of the terrain model on its way: a tile laid out before its // ground comes lies there, or level with the tiles beside, rather than at the height of the layer far below or above void guess_source(height_query Query) { m_guess = std::move(Query); } + // the area the imagery is downloaded in, a polygon of PUWG 1992 (easting, northing): its tiles go to the disk cache in the + // background, the products the settings ask for, and no tile outside it is asked for around the camera. Empty: no area, the + // tiles around the camera as before + void area(std::vector Polygon); + std::vector const &area() const { return m_area; } + bool in_area(double const East, double const North) const; + // the download in the background held, or going on + void area_paused(bool const Paused); + // the squares of the tiles of the area in the world (x, z): 0 waiting, 1 in the disk cache, 2 failed + struct area_square + { + glm::dvec2 low{0.0}; + glm::dvec2 high{0.0}; + int state{0}; + }; + std::vector area_squares() const; + // world (x, z) and PUWG 1992 (easting, northing), by the origin of the scenery + glm::dvec2 to_puwg(glm::dvec2 const &World) const { return {m_config.east - World.x, m_config.north + World.y}; } + glm::dvec2 to_world(glm::dvec2 const &Puwg) const { return {m_config.east - Puwg.x, Puwg.y - m_config.north}; } + // the area kept in the scenery, as the lines after its mark + static std::vector area_lines(std::vector const &Polygon); + static std::vector read_area(std::vector const &Lines); // samples the ground again (after it was edited); tiles are refitted progressively void refit(); // the tiles lying at guessed heights sampled again where the ground has come under them @@ -220,6 +251,8 @@ class editor_orthophoto void draw_map(glm::mat4 const &ViewProjection, glm::dvec3 const &CameraPos, float ScreenWidth, float ScreenHeight) const; void start_workers(); void worker(); + // the tiles of the area queued for the download in the background again, nearest to the camera first + void restart_prefetch(); config m_config; bool m_enabled{false}; @@ -241,12 +274,26 @@ class editor_orthophoto std::uint8_t m_alpha_pending{255}; // latest requested double m_alpha_changed{0.0}; // when the requested value last changed + std::vector m_area; // PUWG, empty: no area + std::set m_area_tiles; // the tiles it touches + int m_uploads{0}; + int m_stale{0}; + // shared with the workers, guarded by m_mutex mutable std::mutex m_mutex; std::condition_variable m_wakeup; std::deque m_queue; std::deque m_done; tile_key m_focus{0, 0}; // workers pick the queued tile closest to it + std::set> m_running; // (map, tile) a worker is on: another job of it waits till that one is done + // the download of the area in the background: the tiles nearest to the camera first, taken when no tile around the camera waits + std::vector m_prefetch; + std::size_t m_prefetch_next{0}; + std::set m_prefetch_failed; + std::set m_prefetch_cached; + bool m_prefetch_paused{false}; + source m_prefetch_source; + std::uint32_t m_prefetch_generation{0}; std::atomic m_stop{false}; std::atomic m_generation{1}; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index ac2c61de..b964dc98 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -12248,3 +12248,40 @@ msgstr "Łuk %d %s" msgid "Vertex %zu: the transition curve between the curve of the adjoining track and R %.0f m is shorter than the required %.0f m" msgstr "Wierzchołek %zu: krzywa przejściowa między łukiem przylegającego toru a R %.0f m jest krótsza niż wymagane %.0f m" + +msgid "Download area" +msgstr "Obszar pobierania" + +msgid "A polygon drawn on the map: what the settings ask for inside it, the orthophoto and the terrain model, +goes to the disk cache in the background, and nothing outside it is downloaded, also around the camera" +msgstr "Wielokąt narysowany na mapie: to, co wybrano w ustawieniach, ortofotomapa i model terenu, pobiera się w nim w tle +do pamięci podręcznej na dysku, a poza nim nic się nie pobiera, także wokół kamery" + +msgid "Click the points of the area on the map. Enter closes it, Backspace takes the last point back, Esc gives up; +a point dragged moves, Shift+click removes it" +msgstr "Klikaj na mapie punkty obszaru. Enter go zamyka, Backspace cofa ostatni punkt, Esc rezygnuje; +przeciągnięty punkt się przesuwa, Shift+klik go usuwa" + +msgid "%zu points" +msgstr "punktów: %zu" + +msgid "Draw on the map" +msgstr "Narysuj na mapie" + +msgid "Draw again" +msgstr "Narysuj od nowa" + +msgid "none: the tiles around the camera, wherever it is" +msgstr "brak: kafle wokół kamery, gdziekolwiek jest" + +msgid "Area: %d of %d tiles in the cache, %d failed" +msgstr "Obszar: %d z %d kafli w pamięci podręcznej, nieudanych %d" + +msgid "Resume" +msgstr "Wznów" + +msgid "Pause" +msgstr "Wstrzymaj" + +msgid "Terrain model: %d of %d squares in the cache, %d failed" +msgstr "Model terenu: %d z %d kwadratów w pamięci podręcznej, nieudanych %d" diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index a2d158f1..3b5c5aba 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -846,7 +846,7 @@ bool node_layers::pending() const namespace { // marks of the lines the editor keeps track of -std::array const editormarks{"//$p", "//$h", "//$r", "//$b", "//$c", "//$o", "//$s", "//$k"}; +std::array const editormarks{"//$p", "//$h", "//$r", "//$b", "//$c", "//$o", "//$s", "//$k", "//$a"}; // other tools put lines of their own after some of these marks: Rainsted its includes after //$r, the launcher the descriptions // of the trainsets after //$o. The editor's lines of them are told by their first word diff --git a/tests/unit/test_area.cpp b/tests/unit/test_area.cpp new file mode 100644 index 00000000..09220da5 --- /dev/null +++ b/tests/unit/test_area.cpp @@ -0,0 +1,66 @@ +#include "stdafx.h" +#include "doctest.h" +#include "editor/editorGeometry.hpp" + +#include +#include +#include +#include +#include + +namespace { + +std::set> squares(std::vector const &Polygon, double const Size) { + auto const found{geometry::squares_in(Polygon, Size)}; + return {found.begin(), found.end()}; +} + +} // namespace + +TEST_CASE("download area: the tiles a triangle touches") { + // x + y < 1000: the squares of 256 m whose corner nearest the origin lies inside + auto const found{squares({{0.0, 0.0}, {1000.0, 0.0}, {0.0, 1000.0}}, 256.0)}; + std::set> expected; + for (int x = 0; x < 4; ++x) + for (int y = 0; x + y < 4; ++y) + expected.emplace(x, y); + CHECK(found == expected); +} + +TEST_CASE("download area: the tiles of a concave polygon leave out the ones in its notch") { + // an L, 300 m wide, its arms 1000 m long + auto const found{squares({{0.0, 0.0}, {1000.0, 0.0}, {1000.0, 300.0}, {300.0, 300.0}, {300.0, 1000.0}, {0.0, 1000.0}}, 256.0)}; + std::set> expected; + for (int x = 0; x < 4; ++x) + expected.insert({{x, 0}, {x, 1}}); + for (int y = 2; y < 4; ++y) + expected.insert({{0, y}, {1, y}}); + CHECK(found == expected); + CHECK(found.count({2, 2}) == 0); +} + +TEST_CASE("download area: a thin band along a line touches the tiles it crosses, none further") { + // 4 m wide along the diagonal, from (10, 10) to (2000, 2000) + auto const found{squares({{10.0, 8.0}, {2000.0, 1998.0}, {2000.0, 2002.0}, {10.0, 12.0}}, 256.0)}; + for (int i = 0; i < 8; ++i) + CHECK(found.count({i, i}) == 1); + CHECK(std::none_of(found.begin(), found.end(), [](std::pair const &Key) { return std::abs(Key.first - Key.second) > 1; })); +} + +TEST_CASE("download area: a polygon inside one tile, or around a corner of four") { + CHECK(squares({{10.0, 10.0}, {20.0, 10.0}, {20.0, 20.0}, {10.0, 20.0}}, 256.0) == std::set>{{0, 0}}); + CHECK(squares({{250.0, 250.0}, {262.0, 250.0}, {262.0, 262.0}, {250.0, 262.0}}, 256.0) == std::set>{{0, 0}, {1, 0}, {0, 1}, {1, 1}}); + // in the grid of the coordinates of Poland, about 500 km from its origin + auto const distant{squares({{500100.0, 600100.0}, {500200.0, 600100.0}, {500200.0, 600200.0}}, 256.0)}; + CHECK(distant == std::set>{{1953, 2344}}); +} + +TEST_CASE("download area: a point inside or outside, a square of no size") { + std::vector const triangle{{0.0, 0.0}, {100.0, 0.0}, {0.0, 100.0}}; + CHECK(geometry::square_touches(triangle, 10.0, 10.0, 0.0)); + CHECK_FALSE(geometry::square_touches(triangle, 80.0, 80.0, 0.0)); + // a square holding the whole polygon touches it, with no corner of its inside + CHECK(geometry::square_touches(triangle, -50.0, -50.0, 300.0)); + // fewer than three points: no area + CHECK(geometry::squares_in({{0.0, 0.0}, {100.0, 100.0}}, 256.0).empty()); +}