2026-01-30 21:01:24 +01:00
|
|
|
import { settings } from '$lib/logic/settings';
|
|
|
|
|
import { get, type Writable } from 'svelte/store';
|
|
|
|
|
import {
|
|
|
|
|
basemaps,
|
|
|
|
|
defaultBasemap,
|
|
|
|
|
maptilerKeyPlaceHolder,
|
|
|
|
|
overlays,
|
|
|
|
|
terrainSources,
|
|
|
|
|
} from '$lib/assets/layers';
|
2026-02-01 18:45:40 +01:00
|
|
|
import { getLayers } from '$lib/components/map/layer-control/utils';
|
2026-01-30 21:01:24 +01:00
|
|
|
import { i18n } from '$lib/i18n.svelte';
|
|
|
|
|
|
|
|
|
|
const { currentBasemap, currentOverlays, customLayers, opacities, terrainSource } = settings;
|
|
|
|
|
|
|
|
|
|
const emptySource: maplibregl.GeoJSONSourceSpecification = {
|
|
|
|
|
type: 'geojson',
|
|
|
|
|
data: {
|
|
|
|
|
type: 'FeatureCollection',
|
|
|
|
|
features: [],
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
export const ANCHOR_LAYER_KEY = {
|
|
|
|
|
overlays: 'overlays-end',
|
|
|
|
|
mapillary: 'mapillary-end',
|
|
|
|
|
tracks: 'tracks-end',
|
|
|
|
|
directionMarkers: 'direction-markers-end',
|
|
|
|
|
distanceMarkers: 'distance-markers-end',
|
2026-02-01 17:18:17 +01:00
|
|
|
startEndMarkers: 'start-end-markers-end',
|
2026-01-30 21:01:24 +01:00
|
|
|
interactions: 'interactions-end',
|
|
|
|
|
overpass: 'overpass-end',
|
|
|
|
|
waypoints: 'waypoints-end',
|
2026-02-14 14:35:35 +01:00
|
|
|
routingControls: 'routing-controls-end',
|
2026-01-30 21:01:24 +01:00
|
|
|
};
|
|
|
|
|
const anchorLayers: maplibregl.LayerSpecification[] = Object.values(ANCHOR_LAYER_KEY).map((id) => ({
|
|
|
|
|
id: id,
|
|
|
|
|
type: 'symbol',
|
|
|
|
|
source: 'empty-source',
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
export class StyleManager {
|
|
|
|
|
private _map: Writable<maplibregl.Map | null>;
|
|
|
|
|
private _maptilerKey: string;
|
|
|
|
|
private _pastOverlays: Set<string> = new Set();
|
|
|
|
|
|
|
|
|
|
constructor(map: Writable<maplibregl.Map | null>, maptilerKey: string) {
|
|
|
|
|
this._map = map;
|
|
|
|
|
this._maptilerKey = maptilerKey;
|
|
|
|
|
this._map.subscribe((map_) => {
|
|
|
|
|
if (map_) {
|
2026-01-31 12:57:08 +01:00
|
|
|
this.updateBasemap();
|
2026-01-30 21:01:24 +01:00
|
|
|
map_.on('style.load', () => this.updateOverlays());
|
|
|
|
|
map_.on('pitch', () => this.updateTerrain());
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-01-31 12:57:08 +01:00
|
|
|
currentBasemap.subscribe(() => this.updateBasemap());
|
2026-01-30 21:01:24 +01:00
|
|
|
currentOverlays.subscribe(() => this.updateOverlays());
|
|
|
|
|
opacities.subscribe(() => this.updateOverlays());
|
|
|
|
|
terrainSource.subscribe(() => this.updateTerrain());
|
2026-02-01 18:45:40 +01:00
|
|
|
customLayers.subscribe(() => this.updateBasemap());
|
2026-01-30 21:01:24 +01:00
|
|
|
}
|
|
|
|
|
|
2026-01-31 12:57:08 +01:00
|
|
|
updateBasemap() {
|
2026-01-30 21:01:24 +01:00
|
|
|
const map_ = get(this._map);
|
|
|
|
|
if (!map_) return;
|
2026-01-31 12:57:08 +01:00
|
|
|
this.buildStyle().then((style) => map_.setStyle(style));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async buildStyle(): Promise<maplibregl.StyleSpecification> {
|
|
|
|
|
const custom = get(customLayers);
|
|
|
|
|
|
|
|
|
|
const style: maplibregl.StyleSpecification = {
|
|
|
|
|
version: 8,
|
|
|
|
|
projection: {
|
|
|
|
|
type: 'globe',
|
|
|
|
|
},
|
|
|
|
|
sources: {
|
|
|
|
|
'empty-source': emptySource,
|
|
|
|
|
},
|
|
|
|
|
layers: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let basemap = get(currentBasemap);
|
|
|
|
|
const basemapInfo = basemaps[basemap] ?? custom[basemap]?.value ?? basemaps[defaultBasemap];
|
|
|
|
|
const basemapStyle = await this.get(basemapInfo);
|
|
|
|
|
|
|
|
|
|
this.merge(style, basemapStyle);
|
|
|
|
|
|
2026-03-28 12:09:31 +01:00
|
|
|
if (this._maptilerKey !== '') {
|
|
|
|
|
const terrain = this.getCurrentTerrain();
|
|
|
|
|
style.sources[terrain.source] = terrainSources[terrain.source];
|
|
|
|
|
style.terrain = terrain.exaggeration > 0 ? terrain : undefined;
|
|
|
|
|
}
|
2026-01-31 12:57:08 +01:00
|
|
|
|
|
|
|
|
style.layers.push(...anchorLayers);
|
|
|
|
|
|
|
|
|
|
return style;
|
2026-01-30 21:01:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateOverlays() {
|
|
|
|
|
const map_ = get(this._map);
|
|
|
|
|
if (!map_) return;
|
|
|
|
|
if (!map_.getSource('empty-source')) return;
|
|
|
|
|
|
|
|
|
|
const custom = get(customLayers);
|
|
|
|
|
const overlayOpacities = get(opacities);
|
|
|
|
|
try {
|
|
|
|
|
const layers = getLayers(get(currentOverlays) ?? {});
|
|
|
|
|
for (let overlay in layers) {
|
|
|
|
|
if (!layers[overlay]) {
|
|
|
|
|
if (this._pastOverlays.has(overlay)) {
|
|
|
|
|
const overlayInfo = custom[overlay]?.value ?? overlays[overlay];
|
|
|
|
|
const overlayStyle = await this.get(overlayInfo);
|
|
|
|
|
for (let layer of overlayStyle.layers ?? []) {
|
|
|
|
|
if (map_.getLayer(layer.id)) {
|
|
|
|
|
map_.removeLayer(layer.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this._pastOverlays.delete(overlay);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const overlayInfo = custom[overlay]?.value ?? overlays[overlay];
|
|
|
|
|
const overlayStyle = await this.get(overlayInfo);
|
|
|
|
|
const opacity = overlayOpacities[overlay];
|
|
|
|
|
|
|
|
|
|
for (let sourceId in overlayStyle.sources) {
|
|
|
|
|
if (!map_.getSource(sourceId)) {
|
|
|
|
|
map_.addSource(sourceId, overlayStyle.sources[sourceId]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let layer of overlayStyle.layers ?? []) {
|
|
|
|
|
if (!map_.getLayer(layer.id)) {
|
|
|
|
|
if (opacity !== undefined) {
|
|
|
|
|
if (layer.type === 'raster') {
|
|
|
|
|
if (!layer.paint) {
|
|
|
|
|
layer.paint = {};
|
|
|
|
|
}
|
|
|
|
|
layer.paint['raster-opacity'] = opacity;
|
|
|
|
|
} else if (layer.type === 'hillshade') {
|
|
|
|
|
if (!layer.paint) {
|
|
|
|
|
layer.paint = {};
|
|
|
|
|
}
|
|
|
|
|
layer.paint['hillshade-exaggeration'] = opacity / 2;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
map_.addLayer(layer, ANCHOR_LAYER_KEY.overlays);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._pastOverlays.add(overlay);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateTerrain() {
|
2026-03-28 12:09:31 +01:00
|
|
|
if (this._maptilerKey === '') return;
|
2026-01-30 21:01:24 +01:00
|
|
|
const map_ = get(this._map);
|
|
|
|
|
if (!map_) return;
|
|
|
|
|
|
|
|
|
|
const mapTerrain = map_.getTerrain();
|
|
|
|
|
const terrain = this.getCurrentTerrain();
|
|
|
|
|
if (JSON.stringify(mapTerrain) !== JSON.stringify(terrain)) {
|
2026-01-31 12:57:08 +01:00
|
|
|
if (terrain.exaggeration > 0) {
|
2026-02-01 15:57:18 +01:00
|
|
|
if (!map_.getSource(terrain.source)) {
|
|
|
|
|
map_.addSource(terrain.source, terrainSources[terrain.source]);
|
|
|
|
|
}
|
2026-01-31 12:57:08 +01:00
|
|
|
map_.setTerrain(terrain);
|
|
|
|
|
} else {
|
|
|
|
|
map_.setTerrain(null);
|
|
|
|
|
}
|
2026-01-30 21:01:24 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async get(
|
|
|
|
|
styleInfo: maplibregl.StyleSpecification | string
|
|
|
|
|
): Promise<maplibregl.StyleSpecification> {
|
|
|
|
|
if (typeof styleInfo === 'string') {
|
|
|
|
|
let styleUrl = styleInfo as string;
|
|
|
|
|
if (styleUrl.includes(maptilerKeyPlaceHolder)) {
|
|
|
|
|
styleUrl = styleUrl.replace(maptilerKeyPlaceHolder, this._maptilerKey);
|
|
|
|
|
}
|
|
|
|
|
const response = await fetch(styleUrl, { cache: 'force-cache' });
|
|
|
|
|
const style = await response.json();
|
|
|
|
|
return style;
|
|
|
|
|
} else {
|
|
|
|
|
return styleInfo;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
merge(style: maplibregl.StyleSpecification, other: maplibregl.StyleSpecification) {
|
|
|
|
|
style.sources = { ...style.sources, ...other.sources };
|
|
|
|
|
for (let layer of other.layers ?? []) {
|
|
|
|
|
if (layer.type === 'symbol' && layer.layout && layer.layout['text-field']) {
|
|
|
|
|
const textField = layer.layout['text-field'];
|
|
|
|
|
if (
|
|
|
|
|
Array.isArray(textField) &&
|
|
|
|
|
textField.length >= 2 &&
|
|
|
|
|
textField[0] === 'coalesce' &&
|
|
|
|
|
Array.isArray(textField[1]) &&
|
|
|
|
|
textField[1][0] === 'get' &&
|
|
|
|
|
typeof textField[1][1] === 'string' &&
|
|
|
|
|
textField[1][1].startsWith('name')
|
|
|
|
|
) {
|
|
|
|
|
layer.layout['text-field'] = [
|
|
|
|
|
'coalesce',
|
|
|
|
|
['get', `name:${i18n.lang}`],
|
|
|
|
|
['get', 'name'],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
style.layers.push(layer);
|
|
|
|
|
}
|
|
|
|
|
if (other.sprite && !style.sprite) {
|
|
|
|
|
style.sprite = other.sprite;
|
|
|
|
|
}
|
|
|
|
|
if (other.glyphs && !style.glyphs) {
|
|
|
|
|
style.glyphs = other.glyphs;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getCurrentTerrain() {
|
|
|
|
|
const terrain = get(terrainSource);
|
|
|
|
|
const source = terrainSources[terrain];
|
|
|
|
|
if (source.url && source.url.includes(maptilerKeyPlaceHolder)) {
|
|
|
|
|
source.url = source.url.replace(maptilerKeyPlaceHolder, this._maptilerKey);
|
|
|
|
|
}
|
|
|
|
|
const map_ = get(this._map);
|
|
|
|
|
return {
|
|
|
|
|
source: terrain,
|
|
|
|
|
exaggeration: !map_ || map_.getPitch() === 0 ? 0 : 1,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|