17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-12 18:40:35 +01:00

feat(editor): the download area drawn on the topo map: a polygon is the boundary of what the editor downloads, the orthophoto and the terrain model alike. Drawn from the settings of the orthophoto (Download area, Draw on the map, the top view on): a click puts a point, a point dragged moves, Shift+click removes it, Backspace takes the last one back, Enter closes the area, Esc gives up; it's kept in the scenery as //$a lines in PUWG 1992, so it stays on the ground when the origin moves. Inside it everything the settings ask for, the standard imagery, the 4K one where it's ticked and covers the place, the squares of the terrain model where the scenery has its terrain, goes to the disk cache in the background, nothing decoded nor uploaded, only while no tile around the camera waits and not while the scenery is driven, with its progress, Pause and Retry under it; outside it nothing is asked for, around the camera, along the tracks or ahead of the laying, nor shown. The workers of the orthophoto are 8, at most 3 of them on the tiles around the camera, each holding an image decoded, the rest downloading the area. The 4K tiles: an image the tile no longer wants once it comes, as a 4K one asked for before the camera moved on, isn't uploaded (the 64 MB and the mipmaps of it), nor decoded where a newer job of the tile waits; no two jobs of one tile run at once; a standard tile coming blank twice is remembered by a .blank file, not downloaded again on each return; a 4K tile is uploaded only in a frame with no other upload. Selftests orthoarea, orthostats, orthohires, orthoareapause; unit tests of the tiles a triangle, a concave polygon, a thin band and a polygon about a corner touch (geometry::squares_in, square_touches)

This commit is contained in:
maj00r
2026-10-26 18:58:09 +01:00
parent 9b596e7ffa
commit d6df785b78
13 changed files with 922 additions and 39 deletions

View File

