@trailstash/ultra 4.3.1 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.gitlab-ci.yml CHANGED
@@ -10,11 +10,13 @@ pages:
10
10
  - npm ci
11
11
  # Update version
12
12
  - jq ".version = \"$(git describe --tags --dirty | sed -e 's/^v//')\"" package.json | sponge package.json
13
+ - mv pages-config.mjs config.mjs
13
14
  - npm run build
14
15
  - npm run build:examples-docs
15
16
  - npm run build:maplibre-examples-docs
16
17
  - npm run build:docs
17
18
  - npm run build:pages
19
+ - find public -type f -regex '.*\.\(htm\|html\|xml\|txt\|text\|js\|css\|svg\)$' -exec gzip -f -k {} \;
18
20
  artifacts:
19
21
  paths:
20
22
  - public
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: Add a color relief layer
3
+ description: Add a color relief layer.
4
+ options:
5
+ center: [ 11.45, 47.2 ]
6
+ zoom: 10
7
+ pitch: 0
8
+ style:
9
+ version: 8
10
+ sources:
11
+ terrainSource:
12
+ type: raster-dem
13
+ url: https://demotiles.maplibre.org/terrain-tiles/tiles.json
14
+ tileSize: 256
15
+ layers:
16
+ - type: color-relief
17
+ source: terrainSource
18
+ color-relief-color:
19
+ - interpolate
20
+ - [linear]
21
+ - [elevation]
22
+ - 400
23
+ - rgb(4, 0, 108)
24
+ - 582.35
25
+ - rgb(5, 1, 154)
26
+ - 764.71
27
+ - rgb(10, 21, 189)
28
+ - 947.06
29
+ - rgb(16, 44, 218)
30
+ - 1129.41
31
+ - rgb(24, 69, 240)
32
+ - 1311.76
33
+ - rgb(20, 112, 193)
34
+ - 1494.12
35
+ - rgb(39, 144, 116)
36
+ - 1676.47
37
+ - rgb(57, 169, 29)
38
+ - 1858.82
39
+ - rgb(111, 186, 5)
40
+ - 2041.18
41
+ - rgb(160, 201, 4)
42
+ - 2223.53
43
+ - rgb(205, 216, 2)
44
+ - 2405.88
45
+ - rgb(244, 221, 4)
46
+ - 2588.24
47
+ - rgb(251, 194, 14)
48
+ - 2770.59
49
+ - rgb(252, 163, 21)
50
+ - 2952.94
51
+ - rgb(253, 128, 20)
52
+ - 3135.29
53
+ - rgb(254, 85, 14)
54
+ - 3317.65
55
+ - rgb(243, 36, 13)
56
+ - 3500
57
+ - rgb(215, 5, 13)
58
+ ---
@@ -27,14 +27,14 @@ export class ButtonModal extends HTMLElement {
27
27
  button {
28
28
  position: relative;
29
29
  }
30
- button div span.close {
30
+ button + div.backdrop span.close {
31
31
  cursor: pointer;
32
32
  float: right;
33
33
  }
34
34
  button.button-modal:after {
35
35
  content: " ${this.text}";
36
36
  }
37
- button > div {
37
+ button + div.backdrop {
38
38
  cursor: default;
39
39
  display: none;
40
40
  z-index: 1;
@@ -45,14 +45,14 @@ export class ButtonModal extends HTMLElement {
45
45
  right: 0;
46
46
  background: rgba(0,0,0,0.5);
47
47
  }
48
- button > div.visible {
48
+ button + div.backdrop.visible {
49
49
  display: flex;
50
50
  flex-direction: column;
51
51
  justify-content: center;
52
52
  align-items: center;
53
53
  padding: 20px;
54
54
  }
55
- button > div > div {
55
+ button + div.backdrop > div {
56
56
  max-height: 100%;
57
57
  max-width: 100%;
58
58
  overflow: auto;
@@ -81,21 +81,19 @@ export class ButtonModal extends HTMLElement {
81
81
  h("span", { class: "close" }, "×"),
82
82
  h("slot", { name: "modal-content" }),
83
83
  );
84
- const backdrop = h("div", {}, div);
84
+ const backdrop = h("div", { className: "backdrop" }, div);
85
85
  const button = h(
86
86
  "button",
87
87
  { class: "button-modal" },
88
88
  h("fa-icon", { icon: this.icon }),
89
- backdrop,
90
89
  );
91
90
  this.refs = { button, div, backdrop };
92
91
 
93
92
  button.addEventListener("click", () => {
94
93
  this.toggle();
95
94
  });
96
- div.addEventListener("click", (e) => {
97
- e.preventDefault();
98
- e.stopPropagation();
95
+ backdrop.addEventListener("click", (e) => {
96
+ if (e.target.classList.contains("backdrop")) this.toggle();
99
97
  });
100
98
  div.querySelector("span.close").addEventListener("click", (e) => {
101
99
  this.toggle();
@@ -104,6 +102,7 @@ export class ButtonModal extends HTMLElement {
104
102
  });
105
103
 
106
104
  shadow.appendChild(button);
105
+ shadow.appendChild(backdrop);
107
106
  }
108
107
  toggle() {
109
108
  this.refs.backdrop.classList.toggle("visible");
@@ -45,6 +45,8 @@ export class CodeEditor extends HTMLElement {
45
45
  value: this.source,
46
46
  });
47
47
  this.refs = { textarea };
48
+ this.refs.textarea.selectionStart = 0;
49
+ this.refs.textarea.selectionEnd = 0;
48
50
  shadow.appendChild(textarea);
49
51
  textarea.addEventListener("dragenter", (e) => {
50
52
  e.preventDefault();
@@ -0,0 +1,282 @@
1
+ import { h, t } from "../lib/dom.js";
2
+ import { style as buttonCSS } from "./button.js";
3
+ import { normalizeCSS } from "../lib/normalize.js";
4
+ import { toQueryParams } from "../lib/queryParams.js";
5
+ import { htmlExport } from "../lib/htmlExport.js";
6
+
7
+ export class ExportModal extends HTMLElement {
8
+ #center;
9
+ #zoom;
10
+ #query;
11
+ #data;
12
+ #style;
13
+
14
+ constructor() {
15
+ super();
16
+ }
17
+
18
+ get center() {
19
+ return this.#center;
20
+ }
21
+ set center(value) {
22
+ this.#center = value;
23
+ this.update();
24
+ }
25
+
26
+ get zoom() {
27
+ return this.#zoom;
28
+ }
29
+ set zoom(value) {
30
+ this.#zoom = value;
31
+ this.update();
32
+ }
33
+
34
+ get query() {
35
+ return this.#query;
36
+ }
37
+ set query(value) {
38
+ this.#query = value;
39
+ this.update();
40
+ }
41
+
42
+ set data(data) {
43
+ this.#data = data;
44
+ this.update();
45
+ }
46
+ get data() {
47
+ return this.#data;
48
+ }
49
+
50
+ set style(style) {
51
+ this.#style = style;
52
+ this.update();
53
+ }
54
+ get style() {
55
+ return this.#style
56
+ ? {
57
+ ...this.#style,
58
+ glyphs:
59
+ this.#style && this.#style.glyphs
60
+ ? new URL(this.#style.glyphs).searchParams.get("url")
61
+ : undefined,
62
+ }
63
+ : null;
64
+ }
65
+ get html() {
66
+ return htmlExport(this.style, "Ultra Export", "", {
67
+ zoom: this.zoom,
68
+ center: this.center,
69
+ });
70
+ }
71
+
72
+ connectedCallback() {
73
+ const shadow = this.attachShadow({ mode: "open" });
74
+
75
+ const dataDownloadButton = h("button", {}, t("download"));
76
+ dataDownloadButton.addEventListener("click", () =>
77
+ this.download(JSON.stringify(this.data), "ultra.geojson"),
78
+ );
79
+ const styleDownloadButton = h("button", {}, t("download"));
80
+ styleDownloadButton.addEventListener("click", () => {
81
+ this.download(JSON.stringify(this.style), "style.json");
82
+ });
83
+ const htmlDownloadButton = h("button", {}, t("download"));
84
+ htmlDownloadButton.addEventListener("click", () => {
85
+ this.download(this.html, "ultra.html");
86
+ });
87
+ const queryDownloadButton = h("button", {}, t("download"));
88
+ queryDownloadButton.addEventListener("click", () => {
89
+ this.download(this.query, "query.ultra");
90
+ });
91
+ const dataCopyButton = h("button", {}, t("copy"));
92
+ dataCopyButton.addEventListener("click", () => {
93
+ navigator.clipboard.writeText(JSON.stringify(this.data));
94
+ });
95
+ const styleCopyButton = h("button", {}, t("copy"));
96
+ styleCopyButton.addEventListener("click", () => {
97
+ navigator.clipboard.writeText(JSON.stringify(this.style));
98
+ });
99
+ const htmlCopyButton = h("button", {}, t("copy"));
100
+ htmlCopyButton.addEventListener("click", () => {
101
+ navigator.clipboard.writeText(this.html);
102
+ });
103
+ const queryCopyButton = h("button", {}, t("copy"));
104
+ queryCopyButton.addEventListener("click", () => {
105
+ navigator.clipboard.writeText(this.query);
106
+ });
107
+ const div = h(
108
+ "div",
109
+ { style: "", slot: "modal-content" },
110
+ h(
111
+ "style",
112
+ {},
113
+ `
114
+ div {
115
+ padding: 0 8px 8px;
116
+ max-width: 100%;
117
+ width: 460px;
118
+ }
119
+ details, .note {
120
+ font-size: 0.8em;
121
+ }
122
+ h3, h5 {
123
+ margin-bottom: 0;
124
+ }
125
+ input[type=text] {
126
+ width: 100%;
127
+ padding: 0;
128
+ margin: 0;
129
+ }
130
+ label {
131
+ display: block;
132
+ }
133
+ button {
134
+ cursor: pointer;
135
+ }
136
+ `,
137
+ ),
138
+ h(
139
+ "div",
140
+ { className: "data" },
141
+ h("h3", {}, "Data"),
142
+ h("p", {}, "Export the query result as GeoJSON"),
143
+ dataDownloadButton,
144
+ t(" "),
145
+ dataCopyButton,
146
+ ),
147
+ h(
148
+ "div",
149
+ { className: "map" },
150
+ h("h3", {}, "Map"),
151
+ h("fa-icon", { icon: "triangle-exclamation" }),
152
+ h(
153
+ "span",
154
+ { className: "note" },
155
+ t(" Note: some styles' tiles have CORS restrictions"),
156
+ ),
157
+ h(
158
+ "p",
159
+ {},
160
+ t("Export the map as a MapLibre style "),
161
+ h(
162
+ "details",
163
+ {},
164
+ h(
165
+ "summary",
166
+ {},
167
+ h("fa-icon", { icon: "triangle-exclamation" }),
168
+ t(" usage notes"),
169
+ ),
170
+ t("Ultra's support for "),
171
+ h(
172
+ "a",
173
+ {
174
+ href: "https://overpass-ultra.us/docs/style/#png-sprites-via-https",
175
+ target: "_blank",
176
+ },
177
+ "HTTPS PNG sprites",
178
+ ),
179
+ t(", "),
180
+ h(
181
+ "a",
182
+ {
183
+ href: "https://overpass-ultra.us/docs/style/#fallback-fontstack",
184
+ target: "_blank",
185
+ },
186
+ "Fallback glyphs",
187
+ ),
188
+ t(", and interactive popups"),
189
+ t(" are run-time features and aren't included in the generated "),
190
+ h("code", {}, "style.json"),
191
+ t(". You may experience issues with missing icons or text."),
192
+ ),
193
+ ),
194
+ styleDownloadButton,
195
+ t(" "),
196
+ styleCopyButton,
197
+ /*
198
+ h(
199
+ "p",
200
+ {},
201
+ t("Export an interactive map"),
202
+ h(
203
+ "details",
204
+ {},
205
+ h(
206
+ "summary",
207
+ {},
208
+ h("fa-icon", { icon: "triangle-exclamation" }),
209
+ t(" usage notes"),
210
+ ),
211
+ t(
212
+ "Ultra's interactive popups are not yet supported in exported HTML.",
213
+ ),
214
+ ),
215
+ ),
216
+ htmlDownloadButton,
217
+ t(" "),
218
+ htmlCopyButton,
219
+ */
220
+ ),
221
+ /*
222
+ h(
223
+ "div",
224
+ { className: "query" },
225
+ h("h3", {}, "Query"),
226
+ h("p", {}, "Export the Ultra query"),
227
+ queryDownloadButton,
228
+ t(" "),
229
+ queryCopyButton,
230
+ ),
231
+ */
232
+ );
233
+ const button = h(
234
+ "button-modal",
235
+ { text: "Export", icon: "file-export" },
236
+ div,
237
+ );
238
+ this.refs = {
239
+ button,
240
+ div,
241
+ };
242
+
243
+ shadow.appendChild(button);
244
+
245
+ this.update();
246
+ }
247
+
248
+ update() {
249
+ if (this.data) {
250
+ this.refs.div.querySelector(".data").style.display = "block";
251
+ } else {
252
+ this.refs.div.querySelector(".data").style.display = "none";
253
+ }
254
+ if (this.style) {
255
+ this.refs.button.refs.button.disabled = false;
256
+ } else {
257
+ this.refs.button.refs.button.disabled = true;
258
+ }
259
+ }
260
+
261
+ download(data, filename) {
262
+ const blob = new Blob([data], { type: "octet/stream" });
263
+ const url = window.URL.createObjectURL(blob);
264
+
265
+ const link = h("a", { download: filename, href: url });
266
+
267
+ // this is necessary as link.click() does not work on the latest firefox
268
+ link.dispatchEvent(
269
+ new MouseEvent("click", {
270
+ bubbles: true,
271
+ cancelable: true,
272
+ view: window,
273
+ }),
274
+ );
275
+
276
+ setTimeout(() => {
277
+ // For Firefox it is necessary to delay revoking the ObjectURL
278
+ window.URL.revokeObjectURL(url);
279
+ link.remove();
280
+ }, 100);
281
+ }
282
+ }
@@ -10,6 +10,8 @@ import {
10
10
  faDownLeftAndUpRightToCenter,
11
11
  faPaintbrush,
12
12
  faPenToSquare,
13
+ faFileExport,
14
+ faTriangleExclamation,
13
15
  } from "@fortawesome/free-solid-svg-icons";
14
16
 
15
17
  import { h } from "../lib/dom.js";
@@ -27,6 +29,8 @@ library.add(faUpRightAndDownLeftFromCenter);
27
29
  library.add(faDownLeftAndUpRightToCenter);
28
30
  library.add(faPaintbrush);
29
31
  library.add(faPenToSquare);
32
+ library.add(faFileExport);
33
+ library.add(faTriangleExclamation);
30
34
 
31
35
  export const css = new CSSStyleSheet();
32
36
  css.replaceSync(`
@@ -2,6 +2,7 @@ import { h, t } from "../lib/dom.js";
2
2
  import { style as buttonCSS } from "./button.js";
3
3
  import { normalizeCSS } from "../lib/normalize.js";
4
4
  import { toQueryParams } from "../lib/queryParams.js";
5
+ import { parseSettings } from "../lib/settings.js";
5
6
 
6
7
  export class ShareModal extends HTMLElement {
7
8
  #center;
@@ -90,6 +91,9 @@ export class ShareModal extends HTMLElement {
90
91
  label {
91
92
  display: block;
92
93
  }
94
+ label:has(input:disabled) {
95
+ color: grey;
96
+ }
93
97
  `,
94
98
  ),
95
99
  h("h3", {}, "Query"),
@@ -1,6 +1,7 @@
1
1
  import equal from "deep-equal";
2
2
  import bbox from "@turf/bbox";
3
3
  import pick from "lodash.pick";
4
+ import { compressToEncodedURIComponent } from "lz-string";
4
5
  import { h } from "../lib/dom.js";
5
6
  import { setBaseStyle } from "../lib/style.js";
6
7
  import { setSetting, parseSettings } from "../lib/settings.js";
@@ -10,11 +11,7 @@ import {
10
11
  getQueryFromQueryParams,
11
12
  toQueryParams,
12
13
  } from "../lib/queryParams.js";
13
- import {
14
- localStorage,
15
- optionsFromStorage,
16
- queryFromStorage,
17
- } from "../lib/localStorage.js";
14
+ import { localStorage } from "../lib/localStorage.js";
18
15
  import { UltraMap } from "./ultra-map.js";
19
16
  import { HelpModal } from "./help-modal.js";
20
17
  import { StylePicker } from "./style-picker.js";
@@ -89,6 +86,10 @@ export class UltraIDE extends HTMLElement {
89
86
  settings = {};
90
87
  styles;
91
88
  help;
89
+ queryStorage = {
90
+ get: () => localStorage.getItem("query"),
91
+ set: (query) => localStorage.setItem("query", query),
92
+ };
92
93
 
93
94
  static MAP_INIT_SETTINGS = [
94
95
  "loadSettingsFromQueryParams",
@@ -114,7 +115,7 @@ export class UltraIDE extends HTMLElement {
114
115
  this.onExitFullscreen = this.onExitFullscreen.bind(this);
115
116
  }
116
117
 
117
- connectedCallback() {
118
+ async connectedCallback() {
118
119
  // Create & populate Shadow DOM
119
120
  const shadow = this.attachShadow({ mode: "open" });
120
121
  shadow.adoptedStyleSheets.push(style);
@@ -144,7 +145,14 @@ export class UltraIDE extends HTMLElement {
144
145
  );
145
146
 
146
147
  // Get initial query and center&zoom
147
- this.query = getQueryFromQueryParams() || queryFromStorage() || this.query;
148
+ try {
149
+ this.query =
150
+ (await Promise.resolve(getQueryFromQueryParams())) ||
151
+ this.queryStorage.get() ||
152
+ this.query;
153
+ } catch (e) {
154
+ alert(e.message);
155
+ }
148
156
  this.autoRun = getAutoRunFromQueryParams();
149
157
  this.settings = {
150
158
  persistState: true,
@@ -195,12 +203,14 @@ export class UltraIDE extends HTMLElement {
195
203
 
196
204
  // initialize share values
197
205
  this.refs.shareButton.query = this.query;
206
+ // this.refs.downloadButton.query = this.query;
198
207
 
199
208
  // set query when resolved if a promise
200
209
  if (this.query.then) {
201
210
  this.query.then((query) => {
202
211
  this.refs.codeEditor.source = query;
203
212
  this.refs.shareButton.query = this.query;
213
+ // this.refs.downloadButton.query = this.query;
204
214
  if (this.autoRun) {
205
215
  this.onClickRun();
206
216
  }
@@ -210,9 +220,36 @@ export class UltraIDE extends HTMLElement {
210
220
  }
211
221
  }
212
222
 
223
+ async updateHash() {
224
+ if (this.query === (await Promise.resolve(getQueryFromQueryParams())))
225
+ return;
226
+ // mostly cribbed from https://github.com/maplibre/maplibre-gl-js/blob/main/src/ui/hash.ts
227
+ // not using URlSearchParams because it URL-enodes / characters
228
+ const q = compressToEncodedURIComponent(this.query);
229
+ let found = false;
230
+ const parts = window.location.hash
231
+ .slice(1)
232
+ .split("&")
233
+ .map((part) => {
234
+ const key = part.split("=")[0];
235
+ if (key === "q" || key === "query") {
236
+ found = true;
237
+ return `q=${q}`;
238
+ }
239
+ return part;
240
+ })
241
+ .filter((a) => a);
242
+ if (!found) {
243
+ parts.push(`q=${q}`);
244
+ }
245
+ const hash = `#${parts.join("&")}`;
246
+ const location = window.location.href.replace(/(#.*)?$/, hash);
247
+ window.history.pushState(window.history.state, null, location);
248
+ }
213
249
  async onClickRun() {
214
250
  this.refs.runButton.loading = true; // Loading button state
215
251
  this.resetMap(); // clear settings from previous run
252
+ await this.updateHash();
216
253
  try {
217
254
  // basic query/option parsing
218
255
  const settings = {
@@ -228,10 +265,11 @@ export class UltraIDE extends HTMLElement {
228
265
  });
229
266
 
230
267
  this.refs.downloadButton.data = null;
268
+ this.refs.downloadButton.style = null;
231
269
  this.controller = new AbortController();
232
- this.refs.downloadButton.data = await this.refs.ultraMap.run(
233
- this.controller,
234
- );
270
+ const { data, mapStyle } = await this.refs.ultraMap.run(this.controller);
271
+ this.refs.downloadButton.data = data;
272
+ this.refs.downloadButton.style = mapStyle;
235
273
  delete this.controller;
236
274
 
237
275
  this.refs.runButton.loading = false;
@@ -282,15 +320,18 @@ export class UltraIDE extends HTMLElement {
282
320
  onChangeCode() {
283
321
  this.query = this.refs.codeEditor.source;
284
322
  this.refs.shareButton.query = this.query;
285
- localStorage.setItem("query", this.query);
323
+ // this.refs.downloadButton.query = this.query;
324
+ this.queryStorage.set(this.query);
286
325
  }
287
326
  onMoveEnd() {
288
327
  const zoom = this.refs.ultraMap.zoom;
289
328
  const center = this.refs.ultraMap.center.toArray();
290
329
  this.refs.shareButton.zoom = zoom;
291
330
  this.refs.shareButton.center = center;
331
+ this.refs.downloadButton.zoom = zoom;
332
+ this.refs.downloadButton.center = center;
292
333
  }
293
- onChangeStyle(e) {
334
+ async onChangeStyle(e) {
294
335
  this.refs.codeEditor.source = setSetting(this.query, {
295
336
  style: e.detail.value,
296
337
  });
@@ -299,7 +340,8 @@ export class UltraIDE extends HTMLElement {
299
340
  settings.mapStyle,
300
341
  this.settings.mapStyle || UltraMap.defaults.mapStyle,
301
342
  );
302
- this.refs.ultraMap.run();
343
+ const { mapStyle } = await this.refs.ultraMap.run();
344
+ this.refs.downloadButton.style = mapStyle;
303
345
  }
304
346
 
305
347
  onEnterFullscreen() {