Skip to content

Commit 2feda5e

Browse files
committed
Add WMTS dimension support with selective enablement
Implement support for WMTS dimensions including time, elevation, and custom dimension types. Features include: - Extract and parse WMTS Dimension elements from capabilities including ISO8601 interval values - Add UI controls (select dropdowns and enable/disable checkboxes) for dimension selection - Implement smart handling of large dimension sets (>200 values) using fixed default values to avoid performance issues - Generate proper MapML map-select elements for enabled dimensions - Handle dimension parameter substitution in both tile and query template URLs - Extract and display WMTS legend URLs from style definitions - Update preview tiles to use dimension default values - Add event listeners to dynamically recreate viewers when dimensions change Dimensions with >200 values are marked as "template" dimensions and use only the default value, while smaller dimensions provide full value selection in both the UI and MapML output.
1 parent 914774c commit 2feda5e

1 file changed

Lines changed: 230 additions & 7 deletions

File tree

src/script/main.js

Lines changed: 230 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -404,10 +404,25 @@ function extractWMTSInfo(xmlDoc, baseUrl) {
404404
const styleName = queryOWS(styleEl, 'Identifier')?.textContent;
405405
const styleTitle = queryOWS(styleEl, 'Title')?.textContent || styleName;
406406
const isDefault = styleEl.getAttribute('isDefault') === 'true';
407-
styles.push({ name: styleName, title: styleTitle, isDefault });
407+
408+
// Extract LegendURL information (WMTS format uses attributes on LegendURL element)
409+
const legendURLs = [];
410+
const legendElements = styleEl.querySelectorAll('LegendURL');
411+
legendElements.forEach(legendEl => {
412+
const href = legendEl.getAttribute('xlink:href') || legendEl.getAttributeNS('http://www.w3.org/1999/xlink', 'href') || '';
413+
const width = legendEl.getAttribute('width');
414+
const height = legendEl.getAttribute('height');
415+
const format = legendEl.getAttribute('format');
416+
417+
if (href) {
418+
legendURLs.push({ width, height, format, href });
419+
}
420+
});
421+
422+
styles.push({ name: styleName, title: styleTitle, isDefault, legendURLs });
408423
});
409424
if (styles.length === 0) {
410-
styles.push({ name: 'default', title: 'Default', isDefault: true });
425+
styles.push({ name: 'default', title: 'Default', isDefault: true, legendURLs: [] });
411426
}
412427

413428
const formats = [];
@@ -418,6 +433,47 @@ function extractWMTSInfo(xmlDoc, baseUrl) {
418433
const infoFormatElements = queryAllOWS(layerEl, 'InfoFormat');
419434
infoFormatElements.forEach(fmt => infoFormats.push(fmt.textContent));
420435

436+
// Extract dimensions
437+
const dimensions = [];
438+
const dimensionElements = queryAllOWS(layerEl, 'Dimension');
439+
dimensionElements.forEach(dimEl => {
440+
const dimName = queryOWS(dimEl, 'Identifier')?.textContent;
441+
const dimDefault = queryOWS(dimEl, 'Default')?.textContent;
442+
const dimUnits = queryOWS(dimEl, 'UOM')?.textContent;
443+
444+
if (dimName) {
445+
// Parse all Value elements
446+
const valueElements = queryAllOWS(dimEl, 'Value');
447+
let allValues = [];
448+
449+
valueElements.forEach(valEl => {
450+
const valContent = valEl.textContent.trim();
451+
if (valContent) {
452+
const parsedValues = parseISO8601Interval(valContent);
453+
allValues = allValues.concat(parsedValues);
454+
}
455+
});
456+
457+
const valueCount = allValues.length;
458+
const usesTemplate = valueCount > 200;
459+
460+
// If using template (>200 values), only store default value
461+
const values = usesTemplate ? [dimDefault || allValues[0]] : allValues;
462+
463+
if (values.length > 0) {
464+
dimensions.push({
465+
name: dimName,
466+
units: dimUnits || '',
467+
default: dimDefault || values[0],
468+
values: values,
469+
valueCount: valueCount,
470+
usesTemplate: usesTemplate
471+
});
472+
console.log('Parsed WMTS dimension:', dimName, 'with', valueCount, 'total values', usesTemplate ? '(using template with default only)' : '(full value list)');
473+
}
474+
}
475+
});
476+
421477
const tmsLinks = [];
422478
const tmsLinkElements = queryAllOWS(layerEl, 'TileMatrixSetLink');
423479
tmsLinkElements.forEach(link => {
@@ -463,7 +519,8 @@ function extractWMTSInfo(xmlDoc, baseUrl) {
463519
resourceURLs,
464520
queryable,
465521
licenseUrl: '',
466-
licenseTitle: ''
522+
licenseTitle: '',
523+
dimensions
467524
});
468525
});
469526

