all repos — onyx @ 9f441bfaaf9f3526676ab85fe4a9a5c3386ca23e

minimal map annotation and location data sharing tool

implement import/export, tweak styles, update readme, and add leaflet license
Iris Lightshard nilix@nilfm.cc
PGP Signature
-----BEGIN PGP SIGNATURE-----

iQIzBAABCAAdFiEEkFh6dA+k/6CXFXU4O3+8IhROY5gFAmMBbCEACgkQO3+8IhRO
Y5h7KQ/9HLKKb0d45ov5L2e4zOO16a0xYfUBON20Ow/uGrHyTAMx5V5X9BDTFje8
87SQZ9K8c+IExG0EDBwNRobXmjbH9/osQd6zy1FQSAY/VIm8fSRqqLTVO+1Lwb0v
PjwR8HPYUVl8rizUpxAY2Z92SD6gzTZT92rV7B0f6Q4KIq9whwkTmt3CkB+AjPjv
vaVt+zGoc1/g9HYNd4iz+x9hzKQ1SW5WRRQXfXbw35/7l92KCSILBXGbJtIHXnpM
PiOooeV4DOgmoUEU3aY4BqhPlJv7IuqKw4srNXlIjc5r95R+jCABrx32q741lt8/
MkA2oBM8w1in25oY8rUr16kbaYNbRyVPqoxIUll1R5CXnicd18ftd+jql3v2CeMI
dJIFB6YUjs6ILDFHUUvOTx8O1h1twhLPsi2W/nAUMp79OnDNmEu7FAF1BnoW3EuD
2nqJ6LKmQbHLyVzHqb4lHIeY6QNLElQ/z1uOcH53l0lLot3cE5Ou2sDRXZlimkjq
4I4lzLCULcL8xjQ83I+vAcbOz+xUnGRErt6B4tjRN/+OyfxQ9eBJU2ziPUpeQCQ3
DapaTxHXEHtDjk3MkPUeLgLFmoN1GU3jVnZVP11jF2YwV/LiDbzPxhFPxCq0E9hI
ke15ouNX7Fx4ZeE0I0z2BWmIu3LdT7aCW/OMk49FM6YNOTO4my8=
=QPYC
-----END PGP SIGNATURE-----
commit

9f441bfaaf9f3526676ab85fe4a9a5c3386ca23e

parent

30766accd61c704d72399bcfdc2591155ae25e53

M README.mdREADME.md

@@ -2,12 +2,70 @@ # onyx/scry

