🎉 React 19 support is here! Clover IIIF v3.0.0 works with the latest React frameworks.
Documentation
Map

Map

A UI component that renders a geographic map using Leaflet (opens in a new tab), with native support for IIIF navPlace (opens in a new tab) geographic annotations and Georeference Extension (opens in a new tab) control points. Optionally renders warped IIIF image overlays via @allmaps/leaflet (opens in a new tab).

navPlace Extension Georeference Extension

Features

Provide a IIIF navPlace GeoJSON value, a Georeference Extension annotation, or both, and the component:

  • Renders GeoJSON features from IIIF navPlace values (FeatureCollection, Feature, or bare geometry)
  • Resolves label properties as IIIF InternationalString (opens in a new tab) or plain strings for tooltips
  • Displays Ground Control Points (GCPs) from a Georeference Extension annotation as numbered markers
  • Optionally renders a warped IIIF image overlay via @allmaps/leaflet (opens in a new tab) when showImageOverlay is enabled
  • Auto-fits the viewport to all displayed data with fitToData
  • Accepts a click callback for coordinate picking (onMapClick)
  • Configurable tile layer (defaults to OpenStreetMap (opens in a new tab))

Installation

  npm install @samvera/clover-iiif

Usage

React

Add the Map component to your jsx or tsx code.

import Map from "@samvera/clover-iiif/map";

Next.js

Implementation with Next.js requires a dynamic import (opens in a new tab) because Leaflet assumes a browser document object.

import dynamic from "next/dynamic";
 
const Map = dynamic(
  () => import("@samvera/clover-iiif").then((Clover) => Clover.Map),
  { ssr: false },
);

navPlace

Render geographic features from a IIIF navPlace (opens in a new tab) value. The navPlace prop accepts a GeoJSON FeatureCollection, Feature, or bare geometry object. Feature properties.label may be a plain string or a IIIF InternationalString, and will be shown as a tooltip and popup.

You can also pass a IIIF resource through iiifContent. Clover will extract navPlace values from Collections, Manifests, Canvases, and Annotations, and the popup will include the resource context that supplied the feature, including its label, summary, and thumbnail when present.

<Map
  iiifContent="https://iiif.io/api/cookbook/recipe/0240-navPlace-on-canvases/manifest.json"
  navPlaceLevel="Canvas"
  fitToData
/>

The example below uses features from IIIF Cookbook recipe 0240: navPlace on Canvases (opens in a new tab), where navPlace is declared directly on each Canvas. To consume it, fetch the manifest and read canvas.navPlace from each item:

// Fetch the manifest and collect navPlace features from all canvases
const manifest = await fetch(
  "https://iiif.io/api/cookbook/recipe/0240-navPlace-on-canvases/manifest.json"
).then((r) => r.json());
 
const navPlace = {
  type: "FeatureCollection",
  features: manifest.items.flatMap(
    (canvas) => canvas.navPlace?.features ?? []
  ),
};
 
// navPlace now contains two features:
// - Current Location of the Laocoön Bronze  [-118.47, 34.07]  (Getty, Los Angeles)
// - Current Location of Painting            [-77.02,  38.89]  (National Gallery, DC)
 
<div style={{ height: "400px" }}>
  <Map navPlace={navPlace} fitToData />
</div>

Polygon

A Polygon geometry draws a filled region. This example uses a bounding box from GeoNames (opens in a new tab) to outline the extent of Lake Michigan.

const navPlace = {
  type: "Feature",
  properties: { label: { en: ["Lake Michigan"] } },
  geometry: {
    type: "Polygon",
    coordinates: [
      [
        [-87.91178, 41.61793], // SW
        [-84.72398, 41.61793], // SE
        [-84.72398, 46.10230], // NE
        [-87.91178, 46.10230], // NW
        [-87.91178, 41.61793], // close ring
      ],
    ],
  },
};
 
<div style={{ height: "400px" }}>
  <Map navPlace={navPlace} fitToData />
</div>

LineString

A LineString draws a connected path between an ordered sequence of coordinates. This example traces Historic Route 66 (opens in a new tab) from Chicago to Santa Monica using city coordinates from GeoNames.

const navPlace = {
  type: "Feature",
  properties: { label: { en: ["Historic Route 66"] } },
  geometry: {
    type: "LineString",
    coordinates: [
      [-87.65005, 41.85003], // Chicago, IL
      [-89.64371, 39.80172], // Springfield, IL
      [-90.19789, 38.62727], // St. Louis, MO
      [-94.51328, 37.08423], // Joplin, MO
      [-97.51643, 35.46756], // Oklahoma City, OK
      [-101.83130, 35.22200], // Amarillo, TX
      [-106.65114, 35.08449], // Albuquerque, NM
      [-111.65127, 35.19807], // Flagstaff, AZ
      [-118.49138, 34.01949], // Santa Monica, CA
    ],
  },
};
 
<div style={{ height: "400px" }}>
  <Map navPlace={navPlace} fitToData />
