all repos — onyx @ 696d3bc270e74bf0ebda97c40474441b6fda3093

minimal map annotation and location data sharing tool

import/export overlays via query string, clickable popups; v0.3.0
Iris Lightshard nilix@nilfm.cc
PGP Signature
-----BEGIN PGP SIGNATURE-----

iQIzBAABCAAdFiEEkFh6dA+k/6CXFXU4O3+8IhROY5gFAmPQ1BcACgkQO3+8IhRO
Y5iHJQ//WmbNQSt1S9yMyQ7/T35X5E93sg2Z2G2SOrv7SbX9ekpaVU8QquceTFxR
EbwXGIAexiYsNDz3znyAc2yQGD9MbPnHnWhLcvffEFmKsB4DmDQCW1mLSiiPAW5X
8WlGZ8DnDbPYd5UcSsscCC/b3LKcE6x7qUogA4vyFJBtr6JqbTiQoVW2XeOWepYu
IU7lYJny3+U57v+Xq3k9WoVilFjQ3q5HXWv/LevV6rmz4ly1ogmL+oStvAfxHhn+
OiNRKjQUlsFLVwhXNIX2/C9ymp/UGUSL3/ULcSy3rxwivZt+Oyi8d0q0J1boWG0j
tburn6lzvLmtNxEaHn7ebr4msvWp66Tcy7zLVoFsODuckQs5s+fg6Oow0Ne9TY4f
NOCcFlsTHuN9dUbyvW9nQ/HWk6fkJgpGaT2U20ppH23upXUysfCTWONqOWYdoRqp
rQzJsS1158oZuVS+eRL7NS/7heX1x+3cUSisQASq4IKzN3gYypfzqQm/NZvRCUzO
BFu+qFjOhA1gXrRAHo7pP93Uu4mLIqXuMy98IATA6gMMrbf9FeRW9B57AsDOCpy2
0Bsv7qANi2CytPIbzWCWSAHLj4rxJzq1Pje41GxioJVtoX14VxbCutfTO+XEpsJR
5gmtG07u2ga1joVQeIw09S06TDcmv89THt0uRHpIlnw14arJK9o=
=Tmjv
-----END PGP SIGNATURE-----
commit

696d3bc270e74bf0ebda97c40474441b6fda3093

parent

95dc1c8c9d466fbd74b51bd1690d45f7b3f599ad

M README.mdREADME.md

@@ -10,6 +10,8 @@ __note__: On mobile, for best experience you should "install to home screen" or "create shortcut" from your browser of choice, so that you can use `onyx` as an app without the browser UI getting in the way.