## about -`onyx/scry` is a lightweight map annotation and location data management and sharing tool built using [leaflet](https://leafletjs.com) and [typescript](https://typescriptlang.org). It is intended as a standalone tool to generate, manage, and share simple location data in the form of points, circles, and polygons. All of these have associated titles and descriptions and can be easily imported or exported to a `json` format for easy sharing. All data is saved locally via the `localStorage` API, and the only network calls are those to retrieve either the streetmap or satellite tile data. +`onyx/scry` is a lightweight map annotation and location data management and sharing tool built using [leaflet](https://leafletjs.com) and [typescript](https://typescriptlang.org). It is intended as a standalone tool to generate, manage, and share simple location data in the form of points, circles, and polygons. All of these have associated titles and descriptions and can be easily imported from or exported to JSON format for easy sharing. All data is saved locally via the `localStorage` API, and the only network calls are those to retrieve either the streetmap or satellite tile data. ## usage -_coming soon_ +When you launch the application for the first time, it will ask for your current location to set the `home` point. If you deny permission or it can't determinte your location, you can set it later via the menu and the map will zoom out to fit the entire Earth. + +Along the bottom is the control bar, containing the following buttons: + +- `Home`: If the `home` point has been set, center the map view on it. +- `Marker`: Enter marker creation mode. +- `Circle`: Enter circle creation mode. +- `Polygon`: Enter polygon creation mode. +- `Tileset`: Swap the map tiles between the streetmap and satellite imagery. +- `Save`: Saves your current overlays to local storage. +- `Clear`: Clears all overlays from the map. +- `Restore`: Restores overlays from local storage. +- `Menu`: Show/hide the overlay management menu. + +Clicking an overlay shows a popup with the name and description. + +### Overlay Creation + +Clicking the `Marker`, `Circle`, or `Polygon` buttons, you enter overlay creation mode for that overlay type. A small window at the bottom of the screen appears with a cancel button which allows you to leave this mode. + +For Markers and Circles, you just click anywhere on the map to bring up the Overlay Creation window. + +For Polygons, you click points on the map to add them to the polygon — you will see the outline once you have added at least two points. Once you have added at least three points, an OK button appears on the window at bottom, and clicking it opens the Overlay Creation window. + +In the Overlay Creation window, you can set a name and optionally a description for the overlay. For circles, you also set the radius in meters, which defaults to 500. Pressing the `OK` button saves the overlay to the map. + +### Overlay Management + +Opening the menu shows a list of all overlays organized by type; clicking on any overlay brings up the `Overlay Detail` window. At the bottom of the menu are also three buttons: + +- `Set Home`: Sets the `home` point to the center of the current map view. +- `Import`: Opens the Import window to import overlay data from JSON format. +- `Export All`: Opens the Export window to export all overlay data to JSON format. + +### Overlay Detail + +This window is similar to the Overlay Creation window save the addition of three buttons: + +- `Go Here`: Centers the map view on this overlay. +- `Export`: Opens the Export window to export this overlay to JSON format. +- `Delete`: Deletes this overlay from the map. + +### Import/Export + +When exporting, the `Copy to Clipboard` button does just that. The JSON data can then be saved to a text file, emailed to a friend, or what have you. + +When importing, the imported data is added to the current overlays. If you want to overwrite your current data, clear it first. + +## build/deploy + +If you want to hack on `onyx-scry` and rebuild it, I recommend rebuilding the `sourcemapper` (written in `go`) in the `buildtools` directory first (in case your `glibc` version is older). + +The build process for `onyx-scry` is a bit unorthodox. Instead of using modules, the `typescript` code is all concatenated before transpiling. The `sourcemapper` is used to show lines in the original source where transpilation errors occured, in a plumbable address format so you can right-click in `acme` and go directly to that line of code. + +From the `src` directory, you can just run `./build.sh`. Once built, the final `javascript` file is located in the `static` directory with the rest of the application. + +Deploying is simple: just serve the `static` directory. ## license `onyx/scry` is distributed under the [MIT license](./LICENSE) - basically do whatever you want with it but leave my name on it. + +`leaflet` is distributed under a similar [2-clause BSD license](./LEAFLET-LICENSE).
M src/10-overlay.tssrc/10-overlay.ts

@@ -237,25 +237,33 @@ }

static load(): OverlayState { const store = localStorage.getItem("overlay_state"); - if (store) { - const model = JSON.parse(store); - return { - markers: model.markers.map((m: OverlayData) => OverlayState.fromData(m)), - circles: model.circles.map((c: OverlayData) => OverlayState.fromData(c)), - polygons: model.polygons.map((p: OverlayData) => OverlayState.fromData(p)), - polyline: new Polyline(), - } as OverlayState - } else { - return new OverlayState(); - } + return store ? OverlayState.import(store) : new OverlayState; + } + + static import(overlayData: string): OverlayState { + const model = JSON.parse(overlayData); + return { + markers: model.markers.map((m: OverlayData) => OverlayState.fromData(m)), + circles: model.circles.map((c: OverlayData) => OverlayState.fromData(c)), + polygons: model.polygons.map((p: OverlayData) => OverlayState.fromData(p)), + polyline: new Polyline(), + } as OverlayState + } + + static exportSingle(overlay: OverlayBase): string { + return JSON.stringify(OverlayState.toData(overlay), null, 2); } - static save(overlayState: OverlayState): void { - localStorage.setItem("overlay_state", JSON.stringify({ + static export(overlayState: OverlayState): string { + return JSON.stringify({ markers: overlayState.markers.map((m: OverlayBase) => OverlayState.toData(m)), circles: overlayState.circles.map((c: OverlayBase) => OverlayState.toData(c)), polygons: overlayState.polygons.map((p: OverlayBase) => OverlayState.toData(p)), - })); + }, null, 2); + } + + static save(overlayState: OverlayState): void { + localStorage.setItem("overlay_state", OverlayState.export(overlayState)); } static clear(overlayState: OverlayState, map: L.Map): OverlayState {

@@ -263,7 +271,9 @@ overlayState.markers.forEach((m: Marker) => m.remove(map));

overlayState.circles.forEach((c: Circle) => c.remove(map)); overlayState.polygons.forEach((p: Polygon) => p.remove(map)); - return new OverlayState(); + const self = new OverlayState(); + self.polyline.add(map); + return self; } private static toData(source: OverlayBase): OverlayData {

@@ -286,6 +296,43 @@ return new Circle(data.name, data.desc, data.points[0], data.options);

case OverlayType.POLYGON: return new Polygon(data.name, data.desc, data.points, data.options); } + } + + static importWrapper(data: string, overlayState: OverlayState, map: L.Map): boolean { + try { + const singleData = <OverlayData>JSON.parse(data); + const overlay = OverlayState.fromData(singleData); + switch (singleData.type) { + case OverlayType.POINT: + overlayState.markers.push(overlay); + break; + case OverlayType.CIRCLE: + overlayState.circles.push(overlay); + break; + case OverlayType.POLYGON: + overlayState.polygons.push(overlay); + break; + } + overlay.add(map); + return true; + } catch {} + try { + const self = OverlayState.import(data); + self.markers.forEach((m: Marker) => { + overlayState.markers.push(m); + m.add(map); + }); + self.circles.forEach((c: Circle) => { + overlayState.circles.push(c); + c.add(map); + }); + self.polygons.forEach((p: Polygon) => { + overlayState.polygons.push(p); + p.add(map); + }); + return true; + } catch {} + return false; }
M src/20-createOverlayModal.tssrc/20-createOverlayModal.ts