@@ -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); 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_orthophoto();
draw_nmt_pending(); draw_nmt_pending();
area_update();
draw_download_area();
// --- ImGuizmo: in-viewport transform gizmo for the selected node --- // --- ImGuizmo: in-viewport transform gizmo for the selected node ---
render_gizmo(); render_gizmo();
@@ -2288,6 +2290,11 @@ void editor_mode::load_orthophoto_settings()
} }
m_orthophoto.settings(config); m_orthophoto.settings(config);
m_orthophoto_origin_edit = {config.north, config.east}; 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<std::string>{} : 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() void editor_mode::save_orthophoto_settings()
@@ -2384,7 +2391,7 @@ void editor_mode::render_orthophoto_ui()
}; };
auto const stats = m_orthophoto.stats(); 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::Text(STR_C("Tiles: %d shown, %d loading, %d failed"), stats.resident, stats.loading, stats.failed);
ImGui::SameLine(); ImGui::SameLine();
@@ -2392,6 +2399,10 @@ void editor_mode::render_orthophoto_ui()
m_orthophoto.retry_failed(); m_orthophoto.retry_failed();
} }
ImGui::Separator();
render_download_area_ui(stats);
ImGui::Separator();
if (ImGui::TreeNode(STR_C("More settings"))) if (ImGui::TreeNode(STR_C("More settings")))
{ {
if (ImGui::Checkbox(STR_C("Fit to terrain"), &config.drape)) if (ImGui::Checkbox(STR_C("Fit to terrain"), &config.drape))
@@ -3055,6 +3066,11 @@ bool editor_mode::save()
m_orthophoto_mark = map; m_orthophoto_mark = map;
scene::Layers.mark(scene::layer_handle{1}, "//$o", {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 terrainadded{false == rootstatements.empty() || false == statements.empty()};
auto const controls{turntable_controls(rootstatements)}; auto const controls{turntable_controls(rootstatements)};
auto const noses{nose_controls(rootstatements)}; auto const noses{nose_controls(rootstatements)};
@@ -6181,6 +6197,8 @@ void editor_mode::enter()
EditorModeFlag = true; EditorModeFlag = true;
load_orthophoto_settings(); load_orthophoto_settings();
m_orthophoto.area_paused(m_area_paused);
m_nmt.prefetch_paused(m_area_paused);
Application.set_cursor(GLFW_CURSOR_NORMAL); Application.set_cursor(GLFW_CURSOR_NORMAL);
} }
@@ -6211,6 +6229,9 @@ void editor_mode::exit()
m_fill_last.clear(); m_fill_last.clear();
m_orthophoto.cancel_pending(); m_orthophoto.cancel_pending();
m_orthophoto.detach_scene(); // the layer is an editor aid, keep it out of the other modes 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 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 // 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 // an open list or menu takes this Esc for itself, the selection behind it stays
if (ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel)) if (ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel))
break; 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)) if (is_press(Action))
{ {
road_cancel(); road_cancel();
@@ -6526,6 +6553,13 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
break; break;
case GLFW_KEY_BACKSPACE: 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 // area fill: remove the last outline point
if (is_press(Action) && ui()->mode() == nodebank_panel::FILL && !m_fill_points.empty()) if (is_press(Action) && ui()->mode() == nodebank_panel::FILL && !m_fill_points.empty())
m_fill_points.pop_back(); 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_ENTER:
case GLFW_KEY_KP_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) if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && m_lay.active)
lay_enter(); lay_enter();
if (is_press(Action) && m_route.trace.active && route_active()) 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; 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) // 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) if (terrain_clicking() && Button == GLFW_MOUSE_BUTTON_LEFT)
{ {

View File

@@ -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 // 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 // what's on its way
void draw_nmt_pending() const; 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 // over the 3d view while the terrain panel isn't seen
void render_nmt_banner(); void render_nmt_banner();
bool nmt_busy() const; 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::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_scenery; // scenery whose origin is loaded
std::string m_orthophoto_mark; std::string m_orthophoto_mark;
std::vector<std::string> 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<glm::dvec2> points;
int dragged{-1};
} m_area_drawing;
std::vector<glm::dvec2> 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 glm::dvec2 m_orthophoto_origin_edit{0.0}; // origin being typed in (northing, easting); applied when editing ends
// hierarchy management // hierarchy management

View File

@@ -33,6 +33,7 @@ http://mozilla.org/MPL/2.0/.
#include <limits> #include <limits>
#include <set> #include <set>
#include <unordered_set> #include <unordered_set>
#include <utility>
namespace namespace
{ {
@@ -511,7 +512,12 @@ std::vector<heightmap::chunk_key> editor_mode::nmt_keys_around() const
auto const first{-(m_terrain_add_count - 1) / 2}; auto const first{-(m_terrain_add_count - 1) / 2};
for (int dz = 0; dz < m_terrain_add_count; ++dz) for (int dz = 0; dz < m_terrain_add_count; ++dz)
for (int dx = 0; dx < m_terrain_add_count; ++dx) 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; return result;
} }
@@ -529,7 +535,7 @@ std::vector<heightmap::chunk_key> editor_mode::nmt_keys_along_tracks() const
auto const low{heightmap::chunk_corner(key)}; auto const low{heightmap::chunk_corner(key)};
// the chunk reaches within the band of the track // 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)}; 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); keys.insert(key);
} }
} }
@@ -890,6 +896,12 @@ std::size_t editor_mode::nmt_shape_tracks(std::vector<TTrack *> const *Only)
void editor_mode::nmt_update() void editor_mode::nmt_update()
{ {
auto &state{m_nmt_state}; 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<glm::dvec2>{}}; wanted != m_nmt_prefetched)
{
m_nmt_prefetched = wanted;
m_nmt.prefetch(wanted);
}
nmt_offer(); nmt_offer();
nmt_follow_tracks(); nmt_follow_tracks();
if (state.stage != nmt_state::automatic::idle) if (state.stage != nmt_state::automatic::idle)
@@ -1007,6 +1019,9 @@ void editor_mode::nmt_grow()
if (false == laying && false == m_streamer.active()) if (false == laying && false == m_streamer.active())
return; return;
auto const at{m_extend.active ? m_extend.mouse : m_lay.active ? m_lay.mouse : glm::dvec3{Camera.Pos}}; 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 // 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}); m_nmt.request({at.x - 100.0, at.z - 100.0}, {at.x + 100.0, at.z + 100.0});
if (m_streamer.resident() == 0) if (m_streamer.resident() == 0)
@@ -1023,7 +1038,8 @@ void editor_mode::nmt_grow()
auto const low{heightmap::chunk_corner(key)}; 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)}; 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)}; 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); missing.emplace_back(distance, key);
} }
if (missing.empty()) if (missing.empty())
@@ -1359,6 +1375,202 @@ void editor_mode::draw_nmt_pending() const
square(low, high, 90, 170, 255, STR("Terrain model loading")); 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<glm::dvec2> 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<int>(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<int>(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<glm::dvec2> 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<ImVec2, 4> 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() void editor_mode::render_nmt_banner()
{ {
auto const &state{m_nmt_state}; auto const &state{m_nmt_state};

View File

@@ -124,6 +124,8 @@ editor_mode::selftest_result editor_mode::selftest_dispatch(std::string const &c
// terrainturn <palette entry> <degrees> the textures of the palette entry turned // terrainturn <palette entry> <degrees> the textures of the palette entry turned
// terrainwater <index> <material> [size] material and repeat of a body of water; terrainwaterdefault <material> // terrainwater <index> <material> [size] material and repeat of a body of water; terrainwaterdefault <material>
// orthophoto <north> <east> <radius> <fit 0|1> [in scene 0|1] the imagery layer on, placed and fitted as given // orthophoto <north> <east> <radius> <fit 0|1> [in scene 0|1] the imagery layer on, placed and fitted as given
// orthoarea <x> <z>... | 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 <x> <z> <x> <z> [radius] [variant] [transitions 0|1] the free ends at the points joined, as a drag from one to the other // join <x> <z> <x> <z> [radius] [variant] [transitions 0|1] the free ends at the points joined, as a drag from one to the other
// layto <x> <z> <x> <z> <layout 0..3> <radius> from the free end at the first point towards the second, as a drag in the layout // layto <x> <z> <x> <z> <layout 0..3> <radius> from the free end at the first point towards the second, as a drag in the layout
// layclicks <layout 0..3> <radius> <x> <z>... clicks of the lay tool at the points, a free end within 3 m joined // layclicks <layout 0..3> <radius> <x> <z>... clicks of the lay tool at the points, a free end within 3 m joined

View File

@@ -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" : "")); 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; 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<glm::dvec2> 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") if (command == "nmtfit")
{ {
// the heights of the terrain model fitted to the tracks, as the button of the terrain panel does // the heights of the terrain model fitted to the tracks, as the button of the terrain panel does

View File

@@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/.
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <utility>
#include <vector> #include <vector>
#include <glm/glm.hpp> #include <glm/glm.hpp>
@@ -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}; 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<glm::dvec2> 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<glm::dvec2> 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<std::pair<int, int>> squares_in(std::vector<glm::dvec2> const &Polygon, double const Size)
{
std::vector<std::pair<int, int>> 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<int>(std::floor(low.x / Size)), static_cast<int>(std::floor(low.y / Size))}};
auto const last{glm::ivec2{static_cast<int>(std::floor(high.x / Size)), static_cast<int>(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 } // namespace geometry

View File

@@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/.
#include "stdafx.h" #include "stdafx.h"
#include "editor/editorNmt.hpp" #include "editor/editorNmt.hpp"
#include "editor/editorHttp.hpp" #include "editor/editorHttp.hpp"
#include "editor/editorGeometry.hpp"
#include "utilities/Logs.h" #include "utilities/Logs.h"
#include "utilities/translation.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_queued.insert(square);
m_queue.push_back(square); m_queue.push_back(square);
} }
if (false == wanted.empty() && m_workers.empty()) if (false == wanted.empty())
for (int i = 0; i < worker_count; ++i) start_workers();
m_workers.emplace_back([this]() { work(); });
} }
if (false == wanted.empty()) if (false == wanted.empty())
m_wake.notify_all(); m_wake.notify_all();
return 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<glm::dvec2> const &Polygon)
{
std::vector<key> keys;
if (can_download())
for (auto const &square : geometry::squares_in(Polygon, square_size))
keys.emplace_back(square);
{
std::lock_guard<std::mutex> 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<std::mutex> lock{m_mutex};
m_prefetch_paused = Paused;
}
m_wake.notify_all();
}
editor_nmt::prefetch_state editor_nmt::prefetched() const
{
std::lock_guard<std::mutex> lock{m_mutex};
return {static_cast<int>(m_prefetch_cached.size()), static_cast<int>(m_prefetch.size()), static_cast<int>(m_prefetch_failed.size())};
}
std::size_t editor_nmt::waiting() const std::size_t editor_nmt::waiting() const
{ {
std::lock_guard<std::mutex> lock{m_mutex}; std::lock_guard<std::mutex> lock{m_mutex};
@@ -338,13 +384,37 @@ void editor_nmt::work()
while (true) while (true)
{ {
key square; key square;
bool prefetching{false};
std::uint32_t generation{0};
{ {
std::unique_lock<std::mutex> lock{m_mutex}; std::unique_lock<std::mutex> 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) if (m_stop)
return; return;
square = m_queue.front(); if (false == m_queue.empty())
m_queue.pop_front(); {
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<std::mutex> lock{m_mutex};
if (generation == m_prefetch_generation)
(ok ? m_prefetch_cached : m_prefetch_failed).insert(square);
continue;
} }
std::string error; std::string error;
auto fetched{fetch(square, error)}; auto fetched{fetch(square, error)};
@@ -363,7 +433,7 @@ std::shared_ptr<editor_nmt::square const> editor_nmt::fetch(key const &Key, std:
auto result{std::make_shared<square>()}; auto result{std::make_shared<square>()};
std::error_code fault; std::error_code fault;
fs::create_directories(cache_folder, 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<std::uint8_t> data; std::vector<std::uint8_t> data;
if (std::ifstream cached{file, std::ios::binary}; cached.is_open()) if (std::ifstream cached{file, std::ios::binary}; cached.is_open())
{ {

View File

@@ -59,6 +59,18 @@ class editor_nmt
void clear(); void clear();
// whether this build can fetch from the service at all // whether this build can fetch from the service at all
static bool can_download(); 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<glm::dvec2> 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 // side of a square of the grid, m
static double constexpr square_size{500.0}; static double constexpr square_size{500.0};
@@ -93,4 +105,13 @@ class editor_nmt
double m_north{0.0}; double m_north{0.0};
double m_east{0.0}; double m_east{0.0};
bool m_stop{false}; bool m_stop{false};
// the squares of the download area for the disk cache
std::vector<key> m_prefetch;
std::size_t m_prefetch_next{0};
std::set<key> m_prefetch_cached;
std::set<key> 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;
}; };

View File

@@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/.
#include "stdafx.h" #include "stdafx.h"
#include "editor/editorOrthophoto.hpp" #include "editor/editorOrthophoto.hpp"
#include "editor/editorHttp.hpp" #include "editor/editorHttp.hpp"
#include "editor/editorGeometry.hpp"
#include "rendering/renderer.h" #include "rendering/renderer.h"
#include "scene/scene.h" #include "scene/scene.h"
@@ -30,6 +31,7 @@ http://mozilla.org/MPL/2.0/.
#include <fstream> #include <fstream>
#include <functional> #include <functional>
#include <limits> #include <limits>
#include <optional>
#include <sstream> #include <sstream>
#include <glad/glad.h> #include <glad/glad.h>
@@ -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_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 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 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 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 std::size_t vertex_budget{60000}; // ImGui uses 16-bit indices and not every backend supports vertex offsets
constexpr float near_w{0.5f}; constexpr float near_w{0.5f};
@@ -182,6 +187,9 @@ void write_file(fs::path const &Path, std::vector<std::uint8_t> const &Data)
fs::remove(temporary, ec); fs::remove(temporary, ec);
} }
// the requests made of the services, the disk cache left out
std::atomic<int> requests_made{0};
// cached file if present, otherwise downloads (with a few retries, the service drops connections and answers 404 now and then) // 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<std::uint8_t> &Out, std::string &Error, stop_check const &Stop) bool fetch(http_client &Client, std::string const &Url, fs::path const &Path, std::vector<std::uint8_t> &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) for (int attempt = 0; attempt < 4; ++attempt)
{ {
++requests_made;
if (Stop()) if (Stop())
{ {
Error = "cancelled"; Error = "cancelled";
@@ -261,8 +270,20 @@ bool is_blank(image const &Image)
return true; 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) 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<std::uint8_t> data; std::vector<std::uint8_t> data;
auto const path{cache_file(Request, false, std::to_string(standard_pixels) + ".jpg")}; auto const path{cache_file(Request, false, std::to_string(standard_pixels) + ".jpg")};
for (int attempt = 0; attempt < 2; ++attempt) 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; return false;
if (!is_blank(Out)) if (!is_blank(Out))
return true; return true;
std::error_code ec;
fs::remove(path, ec); fs::remove(path, ec);
} }
write_file(blank_file(Request), {});
return true; return true;
} }
// returns false on failure (to be retried later); Empty is set when the place has no imagery // 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
bool load_tile(http_client &Client, tile_request const &Request, image &Out, bool &Empty, std::string &Error, stop_check const &Stop) // 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<std::uint8_t> 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; Empty = false;
if (Request.hires && Request.level >= hires_pixels) 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); covered += (probe.rgba[i] != 0);
std::size_t const total = probe.rgba.size() / 4; std::size_t const total = probe.rgba.size() / 4;
auto const superseded = [&]() {
if (false == Superseded())
return false;
Error = "superseded";
return true;
};
if (covered == total) 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) if (covered > 0)
{ {
// edge of the covered area: transparent png, holes filled from the standard imagery // 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; return false;
image standard; image standard;
std::string ignored; std::string ignored;
@@ -569,6 +624,7 @@ void editor_orthophoto::enabled(bool State)
if (State == m_enabled) if (State == m_enabled)
return; return;
m_enabled = State; m_enabled = State;
restart_prefetch();
if (m_enabled) if (m_enabled)
{ {
start_workers(); start_workers();
@@ -608,6 +664,95 @@ void editor_orthophoto::settings(config const &Config)
release_tile(entry.second); release_tile(entry.second);
m_tiles.clear(); m_tiles.clear();
++m_generation; ++m_generation;
// the area in the cache once more, of the other product
restart_prefetch();
}
void editor_orthophoto::area(std::vector<glm::dvec2> 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<std::mutex> 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<std::mutex> lock(m_mutex);
m_prefetch_paused = Paused;
}
m_wakeup.notify_all();
}
void editor_orthophoto::restart_prefetch()
{
std::vector<tile_key> 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<long long>(Key.first) - centre.first};
auto const dy{static_cast<long long>(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<std::mutex> 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<std::string> editor_orthophoto::area_lines(std::vector<glm::dvec2> const &Polygon)
{
std::vector<std::string> 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<glm::dvec2> editor_orthophoto::read_area(std::vector<std::string> const &Lines)
{
std::vector<glm::dvec2> 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() void editor_orthophoto::refit()
@@ -690,11 +835,29 @@ void editor_orthophoto::retry_failed()
entry.second.failures = 0; entry.second.failures = 0;
entry.second.retry_at = 0.0; entry.second.retry_at = 0.0;
} }
// the tiles of the area which failed taken again
{
std::lock_guard<std::mutex> 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 editor_orthophoto::statistics editor_orthophoto::stats() const
{ {
statistics out; statistics out;
out.area_tiles = static_cast<int>(m_area_tiles.size());
out.requests = requests_made.load();
out.uploads = m_uploads;
out.stale = m_stale;
{
std::lock_guard<std::mutex> lock(m_mutex);
out.area_cached = static_cast<int>(m_prefetch_cached.size());
out.area_failed = static_cast<int>(m_prefetch_failed.size());
out.area_paused = m_prefetch_paused;
}
auto const count = [&](tile const &t) { auto const count = [&](tile const &t) {
if (t.level > 0 && !t.empty && t.slot.texture != null_handle) if (t.level > 0 && !t.empty && t.slot.texture != null_handle)
++out.resident; ++out.resident;
@@ -816,6 +979,13 @@ std::size_t editor_orthophoto::apply(result &Result, double Now)
} }
return 0; 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.failures = 0;
t.level = Result.level; 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 // 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()); GfxRenderer->Texture(t.slot.texture).update_from_memory(Result.width, Result.height, Result.pixels.data());
++m_uploads;
return Result.pixels.size(); return Result.pixels.size();
} }
@@ -867,6 +1038,18 @@ std::vector<std::pair<glm::dvec2, glm::dvec2>> editor_orthophoto::waiting() cons
return result; return result;
} }
std::vector<editor_orthophoto::area_square> editor_orthophoto::area_squares() const
{
std::vector<area_square> result;
std::lock_guard<std::mutex> 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 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})}; auto const key{camera_tile({X, 0.0, Z})};
@@ -1234,6 +1417,9 @@ void editor_orthophoto::update(glm::dvec3 const &Camera)
std::lock_guard<std::mutex> lock(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
if (m_done.empty()) if (m_done.empty())
break; 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()); done = std::move(m_done.front());
m_done.pop_front(); m_done.pop_front();
} }
@@ -1301,6 +1487,16 @@ void editor_orthophoto::update(glm::dvec3 const &Camera)
for (int dx = -radius; dx <= radius; ++dx) for (int dx = -radius; dx <= radius; ++dx)
{ {
tile_key const key{centre.first + dx, centre.second + dy}; 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]; tile &t = m_tiles[key];
if (!in_service_area(key.first, key.second)) if (!in_service_area(key.first, key.second))
t.empty = true; t.empty = true;
@@ -1397,29 +1593,71 @@ void editor_orthophoto::worker()
for (;;) for (;;)
{ {
job current; job current;
// a tile of the area to put into the disk cache, where no tile around the camera waits
std::optional<tile_key> prefetch;
source prefetchsource;
std::uint32_t prefetchgeneration{0};
{ {
std::unique_lock<std::mutex> lock(m_mutex); std::unique_lock<std::mutex> 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) if (m_stop)
return; return;
// nearest tile first if (auto const best{nearest()}; best != m_queue.end())
auto best = m_queue.begin();
long long bestdistance = LLONG_MAX;
for (auto it = m_queue.begin(); it != m_queue.end(); ++it)
{ {
// a tile of the map is that many tiles of the imagery a side current = *best;
long long const scale = 1ll << it->map; m_queue.erase(best);
long long const dx = it->key.first * scale + scale / 2 - m_focus.first; m_running.insert({current.map, current.key});
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; else
m_queue.erase(best); {
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<std::mutex> lock(m_mutex);
if (prefetchgeneration == m_prefetch_generation)
{
if (ok)
m_prefetch_cached.insert(*prefetch);
else
m_prefetch_failed.insert(*prefetch);
}
continue;
} }
result out; result out;
@@ -1432,10 +1670,15 @@ void editor_orthophoto::worker()
image picture; image picture;
std::string error; std::string error;
tile_request const request{current.key.first, current.key.second, current.level, current.src.year, current.src.hires}; 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<std::mutex> 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) if (m_stop)
return; 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); 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) if (out.ok && current.alpha < 255)
for (std::size_t i = 3; i < picture.rgba.size(); i += 4) 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.height = picture.height;
out.pixels = std::move(picture.rgba); out.pixels = std::move(picture.rgba);
std::lock_guard<std::mutex> lock(m_mutex); {
m_done.push_back(std::move(out)); std::lock_guard<std::mutex> 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();
} }
} }

