From 8d007291e2cfb6e354b62231b9a039c1be921fc2 Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 31 Jul 2026 19:41:32 +0200 Subject: [PATCH] Put Geoportal imagery behind the plan and clear the scenery on a new map The location picker now shows the real topographic map instead of a hand-drawn outline, fetched as one WMS image for whatever box is on screen; the schematic border is gone and only the city list remains, to jump the view somewhere recognisable. Under the plan itself a georeferenced map gets the orthophoto, tiled through the layout library's own EPSG:2180 grid and cache. What a native build was missing is here: the download (WinHTTP, on worker threads), the decode and the texture. Finished transfers are handed back on the thread that drains them, so both the library's cache and the graphics API stay on one thread. The imagery service turns down browser user agents and fails the odd request even for a plain one, so requests go out as curl and are retried a few times. Starting a new map now drops the loaded scenery and comes back on empty ground, instead of leaving the previous scenery's contents under a plan that has nothing to do with them. The reload is deferred to the top of the frame: taking the editor off the mode stack while it is drawing its own panels would pull the ground out from under them. --- CMakeLists.txt | 1 + application/editormode.cpp | 13 ++ application/planpanel.cpp | 89 ++++++-- application/planpanel.h | 5 + editor/orthophoto.cpp | 440 +++++++++++++++++++++++++++++++++++++ editor/orthophoto.h | 124 +++++++++++ editor/polandmap.cpp | 39 ++-- editor/polandmap.h | 7 +- utilities/Globals.h | 3 + 9 files changed, 667 insertions(+), 54 deletions(-) create mode 100644 editor/orthophoto.cpp create mode 100644 editor/orthophoto.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 27c46871..02bf5e21 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -169,6 +169,7 @@ set(SOURCES "editor/editorSettings.cpp" "editor/puwg1992.cpp" "editor/polandmap.cpp" +"editor/orthophoto.cpp" "editor/editorTerrain.cpp" "editor/editorTerrainStreamer.cpp" "application/editormode.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 35fcea4f..486278fd 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -609,6 +609,19 @@ void editor_mode::redo_last() bool editor_mode::update() { + if (Global.editor_reset_scenery) + { + // starting a new map: drop whatever scenery is loaded and come back on empty ground. done + // here rather than where it was asked for, because taking the editor off the mode stack in + // the middle of drawing its own panels would pull the ground out from under them + Global.editor_reset_scenery = false; + Global.editor_startup = true; // the editor is the base mode from here on, whatever started it + Global.SceneryFile = "pusta.scn"; + Application.pop_mode(); + Application.push_mode(eu07_application::mode::scenarioloader); + return true; + } + Timer::UpdateTimers(true); simulation::State.update_clocks(); diff --git a/application/planpanel.cpp b/application/planpanel.cpp index 81fd1d60..a92b4b70 100644 --- a/application/planpanel.cpp +++ b/application/planpanel.cpp @@ -151,31 +151,24 @@ void plan_panel::render_location_dialog() return; } - ImVec2 const mapsize{560.0f, 460.0f}; + // the request is one square image, so the view is square too and the pixels map one to one + int const mappixels = 512; + ImVec2 const mapsize{static_cast(mappixels), static_cast(mappixels)}; auto const origin{ImGui::GetCursorScreenPos()}; ImGui::InvisibleButton("poland_map", mapsize); auto const hovered{ImGui::IsItemHovered()}; - auto const &outline{editor::poland_outline()}; auto const &places{editor::poland_places()}; // fit the whole country the first time round; afterwards the wheel decides if (m_mapscale <= 0.0) { - auto minx{std::numeric_limits::max()}; - auto miny{std::numeric_limits::max()}; - auto maxx{std::numeric_limits::lowest()}; - auto maxy{std::numeric_limits::lowest()}; - for (auto const &point : outline) - { - minx = std::min(minx, point.x); - maxx = std::max(maxx, point.x); - miny = std::min(miny, point.y); - maxy = std::max(maxy, point.y); - } - m_mapviewx = (minx + maxx) * 0.5; - m_mapviewy = (miny + maxy) * 0.5; - m_mapscale = std::min(mapsize.x / std::max(1.0, maxx - minx), mapsize.y / std::max(1.0, maxy - miny)) * 0.92; + glm::dvec2 min; + glm::dvec2 max; + editor::poland_extent(min, max); + m_mapviewx = (min.x + max.x) * 0.5; + m_mapviewy = (min.y + max.y) * 0.5; + m_mapscale = mapsize.x / std::max({max.x - min.x, max.y - min.y, 1.0}); } ImVec2 const centre{origin.x + mapsize.x * 0.5f, origin.y + mapsize.y * 0.5f}; @@ -203,16 +196,25 @@ void plan_panel::render_location_dialog() } auto *drawlist{ImGui::GetWindowDrawList()}; - drawlist->PushClipRect(origin, ImVec2(origin.x + mapsize.x, origin.y + mapsize.y), true); - drawlist->AddRectFilled(origin, ImVec2(origin.x + mapsize.x, origin.y + mapsize.y), IM_COL32(22, 30, 38, 255)); + auto const mapend{ImVec2(origin.x + mapsize.x, origin.y + mapsize.y)}; + drawlist->PushClipRect(origin, mapend, true); + drawlist->AddRectFilled(origin, mapend, IM_COL32(22, 30, 38, 255)); - std::vector border; - border.reserve(outline.size()); - for (auto const &point : outline) + // the topographic base map behind everything, asked for at exactly the box on screen + double viewminx{0.0}; + double viewminy{0.0}; + double viewmaxx{0.0}; + double viewmaxy{0.0}; + to_map(origin, viewminx, viewmaxy); + to_map(mapend, viewmaxx, viewminy); + auto const backdrop{m_topomap.texture_for({viewminx, viewminy, viewmaxx, viewmaxy}, mappixels)}; + if (backdrop != 0) { - border.push_back(to_screen(point.x, point.y)); + // 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 + auto const &covered{m_topomap.covered()}; + drawlist->AddImage(reinterpret_cast(static_cast(backdrop)), to_screen(covered.min_x, covered.max_y), to_screen(covered.max_x, covered.min_y)); } - drawlist->AddPolyline(border.data(), static_cast(border.size()), IM_COL32(120, 190, 140, 255), true, 1.5f); for (auto const &place : places) { @@ -228,7 +230,7 @@ void plan_panel::render_location_dialog() drawlist->PopClipRect(); - ImGui::TextDisabled("schematic outline - click to place the scenery's zero, wheel zooms"); + ImGui::TextDisabled(m_topomap.loading() ? "fetching the topographic map..." : "click to place the scenery's zero, wheel zooms"); ImGui::SetNextItemWidth(180.0f); ImGui::InputDouble("easting", &m_pickx, 100.0, 1000.0, "%.0f"); @@ -284,6 +286,9 @@ void plan_panel::start_map(bool const Georeferenced, double const Originx, doubl camera.Pos.x = 0.0; camera.Pos.z = 0.0; + // a new map starts on empty ground - whatever scenery was loaded has nothing to do with it + Global.editor_reset_scenery = true; + m_status = Georeferenced ? "map pinned at " + to_string(Originx, 0) + ", " + to_string(Originy, 0) + " (EPSG:2180)" : "fictional map"; } @@ -347,6 +352,32 @@ void plan_panel::draw_on_scene() // stay readable on top of the drawing auto *drawlist{ImGui::GetBackgroundDrawList()}; + // orthophoto under everything else. only a georeferenced map knows where on the ground it sits, + // so a fictional one gets no imagery + if (m_showortho && Global.scenery_georeferenced) + { + auto const centre{Global.pCamera.Pos}; + double planx{0.0}; + double plany{0.0}; + world_to_plan(centre, planx, plany); + auto const reach{static_cast(Global.editor_ortho_extent) * 1.4}; + + for (auto const &tile : m_ortho.collect({planx - reach, plany - reach, planx + reach, plany + reach})) + { + ImVec2 corners[4]; + // the tile is a ground rectangle; on screen it is whatever the camera makes of it + auto const ok = world_to_screen(plan_to_world(tile.box.min_x, tile.box.max_y), corners[0]) && + world_to_screen(plan_to_world(tile.box.max_x, tile.box.max_y), corners[1]) && + world_to_screen(plan_to_world(tile.box.max_x, tile.box.min_y), corners[2]) && + world_to_screen(plan_to_world(tile.box.min_x, tile.box.min_y), corners[3]); + if (false == ok) + { + continue; + } + drawlist->AddImageQuad(reinterpret_cast(static_cast(tile.texture)), corners[0], corners[1], corners[2], corners[3]); + } + } + std::vector points; for (std::size_t n = 0; n < m_solved.size(); ++n) { @@ -458,6 +489,16 @@ void plan_panel::render_toolbar() } ImGui::SameLine(); ImGui::Checkbox("Draw", &m_drawing); + if (Global.scenery_georeferenced) + { + ImGui::SameLine(); + ImGui::Checkbox("Orthophoto", &m_showortho); + if (m_showortho && m_ortho.pending() > 0) + { + ImGui::SameLine(); + ImGui::TextDisabled("(%d tiles on the way)", static_cast(m_ortho.pending())); + } + } if (m_document.niwelety.size() > 1) { diff --git a/application/planpanel.h b/application/planpanel.h index 4a579931..23a15e02 100644 --- a/application/planpanel.h +++ b/application/planpanel.h @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "application/uilayer.h" +#include "editor/orthophoto.h" #include "maj0sted/app/editor_document.hpp" #include "maj0sted/web/editor.hpp" @@ -76,4 +77,8 @@ class plan_panel : public ui_panel double m_mapviewx{0.0}; double m_mapviewy{0.0}; double m_mapscale{0.0}; + // topographic base map behind the picker, and the orthophoto under the plan itself + editor::wms_image m_topomap{maj0sted::web::WmsConfig::geoportal_topo()}; + editor::orthophoto_source m_ortho; + bool m_showortho{true}; }; diff --git a/editor/orthophoto.cpp b/editor/orthophoto.cpp new file mode 100644 index 00000000..e17af2e2 --- /dev/null +++ b/editor/orthophoto.cpp @@ -0,0 +1,440 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/orthophoto.h" + +#include "stb/stb_image.h" +#include "utilities/Logs.h" + +#ifdef _WIN32 +#include +#include +#pragma comment(lib, "winhttp.lib") +#endif + +namespace editor +{ + +namespace +{ + +int const kWorkers = 4; +// the imagery service sits behind a filter that turns down browser user agents and, even for a +// plain one, fails a request now and then; a handful of immediate retries gets past both +int const kAttempts = 4; +char const *kUserAgent = "curl/8.5.0"; + +#ifdef _WIN32 + +// plain HTTPS GET. deliberately small: one request, one answer, no keep-alive - tiles are large +// enough that the connection setup does not dominate, and this way a stuck transfer cannot poison +// anything but itself +bool http_get(std::string const &Url, std::vector &Bytes, std::string &Contenttype) +{ + std::wstring const url(Url.begin(), Url.end()); + + URL_COMPONENTS parts{}; + parts.dwStructSize = sizeof(parts); + parts.dwHostNameLength = 1; + parts.dwUrlPathLength = 1; + parts.dwExtraInfoLength = 1; + if (false == WinHttpCrackUrl(url.c_str(), static_cast(url.size()), 0, &parts)) + { + return false; + } + + std::wstring const host(parts.lpszHostName, parts.dwHostNameLength); + std::wstring const resource = std::wstring(parts.lpszUrlPath, parts.dwUrlPathLength) + std::wstring(parts.lpszExtraInfo, parts.dwExtraInfoLength); + + auto const wideagent = std::wstring(kUserAgent, kUserAgent + std::strlen(kUserAgent)); + HINTERNET session = WinHttpOpen(wideagent.c_str(), WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); + if (session == nullptr) + { + return false; + } + + auto const cleanup = [](HINTERNET handle) { + if (handle != nullptr) + { + WinHttpCloseHandle(handle); + } + }; + + HINTERNET connection = WinHttpConnect(session, host.c_str(), parts.nPort, 0); + if (connection == nullptr) + { + cleanup(session); + return false; + } + + DWORD const flags = (parts.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0); + HINTERNET request = WinHttpOpenRequest(connection, L"GET", resource.c_str(), nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, flags); + if (request == nullptr) + { + cleanup(connection); + cleanup(session); + return false; + } + + auto ok = false; + if (WinHttpSendRequest(request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0) && WinHttpReceiveResponse(request, nullptr)) + { + DWORD status = 0; + DWORD statussize = sizeof(status); + WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &status, &statussize, WINHTTP_NO_HEADER_INDEX); + + if (status == 200) + { + wchar_t typebuffer[128]{}; + DWORD typesize = sizeof(typebuffer); + if (WinHttpQueryHeaders(request, WINHTTP_QUERY_CONTENT_TYPE, WINHTTP_HEADER_NAME_BY_INDEX, typebuffer, &typesize, WINHTTP_NO_HEADER_INDEX)) + { + std::wstring const wide(typebuffer); + Contenttype.assign(wide.begin(), wide.end()); + } + + Bytes.clear(); + DWORD available = 0; + while (WinHttpQueryDataAvailable(request, &available) && available > 0) + { + auto const offset = Bytes.size(); + Bytes.resize(offset + available); + DWORD read = 0; + if (false == WinHttpReadData(request, Bytes.data() + offset, available, &read)) + { + Bytes.clear(); + break; + } + Bytes.resize(offset + read); + } + ok = (false == Bytes.empty()); + } + } + + cleanup(request); + cleanup(connection); + cleanup(session); + + return ok; +} + +#else + +bool http_get(std::string const &, std::vector &, std::string &) +{ + return false; // no native transport on this platform yet +} + +#endif + +} // namespace + +orthophoto_source::orthophoto_source() +{ + // the library asks for a URL and expects the answer whenever it arrives; the transfer is queued + // here and handed back on the thread that drains it, so the service stays single threaded + m_service.set_fetcher([this](std::string const &Url, maj0sted::web::TileService::DoneFn Done) { + { + std::lock_guard lock(m_mutex); + m_queued.push_back({Url, std::move(Done), false, {}, {}}); + } + ++m_inflight; + m_wakeup.notify_one(); + }); + + m_workers.reserve(kWorkers); + for (auto i = 0; i < kWorkers; ++i) + { + m_workers.emplace_back([this]() { worker(); }); + } +} + +orthophoto_source::~orthophoto_source() +{ + m_quitting = true; + m_wakeup.notify_all(); + for (auto &worker : m_workers) + { + if (worker.joinable()) + { + worker.join(); + } + } + + for (auto const &texture : m_textures) + { + if (texture.second != 0) + { + auto const id = texture.second; + glDeleteTextures(1, &id); + } + } +} + +void orthophoto_source::worker() +{ + while (false == m_quitting) + { + transfer job; + { + std::unique_lock lock(m_mutex); + m_wakeup.wait(lock, [this]() { return m_quitting || (false == m_queued.empty()); }); + if (m_quitting) + { + return; + } + job = std::move(m_queued.front()); + m_queued.pop_front(); + } + + for (auto attempt = 0; attempt < kAttempts && false == m_quitting; ++attempt) + { + if (http_get(job.url, job.bytes, job.content_type)) + { + job.ok = true; + break; + } + } + + { + std::lock_guard lock(m_mutex); + m_finished.push_back(std::move(job)); + } + } +} + +std::vector orthophoto_source::collect(maj0sted::web::TileBBox const &View, int const Maxtiles) +{ + // hand finished transfers to the library here, on the caller's thread + std::deque arrived; + { + std::lock_guard lock(m_mutex); + arrived.swap(m_finished); + } + for (auto &job : arrived) + { + if (job.done) + { + job.done(job.ok, std::move(job.bytes), std::move(job.content_type)); + } + --m_inflight; + } + + std::vector tiles; + for (auto const &key : maj0sted::web::TileGrid::tiles_for_view(View, maj0sted::web::TileGrid::kLevel, Maxtiles)) + { + auto const status = m_service.request(key); + if (status == maj0sted::web::TileStatus::Failed) + { + ++m_failed; + continue; + } + if (status != maj0sted::web::TileStatus::Ready) + { + continue; + } + + auto found = m_textures.find(key); + if (found == m_textures.end()) + { + found = m_textures.emplace(key, upload(key)).first; + } + if (found->second != 0) + { + tiles.push_back({maj0sted::web::TileGrid::bbox(key), found->second}); + } + } + + return tiles; +} + +unsigned int orthophoto_source::upload(maj0sted::web::TileKey const &Key) +{ + auto const *image = m_service.peek(Key); + if (image == nullptr || image->bytes.empty()) + { + return 0; + } + + int width = 0; + int height = 0; + int channels = 0; + auto *pixels = stbi_load_from_memory(image->bytes.data(), static_cast(image->bytes.size()), &width, &height, &channels, 3); + if (pixels == nullptr) + { + ErrorLog("Orthophoto: could not decode tile imagery"); + return 0; + } + + GLuint texture = 0; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels); + + stbi_image_free(pixels); + + return texture; +} + +std::size_t orthophoto_source::pending() const +{ + return m_inflight.load(); +} + +std::size_t orthophoto_source::failed() const +{ + return m_failed; +} + +namespace +{ + +// decodes encoded image bytes straight onto a fresh texture; 0 when they are not an image +unsigned int upload_image(std::vector const &Bytes) +{ + int width = 0; + int height = 0; + int channels = 0; + auto *pixels = stbi_load_from_memory(Bytes.data(), static_cast(Bytes.size()), &width, &height, &channels, 3); + if (pixels == nullptr) + { + return 0; + } + + GLuint texture = 0; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels); + + stbi_image_free(pixels); + + return texture; +} + +bool same_box(maj0sted::web::TileBBox const &Left, maj0sted::web::TileBBox const &Right) +{ + auto const close = [](double const A, double const B) { return std::abs(A - B) < 1.0; }; + return close(Left.min_x, Right.min_x) && close(Left.min_y, Right.min_y) && close(Left.max_x, Right.max_x) && close(Left.max_y, Right.max_y); +} + +} // namespace + +wms_image::wms_image(maj0sted::web::WmsConfig Config) : m_config(std::move(Config)) +{ + m_worker = std::thread([this]() { worker(); }); +} + +wms_image::~wms_image() +{ + m_quitting = true; + m_wakeup.notify_all(); + if (m_worker.joinable()) + { + m_worker.join(); + } + if (m_texture != 0) + { + auto const id = m_texture; + glDeleteTextures(1, &id); + } +} + +void wms_image::worker() +{ + while (false == m_quitting) + { + std::string url; + { + std::unique_lock lock(m_mutex); + m_wakeup.wait(lock, [this]() { return m_quitting || m_haswork; }); + if (m_quitting) + { + return; + } + url = m_url; + m_haswork = false; + } + + std::vector bytes; + std::string contenttype; + for (auto attempt = 0; attempt < kAttempts && false == m_quitting; ++attempt) + { + if (http_get(url, bytes, contenttype)) + { + break; + } + bytes.clear(); + } + + { + std::lock_guard lock(m_mutex); + m_bytes = std::move(bytes); + m_hasresult = true; + } + } +} + +unsigned int wms_image::texture_for(maj0sted::web::TileBBox const &Box, int const Pixels) +{ + // a finished download becomes the texture on this thread, where the graphics API is safe to touch + { + std::lock_guard lock(m_mutex); + if (m_hasresult) + { + m_hasresult = false; + m_loading = false; + if (false == m_bytes.empty()) + { + auto const uploaded = upload_image(m_bytes); + if (uploaded != 0) + { + if (m_texture != 0) + { + auto const old = m_texture; + glDeleteTextures(1, &old); + } + m_texture = uploaded; + m_covered = m_inflightbox; + } + } + m_bytes.clear(); + } + } + + // only chase a genuinely different view, and only once the previous one has landed + if (false == same_box(Box, m_requested) && false == m_loading.load()) + { + m_requested = Box; + m_inflightbox = Box; + auto config = m_config; + config.tile_pixels = Pixels; + { + std::lock_guard lock(m_mutex); + m_url = maj0sted::web::wms_getmap_url(config, Box); + m_haswork = true; + } + m_loading = true; + m_wakeup.notify_one(); + } + + return m_texture; +} + +} // namespace editor diff --git a/editor/orthophoto.h b/editor/orthophoto.h new file mode 100644 index 00000000..51dd19d6 --- /dev/null +++ b/editor/orthophoto.h @@ -0,0 +1,124 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "maj0sted/web/tile_cache.hpp" + +namespace editor +{ + +// orthophoto backdrop for the plan view. the tiling scheme, the cache and the retry policy come +// from the track layout library, which already speaks EPSG:2180; this adds what a native build +// needs and the web one got from the browser - the download itself, the decode and the texture. +// +// downloads run on worker threads, but nothing else does: finished transfers are handed back on +// the thread that calls collect(), so both the library's cache and the graphics API are only ever +// touched from there. +class orthophoto_source +{ + + public: + struct ready_tile + { + maj0sted::web::TileBBox box; + unsigned int texture{0}; + }; + + orthophoto_source(); + ~orthophoto_source(); + orthophoto_source(orthophoto_source const &) = delete; + orthophoto_source &operator=(orthophoto_source const &) = delete; + + // asks for every tile covering the view and returns those that are already drawable. tiles still + // on their way simply aren't in the answer yet; call again next frame + std::vector collect(maj0sted::web::TileBBox const &View, int const Maxtiles = 48); + // how many tiles are still being fetched, for the panel to report + std::size_t pending() const; + // how many tiles gave up after exhausting their retries + std::size_t failed() const; + + private: + // types + struct transfer + { + std::string url; + maj0sted::web::TileService::DoneFn done; + bool ok{false}; + std::vector bytes; + std::string content_type; + }; + // methods + 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); + // members + maj0sted::web::TileService m_service; + std::unordered_map m_textures; + + mutable std::mutex m_mutex; + std::condition_variable m_wakeup; + std::deque m_queued; + std::deque m_finished; + std::vector m_workers; + std::atomic m_quitting{false}; + std::atomic m_inflight{0}; + std::size_t m_failed{0}; +}; + +// a single WMS image covering an arbitrary box, for views too coarse for the tile grid - the whole +// country will not fit into 100 m cells. one request is in flight at a time: asking for a different +// box supersedes whatever was being fetched +class wms_image +{ + + public: + explicit wms_image(maj0sted::web::WmsConfig Config); + ~wms_image(); + wms_image(wms_image const &) = delete; + wms_image &operator=(wms_image const &) = delete; + + // asks for the given box at the given size and returns the texture currently available, which + // may still be the previous box's until the new one arrives; 0 when there is nothing to show yet + unsigned int texture_for(maj0sted::web::TileBBox const &Box, int const Pixels); + // the box the returned texture actually covers + maj0sted::web::TileBBox const &covered() const { return m_covered; } + bool loading() const { return m_loading.load(); } + + private: + void worker(); + + maj0sted::web::WmsConfig m_config; + maj0sted::web::TileBBox m_requested{}; + maj0sted::web::TileBBox m_inflightbox{}; + maj0sted::web::TileBBox m_covered{}; + unsigned int m_texture{0}; + + mutable std::mutex m_mutex; + std::condition_variable m_wakeup; + std::string m_url; + bool m_haswork{false}; + std::vector m_bytes; + bool m_hasresult{false}; + std::thread m_worker; + std::atomic m_quitting{false}; + std::atomic m_loading{false}; +}; + +} // namespace editor diff --git a/editor/polandmap.cpp b/editor/polandmap.cpp index 65b97fef..315611bd 100644 --- a/editor/polandmap.cpp +++ b/editor/polandmap.cpp @@ -26,21 +26,10 @@ struct geo_point double longitude; }; -// the border walked clockwise from the north-west corner: Baltic coast, the Kaliningrad and -// Lithuanian borders, the Bug, the Carpathians and Sudetes, then the Nysa and Odra back north. -// tens of kilometres of error in places - this is a locator sketch, nothing more -constexpr geo_point kOutline[]{ - {53.93, 14.27}, {54.05, 14.60}, {54.18, 15.58}, {54.42, 16.41}, {54.58, 16.86}, {54.76, 17.55}, - {54.79, 18.40}, {54.60, 18.65}, {54.52, 18.55}, {54.35, 18.95}, {54.37, 19.35}, {54.45, 19.64}, - {54.37, 20.50}, {54.35, 21.50}, {54.36, 22.00}, {54.36, 22.79}, {54.25, 23.20}, {54.15, 23.48}, - {53.95, 23.90}, {53.20, 23.93}, {52.70, 23.85}, {52.30, 23.60}, {52.07, 23.62}, {51.55, 23.55}, - {50.87, 24.15}, {50.72, 24.15}, {50.40, 23.70}, {50.28, 23.50}, {49.80, 22.92}, {49.20, 22.68}, - {49.09, 22.55}, {49.25, 22.00}, {49.42, 21.75}, {49.35, 21.00}, {49.40, 20.50}, {49.20, 20.10}, - {49.40, 19.80}, {49.40, 19.50}, {49.40, 19.20}, {49.51, 18.85}, {49.75, 18.63}, {49.95, 18.05}, - {50.10, 17.70}, {50.31, 17.38}, {50.45, 16.90}, {50.20, 16.60}, {50.40, 16.35}, {50.44, 16.25}, - {50.75, 15.80}, {50.75, 15.55}, {50.85, 15.00}, {50.87, 14.82}, {51.30, 14.95}, {51.60, 14.75}, - {52.00, 14.70}, {52.35, 14.55}, {52.59, 14.65}, {52.85, 14.40}, {53.10, 14.35}, {53.55, 14.30}, - {53.75, 14.28}}; +// the country's corners, wide enough to hold all of it: the Odra in the west, the Bug in the east, +// the Tatras in the south and the coast in the north +constexpr geo_point kSouthWest{49.00, 14.10}; +constexpr geo_point kNorthEast{54.90, 24.20}; struct named_geo_point { @@ -59,19 +48,17 @@ constexpr named_geo_point kPlaces[]{ } // namespace -std::vector const &poland_outline() +void poland_extent(glm::dvec2 &Min, glm::dvec2 &Max) { - static std::vector const outline = []() { - std::vector points; - points.reserve(std::size(kOutline)); - for (auto const &point : kOutline) - { - points.push_back(puwg1992(point.latitude, point.longitude)); - } - return points; - }(); + // the projection is not axis-aligned with the graticule, so the extreme corners come from + // projecting all four of them and taking the outermost values + auto const sw = puwg1992(kSouthWest.latitude, kSouthWest.longitude); + auto const se = puwg1992(kSouthWest.latitude, kNorthEast.longitude); + auto const nw = puwg1992(kNorthEast.latitude, kSouthWest.longitude); + auto const ne = puwg1992(kNorthEast.latitude, kNorthEast.longitude); - return outline; + Min = {std::min({sw.x, se.x, nw.x, ne.x}), std::min({sw.y, se.y, nw.y, ne.y})}; + Max = {std::max({sw.x, se.x, nw.x, ne.x}), std::max({sw.y, se.y, nw.y, ne.y})}; } std::vector const &poland_places() diff --git a/editor/polandmap.h b/editor/polandmap.h index f237e040..7ee65f63 100644 --- a/editor/polandmap.h +++ b/editor/polandmap.h @@ -24,11 +24,10 @@ struct map_place glm::dvec2 position; }; -// schematic outline of the country's border, in EPSG:2180 metres. coarse on purpose: it is here so -// a region can be pointed at, not so anything can be measured against it -std::vector const &poland_outline(); - // larger cities, to put the view somewhere recognisable before picking the exact spot std::vector const &poland_places(); +// the whole country's extent in EPSG:2180 metres, as the starting view of a location picker +void poland_extent(glm::dvec2 &Min, glm::dvec2 &Max); + } // namespace editor diff --git a/utilities/Globals.h b/utilities/Globals.h index d218d535..57964617 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -86,6 +86,9 @@ struct global_settings { // nie znaczą nic poza nią samą bool scenery_georeferenced{ false }; glm::dvec2 scenery_origin{ 0.0, 0.0 }; + // prośba o zrzucenie wczytanej scenerii i zaczęcie na pustym terenie; obsługiwana na początku + // klatki, bo zmiana trybu w środku rysowania panelu wyrwałaby grunt spod niego samego + bool editor_reset_scenery{ false }; std::string local_start_vehicle{ "EU07-424" }; int iConvertModels{ 0 }; // tworzenie plików binarnych int iConvertIndexRange{ 1000 }; // range of duplicate vertex scan