svelte-maplibre-gl
Version:
Build interactive web maps effortlessly with MapLibre GL JS and Svelte
192 lines (191 loc) • 6.1 kB
JavaScript
import { setContext, getContext } from 'svelte';
const MAP_CONTEXT_KEY = Symbol('MapLibre map context');
const SOURCE_CONTEXT_KEY = Symbol('MapLibre source context');
const LAYER_CONTEXT_KEY = Symbol('MapLibre layer context');
const MARKER_CONTEXT_KEY = Symbol('MapLibre marker context');
// https://svelte.dev/docs/svelte/$state#Classes
class MapContext {
/** Map instance */
_map = $state.raw(null);
/** Callbacks to be called when the map style is loaded */
_listener = undefined;
_pending = [];
/** Names of layers dynamically added */
userLayers = new Set();
/** Names of sources dynamically added */
userSources = new Set();
/** Terrain specification of the current base style */
baseTerrain;
/** Sky specification set by user */
userTerrain;
/** Sky specification of the current base style */
baseSky;
/** Sky specification set by user */
userSky;
/** Light specification of the current base style */
baseLight;
/** Light specification set by user */
userLight;
/** Projection specification of the current base style */
baseProjection;
/** Projection specification set by user */
userProjection;
get map() {
return this._map;
}
set map(value) {
if (this._listener) {
this._map?.off('styledata', this._listener);
this._listener = undefined;
}
this._map = value;
if (this._map) {
this._listener = this._onstyledata.bind(this);
this._map.on('styledata', this._listener);
}
}
addLayer(addLayerObject, beforeId) {
if (!this.map)
throw new Error('Map is not initialized');
this.userLayers.add(addLayerObject.id);
this.waitForStyleLoaded((map) => {
map.addLayer(addLayerObject, beforeId);
});
}
removeLayer(id) {
if (!this.map)
throw new Error('Map is not initialized');
this.userLayers.delete(id);
this.waitForStyleLoaded((map) => {
map.removeLayer(id);
});
}
addSource(id, source) {
this.userSources.add(id);
this.waitForStyleLoaded((map) => {
map.addSource(id, source);
});
}
removeSource(id) {
this.userSources.delete(id);
this.waitForStyleLoaded((map) => {
map.removeSource(id);
});
}
/** Wait for the style to be loaded before calling the function */
waitForStyleLoaded(func) {
if (!this.map) {
return;
}
if (this.map.style._loaded) {
// style is already loaded
func(this.map);
}
else {
// we need to wait the style to be loaded
this._pending.push(func);
}
}
_onstyledata(ev) {
const map = ev.target;
if (map?.style._loaded) {
for (const func of this._pending) {
// call pending tasks
func(map);
}
this._pending = [];
}
}
setStyle(style) {
const { userSources: addedSources, userLayers: addedLayers } = this;
if (!style) {
this.map?.setStyle(null);
return;
}
this.map?.setStyle($state.snapshot(style), {
// Preserves user styles when the base style changes
transformStyle: (previous, next) => {
this.baseLight = next.light;
this.baseProjection = next.projection;
this.baseSky = next.sky;
this.baseTerrain = next.terrain;
if (!previous) {
return next;
}
const sources = next.sources;
for (const [key, value] of Object.entries(previous.sources)) {
if (addedSources.has(key)) {
sources[key] = value;
}
}
const userLayers = previous.layers.filter((layer) => addedLayers.has(layer.id));
const layers = [...next.layers, ...userLayers];
return {
...next,
light: this.userLight || this.baseLight,
projection: this.userProjection || this.baseProjection,
sky: this.userSky || this.baseSky,
terrain: this.userTerrain || this.baseTerrain,
sources,
layers
};
}
});
}
}
export function prepareMapContext() {
const mapCtx = new MapContext();
setContext(MAP_CONTEXT_KEY, mapCtx);
return mapCtx;
}
export function getMapContext() {
const mapCtx = getContext(MAP_CONTEXT_KEY);
if (!mapCtx)
throw new Error('Map context not found');
return mapCtx;
}
// https://svelte.dev/docs/svelte/$state#Classes
class SourceContext {
/** sourceId */
id = $state('');
}
export function prepareSourceContext() {
const sourceCtx = new SourceContext();
setContext(SOURCE_CONTEXT_KEY, sourceCtx);
return sourceCtx;
}
export function getSourceContext() {
const sourceCtx = getContext(SOURCE_CONTEXT_KEY);
if (!sourceCtx || !sourceCtx.id)
throw new Error('Source context not found');
return sourceCtx;
}
class LayerContext {
id = $state('');
}
export function prepareLayerContext() {
const layerCtx = new LayerContext();
setContext(LAYER_CONTEXT_KEY, layerCtx);
return layerCtx;
}
export function getLayerContext() {
const layerCtx = getContext(LAYER_CONTEXT_KEY);
if (!layerCtx || !layerCtx.id)
throw new Error('Layer context not found');
return layerCtx;
}
class MarkerContext {
marker = $state.raw(null);
}
export function prepareMarkerContext() {
const markerCtx = new MarkerContext();
setContext(MARKER_CONTEXT_KEY, markerCtx);
return markerCtx;
}
export function getMarkerContext() {
const markerCtx = getContext(MARKER_CONTEXT_KEY);
if (!markerCtx) {
return null;
}
return markerCtx;
}