@@ -152,7 +152,9 @@ }

} if (exportBtn) { - // show export window with this Overlay's OverlayData + exportBtn.onclick = () => { + MapHandler.exportSingle(args.self); + } } if (deleteBtn) {
A src/24-importExportModal.ts

@@ -0,0 +1,64 @@

+class ImportExportModal implements Modal { + self(): HTMLElement | null { + return document.getElementById("import-export-container"); + } + + visible(): boolean { + return this.self()?.style.display != "none"; + } + + setVisible(v: boolean): void { + const modal = this.self(); + if (modal) { + modal.style.display = v ? "block" : "none"; + } + } + + setTitle(title: string): void { + const modalH2 = document.getElementById("import-export-title"); + if (modalH2) { + modalH2.innerHTML = title; + } + } + + setTextArea(text: string, readonly: boolean): void { + const textarea = document.getElementById("import-export-textarea") as HTMLTextAreaElement; + if (textarea) { + textarea.value = text; + textarea.readOnly = readonly; + } + } + + setErrMsg(text: string, visible: boolean): void { + const errMsg = document.getElementById("import-export-error"); + if (errMsg) { + errMsg.innerHTML = text; + errMsg.style.display = visible ? "unset" : "none"; + } + } + + copyTextArea(): void { + const textarea = document.getElementById("import-export-textarea") as HTMLTextAreaElement; + if (textarea) { + textarea.select(); + textarea.setSelectionRange(0, 9999999); + navigator.clipboard.writeText(textarea.value); + } + } + + getText(): string { + const textarea = document.getElementById("import-export-textarea") as HTMLTextAreaElement; + if (textarea) { + return textarea.value; + } + return ""; + } + + okBtn(): HTMLElement | null { + return document.getElementById("import-export-ok-btn"); + } + + cancelBtn(): HTMLElement | null { + return document.getElementById("import-export-cancel-btn"); + } +}
M src/29-modalCollection.tssrc/29-modalCollection.ts

@@ -4,25 +4,30 @@ cancel: CancelModal;

okCancel: OKCancelModal; info: InfoModal; overlayMgr: OverlayManagementModal; + importExport: ImportExportModal; constructor( createOverlay: CreateOverlayModal, cancel: CancelModal, okCancel: OKCancelModal, info: InfoModal, - overlayMgr: OverlayManagementModal + overlayMgr: OverlayManagementModal, + importExport: ImportExportModal ) { this.createOverlay = createOverlay; this.cancel = cancel; this.okCancel = okCancel; this.info = info; this.overlayMgr = overlayMgr; + this.importExport = importExport; } closeAll(): void { this.createOverlay.setVisible(false); this.cancel.setVisible(false); this.okCancel.setVisible(false); + this.info.setVisible(false); this.overlayMgr.setVisible(false); + this.importExport.setVisible(false); } }
M src/40-handlers.tssrc/40-handlers.ts

