@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
JavaScript
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,
};