mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 22:39:19 +02:00
Editor: fold the plan library into the tree as modules, drop its own name
The track layout library came in as a subproject of its own -- its own CMakeLists, its own install and export rules, its own include tree -- which only made the build harder to follow now that everything else is a module. It is part of the editor, so it lives where the editor lives. - editor/plan_*.cppm / plan_*.cpp: the fifteen interface units and twelve implementation units, flat in editor/ beside editorTerrain and orthophoto, with the plan_ prefix keeping them clear of what was already there (editor/turnouts is the preset catalogue, so the geometry is plan_turnout_geometry). - modules are named eu07.editor.plan_*, like every other module in the tree. - namespaces move under the editor's own: editor::plan, editor::plan::geometry, ::io, ::render. The library's old name is gone from the sources and the docs. - every moved file carries the MPL header the rest of the tree carries; it had none. version.hpp went with the subproject: macros only, and unused. - the umbrella header is dropped -- nothing imported it. - the unit tests survive the fold: editor/tests, behind EU07_EDITOR_PLAN_TESTS (off by default), compiles the interfaces a second time into a small library of its own, because a test can only import modules from a target it links and linking the game would drag the whole simulator in. The code under test needs nothing but a compiler. 7/7 pass. - a switch may now be written with two dashes as well: --editor beside -editor. Builds with clang 22 (-DWITH_BETTER_RENDERER=OFF); -editor and --editor both come up in the editor on empty ground.
This commit is contained in:
275
editor/plan_tilecache.cpp
Normal file
275
editor/plan_tilecache.cpp
Normal file
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
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/.
|
||||
*/
|
||||
module;
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
#include <utility>
|
||||
|
||||
module eu07.editor.plan_tilecache;
|
||||
|
||||
namespace editor::plan {
|
||||
|
||||
namespace {
|
||||
|
||||
// Shortest round-trippable, locale-independent representation of a double —
|
||||
// same convention as editor::plan::io so URLs are stable and precise.
|
||||
std::string num(double value) {
|
||||
char buffer[64];
|
||||
const auto result = std::to_chars(buffer, buffer + sizeof(buffer), value);
|
||||
return std::string(buffer, result.ptr);
|
||||
}
|
||||
|
||||
// Maximum number of retry attempts per tile after the initial fetch fails.
|
||||
constexpr int kMaxRetries = 10;
|
||||
|
||||
} // namespace
|
||||
|
||||
// --- TileGrid ------------------------------------------------------------
|
||||
|
||||
double TileGrid::cell_metres(const TileKey& key) noexcept {
|
||||
return key.zoom >= kCellZoomMin ? static_cast<double>(key.zoom) : kCellMetres;
|
||||
}
|
||||
|
||||
double TileGrid::resolution(int zoom) noexcept {
|
||||
return tile_span(zoom) / static_cast<double>(kTilePixels);
|
||||
}
|
||||
|
||||
double TileGrid::tile_span(int zoom) noexcept {
|
||||
return zoom >= kCellZoomMin ? static_cast<double>(zoom) : kCellMetres;
|
||||
}
|
||||
|
||||
TileKey TileGrid::tile_at(double x, double y, int zoom) noexcept {
|
||||
const double cell = tile_span(zoom);
|
||||
const int key_zoom = zoom >= kCellZoomMin ? zoom : kLevel;
|
||||
const auto safe_index = [cell](double coordinate, double origin) {
|
||||
const double value = std::floor((coordinate - origin) / cell);
|
||||
if (!std::isfinite(value)) return 0;
|
||||
if (value <= static_cast<double>(std::numeric_limits<int>::min())) {
|
||||
return std::numeric_limits<int>::min();
|
||||
}
|
||||
if (value >= static_cast<double>(std::numeric_limits<int>::max())) {
|
||||
return std::numeric_limits<int>::max();
|
||||
}
|
||||
return static_cast<int>(value);
|
||||
};
|
||||
return TileKey{key_zoom, safe_index(x, kOriginX), safe_index(y, kOriginY)};
|
||||
}
|
||||
|
||||
TileBBox TileGrid::bbox(const TileKey& key) noexcept {
|
||||
const double cell = cell_metres(key);
|
||||
const double min_x = kOriginX + static_cast<double>(key.x) * cell;
|
||||
const double min_y = kOriginY + static_cast<double>(key.y) * cell;
|
||||
return TileBBox{min_x, min_y, min_x + cell, min_y + cell};
|
||||
}
|
||||
|
||||
int TileGrid::zoom_for_resolution(double /*metres_per_pixel*/) noexcept {
|
||||
return kLevel; // one fixed 100 m grid, no pyramid
|
||||
}
|
||||
|
||||
std::vector<TileKey> TileGrid::tiles_for_view(const TileBBox& view, int zoom,
|
||||
int max_tiles) {
|
||||
std::vector<TileKey> tiles;
|
||||
if (max_tiles <= 0 || !std::isfinite(view.min_x) ||
|
||||
!std::isfinite(view.min_y) || !std::isfinite(view.max_x) ||
|
||||
!std::isfinite(view.max_y)) {
|
||||
return tiles;
|
||||
}
|
||||
const double min_x = std::min(view.min_x, view.max_x);
|
||||
const double max_x = std::max(view.min_x, view.max_x);
|
||||
const double min_y = std::min(view.min_y, view.max_y);
|
||||
const double max_y = std::max(view.min_y, view.max_y);
|
||||
|
||||
const TileKey lo = tile_at(min_x, min_y, zoom);
|
||||
const TileKey hi = tile_at(max_x, max_y, zoom);
|
||||
|
||||
using Wide = std::int64_t;
|
||||
const Wide width = static_cast<Wide>(hi.x) - lo.x + 1;
|
||||
const Wide height = static_cast<Wide>(hi.y) - lo.y + 1;
|
||||
if (width <= 0 || height <= 0) return tiles;
|
||||
|
||||
Wide columns = width;
|
||||
Wide rows = height;
|
||||
const Wide cap = max_tiles;
|
||||
if (height > cap || width > cap / height) {
|
||||
// A fixed 100 m grid can contain millions of cells when zoomed far out.
|
||||
// Return a small, viewport-centred window instead of walking from a
|
||||
// remote corner or allocating/requesting the whole range.
|
||||
const long double aspect =
|
||||
static_cast<long double>(width) / static_cast<long double>(height);
|
||||
columns = std::clamp<Wide>(
|
||||
static_cast<Wide>(std::sqrt(static_cast<long double>(cap) * aspect)),
|
||||
1, std::min(width, cap));
|
||||
rows = std::clamp<Wide>(cap / columns, 1, height);
|
||||
}
|
||||
|
||||
const Wide centre_x =
|
||||
std::midpoint(static_cast<Wide>(lo.x), static_cast<Wide>(hi.x));
|
||||
const Wide centre_y =
|
||||
std::midpoint(static_cast<Wide>(lo.y), static_cast<Wide>(hi.y));
|
||||
const Wide first_col = std::clamp(
|
||||
centre_x - columns / 2, static_cast<Wide>(lo.x),
|
||||
static_cast<Wide>(hi.x) - columns + 1);
|
||||
const Wide first_row = std::clamp(
|
||||
centre_y - rows / 2, static_cast<Wide>(lo.y),
|
||||
static_cast<Wide>(hi.y) - rows + 1);
|
||||
|
||||
const int key_zoom = zoom >= kCellZoomMin ? zoom : kLevel;
|
||||
tiles.reserve(static_cast<std::size_t>(columns * rows));
|
||||
for (Wide row = first_row; row < first_row + rows; ++row) {
|
||||
for (Wide col = first_col; col < first_col + columns; ++col) {
|
||||
tiles.push_back(
|
||||
TileKey{key_zoom, static_cast<int>(col), static_cast<int>(row)});
|
||||
}
|
||||
}
|
||||
return tiles;
|
||||
}
|
||||
|
||||
// --- WmsConfig -----------------------------------------------------------
|
||||
|
||||
WmsConfig WmsConfig::geoportal_ortho() {
|
||||
WmsConfig cfg;
|
||||
// Public Geoportal ortophoto WMS (EPSG:2180). Swap base_url/layers via
|
||||
// set_config() from the host to point at a different backend.
|
||||
cfg.base_url =
|
||||
"https://mapy.geoportal.gov.pl/wss/service/PZGIK/ORTO/WMS/StandardResolution";
|
||||
cfg.layers = "Raster";
|
||||
cfg.format = "image/jpeg";
|
||||
return cfg;
|
||||
}
|
||||
|
||||
WmsConfig WmsConfig::geoportal_topo() {
|
||||
WmsConfig cfg;
|
||||
cfg.base_url =
|
||||
"https://mapy.geoportal.gov.pl/wss/service/img/guest/TOPO/MapServer/WMSServer";
|
||||
cfg.layers = "Raster";
|
||||
cfg.format = "image/png";
|
||||
return cfg;
|
||||
}
|
||||
|
||||
std::string wms_getmap_url(const WmsConfig& cfg, const TileBBox& box) {
|
||||
std::string url = cfg.base_url;
|
||||
url += (url.find('?') == std::string::npos) ? '?' : '&';
|
||||
url += "SERVICE=WMS";
|
||||
url += "&VERSION=" + cfg.version;
|
||||
url += "&REQUEST=GetMap";
|
||||
url += "&LAYERS=" + cfg.layers;
|
||||
url += "&STYLES=";
|
||||
url += "&SRS=" + cfg.srs;
|
||||
url += "&FORMAT=" + cfg.format;
|
||||
url += "&WIDTH=" + std::to_string(cfg.tile_pixels);
|
||||
url += "&HEIGHT=" + std::to_string(cfg.tile_pixels);
|
||||
// WMS 1.1.1: BBOX = minx,miny,maxx,maxy in the SRS axis order (easting,
|
||||
// northing for EPSG:2180).
|
||||
url += "&BBOX=" + num(box.min_x) + "," + num(box.min_y) + "," + num(box.max_x) +
|
||||
"," + num(box.max_y);
|
||||
return url;
|
||||
}
|
||||
|
||||
// --- TileService ---------------------------------------------------------
|
||||
|
||||
TileService::TileService(WmsConfig cfg) : cfg_{std::move(cfg)} {}
|
||||
|
||||
std::string TileService::url_for(const TileKey& key) const {
|
||||
return wms_getmap_url(cfg_, TileGrid::bbox(key));
|
||||
}
|
||||
|
||||
bool TileService::has(const TileKey& key) const {
|
||||
const auto it = cache_.find(key);
|
||||
return it != cache_.end() && it->second.status == TileStatus::Ready;
|
||||
}
|
||||
|
||||
const TileImage* TileService::peek(const TileKey& key) const {
|
||||
const auto it = cache_.find(key);
|
||||
if (it != cache_.end() && it->second.status == TileStatus::Ready) {
|
||||
return &it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void TileService::store(const TileKey& key, std::vector<std::uint8_t> bytes,
|
||||
std::string content_type) {
|
||||
TileImage& image = cache_[key];
|
||||
image.status = TileStatus::Ready;
|
||||
image.bytes = std::move(bytes);
|
||||
image.content_type = std::move(content_type);
|
||||
}
|
||||
|
||||
TileStatus TileService::request(const TileKey& key) {
|
||||
const auto it = cache_.find(key);
|
||||
if (it != cache_.end()) {
|
||||
// A tile that gave up (10 failed attempts) is retried the next time it is
|
||||
// asked for (a pan/zoom/toggle), so the underlay self-heals once the
|
||||
// endpoint is reachable again — no page reload needed.
|
||||
if (it->second.status == TileStatus::Failed) {
|
||||
it->second.attempts = 0;
|
||||
it->second.status = TileStatus::Loading;
|
||||
begin_fetch(key);
|
||||
}
|
||||
return it->second.status; // otherwise: hit, no network
|
||||
}
|
||||
|
||||
cache_[key].status = TileStatus::Loading;
|
||||
begin_fetch(key);
|
||||
return cache_[key].status;
|
||||
}
|
||||
|
||||
void TileService::complete_fetch(const TileKey& key, bool ok,
|
||||
std::vector<std::uint8_t> bytes,
|
||||
std::string content_type) {
|
||||
if (ok) {
|
||||
store(key, std::move(bytes), std::move(content_type));
|
||||
if (on_ready_) on_ready_(key);
|
||||
return;
|
||||
}
|
||||
// Retry up to kMaxRetries times per tile; after that give up (Failed).
|
||||
TileImage& image = cache_[key];
|
||||
if (++image.attempts <= kMaxRetries) {
|
||||
image.status = TileStatus::Loading;
|
||||
schedule_retry(key);
|
||||
} else {
|
||||
// Give up quietly: do NOT notify the host, so a Failed tile does not by
|
||||
// itself trigger a redraw → re-request → refetch loop in the background.
|
||||
// It is retried only on the next user-driven request (see request()).
|
||||
image.status = TileStatus::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
void TileService::begin_fetch(const TileKey& key) {
|
||||
const std::string url = url_for(key);
|
||||
|
||||
// The host injects the fetcher (the editor's threaded downloader, or a test
|
||||
// stub). Without one there is no transport, so the tile simply fails.
|
||||
if (fetcher_) {
|
||||
const TileKey captured = key;
|
||||
fetcher_(url, [this, captured](bool ok, std::vector<std::uint8_t> bytes,
|
||||
std::string content_type) {
|
||||
complete_fetch(captured, ok, std::move(bytes), std::move(content_type));
|
||||
});
|
||||
return;
|
||||
}
|
||||
complete_fetch(key, false, {}, {});
|
||||
}
|
||||
|
||||
void TileService::retry_now(const TileKey& key) { begin_fetch(key); }
|
||||
|
||||
void TileService::schedule_retry(const TileKey& key) {
|
||||
// Re-issue at once; the injected fetcher owns the timing, so a fetcher that
|
||||
// eventually succeeds ends the retry loop.
|
||||
begin_fetch(key);
|
||||
}
|
||||
|
||||
} // namespace editor::plan
|
||||
Reference in New Issue
Block a user