@gramps-connect/story 0.3.0 → 0.5.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 +23 -5
- package/dist/editor.js +2065 -177
- package/dist/editor.js.map +4 -4
- package/dist/embed.js +1 -1
- package/dist/map-edit.js +361 -0
- package/dist/map-edit.js.map +7 -0
- package/dist/player.js +3 -1
- package/dist/player.js.map +2 -2
- package/dist/types/editor/MapEditor.d.ts +89 -0
- package/dist/types/editor/PlaceField.d.ts +24 -0
- package/dist/types/editor/PlaceMap.d.ts +12 -0
- package/dist/types/editor/PlaceMapDialog.d.ts +18 -0
- package/dist/types/editor/StoryEditor.d.ts +8 -1
- package/dist/types/editor/index.d.ts +7 -1
- package/dist/types/editor/mapEditorStrings.d.ts +50 -0
- package/dist/types/editor/model.d.ts +17 -1
- package/dist/types/editor/placeSearch.d.ts +22 -0
- package/dist/types/editor/placeShapes.d.ts +13 -0
- package/dist/types/editor/strings.d.ts +28 -0
- package/dist/types/mapEdit/geometry.d.ts +91 -0
- package/dist/types/mapEdit/index.d.ts +3 -0
- package/dist/types/mapEdit/mapFile.d.ts +77 -0
- package/dist/types/mapEdit/model.d.ts +55 -0
- package/package.json +18 -2
- package/src/editor/MapEditor.tsx +1545 -0
- package/src/editor/PlaceField.tsx +170 -0
- package/src/editor/PlaceMap.tsx +88 -0
- package/src/editor/PlaceMapDialog.tsx +228 -0
- package/src/editor/StoryEditor.tsx +50 -14
- package/src/editor/index.ts +11 -2
- package/src/editor/mapEditorStrings.ts +109 -0
- package/src/editor/model.ts +38 -1
- package/src/editor/placeSearch.ts +61 -0
- package/src/editor/placeShapes.ts +63 -0
- package/src/editor/strings.ts +56 -0
- package/src/mapEdit/geometry.ts +211 -0
- package/src/mapEdit/index.ts +15 -0
- package/src/mapEdit/mapFile.ts +199 -0
- package/src/mapEdit/model.ts +250 -0
- package/src/player/MapBackground.tsx +3 -1
package/dist/editor.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
// src/editor/StoryEditor.tsx
|
|
2
|
-
import { useCallback, useEffect as
|
|
2
|
+
import { useCallback, useEffect as useEffect9, useMemo as useMemo4, useRef as useRef11, useState as useState10 } from "react";
|
|
3
3
|
import {
|
|
4
|
-
ActionIcon as
|
|
4
|
+
ActionIcon as ActionIcon3,
|
|
5
5
|
Badge,
|
|
6
|
-
Box as
|
|
7
|
-
Button as
|
|
8
|
-
Divider,
|
|
9
|
-
Group as
|
|
10
|
-
Modal,
|
|
6
|
+
Box as Box4,
|
|
7
|
+
Button as Button6,
|
|
8
|
+
Divider as Divider2,
|
|
9
|
+
Group as Group7,
|
|
10
|
+
Modal as Modal2,
|
|
11
11
|
Popover,
|
|
12
12
|
Select,
|
|
13
|
-
Stack as
|
|
14
|
-
Text as
|
|
15
|
-
TextInput as
|
|
16
|
-
Tooltip as
|
|
17
|
-
UnstyledButton as
|
|
13
|
+
Stack as Stack8,
|
|
14
|
+
Text as Text7,
|
|
15
|
+
TextInput as TextInput6,
|
|
16
|
+
Tooltip as Tooltip4,
|
|
17
|
+
UnstyledButton as UnstyledButton3
|
|
18
18
|
} from "@mantine/core";
|
|
19
19
|
import { useMediaQuery } from "@mantine/hooks";
|
|
20
20
|
|
|
@@ -1787,6 +1787,53 @@ function findUnknownFields(value, node, path, warnings, root = story_schema_defa
|
|
|
1787
1787
|
}
|
|
1788
1788
|
}
|
|
1789
1789
|
|
|
1790
|
+
// src/player/geo.ts
|
|
1791
|
+
function visit(coords, bounds) {
|
|
1792
|
+
if (!Array.isArray(coords)) return;
|
|
1793
|
+
if (coords.length >= 2 && typeof coords[0] === "number" && typeof coords[1] === "number") {
|
|
1794
|
+
const [lon, lat] = coords;
|
|
1795
|
+
bounds[0] = Math.min(bounds[0], lon);
|
|
1796
|
+
bounds[1] = Math.min(bounds[1], lat);
|
|
1797
|
+
bounds[2] = Math.max(bounds[2], lon);
|
|
1798
|
+
bounds[3] = Math.max(bounds[3], lat);
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
for (const child of coords) visit(child, bounds);
|
|
1802
|
+
}
|
|
1803
|
+
function visitGeoJson(node, bounds) {
|
|
1804
|
+
if (!node || typeof node !== "object") return;
|
|
1805
|
+
const obj = node;
|
|
1806
|
+
if (obj.coordinates !== void 0) visit(obj.coordinates, bounds);
|
|
1807
|
+
if (obj.geometry) visitGeoJson(obj.geometry, bounds);
|
|
1808
|
+
for (const g of obj.geometries ?? []) visitGeoJson(g, bounds);
|
|
1809
|
+
for (const f of obj.features ?? []) visitGeoJson(f, bounds);
|
|
1810
|
+
}
|
|
1811
|
+
function placeBounds(place) {
|
|
1812
|
+
if (!place) return null;
|
|
1813
|
+
const bounds = [Infinity, Infinity, -Infinity, -Infinity];
|
|
1814
|
+
visitGeoJson(place.geometry, bounds);
|
|
1815
|
+
for (const overlay of place.overlays ?? []) visit(overlay.corners, bounds);
|
|
1816
|
+
return Number.isFinite(bounds[0]) ? bounds : null;
|
|
1817
|
+
}
|
|
1818
|
+
function placeCenter(place) {
|
|
1819
|
+
if (place?.lat !== void 0 && place.lon !== void 0) return [place.lon, place.lat];
|
|
1820
|
+
const bounds = placeBounds(place);
|
|
1821
|
+
return bounds ? [(bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2] : null;
|
|
1822
|
+
}
|
|
1823
|
+
function storyPlaces(places) {
|
|
1824
|
+
const bounds = [Infinity, Infinity, -Infinity, -Infinity];
|
|
1825
|
+
const points = /* @__PURE__ */ new Map();
|
|
1826
|
+
for (const place of places) {
|
|
1827
|
+
const center = placeCenter(place);
|
|
1828
|
+
if (!center) continue;
|
|
1829
|
+
points.set(`${center[0].toFixed(5)},${center[1].toFixed(5)}`, center);
|
|
1830
|
+
visit(center, bounds);
|
|
1831
|
+
const box = placeBounds(place);
|
|
1832
|
+
if (box) visit([[box[0], box[1]], [box[2], box[3]]], bounds);
|
|
1833
|
+
}
|
|
1834
|
+
return points.size ? { points: [...points.values()], bounds } : null;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1790
1837
|
// src/player/StoryPlayer.tsx
|
|
1791
1838
|
import { useEffect as useEffect3, useMemo, useRef as useRef3, useState as useState3 } from "react";
|
|
1792
1839
|
|
|
@@ -1839,53 +1886,6 @@ function timelineYear(date) {
|
|
|
1839
1886
|
return sortYear + (month - 1) / 12 + (day ? (day - 1) / 365 : 0);
|
|
1840
1887
|
}
|
|
1841
1888
|
|
|
1842
|
-
// src/player/geo.ts
|
|
1843
|
-
function visit(coords, bounds) {
|
|
1844
|
-
if (!Array.isArray(coords)) return;
|
|
1845
|
-
if (coords.length >= 2 && typeof coords[0] === "number" && typeof coords[1] === "number") {
|
|
1846
|
-
const [lon, lat] = coords;
|
|
1847
|
-
bounds[0] = Math.min(bounds[0], lon);
|
|
1848
|
-
bounds[1] = Math.min(bounds[1], lat);
|
|
1849
|
-
bounds[2] = Math.max(bounds[2], lon);
|
|
1850
|
-
bounds[3] = Math.max(bounds[3], lat);
|
|
1851
|
-
return;
|
|
1852
|
-
}
|
|
1853
|
-
for (const child of coords) visit(child, bounds);
|
|
1854
|
-
}
|
|
1855
|
-
function visitGeoJson(node, bounds) {
|
|
1856
|
-
if (!node || typeof node !== "object") return;
|
|
1857
|
-
const obj = node;
|
|
1858
|
-
if (obj.coordinates !== void 0) visit(obj.coordinates, bounds);
|
|
1859
|
-
if (obj.geometry) visitGeoJson(obj.geometry, bounds);
|
|
1860
|
-
for (const g of obj.geometries ?? []) visitGeoJson(g, bounds);
|
|
1861
|
-
for (const f of obj.features ?? []) visitGeoJson(f, bounds);
|
|
1862
|
-
}
|
|
1863
|
-
function placeBounds(place) {
|
|
1864
|
-
if (!place) return null;
|
|
1865
|
-
const bounds = [Infinity, Infinity, -Infinity, -Infinity];
|
|
1866
|
-
visitGeoJson(place.geometry, bounds);
|
|
1867
|
-
for (const overlay of place.overlays ?? []) visit(overlay.corners, bounds);
|
|
1868
|
-
return Number.isFinite(bounds[0]) ? bounds : null;
|
|
1869
|
-
}
|
|
1870
|
-
function placeCenter(place) {
|
|
1871
|
-
if (place?.lat !== void 0 && place.lon !== void 0) return [place.lon, place.lat];
|
|
1872
|
-
const bounds = placeBounds(place);
|
|
1873
|
-
return bounds ? [(bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2] : null;
|
|
1874
|
-
}
|
|
1875
|
-
function storyPlaces(places) {
|
|
1876
|
-
const bounds = [Infinity, Infinity, -Infinity, -Infinity];
|
|
1877
|
-
const points = /* @__PURE__ */ new Map();
|
|
1878
|
-
for (const place of places) {
|
|
1879
|
-
const center = placeCenter(place);
|
|
1880
|
-
if (!center) continue;
|
|
1881
|
-
points.set(`${center[0].toFixed(5)},${center[1].toFixed(5)}`, center);
|
|
1882
|
-
visit(center, bounds);
|
|
1883
|
-
const box = placeBounds(place);
|
|
1884
|
-
if (box) visit([[box[0], box[1]], [box[2], box[3]]], bounds);
|
|
1885
|
-
}
|
|
1886
|
-
return points.size ? { points: [...points.values()], bounds } : null;
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
1889
|
// src/player/MapBackground.tsx
|
|
1890
1890
|
import { useEffect, useRef, useState } from "react";
|
|
1891
1891
|
import * as maplibregl from "maplibre-gl";
|
|
@@ -2057,7 +2057,9 @@ function MapBackground({ initialCenter, current, dark, colors, panelFraction, oh
|
|
|
2057
2057
|
type: "fill",
|
|
2058
2058
|
source: SHAPE_SOURCE,
|
|
2059
2059
|
filter: ["==", ["geometry-type"], "Polygon"],
|
|
2060
|
-
|
|
2060
|
+
// And its own `opacity` (what the map editor sets on an area) over
|
|
2061
|
+
// a light default.
|
|
2062
|
+
paint: { "fill-color": color, "fill-opacity": ["coalesce", ["to-number", ["get", "opacity"], 0.25], 0.25] }
|
|
2061
2063
|
});
|
|
2062
2064
|
map.addLayer({
|
|
2063
2065
|
id: LINE_LAYER,
|
|
@@ -2967,6 +2969,30 @@ function slideDate(previous, text, edtf) {
|
|
|
2967
2969
|
if (next.edtf !== previous?.edtf) delete next.sortYear;
|
|
2968
2970
|
return next;
|
|
2969
2971
|
}
|
|
2972
|
+
function patchPlace(place, patch) {
|
|
2973
|
+
if (place && Object.entries(patch).every(([key, value]) => place[key] === value)) return place;
|
|
2974
|
+
const next = patched(place ?? {}, patch);
|
|
2975
|
+
return Object.keys(next).length ? next : void 0;
|
|
2976
|
+
}
|
|
2977
|
+
var roundDegrees = (value) => Math.round(value * 1e5) / 1e5;
|
|
2978
|
+
function parseLatLon(text) {
|
|
2979
|
+
const parts = text.trim().split(/\s*[,;\s]\s*/);
|
|
2980
|
+
if (parts.length !== 2 || parts.some((p) => !/^[-+]?(\d+\.?\d*|\.\d+)$/.test(p))) return null;
|
|
2981
|
+
const [lat, lon] = parts.map(Number);
|
|
2982
|
+
if (Math.abs(lat) > 90 || Math.abs(lon) > 180) return null;
|
|
2983
|
+
return { lat, lon };
|
|
2984
|
+
}
|
|
2985
|
+
function formatLatLon(lat, lon) {
|
|
2986
|
+
return `${roundDegrees(lat)}, ${roundDegrees(lon)}`;
|
|
2987
|
+
}
|
|
2988
|
+
function storyPlaceChoices(doc, exceptIndex) {
|
|
2989
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2990
|
+
doc.slides.forEach((slide, i) => {
|
|
2991
|
+
const name = slide.place?.name?.trim();
|
|
2992
|
+
if (i !== exceptIndex && name && !byName.has(name)) byName.set(name, slide.place);
|
|
2993
|
+
});
|
|
2994
|
+
return [...byName.values()];
|
|
2995
|
+
}
|
|
2970
2996
|
function sameBytes(a, b) {
|
|
2971
2997
|
if (!a || a.length !== b.length) return false;
|
|
2972
2998
|
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
@@ -3249,22 +3275,1803 @@ function PhotosField({ photos, media, urlFor, strings, busy, onChange, onFiles,
|
|
|
3249
3275
|
);
|
|
3250
3276
|
}
|
|
3251
3277
|
|
|
3252
|
-
// src/editor/
|
|
3253
|
-
import { useState as useState6 } from "react";
|
|
3254
|
-
import { Stack as Stack3, UnstyledButton } from "@mantine/core";
|
|
3278
|
+
// src/editor/PlaceField.tsx
|
|
3279
|
+
import { useEffect as useEffect7, useMemo as useMemo2, useRef as useRef7, useState as useState6 } from "react";
|
|
3280
|
+
import { Autocomplete, Button as Button2, Group as Group3, Loader as Loader2, Stack as Stack3, Text as Text3, TextInput as TextInput3, Tooltip as Tooltip2, UnstyledButton } from "@mantine/core";
|
|
3281
|
+
|
|
3282
|
+
// src/editor/PlaceMap.tsx
|
|
3283
|
+
import { useEffect as useEffect6, useRef as useRef6 } from "react";
|
|
3284
|
+
import * as maplibregl2 from "maplibre-gl";
|
|
3285
|
+
import "maplibre-gl/dist/maplibre-gl.css";
|
|
3255
3286
|
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
3287
|
+
function PlaceMap({ point, around, dark = false, label, onPick }) {
|
|
3288
|
+
const container = useRef6(null);
|
|
3289
|
+
const mapRef = useRef6(null);
|
|
3290
|
+
const markerRef = useRef6(null);
|
|
3291
|
+
const onPickRef = useRef6(onPick);
|
|
3292
|
+
onPickRef.current = onPick;
|
|
3293
|
+
const pick = (lngLat) => onPickRef.current([roundDegrees(lngLat.lng), roundDegrees(lngLat.lat)]);
|
|
3294
|
+
const start = useRef6({ point, around });
|
|
3295
|
+
useEffect6(() => {
|
|
3296
|
+
if (!container.current) return;
|
|
3297
|
+
const { point: first, around: box } = start.current;
|
|
3298
|
+
const map = new maplibregl2.Map({
|
|
3299
|
+
container: container.current,
|
|
3300
|
+
style: mapStyleUrl(dark, null),
|
|
3301
|
+
...first ? { center: first, zoom: 9 } : box ? { bounds: box, fitBoundsOptions: { padding: 30, maxZoom: 9 } } : { center: [10, 30], zoom: 0.5 },
|
|
3302
|
+
attributionControl: { compact: true },
|
|
3303
|
+
// Scrolling the form past the map shouldn't zoom it.
|
|
3304
|
+
cooperativeGestures: true
|
|
3305
|
+
});
|
|
3306
|
+
map.addControl(new maplibregl2.NavigationControl({ showCompass: false }), "top-right");
|
|
3307
|
+
map.getCanvas().style.cursor = "crosshair";
|
|
3308
|
+
map.on("click", (e) => pick(e.lngLat));
|
|
3309
|
+
mapRef.current = map;
|
|
3310
|
+
return () => {
|
|
3311
|
+
markerRef.current?.remove();
|
|
3312
|
+
markerRef.current = null;
|
|
3313
|
+
map.remove();
|
|
3314
|
+
mapRef.current = null;
|
|
3315
|
+
};
|
|
3316
|
+
}, [dark]);
|
|
3317
|
+
useEffect6(() => {
|
|
3318
|
+
const map = mapRef.current;
|
|
3319
|
+
if (!map) return;
|
|
3320
|
+
if (!point) {
|
|
3321
|
+
markerRef.current?.remove();
|
|
3322
|
+
markerRef.current = null;
|
|
3323
|
+
return;
|
|
3324
|
+
}
|
|
3325
|
+
if (!markerRef.current) {
|
|
3326
|
+
const el = document.createElement("div");
|
|
3327
|
+
const c = defaultColors(dark);
|
|
3328
|
+
el.innerHTML = `<svg viewBox="0 0 24 24" width="28" height="28" style="display:block;cursor:grab"><path d="${PIN_PATH_D}" fill="${c.mark}" stroke="${c.surface}" stroke-width="1.5" /></svg>`;
|
|
3329
|
+
const marker = new maplibregl2.Marker({ element: el, anchor: "bottom", draggable: true }).setLngLat(point).addTo(map);
|
|
3330
|
+
marker.on("dragend", () => pick(marker.getLngLat()));
|
|
3331
|
+
markerRef.current = marker;
|
|
3332
|
+
} else {
|
|
3333
|
+
markerRef.current.setLngLat(point);
|
|
3334
|
+
}
|
|
3335
|
+
if (!map.getBounds().contains(point)) map.easeTo({ center: point, zoom: Math.max(map.getZoom(), 8) });
|
|
3336
|
+
}, [point?.[0], point?.[1], dark]);
|
|
3337
|
+
return /* @__PURE__ */ jsx7(
|
|
3338
|
+
"div",
|
|
3339
|
+
{
|
|
3340
|
+
ref: container,
|
|
3341
|
+
role: "application",
|
|
3342
|
+
"aria-label": label,
|
|
3343
|
+
style: { height: 240, borderRadius: 6, overflow: "hidden", border: "1px solid var(--mantine-color-default-border)" }
|
|
3344
|
+
}
|
|
3345
|
+
);
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
// src/editor/PlaceField.tsx
|
|
3349
|
+
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3350
|
+
function PlaceField({ place, choices, around, language, search, dark, strings, onChange, onEditMap }) {
|
|
3351
|
+
const lat = place?.lat;
|
|
3352
|
+
const lon = place?.lon;
|
|
3353
|
+
const point = lat !== void 0 && lon !== void 0 ? [lon, lat] : null;
|
|
3354
|
+
const name = place?.name ?? "";
|
|
3355
|
+
const [coords, setCoords] = useState6(point ? formatLatLon(point[1], point[0]) : "");
|
|
3356
|
+
useEffect7(() => {
|
|
3357
|
+
setCoords((typed) => {
|
|
3358
|
+
const parsed = parseLatLon(typed);
|
|
3359
|
+
if (lat === void 0 || lon === void 0) return parsed ? "" : typed;
|
|
3360
|
+
return parsed && parsed.lat === lat && parsed.lon === lon ? typed : formatLatLon(lat, lon);
|
|
3361
|
+
});
|
|
3362
|
+
}, [lat, lon]);
|
|
3363
|
+
const coordsInvalid = coords.trim() !== "" && !parseLatLon(coords);
|
|
3364
|
+
const [found, setFound] = useState6({ state: "idle" });
|
|
3365
|
+
const searching = useRef7(null);
|
|
3366
|
+
useEffect7(() => () => searching.current?.abort(), []);
|
|
3367
|
+
const find = async () => {
|
|
3368
|
+
if (!search || !name.trim()) return;
|
|
3369
|
+
searching.current?.abort();
|
|
3370
|
+
const controller = new AbortController();
|
|
3371
|
+
searching.current = controller;
|
|
3372
|
+
setFound({ state: "searching" });
|
|
3373
|
+
try {
|
|
3374
|
+
const results = await search.search(name.trim(), language, controller.signal);
|
|
3375
|
+
setFound(results.length ? { state: "results", results } : { state: "message", message: strings.findPlaceNone });
|
|
3376
|
+
} catch {
|
|
3377
|
+
if (!controller.signal.aborted) setFound({ state: "message", message: strings.findPlaceFailed });
|
|
3378
|
+
}
|
|
3379
|
+
};
|
|
3380
|
+
const choose = async (result) => {
|
|
3381
|
+
if (!search) return;
|
|
3382
|
+
searching.current?.abort();
|
|
3383
|
+
const controller = new AbortController();
|
|
3384
|
+
searching.current = controller;
|
|
3385
|
+
setFound({ state: "searching" });
|
|
3386
|
+
try {
|
|
3387
|
+
const at = await search.point(result, controller.signal);
|
|
3388
|
+
if (!at) {
|
|
3389
|
+
setFound({ state: "message", message: strings.findPlaceNoPoint(result.label) });
|
|
3390
|
+
return;
|
|
3391
|
+
}
|
|
3392
|
+
setFound({ state: "idle" });
|
|
3393
|
+
onChange((p) => patchPlace(p, { name: result.label, lat: roundDegrees(at.lat), lon: roundDegrees(at.lon) }), "place");
|
|
3394
|
+
} catch {
|
|
3395
|
+
if (!controller.signal.aborted) setFound({ state: "message", message: strings.findPlaceFailed });
|
|
3396
|
+
}
|
|
3397
|
+
};
|
|
3398
|
+
const byName = useMemo2(() => new Map(choices.map((c) => [c.name.trim(), c])), [choices]);
|
|
3399
|
+
const keeps = [
|
|
3400
|
+
place?.geometry ? strings.placeShape : null,
|
|
3401
|
+
place?.overlays?.length ? strings.placeOverlays(place.overlays.length) : null
|
|
3402
|
+
].filter(Boolean);
|
|
3403
|
+
return /* @__PURE__ */ jsxs6(Stack3, { gap: "xs", children: [
|
|
3404
|
+
/* @__PURE__ */ jsxs6(Group3, { justify: "space-between", children: [
|
|
3405
|
+
/* @__PURE__ */ jsx8(Text3, { size: "sm", fw: 500, children: strings.place }),
|
|
3406
|
+
place && /* @__PURE__ */ jsx8(Button2, { variant: "subtle", size: "compact-xs", color: "red", onClick: () => {
|
|
3407
|
+
setFound({ state: "idle" });
|
|
3408
|
+
onChange(() => void 0, "place");
|
|
3409
|
+
}, children: strings.removePlace })
|
|
3410
|
+
] }),
|
|
3411
|
+
/* @__PURE__ */ jsxs6(Group3, { gap: "xs", align: "flex-end", wrap: "nowrap", children: [
|
|
3412
|
+
/* @__PURE__ */ jsx8(
|
|
3413
|
+
Autocomplete,
|
|
3414
|
+
{
|
|
3415
|
+
size: "xs",
|
|
3416
|
+
label: strings.placeName,
|
|
3417
|
+
style: { flex: 1 },
|
|
3418
|
+
value: name,
|
|
3419
|
+
data: [...byName.keys()],
|
|
3420
|
+
comboboxProps: { withinPortal: true },
|
|
3421
|
+
onChange: (value) => onChange((p) => patchPlace(p, { name: value || void 0 }), "place:name"),
|
|
3422
|
+
onOptionSubmit: (value) => {
|
|
3423
|
+
const chosen = byName.get(value);
|
|
3424
|
+
if (chosen) onChange(() => structuredClone(chosen), "place");
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
),
|
|
3428
|
+
search && /* @__PURE__ */ jsx8(Tooltip2, { label: strings.findPlaceHint, multiline: true, w: 260, children: /* @__PURE__ */ jsx8(Button2, { variant: "default", size: "xs", disabled: !name.trim() || found.state === "searching", onClick: () => void find(), children: strings.findPlace }) })
|
|
3429
|
+
] }),
|
|
3430
|
+
found.state === "searching" && /* @__PURE__ */ jsx8(Loader2, { size: "xs" }),
|
|
3431
|
+
found.state === "message" && /* @__PURE__ */ jsx8(Text3, { size: "xs", c: "dimmed", role: "status", children: found.message }),
|
|
3432
|
+
found.state === "results" && /* @__PURE__ */ jsx8(Stack3, { gap: 2, role: "list", "aria-label": strings.findPlace, children: found.results.map((result) => /* @__PURE__ */ jsxs6(
|
|
3433
|
+
UnstyledButton,
|
|
3434
|
+
{
|
|
3435
|
+
role: "listitem",
|
|
3436
|
+
onClick: () => void choose(result),
|
|
3437
|
+
style: { padding: "4px 8px", borderRadius: 4, border: "1px solid var(--mantine-color-default-border)" },
|
|
3438
|
+
children: [
|
|
3439
|
+
/* @__PURE__ */ jsx8(Text3, { size: "sm", children: result.label }),
|
|
3440
|
+
result.description && /* @__PURE__ */ jsx8(Text3, { size: "xs", c: "dimmed", children: result.description })
|
|
3441
|
+
]
|
|
3442
|
+
},
|
|
3443
|
+
result.id
|
|
3444
|
+
)) }),
|
|
3445
|
+
/* @__PURE__ */ jsx8(
|
|
3446
|
+
TextInput3,
|
|
3447
|
+
{
|
|
3448
|
+
size: "xs",
|
|
3449
|
+
label: strings.coordinates,
|
|
3450
|
+
placeholder: strings.coordinatesHint,
|
|
3451
|
+
w: 260,
|
|
3452
|
+
value: coords,
|
|
3453
|
+
error: coordsInvalid ? strings.coordinatesInvalid : void 0,
|
|
3454
|
+
onChange: (e) => {
|
|
3455
|
+
const text = e.currentTarget.value;
|
|
3456
|
+
setCoords(text);
|
|
3457
|
+
const parsed = parseLatLon(text);
|
|
3458
|
+
if (!text.trim()) onChange((p) => patchPlace(p, { lat: void 0, lon: void 0 }), "place:point");
|
|
3459
|
+
else if (parsed) onChange((p) => patchPlace(p, parsed), "place:point");
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
),
|
|
3463
|
+
/* @__PURE__ */ jsx8(
|
|
3464
|
+
PlaceMap,
|
|
3465
|
+
{
|
|
3466
|
+
point,
|
|
3467
|
+
around,
|
|
3468
|
+
dark,
|
|
3469
|
+
label: strings.placeMap,
|
|
3470
|
+
onPick: ([pickedLon, pickedLat]) => onChange((p) => patchPlace(p, { lat: pickedLat, lon: pickedLon }), "place:point")
|
|
3471
|
+
}
|
|
3472
|
+
),
|
|
3473
|
+
/* @__PURE__ */ jsx8(Text3, { size: "xs", c: "dimmed", children: strings.placeMapHint }),
|
|
3474
|
+
keeps.length > 0 && /* @__PURE__ */ jsx8(Text3, { size: "xs", c: "dimmed", children: strings.placeKeeps(keeps.join(", ")) }),
|
|
3475
|
+
onEditMap && /* @__PURE__ */ jsxs6(Group3, { gap: "sm", wrap: "nowrap", align: "center", children: [
|
|
3476
|
+
/* @__PURE__ */ jsx8(Button2, { variant: "default", size: "xs", style: { flex: "none" }, onClick: onEditMap, children: strings.editMap }),
|
|
3477
|
+
/* @__PURE__ */ jsx8(Text3, { size: "xs", c: "dimmed", children: strings.editMapHint })
|
|
3478
|
+
] })
|
|
3479
|
+
] });
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
// src/editor/PlaceMapDialog.tsx
|
|
3483
|
+
import { useMemo as useMemo3, useRef as useRef9, useState as useState8 } from "react";
|
|
3484
|
+
import { ActionIcon as ActionIcon2, Box as Box3, Button as Button4, Group as Group5, Modal, Stack as Stack5, Text as Text5, TextInput as TextInput5, Tooltip as Tooltip3 } from "@mantine/core";
|
|
3485
|
+
|
|
3486
|
+
// src/mapEdit/model.ts
|
|
3487
|
+
var EDITOR_OWNED = /* @__PURE__ */ new Set(["name", "color", "opacity"]);
|
|
3488
|
+
var DROPPED = /* @__PURE__ */ new Set(["coordinateProperties", "_gpxType", "styleUrl", "@geometry-type"]);
|
|
3489
|
+
function round2d(position) {
|
|
3490
|
+
return [Number(position[0].toFixed(9)), Number(position[1].toFixed(9))];
|
|
3491
|
+
}
|
|
3492
|
+
function singleParts(geometry) {
|
|
3493
|
+
switch (geometry.type) {
|
|
3494
|
+
case "Point":
|
|
3495
|
+
return [{ type: "Point", coordinates: round2d(geometry.coordinates) }];
|
|
3496
|
+
case "LineString":
|
|
3497
|
+
return [{ type: "LineString", coordinates: geometry.coordinates.map(round2d) }];
|
|
3498
|
+
case "Polygon":
|
|
3499
|
+
return [{ type: "Polygon", coordinates: geometry.coordinates.map((ring) => ring.map(round2d)) }];
|
|
3500
|
+
case "MultiPoint":
|
|
3501
|
+
return geometry.coordinates.map((c) => ({ type: "Point", coordinates: round2d(c) }));
|
|
3502
|
+
case "MultiLineString":
|
|
3503
|
+
return geometry.coordinates.map((line) => ({ type: "LineString", coordinates: line.map(round2d) }));
|
|
3504
|
+
case "MultiPolygon":
|
|
3505
|
+
return geometry.coordinates.map((poly) => ({ type: "Polygon", coordinates: poly.map((ring) => ring.map(round2d)) }));
|
|
3506
|
+
case "GeometryCollection":
|
|
3507
|
+
return geometry.geometries.flatMap(singleParts);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
var modeOf = (geometry) => geometry.type === "Point" ? "point" : geometry.type === "LineString" ? "linestring" : "polygon";
|
|
3511
|
+
function extraProperties(properties) {
|
|
3512
|
+
const extra = {};
|
|
3513
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
3514
|
+
if (EDITOR_OWNED.has(key) || DROPPED.has(key) || value === void 0 || value === null) continue;
|
|
3515
|
+
extra[key] = value;
|
|
3516
|
+
}
|
|
3517
|
+
return extra;
|
|
3518
|
+
}
|
|
3519
|
+
function partTimes(feature, partCount) {
|
|
3520
|
+
const times = feature.properties?.coordinateProperties?.times;
|
|
3521
|
+
if (!Array.isArray(times) || times.length === 0) return [];
|
|
3522
|
+
if (feature.geometry?.type === "LineString" && times.every((t) => typeof t === "string")) {
|
|
3523
|
+
return [times];
|
|
3524
|
+
}
|
|
3525
|
+
if (feature.geometry?.type === "MultiLineString" && times.length === partCount && times.every(Array.isArray)) {
|
|
3526
|
+
return times;
|
|
3527
|
+
}
|
|
3528
|
+
return [];
|
|
3529
|
+
}
|
|
3530
|
+
function prepareForEditing(features, firstIndex = 0) {
|
|
3531
|
+
const session = { parts: [], sources: /* @__PURE__ */ new Map(), locked: /* @__PURE__ */ new Map() };
|
|
3532
|
+
features.forEach((feature, i) => {
|
|
3533
|
+
const f = firstIndex + i;
|
|
3534
|
+
if (!feature.geometry) return;
|
|
3535
|
+
const properties = feature.properties ?? {};
|
|
3536
|
+
const extra = extraProperties(properties);
|
|
3537
|
+
const geometries = singleParts(feature.geometry);
|
|
3538
|
+
const times = partTimes(feature, geometries.length);
|
|
3539
|
+
const opacityRaw = Number(properties.opacity);
|
|
3540
|
+
const opacity = Number.isFinite(opacityRaw) && opacityRaw >= 0 && opacityRaw <= 1 ? opacityRaw : void 0;
|
|
3541
|
+
geometries.forEach((geometry, index) => {
|
|
3542
|
+
const sourceKey = `f${f}p${index}`;
|
|
3543
|
+
const partTimesHere = times[index];
|
|
3544
|
+
const keepTimes = geometry.type === "LineString" && partTimesHere?.length === geometry.coordinates.length;
|
|
3545
|
+
session.sources.set(sourceKey, {
|
|
3546
|
+
group: `f${f}`,
|
|
3547
|
+
index,
|
|
3548
|
+
extra,
|
|
3549
|
+
...keepTimes ? { times: partTimesHere, coords: geometry.coordinates } : {}
|
|
3550
|
+
});
|
|
3551
|
+
const part = {
|
|
3552
|
+
sourceKey,
|
|
3553
|
+
mode: modeOf(geometry),
|
|
3554
|
+
geometry,
|
|
3555
|
+
name: typeof properties.name === "string" ? properties.name : void 0,
|
|
3556
|
+
color: typeof properties.color === "string" ? properties.color : void 0,
|
|
3557
|
+
...geometry.type === "Polygon" ? { opacity } : {}
|
|
3558
|
+
};
|
|
3559
|
+
if (geometry.type === "Polygon" && geometry.coordinates.length > 1) session.locked.set(sourceKey, part);
|
|
3560
|
+
else session.parts.push(part);
|
|
3561
|
+
});
|
|
3562
|
+
});
|
|
3563
|
+
return session;
|
|
3564
|
+
}
|
|
3565
|
+
function addToSession(session, features) {
|
|
3566
|
+
let next = 0;
|
|
3567
|
+
for (const source of session.sources.values()) next = Math.max(next, Number(source.group.slice(1)) + 1);
|
|
3568
|
+
const added = prepareForEditing(features, next);
|
|
3569
|
+
session.parts.push(...added.parts);
|
|
3570
|
+
for (const [key, source] of added.sources) session.sources.set(key, source);
|
|
3571
|
+
for (const [key, part] of added.locked) session.locked.set(key, part);
|
|
3572
|
+
return added;
|
|
3573
|
+
}
|
|
3574
|
+
function lockPart(session, sourceKey) {
|
|
3575
|
+
const index = session.parts.findIndex((part) => part.sourceKey === sourceKey);
|
|
3576
|
+
if (index < 0) return;
|
|
3577
|
+
session.locked.set(sourceKey, session.parts[index]);
|
|
3578
|
+
session.parts.splice(index, 1);
|
|
3579
|
+
}
|
|
3580
|
+
var samePositions = (a, b) => a.length === b.length && a.every((p, i) => p[0] === b[i][0] && p[1] === b[i][1]);
|
|
3581
|
+
function joinGeometries(geometries) {
|
|
3582
|
+
if (geometries.length === 1) return geometries[0];
|
|
3583
|
+
const kinds = new Set(geometries.map((g) => g.type));
|
|
3584
|
+
if (kinds.size === 1) {
|
|
3585
|
+
if (kinds.has("Point")) return { type: "MultiPoint", coordinates: geometries.map((g) => g.coordinates) };
|
|
3586
|
+
if (kinds.has("LineString")) return { type: "MultiLineString", coordinates: geometries.map((g) => g.coordinates) };
|
|
3587
|
+
return { type: "MultiPolygon", coordinates: geometries.map((g) => g.coordinates) };
|
|
3588
|
+
}
|
|
3589
|
+
return { type: "GeometryCollection", geometries };
|
|
3590
|
+
}
|
|
3591
|
+
function assembleForSaving(session, saved) {
|
|
3592
|
+
const entries = [
|
|
3593
|
+
...saved.map((part) => ({ part, source: part.sourceKey ? session.sources.get(part.sourceKey) : void 0 })),
|
|
3594
|
+
...[...session.locked.values()].map((part) => ({ part, source: session.sources.get(part.sourceKey) }))
|
|
3595
|
+
];
|
|
3596
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
3597
|
+
let newCount = 0;
|
|
3598
|
+
for (const entry of entries) {
|
|
3599
|
+
const { part, source } = entry;
|
|
3600
|
+
const key = source ? `${source.group}|${part.name ?? ""}|${part.color ?? ""}|${part.opacity ?? ""}` : `new${newCount++}`;
|
|
3601
|
+
const bucket = buckets.get(key);
|
|
3602
|
+
if (bucket) bucket.push(entry);
|
|
3603
|
+
else buckets.set(key, [entry]);
|
|
3604
|
+
}
|
|
3605
|
+
const groupOrder = (key) => key.startsWith("new") ? Infinity : Number(key.slice(1, key.indexOf("|")));
|
|
3606
|
+
return [...buckets.entries()].sort(([a], [b]) => groupOrder(a) - groupOrder(b)).map(([, bucket]) => {
|
|
3607
|
+
bucket.sort((a, b) => (a.source?.index ?? 0) - (b.source?.index ?? 0));
|
|
3608
|
+
const first = bucket[0];
|
|
3609
|
+
const geometry = joinGeometries(bucket.map((e) => e.part.geometry));
|
|
3610
|
+
const times = bucket.map(({ part, source }) => part.geometry.type === "LineString" && source?.times && source.coords && samePositions(part.geometry.coordinates, source.coords) ? source.times : void 0);
|
|
3611
|
+
const allTimed = bucket.every((e) => e.part.geometry.type === "LineString") && times.every(Boolean);
|
|
3612
|
+
return {
|
|
3613
|
+
type: "Feature",
|
|
3614
|
+
geometry,
|
|
3615
|
+
properties: {
|
|
3616
|
+
...first.source?.extra ?? {},
|
|
3617
|
+
...first.part.color !== void 0 ? { color: first.part.color } : {},
|
|
3618
|
+
...first.part.name ? { name: first.part.name } : {},
|
|
3619
|
+
...first.part.opacity !== void 0 ? { opacity: first.part.opacity } : {},
|
|
3620
|
+
...allTimed ? { coordinateProperties: { times: bucket.length === 1 ? times[0] : times } } : {}
|
|
3621
|
+
}
|
|
3622
|
+
};
|
|
3623
|
+
});
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
// src/mapEdit/mapFile.ts
|
|
3627
|
+
import { gpx, kml } from "@tmcw/togeojson";
|
|
3628
|
+
var MEDIA_HANDLE_PREFIX = "media-handle:";
|
|
3629
|
+
function applySimplestyle(feature) {
|
|
3630
|
+
const props = feature.properties ?? {};
|
|
3631
|
+
const isRegion = feature.geometry?.type === "Polygon" || feature.geometry?.type === "MultiPolygon";
|
|
3632
|
+
const color = props.color ?? (isRegion ? props.fill ?? props.stroke : props.stroke ?? props["marker-color"]);
|
|
3633
|
+
const opacity = props.opacity ?? (isRegion ? props["fill-opacity"] : void 0);
|
|
3634
|
+
if (color === props.color && opacity === props.opacity) return feature;
|
|
3635
|
+
return { ...feature, properties: { ...props, color, opacity } };
|
|
3636
|
+
}
|
|
3637
|
+
function geoJsonFeatures(doc) {
|
|
3638
|
+
if (doc.type === "FeatureCollection") return doc.features ?? [];
|
|
3639
|
+
if (doc.type === "Feature") return [doc];
|
|
3640
|
+
return [{ type: "Feature", geometry: doc, properties: {} }];
|
|
3641
|
+
}
|
|
3642
|
+
function asGroundOverlay(feature) {
|
|
3643
|
+
const props = feature.properties ?? {};
|
|
3644
|
+
if (props[IMAGE_OVERLAY_PROPERTY] !== true || typeof props.image !== "string") return feature;
|
|
3645
|
+
const { [IMAGE_OVERLAY_PROPERTY]: _marker, image, ...rest } = props;
|
|
3646
|
+
return {
|
|
3647
|
+
...feature,
|
|
3648
|
+
properties: { ...rest, "@geometry-type": "groundoverlay", icon: `${MEDIA_HANDLE_PREFIX}${image}` }
|
|
3649
|
+
};
|
|
3650
|
+
}
|
|
3651
|
+
function parseMapFile(text) {
|
|
3652
|
+
const hasGeometry = (f) => f.geometry != null;
|
|
3653
|
+
if (text.trimStart().startsWith("{")) {
|
|
3654
|
+
return geoJsonFeatures(JSON.parse(text)).filter(hasGeometry).map(asGroundOverlay).map(applySimplestyle);
|
|
3655
|
+
}
|
|
3656
|
+
const dom = new DOMParser().parseFromString(text, "text/xml");
|
|
3657
|
+
if (dom.documentElement.localName === "gpx") {
|
|
3658
|
+
return gpx(dom).features.filter(hasGeometry).map(applySimplestyle);
|
|
3659
|
+
}
|
|
3660
|
+
return kml(dom).features.filter(hasGeometry);
|
|
3661
|
+
}
|
|
3662
|
+
var IMAGE_OVERLAY_PROPERTY = "gramps:image-overlay";
|
|
3663
|
+
|
|
3664
|
+
// src/mapEdit/geometry.ts
|
|
3665
|
+
var DEG_TO_RAD = Math.PI / 180;
|
|
3666
|
+
var RAD_TO_DEG = 180 / Math.PI;
|
|
3667
|
+
function mercatorFromLngLat([lng, lat]) {
|
|
3668
|
+
const x = (180 + lng) / 360;
|
|
3669
|
+
const y = (180 - RAD_TO_DEG * Math.log(Math.tan(Math.PI / 4 + lat * DEG_TO_RAD / 2))) / 360;
|
|
3670
|
+
return [x, y];
|
|
3671
|
+
}
|
|
3672
|
+
function lngLatFromMercator([x, y]) {
|
|
3673
|
+
const lng = x * 360 - 180;
|
|
3674
|
+
const lat = 90 - 2 * Math.atan(Math.exp((y * 360 - 180) * DEG_TO_RAD)) * RAD_TO_DEG;
|
|
3675
|
+
return [lng, lat];
|
|
3676
|
+
}
|
|
3677
|
+
function rotatePoint(point, center, rotationDeg) {
|
|
3678
|
+
const angle = rotationDeg * DEG_TO_RAD;
|
|
3679
|
+
const cos = Math.cos(angle);
|
|
3680
|
+
const sin = Math.sin(angle);
|
|
3681
|
+
const [px, py] = mercatorFromLngLat(point);
|
|
3682
|
+
const [cx, cy] = mercatorFromLngLat(center);
|
|
3683
|
+
const dx = px - cx;
|
|
3684
|
+
const dy = -(py - cy);
|
|
3685
|
+
const rx = dx * cos - dy * sin;
|
|
3686
|
+
const ry = dx * sin + dy * cos;
|
|
3687
|
+
return lngLatFromMercator([cx + rx, cy - ry]);
|
|
3688
|
+
}
|
|
3689
|
+
function scalePoint(point, center, scale) {
|
|
3690
|
+
const [px, py] = mercatorFromLngLat(point);
|
|
3691
|
+
const [cx, cy] = mercatorFromLngLat(center);
|
|
3692
|
+
return lngLatFromMercator([cx + (px - cx) * scale, cy + (py - cy) * scale]);
|
|
3693
|
+
}
|
|
3694
|
+
function mercatorDistance(a, b) {
|
|
3695
|
+
const [ax, ay] = mercatorFromLngLat(a);
|
|
3696
|
+
const [bx, by] = mercatorFromLngLat(b);
|
|
3697
|
+
return Math.hypot(bx - ax, by - ay);
|
|
3698
|
+
}
|
|
3699
|
+
function mercatorBearing(from, to) {
|
|
3700
|
+
const [fx, fy] = mercatorFromLngLat(from);
|
|
3701
|
+
const [tx, ty] = mercatorFromLngLat(to);
|
|
3702
|
+
return Math.atan2(-(ty - fy), tx - fx) * RAD_TO_DEG;
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
// src/editor/MapEditor.tsx
|
|
3706
|
+
import { forwardRef, useEffect as useEffect8, useImperativeHandle, useRef as useRef8, useState as useState7 } from "react";
|
|
3707
|
+
import * as maplibregl3 from "maplibre-gl";
|
|
3708
|
+
import {
|
|
3709
|
+
TerraDraw,
|
|
3710
|
+
TerraDrawLineStringMode,
|
|
3711
|
+
TerraDrawPointMode,
|
|
3712
|
+
TerraDrawPolygonMode,
|
|
3713
|
+
TerraDrawRectangleMode,
|
|
3714
|
+
TerraDrawSelectMode
|
|
3715
|
+
} from "terra-draw";
|
|
3716
|
+
import { TerraDrawMapLibreGLAdapter } from "terra-draw-maplibre-gl-adapter";
|
|
3717
|
+
import {
|
|
3718
|
+
Alert,
|
|
3719
|
+
Box as Box2,
|
|
3720
|
+
Button as Button3,
|
|
3721
|
+
ColorInput,
|
|
3722
|
+
Divider,
|
|
3723
|
+
Group as Group4,
|
|
3724
|
+
Kbd,
|
|
3725
|
+
List,
|
|
3726
|
+
Loader as Loader3,
|
|
3727
|
+
NumberInput,
|
|
3728
|
+
SegmentedControl,
|
|
3729
|
+
Slider,
|
|
3730
|
+
Stack as Stack4,
|
|
3731
|
+
Text as Text4,
|
|
3732
|
+
TextInput as TextInput4
|
|
3733
|
+
} from "@mantine/core";
|
|
3734
|
+
|
|
3735
|
+
// src/editor/mapEditorStrings.ts
|
|
3736
|
+
var MAP_EDITOR_ENGLISH = {
|
|
3737
|
+
point: "Point",
|
|
3738
|
+
line: "Line",
|
|
3739
|
+
polygon: "Polygon",
|
|
3740
|
+
rectangle: "Rectangle",
|
|
3741
|
+
select: "Select",
|
|
3742
|
+
image: "Image",
|
|
3743
|
+
editOptions: "Edit Options",
|
|
3744
|
+
labelText: "Label text",
|
|
3745
|
+
regionName: "Region name (optional)",
|
|
3746
|
+
fillOpacity: "Fill opacity",
|
|
3747
|
+
scale: "Scale",
|
|
3748
|
+
transform: "Transform",
|
|
3749
|
+
overlayName: "Overlay name (optional)",
|
|
3750
|
+
opacity: "Opacity",
|
|
3751
|
+
stackingOrder: "Stacking order",
|
|
3752
|
+
removeImage: "Remove this image",
|
|
3753
|
+
resizeImage: "Drag to resize (keeps its exact shape, warped or not)",
|
|
3754
|
+
rotateImage: "Drag to rotate",
|
|
3755
|
+
warpCorner: "Drag to warp this corner",
|
|
3756
|
+
basemapFailed: "The basemap could not be loaded.",
|
|
3757
|
+
imageFailed: "The image failed to load.",
|
|
3758
|
+
lockedOne: "1 shape can't be edited here",
|
|
3759
|
+
lockedMany: (count) => `${count} shapes can't be edited here`,
|
|
3760
|
+
lockedExplain: "For example, a region with a hole in it, or one whose outline crosses itself. It's drawn dashed, and saved exactly as it was.",
|
|
3761
|
+
helpTitle: "How to use this editor",
|
|
3762
|
+
helpDrawing: "Drawing",
|
|
3763
|
+
helpPoint: "Point: click once to place it.",
|
|
3764
|
+
helpLine: "Line, Polygon: click to place each point, then click that first point's own marker again to finish -- it's styled differently once there's enough points to close the shape. Pressing",
|
|
3765
|
+
helpEnterFinishes: "finishes it too;",
|
|
3766
|
+
helpEscCancels: "cancels it.",
|
|
3767
|
+
helpRectangle: "Rectangle: click and drag from one corner to the opposite corner.",
|
|
3768
|
+
helpImage: "Image: pick a media object to overlay -- see",
|
|
3769
|
+
helpImages: "Images",
|
|
3770
|
+
helpBelow: "below.",
|
|
3771
|
+
helpEditing: "Editing a shape",
|
|
3772
|
+
helpSelect: "Select: click a shape to select it, then drag it or one of its handles.",
|
|
3773
|
+
helpVertex: "Drag a vertex to move it. To add a new point on a Line, Polygon, or Rectangle, drag the small midpoint marker between two existing vertices outward.",
|
|
3774
|
+
helpColor: "The color swatch sets the next shape's color -- or, with a shape selected, changes its color.",
|
|
3775
|
+
helpDelete: "Delete a selected shape or image with",
|
|
3776
|
+
helpOr: "or",
|
|
3777
|
+
helpRotate: "Rotate a selected Polygon, Rectangle, or Line: hold",
|
|
3778
|
+
helpRotateHandle: "while dragging one of its handles.",
|
|
3779
|
+
helpImageMove: "Drag anywhere on an image to move it.",
|
|
3780
|
+
helpImageResize: "Drag its \u2198 handle to resize it -- grows or shrinks from its own center, keeping its exact shape (works the same on a warped image, too).",
|
|
3781
|
+
helpImageRotate: "Drag its \u21BB handle to rotate it -- handy for aligning a scanned old map.",
|
|
3782
|
+
helpImageTransform: `Switch to "Transform" to also show 4 corner handles you can drag independently (rubber-sheeting) -- for a scanned map that isn't printed at a uniform scale/orientation. Switching back to "Scale" just hides those 4 handles again; move/resize/rotate keep working on the image's exact current shape either way.`,
|
|
3783
|
+
helpImageRemove: "Click its \xD7 handle to remove it."
|
|
3784
|
+
};
|
|
3785
|
+
|
|
3786
|
+
// src/editor/MapEditor.tsx
|
|
3787
|
+
import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3788
|
+
var TOOLBAR = [
|
|
3789
|
+
{ mode: "point", label: "point" },
|
|
3790
|
+
{ mode: "linestring", label: "line" },
|
|
3791
|
+
{ mode: "polygon", label: "polygon" },
|
|
3792
|
+
{ mode: "rectangle", label: "rectangle" },
|
|
3793
|
+
{ mode: "select", label: "select" }
|
|
3794
|
+
];
|
|
3795
|
+
var COLOR_SWATCHES = [
|
|
3796
|
+
"#2a78d6",
|
|
3797
|
+
"#d64545",
|
|
3798
|
+
"#3aa657",
|
|
3799
|
+
"#e0a325",
|
|
3800
|
+
"#8654c9",
|
|
3801
|
+
"#2aa7a0",
|
|
3802
|
+
"#e0678a",
|
|
3803
|
+
"#555555"
|
|
3804
|
+
];
|
|
3805
|
+
var EMPTY_LABEL_PREVIEW = { type: "FeatureCollection", features: [] };
|
|
3806
|
+
var OVERLAY_ANCHOR_LAYER = "image-overlay-anchor";
|
|
3807
|
+
var TERRA_DRAW_LAYERS = ["td-polygon", "td-polygon-outline", "td-linestring", "td-point", "td-point-marker"];
|
|
3808
|
+
var LOCKED_SOURCE = "locked-shapes";
|
|
3809
|
+
var LOCKED_FILL_LAYER = "locked-shapes-fill";
|
|
3810
|
+
var LOCKED_LINE_LAYER = "locked-shapes-line";
|
|
3811
|
+
var LABEL_PREVIEW_SOURCE = "label-preview";
|
|
3812
|
+
var LABEL_PREVIEW_LAYER = "label-preview-text";
|
|
3813
|
+
var TOOLBAR_CLASS = "gc-map-editor-toolbar";
|
|
3814
|
+
var TOOLBAR_CSS = `.${TOOLBAR_CLASS} { pointer-events: none; } .${TOOLBAR_CLASS} > * { pointer-events: auto; }`;
|
|
3815
|
+
function overlayCenter(corners) {
|
|
3816
|
+
let lng = 0, lat = 0;
|
|
3817
|
+
for (const [x, y] of corners) {
|
|
3818
|
+
lng += x;
|
|
3819
|
+
lat += y;
|
|
3820
|
+
}
|
|
3821
|
+
return [lng / 4, lat / 4];
|
|
3822
|
+
}
|
|
3823
|
+
function rotateHandleAnchor(corners, center) {
|
|
3824
|
+
const [topLeft, topRight] = corners;
|
|
3825
|
+
const topMid = [(topLeft[0] + topRight[0]) / 2, (topLeft[1] + topRight[1]) / 2];
|
|
3826
|
+
return [center[0] + (topMid[0] - center[0]) * 1.5, center[1] + (topMid[1] - center[1]) * 1.5];
|
|
3827
|
+
}
|
|
3828
|
+
function pointInQuad(point, corners) {
|
|
3829
|
+
let sign = 0;
|
|
3830
|
+
for (let i = 0; i < corners.length; i++) {
|
|
3831
|
+
const a = corners[i];
|
|
3832
|
+
const b = corners[(i + 1) % corners.length];
|
|
3833
|
+
const cross = (b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x);
|
|
3834
|
+
if (cross === 0) continue;
|
|
3835
|
+
const s = cross > 0 ? 1 : -1;
|
|
3836
|
+
if (sign === 0) sign = s;
|
|
3837
|
+
else if (s !== sign) return false;
|
|
3838
|
+
}
|
|
3839
|
+
return true;
|
|
3840
|
+
}
|
|
3841
|
+
var OVERLAY_DRAG_OPACITY = 0.5;
|
|
3842
|
+
var OVERLAY_HANDLE_STYLE = {
|
|
3843
|
+
width: "20px",
|
|
3844
|
+
height: "20px",
|
|
3845
|
+
borderRadius: "50%",
|
|
3846
|
+
background: "white",
|
|
3847
|
+
border: "2px solid #333",
|
|
3848
|
+
display: "flex",
|
|
3849
|
+
alignItems: "center",
|
|
3850
|
+
justifyContent: "center",
|
|
3851
|
+
fontSize: "12px",
|
|
3852
|
+
fontWeight: "700",
|
|
3853
|
+
color: "#333",
|
|
3854
|
+
cursor: "pointer",
|
|
3855
|
+
boxShadow: "0 1px 3px rgba(0,0,0,0.4)",
|
|
3856
|
+
userSelect: "none",
|
|
3857
|
+
zIndex: 4
|
|
3858
|
+
};
|
|
3859
|
+
function loadImageElement(src, failed) {
|
|
3860
|
+
return new Promise((resolve, reject) => {
|
|
3861
|
+
const img = new Image();
|
|
3862
|
+
img.onload = () => resolve(img);
|
|
3863
|
+
img.onerror = () => reject(new Error(failed));
|
|
3864
|
+
img.src = src;
|
|
3865
|
+
});
|
|
3866
|
+
}
|
|
3867
|
+
function extendBounds(bounds, geometry) {
|
|
3868
|
+
if (geometry.type === "Point") {
|
|
3869
|
+
bounds.extend(geometry.coordinates);
|
|
3870
|
+
} else if (geometry.type === "LineString") {
|
|
3871
|
+
for (const c of geometry.coordinates) bounds.extend(c);
|
|
3872
|
+
} else if (geometry.type === "Polygon") {
|
|
3873
|
+
for (const ring of geometry.coordinates) for (const c of ring) bounds.extend(c);
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
var MapEditor = forwardRef(function MapEditor2({
|
|
3877
|
+
initial,
|
|
3878
|
+
initialView,
|
|
3879
|
+
resolveImage,
|
|
3880
|
+
releaseImage,
|
|
3881
|
+
onAddImage,
|
|
3882
|
+
onStatus,
|
|
3883
|
+
onError,
|
|
3884
|
+
dark = false,
|
|
3885
|
+
defaultColor,
|
|
3886
|
+
labelColors,
|
|
3887
|
+
toolbarTop = 12,
|
|
3888
|
+
overlayNames = true,
|
|
3889
|
+
renderOverlayOptions,
|
|
3890
|
+
strings: stringOverrides
|
|
3891
|
+
}, ref) {
|
|
3892
|
+
const strings = { ...MAP_EDITOR_ENGLISH, ...stringOverrides };
|
|
3893
|
+
const live = useRef8({ strings, resolveImage, releaseImage, onError });
|
|
3894
|
+
live.current = { strings, resolveImage, releaseImage, onError };
|
|
3895
|
+
const fail = (message) => live.current.onError?.(message);
|
|
3896
|
+
const release = (url) => live.current.releaseImage?.(url);
|
|
3897
|
+
const [containerEl, setContainerEl] = useState7(null);
|
|
3898
|
+
const mapRef = useRef8(null);
|
|
3899
|
+
const drawRef = useRef8(null);
|
|
3900
|
+
const overlayMountsRef = useRef8(/* @__PURE__ */ new Map());
|
|
3901
|
+
const [mode, setMode] = useState7("select");
|
|
3902
|
+
const modeRef = useRef8(mode);
|
|
3903
|
+
modeRef.current = mode;
|
|
3904
|
+
const [ready, setReady] = useState7(false);
|
|
3905
|
+
const [color, setColor] = useState7(() => defaultColor ?? defaultColors(dark).mark);
|
|
3906
|
+
const colorRef = useRef8(color);
|
|
3907
|
+
colorRef.current = color;
|
|
3908
|
+
const [opacity, setOpacity] = useState7(0.25);
|
|
3909
|
+
const opacityRef = useRef8(opacity);
|
|
3910
|
+
opacityRef.current = opacity;
|
|
3911
|
+
const [labelText, setLabelText] = useState7("");
|
|
3912
|
+
const refreshLabelPreviewRef = useRef8(null);
|
|
3913
|
+
const [selectedId, setSelectedId] = useState7(null);
|
|
3914
|
+
const selectedIdRef = useRef8(selectedId);
|
|
3915
|
+
selectedIdRef.current = selectedId;
|
|
3916
|
+
const labelColorsRef = useRef8(labelColors ?? defaultColors(dark));
|
|
3917
|
+
labelColorsRef.current = labelColors ?? defaultColors(dark);
|
|
3918
|
+
useEffect8(() => {
|
|
3919
|
+
const draw = drawRef.current;
|
|
3920
|
+
const map = mapRef.current;
|
|
3921
|
+
const source = map?.getSource(LABEL_PREVIEW_SOURCE);
|
|
3922
|
+
const refresh = () => {
|
|
3923
|
+
const currentDraw = drawRef.current;
|
|
3924
|
+
const currentSource = mapRef.current?.getSource(LABEL_PREVIEW_SOURCE);
|
|
3925
|
+
if (!currentDraw || !currentSource) return;
|
|
3926
|
+
const features = [];
|
|
3927
|
+
for (const f of currentDraw.getSnapshot()) {
|
|
3928
|
+
if (f.geometry?.type !== "Point") continue;
|
|
3929
|
+
const isSelected = String(f.id) === selectedId;
|
|
3930
|
+
const name = (isSelected ? labelText : f.properties?.name)?.trim();
|
|
3931
|
+
if (!name) continue;
|
|
3932
|
+
features.push({
|
|
3933
|
+
type: "Feature",
|
|
3934
|
+
geometry: f.geometry,
|
|
3935
|
+
properties: { name, color: f.properties?.color }
|
|
3936
|
+
});
|
|
3937
|
+
}
|
|
3938
|
+
currentSource.setData({ type: "FeatureCollection", features });
|
|
3939
|
+
};
|
|
3940
|
+
refreshLabelPreviewRef.current = refresh;
|
|
3941
|
+
if (draw && source) refresh();
|
|
3942
|
+
}, [labelText, selectedId]);
|
|
3943
|
+
const [overlays, setOverlays] = useState7([]);
|
|
3944
|
+
const overlaysRef = useRef8(overlays);
|
|
3945
|
+
overlaysRef.current = overlays;
|
|
3946
|
+
const [selectedOverlayId, setSelectedOverlayId] = useState7(null);
|
|
3947
|
+
const [warpHandleIds, setWarpHandleIds] = useState7(/* @__PURE__ */ new Set());
|
|
3948
|
+
const [hasFeatures, setHasFeatures] = useState7(false);
|
|
3949
|
+
const sessionRef = useRef8(null);
|
|
3950
|
+
const [lockedCount, setLockedCount] = useState7(0);
|
|
3951
|
+
const [lockedNoticeDismissed, setLockedNoticeDismissed] = useState7(false);
|
|
3952
|
+
const hasDrawn = hasFeatures || overlays.length > 0;
|
|
3953
|
+
const hasContent = hasDrawn || lockedCount > 0;
|
|
3954
|
+
const onStatusRef = useRef8(onStatus);
|
|
3955
|
+
onStatusRef.current = onStatus;
|
|
3956
|
+
useEffect8(() => {
|
|
3957
|
+
onStatusRef.current?.({ hasDrawn, hasContent });
|
|
3958
|
+
}, [hasDrawn, hasContent]);
|
|
3959
|
+
useEffect8(() => {
|
|
3960
|
+
if (!containerEl) return;
|
|
3961
|
+
const map = new maplibregl3.Map({
|
|
3962
|
+
container: containerEl,
|
|
3963
|
+
style: mapStyleUrl(dark, null),
|
|
3964
|
+
center: [0, 20],
|
|
3965
|
+
zoom: 1.5,
|
|
3966
|
+
attributionControl: { compact: true }
|
|
3967
|
+
});
|
|
3968
|
+
mapRef.current = map;
|
|
3969
|
+
map.addControl(new maplibregl3.NavigationControl({ showCompass: false }), "top-right");
|
|
3970
|
+
map.dragRotate.disable();
|
|
3971
|
+
map.touchZoomRotate.disableRotation();
|
|
3972
|
+
map.keyboard.disableRotation();
|
|
3973
|
+
const readColor = (feature) => feature.properties.color ?? colorRef.current;
|
|
3974
|
+
const readOpacity = (feature) => feature.properties.opacity ?? opacityRef.current;
|
|
3975
|
+
const draw = new TerraDraw({
|
|
3976
|
+
adapter: new TerraDrawMapLibreGLAdapter({ map }),
|
|
3977
|
+
modes: [
|
|
3978
|
+
new TerraDrawPointMode({ styles: { pointColor: readColor } }),
|
|
3979
|
+
new TerraDrawLineStringMode({ styles: { lineStringColor: readColor } }),
|
|
3980
|
+
new TerraDrawPolygonMode({ styles: { fillColor: readColor, fillOpacity: readOpacity, outlineColor: readColor } }),
|
|
3981
|
+
new TerraDrawRectangleMode({ styles: { fillColor: readColor, fillOpacity: readOpacity, outlineColor: readColor } }),
|
|
3982
|
+
// Dragging (both a whole feature and its individual vertices) and
|
|
3983
|
+
// deletion (terra-draw's own default keybinding, Delete/Backspace
|
|
3984
|
+
// on the current selection) are what this editor needs from
|
|
3985
|
+
// Select for every mode; `rotateable` additionally turns on
|
|
3986
|
+
// terra-draw's own built-in rotate gesture (hold R while dragging
|
|
3987
|
+
// a selected shape's vertex) for anything 2D enough for "rotate"
|
|
3988
|
+
// to mean something -- not `point`, a single vertex has nothing to
|
|
3989
|
+
// rotate around.
|
|
3990
|
+
new TerraDrawSelectMode({
|
|
3991
|
+
// While a feature is selected, terra-draw renders it through
|
|
3992
|
+
// *this* mode's own styling, not the drawing mode that created it
|
|
3993
|
+
// (see TerraDraw's own per-feature style dispatch: only the
|
|
3994
|
+
// selected feature goes through TerraDrawSelectMode.styleFeature;
|
|
3995
|
+
// every other feature keeps using its own mode's, which is why an
|
|
3996
|
+
// unselected shape's color/opacity already reflects its own
|
|
3997
|
+
// properties fine). Without these, the selected-state styling
|
|
3998
|
+
// falls back to terra-draw's fixed defaults regardless of
|
|
3999
|
+
// `feature.properties` -- so dragging the opacity slider (or
|
|
4000
|
+
// recoloring) while a shape is actively selected had no visible
|
|
4001
|
+
// effect until it was deselected. Found live.
|
|
4002
|
+
styles: {
|
|
4003
|
+
selectedPointColor: readColor,
|
|
4004
|
+
selectedLineStringColor: readColor,
|
|
4005
|
+
selectedPolygonColor: readColor,
|
|
4006
|
+
selectedPolygonOutlineColor: readColor,
|
|
4007
|
+
selectedPolygonFillOpacity: readOpacity
|
|
4008
|
+
},
|
|
4009
|
+
// Half terra-draw's own default (40px) -- with labels sitting
|
|
4010
|
+
// right next to their point (see LABEL_PREVIEW_LAYER's
|
|
4011
|
+
// text-offset below) and vertices sometimes placed close
|
|
4012
|
+
// together, the default hit-test radius made clicking one point
|
|
4013
|
+
// too likely to grab a neighboring one instead. Found live.
|
|
4014
|
+
pointerDistance: 20,
|
|
4015
|
+
// Every field restated (terra-draw's own type doesn't allow a
|
|
4016
|
+
// partial override), only `rotate` actually changed from its
|
|
4017
|
+
// default -- plain "r" instead of the default Ctrl+R, an
|
|
4018
|
+
// awkward two-handed chord to hold through an entire drag. Bare
|
|
4019
|
+
// "r" is safe here the same way Backspace-delete is (see the
|
|
4020
|
+
// canvas keydown listener below): terra-draw's own keydown
|
|
4021
|
+
// listener is bound to the map canvas specifically, so it never
|
|
4022
|
+
// fires while typing "r" into a text field.
|
|
4023
|
+
keyEvents: { deselect: "Escape", delete: "Delete", rotate: ["r"], scale: ["Control", "s"] },
|
|
4024
|
+
flags: {
|
|
4025
|
+
point: { feature: { draggable: true } },
|
|
4026
|
+
linestring: {
|
|
4027
|
+
feature: {
|
|
4028
|
+
draggable: true,
|
|
4029
|
+
rotateable: true,
|
|
4030
|
+
coordinates: { draggable: true, deletable: true, midpoints: true }
|
|
4031
|
+
}
|
|
4032
|
+
},
|
|
4033
|
+
polygon: {
|
|
4034
|
+
feature: {
|
|
4035
|
+
draggable: true,
|
|
4036
|
+
rotateable: true,
|
|
4037
|
+
coordinates: { draggable: true, deletable: true, midpoints: true }
|
|
4038
|
+
}
|
|
4039
|
+
},
|
|
4040
|
+
rectangle: {
|
|
4041
|
+
feature: {
|
|
4042
|
+
draggable: true,
|
|
4043
|
+
rotateable: true,
|
|
4044
|
+
coordinates: { draggable: true, deletable: true, midpoints: true }
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
})
|
|
4049
|
+
]
|
|
4050
|
+
});
|
|
4051
|
+
drawRef.current = draw;
|
|
4052
|
+
draw.on("change", () => {
|
|
4053
|
+
setHasFeatures(
|
|
4054
|
+
draw.getSnapshot().some((f) => f.geometry != null && !f.properties?.selectionPoint && !f.properties?.midPoint)
|
|
4055
|
+
);
|
|
4056
|
+
refreshLabelPreviewRef.current?.();
|
|
4057
|
+
});
|
|
4058
|
+
function syncSelection(id) {
|
|
4059
|
+
setSelectedId(id);
|
|
4060
|
+
const feature = id ? draw.getSnapshot().find((f) => String(f.id) === id) : void 0;
|
|
4061
|
+
setLabelText(feature?.properties?.name ?? "");
|
|
4062
|
+
if (feature) {
|
|
4063
|
+
setColor(feature.properties?.color ?? colorRef.current);
|
|
4064
|
+
setOpacity(feature.properties?.opacity ?? 0.25);
|
|
4065
|
+
setSelectedOverlayId(null);
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
draw.on("finish", (id) => {
|
|
4069
|
+
const isRegion = modeRef.current === "polygon" || modeRef.current === "rectangle";
|
|
4070
|
+
draw.updateFeatureProperties(
|
|
4071
|
+
id,
|
|
4072
|
+
isRegion ? { color: colorRef.current, opacity: opacityRef.current } : { color: colorRef.current }
|
|
4073
|
+
);
|
|
4074
|
+
draw.setMode("select");
|
|
4075
|
+
setMode("select");
|
|
4076
|
+
draw.selectFeature(id);
|
|
4077
|
+
syncSelection(String(id));
|
|
4078
|
+
});
|
|
4079
|
+
draw.on("select", (id) => syncSelection(String(id)));
|
|
4080
|
+
draw.on("deselect", () => syncSelection(null));
|
|
4081
|
+
const canvas = map.getCanvas();
|
|
4082
|
+
const onCanvasKeyDown = (e) => {
|
|
4083
|
+
if (e.key !== "Backspace" || !selectedIdRef.current) return;
|
|
4084
|
+
try {
|
|
4085
|
+
draw.removeFeatures([selectedIdRef.current]);
|
|
4086
|
+
} catch {
|
|
4087
|
+
}
|
|
4088
|
+
setSelectedId(null);
|
|
4089
|
+
};
|
|
4090
|
+
canvas.addEventListener("keydown", onCanvasKeyDown);
|
|
4091
|
+
let sawNativePointerdown = false;
|
|
4092
|
+
let pointerBridgeActive = false;
|
|
4093
|
+
let pendingFirstMouseDown = null;
|
|
4094
|
+
const onNativePointerDown = () => {
|
|
4095
|
+
sawNativePointerdown = true;
|
|
4096
|
+
};
|
|
4097
|
+
canvas.addEventListener("pointerdown", onNativePointerDown, { once: true });
|
|
4098
|
+
const dispatchAsPointerEvent = (type, e) => {
|
|
4099
|
+
const synthetic = new MouseEvent(type, {
|
|
4100
|
+
bubbles: true,
|
|
4101
|
+
cancelable: true,
|
|
4102
|
+
view: window,
|
|
4103
|
+
clientX: e.clientX,
|
|
4104
|
+
clientY: e.clientY,
|
|
4105
|
+
button: e.button,
|
|
4106
|
+
buttons: e.buttons
|
|
4107
|
+
});
|
|
4108
|
+
Object.defineProperty(synthetic, "isPrimary", { value: true });
|
|
4109
|
+
canvas.dispatchEvent(synthetic);
|
|
4110
|
+
};
|
|
4111
|
+
const onMouseDownBridge = (e) => {
|
|
4112
|
+
if (pointerBridgeActive) {
|
|
4113
|
+
dispatchAsPointerEvent("pointerdown", e);
|
|
4114
|
+
return;
|
|
4115
|
+
}
|
|
4116
|
+
if (sawNativePointerdown) return;
|
|
4117
|
+
pendingFirstMouseDown = e;
|
|
4118
|
+
requestAnimationFrame(() => {
|
|
4119
|
+
if (sawNativePointerdown || !pendingFirstMouseDown) return;
|
|
4120
|
+
pointerBridgeActive = true;
|
|
4121
|
+
dispatchAsPointerEvent("pointerdown", pendingFirstMouseDown);
|
|
4122
|
+
pendingFirstMouseDown = null;
|
|
4123
|
+
});
|
|
4124
|
+
};
|
|
4125
|
+
const onMouseMoveBridge = (e) => {
|
|
4126
|
+
if (pointerBridgeActive) dispatchAsPointerEvent("pointermove", e);
|
|
4127
|
+
};
|
|
4128
|
+
const onMouseUpBridge = (e) => {
|
|
4129
|
+
if (pointerBridgeActive) dispatchAsPointerEvent("pointerup", e);
|
|
4130
|
+
};
|
|
4131
|
+
canvas.addEventListener("mousedown", onMouseDownBridge);
|
|
4132
|
+
canvas.addEventListener("mousemove", onMouseMoveBridge);
|
|
4133
|
+
canvas.addEventListener("mouseup", onMouseUpBridge);
|
|
4134
|
+
map.on("mousedown", (e) => {
|
|
4135
|
+
const target = e.originalEvent.target;
|
|
4136
|
+
if (target instanceof Node) {
|
|
4137
|
+
for (const mount of overlayMountsRef.current.values()) {
|
|
4138
|
+
if (mount.deleteMarker.getElement().contains(target) || mount.resizeMarker.getElement().contains(target) || mount.rotateMarker.getElement().contains(target) || mount.cornerMarkers.some((marker) => marker.getElement().contains(target))) return;
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
if (map.queryRenderedFeatures(e.point, { layers: TERRA_DRAW_LAYERS }).length > 0) return;
|
|
4142
|
+
const mounts = [...overlayMountsRef.current.entries()].reverse();
|
|
4143
|
+
for (const [id, mount] of mounts) {
|
|
4144
|
+
const corners = mount.box.corners.map((c) => map.project(c));
|
|
4145
|
+
if (pointInQuad(e.point, corners)) {
|
|
4146
|
+
if (selectedIdRef.current) draw.deselectFeature(selectedIdRef.current);
|
|
4147
|
+
setSelectedOverlayId(id);
|
|
4148
|
+
mount.startMove(e);
|
|
4149
|
+
return;
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
setSelectedOverlayId(null);
|
|
4153
|
+
});
|
|
4154
|
+
map.on("error", (e) => {
|
|
4155
|
+
const message = e.error?.message ?? "";
|
|
4156
|
+
if (/style|sprite|tile|fetch|network|Failed/i.test(message)) fail(message || live.current.strings.basemapFailed);
|
|
4157
|
+
});
|
|
4158
|
+
map.on("style.load", () => {
|
|
4159
|
+
try {
|
|
4160
|
+
map.addLayer({ id: OVERLAY_ANCHOR_LAYER, type: "background", paint: { "background-opacity": 0 } });
|
|
4161
|
+
map.addSource(LOCKED_SOURCE, { type: "geojson", data: { type: "FeatureCollection", features: [] } });
|
|
4162
|
+
map.addLayer({
|
|
4163
|
+
id: LOCKED_FILL_LAYER,
|
|
4164
|
+
type: "fill",
|
|
4165
|
+
source: LOCKED_SOURCE,
|
|
4166
|
+
filter: ["==", ["geometry-type"], "Polygon"],
|
|
4167
|
+
paint: { "fill-color": ["coalesce", ["get", "color"], "#888888"], "fill-opacity": 0.15 }
|
|
4168
|
+
});
|
|
4169
|
+
map.addLayer({
|
|
4170
|
+
id: LOCKED_LINE_LAYER,
|
|
4171
|
+
type: "line",
|
|
4172
|
+
source: LOCKED_SOURCE,
|
|
4173
|
+
filter: ["any", ["==", ["geometry-type"], "Polygon"], ["==", ["geometry-type"], "LineString"]],
|
|
4174
|
+
paint: { "line-color": ["coalesce", ["get", "color"], "#888888"], "line-width": 2, "line-dasharray": [2, 2] }
|
|
4175
|
+
});
|
|
4176
|
+
draw.start();
|
|
4177
|
+
draw.setMode("select");
|
|
4178
|
+
const colors = labelColorsRef.current;
|
|
4179
|
+
map.addSource(LABEL_PREVIEW_SOURCE, { type: "geojson", data: EMPTY_LABEL_PREVIEW });
|
|
4180
|
+
map.addLayer({
|
|
4181
|
+
id: LABEL_PREVIEW_LAYER,
|
|
4182
|
+
type: "symbol",
|
|
4183
|
+
source: LABEL_PREVIEW_SOURCE,
|
|
4184
|
+
layout: {
|
|
4185
|
+
"text-field": ["get", "name"],
|
|
4186
|
+
"text-size": 12,
|
|
4187
|
+
"text-font": ["Noto Sans Regular"],
|
|
4188
|
+
// Offset right rather than centered on the point -- centered
|
|
4189
|
+
// text sat directly on top of the point, making it hard to see
|
|
4190
|
+
// (and, in the real map views, click) the point itself.
|
|
4191
|
+
"text-anchor": "left",
|
|
4192
|
+
"text-offset": [0.6, 0]
|
|
4193
|
+
},
|
|
4194
|
+
paint: {
|
|
4195
|
+
"text-color": ["coalesce", ["get", "color"], colors.text],
|
|
4196
|
+
"text-halo-color": colors.surface,
|
|
4197
|
+
"text-halo-width": 1.5
|
|
4198
|
+
}
|
|
4199
|
+
});
|
|
4200
|
+
refreshLabelPreviewRef.current?.();
|
|
4201
|
+
} catch (err) {
|
|
4202
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
4203
|
+
} finally {
|
|
4204
|
+
setReady(true);
|
|
4205
|
+
}
|
|
4206
|
+
});
|
|
4207
|
+
return () => {
|
|
4208
|
+
draw.stop();
|
|
4209
|
+
canvas.removeEventListener("keydown", onCanvasKeyDown);
|
|
4210
|
+
canvas.removeEventListener("pointerdown", onNativePointerDown);
|
|
4211
|
+
canvas.removeEventListener("mousedown", onMouseDownBridge);
|
|
4212
|
+
canvas.removeEventListener("mousemove", onMouseMoveBridge);
|
|
4213
|
+
canvas.removeEventListener("mouseup", onMouseUpBridge);
|
|
4214
|
+
for (const mount of overlayMountsRef.current.values()) {
|
|
4215
|
+
mount.deleteMarker.remove();
|
|
4216
|
+
mount.resizeMarker.remove();
|
|
4217
|
+
mount.rotateMarker.remove();
|
|
4218
|
+
for (const marker of mount.cornerMarkers) marker.remove();
|
|
4219
|
+
release(mount.imageUrl);
|
|
4220
|
+
}
|
|
4221
|
+
overlayMountsRef.current.clear();
|
|
4222
|
+
map.remove();
|
|
4223
|
+
mapRef.current = null;
|
|
4224
|
+
drawRef.current = null;
|
|
4225
|
+
};
|
|
4226
|
+
}, [containerEl]);
|
|
4227
|
+
async function mountOverlay(overlay) {
|
|
4228
|
+
const map = mapRef.current;
|
|
4229
|
+
if (!map) return;
|
|
4230
|
+
const sourceId = `img-${overlay.id}`;
|
|
4231
|
+
const box = { corners: overlay.corners };
|
|
4232
|
+
const imageUrl = await live.current.resolveImage(overlay.image);
|
|
4233
|
+
map.addSource(sourceId, { type: "image", url: imageUrl, coordinates: box.corners });
|
|
4234
|
+
map.addLayer({ id: sourceId, type: "raster", source: sourceId }, OVERLAY_ANCHOR_LAYER);
|
|
4235
|
+
map.setPaintProperty(sourceId, "raster-opacity", overlay.opacity ?? 1);
|
|
4236
|
+
const setCorners = (next) => {
|
|
4237
|
+
box.corners = next;
|
|
4238
|
+
map.getSource(sourceId)?.setCoordinates(box.corners);
|
|
4239
|
+
};
|
|
4240
|
+
const commit = () => {
|
|
4241
|
+
setOverlays((prev) => prev.map((o) => o.id === overlay.id ? { ...o, corners: box.corners } : o));
|
|
4242
|
+
};
|
|
4243
|
+
const words = live.current.strings;
|
|
4244
|
+
const deleteEl = document.createElement("div");
|
|
4245
|
+
deleteEl.textContent = "\xD7";
|
|
4246
|
+
deleteEl.title = words.removeImage;
|
|
4247
|
+
Object.assign(deleteEl.style, OVERLAY_HANDLE_STYLE);
|
|
4248
|
+
const deleteMarker = new maplibregl3.Marker({ element: deleteEl, anchor: "center" });
|
|
4249
|
+
deleteEl.addEventListener("click", (e) => {
|
|
4250
|
+
e.stopPropagation();
|
|
4251
|
+
unmountOverlay(overlay.id);
|
|
4252
|
+
setOverlays((prev) => prev.filter((o) => o.id !== overlay.id));
|
|
4253
|
+
setSelectedOverlayId((current) => current === overlay.id ? null : current);
|
|
4254
|
+
});
|
|
4255
|
+
const resizeEl = document.createElement("div");
|
|
4256
|
+
resizeEl.textContent = "\u2198";
|
|
4257
|
+
resizeEl.title = words.resizeImage;
|
|
4258
|
+
Object.assign(resizeEl.style, OVERLAY_HANDLE_STYLE);
|
|
4259
|
+
const resizeMarker = new maplibregl3.Marker({ element: resizeEl, anchor: "center", draggable: true });
|
|
4260
|
+
let resizeStartCorners = box.corners;
|
|
4261
|
+
let resizeStartDist = 1;
|
|
4262
|
+
resizeMarker.on("dragstart", () => {
|
|
4263
|
+
resizeStartCorners = box.corners;
|
|
4264
|
+
const center = overlayCenter(resizeStartCorners);
|
|
4265
|
+
const p = resizeMarker.getLngLat();
|
|
4266
|
+
resizeStartDist = Math.max(1e-9, mercatorDistance(center, [p.lng, p.lat]));
|
|
4267
|
+
});
|
|
4268
|
+
resizeMarker.on("drag", () => {
|
|
4269
|
+
const center = overlayCenter(resizeStartCorners);
|
|
4270
|
+
const p = resizeMarker.getLngLat();
|
|
4271
|
+
const scale = Math.max(0.02, mercatorDistance(center, [p.lng, p.lat]) / resizeStartDist);
|
|
4272
|
+
setCorners(resizeStartCorners.map((c) => scalePoint(c, center, scale)));
|
|
4273
|
+
repositionHandles();
|
|
4274
|
+
});
|
|
4275
|
+
resizeMarker.on("dragend", commit);
|
|
4276
|
+
const rotateEl = document.createElement("div");
|
|
4277
|
+
rotateEl.textContent = "\u21BB";
|
|
4278
|
+
rotateEl.title = words.rotateImage;
|
|
4279
|
+
Object.assign(rotateEl.style, OVERLAY_HANDLE_STYLE);
|
|
4280
|
+
const rotateMarker = new maplibregl3.Marker({ element: rotateEl, anchor: "center", draggable: true });
|
|
4281
|
+
let rotateStartCorners = box.corners;
|
|
4282
|
+
let rotateStartAngle = 0;
|
|
4283
|
+
rotateMarker.on("dragstart", () => {
|
|
4284
|
+
rotateStartCorners = box.corners;
|
|
4285
|
+
const center = overlayCenter(rotateStartCorners);
|
|
4286
|
+
const p = rotateMarker.getLngLat();
|
|
4287
|
+
rotateStartAngle = mercatorBearing(center, [p.lng, p.lat]);
|
|
4288
|
+
});
|
|
4289
|
+
rotateMarker.on("drag", () => {
|
|
4290
|
+
const center = overlayCenter(rotateStartCorners);
|
|
4291
|
+
const p = rotateMarker.getLngLat();
|
|
4292
|
+
const delta = mercatorBearing(center, [p.lng, p.lat]) - rotateStartAngle;
|
|
4293
|
+
setCorners(rotateStartCorners.map((c) => rotatePoint(c, center, delta)));
|
|
4294
|
+
repositionHandles();
|
|
4295
|
+
});
|
|
4296
|
+
rotateMarker.on("dragend", commit);
|
|
4297
|
+
const cornerMarkers = [0, 1, 2, 3].map((i) => {
|
|
4298
|
+
const el = document.createElement("div");
|
|
4299
|
+
el.title = words.warpCorner;
|
|
4300
|
+
Object.assign(el.style, OVERLAY_HANDLE_STYLE);
|
|
4301
|
+
const marker = new maplibregl3.Marker({ element: el, anchor: "center", draggable: true });
|
|
4302
|
+
marker.on("drag", () => {
|
|
4303
|
+
const corners = [...box.corners];
|
|
4304
|
+
const p = marker.getLngLat();
|
|
4305
|
+
corners[i] = [p.lng, p.lat];
|
|
4306
|
+
setCorners(corners);
|
|
4307
|
+
repositionHandles();
|
|
4308
|
+
});
|
|
4309
|
+
marker.on("dragend", commit);
|
|
4310
|
+
return marker;
|
|
4311
|
+
});
|
|
4312
|
+
const repositionHandles = () => {
|
|
4313
|
+
const [topLeft, topRight, bottomRight, bottomLeft] = box.corners;
|
|
4314
|
+
const center = overlayCenter(box.corners);
|
|
4315
|
+
deleteMarker.setLngLat(center);
|
|
4316
|
+
resizeMarker.setLngLat(bottomRight);
|
|
4317
|
+
rotateMarker.setLngLat(rotateHandleAnchor(box.corners, center));
|
|
4318
|
+
cornerMarkers[0].setLngLat(topLeft);
|
|
4319
|
+
cornerMarkers[1].setLngLat(topRight);
|
|
4320
|
+
cornerMarkers[2].setLngLat(bottomRight);
|
|
4321
|
+
cornerMarkers[3].setLngLat(bottomLeft);
|
|
4322
|
+
};
|
|
4323
|
+
const startMove = (e) => {
|
|
4324
|
+
e.preventDefault();
|
|
4325
|
+
map.dragPan.disable();
|
|
4326
|
+
map.setPaintProperty(sourceId, "raster-opacity", OVERLAY_DRAG_OPACITY);
|
|
4327
|
+
const startPoint = e.point;
|
|
4328
|
+
const startScreenCorners = box.corners.map((corner) => map.project(corner));
|
|
4329
|
+
const container = map.getContainer();
|
|
4330
|
+
const onMove = (moveEvent) => {
|
|
4331
|
+
const rect = container.getBoundingClientRect();
|
|
4332
|
+
const dx = moveEvent.clientX - rect.left - startPoint.x;
|
|
4333
|
+
const dy = moveEvent.clientY - rect.top - startPoint.y;
|
|
4334
|
+
const newCorners = startScreenCorners.map((screenCorner) => {
|
|
4335
|
+
const lngLat = map.unproject([screenCorner.x + dx, screenCorner.y + dy]);
|
|
4336
|
+
return [lngLat.lng, lngLat.lat];
|
|
4337
|
+
});
|
|
4338
|
+
setCorners(newCorners);
|
|
4339
|
+
repositionHandles();
|
|
4340
|
+
};
|
|
4341
|
+
const onUp = () => {
|
|
4342
|
+
document.removeEventListener("mousemove", onMove);
|
|
4343
|
+
document.removeEventListener("mouseup", onUp);
|
|
4344
|
+
map.dragPan.enable();
|
|
4345
|
+
const current = overlaysRef.current.find((o) => o.id === overlay.id);
|
|
4346
|
+
map.setPaintProperty(sourceId, "raster-opacity", current?.opacity ?? 1);
|
|
4347
|
+
commit();
|
|
4348
|
+
};
|
|
4349
|
+
document.addEventListener("mousemove", onMove);
|
|
4350
|
+
document.addEventListener("mouseup", onUp);
|
|
4351
|
+
};
|
|
4352
|
+
repositionHandles();
|
|
4353
|
+
overlayMountsRef.current.set(overlay.id, {
|
|
4354
|
+
sourceId,
|
|
4355
|
+
box,
|
|
4356
|
+
deleteMarker,
|
|
4357
|
+
resizeMarker,
|
|
4358
|
+
rotateMarker,
|
|
4359
|
+
cornerMarkers,
|
|
4360
|
+
startMove,
|
|
4361
|
+
setCorners,
|
|
4362
|
+
repositionHandles,
|
|
4363
|
+
imageUrl
|
|
4364
|
+
});
|
|
4365
|
+
}
|
|
4366
|
+
function unmountOverlay(id) {
|
|
4367
|
+
const map = mapRef.current;
|
|
4368
|
+
const mount = overlayMountsRef.current.get(id);
|
|
4369
|
+
if (!map || !mount) return;
|
|
4370
|
+
mount.deleteMarker.remove();
|
|
4371
|
+
mount.resizeMarker.remove();
|
|
4372
|
+
mount.rotateMarker.remove();
|
|
4373
|
+
for (const marker of mount.cornerMarkers) marker.remove();
|
|
4374
|
+
if (map.getLayer(mount.sourceId)) map.removeLayer(mount.sourceId);
|
|
4375
|
+
if (map.getSource(mount.sourceId)) map.removeSource(mount.sourceId);
|
|
4376
|
+
release(mount.imageUrl);
|
|
4377
|
+
overlayMountsRef.current.delete(id);
|
|
4378
|
+
}
|
|
4379
|
+
function setCornerHandlesVisible(id, visible) {
|
|
4380
|
+
setWarpHandleIds((prev) => {
|
|
4381
|
+
const next = new Set(prev);
|
|
4382
|
+
if (visible) next.add(id);
|
|
4383
|
+
else next.delete(id);
|
|
4384
|
+
return next;
|
|
4385
|
+
});
|
|
4386
|
+
}
|
|
4387
|
+
const showCornerHandles = selectedOverlayId !== null && warpHandleIds.has(selectedOverlayId);
|
|
4388
|
+
useEffect8(() => {
|
|
4389
|
+
const map = mapRef.current;
|
|
4390
|
+
if (!map) return;
|
|
4391
|
+
for (const [id, mount] of overlayMountsRef.current) {
|
|
4392
|
+
if (id !== selectedOverlayId) {
|
|
4393
|
+
mount.deleteMarker.remove();
|
|
4394
|
+
mount.resizeMarker.remove();
|
|
4395
|
+
mount.rotateMarker.remove();
|
|
4396
|
+
for (const marker of mount.cornerMarkers) marker.remove();
|
|
4397
|
+
continue;
|
|
4398
|
+
}
|
|
4399
|
+
mount.deleteMarker.addTo(map);
|
|
4400
|
+
mount.resizeMarker.addTo(map);
|
|
4401
|
+
mount.rotateMarker.addTo(map);
|
|
4402
|
+
for (const marker of mount.cornerMarkers) showCornerHandles ? marker.addTo(map) : marker.remove();
|
|
4403
|
+
}
|
|
4404
|
+
}, [selectedOverlayId, showCornerHandles]);
|
|
4405
|
+
const selectedOverlay = overlays.find((o) => o.id === selectedOverlayId) ?? null;
|
|
4406
|
+
function updateSelectedOverlay(patch) {
|
|
4407
|
+
if (!selectedOverlayId) return;
|
|
4408
|
+
setOverlays((prev) => prev.map((o) => o.id === selectedOverlayId ? { ...o, ...patch } : o));
|
|
4409
|
+
if (patch.opacity !== void 0) {
|
|
4410
|
+
const mount = overlayMountsRef.current.get(selectedOverlayId);
|
|
4411
|
+
if (mount) mapRef.current?.setPaintProperty(mount.sourceId, "raster-opacity", patch.opacity);
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4414
|
+
const loaded = useRef8(false);
|
|
4415
|
+
useEffect8(() => {
|
|
4416
|
+
if (!ready || !initial || loaded.current) return;
|
|
4417
|
+
const draw = drawRef.current;
|
|
4418
|
+
const map = mapRef.current;
|
|
4419
|
+
if (!draw || !map) return;
|
|
4420
|
+
loaded.current = true;
|
|
4421
|
+
const bounds = new maplibregl3.LngLatBounds();
|
|
4422
|
+
if (initial.features.length > 0) {
|
|
4423
|
+
const session = prepareForEditing(initial.features);
|
|
4424
|
+
sessionRef.current = session;
|
|
4425
|
+
for (const geometry of showShapes(session, session)) extendBounds(bounds, geometry);
|
|
4426
|
+
}
|
|
4427
|
+
const drafts = initial.overlays.map((overlay) => ({ ...overlay, id: crypto.randomUUID() }));
|
|
4428
|
+
setOverlays(drafts);
|
|
4429
|
+
for (const draft of drafts) {
|
|
4430
|
+
mountOverlay(draft).catch(() => {
|
|
4431
|
+
});
|
|
4432
|
+
for (const corner of draft.corners) bounds.extend(corner);
|
|
4433
|
+
}
|
|
4434
|
+
if (!bounds.isEmpty()) map.fitBounds(bounds, { padding: 60, maxZoom: 17, duration: 0 });
|
|
4435
|
+
else if (initialView) map.jumpTo(initialView);
|
|
4436
|
+
}, [ready, initial]);
|
|
4437
|
+
function showShapes(session, added) {
|
|
4438
|
+
const draw = drawRef.current;
|
|
4439
|
+
const map = mapRef.current;
|
|
4440
|
+
if (!draw || !map) return [];
|
|
4441
|
+
const parts = [...added.parts];
|
|
4442
|
+
const results = draw.addFeatures(parts.map((part) => ({
|
|
4443
|
+
type: "Feature",
|
|
4444
|
+
geometry: part.geometry,
|
|
4445
|
+
properties: {
|
|
4446
|
+
mode: part.mode,
|
|
4447
|
+
sourceKey: part.sourceKey,
|
|
4448
|
+
color: part.color,
|
|
4449
|
+
name: part.name,
|
|
4450
|
+
...part.mode === "polygon" ? { opacity: part.opacity } : {}
|
|
4451
|
+
}
|
|
4452
|
+
})));
|
|
4453
|
+
results.forEach((result, i) => {
|
|
4454
|
+
if (!result.valid) lockPart(session, parts[i].sourceKey);
|
|
4455
|
+
});
|
|
4456
|
+
const locked = [...session.locked.values()];
|
|
4457
|
+
setLockedCount(locked.length);
|
|
4458
|
+
map.getSource(LOCKED_SOURCE)?.setData({
|
|
4459
|
+
type: "FeatureCollection",
|
|
4460
|
+
features: locked.map((part) => ({ type: "Feature", geometry: part.geometry, properties: { color: part.color } }))
|
|
4461
|
+
});
|
|
4462
|
+
return [...parts, ...added.locked.values()].map((part) => part.geometry);
|
|
4463
|
+
}
|
|
4464
|
+
function handleModeChange(next) {
|
|
4465
|
+
if (!ready) return;
|
|
4466
|
+
setMode(next);
|
|
4467
|
+
setSelectedOverlayId(null);
|
|
4468
|
+
drawRef.current?.setMode(next);
|
|
4469
|
+
}
|
|
4470
|
+
useImperativeHandle(ref, () => ({
|
|
4471
|
+
getResult() {
|
|
4472
|
+
const savedParts = (drawRef.current?.getSnapshot() ?? []).filter((f) => f.geometry != null && !f.properties?.selectionPoint && !f.properties?.midPoint).map((f) => {
|
|
4473
|
+
const name = f.properties?.name?.trim();
|
|
4474
|
+
const regionOpacity = f.geometry.type === "Polygon" ? f.properties?.opacity ?? opacityRef.current : void 0;
|
|
4475
|
+
const sourceKey = f.properties?.sourceKey;
|
|
4476
|
+
return {
|
|
4477
|
+
...typeof sourceKey === "string" ? { sourceKey } : {},
|
|
4478
|
+
geometry: f.geometry,
|
|
4479
|
+
color: f.properties?.color ?? colorRef.current,
|
|
4480
|
+
...name ? { name } : {},
|
|
4481
|
+
...regionOpacity !== void 0 ? { opacity: regionOpacity } : {}
|
|
4482
|
+
};
|
|
4483
|
+
});
|
|
4484
|
+
const session = sessionRef.current ?? { parts: [], sources: /* @__PURE__ */ new Map(), locked: /* @__PURE__ */ new Map() };
|
|
4485
|
+
return {
|
|
4486
|
+
features: assembleForSaving(session, savedParts),
|
|
4487
|
+
overlays: overlaysRef.current.map(({ image, corners, name, opacity: opacity2, order }) => ({ image, corners, name, opacity: opacity2, order }))
|
|
4488
|
+
};
|
|
4489
|
+
},
|
|
4490
|
+
drawnBounds() {
|
|
4491
|
+
const bounds = new maplibregl3.LngLatBounds();
|
|
4492
|
+
for (const f of drawRef.current?.getSnapshot() ?? []) {
|
|
4493
|
+
if (f.geometry == null || f.properties?.selectionPoint || f.properties?.midPoint) continue;
|
|
4494
|
+
extendBounds(bounds, f.geometry);
|
|
4495
|
+
}
|
|
4496
|
+
for (const overlay of overlaysRef.current) for (const corner of overlay.corners) bounds.extend(corner);
|
|
4497
|
+
return bounds.isEmpty() ? null : bounds.toArray().flat();
|
|
4498
|
+
},
|
|
4499
|
+
async addImage(image) {
|
|
4500
|
+
if (overlaysRef.current.some((o) => o.image === image)) return false;
|
|
4501
|
+
const map = mapRef.current;
|
|
4502
|
+
if (!map) return false;
|
|
4503
|
+
const url = await live.current.resolveImage(image);
|
|
4504
|
+
const element = await loadImageElement(url, live.current.strings.imageFailed).finally(() => release(url));
|
|
4505
|
+
const aspect = element.naturalWidth / element.naturalHeight;
|
|
4506
|
+
const centerScreen = map.project(map.getCenter());
|
|
4507
|
+
const halfWidthPx = 130;
|
|
4508
|
+
const halfHeightPx = halfWidthPx / aspect;
|
|
4509
|
+
const topLeft = map.unproject([centerScreen.x - halfWidthPx, centerScreen.y - halfHeightPx]);
|
|
4510
|
+
const bottomRight = map.unproject([centerScreen.x + halfWidthPx, centerScreen.y + halfHeightPx]);
|
|
4511
|
+
const draft = {
|
|
4512
|
+
id: crypto.randomUUID(),
|
|
4513
|
+
image,
|
|
4514
|
+
corners: [
|
|
4515
|
+
[topLeft.lng, topLeft.lat],
|
|
4516
|
+
[bottomRight.lng, topLeft.lat],
|
|
4517
|
+
[bottomRight.lng, bottomRight.lat],
|
|
4518
|
+
[topLeft.lng, bottomRight.lat]
|
|
4519
|
+
],
|
|
4520
|
+
// Leaves room to insert between existing overlays later without
|
|
4521
|
+
// renumbering.
|
|
4522
|
+
order: overlaysRef.current.length * 10
|
|
4523
|
+
};
|
|
4524
|
+
setOverlays((prev) => [...prev, draft]);
|
|
4525
|
+
await mountOverlay(draft);
|
|
4526
|
+
setSelectedOverlayId(draft.id);
|
|
4527
|
+
return true;
|
|
4528
|
+
},
|
|
4529
|
+
addFeatures(features) {
|
|
4530
|
+
const map = mapRef.current;
|
|
4531
|
+
if (!map || features.length === 0) return;
|
|
4532
|
+
const session = sessionRef.current ?? { parts: [], sources: /* @__PURE__ */ new Map(), locked: /* @__PURE__ */ new Map() };
|
|
4533
|
+
sessionRef.current = session;
|
|
4534
|
+
const bounds = new maplibregl3.LngLatBounds();
|
|
4535
|
+
for (const geometry of showShapes(session, addToSession(session, features))) extendBounds(bounds, geometry);
|
|
4536
|
+
if (!bounds.isEmpty()) map.fitBounds(bounds, { padding: 60, maxZoom: 17 });
|
|
4537
|
+
}
|
|
4538
|
+
// mountOverlay and friends read only refs: a fresh handle each render
|
|
4539
|
+
// is fine.
|
|
4540
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
4541
|
+
}));
|
|
4542
|
+
const selectedFeature = selectedId ? drawRef.current?.getSnapshot().find((f) => String(f.id) === selectedId) : void 0;
|
|
4543
|
+
const selectedIsPoint = selectedFeature?.geometry?.type === "Point";
|
|
4544
|
+
const selectedIsRegion = selectedFeature?.geometry?.type === "Polygon";
|
|
4545
|
+
function renderModeButton(entry) {
|
|
4546
|
+
return /* @__PURE__ */ jsx9(
|
|
4547
|
+
Button3,
|
|
4548
|
+
{
|
|
4549
|
+
size: "xs",
|
|
4550
|
+
variant: mode === entry.mode ? "filled" : "default",
|
|
4551
|
+
disabled: !ready,
|
|
4552
|
+
onClick: () => handleModeChange(entry.mode),
|
|
4553
|
+
children: strings[entry.label]
|
|
4554
|
+
},
|
|
4555
|
+
entry.mode
|
|
4556
|
+
);
|
|
4557
|
+
}
|
|
4558
|
+
return /* @__PURE__ */ jsxs7(Box2, { style: { position: "absolute", inset: 0, overflow: "hidden" }, children: [
|
|
4559
|
+
/* @__PURE__ */ jsx9("style", { children: TOOLBAR_CSS }),
|
|
4560
|
+
/* @__PURE__ */ jsx9("div", { ref: setContainerEl, style: { position: "absolute", inset: 0 } }),
|
|
4561
|
+
/* @__PURE__ */ jsxs7(
|
|
4562
|
+
Stack4,
|
|
4563
|
+
{
|
|
4564
|
+
gap: 4,
|
|
4565
|
+
p: 6,
|
|
4566
|
+
className: TOOLBAR_CLASS,
|
|
4567
|
+
style: {
|
|
4568
|
+
position: "absolute",
|
|
4569
|
+
top: toolbarTop,
|
|
4570
|
+
left: 12,
|
|
4571
|
+
zIndex: 3,
|
|
4572
|
+
borderRadius: 8,
|
|
4573
|
+
background: dark ? "rgba(20,20,20,0.75)" : "rgba(255,255,255,0.85)"
|
|
4574
|
+
},
|
|
4575
|
+
children: [
|
|
4576
|
+
TOOLBAR.filter((entry) => entry.mode !== "select").map(renderModeButton),
|
|
4577
|
+
onAddImage && /* @__PURE__ */ jsx9(Button3, { size: "xs", variant: "default", disabled: !ready, onClick: onAddImage, children: strings.image }),
|
|
4578
|
+
TOOLBAR.filter((entry) => entry.mode === "select").map(renderModeButton),
|
|
4579
|
+
/* @__PURE__ */ jsx9(Divider, { my: 2 }),
|
|
4580
|
+
/* @__PURE__ */ jsx9(Text4, { size: "xs", fw: 600, c: "dimmed", children: strings.editOptions }),
|
|
4581
|
+
selectedFeature && /* @__PURE__ */ jsx9(
|
|
4582
|
+
ColorInput,
|
|
4583
|
+
{
|
|
4584
|
+
size: "xs",
|
|
4585
|
+
value: color,
|
|
4586
|
+
onChange: (next) => {
|
|
4587
|
+
setColor(next);
|
|
4588
|
+
const draw = drawRef.current;
|
|
4589
|
+
if (draw && selectedId) draw.updateFeatureProperties(selectedId, { color: next });
|
|
4590
|
+
},
|
|
4591
|
+
swatches: COLOR_SWATCHES,
|
|
4592
|
+
disabled: !ready,
|
|
4593
|
+
popoverProps: { withinPortal: true, zIndex: 1e3 }
|
|
4594
|
+
}
|
|
4595
|
+
),
|
|
4596
|
+
(selectedIsPoint || selectedIsRegion) && /* @__PURE__ */ jsx9(
|
|
4597
|
+
TextInput4,
|
|
4598
|
+
{
|
|
4599
|
+
size: "xs",
|
|
4600
|
+
placeholder: selectedIsPoint ? strings.labelText : strings.regionName,
|
|
4601
|
+
value: labelText,
|
|
4602
|
+
onChange: (e) => {
|
|
4603
|
+
setLabelText(e.currentTarget.value);
|
|
4604
|
+
const draw = drawRef.current;
|
|
4605
|
+
if (draw && selectedId) draw.updateFeatureProperties(selectedId, { name: e.currentTarget.value });
|
|
4606
|
+
},
|
|
4607
|
+
disabled: !ready
|
|
4608
|
+
}
|
|
4609
|
+
),
|
|
4610
|
+
selectedIsRegion && /* @__PURE__ */ jsxs7(Stack4, { gap: 2, children: [
|
|
4611
|
+
/* @__PURE__ */ jsx9(Text4, { size: "xs", c: "dimmed", children: strings.fillOpacity }),
|
|
4612
|
+
/* @__PURE__ */ jsx9(
|
|
4613
|
+
Slider,
|
|
4614
|
+
{
|
|
4615
|
+
size: "xs",
|
|
4616
|
+
min: 0.1,
|
|
4617
|
+
max: 1,
|
|
4618
|
+
step: 0.05,
|
|
4619
|
+
label: (v) => `${Math.round(v * 100)}%`,
|
|
4620
|
+
value: opacity,
|
|
4621
|
+
onChange: (v) => {
|
|
4622
|
+
setOpacity(v);
|
|
4623
|
+
const draw = drawRef.current;
|
|
4624
|
+
if (draw && selectedId) draw.updateFeatureProperties(selectedId, { opacity: v });
|
|
4625
|
+
},
|
|
4626
|
+
disabled: !ready
|
|
4627
|
+
}
|
|
4628
|
+
)
|
|
4629
|
+
] }),
|
|
4630
|
+
selectedOverlayId && /* @__PURE__ */ jsx9(
|
|
4631
|
+
SegmentedControl,
|
|
4632
|
+
{
|
|
4633
|
+
size: "xs",
|
|
4634
|
+
value: showCornerHandles ? "transform" : "scale",
|
|
4635
|
+
onChange: (value) => setCornerHandlesVisible(selectedOverlayId, value === "transform"),
|
|
4636
|
+
disabled: !ready,
|
|
4637
|
+
data: [
|
|
4638
|
+
{ label: strings.scale, value: "scale" },
|
|
4639
|
+
{ label: strings.transform, value: "transform" }
|
|
4640
|
+
]
|
|
4641
|
+
}
|
|
4642
|
+
),
|
|
4643
|
+
selectedOverlay && /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
4644
|
+
overlayNames && /* @__PURE__ */ jsx9(
|
|
4645
|
+
TextInput4,
|
|
4646
|
+
{
|
|
4647
|
+
size: "xs",
|
|
4648
|
+
placeholder: strings.overlayName,
|
|
4649
|
+
value: selectedOverlay.name ?? "",
|
|
4650
|
+
onChange: (e) => updateSelectedOverlay({ name: e.currentTarget.value }),
|
|
4651
|
+
disabled: !ready
|
|
4652
|
+
}
|
|
4653
|
+
),
|
|
4654
|
+
/* @__PURE__ */ jsxs7(Stack4, { gap: 2, children: [
|
|
4655
|
+
/* @__PURE__ */ jsx9(Text4, { size: "xs", c: "dimmed", children: strings.opacity }),
|
|
4656
|
+
/* @__PURE__ */ jsx9(
|
|
4657
|
+
Slider,
|
|
4658
|
+
{
|
|
4659
|
+
size: "xs",
|
|
4660
|
+
min: 0.1,
|
|
4661
|
+
max: 1,
|
|
4662
|
+
step: 0.05,
|
|
4663
|
+
label: (v) => `${Math.round(v * 100)}%`,
|
|
4664
|
+
value: selectedOverlay.opacity ?? 1,
|
|
4665
|
+
onChange: (v) => updateSelectedOverlay({ opacity: v }),
|
|
4666
|
+
disabled: !ready
|
|
4667
|
+
}
|
|
4668
|
+
)
|
|
4669
|
+
] }),
|
|
4670
|
+
/* @__PURE__ */ jsx9(
|
|
4671
|
+
NumberInput,
|
|
4672
|
+
{
|
|
4673
|
+
size: "xs",
|
|
4674
|
+
label: strings.stackingOrder,
|
|
4675
|
+
hideControls: false,
|
|
4676
|
+
value: selectedOverlay.order ?? 0,
|
|
4677
|
+
onChange: (v) => updateSelectedOverlay({ order: Number(v) || 0 }),
|
|
4678
|
+
disabled: !ready
|
|
4679
|
+
}
|
|
4680
|
+
),
|
|
4681
|
+
renderOverlayOptions?.(selectedOverlay.image)
|
|
4682
|
+
] })
|
|
4683
|
+
]
|
|
4684
|
+
}
|
|
4685
|
+
),
|
|
4686
|
+
!ready && /* @__PURE__ */ jsx9(Group4, { justify: "center", align: "center", style: { position: "absolute", inset: 0, zIndex: 2 }, children: /* @__PURE__ */ jsx9(Loader3, { size: "sm" }) }),
|
|
4687
|
+
lockedCount > 0 && !lockedNoticeDismissed && /* @__PURE__ */ jsx9(
|
|
4688
|
+
Alert,
|
|
4689
|
+
{
|
|
4690
|
+
color: "yellow",
|
|
4691
|
+
title: lockedCount === 1 ? strings.lockedOne : strings.lockedMany(lockedCount),
|
|
4692
|
+
withCloseButton: true,
|
|
4693
|
+
onClose: () => setLockedNoticeDismissed(true),
|
|
4694
|
+
style: { position: "absolute", bottom: 12, right: 12, zIndex: 3, maxWidth: 360 },
|
|
4695
|
+
children: strings.lockedExplain
|
|
4696
|
+
}
|
|
4697
|
+
)
|
|
4698
|
+
] });
|
|
4699
|
+
});
|
|
4700
|
+
function MapEditorHelp({ strings: stringOverrides, images = true }) {
|
|
4701
|
+
const strings = { ...MAP_EDITOR_ENGLISH, ...stringOverrides };
|
|
4702
|
+
return /* @__PURE__ */ jsxs7(Stack4, { gap: "md", children: [
|
|
4703
|
+
/* @__PURE__ */ jsx9(Text4, { size: "sm", fw: 600, children: strings.helpDrawing }),
|
|
4704
|
+
/* @__PURE__ */ jsxs7(List, { spacing: 4, size: "sm", children: [
|
|
4705
|
+
/* @__PURE__ */ jsx9(List.Item, { children: strings.helpPoint }),
|
|
4706
|
+
/* @__PURE__ */ jsxs7(List.Item, { children: [
|
|
4707
|
+
strings.helpLine,
|
|
4708
|
+
" ",
|
|
4709
|
+
/* @__PURE__ */ jsx9(Kbd, { children: "Enter" }),
|
|
4710
|
+
" ",
|
|
4711
|
+
strings.helpEnterFinishes,
|
|
4712
|
+
" ",
|
|
4713
|
+
/* @__PURE__ */ jsx9(Kbd, { children: "Esc" }),
|
|
4714
|
+
" ",
|
|
4715
|
+
strings.helpEscCancels
|
|
4716
|
+
] }),
|
|
4717
|
+
/* @__PURE__ */ jsx9(List.Item, { children: strings.helpRectangle }),
|
|
4718
|
+
images && /* @__PURE__ */ jsxs7(List.Item, { children: [
|
|
4719
|
+
strings.helpImage,
|
|
4720
|
+
" ",
|
|
4721
|
+
/* @__PURE__ */ jsx9(Text4, { span: true, fw: 600, children: strings.helpImages }),
|
|
4722
|
+
" ",
|
|
4723
|
+
strings.helpBelow
|
|
4724
|
+
] })
|
|
4725
|
+
] }),
|
|
4726
|
+
/* @__PURE__ */ jsx9(Text4, { size: "sm", fw: 600, children: strings.helpEditing }),
|
|
4727
|
+
/* @__PURE__ */ jsxs7(List, { spacing: 4, size: "sm", children: [
|
|
4728
|
+
/* @__PURE__ */ jsx9(List.Item, { children: strings.helpSelect }),
|
|
4729
|
+
/* @__PURE__ */ jsx9(List.Item, { children: strings.helpVertex }),
|
|
4730
|
+
/* @__PURE__ */ jsx9(List.Item, { children: strings.helpColor }),
|
|
4731
|
+
/* @__PURE__ */ jsxs7(List.Item, { children: [
|
|
4732
|
+
strings.helpDelete,
|
|
4733
|
+
" ",
|
|
4734
|
+
/* @__PURE__ */ jsx9(Kbd, { children: "Delete" }),
|
|
4735
|
+
" ",
|
|
4736
|
+
strings.helpOr,
|
|
4737
|
+
" ",
|
|
4738
|
+
/* @__PURE__ */ jsx9(Kbd, { children: "Backspace" }),
|
|
4739
|
+
"."
|
|
4740
|
+
] }),
|
|
4741
|
+
/* @__PURE__ */ jsxs7(List.Item, { children: [
|
|
4742
|
+
strings.helpRotate,
|
|
4743
|
+
" ",
|
|
4744
|
+
/* @__PURE__ */ jsx9(Kbd, { children: "R" }),
|
|
4745
|
+
" ",
|
|
4746
|
+
strings.helpRotateHandle
|
|
4747
|
+
] })
|
|
4748
|
+
] }),
|
|
4749
|
+
images && /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
4750
|
+
/* @__PURE__ */ jsx9(Text4, { size: "sm", fw: 600, children: strings.helpImages }),
|
|
4751
|
+
/* @__PURE__ */ jsxs7(List, { spacing: 4, size: "sm", children: [
|
|
4752
|
+
/* @__PURE__ */ jsx9(List.Item, { children: strings.helpImageMove }),
|
|
4753
|
+
/* @__PURE__ */ jsx9(List.Item, { children: strings.helpImageResize }),
|
|
4754
|
+
/* @__PURE__ */ jsx9(List.Item, { children: strings.helpImageRotate }),
|
|
4755
|
+
/* @__PURE__ */ jsx9(List.Item, { children: strings.helpImageTransform }),
|
|
4756
|
+
/* @__PURE__ */ jsx9(List.Item, { children: strings.helpImageRemove })
|
|
4757
|
+
] })
|
|
4758
|
+
] })
|
|
4759
|
+
] });
|
|
4760
|
+
}
|
|
4761
|
+
|
|
4762
|
+
// src/editor/placeShapes.ts
|
|
4763
|
+
function placeFeatures(place) {
|
|
4764
|
+
const geometry = place?.geometry;
|
|
4765
|
+
if (!geometry) return [];
|
|
4766
|
+
if (geometry.type === "FeatureCollection") {
|
|
4767
|
+
return (geometry.features ?? []).filter((f) => f && f.geometry);
|
|
4768
|
+
}
|
|
4769
|
+
if (geometry.type === "Feature") return geometry.geometry ? [geometry] : [];
|
|
4770
|
+
return [{ type: "Feature", properties: {}, geometry }];
|
|
4771
|
+
}
|
|
4772
|
+
function placeToEditor(place) {
|
|
4773
|
+
return {
|
|
4774
|
+
features: placeFeatures(place),
|
|
4775
|
+
overlays: (place?.overlays ?? []).map((o) => ({
|
|
4776
|
+
image: o.media,
|
|
4777
|
+
corners: o.corners,
|
|
4778
|
+
...o.opacity !== void 0 ? { opacity: o.opacity } : {},
|
|
4779
|
+
...o.order !== void 0 ? { order: o.order } : {}
|
|
4780
|
+
}))
|
|
4781
|
+
};
|
|
4782
|
+
}
|
|
4783
|
+
function editorToPlace(place, result, dates) {
|
|
4784
|
+
const before = place?.geometry;
|
|
4785
|
+
const geometry = result.features.length === 0 ? void 0 : { ...before?.type === "FeatureCollection" ? before : {}, type: "FeatureCollection", features: result.features };
|
|
4786
|
+
const previous = new Map((place?.overlays ?? []).map((o) => [o.media, o]));
|
|
4787
|
+
const overlays = result.overlays.map((o) => {
|
|
4788
|
+
const { opacity: _o, order: _r, date: _d, ...kept } = previous.get(o.image) ?? {};
|
|
4789
|
+
const date = dates.has(o.image) ? dates.get(o.image) : previous.get(o.image)?.date;
|
|
4790
|
+
return {
|
|
4791
|
+
...kept,
|
|
4792
|
+
media: o.image,
|
|
4793
|
+
corners: o.corners,
|
|
4794
|
+
...o.opacity !== void 0 && o.opacity !== 1 ? { opacity: o.opacity } : {},
|
|
4795
|
+
...o.order !== void 0 ? { order: Math.round(o.order) } : {},
|
|
4796
|
+
...date ? { date } : {}
|
|
4797
|
+
};
|
|
4798
|
+
});
|
|
4799
|
+
return patchPlace(place, {
|
|
4800
|
+
geometry,
|
|
4801
|
+
overlays: overlays.length ? overlays : void 0
|
|
4802
|
+
});
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4805
|
+
// src/editor/PlaceMapDialog.tsx
|
|
4806
|
+
import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
4807
|
+
var BAR_HEIGHT2 = 56;
|
|
4808
|
+
var STORY_MAP_WORDS = {
|
|
4809
|
+
helpImage: "Image: add a picture file, such as a scan of an old map -- see"
|
|
4810
|
+
};
|
|
4811
|
+
async function prepareOverlay(file) {
|
|
4812
|
+
const image = await optimizeImage(file, { purpose: "overlay" });
|
|
4813
|
+
return { data: new Uint8Array(await image.blob.arrayBuffer()), mime: image.mime, ext: image.ext, width: image.width, height: image.height };
|
|
4814
|
+
}
|
|
4815
|
+
function OverlayDateField({ value, language, parse, strings, onChange }) {
|
|
4816
|
+
const [text, setText] = useState8(value?.text ?? "");
|
|
4817
|
+
const request = useRef9(0);
|
|
4818
|
+
const edtf = value?.edtf;
|
|
4819
|
+
return /* @__PURE__ */ jsxs8(Stack5, { gap: 2, maw: 220, children: [
|
|
4820
|
+
/* @__PURE__ */ jsx10(
|
|
4821
|
+
TextInput5,
|
|
4822
|
+
{
|
|
4823
|
+
size: "xs",
|
|
4824
|
+
label: strings.overlayDate,
|
|
4825
|
+
description: strings.overlayDateHint,
|
|
4826
|
+
value: text,
|
|
4827
|
+
onChange: (e) => {
|
|
4828
|
+
const typed = e.currentTarget.value;
|
|
4829
|
+
setText(typed);
|
|
4830
|
+
const id = ++request.current;
|
|
4831
|
+
if (!typed.trim()) {
|
|
4832
|
+
onChange(void 0);
|
|
4833
|
+
return;
|
|
4834
|
+
}
|
|
4835
|
+
void parse(typed, language).then((parsed) => {
|
|
4836
|
+
if (id === request.current) onChange(slideDate(value, typed, parsed));
|
|
4837
|
+
});
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
),
|
|
4841
|
+
text.trim() && /* @__PURE__ */ jsx10(Text5, { size: "xs", c: edtf && edtfYearRange(edtf) ? "dimmed" : "red", children: edtf && edtfYearRange(edtf) ? `EDTF ${edtf}` : strings.preciseDateNotUnderstood })
|
|
4842
|
+
] });
|
|
4843
|
+
}
|
|
4844
|
+
function PlaceMapDialog({
|
|
4845
|
+
place,
|
|
4846
|
+
doc,
|
|
4847
|
+
files,
|
|
4848
|
+
dark,
|
|
4849
|
+
parseDateText,
|
|
4850
|
+
strings,
|
|
4851
|
+
mapStrings: mapStringOverrides,
|
|
4852
|
+
onCancel,
|
|
4853
|
+
onDone
|
|
4854
|
+
}) {
|
|
4855
|
+
const mapStrings = { ...STORY_MAP_WORDS, ...mapStringOverrides };
|
|
4856
|
+
const editorRef = useRef9(null);
|
|
4857
|
+
const initial = useMemo3(() => placeToEditor(place), [place]);
|
|
4858
|
+
const [work, setWork] = useState8({ doc, files });
|
|
4859
|
+
const workRef = useRef9(work);
|
|
4860
|
+
workRef.current = work;
|
|
4861
|
+
const [dates, setDates] = useState8(() => /* @__PURE__ */ new Map());
|
|
4862
|
+
const [message, setMessage] = useState8(null);
|
|
4863
|
+
const [helpOpen, setHelpOpen] = useState8(false);
|
|
4864
|
+
const imageInput = useRef9(null);
|
|
4865
|
+
const shapesInput = useRef9(null);
|
|
4866
|
+
const initialView = useMemo3(() => {
|
|
4867
|
+
if (place?.lat !== void 0 && place.lon !== void 0) return { center: [place.lon, place.lat], zoom: 12 };
|
|
4868
|
+
const all = storyPlaces(doc.slides.map((s2) => s2.place));
|
|
4869
|
+
if (!all) return void 0;
|
|
4870
|
+
const [w, s, e, n] = all.bounds;
|
|
4871
|
+
return { center: [(w + e) / 2, (s + n) / 2], zoom: 5 };
|
|
4872
|
+
}, [place, doc]);
|
|
4873
|
+
const resolveImage = async (key) => {
|
|
4874
|
+
const item = Object.prototype.hasOwnProperty.call(workRef.current.doc.media, key) ? workRef.current.doc.media[key] : void 0;
|
|
4875
|
+
const data = item && workRef.current.files.get(item.src);
|
|
4876
|
+
if (!item || !data) throw new Error(strings.photoFailed(key));
|
|
4877
|
+
return URL.createObjectURL(new Blob([data], { type: item.mime }));
|
|
4878
|
+
};
|
|
4879
|
+
const addImages = async (chosen) => {
|
|
4880
|
+
setMessage(null);
|
|
4881
|
+
for (const file of chosen) {
|
|
4882
|
+
let key;
|
|
4883
|
+
try {
|
|
4884
|
+
const added = addImage(workRef.current.doc, workRef.current.files, await prepareOverlay(file), "map");
|
|
4885
|
+
workRef.current = { doc: added.doc, files: added.files };
|
|
4886
|
+
setWork(workRef.current);
|
|
4887
|
+
key = added.key;
|
|
4888
|
+
} catch {
|
|
4889
|
+
setMessage(strings.photoFailed(file.name || "the image"));
|
|
4890
|
+
continue;
|
|
4891
|
+
}
|
|
4892
|
+
try {
|
|
4893
|
+
if (!await editorRef.current?.addImage(key)) setMessage(strings.overlayAlready);
|
|
4894
|
+
} catch (err) {
|
|
4895
|
+
setMessage(err instanceof Error ? err.message : String(err));
|
|
4896
|
+
}
|
|
4897
|
+
}
|
|
4898
|
+
};
|
|
4899
|
+
const importShapes = async (chosen) => {
|
|
4900
|
+
setMessage(null);
|
|
4901
|
+
for (const file of chosen) {
|
|
4902
|
+
try {
|
|
4903
|
+
const features = parseMapFile(await file.text()).filter((f) => f.properties?.["@geometry-type"] !== "groundoverlay");
|
|
4904
|
+
if (features.length === 0) setMessage(strings.importNothing(file.name));
|
|
4905
|
+
else editorRef.current?.addFeatures(features);
|
|
4906
|
+
} catch {
|
|
4907
|
+
setMessage(strings.importFailed(file.name));
|
|
4908
|
+
}
|
|
4909
|
+
}
|
|
4910
|
+
};
|
|
4911
|
+
const done = () => {
|
|
4912
|
+
const editor = editorRef.current;
|
|
4913
|
+
if (!editor) return;
|
|
4914
|
+
onDone(editorToPlace(place, editor.getResult(), dates), workRef.current.doc.media, workRef.current.files);
|
|
4915
|
+
};
|
|
4916
|
+
const bar = dark ? "rgba(20,20,20,0.85)" : "rgba(255,255,255,0.9)";
|
|
4917
|
+
return /* @__PURE__ */ jsxs8(Modal, { opened: true, onClose: onCancel, fullScreen: true, withCloseButton: false, closeOnEscape: false, padding: 0, children: [
|
|
4918
|
+
/* @__PURE__ */ jsxs8(Box3, { style: { position: "fixed", inset: 0, overflow: "hidden" }, children: [
|
|
4919
|
+
/* @__PURE__ */ jsx10(
|
|
4920
|
+
MapEditor,
|
|
4921
|
+
{
|
|
4922
|
+
ref: editorRef,
|
|
4923
|
+
initial,
|
|
4924
|
+
initialView,
|
|
4925
|
+
resolveImage,
|
|
4926
|
+
releaseImage: (url) => URL.revokeObjectURL(url),
|
|
4927
|
+
onAddImage: () => imageInput.current?.click(),
|
|
4928
|
+
onError: setMessage,
|
|
4929
|
+
dark,
|
|
4930
|
+
toolbarTop: BAR_HEIGHT2 + 12,
|
|
4931
|
+
overlayNames: false,
|
|
4932
|
+
strings: mapStrings,
|
|
4933
|
+
renderOverlayOptions: (key) => /* @__PURE__ */ jsx10(
|
|
4934
|
+
OverlayDateField,
|
|
4935
|
+
{
|
|
4936
|
+
value: dates.has(key) ? dates.get(key) : place?.overlays?.find((o) => o.media === key)?.date,
|
|
4937
|
+
language: doc.language,
|
|
4938
|
+
parse: parseDateText,
|
|
4939
|
+
strings,
|
|
4940
|
+
onChange: (date) => setDates((prev) => new Map(prev).set(key, date))
|
|
4941
|
+
},
|
|
4942
|
+
key
|
|
4943
|
+
)
|
|
4944
|
+
}
|
|
4945
|
+
),
|
|
4946
|
+
/* @__PURE__ */ jsxs8(
|
|
4947
|
+
Group5,
|
|
4948
|
+
{
|
|
4949
|
+
justify: "space-between",
|
|
4950
|
+
wrap: "nowrap",
|
|
4951
|
+
px: "md",
|
|
4952
|
+
gap: "sm",
|
|
4953
|
+
style: { position: "absolute", top: 0, left: 0, right: 0, height: BAR_HEIGHT2, zIndex: 3, background: bar },
|
|
4954
|
+
children: [
|
|
4955
|
+
/* @__PURE__ */ jsxs8(Group5, { gap: 6, wrap: "nowrap", style: { minWidth: 0 }, children: [
|
|
4956
|
+
/* @__PURE__ */ jsx10(Text5, { fw: 600, truncate: true, dir: "auto", children: strings.mapDialogTitle(place?.name) }),
|
|
4957
|
+
/* @__PURE__ */ jsx10(Tooltip3, { label: strings.mapHelp, children: /* @__PURE__ */ jsx10(ActionIcon2, { variant: "subtle", radius: "xl", "aria-label": strings.mapHelp, onClick: () => setHelpOpen(true), children: "\u24D8" }) })
|
|
4958
|
+
] }),
|
|
4959
|
+
/* @__PURE__ */ jsxs8(Group5, { gap: "xs", wrap: "nowrap", children: [
|
|
4960
|
+
/* @__PURE__ */ jsx10(Tooltip3, { label: strings.importShapesHint, multiline: true, w: 280, children: /* @__PURE__ */ jsx10(Button4, { variant: "default", size: "sm", onClick: () => shapesInput.current?.click(), children: strings.importShapes }) }),
|
|
4961
|
+
/* @__PURE__ */ jsx10(Button4, { variant: "default", size: "sm", onClick: onCancel, children: strings.cancel }),
|
|
4962
|
+
/* @__PURE__ */ jsx10(Button4, { size: "sm", onClick: done, children: strings.done })
|
|
4963
|
+
] })
|
|
4964
|
+
]
|
|
4965
|
+
}
|
|
4966
|
+
),
|
|
4967
|
+
message && /* @__PURE__ */ jsx10(
|
|
4968
|
+
Text5,
|
|
4969
|
+
{
|
|
4970
|
+
size: "sm",
|
|
4971
|
+
role: "alert",
|
|
4972
|
+
style: {
|
|
4973
|
+
position: "absolute",
|
|
4974
|
+
bottom: 12,
|
|
4975
|
+
left: 12,
|
|
4976
|
+
zIndex: 3,
|
|
4977
|
+
maxWidth: 480,
|
|
4978
|
+
padding: "6px 10px",
|
|
4979
|
+
borderRadius: 6,
|
|
4980
|
+
background: bar,
|
|
4981
|
+
color: "var(--mantine-color-red-filled)"
|
|
4982
|
+
},
|
|
4983
|
+
children: message
|
|
4984
|
+
}
|
|
4985
|
+
),
|
|
4986
|
+
/* @__PURE__ */ jsx10(
|
|
4987
|
+
"input",
|
|
4988
|
+
{
|
|
4989
|
+
ref: imageInput,
|
|
4990
|
+
type: "file",
|
|
4991
|
+
accept: "image/*",
|
|
4992
|
+
multiple: true,
|
|
4993
|
+
hidden: true,
|
|
4994
|
+
onChange: (e) => {
|
|
4995
|
+
const chosen = [...e.currentTarget.files ?? []];
|
|
4996
|
+
e.currentTarget.value = "";
|
|
4997
|
+
void addImages(chosen);
|
|
4998
|
+
}
|
|
4999
|
+
}
|
|
5000
|
+
),
|
|
5001
|
+
/* @__PURE__ */ jsx10(
|
|
5002
|
+
"input",
|
|
5003
|
+
{
|
|
5004
|
+
ref: shapesInput,
|
|
5005
|
+
type: "file",
|
|
5006
|
+
accept: ".kml,.gpx,.geojson,.json,application/vnd.google-earth.kml+xml,application/gpx+xml,application/geo+json",
|
|
5007
|
+
multiple: true,
|
|
5008
|
+
hidden: true,
|
|
5009
|
+
onChange: (e) => {
|
|
5010
|
+
const chosen = [...e.currentTarget.files ?? []];
|
|
5011
|
+
e.currentTarget.value = "";
|
|
5012
|
+
void importShapes(chosen);
|
|
5013
|
+
}
|
|
5014
|
+
}
|
|
5015
|
+
)
|
|
5016
|
+
] }),
|
|
5017
|
+
/* @__PURE__ */ jsx10(Modal, { opened: helpOpen, onClose: () => setHelpOpen(false), title: strings.mapHelp, size: "md", zIndex: 1e3, children: /* @__PURE__ */ jsx10(MapEditorHelp, { strings: mapStrings }) })
|
|
5018
|
+
] });
|
|
5019
|
+
}
|
|
5020
|
+
|
|
5021
|
+
// src/editor/placeSearch.ts
|
|
5022
|
+
var WIKIDATA_API = "https://www.wikidata.org/w/api.php";
|
|
5023
|
+
var WIKIDATA_ENTITY_DATA = "https://www.wikidata.org/wiki/Special:EntityData";
|
|
5024
|
+
function wikidataLanguage(language) {
|
|
5025
|
+
return (language ?? "").trim().toLowerCase().replace(/_/g, "-") || "en";
|
|
5026
|
+
}
|
|
5027
|
+
async function searchWikidataPlaces(term, language, signal) {
|
|
5028
|
+
const code = wikidataLanguage(language);
|
|
5029
|
+
const params = new URLSearchParams({
|
|
5030
|
+
action: "wbsearchentities",
|
|
5031
|
+
search: term,
|
|
5032
|
+
language: code,
|
|
5033
|
+
uselang: code,
|
|
5034
|
+
format: "json",
|
|
5035
|
+
limit: "7",
|
|
5036
|
+
origin: "*"
|
|
5037
|
+
// what makes an anonymous api.php GET answer cross-origin
|
|
5038
|
+
});
|
|
5039
|
+
const res = await fetch(`${WIKIDATA_API}?${params}`, { signal });
|
|
5040
|
+
if (!res.ok) throw new Error(`Wikidata search failed (${res.status})`);
|
|
5041
|
+
const data = await res.json();
|
|
5042
|
+
return (data.search ?? []).map((hit) => ({
|
|
5043
|
+
id: hit.id,
|
|
5044
|
+
label: hit.label ?? hit.id,
|
|
5045
|
+
...hit.description ? { description: hit.description } : {}
|
|
5046
|
+
}));
|
|
5047
|
+
}
|
|
5048
|
+
async function wikidataPoint(result, signal) {
|
|
5049
|
+
const res = await fetch(`${WIKIDATA_ENTITY_DATA}/${encodeURIComponent(result.id)}.json`, { signal });
|
|
5050
|
+
if (!res.ok) throw new Error(`Wikidata fetch failed for ${result.id} (${res.status})`);
|
|
5051
|
+
const data = await res.json();
|
|
5052
|
+
const entity = data.entities?.[result.id] ?? Object.values(data.entities ?? {})[0];
|
|
5053
|
+
const value = entity?.claims?.P625?.[0]?.mainsnak?.datavalue?.value;
|
|
5054
|
+
if (typeof value?.latitude !== "number" || typeof value.longitude !== "number") return null;
|
|
5055
|
+
return { lat: value.latitude, lon: value.longitude };
|
|
5056
|
+
}
|
|
5057
|
+
var WIKIDATA_PLACE_SEARCH = { search: searchWikidataPlaces, point: wikidataPoint };
|
|
5058
|
+
|
|
5059
|
+
// src/editor/SlideList.tsx
|
|
5060
|
+
import { useState as useState9 } from "react";
|
|
5061
|
+
import { Stack as Stack6, UnstyledButton as UnstyledButton2 } from "@mantine/core";
|
|
5062
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
3256
5063
|
function SlideList({ items, selected, onSelect, onMove, getKey, renderItem, label }) {
|
|
3257
|
-
const [dragging, setDragging] =
|
|
3258
|
-
const [over, setOver] =
|
|
5064
|
+
const [dragging, setDragging] = useState9(null);
|
|
5065
|
+
const [over, setOver] = useState9(null);
|
|
3259
5066
|
const end = () => {
|
|
3260
5067
|
setDragging(null);
|
|
3261
5068
|
setOver(null);
|
|
3262
5069
|
};
|
|
3263
|
-
return /* @__PURE__ */
|
|
5070
|
+
return /* @__PURE__ */ jsx11(Stack6, { gap: 4, role: "listbox", "aria-label": label, children: items.map((item, i) => {
|
|
3264
5071
|
const isSelected = i === selected;
|
|
3265
5072
|
const dropLine = over === i && dragging !== null && dragging !== i ? dragging < i ? { borderBottom: "2px solid var(--mantine-primary-color-filled)" } : { borderTop: "2px solid var(--mantine-primary-color-filled)" } : {};
|
|
3266
|
-
return /* @__PURE__ */
|
|
3267
|
-
|
|
5073
|
+
return /* @__PURE__ */ jsx11(
|
|
5074
|
+
UnstyledButton2,
|
|
3268
5075
|
{
|
|
3269
5076
|
role: "option",
|
|
3270
5077
|
"aria-selected": isSelected,
|
|
@@ -3303,9 +5110,9 @@ function SlideList({ items, selected, onSelect, onMove, getKey, renderItem, labe
|
|
|
3303
5110
|
}
|
|
3304
5111
|
|
|
3305
5112
|
// src/editor/StoryTextField.tsx
|
|
3306
|
-
import { useRef as
|
|
3307
|
-
import { Button as
|
|
3308
|
-
import { jsx as
|
|
5113
|
+
import { useRef as useRef10 } from "react";
|
|
5114
|
+
import { Button as Button5, Group as Group6, Stack as Stack7, Text as Text6, Textarea } from "@mantine/core";
|
|
5115
|
+
import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
3309
5116
|
function applyMarker(el, marker, value, onChange) {
|
|
3310
5117
|
const start = el.selectionStart ?? value.length;
|
|
3311
5118
|
const end = el.selectionEnd ?? value.length;
|
|
@@ -3323,12 +5130,12 @@ function applyMarker(el, marker, value, onChange) {
|
|
|
3323
5130
|
onChange(value.slice(0, start) + replacement + value.slice(end));
|
|
3324
5131
|
}
|
|
3325
5132
|
function StoryTextField({ label, value, onChange, boldLabel = "Bold", italicLabel = "Italic", minRows = 3 }) {
|
|
3326
|
-
const ref =
|
|
3327
|
-
return /* @__PURE__ */
|
|
3328
|
-
/* @__PURE__ */
|
|
3329
|
-
/* @__PURE__ */
|
|
3330
|
-
/* @__PURE__ */
|
|
3331
|
-
|
|
5133
|
+
const ref = useRef10(null);
|
|
5134
|
+
return /* @__PURE__ */ jsxs9(Stack7, { gap: 4, children: [
|
|
5135
|
+
/* @__PURE__ */ jsxs9(Group6, { gap: "xs", children: [
|
|
5136
|
+
/* @__PURE__ */ jsx12(Text6, { size: "sm", fw: 500, children: label }),
|
|
5137
|
+
/* @__PURE__ */ jsx12(
|
|
5138
|
+
Button5,
|
|
3332
5139
|
{
|
|
3333
5140
|
variant: "default",
|
|
3334
5141
|
size: "compact-xs",
|
|
@@ -3339,8 +5146,8 @@ function StoryTextField({ label, value, onChange, boldLabel = "Bold", italicLabe
|
|
|
3339
5146
|
children: "B"
|
|
3340
5147
|
}
|
|
3341
5148
|
),
|
|
3342
|
-
/* @__PURE__ */
|
|
3343
|
-
|
|
5149
|
+
/* @__PURE__ */ jsx12(
|
|
5150
|
+
Button5,
|
|
3344
5151
|
{
|
|
3345
5152
|
variant: "default",
|
|
3346
5153
|
size: "compact-xs",
|
|
@@ -3352,7 +5159,7 @@ function StoryTextField({ label, value, onChange, boldLabel = "Bold", italicLabe
|
|
|
3352
5159
|
}
|
|
3353
5160
|
)
|
|
3354
5161
|
] }),
|
|
3355
|
-
/* @__PURE__ */
|
|
5162
|
+
/* @__PURE__ */ jsx12(
|
|
3356
5163
|
Textarea,
|
|
3357
5164
|
{
|
|
3358
5165
|
ref,
|
|
@@ -3399,6 +5206,34 @@ var EDITOR_ENGLISH = {
|
|
|
3399
5206
|
matchDateAsWritten: "Work it out from the date",
|
|
3400
5207
|
noPreciseDate: "None: this slide stays off the timeline.",
|
|
3401
5208
|
place: "Place",
|
|
5209
|
+
placeName: "Name",
|
|
5210
|
+
removePlace: "Remove place",
|
|
5211
|
+
findPlace: "Find on Wikidata",
|
|
5212
|
+
findPlaceHint: "Looks the name up on Wikidata, to put the pin there. The name is all that's sent, and only when you ask.",
|
|
5213
|
+
findPlaceNone: "Wikidata has no place by that name.",
|
|
5214
|
+
findPlaceFailed: "Couldn't reach Wikidata.",
|
|
5215
|
+
findPlaceNoPoint: (name) => `Wikidata has no map position for ${name}.`,
|
|
5216
|
+
coordinates: "Latitude, longitude",
|
|
5217
|
+
coordinatesHint: "e.g. 47.3769, 8.5417",
|
|
5218
|
+
coordinatesInvalid: "Two numbers: latitude (-90 to 90), then longitude (-180 to 180).",
|
|
5219
|
+
placeMap: "Map: click to put the pin",
|
|
5220
|
+
placeMapHint: "Click the map to put the pin there, or drag the pin.",
|
|
5221
|
+
placeShape: "a shape",
|
|
5222
|
+
placeOverlays: (count) => count === 1 ? "a map overlay" : `${count} map overlays`,
|
|
5223
|
+
placeKeeps: (what) => `This place also has ${what}.`,
|
|
5224
|
+
editMap: "Edit map",
|
|
5225
|
+
editMapHint: "Shapes under the pin (a parish, a route) and scans of old maps laid over it.",
|
|
5226
|
+
mapDialogTitle: (placeName) => placeName ? `Map: ${placeName}` : "Map",
|
|
5227
|
+
mapHelp: "How to use this editor",
|
|
5228
|
+
importShapes: "Import shapes",
|
|
5229
|
+
importShapesHint: "Adds the points, lines and areas in a KML, GPX or GeoJSON file to this map.",
|
|
5230
|
+
importFailed: (fileName) => `Couldn't read ${fileName} as a KML, GPX or GeoJSON file.`,
|
|
5231
|
+
importNothing: (fileName) => `${fileName} has no shapes in it.`,
|
|
5232
|
+
overlayAlready: "That image is already on this map.",
|
|
5233
|
+
overlayDate: "Years it shows",
|
|
5234
|
+
overlayDateHint: "e.g. 1850, or 1850 to 1880. With historical maps, it's hidden on slides outside them.",
|
|
5235
|
+
cancel: "Cancel",
|
|
5236
|
+
done: "Done",
|
|
3402
5237
|
photos: "Photos",
|
|
3403
5238
|
photosHint: "Add, drop or paste a photo. It's resized for the screen, and hidden details like camera and location are left out.",
|
|
3404
5239
|
addPhoto: "Add photo",
|
|
@@ -3421,8 +5256,8 @@ var EDITOR_ENGLISH = {
|
|
|
3421
5256
|
};
|
|
3422
5257
|
|
|
3423
5258
|
// src/editor/StoryEditor.tsx
|
|
3424
|
-
import { jsx as
|
|
3425
|
-
var
|
|
5259
|
+
import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
5260
|
+
var BAR_HEIGHT3 = 56;
|
|
3426
5261
|
var STORY = -1;
|
|
3427
5262
|
function issueSlide(issue) {
|
|
3428
5263
|
const match = /^\/slides\/(\d+)/.exec(issue.path);
|
|
@@ -3435,8 +5270,8 @@ async function prepareImage(file) {
|
|
|
3435
5270
|
function Thumb({ doc, slide, resolveMedia }) {
|
|
3436
5271
|
const ref = slide.media?.[0]?.ref;
|
|
3437
5272
|
const item = ref && Object.prototype.hasOwnProperty.call(doc.media, ref) ? doc.media[ref] : void 0;
|
|
3438
|
-
const [url, setUrl] =
|
|
3439
|
-
|
|
5273
|
+
const [url, setUrl] = useState10(null);
|
|
5274
|
+
useEffect9(() => {
|
|
3440
5275
|
let live = true;
|
|
3441
5276
|
setUrl(null);
|
|
3442
5277
|
if (ref && item) void resolveMedia(ref, item).then((u) => {
|
|
@@ -3446,13 +5281,13 @@ function Thumb({ doc, slide, resolveMedia }) {
|
|
|
3446
5281
|
live = false;
|
|
3447
5282
|
};
|
|
3448
5283
|
}, [ref, item, resolveMedia]);
|
|
3449
|
-
return /* @__PURE__ */
|
|
3450
|
-
|
|
5284
|
+
return /* @__PURE__ */ jsx13(
|
|
5285
|
+
Box4,
|
|
3451
5286
|
{
|
|
3452
5287
|
w: 40,
|
|
3453
5288
|
h: 40,
|
|
3454
5289
|
style: { flex: "none", borderRadius: 4, background: "var(--mantine-color-default-hover)", overflow: "hidden" },
|
|
3455
|
-
children: url && /* @__PURE__ */
|
|
5290
|
+
children: url && /* @__PURE__ */ jsx13("img", { src: url, alt: "", style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } })
|
|
3456
5291
|
}
|
|
3457
5292
|
);
|
|
3458
5293
|
}
|
|
@@ -3466,24 +5301,26 @@ function StoryEditor({
|
|
|
3466
5301
|
dark = false,
|
|
3467
5302
|
strings: stringOverrides,
|
|
3468
5303
|
playerStrings,
|
|
3469
|
-
parseDateText = suggestEdtf
|
|
5304
|
+
parseDateText = suggestEdtf,
|
|
5305
|
+
placeSearch = WIKIDATA_PLACE_SEARCH,
|
|
5306
|
+
mapStrings
|
|
3470
5307
|
}) {
|
|
3471
|
-
const strings =
|
|
3472
|
-
const [history, setHistory] =
|
|
3473
|
-
const [selected, setSelected] =
|
|
3474
|
-
const [presenting, setPresenting] =
|
|
3475
|
-
const [problemsOpen, setProblemsOpen] =
|
|
5308
|
+
const strings = useMemo4(() => ({ ...EDITOR_ENGLISH, ...stringOverrides }), [stringOverrides]);
|
|
5309
|
+
const [history, setHistory] = useState10(() => startHistory(initialStory));
|
|
5310
|
+
const [selected, setSelected] = useState10(0);
|
|
5311
|
+
const [presenting, setPresenting] = useState10(false);
|
|
5312
|
+
const [problemsOpen, setProblemsOpen] = useState10(false);
|
|
3476
5313
|
const wide = useMediaQuery("(min-width: 1000px)", true);
|
|
3477
5314
|
const doc = history.present;
|
|
3478
|
-
const [files, setFiles] =
|
|
3479
|
-
const [busy, setBusy] =
|
|
3480
|
-
const [photoError, setPhotoError] =
|
|
3481
|
-
const docRef =
|
|
5315
|
+
const [files, setFiles] = useState10(() => new Map(initialFiles ?? []));
|
|
5316
|
+
const [busy, setBusy] = useState10(false);
|
|
5317
|
+
const [photoError, setPhotoError] = useState10(null);
|
|
5318
|
+
const docRef = useRef11(doc);
|
|
3482
5319
|
docRef.current = doc;
|
|
3483
|
-
const filesRef =
|
|
5320
|
+
const filesRef = useRef11(files);
|
|
3484
5321
|
filesRef.current = files;
|
|
3485
|
-
const urls =
|
|
3486
|
-
|
|
5322
|
+
const urls = useRef11(/* @__PURE__ */ new Map());
|
|
5323
|
+
useEffect9(() => () => {
|
|
3487
5324
|
for (const url of urls.current.values()) URL.revokeObjectURL(url);
|
|
3488
5325
|
}, []);
|
|
3489
5326
|
const resolveMedia = useCallback(async (_key, item) => {
|
|
@@ -3502,7 +5339,7 @@ function StoryEditor({
|
|
|
3502
5339
|
const apply = useCallback((next, key) => {
|
|
3503
5340
|
setHistory((h) => record(h, next, key));
|
|
3504
5341
|
}, []);
|
|
3505
|
-
|
|
5342
|
+
useEffect9(() => {
|
|
3506
5343
|
onChange?.(doc, files);
|
|
3507
5344
|
}, [doc, files, onChange]);
|
|
3508
5345
|
const index = selected === STORY ? STORY : Math.min(selected, doc.slides.length - 1);
|
|
@@ -3510,7 +5347,20 @@ function StoryEditor({
|
|
|
3510
5347
|
const setSlide = (patch, key) => {
|
|
3511
5348
|
if (index !== STORY) apply(updateSlide(history.present, index, patch), `${slide.id}:${key}`);
|
|
3512
5349
|
};
|
|
3513
|
-
const
|
|
5350
|
+
const setPlace = (slideId2, update, key) => {
|
|
5351
|
+
setHistory((h) => {
|
|
5352
|
+
const at = h.present.slides.findIndex((s) => s.id === slideId2);
|
|
5353
|
+
if (at < 0) return h;
|
|
5354
|
+
const before = h.present.slides[at].place;
|
|
5355
|
+
const after = update(before);
|
|
5356
|
+
return after === before ? h : record(h, updateSlide(h.present, at, { place: after }), `${slideId2}:${key}`);
|
|
5357
|
+
});
|
|
5358
|
+
};
|
|
5359
|
+
const [mapSlideId, setMapSlideId] = useState10(null);
|
|
5360
|
+
const mapSlide = mapSlideId ? doc.slides.find((s) => s.id === mapSlideId) : void 0;
|
|
5361
|
+
const placeChoices = useMemo4(() => storyPlaceChoices(doc, index), [doc, index]);
|
|
5362
|
+
const around = useMemo4(() => storyPlaces(doc.slides.map((s) => s.place))?.bounds ?? null, [doc.slides]);
|
|
5363
|
+
const issues = useMemo4(() => validate2(doc, files).errors, [doc, files]);
|
|
3514
5364
|
const addPhotos = useCallback(async (chosen, slideId2, replace) => {
|
|
3515
5365
|
setBusy(true);
|
|
3516
5366
|
setPhotoError(null);
|
|
@@ -3547,7 +5397,7 @@ function StoryEditor({
|
|
|
3547
5397
|
apply(setSlidePhotos(next, at, photos));
|
|
3548
5398
|
}, [apply, prepare, strings]);
|
|
3549
5399
|
const slideId = slide?.id;
|
|
3550
|
-
|
|
5400
|
+
useEffect9(() => {
|
|
3551
5401
|
if (!slideId) return;
|
|
3552
5402
|
const onPaste = (e) => {
|
|
3553
5403
|
if (e.target?.closest("input, textarea, [contenteditable=true]")) return;
|
|
@@ -3559,7 +5409,7 @@ function StoryEditor({
|
|
|
3559
5409
|
window.addEventListener("paste", onPaste);
|
|
3560
5410
|
return () => window.removeEventListener("paste", onPaste);
|
|
3561
5411
|
}, [slideId, addPhotos]);
|
|
3562
|
-
|
|
5412
|
+
useEffect9(() => {
|
|
3563
5413
|
const onKey = (e) => {
|
|
3564
5414
|
if (!(e.ctrlKey || e.metaKey) || e.altKey) return;
|
|
3565
5415
|
const target = e.target;
|
|
@@ -3576,9 +5426,9 @@ function StoryEditor({
|
|
|
3576
5426
|
window.addEventListener("keydown", onKey);
|
|
3577
5427
|
return () => window.removeEventListener("keydown", onKey);
|
|
3578
5428
|
}, []);
|
|
3579
|
-
const slideActions = slide && /* @__PURE__ */
|
|
3580
|
-
/* @__PURE__ */
|
|
3581
|
-
|
|
5429
|
+
const slideActions = slide && /* @__PURE__ */ jsxs10(Group7, { gap: 6, children: [
|
|
5430
|
+
/* @__PURE__ */ jsx13(Tooltip4, { label: strings.moveUp, children: /* @__PURE__ */ jsx13(
|
|
5431
|
+
ActionIcon3,
|
|
3582
5432
|
{
|
|
3583
5433
|
variant: "default",
|
|
3584
5434
|
"aria-label": strings.moveUp,
|
|
@@ -3590,8 +5440,8 @@ function StoryEditor({
|
|
|
3590
5440
|
children: "\u2191"
|
|
3591
5441
|
}
|
|
3592
5442
|
) }),
|
|
3593
|
-
/* @__PURE__ */
|
|
3594
|
-
|
|
5443
|
+
/* @__PURE__ */ jsx13(Tooltip4, { label: strings.moveDown, children: /* @__PURE__ */ jsx13(
|
|
5444
|
+
ActionIcon3,
|
|
3595
5445
|
{
|
|
3596
5446
|
variant: "default",
|
|
3597
5447
|
"aria-label": strings.moveDown,
|
|
@@ -3603,13 +5453,13 @@ function StoryEditor({
|
|
|
3603
5453
|
children: "\u2193"
|
|
3604
5454
|
}
|
|
3605
5455
|
) }),
|
|
3606
|
-
/* @__PURE__ */
|
|
5456
|
+
/* @__PURE__ */ jsx13(Button6, { variant: "default", size: "xs", onClick: () => {
|
|
3607
5457
|
const r = duplicateSlide(doc, index);
|
|
3608
5458
|
apply(r.doc);
|
|
3609
5459
|
setSelected(r.index);
|
|
3610
5460
|
}, children: strings.duplicateSlide }),
|
|
3611
|
-
/* @__PURE__ */
|
|
3612
|
-
|
|
5461
|
+
/* @__PURE__ */ jsx13(
|
|
5462
|
+
Button6,
|
|
3613
5463
|
{
|
|
3614
5464
|
variant: "default",
|
|
3615
5465
|
size: "xs",
|
|
@@ -3624,10 +5474,10 @@ function StoryEditor({
|
|
|
3624
5474
|
}
|
|
3625
5475
|
)
|
|
3626
5476
|
] });
|
|
3627
|
-
const storyForm = /* @__PURE__ */
|
|
3628
|
-
/* @__PURE__ */
|
|
3629
|
-
/* @__PURE__ */
|
|
3630
|
-
|
|
5477
|
+
const storyForm = /* @__PURE__ */ jsxs10(Stack8, { gap: "md", children: [
|
|
5478
|
+
/* @__PURE__ */ jsx13(Text7, { fw: 600, size: "lg", children: strings.story }),
|
|
5479
|
+
/* @__PURE__ */ jsx13(
|
|
5480
|
+
TextInput6,
|
|
3631
5481
|
{
|
|
3632
5482
|
label: strings.title,
|
|
3633
5483
|
required: true,
|
|
@@ -3635,8 +5485,8 @@ function StoryEditor({
|
|
|
3635
5485
|
onChange: (e) => apply(updateStory(doc, { title: e.currentTarget.value }), "story:title")
|
|
3636
5486
|
}
|
|
3637
5487
|
),
|
|
3638
|
-
/* @__PURE__ */
|
|
3639
|
-
|
|
5488
|
+
/* @__PURE__ */ jsx13(
|
|
5489
|
+
TextInput6,
|
|
3640
5490
|
{
|
|
3641
5491
|
label: strings.language,
|
|
3642
5492
|
description: strings.languageHint,
|
|
@@ -3645,7 +5495,7 @@ function StoryEditor({
|
|
|
3645
5495
|
onChange: (e) => apply(updateStory(doc, { language: e.currentTarget.value.trim() || void 0 }), "story:language")
|
|
3646
5496
|
}
|
|
3647
5497
|
),
|
|
3648
|
-
/* @__PURE__ */
|
|
5498
|
+
/* @__PURE__ */ jsx13(
|
|
3649
5499
|
Select,
|
|
3650
5500
|
{
|
|
3651
5501
|
label: strings.map,
|
|
@@ -3664,20 +5514,20 @@ function StoryEditor({
|
|
|
3664
5514
|
}
|
|
3665
5515
|
)
|
|
3666
5516
|
] });
|
|
3667
|
-
const slideForm = slide && /* @__PURE__ */
|
|
3668
|
-
/* @__PURE__ */
|
|
3669
|
-
/* @__PURE__ */
|
|
5517
|
+
const slideForm = slide && /* @__PURE__ */ jsxs10(Stack8, { gap: "md", children: [
|
|
5518
|
+
/* @__PURE__ */ jsxs10(Group7, { justify: "space-between", wrap: "wrap", gap: "xs", children: [
|
|
5519
|
+
/* @__PURE__ */ jsx13(Text7, { fw: 600, size: "lg", children: index === 0 ? strings.openingSlide : `${index + 1} / ${doc.slides.length}` }),
|
|
3670
5520
|
slideActions
|
|
3671
5521
|
] }),
|
|
3672
|
-
/* @__PURE__ */
|
|
3673
|
-
|
|
5522
|
+
/* @__PURE__ */ jsx13(
|
|
5523
|
+
TextInput6,
|
|
3674
5524
|
{
|
|
3675
5525
|
label: strings.slideTitle,
|
|
3676
5526
|
value: slide.title ?? "",
|
|
3677
5527
|
onChange: (e) => setSlide({ title: e.currentTarget.value || void 0 }, "title")
|
|
3678
5528
|
}
|
|
3679
5529
|
),
|
|
3680
|
-
/* @__PURE__ */
|
|
5530
|
+
/* @__PURE__ */ jsx13(
|
|
3681
5531
|
DateField,
|
|
3682
5532
|
{
|
|
3683
5533
|
value: slide.date,
|
|
@@ -3687,7 +5537,7 @@ function StoryEditor({
|
|
|
3687
5537
|
onChange: (date) => setSlide({ date }, "date")
|
|
3688
5538
|
}
|
|
3689
5539
|
),
|
|
3690
|
-
/* @__PURE__ */
|
|
5540
|
+
/* @__PURE__ */ jsx13(
|
|
3691
5541
|
StoryTextField,
|
|
3692
5542
|
{
|
|
3693
5543
|
label: strings.text,
|
|
@@ -3698,19 +5548,21 @@ function StoryEditor({
|
|
|
3698
5548
|
onChange: (text) => setSlide({ text: text || void 0 }, "text")
|
|
3699
5549
|
}
|
|
3700
5550
|
),
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
slide.place
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
5551
|
+
/* @__PURE__ */ jsx13(
|
|
5552
|
+
PlaceField,
|
|
5553
|
+
{
|
|
5554
|
+
place: slide.place,
|
|
5555
|
+
choices: placeChoices,
|
|
5556
|
+
around,
|
|
5557
|
+
language: doc.language,
|
|
5558
|
+
search: placeSearch,
|
|
5559
|
+
dark,
|
|
5560
|
+
strings,
|
|
5561
|
+
onChange: (update, key) => setPlace(slide.id, update, key),
|
|
5562
|
+
onEditMap: () => setMapSlideId(slide.id)
|
|
5563
|
+
}
|
|
5564
|
+
),
|
|
5565
|
+
/* @__PURE__ */ jsx13(
|
|
3714
5566
|
PhotosField,
|
|
3715
5567
|
{
|
|
3716
5568
|
photos: slide.media ?? [],
|
|
@@ -3723,22 +5575,22 @@ function StoryEditor({
|
|
|
3723
5575
|
onDescription: (key, alt) => apply(updateMedia(doc, key, { alt: alt || void 0 }), `media:${key}:alt`)
|
|
3724
5576
|
}
|
|
3725
5577
|
),
|
|
3726
|
-
photoError && /* @__PURE__ */
|
|
5578
|
+
photoError && /* @__PURE__ */ jsx13(Text7, { size: "xs", c: "red", role: "alert", children: photoError })
|
|
3727
5579
|
] }, slide.id);
|
|
3728
|
-
const list = /* @__PURE__ */
|
|
3729
|
-
/* @__PURE__ */
|
|
3730
|
-
|
|
5580
|
+
const list = /* @__PURE__ */ jsxs10(Stack8, { gap: "xs", children: [
|
|
5581
|
+
/* @__PURE__ */ jsxs10(
|
|
5582
|
+
UnstyledButton3,
|
|
3731
5583
|
{
|
|
3732
5584
|
onClick: () => setSelected(STORY),
|
|
3733
5585
|
style: { padding: "8px", borderRadius: 6, background: index === STORY ? "var(--mantine-primary-color-light)" : void 0 },
|
|
3734
5586
|
children: [
|
|
3735
|
-
/* @__PURE__ */
|
|
3736
|
-
/* @__PURE__ */
|
|
5587
|
+
/* @__PURE__ */ jsx13(Text7, { fw: 600, size: "sm", children: strings.story }),
|
|
5588
|
+
/* @__PURE__ */ jsx13(Text7, { size: "xs", c: "dimmed", children: strings.storySettings })
|
|
3737
5589
|
]
|
|
3738
5590
|
}
|
|
3739
5591
|
),
|
|
3740
|
-
/* @__PURE__ */
|
|
3741
|
-
/* @__PURE__ */
|
|
5592
|
+
/* @__PURE__ */ jsx13(Divider2, { label: strings.slides, labelPosition: "left" }),
|
|
5593
|
+
/* @__PURE__ */ jsx13(
|
|
3742
5594
|
SlideList,
|
|
3743
5595
|
{
|
|
3744
5596
|
label: strings.slides,
|
|
@@ -3750,17 +5602,17 @@ function StoryEditor({
|
|
|
3750
5602
|
setSelected(to);
|
|
3751
5603
|
},
|
|
3752
5604
|
getKey: (s) => s.id,
|
|
3753
|
-
renderItem: (s, i) => /* @__PURE__ */
|
|
3754
|
-
/* @__PURE__ */
|
|
3755
|
-
/* @__PURE__ */
|
|
3756
|
-
/* @__PURE__ */
|
|
3757
|
-
/* @__PURE__ */
|
|
5605
|
+
renderItem: (s, i) => /* @__PURE__ */ jsxs10(Group7, { gap: "sm", wrap: "nowrap", children: [
|
|
5606
|
+
/* @__PURE__ */ jsx13(Thumb, { doc, slide: s, resolveMedia }),
|
|
5607
|
+
/* @__PURE__ */ jsxs10(Box4, { style: { minWidth: 0 }, children: [
|
|
5608
|
+
/* @__PURE__ */ jsx13(Text7, { size: "sm", fw: 500, truncate: true, dir: "auto", c: s.title ? void 0 : "dimmed", children: s.title || strings.untitledSlide }),
|
|
5609
|
+
/* @__PURE__ */ jsx13(Text7, { size: "xs", c: "dimmed", truncate: true, children: i === 0 ? strings.openingSlide : s.date?.text ?? "" })
|
|
3758
5610
|
] })
|
|
3759
5611
|
] })
|
|
3760
5612
|
}
|
|
3761
5613
|
),
|
|
3762
|
-
/* @__PURE__ */
|
|
3763
|
-
|
|
5614
|
+
/* @__PURE__ */ jsxs10(
|
|
5615
|
+
Button6,
|
|
3764
5616
|
{
|
|
3765
5617
|
variant: "light",
|
|
3766
5618
|
size: "xs",
|
|
@@ -3777,7 +5629,7 @@ function StoryEditor({
|
|
|
3777
5629
|
}
|
|
3778
5630
|
)
|
|
3779
5631
|
] });
|
|
3780
|
-
const preview = /* @__PURE__ */
|
|
5632
|
+
const preview = /* @__PURE__ */ jsx13(Box4, { style: { position: "relative", height: wide ? "100%" : 460, borderRadius: 8, overflow: "hidden", border: "1px solid var(--mantine-color-default-border)" }, children: /* @__PURE__ */ jsx13(
|
|
3781
5633
|
StoryPlayer,
|
|
3782
5634
|
{
|
|
3783
5635
|
story: doc,
|
|
@@ -3791,8 +5643,8 @@ function StoryEditor({
|
|
|
3791
5643
|
}
|
|
3792
5644
|
}
|
|
3793
5645
|
) });
|
|
3794
|
-
const column = (children, width) => /* @__PURE__ */
|
|
3795
|
-
|
|
5646
|
+
const column = (children, width) => /* @__PURE__ */ jsx13(
|
|
5647
|
+
Box4,
|
|
3796
5648
|
{
|
|
3797
5649
|
style: {
|
|
3798
5650
|
width,
|
|
@@ -3805,32 +5657,32 @@ function StoryEditor({
|
|
|
3805
5657
|
children
|
|
3806
5658
|
}
|
|
3807
5659
|
);
|
|
3808
|
-
return /* @__PURE__ */
|
|
3809
|
-
/* @__PURE__ */
|
|
3810
|
-
|
|
5660
|
+
return /* @__PURE__ */ jsxs10(Box4, { style: { height: "100%", display: "flex", flexDirection: "column" }, children: [
|
|
5661
|
+
/* @__PURE__ */ jsxs10(
|
|
5662
|
+
Group7,
|
|
3811
5663
|
{
|
|
3812
|
-
h:
|
|
5664
|
+
h: BAR_HEIGHT3,
|
|
3813
5665
|
px: "md",
|
|
3814
5666
|
gap: "sm",
|
|
3815
5667
|
wrap: "nowrap",
|
|
3816
5668
|
justify: "space-between",
|
|
3817
5669
|
style: { flex: "none", borderBottom: "1px solid var(--mantine-color-default-border)" },
|
|
3818
5670
|
children: [
|
|
3819
|
-
/* @__PURE__ */
|
|
5671
|
+
/* @__PURE__ */ jsxs10(Group7, { gap: "sm", wrap: "nowrap", style: { minWidth: 0 }, children: [
|
|
3820
5672
|
brand,
|
|
3821
|
-
/* @__PURE__ */
|
|
5673
|
+
/* @__PURE__ */ jsx13(Text7, { fw: 600, truncate: true, dir: "auto", children: doc.title })
|
|
3822
5674
|
] }),
|
|
3823
|
-
/* @__PURE__ */
|
|
3824
|
-
issues.length > 0 && /* @__PURE__ */
|
|
3825
|
-
/* @__PURE__ */
|
|
3826
|
-
/* @__PURE__ */
|
|
5675
|
+
/* @__PURE__ */ jsxs10(Group7, { gap: "xs", wrap: "nowrap", children: [
|
|
5676
|
+
issues.length > 0 && /* @__PURE__ */ jsxs10(Popover, { opened: problemsOpen, onChange: setProblemsOpen, width: 360, position: "bottom-end", withArrow: true, children: [
|
|
5677
|
+
/* @__PURE__ */ jsx13(Popover.Target, { children: /* @__PURE__ */ jsx13(Badge, { color: "red", variant: "light", style: { cursor: "pointer" }, onClick: () => setProblemsOpen((o) => !o), children: strings.problems(issues.length) }) }),
|
|
5678
|
+
/* @__PURE__ */ jsx13(Popover.Dropdown, { children: /* @__PURE__ */ jsx13(Stack8, { gap: 6, children: issues.map((issue, i) => {
|
|
3827
5679
|
const at = issueSlide(issue);
|
|
3828
|
-
return /* @__PURE__ */
|
|
5680
|
+
return /* @__PURE__ */ jsxs10(UnstyledButton3, { onClick: () => {
|
|
3829
5681
|
setSelected(at);
|
|
3830
5682
|
setProblemsOpen(false);
|
|
3831
5683
|
}, children: [
|
|
3832
|
-
/* @__PURE__ */
|
|
3833
|
-
/* @__PURE__ */
|
|
5684
|
+
/* @__PURE__ */ jsx13(Text7, { size: "sm", fw: 500, children: at === STORY ? strings.story : doc.slides[at]?.title || `${strings.untitledSlide} ${at + 1}` }),
|
|
5685
|
+
/* @__PURE__ */ jsxs10(Text7, { size: "xs", c: "dimmed", children: [
|
|
3834
5686
|
issue.path,
|
|
3835
5687
|
" ",
|
|
3836
5688
|
issue.message
|
|
@@ -3838,16 +5690,16 @@ function StoryEditor({
|
|
|
3838
5690
|
] }, i);
|
|
3839
5691
|
}) }) })
|
|
3840
5692
|
] }),
|
|
3841
|
-
/* @__PURE__ */
|
|
3842
|
-
/* @__PURE__ */
|
|
3843
|
-
/* @__PURE__ */
|
|
5693
|
+
/* @__PURE__ */ jsx13(Tooltip4, { label: strings.undo, children: /* @__PURE__ */ jsx13(ActionIcon3, { variant: "default", size: "lg", "aria-label": strings.undo, disabled: history.past.length === 0, onClick: () => setHistory(undo), children: "\u21B6" }) }),
|
|
5694
|
+
/* @__PURE__ */ jsx13(Tooltip4, { label: strings.redo, children: /* @__PURE__ */ jsx13(ActionIcon3, { variant: "default", size: "lg", "aria-label": strings.redo, disabled: history.future.length === 0, onClick: () => setHistory(redo), children: "\u21B7" }) }),
|
|
5695
|
+
/* @__PURE__ */ jsx13(Button6, { variant: "default", onClick: () => setPresenting(true), children: strings.present }),
|
|
3844
5696
|
toolbar
|
|
3845
5697
|
] })
|
|
3846
5698
|
]
|
|
3847
5699
|
}
|
|
3848
5700
|
),
|
|
3849
|
-
/* @__PURE__ */
|
|
3850
|
-
|
|
5701
|
+
/* @__PURE__ */ jsxs10(
|
|
5702
|
+
Box4,
|
|
3851
5703
|
{
|
|
3852
5704
|
style: {
|
|
3853
5705
|
flex: 1,
|
|
@@ -3863,7 +5715,27 @@ function StoryEditor({
|
|
|
3863
5715
|
]
|
|
3864
5716
|
}
|
|
3865
5717
|
),
|
|
3866
|
-
|
|
5718
|
+
mapSlide && /* @__PURE__ */ jsx13(
|
|
5719
|
+
PlaceMapDialog,
|
|
5720
|
+
{
|
|
5721
|
+
place: mapSlide.place,
|
|
5722
|
+
doc,
|
|
5723
|
+
files,
|
|
5724
|
+
dark,
|
|
5725
|
+
parseDateText,
|
|
5726
|
+
strings,
|
|
5727
|
+
mapStrings,
|
|
5728
|
+
onCancel: () => setMapSlideId(null),
|
|
5729
|
+
onDone: (place, media, nextFiles) => {
|
|
5730
|
+
const at = doc.slides.findIndex((s) => s.id === mapSlide.id);
|
|
5731
|
+
setMapSlideId(null);
|
|
5732
|
+
if (at < 0) return;
|
|
5733
|
+
setFiles(nextFiles);
|
|
5734
|
+
apply(updateSlide({ ...doc, media }, at, { place }));
|
|
5735
|
+
}
|
|
5736
|
+
}
|
|
5737
|
+
),
|
|
5738
|
+
/* @__PURE__ */ jsx13(Modal2, { opened: presenting, onClose: () => setPresenting(false), fullScreen: true, withCloseButton: false, padding: 0, children: /* @__PURE__ */ jsx13(Box4, { style: { position: "fixed", inset: 0 }, children: /* @__PURE__ */ jsx13(
|
|
3867
5739
|
StoryPlayer,
|
|
3868
5740
|
{
|
|
3869
5741
|
story: doc,
|
|
@@ -3880,31 +5752,47 @@ export {
|
|
|
3880
5752
|
COALESCE_MS,
|
|
3881
5753
|
DateField,
|
|
3882
5754
|
EDITOR_ENGLISH,
|
|
5755
|
+
MAP_EDITOR_ENGLISH,
|
|
5756
|
+
MapEditor,
|
|
5757
|
+
MapEditorHelp,
|
|
3883
5758
|
PhotosField,
|
|
5759
|
+
PlaceField,
|
|
5760
|
+
PlaceMapDialog,
|
|
3884
5761
|
SlideList,
|
|
3885
5762
|
StoryEditor,
|
|
3886
5763
|
StoryTextField,
|
|
5764
|
+
WIKIDATA_PLACE_SEARCH,
|
|
3887
5765
|
addImage,
|
|
3888
5766
|
addSlide,
|
|
3889
5767
|
blankStory,
|
|
3890
5768
|
deleteSlide,
|
|
3891
5769
|
duplicateSlide,
|
|
5770
|
+
editorToPlace,
|
|
3892
5771
|
findMedia,
|
|
5772
|
+
formatLatLon,
|
|
3893
5773
|
moveSlide,
|
|
3894
5774
|
nextSlideId,
|
|
5775
|
+
parseLatLon,
|
|
5776
|
+
patchPlace,
|
|
5777
|
+
placeFeatures,
|
|
5778
|
+
placeToEditor,
|
|
3895
5779
|
prepareImage,
|
|
3896
5780
|
pruneMedia,
|
|
3897
5781
|
record,
|
|
3898
5782
|
redo,
|
|
5783
|
+
roundDegrees,
|
|
5784
|
+
searchWikidataPlaces,
|
|
3899
5785
|
setSlidePhotos,
|
|
3900
5786
|
slideDate,
|
|
3901
5787
|
startHistory,
|
|
3902
5788
|
storyFileName,
|
|
5789
|
+
storyPlaceChoices,
|
|
3903
5790
|
suggestEdtf,
|
|
3904
5791
|
undo,
|
|
3905
5792
|
updateMedia,
|
|
3906
5793
|
updateSlide,
|
|
3907
5794
|
updateStory,
|
|
3908
|
-
usedMedia
|
|
5795
|
+
usedMedia,
|
|
5796
|
+
wikidataPoint
|
|
3909
5797
|
};
|
|
3910
5798
|
//# sourceMappingURL=editor.js.map
|