From 632c18fbd13513e4df659ea9c284c4bb4607552c Mon Sep 17 00:00:00 2001 From: Matthias Frei Date: Wed, 14 Jan 2026 15:53:27 +0100 Subject: [PATCH 1/3] Preparation for "crs" support for vector tiles; explicit representation of raster tile source description --- shared/public/RasterVectorLayerDescription.h | 69 +++++++---------- shared/public/VectorLayerDescription.h | 3 +- shared/public/VectorMapSourceDescription.h | 2 +- .../tiled/vector/Tiled2dMapVectorLayer.cpp | 6 +- .../Tiled2dMapVectorLayerParserHelper.cpp | 77 ++++++++----------- .../Tiled2dMapVectorRasterSubLayerConfig.h | 35 +++------ .../tiled/wmts/WmtsTiled2dMapLayerConfig.cpp | 1 - 7 files changed, 75 insertions(+), 118 deletions(-) diff --git a/shared/public/RasterVectorLayerDescription.h b/shared/public/RasterVectorLayerDescription.h index 73f694ac2..a5db49098 100644 --- a/shared/public/RasterVectorLayerDescription.h +++ b/shared/public/RasterVectorLayerDescription.h @@ -11,7 +11,7 @@ #pragma once #include "VectorLayerDescription.h" -#include "Color.h" +#include "VectorMapSourceDescription.h" #include "RasterShaderStyle.h" #include "FeatureValueEvaluator.h" @@ -127,68 +127,53 @@ class RasterVectorStyle { FeatureValueEvaluator blendModeEvaluator; }; +struct RasterVectorMapSourceDescription : public VectorMapSourceDescription { + bool maskTiles; + + RasterVectorMapSourceDescription(std::string identifier, + std::string url, + int minZoom, + int maxZoom, + std::optional<::RectCoord> bounds, + std::optional zoomLevelScaleFactor, + std::optional adaptScaleToScreen, + std::optional numDrawPreviousLayers, + std::optional underzoom, + std::optional overzoom, + std::optional> levels, + bool maskTiles) : + VectorMapSourceDescription(identifier, url, minZoom, maxZoom, bounds, zoomLevelScaleFactor, adaptScaleToScreen, numDrawPreviousLayers, underzoom, overzoom, levels), + maskTiles(maskTiles) {} +}; + class RasterVectorLayerDescription: public VectorLayerDescription { public: VectorLayerType getType() override { return VectorLayerType::raster; }; - std::string url; + std::shared_ptr source; RasterVectorStyle style; - bool adaptScaleToScreen; - int32_t numDrawPreviousLayers; - bool maskTiles; - double zoomLevelScaleFactor; - bool overzoom; - bool underzoom; - std::optional<::RectCoord> bounds; - std::optional coordinateReferenceSystem; - std::optional> levels; RasterVectorLayerDescription(std::string identifier, - std::string source, + std::shared_ptr source, int minZoom, int maxZoom, - int sourceMinZoom, - int sourceMaxZoom, - std::string url, std::shared_ptr filter, - RasterVectorStyle style, - bool adaptScaleToScreen, - int32_t numDrawPreviousLayers, - bool maskTiles, - double zoomLevelScaleFactor, std::optional renderPassIndex, std::shared_ptr interactable, - bool underzoom, - bool overzoom, - std::optional<::RectCoord> bounds, - std::optional coordinateReferenceSystem, - std::optional> levels) : - VectorLayerDescription(identifier, source, "", minZoom, maxZoom, sourceMinZoom, sourceMaxZoom, filter, renderPassIndex, interactable, false, false), - style(style), url(url), underzoom(underzoom), overzoom(overzoom), adaptScaleToScreen(adaptScaleToScreen), numDrawPreviousLayers(numDrawPreviousLayers), - maskTiles(maskTiles), zoomLevelScaleFactor(zoomLevelScaleFactor), bounds(bounds), coordinateReferenceSystem(coordinateReferenceSystem), levels(levels) {}; - + RasterVectorStyle style) + : VectorLayerDescription(identifier, source->identifier, "", minZoom, maxZoom, source->minZoom, source->maxZoom, filter, renderPassIndex, interactable, false, false) + , source(source) + , style(style) {} std::unique_ptr clone() override { return std::make_unique(identifier, source, minZoom, maxZoom, - sourceMinZoom, - sourceMaxZoom, - url, filter, - style, - adaptScaleToScreen, - numDrawPreviousLayers, - maskTiles, - zoomLevelScaleFactor, renderPassIndex, interactable ? interactable->clone() : nullptr, - underzoom, - overzoom, - bounds, - coordinateReferenceSystem, - levels); + style); } virtual UsedKeysCollection getUsedKeys() const override { diff --git a/shared/public/VectorLayerDescription.h b/shared/public/VectorLayerDescription.h index 684bd9213..543b9fa71 100644 --- a/shared/public/VectorLayerDescription.h +++ b/shared/public/VectorLayerDescription.h @@ -11,7 +11,8 @@ #pragma once #include "Value.h" -#include +#include +#include enum VectorLayerType { background, raster, line, polygon, symbol, custom diff --git a/shared/public/VectorMapSourceDescription.h b/shared/public/VectorMapSourceDescription.h index 20081a43e..251d953e7 100644 --- a/shared/public/VectorMapSourceDescription.h +++ b/shared/public/VectorMapSourceDescription.h @@ -11,7 +11,6 @@ #pragma once #include "VectorLayerDescription.h" -#include "Color.h" #include "GeoJsonTypes.h" #include "RectCoord.h" @@ -28,6 +27,7 @@ class VectorMapSourceDescription { std::optional underzoom; std::optional overzoom; std::optional> levels; + std::optional coordinateReferenceSystem; VectorMapSourceDescription(std::string identifier, std::string vectorUrl, diff --git a/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayer.cpp b/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayer.cpp index 96baf38a9..e03f165fa 100644 --- a/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayer.cpp +++ b/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayer.cpp @@ -332,10 +332,8 @@ void Tiled2dMapVectorLayer::initializeVectorLayer() { break; } case raster: { - auto rasterSubLayerConfig = customZoomInfo.has_value() ? std::make_shared( - std::static_pointer_cast(layerDesc), is3d,*customZoomInfo) - : std::make_shared( - std::static_pointer_cast(layerDesc), is3d); + auto rasterSubLayerConfig = std::make_shared( + std::dynamic_pointer_cast(layerDesc), is3d, customZoomInfo); auto sourceMailbox = std::make_shared(mapInterface->getScheduler()); auto sourceActor = Actor(sourceMailbox, diff --git a/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayerParserHelper.cpp b/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayerParserHelper.cpp index ae0d45e59..b828c8ee0 100644 --- a/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayerParserHelper.cpp +++ b/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayerParserHelper.cpp @@ -73,7 +73,7 @@ Tiled2dMapVectorLayerParserResult Tiled2dMapVectorLayerParserHelper::parseStyleJ std::vector> layers; - std::map> rasterLayerMap; + std::map> rasterSourceMap; std::map> geojsonSources; @@ -174,28 +174,22 @@ Tiled2dMapVectorLayerParserResult Tiled2dMapVectorLayerParserHelper::parseStyleJ maxZoom = json.value("maxzoom", 22); } - - RasterVectorStyle style = RasterVectorStyle(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); - rasterLayerMap[key] = std::make_shared(layerName, - key, - 0, - 24, - minZoom, - maxZoom, - url, - nullptr, - style, - adaptScaleToScreen, - numDrawPreviousLayers, - maskTiles, - zoomLevelScaleFactor, - std::nullopt, - nullptr, - underzoom, - overzoom, - bounds, - coordinateReferenceSystem, - levels); + // XXX: coordinateReferenceSystem + // XXX: maskTiles + rasterSourceMap[key] = std::make_shared( + key, + url, + minZoom, + maxZoom, + bounds, + adaptScaleToScreen, + numDrawPreviousLayers, + zoomLevelScaleFactor, + underzoom, + overzoom, + levels, + maskTiles + ); } else if (type == "vector" && val["url"].is_string()) { auto result = LoaderHelper::loadData(replaceUrlParams(val["url"].get(), sourceUrlParams), std::nullopt, loaders); @@ -361,8 +355,8 @@ Tiled2dMapVectorLayerParserResult Tiled2dMapVectorLayerParserHelper::parseStyleJ interactable); layers.push_back(layerDesc); - } else if (val["type"] == "raster" && rasterLayerMap.count(val["source"]) != 0) { - auto layer = rasterLayerMap[val["source"]]; + } else if (val["type"] == "raster" && rasterSourceMap.count(val["source"]) != 0) { + const auto &source = rasterSourceMap[val["source"]]; RasterVectorStyle style = RasterVectorStyle(parser.parseValue(val["paint"]["raster-opacity"]), parser.parseValue(val["paint"]["raster-brightness-min"]), parser.parseValue(val["paint"]["raster-brightness-max"]), @@ -373,30 +367,21 @@ Tiled2dMapVectorLayerParserResult Tiled2dMapVectorLayerParserHelper::parseStyleJ blendMode); std::shared_ptr filter = parser.parseValue(val["filter"]); - bool underzoom = layer->underzoom && !val.contains("minzoom"); - bool overzoom = layer->overzoom && !val.contains("maxzoom"); + // TODO + [[maybe_unused]] + bool underzoom = source->underzoom && !val.contains("minzoom"); + [[maybe_unused]] + bool overzoom = source->overzoom && !val.contains("maxzoom"); - auto newLayer = std::make_shared(val["id"], - val["source"], - val.value("minzoom", layer->minZoom), - val.value("maxzoom", layer->maxZoom), - layer->sourceMinZoom, - layer->sourceMaxZoom, - layer->url, + auto layer = std::make_shared(val["id"], + source, + val.value("minzoom", source->minZoom), + val.value("maxzoom", source->maxZoom), filter, - style, - layer->adaptScaleToScreen, - layer->numDrawPreviousLayers, - layer->maskTiles, - layer->zoomLevelScaleFactor, - layer->renderPassIndex, + renderPassIndex, interactable, - underzoom, - overzoom, - layer->bounds, - layer->coordinateReferenceSystem, - layer->levels); - layers.push_back(newLayer); + style); + layers.push_back(layer); } else if (val["type"] == "line") { std::shared_ptr filter = parser.parseValue(val["filter"]); diff --git a/shared/src/map/layers/tiled/vector/Tiled2dMapVectorRasterSubLayerConfig.h b/shared/src/map/layers/tiled/vector/Tiled2dMapVectorRasterSubLayerConfig.h index 0dca53544..420b017bc 100644 --- a/shared/src/map/layers/tiled/vector/Tiled2dMapVectorRasterSubLayerConfig.h +++ b/shared/src/map/layers/tiled/vector/Tiled2dMapVectorRasterSubLayerConfig.h @@ -24,33 +24,22 @@ class Tiled2dMapVectorRasterSubLayerConfig : public Tiled2dMapVectorLayerConfig Tiled2dMapVectorRasterSubLayerConfig(const std::shared_ptr &layerDescription, const bool is3d, const std::optional &customZoomInfo = std::nullopt) - : Tiled2dMapVectorLayerConfig( - std::make_shared(layerDescription->source, layerDescription->url, layerDescription->sourceMinZoom, - layerDescription->sourceMaxZoom, layerDescription->bounds, - layerDescription->zoomLevelScaleFactor, - layerDescription->adaptScaleToScreen, - layerDescription->numDrawPreviousLayers, - layerDescription->underzoom, - layerDescription->overzoom, - layerDescription->levels), is3d), + : Tiled2dMapVectorLayerConfig(std::static_pointer_cast(layerDescription->source), is3d), description(layerDescription) { if (customZoomInfo.has_value()) { - zoomInfo = Tiled2dMapZoomInfo(customZoomInfo->zoomLevelScaleFactor * description->zoomLevelScaleFactor, - std::max(customZoomInfo->numDrawPreviousLayers, description->numDrawPreviousLayers), - 0, - customZoomInfo->adaptScaleToScreen || description->adaptScaleToScreen, - customZoomInfo->maskTile || description->maskTiles, - customZoomInfo->underzoom && description->underzoom, - customZoomInfo->overzoom && description->overzoom); - } else { - zoomInfo = Tiled2dMapZoomInfo(description->zoomLevelScaleFactor, description->numDrawPreviousLayers, 0, - description->adaptScaleToScreen, description->maskTiles, description->underzoom, - description->overzoom); + // zoomInfo is already initialized in super from the source-description + zoomInfo.zoomLevelScaleFactor *= customZoomInfo->zoomLevelScaleFactor; + zoomInfo.numDrawPreviousLayers = std::max(customZoomInfo->numDrawPreviousLayers, zoomInfo.numDrawPreviousLayers); + zoomInfo.numDrawPreviousOrLaterTLayers = 0; + zoomInfo.adaptScaleToScreen |= customZoomInfo->adaptScaleToScreen; + zoomInfo.maskTile |= customZoomInfo->maskTile; + zoomInfo.underzoom &= customZoomInfo->underzoom; + zoomInfo.overzoom &= customZoomInfo->overzoom; } - if (description->coordinateReferenceSystem == "EPSG:4326") { - customConfig = std::make_shared(layerDescription->source, - layerDescription->url, + if (description->source->coordinateReferenceSystem == "EPSG:4326") { + customConfig = std::make_shared(layerDescription->source->identifier, + layerDescription->source->vectorUrl, zoomInfo, layerDescription->sourceMinZoom, layerDescription->sourceMaxZoom); diff --git a/shared/src/map/layers/tiled/wmts/WmtsTiled2dMapLayerConfig.cpp b/shared/src/map/layers/tiled/wmts/WmtsTiled2dMapLayerConfig.cpp index 4f91171f6..58c750737 100644 --- a/shared/src/map/layers/tiled/wmts/WmtsTiled2dMapLayerConfig.cpp +++ b/shared/src/map/layers/tiled/wmts/WmtsTiled2dMapLayerConfig.cpp @@ -11,7 +11,6 @@ #include "WmtsTiled2dMapLayerConfig.h" #include "Tiled2dMapVectorSettings.h" -#include "Logger.h" #include WmtsTiled2dMapLayerConfig::WmtsTiled2dMapLayerConfig(const WmtsLayerDescription &description, From a1c4dc3d1a190580531784a362a081c4a3c2b42c Mon Sep 17 00:00:00 2001 From: Matthias Frei Date: Tue, 20 Jan 2026 08:22:31 +0100 Subject: [PATCH 2/3] Support "crs" for vector source in style/tile.json Reorganize implementation of layer configs. Combine redundant implementation of default (webmercator), WebMercator again and Epsg4326 cases. For raster sources, enabling EPSG:4326 was already supported before. Now also support EPSG:2056 and EPSG:21871. Support specifying in field "crs" in addition to previously supported "metadata"."crs". --- shared/public/RasterVectorLayerDescription.h | 25 ++- shared/public/Tiled2dMapVectorLayerConfig.h | 154 ++++-------------- shared/public/VectorMapSourceDescription.h | 31 +++- .../coordinates/CoordinateSystemFactory.cpp | 4 +- .../tiled/DefaultTiled2dMapLayerConfigs.cpp | 21 +-- .../tiled/Epsg2056Tiled2dMapLayerConfig.cpp | 83 ++++++++++ .../tiled/Epsg2056Tiled2dMapLayerConfig.h | 32 ++++ .../tiled/Epsg21781Tiled2dMapLayerConfig.cpp | 64 ++++++++ .../tiled/Epsg21781Tiled2dMapLayerConfig.h | 32 ++++ .../tiled/Epsg3857Tiled2dMapLayerConfig.cpp | 62 +++++++ .../tiled/Epsg3857Tiled2dMapLayerConfig.h | 35 ++++ .../tiled/Epsg4326Tiled2dMapLayerConfig.cpp | 95 +++-------- .../tiled/Epsg4326Tiled2dMapLayerConfig.h | 53 ++---- .../tiled/IrregularTiled2dMapLayerConfig.cpp | 72 ++++++++ .../tiled/IrregularTiled2dMapLayerConfig.h | 46 ++++++ .../tiled/RegularTiled2dMapLayerConfig.cpp | 48 ++++++ .../tiled/RegularTiled2dMapLayerConfig.h | 53 ++++++ .../tiled/Tiled2dMapVectorLayerConfig.cpp | 53 ++++++ .../WebMercatorTiled2dMapLayerConfig.cpp | 87 ---------- .../tiled/WebMercatorTiled2dMapLayerConfig.h | 53 ------ .../Tiled2dMapVectorGeoJSONLayerConfig.h | 47 ------ .../tiled/vector/Tiled2dMapVectorLayer.cpp | 31 +++- .../Tiled2dMapVectorLayerParserHelper.cpp | 68 ++++---- .../Tiled2dMapVectorRasterSubLayerConfig.h | 110 ------------- shared/test/TestTileSource.cpp | 26 ++- 25 files changed, 784 insertions(+), 601 deletions(-) create mode 100644 shared/src/map/layers/tiled/Epsg2056Tiled2dMapLayerConfig.cpp create mode 100644 shared/src/map/layers/tiled/Epsg2056Tiled2dMapLayerConfig.h create mode 100644 shared/src/map/layers/tiled/Epsg21781Tiled2dMapLayerConfig.cpp create mode 100644 shared/src/map/layers/tiled/Epsg21781Tiled2dMapLayerConfig.h create mode 100644 shared/src/map/layers/tiled/Epsg3857Tiled2dMapLayerConfig.cpp create mode 100644 shared/src/map/layers/tiled/Epsg3857Tiled2dMapLayerConfig.h create mode 100644 shared/src/map/layers/tiled/IrregularTiled2dMapLayerConfig.cpp create mode 100644 shared/src/map/layers/tiled/IrregularTiled2dMapLayerConfig.h create mode 100644 shared/src/map/layers/tiled/RegularTiled2dMapLayerConfig.cpp create mode 100644 shared/src/map/layers/tiled/RegularTiled2dMapLayerConfig.h create mode 100644 shared/src/map/layers/tiled/Tiled2dMapVectorLayerConfig.cpp delete mode 100644 shared/src/map/layers/tiled/WebMercatorTiled2dMapLayerConfig.cpp delete mode 100644 shared/src/map/layers/tiled/WebMercatorTiled2dMapLayerConfig.h delete mode 100644 shared/src/map/layers/tiled/vector/Tiled2dMapVectorGeoJSONLayerConfig.h delete mode 100644 shared/src/map/layers/tiled/vector/Tiled2dMapVectorRasterSubLayerConfig.h diff --git a/shared/public/RasterVectorLayerDescription.h b/shared/public/RasterVectorLayerDescription.h index a5db49098..f0bee23b8 100644 --- a/shared/public/RasterVectorLayerDescription.h +++ b/shared/public/RasterVectorLayerDescription.h @@ -44,7 +44,7 @@ class RasterVectorStyle { UsedKeysCollection getUsedKeys() const { UsedKeysCollection usedKeys; - std::shared_ptr values[] = { + std::shared_ptr values[] = { rasterOpacityEvaluator.getValue(), rasterBrightnessMinEvaluator.getValue(), rasterBrightnessMaxEvaluator.getValue(), @@ -68,7 +68,7 @@ class RasterVectorStyle { static const BlendMode defaultValue = BlendMode::NORMAL; return blendModeEvaluator.getResult(context, defaultValue).value; } - + RasterShaderStyle getRasterStyle(const EvaluationContext &context) { return { (float) getRasterOpacity(context), @@ -85,17 +85,17 @@ class RasterVectorStyle { double defaultValue = 1.0; return rasterOpacityEvaluator.getResult(context, defaultValue).value; } - + double getRasterBrightnessMin(const EvaluationContext &context) { double defaultValue = 0.0; return rasterBrightnessMinEvaluator.getResult(context, defaultValue).value; } - + double getRasterBrightnessMax(const EvaluationContext &context) { double defaultValue = 1.0; return rasterBrightnessMaxEvaluator.getResult(context, defaultValue).value; } - + double getRasterContrast(const EvaluationContext &context) { double defaultValue = 0.0; return rasterContrastEvaluator.getResult(context, defaultValue).value; @@ -129,7 +129,7 @@ class RasterVectorStyle { struct RasterVectorMapSourceDescription : public VectorMapSourceDescription { bool maskTiles; - + RasterVectorMapSourceDescription(std::string identifier, std::string url, int minZoom, @@ -141,9 +141,16 @@ struct RasterVectorMapSourceDescription : public VectorMapSourceDescription { std::optional underzoom, std::optional overzoom, std::optional> levels, - bool maskTiles) : - VectorMapSourceDescription(identifier, url, minZoom, maxZoom, bounds, zoomLevelScaleFactor, adaptScaleToScreen, numDrawPreviousLayers, underzoom, overzoom, levels), - maskTiles(maskTiles) {} + std::optional coordinateReferenceSystem, + bool maskTiles) + : VectorMapSourceDescription(identifier, url, minZoom, maxZoom, bounds, zoomLevelScaleFactor, adaptScaleToScreen, numDrawPreviousLayers, underzoom, overzoom, levels, coordinateReferenceSystem) + , maskTiles(maskTiles) {} + + virtual Tiled2dMapZoomInfo getZoomInfo(bool is3d) const { + auto zoomInfo = VectorMapSourceDescription::getZoomInfo(is3d); + zoomInfo.maskTile = maskTiles; + return zoomInfo; + } }; class RasterVectorLayerDescription: public VectorLayerDescription { diff --git a/shared/public/Tiled2dMapVectorLayerConfig.h b/shared/public/Tiled2dMapVectorLayerConfig.h index 019b5b43d..c407154bd 100644 --- a/shared/public/Tiled2dMapVectorLayerConfig.h +++ b/shared/public/Tiled2dMapVectorLayerConfig.h @@ -9,145 +9,63 @@ */ #pragma once +#include "RectCoord.h" #include "Tiled2dMapLayerConfig.h" -#include "Tiled2dMapZoomInfo.h" -#include "Tiled2dMapZoomLevelInfo.h" -#include "VectorMapSourceDescription.h" -#include "VectorLayerDescription.h" -#include "CoordinateSystemIdentifiers.h" #include "Tiled2dMapVectorSettings.h" -#include "Logger.h" +#include "Tiled2dMapZoomInfo.h" +#include +#include +#include +/** +* Abstract base class for different layer configurations. +*/ class Tiled2dMapVectorLayerConfig : public Tiled2dMapLayerConfig { public: - Tiled2dMapVectorLayerConfig(const std::shared_ptr &sourceDescription, - const Tiled2dMapZoomInfo &zoomInfo) - : sourceDescription(sourceDescription), zoomInfo(zoomInfo) {} - - Tiled2dMapVectorLayerConfig(const std::shared_ptr &sourceDescription, - const bool is3d) - : sourceDescription(sourceDescription), - zoomInfo(Tiled2dMapZoomInfo( - sourceDescription->zoomLevelScaleFactor ? *sourceDescription->zoomLevelScaleFactor : (is3d ? 0.75 : 1.0), - sourceDescription->numDrawPreviousLayers ? *sourceDescription->numDrawPreviousLayers : 0, - 0, - sourceDescription->adaptScaleToScreen ? *sourceDescription->adaptScaleToScreen : false, - true, - sourceDescription->underzoom ? *sourceDescription->underzoom : false, - sourceDescription->overzoom ? *sourceDescription->overzoom : true)) {} - - ~Tiled2dMapVectorLayerConfig() {} - - int32_t getCoordinateSystemIdentifier() override { - return epsg3857Id; - } - - std::string getTileUrl(int32_t x, int32_t y, int32_t t, int32_t zoom) override { - std::string url = sourceDescription->vectorUrl; - size_t epsg3857Index = url.find("{bbox-epsg-3857}", 0); - if (epsg3857Index != std::string::npos) { - const auto zoomLevelInfos = getDefaultEpsg3857ZoomLevels(zoom, zoom, std::nullopt); - const Tiled2dMapZoomLevelInfo &zoomLevelInfo = zoomLevelInfos.at(0); - RectCoord layerBounds = zoomLevelInfo.bounds; - const double tileWidth = zoomLevelInfo.tileWidthLayerSystemUnits; + virtual double getZoomIdentifier(double zoom) = 0; + virtual double getZoomFactorAtIdentifier(double zoomIdentifier) = 0; - const bool leftToRight = layerBounds.topLeft.x < layerBounds.bottomRight.x; - const bool topToBottom = layerBounds.topLeft.y < layerBounds.bottomRight.y; - const double tileWidthAdj = leftToRight ? tileWidth : -tileWidth; - const double tileHeightAdj = topToBottom ? tileWidth : -tileWidth; - - const double boundsLeft = layerBounds.topLeft.x; - const double boundsTop = layerBounds.topLeft.y; - - const Coord topLeft = Coord(epsg3857Id, x * tileWidthAdj + boundsLeft, y * tileHeightAdj + boundsTop, 0); - const Coord bottomRight = Coord(epsg3857Id, topLeft.x + tileWidthAdj, topLeft.y + tileHeightAdj, 0); - - std::string boxString = std::to_string(topLeft.x) + "," + std::to_string(bottomRight.y) + "," + std::to_string(bottomRight.x) + "," + std::to_string(topLeft.y); - url = url.replace(epsg3857Index, 16, boxString); - return url; +public: + Tiled2dMapVectorLayerConfig( + std::string layerName, + std::string urlFormat, + const std::optional &bounds, + const Tiled2dMapZoomInfo &zoomInfo, + const std::vector &levels + ); - } - size_t zoomIndex = url.find("{z}", 0); - if (zoomIndex == std::string::npos) throw std::invalid_argument("Layer url \'" + url + "\' has no valid format!"); - url = url.replace(zoomIndex, 3, std::to_string(zoom)); - size_t xIndex = url.find("{x}", 0); - if (xIndex == std::string::npos) throw std::invalid_argument("Layer url \'" + url + "\' has no valid format!"); - url = url.replace(xIndex, 3, std::to_string(x)); - size_t yIndex = url.find("{y}", 0); - if (yIndex == std::string::npos) throw std::invalid_argument("Layer url \'" + url + "\' has no valid format!"); - return url.replace(yIndex, 3, std::to_string(y)); - } + virtual ~Tiled2dMapVectorLayerConfig() = default; - std::vector getZoomLevelInfos() override { - return getDefaultEpsg3857ZoomLevels(sourceDescription->minZoom, sourceDescription->maxZoom, sourceDescription->levels); +public: + virtual std::string getLayerName() override { + return layerName; } - std::vector getVirtualZoomLevelInfos() override { - return getDefaultEpsg3857ZoomLevels(0, sourceDescription->minZoom - 1, sourceDescription->levels); - }; + virtual std::string getTileUrl(int32_t x, int32_t y, int32_t t, int32_t zoom) override; - Tiled2dMapZoomInfo getZoomInfo() override { + virtual Tiled2dMapZoomInfo getZoomInfo() override { return zoomInfo; } - std::string getLayerName() override { - return sourceDescription->identifier; + std::optional<::RectCoord> getBounds() override { + return bounds; } - std::optional getVectorSettings() override { + virtual std::optional getVectorSettings() override { return std::nullopt; } - virtual double getZoomIdentifier(double zoom) { - return std::max(0.0, std::round(log(baseValueZoom * zoomInfo.zoomLevelScaleFactor / zoom) / log(2) * 100) / 100); - } - - virtual double getZoomFactorAtIdentifier(double zoomIdentifier) { - double factor = pow(2, zoomIdentifier); - return baseValueZoom * zoomInfo.zoomLevelScaleFactor / factor; - } - - std::optional<::RectCoord> getBounds() override { - if (sourceDescription) { - return sourceDescription->bounds; - } - else { - return std::nullopt; - } - } - - static std::vector getDefaultEpsg3857ZoomLevels(int minZoom, int maxZoom, const std::optional> &levels) { - std::vector infos; - if (levels.has_value()) { - for (const auto &level : levels.value()) { - double factor = pow(2, level); - double zoom = baseValueZoom / factor; - double width = baseValueWidth / factor; - infos.push_back(Tiled2dMapZoomLevelInfo(zoom, width, factor, factor, 1, level, epsg3857Bounds)); - } - } else { - for (int i = minZoom; i <= maxZoom; i++) { - double factor = pow(2, i); - double zoom = baseValueZoom / factor; - double width = baseValueWidth / factor; - infos.push_back(Tiled2dMapZoomLevelInfo(zoom, width, factor, factor, 1, i, epsg3857Bounds)); - } - } - return infos; - } +public: + // Helper to initialize `zoomInfo` + static Tiled2dMapZoomInfo defaultMapZoomInfo(); + // Helper to initalize `levels` from min/max zoom levels value + static std::vector generateLevelsFromMinMax(int minZoomLevel, int maxZoomLevel); protected: - std::shared_ptr sourceDescription; + std::string layerName; + std::string urlFormat; Tiled2dMapZoomInfo zoomInfo; - - static constexpr double baseValueZoom = 500000000.0; - static constexpr double baseValueWidth = 40075016.0; - static const inline int32_t epsg3857Id = CoordinateSystemIdentifiers::EPSG3857(); - static const inline RectCoord epsg3857Bounds = RectCoord( - Coord(epsg3857Id, -20037508.34, 20037508.34, 0.0), - Coord(epsg3857Id, 20037508.34, -20037508.34, 0.0) - ); - - + std::optional bounds; + std::vector levels; // zoom level indices (kept sorted ascending) }; diff --git a/shared/public/VectorMapSourceDescription.h b/shared/public/VectorMapSourceDescription.h index 251d953e7..3aaf59e05 100644 --- a/shared/public/VectorMapSourceDescription.h +++ b/shared/public/VectorMapSourceDescription.h @@ -10,9 +10,11 @@ #pragma once -#include "VectorLayerDescription.h" #include "GeoJsonTypes.h" #include "RectCoord.h" +#include "Tiled2dMapVectorLayerConfig.h" +#include "Tiled2dMapZoomInfo.h" +#include "VectorLayerDescription.h" class VectorMapSourceDescription { public: @@ -27,7 +29,7 @@ class VectorMapSourceDescription { std::optional underzoom; std::optional overzoom; std::optional> levels; - std::optional coordinateReferenceSystem; + std::optional coordinateReferenceSystem; VectorMapSourceDescription(std::string identifier, std::string vectorUrl, @@ -39,10 +41,31 @@ class VectorMapSourceDescription { std::optional numDrawPreviousLayers, std::optional underzoom, std::optional overzoom, - std::optional> levels) : + std::optional> levels, + std::optional coordinateReferenceSystem) : identifier(identifier), vectorUrl(vectorUrl), minZoom(minZoom), maxZoom(maxZoom), bounds(bounds), adaptScaleToScreen(adaptScaleToScreen), numDrawPreviousLayers(numDrawPreviousLayers), - zoomLevelScaleFactor(zoomLevelScaleFactor), underzoom(underzoom), overzoom(overzoom), levels(levels) {} + zoomLevelScaleFactor(zoomLevelScaleFactor), underzoom(underzoom), overzoom(overzoom), levels(levels), + coordinateReferenceSystem(coordinateReferenceSystem) {} + + virtual Tiled2dMapZoomInfo getZoomInfo(bool is3d) const { + return Tiled2dMapZoomInfo( + zoomLevelScaleFactor ? *zoomLevelScaleFactor : (is3d ? 0.75 : 1.0), + numDrawPreviousLayers ? *numDrawPreviousLayers : 0, + 0, + adaptScaleToScreen ? *adaptScaleToScreen : false, + true, + underzoom ? *underzoom : false, + overzoom ? *overzoom : true); + }; + + std::vector getZoomLevels() { + if(levels) { + return *levels; + } else { + return Tiled2dMapVectorLayerConfig::generateLevelsFromMinMax(minZoom, maxZoom); + } + } }; struct SpriteSourceDescription { diff --git a/shared/src/map/coordinates/CoordinateSystemFactory.cpp b/shared/src/map/coordinates/CoordinateSystemFactory.cpp index 40f26e92a..51a965698 100644 --- a/shared/src/map/coordinates/CoordinateSystemFactory.cpp +++ b/shared/src/map/coordinates/CoordinateSystemFactory.cpp @@ -17,8 +17,8 @@ ::MapCoordinateSystem CoordinateSystemFactory::getEpsg2056System() { return MapCoordinateSystem(CoordinateSystemIdentifiers::EPSG2056(), - RectCoord(Coord(CoordinateSystemIdentifiers::EPSG2056(), 2485000.0, 1300000.0, 0), - Coord(CoordinateSystemIdentifiers::EPSG2056(), 2840000.0, 1070000.0, 0)), + RectCoord(Coord(CoordinateSystemIdentifiers::EPSG2056(), 2420000.0, 1350000.0, 0), + Coord(CoordinateSystemIdentifiers::EPSG2056(), 2900000.0, 1030000.0, 0)), 1.0); } diff --git a/shared/src/map/layers/tiled/DefaultTiled2dMapLayerConfigs.cpp b/shared/src/map/layers/tiled/DefaultTiled2dMapLayerConfigs.cpp index 3a2df1559..7bbda7f3b 100644 --- a/shared/src/map/layers/tiled/DefaultTiled2dMapLayerConfigs.cpp +++ b/shared/src/map/layers/tiled/DefaultTiled2dMapLayerConfigs.cpp @@ -5,24 +5,21 @@ // Created by Nicolas Märki on 13.02.2024. // -#ifndef DefaultTiled2dMapLayerConfigs_h -#define DefaultTiled2dMapLayerConfigs_h - #include "DefaultTiled2dMapLayerConfigs.h" -#include "WebMercatorTiled2dMapLayerConfig.h" +#include "Epsg3857Tiled2dMapLayerConfig.h" #include "Epsg4326Tiled2dMapLayerConfig.h" std::shared_ptr DefaultTiled2dMapLayerConfigs::webMercator(const std::string & layerName, const std::string & urlFormat) { - return std::make_shared(layerName, urlFormat); + return std::make_shared(layerName, urlFormat); } std::shared_ptr DefaultTiled2dMapLayerConfigs::webMercatorCustom(const std::string &layerName, const std::string &urlFormat, const std::optional & zoomInfo, int32_t minZoomLevel, int32_t maxZoomLevel) { - if (zoomInfo.has_value()) { - return std::make_shared(layerName, urlFormat, zoomInfo.value(), minZoomLevel, maxZoomLevel); - } - return std::make_shared(layerName, urlFormat, minZoomLevel, maxZoomLevel); + return std::make_shared( + layerName, urlFormat, std::nullopt, + zoomInfo.value_or(Tiled2dMapVectorLayerConfig::defaultMapZoomInfo()), + Tiled2dMapVectorLayerConfig::generateLevelsFromMinMax(minZoomLevel, maxZoomLevel)); } std::shared_ptr @@ -33,7 +30,7 @@ DefaultTiled2dMapLayerConfigs::epsg4326(const std::string &layerName, const std: std::shared_ptr DefaultTiled2dMapLayerConfigs::epsg4326Custom(const std::string &layerName, const std::string &urlFormat, const Tiled2dMapZoomInfo &zoomInfo, int32_t minZoomLevel, int32_t maxZoomLevel) { - return std::make_shared(layerName, urlFormat, zoomInfo, minZoomLevel, maxZoomLevel); + return std::make_shared( + layerName, urlFormat, std::nullopt, zoomInfo, + Tiled2dMapVectorLayerConfig::generateLevelsFromMinMax(minZoomLevel, maxZoomLevel)); } - -#endif /* DefaultTiled2dMapLayerConfigs_h */ diff --git a/shared/src/map/layers/tiled/Epsg2056Tiled2dMapLayerConfig.cpp b/shared/src/map/layers/tiled/Epsg2056Tiled2dMapLayerConfig.cpp new file mode 100644 index 000000000..d858c15ba --- /dev/null +++ b/shared/src/map/layers/tiled/Epsg2056Tiled2dMapLayerConfig.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2026 Ubique Innovation AG + * + * 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 https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +#include "RectCoord.h" +#include "CoordinateSystemIdentifiers.h" +#include "Epsg2056Tiled2dMapLayerConfig.h" + +Epsg2056Tiled2dMapLayerConfig::Epsg2056Tiled2dMapLayerConfig( + std::string layerName, + std::string urlFormat) + : IrregularTiled2dMapLayerConfig( + layerName, urlFormat, std::nullopt, + defaultMapZoomInfo(), + generateLevelsFromMinMax(0, 28), + swisstopoZoomLevelInfos()) +{} + +Epsg2056Tiled2dMapLayerConfig::Epsg2056Tiled2dMapLayerConfig( + std::string layerName, + std::string urlFormat, + const std::optional &bounds, + const Tiled2dMapZoomInfo &zoomInfo, + const std::vector &levels) + : IrregularTiled2dMapLayerConfig( + layerName, urlFormat, bounds, zoomInfo, levels, swisstopoZoomLevelInfos()) +{} + +static Tiled2dMapZoomLevelInfo makeZoomLevelInfo( + const Coord &topLeft, double zoom, float tileWidthLayerSystemUnits, + int32_t numTilesX, int32_t numTilesY, int32_t numTilesT, int32_t zoomLevelIdentifier) +{ + const auto bounds = RectCoord( + topLeft, + Coord(topLeft.systemIdentifier, + topLeft.x + tileWidthLayerSystemUnits * numTilesX, + topLeft.y - tileWidthLayerSystemUnits * numTilesY, 0)); + + return Tiled2dMapZoomLevelInfo(zoom, tileWidthLayerSystemUnits, numTilesX, + numTilesY, numTilesT, zoomLevelIdentifier, + bounds); +} + +std::vector Epsg2056Tiled2dMapLayerConfig::swisstopoZoomLevelInfos() { + const Coord topLeft{CoordinateSystemIdentifiers::EPSG2056(), 2420000.0, 1350000.0, 0.f}; + return { + makeZoomLevelInfo(topLeft, 14285714.2857, 1024000, 1, 1, 1, 0), + makeZoomLevelInfo(topLeft, 13392857.1429, 960000, 1, 1, 1, 1), + makeZoomLevelInfo(topLeft, 12500000.0, 896000, 1, 1, 1, 2), + makeZoomLevelInfo(topLeft, 11607142.8571, 832000, 1, 1, 1, 3), + makeZoomLevelInfo(topLeft, 10714285.7143, 768000, 1, 1, 1, 4), + makeZoomLevelInfo(topLeft, 9821428.57143, 704000, 1, 1, 1, 5), + makeZoomLevelInfo(topLeft, 8928571.42857, 640000, 1, 1, 1, 6), + makeZoomLevelInfo(topLeft, 8035714.28571, 576000, 1, 1, 1, 7), + makeZoomLevelInfo(topLeft, 7142857.14286, 512000, 1, 1, 1, 8), + makeZoomLevelInfo(topLeft, 6250000.0, 448000, 2, 1, 1, 9), + makeZoomLevelInfo(topLeft, 5357142.85714, 384000, 2, 1, 1, 10), + makeZoomLevelInfo(topLeft, 4464285.71429, 320000, 2, 1, 1, 11), + makeZoomLevelInfo(topLeft, 3571428.57143, 256000, 2, 2, 1, 12), + makeZoomLevelInfo(topLeft, 2678571.42857, 192000, 3, 2, 1, 13), + makeZoomLevelInfo(topLeft, 2321428.57143, 166400, 3, 2, 1, 14), + makeZoomLevelInfo(topLeft, 1785714.28571, 128000, 4, 3, 1, 15), + makeZoomLevelInfo(topLeft, 892857.142857, 64000, 8, 5, 1, 16), + makeZoomLevelInfo(topLeft, 357142.857143, 25600, 19, 13, 1, 17), + makeZoomLevelInfo(topLeft, 178571.428571, 12800, 38, 25, 1, 18), + makeZoomLevelInfo(topLeft, 71428.5714286, 5120, 94, 63, 1, 19), + makeZoomLevelInfo(topLeft, 35714.2857143, 2560, 188, 125, 1, 20), + makeZoomLevelInfo(topLeft, 17857.1428571, 1280, 375, 250, 1, 21), + makeZoomLevelInfo(topLeft, 8928.57142857, 640, 750, 500, 1, 22), + makeZoomLevelInfo(topLeft, 7142.85714286, 512, 938, 625, 1, 23), + makeZoomLevelInfo(topLeft, 5357.14285714, 384, 1250, 834, 1, 24), + makeZoomLevelInfo(topLeft, 3571.42857143, 256, 1875, 1250, 1, 25), + makeZoomLevelInfo(topLeft, 1785.71428571, 128, 3750, 2500, 1, 26), + makeZoomLevelInfo(topLeft, 892.857142857, 64, 7500, 5000, 1, 27), + makeZoomLevelInfo(topLeft, 357.142857143, 25.6, 18750, 12500, 1, 28) + }; +} diff --git a/shared/src/map/layers/tiled/Epsg2056Tiled2dMapLayerConfig.h b/shared/src/map/layers/tiled/Epsg2056Tiled2dMapLayerConfig.h new file mode 100644 index 000000000..15b8a7f47 --- /dev/null +++ b/shared/src/map/layers/tiled/Epsg2056Tiled2dMapLayerConfig.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026 Ubique Innovation AG + * + * 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 https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +#pragma once + +#include "IrregularTiled2dMapLayerConfig.h" +#include "Tiled2dMapZoomInfo.h" + +class Epsg2056Tiled2dMapLayerConfig : public IrregularTiled2dMapLayerConfig { +public: + Epsg2056Tiled2dMapLayerConfig( + std::string layerName, + std::string urlFormat + ); + + Epsg2056Tiled2dMapLayerConfig( + std::string layerName, + std::string urlFormat, + const std::optional &bounds, + const Tiled2dMapZoomInfo &zoomInfo, + const std::vector &levels + ); + + static std::vector swisstopoZoomLevelInfos(); +}; diff --git a/shared/src/map/layers/tiled/Epsg21781Tiled2dMapLayerConfig.cpp b/shared/src/map/layers/tiled/Epsg21781Tiled2dMapLayerConfig.cpp new file mode 100644 index 000000000..1f28264bd --- /dev/null +++ b/shared/src/map/layers/tiled/Epsg21781Tiled2dMapLayerConfig.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2026 Ubique Innovation AG + * + * 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 https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +#include "Epsg21781Tiled2dMapLayerConfig.h" +#include "CoordinateSystemIdentifiers.h" +#include "Epsg2056Tiled2dMapLayerConfig.h" +#include "RectCoord.h" + +Epsg21781Tiled2dMapLayerConfig::Epsg21781Tiled2dMapLayerConfig( + std::string layerName, + std::string urlFormat) + : IrregularTiled2dMapLayerConfig( + layerName, urlFormat, std::nullopt, + defaultMapZoomInfo(), + generateLevelsFromMinMax(0, 28), + swisstopoZoomLevelInfos()) +{} + +Epsg21781Tiled2dMapLayerConfig::Epsg21781Tiled2dMapLayerConfig( + std::string layerName, + std::string urlFormat, + const std::optional &bounds, + const Tiled2dMapZoomInfo &zoomInfo, + const std::vector &levels) + : IrregularTiled2dMapLayerConfig( + layerName, urlFormat, bounds, zoomInfo, levels, swisstopoZoomLevelInfos()) +{} + +static Tiled2dMapZoomLevelInfo makeZoomLevelInfo( + const Coord &topLeft, double zoom, float tileWidthLayerSystemUnits, + int32_t numTilesX, int32_t numTilesY, int32_t numTilesT, int32_t zoomLevelIdentifier) +{ + const auto bounds = RectCoord( + topLeft, + Coord(topLeft.systemIdentifier, + topLeft.x + tileWidthLayerSystemUnits * numTilesX, + topLeft.y - tileWidthLayerSystemUnits * numTilesY, 0)); + + return Tiled2dMapZoomLevelInfo(zoom, tileWidthLayerSystemUnits, numTilesX, + numTilesY, numTilesT, zoomLevelIdentifier, + bounds); +} + +std::vector Epsg21781Tiled2dMapLayerConfig::swisstopoZoomLevelInfos() { + // same zoom level pyramid as for 2056, just different coordinate origin. + const Coord topLeft{CoordinateSystemIdentifiers::EPSG21781(), 420000.0, 350000.0, 0.}; + + std::vector result; + const auto epsg2056ZoomLevelInfos = Epsg2056Tiled2dMapLayerConfig::swisstopoZoomLevelInfos(); + for(const auto &other : epsg2056ZoomLevelInfos) { + result.push_back(makeZoomLevelInfo( + topLeft, other.zoom, other.tileWidthLayerSystemUnits, + other.numTilesX, other.numTilesY, other.numTilesT, + other.zoomLevelIdentifier)); + } + return result; +} diff --git a/shared/src/map/layers/tiled/Epsg21781Tiled2dMapLayerConfig.h b/shared/src/map/layers/tiled/Epsg21781Tiled2dMapLayerConfig.h new file mode 100644 index 000000000..3c80627e0 --- /dev/null +++ b/shared/src/map/layers/tiled/Epsg21781Tiled2dMapLayerConfig.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2026 Ubique Innovation AG + * + * 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 https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +#pragma once + +#include "IrregularTiled2dMapLayerConfig.h" +#include "Tiled2dMapZoomInfo.h" + +class Epsg21781Tiled2dMapLayerConfig : public IrregularTiled2dMapLayerConfig { +public: + Epsg21781Tiled2dMapLayerConfig( + std::string layerName, + std::string urlFormat + ); + + Epsg21781Tiled2dMapLayerConfig( + std::string layerName, + std::string urlFormat, + const std::optional &bounds, + const Tiled2dMapZoomInfo &zoomInfo, + const std::vector &levels + ); + + static std::vector swisstopoZoomLevelInfos(); +}; diff --git a/shared/src/map/layers/tiled/Epsg3857Tiled2dMapLayerConfig.cpp b/shared/src/map/layers/tiled/Epsg3857Tiled2dMapLayerConfig.cpp new file mode 100644 index 000000000..4656a4470 --- /dev/null +++ b/shared/src/map/layers/tiled/Epsg3857Tiled2dMapLayerConfig.cpp @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2026 Ubique Innovation AG + * + * 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 https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +#include "Epsg3857Tiled2dMapLayerConfig.h" +#include "CoordinateSystemFactory.h" + +const double Epsg3857Tiled2dMapLayerConfig::BASE_ZOOM = 559082264.029; + +Epsg3857Tiled2dMapLayerConfig::Epsg3857Tiled2dMapLayerConfig( + std::string layerName, + std::string urlFormat) + : RegularTiled2dMapLayerConfig( + layerName, urlFormat, std::nullopt, + defaultMapZoomInfo(), + generateLevelsFromMinMax(0, 20), + CoordinateSystemFactory::getEpsg3857System(), + BASE_ZOOM) +{} + +Epsg3857Tiled2dMapLayerConfig::Epsg3857Tiled2dMapLayerConfig( + std::string layerName, + std::string urlFormat, + const std::optional &bounds, + const Tiled2dMapZoomInfo &zoomInfo, + const std::vector &levels) + : RegularTiled2dMapLayerConfig( + layerName, urlFormat, bounds, zoomInfo, levels, + CoordinateSystemFactory::getEpsg3857System(), BASE_ZOOM) +{} + +std::string Epsg3857Tiled2dMapLayerConfig::getTileUrl(int32_t x, int32_t y, int32_t t, int32_t zoom) { + std::string url = urlFormat; + size_t epsg3857Index = url.find("{bbox-epsg-3857}", 0); + if (epsg3857Index != std::string::npos) { + const auto &zoomLevelInfo = getRegularZoomLevelInfo(zoom); + RectCoord levelBounds = zoomLevelInfo.bounds; + const double tileWidth = zoomLevelInfo.tileWidthLayerSystemUnits; + + const bool leftToRight = levelBounds.topLeft.x < levelBounds.bottomRight.x; + const bool topToBottom = levelBounds.topLeft.y < levelBounds.bottomRight.y; + const double tileWidthAdj = leftToRight ? tileWidth : -tileWidth; + const double tileHeightAdj = topToBottom ? tileWidth : -tileWidth; + + const double boundsLeft = levelBounds.topLeft.x; + const double boundsTop = levelBounds.topLeft.y; + + const Coord topLeft = Coord(coordinateSystem.identifier, x * tileWidthAdj + boundsLeft, y * tileHeightAdj + boundsTop, 0); + const Coord bottomRight = Coord(coordinateSystem.identifier, topLeft.x + tileWidthAdj, topLeft.y + tileHeightAdj, 0); + + std::string boxString = std::to_string(topLeft.x) + "," + std::to_string(bottomRight.y) + "," + std::to_string(bottomRight.x) + "," + std::to_string(topLeft.y); + url = url.replace(epsg3857Index, 16, boxString); + return url; + } + return RegularTiled2dMapLayerConfig::getTileUrl(x, y, t, zoom); +} diff --git a/shared/src/map/layers/tiled/Epsg3857Tiled2dMapLayerConfig.h b/shared/src/map/layers/tiled/Epsg3857Tiled2dMapLayerConfig.h new file mode 100644 index 000000000..68b7dd7ff --- /dev/null +++ b/shared/src/map/layers/tiled/Epsg3857Tiled2dMapLayerConfig.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026 Ubique Innovation AG + * + * 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 https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +#pragma once + +#include "RegularTiled2dMapLayerConfig.h" +#include "Tiled2dMapZoomInfo.h" + +class Epsg3857Tiled2dMapLayerConfig : public RegularTiled2dMapLayerConfig { +public: + Epsg3857Tiled2dMapLayerConfig( + std::string layerName, + std::string urlFormat + ); + + Epsg3857Tiled2dMapLayerConfig( + std::string layerName, + std::string urlFormat, + const std::optional &bounds, + const Tiled2dMapZoomInfo &zoomInfo, + const std::vector &levels + ); + + virtual std::string getTileUrl(int32_t x, int32_t y, int32_t t, int32_t zoom) override; + +private: + const static double BASE_ZOOM; +}; diff --git a/shared/src/map/layers/tiled/Epsg4326Tiled2dMapLayerConfig.cpp b/shared/src/map/layers/tiled/Epsg4326Tiled2dMapLayerConfig.cpp index 20781a0e6..125a3604e 100644 --- a/shared/src/map/layers/tiled/Epsg4326Tiled2dMapLayerConfig.cpp +++ b/shared/src/map/layers/tiled/Epsg4326Tiled2dMapLayerConfig.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Ubique Innovation AG + * Copyright (c) 2026 Ubique Innovation AG * * 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 @@ -9,77 +9,28 @@ */ #include "Epsg4326Tiled2dMapLayerConfig.h" -#include "Tiled2dMapVectorSettings.h" -#include "CoordinateSystemIdentifiers.h" -#include -#include +#include "CoordinateSystemFactory.h" -const RectCoord Epsg4326Tiled2dMapLayerConfig::EPSG_4326_BOUNDS = RectCoord(Coord(CoordinateSystemIdentifiers::EPSG4326(), -180.0, 90.0, 0.0), - Coord(CoordinateSystemIdentifiers::EPSG4326(), 180.0, -90.0, 0.0)); const double Epsg4326Tiled2dMapLayerConfig::BASE_ZOOM = 500000000.0; -const double Epsg4326Tiled2dMapLayerConfig::BASE_WIDTH = 360.0; -Epsg4326Tiled2dMapLayerConfig::Epsg4326Tiled2dMapLayerConfig(std::string layerName, std::string urlFormat) - : layerName(layerName), urlFormat(urlFormat) - {} - -Epsg4326Tiled2dMapLayerConfig::Epsg4326Tiled2dMapLayerConfig(std::string layerName, std::string urlFormat, - const Tiled2dMapZoomInfo &zoomInfo, int32_t minZoomLevel, - int32_t maxZoomLevel) - : layerName(layerName), urlFormat(urlFormat), zoomInfo(zoomInfo), minZoomLevel(minZoomLevel), maxZoomLevel(maxZoomLevel) {} - -int32_t Epsg4326Tiled2dMapLayerConfig::getCoordinateSystemIdentifier() { return CoordinateSystemIdentifiers::EPSG4326(); } - -std::string Epsg4326Tiled2dMapLayerConfig::getTileUrl(int32_t x, int32_t y, int32_t t, int32_t zoom) { - std::string url = urlFormat; - size_t zoomIndex = url.find("{z}", 0); - if (zoomIndex == std::string::npos) - throw std::invalid_argument("Layer url \'" + url + "\' has no valid format!"); - url = url.replace(zoomIndex, 3, std::to_string(zoom)); - size_t xIndex = url.find("{x}", 0); - if (xIndex == std::string::npos) - throw std::invalid_argument("Layer url \'" + url + "\' has no valid format!"); - url = url.replace(xIndex, 3, std::to_string(x)); - size_t yIndex = url.find("{y}", 0); - if (yIndex == std::string::npos) - throw std::invalid_argument("Layer url \'" + url + "\' has no valid format!"); - return url.replace(yIndex, 3, std::to_string(y)); -} - -std::string Epsg4326Tiled2dMapLayerConfig::getLayerName() { return layerName; } - -std::vector Epsg4326Tiled2dMapLayerConfig::getZoomLevelInfos() { - std::vector levels; - levels.reserve(maxZoomLevel - minZoomLevel + 1); - for (int32_t i = minZoomLevel; i <= maxZoomLevel; ++i) { - levels.emplace_back(getZoomLevelInfo(i)); - } - return levels; -} - -std::vector Epsg4326Tiled2dMapLayerConfig::getVirtualZoomLevelInfos() { - std::vector levels; - levels.reserve(minZoomLevel); - for (int32_t i = 0; i < minZoomLevel; ++i) { - levels.emplace_back(getZoomLevelInfo(i)); - } - return levels; -} - -Tiled2dMapZoomInfo Epsg4326Tiled2dMapLayerConfig::getZoomInfo() { - return zoomInfo; -} - -std::optional Epsg4326Tiled2dMapLayerConfig::getVectorSettings() { - return std::nullopt; -} - -std::optional<::RectCoord> Epsg4326Tiled2dMapLayerConfig::getBounds() { - return EPSG_4326_BOUNDS; -} - -Tiled2dMapZoomLevelInfo Epsg4326Tiled2dMapLayerConfig::getZoomLevelInfo(int32_t zoomLevel) { - double tileCount = std::pow(2.0,zoomLevel); - double zoom = BASE_ZOOM / tileCount; - return {zoom, (float) (BASE_WIDTH / tileCount), (int32_t) tileCount, (int32_t) tileCount, 1, zoomLevel, EPSG_4326_BOUNDS}; -} +Epsg4326Tiled2dMapLayerConfig::Epsg4326Tiled2dMapLayerConfig( + std::string layerName, + std::string urlFormat) + : RegularTiled2dMapLayerConfig( + layerName, urlFormat, std::nullopt, + defaultMapZoomInfo(), + generateLevelsFromMinMax(0, 20), + CoordinateSystemFactory::getEpsg4326System(), + BASE_ZOOM) +{} + +Epsg4326Tiled2dMapLayerConfig::Epsg4326Tiled2dMapLayerConfig( + std::string layerName, + std::string urlFormat, + const std::optional &bounds, + const Tiled2dMapZoomInfo &zoomInfo, + const std::vector &levels) + : RegularTiled2dMapLayerConfig( + layerName, urlFormat, bounds, zoomInfo, levels, + CoordinateSystemFactory::getEpsg4326System(), BASE_ZOOM) +{} diff --git a/shared/src/map/layers/tiled/Epsg4326Tiled2dMapLayerConfig.h b/shared/src/map/layers/tiled/Epsg4326Tiled2dMapLayerConfig.h index c9b2ed3d0..99e2e25b7 100644 --- a/shared/src/map/layers/tiled/Epsg4326Tiled2dMapLayerConfig.h +++ b/shared/src/map/layers/tiled/Epsg4326Tiled2dMapLayerConfig.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Ubique Innovation AG + * Copyright (c) 2026 Ubique Innovation AG * * 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 @@ -10,44 +10,23 @@ #pragma once -#include "Tiled2dMapLayerConfig.h" +#include "RegularTiled2dMapLayerConfig.h" #include "Tiled2dMapZoomInfo.h" -#include "Tiled2dMapZoomLevelInfo.h" -#include "WmtsLayerDescription.h" - -class Epsg4326Tiled2dMapLayerConfig : public Tiled2dMapLayerConfig { - public: - Epsg4326Tiled2dMapLayerConfig(std::string layerName, std::string urlFormat); - - Epsg4326Tiled2dMapLayerConfig(std::string layerName, std::string urlFormat, const Tiled2dMapZoomInfo &zoomInfo, int32_t minZoomLevel, int32_t maxZoomLevel); - - virtual int32_t getCoordinateSystemIdentifier() override; - - virtual std::string getTileUrl(int32_t x, int32_t y, int32_t t, int32_t zoom) override; - - virtual std::string getLayerName() override; - - virtual std::vector getZoomLevelInfos() override; - - virtual std::vector getVirtualZoomLevelInfos() override; - - virtual Tiled2dMapZoomInfo getZoomInfo() override; - - virtual std::optional getVectorSettings() override; - - std::optional<::RectCoord> getBounds() override; +class Epsg4326Tiled2dMapLayerConfig : public RegularTiled2dMapLayerConfig { +public: + Epsg4326Tiled2dMapLayerConfig( + std::string layerName, + std::string urlFormat + ); + + Epsg4326Tiled2dMapLayerConfig( + std::string layerName, + std::string urlFormat, + const std::optional &bounds, + const Tiled2dMapZoomInfo &zoomInfo, + const std::vector &levels + ); private: - Tiled2dMapZoomLevelInfo getZoomLevelInfo(int32_t zoomLevel); - - const static RectCoord EPSG_4326_BOUNDS; const static double BASE_ZOOM; - const static double BASE_WIDTH; - - std::string layerName; - std::string urlFormat; - - int32_t minZoomLevel = 0; - int32_t maxZoomLevel = 20; - Tiled2dMapZoomInfo zoomInfo = Tiled2dMapZoomInfo(1.0, 0, 0, true, false, true, true); }; diff --git a/shared/src/map/layers/tiled/IrregularTiled2dMapLayerConfig.cpp b/shared/src/map/layers/tiled/IrregularTiled2dMapLayerConfig.cpp new file mode 100644 index 000000000..bab210de1 --- /dev/null +++ b/shared/src/map/layers/tiled/IrregularTiled2dMapLayerConfig.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026 Ubique Innovation AG + * + * 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 https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +#include "IrregularTiled2dMapLayerConfig.h" + +std::vector IrregularTiled2dMapLayerConfig::getZoomLevelInfos() { + std::vector result; + result.reserve(levels.size()); + for(int level : levels) { + result.push_back(zoomLevelInfos[level]); + } + return result; +} + +std::vector IrregularTiled2dMapLayerConfig::getVirtualZoomLevelInfos() { + const int minZoomLevel = levels.front(); + std::vector result(zoomLevelInfos.begin(), zoomLevelInfos.begin() + minZoomLevel); + return result; + }; + +double IrregularTiled2dMapLayerConfig::getZoomIdentifier(double zoom) { + const int minZoomLevel = levels.front(); + const int maxZoomLevel = levels.back(); + + Tiled2dMapZoomLevelInfo prevZoom = zoomLevelInfos.back(); + + for (auto it = zoomLevelInfos.rbegin() + maxZoomLevel; it != zoomLevelInfos.rend() + minZoomLevel; ++it) { + if ((*it).zoom <= zoom) { + prevZoom = *it; + } else { + // Linear Interpolation + auto y0 = prevZoom.zoomLevelIdentifier; + auto x0 = prevZoom.zoom; + auto y1 = (*it).zoomLevelIdentifier; + auto x1 = (*it).zoom; + + return y0 + (zoom - x0) * (y1 - y0) / (x1 - x0); + } + } + + return prevZoom.zoomLevelIdentifier; +} + +double IrregularTiled2dMapLayerConfig::getZoomFactorAtIdentifier(double zoomIdentifier) { + const int minZoomLevel = levels.front(); + const int maxZoomLevel = levels.back(); + + Tiled2dMapZoomLevelInfo prevZoom = zoomLevelInfos.back(); + + for (auto it = zoomLevelInfos.begin() + minZoomLevel; it != zoomLevelInfos.end() + maxZoomLevel; ++it) { + if ((*it).zoomLevelIdentifier <= zoomIdentifier) { + prevZoom = *it; + } else { + // Linear Interpolation + auto y0 = prevZoom.zoom; + auto x0 = prevZoom.zoomLevelIdentifier; + auto y1 = (*it).zoom; + auto x1 = (*it).zoomLevelIdentifier; + + return y0 + (zoomIdentifier - x0) * (y1 - y0) / (x1 - x0); + } + } + + return prevZoom.zoom; +} diff --git a/shared/src/map/layers/tiled/IrregularTiled2dMapLayerConfig.h b/shared/src/map/layers/tiled/IrregularTiled2dMapLayerConfig.h new file mode 100644 index 000000000..2b3cc706f --- /dev/null +++ b/shared/src/map/layers/tiled/IrregularTiled2dMapLayerConfig.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026 Ubique Innovation AG + * + * 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 https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ +#pragma once + +#include "Tiled2dMapVectorLayerConfig.h" +#include "Tiled2dMapZoomLevelInfo.h" + +/** Base class for Tiled2dMapVectorLayerConfig implementations for _irregular_ + * tile pyramids, i.e. for tile systems that do not follow a simple power-of-2 + * relation between zoom levels, but are defined with a table of fixed zoom + * levels. + */ +class IrregularTiled2dMapLayerConfig : public Tiled2dMapVectorLayerConfig { +public: + IrregularTiled2dMapLayerConfig( + std::string layerName, + std::string urlFormat, + const std::optional &bounds, + const Tiled2dMapZoomInfo &zoomInfo, + const std::vector &levels, + const std::vector &zoomLevelInfos + ) + : Tiled2dMapVectorLayerConfig(layerName, urlFormat, bounds, zoomInfo, levels) + , zoomLevelInfos(zoomLevelInfos) + {} + + virtual int32_t getCoordinateSystemIdentifier() override { + return zoomLevelInfos.front().bounds.topLeft.systemIdentifier; + } + + virtual std::vector getZoomLevelInfos() override; + virtual std::vector getVirtualZoomLevelInfos() override; + + virtual double getZoomIdentifier(double zoom) override; + virtual double getZoomFactorAtIdentifier(double zoomIdentifier) override; + +protected: + std::vector zoomLevelInfos; +}; diff --git a/shared/src/map/layers/tiled/RegularTiled2dMapLayerConfig.cpp b/shared/src/map/layers/tiled/RegularTiled2dMapLayerConfig.cpp new file mode 100644 index 000000000..a3d17563d --- /dev/null +++ b/shared/src/map/layers/tiled/RegularTiled2dMapLayerConfig.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026 Ubique Innovation AG + * + * 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 https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +#include "RegularTiled2dMapLayerConfig.h" +#include + +std::vector RegularTiled2dMapLayerConfig::getZoomLevelInfos() { + std::vector result; + result.reserve(levels.size()); + for(int level : levels) { + result.push_back(getRegularZoomLevelInfo(level)); + } + return result; +} + +std::vector RegularTiled2dMapLayerConfig::getVirtualZoomLevelInfos() { + int minZoomLevel = levels.front(); + std::vector result; + result.reserve(minZoomLevel); + for(int level = 0; level < minZoomLevel; level++) { + result.push_back(getRegularZoomLevelInfo(level)); + } + return result; +}; + +double RegularTiled2dMapLayerConfig::getZoomIdentifier(double zoom) { + return std::max(0.0, std::round(log(baseZoom * zoomInfo.zoomLevelScaleFactor / zoom) / log(2) * 100) / 100); +} + +double RegularTiled2dMapLayerConfig::getZoomFactorAtIdentifier(double zoomIdentifier) { + double factor = pow(2, zoomIdentifier); + return baseZoom * zoomInfo.zoomLevelScaleFactor / factor; +} + +Tiled2dMapZoomLevelInfo RegularTiled2dMapLayerConfig::getRegularZoomLevelInfo(int zoomLevel) { + const double baseValueWidth = (coordinateSystem.bounds.bottomRight.x - coordinateSystem.bounds.topLeft.x); + double factor = pow(2, zoomLevel); + double zoom = baseZoom / factor; + double width = baseValueWidth / factor; + return Tiled2dMapZoomLevelInfo(zoom, width, factor, factor, 1, zoomLevel, coordinateSystem.bounds); +} diff --git a/shared/src/map/layers/tiled/RegularTiled2dMapLayerConfig.h b/shared/src/map/layers/tiled/RegularTiled2dMapLayerConfig.h new file mode 100644 index 000000000..857f1ba68 --- /dev/null +++ b/shared/src/map/layers/tiled/RegularTiled2dMapLayerConfig.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2026 Ubique Innovation AG + * + * 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 https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ +#pragma once + +#include "Tiled2dMapVectorLayerConfig.h" +#include "Tiled2dMapZoomLevelInfo.h" +#include "MapCoordinateSystem.h" + +/** Base class for Tiled2dMapVectorLayerConfig implementations for _regular_ + * tile pyramids, i.e. for tile systems with a simple power-of-2 relation + * between zoom levels. + */ +class RegularTiled2dMapLayerConfig : public Tiled2dMapVectorLayerConfig { +public: + RegularTiled2dMapLayerConfig( + std::string layerName, + std::string urlFormat, + const std::optional &bounds, + const Tiled2dMapZoomInfo &zoomInfo, + const std::vector &levels, + const MapCoordinateSystem &coordinateSystem, + double baseZoom) + : Tiled2dMapVectorLayerConfig(layerName, urlFormat, bounds, zoomInfo, levels) + , coordinateSystem(coordinateSystem) + , baseZoom(baseZoom) + {} + + virtual int32_t getCoordinateSystemIdentifier() override { + return coordinateSystem.identifier; + } + + virtual std::vector getZoomLevelInfos() override; + + virtual std::vector getVirtualZoomLevelInfos() override; + + virtual double getZoomIdentifier(double zoom) override; + + virtual double getZoomFactorAtIdentifier(double zoomIdentifier) override; + +protected: + Tiled2dMapZoomLevelInfo getRegularZoomLevelInfo(int zoomLevel); + +protected: + double baseZoom; + MapCoordinateSystem coordinateSystem; +}; diff --git a/shared/src/map/layers/tiled/Tiled2dMapVectorLayerConfig.cpp b/shared/src/map/layers/tiled/Tiled2dMapVectorLayerConfig.cpp new file mode 100644 index 000000000..8b42f0ac1 --- /dev/null +++ b/shared/src/map/layers/tiled/Tiled2dMapVectorLayerConfig.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2026 Ubique Innovation AG + * + * 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 https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +#include "Tiled2dMapVectorLayerConfig.h" +#include +#include +#include + +Tiled2dMapVectorLayerConfig::Tiled2dMapVectorLayerConfig( + std::string layerName, + std::string urlFormat, + const std::optional &bounds, + const Tiled2dMapZoomInfo &zoomInfo, + const std::vector &levels_ +) + : layerName(layerName) + , urlFormat(urlFormat) + , bounds(bounds) + , zoomInfo(zoomInfo) + , levels(levels_) +{ + std::sort(levels.begin(), levels.end()); +} + +std::string Tiled2dMapVectorLayerConfig::getTileUrl(int32_t x, int32_t y, int32_t t, int32_t zoom) { + std::string url = urlFormat; + size_t zoomIndex = url.find("{z}", 0); + if (zoomIndex == std::string::npos) throw std::invalid_argument("Layer url \'" + url + "\' has no valid format!"); + url = url.replace(zoomIndex, 3, std::to_string(zoom)); + size_t xIndex = url.find("{x}", 0); + if (xIndex == std::string::npos) throw std::invalid_argument("Layer url \'" + url + "\' has no valid format!"); + url = url.replace(xIndex, 3, std::to_string(x)); + size_t yIndex = url.find("{y}", 0); + if (yIndex == std::string::npos) throw std::invalid_argument("Layer url \'" + url + "\' has no valid format!"); + return url.replace(yIndex, 3, std::to_string(y)); +} + +Tiled2dMapZoomInfo Tiled2dMapVectorLayerConfig::defaultMapZoomInfo() { + return Tiled2dMapZoomInfo(1.0, 0, 0, true, false, true, true); +} + +std::vector Tiled2dMapVectorLayerConfig::generateLevelsFromMinMax(int minZoomLevel, int maxZoomLevel) { + std::vector levels(maxZoomLevel - minZoomLevel + 1); + std::iota(levels.begin(), levels.end(), minZoomLevel); + return levels; +} diff --git a/shared/src/map/layers/tiled/WebMercatorTiled2dMapLayerConfig.cpp b/shared/src/map/layers/tiled/WebMercatorTiled2dMapLayerConfig.cpp deleted file mode 100644 index a9bb90b34..000000000 --- a/shared/src/map/layers/tiled/WebMercatorTiled2dMapLayerConfig.cpp +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2021 Ubique Innovation AG - * - * 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 https://mozilla.org/MPL/2.0/. - * - * SPDX-License-Identifier: MPL-2.0 - */ - -#include "WebMercatorTiled2dMapLayerConfig.h" -#include "Tiled2dMapVectorSettings.h" -#include "CoordinateSystemIdentifiers.h" -#include -#include - - -const RectCoord WebMercatorTiled2dMapLayerConfig::WEB_MERCATOR_BOUNDS = RectCoord(Coord(CoordinateSystemIdentifiers::EPSG3857(), -20037508.34, 20037508.34, 0.0), - Coord(CoordinateSystemIdentifiers::EPSG3857(), 20037508.34, -20037508.34, 0.0)); -const double WebMercatorTiled2dMapLayerConfig::BASE_ZOOM = 559082264.029; -const double WebMercatorTiled2dMapLayerConfig::BASE_WIDTH = 40075016; - -WebMercatorTiled2dMapLayerConfig::WebMercatorTiled2dMapLayerConfig(std::string layerName, std::string urlFormat, int32_t minZoomLevel, - int32_t maxZoomLevel) - : layerName(layerName), urlFormat(urlFormat), minZoomLevel(minZoomLevel), maxZoomLevel(maxZoomLevel) - {} - -WebMercatorTiled2dMapLayerConfig::WebMercatorTiled2dMapLayerConfig(std::string layerName, std::string urlFormat, - const Tiled2dMapZoomInfo &zoomInfo, int32_t minZoomLevel, - int32_t maxZoomLevel) - : layerName(layerName), urlFormat(urlFormat), zoomInfo(zoomInfo), minZoomLevel(minZoomLevel), maxZoomLevel(maxZoomLevel) {} - -int32_t WebMercatorTiled2dMapLayerConfig::getCoordinateSystemIdentifier() { return CoordinateSystemIdentifiers::EPSG3857(); } - -std::string WebMercatorTiled2dMapLayerConfig::getTileUrl(int32_t x, int32_t y, int32_t t, int32_t zoom) { - std::string url = urlFormat; - size_t zoomIndex = url.find("{z}", 0); - if (zoomIndex == std::string::npos) - throw std::invalid_argument("Layer url \'" + url + "\' has no valid format!"); - url = url.replace(zoomIndex, 3, std::to_string(zoom)); - size_t xIndex = url.find("{x}", 0); - if (xIndex == std::string::npos) - throw std::invalid_argument("Layer url \'" + url + "\' has no valid format!"); - url = url.replace(xIndex, 3, std::to_string(x)); - size_t yIndex = url.find("{y}", 0); - if (yIndex == std::string::npos) - throw std::invalid_argument("Layer url \'" + url + "\' has no valid format!"); - return url.replace(yIndex, 3, std::to_string(y)); -} - -std::string WebMercatorTiled2dMapLayerConfig::getLayerName() { return layerName; } - -std::vector WebMercatorTiled2dMapLayerConfig::getZoomLevelInfos() { - std::vector levels; - levels.reserve(maxZoomLevel - minZoomLevel + 1); - for (int32_t i = minZoomLevel; i <= maxZoomLevel; ++i) { - levels.emplace_back(getZoomLevelInfo(i)); - } - return levels; -} - -std::vector WebMercatorTiled2dMapLayerConfig::getVirtualZoomLevelInfos() { - std::vector levels; - levels.reserve(minZoomLevel); - for (int32_t i = 0; i < minZoomLevel; ++i) { - levels.emplace_back(getZoomLevelInfo(i)); - } - return levels; -}; - -Tiled2dMapZoomInfo WebMercatorTiled2dMapLayerConfig::getZoomInfo() { - return zoomInfo; -} - -std::optional WebMercatorTiled2dMapLayerConfig::getVectorSettings() { - return std::nullopt; -} - -std::optional<::RectCoord> WebMercatorTiled2dMapLayerConfig::getBounds() { - return WEB_MERCATOR_BOUNDS; -} - -Tiled2dMapZoomLevelInfo WebMercatorTiled2dMapLayerConfig::getZoomLevelInfo(int32_t zoomLevel) { - double tileCount = std::pow(2.0,zoomLevel); - double zoom = BASE_ZOOM / tileCount; - return {zoom, (float) (BASE_WIDTH / tileCount), (int32_t) tileCount, (int32_t) tileCount, 1, zoomLevel, WEB_MERCATOR_BOUNDS}; -} diff --git a/shared/src/map/layers/tiled/WebMercatorTiled2dMapLayerConfig.h b/shared/src/map/layers/tiled/WebMercatorTiled2dMapLayerConfig.h deleted file mode 100644 index b7e0dc02d..000000000 --- a/shared/src/map/layers/tiled/WebMercatorTiled2dMapLayerConfig.h +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2021 Ubique Innovation AG - * - * 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 https://mozilla.org/MPL/2.0/. - * - * SPDX-License-Identifier: MPL-2.0 - */ - -#pragma once - -#include "Tiled2dMapLayerConfig.h" -#include "Tiled2dMapZoomInfo.h" -#include "Tiled2dMapZoomLevelInfo.h" -#include "WmtsLayerDescription.h" - -class WebMercatorTiled2dMapLayerConfig : public Tiled2dMapLayerConfig { - public: - WebMercatorTiled2dMapLayerConfig(std::string layerName, std::string urlFormat, int32_t minZoomLevel = 0, int32_t maxZoomLevel = 20); - - WebMercatorTiled2dMapLayerConfig(std::string layerName, std::string urlFormat, const Tiled2dMapZoomInfo &zoomInfo, int32_t minZoomLevel, int32_t maxZoomLevel); - - virtual int32_t getCoordinateSystemIdentifier() override; - - virtual std::string getTileUrl(int32_t x, int32_t y, int32_t t, int32_t zoom) override; - - virtual std::string getLayerName() override; - - virtual std::vector getZoomLevelInfos() override; - - virtual std::vector getVirtualZoomLevelInfos() override; - - virtual Tiled2dMapZoomInfo getZoomInfo() override; - - virtual std::optional getVectorSettings() override; - - std::optional<::RectCoord> getBounds() override; - -private: - Tiled2dMapZoomLevelInfo getZoomLevelInfo(int32_t zoomLevel); - - const static RectCoord WEB_MERCATOR_BOUNDS; - const static double BASE_ZOOM; - const static double BASE_WIDTH; - - std::string layerName; - std::string urlFormat; - - int32_t minZoomLevel = 0; - int32_t maxZoomLevel = 20; - Tiled2dMapZoomInfo zoomInfo = Tiled2dMapZoomInfo(1.0, 0, 0, true, false, true, true); -}; diff --git a/shared/src/map/layers/tiled/vector/Tiled2dMapVectorGeoJSONLayerConfig.h b/shared/src/map/layers/tiled/vector/Tiled2dMapVectorGeoJSONLayerConfig.h deleted file mode 100644 index 322f49ef3..000000000 --- a/shared/src/map/layers/tiled/vector/Tiled2dMapVectorGeoJSONLayerConfig.h +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2021 Ubique Innovation AG - * - * 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 https://mozilla.org/MPL/2.0/. - * - * SPDX-License-Identifier: MPL-2.0 - */ - -#pragma once -#include "Tiled2dMapVectorLayerConfig.h" - -class Tiled2dMapVectorGeoJSONLayerConfig : public Tiled2dMapVectorLayerConfig { -public: - Tiled2dMapVectorGeoJSONLayerConfig(const std::string &sourceName, const std::weak_ptr geoJSON, const Tiled2dMapZoomInfo &zoomInfo = Tiled2dMapZoomInfo(1.0, 0, 0, false, true, false, true)) - : Tiled2dMapVectorLayerConfig(nullptr, zoomInfo), geoJSON(geoJSON), sourceName(sourceName) {} - - ~Tiled2dMapVectorGeoJSONLayerConfig() {} - - std::string getTileUrl(int32_t x, int32_t y, int32_t t, int32_t zoom) override { - static std::string empty = ""; - return empty; - } - - std::vector getZoomLevelInfos() override { - int maxZoom = 0; - int minZoom = 0; - if (auto geoJSON = this->geoJSON.lock()){ - minZoom = geoJSON->getMinZoom(); - maxZoom = geoJSON->getMaxZoom(); - } - return getDefaultEpsg3857ZoomLevels(minZoom, maxZoom, std::nullopt); - } - - std::vector getVirtualZoomLevelInfos() override { - return {}; - } - - std::string getLayerName() override { - return sourceName; - } - -protected: - std::weak_ptr geoJSON; - const std::string sourceName; -}; diff --git a/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayer.cpp b/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayer.cpp index e03f165fa..2fa393676 100644 --- a/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayer.cpp +++ b/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayer.cpp @@ -19,7 +19,6 @@ #include "BackgroundVectorLayerDescription.h" #include "VectorTileGeometryHandler.h" #include "Tiled2dMapVectorBackgroundSubLayer.h" -#include "Tiled2dMapVectorRasterSubLayerConfig.h" #include "Polygon2dInterface.h" #include "MapCameraInterface.h" #include "QuadMaskObject.h" @@ -45,10 +44,14 @@ #include "Tiled2dMapVectorReadyManager.h" #include "Tiled2dVectorGeoJsonSource.h" #include "Tiled2dMapVectorStyleParser.h" -#include "Tiled2dMapVectorGeoJSONLayerConfig.h" #include "GeoJsonVTFactory.h" #include "VectorLayerFeatureCoordInfo.h" +#include "Epsg2056Tiled2dMapLayerConfig.h" +#include "Epsg21781Tiled2dMapLayerConfig.h" +#include "Epsg3857Tiled2dMapLayerConfig.h" +#include "Epsg4326Tiled2dMapLayerConfig.h" + Tiled2dMapVectorLayer::Tiled2dMapVectorLayer(const std::string &layerName, const std::optional &remoteStyleJsonUrl, const std::vector> &loaders, @@ -237,14 +240,26 @@ Tiled2dMapVectorLayer::getLayerConfig(const std::shared_ptr(source, *customZoomInfo) - : std::make_shared(source, mapInterface->is3d()); + const std::optional crs = source->coordinateReferenceSystem; + const auto &zoomInfo = customZoomInfo.has_value() ? *customZoomInfo : source->getZoomInfo(mapInterface->is3d()); + if(!crs.has_value() || *crs == CoordinateSystemIdentifiers::EPSG3857()) { + return std::make_shared(source->identifier, source->vectorUrl, source->bounds, zoomInfo, source->getZoomLevels()); + } else if(*crs == CoordinateSystemIdentifiers::EPSG4326()){ + return std::make_shared(source->identifier, source->vectorUrl, source->bounds, zoomInfo, source->getZoomLevels()); + } else if(*crs == CoordinateSystemIdentifiers::EPSG2056()){ + return std::static_pointer_cast(std::make_shared(source->identifier, source->vectorUrl, source->bounds, zoomInfo, source->getZoomLevels())); + } else if(*crs == CoordinateSystemIdentifiers::EPSG21781()){ + return std::make_shared(source->identifier, source->vectorUrl, source->bounds, zoomInfo, source->getZoomLevels()); + } + // layer will be ignored + return nullptr; } std::shared_ptr Tiled2dMapVectorLayer::getGeoJSONLayerConfig(const std::string &sourceName, const std::shared_ptr &source) { - return customZoomInfo.has_value() ? std::make_shared(sourceName, source, *customZoomInfo) - : std::make_shared(sourceName, source); + const auto levels = Tiled2dMapVectorLayerConfig::generateLevelsFromMinMax(source->getMinZoom(), source->getMaxZoom()); + auto zoomInfo = customZoomInfo.has_value() ? *customZoomInfo : Tiled2dMapZoomInfo(1.0, 0, 0, false, true, false, true); + return std::make_shared(sourceName, "", std::nullopt, zoomInfo, levels); } void Tiled2dMapVectorLayer::setMapDescription(const std::shared_ptr &mapDescription) { @@ -296,7 +311,6 @@ void Tiled2dMapVectorLayer::initializeVectorLayer() { if (!mapInterface) { return; } - bool is3d = mapInterface->is3d(); std::shared_ptr selfMailbox = mailbox; if (!mailbox) { @@ -332,8 +346,7 @@ void Tiled2dMapVectorLayer::initializeVectorLayer() { break; } case raster: { - auto rasterSubLayerConfig = std::make_shared( - std::dynamic_pointer_cast(layerDesc), is3d, customZoomInfo); + auto rasterSubLayerConfig = getLayerConfig(std::dynamic_pointer_cast(layerDesc)->source); auto sourceMailbox = std::make_shared(mapInterface->getScheduler()); auto sourceActor = Actor(sourceMailbox, diff --git a/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayerParserHelper.cpp b/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayerParserHelper.cpp index b828c8ee0..6da9439cf 100644 --- a/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayerParserHelper.cpp +++ b/shared/src/map/layers/tiled/vector/Tiled2dMapVectorLayerParserHelper.cpp @@ -92,8 +92,8 @@ Tiled2dMapVectorLayerParserResult Tiled2dMapVectorLayerParserHelper::parseStyleJ bool maskTiles = true; double zoomLevelScaleFactor = 1.0; - bool overzoom = val.contains("overzoom") ? tileJsons["overzoom"].get() : true; - bool underzoom = val.contains("underzoom") ? tileJsons["underzoom"].get() : false; + bool overzoom = val.contains("overzoom") ? val["overzoom"].get() : true; + bool underzoom = val.contains("underzoom") ? val["underzoom"].get() : false; std::optional> levels; int minZoom = std::numeric_limits::max(); @@ -111,9 +111,14 @@ Tiled2dMapVectorLayerParserResult Tiled2dMapVectorLayerParserHelper::parseStyleJ minZoom = val.value("minzoom", 0); maxZoom = val.value("maxzoom", 22); } + std::optional coordinateReferenceSystem; + if(val.contains("crs") && val["crs"].is_string()) { + coordinateReferenceSystem = CoordinateSystemIdentifiers::fromCrsIdentifier(val["crs"].get()); + } else if(val["metadata"].is_object() && val["metadata"].contains("crs") && val["metadata"]["crs"].is_string()) { + coordinateReferenceSystem = CoordinateSystemIdentifiers::fromCrsIdentifier(val["metadata"]["crs"].get()); + } std::optional<::RectCoord> bounds; - std::optional coordinateReferenceSystem; if (val["tiles"].is_array()) { auto str = val.dump(); @@ -135,9 +140,6 @@ Tiled2dMapVectorLayerParserResult Tiled2dMapVectorLayerParserHelper::parseStyleJ } } - if(val["metadata"].is_object() && val["metadata"].contains("crs") && val["metadata"]["crs"].is_string()) { - coordinateReferenceSystem = val["metadata"]["crs"].get(); - } } else if (val["url"].is_string()) { auto result = LoaderHelper::loadData(replaceUrlParams(val["url"].get(), sourceUrlParams), std::nullopt, loaders); @@ -166,30 +168,26 @@ Tiled2dMapVectorLayerParserResult Tiled2dMapVectorLayerParserHelper::parseStyleJ } } - if(json["metadata"].is_object() && json["metadata"].contains("crs") && json["metadata"]["crs"].is_string()) { - coordinateReferenceSystem = json["metadata"]["crs"].get(); - } minZoom = json.value("minzoom", 0); maxZoom = json.value("maxzoom", 22); } - // XXX: coordinateReferenceSystem - // XXX: maskTiles rasterSourceMap[key] = std::make_shared( - key, - url, - minZoom, - maxZoom, - bounds, - adaptScaleToScreen, - numDrawPreviousLayers, - zoomLevelScaleFactor, - underzoom, - overzoom, - levels, - maskTiles - ); + key, + url, + minZoom, + maxZoom, + bounds, + zoomLevelScaleFactor, + adaptScaleToScreen, + numDrawPreviousLayers, + underzoom, + overzoom, + levels, + coordinateReferenceSystem, + maskTiles + ); } else if (type == "vector" && val["url"].is_string()) { auto result = LoaderHelper::loadData(replaceUrlParams(val["url"].get(), sourceUrlParams), std::nullopt, loaders); @@ -269,6 +267,10 @@ Tiled2dMapVectorLayerParserResult Tiled2dMapVectorLayerParserHelper::parseStyleJ minZoom = tileJson.value("minzoom", 0); maxZoom = tileJson.value("maxzoom", 22); } + std::optional coordinateReferenceSystem; + if(tileJson.contains("crs") && tileJson["crs"].is_string()) { + coordinateReferenceSystem = CoordinateSystemIdentifiers::fromCrsIdentifier(tileJson["crs"].get()); + } sourceDescriptions.push_back( std::make_shared(identifier, @@ -281,7 +283,9 @@ Tiled2dMapVectorLayerParserResult Tiled2dMapVectorLayerParserHelper::parseStyleJ numDrawPreviousLayers, underzoom, overzoom, - levels)); + levels, + coordinateReferenceSystem + )); } @@ -374,13 +378,13 @@ Tiled2dMapVectorLayerParserResult Tiled2dMapVectorLayerParserHelper::parseStyleJ bool overzoom = source->overzoom && !val.contains("maxzoom"); auto layer = std::make_shared(val["id"], - source, - val.value("minzoom", source->minZoom), - val.value("maxzoom", source->maxZoom), - filter, - renderPassIndex, - interactable, - style); + source, + val.value("minzoom", source->minZoom), + val.value("maxzoom", source->maxZoom), + filter, + renderPassIndex, + interactable, + style); layers.push_back(layer); } else if (val["type"] == "line") { diff --git a/shared/src/map/layers/tiled/vector/Tiled2dMapVectorRasterSubLayerConfig.h b/shared/src/map/layers/tiled/vector/Tiled2dMapVectorRasterSubLayerConfig.h deleted file mode 100644 index 420b017bc..000000000 --- a/shared/src/map/layers/tiled/vector/Tiled2dMapVectorRasterSubLayerConfig.h +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (c) 2021 Ubique Innovation AG - * - * 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 https://mozilla.org/MPL/2.0/. - * - * SPDX-License-Identifier: MPL-2.0 - */ -#pragma once - -#include "VectorLayerDescription.h" -#include "RasterVectorLayerDescription.h" -#include "Tiled2dMapVectorLayerConfig.h" -#include "Tiled2dMapZoomInfo.h" -#include "CoordinateSystemIdentifiers.h" -#include "Tiled2dMapZoomLevelInfo.h" -#include "Logger.h" -#include "Tiled2dMapVectorSettings.h" -#include "Epsg4326Tiled2dMapLayerConfig.h" - -class Tiled2dMapVectorRasterSubLayerConfig : public Tiled2dMapVectorLayerConfig { -public: - Tiled2dMapVectorRasterSubLayerConfig(const std::shared_ptr &layerDescription, - const bool is3d, - const std::optional &customZoomInfo = std::nullopt) - : Tiled2dMapVectorLayerConfig(std::static_pointer_cast(layerDescription->source), is3d), - description(layerDescription) { - if (customZoomInfo.has_value()) { - // zoomInfo is already initialized in super from the source-description - zoomInfo.zoomLevelScaleFactor *= customZoomInfo->zoomLevelScaleFactor; - zoomInfo.numDrawPreviousLayers = std::max(customZoomInfo->numDrawPreviousLayers, zoomInfo.numDrawPreviousLayers); - zoomInfo.numDrawPreviousOrLaterTLayers = 0; - zoomInfo.adaptScaleToScreen |= customZoomInfo->adaptScaleToScreen; - zoomInfo.maskTile |= customZoomInfo->maskTile; - zoomInfo.underzoom &= customZoomInfo->underzoom; - zoomInfo.overzoom &= customZoomInfo->overzoom; - } - - if (description->source->coordinateReferenceSystem == "EPSG:4326") { - customConfig = std::make_shared(layerDescription->source->identifier, - layerDescription->source->vectorUrl, - zoomInfo, - layerDescription->sourceMinZoom, - layerDescription->sourceMaxZoom); - } - } - - int32_t getCoordinateSystemIdentifier() override { - if (customConfig) { - return customConfig->getCoordinateSystemIdentifier(); - } - return Tiled2dMapVectorLayerConfig::getCoordinateSystemIdentifier(); - } - - std::string getTileUrl(int32_t x, int32_t y, int32_t t, int32_t zoom) override { - if (customConfig) { - return customConfig->getTileUrl(x,y,t,zoom); - } - return Tiled2dMapVectorLayerConfig::getTileUrl(x,y,t,zoom); - } - - std::string getLayerName() override { - if (customConfig) { - return customConfig->getLayerName(); - } - return Tiled2dMapVectorLayerConfig::getLayerName(); - } - - std::vector getZoomLevelInfos() override { - if (customConfig) { - return customConfig->getZoomLevelInfos(); - } - return Tiled2dMapVectorLayerConfig::getZoomLevelInfos(); - } - - std::vector getVirtualZoomLevelInfos() override { - if (customConfig) { - return customConfig->getVirtualZoomLevelInfos(); - } - return Tiled2dMapVectorLayerConfig::getVirtualZoomLevelInfos(); - } - - Tiled2dMapZoomInfo getZoomInfo() override { - if (customConfig) { - return customConfig->getZoomInfo(); - } - return Tiled2dMapVectorLayerConfig::getZoomInfo(); - } - - std::optional getVectorSettings() override { - if (customConfig) { - return customConfig->getVectorSettings(); - } - return Tiled2dMapVectorLayerConfig::getVectorSettings(); - } - - std::optional<::RectCoord> getBounds() override { - if (customConfig) { - return customConfig->getBounds(); - } - return Tiled2dMapVectorLayerConfig::getBounds(); - } - - -private: - std::shared_ptr description; - - std::shared_ptr customConfig; -}; diff --git a/shared/test/TestTileSource.cpp b/shared/test/TestTileSource.cpp index dc87cf455..0e28fe3e1 100644 --- a/shared/test/TestTileSource.cpp +++ b/shared/test/TestTileSource.cpp @@ -1,9 +1,9 @@ #include "CoordinateSystemFactory.h" #include "DataLoaderResult.h" +#include "Epsg3857Tiled2dMapLayerConfig.h" #include "LoaderInterface.h" #include "TextureLoaderResult.h" #include "Tiled2dMapSource.h" -#include "WebMercatorTiled2dMapLayerConfig.h" #include "helper/TestScheduler.h" #include "Tiled2dMapSourceImpl.h" @@ -219,9 +219,18 @@ class BlockingTestLoader : public LoaderInterface { std::list blockedLoads; }; +static std::shared_ptr createTestLayerConfig() { + auto zoomInfo = Tiled2dMapVectorLayerConfig::defaultMapZoomInfo(); + zoomInfo.adaptScaleToScreen = false; // Important, otherwise the onVisibleBoundsChanged does not pick up the (in the tests) expected zoom level. + return std::make_shared("mock", "test-data://tile/{z}/{x}/{y}", + std::nullopt, + zoomInfo, + Tiled2dMapVectorLayerConfig::generateLevelsFromMinMax(0, 20) + ); +} + TEST_CASE("VectorTileSource") { - auto layerConfig = std::make_shared( - "mock", "{z}/{x}/{y}", Tiled2dMapZoomInfo(1.0, 0, 0, false, true, false, true), 0, 20); + auto layerConfig = createTestLayerConfig(); auto scheduler = std::make_shared(); std::shared_ptr source = std::make_shared( layerConfig, scheduler, std::vector>{std::make_shared()}); @@ -273,11 +282,10 @@ static std::unordered_map generateDummyData(const std: */ TEST_CASE("Tiled2dMapSource slow fallback does not block local loads") { - auto layerConfig = std::make_shared( - "mock", "test-data://tile/{z}/{x}/{y}", Tiled2dMapZoomInfo(1.0, 0, 0, false, true, false, true), 0, 20); + auto layerConfig = createTestLayerConfig(); // Load the entire world. - auto rect = *layerConfig->getBounds(); + auto rect = CoordinateSystemFactory::getEpsg3857System().bounds; auto zoomLevelInfos = layerConfig->getZoomLevelInfos(); // Try with different zoom levels; that's 1, 16, 256 or 1024 tiles const int z = GENERATE(0, 2, 4, 5); @@ -320,6 +328,7 @@ TEST_CASE("Tiled2dMapSource slow fallback does not block local loads") { // Complete all "local" loads scheduler->drain(); + CAPTURE(z); REQUIRE(source->numLoadingOrQueued() == expectedTiles.size()); while (localLoader->unblockAll()) { scheduler->drain(); @@ -347,11 +356,10 @@ TEST_CASE("Tiled2dMapSource slow fallback does not block local loads") { TEST_CASE("Tiled2dMapSource error load retry") { - auto layerConfig = std::make_shared( - "mock", "test-data://tile/{z}/{x}/{y}", Tiled2dMapZoomInfo(1.0, 0, 0, false, true, false, true), 0, 20); + auto layerConfig = createTestLayerConfig(); // Load the entire world at zoom level 3 - auto rect = *layerConfig->getBounds(); + auto rect = CoordinateSystemFactory::getEpsg3857System().bounds; auto zoomLevelInfos = layerConfig->getZoomLevelInfos(); const int z = 3; std::vector expectedTiles = {{rect, 0, 0, 0, 0, int(zoomLevelInfos[0].zoom)}}; From 06fbbf661c3a55c6a688b87ea698874d9c7b3169 Mon Sep 17 00:00:00 2001 From: Matthias Frei Date: Fri, 23 Jan 2026 17:39:10 +0100 Subject: [PATCH 3/3] Update golden images, small differences for geojson changed tile pyramid For the EPSG 3857 / WebMercator tile pyramid we now consistently use the base zoom value 559082264.029 instead of 500000000.0. Previously. both values where used in different places. Note: the webmercator tile pyramid is specified as urn:ogc:def:wkss:OGC:1.0:GoogleMapsCompatible in OGC WMTS Simple Profile, with the used base zoom value as ScaleDenominator of TileMatrix 0. --- ...MapRendererTest_testTiler_tile_7_66_44.png | Bin 4381 -> 4380 bytes ...MapRendererTest_testTiler_tile_7_66_45.png | Bin 6973 -> 6958 bytes ...MapRendererTest_testTiler_tile_7_67_44.png | Bin 4743 -> 4741 bytes ...MapRendererTest_testTiler_tile_7_67_45.png | Bin 7900 -> 7890 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/jvm/src/test/resources/golden/OffscreenMapRendererTest_testTiler_tile_7_66_44.png b/jvm/src/test/resources/golden/OffscreenMapRendererTest_testTiler_tile_7_66_44.png index 16b93c1cb28af68163ef57e21d6c53fdf5bb7022..7da730f3a8c41e657f6a0cb18876cc8a5ff60f8a 100644 GIT binary patch delta 3460 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 delta 3460 zcmY*cd0f)j+CM)~o6+mSWSV>7OhuYbX>M7VgW94tt!koZnv`IsmWm=jn#P+~G^LFu zw-y_lx~Zk9xxv`DWE!bW;tIK;r6J-1vb}WQ_x zE(ky7voJd(97KcwJ*vS?4*<)dRDGZiK%v(mZN(RJU=Dy=dAQazw?uUcwE+~irq03r z8tv=1E3~GKGR};B=794%iuCu{{soEpq#ykE5$sER z*c2YUc|jy20y+{li?6@_c}{qwp})L_#h9-%G6b`D2CNHzsbQ5u`<&i|&=mtE416B8 zN?VzxD#rkN(Y~H@Vs6-S+LI9%sArcrw+b0&bf3WGE((R3_+0L{VzsIEssIM{8apL< zL+x1H<#!cAu`J!<(pyWw3G;YCK1s0}sNJT*48xdDNyr~fWimOXOQDHZlXH3f2|?)X z&I=0*v{EU5>q6~)2nZefjIhKE?34 zS>RSloi2;uug=Gd58YdzXHh0dxfc?2o0}Kb`!ud=Wi{Nl*1GEWz?6w%YVd~YyO|dF z&)Et}C~$jZJeLrSHNB+RMG(!zVAdC%I_H$H6EQc&KYZS=H7Zi%vYe3<@cQ9O^=S%$ z0$a_ch(}MhV$HY)yr@@N9OOx|$734?=iwN36XA2z2QqlRo06N*nnp+(ZgZd={7A0B z^XliS-qs8!S*mSce?Ef%fg5b*m2f_H4K16<@G4tqZ%Lc0lep;_k#ei6o<&o8L9G z!v#^BJ&ET{328SQ%Vx&|CH#g&IJZmwX>`-wfT+<>>vYjL6ug%KAA)T^f0JgpyY^8{ zW%6IUO|#v1uXt+5#)prd0 zsyE)YL2}kk{6w`(SbnAIxj)!0)V7u?uM+)6vfn9Oqt<^~qfXC)55 zTM~2ITC4|3WL0W7S0A3>{o}7jM-Uu>y(_ur2@BXPZmT3OYCIoaK{lVg7o1q?YNEDr zH%_qI7E9_rg70l{V}~yvW=u8~h3+qS_401*qgwa%7QepSZ&C6f&te4$>_{7n@*b%7 zwe<+_^Q2tVsM|xT$c1pAcs%3^Xj6_L%w%coTHH}d_JsNh{L=l*CH4|+wT?o7Dt~!q zG>Keg&Zxx4*y|Y~;kKD+uMfjHLm5vV4PSTJHu8nEpo~u%GR3D?$X4q{Mug+jweoWQ z(Ac8-6BH44&{!R_hp#?~Qe=@u4-xgpn3nukiou&JH$^Gg^1)*%E3fLhCCOZQ1IfrC zLv##^*bQR>cW-KV|4seTY36-b_SAzZM12gC8gH^e{#0&XfhAgZZ;tAL;~QA9G09fo z*3J935H2RYp{>)*QcxqEd@&U#MX?R;u!lY*q_ak+wp(T9J-7PX=bC8UCo1gurAU|| z+P**n!GVl^yg#2Rb;vjA3ALT+37(A_eY>+3UOIBJZr3%NGO1*RctlJ9HGIkg0<4p{ebdW!cM;n+ZG9d*klFfhGpPhUG(w5_8wg+Welh+<$-wwVhog z*?kO+?jPMWuondTX2&terk^fWH%(>LRDmL2o1l)0jP>5hmf#i>mBLWDp+#Ms0@zTH z=|q?d_{b`NBHRg&RTaHPi_~SJGeD4XWldp6p*^52> zdNy(HV+W}8@q?GnMSe1dmr^&s@MENwBvFfH#J7+1=}b1+gOl*kD=!mn+|o^TeX)|A zTJUQpcUiG4bDwJea(sE@X+~ne^5snUxeT;l|7GbSo+hs;A1`{JCcZNoR_;^bQ^|G3 z#yi^W8ZB~IsG}4N=1<6`MDYPDA5-_1$cPTF<(*s-h?YQCH^V;DqXoS=-JIaXwo_Ol zZB%@%dOU~2Vk;JuBf_y!=91Ith6cmy%_X5jo+fGvKa1?Z5m!U06N^;|&iocwbYS@z z>UP1E-Oj*DnPQA>E~#_2_|Y{?vIjMQr;nR4dI|`uHvWv!2d(7BCX(UWYI=;k=A^<_ zdrv?oPYUCAbsf%T&uSjMpe1MOM9h??CTINF*c7_ss;c2kqWx@KPJQ@&C-eW+AwPpU zYDy!{Q6>oE^_d;XwPqduoVsu{oO~;a#zhwxocE6dH9y~On^9AC)jH@O{9QWq4f%9Q zHr0)@eaUUvME68qQGCX#`j!iHLi#2W0bBO-w1%s);hEX9tLi%mkphJZmux2B?T8Kd znD+Ohow^!}LK~hU)U{{L0gdR3dNAY=Z-!9$)*z^8@s#gB#Nq z?|U7Lb}UnOKX1Y)!X~k7N9M>Hw_Aas{dPg+qKm6o`L-c)C&9o<;N~$XYCQ{wXP9uT z52-)zH1=36;a~5FL|DFZ8$9%Juthu8L#fU&qkF;^rfj?4HB}5S)pq^Z+(CTqb; zvhzNwuJo7hL5}TWW54))BkB56J##oiqSstiy*DdxK!a3!X|1mMVNSQ#N>FcsybJ4? zC}2ITjn-SkE3tJ2gm8PtWqS zj2IdOb>2l;_T~{on!(kyBJRyESdGZuI-;TNjUX1_qIkkOBzj;w=! zo+yRhMmF{e3Jeh_cLF_WJrT;0`b4X8fWcNL&@}Mw{{{Py0%t5rW+ISk+;kLxc4nV) zM{l07vRHy0ofdXqp=T93aS}An@oH65nW{+!jF=J~URlDge1-z&yNZ#T&27dgepmWT z1kjZ;HLLdFVA6>Bz9djg*9NDy?ho%g*bDTbNf`~az`{ni&rDN&F)j; zqM2aG*_76&LO(x4pF6DZiCzLj_vRTLeF*a9F-Pw1B*oJO zQ_a+-HaP>VQt|p>hR*~A3V-qd?o*=|-h&D!M}?D8W2rZ99L~CWiswtDDom$?7Sd_vRmt5Jm(P+||)aEX&#YXrFgq!#Q zDzG!WP09F4G2;oYgbbheuyA8Wrk_h@q?wgt3J^zh=$iy zQ*CWB314??dsm-Qmz_21!I4k3R7e>eUyzNC`kWH1a`FwD?aIcX(dqw~Z}wLcb-a)p z&{w{lZ?;x*H`4!4ay|qOQgENB!afAy3{ZmfaVtgQFk4gSiJu`_(#$coug3XL+S13Z z>?g`~RFwM#dPRLN-y4f^_o0jOH6-Y8 zZmMt-&d{g}6obG`%tOt0OcAk~+dGKnpBAs!#tKSJ+HSlr|pjQ+cY(bal7KUWg he3d3pcE`X%kmuP;EeG&(L0`W>`+Wj&<=zp${12{g*Hi!i diff --git a/jvm/src/test/resources/golden/OffscreenMapRendererTest_testTiler_tile_7_66_45.png b/jvm/src/test/resources/golden/OffscreenMapRendererTest_testTiler_tile_7_66_45.png index b1e9beee3572185da975dbf4837bee24dfa629c5..d51d681884c1f4805fc7eb1dd20e6fe4eaf3220c 100644 GIT binary patch delta 6306 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 koW#<5&= z$Ylhi;Bk+_P{z~+oTJsRm2Fi;?&XX`I<$A%BFe4Bw{U}Abyu5&8Nf8I1&!`B&k301 z(|Y2rA-;^Q1nt*rEq*)Iz040|p@5JOM~D10;aXHv4oU0IsKs|s;0YDEQZ6*MKPhlk&9-zZu&TS=O`yl|Tg)Udf&kT$c2L1{s|TNRvV2d z9#oq3?vW}O+(rbCsF)3hQ-MFcBQqS+q{!(%n~KI_-c&SjP0XwW)8H+W&-x?8Y34l7 zTj!6A>WRLi^2kw>nE&TfxYM z+$E!`hDuIvXFkr=9u1PaM-i$4z-b0kI@E9dUJ62*HexB8#XJA0E7`EUrn5#ktY-lj z1fV-L$sBaQE0O)WuEcw8K4!cXy{zDRe8-n1GxZCE9D3k>4KxUxGC>l8mjvq5peykb zm1*z5sZWAF-872#MaHa@&XRz&W%)cZw{i2C-H?U368n&#b#M*I(iQAu?SXG$ON7IZ zJBWBFzf!3Ndy8s}>?595i|HW+#=WYN_r=2�rf~fWfg~YeJGq=^e>L<}1AE_v_81 z4oHpw_V-~*?FPj;(kf6aotTg9XoMgSRB;88_3DrtHjx<8&fq~o5OFMks}7t`7M79q zN0vO(YCZM2fwJ?Hqg#RMg|RYn+Z}@Np)W^h>8XDR(L2oSHUdl&1YT~SIg^y7YQJC> zi!Cr?@=yR^`Zjp;T1Gypi?>x(=A*_>?J8DUiXR^aELg`5-JP%Q>Kp=>0!W?){2f09 zn?7xT9o;SgY$f delta 6309 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 W`ZH*tqFX)%P`{o$Qu}lGjsFEsxDFNo diff --git a/jvm/src/test/resources/golden/OffscreenMapRendererTest_testTiler_tile_7_67_44.png b/jvm/src/test/resources/golden/OffscreenMapRendererTest_testTiler_tile_7_67_44.png index 934f19b19f7a126ceb44af9ac48c931b6e4270f3..ba9c1e2ab45fdc59e80ee5456852a8bf4fe0dcf3 100644 GIT binary patch literal 4741 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 M$=$K)1nK5~0kxa&WB>pF literal 4743 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 delta 6414 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 nu?m$g>LCy*KwWOjWU_!G1&+