Skip to content

Commit 914774c

Browse files
committed
Fix WMTS TileMatrixSet parsing and support for non-standard identifiers
- Fix TileMatrixSet counting bug that included <TileMatrixSetLink> references as separate TileMatrixSets by skipping elements without <ows:Identifier> - Add support for EPSG:900913 (legacy "Google" code for Web Mercator) by mapping it to OSMTILE projection alongside EPSG:3857 - Add EPSG:4326 validation to verify TileMatrixSet matches WGS84 spec (MatrixWidth=2, MatrixHeight=1 at zoom 0) before mapping to WGS84 - Handle TileMatrix identifiers with prefixes (e.g., "EPSG:900913:0") by detecting common prefix patterns and preserving them in template URLs (e.g., {TileMatrix} becomes "EPSG:900913:{z}" instead of just "{z}") - Extract numeric zoom levels from prefixed TileMatrix identifiers for proper min/max zoom input attributes - Collect MatrixWidth/MatrixHeight from TileMatrix elements for validation These changes enable proper support for GeoServer WMTS services that use non-standard TileMatrix naming conventions and older / non-standard EPSG codes (e.g., EPSG:900913).
1 parent 1dbbd0a commit 914774c

1 file changed

Lines changed: 90 additions & 11 deletions

File tree

src/script/main.js

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -302,13 +302,22 @@ function parseEPSGFromURN(urnString) {
302302
return match ? match[1] : null;
303303
}
304304