</div>

GeometryCollection

A GeometryCollection bundles multiple geometry types into a single navPlace feature. This example maps the western Lake Michigan shoreline as a composite: a bounding Polygon for the lake, a LineString tracing the shore from Gary to Green Bay, and a MultiPoint marking five lakeshore cities, all sharing one label.

const navPlace = {
  type: "Feature",
  properties: { label: { en: ["Lake Michigan Shoreline"] } },
  geometry: {
    type: "GeometryCollection",
    geometries: [
      {
        type: "Polygon",
        coordinates: [[
          [-87.91178, 41.61793],
          [-84.72398, 41.61793],
          [-84.72398, 46.10230],
          [-87.91178, 46.10230],
          [-87.91178, 41.61793],
        ]],
      },
      {
        type: "LineString",
        coordinates: [
          [-87.34643, 41.59337], // Gary, IN
          [-87.65005, 41.85003], // Chicago, IL
          [-87.90647, 43.03890], // Milwaukee, WI
          [-88.01983, 44.51916], // Green Bay, WI
        ],
      },
      {
        type: "MultiPoint",
        coordinates: [
          [-87.34643, 41.59337], // Gary, IN
          [-87.65005, 41.85003], // Chicago, IL
          [-87.90647, 43.03890], // Milwaukee, WI
          [-88.01983, 44.51916], // Green Bay, WI
          [-85.62063, 44.76306], // Traverse City, MI
        ],
      },
    ],
  },
};
 
<div style={{ height: "400px" }}>
  <Map navPlace={navPlace} fitToData />
</div>

Georeference Extension

Render Ground Control Points (GCPs) from a IIIF Georeference Extension (opens in a new tab) annotation. Pass a parsed annotation object to georefAnnotation and the component will extract the GCPs and display them as numbered markers on the map. GCPs are included in fitToData bounds.

The example below uses a real annotation from Northwestern University Libraries' Map of the Township of Evanston, Cook County, Illinois (opens in a new tab). The annotation's target.source is a Canvas.

// Annotation as returned by dc-api; target.source.type is "Canvas"
const georefAnnotation = {
  "@context": [
    "http://iiif.io/api/extension/georef/1/context.json",
    "http://iiif.io/api/presentation/3/context.json",
  ],
  id: "urn:meadow:georeference:2fb1e81a-...",
  type: "Annotation",
  motivation: "georeferencing",
  target: {
    type: "SpecificResource",
    source: {
      id: "https://api.dc.library.northwestern.edu/api/v2/file-sets/2fb1e81a-9e24-420c-b224-0bfd6a279baf?as=iiif",
      type: "Canvas",
      width: 7337,
      height: 9833,
    },
    selector: { type: "SvgSelector", value: "<svg ...>" },
  },
  body: {
    type: "FeatureCollection",
    features: [
      {
        type: "Feature",
        properties: { confidence: "medium", resourceCoords: [577.7, 1074.97] },
        geometry: { type: "Point", coordinates: [-87.71064, 42.06793] },
      },
      // ... 2 more GCPs
    ],
  },
};
 
<div style={{ height: "400px" }}>
  <Map georefAnnotation={georefAnnotation} fitToData />
</div>

Warped Image Overlay

When showImageOverlay is enabled and a georefAnnotation with at least 3 GCPs is provided, the component renders a warped IIIF image overlay using @allmaps/leaflet (opens in a new tab).

Important: @allmaps/leaflet requires the annotation's target.source to reference an IIIF Image API endpoint (type: "ImageService2" or "ImageService3"), not a Canvas. Annotations saved by authoring tools typically use a Canvas source. Use the adaptGeoreferenceAnnotationForOverlay helper to swap the source before passing it to <Map>.

The example below uses the same Evanston map annotation adapted with the FileSet's IIIF Image API v3 endpoint as the ImageService3 source.

import Map, { adaptGeoreferenceAnnotationForOverlay } from "@samvera/clover-iiif/map";
 
// Annotation target.source.type is "Canvas"
const storedAnnotation = { /* ... */ };
 
// IIIF Image API v3 base URL for the FileSet
const imageServiceId =
  "https://iiif.dc.library.northwestern.edu/iiif/3/2fb1e81a-9e24-420c-b224-0bfd6a279baf";
 
// Adapt the annotation so @allmaps/leaflet can use it
const overlayAnnotation = adaptGeoreferenceAnnotationForOverlay(
  storedAnnotation,
  imageServiceId,
);
 
<div style={{ height: "500px" }}>
  <Map
    georefAnnotation={overlayAnnotation}
    showImageOverlay
    imageOverlayOpacity={0.65}
    fitToData
  />
</div>

@allmaps/leaflet is an optional dependency. It is only loaded when showImageOverlay is true, so consumers who don't use this feature pay no bundle cost.


Combining navPlace and Georeference

navPlace and georefAnnotation can be used together. When fitToData is enabled the viewport will expand to cover all features, GCPs, and any additional markers.

