@trailstash/ultra
Version:
A web based tool for making MapLibre GL maps with data from sources such as Overpass, GeoJSON, GPX, KML, TCX, etc
363 lines (346 loc) • 10.4 kB
JavaScript
import equal from "deep-equal";
import bbox from "@turf/bbox";
import pick from "lodash.pick";
import { compressToEncodedURIComponent } from "lz-string";
import { h } from "../lib/dom.js";
import { setBaseStyle } from "../lib/style.js";
import { setSetting, parseSettings } from "../lib/settings.js";
import {
getAutoRunFromQueryParams,
getOptionsFromQueryParams,
getQueryFromQueryParams,
toQueryParams,
} from "../lib/queryParams.js";
import { localStorage } from "../lib/localStorage.js";
import { UltraMap } from "./ultra-map.js";
import { HelpModal } from "./help-modal.js";
import { StylePicker } from "./style-picker.js";
import { alertOnError } from "../lib/error.js";
const style = new CSSStyleSheet();
style.replaceSync(`
:host {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
}
main {
display: flex;
flex-direction: row;
flex-grow: 1;
}
code-editor {
width: 45%;
border: none;
border-right: 1px solid #8f8f9d;
resize: horizontal;
overflow: auto;
}
ultra-map {
flex-grow: 1;
height: 100%;
}
(max-width: 900px) {
main {
flex-direction: column;
}
code-editor {
width: auto;
height: 45%;
resize: vertical;
}
}
code-editor.fs {
display: none;
}
.fs style-picker,
.fs share-button,
.fs help-modal,
.fs download-button {
display: none;
}
.fs run-button {
z-index: 1;
display: block;
position: fixed;
top: 4px;
left: 4px;
}
fs-button {
z-index: 1;
position: fixed;
top: 4px;
right: 4px;
}
:host(:fullscreen) code-editor, :host(:fullscreen) nav-bar {
display: none;
}
run-button, download-button, share-button, style-picker, help-modal {
margin-right: 4px;
}
`);
export class UltraIDE extends HTMLElement {
autoRun = false;
query = "";
settings = {};
styles;
help;
queryStorage = {
get: () => localStorage.getItem("query"),
set: (query) => localStorage.setItem("query", query),
};
static MAP_INIT_SETTINGS = [
"loadSettingsFromQueryParams",
"options",
"controls",
"mapStyle",
"queryProviders",
"persistState",
];
static props = ["settings", "query", "styles", "help"];
constructor() {
super();
// Bind methods
this.onClickRun = this.onClickRun.bind(this);
this.onClickAbort = this.onClickAbort.bind(this);
this.resetMap = this.resetMap.bind(this);
this.onAuxClickRun = this.onAuxClickRun.bind(this);
this.onChangeCode = this.onChangeCode.bind(this);
this.onMoveEnd = this.onMoveEnd.bind(this);
this.onChangeStyle = this.onChangeStyle.bind(this);
this.onEnterFullscreen = this.onEnterFullscreen.bind(this);
this.onExitFullscreen = this.onExitFullscreen.bind(this);
}
async connectedCallback() {
// Create & populate Shadow DOM
const shadow = this.attachShadow({ mode: "open" });
shadow.adoptedStyleSheets.push(style);
shadow.appendChild(
h(
"nav-bar",
{ title: this.settings.title, icon: this.settings.icon },
h("fs-button", { slot: "fs-button" }),
h(
"span",
{ slot: "buttons" },
h("run-button"),
h("download-button"),
h("share-button"),
h("style-picker", {
styles: this.styles || StylePicker.defaults.styles,
}),
h("help-modal", {
help: this.help || HelpModal.defaults.help,
styles: this.styles || StylePicker.defaults.styles,
type: this.settings.type || UltraMap.defaults.type,
queryProviders:
this.settings.queryProviders || UltraMap.defaults.queryProviders,
}),
),
),
);
// Get initial query and center&zoom
try {
this.query =
(await Promise.resolve(getQueryFromQueryParams()).catch(
alertOnError,
)) ||
this.queryStorage.get() ||
this.query;
} catch (e) {
alert(e.message);
}
this.autoRun = getAutoRunFromQueryParams();
this.settings = {
persistState: true,
...this.settings,
loadSettingsFromQueryParams: false,
};
shadow.appendChild(
h(
"main",
{},
h("code-editor", {
autofocus: true,
source: this.query.then ? "" : this.query,
}),
h("ultra-map", pick(this.settings, UltraIDE.MAP_INIT_SETTINGS)),
),
);
this.refs = {
navBar: shadow.querySelector("nav-bar"),
codeEditor: shadow.querySelector("code-editor"),
ultraMap: shadow.querySelector("ultra-map"),
runButton: shadow.querySelector("run-button"),
downloadButton: shadow.querySelector("download-button"),
shareButton: shadow.querySelector("share-button"),
stylePicker: shadow.querySelector("style-picker"),
fsButton: shadow.querySelector("fs-button"),
};
// Setup Event Listeners
this.refs.codeEditor.addEventListener("change", this.onChangeCode);
this.refs.runButton.addEventListener("run", this.onClickRun);
this.refs.codeEditor.addEventListener("run", this.onClickRun);
this.refs.runButton.addEventListener("auxclick", this.onAuxClickRun);
this.refs.runButton.addEventListener("abort", this.onClickAbort);
this.refs.stylePicker.addEventListener("change", this.onChangeStyle);
this.refs.fsButton.addEventListener(
"enter-fullscreen",
this.onEnterFullscreen,
);
this.refs.fsButton.addEventListener(
"exit-fullscreen",
this.onExitFullscreen,
);
this.refs.ultraMap.map.on("moveend", this.onMoveEnd);
this.refs.ultraMap.map.once("idle", this.onMoveEnd);
// initialize share values
this.refs.shareButton.query = this.query;
// this.refs.downloadButton.query = this.query;
// set query when resolved if a promise
if (this.query.then) {
this.query.then((query) => {
this.refs.codeEditor.source = query;
this.refs.shareButton.query = this.query;
// this.refs.downloadButton.query = this.query;
if (this.autoRun) {
this.onClickRun();
}
});
} else if (this.autoRun) {
this.onClickRun();
}
}
async updateHash() {
if (
this.query ===
(await Promise.resolve(getQueryFromQueryParams()).catch(alertOnError))
)
return;
// mostly cribbed from https://github.com/maplibre/maplibre-gl-js/blob/main/src/ui/hash.ts
// not using URlSearchParams because it URL-enodes / characters
const q = compressToEncodedURIComponent(this.query);
let found = false;
const parts = window.location.hash
.slice(1)
.split("&")
.map((part) => {
const key = part.split("=")[0];
if (key === "q" || key === "query") {
found = true;
return `q=${q}`;
}
return part;
})
.filter((a) => a);
if (!found) {
parts.push(`q=${q}`);
}
const hash = `#${parts.join("&")}`;
const location = window.location.href.replace(/(#.*)?$/, hash);
window.history.pushState(window.history.state, null, location);
}
async onClickRun() {
this.refs.runButton.loading = true; // Loading button state
this.resetMap(); // clear settings from previous run
await this.updateHash();
try {
// basic query/option parsing
const settings = {
...this.settings,
...parseSettings(this.query),
};
Object.assign(this.refs.ultraMap, {
...pick(settings, UltraMap.RUNTIME_SETTINGS),
mapStyle: setBaseStyle(
settings.mapStyle,
this.settings.mapStyle || UltraMap.defaults.mapStyle,
),
});
this.refs.downloadButton.data = null;
this.refs.downloadButton.style = null;
this.controller = new AbortController();
const { data, mapStyle } = await this.refs.ultraMap.run(this.controller);
this.refs.downloadButton.data = data;
this.refs.downloadButton.style = mapStyle;
delete this.controller;
this.refs.runButton.loading = false;
} catch (err) {
this.resetMap();
this.refs.ultraMap.run();
this.refs.runButton.loading = false;
if (err.name != "AbortError") {
alert(err);
throw err;
}
}
}
onClickAbort() {
if (this.controller) {
this.controller.abort();
delete this.controller;
}
this.resetMap();
this.refs.ultraMap.run();
this.refs.runButton.loading = false;
}
resetMap() {
Object.assign(
this.refs.ultraMap,
pick(
{
...UltraMap.defaults,
mapStyle: this.settings.mapStyle || UltraMap.defaults.mapStyle,
},
UltraMap.RUNTIME_SETTINGS,
),
);
}
async onAuxClickRun() {
const params = { query: this.query, mode: "map" };
const settings = parseSettings(this.query);
if (
(settings.options?.center === undefined ||
settings.options?.zoom === undefined) &&
settings.options?.bounds === undefined
) {
params.center = this.refs.ultraMap.center.toArray();
params.zoom = this.refs.ultraMap.zoom;
}
window.location = toQueryParams(params);
}
onChangeCode() {
this.query = this.refs.codeEditor.source;
this.refs.shareButton.query = this.query;
// this.refs.downloadButton.query = this.query;
this.queryStorage.set(this.query);
}
onMoveEnd() {
const zoom = this.refs.ultraMap.zoom;
const center = this.refs.ultraMap.center.toArray();
this.refs.shareButton.zoom = zoom;
this.refs.shareButton.center = center;
this.refs.downloadButton.zoom = zoom;
this.refs.downloadButton.center = center;
}
async onChangeStyle(e) {
this.refs.codeEditor.source = setSetting(this.query, {
style: e.detail.value,
});
const settings = parseSettings(this.query);
this.refs.ultraMap.mapStyle = setBaseStyle(
settings.mapStyle,
this.settings.mapStyle || UltraMap.defaults.mapStyle,
);
const { mapStyle } = await this.refs.ultraMap.run();
this.refs.downloadButton.style = mapStyle;
}
onEnterFullscreen() {
this.refs.navBar.classList.add("fs");
this.refs.codeEditor.classList.add("fs");
}
onExitFullscreen() {
this.refs.navBar.classList.remove("fs");
this.refs.codeEditor.classList.remove("fs");
}
}