@@ -400,4 +400,89 @@ }

self.modals.okCancel.setVisible(true); } } + + static exportSingle(overlay: OverlayBase): void { + const self = MapHandler.instance; + if (self) { + self.modals.closeAll(); + self.modals.importExport.setTitle("Export Overlay"); + self.modals.importExport.setErrMsg("", false); + + const okBtn = self.modals.importExport.okBtn(); + if (okBtn) { + okBtn.innerText = "Copy to clipboard"; + okBtn.onclick = () => { + self.modals.importExport.copyTextArea(); + self.modals.closeAll(); + self.modals.info.setMsg("Copied the data to the clipboard"); + self.modals.info.setVisible(true); + } + } + + self.modals.importExport.setTextArea(OverlayState.exportSingle(overlay), true); + self.modals.importExport.setVisible(true); + } + } + + static exportAll(): void { + const self = MapHandler.instance; + if (self) { + self.modals.closeAll(); + self.modals.importExport.setTitle("Export All Overlays"); + self.modals.importExport.setErrMsg("", false); + + const okBtn = self.modals.importExport.okBtn(); + if (okBtn) { + okBtn.innerText = "Copy to clipboard"; + okBtn.onclick = () => { + self.modals.importExport.copyTextArea(); + self.modals.closeAll(); + self.modals.info.setMsg("Copied the data to the clipboard"); + self.modals.info.setVisible(true); + } + } + + self.modals.importExport.setTextArea(OverlayState.export(self.overlays), true); + self.modals.importExport.setVisible(true); + } + } + + static import(): void { + const self = MapHandler.instance; + if (self) { + self.modals.closeAll(); + self.modals.importExport.setTitle("Import Overlay Data"); + self.modals.importExport.setErrMsg("", false); + self.modals.importExport.setTextArea("", false); + + const okBtn = self.modals.importExport.okBtn(); + if (okBtn) { + okBtn.innerText = "Import"; + okBtn.onclick = () => { + MapHandler.doImport(self.modals.importExport.getText()); + } + } + self.modals.importExport.setVisible(true); + } + } + + static doImport(data: string): void { + const self = MapHandler.instance; + if (self) { + if (OverlayState.importWrapper(data, self.overlays, self.map)) { + self.modals.closeAll(); + self.modals.info.setMsg("Import successful"); + self.modals.info.setVisible(true); + } else { + self.modals.importExport.setErrMsg("The data was malformed &mdash; please check that it is valid JSON exported from ONYX/scry", true); + } + } + } + + static closeImportExport(): void { + const self = MapHandler.instance; + if (self) { + self.modals.closeAll(); + } + } }
M src/99-onyx-scry.tssrc/99-onyx-scry.ts

@@ -10,7 +10,7 @@ L.tileLayer(

'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, - attribution: "street map tiles &copy; OpenStreetMap" + attribution: "street map data &copy; OpenStreetMap contributors" })); const satelliteLayer = TileLayerWrapper.constructLayer(

@@ -19,7 +19,7 @@ L.tileLayer(

'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { maxZoom: 19, - attribution: "satellite tiles &copy; Esri" + attribution: "satellite data &copy; Esri" })); TileLayerWrapper.enableOnly("streetLayer", map);

@@ -34,7 +34,8 @@ new CreateOverlayModal(),

new CancelModal(), new OKCancelModal(), new InfoModal(), - new OverlayManagementModal()); + new OverlayManagementModal(), + new ImportExportModal()); MapHandler.init(map, overlays, TileLayerWrapper.layers, modals);

@@ -43,14 +44,16 @@ MapHandler.setButtonClick("addPoint-btn", MapHandler.markerCollect);

