From 943e7e6a8a9035248ddc0aba2b5d2960ac0efe17 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Mon, 23 Sep 2024 00:36:32 +0200 Subject: [PATCH] paste geojson features --- package-lock.json | 8 +-- package.json | 2 +- src/App.tsx | 137 +++++++++++++++++++++++++++------------------- 3 files changed, 85 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index 50ea489..2c8060c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@turf/great-circle": "^7.1.0", "maplibre-gl": "^4.6.0", "protomaps-themes-base": "^3.1.0", - "s2js": "^1.43.5", + "s2js": "^1.43.6", "solid-js": "^1.8.20", "terra-draw": "^1.0.0-beta.1" }, @@ -1865,9 +1865,9 @@ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" }, "node_modules/s2js": { - "version": "1.43.5", - "resolved": "https://registry.npmjs.org/s2js/-/s2js-1.43.5.tgz", - "integrity": "sha512-D6ySUKj3d0TnwYbOZC5K6wzN0+CXoatXtZoveAJUuvABByhV9h7Rs88DY2SQGFSQ7WM4M0jUeoh/AS/itDfwXw==", + "version": "1.43.6", + "resolved": "https://registry.npmjs.org/s2js/-/s2js-1.43.6.tgz", + "integrity": "sha512-cgTyjeqCVcacG699Rl5nZhyoazwoBs9S5DG/e6O1zxZ71IIWwiy2gF7BJxJWZ6Y+MppoGLOqMSSknGWAZrOgLA==", "dependencies": { "bigfloat": "^0.1.1" } diff --git a/package.json b/package.json index c1be3ce..e794f4f 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "@turf/great-circle": "^7.1.0", "maplibre-gl": "^4.6.0", "protomaps-themes-base": "^3.1.0", - "s2js": "^1.43.5", + "s2js": "^1.43.6", "solid-js": "^1.8.20", "terra-draw": "^1.0.0-beta.1" }, diff --git a/src/App.tsx b/src/App.tsx index 664e7b3..7fc9613 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -224,70 +224,109 @@ function App() { const [maxCells, setMaxCells] = createSignal(200); const [drawMode, setDrawMode] = createSignal(""); const [cellUnionText, setCellUnionText] = createSignal(""); + const [geojsonText] = createSignal(""); const [cellUnionLength, setCellUnionLength] = createSignal(0); const [loadError, setLoadError] = createSignal(""); - let textArea: HTMLTextAreaElement | undefined; - - const computeCoveringForDraw = () => { - const snapshot = draw!.getSnapshot() as Feature[]; + const [geojsonLoadError, setGeoJsonLoadError] = createSignal(""); - const covering = getCovering(regionCoverer, snapshot); - displayCovering(covering); + // list of features currently loaded in the map view + const [features, setFeatures] = createSignal([], { + equals: () => false, + }); + const clearFeatures = () => setFeatures((features) => features.slice(0, 0)); + const addFeatures = (additions: Feature[]) => { + setFeatures((features) => { + for (let feat of additions) { + const hash = JSON.stringify(feat); + const duplicate = features.some((f) => JSON.stringify(f) === hash); + if (!duplicate) features.push(feat); + } + return features; + }); }; - const displayCovering = (covering: s2.CellUnion) => { - (map!.getSource("covering") as maplibregl.GeoJSONSource).setData( - getCellVisualization(covering), - ); + // re-render covering when features / tokens change + createEffect(() => + displayCovering(getCovering(regionCoverer, features()), features()), + ); + + let textArea: HTMLTextAreaElement | undefined; + let jsonArea: HTMLTextAreaElement | undefined; + + const displayCovering = (covering: s2.CellUnion, features?: Feature[]) => { + if (!map) return; + + const source: maplibregl.GeoJSONSource = map.getSource("covering")!; + source.setData(getCellVisualization(covering)); setCellUnionText([...covering].map((c) => s2.cellid.toToken(c)).join(", ")); setCellUnionLength(covering.length); + + // dont zoom if all features are from terradraw + if ( + features && + features.every((f) => f.id && f.id.toString().length === 36) + ) { + return; + } + + const rect = covering.rectBound(); + map.fitBounds( + [ + [s1.angle.degrees(rect.lng.lo), s1.angle.degrees(rect.lat.lo)], + [s1.angle.degrees(rect.lng.hi), s1.angle.degrees(rect.lat.hi)], + ], + { padding: 50 }, + ); }; const loadCoveringFromText = () => { - let rect = s2.Rect.emptyRect(); - - draw.clear(); if (!textArea) return; try { const covering = new s2.CellUnion( - ...textArea.value.split(", ").map((token) => { - const cellid = s2.cellid.fromToken(token); - const cell = s2.Cell.fromCellID(cellid); - rect = rect.union(cell.rectBound()); - return cellid; - }), + ...textArea.value + .trim() + .split(",") + .map((t) => t.trim()) + .map(s2.cellid.fromToken), ); + draw.clear(); + clearFeatures(); displayCovering(covering); - map.fitBounds( - [ - [s1.angle.degrees(rect.lng.lo), s1.angle.degrees(rect.lat.lo)], - [s1.angle.degrees(rect.lng.hi), s1.angle.degrees(rect.lat.hi)], - ], - { padding: 50 }, - ); setLoadError(""); } catch (e: any) { setLoadError(e.message); } }; + const loadGeoJsonFromText = () => { + if (!jsonArea) return; + + try { + const feature = JSON.parse(jsonArea.value) as Feature; + if (feature?.type !== "Feature") throw new Error("Invalid Feature"); + clearFeatures(); + addFeatures([feature]); + setGeoJsonLoadError(""); + } catch (e: any) { + setGeoJsonLoadError(e.message.split(":")[0]); + } + }; + createEffect(() => { regionCoverer = new geojson.RegionCoverer({ minLevel: minLevel(), maxLevel: maxLevel(), maxCells: maxCells(), }); - if (draw) { - computeCoveringForDraw(); - } + displayCovering(getCovering(regionCoverer, features()), features()); }); const clear = () => { draw.clear(); - computeCoveringForDraw(); + clearFeatures(); startDrawMode("rectangle"); }; @@ -337,7 +376,7 @@ function App() { draw.on("finish", () => { startDrawMode("render"); - computeCoveringForDraw(); + addFeatures(draw.getSnapshot()); }); draw.start(); @@ -443,31 +482,6 @@ function App() { ); } }); - - // secret paste polygon from clipboard command - var keyMap: { [name: string]: boolean } = {}; - window.onkeydown = window.onkeyup = async (e) => { - keyMap[e.code] = e.type == "keydown"; - if (e.type != "keydown") return; - - // cntrl + shift + v - if (!["ControlLeft", "ShiftLeft", "KeyV"].every((c) => keyMap[c])) return; - - try { - console.error("secret keyboard shortcut activated!!"); - if (!navigator?.clipboard) throw new Error("clipboard API unavailable"); - const clip = await navigator.clipboard.readText(); - const feature = JSON.parse(clip); - if (feature?.type !== "Feature") { - throw new Error("invalid feature"); - } - const covering = getCovering(regionCoverer, [feature]); - displayCovering(covering); - } catch (e) { - console.error("clipboard paste failed"); - console.error(e); - } - }; }); return ( @@ -538,7 +552,16 @@ function App() { ) : ( {cellUnionLength()} cells )} - + + + +
+ {geojsonLoadError() ? ( + {geojsonLoadError()} + ) : ( + + )} +