Files
gpx.studio/website/src/lib/components/map/map.ts

224 lines
7.4 KiB
TypeScript
Raw Normal View History

2025-06-21 21:07:36 +02:00
import mapboxgl from 'mapbox-gl';
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
2025-10-17 23:54:45 +02:00
import { get, writable, type Writable } from 'svelte/store';
import { settings } from '$lib/logic/settings';
const { treeFileView, elevationProfile, bottomPanelSize, rightPanelSize, distanceUnits } = settings;
2025-06-21 21:07:36 +02:00
let fitBoundsOptions: mapboxgl.MapOptions['fitBoundsOptions'] = {
maxZoom: 15,
linear: true,
easing: () => 1,
};
export class MapboxGLMap {
2025-10-17 23:54:45 +02:00
private _map: Writable<mapboxgl.Map | null> = writable(null);
2025-06-21 21:07:36 +02:00
private _onLoadCallbacks: ((map: mapboxgl.Map) => void)[] = [];
2025-10-17 23:54:45 +02:00
private _unsubscribes: (() => void)[] = [];
subscribe(run: (value: mapboxgl.Map | null) => void, invalidate?: () => void) {
return this._map.subscribe(run, invalidate);
}
2025-06-21 21:07:36 +02:00
init(
accessToken: string,
language: string,
hash: boolean,
geocoder: boolean,
geolocate: boolean
) {
const map = new mapboxgl.Map({
container: 'map',
style: {
version: 8,
sources: {},
layers: [],
imports: [
{
id: 'glyphs-and-sprite', // make Mapbox glyphs and sprite available to other styles
url: '',
data: {
version: 8,
sources: {},
layers: [],
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
sprite: `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${accessToken}`,
},
},
{
id: 'basemap',
url: '',
},
{
id: 'overlays',
url: '',
data: {
version: 8,
sources: {},
layers: [],
},
},
],
},
projection: 'globe',
zoom: 0,
hash: hash,
language,
attributionControl: false,
logoPosition: 'bottom-right',
boxZoom: false,
});
map.addControl(
new mapboxgl.AttributionControl({
compact: true,
})
);
map.addControl(
new mapboxgl.NavigationControl({
visualizePitch: true,
})
);
if (geocoder) {
let geocoder = new MapboxGeocoder({
mapboxgl: mapboxgl,
enableEventLogging: false,
collapsed: true,
flyTo: fitBoundsOptions,
language,
localGeocoder: () => [],
localGeocoderOnly: true,
externalGeocoder: (query: string) =>
fetch(
`https://nominatim.openstreetmap.org/search?format=json&q=${query}&limit=5&accept-language=${language}`
)
.then((response) => response.json())
.then((data) => {
return data.map((result: any) => {
return {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [result.lon, result.lat],
},
place_name: result.display_name,
};
});
}),
});
let onKeyDown = geocoder._onKeyDown;
geocoder._onKeyDown = (e: KeyboardEvent) => {
// Trigger search on Enter key only
if (e.key === 'Enter') {
onKeyDown.apply(geocoder, [{ target: geocoder._inputEl }]);
} else if (geocoder._typeahead.data.length > 0) {
geocoder._typeahead.clear();
}
};
map.addControl(geocoder);
}
if (geolocate) {
map.addControl(
new mapboxgl.GeolocateControl({
positionOptions: {
enableHighAccuracy: true,
},
fitBoundsOptions,
trackUserLocation: true,
showUserHeading: true,
})
);
}
const scaleControl = new mapboxgl.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('style.load', () => {
map.addSource('mapbox-dem', {
type: 'raster-dem',
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
tileSize: 512,
maxzoom: 14,
});
if (map.getPitch() > 0) {
map.setTerrain({
source: 'mapbox-dem',
exaggeration: 1,
});
}
map.setFog({
color: 'rgb(186, 210, 235)',
'high-color': 'rgb(36, 92, 223)',
'horizon-blend': 0.1,
'space-color': 'rgb(156, 240, 255)',
});
map.on('pitch', () => {
if (map.getPitch() > 0) {
map.setTerrain({
source: 'mapbox-dem',
exaggeration: 1,
});
} else {
map.setTerrain(null);
}
});
});
map.on('load', () => {
2025-10-17 23:54:45 +02:00
this._map.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-10-17 23:54:45 +02:00
scaleControl.setUnit(get(distanceUnits));
2025-06-21 21:07:36 +02:00
this._onLoadCallbacks.forEach((callback) => callback(map));
this._onLoadCallbacks = [];
});
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
}
onLoad(callback: (map: mapboxgl.Map) => void) {
2025-10-17 23:54:45 +02:00
const map = get(this._map);
if (map) {
callback(map);
2025-06-21 21:07:36 +02:00
} else {
this._onLoadCallbacks.push(callback);
}
}
destroy() {
2025-10-17 23:54:45 +02:00
const map = get(this._map);
if (map) {
map.remove();
this._map.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() {
2025-10-17 23:54:45 +02:00
const map = get(this._map);
if (map) {
map.resize();
2025-06-21 21:07:36 +02:00
}
}
toggle3D() {
2025-10-17 23:54:45 +02:00
const map = get(this._map);
if (map) {
if (map.getPitch() === 0) {
map.easeTo({ pitch: 70 });
2025-06-21 21:07:36 +02:00
} else {
2025-10-17 23:54:45 +02:00
map.easeTo({ pitch: 0 });
2025-06-21 21:07:36 +02:00
}
}
}
}
export const map = new MapboxGLMap();