MapHandler.setButtonClick("addCircle-btn", MapHandler.circleCollect); MapHandler.setButtonClick("addPolygon-btn", MapHandler.polygonCollect); + MapHandler.setButtonClick("tiles-btn", MapHandler.swapTiles); MapHandler.setButtonClick("save-btn", MapHandler.overlaySave); MapHandler.setButtonClick("clear-btn", MapHandler.overlayClear); MapHandler.setButtonClick("restore-btn", MapHandler.overlayReset); MapHandler.setButtonClick("menu-btn", MapHandler.toggleMenu); MapHandler.setButtonClick("set-home-btn", MapHandler.setHome); - - MapHandler.setButtonClick("tiles-btn", MapHandler.swapTiles); + MapHandler.setButtonClick("export-all-btn", MapHandler.exportAll); + MapHandler.setButtonClick("import-export-cancel-btn", MapHandler.closeImportExport); + MapHandler.setButtonClick("import-btn", MapHandler.import); map.on("locationfound", MapHandler.setHome);

@@ -60,6 +63,9 @@ info.setMsg("Could not get location data");

info.setVisible(true); }); + // the menu doesn't open on the first click unless we do this first... not sure why + modals.closeAll(); + const homeData = localStorage.getItem("home"); if (homeData) { const home = <Point>JSON.parse(homeData);

@@ -67,8 +73,6 @@ map.setView(home, 13);

} else { map.locate({setView: true, maxZoom: 13}); } - - modals.closeAll(); } init();
A src/LEAFLET-LICENSE

@@ -0,0 +1,28 @@

+ +BSD 2-Clause License + +Copyright (c) 2010-2022, Vladimir Agafonkin +Copyright (c) 2010-2011, CloudMade +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +Footer
M static/index.htmlstatic/index.html

@@ -4,6 +4,7 @@ <head>

<meta charset='utf-8'> <meta name='description' content='map annotation tool'/> <meta name='viewport' content='width=device-width,initial-scale=1'> +<link rel='stylesheet' type="text/css" href="./leaflet.css"> <link rel='stylesheet' type='text/css' href='./style.css'> <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />

@@ -78,19 +79,24 @@ </div>

</div> <div id="import-export-container"> - <h2></h2> + <div clas="modalHeader"> + <h2 id="import-export-title"></h2> + </div> + <div id="import-export-content"> <textarea id="import-export-textarea"></textarea> + <span id="import-export-error"></span> <div class="multiBtn-container"> <button class="positive-btn" id="import-export-ok-btn">OK</button> <button class="negative-btn" id="import-export-cancel-btn">Cancel</button> + </div> </div> </div> <div id="overlays-menu-container"> <div id="overlays-list"> - <details id="markers-wrapper"><summary>Markers</summary><ul id="markers-list"></ul></details> - <details id="circles-wrapper"><summary>Circles</summary><ul id="circles-list"></ul></details> - <details id="polygons-wrapper"><summary>Polygons</summary><ul id="polygons-list"></ul></details> + <details id="markers-wrapper" open><summary>Markers</summary><ul id="markers-list"></ul></details> + <details id="circles-wrapper" open><summary>Circles</summary><ul id="circles-list"></ul></details> + <details id="polygons-wrapper" open><summary>Polygons</summary><ul id="polygons-list"></ul></details> </div> <div class="multiBtn-container"> <button id="set-home-btn">Set Home</button>

@@ -100,7 +106,6 @@ </div>

</div> </body> -<link rel='stylesheet' type="text/css" href="./leaflet.css"> <script src="./leaflet.js"></script> <script src="./onyx-scry.js"></script> </html>
M static/onyx-scry.jsstatic/onyx-scry.js

