implement import/export, tweak styles, update readme, and add leaflet license

This commit is contained in:
Iris Lightshard 2022-08-20 17:20:01 -06:00
parent 30766accd6
commit 9f441bfaaf
Signed by: Iris Lightshard
GPG key ID: 3B7FBC22144E6398
11 changed files with 599 additions and 64 deletions

View file

@ -2,12 +2,70 @@
## about ## 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 ## 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 ## license
`onyx/scry` is distributed under the [MIT license](./LICENSE) - basically do whatever you want with it but leave my name on it. `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).

View file

@ -237,25 +237,33 @@ class OverlayState {
static load(): OverlayState { static load(): OverlayState {
const store = localStorage.getItem("overlay_state"); const store = localStorage.getItem("overlay_state");
if (store) { return store ? OverlayState.import(store) : new OverlayState;
const model = JSON.parse(store); }
static import(overlayData: string): OverlayState {
const model = JSON.parse(overlayData);
return { return {
markers: model.markers.map((m: OverlayData) => OverlayState.fromData(m)), markers: model.markers.map((m: OverlayData) => OverlayState.fromData(m)),
circles: model.circles.map((c: OverlayData) => OverlayState.fromData(c)), circles: model.circles.map((c: OverlayData) => OverlayState.fromData(c)),
polygons: model.polygons.map((p: OverlayData) => OverlayState.fromData(p)), polygons: model.polygons.map((p: OverlayData) => OverlayState.fromData(p)),
polyline: new Polyline(), polyline: new Polyline(),
} as OverlayState } as OverlayState
} else {
return new OverlayState();
}
} }
static save(overlayState: OverlayState): void { static exportSingle(overlay: OverlayBase): string {
localStorage.setItem("overlay_state", JSON.stringify({ return JSON.stringify(OverlayState.toData(overlay), null, 2);
}
static export(overlayState: OverlayState): string {
return JSON.stringify({
markers: overlayState.markers.map((m: OverlayBase) => OverlayState.toData(m)), markers: overlayState.markers.map((m: OverlayBase) => OverlayState.toData(m)),
circles: overlayState.circles.map((c: OverlayBase) => OverlayState.toData(c)), circles: overlayState.circles.map((c: OverlayBase) => OverlayState.toData(c)),
polygons: overlayState.polygons.map((p: OverlayBase) => OverlayState.toData(p)), 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 { static clear(overlayState: OverlayState, map: L.Map): OverlayState {
@ -263,7 +271,9 @@ class OverlayState {
overlayState.circles.forEach((c: Circle) => c.remove(map)); overlayState.circles.forEach((c: Circle) => c.remove(map));
overlayState.polygons.forEach((p: Polygon) => p.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 { private static toData(source: OverlayBase): OverlayData {
@ -288,5 +298,42 @@ class OverlayState {
} }
} }
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;
}
} }

View file

@ -152,7 +152,9 @@ class CreateOverlayModal implements Modal {
} }
if (exportBtn) { if (exportBtn) {
// show export window with this Overlay's OverlayData exportBtn.onclick = () => {
MapHandler.exportSingle(args.self);
}
} }
if (deleteBtn) { if (deleteBtn) {

View file

@ -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");
}
}

View file

@ -4,25 +4,30 @@ class ModalCollection {
okCancel: OKCancelModal; okCancel: OKCancelModal;
info: InfoModal; info: InfoModal;
overlayMgr: OverlayManagementModal; overlayMgr: OverlayManagementModal;
importExport: ImportExportModal;
constructor( constructor(
createOverlay: CreateOverlayModal, createOverlay: CreateOverlayModal,
cancel: CancelModal, cancel: CancelModal,
okCancel: OKCancelModal, okCancel: OKCancelModal,
info: InfoModal, info: InfoModal,
overlayMgr: OverlayManagementModal overlayMgr: OverlayManagementModal,
importExport: ImportExportModal
) { ) {
this.createOverlay = createOverlay; this.createOverlay = createOverlay;
this.cancel = cancel; this.cancel = cancel;
this.okCancel = okCancel; this.okCancel = okCancel;
this.info = info; this.info = info;
this.overlayMgr = overlayMgr; this.overlayMgr = overlayMgr;
this.importExport = importExport;
} }
closeAll(): void { closeAll(): void {
this.createOverlay.setVisible(false); this.createOverlay.setVisible(false);
this.cancel.setVisible(false); this.cancel.setVisible(false);
this.okCancel.setVisible(false); this.okCancel.setVisible(false);
this.info.setVisible(false);
this.overlayMgr.setVisible(false); this.overlayMgr.setVisible(false);
this.importExport.setVisible(false);
} }
} }

View file

@ -400,4 +400,89 @@ class MapHandler {
self.modals.okCancel.setVisible(true); 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();
}
}
} }

View file

@ -10,7 +10,7 @@ function init(): void {
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
{ {
maxZoom: 19, maxZoom: 19,
attribution: "street map tiles &copy; OpenStreetMap" attribution: "street map data &copy; OpenStreetMap contributors"
})); }));
const satelliteLayer = TileLayerWrapper.constructLayer( const satelliteLayer = TileLayerWrapper.constructLayer(
@ -19,7 +19,7 @@ function init(): void {
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
{ {
maxZoom: 19, maxZoom: 19,
attribution: "satellite tiles &copy; Esri" attribution: "satellite data &copy; Esri"
})); }));
TileLayerWrapper.enableOnly("streetLayer", map); TileLayerWrapper.enableOnly("streetLayer", map);
@ -34,7 +34,8 @@ function init(): void {
new CancelModal(), new CancelModal(),
new OKCancelModal(), new OKCancelModal(),
new InfoModal(), new InfoModal(),
new OverlayManagementModal()); new OverlayManagementModal(),
new ImportExportModal());
MapHandler.init(map, overlays, TileLayerWrapper.layers, modals); MapHandler.init(map, overlays, TileLayerWrapper.layers, modals);
@ -43,14 +44,16 @@ function init(): void {
MapHandler.setButtonClick("addCircle-btn", MapHandler.circleCollect); MapHandler.setButtonClick("addCircle-btn", MapHandler.circleCollect);
MapHandler.setButtonClick("addPolygon-btn", MapHandler.polygonCollect); MapHandler.setButtonClick("addPolygon-btn", MapHandler.polygonCollect);
MapHandler.setButtonClick("tiles-btn", MapHandler.swapTiles);
MapHandler.setButtonClick("save-btn", MapHandler.overlaySave); MapHandler.setButtonClick("save-btn", MapHandler.overlaySave);
MapHandler.setButtonClick("clear-btn", MapHandler.overlayClear); MapHandler.setButtonClick("clear-btn", MapHandler.overlayClear);
MapHandler.setButtonClick("restore-btn", MapHandler.overlayReset); MapHandler.setButtonClick("restore-btn", MapHandler.overlayReset);
MapHandler.setButtonClick("menu-btn", MapHandler.toggleMenu); MapHandler.setButtonClick("menu-btn", MapHandler.toggleMenu);
MapHandler.setButtonClick("set-home-btn", MapHandler.setHome); MapHandler.setButtonClick("set-home-btn", MapHandler.setHome);
MapHandler.setButtonClick("export-all-btn", MapHandler.exportAll);
MapHandler.setButtonClick("tiles-btn", MapHandler.swapTiles); MapHandler.setButtonClick("import-export-cancel-btn", MapHandler.closeImportExport);
MapHandler.setButtonClick("import-btn", MapHandler.import);
map.on("locationfound", MapHandler.setHome); map.on("locationfound", MapHandler.setHome);
@ -60,6 +63,9 @@ function init(): void {
info.setVisible(true); 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"); const homeData = localStorage.getItem("home");
if (homeData) { if (homeData) {
const home = <Point>JSON.parse(homeData); const home = <Point>JSON.parse(homeData);
@ -67,8 +73,6 @@ function init(): void {
} else { } else {
map.locate({setView: true, maxZoom: 13}); map.locate({setView: true, maxZoom: 13});
} }
modals.closeAll();
} }
init(); init();

28
src/LEAFLET-LICENSE Normal file
View file

@ -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

View file

@ -4,6 +4,7 @@
<meta charset='utf-8'> <meta charset='utf-8'>
<meta name='description' content='map annotation tool'/> <meta name='description' content='map annotation tool'/>
<meta name='viewport' content='width=device-width,initial-scale=1'> <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'> <link rel='stylesheet' type='text/css' href='./style.css'>
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="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"> <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> <textarea id="import-export-textarea"></textarea>
<span id="import-export-error"></span>
<div class="multiBtn-container"> <div class="multiBtn-container">
<button class="positive-btn" id="import-export-ok-btn">OK</button> <button class="positive-btn" id="import-export-ok-btn">OK</button>
<button class="negative-btn" id="import-export-cancel-btn">Cancel</button> <button class="negative-btn" id="import-export-cancel-btn">Cancel</button>
</div> </div>
</div>
</div> </div>
<div id="overlays-menu-container"> <div id="overlays-menu-container">
<div id="overlays-list"> <div id="overlays-list">
<details id="markers-wrapper"><summary>Markers</summary><ul id="markers-list"></ul></details> <details id="markers-wrapper" open><summary>Markers</summary><ul id="markers-list"></ul></details>
<details id="circles-wrapper"><summary>Circles</summary><ul id="circles-list"></ul></details> <details id="circles-wrapper" open><summary>Circles</summary><ul id="circles-list"></ul></details>
<details id="polygons-wrapper"><summary>Polygons</summary><ul id="polygons-list"></ul></details> <details id="polygons-wrapper" open><summary>Polygons</summary><ul id="polygons-list"></ul></details>
</div> </div>
<div class="multiBtn-container"> <div class="multiBtn-container">
<button id="set-home-btn">Set Home</button> <button id="set-home-btn">Set Home</button>
@ -100,7 +106,6 @@
</div> </div>
</body> </body>
<link rel='stylesheet' type="text/css" href="./leaflet.css">
<script src="./leaflet.js"></script> <script src="./leaflet.js"></script>
<script src="./onyx-scry.js"></script> <script src="./onyx-scry.js"></script>
</html> </html>

View file

@ -172,8 +172,10 @@ class OverlayState {
} }
static load() { static load() {
const store = localStorage.getItem("overlay_state"); const store = localStorage.getItem("overlay_state");
if (store) { return store ? OverlayState.import(store) : new OverlayState;
const model = JSON.parse(store); }
static import(overlayData) {
const model = JSON.parse(overlayData);
return { return {
markers: model.markers.map((m) => OverlayState.fromData(m)), markers: model.markers.map((m) => OverlayState.fromData(m)),
circles: model.circles.map((c) => OverlayState.fromData(c)), circles: model.circles.map((c) => OverlayState.fromData(c)),
@ -181,22 +183,26 @@ class OverlayState {
polyline: new Polyline(), polyline: new Polyline(),
}; };
} }
else { static exportSingle(overlay) {
return new OverlayState(); return JSON.stringify(OverlayState.toData(overlay), null, 2);
} }
} static export(overlayState) {
static save(overlayState) { return JSON.stringify({
localStorage.setItem("overlay_state", JSON.stringify({
markers: overlayState.markers.map((m) => OverlayState.toData(m)), markers: overlayState.markers.map((m) => OverlayState.toData(m)),
circles: overlayState.circles.map((c) => OverlayState.toData(c)), circles: overlayState.circles.map((c) => OverlayState.toData(c)),
polygons: overlayState.polygons.map((p) => OverlayState.toData(p)), polygons: overlayState.polygons.map((p) => OverlayState.toData(p)),
})); }, null, 2);
}
static save(overlayState) {
localStorage.setItem("overlay_state", OverlayState.export(overlayState));
} }
static clear(overlayState, map) { static clear(overlayState, map) {
overlayState.markers.forEach((m) => m.remove(map)); overlayState.markers.forEach((m) => m.remove(map));
overlayState.circles.forEach((c) => c.remove(map)); overlayState.circles.forEach((c) => c.remove(map));
overlayState.polygons.forEach((p) => p.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) { static toData(source) {
let type = OverlayType.POINT; let type = OverlayType.POINT;
@ -219,6 +225,44 @@ class OverlayState {
return new Polygon(data.name, data.desc, data.points, data.options); 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 { class TileLayerWrapper {
constructor(name, self) { constructor(name, self) {
@ -400,7 +444,9 @@ class CreateOverlayModal {
}; };
} }
if (exportBtn) { if (exportBtn) {
// show export window with this Overlay's OverlayData exportBtn.onclick = () => {
MapHandler.exportSingle(args.self);
};
} }
if (deleteBtn) { if (deleteBtn) {
deleteBtn.onclick = () => { deleteBtn.onclick = () => {
@ -578,6 +624,62 @@ class InfoModal {
} }
} }
} }
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 { class OverlayManagementModal {
self() { self() {
return document.getElementById("overlays-menu-container"); return document.getElementById("overlays-menu-container");
@ -597,18 +699,21 @@ class OverlayManagementModal {
} }
} }
class ModalCollection { class ModalCollection {
constructor(createOverlay, cancel, okCancel, info, overlayMgr) { constructor(createOverlay, cancel, okCancel, info, overlayMgr, importExport) {
this.createOverlay = createOverlay; this.createOverlay = createOverlay;
this.cancel = cancel; this.cancel = cancel;
this.okCancel = okCancel; this.okCancel = okCancel;
this.info = info; this.info = info;
this.overlayMgr = overlayMgr; this.overlayMgr = overlayMgr;
this.importExport = importExport;
} }
closeAll() { closeAll() {
this.createOverlay.setVisible(false); this.createOverlay.setVisible(false);
this.cancel.setVisible(false); this.cancel.setVisible(false);
this.okCancel.setVisible(false); this.okCancel.setVisible(false);
this.info.setVisible(false);
this.overlayMgr.setVisible(false); this.overlayMgr.setVisible(false);
this.importExport.setVisible(false);
} }
} }
class MapHandler { class MapHandler {
@ -986,6 +1091,82 @@ class MapHandler {
self.modals.okCancel.setVisible(true); 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; MapHandler.instance = null;
function init() { function init() {
@ -1006,24 +1187,29 @@ function init() {
overlays.circles.forEach(m => m.add(map)); overlays.circles.forEach(m => m.add(map));
overlays.polygons.forEach(m => m.add(map)); overlays.polygons.forEach(m => m.add(map));
overlays.polyline.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.init(map, overlays, TileLayerWrapper.layers, modals);
MapHandler.setButtonClick("home-btn", MapHandler.goHome); MapHandler.setButtonClick("home-btn", MapHandler.goHome);
MapHandler.setButtonClick("addPoint-btn", MapHandler.markerCollect); MapHandler.setButtonClick("addPoint-btn", MapHandler.markerCollect);
MapHandler.setButtonClick("addCircle-btn", MapHandler.circleCollect); MapHandler.setButtonClick("addCircle-btn", MapHandler.circleCollect);
MapHandler.setButtonClick("addPolygon-btn", MapHandler.polygonCollect); MapHandler.setButtonClick("addPolygon-btn", MapHandler.polygonCollect);
MapHandler.setButtonClick("tiles-btn", MapHandler.swapTiles);
MapHandler.setButtonClick("save-btn", MapHandler.overlaySave); MapHandler.setButtonClick("save-btn", MapHandler.overlaySave);
MapHandler.setButtonClick("clear-btn", MapHandler.overlayClear); MapHandler.setButtonClick("clear-btn", MapHandler.overlayClear);
MapHandler.setButtonClick("restore-btn", MapHandler.overlayReset); MapHandler.setButtonClick("restore-btn", MapHandler.overlayReset);
MapHandler.setButtonClick("menu-btn", MapHandler.toggleMenu); MapHandler.setButtonClick("menu-btn", MapHandler.toggleMenu);
MapHandler.setButtonClick("set-home-btn", MapHandler.setHome); 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("locationfound", MapHandler.setHome);
map.on("locationerror", () => { map.on("locationerror", () => {
const info = modals.info; const info = modals.info;
info.setMsg("Could not get location data"); info.setMsg("Could not get location data");
info.setVisible(true); 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"); const homeData = localStorage.getItem("home");
if (homeData) { if (homeData) {
const home = JSON.parse(homeData); const home = JSON.parse(homeData);
@ -1032,6 +1218,5 @@ function init() {
else { else {
map.locate({ setView: true, maxZoom: 13 }); map.locate({ setView: true, maxZoom: 13 });
} }
modals.closeAll();
} }
init(); init();

View file

@ -59,7 +59,7 @@ body {
color: #1fb9b2; color: #1fb9b2;
border: none; border: none;
background: black; background: black;
font-size: 5vh; font-size: min(5vh, 8vw);
padding-left: 0.5ch; padding-left: 0.5ch;
padding-right: 0.5ch; padding-right: 0.5ch;
border: solid 1px black; border: solid 1px black;
@ -114,9 +114,11 @@ body {
top: 0; top: 0;
width: 100%; width: 100%;
height: auto; height: auto;
background: black;
} }
#createOverlay-container h2 { #createOverlay-container h2,
#import-export-container h2 {
text-align: left; text-align: left;
font-size: 200%; font-size: 200%;
font-weight: normal; font-weight: normal;
@ -147,7 +149,8 @@ body {
#createOverlay-content input[type="text"], #createOverlay-content input[type="text"],
#createOverlay-content textarea, #createOverlay-content textarea,
#createOverlay-content input[type="number"] { #createOverlay-content input[type="number"],
#import-export-container textarea {
display: block; display: block;
width: 100%; width: 100%;
font-size: 150%; font-size: 150%;
@ -161,7 +164,8 @@ body {
} }
#createOverlay-content textarea { #createOverlay-content textarea,
#import-export-container textarea {
resize: none; resize: none;
height: 8em; height: 8em;
} }
@ -221,7 +225,7 @@ body {
} }
.positive-btn, .negative-btn { .positive-btn, .negative-btn {
font-size: 66.66%; font-size: 150%;
margin-top: 1em; margin-top: 1em;
} }
@ -236,7 +240,9 @@ body {
background: #1f9b92 !important; background: #1f9b92 !important;
} }
#createOverlay-submitBtn { #createOverlay-submitBtn,
#import-export-container #import-export-ok-btn,
#import-export-container #import-export-cancel-btn {
float: none; float: none;
font-size: 150%; font-size: 150%;
} }
@ -254,8 +260,7 @@ body {
#cancel-container, #confirm-container, #info-container { #cancel-container, #confirm-container, #info-container {
position: fixed; position: fixed;
font-size: 250%; bottom: min(8vh, 11vw);
bottom: 5em;
display: none; display: none;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
@ -267,16 +272,19 @@ body {
max-width: fit-content; max-width: fit-content;
} }
#info-content, #cancel-msg, #confirm-msg {
font-size: 200%;
}
#info-content { #info-content {
float: left; float: left;
line-height: 200%; line-height: 200%;
} }
#info-container .closeBtn { #info-container .closeBtn {
font-size: 150%; font-size: 200%;
padding: 0; padding: 0;
margin-left: 1ch; margin-left: 1ch;
} }
#import-export-container { #import-export-container {
@ -286,9 +294,25 @@ body {
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate3d(-50%, -50%, 0); transform: translate3d(-50%, -50%, 0);
height: calc(100vh - 2.5em); height: auto;
max-height: 600px; max-height: 100vh;
background: black; 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 { #overlays-menu-container {
@ -314,6 +338,11 @@ body {
margin-bottom: 3em; margin-bottom: 3em;
} }
#overlays-menu-container button {
margin-top: 0.2em;
margin-bottom: 0.2em;
}
#overlays-list { #overlays-list {
font-size: 200%; font-size: 200%;
grid-row: 1; grid-row: 1;
@ -361,3 +390,26 @@ body {
#import-export-container { #import-export-container {
display: none; 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;
}