Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions frontend/src/__tests__/components/resource/ResourceViewer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,19 @@ const iiifManifestData = {
},
} as Parameters<typeof ResourceViewer>[0]['data'];

const iiifImageDataWithoutGeometry = {
attributes: { dct_references_s: {} },
meta: {
ui: {
viewer: {
protocol: 'iiif_image',
endpoint:
'https://cdm17287.contentdm.oclc.org/digital/iiif/wpamaps/2535/info.json',
},
},
},
} as Parameters<typeof ResourceViewer>[0]['data'];

describe('ResourceViewer', () => {
let rectSpy: ReturnType<typeof vi.spyOn>;

Expand Down Expand Up @@ -369,6 +382,33 @@ describe('ResourceViewer', () => {
});

describe('Leaflet-backed viewer remounts', () => {
it('renders IIIF Image API endpoints through the Leaflet IIIF protocol instead of Mirador', async () => {
const { container } = render(
<ResourceViewer data={iiifImageDataWithoutGeometry} pageValue="SHOW" />
);

await act(async () => {});

expect(
container.querySelector('iframe[title="Mirador viewer"]')
).toBeNull();

const viewer = container.querySelector('#leaflet-viewer');
expect(viewer).not.toBeNull();
expect(viewer?.getAttribute('data-leaflet-viewer-protocol-value')).toBe(
'Iiif'
);
expect(viewer?.getAttribute('data-leaflet-viewer-available-value')).toBe(
'true'
);
expect(viewer?.getAttribute('data-leaflet-viewer-url-value')).toBe(
'https://cdm17287.contentdm.oclc.org/digital/iiif/wpamaps/2535/info.json'
);
expect(viewer?.hasAttribute('data-leaflet-viewer-map-geom-value')).toBe(
false
);
});

it('replaces the viewer container when the resource changes', async () => {
const { rerender, container } = render(
<ResourceViewer data={wmsDataWithGeometry} pageValue="SHOW" />
Expand Down
21 changes: 9 additions & 12 deletions frontend/src/components/resource/ResourceViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,8 @@ export function ResourceViewer({ data, pageValue }: ResourceViewerProps) {
const protocol = data.meta?.ui?.viewer?.protocol || '';
const endpoint = data.meta?.ui?.viewer?.endpoint || '';
const geometry = data.meta?.ui?.viewer?.geometry;
const available = !!protocol && !!endpoint && !!geometry;
const available =
!!protocol && !!endpoint && (protocol === 'iiif_image' || !!geometry);
const layerIdentifier =
data.attributes.ogm?.gbl_wxsIdentifier_s ||
data.attributes.ogm?.gbl_wxsidentifier_s ||
Expand All @@ -384,7 +385,7 @@ export function ResourceViewer({ data, pageValue }: ResourceViewerProps) {

// Helper function to determine viewer type
const getViewerType = (protocol: string) => {
if (['iiif_manifest', 'iiif_image'].includes(protocol)) {
if (protocol === 'iiif_manifest') {
return 'mirador';
}
if (['cog', 'pmtiles'].includes(protocol)) {
Expand Down Expand Up @@ -426,6 +427,9 @@ export function ResourceViewer({ data, pageValue }: ResourceViewerProps) {
if (protocol === 'arcgis_image_map_layer') {
return 'ImageMapLayer';
}
if (protocol === 'iiif_image') {
return 'Iiif';
}
if (protocol === 'open_index_map') {
return 'IndexMap';
}
Expand All @@ -451,14 +455,9 @@ export function ResourceViewer({ data, pageValue }: ResourceViewerProps) {
);
}

// For iiif_image, we synthesize a Presentation manifest via a local SSR route.
// Mirador runs in a sandboxed iframe, so use absolute URLs for both real and
// synthesized manifests.
// Mirador runs in a sandboxed iframe, so use an absolute URL for the manifest.
const pageOrigin = window.location.origin;
const manifestUrl =
protocol === 'iiif_image'
? `${pageOrigin}/iiif/manifest?image_service=${encodeURIComponent(endpoint)}`
: new URL(endpoint, pageOrigin).toString();
const manifestUrl = new URL(endpoint, pageOrigin).toString();

const miradorUrl = new URL('/mirador', pageOrigin);
miradorUrl.searchParams.set('manifest', manifestUrl);
Expand Down Expand Up @@ -604,9 +603,7 @@ export function ResourceViewer({ data, pageValue }: ResourceViewerProps) {
data-controller="leaflet-viewer"
data-leaflet-viewer-available-value={available}
data-leaflet-viewer-map-geom-value={JSON.stringify(geometry)}
data-leaflet-viewer-layer-id-value={
data.attributes.ogm.gbl_wxsIdentifier_s || ''
}
data-leaflet-viewer-layer-id-value={layerIdentifier}
data-leaflet-viewer-options-value={JSON.stringify(
leafletViewerOptions
)}
Expand Down
50 changes: 47 additions & 3 deletions frontend/src/geoblacklight/leaflet_viewer_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,27 @@ import BaseLeafletViewerController from '@geoblacklight/frontend/app/javascript/
import Sleep from 'geoblacklight/leaflet/controls/sleep';
import { registerLeafletGestureHandling } from '../config/leafletGestureHandling';

let leafletIiifPromise;

async function ensureLeafletIiif() {
if (Leaflet.tileLayer.iiif) return;

globalThis.L = Leaflet;
if (typeof window !== 'undefined') window.L = Leaflet;

leafletIiifPromise ||= import('leaflet-iiif');
await leafletIiifPromise;

if (!Leaflet.tileLayer.iiif) {
throw new Error('leaflet-iiif did not register L.tileLayer.iiif');
}
}

export default class LeafletViewerController extends BaseLeafletViewerController {
get isIiifImage() {
return this.protocolValue === 'Iiif';
}

// Keep the GeoBlacklight controller behavior, but let local MAP options reach L.map.
async loadMap() {
if (this.map) return;
Expand All @@ -12,12 +32,23 @@ export default class LeafletViewerController extends BaseLeafletViewerController

const sleepSettings = this.optionsValue.SLEEP || { SLEEP: false };
const mapSettings = this.optionsValue.MAP || {};
this.map = map(this.element, { ...sleepSettings, ...mapSettings });
const iiifSettings = this.isIiifImage
? {
center: [0, 0],
crs: Leaflet.CRS.Simple,
zoom: 0,
}
: {};
this.map = map(this.element, {
...sleepSettings,
...mapSettings,
...iiifSettings,
});
if (sleepSettings.SLEEP) this.map.addHandler('SLEEP', Sleep);

this.map.addLayer(this.basemap);
if (!this.isIiifImage) this.map.addLayer(this.basemap);
this.map.addLayer(this.overlay);
this.fitBounds(this.bounds);
if (!this.isIiifImage) this.fitBounds(this.bounds);
this.map.options.selected_color =
this.optionsValue.SELECTED_COLOR || 'blue';

Expand All @@ -38,4 +69,17 @@ export default class LeafletViewerController extends BaseLeafletViewerController

this.dispatch('loaded');
}

async getPreviewOverlay(protocol, url, options) {
if (protocol === 'Iiif') {
await ensureLeafletIiif();
return Leaflet.tileLayer.iiif(url, {
...options,
fitBounds: true,
setMaxBounds: true,
});
}

return super.getPreviewOverlay(protocol, url, options);
}
}
Loading