17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 03:59:19 +02:00
Files
maszyna/editor/plan_tilecache.cpp
maj00r 321c9dd7ab 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.
2026-09-13 13:31:01 +02:00

276 lines
9.8 KiB
C++

/*
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