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.
Files changed (114) hide show
  1. package/dist/cjs/{lerc-Df0hDWkS.js → lerc-XQuzL9p2.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-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
  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-image/ogm-image.js +14 -14
  7. package/dist/collection/components/ogm-map/ogm-map.js +144 -52
  8. package/dist/collection/components/ogm-menubar/ogm-menubar.js +23 -2
  9. package/dist/collection/components/ogm-menubar/ogm-menubar.test.js +34 -0
  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 +30 -28
  13. package/dist/collection/components/ogm-previews/ogm-previews.test.js +3 -3
  14. package/dist/collection/components/ogm-viewer/ogm-viewer.css +6 -2
  15. package/dist/collection/components/ogm-viewer/ogm-viewer.js +29 -18
  16. package/dist/collection/lib/geometry.js +35 -1
  17. package/dist/collection/lib/geometry.test.js +90 -1
  18. package/dist/collection/lib/previewers/cog-deck.js +4 -4
  19. package/dist/collection/lib/previewers/cog.js +11 -11
  20. package/dist/collection/lib/previewers/geojson.js +10 -7
  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 +9 -9
  24. package/dist/collection/lib/previewers/pmtiles-vector.js +12 -51
  25. package/dist/collection/lib/previewers/previewer.js +4 -3
  26. package/dist/collection/lib/previewers/raster.js +15 -11
  27. package/dist/collection/lib/previewers/tiled-vector.js +47 -0
  28. package/dist/collection/lib/previewers/tilejson-raster.js +27 -0
  29. package/dist/collection/lib/previewers/tilejson-vector.js +18 -0
  30. package/dist/collection/lib/previewers/tilejson.test.js +140 -0
  31. package/dist/collection/lib/previewers/vector.js +7 -4
  32. package/dist/collection/lib/previewers/wms.js +86 -6
  33. package/dist/collection/lib/previewers/wms.test.js +133 -0
  34. package/dist/collection/lib/previewers/wmts.js +31 -0
  35. package/dist/collection/lib/previewers/wmts.test.js +108 -0
  36. package/dist/collection/lib/{sources → resources}/cog.js +2 -2
  37. package/dist/collection/lib/{sources → resources}/geojson.js +3 -3
  38. package/dist/collection/lib/{sources → resources}/iiif-manifest.js +2 -2
  39. package/dist/collection/lib/{sources → resources}/iiif-manifest.test.js +3 -3
  40. package/dist/collection/lib/{sources → resources}/iiif.js +2 -2
  41. package/dist/collection/lib/{sources/maplibre.js → resources/map.js} +2 -2
  42. package/dist/collection/lib/{sources → resources}/openindexmap.js +2 -2
  43. package/dist/collection/lib/{sources → resources}/pmtiles.js +2 -2
  44. package/dist/collection/lib/{sources → resources}/raster.js +2 -2
  45. package/dist/collection/lib/{sources/source.js → resources/resource.js} +3 -3
  46. package/dist/collection/lib/{sources → resources}/tilejson.js +21 -4
  47. package/dist/collection/lib/{sources → resources}/tms.js +2 -2
  48. package/dist/collection/lib/{sources → resources}/vector.js +2 -2
  49. package/dist/collection/lib/{sources → resources}/wms.js +46 -6
  50. package/dist/collection/lib/resources/wms.test.js +63 -0
  51. package/dist/collection/lib/resources/wmts.js +317 -0
  52. package/dist/collection/lib/resources/wmts.test.js +235 -0
  53. package/dist/collection/lib/{sources → resources}/xyz.js +2 -2
  54. package/dist/esm/{lerc-CB_Jcu33.js → lerc-BxCv6y50.js} +1 -1
  55. package/dist/esm/loader.js +1 -1
  56. 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
  57. package/dist/esm/ogm-alerts_11.entry.js +1 -1
  58. package/dist/esm/ogm-viewer.js +1 -1
  59. package/dist/ogm-viewer/ogm-viewer.esm.js +1 -1
  60. package/dist/ogm-viewer/p-7EhN80oi.js +2822 -0
  61. package/dist/ogm-viewer/{p-DqRDI3AC.js → p-BO_nFj5K.js} +1 -1
  62. package/dist/ogm-viewer/{p-7936ccc9.entry.js → p-c9af56a3.entry.js} +1 -1
  63. package/dist/types/components/ogm-image/ogm-image.d.ts +2 -2
  64. package/dist/types/components/ogm-map/ogm-map.d.ts +13 -9
  65. package/dist/types/components/ogm-menubar/ogm-menubar.d.ts +2 -1
  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/ogm-viewer/ogm-viewer.d.ts +1 -0
  69. package/dist/types/components.d.ts +24 -6
  70. package/dist/types/lib/geometry.d.ts +5 -1
  71. package/dist/types/lib/previewers/cog-deck.d.ts +3 -3
  72. package/dist/types/lib/previewers/cog.d.ts +6 -6
  73. package/dist/types/lib/previewers/geojson.d.ts +7 -4
  74. package/dist/types/lib/previewers/{maplibre.d.ts → map.d.ts} +10 -7
  75. package/dist/types/lib/previewers/openindexmap.d.ts +2 -2
  76. package/dist/types/lib/previewers/pmtiles-raster.d.ts +2 -3
  77. package/dist/types/lib/previewers/pmtiles-vector.d.ts +4 -11
  78. package/dist/types/lib/previewers/previewer.d.ts +3 -3
  79. package/dist/types/lib/previewers/raster.d.ts +10 -7
  80. package/dist/types/lib/previewers/tiled-vector.d.ts +11 -0
  81. package/dist/types/lib/previewers/tilejson-raster.d.ts +8 -0
  82. package/dist/types/lib/previewers/tilejson-vector.d.ts +6 -0
  83. package/dist/types/lib/previewers/vector.d.ts +9 -5
  84. package/dist/types/lib/previewers/wms.d.ts +17 -5
  85. package/dist/types/lib/previewers/wms.test.d.ts +1 -0
  86. package/dist/types/lib/previewers/wmts.d.ts +11 -0
  87. package/dist/types/lib/previewers/wmts.test.d.ts +1 -0
  88. package/dist/types/lib/{sources → resources}/cog.d.ts +2 -2
  89. package/dist/types/lib/{sources → resources}/geojson.d.ts +2 -2
  90. package/dist/types/lib/{sources → resources}/iiif-manifest.d.ts +2 -2
  91. package/dist/types/lib/resources/iiif-manifest.test.d.ts +1 -0
  92. package/dist/types/lib/resources/iiif.d.ts +5 -0
  93. package/dist/types/lib/{sources/maplibre.d.ts → resources/map.d.ts} +2 -2
  94. package/dist/types/lib/resources/openindexmap.d.ts +5 -0
  95. package/dist/types/lib/{sources → resources}/pmtiles.d.ts +2 -2
  96. package/dist/types/lib/{sources → resources}/raster.d.ts +2 -2
  97. package/dist/types/lib/{sources/source.d.ts → resources/resource.d.ts} +1 -1
  98. package/dist/types/lib/{sources → resources}/tilejson.d.ts +5 -2
  99. package/dist/types/lib/resources/tms.d.ts +5 -0
  100. package/dist/types/lib/{sources → resources}/vector.d.ts +2 -2
  101. package/dist/types/lib/{sources → resources}/wms.d.ts +15 -5
  102. package/dist/types/lib/resources/wms.test.d.ts +1 -0
  103. package/dist/types/lib/resources/wmts.d.ts +59 -0
  104. package/dist/types/lib/resources/wmts.test.d.ts +1 -0
  105. package/dist/types/lib/resources/xyz.d.ts +5 -0
  106. package/package.json +1 -1
  107. package/readme.md +1 -0
  108. package/dist/collection/lib/previewers/maplibre.js +0 -56
  109. package/dist/ogm-viewer/p-CWtUrqsM.js +0 -2822
  110. package/dist/types/lib/sources/iiif.d.ts +0 -5
  111. package/dist/types/lib/sources/openindexmap.d.ts +0 -5
  112. package/dist/types/lib/sources/tms.d.ts +0 -5
  113. package/dist/types/lib/sources/xyz.d.ts +0 -5
  114. /package/dist/types/lib/{sources/iiif-manifest.test.d.ts → previewers/tilejson.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,36 +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
- // Render a blank map if there is no record or no sources
52
- if (!this.record || !this.sources.length)
53
- return h("ogm-map", { theme: this.theme, padding: this.sidebarPadding });
54
- 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 })))))));
54
+ if (!this.record || !this.resources.length)
55
+ return;
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 })))))));
55
57
  }
