ogm-viewer 0.4.0 → 0.5.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 (56) hide show
  1. package/dist/cjs/{lerc-CT61sKYw.js → lerc-Df0hDWkS.js} +6 -6
  2. package/dist/cjs/loader.cjs.js +1 -1
  3. package/dist/cjs/{ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-DKxSOrMP.js → ogm-alerts.ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-preview.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-DMEfXquS.js} +446 -80
  4. package/dist/cjs/ogm-alerts_11.cjs.entry.js +18 -0
  5. package/dist/cjs/ogm-viewer.cjs.js +1 -1
  6. package/dist/collection/assets/icons/exclamation-triangle-fill.svg +3 -0
  7. package/dist/collection/collection-manifest.json +2 -0
  8. package/dist/collection/components/ogm-alerts/ogm-alerts.css +51 -0
  9. package/dist/collection/components/ogm-alerts/ogm-alerts.js +71 -0
  10. package/dist/collection/components/ogm-alerts/ogm-alerts.test.js +26 -0
  11. package/dist/collection/components/ogm-image/ogm-image.js +57 -5
  12. package/dist/collection/components/ogm-map/ogm-map.css +34 -40
  13. package/dist/collection/components/ogm-map/ogm-map.js +76 -22
  14. package/dist/collection/components/ogm-menubar/ogm-menubar.test.js +21 -0
  15. package/dist/collection/components/ogm-preview/ogm-preview.css +16 -0
  16. package/dist/collection/components/ogm-preview/ogm-preview.js +144 -0
  17. package/dist/collection/components/ogm-preview/ogm-preview.test.js +37 -0
  18. package/dist/collection/components/ogm-previews/ogm-previews.css +3 -2
  19. package/dist/collection/components/ogm-previews/ogm-previews.js +7 -17
  20. package/dist/collection/components/ogm-previews/ogm-previews.test.js +83 -0
  21. package/dist/collection/components/ogm-settings/ogm-settings.js +1 -1
  22. package/dist/collection/components/ogm-sidebar/ogm-sidebar.js +2 -2
  23. package/dist/collection/components/ogm-viewer/ogm-viewer.js +36 -15
  24. package/dist/collection/lib/errors.js +137 -0
  25. package/dist/collection/lib/errors.test.js +96 -0
  26. package/dist/collection/lib/sources/geojson.js +2 -1
  27. package/dist/collection/lib/sources/iiif-manifest.js +2 -3
  28. package/dist/collection/lib/sources/iiif-manifest.test.js +4 -4
  29. package/dist/esm/{lerc-Bz409klg.js → lerc-CB_Jcu33.js} +1 -1
  30. package/dist/esm/loader.js +1 -1
  31. package/dist/esm/{ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-C9zV3wMe.js → ogm-alerts.ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-preview.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-Dlc6USWB.js} +446 -82
  32. package/dist/esm/ogm-alerts_11.entry.js +2 -0
  33. package/dist/esm/ogm-viewer.js +1 -1
  34. package/dist/ogm-viewer/assets/icons/exclamation-triangle-fill.svg +3 -0
  35. package/dist/ogm-viewer/ogm-viewer.esm.js +1 -1
  36. package/dist/ogm-viewer/p-7936ccc9.entry.js +1 -0
  37. package/dist/ogm-viewer/p-CWtUrqsM.js +2822 -0
  38. package/dist/ogm-viewer/{p-BrovdLv6.js → p-DqRDI3AC.js} +1 -1
  39. package/dist/types/components/ogm-alerts/ogm-alerts.d.ts +6 -0
  40. package/dist/types/components/ogm-alerts/ogm-alerts.test.d.ts +1 -0
  41. package/dist/types/components/ogm-image/ogm-image.d.ts +5 -0
  42. package/dist/types/components/ogm-map/ogm-map.d.ts +6 -0
  43. package/dist/types/components/ogm-preview/ogm-preview.d.ts +13 -0
  44. package/dist/types/components/ogm-preview/ogm-preview.test.d.ts +1 -0
  45. package/dist/types/components/ogm-previews/ogm-previews.d.ts +1 -5
  46. package/dist/types/components/ogm-previews/ogm-previews.test.d.ts +1 -0
  47. package/dist/types/components/ogm-viewer/ogm-viewer.d.ts +7 -2
  48. package/dist/types/components.d.ts +52 -0
  49. package/dist/types/lib/errors.d.ts +13 -0
  50. package/dist/types/lib/errors.test.d.ts +1 -0
  51. package/dist/types/lib/sources/source.d.ts +1 -1
  52. package/package.json +1 -1
  53. package/dist/cjs/ogm-attributes_9.cjs.entry.js +0 -16
  54. package/dist/esm/ogm-attributes_9.entry.js +0 -2
  55. package/dist/ogm-viewer/p-86b8fafc.entry.js +0 -1
  56. package/dist/ogm-viewer/p-CpCT3zlD.js +0 -2750
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ var ogmAlerts_ogmAttributes_ogmImage_ogmMap_ogmMenubar_ogmMetadata_ogmPreview_ogmPreviews_ogmSettings_ogmSidebar_ogmViewer_entry = require('./ogm-alerts.ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-preview.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-DMEfXquS.js');
4
+ require('./index-mu9UK_Fb.js');
5
+
6
+
7
+
8
+ exports.ogm_alerts = ogmAlerts_ogmAttributes_ogmImage_ogmMap_ogmMenubar_ogmMetadata_ogmPreview_ogmPreviews_ogmSettings_ogmSidebar_ogmViewer_entry.OgmAlerts;
9
+ exports.ogm_attributes = ogmAlerts_ogmAttributes_ogmImage_ogmMap_ogmMenubar_ogmMetadata_ogmPreview_ogmPreviews_ogmSettings_ogmSidebar_ogmViewer_entry.OgmAttributes;
10
+ exports.ogm_image = ogmAlerts_ogmAttributes_ogmImage_ogmMap_ogmMenubar_ogmMetadata_ogmPreview_ogmPreviews_ogmSettings_ogmSidebar_ogmViewer_entry.OgmImage;
11
+ exports.ogm_map = ogmAlerts_ogmAttributes_ogmImage_ogmMap_ogmMenubar_ogmMetadata_ogmPreview_ogmPreviews_ogmSettings_ogmSidebar_ogmViewer_entry.OgmMap;
12
+ exports.ogm_menubar = ogmAlerts_ogmAttributes_ogmImage_ogmMap_ogmMenubar_ogmMetadata_ogmPreview_ogmPreviews_ogmSettings_ogmSidebar_ogmViewer_entry.OgmMenubar;
13
+ exports.ogm_metadata = ogmAlerts_ogmAttributes_ogmImage_ogmMap_ogmMenubar_ogmMetadata_ogmPreview_ogmPreviews_ogmSettings_ogmSidebar_ogmViewer_entry.OgmMetadata;
14
+ exports.ogm_preview = ogmAlerts_ogmAttributes_ogmImage_ogmMap_ogmMenubar_ogmMetadata_ogmPreview_ogmPreviews_ogmSettings_ogmSidebar_ogmViewer_entry.OgmPreview;
15
+ exports.ogm_previews = ogmAlerts_ogmAttributes_ogmImage_ogmMap_ogmMenubar_ogmMetadata_ogmPreview_ogmPreviews_ogmSettings_ogmSidebar_ogmViewer_entry.OgmPreviews;
16
+ exports.ogm_settings = ogmAlerts_ogmAttributes_ogmImage_ogmMap_ogmMenubar_ogmMetadata_ogmPreview_ogmPreviews_ogmSettings_ogmSidebar_ogmViewer_entry.OgmSettings;
17
+ exports.ogm_sidebar = ogmAlerts_ogmAttributes_ogmImage_ogmMap_ogmMenubar_ogmMetadata_ogmPreview_ogmPreviews_ogmSettings_ogmSidebar_ogmViewer_entry.OgmSidebar;
18
+ exports.ogm_viewer = ogmAlerts_ogmAttributes_ogmImage_ogmMap_ogmMenubar_ogmMetadata_ogmPreview_ogmPreviews_ogmSettings_ogmSidebar_ogmViewer_entry.OgmViewer;
@@ -19,7 +19,7 @@ var patchBrowser = () => {
19
19
 
20
20
  patchBrowser().then(async (options) => {
21
21
  await appGlobals.globalScripts();
22
- return index.bootstrapLazy([["ogm-attributes_9.cjs",[[513,"ogm-viewer",{"recordUrl":[1,"record-url"],"theme":[1],"record":[32],"previewOpacity":[32],"sidebarOpen":[32],"loadRecord":[64]},[[0,"sidebarToggled","toggleSidebar"],[0,"opacityChange","adjustPreviewOpacity"],[0,"mapLoading","setLoadingStarted"],[0,"imageLoading","setLoadingStarted"],[0,"mapIdle","setLoadingFinished"],[0,"imageLoaded","setLoadingFinished"]],{"recordUrl":[{"updateRecord":0}]}],[513,"ogm-previews",{"theme":[1],"record":[16],"previewOpacity":[2,"preview-opacity"],"sidebarPadding":[2,"sidebar-padding"],"sources":[32]},null,{"record":[{"getSources":0}]}],[513,"ogm-sidebar",{"record":[16],"theme":[1],"open":[4]}],[513,"ogm-menubar",{"record":[16],"theme":[1],"loading":[4]}],[513,"ogm-map",{"source":[16],"theme":[1],"padding":[2],"easeMapTo":[64]},[[16,"featureSelected","handleFeatureSelected"]],{"source":[{"previewSource":0}],"padding":[{"onPaddingChange":0}]}],[513,"ogm-image",{"source":[16],"theme":[1],"padding":[2]},null,{"source":[{"onSourceChange":0}],"padding":[{"onPaddingChange":0}]}],[513,"ogm-metadata",{"record":[16],"theme":[1],"fieldNames":[16],"filteredRecord":[32]}],[513,"ogm-settings",{"record":[16]},[[0,"input","handleOpacityChange"]]],[513,"ogm-attributes",{"features":[16],"currentIndex":[32]},null,{"features":[{"onFeaturesChange":0}],"currentIndex":[{"onCurrentIndexChange":0}]}]]]], options);
22
+ return index.bootstrapLazy([["ogm-alerts_11.cjs",[[513,"ogm-viewer",{"recordUrl":[1,"record-url"],"theme":[1],"record":[32],"error":[32],"previewOpacity":[32],"sidebarOpen":[32],"loading":[32],"loadRecord":[64]},[[0,"sidebarToggled","toggleSidebar"],[0,"opacityChange","adjustPreviewOpacity"],[0,"mapLoading","setLoadingStarted"],[0,"imageLoading","setLoadingStarted"],[0,"mapIdle","setLoadingFinished"],[0,"imageLoaded","setLoadingFinished"]],{"recordUrl":[{"updateRecord":0}],"record":[{"resetLoading":0}]}],[513,"ogm-previews",{"theme":[1],"record":[16],"previewOpacity":[2,"preview-opacity"],"sidebarPadding":[2,"sidebar-padding"],"sources":[32]},null,{"record":[{"getSources":0}]}],[513,"ogm-sidebar",{"record":[16],"theme":[1],"open":[4]}],[513,"ogm-menubar",{"record":[16],"theme":[1],"loading":[4]}],[513,"ogm-preview",{"theme":[1],"source":[16],"previewOpacity":[2,"preview-opacity"],"sidebarPadding":[2,"sidebar-padding"],"error":[32]},[[0,"previewError","handlePreviewError"]],{"source":[{"resetError":0}]}],[513,"ogm-metadata",{"record":[16],"theme":[1],"fieldNames":[16],"filteredRecord":[32]}],[513,"ogm-settings",{"record":[16]},[[0,"input","handleOpacityChange"]]],[513,"ogm-map",{"source":[16],"theme":[1],"padding":[2],"easeMapTo":[64]},[[16,"featureSelected","handleFeatureSelected"]],{"source":[{"previewSource":0}],"padding":[{"onPaddingChange":0}]}],[513,"ogm-alerts",{"theme":[1],"error":[16]}],[513,"ogm-image",{"source":[16],"theme":[1],"padding":[2]},null,{"source":[{"onSourceChange":0}],"padding":[{"onPaddingChange":0}]}],[513,"ogm-attributes",{"features":[16],"currentIndex":[32]},null,{"features":[{"onFeaturesChange":0}],"currentIndex":[{"onCurrentIndexChange":0}]}]]]], options);
23
23
  });
24
24
 
25
25
  exports.setNonce = index.setNonce;
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-exclamation-triangle-fill" viewBox="0 0 16 16">
2
+ <path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5m.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2"/>
3
+ </svg>
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "entries": [
3
+ "components/ogm-alerts/ogm-alerts.js",
3
4
  "components/ogm-attributes/ogm-attributes.js",
4
5
  "components/ogm-image/ogm-image.js",
5
6
  "components/ogm-map/ogm-map.js",
6
7
  "components/ogm-menubar/ogm-menubar.js",
7
8
  "components/ogm-metadata/ogm-metadata.js",
9
+ "components/ogm-preview/ogm-preview.js",
8
10
  "components/ogm-previews/ogm-previews.js",
9
11
  "components/ogm-settings/ogm-settings.js",
10
12
  "components/ogm-sidebar/ogm-sidebar.js",
@@ -0,0 +1,51 @@
1
+ /* Fill the space left by the preview it replaces and center the error within it */
2
+ :host {
3
+ display: flex;
4
+ width: 100%;
5
+ height: 100%;
6
+ place-items: center;
7
+ place-content: center;
8
+ background: var(--wa-color-surface-default);
9
+ }
10
+
11
+ .alerts {
12
+ padding: var(--wa-space-m);
13
+ max-width: 40rem;
14
+ }
15
+
16
+ .alert {
17
+ box-shadow: var(--wa-shadow-m);
18
+ align-items: start;
19
+ }
20
+
21
+ .alert::part(message) {
22
+ display: flex;
23
+ align-items: start;
24
+ gap: var(--wa-space-s);
25
+ width: 100%;
26
+ }
27
+
28
+ .content {
29
+ flex: auto;
30
+ min-width: 0;
31
+ }
32
+
33
+ .title {
34
+ display: block;
35
+ font-weight: var(--wa-font-weight-bold);
36
+ }
37
+
38
+ .message {
39
+ margin-top: var(--wa-space-3xs);
40
+ font-size: var(--wa-font-size-s);
41
+ }
42
+
43
+ .url {
44
+ margin-top: var(--wa-space-2xs);
45
+ font-family: var(--wa-font-family-code);
46
+ font-size: var(--wa-font-size-xs);
47
+ color: var(--wa-color-text-quiet);
48
+ overflow: hidden;
49
+ text-overflow: ellipsis;
50
+ white-space: nowrap;
51
+ }
@@ -0,0 +1,71 @@
1
+ import { Host, h } from "@stencil/core";
2
+ // Renders a single preview error, centered and filling its container. Used in place of a failed
3
+ // preview (inside <ogm-preview>) or in place of the previews entirely (when the record fails to load).
4
+ export class OgmAlerts {
5
+ theme;
6
+ error;
7
+ render() {
8
+ if (!this.error)
9
+ return null;
10
+ return (h(Host, { class: this.theme && `wa-${this.theme}` }, h("div", { class: "alerts", role: "alert", "aria-live": "assertive" }, h("wa-callout", { variant: "danger", appearance: "filled-outlined", class: "alert" }, h("wa-icon", { slot: "icon", name: "exclamation-triangle-fill", canvas: "auto" }), h("div", { class: "content" }, h("strong", { class: "title" }, this.error.title), h("div", { class: "message" }, this.error.message), this.error.url && (h("div", { class: "url", title: this.error.url }, this.error.url)))))));
11
+ }
12
+ static get is() { return "ogm-alerts"; }
13
+ static get encapsulation() { return "shadow"; }
14
+ static get originalStyleUrls() {
15
+ return {
16
+ "$": ["ogm-alerts.css"]
17
+ };
18
+ }
19
+ static get styleUrls() {
20
+ return {
21
+ "$": ["ogm-alerts.css"]
22
+ };
23
+ }
24
+ static get properties() {
25
+ return {
26
+ "theme": {
27
+ "type": "string",
28
+ "mutable": false,
29
+ "complexType": {
30
+ "original": "'light' | 'dark'",
31
+ "resolved": "\"dark\" | \"light\"",
32
+ "references": {}
33
+ },
34
+ "required": false,
35
+ "optional": false,
36
+ "docs": {
37
+ "tags": [],
38
+ "text": ""
39
+ },
40
+ "getter": false,
41
+ "setter": false,
42
+ "reflect": false,
43
+ "attribute": "theme"
44
+ },
45
+ "error": {
46
+ "type": "unknown",
47
+ "mutable": false,
48
+ "complexType": {
49
+ "original": "PreviewError",
50
+ "resolved": "PreviewError",
51
+ "references": {
52
+ "PreviewError": {
53
+ "location": "import",
54
+ "path": "../../lib/errors",
55
+ "id": "src/lib/errors.ts::PreviewError",
56
+ "referenceLocation": "PreviewError"
57
+ }
58
+ }
59
+ },
60
+ "required": false,
61
+ "optional": true,
62
+ "docs": {
63
+ "tags": [],
64
+ "text": ""
65
+ },
66
+ "getter": false,
67
+ "setter": false
68
+ }
69
+ };
70
+ }
71
+ }
@@ -0,0 +1,26 @@
1
+ import { render, describe, it, expect, h } from "@stencil/vitest";
2
+ import { referenceError } from "../../lib/errors";
3
+ // Mock a network failure on load
4
+ const makeError = (url) => referenceError(new TypeError('Failed to fetch'), 'GeoJSON', url);
5
+ describe('ogm-alerts', () => {
6
+ it('renders nothing when there is no error', async () => {
7
+ const { root } = await render(h("ogm-alerts", null));
8
+ const shadowRoot = root.shadowRoot;
9
+ expect(shadowRoot.querySelector('.alerts')).toBeNull();
10
+ });
11
+ it('renders a danger callout with the error title, message, and url', async () => {
12
+ const { root } = await render(h("ogm-alerts", { error: makeError('http://example.com/data.json') }));
13
+ const shadowRoot = root.shadowRoot;
14
+ const callout = shadowRoot.querySelector('wa-callout');
15
+ expect(callout).not.toBeNull();
16
+ expect(callout.getAttribute('variant')).toBe('danger');
17
+ expect(callout.textContent).toContain('GeoJSON');
18
+ expect(callout.textContent).toContain('CORS');
19
+ expect(callout.textContent).toContain('http://example.com/data.json');
20
+ });
21
+ it('omits the url line when the error has no url', async () => {
22
+ const { root } = await render(h("ogm-alerts", { error: makeError() }));
23
+ const shadowRoot = root.shadowRoot;
24
+ expect(shadowRoot.querySelector('.url')).toBeNull();
25
+ });
26
+ });
@@ -1,6 +1,7 @@
1
1
  import { h, Host } from "@stencil/core";
2
2
  import { Viewer } from "openseadragon";
3
3
  import { getElement, findElement } from "../../lib/elements";
4
+ import { referenceError } from "../../lib/errors";
4
5
  export class OgmImage {
5
6
  el;
6
7
  source;
@@ -8,8 +9,11 @@ export class OgmImage {
8
9
  padding = 0;
9
10
  imageLoaded;
10
11
  imageLoading;
12
+ previewError;
11
13
  // OpenSeadragon viewer instance
12
14
  viewer;
15
+ // Guards against reporting more than one error per load attempt
16
+ errorReported = false;
13
17
  // Set up OpenSeadragon viewer on load
14
18
  async componentDidLoad() {
15
19
  this.viewer = new Viewer({
@@ -26,10 +30,21 @@ export class OgmImage {
26
30
  nextButton: getElement(this.el, '.next'),
27
31
  previousButton: getElement(this.el, '.prev'),
28
32
  });
33
+ // Clear loading state whether we succeeded or failed
29
34
  this.viewer.addHandler('open', () => this.imageLoaded.emit());
35
+ // Surface OpenSeaDragon decode errors here
36
+ this.viewer.addHandler('open-failed', event => {
37
+ this.imageLoaded.emit();
38
+ this.reportError(new Error(event.message));
39
+ });
40
+ // If we do have a source, load the images
30
41
  if (this.source)
31
42
  await this.loadImages();
32
43
  }
44
+ // Destroy the viewer when we are removed from the DOM
45
+ disconnectedCallback() {
46
+ this.viewer?.destroy();
47
+ }
33
48
  // Update preview when source changes
34
49
  async onSourceChange() {
35
50
  if (this.source)
@@ -46,14 +61,29 @@ export class OgmImage {
46
61
  // Get all of the IIIF image URLs and send them to OpenSeadragon
47
62
  // This makes a request to fetch and cache the manifest
48
63
  async loadImages() {
64
+ this.errorReported = false;
49
65
  this.imageLoading.emit();
50
- const images = await this.source.getIIIFImageUrls();
51
- if (!images)
52
- throw new Error('No IIIF images found for source');
53
- this.viewer.open(images);
66
+ try {
67
+ const images = await this.source.getIIIFImageUrls();
68
+ if (!images)
69
+ throw new Error('No IIIF images found for source');
70
+ this.viewer.open(images);
71
+ }
72
+ catch (error) {
73
+ console.error(`Error loading IIIF images for ${this.source.url}:`, error);
74
+ this.imageLoaded.emit();
75
+ this.reportError(error);
76
+ }
77
+ }
78
+ // Emit a single preview error per load attempt
79
+ reportError(error) {
80
+ if (this.errorReported || !this.source)
81
+ return;
82
+ this.errorReported = true;
83
+ this.previewError.emit(referenceError(error, this.source.label(), this.source.url));
54
84
  }
55
85
  render() {
56
- return (h(Host, { key: '155b1ad9e24bc0148c90fa6c0d456a09437e08b6' }, h("div", { key: 'e0cf7750187e9a322f72c59a23f40e5897f31756', id: "openseadragon" }, h("div", { key: '59fadbbbe2e10fa53a0e1a3b311eef2c483d0800', class: "controls" }, h("wa-button", { key: '8c2323085fef8326b663c26aaa42dee935b09f0b', class: "zoom-in", size: "s", appearance: "filled-outlined", pill: true }, h("wa-icon", { key: '129a1e11cf602e922a977a35c9d1001cfb64dc4c', name: "zoom-in", label: "Zoom In", canvas: "auto" })), h("wa-button", { key: '944656ac675915dc589ed72b7cc76a9273a6dc5f', class: "zoom-out", size: "s", appearance: "filled-outlined", pill: true }, h("wa-icon", { key: 'b76f8cda3420937d45431e9d41a3eeac116b9fad', name: "zoom-out", label: "Zoom Out", canvas: "auto" })), h("wa-button", { key: 'c1d11fae3106ef08de6e7de68fdfdcdbe28a06a0', class: "home", size: "s", appearance: "filled-outlined", pill: true }, h("wa-icon", { key: '1a6cc9c1f3b49ef2284faa0c506afe558bfc5e73', name: "house", label: "Reset View", canvas: "auto" })), h("wa-button", { key: '55924e70820e7118745115b08b8b0fce17ffd6c0', class: "full-page", size: "s", appearance: "filled-outlined", pill: true }, h("wa-icon", { key: '0e719e259f142cfec5a8fd0276f6beee2b522e51', name: "arrows-fullscreen", label: "Full Screen", canvas: "auto" })), h("wa-button", { key: '76335703d0b4e9944bde2d077e26db7fa3aba223', class: "next", size: "s", appearance: "filled-outlined", pill: true }, h("wa-icon", { key: 'a543fa9e9822eafb31c35700a1f19df8d23225b1', name: "arrow-right", label: "Next", canvas: "auto" })), h("wa-button", { key: 'a2680c1b66ecea112a01327af560fb1b49607d09', class: "prev", size: "s", appearance: "filled-outlined", pill: true }, h("wa-icon", { key: '00d8226e18ecb42471601b005f90198b8baa14d2', name: "arrow-left", label: "Previous", canvas: "auto" }))))));
86
+ return (h(Host, { key: 'ebbebaddac72024a3b311e0e0e501267a83d1e95' }, h("div", { key: '4f171256535cfd11430e2269fda389ffae3cedae', id: "openseadragon" }, h("div", { key: 'c308186a13fd6b8e0b1fadbf1fd1e6924631f9d6', class: "controls" }, h("wa-button", { key: '1f9e0c4c6d881bf09d7ceb160f470992d71ebe07', class: "zoom-in", size: "s", appearance: "filled-outlined", pill: true }, h("wa-icon", { key: 'cbacb0a388de2c7f349e25172027874a0224a678', name: "zoom-in", label: "Zoom In", canvas: "auto" })), h("wa-button", { key: 'dd8d7e9c5584b124ae105b2187100964f5e3a02a', class: "zoom-out", size: "s", appearance: "filled-outlined", pill: true }, h("wa-icon", { key: '2b5acf2e07c99df9556692bf534bb3847506e4d4', name: "zoom-out", label: "Zoom Out", canvas: "auto" })), h("wa-button", { key: '5534a5c77ea20a1f77544e5acd99d4247dd3d287', class: "home", size: "s", appearance: "filled-outlined", pill: true }, h("wa-icon", { key: 'faadb8ea57bf1b831b292eaf66bcea39b7e215d1', name: "house", label: "Reset View", canvas: "auto" })), h("wa-button", { key: '8979cf58a94da89e0e2d132a92fe45323f07de01', class: "full-page", size: "s", appearance: "filled-outlined", pill: true }, h("wa-icon", { key: '12cb3224e7f14258376c1c0bf6ffc5abda5c2252', name: "arrows-fullscreen", label: "Full Screen", canvas: "auto" })), h("wa-button", { key: 'e79c37f27ff14d5c4991ff30ff88aada467214ec', class: "next", size: "s", appearance: "filled-outlined", pill: true }, h("wa-icon", { key: 'ee10870ace583257393c53ba12b73ab3b015371b', name: "arrow-right", label: "Next", canvas: "auto" })), h("wa-button", { key: '9d8999e199b7b6338261eb16a655d2199520cf22', class: "prev", size: "s", appearance: "filled-outlined", pill: true }, h("wa-icon", { key: 'fde1a9022b9e5f90e269c75ce87943808031e021', name: "arrow-left", label: "Previous", canvas: "auto" }))))));
57
87
  }
58
88
  static get is() { return "ogm-image"; }
59
89
  static get encapsulation() { return "shadow"; }
@@ -163,6 +193,28 @@ export class OgmImage {
163
193
  "resolved": "void",
164
194
  "references": {}
165
195
  }
196
+ }, {
197
+ "method": "previewError",
198
+ "name": "previewError",
199
+ "bubbles": true,
200
+ "cancelable": true,
201
+ "composed": true,
202
+ "docs": {
203
+ "tags": [],
204
+ "text": ""
205
+ },
206
+ "complexType": {
207
+ "original": "PreviewError",
208
+ "resolved": "PreviewError",
209
+ "references": {
210
+ "PreviewError": {
211
+ "location": "import",
212
+ "path": "../../lib/errors",
213
+ "id": "src/lib/errors.ts::PreviewError",
214
+ "referenceLocation": "PreviewError"
215
+ }
216
+ }
217
+ }
166
218
  }];
167
219
  }
168
220
  static get elementRef() { return "el"; }
@@ -6,6 +6,11 @@
6
6
  position: relative;
7
7
  }
8
8
 
9
+ /* Color of the map background ("space" around the globe) */
10
+ .maplibregl-canvas {
11
+ background-color: var(--wa-color-surface-default);
12
+ }
13
+
9
14
  /* Inspection popups. Vertical scrolling (when content is too tall) is handled by the
10
15
  <wa-scroller> inside ogm-attributes, not here - that's what gives it the fading shadow cue. */
11
16
  .maplibregl-popup-content {
@@ -14,6 +19,34 @@
14
19
  padding: 0;
15
20
  box-shadow: var(--wa-shadow-l);
16
21
  border: var(--wa-panel-border-width) solid var(--wa-color-surface-border);
22
+ color: var(--wa-color-text-normal);
23
+ background-color: var(--wa-color-surface-default);
24
+
25
+ .maplibregl-popup-close-button {
26
+ background-color: var(--wa-color-surface-default);
27
+ border-radius: 10px;
28
+ }
29
+ }
30
+
31
+ /* Popup tip (the little triangle) */
32
+ .maplibregl-popup-anchor-right .maplibregl-popup-tip {
33
+ border-left-color: var(--wa-color-surface-border);
34
+ }
35
+
36
+ .maplibregl-popup-anchor-left .maplibregl-popup-tip {
37
+ border-right-color: var(--wa-color-surface-border);
38
+ }
39
+
40
+ .maplibregl-popup-anchor-bottom .maplibregl-popup-tip,
41
+ .maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip,
42
+ .maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip {
43
+ border-top-color: var(--wa-color-surface-border);
44
+ }
45
+
46
+ .maplibregl-popup-anchor-top .maplibregl-popup-tip,
47
+ .maplibregl-popup-anchor-top-right .maplibregl-popup-tip,
48
+ .maplibregl-popup-anchor-top-left .maplibregl-popup-tip {
49
+ border-bottom-color: var(--wa-color-surface-border);
17
50
  }
18
51
 
19
52
  /* Dark mode map adjustments */
@@ -22,10 +55,6 @@
22
55
  stroke: white;
23
56
  }
24
57
 
25
- .maplibregl-canvas {
26
- background-color: black;
27
- }
28
-
29
58
  .maplibregl-ctrl.maplibregl-ctrl-group {
30
59
  filter: invert(1);
31
60
  }
@@ -34,43 +63,8 @@
34
63
  .maplibregl-ctrl-group button:focus:focus-visible {
35
64
  box-shadow: 0 0 2px 2px #ff6900;
36
65
  }
66
+
37
67
  .maplibregl-ctrl button.maplibregl-ctrl-globe-enabled .maplibregl-ctrl-icon {
38
68
  filter: invert(1);
39
69
  }
40
-
41
- details.maplibregl-ctrl.maplibregl-ctrl-attrib {
42
- background-color: black;
43
- color: #999;
44
- }
45
- details.maplibregl-ctrl.maplibregl-ctrl-attrib a {
46
- color: #999;
47
- }
48
- details.maplibregl-ctrl.maplibregl-ctrl-attrib.maplibregl-compact-show .maplibregl-ctrl-attrib-button {
49
- background-color: #999;
50
- }
51
-
52
- div.maplibregl-popup-content {
53
- background-color: black;
54
- color: #ccc;
55
- }
56
-
57
- div.maplibregl-popup-anchor-right div.maplibregl-popup-tip {
58
- border-left-color: black;
59
- }
60
-
61
- div.maplibregl-popup-anchor-left div.maplibregl-popup-tip {
62
- border-right-color: black;
63
- }
64
-
65
- div.maplibregl-popup-anchor-bottom div.maplibregl-popup-tip,
66
- div.maplibregl-popup-anchor-bottom-right div.maplibregl-popup-tip,
67
- div.maplibregl-popup-anchor-bottom-left div.maplibregl-popup-tip {
68
- border-top-color: black;
69
- }
70
-
71
- div.maplibregl-popup-anchor-top div.maplibregl-popup-tip,
72
- div.maplibregl-popup-anchor-top-right div.maplibregl-popup-tip,
73
- div.maplibregl-popup-anchor-top-left div.maplibregl-popup-tip {
74
- border-bottom-color: black;
75
- }
76
70
  }
@@ -8,6 +8,7 @@ import PMTilesSource from "../../lib/sources/pmtiles";
8
8
  import RasterSource from "../../lib/sources/raster";
9
9
  import WmsSource from "../../lib/sources/wms";
10
10
  import { getElement } from "../../lib/elements";
11
+ import { referenceError } from "../../lib/errors";
11
12
  import CogPreviewer from "../../lib/previewers/cog";
12
13
  import GeoJSONPreviewer from "../../lib/previewers/geojson";
13
14
  import OpenIndexMapPreviewer from "../../lib/previewers/openindexmap";
@@ -26,6 +27,9 @@ export class OgmMap {
26
27
  padding = 0;
27
28
  mapIdle;
28
29
  mapLoading;
30
+ previewError;
31
+ // Guards against reporting more than one error per load attempt
32
+ errorReported = false;
29
33
  // MapLibre map instance and popup instance for feature info display
30
34
  map;
31
35
  mapTheme;
@@ -50,10 +54,10 @@ export class OgmMap {
50
54
  });
51
55
  this.getContainer();
52
56
  this.addControls();
53
- this.map.on('idle', () => this.mapIdle.emit());
54
57
  this.map.on('load', () => this.previewSource(this.source));
55
58
  this.map.on('mousemove', this.handleHover.bind(this));
56
59
  this.map.on('click', this.handleClick.bind(this));
60
+ this.map.on('error', this.handleMapError.bind(this));
57
61
  // View as a globe with atmosphere effects
58
62
  this.map.on('style.load', () => {
59
63
  this.map.setProjection({ type: 'globe' });
@@ -118,30 +122,57 @@ export class OgmMap {
118
122
  // Do nothing if we didn't get passed a source
119
123
  if (!source)
120
124
  return;
125
+ // Fresh load attempt: allow one error to be reported again for this source
126
+ this.errorReported = false;
121
127
  // Indicate loading state so we can show the spinner
122
128
  this.mapLoading.emit();
123
- // Close popup if one is open
124
- this.clearFeatureSelection();
125
- this.destroyPopup();
126
- // Clear existing preview
127
- if (this.previewer) {
128
- await this.previewer.clearPreview();
129
- this.previewer = undefined;
129
+ try {
130
+ // Close popup if one is open
131
+ this.clearFeatureSelection();
132
+ this.destroyPopup();
133
+ // Clear existing preview
134
+ if (this.previewer) {
135
+ await this.previewer.clearPreview();
136
+ this.previewer = undefined;
137
+ }
138
+ // Get the appropriate previewer for our source and preview it
139
+ this.previewer = await this.getMapPreviewer(this.map);
140
+ if (!this.previewer)
141
+ throw new Error(`No previewer found for source: ${source.constructor.name}`);
142
+ await this.previewer.preview();
143
+ // Fit to bounds from the record; the spinner stays up until the map finishes moving
144
+ const bounds = await this.source.getBounds();
145
+ if (bounds)
146
+ await this.fitMapBounds(bounds);
147
+ }
148
+ catch (error) {
149
+ console.error(`Error previewing source ${source.url}:`, error);
150
+ if (!this.errorReported) {
151
+ this.errorReported = true;
152
+ this.previewError.emit(referenceError(error, source.label(), source.url));
153
+ }
154
+ }
155
+ finally {
156
+ this.mapIdle.emit();
130
157
  }
131
- // Get the appropriate previewer for our source and preview it
132
- this.previewer = await this.getMapPreviewer(this.map);
133
- if (!this.previewer)
134
- throw new Error(`No previewer found for source: ${source.constructor.name}`);
135
- await this.previewer.preview();
136
- // Fit to bounds from the record
137
- const bounds = await this.source.getBounds();
138
- if (bounds)
139
- await this.fitMapBounds(bounds);
140
- // Emit map idle event to signal that the map is ready
141
- this.mapIdle.emit();
142
158
  }
143
- // Fit the map to the given bounds; return when moving is finished
159
+ // Surface MapLibre errors tied to the current preview source, skipping the
160
+ // noise from basemap/glyph/sprite loads, and deduped to a single alert per load attempt.
161
+ handleMapError(event) {
162
+ if (this.errorReported || !this.source || !this.previewer)
163
+ return;
164
+ if (event.sourceId !== this.previewer.sourceId)
165
+ return;
166
+ this.errorReported = true;
167
+ this.previewError.emit(referenceError(event.error, this.source.label(), this.source.url));
168
+ }
169
+ // Fit the map to the given bounds; resolve once the move finishes. Guard the case where the bounds
170
+ // can't produce a camera - e.g. sidebar padding wider than the viewport, or a hidden, zero-size
171
+ // inactive tab panel - because then fitBounds won't move, 'moveend' never fires, and awaiting this
172
+ // promise (and the loading state that depends on it) would hang forever.
144
173
  async fitMapBounds(bounds) {
174
+ if (!this.map.cameraForBounds(bounds))
175
+ return;
145
176
  return new Promise(resolve => {
146
177
  this.map.once('moveend', () => resolve());
147
178
  this.map.fitBounds(bounds);
@@ -213,7 +244,8 @@ export class OgmMap {
213
244
  }
214
245
  // Create a new popup and set its content and location
215
246
  createPopup(location) {
216
- this.popup = new maplibregl.Popup({ maxWidth: 'none', closeButton: false }).setDOMContent(this.attributesEl).setLngLat(location).addTo(this.map);
247
+ this.popup = new maplibregl.Popup({ maxWidth: 'none' }).setDOMContent(this.attributesEl).setLngLat(location).addTo(this.map);
248
+ this.popup.on('close', this.clearFeatureSelection.bind(this));
217
249
  }
218
250
  // Remove popup from the map and clear the reference
219
251
  destroyPopup() {
@@ -227,7 +259,7 @@ export class OgmMap {
227
259
  return this.previewer?.layerIds || [];
228
260
  }
229
261
  render() {
230
- return h("div", { key: '1c38c0508638813aee913dce29f533845393023b', id: "map", class: `wa-${this.theme}` });
262
+ return h("div", { key: '9519610af20287a4d7e28470c715accc5ffb8430', id: "map", class: `wa-${this.theme}` });
231
263
  }
232
264
  static get is() { return "ogm-map"; }
233
265
  static get encapsulation() { return "shadow"; }
@@ -337,6 +369,28 @@ export class OgmMap {
337
369
  "resolved": "void",
338
370
  "references": {}
339
371
  }
372
+ }, {
373
+ "method": "previewError",
374
+ "name": "previewError",
375
+ "bubbles": true,
376
+ "cancelable": true,
377
+ "composed": true,
378
+ "docs": {
379
+ "tags": [],
380
+ "text": ""
381
+ },
382
+ "complexType": {
383
+ "original": "PreviewError",
384
+ "resolved": "PreviewError",
385
+ "references": {
386
+ "PreviewError": {
387
+ "location": "import",
388
+ "path": "../../lib/errors",
389
+ "id": "src/lib/errors.ts::PreviewError",
390
+ "referenceLocation": "PreviewError"
391
+ }
392
+ }
393
+ }
340
394
  }];
341
395
  }
342
396
  static get methods() {
@@ -46,4 +46,25 @@ describe('ogm-menubar', () => {
46
46
  `);
47
47
  });
48
48
  });
49
+ describe('loading indicator', () => {
50
+ it('shows the spinner while loading', async () => {
51
+ const { root } = await render(h("ogm-menubar", { loading: true }));
52
+ const shadowRoot = root.shadowRoot;
53
+ expect(shadowRoot.querySelector('.loading-spinner')).not.toBeNull();
54
+ });
55
+ it('hides the spinner when not loading', async () => {
56
+ const { root } = await render(h("ogm-menubar", { loading: false }));
57
+ const shadowRoot = root.shadowRoot;
58
+ expect(shadowRoot.querySelector('.loading-spinner')).toBeNull();
59
+ });
60
+ it('toggles the spinner when the loading prop changes', async () => {
61
+ const { root, setProps } = await render(h("ogm-menubar", { loading: false }));
62
+ const shadowRoot = root.shadowRoot;
63
+ expect(shadowRoot.querySelector('.loading-spinner')).toBeNull();
64
+ await setProps({ loading: true });
65
+ expect(shadowRoot.querySelector('.loading-spinner')).not.toBeNull();
66
+ await setProps({ loading: false });
67
+ expect(shadowRoot.querySelector('.loading-spinner')).toBeNull();
68
+ });
69
+ });
49
70
  });
@@ -0,0 +1,16 @@
1
+ :host {
2
+ display: block;
3
+ height: 100%;
4
+ position: relative;
5
+ }
6
+
7
+ ogm-map,
8
+ ogm-image {
9
+ display: block;
10
+ height: 100%;
11
+ }
12
+
13
+ ogm-alerts {
14
+ position: absolute;
15
+ inset: 0;
16
+ }