import/export overlays via query string, clickable popups; v0.3.0

This commit is contained in:
Iris Lightshard 2023-01-25 00:02:47 -07:00
parent 95dc1c8c9d
commit 696d3bc270
Signed by: Iris Lightshard
GPG key ID: 3B7FBC22144E6398
9 changed files with 245 additions and 58 deletions

View file

@ -10,6 +10,8 @@ __note__: On mobile, for best experience you should "install to home screen" or
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,15 +49,27 @@ Opening the menu shows a list of all overlays organized by type; clicking on any
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
If you want to hack on `onyx` and rebuild it, I recommend rebuilding the `sourcemapper` (written in `go`) in the `buildtools` directory first (in case your `glibc` version is older).

View file

@ -74,8 +74,11 @@ abstract class OverlayBase implements Overlay {
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 @@ class Marker extends OverlayBase {
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 @@ class Circle extends OverlayBase {
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 @@ class Polygon extends OverlayBase {
} 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 {

View file

@ -1,3 +1,8 @@
enum ExportMode {
JSON = 0,
URL
}
class CreateOverlayModal implements Modal {
constructor() {
@ -112,6 +117,10 @@ class CreateOverlayModal implements Modal {
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 @@ class CreateOverlayModal implements Modal {
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 @@ class CreateOverlayModal implements Modal {
}
}
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 @@ class CreateOverlayModal implements Modal {
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);
}
}

View file

@ -401,7 +401,7 @@ class MapHandler {
}
}
static exportSingle(overlay: OverlayBase): void {
static exportSingle(overlay: OverlayBase, mode: ExportMode = ExportMode.JSON): void {
const self = MapHandler.instance;
if (self) {
self.modals.closeAll();
@ -419,11 +419,24 @@ class MapHandler {
}
}
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 {
const self = MapHandler.instance;
if (self) {

38
src/80-share.ts Normal file
View file

@ -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];
}

View file

@ -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,10 +72,23 @@ function init(): void {
// the menu doesn't open on the first click unless we do this first... not sure why
modals.closeAll();
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 homeData = localStorage.getItem("home");
if (homeData) {
const home = <Point>JSON.parse(homeData);
map.setView(home, 13);
map.setView(home, 17);
} else {
const okCancel = modals.okCancel;
const okBtn = okCancel.okBtn();
@ -94,6 +107,7 @@ function init(): void {
okCancel.setMsg("Would you like to use location data to set Home?");
okCancel.setVisible(true);
}
}
}
init();

View file

@ -5,7 +5,7 @@
<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 @@
</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 @@
</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>

View file

@ -50,8 +50,11 @@ class OverlayBase {
}
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 @@ class Marker extends OverlayBase {
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 @@ class Circle extends OverlayBase {
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 @@ class Polygon extends OverlayBase {
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 @@ class TextUtils {
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 @@ class CreateOverlayModal {
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 @@ class CreateOverlayModal {
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 @@ class CreateOverlayModal {
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 @@ class CreateOverlayModal {
}
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 @@ class MapHandler {
self.modals.okCancel.setVisible(true);
}
}
static exportSingle(overlay) {
static exportSingle(overlay, mode = ExportMode.JSON) {
const self = MapHandler.instance;
if (self) {
self.modals.closeAll();
@ -1138,10 +1164,22 @@ class MapHandler {
self.modals.info.setVisible(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;
if (self) {
@ -1200,7 +1238,28 @@ class MapHandler {
}
}
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,10 +1305,25 @@ function init() {
});
// the menu doesn't open on the first click unless we do this first... not sure why
modals.closeAll();
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 homeData = localStorage.getItem("home");
if (homeData) {
const home = JSON.parse(homeData);
map.setView(home, 13);
map.setView(home, 17);
}
else {
const okCancel = modals.okCancel;
@ -1269,5 +1343,6 @@ function init() {
okCancel.setMsg("Would you like to use location data to set Home?");
okCancel.setVisible(true);
}
}
}
init();

View file

@ -238,8 +238,8 @@ body {
#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,
@ -426,6 +426,10 @@ body {
border-radius: 0;
}
.leaflet-popup-content {
cursor: pointer;
}
.leaflet-popup-close-button {
color: #c9c9c9 !important;
}