56
58
  static get is() { return "ogm-previews"; }
57
59
  static get encapsulation() { return "shadow"; }
@@ -150,7 +152,7 @@ export class OgmPreviews {
150
152
  }
151
153
  static get states() {
152
154
  return {
153
- "sources": {}
155
+ "resources": {}
154
156
  };
155
157
  }
156
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);
@@ -65,10 +65,10 @@ describe('ogm-previews', () => {
65
65
  expect(tabs).toHaveLength(1);
66
66
  expect(tabs[0].textContent).toContain('WMS');
67
67
  });
68
- it('falls back to an empty map for a record with no previewable sources', async () => {
68
+ it('renders nothing for a record with no previewable sources', async () => {
69
69
  const shadowRoot = await renderPreviews(buildRecord(false));
70
70
  expect(shadowRoot.querySelector('wa-tab-group')).toBeNull();
71
- expect(shadowRoot.querySelector('ogm-map')).not.toBeNull();
71
+ expect(shadowRoot.querySelectorAll('wa-tab')).toHaveLength(0);
72
72
  });
73
73
  it('wraps a map source in an ogm-preview that renders the map', async () => {
74
74
  const previewShadow = await renderPreviewChild(buildRecordWith({ 'http://geojson.org/geojson-spec.html': 'https://example.com/data.json' }));
@@ -1,10 +1,14 @@
1
1
  :host {
2
2
  --stanford-cardinal-red: #8f1414;
3
- --accent-color: var(--wa-color-brand-50);
4
- /* --accent-color: var(--stanford-cardinal-red); */
5
3
  }
6
4
 
7
5
  .container {
6
+ /* Defined here rather than on :host because it resolves a Web Awesome token, which is only
7
+ available on .container (where the palette/theme stylesheet is activated by the
8
+ wa-palette-default / wa-light|wa-dark classes) and its descendants. */
9
+ --accent-color: var(--wa-color-brand-50);
10
+ /* --accent-color: var(--stanford-cardinal-red); */
11
+
8
12
  display: flex;
9
13
  flex-direction: column;
10
14
  position: relative;
@@ -8,23 +8,6 @@ setBasePath(getAssetPath(''));
8
8
  registerIconLibrary('default', {
9
9
  resolver: name => getBasePath(`assets/icons/${name}.svg`),
10
10
  });
11
- // Web Awesome's raw color palette is scoped to `:root`, which can only ever match the actual
12
- // document root - never an element inside a shadow tree, so a component-scoped @import can't
13
- // activate it. Loading it here, at the document level, lets it match `:root` for real, and lets
14
- // its `.wa-light`/`.wa-dark` theme rules match Host element in each component because that element
15
- // is part of the page light DOM and not the shadow DOM. Injecting it here means consumers
16
- // don't have to remember to add it to their own page.
17
- function loadWebAwesomeStylesheet() {
18
- const id = 'ogm-viewer-webawesome-styles';
19
- if (document.getElementById(id))
20
- return;
21
- const link = document.createElement('link');
22
- link.id = id;
23
- link.rel = 'stylesheet';
24
- link.href = getBasePath('assets/webawesome/styles/webawesome.css');
25
- document.head.appendChild(link);
26
- }
27
- loadWebAwesomeStylesheet();
28
11
  // Import all required Web Awesome components
29
12
  import "@awesome.me/webawesome/dist/components/button/button.js";
30
13
  import "@awesome.me/webawesome/dist/components/callout/callout.js";
@@ -36,10 +19,16 @@ import "@awesome.me/webawesome/dist/components/tab-group/tab-group.js";
36
19
  import "@awesome.me/webawesome/dist/components/tab-panel/tab-panel.js";
37
20
  import "@awesome.me/webawesome/dist/components/tab/tab.js";
38
21
  import "@awesome.me/webawesome/dist/components/tooltip/tooltip.js";
22
+ // Web Awesome activates its palette and semantic color variants on `:root` by default (with
23
+ // `.wa-*` classes only needed to override the defaults). `:root` never matches inside a shadow
24
+ // tree, so we opt into each default explicitly on our container to reproduce what a document-level
25
+ // load would give us: the default palette plus the default hue for every color variant.
26
+ const WA_SCOPE = 'wa-palette-default wa-brand-blue wa-neutral-gray wa-success-green wa-warning-yellow wa-danger-red';
39
27
  export class OgmViewer {
40
28
  el;
41
29
  recordUrl;
42
30
  theme = this.getThemePreference();
31
+ hideTitle = false;
43
32
  record;
44
33
  error;
45
34
  previewOpacity = 100;
@@ -103,8 +92,10 @@ export class OgmViewer {
103
92
  return undefined;
104
93
  }
105
94
  }
95
+ // Link Web Awesome's stylesheet inside the shadow tree so it doesn't
96
+ // leak into the host page when rendering.
106
97
  render() {
107
- return (h(Host, { key: '6fae950d74eac18eca8192c3ab22d27cde8f8215', class: `wa-${this.theme}` }, h("div", { key: 'ddaf990f2145b165c24ded5272ec3f0f11d32df1', class: "container" }, h("ogm-menubar", { key: '93de36998fdfe07aaea28ee1d32512d129402ae6', theme: this.theme, record: this.record, loading: this.loading }), h("div", { key: 'cafd7e0862a8c39008caee372da2e79b48df2aec', class: "main-container" }, h("ogm-sidebar", { key: '03b5f864d35a385de6941f3eac9b1da050422eb9', theme: this.theme, record: this.record, open: this.sidebarOpen }), this.error ? (h("ogm-alerts", { theme: this.theme, error: this.error })) : (h("ogm-previews", { theme: this.theme, record: this.record, "preview-opacity": this.previewOpacity, "sidebar-padding": this.sidebarPadding }))))));
98
+ return (h(Host, { key: '042eb882fe5979453b3ea32c11dd9a95dfff6cc1', class: `wa-${this.theme}` }, h("link", { key: '510b4ccabdff543fd3724d9f00fcce2fc0da59e4', rel: "stylesheet", href: getBasePath('assets/webawesome/styles/themes/default.css') }), h("div", { key: 'ca0916da0a4e14fc6dbda29bc0ed7fcbab4389fa', class: `container ${WA_SCOPE} wa-${this.theme}` }, h("ogm-menubar", { key: 'e153abb59c5aa5858e723f1496916517c69a6f79', theme: this.theme, record: this.record, loading: this.loading, hideTitle: this.hideTitle }), h("div", { key: 'e83ad788ed7a7da1cbe6771184ba821fedebf868', class: "main-container" }, h("ogm-sidebar", { key: '664f9ad8f2b64f998e6fe2a67116413a6d1ac377', theme: this.theme, record: this.record, open: this.sidebarOpen }), this.error ? (h("ogm-alerts", { theme: this.theme, error: this.error })) : (h("ogm-previews", { theme: this.theme, record: this.record, "preview-opacity": this.previewOpacity, "sidebar-padding": this.sidebarPadding }))))));
108
99
  }
109
100
  static get is() { return "ogm-viewer"; }
110
101
  static get encapsulation() { return "shadow"; }
@@ -158,6 +149,26 @@ export class OgmViewer {
158
149
  "reflect": false,
159
150
  "attribute": "theme",
160
151
  "defaultValue": "this.getThemePreference()"
152
+ },
153
+ "hideTitle": {
154
+ "type": "boolean",
155
+ "mutable": false,
156
+ "complexType": {
157
+ "original": "boolean",
158
+ "resolved": "boolean",
159
+ "references": {}
160
+ },
161
+ "required": false,
162
+ "optional": false,
163
+ "docs": {
164
+ "tags": [],
165
+ "text": ""
166
+ },
167
+ "getter": false,
168
+ "setter": false,
169
+ "reflect": false,
170
+ "attribute": "hide-title",
171
+ "defaultValue": "false"
161
172
  }
162
173
  };
163
174
  }
@@ -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,19 @@ 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
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
- }
21
- getSourceId() {
22
- return `${this.source.id}-cog`;
14
+ async createSources() {
15
+ return [
16
+ {
17
+ id: `${this.resource.id}-cog`,
18
+ type: 'raster',
19
+ url: await this.resource.getMapLibreSourceUrl(),
20
+ tileSize: this.resource.getTileSize(),
21
+ },
22
+ ];
23
23
  }
24
24
  }
@@ -1,10 +1,13 @@
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
+ async createSources() {
4
+ return [
5
+ {
6
+ id: `${this.resource.id}-geojson`,
7
+ type: await this.resource.getMapLibreSourceType(),
8
+ data: await this.resource.getMapLibreSourceUrl(),
9
+ generateId: true, // autogenerate feature IDs for labeling
10
+ },
11
+ ];
9
12
  }
10
13
  }
@@ -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,17 @@
1
1
  import RasterPreviewer from "./raster";
2
2
  export default class PMTilesRasterPreviewer extends RasterPreviewer {
3
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
- }
10
- getSourceId() {
11
- return `${this.source.id}-pmtiles`;
4
+ async createSources() {
5
+ return [
6
+ {
7
+ id: `${this.resource.id}-pmtiles`,
8
+ type: 'raster',
9
+ url: await this.resource.getMapLibreSourceUrl(),
10
+ },
11
+ ];
12
12
  }
13
13
  // Raster PMTiles have bounds info in the header
14
14
  async getBounds() {
15
- return await this.source.getBounds();
15
+ return await this.resource.getBounds();
16
16
  }
17
17
  }
@@ -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
  }