@@ -172,31 +172,37 @@ this.polyline = new Polyline();

} static load() { const store = localStorage.getItem("overlay_state"); - if (store) { - const model = JSON.parse(store); - return { - markers: model.markers.map((m) => OverlayState.fromData(m)), - circles: model.circles.map((c) => OverlayState.fromData(c)), - polygons: model.polygons.map((p) => OverlayState.fromData(p)), - polyline: new Polyline(), - }; - } - else { - return new OverlayState(); - } + return store ? OverlayState.import(store) : new OverlayState; + } + static import(overlayData) { + const model = JSON.parse(overlayData); + return { + markers: model.markers.map((m) => OverlayState.fromData(m)), + circles: model.circles.map((c) => OverlayState.fromData(c)), + polygons: model.polygons.map((p) => OverlayState.fromData(p)), + polyline: new Polyline(), + }; + } + static exportSingle(overlay) { + return JSON.stringify(OverlayState.toData(overlay), null, 2); } - static save(overlayState) { - localStorage.setItem("overlay_state", JSON.stringify({ + static export(overlayState) { + return JSON.stringify({ markers: overlayState.markers.map((m) => OverlayState.toData(m)), circles: overlayState.circles.map((c) => OverlayState.toData(c)), polygons: overlayState.polygons.map((p) => OverlayState.toData(p)), - })); + }, null, 2); + } + static save(overlayState) { + localStorage.setItem("overlay_state", OverlayState.export(overlayState)); } static clear(overlayState, map) { overlayState.markers.forEach((m) => m.remove(map)); overlayState.circles.forEach((c) => c.remove(map)); overlayState.polygons.forEach((p) => p.remove(map)); - return new OverlayState(); + const self = new OverlayState(); + self.polyline.add(map); + return self; } static toData(source) { let type = OverlayType.POINT;

@@ -219,6 +225,44 @@ case OverlayType.POLYGON:

return new Polygon(data.name, data.desc, data.points, data.options); } } + static importWrapper(data, overlayState, map) { + try { + const singleData = JSON.parse(data); + const overlay = OverlayState.fromData(singleData); + switch (singleData.type) { + case OverlayType.POINT: + overlayState.markers.push(overlay); + break; + case OverlayType.CIRCLE: + overlayState.circles.push(overlay); + break; + case OverlayType.POLYGON: + overlayState.polygons.push(overlay); + break; + } + overlay.add(map); + return true; + } + catch (_a) { } + try { + const self = OverlayState.import(data); + self.markers.forEach((m) => { + overlayState.markers.push(m); + m.add(map); + }); + self.circles.forEach((c) => { + overlayState.circles.push(c); + c.add(map); + }); + self.polygons.forEach((p) => { + overlayState.polygons.push(p); + p.add(map); + }); + return true; + } + catch (_b) { } + return false; + } } class TileLayerWrapper { constructor(name, self) {

@@ -400,7 +444,9 @@ args.map.setView(args.self.center());

}; } if (exportBtn) { - // show export window with this Overlay's OverlayData + exportBtn.onclick = () => { + MapHandler.exportSingle(args.self); + }; } if (deleteBtn) { deleteBtn.onclick = () => {

@@ -578,6 +624,62 @@ modal.style.display = v ? "block" : "none";

} } } +class ImportExportModal { + self() { + return document.getElementById("import-export-container"); + } + visible() { + var _a; + return ((_a = this.self()) === null || _a === void 0 ? void 0 : _a.style.display) != "none"; + } + setVisible(v) { + const modal = this.self(); + if (modal) { + modal.style.display = v ? "block" : "none"; + } + } + setTitle(title) { + const modalH2 = document.getElementById("import-export-title"); + if (modalH2) { + modalH2.innerHTML = title; + } + } + setTextArea(text, readonly) { + const textarea = document.getElementById("import-export-textarea"); + if (textarea) { + textarea.value = text; + textarea.readOnly = readonly; + } + } + setErrMsg(text, visible) { + const errMsg = document.getElementById("import-export-error"); + if (errMsg) { + errMsg.innerHTML = text; + errMsg.style.display = visible ? "unset" : "none"; + } + } + copyTextArea() { + const textarea = document.getElementById("import-export-textarea"); + if (textarea) { + textarea.select(); + textarea.setSelectionRange(0, 9999999); + navigator.clipboard.writeText(textarea.value); + } + } + getText() { + const textarea = document.getElementById("import-export-textarea"); + if (textarea) { + return textarea.value; + } + return ""; + } + okBtn() { + return document.getElementById("import-export-ok-btn"); + } + cancelBtn() { + return document.getElementById("import-export-cancel-btn"); + } +} class OverlayManagementModal { self() { return document.getElementById("overlays-menu-container");

@@ -597,18 +699,21 @@ this.setVisible(false);

} } class ModalCollection { - constructor(createOverlay, cancel, okCancel, info, overlayMgr) { + constructor(createOverlay, cancel, okCancel, info, overlayMgr, importExport) { this.createOverlay = createOverlay; this.cancel = cancel; this.okCancel = okCancel; this.info = info; this.overlayMgr = overlayMgr; + this.importExport = importExport; } closeAll() { this.createOverlay.setVisible(false); this.cancel.setVisible(false); this.okCancel.setVisible(false); + this.info.setVisible(false); this.overlayMgr.setVisible(false); + this.importExport.setVisible(false); } } class MapHandler {

@@ -986,6 +1091,82 @@ }

self.modals.okCancel.setVisible(true); } } + static exportSingle(overlay) { + const self = MapHandler.instance; + if (self) { + self.modals.closeAll(); + self.modals.importExport.setTitle("Export Overlay"); + self.modals.importExport.setErrMsg("", false); + const okBtn = self.modals.importExport.okBtn(); + if (okBtn) { + okBtn.innerText = "Copy to clipboard"; + okBtn.onclick = () => { + self.modals.importExport.copyTextArea(); + self.modals.closeAll(); + self.modals.info.setMsg("Copied the data to the clipboard"); + self.modals.info.setVisible(true); + }; + } + self.modals.importExport.setTextArea(OverlayState.exportSingle(overlay), true); + self.modals.importExport.setVisible(true); + } + } + static exportAll() { + const self = MapHandler.instance; + if (self) { + self.modals.closeAll(); + self.modals.importExport.setTitle("Export All Overlays"); + self.modals.importExport.setErrMsg("", false); + const okBtn = self.modals.importExport.okBtn(); + if (okBtn) { + okBtn.innerText = "Copy to clipboard"; + okBtn.onclick = () => { + self.modals.importExport.copyTextArea(); + self.modals.closeAll(); + self.modals.info.setMsg("Copied the data to the clipboard"); + self.modals.info.setVisible(true); + }; + } + self.modals.importExport.setTextArea(OverlayState.export(self.overlays), true); + self.modals.importExport.setVisible(true); + } + } + static import() { + const self = MapHandler.instance; + if (self) { + self.modals.closeAll(); + self.modals.importExport.setTitle("Import Overlay Data"); + self.modals.importExport.setErrMsg("", false); + self.modals.importExport.setTextArea("", false); + const okBtn = self.modals.importExport.okBtn(); + if (okBtn) { + okBtn.innerText = "Import"; + okBtn.onclick = () => { + MapHandler.doImport(self.modals.importExport.getText()); + }; + } + self.modals.importExport.setVisible(true); + } + } + static doImport(data) { + const self = MapHandler.instance; + if (self) { + if (OverlayState.importWrapper(data, self.overlays, self.map)) { + self.modals.closeAll(); + self.modals.info.setMsg("Import successful"); + self.modals.info.setVisible(true); + } + else { + self.modals.importExport.setErrMsg("The data was malformed &mdash; please check that it is valid JSON exported from ONYX/scry", true); + } + } + } + static closeImportExport() { + const self = MapHandler.instance; + if (self) { + self.modals.closeAll(); + } + } } MapHandler.instance = null; function init() {

@@ -1006,24 +1187,29 @@ overlays.markers.forEach(m => m.add(map));

overlays.circles.forEach(m => m.add(map)); overlays.polygons.forEach(m => m.add(map)); overlays.polyline.add(map); - const modals = new ModalCollection(new CreateOverlayModal(), new CancelModal(), new OKCancelModal(), new InfoModal(), new OverlayManagementModal()); + const modals = new ModalCollection(new CreateOverlayModal(), new CancelModal(), new OKCancelModal(), new InfoModal(), new OverlayManagementModal(), new ImportExportModal()); MapHandler.init(map, overlays, TileLayerWrapper.layers, modals); MapHandler.setButtonClick("home-btn", MapHandler.goHome); MapHandler.setButtonClick("addPoint-btn", MapHandler.markerCollect); MapHandler.setButtonClick("addCircle-btn", MapHandler.circleCollect); MapHandler.setButtonClick("addPolygon-btn", MapHandler.polygonCollect); + MapHandler.setButtonClick("tiles-btn", MapHandler.swapTiles); MapHandler.setButtonClick("save-btn", MapHandler.overlaySave); MapHandler.setButtonClick("clear-btn", MapHandler.overlayClear); MapHandler.setButtonClick("restore-btn", MapHandler.overlayReset); MapHandler.setButtonClick("menu-btn", MapHandler.toggleMenu); MapHandler.setButtonClick("set-home-btn", MapHandler.setHome); - MapHandler.setButtonClick("tiles-btn", MapHandler.swapTiles); + MapHandler.setButtonClick("export-all-btn", MapHandler.exportAll); + MapHandler.setButtonClick("import-export-cancel-btn", MapHandler.closeImportExport); + MapHandler.setButtonClick("import-btn", MapHandler.import); map.on("locationfound", MapHandler.setHome); map.on("locationerror", () => { const info = modals.info; info.setMsg("Could not get location data"); info.setVisible(true); }); + // the menu doesn't open on the first click unless we do this first... not sure why + modals.closeAll(); const homeData = localStorage.getItem("home"); if (homeData) { const home = JSON.parse(homeData);

@@ -1032,6 +1218,5 @@ }

