@gramps-connect/story 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +23 -5
  2. package/dist/editor.js +2065 -177
  3. package/dist/editor.js.map +4 -4
  4. package/dist/embed.js +1 -1
  5. package/dist/map-edit.js +361 -0
  6. package/dist/map-edit.js.map +7 -0
  7. package/dist/player.js +3 -1
  8. package/dist/player.js.map +2 -2
  9. package/dist/types/editor/MapEditor.d.ts +89 -0
  10. package/dist/types/editor/PlaceField.d.ts +24 -0
  11. package/dist/types/editor/PlaceMap.d.ts +12 -0
  12. package/dist/types/editor/PlaceMapDialog.d.ts +18 -0
  13. package/dist/types/editor/StoryEditor.d.ts +8 -1
  14. package/dist/types/editor/index.d.ts +7 -1
  15. package/dist/types/editor/mapEditorStrings.d.ts +50 -0
  16. package/dist/types/editor/model.d.ts +17 -1
  17. package/dist/types/editor/placeSearch.d.ts +22 -0
  18. package/dist/types/editor/placeShapes.d.ts +13 -0
  19. package/dist/types/editor/strings.d.ts +28 -0
  20. package/dist/types/mapEdit/geometry.d.ts +91 -0
  21. package/dist/types/mapEdit/index.d.ts +3 -0
  22. package/dist/types/mapEdit/mapFile.d.ts +77 -0
  23. package/dist/types/mapEdit/model.d.ts +55 -0
  24. package/package.json +18 -2
  25. package/src/editor/MapEditor.tsx +1545 -0
  26. package/src/editor/PlaceField.tsx +170 -0
  27. package/src/editor/PlaceMap.tsx +88 -0
  28. package/src/editor/PlaceMapDialog.tsx +228 -0
  29. package/src/editor/StoryEditor.tsx +50 -14
  30. package/src/editor/index.ts +11 -2
  31. package/src/editor/mapEditorStrings.ts +109 -0
  32. package/src/editor/model.ts +38 -1
  33. package/src/editor/placeSearch.ts +61 -0
  34. package/src/editor/placeShapes.ts +63 -0
  35. package/src/editor/strings.ts +56 -0
  36. package/src/mapEdit/geometry.ts +211 -0
  37. package/src/mapEdit/index.ts +15 -0
  38. package/src/mapEdit/mapFile.ts +199 -0
  39. package/src/mapEdit/model.ts +250 -0
  40. package/src/player/MapBackground.tsx +3 -1