When you launch the application for the first time (and subsequent times if you don't set `home`), 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. +The exception to this behavior is when passing at least `lat` and `lng` values in the query string (eg from a location share link), in which case a marker or circle will be placed on that location and the map centered on it. + 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.

@@ -47,14 +49,26 @@

This window is similar to the Overlay Creation window save the addition of three buttons: - `Go Here`: Centers the map view on this overlay. +- `Share`: Opens the Export window to export this overlay as a shareable link. - `Export`: Opens the Export window to export this overlay to JSON format. - `Delete`: Deletes this overlay from the map. +In addition to being reachable from the menu, you can also reach this screen by clicking on an overlay and then clicking on the text in the tooltip. + ### 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 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. A link from the `Share` button can be given to anyone to put in their browser. When importing, the imported data is added to the current overlays. If you want to overwrite your current data, clear it first. + +When using a share link, the following query string values are valid: + +- `lat`: required; should be a number +- `lng`: required; should be a number +- `rad`: optional; should be a positive number; if omitted, the shared overlay will be a `Marker`. +- `name`: optional; defaults to lat,lng +- `desc`: optional; defaults to empty +- `tile`: optional; if "sat" the tileset will use satellite data; if omitted or any other value, the tileset will use the OpenStreetMap data. ## build/deploy
M src/10-overlay.tssrc/10-overlay.ts

@@ -74,8 +74,11 @@

return `<span class="tiny">${String(long).substring(0, 7)}&deg;${eastWest}, ${String(lat).substring(0,7)}&deg;${northSouth}</span>`; } - setPopupContent(content: string): void { - this.self.bindPopup(content); + setPopupContent(content: string, state: OverlayType): void { + const node = document.createElement('div') + node.innerHTML = content; + node.onclick = (e: any)=>{MapHandler.editOverlay(this, state)}; + this.self.bindPopup(node); } abstract add(map: L.Map): void;

@@ -127,7 +130,10 @@

constructor(name: string, desc: string, point: Point, options: any) { super(name, desc, [ point ], options); this.self = L.marker(point); - this.self.bindPopup(`<h3>${name}</h3><p>${desc}</p>`); + const node = document.createElement('div'); + node.innerHTML = `<h3>${name}</h3><p>${desc}</p>`; + node.onclick=(e: any)=>{MapHandler.editOverlay(this, OverlayType.POINT)}; + this.self.bindPopup(node); } add(map: L.Map) {

@@ -148,7 +154,10 @@

constructor(name: string, desc: string, point: Point, options: any) { super(name, desc, [ point ], options); this.self = L.circle(point, options); - this.self.bindPopup(`<h3>${name}</h3><p>${desc}</p>`); + const node = document.createElement('div'); + node.innerHTML = `<h3>${name}</h3><p>${desc}</p>`; + node.onclick=(e: any)=>{MapHandler.editOverlay(this, OverlayType.CIRCLE)}; + this.self.bindPopup(node); } add(map: L.Map) {

@@ -173,7 +182,10 @@ this.self = L.polygon(points, options);

} else { this.self = L.polyline(points, options); } - this.self.bindPopup(`<h3>${name}</h3><p>${desc}</p>`); + const node = document.createElement('div'); + node.innerHTML = `<h3>${name}</h3><p>${desc}</p>`; + node.onclick=(e: any)=>{MapHandler.editOverlay(this, OverlayType.POLYGON)}; + this.self.bindPopup(node); } center(): Point {
M src/20-createOverlayModal.tssrc/20-createOverlayModal.ts

@@ -1,3 +1,8 @@

+enum ExportMode { + JSON = 0, + URL +} + class CreateOverlayModal implements Modal { constructor() {

@@ -112,6 +117,10 @@ deleteBtn(): HTMLElement | null {

return document.getElementById("delete-btn"); } + shareBtn(): HTMLElement | null { + return document.getElementById("share-btn"); + } + clearInputs(): void { const name = document.getElementById("createOverlay-name") as HTMLInputElement; const desc = document.getElementById("createOverlay-desc") as HTMLInputElement;

@@ -138,6 +147,7 @@ const closePolyContainer = _this.closePolyContainer();

const closePoly = _this.closePolyCheckbox(); const submitBtn = _this.submitBtn(); const editing = args.self ? true : false; + const shareBtn = _this.shareBtn(); _this.clearInputs(); if (radiusContainer) { radiusContainer.style.display = state == OverlayType.CIRCLE ? "block" : "none";

@@ -185,6 +195,12 @@ MapHandler.confirmDelete(args.self as OverlayBase);

} } + if (shareBtn) { + shareBtn.onclick = () => { + MapHandler.exportSingle(args.self, ExportMode.URL); + } + } + this.setName(args.self.name); this.setDesc(args.self.desc); if (state == OverlayType.CIRCLE) {

@@ -201,7 +217,7 @@ }

args.self.name = name; args.self.desc = desc; - args.self.setPopupContent(`<h3>${name}</h3><p>${desc}</p>`); + args.self.setPopupContent(`<h3>${name}</h3><p>${desc}</p>`, state); _this.setVisible(false); } }
M src/40-handlers.tssrc/40-handlers.ts

@@ -401,7 +401,7 @@ self.modals.okCancel.setVisible(true);

} } - static exportSingle(overlay: OverlayBase): void { + static exportSingle(overlay: OverlayBase, mode: ExportMode = ExportMode.JSON): void { const self = MapHandler.instance; if (self) { self.modals.closeAll();

@@ -419,9 +419,22 @@ self.modals.info.setVisible(true);

} } - self.modals.importExport.setTextArea(OverlayState.exportSingle(overlay), true); + switch (mode) { + case ExportMode.JSON: + self.modals.importExport.setTextArea(OverlayState.exportSingle(overlay), true); + break; + case ExportMode.URL: + self.modals.importExport.setTextArea(MapHandler.getOverlayUrl(overlay), true); + break; + } self.modals.importExport.setVisible(true); } + } + + static getOverlayUrl(overlay: OverlayBase): string { + return window.location.origin + + window.location.pathname + + `?lat=${overlay.points[0].lat}&lng=${overlay.points[0].lng}&name=${overlay.name}${(overlay.desc ? "&desc=" + overlay.desc : "")}${(TileLayerWrapper.getActiveLayer() == "satelliteLayer" ? "&tile=sat" : "")}` } static exportAll(): void {
A src/80-share.ts

@@ -0,0 +1,38 @@

+interface StringMap { + [key: string]: string; +} + +function getOverlayFromQuery(): [Circle | Marker | null, string | null] { + let urlParams: StringMap = {}; + let match, + pl = /\+/g, + search = /([^&=]+)=?([^&]*)/g, + decode = function (s: string) { + return decodeURIComponent(s.replace(pl, " ")); + }, + query = window.location.search.substring(1); + + while (match = search.exec(query)) { + urlParams[decode(match[1])] = decode(match[2]); + } + + if (urlParams["lat"] && urlParams["lng"]) { + urlParams["name"] = urlParams["name"] || `${urlParams["lat"]},${urlParams["lng"]}`; + urlParams["desc"] = urlParams["desc"] || ""; + urlParams["tile"] = urlParams["tile"] || "street"; + if (urlParams["rad"]) { + return [new Circle( + urlParams["name"], + urlParams["desc"], + {lat: Number(urlParams["lat"]), lng: Number(urlParams["lng"])}, + {radius: Number(urlParams["rad"])}), urlParams["tile"]]; + } else { + return [new Marker( + urlParams["name"], + urlParams["desc"], + {lat: Number(urlParams["lat"]), lng: Number(urlParams["lng"])}, + {}), urlParams["tile"]]; + } + } + return [null, null]; +}
M src/99-onyx.tssrc/99-onyx.ts

@@ -1,4 +1,4 @@

-const helpLink = "<br>ONYX v0.2.1 [ <a target='_blank' href='https://nilfm.cc/git/onyx/about/LICENSE'>license</a> | <a target='_blank' href='https://nilfm.cc/git/onyx/about'>manual</a> ]"; +const helpLink = "<br>ONYX v0.3.0 [ <a target='_blank' href='https://nilfm.cc/git/onyx/about/LICENSE'>license</a> | <a target='_blank' href='https://nilfm.cc/git/onyx/about'>manual</a> ]"; function init(): void {

@@ -72,27 +72,41 @@

// 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); - map.setView(home, 13); + const [fromQuery, tileset] = getOverlayFromQuery(); + if (tileset === "sat") { + MapHandler.swapTiles(null); + } + if (fromQuery && !overlays.circles.some(c=>c.points[0].lat == fromQuery.points[0].lat && c.points[0].lng == fromQuery.points[0].lng) && !overlays.markers.some(m=>m.points[0].lat == fromQuery.points[0].lat && m.points[0].lng == fromQuery.points[0].lng)) { + if (fromQuery.options.radius) { + overlays.circles.push(fromQuery); + } else { + overlays.markers.push(fromQuery); + } + fromQuery.add(map); + map.setView(fromQuery.points[0], 17); } else { - const okCancel = modals.okCancel; - const okBtn = okCancel.okBtn(); - if (okBtn) { - okBtn.onclick = () => { - modals.closeAll(); - map.locate({setView: true, maxZoom: 13}); + const homeData = localStorage.getItem("home"); + if (homeData) { + const home = <Point>JSON.parse(homeData); + map.setView(home, 17); + } else { + const okCancel = modals.okCancel; + const okBtn = okCancel.okBtn(); + if (okBtn) { + okBtn.onclick = () => { + modals.closeAll(); + map.locate({setView: true, maxZoom: 13}); + } } - } - const cancelBtn = okCancel.cancelBtn(); - if (cancelBtn) { - cancelBtn.onclick = () => { - modals.closeAll(); + const cancelBtn = okCancel.cancelBtn(); + if (cancelBtn) { + cancelBtn.onclick = () => { + modals.closeAll(); + } } + okCancel.setMsg("Would you like to use location data to set Home?"); + okCancel.setVisible(true); } - okCancel.setMsg("Would you like to use location data to set Home?"); - okCancel.setVisible(true); } }
M static/index.htmlstatic/index.html

@@ -5,7 +5,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?v=0.2.1'> +<link rel='stylesheet' type='text/css' href='./style.css?v=0.3.0'> <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" /> <link rel='shortcut icon' href='/favicon.png'>

@@ -55,6 +55,7 @@ <label for="close-poly-checkbox">Close polyline</label><br/>

</div> <div class="multiBtn-container" id="edit-extra-btns"> <button id="goto-btn">Go Here</button> + <button id="share-btn">Share</button> <button id="export-btn">Export</button> <button id="delete-btn">Delete</button> </div>

@@ -112,5 +113,5 @@ </div>

</body> <script src="./leaflet.js?v=1.8.0"></script> -<script src="./onyx.js?v=0.2.1"></script> +<script src="./onyx.js?v=0.3.0"></script> </html>
M static/onyx.jsstatic/onyx.js

@@ -50,8 +50,11 @@ eastWest = "W";

} return `<span class="tiny">${String(long).substring(0, 7)}&deg;${eastWest}, ${String(lat).substring(0, 7)}&deg;${northSouth}</span>`; } - setPopupContent(content) { - this.self.bindPopup(content); + setPopupContent(content, state) { + const node = document.createElement('div'); + node.innerHTML = content; + node.onclick = (e) => { MapHandler.editOverlay(this, state); }; + this.self.bindPopup(node); } static classSanitize(input) { return input.replace(/\-/g, "_");

@@ -90,7 +93,10 @@ constructor(name, desc, point, options) {

super(name, desc, [point], options); this.menuItem = null; this.self = L.marker(point); - this.self.bindPopup(`<h3>${name}</h3><p>${desc}</p>`); + const node = document.createElement('div'); + node.innerHTML = `<h3>${name}</h3><p>${desc}</p>`; + node.onclick = (e) => { MapHandler.editOverlay(this, OverlayType.POINT); }; + this.self.bindPopup(node); } add(map) { this.self.addTo(map);

@@ -106,7 +112,10 @@ constructor(name, desc, point, options) {

super(name, desc, [point], options); this.menuItem = null; this.self = L.circle(point, options); - this.self.bindPopup(`<h3>${name}</h3><p>${desc}</p>`); + const node = document.createElement('div'); + node.innerHTML = `<h3>${name}</h3><p>${desc}</p>`; + node.onclick = (e) => { MapHandler.editOverlay(this, OverlayType.CIRCLE); }; + this.self.bindPopup(node); } add(map) { this.self.addTo(map);

@@ -127,7 +136,10 @@ }

else { this.self = L.polyline(points, options); } - this.self.bindPopup(`<h3>${name}</h3><p>${desc}</p>`); + const node = document.createElement('div'); + node.innerHTML = `<h3>${name}</h3><p>${desc}</p>`; + node.onclick = (e) => { MapHandler.editOverlay(this, OverlayType.POLYGON); }; + this.self.bindPopup(node); } center() { return this.self.getCenter();

@@ -323,6 +335,11 @@ textArea.innerText = text;

return textArea.innerHTML; } } +var ExportMode; +(function (ExportMode) { + ExportMode[ExportMode["JSON"] = 0] = "JSON"; + ExportMode[ExportMode["URL"] = 1] = "URL"; +})(ExportMode || (ExportMode = {})); class CreateOverlayModal { constructor() { const _this = this;

@@ -419,6 +436,9 @@ }

deleteBtn() { return document.getElementById("delete-btn"); } + shareBtn() { + return document.getElementById("share-btn"); + } clearInputs() { const name = document.getElementById("createOverlay-name"); const desc = document.getElementById("createOverlay-desc");

@@ -442,6 +462,7 @@ const closePolyContainer = _this.closePolyContainer();

const closePoly = _this.closePolyCheckbox(); const submitBtn = _this.submitBtn(); const editing = args.self ? true : false; + const shareBtn = _this.shareBtn(); _this.clearInputs(); if (radiusContainer) { radiusContainer.style.display = state == OverlayType.CIRCLE ? "block" : "none";

@@ -483,6 +504,11 @@ deleteBtn.onclick = () => {

MapHandler.confirmDelete(args.self); }; } + if (shareBtn) { + shareBtn.onclick = () => { + MapHandler.exportSingle(args.self, ExportMode.URL); + }; + } this.setName(args.self.name); this.setDesc(args.self.desc); if (state == OverlayType.CIRCLE) {

@@ -497,7 +523,7 @@ return;

} args.self.name = name; args.self.desc = desc; - args.self.setPopupContent(`<h3>${name}</h3><p>${desc}</p>`); + args.self.setPopupContent(`<h3>${name}</h3><p>${desc}</p>`, state); _this.setVisible(false); }; }

@@ -1122,7 +1148,7 @@ }

