ogm-viewer 0.5.0 → 0.6.0
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-Df0hDWkS.js → lerc-XQuzL9p2.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-DMEfXquS.js → ogm-alerts.ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-preview.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-B4jkHqvr.js} +11313 -10658
- 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-image/ogm-image.js +14 -14
- package/dist/collection/components/ogm-map/ogm-map.js +144 -52
- package/dist/collection/components/ogm-menubar/ogm-menubar.js +23 -2
- package/dist/collection/components/ogm-menubar/ogm-menubar.test.js +34 -0
- 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 +30 -28
- package/dist/collection/components/ogm-previews/ogm-previews.test.js +3 -3
- package/dist/collection/components/ogm-viewer/ogm-viewer.css +6 -2
- package/dist/collection/components/ogm-viewer/ogm-viewer.js +29 -18
- 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 +11 -11
- package/dist/collection/lib/previewers/geojson.js +10 -7
- 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 +9 -9
- package/dist/collection/lib/previewers/pmtiles-vector.js +12 -51
- 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-CB_Jcu33.js → lerc-BxCv6y50.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-Dlc6USWB.js → ogm-alerts.ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-preview.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-DtEO48bS.js} +11313 -10658
- 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-7EhN80oi.js +2822 -0
- package/dist/ogm-viewer/{p-DqRDI3AC.js → p-BO_nFj5K.js} +1 -1
- package/dist/ogm-viewer/{p-7936ccc9.entry.js → p-c9af56a3.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 +13 -9
- package/dist/types/components/ogm-menubar/ogm-menubar.d.ts +2 -1
- 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/ogm-viewer/ogm-viewer.d.ts +1 -0
- package/dist/types/components.d.ts +24 -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 -6
- package/dist/types/lib/previewers/geojson.d.ts +7 -4
- 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 -3
- package/dist/types/lib/previewers/pmtiles-vector.d.ts +4 -11
- 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/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 -0
- package/dist/collection/lib/previewers/maplibre.js +0 -56
- package/dist/ogm-viewer/p-CWtUrqsM.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 → previewers/tilejson.test.d.ts} +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, vi, afterEach } from "@stencil/vitest";
|
|
2
|
-
import
|
|
2
|
+
import IIIFManifestResource from "./iiif-manifest";
|
|
3
3
|
const MANIFEST_URL = 'http://example.com/manifest.json';
|
|
4
4
|
// A source always points at a manifest URL; the manifest itself is fetched lazily
|
|
5
|
-
const createSource = () => new
|
|
5
|
+
const createSource = () => new IIIFManifestResource('test-id', MANIFEST_URL);
|
|
6
6
|
// A minimal IIIF v2 manifest with a single image
|
|
7
7
|
const v2Manifest = {
|
|
8
8
|
'@context': 'http://iiif.io/api/presentation/2/context.json',
|
|
@@ -60,7 +60,7 @@ const v3Manifest = {
|
|
|
60
60
|
},
|
|
61
61
|
],
|
|
62
62
|
};
|
|
63
|
-
describe('
|
|
63
|
+
describe('IIIFManifestResource', () => {
|
|
64
64
|
afterEach(() => vi.restoreAllMocks());
|
|
65
65
|
describe('getIIIFImageUrls', () => {
|
|
66
66
|
it('should fetch and extract image URLs from a IIIF v2 manifest', async () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Resource from "./resource";
|
|
2
2
|
// A source of mapped vector or raster data, accessed remotely
|
|
3
|
-
export default class
|
|
3
|
+
export default class MapResource extends Resource {
|
|
4
4
|
// URL used to generate a MapLibre source when adding to map
|
|
5
5
|
getMapLibreSourceUrl() {
|
|
6
6
|
return this.url;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
export default class
|
|
1
|
+
import GeoJsonResource from "./geojson";
|
|
2
|
+
export default class OpenIndexMapResource extends GeoJsonResource {
|
|
3
3
|
// Distinguish the layer name from regular GeoJSON
|
|
4
4
|
async getVectorLayers() {
|
|
5
5
|
return ['indexmap'];
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Adapted from https://github.com/protomaps/PMTiles/blob/main/app/src/tileset.ts
|
|
2
2
|
import { PMTiles, TileType } from "pmtiles";
|
|
3
|
-
import
|
|
3
|
+
import Resource from "./resource";
|
|
4
4
|
// Vector or raster tileset stored in a PMTiles archive at a URL
|
|
5
|
-
export default class
|
|
5
|
+
export default class PMTilesResource extends Resource {
|
|
6
6
|
// PMTiles object for reading metadata and tiles from the archive
|
|
7
7
|
archive;
|
|
8
8
|
// Memoized PMTiles metadata and header
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import MapResource from "./map";
|
|
2
2
|
// Tiled raster data, accessed remotely
|
|
3
|
-
export default class
|
|
3
|
+
export default class RasterResource extends MapResource {
|
|
4
4
|
async isVector() {
|
|
5
5
|
return false;
|
|
6
6
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// A source of previewable data at a URL
|
|
2
|
-
export default class
|
|
2
|
+
export default class Resource {
|
|
3
3
|
// URL to the remote data source
|
|
4
4
|
url;
|
|
5
5
|
// Explicitly provided bounds for the source, if any
|
|
6
6
|
bounds;
|
|
7
|
-
// Unique ID for this
|
|
7
|
+
// Unique ID for this resource
|
|
8
8
|
id;
|
|
9
9
|
// Store the source URL
|
|
10
10
|
constructor(id, url, bounds) {
|
|
@@ -12,7 +12,7 @@ export default class Source {
|
|
|
12
12
|
this.url = url;
|
|
13
13
|
this.bounds = bounds;
|
|
14
14
|
}
|
|
15
|
-
// Used to label the tabs for switching between
|
|
15
|
+
// Used to label the tabs for switching between previews, e.g.
|
|
16
16
|
label() {
|
|
17
17
|
return this.constructor.name;
|
|
18
18
|
}
|
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { fetchOrThrow } from "../errors";
|
|
2
|
+
import MapResource from "./map";
|
|
2
3
|
// Vector or raster tileset described in a TileJSON document at a URL
|
|
3
|
-
export default class
|
|
4
|
+
export default class TileJsonResource extends MapResource {
|
|
4
5
|
// Metadata parsed from TileJSON document
|
|
5
6
|
metadata;
|
|
6
7
|
label() {
|
|
7
8
|
return 'TileJSON';
|
|
8
9
|
}
|
|
10
|
+
getMapLibreSourceUrl() {
|
|
11
|
+
return this.url;
|
|
12
|
+
}
|
|
13
|
+
getScheme() {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
9
16
|
// Fetch and memoize metadata
|
|
10
17
|
async getMetadata() {
|
|
11
18
|
if (!this.metadata) {
|
|
12
|
-
const resp = await
|
|
19
|
+
const resp = await fetchOrThrow(this.url);
|
|
13
20
|
this.metadata = await resp.json();
|
|
14
21
|
}
|
|
15
22
|
return this.metadata;
|
|
@@ -21,16 +28,26 @@ export default class TileJSONSource extends Source {
|
|
|
21
28
|
const pathname = new URL(template).pathname;
|
|
22
29
|
return pathname.endsWith('.pbf') || pathname.endsWith('.mvt') || pathname.endsWith('.mlt');
|
|
23
30
|
}
|
|
24
|
-
// Used to zoom the map to the data once loaded
|
|
31
|
+
// Used to zoom the map to the data once loaded; bounds are optional in TileJSON, and
|
|
32
|
+
// without them we leave the camera wherever it already is
|
|
25
33
|
async getBounds() {
|
|
26
34
|
if (this.bounds)
|
|
27
35
|
return this.bounds;
|
|
28
36
|
const metadata = await this.getMetadata();
|
|
37
|
+
if (!metadata.bounds)
|
|
38
|
+
return undefined;
|
|
29
39
|
return [
|
|
30
40
|
[metadata.bounds[0], metadata.bounds[1]],
|
|
31
41
|
[metadata.bounds[2], metadata.bounds[3]],
|
|
32
42
|
];
|
|
33
43
|
}
|
|
44
|
+
// TileJSON has no tileSize of its own, though some services add one to the document. Raster
|
|
45
|
+
// tiles served from a {z}/{x}/{y} template are 256px by convention, so assume that otherwise;
|
|
46
|
+
// MapLibre would default to 512 and draw them upscaled and blurry.
|
|
47
|
+
async getTileSize() {
|
|
48
|
+
const metadata = await this.getMetadata();
|
|
49
|
+
return metadata.tileSize ?? 256;
|
|
50
|
+
}
|
|
34
51
|
async getVectorLayers() {
|
|
35
52
|
const metadata = await this.getMetadata();
|
|
36
53
|
return metadata.vector_layers.map((l) => l.id);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import MapResource from "./map";
|
|
2
2
|
// A source of vector data, accessed remotely
|
|
3
|
-
export default class
|
|
3
|
+
export default class VectorResource extends MapResource {
|
|
4
4
|
async isVector() {
|
|
5
5
|
return true;
|
|
6
6
|
}
|
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import RasterResource from "./raster";
|
|
2
|
+
// Number of features to ask for when inspecting; the spec defaults this to 1, which
|
|
3
|
+
// hides everything under the cursor but the topmost feature
|
|
4
|
+
const DEFAULT_FEATURE_COUNT = 10;
|
|
5
|
+
const defaultWmsOptions = {
|
|
3
6
|
layerIds: [],
|
|
4
7
|
bbox: '{bbox-epsg-3857}',
|
|
5
|
-
|
|
8
|
+
crs: 'EPSG:3857',
|
|
6
9
|
tileSize: 256,
|
|
7
10
|
format: 'image/png',
|
|
8
11
|
transparent: true,
|
|
9
12
|
version: '1.3.0',
|
|
10
13
|
};
|
|
11
14
|
// Data accessed via WMS GetMap requests, which return raster tiles
|
|
12
|
-
export default class
|
|
15
|
+
export default class WmsResource extends RasterResource {
|
|
13
16
|
options;
|
|
14
17
|
// Memoized metadata via GetCapabilities request
|
|
15
18
|
metadata;
|
|
16
19
|
constructor(id, url, options, bounds) {
|
|
17
20
|
super(id, url, bounds);
|
|
18
|
-
this.options = { ...
|
|
21
|
+
this.options = { ...defaultWmsOptions, ...options };
|
|
19
22
|
// Assume we're using one layer with the given ID if no layer IDs are provided
|
|
20
23
|
if (!this.options.layerIds || this.options.layerIds.length === 0) {
|
|
21
24
|
this.options.layerIds = [id];
|
|
@@ -43,6 +46,9 @@ export default class WmsSource extends RasterSource {
|
|
|
43
46
|
getTileSize() {
|
|
44
47
|
return this.options.tileSize;
|
|
45
48
|
}
|
|
49
|
+
async inspect(options) {
|
|
50
|
+
return await fetch(this.inspectUrl(options));
|
|
51
|
+
}
|
|
46
52
|
// WMS GetMap URL that will fetch tiles for this source
|
|
47
53
|
get tilesUrl() {
|
|
48
54
|
const tilesUrl = new URL(this.url);
|
|
@@ -54,7 +60,7 @@ export default class WmsSource extends RasterSource {
|
|
|
54
60
|
tilesUrl.searchParams.set('width', String(this.options.tileSize));
|
|
55
61
|
tilesUrl.searchParams.set('height', String(this.options.tileSize));
|
|
56
62
|
tilesUrl.searchParams.set('transparent', String(this.options.transparent));
|
|
57
|
-
tilesUrl.searchParams.set('srs', this.options.
|
|
63
|
+
tilesUrl.searchParams.set(this.isVersion130 ? 'crs' : 'srs', this.options.crs);
|
|
58
64
|
tilesUrl.searchParams.set('format', this.options.format);
|
|
59
65
|
tilesUrl.searchParams.set('version', this.options.version);
|
|
60
66
|
// This param can't be encoded because MapLibre needs to template it
|
|
@@ -62,6 +68,40 @@ export default class WmsSource extends RasterSource {
|
|
|
62
68
|
tilesUrlString += `&bbox=${this.options.bbox}`;
|
|
63
69
|
return tilesUrlString;
|
|
64
70
|
}
|
|
71
|
+
// WMS GetFeatureInfo URL, used to fetch information about features at a specific point
|
|
72
|
+
inspectUrl(inspectOptions) {
|
|
73
|
+
const inspectUrl = new URL(this.url);
|
|
74
|
+
// Merge the provided options with the instance's options
|
|
75
|
+
const options = { ...this.options, ...inspectOptions };
|
|
76
|
+
inspectUrl.searchParams.set('service', 'WMS');
|
|
77
|
+
inspectUrl.searchParams.set('request', 'GetFeatureInfo');
|
|
78
|
+
inspectUrl.searchParams.set('info_format', 'application/json');
|
|
79
|
+
inspectUrl.searchParams.set('layers', options.layerIds.join(','));
|
|
80
|
+
inspectUrl.searchParams.set('query_layers', options.layerIds.join(','));
|
|
81
|
+
inspectUrl.searchParams.set('version', options.version);
|
|
82
|
+
inspectUrl.searchParams.set('bbox', options.bbox);
|
|
83
|
+
inspectUrl.searchParams.set('width', String(options.width));
|
|
84
|
+
inspectUrl.searchParams.set('height', String(options.height));
|
|
85
|
+
inspectUrl.searchParams.set('feature_count', String(DEFAULT_FEATURE_COUNT));
|
|
86
|
+
// 1.3.0 renamed SRS to CRS, and renamed the pixel coordinates X,Y to I,J. Servers
|
|
87
|
+
// do enforce the names for the version they were asked for, so pick them to match.
|
|
88
|
+
if (this.isVersion130) {
|
|
89
|
+
inspectUrl.searchParams.set('crs', options.crs);
|
|
90
|
+
inspectUrl.searchParams.set('i', String(Math.round(options.x)));
|
|
91
|
+
inspectUrl.searchParams.set('j', String(Math.round(options.y)));
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
inspectUrl.searchParams.set('srs', options.crs);
|
|
95
|
+
inspectUrl.searchParams.set('x', String(Math.round(options.x)));
|
|
96
|
+
inspectUrl.searchParams.set('y', String(Math.round(options.y)));
|
|
97
|
+
}
|
|
98
|
+
return inspectUrl.toString();
|
|
99
|
+
}
|
|
100
|
+
// Whether this source speaks WMS 1.3.0 or later, which changed some param names
|
|
101
|
+
get isVersion130() {
|
|
102
|
+
const [major, minor] = this.options.version.split('.').map(Number);
|
|
103
|
+
return major > 1 || (major === 1 && minor >= 3);
|
|
104
|
+
}
|
|
65
105
|
// WMS GetCapabilities URL, used to fetch metadata about the layers
|
|
66
106
|
get capabilitiesUrl() {
|
|
67
107
|
const capabilitiesUrl = new URL(this.url);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect } from "@stencil/vitest";
|
|
2
|
+
import WmsResource from "./wms";
|
|
3
|
+
// The GetFeatureInfo URL is built by a protected method, so expose it for testing
|
|
4
|
+
class TestWmsSource extends WmsResource {
|
|
5
|
+
url_for(options) {
|
|
6
|
+
return new URL(this.inspectUrl(options));
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const ENDPOINT = 'https://geoservices.lib.berkeley.edu/geoserver/wms';
|
|
10
|
+
// A click in the middle of a 51x51 pixel window over Calaveras County
|
|
11
|
+
const options = {
|
|
12
|
+
bbox: '-13416701.09,4600766.56,-13411296.18,4606171.48',
|
|
13
|
+
width: 51,
|
|
14
|
+
height: 51,
|
|
15
|
+
x: 25,
|
|
16
|
+
y: 25,
|
|
17
|
+
};
|
|
18
|
+
describe('WmsSource#inspectUrl', () => {
|
|
19
|
+
it('uses the 1.3.0 param names by default', () => {
|
|
20
|
+
const source = new TestWmsSource('s7st30', ENDPOINT, { layerIds: [] });
|
|
21
|
+
const params = source.url_for(options).searchParams;
|
|
22
|
+
expect(params.get('version')).toEqual('1.3.0');
|
|
23
|
+
// 1.3.0 spells these CRS and I,J rather than SRS and X,Y
|
|
24
|
+
expect(params.get('crs')).toEqual('EPSG:3857');
|
|
25
|
+
expect(params.get('i')).toEqual('25');
|
|
26
|
+
expect(params.get('j')).toEqual('25');
|
|
27
|
+
expect(params.get('srs')).toBeNull();
|
|
28
|
+
expect(params.get('x')).toBeNull();
|
|
29
|
+
expect(params.get('y')).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
it('uses the older param names for 1.1.1', () => {
|
|
32
|
+
const source = new TestWmsSource('s7st30', ENDPOINT, { layerIds: [], version: '1.1.1' });
|
|
33
|
+
const params = source.url_for(options).searchParams;
|
|
34
|
+
expect(params.get('srs')).toEqual('EPSG:3857');
|
|
35
|
+
expect(params.get('x')).toEqual('25');
|
|
36
|
+
expect(params.get('y')).toEqual('25');
|
|
37
|
+
expect(params.get('crs')).toBeNull();
|
|
38
|
+
expect(params.get('i')).toBeNull();
|
|
39
|
+
expect(params.get('j')).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
it('passes through the query window and asks about the layer', () => {
|
|
42
|
+
const source = new TestWmsSource('s7st30', ENDPOINT, { layerIds: [] });
|
|
43
|
+
const params = source.url_for(options).searchParams;
|
|
44
|
+
expect(params.get('request')).toEqual('GetFeatureInfo');
|
|
45
|
+
expect(params.get('bbox')).toEqual(options.bbox);
|
|
46
|
+
expect(params.get('width')).toEqual('51');
|
|
47
|
+
expect(params.get('height')).toEqual('51');
|
|
48
|
+
// Defaults the layer to the source ID, and asks about it rather than just drawing it
|
|
49
|
+
expect(params.get('layers')).toEqual('s7st30');
|
|
50
|
+
expect(params.get('query_layers')).toEqual('s7st30');
|
|
51
|
+
});
|
|
52
|
+
it('asks for more than the one feature the spec defaults to', () => {
|
|
53
|
+
const source = new TestWmsSource('s7st30', ENDPOINT, { layerIds: [] });
|
|
54
|
+
const params = source.url_for(options).searchParams;
|
|
55
|
+
expect(Number(params.get('feature_count'))).toBeGreaterThan(1);
|
|
56
|
+
});
|
|
57
|
+
it('rounds pixel coordinates, which must be integers', () => {
|
|
58
|
+
const source = new TestWmsSource('s7st30', ENDPOINT, { layerIds: [] });
|
|
59
|
+
const params = source.url_for({ ...options, x: 25.4, y: 25.6 }).searchParams;
|
|
60
|
+
expect(params.get('i')).toEqual('25');
|
|
61
|
+
expect(params.get('j')).toEqual('26');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -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
|
+
}
|