View File

@@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/.
#include <functional> #include <functional>
#include <map> #include <map>
#include <mutex> #include <mutex>
#include <set>
#include <string> #include <string>
#include <thread> #include <thread>
#include <utility> #include <utility>
@@ -78,6 +79,14 @@ class editor_orthophoto
int resident{0}; // tiles with imagery on screen int resident{0}; // tiles with imagery on screen
int loading{0}; // tiles waiting for or being processed by a worker int loading{0}; // tiles waiting for or being processed by a worker
int failed{0}; // tiles whose last attempt failed 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 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 // 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 // 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); } 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<glm::dvec2> Polygon);
std::vector<glm::dvec2> 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_square> 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<std::string> area_lines(std::vector<glm::dvec2> const &Polygon);
static std::vector<glm::dvec2> read_area(std::vector<std::string> const &Lines);
// samples the ground again (after it was edited); tiles are refitted progressively // samples the ground again (after it was edited); tiles are refitted progressively
void refit(); void refit();
// the tiles lying at guessed heights sampled again where the ground has come under them // 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 draw_map(glm::mat4 const &ViewProjection, glm::dvec3 const &CameraPos, float ScreenWidth, float ScreenHeight) const;
void start_workers(); void start_workers();
void worker(); 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; config m_config;
bool m_enabled{false}; bool m_enabled{false};
@@ -241,12 +274,26 @@ class editor_orthophoto
std::uint8_t m_alpha_pending{255}; // latest requested std::uint8_t m_alpha_pending{255}; // latest requested
double m_alpha_changed{0.0}; // when the requested value last changed double m_alpha_changed{0.0}; // when the requested value last changed
std::vector<glm::dvec2> m_area; // PUWG, empty: no area
std::set<tile_key> m_area_tiles; // the tiles it touches
int m_uploads{0};
int m_stale{0};
// shared with the workers, guarded by m_mutex // shared with the workers, guarded by m_mutex
mutable std::mutex m_mutex; mutable std::mutex m_mutex;
std::condition_variable m_wakeup; std::condition_variable m_wakeup;
std::deque<job> m_queue; std::deque<job> m_queue;
std::deque<result> m_done; std::deque<result> m_done;
tile_key m_focus{0, 0}; // workers pick the queued tile closest to it tile_key m_focus{0, 0}; // workers pick the queued tile closest to it
std::set<std::pair<int, tile_key>> 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<tile_key> m_prefetch;
std::size_t m_prefetch_next{0};
std::set<tile_key> m_prefetch_failed;
std::set<tile_key> m_prefetch_cached;
bool m_prefetch_paused{false};
source m_prefetch_source;
std::uint32_t m_prefetch_generation{0};
std::atomic<bool> m_stop{false}; std::atomic<bool> m_stop{false};
std::atomic<std::uint32_t> m_generation{1}; std::atomic<std::uint32_t> m_generation{1};