self.modals.okCancel.setVisible(true); } } - static exportSingle(overlay) { + static exportSingle(overlay, mode = ExportMode.JSON) { const self = MapHandler.instance; if (self) { self.modals.closeAll();

@@ -1138,9 +1164,21 @@ self.modals.info.setMsg("Copied the data to the clipboard");

self.modals.info.setVisible(true); }; } - self.modals.importExport.setTextArea(OverlayState.exportSingle(overlay), true); + switch (mode) { + case ExportMode.JSON: + self.modals.importExport.setTextArea(OverlayState.exportSingle(overlay), true); + break; + case ExportMode.URL: + self.modals.importExport.setTextArea(MapHandler.getOverlayUrl(overlay), true); + break; + } self.modals.importExport.setVisible(true); } + } + static getOverlayUrl(overlay) { + return window.location.origin + + window.location.pathname + + `?lat=${overlay.points[0].lat}&lng=${overlay.points[0].lng}&name=${overlay.name}${(overlay.desc ? "&desc=" + overlay.desc : "")}${(TileLayerWrapper.getActiveLayer() == "satelliteLayer" ? "&tile=sat" : "")}`; } static exportAll() { const self = MapHandler.instance;

@@ -1200,7 +1238,28 @@ }

} } MapHandler.instance = null; -const helpLink = "<br>ONYX v0.2.1 [ <a target='_blank' href='https://nilfm.cc/git/onyx/about/LICENSE'>license</a> | <a target='_blank' href='https://nilfm.cc/git/onyx/about'>manual</a> ]"; +function getOverlayFromQuery() { + let urlParams = {}; + let match, pl = /\+/g, search = /([^&=]+)=?([^&]*)/g, decode = function (s) { + return decodeURIComponent(s.replace(pl, " ")); + }, query = window.location.search.substring(1); + while (match = search.exec(query)) { + urlParams[decode(match[1])] = decode(match[2]); + } + if (urlParams["lat"] && urlParams["lng"]) { + urlParams["name"] = urlParams["name"] || `${urlParams["lat"]},${urlParams["lng"]}`; + urlParams["desc"] = urlParams["desc"] || ""; + urlParams["tile"] = urlParams["tile"] || "street"; + if (urlParams["rad"]) { + return [new Circle(urlParams["name"], urlParams["desc"], { lat: Number(urlParams["lat"]), lng: Number(urlParams["lng"]) }, { radius: Number(urlParams["rad"]) }), urlParams["tile"]]; + } + else { + return [new Marker(urlParams["name"], urlParams["desc"], { lat: Number(urlParams["lat"]), lng: Number(urlParams["lng"]) }, {}), urlParams["tile"]]; + } + } + return [null, null]; +} +const helpLink = "<br>ONYX v0.3.0 [ <a target='_blank' href='https://nilfm.cc/git/onyx/about/LICENSE'>license</a> | <a target='_blank' href='https://nilfm.cc/git/onyx/about'>manual</a> ]"; function init() { let overlays = new OverlayState(); try {

@@ -1246,28 +1305,44 @@ 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); - map.setView(home, 13); + const [fromQuery, tileset] = getOverlayFromQuery(); + if (tileset === "sat") { + MapHandler.swapTiles(null); + } + if (fromQuery && !overlays.circles.some(c => c.points[0].lat == fromQuery.points[0].lat && c.points[0].lng == fromQuery.points[0].lng) && !overlays.markers.some(m => m.points[0].lat == fromQuery.points[0].lat && m.points[0].lng == fromQuery.points[0].lng)) { + if (fromQuery.options.radius) { + overlays.circles.push(fromQuery); + } + else { + overlays.markers.push(fromQuery); + } + fromQuery.add(map); + map.setView(fromQuery.points[0], 17); } else { - const okCancel = modals.okCancel; - const okBtn = okCancel.okBtn(); - if (okBtn) { - okBtn.onclick = () => { - modals.closeAll(); - map.locate({ setView: true, maxZoom: 13 }); - }; + const homeData = localStorage.getItem("home"); + if (homeData) { + const home = JSON.parse(homeData); + map.setView(home, 17); } - const cancelBtn = okCancel.cancelBtn(); - if (cancelBtn) { - cancelBtn.onclick = () => { - modals.closeAll(); - }; + else { + const okCancel = modals.okCancel; + const okBtn = okCancel.okBtn(); + if (okBtn) { + okBtn.onclick = () => { + modals.closeAll(); + map.locate({ setView: true, maxZoom: 13 }); + }; + } + const cancelBtn = okCancel.cancelBtn(); + if (cancelBtn) { + cancelBtn.onclick = () => { + modals.closeAll(); + }; + } + okCancel.setMsg("Would you like to use location data to set Home?"); + okCancel.setVisible(true); } - okCancel.setMsg("Would you like to use location data to set Home?"); - okCancel.setVisible(true); } } init();
M static/style.cssstatic/style.css

@@ -238,8 +238,8 @@ }

#createOverlay-container .multiBtn-container button:hover, #createOverlay-container .multiBtn-container button:focus, -#createOverlay-container submitBtn:hover, -#createOverlay-container submitBtn:focus, +#createOverlay-submitBtn:hover, +#createOverlay-submitBtn:focus, #set-home-btn:hover, #set-home-btn:focus, #import-btn:hover,

@@ -424,6 +424,10 @@ .leaflet-popup-tip {

color: white; background: black; border-radius: 0; +} + +.leaflet-popup-content { + cursor: pointer; } .leaflet-popup-close-button {