2026-01-30 21:01:24 +01:00
|
|
|
import maplibregl from 'maplibre-gl';
|
|
|
|
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
|
|
|
import MaplibreGeocoder, {
|
|
|
|
|
type MaplibreGeocoderFeatureResults,
|
|
|
|
|
} from '@maplibre/maplibre-gl-geocoder';
|
|
|
|
|
import '@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css';
|
2025-10-17 23:54:45 +02:00
|
|
|
import { get, writable, type Writable } from 'svelte/store';
|
|
|
|
|
import { settings } from '$lib/logic/settings';
|
2025-10-19 16:45:12 +02:00
|
|
|
import { tick } from 'svelte';
|
2026-01-30 21:01:24 +01:00
|
|
|
import { ANCHOR_LAYER_KEY, StyleManager } from '$lib/components/map/style';
|
2025-10-17 23:54:45 +02:00
|
|
|
|
2026-01-30 21:01:24 +01:00
|
|
|
const { treeFileView, elevationProfile, bottomPanelSize, rightPanelSize, distanceUnits } = settings;
|
2025-06-21 21:07:36 +02:00
|
|
|
|
2026-01-30 21:01:24 +01:00
|
|
|
let fitBoundsOptions: maplibregl.MapOptions['fitBoundsOptions'] = {
|
2025-06-21 21:07:36 +02:00
|
|
|
maxZoom: 15,
|
|
|
|
|
linear: true,
|
|
|
|
|
easing: () => 1,
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-30 21:01:24 +01:00
|
|
|
export class MapLibreGLMap {
|
|
|
|
|
private _maptilerKey: string = '';
|
|
|
|
|
private _map: maplibregl.Map | null = null;
|
|
|
|
|
private _mapStore: Writable<maplibregl.Map | null> = writable(null);
|
|
|
|
|
private _styleManager: StyleManager | null = null;
|
|
|
|
|
private _onLoadCallbacks: ((map: maplibregl.Map) => void)[] = [];
|
2025-10-17 23:54:45 +02:00
|
|
|
private _unsubscribes: (() => void)[] = [];
|
2026-01-30 21:01:24 +01:00
|
|
|
private callOnLoadBinded: () => void = this.callOnLoad.bind(this);
|
2025-10-17 23:54:45 +02:00
|
|
|
|
2026-01-30 21:01:24 +01:00
|
|
|
subscribe(run: (value: maplibregl.Map | null) => void, invalidate?: () => void) {
|
|
|
|
|
return this._mapStore.subscribe(run, invalidate);
|
2025-10-17 23:54:45 +02:00
|
|
|
}
|
2025-06-21 21:07:36 +02:00
|
|
|
|
|
|
|
|
init(
|
2026-01-30 21:01:24 +01:00
|
|
|
maptilerKey: string,
|
2025-06-21 21:07:36 +02:00
|
|
|
language: string,
|
|
|
|
|
hash: boolean,
|
|
|
|
|
geocoder: boolean,
|
|
|
|
|
geolocate: boolean
|
|
|
|
|
) {
|
2026-01-30 21:01:24 +01:00
|
|
|
this._maptilerKey = maptilerKey;
|
|
|
|
|
this._styleManager = new StyleManager(this._mapStore, this._maptilerKey);
|
|
|
|
|
const map = new maplibregl.Map({
|
2025-06-21 21:07:36 +02:00
|
|
|
container: 'map',
|
|
|
|
|
style: {
|
|
|
|
|
version: 8,
|
2026-01-30 21:01:24 +01:00
|
|
|
projection: {
|
|
|
|
|
type: 'globe',
|
|
|
|
|
},
|
2025-06-21 21:07:36 +02:00
|
|
|
sources: {},
|
|
|
|
|
layers: [],
|
|
|
|
|
},
|
|
|
|
|
zoom: 0,
|
|
|
|
|
hash: hash,
|
|
|
|
|
boxZoom: false,
|
2026-01-30 21:01:24 +01:00
|
|
|
maxPitch: 85,
|
2025-06-21 21:07:36 +02:00
|
|
|
});
|
|
|
|
|
map.addControl(
|
2026-01-30 21:01:24 +01:00
|
|
|
new maplibregl.NavigationControl({
|
2025-06-21 21:07:36 +02:00
|
|
|
visualizePitch: true,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
if (geocoder) {
|
2026-01-30 21:01:24 +01:00
|
|
|
let geocoder = new MaplibreGeocoder(
|
|
|
|
|
{
|
|
|
|
|
forwardGeocode: async (config) => {
|
|
|
|
|
const results: MaplibreGeocoderFeatureResults = {
|
|
|
|
|
features: [],
|
|
|
|
|
type: 'FeatureCollection',
|
|
|
|
|
};
|
|
|
|
|
try {
|
|
|
|
|
const request = `https://nominatim.openstreetmap.org/search?format=json&q=${config.query}&limit=5&accept-language=${language}`;
|
|
|
|
|
const response = await fetch(request);
|
|
|
|
|
const geojson = await response.json();
|
|
|
|
|
results.features = geojson.map((result: any) => {
|
2025-06-21 21:07:36 +02:00
|
|
|
return {
|
|
|
|
|
type: 'Feature',
|
|
|
|
|
geometry: {
|
|
|
|
|
type: 'Point',
|
|
|
|
|
coordinates: [result.lon, result.lat],
|
|
|
|
|
},
|
|
|
|
|
place_name: result.display_name,
|
|
|
|
|
};
|
|
|
|
|
});
|
2026-01-30 21:01:24 +01:00
|
|
|
} catch (e) {}
|
|
|
|
|
return results;
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
maplibregl: maplibregl,
|
|
|
|
|
enableEventLogging: false,
|
|
|
|
|
collapsed: true,
|
|
|
|
|
flyTo: fitBoundsOptions,
|
|
|
|
|
language,
|
2025-06-21 21:07:36 +02:00
|
|
|
}
|
2026-01-30 21:01:24 +01:00
|
|
|
);
|
2025-06-21 21:07:36 +02:00
|
|
|
map.addControl(geocoder);
|
|
|
|
|
}
|
|
|
|
|
if (geolocate) {
|
|
|
|
|
map.addControl(
|
2026-01-30 21:01:24 +01:00
|
|
|
new maplibregl.GeolocateControl({
|
2025-06-21 21:07:36 +02:00
|
|
|
positionOptions: {
|
|
|
|
|
enableHighAccuracy: true,
|
|
|
|
|
},
|
|
|
|
|
fitBoundsOptions,
|
|
|
|
|
trackUserLocation: true,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-01-30 21:01:24 +01:00
|
|
|
const scaleControl = new maplibregl.ScaleControl({
|
2025-10-17 23:54:45 +02:00
|
|
|
unit: get(distanceUnits),
|
2025-06-21 21:07:36 +02:00
|
|
|
});
|
|
|
|
|
map.addControl(scaleControl);
|
|
|
|
|
map.on('load', () => {
|
2026-01-30 21:01:24 +01:00
|
|
|
this._map = map;
|
|
|
|
|
this._mapStore.set(map); // only set the store after the map has loaded
|
2025-06-21 21:07:36 +02:00
|
|
|
window._map = map; // entry point for extensions
|
2025-11-09 19:20:10 +01:00
|
|
|
this.resize();
|
2025-10-17 23:54:45 +02:00
|
|
|
scaleControl.setUnit(get(distanceUnits));
|
2025-06-21 21:07:36 +02:00
|
|
|
});
|
2026-01-30 21:01:24 +01:00
|
|
|
map.on('style.load', this.callOnLoadBinded);
|
2025-10-17 23:54:45 +02:00
|
|
|
|
|
|
|
|
this._unsubscribes.push(treeFileView.subscribe(() => this.resize()));
|
|
|
|
|
this._unsubscribes.push(elevationProfile.subscribe(() => this.resize()));
|
|
|
|
|
this._unsubscribes.push(bottomPanelSize.subscribe(() => this.resize()));
|
|
|
|
|
this._unsubscribes.push(rightPanelSize.subscribe(() => this.resize()));
|
|
|
|
|
this._unsubscribes.push(
|
|
|
|
|
distanceUnits.subscribe((units) => {
|
|
|
|
|
scaleControl.setUnit(units);
|
|
|
|
|
})
|
|
|
|
|
);
|
2025-06-21 21:07:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
destroy() {
|
2026-01-30 21:01:24 +01:00
|
|
|
if (this._map) {
|
|
|
|
|
this._map.remove();
|
|
|
|
|
this._mapStore.set(null);
|
2025-06-21 21:07:36 +02:00
|
|
|
}
|
2025-10-17 23:54:45 +02:00
|
|
|
this._unsubscribes.forEach((unsubscribe) => unsubscribe());
|
|
|
|
|
this._unsubscribes = [];
|
2025-06-21 21:07:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resize() {
|
2026-01-30 21:01:24 +01:00
|
|
|
if (this._map) {
|
2025-10-19 16:45:12 +02:00
|
|
|
tick().then(() => {
|
2026-01-30 21:01:24 +01:00
|
|
|
this._map?.resize();
|
2025-10-19 16:45:12 +02:00
|
|
|
});
|
2025-06-21 21:07:36 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toggle3D() {
|
2026-01-30 21:01:24 +01:00
|
|
|
if (this._map) {
|
|
|
|
|
if (this._map.getPitch() === 0) {
|
|
|
|
|
this._map.easeTo({ pitch: 70 });
|
2025-06-21 21:07:36 +02:00
|
|
|
} else {
|
2026-01-30 21:01:24 +01:00
|
|
|
this._map.easeTo({ pitch: 0 });
|
2025-06-21 21:07:36 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-16 19:16:28 +01:00
|
|
|
|
2026-01-30 21:01:24 +01:00
|
|
|
onLoad(callback: (map: maplibregl.Map) => void) {
|
|
|
|
|
if (this._map) {
|
|
|
|
|
callback(this._map);
|
|
|
|
|
} else {
|
|
|
|
|
this._onLoadCallbacks.push(callback);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
callOnLoad() {
|
|
|
|
|
if (this._map && this._map.getLayer(ANCHOR_LAYER_KEY.overlays)) {
|
|
|
|
|
this._onLoadCallbacks.forEach((callback) => callback(this._map!));
|
|
|
|
|
this._onLoadCallbacks = [];
|
|
|
|
|
this._map.off('style.load', this.callOnLoadBinded);
|
2026-01-16 19:16:28 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-06-21 21:07:36 +02:00
|
|
|
}
|
|
|
|
|
|
2026-01-30 21:01:24 +01:00
|
|
|
export const map = new MapLibreGLMap();
|