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.
Files changed (117) hide show
  1. package/dist/cjs/{lerc-IU-tWkVO.js → lerc-DQmE0Pfk.js} +1 -1
  2. package/dist/cjs/loader.cjs.js +1 -1
  3. 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
  4. package/dist/cjs/ogm-alerts_11.cjs.entry.js +1 -1
  5. package/dist/cjs/ogm-viewer.cjs.js +1 -1
  6. package/dist/collection/components/ogm-attributes/ogm-attributes.js +1 -1
  7. package/dist/collection/components/ogm-attributes/ogm-attributes.test.js +24 -0
  8. package/dist/collection/components/ogm-image/ogm-image.js +14 -14
  9. package/dist/collection/components/ogm-map/ogm-map.js +135 -53
  10. package/dist/collection/components/ogm-preview/ogm-preview.js +13 -13
  11. package/dist/collection/components/ogm-preview/ogm-preview.test.js +5 -5
  12. package/dist/collection/components/ogm-previews/ogm-previews.js +29 -26
  13. package/dist/collection/components/ogm-previews/ogm-previews.test.js +1 -1
  14. package/dist/collection/lib/geometry.js +35 -1
  15. package/dist/collection/lib/geometry.test.js +90 -1
  16. package/dist/collection/lib/previewers/cog-deck.js +4 -4
  17. package/dist/collection/lib/previewers/cog.js +15 -11
  18. package/dist/collection/lib/previewers/cog.test.js +61 -0
  19. package/dist/collection/lib/previewers/geojson.js +15 -7
  20. package/dist/collection/lib/previewers/geojson.test.js +81 -0
  21. package/dist/collection/lib/previewers/map.js +51 -0
  22. package/dist/collection/lib/previewers/openindexmap.js +2 -2
  23. package/dist/collection/lib/previewers/pmtiles-raster.js +13 -9
  24. package/dist/collection/lib/previewers/pmtiles-vector.js +12 -51
  25. package/dist/collection/lib/previewers/pmtiles.test.js +62 -0
  26. package/dist/collection/lib/previewers/previewer.js +4 -3
  27. package/dist/collection/lib/previewers/raster.js +15 -11
  28. package/dist/collection/lib/previewers/tiled-vector.js +47 -0
  29. package/dist/collection/lib/previewers/tilejson-raster.js +27 -0
  30. package/dist/collection/lib/previewers/tilejson-vector.js +18 -0
  31. package/dist/collection/lib/previewers/tilejson.test.js +140 -0
  32. package/dist/collection/lib/previewers/vector.js +7 -4
  33. package/dist/collection/lib/previewers/wms.js +86 -6
  34. package/dist/collection/lib/previewers/wms.test.js +133 -0
  35. package/dist/collection/lib/previewers/wmts.js +31 -0
  36. package/dist/collection/lib/previewers/wmts.test.js +108 -0
  37. package/dist/collection/lib/{sources → resources}/cog.js +2 -2
  38. package/dist/collection/lib/{sources → resources}/geojson.js +3 -3
  39. package/dist/collection/lib/{sources → resources}/iiif-manifest.js +2 -2
  40. package/dist/collection/lib/{sources → resources}/iiif-manifest.test.js +3 -3
  41. package/dist/collection/lib/{sources → resources}/iiif.js +2 -2
  42. package/dist/collection/lib/{sources/maplibre.js → resources/map.js} +2 -2
  43. package/dist/collection/lib/{sources → resources}/openindexmap.js +2 -2
  44. package/dist/collection/lib/{sources → resources}/pmtiles.js +2 -2
  45. package/dist/collection/lib/{sources → resources}/raster.js +2 -2
  46. package/dist/collection/lib/{sources/source.js → resources/resource.js} +3 -3
  47. package/dist/collection/lib/{sources → resources}/tilejson.js +21 -4
  48. package/dist/collection/lib/{sources → resources}/tms.js +2 -2
  49. package/dist/collection/lib/{sources → resources}/vector.js +2 -2
  50. package/dist/collection/lib/{sources → resources}/wms.js +46 -6
  51. package/dist/collection/lib/resources/wms.test.js +63 -0
  52. package/dist/collection/lib/resources/wmts.js +317 -0
  53. package/dist/collection/lib/resources/wmts.test.js +235 -0
  54. package/dist/collection/lib/{sources → resources}/xyz.js +2 -2
  55. package/dist/esm/{lerc-yC3LNu-h.js → lerc-BQHa9iiK.js} +1 -1
  56. package/dist/esm/loader.js +1 -1
  57. 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
  58. package/dist/esm/ogm-alerts_11.entry.js +1 -1
  59. package/dist/esm/ogm-viewer.js +1 -1
  60. package/dist/ogm-viewer/ogm-viewer.esm.js +1 -1
  61. package/dist/ogm-viewer/p-667JHO1P.js +2822 -0
  62. package/dist/ogm-viewer/{p-_eTjrSO2.js → p-9T5uP6M8.js} +1 -1
  63. package/dist/ogm-viewer/{p-11cf948e.entry.js → p-c95acce0.entry.js} +1 -1
  64. package/dist/types/components/ogm-image/ogm-image.d.ts +2 -2
  65. package/dist/types/components/ogm-map/ogm-map.d.ts +12 -9
  66. package/dist/types/components/ogm-preview/ogm-preview.d.ts +2 -2
  67. package/dist/types/components/ogm-previews/ogm-previews.d.ts +2 -2
  68. package/dist/types/components.d.ts +6 -6
  69. package/dist/types/lib/geometry.d.ts +5 -1
  70. package/dist/types/lib/previewers/cog-deck.d.ts +3 -3
  71. package/dist/types/lib/previewers/cog.d.ts +6 -5
  72. package/dist/types/lib/previewers/cog.test.d.ts +1 -0
  73. package/dist/types/lib/previewers/geojson.d.ts +8 -4
  74. package/dist/types/lib/previewers/geojson.test.d.ts +1 -0
  75. package/dist/types/lib/previewers/{maplibre.d.ts → map.d.ts} +10 -7
  76. package/dist/types/lib/previewers/openindexmap.d.ts +2 -2
  77. package/dist/types/lib/previewers/pmtiles-raster.d.ts +2 -2
  78. package/dist/types/lib/previewers/pmtiles-vector.d.ts +4 -11
  79. package/dist/types/lib/previewers/pmtiles.test.d.ts +1 -0
  80. package/dist/types/lib/previewers/previewer.d.ts +3 -3
  81. package/dist/types/lib/previewers/raster.d.ts +10 -7
  82. package/dist/types/lib/previewers/tiled-vector.d.ts +11 -0
  83. package/dist/types/lib/previewers/tilejson-raster.d.ts +8 -0
  84. package/dist/types/lib/previewers/tilejson-vector.d.ts +6 -0
  85. package/dist/types/lib/previewers/tilejson.test.d.ts +1 -0
  86. package/dist/types/lib/previewers/vector.d.ts +9 -5
  87. package/dist/types/lib/previewers/wms.d.ts +17 -5
  88. package/dist/types/lib/previewers/wms.test.d.ts +1 -0
  89. package/dist/types/lib/previewers/wmts.d.ts +11 -0
  90. package/dist/types/lib/previewers/wmts.test.d.ts +1 -0
  91. package/dist/types/lib/{sources → resources}/cog.d.ts +2 -2
  92. package/dist/types/lib/{sources → resources}/geojson.d.ts +2 -2
  93. package/dist/types/lib/{sources → resources}/iiif-manifest.d.ts +2 -2
  94. package/dist/types/lib/resources/iiif-manifest.test.d.ts +1 -0
  95. package/dist/types/lib/resources/iiif.d.ts +5 -0
  96. package/dist/types/lib/{sources/maplibre.d.ts → resources/map.d.ts} +2 -2
  97. package/dist/types/lib/resources/openindexmap.d.ts +5 -0
  98. package/dist/types/lib/{sources → resources}/pmtiles.d.ts +2 -2
  99. package/dist/types/lib/{sources → resources}/raster.d.ts +2 -2
  100. package/dist/types/lib/{sources/source.d.ts → resources/resource.d.ts} +1 -1
  101. package/dist/types/lib/{sources → resources}/tilejson.d.ts +5 -2
  102. package/dist/types/lib/resources/tms.d.ts +5 -0
  103. package/dist/types/lib/{sources → resources}/vector.d.ts +2 -2
  104. package/dist/types/lib/{sources → resources}/wms.d.ts +15 -5
  105. package/dist/types/lib/resources/wms.test.d.ts +1 -0
  106. package/dist/types/lib/resources/wmts.d.ts +59 -0
  107. package/dist/types/lib/resources/wmts.test.d.ts +1 -0
  108. package/dist/types/lib/resources/xyz.d.ts +5 -0
  109. package/package.json +1 -1
  110. package/readme.md +1 -1
  111. package/dist/collection/lib/previewers/maplibre.js +0 -56
  112. package/dist/ogm-viewer/p-DPOGJxir.js +0 -2822
  113. package/dist/types/lib/sources/iiif.d.ts +0 -5
  114. package/dist/types/lib/sources/openindexmap.d.ts +0 -5
  115. package/dist/types/lib/sources/tms.d.ts +0 -5
  116. package/dist/types/lib/sources/xyz.d.ts +0 -5
  117. /package/dist/types/{lib/sources/iiif-manifest.test.d.ts → components/ogm-attributes/ogm-attributes.test.d.ts} +0 -0
