ogm-viewer 0.5.1 → 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-IU-tWkVO.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-BAGJpqMB.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} +11302 -10648
- 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 +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 +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-yC3LNu-h.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-BpVupgSB.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} +11302 -10648
- 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-_eTjrSO2.js → p-BO_nFj5K.js} +1 -1
- package/dist/ogm-viewer/{p-11cf948e.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 +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 -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/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 → previewers/tilejson.test.d.ts} +0 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "@stencil/vitest";
|
|
2
|
+
import TileJsonRasterPreviewer from "./tilejson-raster";
|
|
3
|
+
import TileJsonVectorPreviewer from "./tilejson-vector";
|
|
4
|
+
import TileJsonResource from "../resources/tilejson";
|
|
5
|
+
// Just enough of a MapLibre map to record what the previewer adds
|
|
6
|
+
class FakeMap {
|
|
7
|
+
sources = new Map();
|
|
8
|
+
layers = new Map();
|
|
9
|
+
getSource(id) {
|
|
10
|
+
return this.sources.get(id);
|
|
11
|
+
}
|
|
12
|
+
addSource(id, spec) {
|
|
13
|
+
this.sources.set(id, spec);
|
|
14
|
+
}
|
|
15
|
+
removeSource(id) {
|
|
16
|
+
this.sources.delete(id);
|
|
17
|
+
}
|
|
18
|
+
getLayer(id) {
|
|
19
|
+
return this.layers.get(id);
|
|
20
|
+
}
|
|
21
|
+
addLayer(layer) {
|
|
22
|
+
// MapLibre refuses a layer whose source hasn't been added yet
|
|
23
|
+
if (!this.sources.has(layer.source))
|
|
24
|
+
throw new Error(`No source ${layer.source} for layer ${layer.id}`);
|
|
25
|
+
this.layers.set(layer.id, layer);
|
|
26
|
+
}
|
|
27
|
+
removeLayer(id) {
|
|
28
|
+
this.layers.delete(id);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// The previewers only read the colors and label styles, none of which these tests assert on
|
|
32
|
+
const style = { opacity: 0.8, fillColor: '#00f', strokeColor: '#009', textColor: '#000', textFont: 'Noto Sans Regular', textSize: 12 };
|
|
33
|
+
const TILEJSON_URL = 'https://example.com/tilejson.json';
|
|
34
|
+
const BALKANS_BOUNDS = [19.333333, 39.75, 29.333333, 43.083333];
|
|
35
|
+
// A raster tileset, as a service like titiler describes one
|
|
36
|
+
const rasterDoc = {
|
|
37
|
+
tilejson: '3.0.0',
|
|
38
|
+
tiles: ['https://example.com/tiles/{z}/{x}/{y}@1x.png'],
|
|
39
|
+
minzoom: 0,
|
|
40
|
+
maxzoom: 16,
|
|
41
|
+
bounds: BALKANS_BOUNDS,
|
|
42
|
+
};
|
|
43
|
+
// A vector tileset with more than one layer in its tiles
|
|
44
|
+
const vectorDoc = {
|
|
45
|
+
tilejson: '3.0.0',
|
|
46
|
+
tiles: ['https://example.com/tiles/{z}/{x}/{y}.pbf'],
|
|
47
|
+
vector_layers: [{ id: 'districts' }, { id: 'places' }],
|
|
48
|
+
bounds: BALKANS_BOUNDS,
|
|
49
|
+
};
|
|
50
|
+
// Serve a TileJSON document at the source URL. Every source fetches it lazily, so nothing has been
|
|
51
|
+
// requested until the previewer asks.
|
|
52
|
+
const serve = (doc) => {
|
|
53
|
+
vi.spyOn(global, 'fetch').mockResolvedValue(new Response(JSON.stringify(doc)));
|
|
54
|
+
return new TileJsonResource('princeton-fk4544658v', TILEJSON_URL);
|
|
55
|
+
};
|
|
56
|
+
afterEach(() => vi.restoreAllMocks());
|
|
57
|
+
describe('TileJsonRasterPreviewer#preview', () => {
|
|
58
|
+
const preview = async (doc) => {
|
|
59
|
+
const map = new FakeMap();
|
|
60
|
+
const previewer = new TileJsonRasterPreviewer(serve(doc), map, style);
|
|
61
|
+
await previewer.preview();
|
|
62
|
+
return { map, previewer };
|
|
63
|
+
};
|
|
64
|
+
it('hands MapLibre the document URL rather than the tile template', async () => {
|
|
65
|
+
const { map } = await preview(rasterDoc);
|
|
66
|
+
const source = map.sources.get('princeton-fk4544658v-tilejson');
|
|
67
|
+
expect(source.type).toEqual('raster');
|
|
68
|
+
expect(source.url).toEqual(TILEJSON_URL);
|
|
69
|
+
expect(source.tiles).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
it('draws the tiles at 256px, the size a template implies', async () => {
|
|
72
|
+
const { map } = await preview(rasterDoc);
|
|
73
|
+
expect(map.sources.get('princeton-fk4544658v-tilejson').tileSize).toEqual(256);
|
|
74
|
+
});
|
|
75
|
+
it('defers to a tile size the document declares', async () => {
|
|
76
|
+
const { map } = await preview({ ...rasterDoc, tileSize: 512 });
|
|
77
|
+
expect(map.sources.get('princeton-fk4544658v-tilejson').tileSize).toEqual(512);
|
|
78
|
+
});
|
|
79
|
+
it('adds a single raster layer over the source', async () => {
|
|
80
|
+
const { map, previewer } = await preview(rasterDoc);
|
|
81
|
+
expect([...map.layers.keys()]).toEqual(['princeton-fk4544658v-tilejson']);
|
|
82
|
+
expect(map.layers.get('princeton-fk4544658v-tilejson').type).toEqual('raster');
|
|
83
|
+
expect(previewer.layerIds).toEqual(['princeton-fk4544658v-tilejson']);
|
|
84
|
+
});
|
|
85
|
+
it('takes its bounds from the document', async () => {
|
|
86
|
+
const { previewer } = await preview(rasterDoc);
|
|
87
|
+
expect(await previewer.getBounds()).toEqual([
|
|
88
|
+
[19.333333, 39.75],
|
|
89
|
+
[29.333333, 43.083333],
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
it('has no bounds when the document leaves them out', async () => {
|
|
93
|
+
const { bounds: _bounds, ...doc } = rasterDoc;
|
|
94
|
+
const { previewer } = await preview(doc);
|
|
95
|
+
expect(await previewer.getBounds()).toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
it('removes what it added when cleared', async () => {
|
|
98
|
+
const { map, previewer } = await preview(rasterDoc);
|
|
99
|
+
await previewer.clearPreview();
|
|
100
|
+
expect(map.sources.size).toEqual(0);
|
|
101
|
+
expect(map.layers.size).toEqual(0);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('TileJsonVectorPreviewer#preview', () => {
|
|
105
|
+
const preview = async (doc) => {
|
|
106
|
+
const map = new FakeMap();
|
|
107
|
+
const previewer = new TileJsonVectorPreviewer(serve(doc), map, style);
|
|
108
|
+
await previewer.preview();
|
|
109
|
+
return { map, previewer };
|
|
110
|
+
};
|
|
111
|
+
it('hands MapLibre the document URL and the tile encoding', async () => {
|
|
112
|
+
const { map } = await preview(vectorDoc);
|
|
113
|
+
const source = map.sources.get('princeton-fk4544658v-tilejson');
|
|
114
|
+
expect(source.type).toEqual('vector');
|
|
115
|
+
expect(source.url).toEqual(TILEJSON_URL);
|
|
116
|
+
expect(source.encoding).toEqual('mvt');
|
|
117
|
+
});
|
|
118
|
+
it('asks for MapLibre Tile decoding when the tiles are .mlt', async () => {
|
|
119
|
+
const { map } = await preview({ ...vectorDoc, tiles: ['https://example.com/tiles/{z}/{x}/{y}.mlt'] });
|
|
120
|
+
expect(map.sources.get('princeton-fk4544658v-tilejson').encoding).toEqual('mlt');
|
|
121
|
+
});
|
|
122
|
+
it('styles every layer the document lists', async () => {
|
|
123
|
+
const { map } = await preview(vectorDoc);
|
|
124
|
+
const suffixes = ['polygons', 'polygon-outlines', 'lines', 'points', 'polygon-labels', 'line-labels', 'point-labels'];
|
|
125
|
+
expect([...map.layers.keys()]).toEqual(['districts', 'places'].flatMap(layer => suffixes.map(suffix => `princeton-fk4544658v-tilejson-${layer}-${suffix}`)));
|
|
126
|
+
});
|
|
127
|
+
it('names the layer within the tiles that each style layer reads', async () => {
|
|
128
|
+
const { map } = await preview(vectorDoc);
|
|
129
|
+
// Without this a layer would draw from whichever layer of the tiles came first
|
|
130
|
+
expect(map.layers.get('princeton-fk4544658v-tilejson-places-points')['source-layer']).toEqual('places');
|
|
131
|
+
expect([...map.layers.values()].every(layer => layer['source-layer'])).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
it('takes its bounds from the document', async () => {
|
|
134
|
+
const { previewer } = await preview(vectorDoc);
|
|
135
|
+
expect(await previewer.getBounds()).toEqual([
|
|
136
|
+
[19.333333, 39.75],
|
|
137
|
+
[29.333333, 43.083333],
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
export default class VectorPreviewer extends
|
|
1
|
+
import MapPreviewer from "./map";
|
|
2
|
+
export default class VectorPreviewer extends MapPreviewer {
|
|
3
|
+
getSourceId() {
|
|
4
|
+
return this.resource.id;
|
|
5
|
+
}
|
|
3
6
|
// Set opacity of all layers in this previewer
|
|
4
7
|
async setOpacity(opacity) {
|
|
5
8
|
this.opacity = opacity;
|
|
@@ -8,10 +11,10 @@ export default class VectorPreviewer extends MapLibrePreviewer {
|
|
|
8
11
|
}
|
|
9
12
|
}
|
|
10
13
|
async getBounds() {
|
|
11
|
-
return await this.
|
|
14
|
+
return await this.resource.getBounds();
|
|
12
15
|
}
|
|
13
16
|
async createLayers() {
|
|
14
|
-
const layerIds = await this.
|
|
17
|
+
const layerIds = await this.resource.getVectorLayers();
|
|
15
18
|
return layerIds.flatMap(layerId => {
|
|
16
19
|
return [
|
|
17
20
|
this.createPolygonLayer(layerId),
|
|
@@ -1,14 +1,94 @@
|
|
|
1
1
|
import RasterPreviewer from "./raster";
|
|
2
|
+
// Contents of the highlight source when nothing is selected
|
|
3
|
+
const NO_FEATURES = { type: 'FeatureCollection', features: [] };
|
|
2
4
|
export default class WmsPreviewer extends RasterPreviewer {
|
|
3
5
|
// WMS sources have no scheme
|
|
4
|
-
async
|
|
6
|
+
async createSources() {
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
id: `${this.resource.id}-wms`,
|
|
10
|
+
type: 'raster',
|
|
11
|
+
tiles: [await this.resource.getMapLibreSourceUrl()],
|
|
12
|
+
tileSize: this.resource.getTileSize(),
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
getSourceId() {
|
|
17
|
+
return `${this.resource.id}-wms`;
|
|
18
|
+
}
|
|
19
|
+
// The server draws the tiles, so unlike a vector preview there are no client-side features to
|
|
20
|
+
// restyle when one is selected. GetFeatureInfo does hand back the geometry it matched, so we
|
|
21
|
+
// keep a GeoJSON source alongside the tiles and draw the selection into it.
|
|
22
|
+
get highlightSourceId() {
|
|
23
|
+
return `${this.getSourceId()}-highlight`;
|
|
24
|
+
}
|
|
25
|
+
// Add the highlight source ahead of the layers that draw from it
|
|
26
|
+
async preview() {
|
|
27
|
+
if (!this.map.getSource(this.highlightSourceId)) {
|
|
28
|
+
this.map.addSource(this.highlightSourceId, { type: 'geojson', data: NO_FEATURES });
|
|
29
|
+
}
|
|
30
|
+
await super.preview();
|
|
31
|
+
}
|
|
32
|
+
// The highlight layers go with the rest of the preview, since they're tracked in layerIds,
|
|
33
|
+
// but the extra source is ours to clean up
|
|
34
|
+
async clearPreview() {
|
|
35
|
+
await super.clearPreview();
|
|
36
|
+
if (this.map.getSource(this.highlightSourceId)) {
|
|
37
|
+
this.map.removeSource(this.highlightSourceId);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Outline the given features on top of the tiles, replacing any previous highlight
|
|
41
|
+
highlightFeatures(features) {
|
|
42
|
+
const data = {
|
|
43
|
+
type: 'FeatureCollection',
|
|
44
|
+
// A server can answer with attributes and no geometry, which we have nothing to draw for
|
|
45
|
+
features: features.filter(feature => feature.geometry).map(({ id, geometry }) => ({ type: 'Feature', id, geometry, properties: {} })),
|
|
46
|
+
};
|
|
47
|
+
this.highlightSource?.setData(data);
|
|
48
|
+
}
|
|
49
|
+
// Drop the highlight, leaving the tiles alone
|
|
50
|
+
clearHighlight() {
|
|
51
|
+
this.highlightSource?.setData(NO_FEATURES);
|
|
52
|
+
}
|
|
53
|
+
// Delegate inspection to the source, which makes the GetFeatureInfo request
|
|
54
|
+
async inspect(options) {
|
|
55
|
+
return await this.resource.inspect(options);
|
|
56
|
+
}
|
|
57
|
+
// Tiles first, so the highlight draws over them
|
|
58
|
+
async createLayers() {
|
|
59
|
+
return [...(await super.createLayers()), this.createHighlightOutlineLayer(), this.createHighlightPointLayer()];
|
|
60
|
+
}
|
|
61
|
+
// Outline for selected polygons and lines
|
|
62
|
+
createHighlightOutlineLayer() {
|
|
5
63
|
return {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
64
|
+
id: `${this.highlightSourceId}-outlines`,
|
|
65
|
+
type: 'line',
|
|
66
|
+
source: this.highlightSourceId,
|
|
67
|
+
paint: {
|
|
68
|
+
'line-color': this.style.strokeSelectedColor,
|
|
69
|
+
'line-width': 2,
|
|
70
|
+
},
|
|
71
|
+
filter: ['!=', ['geometry-type'], 'Point'],
|
|
9
72
|
};
|
|
10
73
|
}
|
|
11
|
-
|
|
12
|
-
|
|
74
|
+
// Outline for selected points
|
|
75
|
+
createHighlightPointLayer() {
|
|
76
|
+
return {
|
|
77
|
+
id: `${this.highlightSourceId}-points`,
|
|
78
|
+
type: 'circle',
|
|
79
|
+
source: this.highlightSourceId,
|
|
80
|
+
paint: {
|
|
81
|
+
'circle-color': this.style.fillSelectedColor,
|
|
82
|
+
'circle-opacity': this.style.fillHighlightOpacity,
|
|
83
|
+
'circle-stroke-color': this.style.strokeSelectedColor,
|
|
84
|
+
'circle-stroke-width': 2,
|
|
85
|
+
'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 2, 12, 4],
|
|
86
|
+
},
|
|
87
|
+
filter: ['==', ['geometry-type'], 'Point'],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// The MapLibre source holding the highlighted geometry
|
|
91
|
+
get highlightSource() {
|
|
92
|
+
return this.map.getSource(this.highlightSourceId);
|
|
13
93
|
}
|
|
14
94
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "@stencil/vitest";
|
|
2
|
+
import WmsPreviewer from "./wms";
|
|
3
|
+
import WmsResource from "../resources/wms";
|
|
4
|
+
// Just enough of a MapLibre map to record what the previewer adds, removes and draws
|
|
5
|
+
class FakeMap {
|
|
6
|
+
sources = new Map();
|
|
7
|
+
layers = new Map();
|
|
8
|
+
getSource(id) {
|
|
9
|
+
const source = this.sources.get(id);
|
|
10
|
+
if (!source)
|
|
11
|
+
return undefined;
|
|
12
|
+
return { ...source, setData: (data) => (source.data = data) };
|
|
13
|
+
}
|
|
14
|
+
addSource(id, spec) {
|
|
15
|
+
this.sources.set(id, { ...spec });
|
|
16
|
+
}
|
|
17
|
+
removeSource(id) {
|
|
18
|
+
this.sources.delete(id);
|
|
19
|
+
}
|
|
20
|
+
getLayer(id) {
|
|
21
|
+
return this.layers.get(id);
|
|
22
|
+
}
|
|
23
|
+
addLayer(layer) {
|
|
24
|
+
// MapLibre refuses a layer whose source hasn't been added yet
|
|
25
|
+
if (!this.sources.has(layer.source))
|
|
26
|
+
throw new Error(`No source ${layer.source} for layer ${layer.id}`);
|
|
27
|
+
this.layers.set(layer.id, layer);
|
|
28
|
+
}
|
|
29
|
+
removeLayer(id) {
|
|
30
|
+
this.layers.delete(id);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// The previewer only reads the colors used by the highlight
|
|
34
|
+
const style = { opacity: 0.8, strokeSelectedColor: '#0a0', fillSelectedColor: '#0f0', fillHighlightOpacity: 0.8 };
|
|
35
|
+
const TRACT_GEOMETRY = {
|
|
36
|
+
type: 'MultiPolygon',
|
|
37
|
+
coordinates: [
|
|
38
|
+
[
|
|
39
|
+
[
|
|
40
|
+
[-120.4, 38.3],
|
|
41
|
+
[-120.3, 38.3],
|
|
42
|
+
[-120.3, 38.4],
|
|
43
|
+
[-120.4, 38.3],
|
|
44
|
+
],
|
|
45
|
+
],
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
// A tract as GetFeatureInfo returns it, already reprojected to degrees by the caller. These are
|
|
49
|
+
// plain objects rather than real MapGeoJSONFeatures, which is also what the caller hands over.
|
|
50
|
+
const tract = (id, geometry = TRACT_GEOMETRY) => ({ type: 'Feature', id, geometry, properties: { TRACT: '0301' } });
|
|
51
|
+
const HIGHLIGHT_SOURCE = 's7st30-wms-highlight';
|
|
52
|
+
let map;
|
|
53
|
+
let previewer;
|
|
54
|
+
beforeEach(async () => {
|
|
55
|
+
map = new FakeMap();
|
|
56
|
+
const source = new WmsResource('s7st30', 'https://geoservices.lib.berkeley.edu/geoserver/wms', { layerIds: [] });
|
|
57
|
+
previewer = new WmsPreviewer(source, map, style);
|
|
58
|
+
await previewer.preview();
|
|
59
|
+
});
|
|
60
|
+
describe('WmsPreviewer#preview', () => {
|
|
61
|
+
it('adds a highlight source alongside the tiles', () => {
|
|
62
|
+
expect([...map.sources.keys()]).toEqual(['s7st30-wms-highlight', 's7st30-wms']);
|
|
63
|
+
expect(map.sources.get(HIGHLIGHT_SOURCE)?.type).toEqual('geojson');
|
|
64
|
+
});
|
|
65
|
+
it('draws the highlight over the tiles', () => {
|
|
66
|
+
// Order is paint order: the raster layer first, then the layers that highlight over it
|
|
67
|
+
expect([...map.layers.keys()]).toEqual(['s7st30-wms', 's7st30-wms-highlight-outlines', 's7st30-wms-highlight-points']);
|
|
68
|
+
expect(previewer.layerIds).toEqual([...map.layers.keys()]);
|
|
69
|
+
});
|
|
70
|
+
it('outlines polygons and lines but circles only points', () => {
|
|
71
|
+
const outlines = map.layers.get('s7st30-wms-highlight-outlines');
|
|
72
|
+
const points = map.layers.get('s7st30-wms-highlight-points');
|
|
73
|
+
expect(outlines?.type).toEqual('line');
|
|
74
|
+
expect(points?.type).toEqual('circle');
|
|
75
|
+
// Both draw from the highlight source, not from the tiles
|
|
76
|
+
expect(outlines?.source).toEqual(HIGHLIGHT_SOURCE);
|
|
77
|
+
expect(points?.source).toEqual(HIGHLIGHT_SOURCE);
|
|
78
|
+
});
|
|
79
|
+
it('starts with nothing highlighted', () => {
|
|
80
|
+
expect(map.sources.get(HIGHLIGHT_SOURCE)?.data).toEqual({ type: 'FeatureCollection', features: [] });
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('WmsPreviewer#highlightFeatures', () => {
|
|
84
|
+
it('draws the geometry of the given features', () => {
|
|
85
|
+
previewer.highlightFeatures([tract('s7st30.18')]);
|
|
86
|
+
const data = map.sources.get(HIGHLIGHT_SOURCE)?.data;
|
|
87
|
+
expect(data.features).toHaveLength(1);
|
|
88
|
+
expect(data.features[0].id).toEqual('s7st30.18');
|
|
89
|
+
expect(data.features[0].geometry).toEqual(TRACT_GEOMETRY);
|
|
90
|
+
});
|
|
91
|
+
it('replaces the previous highlight rather than adding to it', () => {
|
|
92
|
+
previewer.highlightFeatures([tract('s7st30.18')]);
|
|
93
|
+
previewer.highlightFeatures([tract('s7st30.19')]);
|
|
94
|
+
const data = map.sources.get(HIGHLIGHT_SOURCE)?.data;
|
|
95
|
+
expect(data.features).toHaveLength(1);
|
|
96
|
+
expect(data.features[0].id).toEqual('s7st30.19');
|
|
97
|
+
});
|
|
98
|
+
it('draws every feature when several are selected', () => {
|
|
99
|
+
previewer.highlightFeatures([tract('s7st30.18'), tract('s7st30.19')]);
|
|
100
|
+
const data = map.sources.get(HIGHLIGHT_SOURCE)?.data;
|
|
101
|
+
expect(data.features.map(feature => feature.id)).toEqual(['s7st30.18', 's7st30.19']);
|
|
102
|
+
});
|
|
103
|
+
it('skips features a server returned without geometry', () => {
|
|
104
|
+
// The spec allows a null geometry, and a server can answer with attributes alone
|
|
105
|
+
previewer.highlightFeatures([tract('s7st30.18'), tract('s7st30.19', null)]);
|
|
106
|
+
const data = map.sources.get(HIGHLIGHT_SOURCE)?.data;
|
|
107
|
+
expect(data.features).toHaveLength(1);
|
|
108
|
+
expect(data.features[0].id).toEqual('s7st30.18');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('WmsPreviewer#clearHighlight', () => {
|
|
112
|
+
it('empties the highlight source but leaves the layers in place', () => {
|
|
113
|
+
previewer.highlightFeatures([tract('s7st30.18')]);
|
|
114
|
+
previewer.clearHighlight();
|
|
115
|
+
expect(map.sources.get(HIGHLIGHT_SOURCE)?.data).toEqual({ type: 'FeatureCollection', features: [] });
|
|
116
|
+
expect(map.layers.size).toEqual(3);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('WmsPreviewer#clearPreview', () => {
|
|
120
|
+
it('removes the highlight along with the tiles', async () => {
|
|
121
|
+
previewer.highlightFeatures([tract('s7st30.18')]);
|
|
122
|
+
await previewer.clearPreview();
|
|
123
|
+
expect(map.sources.size).toEqual(0);
|
|
124
|
+
expect(map.layers.size).toEqual(0);
|
|
125
|
+
expect(previewer.layerIds).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
it('can be previewed again afterwards', async () => {
|
|
128
|
+
await previewer.clearPreview();
|
|
129
|
+
await previewer.preview();
|
|
130
|
+
expect(map.sources.has(HIGHLIGHT_SOURCE)).toBe(true);
|
|
131
|
+
expect(map.layers.size).toEqual(3);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import RasterPreviewer from "./raster";
|
|
2
|
+
export default class WmtsPreviewer extends RasterPreviewer {
|
|
3
|
+
layers;
|
|
4
|
+
// WMTS sources have multiple XYZ tile URLs for fallbacks, and each layer
|
|
5
|
+
// can have different tile size, zoom range, bounds, etc.
|
|
6
|
+
async createSources() {
|
|
7
|
+
const layers = await this.resource.getLayers();
|
|
8
|
+
return layers.map(layer => ({
|
|
9
|
+
id: `${this.resource.id}-${layer.id}`,
|
|
10
|
+
type: 'raster',
|
|
11
|
+
tiles: layer.tileUrls,
|
|
12
|
+
scheme: this.resource.getScheme(),
|
|
13
|
+
tileSize: layer.tileSize,
|
|
14
|
+
minzoom: layer.minzoom,
|
|
15
|
+
maxzoom: layer.maxzoom,
|
|
16
|
+
...(layer.bounds && { bounds: layer.bounds }),
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
// One layer per source, with the same ID as the source
|
|
20
|
+
async createLayers() {
|
|
21
|
+
const layers = await this.resource.getLayers();
|
|
22
|
+
return layers.map(layer => ({
|
|
23
|
+
id: `${this.resource.id}-${layer.id}`,
|
|
24
|
+
type: 'raster',
|
|
25
|
+
source: `${this.resource.id}-${layer.id}`,
|
|
26
|
+
paint: {
|
|
27
|
+
'raster-opacity': this.opacity,
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "@stencil/vitest";
|
|
2
|
+
import WmtsPreviewer from "./wmts";
|
|
3
|
+
import WmtsResource from "../resources/wmts";
|
|
4
|
+
// Just enough of a MapLibre map to record what the previewer adds
|
|
5
|
+
class FakeMap {
|
|
6
|
+
sources = new Map();
|
|
7
|
+
layers = new Map();
|
|
8
|
+
getSource(id) {
|
|
9
|
+
return this.sources.get(id);
|
|
10
|
+
}
|
|
11
|
+
addSource(id, spec) {
|
|
12
|
+
this.sources.set(id, spec);
|
|
13
|
+
}
|
|
14
|
+
removeSource(id) {
|
|
15
|
+
this.sources.delete(id);
|
|
16
|
+
}
|
|
17
|
+
getLayer(id) {
|
|
18
|
+
return this.layers.get(id);
|
|
19
|
+
}
|
|
20
|
+
addLayer(layer) {
|
|
21
|
+
// MapLibre refuses a layer whose source hasn't been added yet
|
|
22
|
+
if (!this.sources.has(layer.source))
|
|
23
|
+
throw new Error(`No source ${layer.source} for layer ${layer.id}`);
|
|
24
|
+
this.layers.set(layer.id, layer);
|
|
25
|
+
}
|
|
26
|
+
removeLayer(id) {
|
|
27
|
+
this.layers.delete(id);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Reading the capabilities document is the resource's job and is tested there
|
|
31
|
+
class StubWmtsResource extends WmtsResource {
|
|
32
|
+
layers = [];
|
|
33
|
+
async getLayers() {
|
|
34
|
+
return this.layers;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// The previewer only reads the opacity
|
|
38
|
+
const style = { opacity: 0.8 };
|
|
39
|
+
// A layer served from several tile hosts, and one on a grid with its own size and limits
|
|
40
|
+
const LAYERS = [
|
|
41
|
+
{
|
|
42
|
+
id: 'lights',
|
|
43
|
+
title: 'Night Lights',
|
|
44
|
+
tileUrls: ['https://one.example.org/lights/{z}/{y}/{x}.png', 'https://two.example.org/lights/{z}/{y}/{x}.png'],
|
|
45
|
+
tileSize: 256,
|
|
46
|
+
minzoom: 0,
|
|
47
|
+
maxzoom: 8,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'ortho',
|
|
51
|
+
title: 'Orthophoto',
|
|
52
|
+
tileUrls: ['https://one.example.org/ortho/{z}/{y}/{x}.jpeg'],
|
|
53
|
+
tileSize: 512,
|
|
54
|
+
minzoom: 5,
|
|
55
|
+
maxzoom: 20,
|
|
56
|
+
bounds: [16.17, 48.1, 16.58, 48.33],
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
let map;
|
|
60
|
+
let resource;
|
|
61
|
+
let previewer;
|
|
62
|
+
beforeEach(async () => {
|
|
63
|
+
map = new FakeMap();
|
|
64
|
+
resource = new StubWmtsResource('night-lights', 'https://example.org/wmts/1.0.0/WMTSCapabilities.xml', { layerIds: [] });
|
|
65
|
+
resource.layers = LAYERS;
|
|
66
|
+
previewer = new WmtsPreviewer(resource, map, style);
|
|
67
|
+
await previewer.preview();
|
|
68
|
+
});
|
|
69
|
+
describe('WmtsPreviewer#preview', () => {
|
|
70
|
+
it('adds a source and a layer for each layer of the service', () => {
|
|
71
|
+
expect([...map.sources.keys()]).toEqual(['night-lights-lights', 'night-lights-ortho']);
|
|
72
|
+
expect([...map.layers.keys()]).toEqual(['night-lights-lights', 'night-lights-ortho']);
|
|
73
|
+
expect(map.layers.get('night-lights-ortho')?.source).toEqual('night-lights-ortho');
|
|
74
|
+
});
|
|
75
|
+
it('keeps every tile host the service offers', () => {
|
|
76
|
+
expect(map.sources.get('night-lights-lights')?.tiles).toEqual(LAYERS[0].tileUrls);
|
|
77
|
+
});
|
|
78
|
+
it('draws the tiles as XYZ rasters', () => {
|
|
79
|
+
const source = map.sources.get('night-lights-lights');
|
|
80
|
+
expect(source?.type).toEqual('raster');
|
|
81
|
+
expect(source?.scheme).toEqual('xyz');
|
|
82
|
+
});
|
|
83
|
+
it('carries the tile size of the grid rather than letting MapLibre default it to 512', () => {
|
|
84
|
+
expect(map.sources.get('night-lights-lights')?.tileSize).toEqual(256);
|
|
85
|
+
expect(map.sources.get('night-lights-ortho')?.tileSize).toEqual(512);
|
|
86
|
+
});
|
|
87
|
+
it('bounds the source to the zooms the grid defines', () => {
|
|
88
|
+
// Without these MapLibre keeps asking past the end of the grid instead of overzooming
|
|
89
|
+
// the deepest level it has
|
|
90
|
+
expect(map.sources.get('night-lights-lights')?.maxzoom).toEqual(8);
|
|
91
|
+
expect(map.sources.get('night-lights-ortho')?.minzoom).toEqual(5);
|
|
92
|
+
expect(map.sources.get('night-lights-ortho')?.maxzoom).toEqual(20);
|
|
93
|
+
});
|
|
94
|
+
it('bounds the source to the layer extent, and omits the key when there is none', () => {
|
|
95
|
+
// MapLibre validates the sources it's handed, so an absent extent has to be absent rather
|
|
96
|
+
// than undefined
|
|
97
|
+
expect(map.sources.get('night-lights-ortho')?.bounds).toEqual([16.17, 48.1, 16.58, 48.33]);
|
|
98
|
+
expect(map.sources.get('night-lights-lights')).not.toHaveProperty('bounds');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe('WmtsPreviewer#clearPreview', () => {
|
|
102
|
+
it('removes every source and layer it added', async () => {
|
|
103
|
+
await previewer.clearPreview();
|
|
104
|
+
expect(map.sources.size).toEqual(0);
|
|
105
|
+
expect(map.layers.size).toEqual(0);
|
|
106
|
+
expect(previewer.layerIds).toEqual([]);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import geojsonExtent from "@mapbox/geojson-extent";
|
|
2
|
-
import
|
|
2
|
+
import VectorResource from "./vector";
|
|
3
3
|
import { fetchOrThrow } from "../errors";
|
|
4
|
-
export default class
|
|
4
|
+
export default class GeoJsonResource extends VectorResource {
|
|
5
5
|
// Data parsed from GeoJSON document
|
|
6
6
|
data;
|
|
7
7
|
// Fetch and memoize data
|
|
@@ -15,7 +15,7 @@ export default class GeoJSONSource extends VectorSource {
|
|
|
15
15
|
label() {
|
|
16
16
|
return 'GeoJSON';
|
|
17
17
|
}
|
|
18
|
-
// GeoJSON is always a vector
|
|
18
|
+
// GeoJSON is always a vector resource
|
|
19
19
|
async isVector() {
|
|
20
20
|
return true;
|
|
21
21
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import IIIFResource from "./iiif";
|
|
2
2
|
import { fetchOrThrow } from "../errors";
|
|
3
3
|
// A manifest containing multiple IIIF image URLs for preview
|
|
4
|
-
export default class
|
|
4
|
+
export default class IIIFManifestResource extends IIIFResource {
|
|
5
5
|
// The parsed manifest contents
|
|
6
6
|
manifest;
|
|
7
7
|
label() {
|
|
@@ -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
|
}
|