extension api v2, ref #395

This commit is contained in:
vcoppe committed 2026-11-08 17:23:22 +01:00
1 parent 7474f892ce
commit 4050420b3c
8 files changed
+989 -58

No files matched your search

@@ -1,4 +1,89 @@
import type { LayerTreeType } from '$lib/data/layers';
import type { LayerSpecification, StyleSpecification } from 'maplibre-gl';
/** Parts of a style that apply to a whole map, not to an overlay. */
const ignoredStyleProperties = ['sprite', 'glyphs', 'terrain', 'sky', 'light', 'projection'];
/** The overlays that were warned about those: a style at a URL is read again and again. */
const warnedOverlays = new Set<string>();
/**
* A copy of the style of an overlay whose sources and layers have ids of their own, so that they
* cannot clash with the ones of gpx.studio or of another overlay.
*/
export function prefixStyle(overlayId: string, style: StyleSpecification): StyleSpecification {
if (typeof style !== 'object' || style === null || !Array.isArray(style.layers)) {
throw new Error(`The style of overlay ${overlayId} must have a list of layers.`);
}
const ignored = ignoredStyleProperties.filter((property) => Object.hasOwn(style, property));
if (ignored.length > 0 && !warnedOverlays.has(overlayId)) {
warnedOverlays.add(overlayId);
console.warn(`Overlay ${overlayId} ignores these style properties: ${ignored.join(', ')}.`);
}
const prefixed = (id: string) => `${overlayId}/${id}`;
const sources = style.sources ?? {};
const layerIds = new Set<string>();
const layers = style.layers.map((layer) => {
if (!layer.id || layerIds.has(layer.id)) {
throw new Error(`Overlay ${overlayId} has a missing or repeated layer id.`);
}
layerIds.add(layer.id);
if ('source' in layer) {
if (!Object.hasOwn(sources, layer.source)) {
throw new Error(
`Layer ${layer.id} of overlay ${overlayId} uses unknown source ${layer.source}.`
);
}
return { ...layer, id: prefixed(layer.id), source: prefixed(layer.source) };
}
return { ...layer, id: prefixed(layer.id) };
}) as LayerSpecification[];
return {
...style,
sources: Object.fromEntries(
Object.entries(sources).map(([id, source]) => [prefixed(id), source])
),
layers,
};
}
/** The paint properties that make a layer transparent, by type of layer. */
const opacityProperties: Partial<Record<LayerSpecification['type'], string[]>> = {
background: ['background-opacity'],
circle: ['circle-opacity', 'circle-stroke-opacity'],
'color-relief': ['color-relief-opacity'],
fill: ['fill-opacity'],
'fill-extrusion': ['fill-extrusion-opacity'],
heatmap: ['heatmap-opacity'],
line: ['line-opacity'],
raster: ['raster-opacity'],
symbol: ['icon-opacity', 'text-opacity'],
};
/**
* The paint properties that give a layer an opacity. The opacity of an overlay applies on top of
* those of its layers, so that they stay different from one another. A value that is computed
* (an expression) cannot always be scaled: the opacity replaces it, unless it is left at 1.
*/
export function getOpacityPaint(
layer: LayerSpecification,
opacity: number
): Record<string, unknown> {
if (layer.type === 'hillshade') {
return { 'hillshade-exaggeration': opacity / 2 };
}
const paint = (layer.paint ?? {}) as Record<string, unknown>;
return Object.fromEntries(
(opacityProperties[layer.type] ?? []).map((property) => {
const value = paint[property];
if (typeof value === 'number') {
return [property, value * opacity];
}
return [property, value !== undefined && opacity === 1 ? value : opacity];
})
);
}
export function anySelectedLayer(node: LayerTreeType) {
return (