@@ -733,7 +790,15 @@ function displayWMTSInfo(info, source, url) {
733790
const tileResources = layer.resourceURLs['tile'] || [];
734791
const pngResource = tileResources.find(r => r.format && r.format.includes('png')) || tileResources[0];
735792
const tileTemplate = pngResource ? pngResource.template : '';
736-
const previewUrl = buildWMTSTileUrl(tileTemplate, layer, firstTMS ? firstTMS.identifier : '', defaultStyle.name, 'image/png', '2', '1', '1');
793+
let previewUrl = buildWMTSTileUrl(tileTemplate, layer, firstTMS ? firstTMS.identifier : '', defaultStyle.name, 'image/png', '2', '1', '1');
794+
795+
// Replace dimension parameters with default values for preview
796+
if (layer.dimensions && layer.dimensions.length > 0) {
797+
layer.dimensions.forEach(function(dimension) {
798+
const dimPattern = new RegExp('\\{' + dimension.name + '\\}', 'g');
799+
previewUrl = previewUrl.replace(dimPattern, dimension.default);
800+
});
801+
}
737802
const queryResources = layer.resourceURLs['FeatureInfo'] || [];
738803
const hasQuery = layer.queryable && queryResources.length > 0;
739804

@@ -762,9 +827,19 @@ function displayWMTSInfo(info, source, url) {
762827

763828
const formatHtml = layer.formats.length > 0 ? '<div class="format-selector"><label for="img-format-' + index + '">Image Format:</label><select id="img-format-' + index + '" class="format-select">' + formatOptions + '</select></div>' : '';
764829

830+
// Add dimension selectors HTML
831+
const dimensionHtml = (layer.dimensions && layer.dimensions.length > 0) ? layer.dimensions.map((dim, dimIdx) => {
832+
if (dim.usesTemplate) {
833+
return '<div class="dimension-info"><strong>' + dim.name + ':</strong> ' + dim.default + ' <em>(fixed value - ' + dim.valueCount + ' total values)</em></div>';
834+
} else {
835+
const dimOptions = dim.values.map(val => '<option value="' + val + '"' + (val === dim.default ? ' selected' : '') + '>' + val + '</option>').join('');
836+
return '<div class="dimension-selector"><input type="checkbox" id="dim-enabled-' + index + '-' + dimIdx + '" class="dimension-checkbox" data-dimension-name="' + dim.name + '" checked /><label for="dim-' + index + '-' + dimIdx + '">' + dim.name + ':</label><select id="dim-' + index + '-' + dimIdx + '" class="dimension-select" data-dimension-name="' + dim.name + '">' + dimOptions + '</select></div>';
837+
}
838+
}).join('') : '';
839+
765840
const previewHtml = previewUrl ? '<img src="' + previewUrl + '" alt="Preview of ' + layer.title + '" class="layer-preview" id="preview-' + index + '" />' : '<p>No preview available</p>';
766841

767-
return '<div class="layer-item" data-layer-index="' + index + '" data-service-type="WMTS"><div class="layer-controls"><div class="layer-header"><input type="checkbox" id="layer-' + index + '" class="layer-checkbox" /><label for="layer-' + index + '"><strong>' + layer.title + '</strong></label></div><p class="layer-name">Identifier: ' + layer.name + '</p>' + projectionHtml + abstractHtml + '<div class="bounds-selector"><input type="checkbox" id="bounds-' + index + '" class="bounds-checkbox" title="Include layer bounds" checked /><label for="bounds-' + index + '" class="bounds-label">Include Bounds</label></div>' + queryHtml + styleHtml + formatHtml + '</div><div class="layer-viewer-container" id="viewer-container-' + index + '">' + previewHtml + '</div></div>';
842+
return '<div class="layer-item" data-layer-index="' + index + '" data-service-type="WMTS"><div class="layer-controls"><div class="layer-header"><input type="checkbox" id="layer-' + index + '" class="layer-checkbox" /><label for="layer-' + index + '"><strong>' + layer.title + '</strong></label></div><p class="layer-name">Identifier: ' + layer.name + '</p>' + projectionHtml + abstractHtml + '<div class="bounds-selector"><input type="checkbox" id="bounds-' + index + '" class="bounds-checkbox" title="Include layer bounds" checked /><label for="bounds-' + index + '" class="bounds-label">Include Bounds</label></div>' + queryHtml + styleHtml + formatHtml + dimensionHtml + '</div><div class="layer-viewer-container" id="viewer-container-' + index + '">' + previewHtml + '</div></div>';
768843
}).join('');
769844

