ogm-viewer 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/{lerc-IU-tWkVO.js → lerc-DQmE0Pfk.js} +1 -1
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/{ogm-alerts.ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-preview.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-BAGJpqMB.js → ogm-alerts.ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-preview.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-B_nGMI1e.js} +12000 -11333
- package/dist/cjs/ogm-alerts_11.cjs.entry.js +1 -1
- package/dist/cjs/ogm-viewer.cjs.js +1 -1
- package/dist/collection/components/ogm-attributes/ogm-attributes.js +1 -1
- package/dist/collection/components/ogm-attributes/ogm-attributes.test.js +24 -0
- package/dist/collection/components/ogm-image/ogm-image.js +14 -14
- package/dist/collection/components/ogm-map/ogm-map.js +135 -53
- package/dist/collection/components/ogm-preview/ogm-preview.js +13 -13
- package/dist/collection/components/ogm-preview/ogm-preview.test.js +5 -5
- package/dist/collection/components/ogm-previews/ogm-previews.js +29 -26
- package/dist/collection/components/ogm-previews/ogm-previews.test.js +1 -1
- package/dist/collection/lib/geometry.js +35 -1
- package/dist/collection/lib/geometry.test.js +90 -1
- package/dist/collection/lib/previewers/cog-deck.js +4 -4
- package/dist/collection/lib/previewers/cog.js +15 -11
- package/dist/collection/lib/previewers/cog.test.js +61 -0
- package/dist/collection/lib/previewers/geojson.js +15 -7
- package/dist/collection/lib/previewers/geojson.test.js +81 -0
- package/dist/collection/lib/previewers/map.js +51 -0
- package/dist/collection/lib/previewers/openindexmap.js +2 -2
- package/dist/collection/lib/previewers/pmtiles-raster.js +13 -9
- package/dist/collection/lib/previewers/pmtiles-vector.js +12 -51
- package/dist/collection/lib/previewers/pmtiles.test.js +62 -0
- package/dist/collection/lib/previewers/previewer.js +4 -3
- package/dist/collection/lib/previewers/raster.js +15 -11
- package/dist/collection/lib/previewers/tiled-vector.js +47 -0
- package/dist/collection/lib/previewers/tilejson-raster.js +27 -0
- package/dist/collection/lib/previewers/tilejson-vector.js +18 -0
- package/dist/collection/lib/previewers/tilejson.test.js +140 -0
- package/dist/collection/lib/previewers/vector.js +7 -4
- package/dist/collection/lib/previewers/wms.js +86 -6
- package/dist/collection/lib/previewers/wms.test.js +133 -0
- package/dist/collection/lib/previewers/wmts.js +31 -0
- package/dist/collection/lib/previewers/wmts.test.js +108 -0
- package/dist/collection/lib/{sources → resources}/cog.js +2 -2
- package/dist/collection/lib/{sources → resources}/geojson.js +3 -3
- package/dist/collection/lib/{sources → resources}/iiif-manifest.js +2 -2
- package/dist/collection/lib/{sources → resources}/iiif-manifest.test.js +3 -3
- package/dist/collection/lib/{sources → resources}/iiif.js +2 -2
- package/dist/collection/lib/{sources/maplibre.js → resources/map.js} +2 -2
- package/dist/collection/lib/{sources → resources}/openindexmap.js +2 -2
- package/dist/collection/lib/{sources → resources}/pmtiles.js +2 -2
- package/dist/collection/lib/{sources → resources}/raster.js +2 -2
- package/dist/collection/lib/{sources/source.js → resources/resource.js} +3 -3
- package/dist/collection/lib/{sources → resources}/tilejson.js +21 -4
- package/dist/collection/lib/{sources → resources}/tms.js +2 -2
- package/dist/collection/lib/{sources → resources}/vector.js +2 -2
- package/dist/collection/lib/{sources → resources}/wms.js +46 -6
- package/dist/collection/lib/resources/wms.test.js +63 -0
- package/dist/collection/lib/resources/wmts.js +317 -0
- package/dist/collection/lib/resources/wmts.test.js +235 -0
- package/dist/collection/lib/{sources → resources}/xyz.js +2 -2
- package/dist/esm/{lerc-yC3LNu-h.js → lerc-BQHa9iiK.js} +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/esm/{ogm-alerts.ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-preview.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-BpVupgSB.js → ogm-alerts.ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-preview.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-DSguoh85.js} +12000 -11333
- package/dist/esm/ogm-alerts_11.entry.js +1 -1
- package/dist/esm/ogm-viewer.js +1 -1
- package/dist/ogm-viewer/ogm-viewer.esm.js +1 -1
- package/dist/ogm-viewer/p-667JHO1P.js +2822 -0
- package/dist/ogm-viewer/{p-_eTjrSO2.js → p-9T5uP6M8.js} +1 -1
- package/dist/ogm-viewer/{p-11cf948e.entry.js → p-c95acce0.entry.js} +1 -1
- package/dist/types/components/ogm-image/ogm-image.d.ts +2 -2
- package/dist/types/components/ogm-map/ogm-map.d.ts +12 -9
- package/dist/types/components/ogm-preview/ogm-preview.d.ts +2 -2
- package/dist/types/components/ogm-previews/ogm-previews.d.ts +2 -2
- package/dist/types/components.d.ts +6 -6
- package/dist/types/lib/geometry.d.ts +5 -1
- package/dist/types/lib/previewers/cog-deck.d.ts +3 -3
- package/dist/types/lib/previewers/cog.d.ts +6 -5
- package/dist/types/lib/previewers/cog.test.d.ts +1 -0
- package/dist/types/lib/previewers/geojson.d.ts +8 -4
- package/dist/types/lib/previewers/geojson.test.d.ts +1 -0
- package/dist/types/lib/previewers/{maplibre.d.ts → map.d.ts} +10 -7
- package/dist/types/lib/previewers/openindexmap.d.ts +2 -2
- package/dist/types/lib/previewers/pmtiles-raster.d.ts +2 -2
- package/dist/types/lib/previewers/pmtiles-vector.d.ts +4 -11
- package/dist/types/lib/previewers/pmtiles.test.d.ts +1 -0
- package/dist/types/lib/previewers/previewer.d.ts +3 -3
- package/dist/types/lib/previewers/raster.d.ts +10 -7
- package/dist/types/lib/previewers/tiled-vector.d.ts +11 -0
- package/dist/types/lib/previewers/tilejson-raster.d.ts +8 -0
- package/dist/types/lib/previewers/tilejson-vector.d.ts +6 -0
- package/dist/types/lib/previewers/tilejson.test.d.ts +1 -0
- package/dist/types/lib/previewers/vector.d.ts +9 -5
- package/dist/types/lib/previewers/wms.d.ts +17 -5
- package/dist/types/lib/previewers/wms.test.d.ts +1 -0
- package/dist/types/lib/previewers/wmts.d.ts +11 -0
- package/dist/types/lib/previewers/wmts.test.d.ts +1 -0
- package/dist/types/lib/{sources → resources}/cog.d.ts +2 -2
- package/dist/types/lib/{sources → resources}/geojson.d.ts +2 -2
- package/dist/types/lib/{sources → resources}/iiif-manifest.d.ts +2 -2
- package/dist/types/lib/resources/iiif-manifest.test.d.ts +1 -0
- package/dist/types/lib/resources/iiif.d.ts +5 -0
- package/dist/types/lib/{sources/maplibre.d.ts → resources/map.d.ts} +2 -2
- package/dist/types/lib/resources/openindexmap.d.ts +5 -0
- package/dist/types/lib/{sources → resources}/pmtiles.d.ts +2 -2
- package/dist/types/lib/{sources → resources}/raster.d.ts +2 -2
- package/dist/types/lib/{sources/source.d.ts → resources/resource.d.ts} +1 -1
- package/dist/types/lib/{sources → resources}/tilejson.d.ts +5 -2
- package/dist/types/lib/resources/tms.d.ts +5 -0
- package/dist/types/lib/{sources → resources}/vector.d.ts +2 -2
- package/dist/types/lib/{sources → resources}/wms.d.ts +15 -5
- package/dist/types/lib/resources/wms.test.d.ts +1 -0
- package/dist/types/lib/resources/wmts.d.ts +59 -0
- package/dist/types/lib/resources/wmts.test.d.ts +1 -0
- package/dist/types/lib/resources/xyz.d.ts +5 -0
- package/package.json +1 -1
- package/readme.md +1 -1
- package/dist/collection/lib/previewers/maplibre.js +0 -56
- package/dist/ogm-viewer/p-DPOGJxir.js +0 -2822
- package/dist/types/lib/sources/iiif.d.ts +0 -5
- package/dist/types/lib/sources/openindexmap.d.ts +0 -5
- package/dist/types/lib/sources/tms.d.ts +0 -5
- package/dist/types/lib/sources/xyz.d.ts +0 -5
- /package/dist/types/{lib/sources/iiif-manifest.test.d.ts → components/ogm-attributes/ogm-attributes.test.d.ts} +0 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { LngLatBounds } from "maplibre-gl";
|
|
2
|
+
import RasterResource from "./raster";
|
|
3
|
+
// Half the circumference of the Web Mercator world in meters. The XYZ grid hangs from
|
|
4
|
+
// (-XYZ_ORIGIN, XYZ_ORIGIN), the northwest corner of the world.
|
|
5
|
+
const XYZ_ORIGIN = 20037508.342789244;
|
|
6
|
+
// How far a set's stated origin may sit from that corner and still count as the same grid.
|
|
7
|
+
// Services round the value to varying precision; half a meter is far below one pixel at any
|
|
8
|
+
// zoom a WMTS publishes.
|
|
9
|
+
const ORIGIN_TOLERANCE = 0.5;
|
|
10
|
+
// Codes that all mean Web Mercator: the EPSG one, the deprecated OGC one, and the ESRI one
|
|
11
|
+
const WEB_MERCATOR_CODES = ['3857', '900913', '102100'];
|
|
12
|
+
// The latitude the Web Mercator world stops at. A layer may claim an extent that runs to the
|
|
13
|
+
// pole, but there is no map there to bound it against.
|
|
14
|
+
const MERCATOR_MAX_LATITUDE = 85.051129;
|
|
15
|
+
// Layers (potentially multiple) accessed via WMTS GetTile requests
|
|
16
|
+
// NOTE: in Aardvark, the reference URL is the GetCapabilities URL, not the tile URL
|
|
17
|
+
export default class WmtsResource extends RasterResource {
|
|
18
|
+
options;
|
|
19
|
+
// Memoized metadata via GetCapabilities request
|
|
20
|
+
metadata;
|
|
21
|
+
constructor(id, url, options, bounds) {
|
|
22
|
+
super(id, url, bounds);
|
|
23
|
+
this.options = options;
|
|
24
|
+
// Assume we're using one layer with the given ID if no layer IDs are provided
|
|
25
|
+
if (!this.options.layerIds || this.options.layerIds.length === 0) {
|
|
26
|
+
this.options.layerIds = [id];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
label() {
|
|
30
|
+
return 'Web Map Tile Service (WMTS)';
|
|
31
|
+
}
|
|
32
|
+
// Fetch and memoize WMTS GetCapabilities XML document
|
|
33
|
+
async getMetadata() {
|
|
34
|
+
if (!this.metadata) {
|
|
35
|
+
const resp = await fetch(this.url);
|
|
36
|
+
const text = await resp.text();
|
|
37
|
+
this.metadata = new DOMParser().parseFromString(text, 'application/xml');
|
|
38
|
+
}
|
|
39
|
+
return this.metadata;
|
|
40
|
+
}
|
|
41
|
+
// Build a spec for each layer we were asked for. Layers published only in tile grids
|
|
42
|
+
// MapLibre can't draw are dropped, and if that leaves nothing we raise rather than
|
|
43
|
+
// hand back an empty preview.
|
|
44
|
+
async getLayers() {
|
|
45
|
+
const metadata = await this.getMetadata();
|
|
46
|
+
const tileMatrixSets = this.getTileMatrixSets(metadata);
|
|
47
|
+
// Narrow to the requested layers before building anything: a service like NASA GIBS
|
|
48
|
+
// lists close to a thousand, and each one costs a walk of its subtree.
|
|
49
|
+
const elements = Array.from(metadata.getElementsByTagName('Layer')).filter(element => this.isRequested(element));
|
|
50
|
+
if (elements.length === 0)
|
|
51
|
+
throw new Error(`This service doesn't publish a layer named ${this.options.layerIds.join(', ')}.`);
|
|
52
|
+
const layers = elements.map(element => this.createLayer(element, tileMatrixSets)).filter(layer => layer !== undefined);
|
|
53
|
+
if (layers.length === 0)
|
|
54
|
+
throw new Error(this.unsupportedGridMessage(elements, tileMatrixSets));
|
|
55
|
+
return layers;
|
|
56
|
+
}
|
|
57
|
+
// Whether a <Layer> element is one of the layers this resource was constructed for.
|
|
58
|
+
// An empty list means every layer in the document, which can be a lot of them.
|
|
59
|
+
isRequested(element) {
|
|
60
|
+
if (this.options.layerIds.length === 0)
|
|
61
|
+
return true;
|
|
62
|
+
const layerId = element.getElementsByTagName('ows:Identifier')[0]?.textContent?.trim();
|
|
63
|
+
return this.options.layerIds.includes(layerId ?? '');
|
|
64
|
+
}
|
|
65
|
+
// Create a WmtsLayer spec from a <Layer> element in the GetCapabilities document, or
|
|
66
|
+
// undefined if none of the grids it's published in can be drawn
|
|
67
|
+
createLayer(element, tileMatrixSets) {
|
|
68
|
+
// Get the identifier and title of the layer
|
|
69
|
+
const layerId = element.getElementsByTagName('ows:Identifier')[0]?.textContent;
|
|
70
|
+
const title = element.getElementsByTagName('ows:Title')[0]?.textContent;
|
|
71
|
+
// Get the identifier of the style with attribute isDefault="true", or the first style if none are marked as default
|
|
72
|
+
const styles = Array.from(element.getElementsByTagName('Style'));
|
|
73
|
+
const defaultStyle = styles.find(style => style.getAttribute('isDefault') === 'true');
|
|
74
|
+
const style = defaultStyle ?? styles[0];
|
|
75
|
+
const styleId = style?.getElementsByTagName('ows:Identifier')[0]?.textContent?.trim();
|
|
76
|
+
// Use the first grid the layer offers that MapLibre can actually draw, which is not
|
|
77
|
+
// necessarily the first one listed: services often lead with a national grid
|
|
78
|
+
const tileMatrixSet = this.getLinkedTileMatrixSets(element, tileMatrixSets).find(set => set.grid);
|
|
79
|
+
const grid = tileMatrixSet?.grid;
|
|
80
|
+
if (!tileMatrixSet || !grid)
|
|
81
|
+
return undefined;
|
|
82
|
+
// Get the default values for all dimensions listed for the layer
|
|
83
|
+
// TODO: actually support adjusting these in the previewer?
|
|
84
|
+
const dimensions = Array.from(element.getElementsByTagName('Dimension'));
|
|
85
|
+
const dimensionDefaults = dimensions.reduce((acc, dimension) => {
|
|
86
|
+
const name = dimension.getElementsByTagName('ows:Identifier')[0]?.textContent?.trim();
|
|
87
|
+
const defaultValue = dimension.getElementsByTagName('Default')[0]?.textContent?.trim();
|
|
88
|
+
if (name && defaultValue) {
|
|
89
|
+
acc[name] = defaultValue;
|
|
90
|
+
}
|
|
91
|
+
return acc;
|
|
92
|
+
}, {});
|
|
93
|
+
// For each tiled ResourceURL, reformat as XYZ-style URL
|
|
94
|
+
// Each MapLibre layer will be tied to a MapLibre source with multiple tile URLs
|
|
95
|
+
const templates = Array.from(element.getElementsByTagName('ResourceURL'))
|
|
96
|
+
.filter(resourceUrl => resourceUrl.getAttribute('resourceType') === 'tile')
|
|
97
|
+
.map(resourceUrl => resourceUrl.getAttribute('template') ?? '');
|
|
98
|
+
const tileUrls = this.preferKnownHost(templates).map(template => this.formatTileUrl(template, styleId, tileMatrixSet, dimensionDefaults));
|
|
99
|
+
const bounds = this.parseBounds(element) ?? this.recordBounds();
|
|
100
|
+
return {
|
|
101
|
+
id: layerId,
|
|
102
|
+
title: title,
|
|
103
|
+
tileUrls: tileUrls,
|
|
104
|
+
tileSize: this.options.tileSize ?? grid.tileSize,
|
|
105
|
+
minzoom: clamp(this.boundsMinzoom(bounds), grid.minzoom, grid.maxzoom),
|
|
106
|
+
maxzoom: grid.maxzoom,
|
|
107
|
+
bounds: bounds,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// The extent the layer covers, from its <ows:WGS84BoundingBox>. OWS writes both corners as
|
|
111
|
+
// "longitude latitude" in CRS84 whatever axis order the layer's own CRS uses, so the numbers
|
|
112
|
+
// come out in MapLibre's order as they are read.
|
|
113
|
+
parseBounds(element) {
|
|
114
|
+
const box = element.getElementsByTagName('ows:WGS84BoundingBox')[0];
|
|
115
|
+
if (!box)
|
|
116
|
+
return undefined;
|
|
117
|
+
const [west, south] = numericCorner(box, 'ows:LowerCorner');
|
|
118
|
+
const [east, north] = numericCorner(box, 'ows:UpperCorner');
|
|
119
|
+
if ([west, south, east, north].some(value => !Number.isFinite(value)))
|
|
120
|
+
return undefined;
|
|
121
|
+
return [west, south, east, north];
|
|
122
|
+
}
|
|
123
|
+
// The extent the record claims, for a service that publishes none of its own. Aardvark states
|
|
124
|
+
// it per record rather than per layer, so it's the coarser of the two, but it still keeps us
|
|
125
|
+
// from treating a city as if it covered the world.
|
|
126
|
+
recordBounds() {
|
|
127
|
+
if (!this.bounds)
|
|
128
|
+
return undefined;
|
|
129
|
+
const bounds = LngLatBounds.convert(this.bounds);
|
|
130
|
+
return [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()];
|
|
131
|
+
}
|
|
132
|
+
// The first zoom at which a single tile no longer dwarfs the layer. Below it every tile we
|
|
133
|
+
// could ask for lies mostly outside the layer, and a service that draws that emptiness
|
|
134
|
+
// instead of answering 404 - Vienna's orthophoto pads it with opaque white, JPEG having no
|
|
135
|
+
// alpha channel to leave it out with - spreads the padding across the map: at zoom 1 the one
|
|
136
|
+
// tile that touches Vienna covers a quarter of the world. Bounds alone don't help, since
|
|
137
|
+
// MapLibre keeps any tile that so much as intersects them. A tile spans 1/2^z of the world
|
|
138
|
+
// on each axis, so the layer first fills one when 2^z reaches 1 over its widest extent.
|
|
139
|
+
boundsMinzoom(bounds) {
|
|
140
|
+
if (!bounds)
|
|
141
|
+
return 0;
|
|
142
|
+
const [west, south, east, north] = bounds;
|
|
143
|
+
const width = east < west ? east - west + 360 : east - west;
|
|
144
|
+
const extent = Math.max(width / 360, mercatorY(south) - mercatorY(north));
|
|
145
|
+
// An extent of nothing is a layer we can never fit a tile to; let the caller's clamp put it
|
|
146
|
+
// at the deepest zoom the grid has rather than the shallowest
|
|
147
|
+
if (!(extent > 0))
|
|
148
|
+
return Infinity;
|
|
149
|
+
return Math.ceil(Math.log2(1 / extent));
|
|
150
|
+
}
|
|
151
|
+
// Narrow the tile templates to the host we know answers. Services list several hosts to
|
|
152
|
+
// shard requests across, but MapLibre assigns each tile to one of them by coordinate -
|
|
153
|
+
// urls[(x + y) % urls.length] - and never retries elsewhere, so one stale hostname
|
|
154
|
+
// silently costs that share of the tiles. The host that served the capabilities document
|
|
155
|
+
// is the one we have evidence for: it resolved, answered, and allowed the cross-origin
|
|
156
|
+
// read. Where any template is on that host we keep only those, and upgrade them to https
|
|
157
|
+
// if that's how we reached it, since a document served over https can still advertise its
|
|
158
|
+
// tiles over http - which the browser then blocks as mixed content. Sharding buys little
|
|
159
|
+
// over HTTP/2 anyway, where the whole point of it, the per-origin connection limit, is gone.
|
|
160
|
+
preferKnownHost(templates) {
|
|
161
|
+
const capabilities = new URL(this.url);
|
|
162
|
+
const known = templates.filter(template => hostOf(template) === capabilities.host);
|
|
163
|
+
if (known.length === 0)
|
|
164
|
+
return templates;
|
|
165
|
+
if (capabilities.protocol !== 'https:')
|
|
166
|
+
return known;
|
|
167
|
+
return known.map(template => template.replace(/^http:/, 'https:'));
|
|
168
|
+
}
|
|
169
|
+
// The tile matrix sets a layer links to, in the order it lists them. Links naming a set
|
|
170
|
+
// the document never defines are skipped.
|
|
171
|
+
getLinkedTileMatrixSets(element, tileMatrixSets) {
|
|
172
|
+
return Array.from(element.getElementsByTagName('TileMatrixSetLink'))
|
|
173
|
+
.map(link => link.getElementsByTagName('TileMatrixSet')[0]?.textContent?.trim())
|
|
174
|
+
.map(id => (id ? tileMatrixSets.get(id) : undefined))
|
|
175
|
+
.filter(set => set !== undefined);
|
|
176
|
+
}
|
|
177
|
+
// Every <TileMatrixSet> definition in the document, keyed by identifier. Each layer also
|
|
178
|
+
// nests a <TileMatrixSet> inside its <TileMatrixSetLink> elements, but those hold only an
|
|
179
|
+
// identifier; the definitions are the ones with <TileMatrix> children.
|
|
180
|
+
getTileMatrixSets(metadata) {
|
|
181
|
+
const sets = Array.from(metadata.getElementsByTagName('TileMatrixSet'))
|
|
182
|
+
.filter(element => element.getElementsByTagName('TileMatrix').length > 0)
|
|
183
|
+
.map(element => this.parseTileMatrixSet(element))
|
|
184
|
+
.filter(set => set !== undefined);
|
|
185
|
+
return new Map(sets.map(set => [set.id, set]));
|
|
186
|
+
}
|
|
187
|
+
// Read a <TileMatrixSet> definition, working out whether MapLibre can draw from it
|
|
188
|
+
parseTileMatrixSet(element) {
|
|
189
|
+
const id = element.getElementsByTagName('ows:Identifier')[0]?.textContent?.trim();
|
|
190
|
+
const crs = element.getElementsByTagName('ows:SupportedCRS')[0]?.textContent?.trim();
|
|
191
|
+
if (!id || !crs)
|
|
192
|
+
return undefined;
|
|
193
|
+
return { id, crs, grid: this.parseXyzGrid(element, crs) };
|
|
194
|
+
}
|
|
195
|
+
// Reduce a tile matrix set to the numbers MapLibre needs, or undefined if its geometry
|
|
196
|
+
// isn't the XYZ grid. MapLibre draws raster tiles from that one grid only: Web Mercator,
|
|
197
|
+
// hung from the northwest corner of the world, 2^z square tiles on a side at zoom z. Tile
|
|
198
|
+
// indices in any other grid address different ground - a geographic (EPSG:4326) grid, for
|
|
199
|
+
// instance, spaces its rows evenly in latitude and is twice as wide as it is tall - so
|
|
200
|
+
// feeding them to MapLibre draws the wrong place, or falls off the edge of the matrix.
|
|
201
|
+
parseXyzGrid(element, crs) {
|
|
202
|
+
if (!isWebMercator(crs))
|
|
203
|
+
return undefined;
|
|
204
|
+
const elements = Array.from(element.getElementsByTagName('TileMatrix'));
|
|
205
|
+
const levels = elements.map(matrix => this.parseTileMatrix(matrix)).filter(level => level !== undefined);
|
|
206
|
+
if (levels.length === 0 || levels.length !== elements.length)
|
|
207
|
+
return undefined;
|
|
208
|
+
// Every level has to agree on tile size and on how it spells its identifier, and each
|
|
209
|
+
// has to hold exactly the tiles that its zoom of the XYZ grid holds
|
|
210
|
+
const [first] = levels;
|
|
211
|
+
if (levels.some(level => level.tileWidth !== first.tileWidth || level.prefix !== first.prefix))
|
|
212
|
+
return undefined;
|
|
213
|
+
if (levels.some(level => !isXyzLevel(level)))
|
|
214
|
+
return undefined;
|
|
215
|
+
const zooms = levels.map(level => level.zoom);
|
|
216
|
+
return {
|
|
217
|
+
tileSize: first.tileWidth,
|
|
218
|
+
levelPrefix: first.prefix,
|
|
219
|
+
minzoom: Math.min(...zooms),
|
|
220
|
+
maxzoom: Math.max(...zooms),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// Read one <TileMatrix>, or undefined if its identifier doesn't end in a zoom level
|
|
224
|
+
parseTileMatrix(element) {
|
|
225
|
+
const identifier = element.getElementsByTagName('ows:Identifier')[0]?.textContent?.trim() ?? '';
|
|
226
|
+
const level = identifier.match(/^(.*[^0-9])?([0-9]+)$/);
|
|
227
|
+
if (!level)
|
|
228
|
+
return undefined;
|
|
229
|
+
// MapLibre writes the zoom into {z} in plain decimal, so a level spelled any other way
|
|
230
|
+
// (zero-padded, say) would give us a URL the service doesn't answer to
|
|
231
|
+
if (String(Number(level[2])) !== level[2])
|
|
232
|
+
return undefined;
|
|
233
|
+
return {
|
|
234
|
+
zoom: Number(level[2]),
|
|
235
|
+
prefix: level[1] ?? '',
|
|
236
|
+
tileWidth: numericChild(element, 'TileWidth'),
|
|
237
|
+
tileHeight: numericChild(element, 'TileHeight'),
|
|
238
|
+
matrixWidth: numericChild(element, 'MatrixWidth'),
|
|
239
|
+
matrixHeight: numericChild(element, 'MatrixHeight'),
|
|
240
|
+
topLeft: (element.getElementsByTagName('TopLeftCorner')[0]?.textContent ?? '').trim().split(/\s+/).map(Number),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
// Rewrite the tile URL template to a MapLibre-compatible (XYZ-style) URL
|
|
244
|
+
formatTileUrl(template, style, tileMatrixSet, dimensionDefaults) {
|
|
245
|
+
let url = template
|
|
246
|
+
.replace('{TileMatrixSet}', tileMatrixSet.id)
|
|
247
|
+
.replace('{Style}', style ?? '')
|
|
248
|
+
.replace('{TileMatrix}', `${tileMatrixSet.grid?.levelPrefix ?? ''}{z}`)
|
|
249
|
+
.replace('{TileRow}', '{y}')
|
|
250
|
+
.replace('{TileCol}', '{x}');
|
|
251
|
+
if (dimensionDefaults) {
|
|
252
|
+
for (const [key, value] of Object.entries(dimensionDefaults)) {
|
|
253
|
+
url = url.replace(`{${key}}`, value);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return url;
|
|
257
|
+
}
|
|
258
|
+
// Explain that we found the layers but can't draw any of them, naming the grids they were
|
|
259
|
+
// published in so the mismatch is visible without reading the GetCapabilities document
|
|
260
|
+
unsupportedGridMessage(elements, tileMatrixSets) {
|
|
261
|
+
const offered = elements.flatMap(element => this.getLinkedTileMatrixSets(element, tileMatrixSets)).map(set => `${set.id} (${set.crs})`);
|
|
262
|
+
const grids = Array.from(new Set(offered)).join(', ');
|
|
263
|
+
const named = this.options.layerIds.join(', ');
|
|
264
|
+
return `This service publishes ${named} only in tile grids that can't be displayed${grids ? `: ${grids}` : ''}. Previewing WMTS needs a Web Mercator (EPSG:3857) grid, such as GoogleMapsCompatible.`;
|
|
265
|
+
}
|
|
266
|
+
// We rewrite WMTS tile URLs to XYZ-style URLs, so the scheme is always 'xyz'
|
|
267
|
+
getScheme() {
|
|
268
|
+
return 'xyz';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// WMTS advertises the CRS in any of several notations - "EPSG:3857",
|
|
272
|
+
// "urn:ogc:def:crs:EPSG:6.18:3:3857", "http://www.opengis.net/def/crs/EPSG/0/3857" - all of
|
|
273
|
+
// which end in the code.
|
|
274
|
+
function isWebMercator(crs) {
|
|
275
|
+
const code = crs.split(/[:/]/).pop() ?? '';
|
|
276
|
+
return WEB_MERCATOR_CODES.includes(code);
|
|
277
|
+
}
|
|
278
|
+
// Whether a level holds exactly what its zoom of the XYZ grid holds: square tiles, 2^z of
|
|
279
|
+
// them on a side, hung from the northwest corner of the world
|
|
280
|
+
function isXyzLevel(matrix) {
|
|
281
|
+
const tiles = 2 ** matrix.zoom;
|
|
282
|
+
const [x, y] = matrix.topLeft;
|
|
283
|
+
return (matrix.tileWidth === matrix.tileHeight &&
|
|
284
|
+
matrix.matrixWidth === tiles &&
|
|
285
|
+
matrix.matrixHeight === tiles &&
|
|
286
|
+
Math.abs(x + XYZ_ORIGIN) <= ORIGIN_TOLERANCE &&
|
|
287
|
+
Math.abs(y - XYZ_ORIGIN) <= ORIGIN_TOLERANCE);
|
|
288
|
+
}
|
|
289
|
+
// The host a tile template points at, or undefined if it isn't an absolute URL. The braces
|
|
290
|
+
// left in the template for MapLibre don't bother the URL parser.
|
|
291
|
+
function hostOf(template) {
|
|
292
|
+
try {
|
|
293
|
+
return new URL(template).host;
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Read a numeric child element, or NaN when it's missing or isn't a number. NaN fails every
|
|
300
|
+
// comparison the grid checks make, which is what we want from a malformed definition.
|
|
301
|
+
function numericChild(element, tagName) {
|
|
302
|
+
const text = element.getElementsByTagName(tagName)[0]?.textContent?.trim();
|
|
303
|
+
return text ? Number(text) : NaN;
|
|
304
|
+
}
|
|
305
|
+
// One corner of an OWS bounding box, written as space-separated ordinates
|
|
306
|
+
function numericCorner(element, tagName) {
|
|
307
|
+
const text = element.getElementsByTagName(tagName)[0]?.textContent?.trim() ?? '';
|
|
308
|
+
return text.split(/\s+/).map(Number);
|
|
309
|
+
}
|
|
310
|
+
// Where a latitude falls in the Web Mercator world, 0 at the north edge and 1 at the south
|
|
311
|
+
function mercatorY(lat) {
|
|
312
|
+
const clamped = clamp(lat, -MERCATOR_MAX_LATITUDE, MERCATOR_MAX_LATITUDE);
|
|
313
|
+
return (180 - (180 / Math.PI) * Math.log(Math.tan(Math.PI / 4 + (clamped * Math.PI) / 360))) / 360;
|
|
314
|
+
}
|
|
315
|
+
function clamp(value, min, max) {
|
|
316
|
+
return Math.min(Math.max(value, min), max);
|
|
317
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/** @vitest-environment happy-dom */
|
|
2
|
+
import { describe, it, expect } from "@stencil/vitest";
|
|
3
|
+
import { LngLatBounds } from "maplibre-gl";
|
|
4
|
+
import WmtsResource from "./wmts";
|
|
5
|
+
const ENDPOINT = 'https://example.org/wmts/1.0.0/WMTSCapabilities.xml';
|
|
6
|
+
// Reads a hand-built capabilities document instead of fetching one
|
|
7
|
+
class TestWmtsResource extends WmtsResource {
|
|
8
|
+
xml;
|
|
9
|
+
constructor(id, url, options, xml, bounds) {
|
|
10
|
+
super(id, url, options, bounds);
|
|
11
|
+
this.xml = xml;
|
|
12
|
+
}
|
|
13
|
+
async getMetadata() {
|
|
14
|
+
return new DOMParser().parseFromString(this.xml, 'application/xml');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// One zoom of the XYZ grid: 2^z square tiles hung from the northwest corner of the world
|
|
18
|
+
const mercatorLevel = (zoom, { prefix = '', tileSize = 256 } = {}) => `
|
|
19
|
+
<TileMatrix>
|
|
20
|
+
<ows:Identifier>${prefix}${zoom}</ows:Identifier>
|
|
21
|
+
<TopLeftCorner>-20037508.34278925 20037508.34278925</TopLeftCorner>
|
|
22
|
+
<TileWidth>${tileSize}</TileWidth>
|
|
23
|
+
<TileHeight>${tileSize}</TileHeight>
|
|
24
|
+
<MatrixWidth>${2 ** zoom}</MatrixWidth>
|
|
25
|
+
<MatrixHeight>${2 ** zoom}</MatrixHeight>
|
|
26
|
+
</TileMatrix>`;
|
|
27
|
+
// Tiles per side at each zoom of NASA GIBS' geographic "500m" grid, which halves down from
|
|
28
|
+
// 160x80 tiles of 2.25 degrees and rounds up - so it is neither square nor a quadtree
|
|
29
|
+
const GEOGRAPHIC_LEVELS = [
|
|
30
|
+
[2, 1],
|
|
31
|
+
[3, 2],
|
|
32
|
+
[5, 3],
|
|
33
|
+
[10, 5],
|
|
34
|
+
];
|
|
35
|
+
const geographicLevel = (zoom) => `
|
|
36
|
+
<TileMatrix>
|
|
37
|
+
<ows:Identifier>${zoom}</ows:Identifier>
|
|
38
|
+
<TopLeftCorner>-180 90</TopLeftCorner>
|
|
39
|
+
<TileWidth>512</TileWidth>
|
|
40
|
+
<TileHeight>512</TileHeight>
|
|
41
|
+
<MatrixWidth>${GEOGRAPHIC_LEVELS[zoom][0]}</MatrixWidth>
|
|
42
|
+
<MatrixHeight>${GEOGRAPHIC_LEVELS[zoom][1]}</MatrixHeight>
|
|
43
|
+
</TileMatrix>`;
|
|
44
|
+
const tileMatrixSet = (id, crs, levels) => `
|
|
45
|
+
<TileMatrixSet>
|
|
46
|
+
<ows:Identifier>${id}</ows:Identifier>
|
|
47
|
+
<ows:SupportedCRS>${crs}</ows:SupportedCRS>
|
|
48
|
+
${levels}
|
|
49
|
+
</TileMatrixSet>`;
|
|
50
|
+
const ZOOMS = [0, 1, 2, 3];
|
|
51
|
+
const MERCATOR_SET = tileMatrixSet('GoogleMapsCompatible_Level3', 'urn:ogc:def:crs:EPSG:6.18:3:3857', ZOOMS.map(zoom => mercatorLevel(zoom)).join(''));
|
|
52
|
+
const GEOGRAPHIC_SET = tileMatrixSet('500m', 'urn:ogc:def:crs:OGC:1.3:CRS84', ZOOMS.map(geographicLevel).join(''));
|
|
53
|
+
// GeoServer and friends qualify the level with the grid it belongs to
|
|
54
|
+
const QUALIFIED_SET = tileMatrixSet('EPSG:900913', 'EPSG:900913', ZOOMS.map(zoom => mercatorLevel(zoom, { prefix: 'EPSG:900913:' })).join(''));
|
|
55
|
+
// Web Mercator, but with a level that doesn't hold the 2^z by 2^z tiles that zoom has
|
|
56
|
+
const RAGGED_SET = tileMatrixSet('ragged', 'EPSG:3857', `${mercatorLevel(0)}
|
|
57
|
+
<TileMatrix>
|
|
58
|
+
<ows:Identifier>1</ows:Identifier>
|
|
59
|
+
<TopLeftCorner>-20037508.34278925 20037508.34278925</TopLeftCorner>
|
|
60
|
+
<TileWidth>256</TileWidth>
|
|
61
|
+
<TileHeight>256</TileHeight>
|
|
62
|
+
<MatrixWidth>3</MatrixWidth>
|
|
63
|
+
<MatrixHeight>2</MatrixHeight>
|
|
64
|
+
</TileMatrix>`);
|
|
65
|
+
const TIME_DIMENSION = `
|
|
66
|
+
<Dimension>
|
|
67
|
+
<ows:Identifier>Time</ows:Identifier>
|
|
68
|
+
<UOM>ISO8601</UOM>
|
|
69
|
+
<Default>2016-01-01</Default>
|
|
70
|
+
</Dimension>`;
|
|
71
|
+
const TILE_TEMPLATE = 'https://tiles.example.org/lights/{Style}/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}.png';
|
|
72
|
+
const boundingBox = (west, south, east, north) => `
|
|
73
|
+
<ows:WGS84BoundingBox crs="urn:ogc:def:crs:OGC:2:84">
|
|
74
|
+
<ows:LowerCorner>${west} ${south}</ows:LowerCorner>
|
|
75
|
+
<ows:UpperCorner>${east} ${north}</ows:UpperCorner>
|
|
76
|
+
</ows:WGS84BoundingBox>`;
|
|
77
|
+
const layer = ({ id = 'lights', links, templates = [TILE_TEMPLATE], dimension = '', extraUrls = '', bbox = '' }) => `
|
|
78
|
+
<Layer>
|
|
79
|
+
<ows:Title>Night Lights</ows:Title>
|
|
80
|
+
${bbox}
|
|
81
|
+
<ows:Identifier>${id}</ows:Identifier>
|
|
82
|
+
<Style isDefault="true"><ows:Identifier>default</ows:Identifier></Style>
|
|
83
|
+
${dimension}
|
|
84
|
+
${links.map(link => `<TileMatrixSetLink><TileMatrixSet>${link}</TileMatrixSet></TileMatrixSetLink>`).join('')}
|
|
85
|
+
${templates.map(template => `<ResourceURL resourceType="tile" format="image/png" template="${template}"/>`).join('')}
|
|
86
|
+
${extraUrls}
|
|
87
|
+
</Layer>`;
|
|
88
|
+
const capabilities = (...contents) => `<?xml version="1.0" encoding="UTF-8"?>
|
|
89
|
+
<Capabilities xmlns="http://www.opengis.net/wmts/1.0" xmlns:ows="http://www.opengis.net/ows/1.1" version="1.0.0">
|
|
90
|
+
<Contents>${contents.join('')}</Contents>
|
|
91
|
+
</Capabilities>`;
|
|
92
|
+
const resourceFor = (xml, layerIds = ['lights'], url = ENDPOINT, bounds) => new TestWmtsResource('night-lights', url, { layerIds }, xml, bounds);
|
|
93
|
+
describe('WmtsResource#getLayers', () => {
|
|
94
|
+
it('rewrites a Web Mercator layer as XYZ tile URLs', async () => {
|
|
95
|
+
const resource = resourceFor(capabilities(layer({ links: ['GoogleMapsCompatible_Level3'] }), MERCATOR_SET));
|
|
96
|
+
const [drawn] = await resource.getLayers();
|
|
97
|
+
expect(drawn.id).toEqual('lights');
|
|
98
|
+
expect(drawn.title).toEqual('Night Lights');
|
|
99
|
+
expect(drawn.tileUrls).toEqual(['https://tiles.example.org/lights/default/GoogleMapsCompatible_Level3/{z}/{y}/{x}.png']);
|
|
100
|
+
});
|
|
101
|
+
it('takes the tile size and zoom range from the tile matrix set', async () => {
|
|
102
|
+
const bigTiles = tileMatrixSet('big', 'EPSG:3857', [2, 3].map(zoom => mercatorLevel(zoom, { tileSize: 512 })).join(''));
|
|
103
|
+
const resource = resourceFor(capabilities(layer({ links: ['big'] }), bigTiles));
|
|
104
|
+
const [drawn] = await resource.getLayers();
|
|
105
|
+
// Both differ from what MapLibre would assume on its own, which is 512px tiles over zooms 0-22
|
|
106
|
+
expect(drawn.tileSize).toEqual(512);
|
|
107
|
+
expect(drawn.minzoom).toEqual(2);
|
|
108
|
+
expect(drawn.maxzoom).toEqual(3);
|
|
109
|
+
});
|
|
110
|
+
it('substitutes the default value of each dimension', async () => {
|
|
111
|
+
const template = 'https://tiles.example.org/lights/default/{Time}/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}.png';
|
|
112
|
+
const resource = resourceFor(capabilities(layer({ links: ['GoogleMapsCompatible_Level3'], templates: [template], dimension: TIME_DIMENSION }), MERCATOR_SET));
|
|
113
|
+
const [drawn] = await resource.getLayers();
|
|
114
|
+
expect(drawn.tileUrls).toEqual(['https://tiles.example.org/lights/default/2016-01-01/GoogleMapsCompatible_Level3/{z}/{y}/{x}.png']);
|
|
115
|
+
});
|
|
116
|
+
it('ignores resource URLs that serve something other than tiles', async () => {
|
|
117
|
+
const extraUrls = '<ResourceURL resourceType="Domains" format="text/xml" template="https://tiles.example.org/lights/domains/{TileMatrixSet}/all.xml"/>';
|
|
118
|
+
const resource = resourceFor(capabilities(layer({ links: ['GoogleMapsCompatible_Level3'], extraUrls }), MERCATOR_SET));
|
|
119
|
+
const [drawn] = await resource.getLayers();
|
|
120
|
+
expect(drawn.tileUrls).toHaveLength(1);
|
|
121
|
+
});
|
|
122
|
+
it('keeps the prefix on tile matrix identifiers that are qualified by their grid', async () => {
|
|
123
|
+
const resource = resourceFor(capabilities(layer({ links: ['EPSG:900913'] }), QUALIFIED_SET));
|
|
124
|
+
const [drawn] = await resource.getLayers();
|
|
125
|
+
expect(drawn.tileUrls).toEqual(['https://tiles.example.org/lights/default/EPSG:900913/EPSG:900913:{z}/{y}/{x}.png']);
|
|
126
|
+
});
|
|
127
|
+
it('passes over a grid it cannot draw for one it can, whatever the order', async () => {
|
|
128
|
+
const resource = resourceFor(capabilities(layer({ links: ['500m', 'GoogleMapsCompatible_Level3'] }), GEOGRAPHIC_SET, MERCATOR_SET));
|
|
129
|
+
const [drawn] = await resource.getLayers();
|
|
130
|
+
expect(drawn.tileUrls).toEqual(['https://tiles.example.org/lights/default/GoogleMapsCompatible_Level3/{z}/{y}/{x}.png']);
|
|
131
|
+
});
|
|
132
|
+
it('refuses a layer published only in a geographic grid, naming the grid and its CRS', async () => {
|
|
133
|
+
const resource = resourceFor(capabilities(layer({ links: ['500m'] }), GEOGRAPHIC_SET));
|
|
134
|
+
// Tile indices in a geographic grid address different ground than MapLibre's XYZ ones,
|
|
135
|
+
// so drawing it would put imagery in the wrong place or fall off the end of a row
|
|
136
|
+
await expect(resource.getLayers()).rejects.toThrow('500m (urn:ogc:def:crs:OGC:1.3:CRS84)');
|
|
137
|
+
await expect(resource.getLayers()).rejects.toThrow(/Web Mercator/);
|
|
138
|
+
});
|
|
139
|
+
it('refuses a Web Mercator grid whose levels are not the XYZ ones', async () => {
|
|
140
|
+
const resource = resourceFor(capabilities(layer({ links: ['ragged'] }), RAGGED_SET));
|
|
141
|
+
await expect(resource.getLayers()).rejects.toThrow('ragged (EPSG:3857)');
|
|
142
|
+
});
|
|
143
|
+
it('refuses a layer the service does not publish', async () => {
|
|
144
|
+
const resource = resourceFor(capabilities(layer({ links: ['GoogleMapsCompatible_Level3'] }), MERCATOR_SET), ['moonlight']);
|
|
145
|
+
await expect(resource.getLayers()).rejects.toThrow('moonlight');
|
|
146
|
+
});
|
|
147
|
+
it('returns only the requested layer when the service lists many', async () => {
|
|
148
|
+
const others = [1, 2, 3].map(index => layer({ id: `other-${index}`, links: ['GoogleMapsCompatible_Level3'] })).join('');
|
|
149
|
+
const resource = resourceFor(capabilities(others, layer({ links: ['GoogleMapsCompatible_Level3'] }), MERCATOR_SET));
|
|
150
|
+
const drawn = await resource.getLayers();
|
|
151
|
+
expect(drawn.map(each => each.id)).toEqual(['lights']);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
// Deep enough to hold the zoom a city-sized layer starts at
|
|
155
|
+
const DEEP_SET = tileMatrixSet('GoogleMapsCompatible', 'EPSG:3857', Array.from({ length: 20 }, (_, zoom) => mercatorLevel(zoom)).join(''));
|
|
156
|
+
// Vienna, as its orthophoto service states it, and as the Aardvark record does
|
|
157
|
+
const VIENNA = boundingBox(16.17, 48.1, 16.58, 48.33);
|
|
158
|
+
// Built the way bboxToBounds builds it from a record's ENVELOPE, since that is what arrives here
|
|
159
|
+
const VIENNA_RECORD = new LngLatBounds([16.133423, 48.106056], [16.626434, 48.3348]);
|
|
160
|
+
const boundedLayers = (bbox, grid = DEEP_SET, id = 'GoogleMapsCompatible', bounds) => resourceFor(capabilities(layer({ links: [id], bbox }), grid), ['lights'], ENDPOINT, bounds).getLayers();
|
|
161
|
+
describe('WmtsResource#getLayers extent', () => {
|
|
162
|
+
it('carries the published bounding box through in MapLibre order', async () => {
|
|
163
|
+
const [drawn] = await boundedLayers(VIENNA);
|
|
164
|
+
expect(drawn.bounds).toEqual([16.17, 48.1, 16.58, 48.33]);
|
|
165
|
+
});
|
|
166
|
+
it('starts a city-sized layer at the zoom where it first fills a tile', async () => {
|
|
167
|
+
// Below this every tile is mostly outside the layer, and a service that draws the empty
|
|
168
|
+
// part rather than answering 404 paints it over the map
|
|
169
|
+
const [drawn] = await boundedLayers(VIENNA);
|
|
170
|
+
expect(drawn.minzoom).toEqual(10);
|
|
171
|
+
expect(drawn.maxzoom).toEqual(19);
|
|
172
|
+
});
|
|
173
|
+
it('leaves a layer that covers the world at the first zoom of its grid', async () => {
|
|
174
|
+
const [drawn] = await boundedLayers(boundingBox(-180, -90, 180, 90));
|
|
175
|
+
expect(drawn.minzoom).toEqual(0);
|
|
176
|
+
});
|
|
177
|
+
it('measures a bounding box that crosses the antimeridian the short way round', async () => {
|
|
178
|
+
// Two degrees of longitude at the date line, not the 358 that subtracting the corners gives
|
|
179
|
+
const [drawn] = await boundedLayers(boundingBox(179, -0.1, -179, 0.1));
|
|
180
|
+
expect(drawn.minzoom).toEqual(8);
|
|
181
|
+
});
|
|
182
|
+
it('never starts a layer past the end of its grid', async () => {
|
|
183
|
+
const [drawn] = await boundedLayers(boundingBox(16.4, 48.2, 16.4, 48.2), MERCATOR_SET, 'GoogleMapsCompatible_Level3');
|
|
184
|
+
expect(drawn.minzoom).toEqual(3);
|
|
185
|
+
expect(drawn.maxzoom).toEqual(3);
|
|
186
|
+
});
|
|
187
|
+
it('falls back to the extent on the record when the service publishes none', async () => {
|
|
188
|
+
const [drawn] = await boundedLayers('', DEEP_SET, 'GoogleMapsCompatible', VIENNA_RECORD);
|
|
189
|
+
expect(drawn.bounds).toEqual([16.133423, 48.106056, 16.626434, 48.3348]);
|
|
190
|
+
expect(drawn.minzoom).toEqual(10);
|
|
191
|
+
});
|
|
192
|
+
it('prefers the extent the service publishes to the one on the record', async () => {
|
|
193
|
+
// The record states one extent for everything it references; the service states one per layer
|
|
194
|
+
const [drawn] = await boundedLayers(VIENNA, DEEP_SET, 'GoogleMapsCompatible', [-180, -85, 180, 85]);
|
|
195
|
+
expect(drawn.bounds).toEqual([16.17, 48.1, 16.58, 48.33]);
|
|
196
|
+
expect(drawn.minzoom).toEqual(10);
|
|
197
|
+
});
|
|
198
|
+
it('leaves the extent unset when neither the service nor the record gives one', async () => {
|
|
199
|
+
const [drawn] = await boundedLayers('');
|
|
200
|
+
expect(drawn.bounds).toBeUndefined();
|
|
201
|
+
expect(drawn.minzoom).toEqual(0);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
// Vienna's service, in miniature: tiles sharded over several hosts, only one of which is the
|
|
205
|
+
// host that answered the capabilities request, and every one of them advertised over http
|
|
206
|
+
const SHARDED_TEMPLATES = ['maps1', 'maps2', 'maps'].map(host => `http://${host}.example.org/lb2016/{Style}/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}.jpeg`);
|
|
207
|
+
const shardedLayers = (url, templates = SHARDED_TEMPLATES) => resourceFor(capabilities(layer({ links: ['GoogleMapsCompatible_Level3'], templates }), MERCATOR_SET), ['lights'], url).getLayers();
|
|
208
|
+
describe('WmtsResource#getLayers tile hosts', () => {
|
|
209
|
+
it('keeps only the host that served the capabilities document', async () => {
|
|
210
|
+
// MapLibre assigns each tile to a host by coordinate and never retries elsewhere, so a
|
|
211
|
+
// hostname that no longer resolves costs us that share of the tiles for good
|
|
212
|
+
const [drawn] = await shardedLayers('https://maps.example.org/wmts/1.0.0/WMTSCapabilities.xml');
|
|
213
|
+
expect(drawn.tileUrls).toEqual(['https://maps.example.org/lb2016/default/GoogleMapsCompatible_Level3/{z}/{y}/{x}.jpeg']);
|
|
214
|
+
});
|
|
215
|
+
it('reaches that host over https when that is how the capabilities came back', async () => {
|
|
216
|
+
// A document served over https can still advertise http tiles, which the browser blocks
|
|
217
|
+
// as mixed content
|
|
218
|
+
const [drawn] = await shardedLayers('https://maps.example.org/wmts/1.0.0/WMTSCapabilities.xml');
|
|
219
|
+
expect(drawn.tileUrls[0].startsWith('https://')).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
it('leaves every host in place when none of them served the capabilities', async () => {
|
|
222
|
+
const [drawn] = await shardedLayers('https://www.example.org/wmts/1.0.0/WMTSCapabilities.xml');
|
|
223
|
+
expect(drawn.tileUrls).toHaveLength(3);
|
|
224
|
+
expect(drawn.tileUrls[0]).toContain('http://maps1.example.org/');
|
|
225
|
+
});
|
|
226
|
+
it('never downgrades a tile host that already speaks https', async () => {
|
|
227
|
+
const templates = ['https://maps.example.org/lb2016/{Style}/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}.jpeg'];
|
|
228
|
+
const [drawn] = await shardedLayers('http://maps.example.org/wmts/1.0.0/WMTSCapabilities.xml', templates);
|
|
229
|
+
expect(drawn.tileUrls[0].startsWith('https://')).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
it('leaves a service whose tiles live on a host of their own alone', async () => {
|
|
232
|
+
const [drawn] = await resourceFor(capabilities(layer({ links: ['GoogleMapsCompatible_Level3'] }), MERCATOR_SET)).getLayers();
|
|
233
|
+
expect(drawn.tileUrls).toEqual(['https://tiles.example.org/lights/default/GoogleMapsCompatible_Level3/{z}/{y}/{x}.png']);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as inflate_1 } from './pako.esm-KbdoS3Oq.js';
|
|
2
|
-
import { g as getDefaultExportFromCjs, L as LercAddCompression, a as LercParameters } from './ogm-alerts.ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-preview.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-
|
|
2
|
+
import { g as getDefaultExportFromCjs, L as LercAddCompression, a as LercParameters } from './ogm-alerts.ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-preview.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-DSguoh85.js';
|
|
3
3
|
import { B as BaseDecoder } from './basedecoder-BK531nfi.js';
|
|
4
4
|
import './index-DTLAlWKQ.js';
|
|
5
5
|
|
package/dist/esm/loader.js
CHANGED
|
@@ -5,7 +5,7 @@ import { g as globalScripts } from './app-globals-DQuL1Twl.js';
|
|
|
5
5
|
const defineCustomElements = async (win, options) => {
|
|
6
6
|
if (typeof window === 'undefined') return undefined;
|
|
7
7
|
await globalScripts();
|
|
8
|
-
return bootstrapLazy([["ogm-alerts_11",[[513,"ogm-viewer",{"recordUrl":[1,"record-url"],"theme":[1],"hideTitle":[4,"hide-title"],"record":[32],"error":[32],"previewOpacity":[32],"sidebarOpen":[32],"loading":[32],"loadRecord":[64]},[[0,"sidebarToggled","toggleSidebar"],[0,"opacityChange","adjustPreviewOpacity"],[0,"mapLoading","setLoadingStarted"],[0,"imageLoading","setLoadingStarted"],[0,"mapIdle","setLoadingFinished"],[0,"imageLoaded","setLoadingFinished"]],{"recordUrl":[{"updateRecord":0}],"record":[{"resetLoading":0}]}],[513,"ogm-previews",{"theme":[1],"record":[16],"previewOpacity":[2,"preview-opacity"],"sidebarPadding":[2,"sidebar-padding"],"
|
|
8
|
+
return bootstrapLazy([["ogm-alerts_11",[[513,"ogm-viewer",{"recordUrl":[1,"record-url"],"theme":[1],"hideTitle":[4,"hide-title"],"record":[32],"error":[32],"previewOpacity":[32],"sidebarOpen":[32],"loading":[32],"loadRecord":[64]},[[0,"sidebarToggled","toggleSidebar"],[0,"opacityChange","adjustPreviewOpacity"],[0,"mapLoading","setLoadingStarted"],[0,"imageLoading","setLoadingStarted"],[0,"mapIdle","setLoadingFinished"],[0,"imageLoaded","setLoadingFinished"]],{"recordUrl":[{"updateRecord":0}],"record":[{"resetLoading":0}]}],[513,"ogm-previews",{"theme":[1],"record":[16],"previewOpacity":[2,"preview-opacity"],"sidebarPadding":[2,"sidebar-padding"],"resources":[32]},null,{"record":[{"getSources":0}]}],[513,"ogm-sidebar",{"record":[16],"theme":[1],"open":[4]}],[513,"ogm-menubar",{"record":[16],"theme":[1],"loading":[4],"hideTitle":[4,"hide-title"]}],[513,"ogm-preview",{"theme":[1],"previewResource":[16],"previewOpacity":[2,"preview-opacity"],"sidebarPadding":[2,"sidebar-padding"],"error":[32]},[[0,"previewError","handlePreviewError"]],{"previewResource":[{"resetError":0}]}],[513,"ogm-metadata",{"record":[16],"theme":[1],"fieldNames":[16],"filteredRecord":[32]}],[513,"ogm-settings",{"record":[16]},[[0,"input","handleOpacityChange"]]],[513,"ogm-map",{"previewResource":[16],"theme":[1],"padding":[2],"easeMapTo":[64]},[[16,"featureSelected","handleFeatureSelected"]],{"previewResource":[{"loadResource":0}],"theme":[{"onThemeChange":0}],"padding":[{"onPaddingChange":0}]}],[513,"ogm-alerts",{"theme":[1],"error":[16]}],[513,"ogm-image",{"previewResource":[16],"theme":[1],"padding":[2]},null,{"previewResource":[{"onSourceChange":0}],"padding":[{"onPaddingChange":0}]}],[513,"ogm-attributes",{"features":[16],"currentIndex":[32]},null,{"features":[{"onFeaturesChange":0}],"currentIndex":[{"onCurrentIndexChange":0}]}]]]], options);
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export { defineCustomElements };
|