@georeferencing/react 0.2.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 +8 -4
- package/dist/Georeferencer.js +20 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/panels/FeaturePanel.js +1 -2
- package/dist/panels/ImagePanel.js +33 -2
- 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
|
|
@@ -176,7 +177,10 @@ sticky bar:
|
|
|
176
177
|
Completed steps in the step bar are clickable. Desktop keeps both views side by
|
|
177
178
|
side; below 720 px, Image/Map buttons switch panes and follow pending points.
|
|
178
179
|
In both views, dragging pans and scrolling zooms; a click without dragging places
|
|
179
|
-
a point.
|
|
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.
|
|
180
184
|
|
|
181
185
|
OpenLayers' `Map` creates its default interactions with `onFocusOnly: true`. If
|
|
182
186
|
the map target has a `tabindex` (for keyboard navigation), mouse panning and wheel
|
package/dist/Georeferencer.js
CHANGED
|
@@ -15,6 +15,15 @@ import { PreviewControls } from "./panels/PreviewControls.js";
|
|
|
15
15
|
import { ReferencePanel } from "./panels/ReferencePanel.js";
|
|
16
16
|
const DEFAULT_BINDING = {};
|
|
17
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
|
+
}
|
|
18
27
|
/**
|
|
19
28
|
* Ready-made React editor for an existing OpenLayers map. Composes image, GCP, alignment, reference, output and optional drawing controls.
|
|
20
29
|
*
|
|
@@ -49,7 +58,7 @@ export function Georeferencer({ controller, referenceMap, bindingOptions = DEFAU
|
|
|
49
58
|
setMobileView("image");
|
|
50
59
|
// A metadata-only restored document still needs its matching local bytes.
|
|
51
60
|
if (imageId && imageReady && controller.getSnapshot().mode === "align")
|
|
52
|
-
controller
|
|
61
|
+
selectTool(controller, "gcp");
|
|
53
62
|
}, [controller, guided, imageId, imageReady]);
|
|
54
63
|
useEffect(() => {
|
|
55
64
|
if (guided)
|
|
@@ -58,7 +67,7 @@ export function Georeferencer({ controller, referenceMap, bindingOptions = DEFAU
|
|
|
58
67
|
// Matching always uses the point tool; dragging pans, so no separate pan mode is needed.
|
|
59
68
|
useEffect(() => {
|
|
60
69
|
if (guided && step === 1 && s.tool !== "gcp" && s.mode === "align")
|
|
61
|
-
controller
|
|
70
|
+
selectTool(controller, "gcp");
|
|
62
71
|
}, [controller, guided, step, s.tool, s.mode]);
|
|
63
72
|
useEffect(() => {
|
|
64
73
|
if (guided && mobileView === "map")
|
|
@@ -66,7 +75,15 @@ export function Georeferencer({ controller, referenceMap, bindingOptions = DEFAU
|
|
|
66
75
|
}, [guided, mobileView, referenceMap]);
|
|
67
76
|
useEffect(() => {
|
|
68
77
|
controller.start();
|
|
69
|
-
|
|
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
|
+
}
|
|
70
87
|
return () => {
|
|
71
88
|
binding.current?.detach();
|
|
72
89
|
binding.current = null;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ready-made editor, composable panels and subscription hook with optional exports.
|
|
3
3
|
* Import from `@georeferencing/react`.
|
|
4
|
-
* @module react
|
|
4
|
+
* @module @georeferencing/react
|
|
5
|
+
* @group @georeferencing/react
|
|
5
6
|
*/
|
|
6
7
|
export type { ControllerOptions, Document, Features, Gcp, ImageMetadata, SaveEnvelope, } from "@georeferencing/core";
|
|
7
8
|
export { createDocument, GeoreferencerController } from "@georeferencing/core";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ready-made editor, composable panels and subscription hook with optional exports.
|
|
3
3
|
* Import from `@georeferencing/react`.
|
|
4
|
-
* @module react
|
|
4
|
+
* @module @georeferencing/react
|
|
5
|
+
* @group @georeferencing/react
|
|
5
6
|
*/
|
|
6
7
|
export { createDocument, GeoreferencerController } from "@georeferencing/core";
|
|
7
8
|
export { ALL_CONTROLS, MINIMAL_CONTROLS } from "./controls.js";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { validateFeatures } from "@georeferencing/core";
|
|
3
2
|
import { useGeoreferencer } from "../hooks/useGeoreferencer.js";
|
|
4
3
|
import { identity } from "../localization.js";
|
|
5
4
|
/** Plain-language names for drawing tools and the geometry types they create. */
|
|
@@ -28,7 +27,7 @@ t = identity, }) {
|
|
|
28
27
|
const s = useGeoreferencer(controller), d = s.document;
|
|
29
28
|
if (!controller.options.digitizing)
|
|
30
29
|
return null;
|
|
31
|
-
return (_jsxs("section", { children: [_jsxs("h2", { children: [t("Drawings"), " ", _jsx("small", { children: d.features.features.length })] }),
|
|
30
|
+
return (_jsxs("section", { children: [_jsxs("h2", { children: [t("Drawings"), " ", _jsx("small", { children: d.features.features.length })] }), controller.getFeatureErrors().map((error) => (_jsx("p", { className: "rg-error", children: t(error) }, error))), _jsx("div", { className: "rg-toolbar", children: ["Point", "LineString", "Polygon", "modify", "navigate"].map((tool) => (_jsx("button", { type: "button", disabled: s.mode !== "draw", "aria-pressed": s.tool === tool, onClick: () => controller.setTool(tool), children: t(TOOL_LABELS[tool]) }, tool))) }), _jsx("p", { className: "rg-hint", children: t("Double-click to finish lines and areas. Escape cancels. Use Edit shapes to move vertices; Alt-click a vertex to remove it.") }), d.features.features.map((f, i) => (_jsxs("div", { className: "rg-feature", children: [_jsxs("span", { children: [i + 1, " \u00B7 ", t(TOOL_LABELS[f.geometry.type])] }), _jsx("button", { type: "button", disabled: s.mode !== "draw", "aria-label": `${t("Delete feature")} ${i + 1}`, onClick: () => controller.deleteFeature(String(f.id)), children: "\u00D7" }), propertyEditor?.(f, (p) => controller.updateProperties(String(f.id), p))] }, f.id))), d.features.features.length > 0 &&
|
|
32
31
|
d.featuresReviewedAgainstAlignmentRevision !== d.alignmentRevision && (_jsx("button", { type: "button", disabled: d.confirmedAlignmentRevision !== d.alignmentRevision, onClick: () => controller.reviewFeatures(), children: t("I reviewed drawings against this alignment") })), _jsx("button", { type: "button", className: "rg-primary", disabled: !controller.options.onSave ||
|
|
33
32
|
!controller.canSaveFeatures() ||
|
|
34
33
|
s.saving === "running", onClick: () => void controller.save().catch((e) => controller.reportError(e)), children: t(s.saving === "running" ? "Saving…" : "Save features") }), _jsx("p", { "aria-live": "polite", children: s.saving === "succeeded"
|
|
@@ -18,7 +18,7 @@ navigation = true,
|
|
|
18
18
|
displayAdjustment = true,
|
|
19
19
|
/** Extra actions inside the empty drop zone, such as a sample image button. */
|
|
20
20
|
emptyActions, }) {
|
|
21
|
-
const s = useGeoreferencer(controller), image = s.document.sourceImage, svg = useRef(null);
|
|
21
|
+
const s = useGeoreferencer(controller), image = s.document.sourceImage, imageWidth = image?.width, svg = useRef(null);
|
|
22
22
|
const view = s.imageView ?? [
|
|
23
23
|
0, 0, 100, 100,
|
|
24
24
|
];
|
|
@@ -30,6 +30,10 @@ emptyActions, }) {
|
|
|
30
30
|
]);
|
|
31
31
|
const filterId = `rg-stretch-${useId().replace(/:/g, "")}`;
|
|
32
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);
|
|
33
37
|
const stretchHistogram = (local) => {
|
|
34
38
|
const pixels = displayPixels.current;
|
|
35
39
|
if (!pixels || !image)
|
|
@@ -97,6 +101,8 @@ emptyActions, }) {
|
|
|
97
101
|
const ctx = canvas.getContext("2d");
|
|
98
102
|
ctx.drawImage(img, 0, 0);
|
|
99
103
|
displayPixels.current = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
104
|
+
if (imageWidth)
|
|
105
|
+
setPreviewScale(img.width / imageWidth);
|
|
100
106
|
const data = displayPixels.current.data, bins = Array(32).fill(0);
|
|
101
107
|
for (let i = 0; i < data.length; i += 4)
|
|
102
108
|
if (data[i + 3])
|
|
@@ -108,10 +114,31 @@ emptyActions, }) {
|
|
|
108
114
|
return () => {
|
|
109
115
|
disposed = true;
|
|
110
116
|
displayPixels.current = null;
|
|
117
|
+
setPreviewScale(0);
|
|
111
118
|
img.onload = null;
|
|
112
119
|
img.src = "";
|
|
113
120
|
};
|
|
114
|
-
}, [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]);
|
|
115
142
|
const point = (e) => {
|
|
116
143
|
const matrix = svg.current?.getScreenCTM();
|
|
117
144
|
if (!matrix)
|
|
@@ -240,6 +267,10 @@ emptyActions, }) {
|
|
|
240
267
|
setDragPoint(null);
|
|
241
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: {
|
|
242
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",
|
|
243
274
|
} })), s.document.gcps.map((gcp) => {
|
|
244
275
|
const p = dragPoint?.id === gcp.id ? dragPoint.point : gcp.image;
|
|
245
276
|
return (_jsxs("g", { transform: `translate(${p[0]} ${p[1]})`, onPointerDown: (e) => {
|
|
@@ -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.2.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",
|