From d4b1d73338fb461217e0f79098bdda43269d84f9 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 31 Jul 2026 19:52:06 +0200 Subject: [PATCH] Keep tiles on disk, add basket curves, undo the image flip The flip was my mistake. Measured against the service instead of guessed: fetching a box and its northern half shows the half matching the top of the whole, so the images arrive with their first row at the north edge and the default texture coordinates were right all along. Both the base map and the orthophoto go back to them. Imagery of a given piece of ground does not change, so a fetched tile is now kept under editor/cache/orto and the next session starts from it. Until now the cache lived only in memory, which meant going back to a slow and flaky service for ground that had already been fetched once. Gaps can be fitted with a basket curve as well: the library's compound mode, with clothoids at both ends and a chain of arcs between them. Arcs can be added and removed; the closing one takes whatever deflection the earlier ones leave, so its length is not offered for editing. --- application/planpanel.cpp | 108 ++++++++++++++++++++++++++++++++++---- application/planpanel.h | 2 + editor/orthophoto.cpp | 55 +++++++++++++++++++ editor/orthophoto.h | 5 ++ 4 files changed, 159 insertions(+), 11 deletions(-) diff --git a/application/planpanel.cpp b/application/planpanel.cpp index 03be0607..61afd573 100644 --- a/application/planpanel.cpp +++ b/application/planpanel.cpp @@ -210,11 +210,10 @@ void plan_panel::render_location_dialog() { // the picture on screen is the one that was fetched, which may lag the current view by a // request; drawing it against its own box keeps it registered while the new one arrives + // the image arrives with its first row at the north edge, so the default texture coordinates + // already line up: the first row belongs at the top, against the box's northern edge auto const &covered{m_topomap.covered()}; - // the image arrives with its first row at the north edge, which is the opposite of what the - // default texture coordinates assume, so the vertical ones are swapped - drawlist->AddImage(reinterpret_cast(static_cast(backdrop)), to_screen(covered.min_x, covered.max_y), to_screen(covered.max_x, covered.min_y), ImVec2(0.0f, 1.0f), - ImVec2(1.0f, 0.0f)); + drawlist->AddImage(reinterpret_cast(static_cast(backdrop)), to_screen(covered.min_x, covered.max_y), to_screen(covered.max_x, covered.min_y)); } auto const picked{to_screen(m_pickx, m_picky)}; @@ -352,9 +351,8 @@ void plan_panel::draw_on_scene() { continue; } - // same north-first row order as the picker's base map, so the same swap applies - drawlist->AddImageQuad(reinterpret_cast(static_cast(tile.texture)), corners[0], corners[1], corners[2], corners[3], ImVec2(0.0f, 1.0f), ImVec2(1.0f, 1.0f), - ImVec2(1.0f, 0.0f), ImVec2(0.0f, 0.0f)); + // first row is the northern edge, which is the corner the default coordinates start from + drawlist->AddImageQuad(reinterpret_cast(static_cast(tile.texture)), corners[0], corners[1], corners[2], corners[3]); } } @@ -514,7 +512,7 @@ void plan_panel::render_gaps() return; } - ImGui::BeginChild("plan_gaps", ImVec2(0.0f, 130.0f), false); + ImGui::BeginChild("plan_gaps", ImVec2(0.0f, 220.0f), false); for (std::size_t gap = 0; gap < niweleta->fits.size(); ++gap) { auto &fit{niweleta->fits[gap]}; @@ -522,16 +520,23 @@ void plan_panel::render_gaps() ImGui::Text("gap %d", static_cast(gap)); ImGui::SameLine(70.0f); - ImGui::SetNextItemWidth(150.0f); - if (ImGui::Combo("##mode", &fit.mode, "none\0arc\0arc + clothoids\0")) + ImGui::SetNextItemWidth(170.0f); + // the library numbers these; 3 is the unified compound, any number of arcs eased by clothoids + if (ImGui::Combo("##mode", &fit.mode, "none\0arc\0arc + clothoids\0compound\0")) { if (fit.mode != 0 && fit.radius <= 0.0) { fit.radius = 300.0; // something drawable to start from, rather than a rejected fit } + if (fit.mode == 3 && fit.arcs.empty()) + { + // a basket needs at least two arcs to be one; the last absorbs the leftover deflection + fit.arcs.push_back({600.0, 60.0, 0.0}); + fit.arcs.push_back({300.0, 0.0, 0.0}); + } solve(); } - if (fit.mode != 0) + if (fit.mode == 1 || fit.mode == 2) { ImGui::SameLine(); ImGui::SetNextItemWidth(120.0f); @@ -549,6 +554,10 @@ void plan_panel::render_gaps() solve(); } } + if (fit.mode == 3) + { + render_compound(fit); + } // a fit the library could not produce is simply absent from its answer; say so rather than // leaving the parameters looking as though they took effect if (fit.mode != 0 && false == fit_applied(gap)) @@ -562,6 +571,83 @@ void plan_panel::render_gaps() ImGui::EndChild(); } +void plan_panel::render_compound(maj0sted::web::GapFit &Fit) +{ + ImGui::Indent(); + + ImGui::SetNextItemWidth(110.0f); + if (ImGui::InputDouble("entry", &Fit.entry_t, 5.0, 20.0, "%.1f")) + { + solve(); + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(110.0f); + if (ImGui::InputDouble("exit", &Fit.exit_t, 5.0, 20.0, "%.1f")) + { + solve(); + } + ImGui::SameLine(); + ImGui::TextDisabled("end clothoids"); + + for (std::size_t i = 0; i < Fit.arcs.size(); ++i) + { + auto &arc{Fit.arcs[i]}; + auto const last{i + 1 == Fit.arcs.size()}; + ImGui::PushID(static_cast(i)); + + ImGui::Text("%d.", static_cast(i + 1)); + ImGui::SameLine(); + ImGui::SetNextItemWidth(110.0f); + if (ImGui::InputDouble("R", &arc.radius, 10.0, 100.0, "%.1f")) + { + solve(); + } + // the closing arc takes up whatever deflection the earlier ones leave, so its length is not + // ours to set, and there is no arc after it to ease into + if (false == last) + { + ImGui::SameLine(); + ImGui::SetNextItemWidth(110.0f); + if (ImGui::InputDouble("len", &arc.length, 10.0, 50.0, "%.1f")) + { + solve(); + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(110.0f); + if (ImGui::InputDouble("ease", &arc.transition_to_next, 5.0, 20.0, "%.1f")) + { + solve(); + } + } + else + { + ImGui::SameLine(); + ImGui::TextDisabled("closes the curve"); + } + + ImGui::PopID(); + } + + if (ImGui::SmallButton("Add arc")) + { + // inserted before the closing arc, so that one stays the one that closes the curve + auto const radius{Fit.arcs.empty() ? 300.0 : Fit.arcs.back().radius}; + Fit.arcs.insert(Fit.arcs.end() - (Fit.arcs.empty() ? 0 : 1), {radius, 60.0, 0.0}); + solve(); + } + if (Fit.arcs.size() > 2) + { + ImGui::SameLine(); + if (ImGui::SmallButton("Remove arc")) + { + Fit.arcs.erase(Fit.arcs.end() - 2); + solve(); + } + } + + ImGui::Unindent(); +} + void plan_panel::render_storage() { ImGui::Separator(); diff --git a/application/planpanel.h b/application/planpanel.h index 23a15e02..a5d98bfe 100644 --- a/application/planpanel.h +++ b/application/planpanel.h @@ -32,6 +32,8 @@ class plan_panel : public ui_panel // methods void render_toolbar(); void render_gaps(); + // parameters of a basket curve: the end clothoids and the chain of arcs between them + void render_compound(maj0sted::web::GapFit &Fit); void render_storage(); // asks what kind of scenery is being started, and where on the map it sits void render_newmap_dialog(); diff --git a/editor/orthophoto.cpp b/editor/orthophoto.cpp index e17af2e2..9bc1335f 100644 --- a/editor/orthophoto.cpp +++ b/editor/orthophoto.cpp @@ -230,6 +230,12 @@ std::vector orthophoto_source::collect(maj0sted:: std::vector tiles; for (auto const &key : maj0sted::web::TileGrid::tiles_for_view(View, maj0sted::web::TileGrid::kLevel, Maxtiles)) { + // a tile kept from an earlier session seeds the cache, so no request goes out for it at all + if (false == m_service.has(key)) + { + load_from_disk(key); + } + auto const status = m_service.request(key); if (status == maj0sted::web::TileStatus::Failed) { @@ -244,6 +250,8 @@ std::vector orthophoto_source::collect(maj0sted:: auto found = m_textures.find(key); if (found == m_textures.end()) { + // the first time a tile is drawn is also the moment its bytes are known to be good + save_to_disk(key); found = m_textures.emplace(key, upload(key)).first; } if (found->second != 0) @@ -288,6 +296,53 @@ unsigned int orthophoto_source::upload(maj0sted::web::TileKey const &Key) return texture; } +std::string orthophoto_source::tile_path(maj0sted::web::TileKey const &Key) +{ + return "editor/cache/orto/" + std::to_string(Key.zoom) + "_" + std::to_string(Key.x) + "_" + std::to_string(Key.y) + ".img"; +} + +bool orthophoto_source::load_from_disk(maj0sted::web::TileKey const &Key) +{ + std::ifstream file(tile_path(Key), std::ios::binary); + if (false == file.is_open()) + { + return false; + } + + std::vector bytes{std::istreambuf_iterator(file), std::istreambuf_iterator()}; + if (bytes.empty()) + { + return false; + } + + m_service.store(Key, std::move(bytes)); + + return true; +} + +void orthophoto_source::save_to_disk(maj0sted::web::TileKey const &Key) +{ + auto const *image = m_service.peek(Key); + if (image == nullptr || image->bytes.empty()) + { + return; + } + + auto const path = tile_path(Key); + std::error_code error; + std::filesystem::create_directories(std::filesystem::path(path).parent_path(), error); + if (std::filesystem::exists(path, error)) + { + return; // came from the disk in the first place + } + + std::ofstream file(path, std::ios::binary); + if (file.is_open()) + { + file.write(reinterpret_cast(image->bytes.data()), static_cast(image->bytes.size())); + } +} + std::size_t orthophoto_source::pending() const { return m_inflight.load(); diff --git a/editor/orthophoto.h b/editor/orthophoto.h index 51dd19d6..d11dc887 100644 --- a/editor/orthophoto.h +++ b/editor/orthophoto.h @@ -68,6 +68,11 @@ class orthophoto_source void worker(); // decodes the cached bytes of a tile and uploads them; returns 0 when the image is unusable unsigned int upload(maj0sted::web::TileKey const &Key); + // imagery of a given piece of ground never changes, so a fetched tile is kept on disk and the + // next session starts with it instead of going back to a service that is slow and flaky + static std::string tile_path(maj0sted::web::TileKey const &Key); + bool load_from_disk(maj0sted::web::TileKey const &Key); + void save_to_disk(maj0sted::web::TileKey const &Key); // members maj0sted::web::TileService m_service; std::unordered_map m_textures;