@@ -1,20 +1,21 @@
1
1
  import { h, Host } from "@stencil/core";
2
- import CogSource from "../../lib/sources/cog";
3
- import GeoJSONSource from "../../lib/sources/geojson";
4
- import OpenIndexMapSource from "../../lib/sources/openindexmap";
5
- import PMTilesSource from "../../lib/sources/pmtiles";
6
- import TileJSONSource from "../../lib/sources/tilejson";
7
- import TMSSource from "../../lib/sources/tms";
8
- import WmsSource from "../../lib/sources/wms";
9
- import XYZSource from "../../lib/sources/xyz";
10
- import IIIFSource from "../../lib/sources/iiif";
11
- import IIIFManifestSource from "../../lib/sources/iiif-manifest";
2
+ import CogResource from "../../lib/resources/cog";
3
+ import GeoJsonResource from "../../lib/resources/geojson";
4
+ import OpenIndexMapResource from "../../lib/resources/openindexmap";
5
+ import PMTilesResource from "../../lib/resources/pmtiles";
6
+ import TileJsonResource from "../../lib/resources/tilejson";
7
+ import TmsResource from "../../lib/resources/tms";
8
+ import WmsResource from "../../lib/resources/wms";
9
+ import WmtsResource from "../../lib/resources/wmts";
10
+ import XyzResource from "../../lib/resources/xyz";
11
+ import IIIFResource from "../../lib/resources/iiif";
12
+ import IIIFManifestResource from "../../lib/resources/iiif-manifest";
12
13
  export class OgmPreviews {
13
14
  theme;
14
15
  record;
15
16
  previewOpacity;
16
17
  sidebarPadding;
17
- sources = [];
18
+ resources = [];
18
19
  // @Watch only fires on changes; handle the initial load here
19
20
  componentWillLoad() {
20
21
  if (this.record)
@@ -22,35 +23,37 @@ export class OgmPreviews {
22
23
  }
23
24
  // Given a record, get all of the valid sources that can be used to preview it on a map
24
25
  getSources(record) {
25
- while (this.sources.length)
26
- this.sources.pop();
26
+ while (this.resources.length)
27
+ this.resources.pop();
27
28
  const recordBounds = record.getBounds();
28
29
  if (record.references.iiifImageUrl)
29
- this.sources.push(new IIIFSource(record.id, record.references.iiifImageUrl, recordBounds));
30
+ this.resources.push(new IIIFResource(record.id, record.references.iiifImageUrl, recordBounds));
30
31
  if (record.references.iiifManifestUrl)
31
- this.sources.push(new IIIFManifestSource(record.id, record.references.iiifManifestUrl, recordBounds));
32
+ this.resources.push(new IIIFManifestResource(record.id, record.references.iiifManifestUrl, recordBounds));
32
33
  if (record.references.pmtilesUrl)
33
- this.sources.push(new PMTilesSource(record.id, record.references.pmtilesUrl, recordBounds));
34
+ this.resources.push(new PMTilesResource(record.id, record.references.pmtilesUrl, recordBounds));
34
35
  if (record.references.tilejsonUrl)
35
- this.sources.push(new TileJSONSource(record.id, record.references.tilejsonUrl, recordBounds));
36
+ this.resources.push(new TileJsonResource(record.id, record.references.tilejsonUrl, recordBounds));
36
37
  if (record.references.indexMapUrl)
37
- this.sources.push(new OpenIndexMapSource(record.id, record.references.indexMapUrl, recordBounds));
38
+ this.resources.push(new OpenIndexMapResource(record.id, record.references.indexMapUrl, recordBounds));
38
39
  if (record.references.geojsonUrl)
39
- this.sources.push(new GeoJSONSource(record.id, record.references.geojsonUrl, recordBounds));
40
+ this.resources.push(new GeoJsonResource(record.id, record.references.geojsonUrl, recordBounds));
40
41
  if (record.references.cogUrl)
41
- this.sources.push(new CogSource(record.id, record.references.cogUrl, recordBounds));
42
+ this.resources.push(new CogResource(record.id, record.references.cogUrl, recordBounds));
42
43
  if (record.references.tmsUrl)
43
- this.sources.push(new TMSSource(record.id, record.references.tmsUrl, recordBounds));
44
+ this.resources.push(new TmsResource(record.id, record.references.tmsUrl, recordBounds));
44
45
  if (record.references.xyzUrl)
45
- this.sources.push(new XYZSource(record.id, record.references.xyzUrl, recordBounds));
46
+ this.resources.push(new XyzResource(record.id, record.references.xyzUrl, recordBounds));
47
+ if (record.references.wmtsUrl && record.wxsIdentifier)
48
+ this.resources.push(new WmtsResource(record.id, record.references.wmtsUrl, { layerIds: [record.wxsIdentifier] }, recordBounds));
46
49
  if (record.references.wmsUrl && record.wxsIdentifier)
47
- this.sources.push(new WmsSource(record.id, record.references.wmsUrl, { layerIds: [record.wxsIdentifier] }, recordBounds));
50
+ this.resources.push(new WmsResource(record.id, record.references.wmsUrl, { layerIds: [record.wxsIdentifier] }, recordBounds));
48
51
  }
49
52
  // Render as tabs for switching between sources
50
53
  render() {
51
- if (!this.record || !this.sources.length)
54
+ if (!this.record || !this.resources.length)
52
55
  return;
53
- return (h(Host, { class: this.theme && `wa-${this.theme}` }, h("wa-tab-group", null, this.sources.map((source, idx) => (h("wa-tab", { key: idx, panel: `${source.constructor.name}-${source.id}-${idx}` }, source.label()))), this.sources.map((source, idx) => (h("wa-tab-panel", { key: idx, name: `${source.constructor.name}-${source.id}-${idx}`, active: idx === 0 }, h("ogm-preview", { theme: this.theme, source: source, "preview-opacity": this.previewOpacity, "sidebar-padding": this.sidebarPadding })))))));
56
+ return (h(Host, { class: this.theme && `wa-${this.theme}` }, h("wa-tab-group", null, this.resources.map((resource, idx) => (h("wa-tab", { key: idx, panel: `${resource.constructor.name}-${resource.id}-${idx}` }, resource.label()))), this.resources.map((resource, idx) => (h("wa-tab-panel", { key: idx, name: `${resource.constructor.name}-${resource.id}-${idx}`, active: idx === 0 }, h("ogm-preview", { theme: this.theme, previewResource: resource, "preview-opacity": this.previewOpacity, "sidebar-padding": this.sidebarPadding })))))));
54
57
  }
55
58
  static get is() { return "ogm-previews"; }
56
59
  static get encapsulation() { return "shadow"; }
@@ -149,7 +152,7 @@ export class OgmPreviews {
149
152
  }
150
153
  static get states() {
151
154
  return {
152
- "sources": {}
155
+ "resources": {}
153
156
  };
154
157
  }
155
158
  static get watchers() {
@@ -34,7 +34,7 @@ const renderPreviews = async (record) => {
34
34
  const flush = () => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
35
35
  // Render previews and drill into the single <ogm-preview> to see which preview (map or image) it
36
36
  // chose. Going through <ogm-previews> means the source is constructed in the built component's module
37
- // realm, so <ogm-preview>'s `instanceof IIIFSource` check holds (unlike a source built in the test).
37
+ // realm, so <ogm-preview>'s `instanceof IIIFResource` check holds (unlike a source built in the test).
38
38
  const renderPreviewChild = async (record) => {
39
39
  const container = document.createElement('div');
40
40
  document.body.appendChild(container);
@@ -1,7 +1,9 @@
1
- import { LngLatBounds } from "maplibre-gl";
1
+ import { LngLatBounds, MercatorCoordinate } from "maplibre-gl";
2
2
  import { wktToGeoJSON } from "@terraformer/wkt";
3
3
  // Regular expression to match ENVELOPE syntax in bbox strings
4
4
  export const ENVELOPE_REGEX = /^ENVELOPE\((?<west>[^,]+),(?<east>[^,]+),(?<north>[^,]+),(?<south>[^,]+)\)$/;
5
+ // EPSG:3857 spans this many meters from the origin on both axes
6
+ const MERCATOR_EXTENT = 20037508.342789244;
5
7
  // Convert LngLatBounds to GeoJSON Polygon
6
8
  export const boundsToGeoJSON = (bounds) => {
7
9
  return {
@@ -27,6 +29,38 @@ export const bboxToBounds = (bbox) => {
27
29
  const { west, east, north, south } = coords.groups;
28
30
  return new LngLatBounds([parseFloat(west), parseFloat(south)], [parseFloat(east), parseFloat(north)]);
29
31
  };
32
+ // Convert a geographic coordinate to EPSG:3857 (Web Mercator) meters
33
+ export const lngLatToMercator = (lngLat) => {
34
+ // MercatorCoordinate is normalized to 0..1, with y increasing southward
35
+ const { x, y } = MercatorCoordinate.fromLngLat(lngLat);
36
+ return [(x * 2 - 1) * MERCATOR_EXTENT, (1 - y * 2) * MERCATOR_EXTENT];
37
+ };
38
+ // Axis-aligned envelope around the given coordinates, as the minx,miny,maxx,maxy
39
+ // string in EPSG:3857 meters that OGC services expect for a BBOX parameter
40
+ export const mercatorBbox = (coords) => {
41
+ const points = coords.map(lngLatToMercator);
42
+ const xs = points.map(([x]) => x);
43
+ const ys = points.map(([, y]) => y);
44
+ return [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)].join(',');
45
+ };
46
+ // Convert an EPSG:3857 (Web Mercator) coordinate in meters back to a geographic coordinate
47
+ export const mercatorToLngLat = ([x, y]) => {
48
+ const { lng, lat } = new MercatorCoordinate((x / MERCATOR_EXTENT + 1) / 2, (1 - y / MERCATOR_EXTENT) / 2).toLngLat();
49
+ return [lng, lat];
50
+ };
51
+ const mercatorPositionsToLngLat = (coordinates) => {
52
+ if (typeof coordinates[0] === 'number')
53
+ return mercatorToLngLat(coordinates);
54
+ return coordinates.map(mercatorPositionsToLngLat);
55
+ };
56
+ // Reproject a GeoJSON geometry from EPSG:3857 meters to geographic coordinates. OGC services
57
+ // answer in the CRS the request asked for, but MapLibre sources are always in degrees.
58
+ export const mercatorGeomToLngLat = (geometry) => {
59
+ if (geometry.type === 'GeometryCollection') {
60
+ return { ...geometry, geometries: geometry.geometries.map(mercatorGeomToLngLat) };
61
+ }
62
+ return { ...geometry, coordinates: mercatorPositionsToLngLat(geometry.coordinates) };
63
+ };
30
64
  // Convert either WKT or ENVELOPE format geometry to GeoJSON
31
65
  // If WKT parsing fails, try ENVELOPE instead
32
66
  export const geomToGeoJSON = (geometry) => {
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, vi } from "@stencil/vitest";
2
2
  import { LngLatBounds } from "maplibre-gl";
3
- import { bboxToBounds, boundsToGeoJSON, geomToGeoJSON } from "./geometry";
3
+ import { bboxToBounds, boundsToGeoJSON, geomToGeoJSON, lngLatToMercator, mercatorBbox, mercatorGeomToLngLat, mercatorToLngLat } from "./geometry";
4
4
  describe('geomToGeoJSON', () => {
5
5
  it('should convert WKT to GeoJSON', () => {
6
6
  const wkt = 'POINT (-122.6764 45.5165)';
@@ -70,3 +70,92 @@ describe('boundsToGeoJSON', () => {
70
70
  });
71
71
  });
72
72
  });
73
+ describe('lngLatToMercator', () => {
74
+ it('should put the null island at the origin', () => {
75
+ expect(lngLatToMercator([0, 0])).toEqual([0, 0]);
76
+ });
77
+ it('should convert degrees to EPSG:3857 meters', () => {
78
+ const [x, y] = lngLatToMercator([-120.99553605196368, 37.83228807647538]);
79
+ expect(x).toBeCloseTo(-13469161.46, 1);
80
+ expect(y).toBeCloseTo(4555760.76, 1);
81
+ });
82
+ it('should count north and east as positive', () => {
83
+ const [x, y] = lngLatToMercator([120, 60]);
84
+ expect(x).toBeGreaterThan(0);
85
+ expect(y).toBeGreaterThan(0);
86
+ });
87
+ });
88
+ describe('mercatorBbox', () => {
89
+ it('should envelope the coordinates as minx,miny,maxx,maxy', () => {
90
+ // Corners of a query window, in the arbitrary order they were unprojected in
91
+ const bbox = mercatorBbox([
92
+ [-120.9, 38.5],
93
+ [-120.0, 38.5],
94
+ [-120.0, 37.8],
95
+ [-120.9, 37.8],
96
+ ]);
97
+ const [minX, minY, maxX, maxY] = bbox.split(',').map(Number);
98
+ expect(minX).toBeLessThan(maxX);
99
+ expect(minY).toBeLessThan(maxY);
100
+ expect([minX, minY]).toEqual(lngLatToMercator([-120.9, 37.8]));
101
+ expect([maxX, maxY]).toEqual(lngLatToMercator([-120.0, 38.5]));
102
+ });
103
+ });
104
+ describe('mercatorToLngLat', () => {
105
+ it('should place the origin at null island', () => {
106
+ expect(mercatorToLngLat([0, 0])).toEqual([0, 0]);
107
+ });
108
+ it('should invert lngLatToMercator', () => {
109
+ const [lng, lat] = mercatorToLngLat(lngLatToMercator([-120.99553605196368, 37.83228807647538]));
110
+ expect(lng).toBeCloseTo(-120.99553605196368, 9);
111
+ expect(lat).toBeCloseTo(37.83228807647538, 9);
112
+ });
113
+ it('should convert a coordinate a WMS returned in EPSG:3857 meters', () => {
114
+ // A vertex of a Calaveras County tract, as GeoServer reported it
115
+ const [lng, lat] = mercatorToLngLat([-13402032.01418895, 4622299.14920388]);
116
+ expect(lng).toBeCloseTo(-120.39250196605, 6);
117
+ expect(lat).toBeCloseTo(38.30286413751, 6);
118
+ });
119
+ });
120
+ describe('mercatorGeomToLngLat', () => {
121
+ it('should reproject a point', () => {
122
+ const geometry = mercatorGeomToLngLat({ type: 'Point', coordinates: lngLatToMercator([-120.5, 38.2]) });
123
+ expect(geometry.type).toEqual('Point');
124
+ expect(geometry.coordinates[0]).toBeCloseTo(-120.5, 9);
125
+ expect(geometry.coordinates[1]).toBeCloseTo(38.2, 9);
126
+ });
127
+ it('should reproject every ring of a nested geometry', () => {
128
+ const ring = [
129
+ [-120.9, 38.5],
130
+ [-120.0, 38.5],
131
+ [-120.0, 37.8],
132
+ [-120.9, 38.5],
133
+ ];
134
+ const geometry = mercatorGeomToLngLat({
135
+ type: 'MultiPolygon',
136
+ coordinates: [[ring.map(lngLatToMercator)]],
137
+ });
138
+ expect(geometry.type).toEqual('MultiPolygon');
139
+ geometry.coordinates[0][0].forEach(([lng, lat], index) => {
140
+ expect(lng).toBeCloseTo(ring[index][0], 9);
141
+ expect(lat).toBeCloseTo(ring[index][1], 9);
142
+ });
143
+ });
144
+ it('should reproject the members of a geometry collection', () => {
145
+ const geometry = mercatorGeomToLngLat({
146
+ type: 'GeometryCollection',
147
+ geometries: [
148
+ { type: 'Point', coordinates: [0, 0] },
149
+ { type: 'LineString', coordinates: [lngLatToMercator([-120.5, 38.2]), lngLatToMercator([-120.4, 38.3])] },
150
+ ],
151
+ });
152
+ expect(geometry.geometries).toHaveLength(2);
153
+ expect(geometry.geometries[0].coordinates).toEqual([0, 0]);
154
+ expect(geometry.geometries[1].coordinates[1][0]).toBeCloseTo(-120.4, 9);
155
+ });
156
+ it('should leave the original geometry untouched', () => {
157
+ const original = { type: 'Point', coordinates: [-13402032.01418895, 4622299.14920388] };
158
+ mercatorGeomToLngLat(original);
159
+ expect(original.coordinates).toEqual([-13402032.01418895, 4622299.14920388]);
160
+ });
161
+ });
@@ -18,8 +18,8 @@ export default class DeckCogPreviewer extends Previewer {
18
18
  // Current opacity state
19
19
  opacity;
20
20
  // Initialize with opacity at the theme's opacity value
21
- constructor(source, map, style) {
22
- super(source);
21
+ constructor(resource, map, style) {
22
+ super(resource);
23
23
  this.map = map;
24
24
  this.style = style;
25
25
  this.opacity = this.style.opacity;
@@ -39,12 +39,12 @@ export default class DeckCogPreviewer extends Previewer {
39
39
  }
40
40
  }
41
41
  getSourceId() {
42
- return `${this.source.id}-cog`;
42
+ return `${this.resource.id}-cog`;
43
43
  }
44
44
  createLayer() {
45
45
  return new COGLayer({
46
46
  id: this.getSourceId(),
47
- geotiff: this.source.getMapLibreSourceUrl(),
47
+ geotiff: this.resource.getMapLibreSourceUrl(),
48
48
  onGeoTIFFLoad: (_data, options) => {
49
49
  const { west, south, east, north } = options.geographicBounds;
50
50
  this.bounds = [
@@ -6,19 +6,23 @@ import RasterPreviewer from "./raster";
6
6
  // See: https://github.com/geomatico/maplibre-cog-protocol
7
7
  export default class CogPreviewer extends RasterPreviewer {
8
8
  // Register the 'cog://' protocol handler with MapLibre when the previewer is created
9
- constructor(source, map, style) {
10
- super(source, map, style);
9
+ constructor(resource, map, style) {
10
+ super(resource, map, style);
11
11
  maplibregl.addProtocol('cog', cogProtocol);
12
12
  }
13
- // COG sources use 'url' instead of 'tiles' and have no scheme
14
- async createSource() {
15
- return {
16
- type: 'raster',
17
- url: await this.source.getMapLibreSourceUrl(),
18
- tileSize: this.source.getTileSize(),
19
- };
20
- }
13
+ // A COG has no scheme to name it by, and the raster layer draws from this too
21
14
  getSourceId() {
22
- return `${this.source.id}-cog`;
15
+ return `${this.resource.id}-cog`;
16
+ }
17
+ // COG sources use 'url' instead of 'tiles' and have no scheme
18
+ async createSources() {
19
+ return [
20
+ {
21
+ id: this.getSourceId(),
22
+ type: 'raster',
23
+ url: await this.resource.getMapLibreSourceUrl(),
24
+ tileSize: this.resource.getTileSize(),
25
+ },
26
+ ];
23
27
  }
24
28
  }
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect } from "@stencil/vitest";
2
+ import CogPreviewer from "./cog";
3
+ import CogResource from "../resources/cog";
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
+ // The previewer only reads the opacity
31
+ const style = { opacity: 0.8 };
32
+ const COG_URL = 'https://example.com/scan.tif';
33
+ // Nothing here reads the GeoTIFF: the source is built from the URL alone
34
+ const preview = async () => {
35
+ const map = new FakeMap();
36
+ const previewer = new CogPreviewer(new CogResource('princeton-fk4544658v', COG_URL), map, style);
37
+ await previewer.preview();
38
+ return { map, previewer };
39
+ };
40
+ describe('CogPreviewer#preview', () => {
41
+ it('hands MapLibre the URL behind the cog:// protocol', async () => {
42
+ const { map, previewer } = await preview();
43
+ const source = map.sources.get('princeton-fk4544658v-cog');
44
+ expect(source.type).toEqual('raster');
45
+ expect(source.url).toEqual(`cog://${COG_URL}`);
46
+ expect(previewer.sourceIds).toEqual(['princeton-fk4544658v-cog']);
47
+ });
48
+ it('draws its raster layer from the source it added', async () => {
49
+ const { map, previewer } = await preview();
50
+ // A layer pointing at any other ID would be dropped by MapLibre, drawing nothing
51
+ expect([...map.layers.values()].every(layer => map.sources.has(layer.source))).toBe(true);
52
+ expect([...map.layers.keys()]).toEqual(['princeton-fk4544658v-cog']);
53
+ expect(previewer.layerIds).toEqual(['princeton-fk4544658v-cog']);
54
+ });
55
+ it('removes what it added when cleared', async () => {
56
+ const { map, previewer } = await preview();
57
+ await previewer.clearPreview();
58
+ expect(map.sources.size).toEqual(0);
59
+ expect(map.layers.size).toEqual(0);
60
+ });
61
+ });
@@ -1,10 +1,18 @@
1
1
  import VectorPreviewer from "./vector";
2
- export default class GeoJSONPreviewer extends VectorPreviewer {
3
- async createSource() {
4
- return {
5
- type: await this.source.getMapLibreSourceType(),
6
- data: await this.source.getMapLibreSourceUrl(),
7
- generateId: true,
8
- };
2
+ export default class GeoJsonPreviewer extends VectorPreviewer {
3
+ // A record can reference the same data more than one way, so keep the sources distinct. The
4
+ // style layers draw from this too, so both have to come from here.
5
+ getSourceId() {
6
+ return `${this.resource.id}-geojson`;
7
+ }
8
+ async createSources() {
9
+ return [
10
+ {
11
+ id: this.getSourceId(),
12
+ type: await this.resource.getMapLibreSourceType(),
13
+ data: await this.resource.getMapLibreSourceUrl(),
14
+ generateId: true, // autogenerate feature IDs for labeling
15
+ },
16
+ ];
9
17
  }
10
18
  }
@@ -0,0 +1,81 @@
1
+ import { describe, it, expect } from "@stencil/vitest";
2
+ import GeoJsonPreviewer from "./geojson";
3
+ import OpenIndexMapPreviewer from "./openindexmap";
4
+ import GeoJsonResource from "../resources/geojson";
5
+ import OpenIndexMapResource from "../resources/openindexmap";
6
+ // Just enough of a MapLibre map to record what the previewer adds
7
+ class FakeMap {
8
+ sources = new Map();
9
+ layers = new Map();
10
+ getSource(id) {
11
+ return this.sources.get(id);
12
+ }
13
+ addSource(id, spec) {
14
+ this.sources.set(id, spec);
15
+ }
16
+ removeSource(id) {
17
+ this.sources.delete(id);
18
+ }
19
+ getLayer(id) {
20
+ return this.layers.get(id);
21
+ }
22
+ addLayer(layer) {
23
+ // MapLibre refuses a layer whose source hasn't been added yet
24
+ if (!this.sources.has(layer.source))
25
+ throw new Error(`No source ${layer.source} for layer ${layer.id}`);
26
+ this.layers.set(layer.id, layer);
27
+ }
28
+ removeLayer(id) {
29
+ this.layers.delete(id);
30
+ }
31
+ }
32
+ // The previewers only read the colors and label styles, none of which these tests assert on
33
+ const style = { opacity: 0.8, fillColor: '#00f', strokeColor: '#009', textColor: '#000', textFont: 'Noto Sans Regular', textSize: 12 };
34
+ const GEOJSON_URL = 'https://example.com/index-map.json';
35
+ // Nothing here fetches: the source URL and the layer names are known without reading the document
36
+ const previewGeoJson = async () => {
37
+ const map = new FakeMap();
38
+ const previewer = new GeoJsonPreviewer(new GeoJsonResource('princeton-fk4544658v', GEOJSON_URL), map, style);
39
+ await previewer.preview();
40
+ return { map, previewer };
41
+ };
42
+ const previewIndexMap = async () => {
43
+ const map = new FakeMap();
44
+ const previewer = new OpenIndexMapPreviewer(new OpenIndexMapResource('princeton-fk4544658v', GEOJSON_URL), map, style);
45
+ await previewer.preview();
46
+ return { map, previewer };
47
+ };
48
+ const SUFFIXES = ['polygons', 'polygon-outlines', 'lines', 'points', 'polygon-labels', 'line-labels', 'point-labels'];
49
+ describe('GeoJsonPreviewer#preview', () => {
50
+ it('hands MapLibre the document URL as a geojson source', async () => {
51
+ const { map, previewer } = await previewGeoJson();
52
+ const source = map.sources.get('princeton-fk4544658v-geojson');
53
+ expect(source.type).toEqual('geojson');
54
+ expect(source.data).toEqual(GEOJSON_URL);
55
+ expect(previewer.sourceIds).toEqual(['princeton-fk4544658v-geojson']);
56
+ });
57
+ it('draws its style layers from the source it added', async () => {
58
+ const { map } = await previewGeoJson();
59
+ // A layer pointing at any other ID would be dropped by MapLibre, drawing nothing
60
+ expect([...map.layers.values()].every(layer => map.sources.has(layer.source))).toBe(true);
61
+ expect([...map.layers.keys()]).toEqual(SUFFIXES.map(suffix => `princeton-fk4544658v-geojson-geojson-${suffix}`));
62
+ });
63
+ it('removes what it added when cleared', async () => {
64
+ const { map, previewer } = await previewGeoJson();
65
+ await previewer.clearPreview();
66
+ expect(map.sources.size).toEqual(0);
67
+ expect(map.layers.size).toEqual(0);
68
+ });
69
+ });
70
+ describe('OpenIndexMapPreviewer#preview', () => {
71
+ it('draws the index map polygons from the source it added', async () => {
72
+ const { map } = await previewIndexMap();
73
+ const polygons = map.layers.get('princeton-fk4544658v-geojson-indexmap-polygons');
74
+ expect(polygons.type).toEqual('fill');
75
+ expect(map.sources.has(polygons.source)).toBe(true);
76
+ });
77
+ it('styles the one layer an index map has', async () => {
78
+ const { map } = await previewIndexMap();
79
+ expect([...map.layers.keys()]).toEqual(SUFFIXES.map(suffix => `princeton-fk4544658v-geojson-indexmap-${suffix}`));
80
+ });
81
+ });
@@ -0,0 +1,51 @@
1
+ import Previewer from "./previewer";
2
+ export default class MapPreviewer extends Previewer {
3
+ // Store reference to the map and styles
4
+ style;
5
+ map;
6
+ // Stored state for added MapLibre sources and layers to allow for cleanup
7
+ sourceIds = [];
8
+ layerIds = [];
9
+ // Current opacity state
10
+ opacity;
11
+ // Initialize with opacity at the theme's opacity value
12
+ constructor(resource, map, style) {
13
+ super(resource);
14
+ this.map = map;
15
+ this.style = style;
16
+ this.opacity = this.style.opacity;
17
+ }
18
+ // Add source and preview layers if they don't already exist
19
+ async preview() {
20
+ const sources = await this.createSources();
21
+ sources.forEach(source => {
22
+ const { id, ...sourceSpec } = source;
23
+ if (this.map.getSource(id))
24
+ return;
25
+ this.map.addSource(id, sourceSpec);
26
+ this.sourceIds.push(id);
27
+ });
28
+ const layers = await this.createLayers();
29
+ layers.forEach(layer => {
30
+ if (this.map.getLayer(layer.id))
31
+ return;
32
+ this.map.addLayer(layer);
33
+ this.layerIds.push(layer.id);
34
+ });
35
+ }
36
+ // Remove preview layers and sources
37
+ async clearPreview() {
38
+ this.layerIds.forEach(layerId => {
39
+ if (this.map.getLayer(layerId)) {
40
+ this.map.removeLayer(layerId);
41
+ }
42
+ });
43
+ this.layerIds = [];
44
+ this.sourceIds.forEach(sourceId => {
45
+ if (this.map.getSource(sourceId)) {
46
+ this.map.removeSource(sourceId);
47
+ }
48
+ });
49
+ this.sourceIds = [];
50
+ }
51
+ }
@@ -1,3 +1,3 @@
1
- import GeoJSONPreviewer from "./geojson";
2
- export default class OpenIndexMapPreviewer extends GeoJSONPreviewer {
1
+ import GeoJsonPreviewer from "./geojson";
2
+ export default class OpenIndexMapPreviewer extends GeoJsonPreviewer {
3
3
  }
@@ -1,17 +1,21 @@
1
1
  import RasterPreviewer from "./raster";
2
2
  export default class PMTilesRasterPreviewer extends RasterPreviewer {
3
- // PMTiles sources use 'url' instead of 'tiles' and have no scheme or tileSize
4
- async createSource() {
5
- return {
6
- type: 'raster',
7
- url: await this.source.getMapLibreSourceUrl(),
8
- };
9
- }
3
+ // An archive has no scheme to name it by, and the raster layer draws from this too
10
4
  getSourceId() {
11
- return `${this.source.id}-pmtiles`;
5
+ return `${this.resource.id}-pmtiles`;
6
+ }
7
+ // PMTiles sources use 'url' instead of 'tiles' and have no scheme or tileSize
8
+ async createSources() {
9
+ return [
10
+ {
11
+ id: this.getSourceId(),
12
+ type: 'raster',
13
+ url: await this.resource.getMapLibreSourceUrl(),
14
+ },
15
+ ];
12
16
  }
13
17
  // Raster PMTiles have bounds info in the header
14
18
  async getBounds() {
15
- return await this.source.getBounds();
19
+ return await this.resource.getBounds();
16
20
  }
17
21
  }
@@ -1,53 +1,14 @@
1
- import VectorPreviewer from "./vector";
2
- export default class PMTilesVectorPreviewer extends VectorPreviewer {
3
- async createSource() {
4
- return {
5
- type: 'vector',
6
- url: this.source.getMapLibreSourceUrl(),
7
- encoding: await this.source.getVectorEncoding(),
8
- };
9
- }
10
- /* Add 'source-layer' property because PMTiles can contain multiple layers */
11
- createPolygonLayer(layerId) {
12
- return {
13
- ...super.createPolygonLayer(layerId),
14
- 'source-layer': layerId,
15
- };
16
- }
17
- createPolygonOutlineLayer(layerId) {
18
- return {
19
- ...super.createPolygonOutlineLayer(layerId),
20
- 'source-layer': layerId,
21
- };
22
- }
23
- createLineLayer(layerId) {
24
- return {
25
- ...super.createLineLayer(layerId),
26
- 'source-layer': layerId,
27
- };
28
- }
29
- createPointLayer(layerId) {
30
- return {
31
- ...super.createPointLayer(layerId),
32
- 'source-layer': layerId,
33
- };
34
- }
35
- createPolygonLabelLayer(layerId) {
36
- return {
37
- ...super.createPolygonLabelLayer(layerId),
38
- 'source-layer': layerId,
39
- };
40
- }
41
- createLineLabelLayer(layerId) {
42
- return {
43
- ...super.createLineLabelLayer(layerId),
44
- 'source-layer': layerId,
45
- };
46
- }
47
- createPointLabelLayer(layerId) {
48
- return {
49
- ...super.createPointLabelLayer(layerId),
50
- 'source-layer': layerId,
51
- };
1
+ import TiledVectorPreviewer from "./tiled-vector";
2
+ export default class PMTilesVectorPreviewer extends TiledVectorPreviewer {
3
+ // Only one source for PMTiles
4
+ async createSources() {
5
+ return [
6
+ {
7
+ id: this.resource.id,
8
+ type: 'vector',
9
+ url: this.resource.getMapLibreSourceUrl(),
10
+ encoding: await this.resource.getVectorEncoding(),
11
+ },
12
+ ];
52
13
  }
53
14
  }