UNPKG

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