@georeferencing/react 0.1.0 → 0.3.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.
- package/README.md +86 -29
- package/dist/Georeferencer.d.ts +18 -5
- package/dist/Georeferencer.js +142 -183
- package/dist/components/accuracy.d.ts +15 -0
- package/dist/components/accuracy.js +39 -0
- package/dist/components/exports.d.ts +32 -0
- package/dist/components/exports.js +152 -0
- package/dist/components/fields.d.ts +5 -2
- package/dist/components/fields.js +11 -6
- package/dist/controls.d.ts +40 -0
- package/dist/controls.js +28 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/panels/AlignmentPanel.js +5 -2
- package/dist/panels/FeaturePanel.js +12 -9
- package/dist/panels/GcpPanel.d.ts +19 -1
- package/dist/panels/GcpPanel.js +26 -10
- package/dist/panels/ImagePanel.d.ts +14 -1
- package/dist/panels/ImagePanel.js +89 -19
- package/dist/panels/PreviewControls.d.ts +3 -1
- package/dist/panels/PreviewControls.js +7 -6
- package/dist/styles.css +203 -49
- package/dist/utils/downloadBlob.d.ts +3 -2
- package/dist/utils/downloadBlob.js +13 -4
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -30,9 +30,10 @@ Importing the package is SSR-safe. Create maps and start browser processing only
|
|
|
30
30
|
on the client.
|
|
31
31
|
|
|
32
32
|
**Install the same version of `@georeferencing/react`, `@georeferencing/core` and
|
|
33
|
-
`@georeferencing/plugins`**, and upgrade them together. React
|
|
34
|
-
re-exports its controller; a mismatched version
|
|
35
|
-
|
|
33
|
+
`@georeferencing/plugins`**, and upgrade them together. React declares core as a peer
|
|
34
|
+
dependency and re-exports its controller; a mismatched version is reported as a
|
|
35
|
+
peer-dependency conflict rather than installing a second core with a separate
|
|
36
|
+
projection registry. See
|
|
36
37
|
[keep package versions aligned](https://georeferencing-api-docs.gh.tobilg.com/Getting_started/#keep-package-versions-aligned).
|
|
37
38
|
|
|
38
39
|
## Integrate with a host-owned map
|
|
@@ -149,35 +150,91 @@ sidecars. Export completion never acknowledges host persistence. See the
|
|
|
149
150
|
[plugins README](https://github.com/tobilg/georeferencing/blob/main/packages/plugins/README.md)
|
|
150
151
|
for format settings, cancellation and limits.
|
|
151
152
|
|
|
152
|
-
## Guided
|
|
153
|
+
## Guided four-step layout
|
|
153
154
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
Supply `referenceView` to `Georeferencer` to opt into the guided layout. It is a
|
|
162
|
-
React node containing the host's map target and optional map controls; the host
|
|
163
|
-
still owns the map and must attach its target before editor effects run (a React
|
|
164
|
-
callback ref is suitable). Without this prop, the existing panel layout remains.
|
|
165
|
-
See the [complete demo host](https://github.com/tobilg/georeferencing/blob/main/packages/demo/main.tsx)
|
|
155
|
+
Supply `referenceView` to `Georeferencer` to use the guided layout. It is a React
|
|
156
|
+
node containing the host's map target and optional map controls; the host still
|
|
157
|
+
owns the map and must attach its target before editor effects run (a React
|
|
158
|
+
callback ref is suitable). Without this prop, the classic workbench with every
|
|
159
|
+
control remains. See the
|
|
160
|
+
[complete demo host](https://github.com/tobilg/georeferencing/blob/main/packages/demo/main.tsx)
|
|
166
161
|
for a typed integration with target attachment and cleanup.
|
|
167
162
|
|
|
168
|
-
The guided layout
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
image
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
163
|
+
The guided layout leads users through four steps, each with one main action in a
|
|
164
|
+
sticky bar:
|
|
165
|
+
|
|
166
|
+
1. **Load image**: drop zone with **Choose image** and any `emptyImageActions`
|
|
167
|
+
(for example a sample-image button).
|
|
168
|
+
2. **Match points**: click a spot in the image, then the same spot on the map.
|
|
169
|
+
Progress shows how many points the model needs; **Run alignment** fits them.
|
|
170
|
+
3. **Check alignment**: the overlay on the map, a plain-language accuracy summary
|
|
171
|
+
(average residual in image pixels, or a hint to add a point when the fit is
|
|
172
|
+
exact) and the point list. **Adjust points** goes back; **Looks good, continue**
|
|
173
|
+
confirms the alignment.
|
|
174
|
+
4. **Export or draw**: download buttons for the configured formats and, with
|
|
175
|
+
`digitizing`, drawing tools and **Save features**. **Back to check** returns.
|
|
176
|
+
|
|
177
|
+
Completed steps in the step bar are clickable. Desktop keeps both views side by
|
|
178
|
+
side; below 720 px, Image/Map buttons switch panes and follow pending points.
|
|
179
|
+
In both views, dragging pans and scrolling zooms; a click without dragging places
|
|
180
|
+
a point. Zooming the image beyond the reduced preview's resolution loads the
|
|
181
|
+
full-resolution image (`controller.requestDetailImage()`), and source pixels are shown
|
|
182
|
+
crisply once each covers several screen pixels, so points can be placed at the
|
|
183
|
+
original resolution.
|
|
184
|
+
|
|
185
|
+
OpenLayers' `Map` creates its default interactions with `onFocusOnly: true`. If
|
|
186
|
+
the map target has a `tabindex` (for keyboard navigation), mouse panning and wheel
|
|
187
|
+
zoom then only work after the map has focus. Create the host map with
|
|
188
|
+
`interactions: defaults({ onFocusOnly: false })` from `ol/interaction/defaults.js`,
|
|
189
|
+
as the demo does, so both work immediately.
|
|
190
|
+
|
|
191
|
+
### Choose the controls
|
|
192
|
+
|
|
193
|
+
The guided layout starts minimal. Add expert controls individually with
|
|
194
|
+
`controls`; omitted flags fall back to `MINIMAL_CONTROLS`:
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
import { ALL_CONTROLS, Georeferencer } from "@georeferencing/react";
|
|
198
|
+
|
|
199
|
+
<Georeferencer
|
|
200
|
+
controller={controller}
|
|
201
|
+
referenceMap={map}
|
|
202
|
+
referenceView={mapView}
|
|
203
|
+
controls={{ transformation: true, outputSettings: true }}
|
|
204
|
+
/>;
|
|
205
|
+
// Every control: controls={ALL_CONTROLS}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
| Flag | Default | Adds |
|
|
209
|
+
| --- | --- | --- |
|
|
210
|
+
| `history` | on | Undo and redo |
|
|
211
|
+
| `previewMode` | off | Manual/automatic preview selector (otherwise the controller's `previewMode` applies) |
|
|
212
|
+
| `transformation` | off | Transformation model selector while matching |
|
|
213
|
+
| `pointTable` | off | Full point table with editable coordinates, CRS and residuals (otherwise a compact list) |
|
|
214
|
+
| `manualEntry` | off | Add a point pair by typing coordinates, the keyboard alternative to clicking |
|
|
215
|
+
| `navigation` | off | Previous/next image and map views, linked navigation |
|
|
216
|
+
| `displayAdjustment` | off | Brightness, contrast and histogram stretch |
|
|
217
|
+
| `referenceStatus` | off | Load status of reference providers |
|
|
218
|
+
| `outputSettings` | off | Raster output CRS, resampling, compression, no-data, pixel size and bounds |
|
|
219
|
+
| `sessionFiles` | off | Restore session, import `.points`, save draft and early session downloads |
|
|
220
|
+
| `unsavedIndicator` | off | Unsaved-changes indicator |
|
|
221
|
+
|
|
222
|
+
Enable `manualEntry` when keyboard-only point entry is required.
|
|
223
|
+
|
|
224
|
+
### Preview policy and panels
|
|
225
|
+
|
|
226
|
+
`previewMode: "manual"` on the controller waits for **Run alignment**;
|
|
227
|
+
`"automatic"` updates eligible previews after committed edits without advancing
|
|
228
|
+
the user's step. Worker validation reports singular or unusable arrangements when
|
|
229
|
+
a run is requested.
|
|
230
|
+
|
|
231
|
+
The composable panels keep every control by default and accept the same opt-outs:
|
|
232
|
+
`GcpPanel` takes `variant` (`"table"` or `"compact"`), `manualEntry` and `tools`;
|
|
233
|
+
`ImagePanel` takes `navigation`, `displayAdjustment` and `emptyActions`;
|
|
234
|
+
`PreviewControls` takes `modeSelector` and calls `onReview` only for a successful
|
|
235
|
+
preview matching the requested revision. `AlignmentPanel` accepts
|
|
236
|
+
`controls={false}` when transformation and preview controls are rendered
|
|
237
|
+
elsewhere. These panels never take map or engine ownership.
|
|
181
238
|
|
|
182
239
|
## References, projections and snapping
|
|
183
240
|
|
package/dist/Georeferencer.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ExportResult, GeoreferencerController } from "@georeferencing/core
|
|
|
2
2
|
import type { BindingOptions } from "@georeferencing/core/openlayers";
|
|
3
3
|
import type OLMap from "ol/Map.js";
|
|
4
4
|
import type { ReactNode } from "react";
|
|
5
|
+
import type { GeoreferencerControls } from "./controls.js";
|
|
5
6
|
import type { Translate } from "./localization.js";
|
|
6
7
|
import { FeaturePanel } from "./panels/FeaturePanel.js";
|
|
7
8
|
/**
|
|
@@ -10,11 +11,23 @@ import { FeaturePanel } from "./panels/FeaturePanel.js";
|
|
|
10
11
|
*/
|
|
11
12
|
export interface GeoreferencerProps {
|
|
12
13
|
/**
|
|
13
|
-
* Opt into
|
|
14
|
-
* target and controls. The host still creates, targets and
|
|
15
|
-
* slot stays mounted across
|
|
14
|
+
* Opt into the guided four-step layout (load, match, check, export/draw) by supplying
|
|
15
|
+
* the host map's rendered target and controls. The host still creates, targets and
|
|
16
|
+
* disposes the map. This slot stays mounted across all steps and responsive
|
|
17
|
+
* image/map tabs.
|
|
16
18
|
*/
|
|
17
19
|
referenceView?: ReactNode;
|
|
20
|
+
/**
|
|
21
|
+
* Optional controls of the guided layout. Omitted flags use {@link MINIMAL_CONTROLS};
|
|
22
|
+
* pass {@link ALL_CONTROLS} for every expert control. The classic layout always shows
|
|
23
|
+
* all controls.
|
|
24
|
+
*/
|
|
25
|
+
controls?: Partial<GeoreferencerControls>;
|
|
26
|
+
/**
|
|
27
|
+
* Extra actions shown in the empty image drop zone next to "Choose image", for example
|
|
28
|
+
* a button that loads a sample image.
|
|
29
|
+
*/
|
|
30
|
+
emptyImageActions?: ReactNode;
|
|
18
31
|
/**
|
|
19
32
|
* Host-owned authoritative controller; the component resumes/suspends it but does not
|
|
20
33
|
* permanently dispose it.
|
|
@@ -53,6 +66,6 @@ export interface GeoreferencerProps {
|
|
|
53
66
|
/**
|
|
54
67
|
* Ready-made React editor for an existing OpenLayers map. Composes image, GCP, alignment, reference, output and optional drawing controls.
|
|
55
68
|
*
|
|
56
|
-
* Its effects attach/detach owned map resources and resume/suspend the controller for React Strict Mode. It installs a Save/Discard/Cancel guard and cancels pending dialogs on cleanup. The host retains final ownership of map, controller and engine. Import the optional scoped stylesheet from `@georeferencing/react/styles.css`.
|
|
69
|
+
* Supplying `referenceView` selects the guided four-step layout, whose optional controls are chosen with `controls`; otherwise the classic workbench shows every control. Its effects attach/detach owned map resources and resume/suspend the controller for React Strict Mode. It installs a Save/Discard/Cancel guard and cancels pending dialogs on cleanup. The host retains final ownership of map, controller and engine. Import the optional scoped stylesheet from `@georeferencing/react/styles.css`.
|
|
57
70
|
*/
|
|
58
|
-
export declare function Georeferencer({ controller, referenceMap, bindingOptions, t, className, propertyEditor, onExport, formatError, referenceView, }: GeoreferencerProps): import("react/jsx-runtime").JSX.Element;
|
|
71
|
+
export declare function Georeferencer({ controller, referenceMap, bindingOptions, t, className, propertyEditor, onExport, formatError, referenceView, controls: controlOverrides, emptyImageActions, }: GeoreferencerProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/Georeferencer.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { MODELS } from "@georeferencing/core";
|
|
3
3
|
import { attachReferenceMap } from "@georeferencing/core/openlayers";
|
|
4
4
|
import { useEffect, useId, useRef, useState } from "react";
|
|
5
|
-
import {
|
|
5
|
+
import { AccuracySummary } from "./components/accuracy.js";
|
|
6
|
+
import { ExportButtons, ExportProgress, OutputFields, SessionTools, } from "./components/exports.js";
|
|
7
|
+
import { MINIMAL_CONTROLS } from "./controls.js";
|
|
6
8
|
import { useGeoreferencer } from "./hooks/useGeoreferencer.js";
|
|
7
9
|
import { identity } from "./localization.js";
|
|
8
10
|
import { AlignmentPanel } from "./panels/AlignmentPanel.js";
|
|
@@ -11,48 +13,77 @@ import { GcpPanel } from "./panels/GcpPanel.js";
|
|
|
11
13
|
import { ImagePanel } from "./panels/ImagePanel.js";
|
|
12
14
|
import { PreviewControls } from "./panels/PreviewControls.js";
|
|
13
15
|
import { ReferencePanel } from "./panels/ReferencePanel.js";
|
|
14
|
-
import { downloadBlob } from "./utils/downloadBlob.js";
|
|
15
16
|
const DEFAULT_BINDING = {};
|
|
17
|
+
const NO_CONTROLS = {};
|
|
18
|
+
/** Activate a tool from an effect, reporting instead of throwing into React. */
|
|
19
|
+
function selectTool(controller, tool) {
|
|
20
|
+
try {
|
|
21
|
+
controller.setTool(tool);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
controller.reportError(error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
16
27
|
/**
|
|
17
28
|
* Ready-made React editor for an existing OpenLayers map. Composes image, GCP, alignment, reference, output and optional drawing controls.
|
|
18
29
|
*
|
|
19
|
-
* Its effects attach/detach owned map resources and resume/suspend the controller for React Strict Mode. It installs a Save/Discard/Cancel guard and cancels pending dialogs on cleanup. The host retains final ownership of map, controller and engine. Import the optional scoped stylesheet from `@georeferencing/react/styles.css`.
|
|
30
|
+
* Supplying `referenceView` selects the guided four-step layout, whose optional controls are chosen with `controls`; otherwise the classic workbench shows every control. Its effects attach/detach owned map resources and resume/suspend the controller for React Strict Mode. It installs a Save/Discard/Cancel guard and cancels pending dialogs on cleanup. The host retains final ownership of map, controller and engine. Import the optional scoped stylesheet from `@georeferencing/react/styles.css`.
|
|
20
31
|
*/
|
|
21
|
-
export function Georeferencer({ controller, referenceMap, bindingOptions = DEFAULT_BINDING, t = identity, className = "", propertyEditor, onExport, formatError, referenceView, }) {
|
|
32
|
+
export function Georeferencer({ controller, referenceMap, bindingOptions = DEFAULT_BINDING, t = identity, className = "", propertyEditor, onExport, formatError, referenceView, controls: controlOverrides = NO_CONTROLS, emptyImageActions, }) {
|
|
22
33
|
const guardTitle = useId();
|
|
23
34
|
const s = useGeoreferencer(controller), binding = useRef(null);
|
|
24
|
-
const [guard, setGuard] = useState(null), dialog = useRef(null)
|
|
25
|
-
const pendingSession = useRef(null);
|
|
35
|
+
const [guard, setGuard] = useState(null), dialog = useRef(null);
|
|
26
36
|
const guided = referenceView !== undefined;
|
|
27
|
-
const
|
|
28
|
-
const [
|
|
37
|
+
const controls = { ...MINIMAL_CONTROLS, ...controlOverrides };
|
|
38
|
+
const [stage, setStage] = useState("match");
|
|
29
39
|
const [mobileView, setMobileView] = useState("image");
|
|
30
|
-
const
|
|
40
|
+
const d = s.document;
|
|
41
|
+
const imageId = d.sourceImage?.id;
|
|
31
42
|
const imageReady = Boolean(s.imageUrl);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const
|
|
43
|
+
const confirmed = d.confirmedAlignmentRevision === d.alignmentRevision;
|
|
44
|
+
// 0 load image · 1 match points · 2 check alignment · 3 export or draw
|
|
45
|
+
const step = !imageReady
|
|
46
|
+
? 0
|
|
47
|
+
: s.mode === "draw" || (stage === "finish" && confirmed && s.fit)
|
|
48
|
+
? 3
|
|
49
|
+
: stage !== "match" && s.fit
|
|
50
|
+
? 2
|
|
51
|
+
: 1;
|
|
52
|
+
const nextPoint = Math.max(0, ...d.gcps.map((p) => p.label)) + 1;
|
|
53
|
+
const hasWork = d.gcps.length > 0 || d.features.features.length > 0;
|
|
35
54
|
useEffect(() => {
|
|
36
55
|
if (!guided)
|
|
37
56
|
return;
|
|
38
|
-
|
|
39
|
-
setReviewedDocumentRevision(null);
|
|
57
|
+
setStage("match");
|
|
40
58
|
setMobileView("image");
|
|
41
59
|
// A metadata-only restored document still needs its matching local bytes.
|
|
42
60
|
if (imageId && imageReady && controller.getSnapshot().mode === "align")
|
|
43
|
-
controller
|
|
61
|
+
selectTool(controller, "gcp");
|
|
44
62
|
}, [controller, guided, imageId, imageReady]);
|
|
45
63
|
useEffect(() => {
|
|
46
64
|
if (guided)
|
|
47
65
|
setMobileView(s.pendingImagePoint ? "map" : "image");
|
|
48
66
|
}, [guided, s.pendingImagePoint]);
|
|
67
|
+
// Matching always uses the point tool; dragging pans, so no separate pan mode is needed.
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (guided && step === 1 && s.tool !== "gcp" && s.mode === "align")
|
|
70
|
+
selectTool(controller, "gcp");
|
|
71
|
+
}, [controller, guided, step, s.tool, s.mode]);
|
|
49
72
|
useEffect(() => {
|
|
50
73
|
if (guided && mobileView === "map")
|
|
51
74
|
referenceMap.updateSize();
|
|
52
75
|
}, [guided, mobileView, referenceMap]);
|
|
53
76
|
useEffect(() => {
|
|
54
77
|
controller.start();
|
|
55
|
-
|
|
78
|
+
try {
|
|
79
|
+
binding.current = attachReferenceMap(referenceMap, controller, bindingOptions);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
// Invalid projection or datum-grid configuration: keep the editor mounted and
|
|
83
|
+
// show the error instead of unmounting the host tree.
|
|
84
|
+
binding.current = null;
|
|
85
|
+
controller.reportError(error);
|
|
86
|
+
}
|
|
56
87
|
return () => {
|
|
57
88
|
binding.current?.detach();
|
|
58
89
|
binding.current = null;
|
|
@@ -92,6 +123,75 @@ export function Georeferencer({ controller, referenceMap, bindingOptions = DEFAU
|
|
|
92
123
|
controller.reportError(e);
|
|
93
124
|
}
|
|
94
125
|
};
|
|
126
|
+
const error = s.error
|
|
127
|
+
? s.errorDetail && formatError
|
|
128
|
+
? formatError(s.errorDetail)
|
|
129
|
+
: t(s.error)
|
|
130
|
+
: null;
|
|
131
|
+
const unsaved = (_jsx("span", { className: "rg-dirty", children: t(s.dirty && hasWork ? "Unsaved changes" : "No unsaved changes") }));
|
|
132
|
+
const goToMatching = () => run(() => {
|
|
133
|
+
if (controller.getSnapshot().mode === "draw")
|
|
134
|
+
controller.returnToAlignment();
|
|
135
|
+
setStage("match");
|
|
136
|
+
setMobileView("image");
|
|
137
|
+
controller.setTool("gcp");
|
|
138
|
+
});
|
|
139
|
+
const goToReview = () => run(() => {
|
|
140
|
+
if (controller.getSnapshot().mode === "draw")
|
|
141
|
+
controller.returnToAlignment();
|
|
142
|
+
setStage("review");
|
|
143
|
+
setMobileView("map");
|
|
144
|
+
controller.setTool("navigate");
|
|
145
|
+
});
|
|
146
|
+
const finish = () => run(() => {
|
|
147
|
+
controller.confirm();
|
|
148
|
+
setStage("finish");
|
|
149
|
+
setMobileView("map");
|
|
150
|
+
});
|
|
151
|
+
const steps = [
|
|
152
|
+
"Load image",
|
|
153
|
+
"Match points",
|
|
154
|
+
"Check alignment",
|
|
155
|
+
controller.options.digitizing ? "Export or draw" : "Export",
|
|
156
|
+
];
|
|
157
|
+
const instruction = step === 0
|
|
158
|
+
? [
|
|
159
|
+
"Choose an image to begin",
|
|
160
|
+
"Drop an image or choose a file. It stays on this device.",
|
|
161
|
+
]
|
|
162
|
+
: step === 1
|
|
163
|
+
? [
|
|
164
|
+
s.pendingImagePoint
|
|
165
|
+
? `${t("Now click the same spot on the map")} · ${t("Point")} ${nextPoint}`
|
|
166
|
+
: `${t("Click a recognizable spot in the image")} · ${t("Point")} ${nextPoint}`,
|
|
167
|
+
stage !== "match"
|
|
168
|
+
? s.previewMode === "manual"
|
|
169
|
+
? "Points or settings changed. Run alignment again to check the result."
|
|
170
|
+
: "Alignment changed. Wait for the updated preview."
|
|
171
|
+
: "Use ground-level corners of buildings, quays or bridges, spread across the whole image. Drag to pan and scroll to zoom in both views; Esc cancels a point.",
|
|
172
|
+
]
|
|
173
|
+
: step === 2
|
|
174
|
+
? [
|
|
175
|
+
"Does the image line up with the map?",
|
|
176
|
+
"Compare roads, shorelines and building corners across the whole image. If something is off, adjust or add points.",
|
|
177
|
+
]
|
|
178
|
+
: controller.options.digitizing
|
|
179
|
+
? [
|
|
180
|
+
"Download the result or draw on the map",
|
|
181
|
+
"Your alignment is confirmed. Download files, or draw points, lines and areas on top of the aligned image.",
|
|
182
|
+
]
|
|
183
|
+
: [
|
|
184
|
+
"Download the result",
|
|
185
|
+
"Your alignment is confirmed. Download the georeferenced image.",
|
|
186
|
+
];
|
|
187
|
+
const primaryFormats = s.exportFormats.filter((format) => format.raster);
|
|
188
|
+
const otherFormats = s.exportFormats.filter((format) => !format.raster);
|
|
189
|
+
const toolRow = (step === 1 || step === 3) &&
|
|
190
|
+
(controls.history ||
|
|
191
|
+
controls.navigation ||
|
|
192
|
+
controls.transformation ||
|
|
193
|
+
controls.unsavedIndicator ||
|
|
194
|
+
["LineString", "Polygon"].includes(s.tool)) ? (_jsxs("div", { className: "rg-toolbar rg-match-tools", children: [step === 3 && ["LineString", "Polygon"].includes(s.tool) && (_jsx("button", { type: "button", onClick: () => binding.current?.finishDrawing(), children: t("Finish drawing") })), controls.history && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", disabled: !s.canUndo, onClick: () => controller.undo(), children: t("Undo") }), _jsx("button", { type: "button", disabled: !s.canRedo, onClick: () => controller.redo(), children: t("Redo") })] })), controls.navigation && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: () => binding.current?.navigateHistory(-1), children: t("Previous map view") }), _jsx("button", { type: "button", onClick: () => binding.current?.navigateHistory(1), children: t("Next map view") })] })), (controls.transformation || controls.unsavedIndicator) && (_jsxs("div", { className: "rg-tools-end", children: [controls.transformation && step === 1 && (_jsxs("label", { className: "rg-inline-field", children: [t("Transformation"), _jsx("select", { value: d.model, onChange: (e) => controller.setModel(e.target.value), children: Object.entries(MODELS).map(([key, value]) => (_jsxs("option", { value: key, children: [t(value.label), " \u00B7 ", value.minimum, "+"] }, key))) })] })), controls.unsavedIndicator && unsaved] }))] })) : null;
|
|
95
195
|
return (_jsxs("section", { className: `rg-editor ${guided ? "rg-guided" : ""} ${className}`, "aria-label": t("Georeferencing editor"), onKeyDownCapture: (e) => {
|
|
96
196
|
if (guided && e.key === "Escape" && s.pendingImagePoint) {
|
|
97
197
|
controller.setPendingPoint(null);
|
|
@@ -103,173 +203,32 @@ export function Georeferencer({ controller, referenceMap, bindingOptions = DEFAU
|
|
|
103
203
|
binding.current?.cancelDrawing();
|
|
104
204
|
e.stopPropagation();
|
|
105
205
|
}
|
|
106
|
-
}, children: [guided ? (_jsxs(_Fragment, { children: [_jsx("ol", { className: "rg-steps", "aria-label": t("Georeferencing steps"), children:
|
|
107
|
-
"
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
? `${t("Select the matching location on the map")} · ${t("Point")} ${nextPoint}`
|
|
123
|
-
: `${t("Select a point in the image")} · ${t("Point")} ${nextPoint}` }), _jsx("p", { children: t(reviewedDocumentRevision !== null &&
|
|
124
|
-
reviewedDocumentRevision !== s.document.documentRevision &&
|
|
125
|
-
!s.fit
|
|
126
|
-
? s.previewMode === "manual"
|
|
127
|
-
? "Points or settings changed. Run alignment again before reviewing or exporting."
|
|
128
|
-
: "Alignment changed. Wait for the updated preview before reviewing."
|
|
129
|
-
: review
|
|
130
|
-
? "Check landmarks across the image before exporting or accepting alignment."
|
|
131
|
-
: "Match ground-level corners across the image. Shift-drag to pan; Escape cancels a pending pair.") })] }), !review && s.mode === "align" && (_jsx(PreviewControls, { controller: controller, t: t, onReview: () => {
|
|
132
|
-
setEditing(false);
|
|
133
|
-
setReviewedDocumentRevision(controller.getSnapshot().document.documentRevision);
|
|
134
|
-
setMobileView("map");
|
|
135
|
-
} })), review && s.mode === "align" && (_jsx("button", { type: "button", onClick: () => {
|
|
136
|
-
setEditing(true);
|
|
137
|
-
controller.setTool("gcp");
|
|
138
|
-
}, children: t("Edit points") }))] }), s.error && (_jsx("p", { className: "rg-error", role: "alert", children: s.errorDetail && formatError
|
|
139
|
-
? formatError(s.errorDetail)
|
|
140
|
-
: t(s.error) })), _jsxs("div", { className: "rg-toolbar rg-match-tools", children: [["LineString", "Polygon"].includes(s.tool) && (_jsx("button", { type: "button", onClick: () => binding.current?.finishDrawing(), children: t("Finish drawing") })), _jsx("button", { type: "button", disabled: !imageReady || s.mode === "draw", "aria-pressed": s.tool === "gcp", onClick: () => {
|
|
141
|
-
setEditing(true);
|
|
142
|
-
controller.setTool("gcp");
|
|
143
|
-
}, children: t("Match points") }), _jsx("button", { type: "button", disabled: !imageReady || s.mode === "draw", "aria-pressed": s.tool === "navigate", onClick: () => controller.setTool("navigate"), children: t("Pan image") }), _jsx("button", { type: "button", disabled: !s.pendingImagePoint, onClick: () => controller.setPendingPoint(null), children: t("Cancel pending pair") }), _jsx("button", { type: "button", disabled: !s.canUndo, onClick: () => controller.undo(), children: t("Undo") }), _jsx("button", { type: "button", disabled: !s.canRedo, onClick: () => controller.redo(), children: t("Redo") }), _jsxs("label", { children: [t("Transformation"), _jsx("select", { disabled: !imageId || s.mode === "draw", value: s.document.model, onChange: (e) => controller.setModel(e.target.value), children: Object.entries(MODELS).map(([key, value]) => (_jsxs("option", { value: key, children: [t(value.label), " \u00B7 ", value.minimum, "+"] }, key))) })] }), _jsx("span", { className: "rg-dirty", children: t(s.dirty ? "Unsaved changes" : "No unsaved changes") })] }), _jsxs("fieldset", { className: "rg-view-tabs", "aria-label": t("Workspace view"), children: [_jsx("button", { type: "button", "aria-pressed": mobileView === "image", onClick: () => setMobileView("image"), children: t("Image") }), _jsxs("button", { type: "button", "aria-pressed": mobileView === "map", onClick: () => setMobileView("map"), children: [t("Map"), s.pendingImagePoint ? ` · ${t("Pick here next")}` : ""] })] }), _jsxs("div", { className: "rg-paired-views", "data-mobile-view": mobileView, children: [_jsx("div", { className: "rg-source-pane", "data-active": s.tool === "gcp" && !s.pendingImagePoint && !review, children: _jsx(ImagePanel, { controller: controller, t: t }) }), _jsx("div", { className: "rg-reference-pane", "data-active": !!s.pendingImagePoint, children: referenceView })] }), (review || s.mode === "draw") && (_jsxs("div", { className: "rg-review-panels", children: [_jsx(AlignmentPanel, { controller: controller, t: t, controls: false }), s.mode === "draw" && (_jsx(FeaturePanel, { controller: controller, propertyEditor: propertyEditor, t: t }))] })), _jsx(GcpPanel, { controller: controller, t: t }), _jsxs("details", { className: "rg-advanced", children: [_jsx("summary", { children: t("Advanced navigation & reference status") }), _jsxs("div", { className: "rg-toolbar", children: [_jsx("button", { type: "button", disabled: !s.preview, onClick: () => binding.current?.fitOverlay(), children: t("Fit map to image") }), _jsx("button", { type: "button", onClick: () => binding.current?.navigateHistory(-1), children: t("Previous map view") }), _jsx("button", { type: "button", onClick: () => binding.current?.navigateHistory(1), children: t("Next map view") })] }), _jsx(ReferencePanel, { controller: controller, t: t })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("header", { className: "rg-editor-heading", children: [_jsxs("div", { children: [_jsx("span", { className: "rg-eyebrow", children: t("GEOREFERENCE") }), _jsx("h1", { children: t("Place the image. Trace the detail.") })] }), _jsx("span", { className: "rg-dirty", children: t(s.dirty ? "Unsaved changes" : "No unsaved changes") })] }), _jsxs("div", { className: "rg-toolbar rg-history", children: [_jsx("button", { type: "button", disabled: !s.canUndo, onClick: () => controller.undo(), children: t("Undo") }), _jsx("button", { type: "button", disabled: !s.canRedo, onClick: () => controller.redo(), children: t("Redo") }), _jsx("button", { type: "button", disabled: !s.preview, onClick: () => binding.current?.fitOverlay(), children: t("Fit map to image") }), _jsx("button", { type: "button", onClick: () => binding.current?.navigateHistory(-1), children: t("Previous map view") }), _jsx("button", { type: "button", onClick: () => binding.current?.navigateHistory(1), children: t("Next map view") }), ["LineString", "Polygon"].includes(s.tool) && (_jsx("button", { type: "button", onClick: () => binding.current?.finishDrawing(), children: t("Finish drawing") }))] }), _jsxs("div", { className: "rg-workbench", children: [_jsx(ImagePanel, { controller: controller, t: t }), _jsxs("div", { className: "rg-settings", children: [_jsx(AlignmentPanel, { controller: controller, t: t }), _jsx(FeaturePanel, { controller: controller, propertyEditor: propertyEditor, t: t })] })] }), _jsx(ReferencePanel, { controller: controller, t: t }), _jsx(GcpPanel, { controller: controller, t: t })] })), _jsxs("details", { className: "rg-export", children: [_jsx("summary", { children: t(guided && !review && s.mode !== "draw"
|
|
144
|
-
? "Session files"
|
|
145
|
-
: "Raster output & session files") }), visibleFormats.some((format) => format.raster) && (_jsxs("div", { className: "rg-output-fields", children: [_jsxs("label", { children: [t("Output CRS"), _jsx(TextField, { label: t("Output CRS"), value: s.document.output.crs, change: (crs) => run(() => {
|
|
146
|
-
if (!crs)
|
|
147
|
-
throw Error("Output CRS is required.");
|
|
148
|
-
controller.setOutput({
|
|
149
|
-
...s.document.output,
|
|
150
|
-
crs,
|
|
151
|
-
});
|
|
152
|
-
}) })] }), _jsxs("label", { children: [t("Resampling"), _jsx("select", { value: s.document.output.resampler, onChange: (e) => controller.setOutput({
|
|
153
|
-
...s.document.output,
|
|
154
|
-
resampler: e.target.value,
|
|
155
|
-
}), children: ["nearest", "bilinear", "cubic", "cubicSpline", "lanczos"].map((m) => (_jsx("option", { children: m }, m))) })] }), s.exportFormats.some((format) => format.id === "geotiff") && (_jsxs("label", { children: [t("TIFF compression"), _jsxs("select", { value: s.document.output.compression ?? "none", onChange: (e) => controller.setOutput({
|
|
156
|
-
...s.document.output,
|
|
157
|
-
compression: e.target.value,
|
|
158
|
-
predictor: e.target.value === "deflate"
|
|
159
|
-
? s.document.output.predictor
|
|
160
|
-
: 1,
|
|
161
|
-
}), children: [_jsx("option", { value: "none", children: t("Uncompressed") }), _jsx("option", { value: "deflate", children: t("Deflate (lossless)") }), _jsx("option", { value: "packbits", children: t("PackBits (lossless)") })] })] })), _jsxs("label", { children: [t("Input no-data (byte or R,G,B; blank = alpha)"), _jsx(TextField, { label: t("Input no-data"), value: Array.isArray(s.document.output.sourceNoData)
|
|
162
|
-
? s.document.output.sourceNoData.join(",")
|
|
163
|
-
: String(s.document.output.sourceNoData ?? ""), change: (value) => run(() => {
|
|
164
|
-
const values = value.split(",").map(Number);
|
|
165
|
-
if (value &&
|
|
166
|
-
(![1, 3].includes(values.length) ||
|
|
167
|
-
values.some((v) => !Number.isInteger(v) || v < 0 || v > 255)))
|
|
168
|
-
throw Error("Input no-data requires one or three bytes (0–255).");
|
|
169
|
-
controller.setOutput({
|
|
170
|
-
...s.document.output,
|
|
171
|
-
sourceNoData: value
|
|
172
|
-
? values.length === 1
|
|
173
|
-
? values[0]
|
|
174
|
-
: values
|
|
175
|
-
: undefined,
|
|
176
|
-
});
|
|
177
|
-
}) })] }), s.exportFormats.some((format) => format.id === "geotiff") && (_jsxs("label", { children: [t("Output no-data (byte; blank = alpha)"), _jsx(TextField, { label: t("Output no-data"), value: String(s.document.output.noData ?? ""), change: (value) => run(() => {
|
|
178
|
-
const n = Number(value);
|
|
179
|
-
if (value && (!Number.isInteger(n) || n < 0 || n > 255))
|
|
180
|
-
throw Error("Output no-data requires a byte (0–255).");
|
|
181
|
-
controller.setOutput({
|
|
182
|
-
...s.document.output,
|
|
183
|
-
noData: value ? n : undefined,
|
|
184
|
-
});
|
|
185
|
-
}) })] })), s.exportFormats.some((format) => format.id === "geotiff") && (_jsxs(_Fragment, { children: [_jsxs("label", { children: [t("Rows per TIFF strip"), _jsx(NumberField, { label: t("Rows per TIFF strip"), value: s.document.output.rowsPerStrip ?? 256, change: (n) => run(() => {
|
|
186
|
-
if (!Number.isInteger(n) || n < 1 || n > 4096)
|
|
187
|
-
throw Error("Rows per strip must be 1–4096.");
|
|
188
|
-
controller.setOutput({
|
|
189
|
-
...s.document.output,
|
|
190
|
-
rowsPerStrip: n,
|
|
191
|
-
});
|
|
192
|
-
}) })] }), _jsxs("label", { children: [t("Horizontal TIFF predictor"), _jsx("input", { type: "checkbox", disabled: s.document.output.compression !== "deflate", checked: s.document.output.predictor === 2, onChange: (e) => controller.setOutput({
|
|
193
|
-
...s.document.output,
|
|
194
|
-
predictor: e.target.checked ? 2 : 1,
|
|
195
|
-
}) })] })] })), _jsxs("label", { children: [t("Pixel size (x,y; blank = estimated)"), " \u00B7", " ", coordinateUnits(s.document.output.crs, bindingOptions.definitions), _jsx(TextField, { label: t("Pixel size"), value: s.document.output.resolution?.join(",") ?? "", change: (value) => run(() => {
|
|
196
|
-
const values = value.split(",").map(Number);
|
|
197
|
-
if (value &&
|
|
198
|
-
(values.length > 2 ||
|
|
199
|
-
values.some((v) => !Number.isFinite(v) || v <= 0)))
|
|
200
|
-
throw Error("Pixel size requires one or two positive numbers.");
|
|
201
|
-
controller.setOutput({
|
|
202
|
-
...s.document.output,
|
|
203
|
-
resolution: value
|
|
204
|
-
? [values[0], values[1] ?? values[0]]
|
|
205
|
-
: undefined,
|
|
206
|
-
});
|
|
207
|
-
}) })] }), _jsxs("label", { children: [t("Output bounds (minX,minY,maxX,maxY; blank = footprint)"), _jsx(TextField, { label: t("Output bounds"), value: s.document.output.bounds?.join(",") ?? "", change: (value) => run(() => {
|
|
208
|
-
const values = value.split(",").map(Number);
|
|
209
|
-
if (value &&
|
|
210
|
-
(values.length !== 4 ||
|
|
211
|
-
values.some((v) => !Number.isFinite(v)) ||
|
|
212
|
-
values[0] >= values[2] ||
|
|
213
|
-
values[1] >= values[3]))
|
|
214
|
-
throw Error("Bounds require four finite numbers with min < max.");
|
|
215
|
-
controller.setOutput({
|
|
216
|
-
...s.document.output,
|
|
217
|
-
bounds: value
|
|
218
|
-
? values
|
|
219
|
-
: undefined,
|
|
220
|
-
});
|
|
221
|
-
}) })] })] })), visibleFormats.length > 0 && (_jsxs("div", { className: "rg-toolbar", children: [visibleFormats.map((format) => {
|
|
222
|
-
const unavailable = controller.getExportUnavailable(format.id);
|
|
223
|
-
return (_jsx("button", { type: "button", "data-export-format": format.id, title: unavailable ? t(unavailable) : undefined, disabled: Boolean(unavailable) || s.exporting === "running", onClick: () => void controller
|
|
224
|
-
.export(format.id)
|
|
225
|
-
.then(async (result) => {
|
|
226
|
-
if (!result)
|
|
227
|
-
return;
|
|
228
|
-
if (onExport)
|
|
229
|
-
await onExport(result);
|
|
230
|
-
else
|
|
231
|
-
for (const file of result.files)
|
|
232
|
-
downloadBlob(file.blob, file.name);
|
|
233
|
-
})
|
|
234
|
-
.catch((error) => controller.reportError(error)), children: t(format.label) }, format.id));
|
|
235
|
-
}), _jsx("button", { type: "button", disabled: s.exporting !== "running", onClick: () => controller.cancelExport(), children: t("Cancel export") })] })), s.exporting === "running" && (_jsx("progress", { "aria-label": t("Export progress"), value: s.progress, max: "1" })), _jsxs("div", { className: "rg-toolbar", children: [_jsxs("label", { className: "rg-file", children: [t("Restore session"), _jsx("input", { type: "file", "aria-label": t("Restore session"), accept: ".json", onChange: async (e) => {
|
|
236
|
-
const file = e.target.files?.[0];
|
|
237
|
-
if (file) {
|
|
238
|
-
pendingSession.current = await file.text();
|
|
239
|
-
sessionInput.current?.click();
|
|
240
|
-
}
|
|
241
|
-
e.target.value = "";
|
|
242
|
-
} })] }), _jsx("input", { ref: sessionInput, "aria-label": t("Matching session image"), type: "file", hidden: true, accept: "image/*,.tif,.tiff", onChange: (e) => {
|
|
243
|
-
const file = e.target.files?.[0];
|
|
244
|
-
if (file && pendingSession.current)
|
|
245
|
-
void controller
|
|
246
|
-
.restoreSession(pendingSession.current, file)
|
|
247
|
-
.catch((e) => controller.reportError(e));
|
|
248
|
-
e.target.value = "";
|
|
249
|
-
} }), _jsxs("label", { className: "rg-file", children: [t("Import .points"), _jsx("input", { type: "file", "aria-label": t("Import .points"), accept: ".points,.csv", disabled: !s.document.sourceImage || s.mode === "draw", onChange: async (e) => {
|
|
250
|
-
const f = e.target.files?.[0];
|
|
251
|
-
if (f) {
|
|
252
|
-
try {
|
|
253
|
-
const imported = importPoints(await f.text(), s.document.workingCrs);
|
|
254
|
-
controller.replaceGcps(imported.gcps);
|
|
255
|
-
}
|
|
256
|
-
catch (e) {
|
|
257
|
-
controller.reportError(e);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
e.target.value = "";
|
|
261
|
-
} })] }), controller.options.onSaveDraft && (_jsx("button", { type: "button", disabled: !s.document.sourceImage || s.saving === "running", onClick: () => void controller
|
|
262
|
-
.save("draft")
|
|
263
|
-
.catch((e) => controller.reportError(e)), children: t("Save draft") }))] })] }), _jsx("div", { className: "rg-status", role: "status", "aria-live": "polite", children: (s.error
|
|
264
|
-
? s.errorDetail && formatError
|
|
265
|
-
? formatError(s.errorDetail)
|
|
266
|
-
: t(s.error)
|
|
267
|
-
: null) ??
|
|
268
|
-
(s.loading === "running"
|
|
206
|
+
}, children: [guided ? (_jsxs(_Fragment, { children: [_jsx("ol", { className: "rg-steps", "aria-label": t("Georeferencing steps"), children: steps.map((label, i) => {
|
|
207
|
+
const state = i < step ? "done" : i === step ? "current" : "todo";
|
|
208
|
+
const back = i < step && i === 1
|
|
209
|
+
? goToMatching
|
|
210
|
+
: i < step && i === 2
|
|
211
|
+
? goToReview
|
|
212
|
+
: undefined;
|
|
213
|
+
const content = (_jsxs(_Fragment, { children: [_jsx("span", { className: "rg-step-index", "aria-hidden": "true", children: state === "done" ? "✓" : i + 1 }), _jsx("span", { className: "rg-step-label", children: t(label) })] }));
|
|
214
|
+
return (_jsx("li", { "data-state": state, "aria-current": state === "current" ? "step" : undefined, children: back ? (_jsx("button", { type: "button", className: "rg-step-button", onClick: back, children: content })) : (content) }, label));
|
|
215
|
+
}) }), _jsxs("div", { className: "rg-matching-bar", children: [_jsxs("div", { className: "rg-instruction", role: "status", "aria-live": "polite", children: [_jsx("strong", { children: t(instruction[0]) }), _jsxs("p", { children: [t(instruction[1]), s.pendingImagePoint && (_jsxs(_Fragment, { children: [" ", _jsx("button", { type: "button", className: "rg-link", onClick: () => controller.setPendingPoint(null), children: t("Cancel this point") })] }))] })] }), _jsxs("div", { className: "rg-step-actions", children: [step === 1 && (_jsx(PreviewControls, { controller: controller, t: t, modeSelector: controls.previewMode, onReview: () => {
|
|
216
|
+
setStage("review");
|
|
217
|
+
setMobileView("map");
|
|
218
|
+
} })), step === 2 && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: goToMatching, children: t("Adjust points") }), _jsx("button", { type: "button", onClick: () => binding.current?.fitOverlay(), children: t("Zoom map to image") }), _jsx("button", { type: "button", className: "rg-primary", disabled: !s.fit || s.fitRevision !== d.alignmentRevision, onClick: finish, children: t("Looks good, continue") })] })), step === 3 && (_jsx("button", { type: "button", onClick: goToReview, children: t("Back to check") }))] })] }), error && (_jsx("p", { className: "rg-error", role: "alert", children: error })), toolRow, _jsxs("fieldset", { className: "rg-view-tabs", "aria-label": t("Workspace view"), children: [_jsx("button", { type: "button", "aria-pressed": mobileView === "image", onClick: () => setMobileView("image"), children: t("Image") }), _jsxs("button", { type: "button", "aria-pressed": mobileView === "map", onClick: () => setMobileView("map"), children: [t("Map"), s.pendingImagePoint ? ` · ${t("Pick here next")}` : ""] })] }), _jsxs("div", { className: "rg-paired-views", "data-mobile-view": mobileView, children: [_jsx("div", { className: "rg-source-pane", "data-active": s.tool === "gcp" && !s.pendingImagePoint && step === 1, children: _jsx(ImagePanel, { controller: controller, t: t, navigation: controls.navigation, displayAdjustment: controls.displayAdjustment, emptyActions: emptyImageActions }) }), _jsx("div", { className: "rg-reference-pane", "data-active": !!s.pendingImagePoint, children: referenceView })] }), step === 2 && s.fit && (_jsxs("div", { className: "rg-review-panels", children: [_jsxs("section", { className: "rg-card", children: [_jsx("h2", { children: t("Accuracy") }), _jsx(AccuracySummary, { fit: s.fit, model: d.model, gcps: d.gcps, workingCrs: d.workingCrs, t: t }), _jsxs("label", { className: "rg-inline", children: [_jsx("input", { type: "checkbox", checked: s.visible, onChange: (e) => controller.setDisplay({ visible: e.target.checked }) }), t("Show aligned image on the map")] }), _jsxs("label", { children: [t("Image opacity"), _jsx("input", { "aria-label": t("Overlay opacity"), type: "range", min: "0", max: "1", step: "0.05", value: s.opacity, onChange: (e) => controller.setDisplay({
|
|
219
|
+
opacity: Number(e.target.value),
|
|
220
|
+
}) })] })] }), _jsx(GcpPanel, { controller: controller, t: t, variant: controls.pointTable ? "table" : "compact", manualEntry: false, tools: false })] })), step === 1 && (d.gcps.length > 0 || controls.manualEntry) && (_jsx(GcpPanel, { controller: controller, t: t, variant: controls.pointTable ? "table" : "compact", manualEntry: controls.manualEntry, tools: false })), step === 3 && (_jsxs("div", { className: "rg-review-panels", children: [s.exportFormats.length > 0 && (_jsxs("section", { className: "rg-card", children: [_jsx("h2", { children: t("Download") }), _jsx(ExportButtons, { controller: controller, t: t, formats: primaryFormats, onExport: onExport, primary: () => true, status: false }), otherFormats.length > 0 && (_jsxs(_Fragment, { children: [_jsx("h3", { children: t("More downloads") }), _jsx(ExportButtons, { controller: controller, t: t, formats: otherFormats, onExport: onExport, status: false })] })), _jsx(ExportProgress, { controller: controller, t: t }), controls.outputSettings && primaryFormats.length > 0 && (_jsxs("details", { children: [_jsx("summary", { children: t("Output settings") }), _jsx(OutputFields, { controller: controller, t: t, definitions: bindingOptions.definitions })] }))] })), controller.options.digitizing && (_jsx("div", { className: "rg-card", children: _jsx(FeaturePanel, { controller: controller, propertyEditor: propertyEditor, t: t }) }))] })), controls.referenceStatus && (_jsxs("details", { className: "rg-advanced", children: [_jsx("summary", { children: t("Reference sources") }), _jsx(ReferencePanel, { controller: controller, t: t })] })), controls.sessionFiles && (_jsxs("details", { className: "rg-export", children: [_jsx("summary", { children: t("Session files") }), step < 3 && (_jsx(ExportButtons, { controller: controller, t: t, formats: s.exportFormats.filter((format) => format.requiresFit === false), onExport: onExport, status: false })), step < 3 && _jsx(ExportProgress, { controller: controller, t: t }), _jsx(SessionTools, { controller: controller, t: t })] }))] })) : (_jsxs(_Fragment, { children: [_jsxs("header", { className: "rg-editor-heading", children: [_jsxs("div", { children: [_jsx("span", { className: "rg-eyebrow", children: t("GEOREFERENCE") }), _jsx("h1", { children: t("Place the image. Trace the detail.") })] }), unsaved] }), _jsxs("div", { className: "rg-toolbar rg-history", children: [_jsx("button", { type: "button", disabled: !s.canUndo, onClick: () => controller.undo(), children: t("Undo") }), _jsx("button", { type: "button", disabled: !s.canRedo, onClick: () => controller.redo(), children: t("Redo") }), _jsx("button", { type: "button", disabled: !s.preview, onClick: () => binding.current?.fitOverlay(), children: t("Fit map to image") }), _jsx("button", { type: "button", onClick: () => binding.current?.navigateHistory(-1), children: t("Previous map view") }), _jsx("button", { type: "button", onClick: () => binding.current?.navigateHistory(1), children: t("Next map view") }), ["LineString", "Polygon"].includes(s.tool) && (_jsx("button", { type: "button", onClick: () => binding.current?.finishDrawing(), children: t("Finish drawing") }))] }), _jsxs("div", { className: "rg-workbench", children: [_jsx(ImagePanel, { controller: controller, t: t, emptyActions: emptyImageActions }), _jsxs("div", { className: "rg-settings", children: [_jsx(AlignmentPanel, { controller: controller, t: t }), _jsx(FeaturePanel, { controller: controller, propertyEditor: propertyEditor, t: t })] })] }), _jsx(ReferencePanel, { controller: controller, t: t }), _jsx(GcpPanel, { controller: controller, t: t }), _jsxs("details", { className: "rg-export", children: [_jsx("summary", { children: t("Raster output & session files") }), s.exportFormats.some((format) => format.raster) && (_jsx(OutputFields, { controller: controller, t: t, definitions: bindingOptions.definitions })), _jsx(ExportButtons, { controller: controller, t: t, formats: s.exportFormats, onExport: onExport }), _jsx(SessionTools, { controller: controller, t: t })] })] })), _jsx("div", { className: "rg-status", role: "status", "aria-live": "polite", children: guided
|
|
221
|
+
? s.loading === "running"
|
|
269
222
|
? t("Inspecting local image…")
|
|
270
223
|
: s.exporting === "succeeded"
|
|
271
|
-
? t("
|
|
272
|
-
:
|
|
224
|
+
? t("Export complete.")
|
|
225
|
+
: null
|
|
226
|
+
: (error ??
|
|
227
|
+
(s.loading === "running"
|
|
228
|
+
? t("Inspecting local image…")
|
|
229
|
+
: s.exporting === "succeeded"
|
|
230
|
+
? t("Raster export complete.")
|
|
231
|
+
: t("Image processing stays on this device."))) }), _jsxs("dialog", { ref: dialog, className: "rg-guard", onCancel: (e) => {
|
|
273
232
|
e.preventDefault();
|
|
274
233
|
decide("cancel");
|
|
275
234
|
}, "aria-labelledby": guardTitle, children: [_jsx("h2", { id: guardTitle, children: t("Protect unsaved work") }), _jsx("p", { children: t("Save or discard the current document before replacing it.") }), !guard?.context.canSave && (_jsx("p", { children: t("Saving is unavailable for this draft. Cancel to export a session, or discard.") })), _jsxs("div", { className: "rg-toolbar", children: [_jsx("button", { type: "button", onClick: () => decide("cancel"), children: t("Cancel") }), _jsx("button", { type: "button", onClick: () => decide("discard"), children: t("Discard") }), _jsx("button", { type: "button", disabled: !guard?.context.canSave, onClick: () => decide("save"), children: t(guard?.context.saveKind === "draft"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Fit, Gcp, Model } from "@georeferencing/core";
|
|
2
|
+
import type { Translate } from "../localization.js";
|
|
3
|
+
/**
|
|
4
|
+
* Whether residuals are necessarily zero: thin plate splines interpolate every point,
|
|
5
|
+
* and every other model matches exactly its minimum number of points.
|
|
6
|
+
*/
|
|
7
|
+
export declare function exactFit(model: Model, gcps: readonly Gcp[]): boolean;
|
|
8
|
+
/** Plain-language fit quality from the root mean square of image-pixel residuals. */
|
|
9
|
+
export declare function AccuracySummary({ fit, model, gcps, workingCrs, t, }: {
|
|
10
|
+
fit: Fit;
|
|
11
|
+
model: Model;
|
|
12
|
+
gcps: readonly Gcp[];
|
|
13
|
+
workingCrs: string;
|
|
14
|
+
t: Translate;
|
|
15
|
+
}): import("react/jsx-runtime").JSX.Element;
|