else { map.locate({ setView: true, maxZoom: 13 }); } - modals.closeAll(); } init();
M static/style.cssstatic/style.css

@@ -59,7 +59,7 @@ #mapControls button {

color: #1fb9b2; border: none; background: black; - font-size: 5vh; + font-size: min(5vh, 8vw); padding-left: 0.5ch; padding-right: 0.5ch; border: solid 1px black;

@@ -114,9 +114,11 @@ position: sticky;

top: 0; width: 100%; height: auto; + background: black; } -#createOverlay-container h2 { +#createOverlay-container h2, +#import-export-container h2 { text-align: left; font-size: 200%; font-weight: normal;

@@ -147,7 +149,8 @@ }

#createOverlay-content input[type="text"], #createOverlay-content textarea, -#createOverlay-content input[type="number"] { +#createOverlay-content input[type="number"], +#import-export-container textarea { display: block; width: 100%; font-size: 150%;

@@ -161,7 +164,8 @@ background: #222222;

} -#createOverlay-content textarea { +#createOverlay-content textarea, +#import-export-container textarea { resize: none; height: 8em; }

@@ -221,7 +225,7 @@ border: solid 2px white;

} .positive-btn, .negative-btn { - font-size: 66.66%; + font-size: 150%; margin-top: 1em; }

