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

233 lines (225 loc) 6.83 kB
import { validateStyleMin, v8 } from "@maplibre/maplibre-gl-style-spec"; import FallbackGlyphProtocol from "./glyphFallback.js"; 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) { style.glyphs = FallbackGlyphProtocol.makeURL(style.glyphs); 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); if (errors.length) { throw new Error( "Invalid style:\n\n" + errors.map(({ message }) => message).join("\n"), ); } return style; } export const handleStyleImageMissing = (ev) => { if (ev.id.startsWith("https://") || ev.id.startsWith("data:")) { ev.target.loadImage(ev.id, (error, image) => { if (error) throw error; if (!ev.target.hasImage(ev.id)) { ev.target.addImage(ev.id, image); } }); } };