diff --git a/src/utils/references.ts b/src/utils/references.ts
index 1935730..9a692be 100644
--- a/src/utils/references.ts
+++ b/src/utils/references.ts
@@ -1,3 +1,6 @@
+import iiif3 from '@iiif/presentation-3';
+import iiif2 from '@iiif/presentation-2';
+
// Map reference URI keys to user-friendly names
export const REFERENCE_URIS = {
'https://github.com/cogeotiff/cog-spec': 'COG',
@@ -50,6 +53,9 @@ export class References {
// Underlying object to hold references
private references: ReferencesRecord;
+ // Cache for fetched IIIF manifest
+ iiifManifest: iiif3.Manifest | iiif2.Manifest | null;
+
// Create a new instance with the JSON string from a record
constructor(dct_references_s: string) {
try {
@@ -61,30 +67,68 @@ export class References {
}
// The WMS URL, if any
- get wms() {
+ get wmsUrl() {
return this.references['http://www.opengis.net/def/serviceType/ogc/wms'];
}
// The cloud-optimized GeoTIFF URL, if any
- get cog() {
+ get cogUrl() {
return this.references['https://github.com/cogeotiff/cog-spec'];
}
// The TMS URL, if any
- get tms() {
+ get tmsUrl() {
return this.references['https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification'];
}
// The XYZ tiles URL, if any
- get xyz() {
+ get xyzUrl() {
return this.references['https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames'];
}
// The GeoJSON URL, if any
- get geojson() {
+ get geojsonUrl() {
return this.references['http://geojson.org/geojson-spec.html'];
}
+ // The TileJSON URL, if any
+ get tilejsonUrl() {
+ return this.references['https://tilejson.org/specification/2.2.0/schema.json'];
+ }
+
+ // The Index map URL, if any
+ get indexMapUrl() {
+ return this.references['https://openindexmaps.org'];
+ }
+
+ // The PMTiles URL, if any
+ get pmtilesUrl() {
+ return this.references['https://pmtiles.org'];
+ }
+
+ // The WMTS URL, if any
+ get wmtsUrl() {
+ return this.references['http://www.opengis.net/def/serviceType/ogc/wmts'];
+ }
+
+ // The IIIF image URL, if any
+ get iiifImageUrl() {
+ return this.references['http://iiif.io/api/image'];
+ }
+
+ // The IIIF manifest URL, if any
+ get iiifManifestUrl() {
+ return this.references['http://iiif.io/api/presentation#manifest'];
+ }
+
+ async iiifImages() {
+ if (this.iiifImageUrl) return [this.iiifImageUrl];
+ if (!this.iiifManifest && this.iiifManifestUrl) await this.fetchManifest();
+ if (!this.iiifManifest) return [];
+ if (this.iiifVersion == 3) return this.extractIiif3ImageUrls(this.iiifManifest as iiif3.Manifest);
+ if (this.iiifVersion == 2) return this.extractIiif2ImageUrls(this.iiifManifest as iiif2.Manifest);
+ }
+
// List of download links with URL and label
get downloadLinks(): LabelledLinks {
const fieldContents = this.references['http://schema.org/downloadUrl'];
@@ -99,4 +143,93 @@ export class References {
.filter(([uri]) => METADATA_REFERENCE_URIS.includes(uri))
.map(([uri, url]: [ReferenceURI, string]) => ({ url, label: REFERENCE_URIS[uri] }));
}
+
+ // True if the record has at least one reference that can be rendered for preview
+ get previewable() {
+ return this.previewableReferences.some(Boolean);
+ }
+
+ // True if the record has a reference that can be rendered on a map
+ get mapPreviewable() {
+ return this.mapPreviewableReferences.some(Boolean);
+ }
+
+ // True if the record has any IIIF references (image or manifest)
+ get iiifPreviewable() {
+ return this.iiifReferences.some(Boolean);
+ }
+
+ // True if the record can only be previewed via IIIF references (image or manifest)
+ get iiifOnly() {
+ return !this.mapPreviewable && this.iiifPreviewable;
+ }
+
+ // Get all references that can be rendered for preview
+ private get previewableReferences() {
+ return this.mapPreviewableReferences.concat(this.iiifReferences);
+ }
+
+ // Get all references that can be rendered on a map
+ private get mapPreviewableReferences() {
+ return [this.wmsUrl, this.cogUrl, this.tmsUrl, this.xyzUrl, this.geojsonUrl, this.tilejsonUrl, this.indexMapUrl, this.pmtilesUrl, this.wmtsUrl];
+ }
+
+ // Get all IIIF references (image and manifest)
+ private get iiifReferences() {
+ return [this.iiifImageUrl, this.iiifManifestUrl];
+ }
+
+ // Get the IIIF presentation spec version of the manifest, if we have one
+ private get iiifVersion() {
+ if (!this.iiifManifest) return null;
+ return this.iiifManifest['@context']?.includes('http://iiif.io/api/presentation/3/context.json') ? 3 : 2;
+ }
+
+ // Given a v2 manifest, extract all of the IIIF images and format as info.json URLs
+ private extractIiif2ImageUrls(manifest: iiif2.Manifest): string[] {
+ return manifest.sequences
+ .flatMap(seq => seq.canvases)
+ .flatMap(can => can.images)
+ .flatMap(img => img.resource)
+ .flatMap(res => (res['@type'] === 'dctypes:Image' ? res.service['@id'] + '/info.json' : []));
+ }
+
+ // Given a v3 manifest, extract all of the IIIF images and format as info.json URLs
+ private extractIiif3ImageUrls(manifest: iiif3.Manifest): string[] {
+ // Recursively search the '.items' key until we end up with nodes that have type 'ImageService2'
+ return (
+ manifest.items
+ .flatMap(canvas => canvas.items)
+ .flatMap(annotationPage => annotationPage.items)
+ .flatMap(annotation => {
+ if (annotation.body instanceof Array) {
+ return annotation.body;
+ } else {
+ return [annotation.body];
+ }
+ })
+ //@ts-ignore
+ .flatMap(annotationBody => annotationBody.service)
+ .flatMap(service => service.id + '/info.json')
+ );
+ }
+
+ // TODO: use navPlace as the bounds source if available
+
+ // Attempt to fetch and parse the IIIF manifest, if any
+ async fetchManifest(): Promise {
+ if (!this.iiifManifestUrl) return null;
+
+ try {
+ const response = await fetch(this.iiifManifestUrl);
+ if (!response.ok) throw new Error(`Unexpected response fetching IIIF manifest: ${response.statusText}`);
+ const manifest = await response.json();
+ this.iiifManifest = manifest;
+ return manifest;
+ } catch (error) {
+ console.error(error.message);
+ this.iiifManifest = null;
+ return null;
+ }
+ }
}
diff --git a/src/utils/sources.ts b/src/utils/sources.ts
index 14ed7b7..95b7b6f 100644
--- a/src/utils/sources.ts
+++ b/src/utils/sources.ts
@@ -97,7 +97,7 @@ const getRecordSource = (record: OgmRecord): AddSourceObject => {
const recordXYZSource = (record: OgmRecord): AddSourceObject => {
// If no XYZ reference, nothing to do
- const xyzUrl = record.references.xyz;
+ const xyzUrl = record.references.xyzUrl;
if (!xyzUrl) return null;
return {
@@ -115,7 +115,7 @@ const recordXYZSource = (record: OgmRecord): AddSourceObject => {
// Given a record, create a MapLibre TMS source, if possible
const recordTMSSource = (record: OgmRecord): AddSourceObject => {
// If no TMS reference, nothing to do
- const tmsUrl = record.references.tms;
+ const tmsUrl = record.references.tmsUrl;
if (!tmsUrl) return null;
return {
@@ -133,7 +133,7 @@ const recordTMSSource = (record: OgmRecord): AddSourceObject => {
// Given a record, create a MapLibre GeoJSON source, if possible
const recordGeoJSONSource = (record: OgmRecord): AddSourceObject => {
// If no GeoJSON reference, nothing to do
- const geojsonUrl = record.references.geojson;
+ const geojsonUrl = record.references.geojsonUrl;
if (!geojsonUrl) return null;
// Create a GeoJSON source with the record's ID and attribution
@@ -150,7 +150,7 @@ const recordGeoJSONSource = (record: OgmRecord): AddSourceObject => {
// Given a record, create a MapLibre COG source, if possible
const recordCOGSource = (record: OgmRecord): AddSourceObject => {
// If no COG reference, nothing to do
- const cogUrl = record.references.cog;
+ const cogUrl = record.references.cogUrl;
if (!cogUrl) return null;
// Add the cog:// protocol that will tell MapLibre to use the plugin
@@ -170,7 +170,7 @@ const recordCOGSource = (record: OgmRecord): AddSourceObject => {
// Given a record, create a MapLibre WMS source, if possible
const recordWMSSource = (record: OgmRecord): AddSourceObject => {
// If no WMS reference or no WXS layer identifier, nothing we can do
- const wmsUrl = record.references.wms;
+ const wmsUrl = record.references.wmsUrl;
if (!wmsUrl) return null;
const layerIds = [record.wxsIdentifier];
if (!layerIds[0]) return null;