305-
function mapTileMatrixSetToProjection(crsCode) {
305+
function mapTileMatrixSetToProjection(crsCode, firstTileMatrix = null) {
306306
const epsgMap = {
307307
'3857': 'OSMTILE',
308+
'900913': 'OSMTILE', // Old "Google" code for Web Mercator
308309
'3978': 'CBMTILE',
309-
'4326': 'WGS84',
310310
'5936': 'APSTILE'
311311
};
312+
313+
// EPSG:4326 requires validation - must have 2x1 tiles at zoom 0 to be WGS84
314+
if (crsCode === '4326' && firstTileMatrix) {
315+
if (firstTileMatrix.matrixWidth === 2 && firstTileMatrix.matrixHeight === 1) {
316+
return 'WGS84';
317+
}
318+
return null; // EPSG:4326 but not WGS84-compliant tiling scheme
319+
}
320+
312321
return epsgMap[crsCode] || null;
313322
}
314323

@@ -336,18 +345,28 @@ function extractWMTSInfo(xmlDoc, baseUrl) {
336345

337346
tmsElements.forEach(tmsEl => {
338347
const identifier = queryOWS(tmsEl, 'Identifier')?.textContent;
348+
// Skip TileMatrixSet elements without an Identifier (these are references, not definitions)
349+
if (!identifier) return;
350+
339351
const crsURN = queryOWS(tmsEl, 'SupportedCRS')?.textContent;
340352
const epsgCode = parseEPSGFromURN(crsURN);
341-
const projection = epsgCode ? mapTileMatrixSetToProjection(epsgCode) : null;
342353

343354
const tileMatrices = [];
344355
const tmElements = queryAllOWS(tmsEl, 'TileMatrix');
345356
tmElements.forEach(tm => {
357+
const matrixWidth = parseInt(queryOWS(tm, 'MatrixWidth')?.textContent);
358+
const matrixHeight = parseInt(queryOWS(tm, 'MatrixHeight')?.textContent);
346359
tileMatrices.push({
347-
identifier: queryOWS(tm, 'Identifier')?.textContent
360+
identifier: queryOWS(tm, 'Identifier')?.textContent,
361+
matrixWidth: matrixWidth || null,
362+
matrixHeight: matrixHeight || null
348363
});
349364
});
350365

366+
// Pass first TileMatrix for validation (especially for EPSG:4326)
367+
const firstTileMatrix = tileMatrices.length > 0 ? tileMatrices[0] : null;
368+
const projection = epsgCode ? mapTileMatrixSetToProjection(epsgCode, firstTileMatrix) : null;
369+
351370
tileMatrixSets[identifier] = {
352371
identifier,
353372
crs: crsURN,
@@ -750,7 +769,9 @@ function displayWMTSInfo(info, source, url) {
750769

751770
const supportedCount = Object.values(info.tileMatrixSets).filter(tms => tms.supported).length;
752771

753-
serviceDetails.innerHTML = sourceNote + '<p><strong>Title:</strong> ' + info.title + ' ' + serviceTypeBadge + '</p><p><strong>Version:</strong> ' + info.version + '</p><p><strong>TileMatrixSets:</strong> ' + Object.keys(info.tileMatrixSets).length + ' (' + supportedCount + ' supported)</p><details class="service-abstract"><summary><strong>Abstract</strong></summary><p>' + info.abstract + '</p></details><h3>Available Layers (' + info.layers.length + ')</h3><div class="layers-list">' + layersList + '</div>';
772+
const urlNote = source !== 'file' ? '<p><strong>Loaded URL:</strong> <a href="' + url + '" target="_blank" rel="noopener noreferrer">' + url + '</a></p>' : '';
773+
774+
serviceDetails.innerHTML = sourceNote + '<p><strong>Title:</strong> ' + info.title + ' ' + serviceTypeBadge + '</p><p><strong>Version:</strong> ' + info.version + '</p><p><strong>TileMatrixSets:</strong> ' + Object.keys(info.tileMatrixSets).length + ' (' + supportedCount + ' supported)</p><details class="service-abstract"><summary><strong>Abstract</strong></summary><p>' + info.abstract + '</p></details>' + urlNote + '<h3>Available Layers (' + info.layers.length + ')</h3><div class="layers-list">' + layersList + '</div>';
754775

755776
serviceInfo.classList.remove('hidden');
756777

@@ -895,6 +916,7 @@ function displayServiceInfo(info, source, loadedUrl) {
895916
const sourceNote = source === 'file'
896917
? '<p><em>(Loaded from file)</em></p>'
897918
: '';
919+
const serviceTypeBadge = '<span class="service-type-badge" style="background: #2196F3; color: white; padding: 2px 8px; border-radius: 3px; font-size: 0.9em; margin-left: 10px;">WMS</span>';
898920

899921
const formatOptions = info.getFeatureInfoFormats.map(fmt =>
900922
`<option value="${fmt}">${fmt}</option>`
@@ -979,7 +1001,7 @@ function displayServiceInfo(info, source, loadedUrl) {
9791001

9801002
serviceDetails.innerHTML = `
9811003
${sourceNote}
982-
<p><strong>Title:</strong> ${info.title}</p>
1004+
<p><strong>Title:</strong> ${info.title} ${serviceTypeBadge}</p>
9831005
<p><strong>Version:</strong> ${info.version}</p>
9841006
<details class="service-abstract">
9851007
<summary><strong>Abstract</strong></summary>
@@ -1255,8 +1277,13 @@ function createQueryLink(layer, version, projectionCode, infoFormat, layerIndex,
12551277
tref += `&INFO_FORMAT=${encodeURIComponent(infoFormat)}`;
12561278

12571279
// Add STYLES parameter if a style is selected
1280+
// Use empty STYLES= for 'default-' prefixed styles to work around GeoServer LayerGroup issues
12581281
if (styleName) {
1259-
tref += '&STYLES={style}';
1282+
if (styleName.toLowerCase().startsWith('default-')) {
1283+
tref += '&STYLES=';
1284+
} else {
1285+
tref += '&STYLES={style}';
1286+
}
12601287
}
12611288

12621289
// Add dimension parameters to query link only if enabled in UI
@@ -1769,8 +1796,27 @@ function addWMTSLayerToViewer(viewer, layer, tileMatrixSet, selectedFormat, quer
17691796
mapExtent.setAttribute('units', viewerProjection);
17701797
mapExtent.setAttribute('checked', '');
17711798

1772-
const minZoom = tileMatrixSet.tileMatrices.length > 0 ? tileMatrixSet.tileMatrices[0].identifier : '0';
1773-
const maxZoom = tileMatrixSet.tileMatrices.length > 0 ? tileMatrixSet.tileMatrices[tileMatrixSet.tileMatrices.length - 1].identifier : '18';
1799+
// Extract zoom levels from TileMatrix identifiers, handling prefixes like "EPSG:900913:0"
1800+
let minZoom = '0';
1801+
let maxZoom = '18';
1802+
1803+
if (tileMatrixSet.tileMatrices.length > 0) {
1804+
const firstId = tileMatrixSet.tileMatrices[0].identifier;
1805+
const lastId = tileMatrixSet.tileMatrices[tileMatrixSet.tileMatrices.length - 1].identifier;
1806+
1807+
// Try to extract numeric suffix from identifiers like "EPSG:900913:0"
1808+
const firstMatch = firstId ? firstId.match(/:(\d+)$/) : null;
1809+
const lastMatch = lastId ? lastId.match(/:(\d+)$/) : null;
1810+
1811+
if (firstMatch && lastMatch) {
1812+
minZoom = firstMatch[1];
1813+
maxZoom = lastMatch[1];
1814+
} else {
1815+
// Use the identifiers as-is if they're already numeric
1816+
minZoom = firstId || '0';
1817+
maxZoom = lastId || '18';
1818+
}
1819+
}
17741820

17751821
const zoomInput = document.createElement('map-input');
17761822
zoomInput.setAttribute('name', 'z');
@@ -1838,7 +1884,25 @@ function addWMTSLayerToViewer(viewer, layer, tileMatrixSet, selectedFormat, quer
18381884

18391885
let tref = tileResource.template;
18401886
tref = tref.replace(/{TileMatrixSet}/g, tileMatrixSet.identifier);
1841-
tref = tref.replace(/{TileMatrix}/g, '{z}');
1887+
1888+
// Handle TileMatrix identifiers that may have a prefix (e.g., "EPSG:900913:0")
1889+
// Extract prefix if all TileMatrix identifiers follow a "prefix:number" pattern
1890+
let tileMatrixReplacement = '{z}';
1891+
if (tileMatrixSet.tileMatrices && tileMatrixSet.tileMatrices.length > 0) {
1892+
const firstId = tileMatrixSet.tileMatrices[0].identifier;
1893+
const lastId = tileMatrixSet.tileMatrices[tileMatrixSet.tileMatrices.length - 1].identifier;
1894+
1895+
// Check if identifiers end with numbers and have a common prefix
1896+
const firstMatch = firstId ? firstId.match(/^(.+):(\d+)$/) : null;
1897+
const lastMatch = lastId ? lastId.match(/^(.+):(\d+)$/) : null;
1898+
1899+
if (firstMatch && lastMatch && firstMatch[1] === lastMatch[1]) {
1900+
// All TileMatrix identifiers share the same prefix
1901+
tileMatrixReplacement = firstMatch[1] + ':{z}';
1902+
}
1903+
}
1904+
1905+
tref = tref.replace(/{TileMatrix}/g, tileMatrixReplacement);
18421906
tref = tref.replace(/{TileRow}/g, '{y}');
18431907
tref = tref.replace(/{TileCol}/g, '{x}');
18441908
tref = tref.replace(/{Style}/g, styleName);
@@ -1859,7 +1923,22 @@ function addWMTSLayerToViewer(viewer, layer, tileMatrixSet, selectedFormat, quer
18591923

18601924
let qtref = queryResource.template;
18611925
qtref = qtref.replace(/{TileMatrixSet}/g, tileMatrixSet.identifier);
1862-
qtref = qtref.replace(/{TileMatrix}/g, '{z}');
1926+
1927+
// Handle TileMatrix identifiers that may have a prefix (e.g., "EPSG:900913:0")
1928+
let tileMatrixReplacement = '{z}';
1929+
if (tileMatrixSet.tileMatrices && tileMatrixSet.tileMatrices.length > 0) {
1930+
const firstId = tileMatrixSet.tileMatrices[0].identifier;
1931+
const lastId = tileMatrixSet.tileMatrices[tileMatrixSet.tileMatrices.length - 1].identifier;
1932+
1933+
const firstMatch = firstId ? firstId.match(/^(.+):(\d+)$/) : null;
1934+
const lastMatch = lastId ? lastId.match(/^(.+):(\d+)$/) : null;
1935+
1936+
if (firstMatch && lastMatch && firstMatch[1] === lastMatch[1]) {
1937+
tileMatrixReplacement = firstMatch[1] + ':{z}';
1938+
}
1939+
}
1940+
1941+
qtref = qtref.replace(/{TileMatrix}/g, tileMatrixReplacement);
18631942
qtref = qtref.replace(/{TileRow}/g, '{y}');
18641943
qtref = qtref.replace(/{TileCol}/g, '{x}');
18651944
qtref = qtref.replace(/{Style}/g, styleName);

0 commit comments

Comments
 (0)