@@ -236,7 +240,9 @@ color: black !important;

background: #1f9b92 !important; } -#createOverlay-submitBtn { +#createOverlay-submitBtn, +#import-export-container #import-export-ok-btn, +#import-export-container #import-export-cancel-btn { float: none; font-size: 150%; }

@@ -254,8 +260,7 @@ }

#cancel-container, #confirm-container, #info-container { position: fixed; - font-size: 250%; - bottom: 5em; + bottom: min(8vh, 11vw); display: none; left: 50%; transform: translateX(-50%);

@@ -267,16 +272,19 @@ width: 100%;

max-width: fit-content; } +#info-content, #cancel-msg, #confirm-msg { + font-size: 200%; +} + #info-content { float: left; line-height: 200%; } #info-container .closeBtn { - font-size: 150%; + font-size: 200%; padding: 0; margin-left: 1ch; - } #import-export-container {

@@ -286,9 +294,25 @@ max-width: 800px;

top: 50%; left: 50%; transform: translate3d(-50%, -50%, 0); - height: calc(100vh - 2.5em); - max-height: 600px; + height: auto; + max-height: 100vh; background: black; + color: white; + z-index:4; + box-sizing: border-box; +} + +#import-export-content { + padding: 1em; +} + +#import-export-container .multiBtn-container { + text-align: right; +} + +#import-export-error { + color: crimson; + display: none; } #overlays-menu-container {

@@ -312,6 +336,11 @@ #overlays-menu-container .multiBtn-container {

text-align: center; grid-row: 2; margin-bottom: 3em; +} + +#overlays-menu-container button { + margin-top: 0.2em; + margin-bottom: 0.2em; } #overlays-list {

@@ -360,4 +389,27 @@ }

#import-export-container { display: none; +} + +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { + color: white; + background: black; + border-radius: 0; +} + +.leaflet-popup-close-button { + color: #c9c9c9 !important; +} + +.leaflet-popup-close-button:hover, +.leaflet-popup-close-button:focus { + color: crimson !important; +} + +.leaflet-control-attribution { + position: fixed; + z-index: 2; + top: 0; + right: 0; }