17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-19 04:59:20 +02:00

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.
This commit is contained in:
maj00r
2026-07-31 19:41:32 +02:00
committed by maj00r
parent bf82c35734
commit 8d007291e2
9 changed files with 667 additions and 54 deletions

View File

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

View File

@@ -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();

View File

@@ -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<float>(mappixels), static_cast<float>(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<double>::max()};
auto miny{std::numeric_limits<double>::max()};
auto maxx{std::numeric_limits<double>::lowest()};
auto maxy{std::numeric_limits<double>::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<ImVec2> 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<ImTextureID>(static_cast<intptr_t>(backdrop)), to_screen(covered.min_x, covered.max_y), to_screen(covered.max_x, covered.min_y));
}
drawlist->AddPolyline(border.data(), static_cast<int>(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<double>(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<ImTextureID>(static_cast<intptr_t>(tile.texture)), corners[0], corners[1], corners[2], corners[3]);
}
}
std::vector<ImVec2> 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<int>(m_ortho.pending()));
}
}
if (m_document.niwelety.size() > 1)
{

View File

@@ -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};
};

440
editor/orthophoto.cpp Normal file
View File

@@ -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 <windows.h>
#include <winhttp.h>
#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<std::uint8_t> &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<DWORD>(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::uint8_t> &, 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(m_mutex);
m_finished.push_back(std::move(job));
}
}
}
std::vector<orthophoto_source::ready_tile> 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<transfer> arrived;
{
std::lock_guard<std::mutex> 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<ready_tile> 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<int>(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<std::uint8_t> const &Bytes)
{
int width = 0;
int height = 0;
int channels = 0;
auto *pixels = stbi_load_from_memory(Bytes.data(), static_cast<int>(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<std::mutex> 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<std::uint8_t> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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

124
editor/orthophoto.h Normal file
View File

@@ -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 <atomic>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
#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<ready_tile> 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<std::uint8_t> 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<maj0sted::web::TileKey, unsigned int, maj0sted::web::TileKeyHash> m_textures;
mutable std::mutex m_mutex;
std::condition_variable m_wakeup;
std::deque<transfer> m_queued;
std::deque<transfer> m_finished;
std::vector<std::thread> m_workers;
std::atomic<bool> m_quitting{false};
std::atomic<std::size_t> 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<std::uint8_t> m_bytes;
bool m_hasresult{false};
std::thread m_worker;
std::atomic<bool> m_quitting{false};
std::atomic<bool> m_loading{false};
};
} // namespace editor

View File

@@ -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<glm::dvec2> const &poland_outline()
void poland_extent(glm::dvec2 &Min, glm::dvec2 &Max)
{
static std::vector<glm::dvec2> const outline = []() {
std::vector<glm::dvec2> 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<map_place> const &poland_places()

View File

@@ -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<glm::dvec2> const &poland_outline();
// larger cities, to put the view somewhere recognisable before picking the exact spot
std::vector<map_place> 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

View File

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