@@ -1,6 +1,7 @@
1
+ // Base class for rendering a resource into a preview
1
2
  export default class Previewer {
2
- source;
3
- constructor(source) {
4
- this.source = source;
3
+ resource;
4
+ constructor(resource) {
5
+ this.resource = resource;
5
6
  }
6
7
  }
@@ -1,17 +1,21 @@
1
- import MapLibrePreviewer from "./maplibre";
2
- export default class RasterPreviewer extends MapLibrePreviewer {
3
- async createSource() {
4
- return {
5
- type: 'raster',
6
- tiles: [await this.source.getMapLibreSourceUrl()],
7
- scheme: this.source.getScheme(),
8
- tileSize: this.source.getTileSize(),
9
- };
1
+ import MapPreviewer from "./map";
2
+ export default class RasterPreviewer extends MapPreviewer {
3
+ // By default, rasters only have one source
4
+ async createSources() {
5
+ return [
6
+ {
7
+ id: this.getSourceId(),
8
+ type: 'raster',
9
+ tiles: [await this.resource.getMapLibreSourceUrl()],
10
+ scheme: this.resource.getScheme(),
11
+ tileSize: this.resource.getTileSize(),
12
+ },
13
+ ];
10
14
  }
11
15
  getSourceId() {
12
- return `${this.source.id}-${this.source.getScheme()}`;
16
+ return `${this.resource.id}-${this.resource.getScheme()}`;
13
17
  }
14
- // Rasters only have one layer
18
+ // Rasters only have one layer of their own; subclasses may add companion layers after it
15
19
  async createLayers() {
16
20
  return [
17
21
  {