implement import/export, tweak styles, update readme, and add leaflet license
This commit is contained in:
parent
30766accd6
commit
9f441bfaaf
11 changed files with 599 additions and 64 deletions
62
README.md
62
README.md
|
@ -2,12 +2,70 @@
|
|||
|
||||
## 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).
|
|
@ -237,25 +237,33 @@ class OverlayState {
|
|||
|
||||
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 save(overlayState: OverlayState): void {
|
||||
localStorage.setItem("overlay_state", JSON.stringify({
|
||||
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 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 @@ class OverlayState {
|
|||
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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -152,7 +152,9 @@ class CreateOverlayModal implements Modal {
|
|||
}
|
||||
|
||||
if (exportBtn) {
|
||||
// show export window with this Overlay's OverlayData
|
||||
exportBtn.onclick = () => {
|
||||
MapHandler.exportSingle(args.self);
|
||||
}
|
||||
}
|
||||
|
||||
if (deleteBtn) {
|
||||
|
|
64
src/24-importExportModal.ts
Normal file
64
src/24-importExportModal.ts
Normal 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");
|
||||
}
|
||||
}
|
|
@ -4,25 +4,30 @@ class ModalCollection {
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -400,4 +400,89 @@ class MapHandler {
|
|||
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 — please check that it is valid JSON exported from ONYX/scry", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static closeImportExport(): void {
|
||||
const self = MapHandler.instance;
|
||||
if (self) {
|
||||
self.modals.closeAll();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ function init(): void {
|
|||
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
{
|
||||
maxZoom: 19,
|
||||
attribution: "street map tiles © OpenStreetMap"
|
||||
attribution: "street map data © OpenStreetMap contributors"
|
||||
}));
|
||||
|
||||
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}',
|
||||
{
|
||||
maxZoom: 19,
|
||||
attribution: "satellite tiles © Esri"
|
||||
attribution: "satellite data © Esri"
|
||||
}));
|
||||
|
||||
TileLayerWrapper.enableOnly("streetLayer", map);
|
||||
|
@ -34,7 +34,8 @@ function init(): void {
|
|||
new CancelModal(),
|
||||
new OKCancelModal(),
|
||||
new InfoModal(),
|
||||
new OverlayManagementModal());
|
||||
new OverlayManagementModal(),
|
||||
new ImportExportModal());
|
||||
|
||||
MapHandler.init(map, overlays, TileLayerWrapper.layers, modals);
|
||||
|
||||
|
@ -43,14 +44,16 @@ function init(): void {
|
|||
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 @@ function init(): void {
|
|||
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 @@ function init(): void {
|
|||
} else {
|
||||
map.locate({setView: true, maxZoom: 13});
|
||||
}
|
||||
|
||||
modals.closeAll();
|
||||
}
|
||||
|
||||
init();
|
||||
|
|
28
src/LEAFLET-LICENSE
Normal file
28
src/LEAFLET-LICENSE
Normal 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
|
|
@ -4,6 +4,7 @@
|
|||
<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 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>
|
||||
|
||||
</body>
|
||||
<link rel='stylesheet' type="text/css" href="./leaflet.css">
|
||||
<script src="./leaflet.js"></script>
|
||||
<script src="./onyx-scry.js"></script>
|
||||
</html>
|
|
@ -172,31 +172,37 @@ class OverlayState {
|
|||
}
|
||||
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 save(overlayState) {
|
||||
localStorage.setItem("overlay_state", JSON.stringify({
|
||||
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 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 @@ class OverlayState {
|
|||
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 @@ class CreateOverlayModal {
|
|||
};
|
||||
}
|
||||
if (exportBtn) {
|
||||
// show export window with this Overlay's OverlayData
|
||||
exportBtn.onclick = () => {
|
||||
MapHandler.exportSingle(args.self);
|
||||
};
|
||||
}
|
||||
if (deleteBtn) {
|
||||
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 {
|
||||
self() {
|
||||
return document.getElementById("overlays-menu-container");
|
||||
|
@ -597,18 +699,21 @@ class OverlayManagementModal {
|
|||
}
|
||||
}
|
||||
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 @@ class MapHandler {
|
|||
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 — 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 @@ function init() {
|
|||
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 @@ function init() {
|
|||
else {
|
||||
map.locate({ setView: true, maxZoom: 13 });
|
||||
}
|
||||
modals.closeAll();
|
||||
}
|
||||
init();
|
||||
|
|
|
@ -59,7 +59,7 @@ body {
|
|||
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 @@ body {
|
|||
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 @@ body {
|
|||
|
||||
#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 @@ body {
|
|||
|
||||
}
|
||||
|
||||
#createOverlay-content textarea {
|
||||
#createOverlay-content textarea,
|
||||
#import-export-container textarea {
|
||||
resize: none;
|
||||
height: 8em;
|
||||
}
|
||||
|
@ -221,7 +225,7 @@ body {
|
|||
}
|
||||
|
||||
.positive-btn, .negative-btn {
|
||||
font-size: 66.66%;
|
||||
font-size: 150%;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
|
@ -236,7 +240,9 @@ body {
|
|||
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 @@ body {
|
|||
|
||||
#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 @@ body {
|
|||
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 @@ body {
|
|||
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 {
|
||||
|
@ -314,6 +338,11 @@ body {
|
|||
margin-bottom: 3em;
|
||||
}
|
||||
|
||||
#overlays-menu-container button {
|
||||
margin-top: 0.2em;
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
|
||||
#overlays-list {
|
||||
font-size: 200%;
|
||||
grid-row: 1;
|
||||
|
@ -361,3 +390,26 @@ body {
|
|||
#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;
|
||||
}
|
Loading…
Reference in a new issue