mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-04-08 04:40:21 +00:00
switch to maplibre, but laggy
This commit is contained in:
@@ -1,30 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||
import { page } from '$app/state';
|
||||
import { map } from '$lib/components/map/map';
|
||||
import { PUBLIC_MAPTILER_KEY } from '$env/static/public';
|
||||
|
||||
let {
|
||||
accessToken = PUBLIC_MAPBOX_TOKEN,
|
||||
maptilerKey = PUBLIC_MAPTILER_KEY,
|
||||
geolocate = true,
|
||||
geocoder = true,
|
||||
hash = true,
|
||||
class: className = '',
|
||||
}: {
|
||||
accessToken?: string;
|
||||
maptilerKey?: string;
|
||||
geolocate?: boolean;
|
||||
geocoder?: boolean;
|
||||
hash?: boolean;
|
||||
class?: string;
|
||||
} = $props();
|
||||
|
||||
mapboxgl.accessToken = accessToken;
|
||||
|
||||
let webgl2Supported = $state(true);
|
||||
let embeddedApp = $state(false);
|
||||
|
||||
@@ -48,7 +43,7 @@
|
||||
language = 'en';
|
||||
}
|
||||
|
||||
map.init(PUBLIC_MAPBOX_TOKEN, language, hash, geocoder, geolocate);
|
||||
map.init(maptilerKey, language, hash, geocoder, geolocate);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -81,21 +76,21 @@
|
||||
<style lang="postcss">
|
||||
@reference "../../../app.css";
|
||||
|
||||
div :global(.mapboxgl-map) {
|
||||
div :global(.maplibregl-map) {
|
||||
@apply font-sans;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-ctrl-top-right > .mapboxgl-ctrl) {
|
||||
div :global(.maplibregl-ctrl-top-right > .maplibregl-ctrl) {
|
||||
@apply shadow-md;
|
||||
@apply bg-background;
|
||||
@apply text-foreground;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-ctrl-icon) {
|
||||
div :global(.maplibregl-ctrl-icon) {
|
||||
@apply dark:brightness-[4.7];
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-ctrl-geocoder) {
|
||||
div :global(.maplibregl-ctrl-geocoder) {
|
||||
@apply flex;
|
||||
@apply flex-row;
|
||||
@apply w-fit;
|
||||
@@ -110,27 +105,27 @@
|
||||
@apply text-foreground;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-ctrl-geocoder .suggestions > li > a) {
|
||||
div :global(.maplibregl-ctrl-geocoder .suggestions > li > a) {
|
||||
@apply text-foreground;
|
||||
@apply hover:text-accent-foreground;
|
||||
@apply hover:bg-accent;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-ctrl-geocoder .suggestions > .active > a) {
|
||||
div :global(.maplibregl-ctrl-geocoder .suggestions > .active > a) {
|
||||
@apply bg-background;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-ctrl-geocoder--button) {
|
||||
div :global(.maplibregl-ctrl-geocoder--button) {
|
||||
@apply bg-transparent;
|
||||
@apply hover:bg-transparent;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-ctrl-geocoder--icon) {
|
||||
div :global(.maplibregl-ctrl-geocoder--icon) {
|
||||
@apply fill-foreground;
|
||||
@apply hover:fill-accent-foreground;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-ctrl-geocoder--icon-search) {
|
||||
div :global(.maplibregl-ctrl-geocoder--icon-search) {
|
||||
@apply relative;
|
||||
@apply top-0;
|
||||
@apply left-0;
|
||||
@@ -138,7 +133,7 @@
|
||||
@apply w-[29px];
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-ctrl-geocoder--input) {
|
||||
div :global(.maplibregl-ctrl-geocoder--input) {
|
||||
@apply relative;
|
||||
@apply w-64;
|
||||
@apply py-0;
|
||||
@@ -149,12 +144,12 @@
|
||||
@apply text-foreground;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-ctrl-geocoder--collapsed .mapboxgl-ctrl-geocoder--input) {
|
||||
div :global(.maplibregl-ctrl-geocoder--collapsed .maplibregl-ctrl-geocoder--input) {
|
||||
@apply w-0;
|
||||
@apply p-0;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-ctrl-top-right) {
|
||||
div :global(.maplibregl-ctrl-top-right) {
|
||||
@apply z-40;
|
||||
@apply flex;
|
||||
@apply flex-col;
|
||||
@@ -163,77 +158,76 @@
|
||||
@apply overflow-hidden;
|
||||
}
|
||||
|
||||
.horizontal :global(.mapboxgl-ctrl-bottom-left) {
|
||||
.horizontal :global(.maplibregl-ctrl-bottom-left) {
|
||||
@apply bottom-[42px];
|
||||
}
|
||||
|
||||
.horizontal :global(.mapboxgl-ctrl-bottom-right) {
|
||||
.horizontal :global(.maplibregl-ctrl-bottom-right) {
|
||||
@apply bottom-[42px];
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-ctrl-attrib) {
|
||||
div :global(.maplibregl-ctrl-attrib) {
|
||||
@apply dark:bg-transparent;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-compact-show.mapboxgl-ctrl-attrib) {
|
||||
div :global(.maplibregl-compact-show.maplibregl-ctrl-attrib) {
|
||||
@apply dark:bg-background;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-ctrl-attrib-button) {
|
||||
div :global(.maplibregl-ctrl-attrib-button) {
|
||||
@apply dark:bg-foreground;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-compact-show .mapboxgl-ctrl-attrib-button) {
|
||||
div :global(.maplibregl-compact-show .maplibregl-ctrl-attrib-button) {
|
||||
@apply dark:bg-foreground;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-ctrl-attrib a) {
|
||||
div :global(.maplibregl-ctrl-attrib a) {
|
||||
@apply text-foreground;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-popup) {
|
||||
@apply w-fit;
|
||||
div :global(.maplibregl-popup) {
|
||||
@apply z-50;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-popup-content) {
|
||||
div :global(.maplibregl-popup-content) {
|
||||
@apply p-0;
|
||||
@apply bg-transparent;
|
||||
@apply shadow-none;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-popup-anchor-top .mapboxgl-popup-tip) {
|
||||
div :global(.maplibregl-popup-anchor-top .maplibregl-popup-tip) {
|
||||
@apply border-b-background;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip) {
|
||||
div :global(.maplibregl-popup-anchor-top-left .maplibregl-popup-tip) {
|
||||
@apply border-b-background;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip) {
|
||||
div :global(.maplibregl-popup-anchor-top-right .maplibregl-popup-tip) {
|
||||
@apply border-b-background;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip) {
|
||||
div :global(.maplibregl-popup-anchor-bottom .maplibregl-popup-tip) {
|
||||
@apply border-t-background;
|
||||
@apply drop-shadow-md;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip) {
|
||||
div :global(.maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip) {
|
||||
@apply border-t-background;
|
||||
@apply drop-shadow-md;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip) {
|
||||
div :global(.maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip) {
|
||||
@apply border-t-background;
|
||||
@apply drop-shadow-md;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-popup-anchor-left .mapboxgl-popup-tip) {
|
||||
div :global(.maplibregl-popup-anchor-left .maplibregl-popup-tip) {
|
||||
@apply border-r-background;
|
||||
}
|
||||
|
||||
div :global(.mapboxgl-popup-anchor-right .mapboxgl-popup-tip) {
|
||||
div :global(.maplibregl-popup-anchor-right .maplibregl-popup-tip) {
|
||||
@apply border-l-background;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
let control: CustomControl | null = null;
|
||||
|
||||
onMount(() => {
|
||||
map.onLoad((map: mapboxgl.Map) => {
|
||||
map.onLoad((map: maplibregl.Map) => {
|
||||
if (position.includes('right')) container.classList.add('float-right');
|
||||
else container.classList.add('float-left');
|
||||
container.classList.remove('hidden');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Map, type IControl } from 'mapbox-gl';
|
||||
import { type Map, type IControl } from 'maplibre-gl';
|
||||
|
||||
export default class CustomControl implements IControl {
|
||||
_map: Map | undefined;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
|
||||
import { DistanceMarkers } from '$lib/components/map/gpx-layer/distance-markers';
|
||||
import { StartEndMarkers } from '$lib/components/map/gpx-layer/start-end-markers';
|
||||
@@ -9,13 +9,10 @@
|
||||
let distanceMarkers: DistanceMarkers;
|
||||
let startEndMarkers: StartEndMarkers;
|
||||
|
||||
onMount(() => {
|
||||
map.onLoad((map_) => {
|
||||
gpxLayers.init();
|
||||
startEndMarkers = new StartEndMarkers();
|
||||
distanceMarkers = new DistanceMarkers();
|
||||
});
|
||||
|
||||
map.onLoad((map_) => {
|
||||
createPopups(map_);
|
||||
});
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="justify-start"
|
||||
href={`https://www.openstreetmap.org/edit?#map=${(($map?.getZoom() ?? 17) + 1).toFixed(0)}/${trackpoint.item.getLatitude().toFixed(5)}/${trackpoint.item.getLongitude().toFixed(5)}`}
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { gpxStatistics } from '$lib/logic/statistics';
|
||||
import { getConvertedDistanceToKilometers } from '$lib/units';
|
||||
import type { GeoJSONSource } from 'mapbox-gl';
|
||||
import { get } from 'svelte/store';
|
||||
import { map } from '$lib/components/map/map';
|
||||
import { allHidden } from '$lib/logic/hidden';
|
||||
import type { GeoJSONSource } from 'maplibre-gl';
|
||||
import { ANCHOR_LAYER_KEY } from '../style';
|
||||
|
||||
const { distanceMarkers, distanceUnits } = settings;
|
||||
|
||||
@@ -22,7 +23,7 @@ export class DistanceMarkers {
|
||||
this.unsubscribes.push(
|
||||
map.subscribe((map_) => {
|
||||
if (map_) {
|
||||
map_.on('style.import.load', this.updateBinded);
|
||||
map_.on('style.load', this.updateBinded);
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -44,44 +45,45 @@ export class DistanceMarkers {
|
||||
});
|
||||
}
|
||||
if (!map_.getLayer('distance-markers')) {
|
||||
map_.addLayer({
|
||||
id: 'distance-markers',
|
||||
type: 'symbol',
|
||||
source: 'distance-markers',
|
||||
filter: [
|
||||
'match',
|
||||
['get', 'level'],
|
||||
100,
|
||||
['>=', ['zoom'], 0],
|
||||
50,
|
||||
['>=', ['zoom'], 7],
|
||||
25,
|
||||
[
|
||||
'any',
|
||||
['all', ['>=', ['zoom'], 8], ['<=', ['zoom'], 9]],
|
||||
map_.addLayer(
|
||||
{
|
||||
id: 'distance-markers',
|
||||
type: 'symbol',
|
||||
source: 'distance-markers',
|
||||
filter: [
|
||||
'match',
|
||||
['get', 'level'],
|
||||
100,
|
||||
['>=', ['zoom'], 0],
|
||||
50,
|
||||
['>=', ['zoom'], 7],
|
||||
25,
|
||||
[
|
||||
'any',
|
||||
['all', ['>=', ['zoom'], 8], ['<=', ['zoom'], 9]],
|
||||
['>=', ['zoom'], 11],
|
||||
],
|
||||
10,
|
||||
['>=', ['zoom'], 10],
|
||||
5,
|
||||
['>=', ['zoom'], 11],
|
||||
1,
|
||||
['>=', ['zoom'], 13],
|
||||
false,
|
||||
],
|
||||
10,
|
||||
['>=', ['zoom'], 10],
|
||||
5,
|
||||
['>=', ['zoom'], 11],
|
||||
1,
|
||||
['>=', ['zoom'], 13],
|
||||
false,
|
||||
],
|
||||
layout: {
|
||||
'text-field': ['get', 'distance'],
|
||||
'text-size': 14,
|
||||
'text-font': ['Open Sans Bold'],
|
||||
layout: {
|
||||
'text-field': ['get', 'distance'],
|
||||
'text-size': 14,
|
||||
'text-font': ['Open Sans Bold'],
|
||||
},
|
||||
paint: {
|
||||
'text-color': 'black',
|
||||
'text-halo-width': 2,
|
||||
'text-halo-color': 'white',
|
||||
},
|
||||
},
|
||||
paint: {
|
||||
'text-color': 'black',
|
||||
'text-halo-width': 2,
|
||||
'text-halo-color': 'white',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
map_.moveLayer('distance-markers');
|
||||
ANCHOR_LAYER_KEY.distanceMarkers
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (map_.getLayer('distance-markers')) {
|
||||
|
||||
@@ -3,13 +3,14 @@ import { MapPopup } from '$lib/components/map/map-popup';
|
||||
export let waypointPopup: MapPopup | null = null;
|
||||
export let trackpointPopup: MapPopup | null = null;
|
||||
|
||||
export function createPopups(map: mapboxgl.Map) {
|
||||
export function createPopups(map: maplibregl.Map) {
|
||||
removePopups();
|
||||
waypointPopup = new MapPopup(map, {
|
||||
closeButton: false,
|
||||
focusAfterOpen: false,
|
||||
maxWidth: undefined,
|
||||
offset: {
|
||||
center: [0, 0],
|
||||
top: [0, 0],
|
||||
'top-left': [0, 0],
|
||||
'top-right': [0, 0],
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { get, type Readable } from 'svelte/store';
|
||||
import mapboxgl, { type FilterSpecification } from 'mapbox-gl';
|
||||
import maplibregl, {
|
||||
type GeoJSONSource,
|
||||
type FilterSpecification,
|
||||
type MapLayerMouseEvent,
|
||||
type MapLayerTouchEvent,
|
||||
} from 'maplibre-gl';
|
||||
import { map } from '$lib/components/map/map';
|
||||
import { waypointPopup, trackpointPopup } from './gpx-layer-popup';
|
||||
import {
|
||||
@@ -22,6 +27,8 @@ import { fileActionManager } from '$lib/logic/file-action-manager';
|
||||
import { fileActions } from '$lib/logic/file-actions';
|
||||
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
|
||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
||||
import { gpxColors } from './gpx-layers';
|
||||
|
||||
const colors = [
|
||||
'#ff0000',
|
||||
@@ -43,16 +50,35 @@ for (let color of colors) {
|
||||
}
|
||||
|
||||
// Get the color with the least amount of uses
|
||||
function getColor() {
|
||||
function getColor(fileId: string) {
|
||||
let color = colors.reduce((a, b) => (colorCount[a] <= colorCount[b] ? a : b));
|
||||
colorCount[color]++;
|
||||
gpxColors.update((colors) => {
|
||||
colors.set(fileId, color);
|
||||
return colors;
|
||||
});
|
||||
return color;
|
||||
}
|
||||
|
||||
function decrementColor(color: string) {
|
||||
function replaceColor(fileId: string, oldColor: string, newColor: string) {
|
||||
if (colorCount.hasOwnProperty(oldColor)) {
|
||||
colorCount[oldColor]--;
|
||||
}
|
||||
colorCount[newColor]++;
|
||||
gpxColors.update((colors) => {
|
||||
colors.set(fileId, newColor);
|
||||
return colors;
|
||||
});
|
||||
}
|
||||
|
||||
function removeColor(fileId: string, color: string) {
|
||||
if (colorCount.hasOwnProperty(color)) {
|
||||
colorCount[color]--;
|
||||
}
|
||||
gpxColors.update((colors) => {
|
||||
colors.delete(fileId);
|
||||
return colors;
|
||||
});
|
||||
}
|
||||
|
||||
export function getSvgForSymbol(symbol?: string | undefined, layerColor?: string | undefined) {
|
||||
@@ -94,38 +120,38 @@ export class GPXLayer {
|
||||
selected: boolean = false;
|
||||
currentWaypointData: GeoJSON.FeatureCollection | null = null;
|
||||
draggedWaypointIndex: number | null = null;
|
||||
draggingStartingPosition: mapboxgl.Point = new mapboxgl.Point(0, 0);
|
||||
draggingStartingPosition: maplibregl.Point = new maplibregl.Point(0, 0);
|
||||
unsubscribe: Function[] = [];
|
||||
|
||||
updateBinded: () => void = this.update.bind(this);
|
||||
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
|
||||
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
|
||||
layerOnMouseMoveBinded: (e: any) => void = this.layerOnMouseMove.bind(this);
|
||||
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
|
||||
layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this);
|
||||
waypointLayerOnMouseEnterBinded: (e: mapboxgl.MapMouseEvent) => void =
|
||||
layerOnClickBinded: (e: MapLayerMouseEvent) => void = this.layerOnClick.bind(this);
|
||||
layerOnContextMenuBinded: (e: MapLayerMouseEvent) => void = this.layerOnContextMenu.bind(this);
|
||||
waypointLayerOnMouseEnterBinded: (e: MapLayerMouseEvent) => void =
|
||||
this.waypointLayerOnMouseEnter.bind(this);
|
||||
waypointLayerOnMouseLeaveBinded: (e: mapboxgl.MapMouseEvent) => void =
|
||||
waypointLayerOnMouseLeaveBinded: (e: MapLayerMouseEvent) => void =
|
||||
this.waypointLayerOnMouseLeave.bind(this);
|
||||
waypointLayerOnClickBinded: (e: mapboxgl.MapMouseEvent) => void =
|
||||
waypointLayerOnClickBinded: (e: MapLayerMouseEvent) => void =
|
||||
this.waypointLayerOnClick.bind(this);
|
||||
waypointLayerOnMouseDownBinded: (e: mapboxgl.MapMouseEvent) => void =
|
||||
waypointLayerOnMouseDownBinded: (e: MapLayerMouseEvent) => void =
|
||||
this.waypointLayerOnMouseDown.bind(this);
|
||||
waypointLayerOnTouchStartBinded: (e: mapboxgl.MapTouchEvent) => void =
|
||||
waypointLayerOnTouchStartBinded: (e: MapLayerTouchEvent) => void =
|
||||
this.waypointLayerOnTouchStart.bind(this);
|
||||
waypointLayerOnMouseMoveBinded: (e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) => void =
|
||||
waypointLayerOnMouseMoveBinded: (e: MapLayerMouseEvent | MapLayerTouchEvent) => void =
|
||||
this.waypointLayerOnMouseMove.bind(this);
|
||||
waypointLayerOnMouseUpBinded: (e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) => void =
|
||||
waypointLayerOnMouseUpBinded: (e: MapLayerMouseEvent | MapLayerTouchEvent) => void =
|
||||
this.waypointLayerOnMouseUp.bind(this);
|
||||
|
||||
constructor(fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
|
||||
this.fileId = fileId;
|
||||
this.file = file;
|
||||
this.layerColor = getColor();
|
||||
this.layerColor = getColor(fileId);
|
||||
this.unsubscribe.push(
|
||||
map.subscribe(($map) => {
|
||||
if ($map) {
|
||||
$map.on('style.import.load', this.updateBinded);
|
||||
$map.on('style.load', this.updateBinded);
|
||||
this.update();
|
||||
}
|
||||
})
|
||||
@@ -158,14 +184,14 @@ export class GPXLayer {
|
||||
file._data.style.color &&
|
||||
this.layerColor !== `#${file._data.style.color}`
|
||||
) {
|
||||
decrementColor(this.layerColor);
|
||||
replaceColor(this.fileId, this.layerColor, `#${file._data.style.color}`);
|
||||
this.layerColor = `#${file._data.style.color}`;
|
||||
}
|
||||
|
||||
this.loadIcons();
|
||||
|
||||
try {
|
||||
let source = _map.getSource(this.fileId) as mapboxgl.GeoJSONSource | undefined;
|
||||
let source = _map.getSource(this.fileId) as GeoJSONSource | undefined;
|
||||
if (source) {
|
||||
source.setData(this.getGeoJSON());
|
||||
} else {
|
||||
@@ -176,20 +202,23 @@ export class GPXLayer {
|
||||
}
|
||||
|
||||
if (!_map.getLayer(this.fileId)) {
|
||||
_map.addLayer({
|
||||
id: this.fileId,
|
||||
type: 'line',
|
||||
source: this.fileId,
|
||||
layout: {
|
||||
'line-join': 'round',
|
||||
'line-cap': 'round',
|
||||
_map.addLayer(
|
||||
{
|
||||
id: this.fileId,
|
||||
type: 'line',
|
||||
source: this.fileId,
|
||||
layout: {
|
||||
'line-join': 'round',
|
||||
'line-cap': 'round',
|
||||
},
|
||||
paint: {
|
||||
'line-color': ['get', 'color'],
|
||||
'line-width': ['get', 'width'],
|
||||
'line-opacity': ['get', 'opacity'],
|
||||
},
|
||||
},
|
||||
paint: {
|
||||
'line-color': ['get', 'color'],
|
||||
'line-width': ['get', 'width'],
|
||||
'line-opacity': ['get', 'opacity'],
|
||||
},
|
||||
});
|
||||
ANCHOR_LAYER_KEY.tracks
|
||||
);
|
||||
|
||||
_map.on('click', this.fileId, this.layerOnClickBinded);
|
||||
_map.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
||||
@@ -197,9 +226,8 @@ export class GPXLayer {
|
||||
_map.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
||||
_map.on('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
||||
}
|
||||
|
||||
let waypointSource = _map.getSource(this.fileId + '-waypoints') as
|
||||
| mapboxgl.GeoJSONSource
|
||||
| GeoJSONSource
|
||||
| undefined;
|
||||
this.currentWaypointData = this.getWaypointsGeoJSON();
|
||||
if (waypointSource) {
|
||||
@@ -212,18 +240,21 @@ export class GPXLayer {
|
||||
}
|
||||
|
||||
if (!_map.getLayer(this.fileId + '-waypoints')) {
|
||||
_map.addLayer({
|
||||
id: this.fileId + '-waypoints',
|
||||
type: 'symbol',
|
||||
source: this.fileId + '-waypoints',
|
||||
layout: {
|
||||
'icon-image': ['get', 'icon'],
|
||||
'icon-size': 0.3,
|
||||
'icon-anchor': 'bottom',
|
||||
'icon-padding': 0,
|
||||
'icon-allow-overlap': true,
|
||||
_map.addLayer(
|
||||
{
|
||||
id: this.fileId + '-waypoints',
|
||||
type: 'symbol',
|
||||
source: this.fileId + '-waypoints',
|
||||
layout: {
|
||||
'icon-image': ['get', 'icon'],
|
||||
'icon-size': 0.3,
|
||||
'icon-anchor': 'bottom',
|
||||
'icon-padding': 0,
|
||||
'icon-allow-overlap': true,
|
||||
},
|
||||
},
|
||||
});
|
||||
ANCHOR_LAYER_KEY.waypoints
|
||||
);
|
||||
|
||||
_map.on(
|
||||
'mouseenter',
|
||||
@@ -272,7 +303,7 @@ export class GPXLayer {
|
||||
'text-halo-color': 'white',
|
||||
},
|
||||
},
|
||||
_map.getLayer('distance-markers') ? 'distance-markers' : undefined
|
||||
ANCHOR_LAYER_KEY.directionMarkers
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -325,7 +356,7 @@ export class GPXLayer {
|
||||
_map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
||||
_map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
||||
_map.off('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
||||
_map.off('style.import.load', this.updateBinded);
|
||||
_map.off('style.load', this.updateBinded);
|
||||
|
||||
_map.off(
|
||||
'mouseenter',
|
||||
@@ -364,7 +395,7 @@ export class GPXLayer {
|
||||
|
||||
this.unsubscribe.forEach((unsubscribe) => unsubscribe());
|
||||
|
||||
decrementColor(this.layerColor);
|
||||
removeColor(this.fileId, this.layerColor);
|
||||
}
|
||||
|
||||
moveToFront() {
|
||||
@@ -373,13 +404,13 @@ export class GPXLayer {
|
||||
return;
|
||||
}
|
||||
if (_map.getLayer(this.fileId)) {
|
||||
_map.moveLayer(this.fileId);
|
||||
_map.moveLayer(this.fileId, ANCHOR_LAYER_KEY.tracks);
|
||||
}
|
||||
if (_map.getLayer(this.fileId + '-waypoints')) {
|
||||
_map.moveLayer(this.fileId + '-waypoints');
|
||||
_map.moveLayer(this.fileId + '-waypoints', ANCHOR_LAYER_KEY.waypoints);
|
||||
}
|
||||
if (_map.getLayer(this.fileId + '-direction')) {
|
||||
_map.moveLayer(this.fileId + '-direction');
|
||||
_map.moveLayer(this.fileId + '-direction', ANCHOR_LAYER_KEY.directionMarkers);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,7 +451,7 @@ export class GPXLayer {
|
||||
}
|
||||
}
|
||||
|
||||
layerOnClick(e: mapboxgl.MapMouseEvent) {
|
||||
layerOnClick(e: MapLayerMouseEvent) {
|
||||
if (
|
||||
get(currentTool) === Tool.ROUTING &&
|
||||
get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])
|
||||
@@ -478,7 +509,7 @@ export class GPXLayer {
|
||||
}
|
||||
}
|
||||
|
||||
waypointLayerOnMouseEnter(e: mapboxgl.MapMouseEvent) {
|
||||
waypointLayerOnMouseEnter(e: MapLayerMouseEvent) {
|
||||
if (this.draggedWaypointIndex !== null) {
|
||||
return;
|
||||
}
|
||||
@@ -498,7 +529,7 @@ export class GPXLayer {
|
||||
mapCursor.notify(MapCursorState.WAYPOINT_HOVER, false);
|
||||
}
|
||||
|
||||
waypointLayerOnClick(e: mapboxgl.MapMouseEvent) {
|
||||
waypointLayerOnClick(e: MapLayerMouseEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
let waypointIndex = e.features![0].properties!.waypointIndex;
|
||||
@@ -540,7 +571,7 @@ export class GPXLayer {
|
||||
}
|
||||
}
|
||||
|
||||
waypointLayerOnMouseDown(e: mapboxgl.MapMouseEvent) {
|
||||
waypointLayerOnMouseDown(e: MapLayerMouseEvent) {
|
||||
if (get(currentTool) !== Tool.WAYPOINT || !this.selected) {
|
||||
return;
|
||||
}
|
||||
@@ -559,7 +590,7 @@ export class GPXLayer {
|
||||
_map.once('mouseup', this.waypointLayerOnMouseUpBinded);
|
||||
}
|
||||
|
||||
waypointLayerOnTouchStart(e: mapboxgl.MapTouchEvent) {
|
||||
waypointLayerOnTouchStart(e: MapLayerTouchEvent) {
|
||||
if (e.points.length !== 1 || get(currentTool) !== Tool.WAYPOINT || !this.selected) {
|
||||
return;
|
||||
}
|
||||
@@ -578,7 +609,7 @@ export class GPXLayer {
|
||||
_map.once('touchend', this.waypointLayerOnMouseUpBinded);
|
||||
}
|
||||
|
||||
waypointLayerOnMouseMove(e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) {
|
||||
waypointLayerOnMouseMove(e: MapLayerMouseEvent | MapLayerTouchEvent) {
|
||||
if (this.draggedWaypointIndex === null || e.point.equals(this.draggingStartingPosition)) {
|
||||
return;
|
||||
}
|
||||
@@ -590,14 +621,14 @@ export class GPXLayer {
|
||||
).coordinates = [e.lngLat.lng, e.lngLat.lat];
|
||||
|
||||
let waypointSource = get(map)?.getSource(this.fileId + '-waypoints') as
|
||||
| mapboxgl.GeoJSONSource
|
||||
| GeoJSONSource
|
||||
| undefined;
|
||||
if (waypointSource) {
|
||||
waypointSource.setData(this.currentWaypointData!);
|
||||
}
|
||||
}
|
||||
|
||||
waypointLayerOnMouseUp(e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) {
|
||||
waypointLayerOnMouseUp(e: MapLayerMouseEvent | MapLayerTouchEvent) {
|
||||
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
|
||||
|
||||
get(map)?.off('mousemove', this.waypointLayerOnMouseMoveBinded);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GPXFileStateCollectionObserver } from '$lib/logic/file-state';
|
||||
import { writable } from 'svelte/store';
|
||||
import { GPXLayer } from './gpx-layer';
|
||||
|
||||
export class GPXLayerCollection {
|
||||
@@ -42,3 +43,4 @@ export class GPXLayerCollection {
|
||||
}
|
||||
|
||||
export const gpxLayers = new GPXLayerCollection();
|
||||
export const gpxColors = writable(new Map<string, string>());
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
||||
import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
import { get } from 'svelte/store';
|
||||
import { map } from '$lib/components/map/map';
|
||||
import { allHidden } from '$lib/logic/hidden';
|
||||
|
||||
export class StartEndMarkers {
|
||||
start: mapboxgl.Marker;
|
||||
end: mapboxgl.Marker;
|
||||
start: maplibregl.Marker;
|
||||
end: maplibregl.Marker;
|
||||
updateBinded: () => void = this.update.bind(this);
|
||||
unsubscribes: (() => void)[] = [];
|
||||
|
||||
@@ -19,8 +19,8 @@ export class StartEndMarkers {
|
||||
endElement.style.background =
|
||||
'repeating-conic-gradient(#fff 0 90deg, #000 0 180deg) 0 0/8px 8px round';
|
||||
|
||||
this.start = new mapboxgl.Marker({ element: startElement });
|
||||
this.end = new mapboxgl.Marker({ element: endElement });
|
||||
this.start = new maplibregl.Marker({ element: startElement });
|
||||
this.end = new maplibregl.Marker({ element: endElement });
|
||||
|
||||
map.onLoad(() => this.update());
|
||||
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
|
||||
|
||||
@@ -42,13 +42,8 @@
|
||||
let maxZoom: number = $state(20);
|
||||
let layerType: 'basemap' | 'overlay' = $state('basemap');
|
||||
let resourceType: 'raster' | 'vector' = $derived.by(() => {
|
||||
if (tileUrls[0].length > 0) {
|
||||
if (
|
||||
tileUrls[0].includes('.json') ||
|
||||
(tileUrls[0].includes('api.mapbox.com/styles') && !tileUrls[0].includes('tiles'))
|
||||
) {
|
||||
return 'vector';
|
||||
}
|
||||
if (tileUrls[0].length > 0 && tileUrls[0].includes('.json')) {
|
||||
return 'vector';
|
||||
}
|
||||
return 'raster';
|
||||
});
|
||||
|
||||
@@ -5,12 +5,8 @@
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||
import { Layers } from '@lucide/svelte';
|
||||
import { basemaps, defaultBasemap, overlays } from '$lib/assets/layers';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { map } from '$lib/components/map/map';
|
||||
import { customBasemapUpdate, getLayers } from './utils';
|
||||
import type { ImportSpecification, StyleSpecification } from 'mapbox-gl';
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let overpassLayer: OverpassLayer;
|
||||
@@ -23,125 +19,14 @@
|
||||
selectedBasemapTree,
|
||||
selectedOverlayTree,
|
||||
selectedOverpassTree,
|
||||
customLayers,
|
||||
opacities,
|
||||
} = settings;
|
||||
|
||||
function setStyle() {
|
||||
if (!$map) {
|
||||
return;
|
||||
}
|
||||
let basemap = basemaps.hasOwnProperty($currentBasemap)
|
||||
? basemaps[$currentBasemap]
|
||||
: ($customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap]);
|
||||
$map.removeImport('basemap');
|
||||
if (typeof basemap === 'string') {
|
||||
$map.addImport({ id: 'basemap', url: basemap }, 'overlays');
|
||||
} else {
|
||||
$map.addImport(
|
||||
{
|
||||
id: 'basemap',
|
||||
url: '',
|
||||
data: basemap as StyleSpecification,
|
||||
},
|
||||
'overlays'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if ($map && ($currentBasemap || $customBasemapUpdate)) {
|
||||
untrack(() => setStyle());
|
||||
}
|
||||
});
|
||||
|
||||
function addOverlay(id: string) {
|
||||
if (!$map) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id];
|
||||
if (typeof overlay === 'string') {
|
||||
$map.addImport({ id, url: overlay });
|
||||
} else {
|
||||
if ($opacities.hasOwnProperty(id)) {
|
||||
overlay = {
|
||||
...overlay,
|
||||
layers: (overlay as StyleSpecification).layers.map((layer) => {
|
||||
if (layer.type === 'raster') {
|
||||
if (!layer.paint) {
|
||||
layer.paint = {};
|
||||
}
|
||||
layer.paint['raster-opacity'] = $opacities[id];
|
||||
}
|
||||
return layer;
|
||||
}),
|
||||
};
|
||||
}
|
||||
$map.addImport({
|
||||
id,
|
||||
url: '',
|
||||
data: overlay as StyleSpecification,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// No reliable way to check if the map is ready to add sources and layers
|
||||
}
|
||||
}
|
||||
|
||||
function updateOverlays() {
|
||||
if ($map && $currentOverlays && $opacities) {
|
||||
let overlayLayers = getLayers($currentOverlays);
|
||||
try {
|
||||
let activeOverlays =
|
||||
$map
|
||||
.getStyle()
|
||||
.imports?.reduce(
|
||||
(
|
||||
acc: Record<string, ImportSpecification>,
|
||||
imprt: ImportSpecification
|
||||
) => {
|
||||
if (!['basemap', 'overlays'].includes(imprt.id)) {
|
||||
acc[imprt.id] = imprt;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
) || {};
|
||||
let toRemove = Object.keys(activeOverlays).filter((id) => !overlayLayers[id]);
|
||||
toRemove.forEach((id) => {
|
||||
$map?.removeImport(id);
|
||||
});
|
||||
let toAdd = Object.entries(overlayLayers)
|
||||
.filter(([id, selected]) => selected && !activeOverlays.hasOwnProperty(id))
|
||||
.map(([id]) => id);
|
||||
toAdd.forEach((id) => {
|
||||
addOverlay(id);
|
||||
});
|
||||
} catch (e) {
|
||||
// No reliable way to check if the map is ready to add sources and layers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if ($map && $currentOverlays && $opacities) {
|
||||
untrack(() => updateOverlays());
|
||||
}
|
||||
});
|
||||
|
||||
map.onLoad((_map: mapboxgl.Map) => {
|
||||
map.onLoad((_map: maplibregl.Map) => {
|
||||
if (overpassLayer) {
|
||||
overpassLayer.remove();
|
||||
}
|
||||
overpassLayer = new OverpassLayer(_map);
|
||||
overpassLayer.add();
|
||||
let first = true;
|
||||
_map.on('style.import.load', () => {
|
||||
if (!first) return;
|
||||
first = false;
|
||||
updateOverlays();
|
||||
});
|
||||
});
|
||||
|
||||
let open = $state(false);
|
||||
|
||||
@@ -213,7 +213,9 @@
|
||||
isSelected($currentOverlays, selectedOverlay)
|
||||
) {
|
||||
try {
|
||||
$map.removeImport(selectedOverlay);
|
||||
if ($map.getLayer(selectedOverlay)) {
|
||||
$map.removeLayer(selectedOverlay);
|
||||
}
|
||||
} catch (e) {
|
||||
// No reliable way to check if the map is ready to remove sources and layers
|
||||
}
|
||||
|
||||
@@ -54,28 +54,27 @@
|
||||
|
||||
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw] gap-0">
|
||||
<Card.Header class="p-0 gap-0">
|
||||
<Card.Title class="text-md">
|
||||
<div class="flex flex-row gap-3">
|
||||
<div class="flex flex-col">
|
||||
{name}
|
||||
<div class="text-muted-foreground text-xs font-normal">
|
||||
{poi.item.lat.toFixed(6)}° {poi.item.lon.toFixed(6)}°
|
||||
</div>
|
||||
<Card.Title class="text-md flex flex-row">
|
||||
<div class="flex flex-col">
|
||||
<p>{name}</p>
|
||||
<div class="text-muted-foreground text-xs font-normal">
|
||||
{poi.item.lat.toFixed(6)}° {poi.item.lon.toFixed(6)}°
|
||||
</div>
|
||||
<Button
|
||||
class="ml-auto"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
href="https://www.openstreetmap.org/edit?editor=id&{poi.item.type ??
|
||||
'node'}={poi.item.id}"
|
||||
target="_blank"
|
||||
>
|
||||
<PencilLine size="16" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
class="ml-auto"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
href="https://www.openstreetmap.org/edit?editor=id&{poi.item.type ?? 'node'}={poi
|
||||
.item.id}"
|
||||
target="_blank"
|
||||
>
|
||||
<PencilLine size="16" />
|
||||
</Button>
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content class="flex flex-col p-0 text-sm mt-1 whitespace-normal break-all">
|
||||
<Card.Content class="flex flex-col gap-1 p-0 text-sm whitespace-normal break-all">
|
||||
<ScrollArea class="flex flex-col max-h-[30dvh]">
|
||||
{#if tags.image || tags['image:0']}
|
||||
<div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto">
|
||||
@@ -100,8 +99,14 @@
|
||||
{/each}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<Button class="mt-2" variant="outline" disabled={$selection.size === 0} onclick={addToFile}>
|
||||
<MapPin size="16" />
|
||||
<Button
|
||||
size="sm"
|
||||
class="mt-1 justify-start"
|
||||
variant="outline"
|
||||
disabled={$selection.size === 0}
|
||||
onclick={addToFile}
|
||||
>
|
||||
<MapPin size="14" />
|
||||
{i18n._('toolbar.waypoint.add')}
|
||||
</Button>
|
||||
</Card.Content>
|
||||
|
||||
@@ -103,7 +103,7 @@ export class ExtensionAPI {
|
||||
if (current && isSelected(current, overlay.id)) {
|
||||
show = true;
|
||||
try {
|
||||
get(map)?.removeImport(overlay.id);
|
||||
get(map)?.removeLayer(overlay.id);
|
||||
} catch (e) {
|
||||
// No reliable way to check if the map is ready to remove sources and layers
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { overpassQueryData } from '$lib/assets/layers';
|
||||
import { MapPopup } from '$lib/components/map/map-popup';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { db } from '$lib/db';
|
||||
import type { GeoJSONSource } from 'maplibre-gl';
|
||||
import { ANCHOR_LAYER_KEY } from '../style';
|
||||
|
||||
const { currentOverpassQueries } = settings;
|
||||
|
||||
@@ -20,11 +22,11 @@ liveQuery(() => db.overpassdata.toArray()).subscribe((pois) => {
|
||||
});
|
||||
|
||||
export class OverpassLayer {
|
||||
overpassUrl = 'https://overpass.private.coffee/api/interpreter';
|
||||
overpassUrl = 'https://maps.mail.ru/osm/tools/overpass/api/interpreter';
|
||||
minZoom = 12;
|
||||
queryZoom = 12;
|
||||
expirationTime = 7 * 24 * 3600 * 1000;
|
||||
map: mapboxgl.Map;
|
||||
map: maplibregl.Map;
|
||||
popup: MapPopup;
|
||||
|
||||
currentQueries: Set<string> = new Set();
|
||||
@@ -35,7 +37,7 @@ export class OverpassLayer {
|
||||
updateBinded = this.update.bind(this);
|
||||
onHoverBinded = this.onHover.bind(this);
|
||||
|
||||
constructor(map: mapboxgl.Map) {
|
||||
constructor(map: maplibregl.Map) {
|
||||
this.map = map;
|
||||
this.popup = new MapPopup(map, {
|
||||
closeButton: false,
|
||||
@@ -47,7 +49,7 @@ export class OverpassLayer {
|
||||
|
||||
add() {
|
||||
this.map.on('moveend', this.queryIfNeededBinded);
|
||||
this.map.on('style.import.load', this.updateBinded);
|
||||
this.map.on('style.load', this.updateBinded);
|
||||
this.unsubscribes.push(data.subscribe(this.updateBinded));
|
||||
this.unsubscribes.push(
|
||||
currentOverpassQueries.subscribe(() => {
|
||||
@@ -74,7 +76,7 @@ export class OverpassLayer {
|
||||
let d = get(data);
|
||||
|
||||
try {
|
||||
let source = this.map.getSource('overpass') as mapboxgl.GeoJSONSource | undefined;
|
||||
let source = this.map.getSource('overpass') as GeoJSONSource | undefined;
|
||||
if (source) {
|
||||
source.setData(d);
|
||||
} else {
|
||||
@@ -85,17 +87,20 @@ export class OverpassLayer {
|
||||
}
|
||||
|
||||
if (!this.map.getLayer('overpass')) {
|
||||
this.map.addLayer({
|
||||
id: 'overpass',
|
||||
type: 'symbol',
|
||||
source: 'overpass',
|
||||
layout: {
|
||||
'icon-image': ['get', 'icon'],
|
||||
'icon-size': 0.25,
|
||||
'icon-padding': 0,
|
||||
'icon-allow-overlap': ['step', ['zoom'], false, 14, true],
|
||||
this.map.addLayer(
|
||||
{
|
||||
id: 'overpass',
|
||||
type: 'symbol',
|
||||
source: 'overpass',
|
||||
layout: {
|
||||
'icon-image': ['get', 'icon'],
|
||||
'icon-size': 0.25,
|
||||
'icon-padding': 0,
|
||||
'icon-allow-overlap': ['step', ['zoom'], false, 14, true],
|
||||
},
|
||||
},
|
||||
});
|
||||
ANCHOR_LAYER_KEY.overpass
|
||||
);
|
||||
|
||||
this.map.on('mouseenter', 'overpass', this.onHoverBinded);
|
||||
this.map.on('click', 'overpass', this.onHoverBinded);
|
||||
@@ -111,7 +116,7 @@ export class OverpassLayer {
|
||||
|
||||
remove() {
|
||||
this.map.off('moveend', this.queryIfNeededBinded);
|
||||
this.map.off('style.import.load', this.updateBinded);
|
||||
this.map.off('style.load', this.updateBinded);
|
||||
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TrackPoint, Waypoint } from 'gpx';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
import { mount, tick, unmount } from 'svelte';
|
||||
import { get, writable, type Writable } from 'svelte/store';
|
||||
import MapPopupComponent from '$lib/components/map/MapPopup.svelte';
|
||||
@@ -11,15 +11,15 @@ export type PopupItem<T = Waypoint | TrackPoint | any> = {
|
||||
};
|
||||
|
||||
export class MapPopup {
|
||||
map: mapboxgl.Map;
|
||||
popup: mapboxgl.Popup;
|
||||
map: maplibregl.Map;
|
||||
popup: maplibregl.Popup;
|
||||
item: Writable<PopupItem | null> = writable(null);
|
||||
component: ReturnType<typeof mount>;
|
||||
maybeHideBinded = this.maybeHide.bind(this);
|
||||
|
||||
constructor(map: mapboxgl.Map, options?: mapboxgl.PopupOptions) {
|
||||
constructor(map: maplibregl.Map, options?: maplibregl.PopupOptions) {
|
||||
this.map = map;
|
||||
this.popup = new mapboxgl.Popup(options);
|
||||
this.popup = new maplibregl.Popup(options);
|
||||
this.component = mount(MapPopupComponent, {
|
||||
target: document.body,
|
||||
props: {
|
||||
@@ -51,7 +51,7 @@ export class MapPopup {
|
||||
this.map.on('mousemove', this.maybeHideBinded);
|
||||
}
|
||||
|
||||
maybeHide(e: mapboxgl.MapMouseEvent) {
|
||||
maybeHide(e: maplibregl.MapMouseEvent) {
|
||||
const item = get(this.item);
|
||||
if (item === null) {
|
||||
this.hide();
|
||||
@@ -75,10 +75,10 @@ export class MapPopup {
|
||||
getCoordinates() {
|
||||
const item = get(this.item);
|
||||
if (item === null) {
|
||||
return new mapboxgl.LngLat(0, 0);
|
||||
return new maplibregl.LngLat(0, 0);
|
||||
}
|
||||
return item.item instanceof Waypoint || item.item instanceof TrackPoint
|
||||
? item.item.getCoordinates()
|
||||
: new mapboxgl.LngLat(item.item.lon, item.item.lat);
|
||||
: new maplibregl.LngLat(item.item.lon, item.item.lat);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +1,77 @@
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
|
||||
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';
|
||||
import { get, writable, type Writable } from 'svelte/store';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { tick } from 'svelte';
|
||||
import { terrainSources } from '$lib/assets/layers';
|
||||
import { ANCHOR_LAYER_KEY, StyleManager } from '$lib/components/map/style';
|
||||
|
||||
const {
|
||||
treeFileView,
|
||||
elevationProfile,
|
||||
bottomPanelSize,
|
||||
rightPanelSize,
|
||||
distanceUnits,
|
||||
terrainSource,
|
||||
} = settings;
|
||||
const { treeFileView, elevationProfile, bottomPanelSize, rightPanelSize, distanceUnits } = settings;
|
||||
|
||||
let fitBoundsOptions: mapboxgl.MapOptions['fitBoundsOptions'] = {
|
||||
let fitBoundsOptions: maplibregl.MapOptions['fitBoundsOptions'] = {
|
||||
maxZoom: 15,
|
||||
linear: true,
|
||||
easing: () => 1,
|
||||
};
|
||||
|
||||
export class MapboxGLMap {
|
||||
private _map: Writable<mapboxgl.Map | null> = writable(null);
|
||||
private _onLoadCallbacks: ((map: mapboxgl.Map) => void)[] = [];
|
||||
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)[] = [];
|
||||
private _unsubscribes: (() => void)[] = [];
|
||||
private callOnLoadBinded: () => void = this.callOnLoad.bind(this);
|
||||
|
||||
subscribe(run: (value: mapboxgl.Map | null) => void, invalidate?: () => void) {
|
||||
return this._map.subscribe(run, invalidate);
|
||||
subscribe(run: (value: maplibregl.Map | null) => void, invalidate?: () => void) {
|
||||
return this._mapStore.subscribe(run, invalidate);
|
||||
}
|
||||
|
||||
init(
|
||||
accessToken: string,
|
||||
maptilerKey: string,
|
||||
language: string,
|
||||
hash: boolean,
|
||||
geocoder: boolean,
|
||||
geolocate: boolean
|
||||
) {
|
||||
const map = new mapboxgl.Map({
|
||||
this._maptilerKey = maptilerKey;
|
||||
this._styleManager = new StyleManager(this._mapStore, this._maptilerKey);
|
||||
const map = new maplibregl.Map({
|
||||
container: 'map',
|
||||
style: {
|
||||
version: 8,
|
||||
projection: {
|
||||
type: 'globe',
|
||||
},
|
||||
sources: {},
|
||||
layers: [],
|
||||
imports: [
|
||||
{
|
||||
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,
|
||||
maxPitch: 85,
|
||||
});
|
||||
map.addControl(
|
||||
new mapboxgl.AttributionControl({
|
||||
compact: true,
|
||||
})
|
||||
);
|
||||
map.addControl(
|
||||
new mapboxgl.NavigationControl({
|
||||
new maplibregl.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) => {
|
||||
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) => {
|
||||
return {
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
@@ -101,61 +81,43 @@ export class MapboxGLMap {
|
||||
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();
|
||||
} catch (e) {}
|
||||
return results;
|
||||
},
|
||||
},
|
||||
{
|
||||
maplibregl: maplibregl,
|
||||
enableEventLogging: false,
|
||||
collapsed: true,
|
||||
flyTo: fitBoundsOptions,
|
||||
language,
|
||||
}
|
||||
};
|
||||
);
|
||||
map.addControl(geocoder);
|
||||
}
|
||||
if (geolocate) {
|
||||
map.addControl(
|
||||
new mapboxgl.GeolocateControl({
|
||||
new maplibregl.GeolocateControl({
|
||||
positionOptions: {
|
||||
enableHighAccuracy: true,
|
||||
},
|
||||
fitBoundsOptions,
|
||||
trackUserLocation: true,
|
||||
showUserHeading: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
const scaleControl = new mapboxgl.ScaleControl({
|
||||
const scaleControl = new maplibregl.ScaleControl({
|
||||
unit: get(distanceUnits),
|
||||
});
|
||||
map.addControl(scaleControl);
|
||||
map.on('style.load', () => {
|
||||
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', this.setTerrain.bind(this));
|
||||
this.setTerrain();
|
||||
});
|
||||
map.on('style.import.load', () => {
|
||||
const basemap = map.getStyle().imports?.find((imprt) => imprt.id === 'basemap');
|
||||
if (basemap && basemap.data && basemap.data.glyphs) {
|
||||
map.setGlyphsUrl(basemap.data.glyphs);
|
||||
}
|
||||
});
|
||||
map.on('load', () => {
|
||||
this._map.set(map); // only set the store after the map has loaded
|
||||
this._map = map;
|
||||
this._mapStore.set(map); // only set the store after the map has loaded
|
||||
window._map = map; // entry point for extensions
|
||||
this.resize();
|
||||
this.setTerrain();
|
||||
scaleControl.setUnit(get(distanceUnits));
|
||||
|
||||
this._onLoadCallbacks.forEach((callback) => callback(map));
|
||||
this._onLoadCallbacks = [];
|
||||
});
|
||||
map.on('style.load', this.callOnLoadBinded);
|
||||
|
||||
this._unsubscribes.push(treeFileView.subscribe(() => this.resize()));
|
||||
this._unsubscribes.push(elevationProfile.subscribe(() => this.resize()));
|
||||
@@ -166,70 +128,50 @@ export class MapboxGLMap {
|
||||
scaleControl.setUnit(units);
|
||||
})
|
||||
);
|
||||
this._unsubscribes.push(terrainSource.subscribe(() => this.setTerrain()));
|
||||
}
|
||||
|
||||
onLoad(callback: (map: mapboxgl.Map) => void) {
|
||||
const map = get(this._map);
|
||||
if (map) {
|
||||
callback(map);
|
||||
} else {
|
||||
this._onLoadCallbacks.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
const map = get(this._map);
|
||||
if (map) {
|
||||
map.remove();
|
||||
this._map.set(null);
|
||||
if (this._map) {
|
||||
this._map.remove();
|
||||
this._mapStore.set(null);
|
||||
}
|
||||
this._unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
this._unsubscribes = [];
|
||||
}
|
||||
|
||||
resize() {
|
||||
const map = get(this._map);
|
||||
if (map) {
|
||||
if (this._map) {
|
||||
tick().then(() => {
|
||||
map.resize();
|
||||
this._map?.resize();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toggle3D() {
|
||||
const map = get(this._map);
|
||||
if (map) {
|
||||
if (map.getPitch() === 0) {
|
||||
map.easeTo({ pitch: 70 });
|
||||
if (this._map) {
|
||||
if (this._map.getPitch() === 0) {
|
||||
this._map.easeTo({ pitch: 70 });
|
||||
} else {
|
||||
map.easeTo({ pitch: 0 });
|
||||
this._map.easeTo({ pitch: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTerrain() {
|
||||
const map = get(this._map);
|
||||
if (map) {
|
||||
const source = get(terrainSource);
|
||||
try {
|
||||
if (!map.getSource(source)) {
|
||||
map.addSource(source, terrainSources[source]);
|
||||
}
|
||||
if (map.getPitch() > 0) {
|
||||
map.setTerrain({
|
||||
source: source,
|
||||
exaggeration: 1,
|
||||
});
|
||||
} else {
|
||||
map.setTerrain(null);
|
||||
}
|
||||
} catch (e) {
|
||||
// No reliable way to check if the map is ready to add sources and layers
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const map = new MapboxGLMap();
|
||||
export const map = new MapLibreGLMap();
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
let container: HTMLElement;
|
||||
|
||||
onMount(() => {
|
||||
map.onLoad((map: mapboxgl.Map) => {
|
||||
map.onLoad((map: maplibregl.Map) => {
|
||||
googleRedirect = new GoogleRedirect(map);
|
||||
mapillaryLayer = new MapillaryLayer(map, container, mapillaryOpen);
|
||||
});
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||
import type mapboxgl from 'mapbox-gl';
|
||||
|
||||
export class GoogleRedirect {
|
||||
map: mapboxgl.Map;
|
||||
map: maplibregl.Map;
|
||||
enabled = false;
|
||||
|
||||
constructor(map: mapboxgl.Map) {
|
||||
constructor(map: maplibregl.Map) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
@@ -25,7 +24,7 @@ export class GoogleRedirect {
|
||||
this.map.off('click', this.openStreetView);
|
||||
}
|
||||
|
||||
openStreetView(e: mapboxgl.MapMouseEvent) {
|
||||
openStreetView(e: maplibregl.MapMouseEvent) {
|
||||
window.open(
|
||||
`https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${e.lngLat.lat},${e.lngLat.lng}`
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import mapboxgl, { type LayerSpecification, type VectorSourceSpecification } from 'mapbox-gl';
|
||||
import maplibregl, { type LayerSpecification, type VectorSourceSpecification } from 'maplibre-gl';
|
||||
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
|
||||
import 'mapillary-js/dist/mapillary.css';
|
||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||
import { ANCHOR_LAYER_KEY } from '../style';
|
||||
|
||||
const mapillarySource: VectorSourceSpecification = {
|
||||
type: 'vector',
|
||||
@@ -41,8 +42,8 @@ const mapillaryImageLayer: LayerSpecification = {
|
||||
};
|
||||
|
||||
export class MapillaryLayer {
|
||||
map: mapboxgl.Map;
|
||||
marker: mapboxgl.Marker;
|
||||
map: maplibregl.Map;
|
||||
marker: maplibregl.Marker;
|
||||
viewer: Viewer;
|
||||
|
||||
active = false;
|
||||
@@ -52,7 +53,7 @@ export class MapillaryLayer {
|
||||
onMouseEnterBinded = this.onMouseEnter.bind(this);
|
||||
onMouseLeaveBinded = this.onMouseLeave.bind(this);
|
||||
|
||||
constructor(map: mapboxgl.Map, container: HTMLElement, popupOpen: { value: boolean }) {
|
||||
constructor(map: maplibregl.Map, container: HTMLElement, popupOpen: { value: boolean }) {
|
||||
this.map = map;
|
||||
|
||||
this.viewer = new Viewer({
|
||||
@@ -61,15 +62,12 @@ export class MapillaryLayer {
|
||||
});
|
||||
|
||||
const element = document.createElement('div');
|
||||
element.className = 'mapboxgl-user-location mapboxgl-user-location-show-heading';
|
||||
element.className = 'maplibregl-user-location maplibregl-user-location-show-heading';
|
||||
const dot = document.createElement('div');
|
||||
dot.className = 'mapboxgl-user-location-dot';
|
||||
const heading = document.createElement('div');
|
||||
heading.className = 'mapboxgl-user-location-heading';
|
||||
dot.className = 'maplibregl-user-location-dot';
|
||||
element.appendChild(dot);
|
||||
element.appendChild(heading);
|
||||
|
||||
this.marker = new mapboxgl.Marker({
|
||||
this.marker = new maplibregl.Marker({
|
||||
rotationAlignment: 'map',
|
||||
element,
|
||||
});
|
||||
@@ -99,10 +97,10 @@ export class MapillaryLayer {
|
||||
this.map.addSource('mapillary', mapillarySource);
|
||||
}
|
||||
if (!this.map.getLayer('mapillary-sequence')) {
|
||||
this.map.addLayer(mapillarySequenceLayer);
|
||||
this.map.addLayer(mapillarySequenceLayer, ANCHOR_LAYER_KEY.mapillary);
|
||||
}
|
||||
if (!this.map.getLayer('mapillary-image')) {
|
||||
this.map.addLayer(mapillaryImageLayer);
|
||||
this.map.addLayer(mapillaryImageLayer, ANCHOR_LAYER_KEY.mapillary);
|
||||
}
|
||||
this.map.on('style.load', this.addBinded);
|
||||
this.map.on('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
||||
@@ -134,7 +132,7 @@ export class MapillaryLayer {
|
||||
this.popupOpen.value = false;
|
||||
}
|
||||
|
||||
onMouseEnter(e: mapboxgl.MapMouseEvent) {
|
||||
onMouseEnter(e: maplibregl.MapLayerMouseEvent) {
|
||||
if (
|
||||
e.features &&
|
||||
e.features.length > 0 &&
|
||||
|
||||
221
website/src/lib/components/map/style.ts
Normal file
221
website/src/lib/components/map/style.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { get, type Writable } from 'svelte/store';
|
||||
import {
|
||||
basemaps,
|
||||
defaultBasemap,
|
||||
maptilerKeyPlaceHolder,
|
||||
overlays,
|
||||
terrainSources,
|
||||
} from '$lib/assets/layers';
|
||||
import { customBasemapUpdate, getLayers } from '$lib/components/map/layer-control/utils';
|
||||
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',
|
||||
interactions: 'interactions-end',
|
||||
overpass: 'overpass-end',
|
||||
waypoints: 'waypoints-end',
|
||||
};
|
||||
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_) {
|
||||
this.update();
|
||||
map_.on('style.load', () => this.updateOverlays());
|
||||
map_.on('pitch', () => this.updateTerrain());
|
||||
}
|
||||
});
|
||||
currentBasemap.subscribe(() => this.update());
|
||||
customBasemapUpdate.subscribe(() => this.update());
|
||||
currentOverlays.subscribe(() => this.updateOverlays());
|
||||
opacities.subscribe(() => this.updateOverlays());
|
||||
terrainSource.subscribe(() => this.updateTerrain());
|
||||
}
|
||||
|
||||
update() {
|
||||
const map_ = get(this._map);
|
||||
if (!map_) return;
|
||||
this.build().then((style) => map_.setStyle(style));
|
||||
}
|
||||
|
||||
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() {
|
||||
const map_ = get(this._map);
|
||||
if (!map_) return;
|
||||
|
||||
const mapTerrain = map_.getTerrain();
|
||||
const terrain = this.getCurrentTerrain();
|
||||
if (JSON.stringify(mapTerrain) !== JSON.stringify(terrain)) {
|
||||
map_.setTerrain(terrain);
|
||||
}
|
||||
}
|
||||
|
||||
async build(): 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);
|
||||
|
||||
style.terrain = this.getCurrentTerrain();
|
||||
style.sources[style.terrain.source] = terrainSources[style.terrain.source];
|
||||
|
||||
style.layers.push(...anchorLayers);
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user