View File

@@ -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" 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" 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"

View File

@@ -846,7 +846,7 @@ bool node_layers::pending() const
namespace namespace
{ {
// marks of the lines the editor keeps track of // marks of the lines the editor keeps track of
std::array<char const *, 8> const editormarks{"//$p", "//$h", "//$r", "//$b", "//$c", "//$o", "//$s", "//$k"}; std::array<char const *, 9> 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 // 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 // of the trainsets after //$o. The editor's lines of them are told by their first word

66
tests/unit/test_area.cpp Normal file
View File

@@ -0,0 +1,66 @@
#include "stdafx.h"
#include "doctest.h"
#include "editor/editorGeometry.hpp"
#include <algorithm>
#include <cstdlib>
#include <set>
#include <utility>
#include <vector>
namespace {
std::set<std::pair<int, int>> squares(std::vector<glm::dvec2> 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<std::pair<int, int>> 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<std::pair<int, int>> 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<int, int> 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<std::pair<int, int>>{{0, 0}});
CHECK(squares({{250.0, 250.0}, {262.0, 250.0}, {262.0, 262.0}, {250.0, 262.0}}, 256.0) == std::set<std::pair<int, int>>{{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<std::pair<int, int>>{{1953, 2344}});
}
TEST_CASE("download area: a point inside or outside, a square of no size") {
std::vector<glm::dvec2> 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());
}