<div style={{ height: "400px" }}>
  <Map navPlace={navPlace} georefAnnotation={georefAnnotation} fitToData />
</div>

Coordinate Picking

Use onMapClick and useCrosshairCursor to turn the map into a coordinate picker. The callback receives [longitude, latitude] rounded to five decimal places.

const [coordinates, setCoordinates] = React.useState(null);
 
<div>
  <div style={{ height: "400px" }}>
    <Map
      center={{ latitude: 42.045, longitude: -87.688, zoom: 11 }}
      markers={
        coordinates
          ? [
              {
                latitude: coordinates.lat,
                longitude: coordinates.lng,
                label: "Selected coordinate",
              },
            ]
          : []
      }
      onMapClick={([lng, lat]) => setCoordinates({ lng, lat })}
      useCrosshairCursor
    />
  </div>
 
  <output>
    {coordinates
      ? `longitude: ${coordinates.lng}, latitude: ${coordinates.lat}`
      : "Click the map to pick coordinates."}
  </output>
</div>

API Reference

Map props

PropTypeRequiredDefault
navPlaceGeoJSON.FeatureCollection | Feature | GeometryNo
iiifContentstring | objectNo
navPlaceLevelCollection | Manifest | Canvas | Annotation | allNoall
georefAnnotationGeoreferenceAnnotationNo
showControlPointsbooleanNotrue
showImageOverlaybooleanNofalse
imageOverlayOpacitynumber (0-1)No0.65
geoJsonGeoJSON.FeatureCollection | FeatureNo
markersMapMarker[]No[]
centerMapCenterNoWorld view
fitToDatabooleanNofalse
onMapClick(coordinates: [number, number]) => voidNo
useCrosshairCursorbooleanNofalse
tileLayerMapTileLayerNoOpenStreetMap

GeoreferenceAnnotation type

A IIIF Georeference Extension (opens in a new tab) annotation. The body is a GeoJSON FeatureCollection whose features carry properties.resourceCoords (image pixel [x, y]) paired with a Point geometry (geographic [longitude, latitude]).

interface GeoreferenceAnnotation {
  "@context"?: string | string[];
  id?: string;
  type: "Annotation";
  motivation: "georeferencing";
  target: {
    type: "SpecificResource";
    source: {
      id: string;
      type: string; // "Canvas" | "ImageService2" | "ImageService3"
      width?: number;
      height?: number;
    };
    selector?: { type: "SvgSelector"; value: string };
  };
  body: GeoJSON.FeatureCollection;
}

MapMarker type

interface MapMarker {
  latitude: number;
  longitude: number;
  label?: string;
  color?: string;
}

MapCenter type

interface MapCenter {
  latitude: number;
  longitude: number;
  zoom: number;
}

MapTileLayer type

interface MapTileLayer {
  url: string; // e.g. "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
  attribution: string;
}

Helpers

The following functions are exported from @samvera/clover-iiif/helpers and @samvera/clover-iiif to assist with processing IIIF georeference and navPlace data.

parseGeoreferenceAnnotation

Extracts Ground Control Points from a GeoreferenceAnnotation. Features with missing or non-numeric coordinates are silently skipped.

import { helpers } from "@samvera/clover-iiif";
 
const gcps = helpers.parseGeoreferenceAnnotation(annotation);
// → [{ id, resourceCoords: [x, y], geoCoords: [lng, lat] }, ...]

adaptGeoreferenceAnnotationForOverlay

Swaps a Canvas source for an ImageService source so the annotation can be used with @allmaps/leaflet. Does not mutate the input.

import { adaptGeoreferenceAnnotationForOverlay } from "@samvera/clover-iiif";
 
const overlayAnnotation = adaptGeoreferenceAnnotationForOverlay(
  storedAnnotation, // GeoreferenceAnnotation with Canvas source
  imageServiceId, // IIIF Image API base URL
  "ImageService3", // default; pass "ImageService2" for Image API 2 services
);

normalizeNavPlace

Normalises any GeoJSON value (FeatureCollection, Feature, or bare geometry) to a FeatureCollection. Returns null for unrecognised input.

import { helpers } from "@samvera/clover-iiif";
 
const featureCollection = helpers.normalizeNavPlace(canvas.navPlace);

extractNavPlaceFeatures

Extracts navPlace features from IIIF resources and annotates each feature with the Collection, Manifest, Canvas, or Annotation that supplied it.

import { extractNavPlaceFeatures } from "@samvera/clover-iiif";
 
const features = extractNavPlaceFeatures(manifest, { levels: ["Canvas"] });

getNavPlaceLabel

Resolves a navPlace Feature label property to a display string, handling both plain strings and IIIF InternationalString (opens in a new tab).

import { helpers } from "@samvera/clover-iiif";
 
const label = helpers.getNavPlaceLabel(feature.properties.label);
// → "Current Location of the Laocoön Bronze"