@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.
@@ -15,6 +15,12 @@ import { handleStyleImageMissing } from "../lib/sprites.js";
15
15
  import { handleMouseClick, handleMouseMove } from "../lib/queryMap.js";
16
16
  import { localStorage, optionsFromStorage } from "../lib/localStorage.js";
17
17
  import { HTMLControl } from "./html-control.js";
18
+ import makeSandbox from "../lib/sandbox.js";
19
+
20
+ let sandbox;
21
+ try {
22
+ sandbox = await makeSandbox();
23
+ } catch {}
18
24
 
19
25
  const css = new CSSStyleSheet();
20
26
  css.replaceSync(`
@@ -57,6 +63,7 @@ export class UltraMap extends HTMLElement {
57
63
  #shadow;
58
64
 
59
65
  #cachedBBox;
66
+ #cachedTransform;
60
67
  #cachedType;
61
68
  #cachedQuery;
62
69
  #cachedSource;
@@ -77,6 +84,9 @@ export class UltraMap extends HTMLElement {
77
84
  fitBounds: undefined,
78
85
  queryProviders,
79
86
  persistState: false,
87
+ transform: undefined,
88
+ title: undefined,
89
+ description: undefined,
80
90
  };
81
91
 
82
92
  loadSettingsFromQueryParams = UltraMap.defaults.loadSettingsFromQueryParams;
@@ -100,6 +110,11 @@ export class UltraMap extends HTMLElement {
100
110
 
101
111
  queryProviders = UltraMap.defaults.queryProviders;
102
112
 
113
+ transform = UltraMap.defaults.transform;
114
+
115
+ title = UltraMap.defaults.title;
116
+ description = UltraMap.defaults.description;
117
+
103
118
  static CONFIG_SETTINGS = [
104
119
  "queryProviders",
105
120
  "loadSettingsFromQueryParams",
@@ -129,6 +144,7 @@ export class UltraMap extends HTMLElement {
129
144
  "query",
130
145
  "fitBounds",
131
146
  "mapStyle",
147
+ "transform",
132
148
  ];
133
149
 
134
150
  constructor() {
@@ -166,8 +182,8 @@ export class UltraMap extends HTMLElement {
166
182
  this.#shadow.adoptedStyleSheets.push(css);
167
183
 
168
184
  if (this.loadSettingsFromQueryParams) {
169
- return Promise.resolve(getQueryFromQueryParams() || this.query).then(
170
- async (query) => {
185
+ return Promise.resolve(getQueryFromQueryParams() || this.query)
186
+ .then(async (query) => {
171
187
  const querySettings = parseSettings(query);
172
188
  const settings = {
173
189
  ...this,
@@ -188,8 +204,8 @@ export class UltraMap extends HTMLElement {
188
204
  });
189
205
 
190
206
  return this.#init(await getStyle(this.mapStyle));
191
- },
192
- );
207
+ })
208
+ .catch(alert);
193
209
  } else if (this.persistState) {
194
210
  this.options = {
195
211
  ...this.options,
@@ -282,8 +298,9 @@ export class UltraMap extends HTMLElement {
282
298
  }
283
299
 
284
300
  async run(controller) {
285
- this.refs.mapLibre.mapStyle = await getStyle(this.mapStyle);
286
- const data = await this.#run(controller);
301
+ const mapStyle = await getStyle(this.mapStyle);
302
+ this.refs.mapLibre.mapStyle = mapStyle;
303
+ const result = await this.#run(controller);
287
304
  if (this.#fitBounds) {
288
305
  if (
289
306
  this.#cachedSource?.type === "geojson" &&
@@ -299,7 +316,7 @@ export class UltraMap extends HTMLElement {
299
316
  );
300
317
  }
301
318
  }
302
- return data;
319
+ return result || { mapStyle };
303
320
  }
304
321
  async #runUnbound(controller) {
305
322
  if (!this.query) {
@@ -320,6 +337,7 @@ export class UltraMap extends HTMLElement {
320
337
  const query = setQueryBounds(this.query, this.refs.mapLibre.bounds);
321
338
  if (
322
339
  !this.#cachedSource ||
340
+ this.transform !== this.#cachedTransform ||
323
341
  this.type !== this.#cachedType ||
324
342
  query !== this.#cachedQuery ||
325
343
  (queryProvider.invalidateCacheOnBBox &&
@@ -331,20 +349,30 @@ export class UltraMap extends HTMLElement {
331
349
  this.refs.mapLibre.bounds,
332
350
  );
333
351
  this.#cachedQuery = query;
352
+ this.#cachedTransform = this.transform;
334
353
  this.#cachedType = this.type;
335
- this.#cachedSource = await queryProvider.source(query, controller, {
354
+ let source = await queryProvider.source(query, controller, {
336
355
  server: this.server,
337
356
  bounds: this.refs.mapLibre.bounds,
338
357
  });
358
+ if (this.transform) {
359
+ if (sandbox) {
360
+ source.data = await sandbox(this.transform, source.data);
361
+ } else {
362
+ throw new Error("sandbox could not be initialized");
363
+ }
364
+ }
365
+ this.#cachedSource = source;
339
366
  }
340
- this.refs.mapLibre.mapStyle = await getStyle(this.mapStyle, {
367
+ const mapStyle = await getStyle(this.mapStyle, {
341
368
  // Don't love this...
342
369
  source: this.#cachedSource,
343
370
  layers: queryProvider.layers
344
371
  ? await Promise.resolve(queryProvider.layers("ultra", query))
345
372
  : [],
346
373
  });
347
- return this.#cachedSource.data;
374
+ this.refs.mapLibre.mapStyle = mapStyle;
375
+ return { data: this.#cachedSource.data, mapStyle };
348
376
  } finally {
349
377
  this.refs.loadingIndicator.style.display = "none";
350
378
  }
package/config.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  export const defaultMode = "ide";
2
2
  export const modes = {
3
3
  ide: {
4
- settings: {},
4
+ settings: { options: { hash: "m" }},
5
5
  },
6
6
  map: {
7
7
  settings: {},
@@ -12,7 +12,7 @@ Here are some more resources on Ultra:
12
12
 
13
13
  ## Talks
14
14
 
15
- - [Mapping USA 2025 - Slides](https://ultra-mapping-usa-2025.glitch.me/#1)
15
+ - [Mapping USA 2025](https://openstreetmap.us/events/mapping-usa/2025/making-maps-with-ultra/)
16
16
 
17
17
  ## Third party blog posts
18
18
 
@@ -0,0 +1,94 @@
1
+ # Open with Ultra
2
+
3
+ **Open with Ultra** is a [bookmarklet](https://en.wikipedia.org/wiki/Bookmarklet) for making it
4
+ easier to use Ultra.
5
+
6
+ ## Install
7
+
8
+ Install it by dragging the **Open with Ultra** link below to your bookmarks bar.
9
+
10
+ <center><a id="bookmarklet">**Open with Ultra**</a></center>
11
+
12
+ <style>
13
+ #bookmarklet {
14
+ display: inline-block;
15
+ background: #526cfe;
16
+ color: white;
17
+ box-shadow: 0 0 .2rem rgba(0,0,0,.1),0 .2rem .4rem rgba(0,0,0,.2);
18
+ padding: 10px;
19
+ }
20
+ </style>
21
+
22
+ <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jquery@3.7.1"></script>
23
+ <script type="text/javascript" src="https://cdn.jsdelivr.net/gh/dschep/jQuery-Bookmarklet@master/jquery.bookmarklet.js"></script>
24
+ <script>
25
+ const b = document.querySelector("#bookmarklet")
26
+ b.onclick = e => { e.preventDefault(); return false; };
27
+ $(b).bookmarkletHelperArrow();
28
+ const code = () => {
29
+ const origin = "https://overpass-ultra.us";
30
+ if (window.location.host === "overpass-turbo.eu") {
31
+ const search = new URLSearchParams();
32
+ search.set(
33
+ "query",
34
+ JSON.parse(localStorage.getItem("overpass-ide_code")).overpass,
35
+ );
36
+ if (localStorage.getItem("overpass-ide_coords_zoom")) {
37
+ search.set(
38
+ "m",
39
+ [
40
+ parseInt(localStorage.getItem("overpass-ide_coords_zoom")) - 1,
41
+ localStorage.getItem("overpass-ide_coords_lat"),
42
+ localStorage.getItem("overpass-ide_coords_lon"),
43
+ ].join("/"),
44
+ );
45
+ }
46
+ window.location = `${origin}/#${search.toString()}`;
47
+ } else if (window.location.host === "qlever.cs.uni-freiburg.de") {
48
+ const search = new URLSearchParams();
49
+ search.set("query", `---\ntype: sparql\nserver: https://qlever.cs.uni-freiburg.de/api/${document.querySelector("#backend-slug").textContent}\n---\n${document.querySelector(".CodeMirror").CodeMirror.getValue()}`);
50
+ window.location = `${origin}/#${search.toString()}`;
51
+ } else if (window.location.host === "sophox.org") {
52
+ const search = new URLSearchParams();
53
+ search.set("query", `---\ntype: sparql\nserver: https://sophox.org/sparql\n---\n${localStorage.getItem("wikibase.queryService.ui.Editor")}`);
54
+ window.location = `${origin}/#${search.toString()}`;
55
+ } else if (window.location.host === "gist.github.com") {
56
+ window.location = `${origin}/#query=gist:${window.location.pathname.split("/").slice(-1)[0]}`;
57
+ } else if (window.location.host === "github.com") {
58
+ window.location = `${origin}/#query=url:https://raw.githubusercontent.com${window.location.pathname.replace("/blob", "")}`;
59
+ } else if (window.location.host === "geojson.io") {
60
+ window.location = `${origin}/#query=${encodeURIComponent(JSON.stringify(window.api.data.all().map))}`;
61
+ } else {
62
+ window.location = `${origin}/#query=${encodeURIComponent(window.location)}`;
63
+ }
64
+ }
65
+ b.href = `javascript:(${code.toString()})();`
66
+ </script>
67
+
68
+ ## Behavior
69
+
70
+ It features special support for:
71
+
72
+ - [overpass-turbo.eu](https://overpass-turbo.eu) - Loads query & viewport from overpass turbo in
73
+ Ultra
74
+ - [QLever](https://qlever.cs.uni-freiburg.de/) - Loads query & server from the QLever in Ultra
75
+ - [Sophox](https://sopox.org/) - Loads query & server from the Sophox in Ultra
76
+ - [geojson.io](https://geojson.io) - loads the GeoJSON from geojson.io as the query in Ultra
77
+ - [Gists](https://gist.github.com) - loads the current gist as the query in Ultra
78
+ - [Github](https://github.com) - loads the `githubusercontent.com` URL for a file loaded in the web
79
+ UI
80
+
81
+ For all other sites, it loads the URL as the query.
82
+
83
+ Ultra features some query providers which work well with this:
84
+
85
+ - `osmWebsite` - detects `https://openstreetmap.org/[node|way|relation]/:id` URLs and loads that
86
+ object via the Overpass API
87
+ - `osmWiki` - detects `https://wiki.openstreetmap.org/wiki/Key:` and
88
+ `https://wiki.openstreetmap.org/wiki/Tag:` URLs and loads that object with that tag or key via
89
+ the Overpass API
90
+ - `taginfo` - detects `https://taginfo.openstreetmap.org/key/` and
91
+ `https://taginfo.openstreetmap.org/tags/` URLs and loads that object with that tag or key via the
92
+ Overpass API
93
+ - `kml` - detects `https://www.google.com/maps/d` (Google My Maps) URLs and loads that map via the
94
+ KML export.
package/docs/style.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Styling
2
2
 
3
- The MapLibre styling can be attached to a query via the `style:` key of the [YAML
4
- front-matter](./yaml.md).
3
+ [MapLibre styling](https://maplibe.org/maplibre-style-spec/) can be attached to a query
4
+ via the `style:` key of the [YAML front-matter](./yaml.md).
5
5
 
6
6
  ```
7
7
  ---
@@ -18,7 +18,7 @@ Example: [http://overpass-ultra.us/#query=gist:8ecb8ba0a0136f4f0dbc36de82061de4]
18
18
 
19
19
  You can load a query from any url by using a string prefixed with `url:`
20
20
 
21
- Example: [http://overpass-ultra.us/#query=url:https%3A%2F%2Fgist.githubusercontent.com%2Fdschep%2F8ecb8ba0a0136f4f0dbc36de82061de4%2Fraw%2F40ce5053cc664984d677bca20a2a2d3371ce09b0%2Fgistfile1.txt](http://overpass-ultra.us/#query=url:https%3A%2F%2Fgist.githubusercontent.com%2Fdschep%2F8ecb8ba0a0136f4f0dbc36de82061de4%2Fraw%2F40ce5053cc664984d677bca20a2a2d3371ce09b0%2Fgistfile1.txt)
21
+ Example: [http://overpass-ultra.us/#query=url:https%3A%2F%2Fraw.githubusercontent.com%2FMapRVA%2Fmaprva.org%2F76eb1cd1bc8c022399f578e38f89917b98c97056%2F_ultra-maps%2Fsurveillance.ultra](http://overpass-ultra.us/#query=https%3A%2F%2Fraw.githubusercontent.com%2FMapRVA%2Fmaprva.org%2F76eb1cd1bc8c022399f578e38f89917b98c97056%2F_ultra-maps%2Fsurveillance.ultra)
22
22
 
23
23
  ## `q` <small>(lz-string-compressed string)</small>
24
24
 
package/docs/yaml.md CHANGED
@@ -31,7 +31,7 @@ server: https://overpass.private.coffee/api/
31
31
 
32
32
  ## `popupTemplate`
33
33
 
34
- Customize the interactive popup with a [LiquidJS]() template. Or set it to `false` to disable the
34
+ Customize the interactive popup with a [LiquidJS](https://liquidjs.com/) template. Or set it to `false` to disable the
35
35
  interactive popup.
36
36
 
37
37
  ```
@@ -306,3 +306,25 @@ Specify which sources can be queried by mouse-click
306
306
  querySources: [ultra] # this is the default
307
307
  ---
308
308
  ```
309
+
310
+ ## `transform`
311
+
312
+ This allows you to specify javascript to mutate the query result before it is added to the map
313
+ style. Your code must export a function that accepts GeoJSON as a parameter and returns GeoJSON.
314
+ To import libraries, use a CDN like [skypack.dev](https://skypack.dev) or [esm.sh](https://esm.sh).
315
+
316
+ For example, to buffer highways by 10 meters:
317
+
318
+ ```
319
+ ---
320
+ transform: |
321
+ import { buffer } from "https://cdn.skypack.dev/@turf/buffer";
322
+
323
+ export default function(data) {
324
+ return buffer(data, 0.01);
325
+ }
326
+ ---
327
+ [bbox:{{bbox}}];
328
+ way[highway];
329
+ out geom;
330
+ ```
package/index.html CHANGED
@@ -11,13 +11,21 @@
11
11
  <title>{{ title | default: "Ultra"}}</title>
12
12
 
13
13
  <!-- Meta tags for SEO and social sharing -->
14
- {% if url %}
15
- <link rel="canonical" href="{{ url }}" />
16
- {% endif %}
14
+ <meta property="og:title" content="{{ title | default: "Ultra"}}" />
17
15
  <meta
18
16
  name="description"
19
17
  content="{{ description | default: "A web based tool for making MapLibre GL maps with data from sources such as Overpass, GeoJSON, GPX, KML, TCX, etc" }}"
20
18
  />
19
+ <meta
20
+ name="og:description"
21
+ content="{{ description | default: "A web based tool for making MapLibre GL maps with data from sources such as Overpass, GeoJSON, GPX, KML, TCX, etc" }}"
22
+ />
23
+ <meta property="og:type" content="website" />
24
+ {% if url %}
25
+ <link rel="canonical" href="{{ url }}" />
26
+ <meta property="og:url" content="{{ url }}" />
27
+ <meta property="og:image" content="{{ url }}/og:image.png" />
28
+ {% endif %}
21
29
  <meta name="robots" content="index,follow" />
22
30
 
23
31
  <script>
package/index.js CHANGED
@@ -13,7 +13,7 @@ import { UltraIDE } from "./components/ultra-ide.js";
13
13
  import { CodeEditor } from "./components/code-editor.js";
14
14
  import { NavBar } from "./components/nav-bar.js";
15
15
  import { RunButton } from "./components/run-button.js";
16
- import { DownloadButton } from "./components/download-button.js";
16
+ import { ExportModal as DownloadButton } from "./components/export-modal.js";
17
17
  import { StylePicker } from "./components/style-picker.js";
18
18
  import { ShareModal as ShareButton } from "./components/share-modal.js";
19
19
  import { HelpModal } from "./components/help-modal.js";
@@ -0,0 +1,74 @@
1
+ import { version as maplibreVersion } from "maplibre-gl/package.json";
2
+ import { version } from "../package.json";
3
+
4
+ let ultraVersion = version;
5
+ if (version === "dev") {
6
+ ultraVersion = "latest";
7
+ } else if (version.includes("-")) {
8
+ ultraVersion = version.split("-")[0];
9
+ }
10
+
11
+ export const htmlExport = (
12
+ style,
13
+ title = "Ultra Export",
14
+ description = "",
15
+ options = {},
16
+ ) => {
17
+ return `<!DOCTYPE html>
18
+ <html lang="en">
19
+ <head>
20
+ <meta charset="utf-8" />
21
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
22
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
23
+
24
+ <title>${title}</title>
25
+ <link
26
+ href="https://esm.sh/maplibre-gl@${maplibreVersion}/dist/maplibre-gl.css"
27
+ rel="stylesheet"
28
+ />
29
+
30
+ <style>
31
+ html,
32
+ body,
33
+ #map {
34
+ height: 100%;
35
+ width: 100%;
36
+ margin: 0;
37
+ padding: 0;
38
+ }
39
+ </style>
40
+ </head>
41
+ <body>
42
+ <div id="map"></div>
43
+
44
+ <script type="module">
45
+ import maplibregl from "https://esm.sh/maplibre-gl@${maplibreVersion}";
46
+
47
+ const style = ${JSON.stringify(style)};
48
+
49
+ maplibregl.setRTLTextPlugin("https://esm.sh/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.min.js", true);
50
+
51
+ import { Protocol as PMTilesProtocol} from "https://esm.sh/pmtiles";
52
+ const pmtiles = new PMTilesProtocol();
53
+ maplibregl.addProtocol("pmtiles", pmtiles.tile);
54
+
55
+ import { Protocol as FallbackGlyphProtocol } from "https://esm.sh/@trailstash/ultra@${ultraVersion}/lib/glyphFallback.js";
56
+ const fallbackGlyphsProtocol = new FallbackGlyphProtocol();
57
+ maplibregl.addProtocol(FallbackGlyphProtocol.name, fallbackGlyphsProtocol.tile);
58
+ style.glyphs = FallbackGlyphProtocol.makeURL(style.glyphs);
59
+
60
+ const options = ${JSON.stringify(options)};
61
+ var map = new maplibregl.Map({
62
+ ...options,
63
+ container: "map",
64
+ style,
65
+ });
66
+
67
+ import { handleStyleImageMissing } from "https://esm.sh/@trailstash/ultra@${ultraVersion}/lib/sprites.js";
68
+ map.on("styleimagemissing", handleStyleImageMissing);
69
+
70
+ // TODO: popup?
71
+ </script>
72
+ </body>
73
+ </html>`;
74
+ };
@@ -29,6 +29,3 @@ export const optionsFromStorage = () => {
29
29
  }
30
30
  return options;
31
31
  };
32
- export const queryFromStorage = () => {
33
- return localStorage.getItem("query");
34
- };
@@ -1,4 +1,4 @@
1
- import { fetchIfHTTPS } from "./util.js";
1
+ import { fetchIfHTTP } from "./util.js";
2
2
 
3
3
  const layers = (source) => [
4
4
  {
@@ -119,7 +119,7 @@ const geoJsonTypes = [
119
119
  "MultiPolygon",
120
120
  ];
121
121
  async function detect(query) {
122
- query = await fetchIfHTTPS(query);
122
+ query = await fetchIfHTTP(query);
123
123
  try {
124
124
  const json = JSON.parse(query);
125
125
  if (geoJsonTypes.includes(json.type)) {
@@ -1,4 +1,4 @@
1
- import { fetchIfHTTPS } from "./util.js";
1
+ import { fetchIfHTTP } from "./util.js";
2
2
  import { gpx } from "@tmcw/togeojson";
3
3
 
4
4
  const layers = (source) => [
@@ -76,7 +76,7 @@ const layers = (source) => [
76
76
  ];
77
77
 
78
78
  const detect = async (query) => {
79
- query = await fetchIfHTTPS(query);
79
+ query = await fetchIfHTTP(query);
80
80
  const doc = new window.DOMParser().parseFromString(query, "text/xml");
81
81
  if (
82
82
  !doc.querySelector("parsererror") &&
@@ -1,4 +1,4 @@
1
- import { fetchIfHTTPS } from "./util.js";
1
+ import { fetchIfHTTP } from "./util.js";
2
2
  import { kml } from "@tmcw/togeojson";
3
3
 
4
4
  const layers = (source) => [
@@ -78,7 +78,7 @@ const detect = async (query) => {
78
78
  if (query.startsWith("https://www.google.com/maps/d/")) {
79
79
  return true;
80
80
  }
81
- query = await fetchIfHTTPS(query);
81
+ query = await fetchIfHTTP(query);
82
82
  const doc = new window.DOMParser().parseFromString(query, "text/xml");
83
83
  return (
84
84
  !doc.querySelector("parsererror") &&
@@ -1,4 +1,4 @@
1
- import { fetchIfHTTPS } from "./util.js";
1
+ import { fetchIfHTTP } from "./util.js";
2
2
 
3
3
  const layers = (source) => [
4
4
  {
@@ -16,7 +16,7 @@ const detect = async (query) => {
16
16
  if (query.match(/{(x|y|z)}.*\.(png|webp|jpe?g)$/)) {
17
17
  return true;
18
18
  }
19
- query = await fetchIfHTTPS(query);
19
+ query = await fetchIfHTTP(query);
20
20
  try {
21
21
  const json = JSON.parse(query);
22
22
  return json.tilejson && IMAGE_FORMATS.has(json.formati);
@@ -1,4 +1,4 @@
1
- import { fetchIfHTTPS } from "./util.js";
1
+ import { fetchIfHTTP } from "./util.js";
2
2
  import { tcx } from "@tmcw/togeojson";
3
3
 
4
4
  const layers = (source) => [
@@ -66,7 +66,7 @@ const layers = (source) => [
66
66
  ];
67
67
 
68
68
  const detect = async (query) => {
69
- query = await fetchIfHTTPS(query);
69
+ query = await fetchIfHTTP(query);
70
70
  const doc = new window.DOMParser().parseFromString(query, "text/xml");
71
71
  if (
72
72
  !doc.querySelector("parsererror") &&
@@ -1,5 +1,5 @@
1
- export const fetchIfHTTPS = async (query) => {
2
- if (query.startsWith("https://")) {
1
+ export const fetchIfHTTP = async (query) => {
2
+ if (query.startsWith("http://") || query.startsWith("https://")) {
3
3
  try {
4
4
  const resp = await fetch(query, { cors: true });
5
5
  if (resp.ok) {
@@ -1,20 +1,17 @@
1
- import { fetchIfHTTPS } from "./util.js";
1
+ import { fetchIfHTTP } from "./util.js";
2
2
  import { PMTiles } from "pmtiles";
3
3
 
4
- // Created with https://colorbrewer2.org/#type=qualitative&scheme=Paired&n=12
4
+ // Created with https://colorbrewer2.org/?type=qualitative&scheme=Set1&n=9
5
5
  const colors = [
6
- "#8dd3c7",
7
- "#ffffb3",
8
- "#bebada",
9
- "#fb8072",
10
- "#80b1d3",
11
- "#fdb462",
12
- "#b3de69",
13
- "#fccde5",
14
- "#d9d9d9",
15
- "#bc80bd",
16
- "#ccebc5",
17
- "#ffed6f",
6
+ "#e41a1c",
7
+ "#377eb8",
8
+ "#4daf4a",
9
+ "#984ea3",
10
+ "#ff7f00",
11
+ "#ffff33",
12
+ "#a65628",
13
+ "#f781bf",
14
+ "#999999",
18
15
  ];
19
16
 
20
17
  export default {
@@ -64,6 +61,7 @@ export default {
64
61
  filter: ["==", ["geometry-type"], "LineString"],
65
62
  paint: {
66
63
  "line-color": colors[i % colors.length],
64
+ "line-width": 2,
67
65
  },
68
66
  layout: {
69
67
  "line-join": "round",
@@ -78,7 +76,7 @@ export default {
78
76
  filter: ["==", ["geometry-type"], "Point"],
79
77
  paint: {
80
78
  "circle-color": colors[i % colors.length],
81
- "circle-radius": 2,
79
+ "circle-radius": 3,
82
80
  },
83
81
  });
84
82
  }
@@ -86,10 +84,15 @@ export default {
86
84
  return layers;
87
85
  },
88
86
  detect: async (query) => {
89
- query = await fetchIfHTTPS(query);
87
+ if (query.startsWith("pmtiles://")) {
88
+ const pmtiles = new PMTiles(query.slice("pmtiles://".length));
89
+ const tileJSON = await pmtiles.getTileJson();
90
+ return tileJSON.tilejson && tileJSON.vector_layers;
91
+ }
92
+ query = await fetchIfHTTP(query);
90
93
  try {
91
- const json = JSON.parse(query);
92
- return json.tilejson && json.format === "pbf";
94
+ const tileJSON = JSON.parse(query);
95
+ return tileJSON.tilejson && tileJSON.vector_layers;
93
96
  } catch {}
94
97
  },
95
98
  };
package/lib/sandbox.js ADDED
@@ -0,0 +1,93 @@
1
+ const makeSandbox = () => {
2
+ const iframe = window.document.createElement("iframe");
3
+ const sandboxId = crypto.randomUUID();
4
+ iframe.style.display = "none";
5
+ iframe.src =
6
+ "data:text/html;base64," +
7
+ btoa(`<!DOCTYPE html>
8
+ <script type="module">
9
+ const p = "${window.location.origin}"
10
+
11
+ window.addEventListener(
12
+ "message",
13
+ async (event) => {
14
+ console.debug(event);
15
+
16
+ if (event.origin !== p) return;
17
+
18
+ let id;
19
+ try {
20
+ const msg = JSON.parse(event.data);
21
+ id = msg.id;
22
+ console.debug(msg)
23
+
24
+ const { default: transform } = await import(msg.transform);
25
+
26
+ const o = await Promise.resolve(transform(msg.data));
27
+ event.source.postMessage(JSON.stringify({id: msg.id, data: o}), p);
28
+ } catch (e) {
29
+ event.source.postMessage(JSON.stringify({id: id, error: e.toString()}), p);
30
+ }
31
+ },
32
+ false,
33
+ );
34
+
35
+ window.parent.postMessage(JSON.stringify({id: "${sandboxId}"}), p);
36
+ </script>
37
+ `);
38
+
39
+ const run = (transform, data) => {
40
+ const messageID = crypto.randomUUID();
41
+ return new Promise((resolve, reject) => {
42
+ const listener = (event) => {
43
+ console.debug(event);
44
+ try {
45
+ const msg = JSON.parse(event.data);
46
+ if (msg.id === messageID) {
47
+ window.removeEventListener("message", listener, false);
48
+ if (msg.error) {
49
+ reject(msg.error);
50
+ } else {
51
+ resolve(msg.data);
52
+ }
53
+ } else {
54
+ console.warn("unexpected msg!");
55
+ }
56
+ } catch (e) {
57
+ console.error(e);
58
+ return;
59
+ }
60
+ };
61
+ window.addEventListener("message", listener, false);
62
+ iframe.contentWindow.postMessage(
63
+ JSON.stringify({
64
+ id: messageID,
65
+ transform: `data:text/javascript;base64,${btoa(transform)}`,
66
+ data,
67
+ }),
68
+ "*",
69
+ );
70
+ });
71
+ };
72
+
73
+ return new Promise((resolve) => {
74
+ const listener = (event) => {
75
+ window.removeEventListener("message", listener, false);
76
+ console.debug(event);
77
+ try {
78
+ const msg = JSON.parse(event.data);
79
+ if (msg.id === sandboxId) {
80
+ resolve(run);
81
+ return;
82
+ }
83
+ console.warn("unexpected msg!");
84
+ } catch (e) {
85
+ console.error(e);
86
+ return;
87
+ }
88
+ };
89
+ window.addEventListener("message", listener, false);
90
+ window.document.body.appendChild(iframe);
91
+ });
92
+ };
93
+ export default makeSandbox;