770845
const supportedCount = Object.values(info.tileMatrixSets).filter(tms => tms.supported).length;
@@ -909,6 +984,62 @@ function displayWMTSInfo(info, source, url) {
909984
});
910985
}
911986
}
987+
988+
// Add event listeners to dimension selectors and checkboxes
989+
if (layer.dimensions && layer.dimensions.length > 0) {
990+
layer.dimensions.forEach((dim, dimIdx) => {
991+
if (!dim.usesTemplate) {
992+
const dimensionSelect = document.getElementById('dim-' + index + '-' + dimIdx);
993+
const dimensionCheckbox = document.getElementById('dim-enabled-' + index + '-' + dimIdx);
994+
995+
if (dimensionSelect) {
996+
dimensionSelect.addEventListener('change', function(e) {
997+
const layerCheckbox = document.getElementById('layer-' + index);
998+
if (layerCheckbox.checked) {
999+
const queryCheckbox = document.getElementById('query-' + index);
1000+
const queryEnabled = queryCheckbox ? queryCheckbox.checked : false;
1001+
const formatSelect = document.getElementById('format-' + index);
1002+
const selectedFormat = formatSelect ? formatSelect.value : (layer.infoFormats[0] || 'text/html');
1003+
const styleSelect = document.getElementById('style-' + index);
1004+
const selectedStyle = styleSelect ? styleSelect.value : (layer.styles[0] ? layer.styles[0].name : 'default');
1005+
const imgFormatSelect = document.getElementById('img-format-' + index);
1006+
const selectedImgFormat = imgFormatSelect ? imgFormatSelect.value : (layer.formats[0] || 'image/png');
1007+
const projectionSelect = document.getElementById('projection-' + index);
1008+
const selectedProjection = projectionSelect ? projectionSelect.value : 'OSMTILE';
1009+
const boundsCheckbox = document.getElementById('bounds-' + index);
1010+
const boundsEnabled = boundsCheckbox ? boundsCheckbox.checked : true;
1011+
// Recreate viewer with new dimension value
1012+
removeViewerForLayer(index);
1013+
createViewerForWMTSLayer(index, layer, info, selectedFormat, queryEnabled, selectedStyle, selectedImgFormat, selectedProjection, boundsEnabled);
1014+
}
1015+
});
1016+
}
1017+
1018+
if (dimensionCheckbox) {
1019+
dimensionCheckbox.addEventListener('change', function(e) {
1020+
const layerCheckbox = document.getElementById('layer-' + index);
1021+
if (layerCheckbox.checked) {
1022+
const queryCheckbox = document.getElementById('query-' + index);
1023+
const queryEnabled = queryCheckbox ? queryCheckbox.checked : false;
1024+
const formatSelect = document.getElementById('format-' + index);
1025+
const selectedFormat = formatSelect ? formatSelect.value : (layer.infoFormats[0] || 'text/html');
1026+
const styleSelect = document.getElementById('style-' + index);
1027+
const selectedStyle = styleSelect ? styleSelect.value : (layer.styles[0] ? layer.styles[0].name : 'default');
1028+
const imgFormatSelect = document.getElementById('img-format-' + index);
1029+
const selectedImgFormat = imgFormatSelect ? imgFormatSelect.value : (layer.formats[0] || 'image/png');
1030+
const projectionSelect = document.getElementById('projection-' + index);
1031+
const selectedProjection = projectionSelect ? projectionSelect.value : 'OSMTILE';
1032+
const boundsCheckbox = document.getElementById('bounds-' + index);
1033+
const boundsEnabled = boundsCheckbox ? boundsCheckbox.checked : true;
1034+
// Recreate viewer when dimension is enabled/disabled
1035+
removeViewerForLayer(index);
1036+
createViewerForWMTSLayer(index, layer, info, selectedFormat, queryEnabled, selectedStyle, selectedImgFormat, selectedProjection, boundsEnabled);
1037+
}
1038+
});
1039+
}
1040+
}
1041+
});
1042+
}
9121043
});
9131044
}
9141045

