@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
|
@@ -11,8 +11,14 @@ export function ImagePanel({
|
|
|
11
11
|
/** Shared authoritative editor controller; the panel does not own its lifecycle. */
|
|
12
12
|
controller,
|
|
13
13
|
/** Optional translation function; defaults to returning English messages unchanged. */
|
|
14
|
-
t = identity,
|
|
15
|
-
|
|
14
|
+
t = identity,
|
|
15
|
+
/** Show previous/next view history and linked image/map navigation. */
|
|
16
|
+
navigation = true,
|
|
17
|
+
/** Show brightness, contrast and histogram display adjustment. */
|
|
18
|
+
displayAdjustment = true,
|
|
19
|
+
/** Extra actions inside the empty drop zone, such as a sample image button. */
|
|
20
|
+
emptyActions, }) {
|
|
21
|
+
const s = useGeoreferencer(controller), image = s.document.sourceImage, imageWidth = image?.width, svg = useRef(null);
|
|
16
22
|
const view = s.imageView ?? [
|
|
17
23
|
0, 0, 100, 100,
|
|
18
24
|
];
|
|
@@ -24,6 +30,10 @@ t = identity, }) {
|
|
|
24
30
|
]);
|
|
25
31
|
const filterId = `rg-stretch-${useId().replace(/:/g, "")}`;
|
|
26
32
|
const displayPixels = useRef(null);
|
|
33
|
+
// Preview pixels per canonical image pixel, known once the preview has loaded.
|
|
34
|
+
const [previewScale, setPreviewScale] = useState(0);
|
|
35
|
+
// On-screen pixels per canonical image pixel for the current view.
|
|
36
|
+
const [screenScale, setScreenScale] = useState(0);
|
|
27
37
|
const stretchHistogram = (local) => {
|
|
28
38
|
const pixels = displayPixels.current;
|
|
29
39
|
if (!pixels || !image)
|
|
@@ -91,6 +101,8 @@ t = identity, }) {
|
|
|
91
101
|
const ctx = canvas.getContext("2d");
|
|
92
102
|
ctx.drawImage(img, 0, 0);
|
|
93
103
|
displayPixels.current = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
104
|
+
if (imageWidth)
|
|
105
|
+
setPreviewScale(img.width / imageWidth);
|
|
94
106
|
const data = displayPixels.current.data, bins = Array(32).fill(0);
|
|
95
107
|
for (let i = 0; i < data.length; i += 4)
|
|
96
108
|
if (data[i + 3])
|
|
@@ -102,10 +114,31 @@ t = identity, }) {
|
|
|
102
114
|
return () => {
|
|
103
115
|
disposed = true;
|
|
104
116
|
displayPixels.current = null;
|
|
117
|
+
setPreviewScale(0);
|
|
105
118
|
img.onload = null;
|
|
106
119
|
img.src = "";
|
|
107
120
|
};
|
|
108
|
-
}, [s.imageUrl]);
|
|
121
|
+
}, [s.imageUrl, imageWidth]);
|
|
122
|
+
// Track the on-screen scale; the viewBox keeps its aspect ratio ("meet").
|
|
123
|
+
const viewWidth = view[2], viewHeight = view[3];
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
const element = svg.current;
|
|
126
|
+
if (!element)
|
|
127
|
+
return;
|
|
128
|
+
const measure = () => setScreenScale(Math.min(element.clientWidth / viewWidth, element.clientHeight / viewHeight));
|
|
129
|
+
measure();
|
|
130
|
+
if (typeof ResizeObserver === "undefined")
|
|
131
|
+
return;
|
|
132
|
+
const observer = new ResizeObserver(measure);
|
|
133
|
+
observer.observe(element);
|
|
134
|
+
return () => observer.disconnect();
|
|
135
|
+
}, [viewWidth, viewHeight]);
|
|
136
|
+
// Zoomed beyond the preview resolution: show full-resolution detail for precise picking.
|
|
137
|
+
const needsDetail = previewScale > 0 && screenScale > previewScale * 1.25;
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (needsDetail)
|
|
140
|
+
void controller.requestDetailImage();
|
|
141
|
+
}, [needsDetail, controller]);
|
|
109
142
|
const point = (e) => {
|
|
110
143
|
const matrix = svg.current?.getScreenCTM();
|
|
111
144
|
if (!matrix)
|
|
@@ -113,31 +146,55 @@ t = identity, }) {
|
|
|
113
146
|
const p = new DOMPoint(e.clientX, e.clientY).matrixTransform(matrix.inverse());
|
|
114
147
|
return [p.x, p.y];
|
|
115
148
|
};
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
149
|
+
// Read the latest view from the controller so rapid wheel events never use a stale render.
|
|
150
|
+
const zoom = (factor, center) => {
|
|
151
|
+
const v = controller.getSnapshot().imageView ?? view;
|
|
152
|
+
const c = center ?? [v[0] + v[2] / 2, v[1] + v[3] / 2];
|
|
153
|
+
controller.setImageView([
|
|
154
|
+
c[0] + (v[0] - c[0]) * factor,
|
|
155
|
+
c[1] + (v[1] - c[1]) * factor,
|
|
156
|
+
Math.max(1, v[2] * factor),
|
|
157
|
+
Math.max(1, v[3] * factor),
|
|
158
|
+
]);
|
|
159
|
+
};
|
|
160
|
+
const zoomRef = useRef(zoom);
|
|
161
|
+
zoomRef.current = zoom;
|
|
162
|
+
const hasImage = Boolean(image);
|
|
163
|
+
// React registers wheel listeners as passive; a native one can stop the page scrolling.
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
const element = svg.current;
|
|
166
|
+
if (!hasImage || !element)
|
|
167
|
+
return;
|
|
168
|
+
const wheel = (e) => {
|
|
169
|
+
e.preventDefault();
|
|
170
|
+
const matrix = element.getScreenCTM();
|
|
171
|
+
const center = matrix
|
|
172
|
+
? new DOMPoint(e.clientX, e.clientY).matrixTransform(matrix.inverse())
|
|
173
|
+
: undefined;
|
|
174
|
+
zoomRef.current(e.deltaY > 0 ? 1.15 : 1 / 1.15, center ? [center.x, center.y] : undefined);
|
|
175
|
+
};
|
|
176
|
+
element.addEventListener("wheel", wheel, { passive: false });
|
|
177
|
+
return () => element.removeEventListener("wheel", wheel);
|
|
178
|
+
}, [hasImage]);
|
|
122
179
|
const down = (e) => {
|
|
123
180
|
if (!image)
|
|
124
181
|
return;
|
|
125
182
|
e.preventDefault();
|
|
126
183
|
e.currentTarget.focus({ preventScroll: true });
|
|
127
184
|
const p = point(e);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
controller.setPendingPoint(p);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
185
|
+
const inside = p[0] >= 0 && p[1] >= 0 && p[0] <= image.width && p[1] <= image.height;
|
|
186
|
+
// Dragging always pans; a click without movement places a point while matching.
|
|
133
187
|
drag.current = {
|
|
134
188
|
start: [e.clientX, e.clientY],
|
|
135
189
|
view,
|
|
136
190
|
pointerId: e.pointerId,
|
|
191
|
+
click: s.tool === "gcp" && e.button === 0 && !e.shiftKey && inside
|
|
192
|
+
? p
|
|
193
|
+
: undefined,
|
|
137
194
|
};
|
|
138
195
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
139
196
|
};
|
|
140
|
-
return (_jsxs("section", { className: "rg-image-panel", "aria-label": t("Source image"), onDragOver: (e) => e.preventDefault(), onDrop: (e) => {
|
|
197
|
+
return (_jsxs("section", { className: "rg-image-panel", "data-empty": image ? undefined : "", "aria-label": t("Source image"), onDragOver: (e) => e.preventDefault(), onDrop: (e) => {
|
|
141
198
|
e.preventDefault();
|
|
142
199
|
const file = e.dataTransfer.files[0];
|
|
143
200
|
if (file)
|
|
@@ -183,6 +240,10 @@ t = identity, }) {
|
|
|
183
240
|
setDragPoint({ id: d.id, point: d.point });
|
|
184
241
|
}
|
|
185
242
|
else {
|
|
243
|
+
if (!d.moved &&
|
|
244
|
+
Math.hypot(e.clientX - d.start[0], e.clientY - d.start[1]) < 4)
|
|
245
|
+
return;
|
|
246
|
+
d.moved = true;
|
|
186
247
|
const scale = Math.max(view[2] / e.currentTarget.clientWidth, view[3] / e.currentTarget.clientHeight);
|
|
187
248
|
setView([
|
|
188
249
|
d.view[0] - (e.clientX - d.start[0]) * scale,
|
|
@@ -196,16 +257,20 @@ t = identity, }) {
|
|
|
196
257
|
drag.current = null;
|
|
197
258
|
if (d?.id && d.point)
|
|
198
259
|
controller.updateGcp(d.id, { image: d.point });
|
|
260
|
+
else if (d?.click && !d.moved)
|
|
261
|
+
controller.setPendingPoint(d.click);
|
|
199
262
|
setDragPoint(null);
|
|
200
263
|
if (e.currentTarget.hasPointerCapture(e.pointerId))
|
|
201
264
|
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
202
265
|
}, onPointerCancel: () => {
|
|
203
266
|
drag.current = null;
|
|
204
267
|
setDragPoint(null);
|
|
205
|
-
}, onWheel: (e) => {
|
|
206
|
-
zoom(e.deltaY > 0 ? 1.15 : 1 / 1.15, point(e));
|
|
207
268
|
}, children: [_jsx("defs", { children: _jsx("filter", { id: filterId, colorInterpolationFilters: "sRGB", children: _jsxs("feComponentTransfer", { children: [_jsx("feFuncR", { type: "linear", slope: 255 / (stretch[0][1] - stretch[0][0]), intercept: -stretch[0][0] / (stretch[0][1] - stretch[0][0]) }), _jsx("feFuncG", { type: "linear", slope: 255 / (stretch[1][1] - stretch[1][0]), intercept: -stretch[1][0] / (stretch[1][1] - stretch[1][0]) }), _jsx("feFuncB", { type: "linear", slope: 255 / (stretch[2][1] - stretch[2][0]), intercept: -stretch[2][0] / (stretch[2][1] - stretch[2][0]) })] }) }) }), s.imageUrl && (_jsx("image", { href: s.imageUrl, width: image.width, height: image.height, style: {
|
|
208
269
|
filter: `url(#${filterId}) brightness(${brightness}) contrast(${contrast})`,
|
|
270
|
+
} })), s.detailImageUrl && needsDetail && (_jsx("image", { href: s.detailImageUrl, width: image.width, height: image.height, "data-detail": "", style: {
|
|
271
|
+
filter: `url(#${filterId}) brightness(${brightness}) contrast(${contrast})`,
|
|
272
|
+
// Show individual source pixels once each covers several screen pixels.
|
|
273
|
+
imageRendering: screenScale >= 4 ? "pixelated" : "auto",
|
|
209
274
|
} })), s.document.gcps.map((gcp) => {
|
|
210
275
|
const p = dragPoint?.id === gcp.id ? dragPoint.point : gcp.image;
|
|
211
276
|
return (_jsxs("g", { transform: `translate(${p[0]} ${p[1]})`, onPointerDown: (e) => {
|
|
@@ -228,7 +293,7 @@ t = identity, }) {
|
|
|
228
293
|
-image.height * 0.06,
|
|
229
294
|
image.width * 1.08,
|
|
230
295
|
image.height * 1.12,
|
|
231
|
-
]), children: t("Fit image") }), _jsx("button", { type: "button", onClick: () => controller.navigateImageHistory(-1), children: t("Previous image view") }), _jsx("button", { type: "button", onClick: () => controller.navigateImageHistory(1), children: t("Next image view") }), _jsx("button", { type: "button", onClick: () => void controller.removeImage(), children: t("Remove image") })] }), _jsxs("label", { children: [t("Linked navigation"), _jsxs("select", { disabled: !s.fit, value: s.linkedNavigation, onChange: (e) => controller.setLinkedNavigation(e.target.value), children: [_jsx("option", { value: "off", children: t("Independent views") }), _jsx("option", { value: "image-to-map", children: t("Map follows image") }), _jsx("option", { value: "map-to-image", children: t("Image follows map") })] })] }), _jsxs("details", { children: [_jsx("summary", { children: t("Display adjustment & histogram") }), _jsxs("div", { className: "rg-toolbar", children: [_jsx("button", { type: "button", onClick: () => stretchHistogram(false), children: t("Full histogram stretch") }), _jsx("button", { type: "button", onClick: () => stretchHistogram(true), children: t("Local histogram stretch") }), _jsx("button", { type: "button", onClick: () => {
|
|
296
|
+
]), children: t("Fit image") }), navigation && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: () => controller.navigateImageHistory(-1), children: t("Previous image view") }), _jsx("button", { type: "button", onClick: () => controller.navigateImageHistory(1), children: t("Next image view") })] })), _jsx("button", { type: "button", onClick: () => void controller.removeImage(), children: t("Remove image") })] }), navigation && (_jsxs("label", { children: [t("Linked navigation"), _jsxs("select", { disabled: !s.fit, value: s.linkedNavigation, onChange: (e) => controller.setLinkedNavigation(e.target.value), children: [_jsx("option", { value: "off", children: t("Independent views") }), _jsx("option", { value: "image-to-map", children: t("Map follows image") }), _jsx("option", { value: "map-to-image", children: t("Image follows map") })] })] })), displayAdjustment && (_jsxs("details", { children: [_jsx("summary", { children: t("Display adjustment & histogram") }), _jsxs("div", { className: "rg-toolbar", children: [_jsx("button", { type: "button", onClick: () => stretchHistogram(false), children: t("Full histogram stretch") }), _jsx("button", { type: "button", onClick: () => stretchHistogram(true), children: t("Local histogram stretch") }), _jsx("button", { type: "button", onClick: () => {
|
|
232
297
|
setStretch([
|
|
233
298
|
[0, 255],
|
|
234
299
|
[0, 255],
|
|
@@ -236,5 +301,10 @@ t = identity, }) {
|
|
|
236
301
|
]);
|
|
237
302
|
setBrightness(1);
|
|
238
303
|
setContrast(1);
|
|
239
|
-
}, children: t("Reset display") })] }), _jsxs("label", { children: [t("Brightness"), _jsx("input", { type: "range", min: "0.25", max: "2", step: "0.05", value: brightness, onChange: (e) => setBrightness(Number(e.target.value)) })] }), _jsxs("label", { children: [t("Contrast"), _jsx("input", { type: "range", min: "0.25", max: "2", step: "0.05", value: contrast, onChange: (e) => setContrast(Number(e.target.value)) })] }), _jsx("svg", { viewBox: "0 0 320 50", "aria-label": t("RGB intensity histogram of preview"), className: "rg-histogram", children: histogram.map((v, i) => (_jsx("rect", { x: i * 10, y: 50 - (50 * v) / Math.max(1, ...histogram), width: "9", height: (50 * v) / Math.max(1, ...histogram), fill: "currentColor" }, i))) }), _jsx("small", { children: t("Display only. Original samples are preserved for export.") })] })] })) : (_jsxs("div", { className: "rg-empty", children: [_jsx("span", { className: "rg-cross", children: "\uFF0B" }), _jsx("p", { children: t("Drop a map, plan, or photograph here.") }), _jsx("small", { children: t("PNG, JPEG, WebP, ordinary TIFF · up to 25 MiB · stays on this device") })
|
|
304
|
+
}, children: t("Reset display") })] }), _jsxs("label", { children: [t("Brightness"), _jsx("input", { type: "range", min: "0.25", max: "2", step: "0.05", value: brightness, onChange: (e) => setBrightness(Number(e.target.value)) })] }), _jsxs("label", { children: [t("Contrast"), _jsx("input", { type: "range", min: "0.25", max: "2", step: "0.05", value: contrast, onChange: (e) => setContrast(Number(e.target.value)) })] }), _jsx("svg", { viewBox: "0 0 320 50", "aria-label": t("RGB intensity histogram of preview"), className: "rg-histogram", children: histogram.map((v, i) => (_jsx("rect", { x: i * 10, y: 50 - (50 * v) / Math.max(1, ...histogram), width: "9", height: (50 * v) / Math.max(1, ...histogram), fill: "currentColor" }, i))) }), _jsx("small", { children: t("Display only. Original samples are preserved for export.") })] }))] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "rg-image-meta", children: t("No image selected") }), _jsxs("div", { className: "rg-empty", children: [_jsx("span", { className: "rg-cross", children: "\uFF0B" }), _jsx("p", { children: t("Drop a map, plan, or photograph here.") }), _jsx("small", { children: t("PNG, JPEG, WebP, ordinary TIFF · up to 25 MiB · stays on this device") }), _jsxs("div", { className: "rg-toolbar rg-empty-actions", children: [_jsxs("label", { className: "rg-file rg-primary", children: [t("Choose image"), _jsx("input", { "aria-label": t("Choose image file"), type: "file", accept: "image/png,image/jpeg,image/webp,image/tiff,.tif,.tiff", onChange: (e) => {
|
|
305
|
+
const file = e.target.files?.[0];
|
|
306
|
+
if (file)
|
|
307
|
+
void controller.loadImage(file);
|
|
308
|
+
e.target.value = "";
|
|
309
|
+
} })] }), emptyActions] })] })] }))] }));
|
|
240
310
|
}
|
|
@@ -5,11 +5,13 @@ import type { Translate } from "../localization.js";
|
|
|
5
5
|
* enough complete, enabled pairs; the worker validates rank and domain. `onReview`
|
|
6
6
|
* fires only for a successful preview of the same document/alignment revision.
|
|
7
7
|
*/
|
|
8
|
-
export declare function PreviewControls({ controller, t, onReview, }: {
|
|
8
|
+
export declare function PreviewControls({ controller, t, onReview, modeSelector, }: {
|
|
9
9
|
/** Shared authoritative controller. */
|
|
10
10
|
controller: GeoreferencerController;
|
|
11
11
|
/** Translate default English labels. */
|
|
12
12
|
t?: Translate;
|
|
13
13
|
/** Optional transition from matching into review after a current preview is ready. */
|
|
14
14
|
onReview?: () => void;
|
|
15
|
+
/** Show the manual/automatic preview selector. Defaults to true. */
|
|
16
|
+
modeSelector?: boolean;
|
|
15
17
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -7,11 +7,12 @@ import { identity } from "../localization.js";
|
|
|
7
7
|
* enough complete, enabled pairs; the worker validates rank and domain. `onReview`
|
|
8
8
|
* fires only for a successful preview of the same document/alignment revision.
|
|
9
9
|
*/
|
|
10
|
-
export function PreviewControls({ controller, t = identity, onReview, }) {
|
|
10
|
+
export function PreviewControls({ controller, t = identity, onReview, modeSelector = true, }) {
|
|
11
11
|
const s = useGeoreferencer(controller), d = s.document;
|
|
12
12
|
const count = d.gcps.filter((p) => p.enabled).length;
|
|
13
|
-
const
|
|
14
|
-
|
|
13
|
+
const minimum = MODELS[d.model].minimum;
|
|
14
|
+
const missing = Math.max(0, minimum - count);
|
|
15
|
+
return (_jsxs("div", { className: "rg-preview-controls", children: [modeSelector && (_jsxs("label", { children: [t("Preview updates"), _jsxs("select", { value: s.previewMode, onChange: (e) => controller.setPreviewMode(e.target.value), children: [_jsx("option", { value: "manual", children: t("Manual") }), _jsx("option", { value: "automatic", children: t("Automatic") })] })] })), _jsxs("div", { children: [_jsx("button", { type: "button", className: "rg-primary", disabled: !s.imageUrl ||
|
|
15
16
|
!!s.pendingImagePoint ||
|
|
16
17
|
missing > 0 ||
|
|
17
18
|
s.fitting === "running" ||
|
|
@@ -30,7 +31,7 @@ export function PreviewControls({ controller, t = identity, onReview, }) {
|
|
|
30
31
|
? "Running alignment…"
|
|
31
32
|
: s.fit && s.previewMode === "automatic"
|
|
32
33
|
? "Review alignment"
|
|
33
|
-
: "Run alignment") }),
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
: "Run alignment") }), _jsx("small", { className: "rg-readiness", "aria-live": "polite", children: missing > 0
|
|
35
|
+
? `${count} ${t("of")} ${minimum} ${t("points matched")}`
|
|
36
|
+
: `${count} ${t(count === 1 ? "point matched" : "points matched")} · ${t("more points improve accuracy")}` })] })] }));
|
|
36
37
|
}
|
package/dist/styles.css
CHANGED
|
@@ -277,18 +277,19 @@
|
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
/* Guided
|
|
281
|
-
.rg-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
.rg-preview-controls label {
|
|
287
|
-
min-width: 115px;
|
|
280
|
+
/* Guided step-by-step layout; all selectors remain scoped to the editor. */
|
|
281
|
+
.rg-editor .rg-primary:hover:not(:disabled) {
|
|
282
|
+
background: #0e5a4e;
|
|
283
|
+
border-color: #0e5a4e;
|
|
284
|
+
color: white;
|
|
288
285
|
}
|
|
289
|
-
.rg-
|
|
290
|
-
|
|
291
|
-
|
|
286
|
+
.rg-visually-hidden {
|
|
287
|
+
position: absolute;
|
|
288
|
+
width: 1px;
|
|
289
|
+
height: 1px;
|
|
290
|
+
overflow: hidden;
|
|
291
|
+
clip-path: inset(50%);
|
|
292
|
+
white-space: nowrap;
|
|
292
293
|
}
|
|
293
294
|
.rg-guided {
|
|
294
295
|
padding: 18px;
|
|
@@ -296,45 +297,136 @@
|
|
|
296
297
|
.rg-steps {
|
|
297
298
|
display: flex;
|
|
298
299
|
flex-wrap: wrap;
|
|
299
|
-
gap:
|
|
300
|
+
gap: 8px 22px;
|
|
300
301
|
list-style: none;
|
|
301
302
|
margin: 0;
|
|
302
303
|
padding: 0 0 14px;
|
|
303
304
|
border-bottom: 1px solid var(--rg-border);
|
|
304
|
-
font-size:
|
|
305
|
+
font-size: 13px;
|
|
305
306
|
color: #5b6c65;
|
|
306
307
|
}
|
|
307
|
-
.rg-steps
|
|
308
|
+
.rg-steps li,
|
|
309
|
+
.rg-editor .rg-step-button {
|
|
310
|
+
display: inline-flex;
|
|
311
|
+
align-items: center;
|
|
312
|
+
gap: 8px;
|
|
313
|
+
}
|
|
314
|
+
.rg-editor .rg-step-button {
|
|
315
|
+
padding: 0;
|
|
316
|
+
border: 0;
|
|
317
|
+
background: none;
|
|
318
|
+
color: inherit;
|
|
319
|
+
font-size: inherit;
|
|
320
|
+
}
|
|
321
|
+
.rg-editor .rg-step-button:hover .rg-step-label {
|
|
322
|
+
text-decoration: underline;
|
|
323
|
+
}
|
|
324
|
+
.rg-step-index {
|
|
325
|
+
display: inline-grid;
|
|
326
|
+
place-items: center;
|
|
327
|
+
width: 24px;
|
|
328
|
+
height: 24px;
|
|
329
|
+
border-radius: 50%;
|
|
330
|
+
border: 1.5px solid #9dab9e;
|
|
331
|
+
font-size: 12px;
|
|
332
|
+
font-weight: 700;
|
|
333
|
+
font-variant-numeric: tabular-nums;
|
|
334
|
+
}
|
|
335
|
+
.rg-steps [data-state="current"] {
|
|
308
336
|
color: var(--rg-accent);
|
|
309
|
-
font-weight:
|
|
337
|
+
font-weight: 700;
|
|
338
|
+
}
|
|
339
|
+
.rg-steps [data-state="current"] .rg-step-index {
|
|
340
|
+
border-color: var(--rg-accent);
|
|
341
|
+
background: #e3efe9;
|
|
342
|
+
}
|
|
343
|
+
.rg-steps [data-state="done"] {
|
|
344
|
+
color: #20322f;
|
|
345
|
+
}
|
|
346
|
+
.rg-steps [data-state="done"] .rg-step-index {
|
|
347
|
+
border-color: var(--rg-accent);
|
|
348
|
+
background: var(--rg-accent);
|
|
349
|
+
color: white;
|
|
310
350
|
}
|
|
311
351
|
.rg-matching-bar {
|
|
312
|
-
display:
|
|
313
|
-
|
|
352
|
+
display: grid;
|
|
353
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
314
354
|
align-items: center;
|
|
315
|
-
gap:
|
|
316
|
-
padding:
|
|
355
|
+
gap: 8px 24px;
|
|
356
|
+
padding: 14px 0 10px;
|
|
357
|
+
}
|
|
358
|
+
.rg-guided .rg-matching-bar {
|
|
359
|
+
position: sticky;
|
|
360
|
+
top: 0;
|
|
361
|
+
z-index: 5;
|
|
362
|
+
background: var(--rg-paper);
|
|
363
|
+
border-bottom: 1px solid var(--rg-border);
|
|
317
364
|
}
|
|
318
365
|
.rg-instruction strong {
|
|
319
366
|
font-size: 16px;
|
|
320
367
|
}
|
|
321
368
|
.rg-instruction p {
|
|
322
|
-
max-width:
|
|
369
|
+
max-width: 640px;
|
|
370
|
+
margin: 4px 0 0;
|
|
323
371
|
font-size: 12px;
|
|
324
372
|
color: #5b6c65;
|
|
325
373
|
}
|
|
374
|
+
.rg-editor .rg-link {
|
|
375
|
+
padding: 0;
|
|
376
|
+
border: 0;
|
|
377
|
+
background: none;
|
|
378
|
+
color: var(--rg-accent);
|
|
379
|
+
font-size: inherit;
|
|
380
|
+
text-decoration: underline;
|
|
381
|
+
}
|
|
382
|
+
.rg-step-actions {
|
|
383
|
+
display: flex;
|
|
384
|
+
flex-wrap: wrap;
|
|
385
|
+
align-items: center;
|
|
386
|
+
justify-content: flex-end;
|
|
387
|
+
gap: 8px;
|
|
388
|
+
}
|
|
389
|
+
.rg-preview-controls {
|
|
390
|
+
display: flex;
|
|
391
|
+
align-items: center;
|
|
392
|
+
gap: 16px;
|
|
393
|
+
}
|
|
394
|
+
.rg-preview-controls label {
|
|
395
|
+
display: flex;
|
|
396
|
+
align-items: center;
|
|
397
|
+
gap: 8px;
|
|
398
|
+
margin: 0;
|
|
399
|
+
}
|
|
400
|
+
.rg-preview-controls > div {
|
|
401
|
+
text-align: right;
|
|
402
|
+
}
|
|
403
|
+
.rg-readiness {
|
|
404
|
+
display: block;
|
|
405
|
+
margin-top: 4px;
|
|
406
|
+
}
|
|
326
407
|
.rg-match-tools {
|
|
327
|
-
margin:
|
|
408
|
+
margin: 10px 0 12px;
|
|
328
409
|
}
|
|
329
|
-
.rg-
|
|
410
|
+
.rg-tools-end {
|
|
330
411
|
display: flex;
|
|
331
412
|
align-items: center;
|
|
332
|
-
gap:
|
|
413
|
+
gap: 14px;
|
|
333
414
|
margin-left: auto;
|
|
334
415
|
}
|
|
335
|
-
.rg-
|
|
416
|
+
.rg-editor .rg-inline-field {
|
|
417
|
+
display: flex;
|
|
418
|
+
align-items: center;
|
|
419
|
+
gap: 8px;
|
|
420
|
+
margin: 0;
|
|
421
|
+
white-space: nowrap;
|
|
422
|
+
}
|
|
423
|
+
.rg-editor .rg-inline-field select {
|
|
424
|
+
width: auto;
|
|
336
425
|
max-width: 225px;
|
|
337
426
|
}
|
|
427
|
+
.rg-guided .rg-dirty {
|
|
428
|
+
line-height: 1.6;
|
|
429
|
+
}
|
|
338
430
|
.rg-paired-views {
|
|
339
431
|
display: grid;
|
|
340
432
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
@@ -355,54 +447,124 @@
|
|
|
355
447
|
}
|
|
356
448
|
.rg-guided .rg-image-panel .rg-panel-heading {
|
|
357
449
|
min-height: 39px;
|
|
450
|
+
align-items: center;
|
|
358
451
|
}
|
|
359
452
|
.rg-guided .rg-image-panel .rg-panel-heading h2 {
|
|
360
453
|
margin: 0;
|
|
361
454
|
}
|
|
455
|
+
.rg-guided .rg-image-panel .rg-panel-heading .rg-file {
|
|
456
|
+
margin: 0;
|
|
457
|
+
}
|
|
458
|
+
.rg-guided .rg-image-panel[data-empty] .rg-panel-heading .rg-file {
|
|
459
|
+
display: none;
|
|
460
|
+
}
|
|
362
461
|
.rg-guided .rg-image-meta {
|
|
363
462
|
margin-top: 4px;
|
|
364
463
|
}
|
|
464
|
+
.rg-empty-actions {
|
|
465
|
+
justify-content: center;
|
|
466
|
+
margin-top: 16px;
|
|
467
|
+
}
|
|
365
468
|
.rg-view-tabs {
|
|
366
469
|
display: none;
|
|
470
|
+
border: 0;
|
|
471
|
+
margin: 0;
|
|
472
|
+
padding: 0;
|
|
367
473
|
}
|
|
368
474
|
.rg-review-panels {
|
|
369
475
|
display: grid;
|
|
370
476
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
371
|
-
gap:
|
|
372
|
-
padding: 18px 0;
|
|
477
|
+
gap: 18px;
|
|
478
|
+
padding: 18px 0 0;
|
|
373
479
|
}
|
|
374
|
-
.rg-review-panels section + section {
|
|
480
|
+
.rg-editor .rg-review-panels section + section {
|
|
375
481
|
margin-top: 0;
|
|
376
482
|
padding-top: 0;
|
|
377
483
|
border-top: none;
|
|
378
484
|
}
|
|
485
|
+
.rg-card {
|
|
486
|
+
min-width: 0;
|
|
487
|
+
padding: 16px;
|
|
488
|
+
border: 1px solid var(--rg-border);
|
|
489
|
+
border-radius: 6px;
|
|
490
|
+
background: white;
|
|
491
|
+
}
|
|
492
|
+
.rg-editor .rg-review-panels .rg-card {
|
|
493
|
+
padding: 16px;
|
|
494
|
+
}
|
|
495
|
+
.rg-editor .rg-card h3 {
|
|
496
|
+
margin: 16px 0 0;
|
|
497
|
+
font-size: 13px;
|
|
498
|
+
font-weight: 650;
|
|
499
|
+
}
|
|
500
|
+
.rg-accuracy {
|
|
501
|
+
margin-bottom: 12px;
|
|
502
|
+
padding: 10px 12px;
|
|
503
|
+
border-left: 4px solid #9dab9e;
|
|
504
|
+
background: #f4f6f1;
|
|
505
|
+
}
|
|
506
|
+
.rg-accuracy[data-quality="good"] {
|
|
507
|
+
border-left-color: #2f8a5b;
|
|
508
|
+
}
|
|
509
|
+
.rg-accuracy[data-quality="fair"] {
|
|
510
|
+
border-left-color: #c48e45;
|
|
511
|
+
}
|
|
512
|
+
.rg-accuracy[data-quality="poor"] {
|
|
513
|
+
border-left-color: #b4452d;
|
|
514
|
+
}
|
|
515
|
+
.rg-accuracy p {
|
|
516
|
+
margin: 4px 0;
|
|
517
|
+
font-size: 12px;
|
|
518
|
+
}
|
|
379
519
|
.rg-guided .rg-gcp-panel {
|
|
380
520
|
border-top: 1px solid var(--rg-border);
|
|
381
521
|
margin-top: 18px;
|
|
382
522
|
padding-top: 16px;
|
|
383
523
|
}
|
|
384
|
-
.rg-
|
|
385
|
-
|
|
386
|
-
|
|
524
|
+
.rg-editor .rg-review-panels .rg-gcp-panel {
|
|
525
|
+
margin-top: 0;
|
|
526
|
+
padding-top: 0;
|
|
527
|
+
border-top: none;
|
|
528
|
+
}
|
|
529
|
+
.rg-editor .rg-point-list {
|
|
530
|
+
width: auto;
|
|
531
|
+
min-width: 280px;
|
|
387
532
|
}
|
|
388
533
|
.rg-advanced {
|
|
389
534
|
margin-top: 16px;
|
|
390
535
|
}
|
|
536
|
+
.rg-export-progress {
|
|
537
|
+
display: flex;
|
|
538
|
+
align-items: center;
|
|
539
|
+
gap: 12px;
|
|
540
|
+
margin: 10px 0;
|
|
541
|
+
}
|
|
542
|
+
.rg-status:empty {
|
|
543
|
+
margin: 0;
|
|
544
|
+
padding: 0;
|
|
545
|
+
border: 0;
|
|
546
|
+
}
|
|
391
547
|
@media (max-width: 720px) {
|
|
392
548
|
.rg-guided {
|
|
393
549
|
padding: 12px;
|
|
394
550
|
}
|
|
551
|
+
.rg-steps li:not([data-state="current"]) .rg-step-label {
|
|
552
|
+
display: none;
|
|
553
|
+
}
|
|
395
554
|
.rg-matching-bar {
|
|
396
|
-
|
|
397
|
-
align-items: stretch;
|
|
398
|
-
gap: 4px;
|
|
555
|
+
grid-template-columns: minmax(0, 1fr);
|
|
399
556
|
}
|
|
557
|
+
.rg-step-actions,
|
|
400
558
|
.rg-preview-controls {
|
|
401
|
-
justify-content:
|
|
559
|
+
justify-content: flex-start;
|
|
402
560
|
}
|
|
403
|
-
.rg-
|
|
561
|
+
.rg-preview-controls > div {
|
|
562
|
+
text-align: left;
|
|
563
|
+
}
|
|
564
|
+
.rg-tools-end {
|
|
404
565
|
margin-left: 0;
|
|
405
566
|
width: 100%;
|
|
567
|
+
flex-wrap: wrap;
|
|
406
568
|
}
|
|
407
569
|
.rg-paired-views {
|
|
408
570
|
grid-template-columns: minmax(0, 1fr);
|
|
@@ -423,18 +585,10 @@
|
|
|
423
585
|
grid-template-columns: minmax(0, 1fr);
|
|
424
586
|
}
|
|
425
587
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
margin: 0;
|
|
433
|
-
padding: 0;
|
|
434
|
-
}
|
|
435
|
-
.rg-guided .rg-matching-bar {
|
|
436
|
-
position: sticky;
|
|
437
|
-
top: 0;
|
|
438
|
-
z-index: 5;
|
|
439
|
-
background: var(--rg-paper);
|
|
588
|
+
.rg-editor .rg-error {
|
|
589
|
+
padding: 8px 12px;
|
|
590
|
+
border-left: 3px solid #b4452d;
|
|
591
|
+
background: #fbefea;
|
|
592
|
+
color: #7a2614;
|
|
593
|
+
font-size: 12px;
|
|
440
594
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Start a browser download and revoke its temporary object URL after one
|
|
3
|
-
*
|
|
2
|
+
* Start a browser download and revoke its temporary object URL after one minute, which
|
|
3
|
+
* leaves large files (such as full-resolution GeoTIFFs) time to start saving in every
|
|
4
|
+
* browser. Invoke from a client-side user action; this utility performs no upload.
|
|
4
5
|
*/
|
|
5
6
|
export declare function downloadBlob(blob: Blob, name: string): void;
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Start a browser download and revoke its temporary object URL after one
|
|
3
|
-
*
|
|
2
|
+
* Start a browser download and revoke its temporary object URL after one minute, which
|
|
3
|
+
* leaves large files (such as full-resolution GeoTIFFs) time to start saving in every
|
|
4
|
+
* browser. Invoke from a client-side user action; this utility performs no upload.
|
|
4
5
|
*/
|
|
5
6
|
export function downloadBlob(blob, name) {
|
|
6
7
|
const url = URL.createObjectURL(blob), a = document.createElement("a");
|
|
7
8
|
a.href = url;
|
|
8
9
|
a.download = name;
|
|
9
|
-
a.
|
|
10
|
-
|
|
10
|
+
a.style.display = "none";
|
|
11
|
+
// Some browsers ignore clicks on anchors outside the document.
|
|
12
|
+
document.body.append(a);
|
|
13
|
+
try {
|
|
14
|
+
a.click();
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
a.remove();
|
|
18
|
+
}
|
|
19
|
+
setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
|
11
20
|
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.3.0",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
@@ -31,15 +31,14 @@
|
|
|
31
31
|
},
|
|
32
32
|
"./styles.css": "./dist/styles.css"
|
|
33
33
|
},
|
|
34
|
-
"dependencies": {
|
|
35
|
-
"@georeferencing/core": "^0.1.0"
|
|
36
|
-
},
|
|
37
34
|
"peerDependencies": {
|
|
35
|
+
"@georeferencing/core": "^0.3.0",
|
|
38
36
|
"react": ">=19.3.0 <20",
|
|
39
37
|
"react-dom": ">=19.3.0 <20",
|
|
40
38
|
"ol": ">=10.10.0 <11"
|
|
41
39
|
},
|
|
42
40
|
"devDependencies": {
|
|
41
|
+
"@georeferencing/core": "0.3.0",
|
|
43
42
|
"typescript": "5.9.3",
|
|
44
43
|
"react": "19.3.0",
|
|
45
44
|
"react-dom": "19.3.0",
|