@@ -0,0 +1,361 @@
1
+ // src/mapEdit/model.ts
2
+ var EDITOR_OWNED = /* @__PURE__ */ new Set(["name", "color", "opacity"]);
3
+ var DROPPED = /* @__PURE__ */ new Set(["coordinateProperties", "_gpxType", "styleUrl", "@geometry-type"]);
4
+ function round2d(position) {
5
+ return [Number(position[0].toFixed(9)), Number(position[1].toFixed(9))];
6
+ }
7
+ function singleParts(geometry) {
8
+ switch (geometry.type) {
9
+ case "Point":
10
+ return [{ type: "Point", coordinates: round2d(geometry.coordinates) }];
11
+ case "LineString":
12
+ return [{ type: "LineString", coordinates: geometry.coordinates.map(round2d) }];
13
+ case "Polygon":
14
+ return [{ type: "Polygon", coordinates: geometry.coordinates.map((ring) => ring.map(round2d)) }];
15
+ case "MultiPoint":
16
+ return geometry.coordinates.map((c) => ({ type: "Point", coordinates: round2d(c) }));
17
+ case "MultiLineString":
18
+ return geometry.coordinates.map((line) => ({ type: "LineString", coordinates: line.map(round2d) }));
19
+ case "MultiPolygon":
20
+ return geometry.coordinates.map((poly) => ({ type: "Polygon", coordinates: poly.map((ring) => ring.map(round2d)) }));
21
+ case "GeometryCollection":
22
+ return geometry.geometries.flatMap(singleParts);
23
+ }
24
+ }
25
+ var modeOf = (geometry) => geometry.type === "Point" ? "point" : geometry.type === "LineString" ? "linestring" : "polygon";
26
+ function extraProperties(properties) {
27
+ const extra = {};
28
+ for (const [key, value] of Object.entries(properties)) {
29
+ if (EDITOR_OWNED.has(key) || DROPPED.has(key) || value === void 0 || value === null) continue;
30
+ extra[key] = value;
31
+ }
32
+ return extra;
33
+ }
34
+ function partTimes(feature, partCount) {
35
+ const times = feature.properties?.coordinateProperties?.times;
36
+ if (!Array.isArray(times) || times.length === 0) return [];
37
+ if (feature.geometry?.type === "LineString" && times.every((t) => typeof t === "string")) {
38
+ return [times];
39
+ }
40
+ if (feature.geometry?.type === "MultiLineString" && times.length === partCount && times.every(Array.isArray)) {
41
+ return times;
42
+ }
43
+ return [];
44
+ }
45
+ function prepareForEditing(features, firstIndex = 0) {
46
+ const session = { parts: [], sources: /* @__PURE__ */ new Map(), locked: /* @__PURE__ */ new Map() };
47
+ features.forEach((feature, i) => {
48
+ const f = firstIndex + i;
49
+ if (!feature.geometry) return;
50
+ const properties = feature.properties ?? {};
51
+ const extra = extraProperties(properties);
52
+ const geometries = singleParts(feature.geometry);
53
+ const times = partTimes(feature, geometries.length);
54
+ const opacityRaw = Number(properties.opacity);
55
+ const opacity = Number.isFinite(opacityRaw) && opacityRaw >= 0 && opacityRaw <= 1 ? opacityRaw : void 0;
56
+ geometries.forEach((geometry, index) => {
57
+ const sourceKey = `f${f}p${index}`;
58
+ const partTimesHere = times[index];
59
+ const keepTimes = geometry.type === "LineString" && partTimesHere?.length === geometry.coordinates.length;
60
+ session.sources.set(sourceKey, {
61
+ group: `f${f}`,
62
+ index,
63
+ extra,
64
+ ...keepTimes ? { times: partTimesHere, coords: geometry.coordinates } : {}
65
+ });
66
+ const part = {
67
+ sourceKey,
68
+ mode: modeOf(geometry),
69
+ geometry,
70
+ name: typeof properties.name === "string" ? properties.name : void 0,
71
+ color: typeof properties.color === "string" ? properties.color : void 0,
72
+ ...geometry.type === "Polygon" ? { opacity } : {}
73
+ };
74
+ if (geometry.type === "Polygon" && geometry.coordinates.length > 1) session.locked.set(sourceKey, part);
75
+ else session.parts.push(part);
76
+ });
77
+ });
78
+ return session;
79
+ }
80
+ function addToSession(session, features) {
81
+ let next = 0;
82
+ for (const source of session.sources.values()) next = Math.max(next, Number(source.group.slice(1)) + 1);
83
+ const added = prepareForEditing(features, next);
84
+ session.parts.push(...added.parts);
85
+ for (const [key, source] of added.sources) session.sources.set(key, source);
86
+ for (const [key, part] of added.locked) session.locked.set(key, part);
87
+ return added;
88
+ }
89
+ function lockPart(session, sourceKey) {
90
+ const index = session.parts.findIndex((part) => part.sourceKey === sourceKey);
91
+ if (index < 0) return;
92
+ session.locked.set(sourceKey, session.parts[index]);
93
+ session.parts.splice(index, 1);
94
+ }
95
+ var samePositions = (a, b) => a.length === b.length && a.every((p, i) => p[0] === b[i][0] && p[1] === b[i][1]);
96
+ function joinGeometries(geometries) {
97
+ if (geometries.length === 1) return geometries[0];
98
+ const kinds = new Set(geometries.map((g) => g.type));
99
+ if (kinds.size === 1) {
100
+ if (kinds.has("Point")) return { type: "MultiPoint", coordinates: geometries.map((g) => g.coordinates) };
101
+ if (kinds.has("LineString")) return { type: "MultiLineString", coordinates: geometries.map((g) => g.coordinates) };
102
+ return { type: "MultiPolygon", coordinates: geometries.map((g) => g.coordinates) };
103
+ }
104
+ return { type: "GeometryCollection", geometries };
105
+ }
106
+ function assembleForSaving(session, saved) {
107
+ const entries = [
108
+ ...saved.map((part) => ({ part, source: part.sourceKey ? session.sources.get(part.sourceKey) : void 0 })),
109
+ ...[...session.locked.values()].map((part) => ({ part, source: session.sources.get(part.sourceKey) }))
110
+ ];
111
+ const buckets = /* @__PURE__ */ new Map();
112
+ let newCount = 0;
113
+ for (const entry of entries) {
114
+ const { part, source } = entry;
115
+ const key = source ? `${source.group}|${part.name ?? ""}|${part.color ?? ""}|${part.opacity ?? ""}` : `new${newCount++}`;
116
+ const bucket = buckets.get(key);
117
+ if (bucket) bucket.push(entry);
118
+ else buckets.set(key, [entry]);
119
+ }
120
+ const groupOrder = (key) => key.startsWith("new") ? Infinity : Number(key.slice(1, key.indexOf("|")));
121
+ return [...buckets.entries()].sort(([a], [b]) => groupOrder(a) - groupOrder(b)).map(([, bucket]) => {
122
+ bucket.sort((a, b) => (a.source?.index ?? 0) - (b.source?.index ?? 0));
123
+ const first = bucket[0];
124
+ const geometry = joinGeometries(bucket.map((e) => e.part.geometry));
125
+ const times = bucket.map(({ part, source }) => part.geometry.type === "LineString" && source?.times && source.coords && samePositions(part.geometry.coordinates, source.coords) ? source.times : void 0);
126
+ const allTimed = bucket.every((e) => e.part.geometry.type === "LineString") && times.every(Boolean);
127
+ return {
128
+ type: "Feature",
129
+ geometry,
130
+ properties: {
131
+ ...first.source?.extra ?? {},
132
+ ...first.part.color !== void 0 ? { color: first.part.color } : {},
133
+ ...first.part.name ? { name: first.part.name } : {},
134
+ ...first.part.opacity !== void 0 ? { opacity: first.part.opacity } : {},
135
+ ...allTimed ? { coordinateProperties: { times: bucket.length === 1 ? times[0] : times } } : {}
136
+ }
137
+ };
138
+ });
139
+ }
140
+
141
+ // src/mapEdit/mapFile.ts
142
+ import { gpx, kml } from "@tmcw/togeojson";
143
+ var MEDIA_HANDLE_PREFIX = "media-handle:";
144
+ function applySimplestyle(feature) {
145
+ const props = feature.properties ?? {};
146
+ const isRegion = feature.geometry?.type === "Polygon" || feature.geometry?.type === "MultiPolygon";
147
+ const color = props.color ?? (isRegion ? props.fill ?? props.stroke : props.stroke ?? props["marker-color"]);
148
+ const opacity = props.opacity ?? (isRegion ? props["fill-opacity"] : void 0);
149
+ if (color === props.color && opacity === props.opacity) return feature;
150
+ return { ...feature, properties: { ...props, color, opacity } };
151
+ }
152
+ function geoJsonFeatures(doc) {
153
+ if (doc.type === "FeatureCollection") return doc.features ?? [];
154
+ if (doc.type === "Feature") return [doc];
155
+ return [{ type: "Feature", geometry: doc, properties: {} }];
156
+ }
157
+ function asGroundOverlay(feature) {
158
+ const props = feature.properties ?? {};
159
+ if (props[IMAGE_OVERLAY_PROPERTY] !== true || typeof props.image !== "string") return feature;
160
+ const { [IMAGE_OVERLAY_PROPERTY]: _marker, image, ...rest } = props;
161
+ return {
162
+ ...feature,
163
+ properties: { ...rest, "@geometry-type": "groundoverlay", icon: `${MEDIA_HANDLE_PREFIX}${image}` }
164
+ };
165
+ }
166
+ function parseMapFile(text) {
167
+ const hasGeometry = (f) => f.geometry != null;
168
+ if (text.trimStart().startsWith("{")) {
169
+ return geoJsonFeatures(JSON.parse(text)).filter(hasGeometry).map(asGroundOverlay).map(applySimplestyle);
170
+ }
171
+ const dom = new DOMParser().parseFromString(text, "text/xml");
172
+ if (dom.documentElement.localName === "gpx") {
173
+ return gpx(dom).features.filter(hasGeometry).map(applySimplestyle);
174
+ }
175
+ return kml(dom).features.filter(hasGeometry);
176
+ }
177
+ function overlaySourceOfText(text) {
178
+ if (!text.trimStart().startsWith("{")) return void 0;
179
+ try {
180
+ const source = JSON.parse(text)[SOURCE_PROPERTY];
181
+ return typeof source === "string" ? source : void 0;
182
+ } catch {
183
+ return void 0;
184
+ }
185
+ }
186
+ function polygonRingsOf(feature) {
187
+ if (feature.geometry?.type === "Polygon") return [feature.geometry.coordinates[0]];
188
+ if (feature.geometry?.type === "GeometryCollection") {
189
+ return feature.geometry.geometries.filter((g) => g.type === "Polygon").map((g) => g.coordinates[0]);
190
+ }
191
+ return [];
192
+ }
193
+ function isKmlRegion(feature) {
194
+ return polygonRingsOf(feature).length > 0;
195
+ }
196
+ function lineCoordsOf(feature) {
197
+ const geometry = feature.geometry;
198
+ if (geometry?.type === "LineString") return geometry.coordinates;
199
+ if (geometry?.type === "MultiLineString") return geometry.coordinates.flat();
200
+ if (geometry?.type === "GeometryCollection" && geometry.geometries.length > 0 && geometry.geometries.every((g) => g.type === "LineString")) {
201
+ return geometry.geometries.flatMap((g) => g.type === "LineString" ? g.coordinates : []);
202
+ }
203
+ return [];
204
+ }
205
+ function isKmlLine(feature) {
206
+ return !isKmlRegion(feature) && lineCoordsOf(feature).length > 0;
207
+ }
208
+ var IMAGE_OVERLAY_PROPERTY = "gramps:image-overlay";
209
+ function overlayFeature(overlay) {
210
+ const ring = [...overlay.corners, overlay.corners[0]];
211
+ return {
212
+ type: "Feature",
213
+ geometry: { type: "Polygon", coordinates: [ring] },
214
+ properties: {
215
+ [IMAGE_OVERLAY_PROPERTY]: true,
216
+ image: overlay.handle,
217
+ ...overlay.name ? { name: overlay.name } : {},
218
+ ...overlay.opacity !== void 0 && overlay.opacity !== 1 ? { opacity: overlay.opacity } : {},
219
+ ...overlay.order !== void 0 ? { order: overlay.order } : {}
220
+ }
221
+ };
222
+ }
223
+ var SOURCE_PROPERTY = "gramps:source";
224
+ function mapItemToGeoJson(features, overlays = [], source) {
225
+ const collection = {
226
+ type: "FeatureCollection",
227
+ ...source ? { [SOURCE_PROPERTY]: source } : {},
228
+ features: [...features, ...overlays.map(overlayFeature)]
229
+ };
230
+ return JSON.stringify(collection);
231
+ }
232
+
233
+ // src/mapEdit/geometry.ts
234
+ var DEG_TO_RAD = Math.PI / 180;
235
+ var RAD_TO_DEG = 180 / Math.PI;
236
+ function mercatorFromLngLat([lng, lat]) {
237
+ const x = (180 + lng) / 360;
238
+ const y = (180 - RAD_TO_DEG * Math.log(Math.tan(Math.PI / 4 + lat * DEG_TO_RAD / 2))) / 360;
239
+ return [x, y];
240
+ }
241
+ function lngLatFromMercator([x, y]) {
242
+ const lng = x * 360 - 180;
243
+ const lat = 90 - 2 * Math.atan(Math.exp((y * 360 - 180) * DEG_TO_RAD)) * RAD_TO_DEG;
244
+ return [lng, lat];
245
+ }
246
+ function rotatePoint(point, center, rotationDeg) {
247
+ const angle = rotationDeg * DEG_TO_RAD;
248
+ const cos = Math.cos(angle);
249
+ const sin = Math.sin(angle);
250
+ const [px, py] = mercatorFromLngLat(point);
251
+ const [cx, cy] = mercatorFromLngLat(center);
252
+ const dx = px - cx;
253
+ const dy = -(py - cy);
254
+ const rx = dx * cos - dy * sin;
255
+ const ry = dx * sin + dy * cos;
256
+ return lngLatFromMercator([cx + rx, cy - ry]);
257
+ }
258
+ function scalePoint(point, center, scale) {
259
+ const [px, py] = mercatorFromLngLat(point);
260
+ const [cx, cy] = mercatorFromLngLat(center);
261
+ return lngLatFromMercator([cx + (px - cx) * scale, cy + (py - cy) * scale]);
262
+ }
263
+ function mercatorDistance(a, b) {
264
+ const [ax, ay] = mercatorFromLngLat(a);
265
+ const [bx, by] = mercatorFromLngLat(b);
266
+ return Math.hypot(bx - ax, by - ay);
267
+ }
268
+ function mercatorBearing(from, to) {
269
+ const [fx, fy] = mercatorFromLngLat(from);
270
+ const [tx, ty] = mercatorFromLngLat(to);
271
+ return Math.atan2(-(ty - fy), tx - fx) * RAD_TO_DEG;
272
+ }
273
+ function cornersFromBox(box) {
274
+ const center = [(box.west + box.east) / 2, (box.north + box.south) / 2];
275
+ const corners = [
276
+ [box.west, box.north],
277
+ [box.east, box.north],
278
+ [box.east, box.south],
279
+ [box.west, box.south]
280
+ ];
281
+ return corners.map((p) => rotatePoint(p, center, box.rotation));
282
+ }
283
+ function visitCoordinates(coords, bounds) {
284
+ if (typeof coords[0] === "number") {
285
+ const [lng, lat] = coords;
286
+ if (lng < bounds[0]) bounds[0] = lng;
287
+ if (lat < bounds[1]) bounds[1] = lat;
288
+ if (lng > bounds[2]) bounds[2] = lng;
289
+ if (lat > bounds[3]) bounds[3] = lat;
290
+ return;
291
+ }
292
+ for (const c of coords) visitCoordinates(c, bounds);
293
+ }
294
+ function kmlBounds(features) {
295
+ const bounds = [Infinity, Infinity, -Infinity, -Infinity];
296
+ for (const feature of features) {
297
+ const geometry = feature.geometry;
298
+ if (!geometry) continue;
299
+ if (geometry.type === "GeometryCollection") {
300
+ for (const g of geometry.geometries) if ("coordinates" in g) visitCoordinates(g.coordinates, bounds);
301
+ continue;
302
+ }
303
+ visitCoordinates(geometry.coordinates, bounds);
304
+ }
305
+ return Number.isFinite(bounds[0]) ? bounds : null;
306
+ }
307
+ function kmlCenter(features) {
308
+ const bounds = kmlBounds(features);
309
+ if (!bounds) return null;
310
+ return [(bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2];
311
+ }
312
+ function kmlOverlayBounds(overlays) {
313
+ if (overlays.length === 0) return null;
314
+ const bounds = [Infinity, Infinity, -Infinity, -Infinity];
315
+ for (const overlay of overlays) {
316
+ for (const [lng, lat] of overlay.corners) {
317
+ if (lng < bounds[0]) bounds[0] = lng;
318
+ if (lat < bounds[1]) bounds[1] = lat;
319
+ if (lng > bounds[2]) bounds[2] = lng;
320
+ if (lat > bounds[3]) bounds[3] = lat;
321
+ }
322
+ }
323
+ return bounds;
324
+ }
325
+ function unionBounds(a, b) {
326
+ if (!a) return b;
327
+ if (!b) return a;
328
+ return [Math.min(a[0], b[0]), Math.min(a[1], b[1]), Math.max(a[2], b[2]), Math.max(a[3], b[3])];
329
+ }
330
+ function kmlOverlayCenter(overlays) {
331
+ const bounds = kmlOverlayBounds(overlays);
332
+ if (!bounds) return null;
333
+ return [(bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2];
334
+ }
335
+ export {
336
+ IMAGE_OVERLAY_PROPERTY,
337
+ MEDIA_HANDLE_PREFIX,
338
+ SOURCE_PROPERTY,
339
+ addToSession,
340
+ assembleForSaving,
341
+ cornersFromBox,
342
+ isKmlLine,
343
+ isKmlRegion,
344
+ kmlBounds,
345
+ kmlCenter,
346
+ kmlOverlayBounds,
347
+ kmlOverlayCenter,
348
+ lineCoordsOf,
349
+ lockPart,
350
+ mapItemToGeoJson,
351
+ mercatorBearing,
352
+ mercatorDistance,
353
+ overlaySourceOfText,
354
+ parseMapFile,
355
+ polygonRingsOf,
356
+ prepareForEditing,
357
+ rotatePoint,
358
+ scalePoint,
359
+ unionBounds
360
+ };
361
+ //# sourceMappingURL=map-edit.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/mapEdit/model.ts", "../src/mapEdit/mapFile.ts", "../src/mapEdit/geometry.ts"],
4
+ "sourcesContent": ["// Turning a map file's features into what the map editor's terra-draw\n// canvas can edit, and back into features to save (as GeoJSON, see\n// mapItemToGeoJson), with nothing dropped on the way. terra-draw only\n// knows single Points, LineStrings and Polygons with 2D, at-most-9-decimal\n// coordinates, and only keeps the few properties the editor itself sets\n// (name/color/opacity) --\n// while a KML, GeoJSON or GPX file can hold multi-part shapes, arbitrary\n// properties and a GPS track's per-point times. So:\n//\n// - a multi-part shape is split into single parts that share a group,\n// and rejoined on save (parts whose name/colour/opacity were changed\n// apart become separate shapes, since one KML Placemark has one style);\n// - every other property is set aside per part and put back on save;\n// - a track's times are kept only while its points are unchanged --\n// reshaped, it no longer matches the recording, so it saves as a plain\n// line;\n// - elevations are dropped (nothing here uses them);\n// - anything terra-draw still can't take (a polygon with holes, one that\n// crosses itself) is \"locked\": shown but not editable, and saved back\n// exactly as it was, rather than silently left out of the save.\nimport type { Feature, Geometry, LineString, Point, Polygon, Position } from \"geojson\";\n\n/** What the editor reads and writes on each terra-draw feature itself. */\nconst EDITOR_OWNED = new Set([\"name\", \"color\", \"opacity\"]);\n/** Parser bookkeeping with nothing worth saving: the per-point data that's\n * carried separately (coordinateProperties), @tmcw/togeojson's GPX type tag,\n * and a KML style reference whose <Style> doesn't come along. */\nconst DROPPED = new Set([\"coordinateProperties\", \"_gpxType\", \"styleUrl\", \"@geometry-type\"]);\n\nexport type EditableGeometry = Point | LineString | Polygon;\nexport type EditMode = \"point\" | \"linestring\" | \"polygon\";\n\n/** One single-geometry part, ready for terra-draw. `sourceKey` rides on the\n * terra-draw feature as a property so save can find what was set aside. */\nexport interface EditablePart {\n sourceKey: string;\n mode: EditMode;\n geometry: EditableGeometry;\n name?: string;\n color?: string;\n opacity?: number;\n}\n\ninterface PartSource {\n group: string;\n /** Position within its group, so rejoined parts keep their order. */\n index: number;\n extra: Record<string, unknown>;\n /** A track part's per-point times, and the (2D, rounded) points they\n * belong to -- kept only while the saved points still match. */\n times?: string[];\n coords?: Position[];\n}\n\nexport interface EditSession {\n parts: EditablePart[];\n sources: Map<string, PartSource>;\n /** Parts terra-draw wouldn't accept -- see lockPart. */\n locked: Map<string, EditablePart>;\n}\n\n/** terra-draw rejects coordinates with more than 9 decimal places (its\n * coordinatePrecision) -- well under a millimetre, so rounding loses\n * nothing real. Also drops a third (elevation) value. */\nfunction round2d(position: Position): Position {\n return [Number(position[0].toFixed(9)), Number(position[1].toFixed(9))];\n}\n\nfunction singleParts(geometry: Geometry): EditableGeometry[] {\n switch (geometry.type) {\n case \"Point\":\n return [{ type: \"Point\", coordinates: round2d(geometry.coordinates) }];\n case \"LineString\":\n return [{ type: \"LineString\", coordinates: geometry.coordinates.map(round2d) }];\n case \"Polygon\":\n return [{ type: \"Polygon\", coordinates: geometry.coordinates.map((ring) => ring.map(round2d)) }];\n case \"MultiPoint\":\n return geometry.coordinates.map((c) => ({ type: \"Point\", coordinates: round2d(c) }));\n case \"MultiLineString\":\n return geometry.coordinates.map((line) => ({ type: \"LineString\", coordinates: line.map(round2d) }));\n case \"MultiPolygon\":\n return geometry.coordinates.map((poly) => ({ type: \"Polygon\", coordinates: poly.map((ring) => ring.map(round2d)) }));\n case \"GeometryCollection\":\n return geometry.geometries.flatMap(singleParts);\n }\n}\n\nconst modeOf = (geometry: EditableGeometry): EditMode =>\n geometry.type === \"Point\" ? \"point\" : geometry.type === \"LineString\" ? \"linestring\" : \"polygon\";\n\n/** A feature's properties minus the editor's own and the parser's\n * bookkeeping -- kept as they are, nested values included. */\nfunction extraProperties(properties: Record<string, unknown>): Record<string, unknown> {\n const extra: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(properties)) {\n if (EDITOR_OWNED.has(key) || DROPPED.has(key) || value === undefined || value === null) continue;\n extra[key] = value;\n }\n return extra;\n}\n\n/** Each part's times, if the feature has them in @tmcw/togeojson's own\n * shape: string[] for a LineString, string[][] for a MultiLineString. */\nfunction partTimes(feature: Feature, partCount: number): (string[] | undefined)[] {\n const times = (feature.properties?.coordinateProperties as { times?: unknown } | undefined)?.times;\n if (!Array.isArray(times) || times.length === 0) return [];\n if (feature.geometry?.type === \"LineString\" && times.every((t) => typeof t === \"string\")) {\n return [times as string[]];\n }\n if (feature.geometry?.type === \"MultiLineString\" && times.length === partCount && times.every(Array.isArray)) {\n return times as string[][];\n }\n return [];\n}\n\n/** Splits a file's features (e.g. parseMapFile's) into\n * editable single parts. A polygon with holes is locked from the start:\n * terra-draw's polygon editing only knows an outer ring. */\nexport function prepareForEditing(features: Feature[], firstIndex = 0): EditSession {\n const session: EditSession = { parts: [], sources: new Map(), locked: new Map() };\n features.forEach((feature, i) => {\n const f = firstIndex + i;\n if (!feature.geometry) return;\n const properties = (feature.properties ?? {}) as Record<string, unknown>;\n const extra = extraProperties(properties);\n const geometries = singleParts(feature.geometry);\n const times = partTimes(feature, geometries.length);\n const opacityRaw = Number(properties.opacity);\n const opacity = Number.isFinite(opacityRaw) && opacityRaw >= 0 && opacityRaw <= 1 ? opacityRaw : undefined;\n geometries.forEach((geometry, index) => {\n const sourceKey = `f${f}p${index}`;\n const partTimesHere = times[index];\n const keepTimes = geometry.type === \"LineString\" && partTimesHere?.length === geometry.coordinates.length;\n session.sources.set(sourceKey, {\n group: `f${f}`, index, extra,\n ...(keepTimes ? { times: partTimesHere, coords: geometry.coordinates } : {}),\n });\n const part: EditablePart = {\n sourceKey, mode: modeOf(geometry), geometry,\n name: typeof properties.name === \"string\" ? properties.name : undefined,\n color: typeof properties.color === \"string\" ? properties.color : undefined,\n ...(geometry.type === \"Polygon\" ? { opacity } : {}),\n };\n if (geometry.type === \"Polygon\" && geometry.coordinates.length > 1) session.locked.set(sourceKey, part);\n else session.parts.push(part);\n });\n });\n return session;\n}\n\n/** Adds more features (a file imported while editing) after the session's\n * own, so they save after them too. Returns the session of just the new\n * ones, whose parts the canvas still has to take. */\nexport function addToSession(session: EditSession, features: Feature[]): EditSession {\n let next = 0;\n for (const source of session.sources.values()) next = Math.max(next, Number(source.group.slice(1)) + 1);\n const added = prepareForEditing(features, next);\n session.parts.push(...added.parts);\n for (const [key, source] of added.sources) session.sources.set(key, source);\n for (const [key, part] of added.locked) session.locked.set(key, part);\n return added;\n}\n\n/** Marks a part terra-draw refused to load (addFeatures' validation): it's\n * no longer editable, and assembleForSaving writes it back unchanged. */\nexport function lockPart(session: EditSession, sourceKey: string): void {\n const index = session.parts.findIndex((part) => part.sourceKey === sourceKey);\n if (index < 0) return;\n session.locked.set(sourceKey, session.parts[index]);\n session.parts.splice(index, 1);\n}\n\n/** A part as saved from the canvas: its (possibly edited) geometry and the\n * editor's own properties, plus the sourceKey it was loaded with -- absent\n * for a shape drawn new in this session. */\nexport interface SavedPart {\n sourceKey?: string;\n geometry: EditableGeometry;\n name?: string;\n color?: string;\n opacity?: number;\n}\n\nconst samePositions = (a: Position[], b: Position[]) =>\n a.length === b.length && a.every((p, i) => p[0] === b[i][0] && p[1] === b[i][1]);\n\n/** Joins single geometries back into one -- Multi* when they're all one\n * kind, a GeometryCollection when mixed (what a KML MultiGeometry is). */\nfunction joinGeometries(geometries: EditableGeometry[]): Geometry {\n if (geometries.length === 1) return geometries[0];\n const kinds = new Set(geometries.map((g) => g.type));\n if (kinds.size === 1) {\n if (kinds.has(\"Point\")) return { type: \"MultiPoint\", coordinates: geometries.map((g) => (g as Point).coordinates) };\n if (kinds.has(\"LineString\")) return { type: \"MultiLineString\", coordinates: geometries.map((g) => (g as LineString).coordinates) };\n return { type: \"MultiPolygon\", coordinates: geometries.map((g) => (g as Polygon).coordinates) };\n }\n return { type: \"GeometryCollection\", geometries };\n}\n\n/** Everything to save: the canvas's parts with their set-aside data put\n * back, multi-part shapes rejoined, and locked parts as they were loaded.\n * Features come out in their original file order, new shapes last. */\nexport function assembleForSaving(session: EditSession, saved: SavedPart[]): Feature[] {\n interface Entry { part: SavedPart; source?: PartSource }\n const entries: Entry[] = [\n ...saved.map((part) => ({ part, source: part.sourceKey ? session.sources.get(part.sourceKey) : undefined })),\n ...[...session.locked.values()].map((part) => ({ part, source: session.sources.get(part.sourceKey) })),\n ];\n // One bucket per original feature and style (a part restyled apart from\n // its siblings becomes its own shape), and one per new shape.\n const buckets = new Map<string, Entry[]>();\n let newCount = 0;\n for (const entry of entries) {\n const { part, source } = entry;\n const key = source\n ? `${source.group}|${part.name ?? \"\"}|${part.color ?? \"\"}|${part.opacity ?? \"\"}`\n : `new${newCount++}`;\n const bucket = buckets.get(key);\n if (bucket) bucket.push(entry);\n else buckets.set(key, [entry]);\n }\n const groupOrder = (key: string) => (key.startsWith(\"new\") ? Infinity : Number(key.slice(1, key.indexOf(\"|\"))));\n return [...buckets.entries()]\n .sort(([a], [b]) => groupOrder(a) - groupOrder(b))\n .map(([, bucket]) => {\n bucket.sort((a, b) => (a.source?.index ?? 0) - (b.source?.index ?? 0));\n const first = bucket[0];\n const geometry = joinGeometries(bucket.map((e) => e.part.geometry));\n // Times only when every line in the shape still has its own, on\n // unchanged points -- @tmcw/togeojson's own shape for them is all of\n // a track's segments timed or none.\n const times = bucket.map(({ part, source }) =>\n part.geometry.type === \"LineString\" && source?.times && source.coords\n && samePositions(part.geometry.coordinates, source.coords)\n ? source.times\n : undefined);\n const allTimed = bucket.every((e) => e.part.geometry.type === \"LineString\") && times.every(Boolean);\n return {\n type: \"Feature\",\n geometry,\n properties: {\n ...(first.source?.extra ?? {}),\n ...(first.part.color !== undefined ? { color: first.part.color } : {}),\n ...(first.part.name ? { name: first.part.name } : {}),\n ...(first.part.opacity !== undefined ? { opacity: first.part.opacity } : {}),\n ...(allTimed ? { coordinateProperties: { times: bucket.length === 1 ? times[0] : times } } : {}),\n },\n } as Feature;\n });\n}\n", "// Reading and writing map files: KML, GPX and GeoJSON in (read into GeoJSON\n// features, with each format's own colours where they're reliable), and\n// GeoJSON out, with image overlays as Polygon features marked\n// IMAGE_OVERLAY_PROPERTY. A GeoJSON overlay's `image` names its picture;\n// read back, it becomes the same feature @tmcw/togeojson makes of a KML\n// GroundOverlay, with the picture behind MEDIA_HANDLE_PREFIX. Moved here\n// from Gramps Connect (kmlMedia.ts and mapFileWrite.ts), which shares it\n// with the Story Editor. GPX and KML need a DOMParser (a browser, or jsdom).\n//\n// GeoJSON keeps every property as JSON and multi-part shapes as they are,\n// with no format translation to lose anything in; a GPS track's per-point\n// times ride along in `coordinateProperties.times`, the same place\n// @tmcw/togeojson's GPX/KML readers put them.\nimport type { Feature, FeatureCollection, GeoJsonObject, Geometry, Position } from \"geojson\";\nimport { gpx, kml } from \"@tmcw/togeojson\";\nimport type { OverlayCorners } from \"./geometry\";\n\n/** An image overlay's picture, as a Media handle behind this marker (not a\n * real URL: an app-internal reference, since a resolvable link would need\n * an expiring auth token baked in) -- in a KML GroundOverlay's href, and\n * what asGroundOverlay makes of a GeoJSON one's `image`. */\nexport const MEDIA_HANDLE_PREFIX = \"media-handle:\";\n\n/** A GeoJSON or GPX file's own colours, in the simplestyle convention\n * (`stroke`/`fill`/`fill-opacity`/`marker-color`, which both GeoJSON tools\n * and @tmcw/togeojson's gpx() use), copied onto the `color`/`opacity`\n * properties every map layer actually reads (see MapCanvas.tsx's\n * KML_FILL_LAYER). Fills only what's missing, so an explicit `color` wins.\n * Not applied to KML: Gramps Connect's own KML carries `color` already, and\n * leaving foreign KML alone keeps every existing overlay looking the same. */\nfunction applySimplestyle(feature: Feature): Feature {\n const props = feature.properties ?? {};\n const isRegion = feature.geometry?.type === \"Polygon\" || feature.geometry?.type === \"MultiPolygon\";\n const color = props.color ?? (isRegion ? props.fill ?? props.stroke : props.stroke ?? props[\"marker-color\"]);\n const opacity = props.opacity ?? (isRegion ? props[\"fill-opacity\"] : undefined);\n if (color === props.color && opacity === props.opacity) return feature;\n return { ...feature, properties: { ...props, color, opacity } };\n}\n\n/** A GeoJSON file's features: a FeatureCollection, a single Feature or a\n * bare Geometry are all valid GeoJSON documents. */\nfunction geoJsonFeatures(doc: GeoJsonObject): Feature<Geometry | null>[] {\n if (doc.type === \"FeatureCollection\") return (doc as { features?: Feature<Geometry | null>[] }).features ?? [];\n if (doc.type === \"Feature\") return [doc as Feature<Geometry | null>];\n return [{ type: \"Feature\", geometry: doc as Geometry, properties: {} }];\n}\n\n/** Gramps Connect's GeoJSON image overlay (see mapItemToGeoJson), turned into\n * the same feature @tmcw/togeojson makes of a KML <gx:LatLonQuad>\n * GroundOverlay -- so fetchAllKmlImageOverlays and everything that skips\n * overlays read both formats the same way. */\nfunction asGroundOverlay(feature: Feature): Feature {\n const props = feature.properties ?? {};\n if (props[IMAGE_OVERLAY_PROPERTY] !== true || typeof props.image !== \"string\") return feature;\n const { [IMAGE_OVERLAY_PROPERTY]: _marker, image, ...rest } = props;\n return {\n ...feature,\n properties: { ...rest, \"@geometry-type\": \"groundoverlay\", icon: `${MEDIA_HANDLE_PREFIX}${image}` },\n };\n}\n\n/** A map file's text, as GeoJSON features. The format is read off the\n * content (GeoJSON is JSON; GPX and KML differ by root element) rather\n * than the media object's stored mime, which a file attached by another\n * client may not have set to anything Gramps Connect recognizes. Throws on\n * unparseable text; features with no geometry are dropped -- a KML\n * Placemark without Point/LineString/Polygon (see @tmcw/togeojson's\n * KMLOptions.skipNullGeometry), or a GeoJSON Feature with `geometry: null`,\n * is of no use to a caller here, so dropped rather than threaded through\n * as `Feature<Geometry | null>` everywhere downstream. */\nexport function parseMapFile(text: string): Feature[] {\n const hasGeometry = (f: Feature<Geometry | null>): f is Feature => f.geometry != null;\n if (text.trimStart().startsWith(\"{\")) {\n return geoJsonFeatures(JSON.parse(text) as GeoJsonObject).filter(hasGeometry).map(asGroundOverlay).map(applySimplestyle);\n }\n const dom = new DOMParser().parseFromString(text, \"text/xml\");\n if (dom.documentElement.localName === \"gpx\") {\n return gpx(dom).features.filter(hasGeometry).map(applySimplestyle);\n }\n return kml(dom).features.filter(hasGeometry);\n}\n\n/** An overlay file's SOURCE_PROPERTY, if it's GeoJSON that has one. */\nexport function overlaySourceOfText(text: string): string | undefined {\n if (!text.trimStart().startsWith(\"{\")) return undefined;\n try {\n const source = (JSON.parse(text) as Record<string, unknown>)[SOURCE_PROPERTY];\n return typeof source === \"string\" ? source : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Every Polygon ring inside `feature` -- itself (a plain Polygon), or each\n * Polygon sub-geometry of a GeometryCollection. A country-level Wikidata/\n * Commons outline (e.g. the United States) is often a disjoint shape --\n * islands/territories a single Polygon ring can't express -- unlike a\n * state's simpler single-ring Polygon. A KML file stores one as a\n * <MultiGeometry> of several <Polygon>s, and @tmcw/togeojson reads *any* multi-child\n * Placemark geometry back as a GeometryCollection (never reconstituting\n * MultiPolygon) -- so that's the shape that actually reaches here, not\n * \"MultiPolygon\" itself (confirmed live, 2026-09-09, round-tripping a\n * synthetic MultiPolygon through both libraries). A non-Polygon sub-geometry\n * (there shouldn't be one -- Gramps Connect never writes anything else into a\n * region's own Placemark) is simply skipped rather than failing the whole\n * feature. `[]` for anything else (a Point, a bare LineString route, ...),\n * meaning \"not a region\" to every caller below. */\nexport function polygonRingsOf(feature: Feature): Position[][] {\n if (feature.geometry?.type === \"Polygon\") return [feature.geometry.coordinates[0]];\n if (feature.geometry?.type === \"GeometryCollection\") {\n return feature.geometry.geometries.filter((g) => g.type === \"Polygon\").map((g) => g.coordinates[0]);\n }\n return [];\n}\n\n/** True for a feature fetchAllKmlRegions/MapCanvas.tsx's own shapes effect\n * both treat as one drawable/controllable region -- see polygonRingsOf.\n * Exported so MapCanvas.tsx's key/hidden/opacity stamping can use the exact\n * same test fetchAllKmlRegions does: the two must never drift, or a row's\n * key stops matching the feature it's meant to control -- found live as a\n * place whose outline drew on the map (MapLibre's GeoJSON source splits a\n * GeometryCollection into one sub-feature per geometry before rendering,\n * each individually typed \"Polygon\") but never showed up in\n * OverlayLayersPanel.tsx's list. */\nexport function isKmlRegion(feature: Feature): boolean {\n return polygonRingsOf(feature).length > 0;\n}\n\n/** Every vertex of `feature` if it's a line: a LineString, a MultiLineString\n * (a GPX track with several segments) or a GeometryCollection of only\n * LineStrings (how @tmcw/togeojson reads a KML MultiGeometry -- see\n * polygonRingsOf). `[]` for anything else, meaning \"not a line\". */\nexport function lineCoordsOf(feature: Feature): Position[] {\n const geometry = feature.geometry;\n if (geometry?.type === \"LineString\") return geometry.coordinates;\n if (geometry?.type === \"MultiLineString\") return geometry.coordinates.flat();\n if (geometry?.type === \"GeometryCollection\" && geometry.geometries.length > 0\n && geometry.geometries.every((g) => g.type === \"LineString\")) {\n return geometry.geometries.flatMap((g) => (g.type === \"LineString\" ? g.coordinates : []));\n }\n return [];\n}\n\n/** True for a feature fetchAllKmlLines lists and MapCanvas.tsx's shapes\n * effect lets the panel hide/dim -- shared for the same reason as\n * isKmlRegion: the two must never drift apart. A region is never a line. */\nexport function isKmlLine(feature: Feature): boolean {\n return !isKmlRegion(feature) && lineCoordsOf(feature).length > 0;\n}\n\n/** An image overlay, as MapItemEditorDialog.tsx tracks one -- always 4\n * explicit world corners; see kmlMedia.ts's KmlImageOverlay, which this\n * mirrors on the write side. */\nexport interface ImageOverlay {\n handle: string;\n corners: OverlayCorners;\n name?: string;\n opacity?: number;\n order?: number;\n}\n\n/** The property marking a feature as an image overlay rather than a shape:\n * its Polygon is the image's 4 corners (top-left, top-right, bottom-right,\n * bottom-left, closed), and `image` is the picture's Media handle. This\n * app's own convention -- plain GeoJSON has no image overlays -- read back\n * by parseMapFile. */\nexport const IMAGE_OVERLAY_PROPERTY = \"gramps:image-overlay\";\n\nfunction overlayFeature(overlay: ImageOverlay): Feature {\n const ring = [...overlay.corners, overlay.corners[0]];\n return {\n type: \"Feature\",\n geometry: { type: \"Polygon\", coordinates: [ring] },\n properties: {\n [IMAGE_OVERLAY_PROPERTY]: true,\n image: overlay.handle,\n ...(overlay.name ? { name: overlay.name } : {}),\n ...(overlay.opacity !== undefined && overlay.opacity !== 1 ? { opacity: overlay.opacity } : {}),\n ...(overlay.order !== undefined ? { order: overlay.order } : {}),\n },\n };\n}\n\n/** Records which Media object an overlay was made from (mapOverlays.ts) --\n * a \"foreign member\" of the FeatureCollection, which GeoJSON allows. */\nexport const SOURCE_PROPERTY = \"gramps:source\";\n\n/** Every shape plus every image overlay, as one GeoJSON FeatureCollection.\n * Overlays go last: kmlMedia.ts's region/line row indexes count every\n * feature in the file but MapCanvas.tsx's leave overlays out, and the two\n * only agree while no overlay comes before a shape. */\nexport function mapItemToGeoJson(features: Feature[], overlays: ImageOverlay[] = [], source?: string): string {\n const collection: FeatureCollection & { [SOURCE_PROPERTY]?: string } = {\n type: \"FeatureCollection\",\n ...(source ? { [SOURCE_PROPERTY]: source } : {}),\n features: [...features, ...overlays.map(overlayFeature)],\n };\n return JSON.stringify(collection);\n}\n", "// Map geometry for shapes and image overlays, as plain math (no map\n// library): image overlay corners moved, scaled and rotated in Web Mercator\n// space so a rectangle stays one at any latitude, and bounding boxes over\n// features and overlays. Moved here from Gramps Connect (its kmlMedia.ts),\n// which shares it with the Story Editor; the \"kml\" in some names is from\n// there, where KML was the first map file format.\nimport type { Feature, Position } from \"geojson\";\n\n/** An overlay's 4 world corners, maplibre's own ImageSource `coordinates`\n * order (top-left, top-right, bottom-right, bottom-left) and type. */\nexport type OverlayCorners = [[number, number], [number, number], [number, number], [number, number]];\n\nconst DEG_TO_RAD = Math.PI / 180;\nconst RAD_TO_DEG = 180 / Math.PI;\n\n/** [lng,lat] -> normalized Web Mercator x/y (x east-positive, y\n * *south*-positive) -- the exact projection maplibre's own map.project()\n * uses internally, reimplemented here as plain math (no maplibre-gl\n * import) so this module stays free of that ~900KB dependency (see\n * MapItemEditorDialog.tsx's own doc comment on why maplibre-gl is always\n * behind a lazy import elsewhere in Gramps Connect). Unlike raw [lng,lat],\n * Mercator x/y is locally isotropic -- equal scale in both directions at\n * any given point, which is the whole point of the projection -- making it\n * the correct space to rotate a box in; see rotatePoint. */\nfunction mercatorFromLngLat([lng, lat]: [number, number]): [number, number] {\n const x = (180 + lng) / 360;\n const y = (180 - RAD_TO_DEG * Math.log(Math.tan(Math.PI / 4 + (lat * DEG_TO_RAD) / 2))) / 360;\n return [x, y];\n}\n\n/** mercatorFromLngLat's inverse. */\nfunction lngLatFromMercator([x, y]: [number, number]): [number, number] {\n const lng = x * 360 - 180;\n const lat = 90 - 2 * Math.atan(Math.exp((y * 360 - 180) * DEG_TO_RAD)) * RAD_TO_DEG;\n return [lng, lat];\n}\n\n/** Rotates one point by `rotationDeg` around `center` (positive =\n * counterclockwise, matching KML's own <LatLonBox><rotation> convention)\n * -- done in Mercator-projected space (see mercatorFromLngLat), not raw\n * [lng,lat], so a rectangle stays a rectangle (right angles preserved)\n * after rotating, regardless of latitude. A naive rotation directly on\n * [lng,lat] -- this function's original implementation -- shears instead,\n * since a degree of longitude and a degree of latitude aren't the same\n * ground (or screen) distance away from the equator (found live: a square\n * image's corners visibly stopped being 90 degrees once rotated away from\n * 0). Gramps Connect's own image overlays are never meant to be opened in an\n * external viewer (their picture is an app-internal Media reference), so diverging here\n * from what Google Earth's own <LatLonBox><rotation> would render is an\n * acceptable trade for Gramps Connect always rendering its own files correctly.\n * Pass a negative `rotationDeg` to go the other way (world -> local). */\nexport function rotatePoint(point: [number, number], center: [number, number], rotationDeg: number): [number, number] {\n const angle = rotationDeg * DEG_TO_RAD;\n const cos = Math.cos(angle);\n const sin = Math.sin(angle);\n const [px, py] = mercatorFromLngLat(point);\n const [cx, cy] = mercatorFromLngLat(center);\n // Mercator y increases southward -- negated here so this rotation is\n // expressed in a conventional north-up, east-right plane (dx\n // east-positive, dy north-positive), keeping \"positive rotationDeg =\n // counterclockwise\" the same sense every caller already expects.\n const dx = px - cx;\n const dy = -(py - cy);\n const rx = dx * cos - dy * sin;\n const ry = dx * sin + dy * cos;\n return lngLatFromMercator([cx + rx, cy - ry]);\n}\n\n/** Scales the distance from `center` to `point` by `scale`, in the same\n * Mercator-projected space rotatePoint rotates in -- so scaling a\n * rectangle (or an already-warped quad) uniformly from its own center\n * preserves its exact shape/aspect ratio regardless of latitude, the same\n * way rotatePoint preserves angles. MapItemEditorDialog.tsx's resize\n * handle applies this to all 4 corners at once (see mountOverlay). */\nexport function scalePoint(point: [number, number], center: [number, number], scale: number): [number, number] {\n const [px, py] = mercatorFromLngLat(point);\n const [cx, cy] = mercatorFromLngLat(center);\n return lngLatFromMercator([cx + (px - cx) * scale, cy + (py - cy) * scale]);\n}\n\n/** The distance between `a` and `b` in the same Mercator-projected space\n * scalePoint scales in -- not a real-world distance (Mercator distorts\n * those away from the equator), just a consistent unit for computing a\n * resize handle's before/after scale ratio, which only ever needs to be\n * internally consistent with scalePoint's own projection, not physically\n * meaningful on its own. */\nexport function mercatorDistance(a: [number, number], b: [number, number]): number {\n const [ax, ay] = mercatorFromLngLat(a);\n const [bx, by] = mercatorFromLngLat(b);\n return Math.hypot(bx - ax, by - ay);\n}\n\n/** The angle (degrees, positive = counterclockwise, 0 = due east) from\n * `from` to `to` -- measured in the same Mercator-projected, north-up\n * plane rotatePoint itself rotates in, so a caller tracking \"how far has\n * the user dragged this handle around the center\" gets an angle that\n * matches how rotatePoint will then actually render it (unlike a plain\n * atan2 on raw [lng,lat], which suffers the same latitude-dependent skew\n * rotatePoint's own doc comment describes). */\nexport function mercatorBearing(from: [number, number], to: [number, number]): number {\n const [fx, fy] = mercatorFromLngLat(from);\n const [tx, ty] = mercatorFromLngLat(to);\n return Math.atan2(-(ty - fy), tx - fx) * RAD_TO_DEG;\n}\n\n/** An axis-aligned box plus a rotation about its own center's 4 world\n * corners -- KML's own <LatLonBox><rotation> model. Only ever needed to\n * interpret an *old* overlay saved before Gramps Connect always wrote\n * explicit corners (see its fetchAllKmlImageOverlays); the map editor\n * itself only ever deals with corners directly, never this box shape. */\nexport function cornersFromBox(\n box: { north: number; south: number; east: number; west: number; rotation: number },\n): OverlayCorners {\n const center: [number, number] = [(box.west + box.east) / 2, (box.north + box.south) / 2];\n const corners: [number, number][] = [\n [box.west, box.north], [box.east, box.north], [box.east, box.south], [box.west, box.south],\n ];\n return corners.map((p) => rotatePoint(p, center, box.rotation)) as OverlayCorners;\n}\n\nfunction visitCoordinates(coords: unknown, bounds: [number, number, number, number]): void {\n if (typeof (coords as Position)[0] === \"number\") {\n const [lng, lat] = coords as Position;\n if (lng < bounds[0]) bounds[0] = lng;\n if (lat < bounds[1]) bounds[1] = lat;\n if (lng > bounds[2]) bounds[2] = lng;\n if (lat > bounds[3]) bounds[3] = lat;\n return;\n }\n for (const c of coords as unknown[]) visitCoordinates(c, bounds);\n}\n\n/** Every coordinate's bounding box across a set of KML features, as\n * [west, south, east, north] -- maplibre's own LngLatBoundsLike corner\n * order, so a caller can hand this straight to fitBounds/cameraForBounds to\n * frame a field boundary or a route at whatever zoom actually fits it,\n * rather than guessing one. Null when the features carry no coordinates at\n * all (an empty file, or every fetch above failed). */\nexport function kmlBounds(features: Feature[]): [west: number, south: number, east: number, north: number] | null {\n const bounds: [number, number, number, number] = [Infinity, Infinity, -Infinity, -Infinity];\n for (const feature of features) {\n const geometry = feature.geometry;\n if (!geometry) continue;\n if (geometry.type === \"GeometryCollection\") {\n for (const g of geometry.geometries) if (\"coordinates\" in g) visitCoordinates(g.coordinates, bounds);\n continue;\n }\n visitCoordinates(geometry.coordinates, bounds);\n }\n return Number.isFinite(bounds[0]) ? bounds : null;\n}\n\n/** The center of kmlBounds -- a plain bbox center rather than an\n * area-weighted centroid, which is all a \"roughly where does this file sit\"\n * marker position needs. */\nexport function kmlCenter(features: Feature[]): [lat: number, long: number] | null {\n const bounds = kmlBounds(features);\n if (!bounds) return null;\n return [(bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2];\n}\n\n/** Same bbox as kmlBounds, but over a set of image overlays' corners instead\n * of vector features -- overlays (GroundOverlay/region placemarks) carry no\n * Point/LineString/Polygon geometry for kmlBounds to find anything in, since\n * fetchAllKmlFeatures excludes them entirely. Lets a caller fit the camera to\n * an overlay-only KML attachment the same way it would a vector one. */\nexport function kmlOverlayBounds(\n overlays: { corners: OverlayCorners }[],\n): [west: number, south: number, east: number, north: number] | null {\n if (overlays.length === 0) return null;\n const bounds: [number, number, number, number] = [Infinity, Infinity, -Infinity, -Infinity];\n for (const overlay of overlays) {\n for (const [lng, lat] of overlay.corners) {\n if (lng < bounds[0]) bounds[0] = lng;\n if (lat < bounds[1]) bounds[1] = lat;\n if (lng > bounds[2]) bounds[2] = lng;\n if (lat > bounds[3]) bounds[3] = lat;\n }\n }\n return bounds;\n}\n\n/** Combines two nullable bboxes (as returned by kmlBounds/kmlOverlayBounds)\n * into the box that encloses both -- a place's KML attachment(s) can carry\n * vector shapes and image overlays side by side, and framing the camera to\n * just one kind would clip whichever the caller didn't ask for. Either side\n * missing (no shapes, or no overlays) just passes the other through; both\n * missing is null, same as either input alone would be. */\nexport function unionBounds(\n a: [west: number, south: number, east: number, north: number] | null,\n b: [west: number, south: number, east: number, north: number] | null,\n): [west: number, south: number, east: number, north: number] | null {\n if (!a) return b;\n if (!b) return a;\n return [Math.min(a[0], b[0]), Math.min(a[1], b[1]), Math.max(a[2], b[2]), Math.max(a[3], b[3])];\n}\n\n/** Same bbox-center guess as kmlCenter, but over a set of image overlays'\n * corners instead of vector features -- useVisualData.ts's fallback for a\n * PendingKmlPlace whose only KML attachment is a GroundOverlay (an\n * overlay-only file: no Point/LineString/Polygon placemark for kmlCenter to\n * find anything in, since fetchAllKmlFeatures excludes GroundOverlay\n * placemarks entirely). Without this, a place with only an image overlay\n * attached and no lat/long of its own never gets a derived position and so\n * never renders on the map until someone sets its coordinates by hand\n * (found live). */\nexport function kmlOverlayCenter(overlays: { corners: OverlayCorners }[]): [lat: number, long: number] | null {\n const bounds = kmlOverlayBounds(overlays);\n if (!bounds) return null;\n return [(bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2];\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/dist/player.js CHANGED
@@ -268,7 +268,9 @@ function MapBackground({ initialCenter, current, dark, colors, panelFraction, oh
268
268
  type: "fill",
269
269
  source: SHAPE_SOURCE,
270
270
  filter: ["==", ["geometry-type"], "Polygon"],
271
- paint: { "fill-color": color, "fill-opacity": 0.25 }
271
+ // And its own `opacity` (what the map editor sets on an area) over
272
+ // a light default.
273
+ paint: { "fill-color": color, "fill-opacity": ["coalesce", ["to-number", ["get", "opacity"], 0.25], 0.25] }
272
274
  });
273
275
  map.addLayer({
274
276
  id: LINE_LAYER,