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