@gramps-connect/story 0.4.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 +13 -4
- package/dist/editor.js +1710 -121
- 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 +4 -1
- package/dist/types/editor/PlaceMapDialog.d.ts +18 -0
- package/dist/types/editor/StoryEditor.d.ts +4 -1
- package/dist/types/editor/index.d.ts +4 -0
- package/dist/types/editor/mapEditorStrings.d.ts +50 -0
- package/dist/types/editor/placeShapes.d.ts +13 -0
- package/dist/types/editor/strings.d.ts +13 -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 +12 -2
- package/src/editor/PlaceMapDialog.tsx +228 -0
- package/src/editor/StoryEditor.tsx +24 -1
- package/src/editor/index.ts +6 -0
- package/src/editor/mapEditorStrings.ts +109 -0
- package/src/editor/placeShapes.ts +63 -0
- package/src/editor/strings.ts +27 -1
- 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,19 +1,19 @@
|
|
|
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
|
|
13
|
+
Stack as Stack8,
|
|
14
|
+
Text as Text7,
|
|
15
|
+
TextInput as TextInput6,
|
|
16
|
+
Tooltip as Tooltip4,
|
|
17
17
|
UnstyledButton as UnstyledButton3
|
|
18
18
|
} from "@mantine/core";
|
|
19
19
|
import { useMediaQuery } from "@mantine/hooks";
|
|
@@ -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,
|
|
@@ -3345,7 +3347,7 @@ function PlaceMap({ point, around, dark = false, label, onPick }) {
|
|
|
3345
3347
|
|
|
3346
3348
|
// src/editor/PlaceField.tsx
|
|
3347
3349
|
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3348
|
-
function PlaceField({ place, choices, around, language, search, dark, strings, onChange }) {
|
|
3350
|
+
function PlaceField({ place, choices, around, language, search, dark, strings, onChange, onEditMap }) {
|
|
3349
3351
|
const lat = place?.lat;
|
|
3350
3352
|
const lon = place?.lon;
|
|
3351
3353
|
const point = lat !== void 0 && lon !== void 0 ? [lon, lat] : null;
|
|
@@ -3469,7 +3471,1550 @@ function PlaceField({ place, choices, around, language, search, dark, strings, o
|
|
|
3469
3471
|
}
|
|
3470
3472
|
),
|
|
3471
3473
|
/* @__PURE__ */ jsx8(Text3, { size: "xs", c: "dimmed", children: strings.placeMapHint }),
|
|
3472
|
-
keeps.length > 0 && /* @__PURE__ */ jsx8(Text3, { size: "xs", c: "dimmed", children: strings.placeKeeps(keeps.join(", ")) })
|
|
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 }) })
|
|
3473
5018
|
] });
|
|
3474
5019
|
}
|
|
3475
5020
|
|
|
@@ -3512,20 +5057,20 @@ async function wikidataPoint(result, signal) {
|
|
|
3512
5057
|
var WIKIDATA_PLACE_SEARCH = { search: searchWikidataPlaces, point: wikidataPoint };
|
|
3513
5058
|
|
|
3514
5059
|
// src/editor/SlideList.tsx
|
|
3515
|
-
import { useState as
|
|
3516
|
-
import { Stack as
|
|
3517
|
-
import { jsx as
|
|
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";
|
|
3518
5063
|
function SlideList({ items, selected, onSelect, onMove, getKey, renderItem, label }) {
|
|
3519
|
-
const [dragging, setDragging] =
|
|
3520
|
-
const [over, setOver] =
|
|
5064
|
+
const [dragging, setDragging] = useState9(null);
|
|
5065
|
+
const [over, setOver] = useState9(null);
|
|
3521
5066
|
const end = () => {
|
|
3522
5067
|
setDragging(null);
|
|
3523
5068
|
setOver(null);
|
|
3524
5069
|
};
|
|
3525
|
-
return /* @__PURE__ */
|
|
5070
|
+
return /* @__PURE__ */ jsx11(Stack6, { gap: 4, role: "listbox", "aria-label": label, children: items.map((item, i) => {
|
|
3526
5071
|
const isSelected = i === selected;
|
|
3527
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)" } : {};
|
|
3528
|
-
return /* @__PURE__ */
|
|
5073
|
+
return /* @__PURE__ */ jsx11(
|
|
3529
5074
|
UnstyledButton2,
|
|
3530
5075
|
{
|
|
3531
5076
|
role: "option",
|
|
@@ -3565,9 +5110,9 @@ function SlideList({ items, selected, onSelect, onMove, getKey, renderItem, labe
|
|
|
3565
5110
|
}
|
|
3566
5111
|
|
|
3567
5112
|
// src/editor/StoryTextField.tsx
|
|
3568
|
-
import { useRef as
|
|
3569
|
-
import { Button as
|
|
3570
|
-
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";
|
|
3571
5116
|
function applyMarker(el, marker, value, onChange) {
|
|
3572
5117
|
const start = el.selectionStart ?? value.length;
|
|
3573
5118
|
const end = el.selectionEnd ?? value.length;
|
|
@@ -3585,12 +5130,12 @@ function applyMarker(el, marker, value, onChange) {
|
|
|
3585
5130
|
onChange(value.slice(0, start) + replacement + value.slice(end));
|
|
3586
5131
|
}
|
|
3587
5132
|
function StoryTextField({ label, value, onChange, boldLabel = "Bold", italicLabel = "Italic", minRows = 3 }) {
|
|
3588
|
-
const ref =
|
|
3589
|
-
return /* @__PURE__ */
|
|
3590
|
-
/* @__PURE__ */
|
|
3591
|
-
/* @__PURE__ */
|
|
3592
|
-
/* @__PURE__ */
|
|
3593
|
-
|
|
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,
|
|
3594
5139
|
{
|
|
3595
5140
|
variant: "default",
|
|
3596
5141
|
size: "compact-xs",
|
|
@@ -3601,8 +5146,8 @@ function StoryTextField({ label, value, onChange, boldLabel = "Bold", italicLabe
|
|
|
3601
5146
|
children: "B"
|
|
3602
5147
|
}
|
|
3603
5148
|
),
|
|
3604
|
-
/* @__PURE__ */
|
|
3605
|
-
|
|
5149
|
+
/* @__PURE__ */ jsx12(
|
|
5150
|
+
Button5,
|
|
3606
5151
|
{
|
|
3607
5152
|
variant: "default",
|
|
3608
5153
|
size: "compact-xs",
|
|
@@ -3614,7 +5159,7 @@ function StoryTextField({ label, value, onChange, boldLabel = "Bold", italicLabe
|
|
|
3614
5159
|
}
|
|
3615
5160
|
)
|
|
3616
5161
|
] }),
|
|
3617
|
-
/* @__PURE__ */
|
|
5162
|
+
/* @__PURE__ */ jsx12(
|
|
3618
5163
|
Textarea,
|
|
3619
5164
|
{
|
|
3620
5165
|
ref,
|
|
@@ -3675,7 +5220,20 @@ var EDITOR_ENGLISH = {
|
|
|
3675
5220
|
placeMapHint: "Click the map to put the pin there, or drag the pin.",
|
|
3676
5221
|
placeShape: "a shape",
|
|
3677
5222
|
placeOverlays: (count) => count === 1 ? "a map overlay" : `${count} map overlays`,
|
|
3678
|
-
placeKeeps: (what) => `This place also has ${what}
|
|
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",
|
|
3679
5237
|
photos: "Photos",
|
|
3680
5238
|
photosHint: "Add, drop or paste a photo. It's resized for the screen, and hidden details like camera and location are left out.",
|
|
3681
5239
|
addPhoto: "Add photo",
|
|
@@ -3698,8 +5256,8 @@ var EDITOR_ENGLISH = {
|
|
|
3698
5256
|
};
|
|
3699
5257
|
|
|
3700
5258
|
// src/editor/StoryEditor.tsx
|
|
3701
|
-
import { jsx as
|
|
3702
|
-
var
|
|
5259
|
+
import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
5260
|
+
var BAR_HEIGHT3 = 56;
|
|
3703
5261
|
var STORY = -1;
|
|
3704
5262
|
function issueSlide(issue) {
|
|
3705
5263
|
const match = /^\/slides\/(\d+)/.exec(issue.path);
|
|
@@ -3712,8 +5270,8 @@ async function prepareImage(file) {
|
|
|
3712
5270
|
function Thumb({ doc, slide, resolveMedia }) {
|
|
3713
5271
|
const ref = slide.media?.[0]?.ref;
|
|
3714
5272
|
const item = ref && Object.prototype.hasOwnProperty.call(doc.media, ref) ? doc.media[ref] : void 0;
|
|
3715
|
-
const [url, setUrl] =
|
|
3716
|
-
|
|
5273
|
+
const [url, setUrl] = useState10(null);
|
|
5274
|
+
useEffect9(() => {
|
|
3717
5275
|
let live = true;
|
|
3718
5276
|
setUrl(null);
|
|
3719
5277
|
if (ref && item) void resolveMedia(ref, item).then((u) => {
|
|
@@ -3723,13 +5281,13 @@ function Thumb({ doc, slide, resolveMedia }) {
|
|
|
3723
5281
|
live = false;
|
|
3724
5282
|
};
|
|
3725
5283
|
}, [ref, item, resolveMedia]);
|
|
3726
|
-
return /* @__PURE__ */
|
|
3727
|
-
|
|
5284
|
+
return /* @__PURE__ */ jsx13(
|
|
5285
|
+
Box4,
|
|
3728
5286
|
{
|
|
3729
5287
|
w: 40,
|
|
3730
5288
|
h: 40,
|
|
3731
5289
|
style: { flex: "none", borderRadius: 4, background: "var(--mantine-color-default-hover)", overflow: "hidden" },
|
|
3732
|
-
children: url && /* @__PURE__ */
|
|
5290
|
+
children: url && /* @__PURE__ */ jsx13("img", { src: url, alt: "", style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } })
|
|
3733
5291
|
}
|
|
3734
5292
|
);
|
|
3735
5293
|
}
|
|
@@ -3744,24 +5302,25 @@ function StoryEditor({
|
|
|
3744
5302
|
strings: stringOverrides,
|
|
3745
5303
|
playerStrings,
|
|
3746
5304
|
parseDateText = suggestEdtf,
|
|
3747
|
-
placeSearch = WIKIDATA_PLACE_SEARCH
|
|
5305
|
+
placeSearch = WIKIDATA_PLACE_SEARCH,
|
|
5306
|
+
mapStrings
|
|
3748
5307
|
}) {
|
|
3749
|
-
const strings =
|
|
3750
|
-
const [history, setHistory] =
|
|
3751
|
-
const [selected, setSelected] =
|
|
3752
|
-
const [presenting, setPresenting] =
|
|
3753
|
-
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);
|
|
3754
5313
|
const wide = useMediaQuery("(min-width: 1000px)", true);
|
|
3755
5314
|
const doc = history.present;
|
|
3756
|
-
const [files, setFiles] =
|
|
3757
|
-
const [busy, setBusy] =
|
|
3758
|
-
const [photoError, setPhotoError] =
|
|
3759
|
-
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);
|
|
3760
5319
|
docRef.current = doc;
|
|
3761
|
-
const filesRef =
|
|
5320
|
+
const filesRef = useRef11(files);
|
|
3762
5321
|
filesRef.current = files;
|
|
3763
|
-
const urls =
|
|
3764
|
-
|
|
5322
|
+
const urls = useRef11(/* @__PURE__ */ new Map());
|
|
5323
|
+
useEffect9(() => () => {
|
|
3765
5324
|
for (const url of urls.current.values()) URL.revokeObjectURL(url);
|
|
3766
5325
|
}, []);
|
|
3767
5326
|
const resolveMedia = useCallback(async (_key, item) => {
|
|
@@ -3780,7 +5339,7 @@ function StoryEditor({
|
|
|
3780
5339
|
const apply = useCallback((next, key) => {
|
|
3781
5340
|
setHistory((h) => record(h, next, key));
|
|
3782
5341
|
}, []);
|
|
3783
|
-
|
|
5342
|
+
useEffect9(() => {
|
|
3784
5343
|
onChange?.(doc, files);
|
|
3785
5344
|
}, [doc, files, onChange]);
|
|
3786
5345
|
const index = selected === STORY ? STORY : Math.min(selected, doc.slides.length - 1);
|
|
@@ -3797,9 +5356,11 @@ function StoryEditor({
|
|
|
3797
5356
|
return after === before ? h : record(h, updateSlide(h.present, at, { place: after }), `${slideId2}:${key}`);
|
|
3798
5357
|
});
|
|
3799
5358
|
};
|
|
3800
|
-
const
|
|
3801
|
-
const
|
|
3802
|
-
const
|
|
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]);
|
|
3803
5364
|
const addPhotos = useCallback(async (chosen, slideId2, replace) => {
|
|
3804
5365
|
setBusy(true);
|
|
3805
5366
|
setPhotoError(null);
|
|
@@ -3836,7 +5397,7 @@ function StoryEditor({
|
|
|
3836
5397
|
apply(setSlidePhotos(next, at, photos));
|
|
3837
5398
|
}, [apply, prepare, strings]);
|
|
3838
5399
|
const slideId = slide?.id;
|
|
3839
|
-
|
|
5400
|
+
useEffect9(() => {
|
|
3840
5401
|
if (!slideId) return;
|
|
3841
5402
|
const onPaste = (e) => {
|
|
3842
5403
|
if (e.target?.closest("input, textarea, [contenteditable=true]")) return;
|
|
@@ -3848,7 +5409,7 @@ function StoryEditor({
|
|
|
3848
5409
|
window.addEventListener("paste", onPaste);
|
|
3849
5410
|
return () => window.removeEventListener("paste", onPaste);
|
|
3850
5411
|
}, [slideId, addPhotos]);
|
|
3851
|
-
|
|
5412
|
+
useEffect9(() => {
|
|
3852
5413
|
const onKey = (e) => {
|
|
3853
5414
|
if (!(e.ctrlKey || e.metaKey) || e.altKey) return;
|
|
3854
5415
|
const target = e.target;
|
|
@@ -3865,9 +5426,9 @@ function StoryEditor({
|
|
|
3865
5426
|
window.addEventListener("keydown", onKey);
|
|
3866
5427
|
return () => window.removeEventListener("keydown", onKey);
|
|
3867
5428
|
}, []);
|
|
3868
|
-
const slideActions = slide && /* @__PURE__ */
|
|
3869
|
-
/* @__PURE__ */
|
|
3870
|
-
|
|
5429
|
+
const slideActions = slide && /* @__PURE__ */ jsxs10(Group7, { gap: 6, children: [
|
|
5430
|
+
/* @__PURE__ */ jsx13(Tooltip4, { label: strings.moveUp, children: /* @__PURE__ */ jsx13(
|
|
5431
|
+
ActionIcon3,
|
|
3871
5432
|
{
|
|
3872
5433
|
variant: "default",
|
|
3873
5434
|
"aria-label": strings.moveUp,
|
|
@@ -3879,8 +5440,8 @@ function StoryEditor({
|
|
|
3879
5440
|
children: "\u2191"
|
|
3880
5441
|
}
|
|
3881
5442
|
) }),
|
|
3882
|
-
/* @__PURE__ */
|
|
3883
|
-
|
|
5443
|
+
/* @__PURE__ */ jsx13(Tooltip4, { label: strings.moveDown, children: /* @__PURE__ */ jsx13(
|
|
5444
|
+
ActionIcon3,
|
|
3884
5445
|
{
|
|
3885
5446
|
variant: "default",
|
|
3886
5447
|
"aria-label": strings.moveDown,
|
|
@@ -3892,13 +5453,13 @@ function StoryEditor({
|
|
|
3892
5453
|
children: "\u2193"
|
|
3893
5454
|
}
|
|
3894
5455
|
) }),
|
|
3895
|
-
/* @__PURE__ */
|
|
5456
|
+
/* @__PURE__ */ jsx13(Button6, { variant: "default", size: "xs", onClick: () => {
|
|
3896
5457
|
const r = duplicateSlide(doc, index);
|
|
3897
5458
|
apply(r.doc);
|
|
3898
5459
|
setSelected(r.index);
|
|
3899
5460
|
}, children: strings.duplicateSlide }),
|
|
3900
|
-
/* @__PURE__ */
|
|
3901
|
-
|
|
5461
|
+
/* @__PURE__ */ jsx13(
|
|
5462
|
+
Button6,
|
|
3902
5463
|
{
|
|
3903
5464
|
variant: "default",
|
|
3904
5465
|
size: "xs",
|
|
@@ -3913,10 +5474,10 @@ function StoryEditor({
|
|
|
3913
5474
|
}
|
|
3914
5475
|
)
|
|
3915
5476
|
] });
|
|
3916
|
-
const storyForm = /* @__PURE__ */
|
|
3917
|
-
/* @__PURE__ */
|
|
3918
|
-
/* @__PURE__ */
|
|
3919
|
-
|
|
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,
|
|
3920
5481
|
{
|
|
3921
5482
|
label: strings.title,
|
|
3922
5483
|
required: true,
|
|
@@ -3924,8 +5485,8 @@ function StoryEditor({
|
|
|
3924
5485
|
onChange: (e) => apply(updateStory(doc, { title: e.currentTarget.value }), "story:title")
|
|
3925
5486
|
}
|
|
3926
5487
|
),
|
|
3927
|
-
/* @__PURE__ */
|
|
3928
|
-
|
|
5488
|
+
/* @__PURE__ */ jsx13(
|
|
5489
|
+
TextInput6,
|
|
3929
5490
|
{
|
|
3930
5491
|
label: strings.language,
|
|
3931
5492
|
description: strings.languageHint,
|
|
@@ -3934,7 +5495,7 @@ function StoryEditor({
|
|
|
3934
5495
|
onChange: (e) => apply(updateStory(doc, { language: e.currentTarget.value.trim() || void 0 }), "story:language")
|
|
3935
5496
|
}
|
|
3936
5497
|
),
|
|
3937
|
-
/* @__PURE__ */
|
|
5498
|
+
/* @__PURE__ */ jsx13(
|
|
3938
5499
|
Select,
|
|
3939
5500
|
{
|
|
3940
5501
|
label: strings.map,
|
|
@@ -3953,20 +5514,20 @@ function StoryEditor({
|
|
|
3953
5514
|
}
|
|
3954
5515
|
)
|
|
3955
5516
|
] });
|
|
3956
|
-
const slideForm = slide && /* @__PURE__ */
|
|
3957
|
-
/* @__PURE__ */
|
|
3958
|
-
/* @__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}` }),
|
|
3959
5520
|
slideActions
|
|
3960
5521
|
] }),
|
|
3961
|
-
/* @__PURE__ */
|
|
3962
|
-
|
|
5522
|
+
/* @__PURE__ */ jsx13(
|
|
5523
|
+
TextInput6,
|
|
3963
5524
|
{
|
|
3964
5525
|
label: strings.slideTitle,
|
|
3965
5526
|
value: slide.title ?? "",
|
|
3966
5527
|
onChange: (e) => setSlide({ title: e.currentTarget.value || void 0 }, "title")
|
|
3967
5528
|
}
|
|
3968
5529
|
),
|
|
3969
|
-
/* @__PURE__ */
|
|
5530
|
+
/* @__PURE__ */ jsx13(
|
|
3970
5531
|
DateField,
|
|
3971
5532
|
{
|
|
3972
5533
|
value: slide.date,
|
|
@@ -3976,7 +5537,7 @@ function StoryEditor({
|
|
|
3976
5537
|
onChange: (date) => setSlide({ date }, "date")
|
|
3977
5538
|
}
|
|
3978
5539
|
),
|
|
3979
|
-
/* @__PURE__ */
|
|
5540
|
+
/* @__PURE__ */ jsx13(
|
|
3980
5541
|
StoryTextField,
|
|
3981
5542
|
{
|
|
3982
5543
|
label: strings.text,
|
|
@@ -3987,7 +5548,7 @@ function StoryEditor({
|
|
|
3987
5548
|
onChange: (text) => setSlide({ text: text || void 0 }, "text")
|
|
3988
5549
|
}
|
|
3989
5550
|
),
|
|
3990
|
-
/* @__PURE__ */
|
|
5551
|
+
/* @__PURE__ */ jsx13(
|
|
3991
5552
|
PlaceField,
|
|
3992
5553
|
{
|
|
3993
5554
|
place: slide.place,
|
|
@@ -3997,10 +5558,11 @@ function StoryEditor({
|
|
|
3997
5558
|
search: placeSearch,
|
|
3998
5559
|
dark,
|
|
3999
5560
|
strings,
|
|
4000
|
-
onChange: (update, key) => setPlace(slide.id, update, key)
|
|
5561
|
+
onChange: (update, key) => setPlace(slide.id, update, key),
|
|
5562
|
+
onEditMap: () => setMapSlideId(slide.id)
|
|
4001
5563
|
}
|
|
4002
5564
|
),
|
|
4003
|
-
/* @__PURE__ */
|
|
5565
|
+
/* @__PURE__ */ jsx13(
|
|
4004
5566
|
PhotosField,
|
|
4005
5567
|
{
|
|
4006
5568
|
photos: slide.media ?? [],
|
|
@@ -4013,22 +5575,22 @@ function StoryEditor({
|
|
|
4013
5575
|
onDescription: (key, alt) => apply(updateMedia(doc, key, { alt: alt || void 0 }), `media:${key}:alt`)
|
|
4014
5576
|
}
|
|
4015
5577
|
),
|
|
4016
|
-
photoError && /* @__PURE__ */
|
|
5578
|
+
photoError && /* @__PURE__ */ jsx13(Text7, { size: "xs", c: "red", role: "alert", children: photoError })
|
|
4017
5579
|
] }, slide.id);
|
|
4018
|
-
const list = /* @__PURE__ */
|
|
4019
|
-
/* @__PURE__ */
|
|
5580
|
+
const list = /* @__PURE__ */ jsxs10(Stack8, { gap: "xs", children: [
|
|
5581
|
+
/* @__PURE__ */ jsxs10(
|
|
4020
5582
|
UnstyledButton3,
|
|
4021
5583
|
{
|
|
4022
5584
|
onClick: () => setSelected(STORY),
|
|
4023
5585
|
style: { padding: "8px", borderRadius: 6, background: index === STORY ? "var(--mantine-primary-color-light)" : void 0 },
|
|
4024
5586
|
children: [
|
|
4025
|
-
/* @__PURE__ */
|
|
4026
|
-
/* @__PURE__ */
|
|
5587
|
+
/* @__PURE__ */ jsx13(Text7, { fw: 600, size: "sm", children: strings.story }),
|
|
5588
|
+
/* @__PURE__ */ jsx13(Text7, { size: "xs", c: "dimmed", children: strings.storySettings })
|
|
4027
5589
|
]
|
|
4028
5590
|
}
|
|
4029
5591
|
),
|
|
4030
|
-
/* @__PURE__ */
|
|
4031
|
-
/* @__PURE__ */
|
|
5592
|
+
/* @__PURE__ */ jsx13(Divider2, { label: strings.slides, labelPosition: "left" }),
|
|
5593
|
+
/* @__PURE__ */ jsx13(
|
|
4032
5594
|
SlideList,
|
|
4033
5595
|
{
|
|
4034
5596
|
label: strings.slides,
|
|
@@ -4040,17 +5602,17 @@ function StoryEditor({
|
|
|
4040
5602
|
setSelected(to);
|
|
4041
5603
|
},
|
|
4042
5604
|
getKey: (s) => s.id,
|
|
4043
|
-
renderItem: (s, i) => /* @__PURE__ */
|
|
4044
|
-
/* @__PURE__ */
|
|
4045
|
-
/* @__PURE__ */
|
|
4046
|
-
/* @__PURE__ */
|
|
4047
|
-
/* @__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 ?? "" })
|
|
4048
5610
|
] })
|
|
4049
5611
|
] })
|
|
4050
5612
|
}
|
|
4051
5613
|
),
|
|
4052
|
-
/* @__PURE__ */
|
|
4053
|
-
|
|
5614
|
+
/* @__PURE__ */ jsxs10(
|
|
5615
|
+
Button6,
|
|
4054
5616
|
{
|
|
4055
5617
|
variant: "light",
|
|
4056
5618
|
size: "xs",
|
|
@@ -4067,7 +5629,7 @@ function StoryEditor({
|
|
|
4067
5629
|
}
|
|
4068
5630
|
)
|
|
4069
5631
|
] });
|
|
4070
|
-
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(
|
|
4071
5633
|
StoryPlayer,
|
|
4072
5634
|
{
|
|
4073
5635
|
story: doc,
|
|
@@ -4081,8 +5643,8 @@ function StoryEditor({
|
|
|
4081
5643
|
}
|
|
4082
5644
|
}
|
|
4083
5645
|
) });
|
|
4084
|
-
const column = (children, width) => /* @__PURE__ */
|
|
4085
|
-
|
|
5646
|
+
const column = (children, width) => /* @__PURE__ */ jsx13(
|
|
5647
|
+
Box4,
|
|
4086
5648
|
{
|
|
4087
5649
|
style: {
|
|
4088
5650
|
width,
|
|
@@ -4095,32 +5657,32 @@ function StoryEditor({
|
|
|
4095
5657
|
children
|
|
4096
5658
|
}
|
|
4097
5659
|
);
|
|
4098
|
-
return /* @__PURE__ */
|
|
4099
|
-
/* @__PURE__ */
|
|
4100
|
-
|
|
5660
|
+
return /* @__PURE__ */ jsxs10(Box4, { style: { height: "100%", display: "flex", flexDirection: "column" }, children: [
|
|
5661
|
+
/* @__PURE__ */ jsxs10(
|
|
5662
|
+
Group7,
|
|
4101
5663
|
{
|
|
4102
|
-
h:
|
|
5664
|
+
h: BAR_HEIGHT3,
|
|
4103
5665
|
px: "md",
|
|
4104
5666
|
gap: "sm",
|
|
4105
5667
|
wrap: "nowrap",
|
|
4106
5668
|
justify: "space-between",
|
|
4107
5669
|
style: { flex: "none", borderBottom: "1px solid var(--mantine-color-default-border)" },
|
|
4108
5670
|
children: [
|
|
4109
|
-
/* @__PURE__ */
|
|
5671
|
+
/* @__PURE__ */ jsxs10(Group7, { gap: "sm", wrap: "nowrap", style: { minWidth: 0 }, children: [
|
|
4110
5672
|
brand,
|
|
4111
|
-
/* @__PURE__ */
|
|
5673
|
+
/* @__PURE__ */ jsx13(Text7, { fw: 600, truncate: true, dir: "auto", children: doc.title })
|
|
4112
5674
|
] }),
|
|
4113
|
-
/* @__PURE__ */
|
|
4114
|
-
issues.length > 0 && /* @__PURE__ */
|
|
4115
|
-
/* @__PURE__ */
|
|
4116
|
-
/* @__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) => {
|
|
4117
5679
|
const at = issueSlide(issue);
|
|
4118
|
-
return /* @__PURE__ */
|
|
5680
|
+
return /* @__PURE__ */ jsxs10(UnstyledButton3, { onClick: () => {
|
|
4119
5681
|
setSelected(at);
|
|
4120
5682
|
setProblemsOpen(false);
|
|
4121
5683
|
}, children: [
|
|
4122
|
-
/* @__PURE__ */
|
|
4123
|
-
/* @__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: [
|
|
4124
5686
|
issue.path,
|
|
4125
5687
|
" ",
|
|
4126
5688
|
issue.message
|
|
@@ -4128,16 +5690,16 @@ function StoryEditor({
|
|
|
4128
5690
|
] }, i);
|
|
4129
5691
|
}) }) })
|
|
4130
5692
|
] }),
|
|
4131
|
-
/* @__PURE__ */
|
|
4132
|
-
/* @__PURE__ */
|
|
4133
|
-
/* @__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 }),
|
|
4134
5696
|
toolbar
|
|
4135
5697
|
] })
|
|
4136
5698
|
]
|
|
4137
5699
|
}
|
|
4138
5700
|
),
|
|
4139
|
-
/* @__PURE__ */
|
|
4140
|
-
|
|
5701
|
+
/* @__PURE__ */ jsxs10(
|
|
5702
|
+
Box4,
|
|
4141
5703
|
{
|
|
4142
5704
|
style: {
|
|
4143
5705
|
flex: 1,
|
|
@@ -4153,7 +5715,27 @@ function StoryEditor({
|
|
|
4153
5715
|
]
|
|
4154
5716
|
}
|
|
4155
5717
|
),
|
|
4156
|
-
|
|
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(
|
|
4157
5739
|
StoryPlayer,
|
|
4158
5740
|
{
|
|
4159
5741
|
story: doc,
|
|
@@ -4170,8 +5752,12 @@ export {
|
|
|
4170
5752
|
COALESCE_MS,
|
|
4171
5753
|
DateField,
|
|
4172
5754
|
EDITOR_ENGLISH,
|
|
5755
|
+
MAP_EDITOR_ENGLISH,
|
|
5756
|
+
MapEditor,
|
|
5757
|
+
MapEditorHelp,
|
|
4173
5758
|
PhotosField,
|
|
4174
5759
|
PlaceField,
|
|
5760
|
+
PlaceMapDialog,
|
|
4175
5761
|
SlideList,
|
|
4176
5762
|
StoryEditor,
|
|
4177
5763
|
StoryTextField,
|
|
@@ -4181,12 +5767,15 @@ export {
|
|
|
4181
5767
|
blankStory,
|
|
4182
5768
|
deleteSlide,
|
|
4183
5769
|
duplicateSlide,
|
|
5770
|
+
editorToPlace,
|
|
4184
5771
|
findMedia,
|
|
4185
5772
|
formatLatLon,
|
|
4186
5773
|
moveSlide,
|
|
4187
5774
|
nextSlideId,
|
|
4188
5775
|
parseLatLon,
|
|
4189
5776
|
patchPlace,
|
|
5777
|
+
placeFeatures,
|
|
5778
|
+
placeToEditor,
|
|
4190
5779
|
prepareImage,
|
|
4191
5780
|
pruneMedia,
|
|
4192
5781
|
record,
|