@georeferencing/react
v0.6.0
Published
React editor and panels for the headless georeferencing core, with any map library through map adapters
Readme
@georeferencing/react
React 19 components and hooks for georeferencing local images on an existing OpenLayers, MapLibre GL or Leaflet map, connected through a map adapter. This package provides the ready-made editor, composable panels, subscription hook, unsaved-work dialog and scoped styles.
The authoritative session and processing engine live in
@georeferencing/core.
Optional exports come from
@georeferencing/plugins.
React has no runtime dependency on plugins, pdf-lib or any map library: it displays
formats configured on the controller and attaches the map adapter you pass.
Installation and compatibility
pnpm add @georeferencing/core @georeferencing/react react@19 react-dom@19
# A map adapter, for example OpenLayers (or @georeferencing/maplibre, @georeferencing/leaflet):
pnpm add @georeferencing/openlayers ol@10
# Optional output formats:
pnpm add @georeferencing/plugins
# TypeScript React applications also need the React declarations:
pnpm add -D @types/react@19 @types/react-dom@19Verified peer ranges are React/React DOM >=19.3.0 <20; map-library ranges are peers
of the adapter packages. The host owns these libraries and its map. Packages ship ESM and
TypeDoc-bearing TypeScript declarations. Node.js 22.12+ is the declared tooling
requirement; the repository uses pnpm 12.x.
Importing the package is SSR-safe. Create maps and start browser processing only on the client.
Install the same version of @georeferencing/react, @georeferencing/core,
@georeferencing/plugins and your map adapter, and upgrade them together. React declares core as a peer
dependency and re-exports its controller; a mismatched version is reported as a
peer-dependency conflict rather than installing a second core with a separate
projection registry. See
keep package versions aligned.
Integrate with a host-owned map
The map must already have a visible target, dimensions and initialized view. Make
one stable controller/engine per session, and inject your real save service. Create
the map adapter once per map (here with useMemo): a new adapter re-attaches the map.
This example needs no export plugin and sends geographic features to the host.
import type { ControllerOptions } from "@georeferencing/core";
import { GeoreferencerController } from "@georeferencing/core";
import { createWorkerEngine } from "@georeferencing/core/engine";
import { openLayers } from "@georeferencing/openlayers";
import { Georeferencer } from "@georeferencing/react";
import type OLMap from "ol/Map.js";
import { useMemo } from "react";
import "ol/ol.css";
import "@georeferencing/react/styles.css";
export function createEditorSession(
workingCrs: string,
persistFeatures: NonNullable<ControllerOptions["onSave"]>,
persistDraft?: ControllerOptions["onSaveDraft"],
) {
const engine = createWorkerEngine();
const controller = new GeoreferencerController({
workingCrs,
engine,
digitizing: true,
onSave: persistFeatures,
onSaveDraft: persistDraft,
});
return {
controller,
dispose() {
controller.dispose();
engine.dispose();
},
};
}
export function ImageEditor({
map,
session,
}: {
map: OLMap;
session: ReturnType<typeof createEditorSession>;
}) {
const adapter = useMemo(() => openLayers(map), [map]);
return <Georeferencer controller={session.controller} map={adapter} />;
}Map libraries
The editor works the same way with every adapter; only creating the map and the adapter differs:
| | OpenLayers | MapLibre GL JS | Leaflet |
| --- | --- | --- | --- |
| Adapter | openLayers(map, options) | maplibre(map, options) | leaflet(map, { lib: L, ...options }) |
| Package | @georeferencing/openlayers + ol | @georeferencing/maplibre + maplibre-gl | @georeferencing/leaflet + leaflet |
| Stylesheet | ol/ol.css | maplibre-gl/dist/maplibre-gl.css (required for marker placement) | leaflet/dist/leaflet.css |
| Setup | defaults({ onFocusOnly: false }) for a focusable map target | setWorkerUrl(...) before creating maps | Pass the Leaflet module as lib |
| Drawing | Built in | Optional terra-draw + terra-draw-maplibre-gl-adapter | Optional terra-draw + terra-draw-leaflet-adapter |
| PDF map page | captureOpenLayersMap(map) | captureMapLibreMap(map) | Not available |
In the guided layout, the map's container is rendered by the editor (inside
referenceView), so the map cannot exist before the editor first renders. The
React and map libraries guide
shows a small useHostMap hook for this case and a complete, tested integration for
each library. The map adapters guide
compares the adapters' capabilities.
The host calls createEditorSession once, for example when opening an editor,
and retains it across renders. Render ImageEditor only after the host map exists.
On permanent close, resolve unsaved work, unmount the editor, then dispose the
session. A temporary detach/remount must retain the same controller.
With Vite, use ES module workers:
export default { worker: { format: "es" } };The stylesheet is optional. It uses rg- classes; the host controls map layout
and dimensions. Core's engine and each optional codec support explicit worker
URLs/factories for deployments that do not preserve default bundled asset paths.
Ready-made editor props
| Prop | Contract |
| --- | --- |
| controller | Required stable authoritative session controller |
| map | Required stable map adapter, such as openLayers(map, options), maplibre(map, options) or leaflet(map, { lib: L }); references, projections, snapping and initial framing are adapter options |
| definitions | Host projection definitions for coordinate-unit labels in output settings |
| t | Translate default English text, including configured format labels |
| formatError | Format structured errors by code/message/operation ID |
| className | Additional editor root class |
| propertyEditor | Host feature-property form; update replacement JSON properties |
| onExport | Receive any configured format's artifacts instead of automatic downloads |
Create the adapter once (for example with useMemo) or outside rendering. Changing
its identity re-attaches the map. The ready-made component owns its attachment; do not
also attach the same controller and map yourself. Map-history and "Finish drawing"
buttons appear only when the adapter's binding supports them.
Select exports on demand
No format is enabled implicitly. Configure the controller at creation or replace its registry at runtime:
import type { GeoreferencerController } from "@georeferencing/core";
import { geoTiff } from "@georeferencing/plugins/geotiff";
import { jpeg } from "@georeferencing/plugins/jpeg";
import type { MapCapture } from "@georeferencing/core/map";
import { pdf } from "@georeferencing/plugins/pdf";
export function enableExports(
controller: GeoreferencerController,
captureMap: () => Promise<MapCapture> | undefined,
) {
controller.setExportFormats([
geoTiff(),
jpeg({ quality: 0.92 }),
pdf({ capture: captureMap }),
]);
}The UI shows only these actions. setExportFormats replaces the registry and
cancels active export work. Import a subset to exclude unused implementations
from the browser bundle; heavy format code loads on request.
The default UI downloads every artifact. onExport(result) can handle them in
host code; inspect result.format, result.document and each { name, blob }
in result.files. JPEG and world-file exports include required CRS/placement
sidecars. Export completion never acknowledges host persistence. See the
plugins README
for format settings, cancellation and limits.
Guided four-step layout
Supply referenceView to Georeferencer to use the guided layout. It is a React
node containing the host's map target and optional map controls; the host still
owns the map and must attach its target before editor effects run (a React
callback ref is suitable). Without this prop, the classic workbench with every
control remains. See the
complete demo host
for a typed integration with target attachment and cleanup.
The guided layout leads users through four steps, each with one main action in a sticky bar:
- Load image: drop zone with Choose image and any
emptyImageActions(for example a sample-image button). - Match points: click a spot in the image, then the same spot on the map. Progress shows how many points the model needs; Run alignment fits them.
- Check alignment: the overlay on the map, a plain-language accuracy summary (average residual in image pixels, or a hint to add a point when the fit is exact) and the point list. Adjust points goes back; Looks good, continue confirms the alignment.
- Export or draw: download buttons for the configured formats and, with
digitizing, drawing tools and Save features. Back to check returns.
Completed steps in the step bar are clickable. Desktop keeps both views side by
side; below 720 px, Image/Map buttons switch panes and follow pending points.
In both views, dragging pans and scrolling zooms; a click without dragging places
a point. Zooming the image beyond the reduced preview's resolution loads the
full-resolution image (controller.requestDetailImage()), and source pixels are shown
crisply once each covers several screen pixels, so points can be placed at the
original resolution.
With OpenLayers, the Map creates its default interactions with onFocusOnly: true. If
the map target has a tabindex (for keyboard navigation), mouse panning and wheel
zoom then only work after the map has focus. Create the host map with
interactions: defaults({ onFocusOnly: false }) from ol/interaction/defaults.js,
as the demo does, so both work immediately.
Choose the controls
The guided layout starts minimal. Add expert controls individually with
controls; omitted flags fall back to MINIMAL_CONTROLS:
import { ALL_CONTROLS, Georeferencer } from "@georeferencing/react";
<Georeferencer
controller={controller}
map={adapter}
referenceView={mapView}
controls={{ transformation: true, outputSettings: true }}
/>;
// Every control: controls={ALL_CONTROLS}| Flag | Default | Adds |
| --- | --- | --- |
| history | on | Undo and redo |
| previewMode | off | Manual/automatic preview selector (otherwise the controller's previewMode applies) |
| transformation | off | Transformation model selector while matching |
| pointTable | off | Full point table with editable coordinates, CRS and residuals (otherwise a compact list) |
| manualEntry | off | Add a point pair by typing coordinates, the keyboard alternative to clicking |
| navigation | off | Previous/next image and map views, linked navigation |
| displayAdjustment | off | Brightness, contrast and histogram stretch |
| referenceStatus | off | Load status of reference providers |
| outputSettings | off | Raster output CRS, resampling, compression, no-data, pixel size and bounds |
| sessionFiles | off | Restore session, import .points, save draft and early session downloads |
| unsavedIndicator | off | Unsaved-changes indicator |
Enable manualEntry when keyboard-only point entry is required.
Preview policy and panels
previewMode: "manual" on the controller waits for Run alignment;
"automatic" updates eligible previews after committed edits without advancing
the user's step. Worker validation reports singular or unusable arrangements when
a run is requested.
The composable panels keep every control by default and accept the same opt-outs:
GcpPanel takes variant ("table" or "compact"), manualEntry and tools;
ImagePanel takes navigation, displayAdjustment and emptyActions;
PreviewControls takes modeSelector and calls onReview only for a successful
preview matching the requested revision. AlignmentPanel accepts
controls={false} when transformation and preview controls are rendered
elsewhere. These panels never take map or engine ownership.
References, projections and snapping
Configure references, projections and snapping as options of the map adapter. Providers include bounded viewport WFS, static GeoJSON and custom abortable loaders with every adapter; the OpenLayers adapter also reads WFS GML and borrowed host vector layers, which retain their host loading and ownership. Reference errors and partial results appear in the UI without disabling manual target entry.
Configure real endpoint/type names, request/response CRSs and axis conventions.
Preserve AbortSignal when injecting authentication. Query bounds limit reference
loading; initialView only frames the map. Neither georeferences the image.
Drawing bounds and raster bounds are independent.
Register the same custom projection definitions and NTv2 grids with the engine
and binding. Image GCPs use original-resolution, orientation-normalized pixels;
feature drafts/save payloads use longitude/latitude regardless of the map view.
Supplying provider snapping opts into GCP snapping. Drawing snapping is separately
controlled by digitizingSnapping.references and .drafts. The
reference guide
contains typed provider examples.
Image lifecycle, confirmation and saving
The image picker and drop zone use controller lifecycle methods. Selection supports files up to 25 MiB, subject to decoded-pixel and memory budgets. Replacing/removing an image invokes the ready-made Save/Discard/Cancel dialog when needed, cancels old processing and preserves the host map view. Original bytes stay outside the serializable document. Restoring a JSON session requires the matching source file.
Digitizing is opt-in through digitizing: true. A valid current alignment must be
explicitly confirmed before drawing multiple points, lines or polygons. Features
can be modified, deleted and undone/redone. Returning to alignment leaves them
geographically anchored; subsequent alignment changes require confirmation and
explicit feature review before saving accepted features again.
onChange mirrors draft state. onSave receives an immutable accepted-feature
envelope with stable IDs, source fingerprint, alignment and provenance.
onSaveDraft can persist unfinished sessions separately. A save callback resolves
after actual storage success and rejects on failure; a failed draft remains
retryable. Older save completion does not acknowledge newer edits. Feature saving
never requires raster export. Hosts must separately persist source bytes and
protect route/window closure; the image-transition dialog is not a general router
or browser-close guard.
React Strict Mode and ownership
The editor resumes the controller during setup and suspends it during cleanup. It adds/removes only package-owned map layers/interactions. It does not dispose the host map, borrowed layers, controller or engine on unmount. This permits React 19 Strict Mode setup/cleanup cycles and later remounts.
Use permanent dispose() only after the host has finished with the session;
do not put it in an effect cleanup that Strict Mode uses for temporary teardown.
Host interactions remain host-owned. Use onActiveToolChange in controller options
to coordinate conflicting host drawing tools explicitly. Pending host saves are
revision-safe but are not cancelled by UI suspension.
Compose a custom UI
| Export | Purpose |
| --- | --- |
| Georeferencer | Complete editor, map attachment, guard dialog and output controls |
| useGeoreferencer(controller) | Subscribe to the authoritative snapshot |
| ImagePanel | Image selection, image view and image-side GCP interaction |
| GcpPanel | Paired-point table, numeric editing and diagnostics |
| PreviewControls | Manual/automatic preview policy, readiness and explicit run/review |
| AlignmentPanel | Transformation selection, confirmation and alignment controls |
| ReferencePanel | Reference loading/error/incomplete-result feedback |
| FeaturePanel | Drawing tools, features/properties, review and save controls |
Composable panels do not install a map binding or unsaved-work dialog. Supply a
controller with a host guard, attach the map once with an adapter and manage
suspension yourself:
import type { GeoreferencerController } from "@georeferencing/core";
import { attachReferenceMap, type BindingOptions } from "@georeferencing/openlayers";
import {
AlignmentPanel, FeaturePanel, GcpPanel, ImagePanel,
ReferencePanel, useGeoreferencer,
} from "@georeferencing/react";
import type OLMap from "ol/Map.js";
import { useEffect } from "react";
export function CustomEditor({ map, controller, options }: {
map: OLMap;
controller: GeoreferencerController;
options: BindingOptions;
}) {
const state = useGeoreferencer(controller);
useEffect(() => {
controller.start();
const binding = attachReferenceMap(map, controller, options);
return () => {
binding.detach();
controller.suspend();
};
}, [map, controller, options]);
return (
<div className="rg-editor">
<ImagePanel controller={controller} />
<GcpPanel controller={controller} />
<AlignmentPanel controller={controller} />
<ReferencePanel controller={controller} />
<FeaturePanel controller={controller} />
<p role="status">{state.error ?? `Document revision ${state.document.documentRevision}`}</p>
</div>
);
}Custom layouts provide their own export/session actions through controller APIs.
Keep the options object stable. Omitting a headless guard safely cancels dirty
image transitions instead of silently discarding them.
Localization and property forms
Use t for UI messages and formatError for structured dynamic errors. Business
properties remain host-defined JSON. Never mutate the frozen feature passed to
a property renderer; call its update callback with replacement properties.
import type { GeoreferencerProps } from "@georeferencing/react";
export const propertyEditor: GeoreferencerProps["propertyEditor"] = (feature, update) => (
<label>
Name
<input
value={String(feature.properties.name ?? "")}
onChange={(event) => update({ ...feature.properties, name: event.target.value })}
/>
</label>
);
export const translate: NonNullable<GeoreferencerProps["t"]> = (message) => {
const messages: Record<string, string> = { "Choose image": "Bild auswählen" };
return messages[message] ?? message;
};Pass these functions as props to Georeferencer, or to the relevant composable
panels. The package includes focusable map/image controls, labeled inputs and
status feedback; hosts remain responsible for accessible custom property editors,
layout, translations and their application's overall keyboard behavior.
Source organization
src/index.ts defines the public exports. The editor lives in Georeferencer.tsx,
composable panels in panels/, the subscription hook in hooks/, shared fields
in components/, and download handling in utils/. Translation types/defaults
live in localization.ts. These internal modules preserve the public API; consumers
continue importing from @georeferencing/react and its styles.css entry.
Troubleshooting and license
@georeferencing/react exposes . and /styles.css. Declarations carry TypeDoc
comments for editor help; the same comments are published as the
API documentation. Source, issues and contribution notes are in the
GitHub repository. MIT licensed.
| Symptom | Check |
| --- | --- |
| Blank map or missing overlay | Host map target/size, valid current fit and overlay visibility |
| Repeated reference loads / lost map interaction state | Stable controller, map and memoized binding options |
| Duplicate editor layers/interactions | Only one attachment per controller/map; avoid manual binding alongside Georeferencer |
| Drawing/save controls unavailable | Opt-in digitizing, valid confirmation, feature review and configured save handler |
| Export controls absent | Register the desired optional plugins |
| Worker or map-capture error | Worker URLs/CSP, supported browser APIs and CORS-safe map layers |