@@ -1753,14 +1884,14 @@ function createViewerForWMTSLayer(index, layer, serviceInfo, selectedFormat, que
17531884
viewer.appendChild(baseLayer);
17541885
}
17551886

1756-
addWMTSLayerToViewer(viewer, layer, tileMatrixSet, selectedFormat, queryEnabled, styleName, imgFormat, includeBounds);
1887+
addWMTSLayerToViewer(viewer, layer, tileMatrixSet, selectedFormat, queryEnabled, styleName, imgFormat, includeBounds, index);
17571888

17581889
container.appendChild(viewer);
17591890

17601891
console.log('Created WMTS viewer for layer:', layer.name);
17611892
}
17621893

1763-
function addWMTSLayerToViewer(viewer, layer, tileMatrixSet, selectedFormat, queryEnabled, selectedStyle, imageFormat, boundsEnabled) {
1894+
function addWMTSLayerToViewer(viewer, layer, tileMatrixSet, selectedFormat, queryEnabled, selectedStyle, imageFormat, boundsEnabled, layerIndex) {
17641895
const viewerProjection = viewer.getAttribute('projection') || 'OSMTILE';
17651896
const bbox = layer.bbox;
17661897

@@ -1791,6 +1922,29 @@ function addWMTSLayerToViewer(viewer, layer, tileMatrixSet, selectedFormat, quer
17911922
}
17921923
mapLayer.appendChild(licenseLink);
17931924
}
1925+
1926+
// Add legend link for the selected style only (before map-extent)
1927+
if (layer.styles && layer.styles.length > 0 && styleName) {
1928+
const selectedStyle = layer.styles.find(function(style) { return style.name === styleName; });
1929+
if (selectedStyle && selectedStyle.legendURLs && selectedStyle.legendURLs.length > 0) {
1930+
// Only add the first legend URL for the selected style
1931+
const legend = selectedStyle.legendURLs[0];
1932+
const legendLink = document.createElement('map-link');
1933+
legendLink.setAttribute('rel', 'legend');
1934+
legendLink.setAttribute('href', legend.href);
1935+
if (selectedStyle.title) {
1936+
legendLink.setAttribute('title', selectedStyle.title);
1937+
}
1938+
if (legend.width) {
1939+
legendLink.setAttribute('width', legend.width);
1940+
}
1941+
if (legend.height) {
1942+
legendLink.setAttribute('height', legend.height);
1943+
}
1944+
mapLayer.appendChild(legendLink);
1945+
console.log('Added WMTS legend link for selected style:', selectedStyle.title);
1946+
}
1947+
}
17941948

17951949
const mapExtent = document.createElement('map-extent');
17961950
mapExtent.setAttribute('units', viewerProjection);
@@ -1874,6 +2028,46 @@ function addWMTSLayerToViewer(viewer, layer, tileMatrixSet, selectedFormat, quer
18742028

18752029
mapExtent.appendChild(mapSelect);
18762030
}
2031+
2032+
// Add dimension selectors if dimensions are available and not using template
2033+
if (layer.dimensions && layer.dimensions.length > 0) {
2034+
layer.dimensions.forEach(function(dimension, dimIdx) {
2035+
if (!dimension.usesTemplate) {
2036+
// Check if this dimension is enabled in the UI
2037+
const dimensionCheckbox = layerIndex !== undefined ? document.getElementById('dim-enabled-' + layerIndex + '-' + dimIdx) : null;
2038+
const isDimensionEnabled = !dimensionCheckbox || dimensionCheckbox.checked;
2039+
2040+
if (!isDimensionEnabled) {
2041+
console.log('Skipping disabled WMTS dimension:', dimension.name);
2042+
return;
2043+
}
2044+
2045+
// Get the selected value from the UI dropdown
2046+
const dimensionSelect = layerIndex !== undefined ? document.getElementById('dim-' + layerIndex + '-' + dimIdx) : null;
2047+
const selectedValue = dimensionSelect ? dimensionSelect.value : dimension.default;
2048+
2049+
const mapSelect = document.createElement('map-select');
2050+
mapSelect.setAttribute('id', dimension.name + '-selector');
2051+
mapSelect.setAttribute('name', dimension.name);
2052+
2053+
dimension.values.forEach(function(value) {
2054+
const mapOption = document.createElement('map-option');
2055+
mapOption.setAttribute('value', value);
2056+
mapOption.textContent = value;
2057+
2058+
// Mark the selected value from UI
2059+
if (value === selectedValue) {
2060+
mapOption.setAttribute('selected', '');
2061+
}
2062+
2063+
mapSelect.appendChild(mapOption);
2064+
});
2065+
2066+
mapExtent.appendChild(mapSelect);
2067+
console.log('Added WMTS dimension selector for:', dimension.name, 'with', dimension.values.length, 'options');
2068+
}
2069+
});
2070+
}
18772071

18782072
const tileResources = layer.resourceURLs['tile'] || [];
18792073
let tileResource = tileResources.find(function(r) { return r.format === imgFormat; }) || tileResources[0];
@@ -1908,6 +2102,21 @@ function addWMTSLayerToViewer(viewer, layer, tileMatrixSet, selectedFormat, quer
19082102
tref = tref.replace(/{Style}/g, styleName);
19092103
tref = tref.replace(/{style}/g, styleName);
19102104

2105+
// Handle dimension parameters in template URL
2106+
if (layer.dimensions && layer.dimensions.length > 0) {
2107+
layer.dimensions.forEach(function(dimension) {
2108+
const dimPattern = new RegExp('\\{' + dimension.name + '\\}', 'g');
2109+
if (dimension.usesTemplate) {
2110+
// Replace with literal default value for >200 value dimensions
2111+
tref = tref.replace(dimPattern, dimension.default);
2112+
console.log('Replaced WMTS dimension', dimension.name, 'with default value:', dimension.default);
2113+
} else {
2114+
// Keep as template variable for MapML substitution
2115+
tref = tref.replace(dimPattern, '{' + dimension.name + '}');
2116+
}
2117+
});
2118+
}
2119+
19112120
mapLink.setAttribute('tref', tref);
19122121
mapExtent.appendChild(mapLink);
19132122
}
@@ -1948,6 +2157,20 @@ function addWMTSLayerToViewer(viewer, layer, tileMatrixSet, selectedFormat, quer
19482157
qtref = qtref.replace(/{InfoFormat}/g, selectedFormat);
19492158
qtref = qtref.replace(/{infoformat}/g, selectedFormat);
19502159

2160+
// Handle dimension parameters in query template URL
2161+
if (layer.dimensions && layer.dimensions.length > 0) {
2162+
layer.dimensions.forEach(function(dimension) {
2163+
const dimPattern = new RegExp('\\{' + dimension.name + '\\}', 'g');
2164+
if (dimension.usesTemplate) {
2165+
// Replace with literal default value for >200 value dimensions
2166+
qtref = qtref.replace(dimPattern, dimension.default);
2167+
} else {
2168+
// Keep as template variable for MapML substitution
2169+
qtref = qtref.replace(dimPattern, '{' + dimension.name + '}');
2170+
}
2171+
});
2172+
}
2173+
19512174
queryLink.setAttribute('tref', qtref);
19522175
mapExtent.appendChild(queryLink);
19532176
}

0 commit comments

Comments
 (0)