@trailstash/ultra
Version:
A web based tool for making MapLibre GL maps with data from sources such as Overpass, GeoJSON, GPX, KML, TCX, etc
222 lines (215 loc) • 6.5 kB
JavaScript
import { validateStyleMin, v8 } from "@maplibre/maplibre-gl-style-spec";
const paintKeys = new Set(
Object.entries(v8)
.filter(([k]) => k.startsWith("paint_"))
.map(([, v]) => Object.keys(v))
.reduce((a, c) => [...a, ...c], []),
);
const layoutKeys = new Set(
Object.entries(v8)
.filter(([k]) => k.startsWith("layout_"))
.map(([, v]) => Object.keys(v))
.reduce((a, c) => [...a, ...c], []),
);
export function setBaseStyle(style, baseStyle, overwrite = false) {
if (!baseStyle) {
return style;
}
if (typeof style === "string") {
return overwrite ? baseStyle : style;
} else if (overwrite) {
return {
...style,
extends: baseStyle,
};
} else {
return {
extends: baseStyle,
...style,
};
}
}
export async function getStyle(style, source) {
if (typeof style === "string") {
// Style is just a string, fetch it, parse it, and add ultra source & layers if defined
const resp = await fetch(style);
const styleJSON = await resp.json();
return ensureSourceAndLayers(styleJSON, source);
} else if (style && !style.version) {
// extended style! Merge layers & sources
let parentStyle;
if (typeof style.extends === "string") {
const resp = await fetch(style.extends);
parentStyle = await resp.json();
} else {
parentStyle = style.extends;
}
const layers = [...parentStyle.layers];
for (const i in style.layers) {
const { beforeLayerId, ...layer } = style.layers[i];
// If the layer defines the ID, see if it belongs to an existing layer
const curLayerIdx = layers.findIndex(({ id }) => id === layer.id);
let layerIdx = -1;
let newLayer = movePaintAndLayoutKeys(ensureLayerProps({ ...layer }, i));
// If the layer exists, merge with defined layer, remove from layers, and default to
// reinserting it at the same index
if (curLayerIdx >= 0) {
newLayer = movePaintAndLayoutKeys({
...layers[curLayerIdx],
...layer,
});
layerIdx = curLayerIdx;
layers.splice(curLayerIdx, 1);
}
// find insertion point if beforeLayerId is specified.
// If insertion point is not found, layer is appended instead.
// an intetional side-effect of this is that you can specify an non-existing layer to move an
// existing layer from a style to the top
if (beforeLayerId) {
layerIdx = layers.findIndex(({ id }) => id === beforeLayerId);
}
if (newLayer.source === "ultra" && !source) {
continue;
}
if (layerIdx >= 0) {
layers.splice(layerIdx, 0, newLayer);
} else {
layers.push(newLayer);
}
}
if (!parentStyle.sprite) {
parentStyle.sprite = [];
} else if (typeof parentStyle.sprite === "string") {
parentStyle.sprite = [{ id: "default", url: parentStyle.sprite }];
}
if (!style.sprite) {
style.sprite = [];
} else if (typeof style.sprite === "string") {
style.sprite = [{ id: "default", url: style.sprite }];
}
const spritesheets = style.sprite.map(({ id }) => id);
const extendedStyle = {
...parentStyle,
...style,
sources: { ...parentStyle.sources, ...(style.sources || {}) },
sprite: [
...parentStyle.sprite.filter(({ id }) => !spritesheets.includes(id)),
...style.sprite,
],
layers,
};
return ensureSourceAndLayers(extendedStyle, source);
} else {
// "complete" style, process layers & add ultra source & layers if defined
return ensureSourceAndLayers(
{
...style,
layers: style.layers
?.map(ensureLayerProps)
.filter((l) => l.source !== "ultra" || source)
.map(movePaintAndLayoutKeys),
},
source,
);
}
}
function movePaintAndLayoutKeys(layer) {
for (const [key, value] of Object.entries(layer)) {
if (paintKeys.has(key)) {
if (!layer.paint) {
layer.paint = {};
}
layer.paint[key] = value;
delete layer[key];
}
if (layoutKeys.has(key)) {
if (!layer.layout) {
layer.layout = {};
}
layer.layout[key] = value;
delete layer[key];
}
}
return layer;
}
// ensureLayerProps generates a layer ID if one is not set and sets the source to `ultra` if
// not set
function ensureLayerProps(layer, i) {
if (!layer.id) {
layer.id = `ultra-${i}`;
}
if (!layer.source && layer.type !== "background") {
layer.source = "ultra";
}
return layer;
}
export function ensureSourceAndLayers(style, source) {
if (!style.sources) {
style.sources = {};
}
if (!style.sprite) {
style.sprite = [];
} else if (typeof style.sprite === "string") {
style.sprite = [{ id: "default", url: style.sprite }];
}
const spritesheets = style.sprite.map(({ id }) => id);
//if (!spritesheets.includes("emoji")) {
// style.sprite.push({
// id: "emoji",
// url: new URL("sprites/emoji", window.location).toString(),
// });
//}
//if (!spritesheets.includes("maki")) {
// style.sprite.push({
// id: "maki",
// url: new URL("sprites/maki", window.location).toString(),
// });
//}
//if (!spritesheets.includes("temaki")) {
// style.sprite.push({
// id: "temaki",
// url: new URL("sprites/temaki", window.location).toString(),
// });
//}
if (source) {
let foundAttribution = false;
for (const sourceName in style.sources) {
if (
style.sources[sourceName].attribution
?.toLowerCase()
.includes("openstreetmap") ||
style.sources[sourceName].url?.startsWith("https://tile.ourmap.us")
) {
foundAttribution = true;
}
}
style.sources.ultra = source.source;
if (foundAttribution && style.sources.ultra.attribution) {
style.sources.ultra.attribution = "";
}
}
if (!style.layers) {
style.layers = [];
}
let hasUltraLayer = false;
for (const layer of style.layers) {
if (layer.source === "ultra") {
hasUltraLayer = true;
break;
}
}
if (source && source.layers && !hasUltraLayer) {
for (const layer of source.layers) {
style.layers.push(layer);
}
}
const errors = validateStyleMin(style, v8).filter(
({ severity }) => severity != "warning",
);
if (errors.length) {
throw new Error(
"Invalid style:\n\n" + errors.map(({ message }) => message).join("\n"),
);
}
return style;
}