Use declared WMTS TileMatrix identifiers in tile URLs (fixes BlueTopo 400s) - #182
Merged
Conversation
added 18 commits
July 31, 2026 17:11
Use zoom_levels[].id for TileMatrix URL substitution instead of bare numeric zoom index
TileLayout::getUrl() substituted the bare numeric zoom index for the
{TileMatrix} key. GeoServer GWC (e.g. the BlueTopo WMTS presets) names
its tile matrices <gridset>:<z> (EPSG:3857:<z>), so every tile request
returned HTTP 400 (Unknown TILEMATRIX) and the presets never rendered.
Emit zoom_levels[address.zoomLevel()].id, already parsed from the WMTS
<ows:Identifier> in capabilities.cpp. Fall back to the bare numeric
index when id is empty so bare-numeric servers (OSM/XYZ, NOAA ArcGIS)
keep working unchanged.
Refs #178
The review-issue premise that no URL generation tests exist was wrong: test/test_wms_url_generation.cpp already covers getUrl(). Per the plan checkpoint decision, fold the plan-review suggestion in — extend that file with the new TileMatrix cases instead of adding a new test file plus CMake registration. Refs #178
Record the TileMatrix id fix, the extended URL-generation tests, the plan sync, and the standalone + counterfactual verification (multi-layer build unavailable in-container). Refs #178
…178) The {TileMatrix} substitution in getUrl() splices the raw WMTS <ows:Identifier> (server-controlled) into the URL path. Percent-encode it so a hostile capabilities document cannot inject path/query/fragment delimiters. ':' is excluded from encoding (legal path-segment char and load-bearing for GeoServer GWC ids like EPSG:3857:<z>); the bare-numeric fallback is digits-only and passes through unchanged.
Two cases the OSM stand-in (id == index) could not exercise: - TileMatrixPrefersIdOverIndexWhenMismatched: id numeric tail != position, proving getUrl() emits the declared id, not the loop index. - TileMatrixIdIsPercentEncoded: a '/' in the id is encoded to %2F while ':' is preserved, covering the R1 percent-encoding fix.
There was a problem hiding this comment.
Pull request overview
Fixes WMTS tile URL generation for GeoServer GWC-based sources (e.g., NOAA nowCOAST / BlueTopo) by ensuring {TileMatrix} uses the declared WMTS TileMatrix identifier rather than the numeric zoom index, and hardens WMTS template substitutions against URL-delimiter injection via percent-encoding.
Changes:
- Update
{TileMatrix}substitution to emitzoom_levels[z].idwith a numeric fallback, preserving bare-numeric server behavior. - Percent-encode server-controlled WMTS substitution values (TileMatrix id in
TileLayout::getUrl(), plusStyleandTileMatrixSetinwmts::Capabilities::getLayout()), preserving:for GWC compatibility. - Add focused gtest coverage for TileMatrix id selection, fallback, and encoding behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| test/test_wms_url_generation.cpp | Adds regression tests covering TileMatrix id-over-index behavior, empty-id fallback, and percent-encoding (with : preserved). |
| src/camp_map/wmts/capabilities.cpp | Percent-encodes server-controlled Style and TileMatrixSet substitutions when assembling WMTS resource URL templates. |
| src/camp_map/map_tiles/tile_layout.cpp | Fixes {TileMatrix} URL substitution to use declared per-zoom identifiers, with fallback and delimiter-safe encoding. |
| .agent/work-plans/issue-178/progress.md | Adds the issue work-plan progress log capturing rationale, verification notes, and review iterations. |
| .agent/work-plans/issue-178/plan.md | Adds the implementation plan documenting scope, approach, and affected files for issue #178. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
src/camp_map/wmts/capabilities.cpp:82
layer.styles.front()is called whendefault_styleis empty, butLayerparsing can yield an emptystylesvector if the capabilities document has no<Style>elements. That would be undefined behavior. Please guard thefront()access so a malformed/partial capabilities document can’t crash the client during URL template assembly.
auto style = layer.default_style;
if(style.isEmpty())
style = layer.styles.front();
// [camp#178 review R2] style is a raw server-controlled value (the
src/camp_map/wmts/capabilities.cpp:95
layer.tile_matrix_set_links.front()is accessed whentile_matrix_setis null, butLayerparsing can yield an emptytile_matrix_set_linksvector if the capabilities document has no<TileMatrixSetLink>. That would be undefined behavior. Please guard thefront()access so a malformed/partial capabilities document can’t crash the client.
// \todo check that passed in tile_matrix_set exists.
if(tile_matrix_set.isNull())
tile_matrix_set = layer.tile_matrix_set_links.front();
// [camp#178 review R2] tile_matrix_set is likewise server-controlled
Comment on lines
+44
to
+45
| const std::string& id = zoom_levels[address.zoomLevel()].id; | ||
| const std::string value = id.empty() ? std::to_string(address.zoomLevel()) : id; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #178
Summary
GeoServer GWC (BlueTopo / nowCOAST) names WMTS tile matrices with gridset-prefixed
identifiers (
EPSG:3857:<z>), butTileLayout::getUrl()substituted{TileMatrix}with the bare numeric zoom index — so the BlueTopo presets got HTTP 400
(
Unknown TILEMATRIX) on every tile.{TileMatrix}now emitszoom_levels[z].id— the identifier alreadyparsed from the WMTS capabilities
<ows:Identifier>— falling back to the barenumeric index when
idis empty, so bare-numeric servers (OSM/XYZ, NOAA ArcGIS)keep working byte-for-byte.
substitution values (TileMatrix id in
getUrl();Style/TileMatrixSetinwmts::Capabilities::getLayout()) are percent-encoded colon-aware(
QUrl::toPercentEncoding(value, ":")—:preserved as load-bearing for GWCids), closing a URL-delimiter injection surface from hostile capabilities
documents. The layer identifier has no discrete seam (fused into the
server-provided resource URL template) and is unchanged.
test/test_wms_url_generation.cpp— bare-numericbackward compat, gridset-prefixed id, empty-id fallback, mismatched
id-over-index, and percent-encoding with colon survival.
Lifecycle (run-issue): issue review → plan → plan review (approve-with-suggestions,
folded in) → implementation → 3 pre-push review rounds (final: approved, 0 must-fix,
Ship: recommended). Full timeline in
.agent/work-plans/issue-178/progress.md.Test plan
./ui_ws/build.sh camp && ./ui_ws/test.sh campon a fully-sourced host env:193 tests, 0 errors, 0 failures, 1 skipped (ADR-0018).
(prefixed URL previously verified to return 200 image/png).
Authored-By:
Claude Code AgentModel:
Claude Fable 5