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

200 lines (194 loc) 5.84 kB
import osm2geojson from "osm2geojson-ultra"; export const popupTemplate = ` {%- if type and id %} <h2> {{ type }} <a href="https://openstreetmap.org/{{ type }}/{{ id }}" target="_blank">{{ id }}</a> <a href="https://openstreetmap.org/edit?{{ type }}={{ id }}" target="_blank">✏️</a> </h2> {%- endif %} {%- if tags.size > 0 %} <h3>Tags</h3> {%- for tag in tags %} {%- if tag[0] contains "website" %} <code>{{ tag[0] }} = <a href="{{ tag[1] }}" target="_blank">{{ tag[1] }}</a></code> {%- elsif tag[0] contains "wikidata" %} <code>{{ tag[0] }} = <a href="https://wikidata.org/wiki/{{ tag[1] }}" target="_blank">{{ tag[1] }}</a></code> {%- elsif tag[0] contains "wikipedia" %} {% assign lang = tag[1] | split: ":" | first %} <code>{{ tag[0] }} = <a href="https://{{ lang }}.wikipedia.org/wiki/{{ tag[1] | replace_first: lang, "" | replace_first: ":", "" }}" target="_blank">{{ tag[1] }}</a></code> {%- else %} <code>{{ tag[0] }} = {{ tag[1] }}</code> {%- endif %} <br> {%- endfor %} {%- endif %} {%- if meta %} <h3>Meta</h3> {%- for tag in meta %} {%- if tag[0] == "changeset" %} <code>{{ tag[0] }} = <a href="https://openstreetmap.org/changeset/{{ tag[1] }}" target="_blank">{{ tag[1] }}</a></code> {%- elsif tag[0] == "user" %} <code>{{ tag[0] }} = <a href="https://openstreetmap.org/user/{{ tag[1] }}" target="_blank">{{ tag[1] }}</a></code> {%- else %} <code>{{ tag[0] }} = {{ tag[1] }}</code> {%- endif %} <br> {%- endfor %} {%- endif %} {%- if coordinates %} <h3>Coordinates</h3> <a href="geo:{{coordinates[1]}},{{coordinates[0]}}">{{coordinates[1] | round: 6 }} / {{coordinates[0] | round: 6 }}</a> <small>(lat/lon)</small> {%- endif %} `; export const popupContextBuilder = ({ properties, geometry }) => { const templateContext = { id: properties["@id"], type: properties["@type"], tags: Object.fromEntries( Object.entries(properties).filter(([k]) => !k.startsWith("@")), ), }; const meta = Object.fromEntries( Object.entries(properties).filter( ([k]) => k.startsWith("@") && !["@id", "@type"].includes(k), ), ); if (Object.keys(meta).length > 0) { templateContext.meta = meta; } delete templateContext.tags["@type"]; delete templateContext.tags["@id"]; delete templateContext.tags["@meta"]; if (properties["@type"] === "node") { templateContext.coordinates = geometry.coordinates; } return templateContext; }; export const layers = (source) => [ { id: `${source}-polygons`, type: "fill", source, filter: ["all", ["==", ["geometry-type"], "Polygon"]], paint: { "fill-color": "rgba(255, 204, 0, .5)", }, }, { id: `${source}-polygons-stroke`, type: "line", source, filter: ["all", ["==", ["geometry-type"], "Polygon"]], layout: { "line-join": "round", "line-cap": "round" }, paint: { "line-width": 2, "line-color": "rgba(0, 51, 255, 0.6)" }, }, { id: `${source}-lines`, type: "line", source, filter: ["all", ["==", ["geometry-type"], "LineString"]], paint: { "line-width": 5, "line-color": "rgba(0, 51, 255, 0.6)", }, layout: { "line-join": "round", "line-cap": "round" }, }, { id: `${source}-points`, type: "circle", source, filter: ["all", ["==", ["geometry-type"], "Point"]], paint: { "circle-stroke-width": 2, "circle-stroke-color": "rgba(0, 51, 255, 0.6)", "circle-color": "rgba(255, 204, 0, 0.6)", }, }, ]; async function osmxmlSource(query, controller) { let text = query; const doc = new window.DOMParser().parseFromString(text, "text/xml"); if (doc.querySelector("parsererror")) { const resp = await fetch(query, { signal: controller.signal }); text = await resp.text(); } const data = osm2geojson(text, { completeFeature: true, renderTagged: true, excludeWay: false, }); return { type: "geojson", data, generateId: true }; } async function osmjsonSource(query, controller) { let json; try { json = JSON.parse(query); } catch { const resp = await fetch(query, { signal: controller.signal }); json = await resp.json(); } const data = osm2geojson(json, { completeFeature: true, renderTagged: true, excludeWay: false, }); return { type: "geojson", data, generateId: true, }; } // OSM XML & JSON are detected in 2 ways: // URLs - they look like /api/0.6 URLs with node, way/full, or relation/full // Local XML document with a top-level `osm` node // Local JSON document with a top-level `version` key with a value of `0.6` const osmApiRegex = /^\/api\/0.6\/(node\/(\d+)|way\/(\d+)\/full|relation\/(\d+)\/full)(\.json)?/; const detectXML = (query) => { const doc = new window.DOMParser().parseFromString(query, "text/xml"); if ( !doc.querySelector("parsererror") && Array.from(doc.childNodes).some((x) => x.nodeName === "osm") ) { return query; } try { const url = new URL(query); const match = url.pathname.match(osmApiRegex); if (match && !match.slice(-1)[0]) { return query; } } catch {} }; const detectJSON = (query) => { try { const json = JSON.parse(query); if (json.version === "0.6") { return query; } } catch {} try { const url = new URL(query); const match = url.pathname.match(osmApiRegex); if (match && match.slice(-1)[0]) { return query; } } catch {} }; export const osmxml = { source: osmxmlSource, fitBounds: true, layers, popupTemplate, popupContextBuilder, detect: detectXML, }; export const osmjson = { source: osmjsonSource, fitBounds: true, layers, popupTemplate, popupContextBuilder, detect: detectJSON, };