33 Commits

Author SHA1 Message Date
vcoppe
f3c298642c New translations mapbox.mdx (German) 2026-01-12 11:58:16 +01:00
vcoppe
b2568aa338 New translations en.json (Serbian (Latin)) 2026-01-11 20:50:09 +01:00
vcoppe
77ca98d2f5 New translations en.json (Chinese Traditional, Hong Kong) 2026-01-11 20:50:03 +01:00
vcoppe
aa370b6961 New translations en.json (Latvian) 2026-01-11 20:50:02 +01:00
vcoppe
4bb77f113c New translations en.json (Thai) 2026-01-11 20:50:01 +01:00
vcoppe
5319733cf3 New translations en.json (Indonesian) 2026-01-11 20:50:00 +01:00
vcoppe
84883f44c0 New translations en.json (Portuguese, Brazilian) 2026-01-11 20:49:59 +01:00
vcoppe
92f442a79e New translations en.json (Vietnamese) 2026-01-11 20:49:58 +01:00
vcoppe
dd2637971b New translations en.json (Ukrainian) 2026-01-11 20:49:57 +01:00
vcoppe
0667f6ece2 New translations en.json (Turkish) 2026-01-11 20:49:56 +01:00
vcoppe
c62b75c6ef New translations en.json (Swedish) 2026-01-11 20:49:55 +01:00
vcoppe
2fb7e77b63 New translations en.json (Russian) 2026-01-11 20:49:54 +01:00
vcoppe
f4ced434a7 New translations en.json (Portuguese) 2026-01-11 20:49:53 +01:00
vcoppe
03dec13229 New translations en.json (Norwegian) 2026-01-11 20:49:52 +01:00
vcoppe
411afdfdb1 New translations en.json (Dutch) 2026-01-11 20:49:51 +01:00
vcoppe
adbdf86de4 New translations en.json (Lithuanian) 2026-01-11 20:49:50 +01:00
vcoppe
866a6a6916 New translations en.json (Korean) 2026-01-11 20:49:49 +01:00
vcoppe
bd42a7e270 New translations en.json (Hungarian) 2026-01-11 20:49:48 +01:00
vcoppe
2b951615ae New translations en.json (Italian) 2026-01-11 20:49:45 +01:00
vcoppe
fabf36c2ba New translations en.json (Polish) 2026-01-11 20:49:44 +01:00
vcoppe
d216b36ed6 New translations en.json (Chinese Simplified) 2026-01-11 20:49:43 +01:00
vcoppe
e8f40fe206 New translations en.json (Hebrew) 2026-01-11 20:49:33 +01:00
vcoppe
3dc24d95e5 New translations en.json (Finnish) 2026-01-11 20:49:32 +01:00
vcoppe
744f36f981 New translations en.json (Basque) 2026-01-11 20:49:31 +01:00
vcoppe
2e24dedcb2 New translations en.json (Greek) 2026-01-11 20:49:30 +01:00
vcoppe
e380764188 New translations en.json (Danish) 2026-01-11 20:49:29 +01:00
vcoppe
47c3edd496 New translations en.json (Czech) 2026-01-11 20:49:28 +01:00
vcoppe
d6eeddaa0e New translations en.json (Catalan) 2026-01-11 20:49:27 +01:00
vcoppe
e57692c4f0 New translations en.json (Belarusian) 2026-01-11 20:49:25 +01:00
vcoppe
8c77490f1b New translations en.json (French) 2026-01-11 20:49:24 +01:00
vcoppe
6c4baf76bc New translations en.json (Romanian) 2026-01-11 20:49:23 +01:00
vcoppe
64e1c24a58 New translations en.json (German) 2026-01-11 20:49:22 +01:00
vcoppe
87e2571170 New translations en.json (Spanish) 2026-01-11 20:49:21 +01:00
110 changed files with 2595 additions and 2953 deletions

View File

@@ -31,7 +31,7 @@ jobs:
- name: Create env file - name: Create env file
run: | run: |
touch website/.env touch website/.env
echo PUBLIC_MAPTILER_KEY=${{ secrets.PUBLIC_MAPTILER_KEY }} >> website/.env echo PUBLIC_MAPBOX_TOKEN=${{ secrets.PUBLIC_MAPBOX_TOKEN }} >> website/.env
cat website/.env cat website/.env
- name: Build website - name: Build website

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2026 gpx.studio Copyright (c) 2025 gpx.studio
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -42,11 +42,11 @@ npm run build
### Running the website ### Running the website
To be able to load the map, you will need to create your own <a href="https://cloud.maptiler.com/auth/widget?next=https://cloud.maptiler.com/maps/" target="_blank">MapTiler key</a> and store it in a `.env` file in the `website` directory. To be able to load the map, you will need to create your own <a href="https://account.mapbox.com/auth/signup" target="_blank">Mapbox access token</a> and store it in a `.env` file in the `website` directory.
```bash ```bash
cd website cd website
echo PUBLIC_MAPTILER_KEY={YOUR_MAPTILER_KEY} >> .env echo PUBLIC_MAPBOX_TOKEN={YOUR_MAPBOX_TOKEN} >> .env
npm install npm install
npm run dev npm run dev
``` ```
@@ -69,9 +69,9 @@ This project has been made possible thanks to the following open source projects
- [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) — fast GPX file parsing - [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) — fast GPX file parsing
- [SortableJS](https://github.com/SortableJS/Sortable) — creating a sortable file tree - [SortableJS](https://github.com/SortableJS/Sortable) — creating a sortable file tree
- Mapping: - Mapping:
- [MapLibre GL JS](https://github.com/maplibre/maplibre-gl-js) — beautiful and fast interactive maps - [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js) — beautiful and fast interactive maps
- [brouter](https://github.com/abrensch/brouter) — routing engine - [brouter](https://github.com/abrensch/brouter) — routing engine
- [OpenStreetMap](https://www.openstreetmap.org) — map data used by most of the map layers, and by the routing engine - [OpenStreetMap](https://www.openstreetmap.org) — map data used by Mapbox and brouter
- Search: - Search:
- [DocSearch](https://github.com/algolia/docsearch) — search engine for the documentation - [DocSearch](https://github.com/algolia/docsearch) — search engine for the documentation

View File

@@ -1 +1 @@
PUBLIC_MAPTILER_KEY=YOUR_MAPTILER_KEY PUBLIC_MAPBOX_TOKEN=YOUR_MAPBOX_TOKEN

1222
website/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,9 +23,10 @@
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"@types/events": "^3.0.3", "@types/events": "^3.0.3",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/mapbox__sphericalmercator": "^1.2.3",
"@types/mapbox__tilebelt": "^1.0.4", "@types/mapbox__tilebelt": "^1.0.4",
"@types/mapbox-gl": "^3.4.1",
"@types/node": "^22.15.30", "@types/node": "^22.15.30",
"@types/png.js": "^0.2.3",
"@types/sanitize-html": "^2.16.0", "@types/sanitize-html": "^2.16.0",
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^8.33.1", "@typescript-eslint/eslint-plugin": "^8.33.1",
@@ -61,9 +62,10 @@
"dependencies": { "dependencies": {
"@docsearch/js": "^3.9.0", "@docsearch/js": "^3.9.0",
"@internationalized/date": "^3.8.2", "@internationalized/date": "^3.8.2",
"@mapbox/mapbox-gl-geocoder": "^5.0.3",
"@mapbox/sphericalmercator": "^2.0.1", "@mapbox/sphericalmercator": "^2.0.1",
"@mapbox/tilebelt": "^2.0.2", "@mapbox/tilebelt": "^2.0.2",
"@maplibre/maplibre-gl-geocoder": "^1.9.4", "@types/mapbox__sphericalmercator": "^1.2.3",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"chartjs-plugin-zoom": "^2.2.0", "chartjs-plugin-zoom": "^2.2.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -72,8 +74,9 @@
"gpx": "file:../gpx", "gpx": "file:../gpx",
"immer": "^10.1.1", "immer": "^10.1.1",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"mapbox-gl": "^3.17.0",
"mapillary-js": "^4.1.2", "mapillary-js": "^4.1.2",
"maplibre-gl": "^5.16.0", "png.js": "^0.2.1",
"sanitize-html": "^2.17.0", "sanitize-html": "^2.17.0",
"sortablejs": "^1.15.6", "sortablejs": "^1.15.6",
"tailwind-merge": "^3.3.0" "tailwind-merge": "^3.3.0"

View File

@@ -17,6 +17,7 @@
} }
}, },
"sprite": "https://demotiles.maplibre.org/styles/osm-bright-gl-style/sprite", "sprite": "https://demotiles.maplibre.org/styles/osm-bright-gl-style/sprite",
"glyphs": "https://api.maptiler.com/fonts/{fontstack}/{range}.pbf?key={key}",
"layers": [ "layers": [
{ {
"id": "background", "id": "background",

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

Before

Width:  |  Height:  |  Size: 3.6 MiB

After

Width:  |  Height:  |  Size: 3.6 MiB

View File

@@ -22,18 +22,15 @@ import {
Binoculars, Binoculars,
Toilet, Toilet,
} from 'lucide-static'; } from 'lucide-static';
import { type RasterDEMSourceSpecification, type StyleSpecification } from 'maplibre-gl'; import { type StyleSpecification } from 'mapbox-gl';
import ignFrTopo from './custom/ign-fr-topo.json'; import ignFrTopo from './custom/ign-fr-topo.json';
import ignFrPlan from './custom/ign-fr-plan.json'; import ignFrPlan from './custom/ign-fr-plan.json';
import ignFrSatellite from './custom/ign-fr-satellite.json'; import ignFrSatellite from './custom/ign-fr-satellite.json';
import bikerouterGravel from './custom/bikerouter-gravel.json'; import bikerouterGravel from './custom/bikerouter-gravel.json';
export const maptilerKeyPlaceHolder = 'MAPTILER_KEY';
export const basemaps: { [key: string]: string | StyleSpecification } = { export const basemaps: { [key: string]: string | StyleSpecification } = {
maptilerTopo: `https://api.maptiler.com/maps/topo-v4/style.json?key=${maptilerKeyPlaceHolder}`, mapboxOutdoors: 'mapbox://styles/mapbox/outdoors-v12',
maptilerOutdoors: `https://api.maptiler.com/maps/outdoor-v4/style.json?key=${maptilerKeyPlaceHolder}`, mapboxSatellite: 'mapbox://styles/mapbox/satellite-streets-v12',
maptilerSatellite: `https://api.maptiler.com/maps/hybrid-v4/style.json?key=${maptilerKeyPlaceHolder}`,
openStreetMap: { openStreetMap: {
version: 8, version: 8,
sources: { sources: {
@@ -391,22 +388,6 @@ export const overlays: { [key: string]: string | StyleSpecification } = {
}, },
], ],
}, },
mapterhornHillshade: {
version: 8,
sources: {
mapterhornHillshade: {
type: 'raster-dem',
url: 'https://tiles.mapterhorn.com/tilejson.json',
},
},
layers: [
{
id: 'mapterhornHillshade',
type: 'hillshade',
source: 'mapterhornHillshade',
},
],
},
swisstopoSlope: { swisstopoSlope: {
version: 8, version: 8,
sources: { sources: {
@@ -776,9 +757,8 @@ export type LayerTreeType = { [key: string]: LayerTreeType | boolean };
export const basemapTree: LayerTreeType = { export const basemapTree: LayerTreeType = {
basemaps: { basemaps: {
world: { world: {
maptilerTopo: true, mapboxOutdoors: true,
maptilerOutdoors: true, mapboxSatellite: true,
maptilerSatellite: true,
openStreetMap: true, openStreetMap: true,
openTopoMap: true, openTopoMap: true,
openHikingMap: true, openHikingMap: true,
@@ -839,9 +819,8 @@ export const overlayTree: LayerTreeType = {
waymarkedTrailsHorseRiding: true, waymarkedTrailsHorseRiding: true,
waymarkedTrailsWinter: true, waymarkedTrailsWinter: true,
}, },
bikerouterGravel: true,
cyclOSMlite: true, cyclOSMlite: true,
mapterhornHillshade: true, bikerouterGravel: true,
openRailwayMap: true, openRailwayMap: true,
}, },
countries: { countries: {
@@ -911,7 +890,7 @@ export const overpassTree: LayerTreeType = {
}; };
// Default basemap used // Default basemap used
export const defaultBasemap = 'maptilerTopo'; export const defaultBasemap = 'mapboxOutdoors';
// Default overlays used (none) // Default overlays used (none)
export const defaultOverlays: LayerTreeType = { export const defaultOverlays: LayerTreeType = {
@@ -925,9 +904,8 @@ export const defaultOverlays: LayerTreeType = {
waymarkedTrailsHorseRiding: false, waymarkedTrailsHorseRiding: false,
waymarkedTrailsWinter: false, waymarkedTrailsWinter: false,
}, },
bikerouterGravel: false,
cyclOSMlite: false, cyclOSMlite: false,
mapterhornHillshade: false, bikerouterGravel: false,
openRailwayMap: false, openRailwayMap: false,
}, },
countries: { countries: {
@@ -1000,9 +978,8 @@ export const defaultOverpassQueries: LayerTreeType = {
export const defaultBasemapTree: LayerTreeType = { export const defaultBasemapTree: LayerTreeType = {
basemaps: { basemaps: {
world: { world: {
maptilerTopo: true, mapboxOutdoors: true,
maptilerOutdoors: true, mapboxSatellite: true,
maptilerSatellite: true,
openStreetMap: true, openStreetMap: true,
openTopoMap: true, openTopoMap: true,
openHikingMap: true, openHikingMap: true,
@@ -1063,9 +1040,8 @@ export const defaultOverlayTree: LayerTreeType = {
waymarkedTrailsHorseRiding: false, waymarkedTrailsHorseRiding: false,
waymarkedTrailsWinter: false, waymarkedTrailsWinter: false,
}, },
bikerouterGravel: false,
cyclOSMlite: false, cyclOSMlite: false,
mapterhornHillshade: false, bikerouterGravel: false,
openRailwayMap: false, openRailwayMap: false,
}, },
countries: { countries: {
@@ -1141,7 +1117,7 @@ export type CustomLayer = {
maxZoom: number; maxZoom: number;
layerType: 'basemap' | 'overlay'; layerType: 'basemap' | 'overlay';
resourceType: 'raster' | 'vector'; resourceType: 'raster' | 'vector';
value: string | maplibregl.StyleSpecification; value: string | {};
}; };
type OverpassQueryData = { type OverpassQueryData = {
@@ -1458,16 +1434,3 @@ export const overpassQueryData: Record<string, OverpassQueryData> = {
symbol: 'Anchor', symbol: 'Anchor',
}, },
}; };
export const terrainSources: { [key: string]: RasterDEMSourceSpecification } = {
'maptiler-dem': {
type: 'raster-dem',
url: `https://api.maptiler.com/tiles/terrain-rgb-v2/tiles.json?key=${maptilerKeyPlaceHolder}`,
},
mapterhorn: {
type: 'raster-dem',
url: 'https://tiles.mapterhorn.com/tilejson.json',
},
};
export const defaultTerrainSource = 'maptiler-dem';

View File

@@ -18,7 +18,7 @@
href="https://github.com/gpxstudio/gpx.studio/blob/main/LICENSE" href="https://github.com/gpxstudio/gpx.studio/blob/main/LICENSE"
target="_blank" target="_blank"
> >
MIT © 2026 gpx.studio MIT © 2025 gpx.studio
</Button> </Button>
<LanguageSelect class="w-40 mt-3" /> <LanguageSelect class="w-40 mt-3" />
</div> </div>

View File

@@ -31,13 +31,13 @@
<Card.Root <Card.Root
class="h-full {orientation === 'vertical' class="h-full {orientation === 'vertical'
? 'min-w-40 sm:min-w-44' ? 'min-w-40 sm:min-w-44 text-sm sm:text-base'
: 'w-full h-10'} border-none shadow-none p-0 text-sm sm:text-base" : 'w-full'} border-none shadow-none p-0"
> >
<Card.Content <Card.Content
class="h-full flex {orientation === 'vertical' class="h-full flex {orientation === 'vertical'
? 'flex-col justify-center' ? 'flex-col justify-center'
: 'flex-row w-full justify-evenly'} gap-4 p-0" : 'flex-row w-full justify-between'} gap-4 p-0"
> >
<Tooltip label={i18n._('quantities.distance')}> <Tooltip label={i18n._('quantities.distance')}>
<span class="flex flex-row items-center"> <span class="flex flex-row items-center">

View File

@@ -8,7 +8,7 @@
...others ...others
}: { }: {
iconOnly?: boolean; iconOnly?: boolean;
company?: 'gpx.studio' | 'maptiler' | 'github' | 'crowdin' | 'facebook' | 'reddit'; company?: 'gpx.studio' | 'mapbox' | 'github' | 'crowdin' | 'facebook' | 'reddit';
[key: string]: any; [key: string]: any;
} = $props(); } = $props();
</script> </script>
@@ -19,10 +19,10 @@
alt="Logo of gpx.studio." alt="Logo of gpx.studio."
{...others} {...others}
/> />
{:else if company === 'maptiler'} {:else if company === 'mapbox'}
<img <img
src="{base}/maptiler-logo{mode.current === 'dark' ? '-dark' : ''}.svg" src="{base}/mapbox-logo-{mode.current === 'dark' ? 'white' : 'black'}.svg"
alt="Logo of Maptiler." alt="Logo of Mapbox."
{...others} {...others}
/> />
{:else if company === 'github'} {:else if company === 'github'}

View File

@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import maptilerTopoMap from '$lib/assets/img/home/maptiler-topo.png?enhanced'; import mapboxOutdoorsMap from '$lib/assets/img/home/mapbox-outdoors.png?enhanced';
import waymarkedMap from '$lib/assets/img/home/waymarked.png?enhanced'; import waymarkedMap from '$lib/assets/img/home/waymarked.png?enhanced';
</script> </script>
<div class="relative h-80 aspect-square rounded-2xl shadow-xl overflow-clip"> <div class="relative h-80 aspect-square rounded-2xl shadow-xl overflow-clip">
<enhanced:img src={maptilerTopoMap} alt="MapTiler Topo map screenshot." class="absolute" /> <enhanced:img src={mapboxOutdoorsMap} alt="Mapbox Outdoors map screenshot." class="absolute" />
<enhanced:img <enhanced:img
src={waymarkedMap} src={waymarkedMap}
alt="Waymarked Trails map screenshot." alt="Waymarked Trails map screenshot."

View File

@@ -18,7 +18,7 @@
Construction, Construction,
} from '@lucide/svelte'; } from '@lucide/svelte';
import type { Readable, Writable } from 'svelte/store'; import type { Readable, Writable } from 'svelte/store';
import type { Coordinates, GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx'; import type { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { ElevationProfile } from '$lib/components/elevation-profile/elevation-profile'; import { ElevationProfile } from '$lib/components/elevation-profile/elevation-profile';
@@ -28,14 +28,12 @@
let { let {
gpxStatistics, gpxStatistics,
slicedGPXStatistics, slicedGPXStatistics,
hoveredPoint,
additionalDatasets, additionalDatasets,
elevationFill, elevationFill,
showControls = true, showControls = true,
}: { }: {
gpxStatistics: Readable<GPXStatisticsGroup>; gpxStatistics: Readable<GPXStatisticsGroup>;
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>; slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
hoveredPoint: Writable<Coordinates | null>;
additionalDatasets: Writable<string[]>; additionalDatasets: Writable<string[]>;
elevationFill: Writable<'slope' | 'surface' | 'highway' | undefined>; elevationFill: Writable<'slope' | 'surface' | 'highway' | undefined>;
showControls?: boolean; showControls?: boolean;
@@ -49,7 +47,6 @@
elevationProfile = new ElevationProfile( elevationProfile = new ElevationProfile(
gpxStatistics, gpxStatistics,
slicedGPXStatistics, slicedGPXStatistics,
hoveredPoint,
additionalDatasets, additionalDatasets,
elevationFill, elevationFill,
canvas, canvas,
@@ -64,7 +61,7 @@
}); });
</script> </script>
<div class="h-full grow min-w-0 min-h-0 relative"> <div class="h-full grow min-w-0 relative py-2">
<canvas bind:this={overlay} class="w-full h-full absolute pointer-events-none"></canvas> <canvas bind:this={overlay} class="w-full h-full absolute pointer-events-none"></canvas>
<canvas bind:this={canvas} class="w-full h-full absolute"></canvas> <canvas bind:this={canvas} class="w-full h-full absolute"></canvas>
{#if showControls} {#if showControls}

View File

@@ -20,8 +20,10 @@ import Chart, {
type ScriptableLineSegmentContext, type ScriptableLineSegmentContext,
type TooltipItem, type TooltipItem,
} from 'chart.js/auto'; } from 'chart.js/auto';
import mapboxgl from 'mapbox-gl';
import { get, type Readable, type Writable } from 'svelte/store'; import { get, type Readable, type Writable } from 'svelte/store';
import type { Coordinates, GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx'; import { map } from '$lib/components/map/map';
import type { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
import { mode } from 'mode-watcher'; import { mode } from 'mode-watcher';
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors'; import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors';
@@ -40,7 +42,7 @@ interface ElevationProfilePoint {
length: number; length: number;
}; };
extensions: Record<string, any>; extensions: Record<string, any>;
coordinates: Coordinates; coordinates: [number, number];
index: number; index: number;
} }
@@ -48,19 +50,18 @@ export class ElevationProfile {
private _chart: Chart | null = null; private _chart: Chart | null = null;
private _canvas: HTMLCanvasElement; private _canvas: HTMLCanvasElement;
private _overlay: HTMLCanvasElement; private _overlay: HTMLCanvasElement;
private _marker: mapboxgl.Marker | null = null;
private _dragging = false; private _dragging = false;
private _panning = false; private _panning = false;
private _gpxStatistics: Readable<GPXStatisticsGroup>; private _gpxStatistics: Readable<GPXStatisticsGroup>;
private _slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>; private _slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
private _hoveredPoint: Writable<Coordinates | null>;
private _additionalDatasets: Readable<string[]>; private _additionalDatasets: Readable<string[]>;
private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>; private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>;
constructor( constructor(
gpxStatistics: Readable<GPXStatisticsGroup>, gpxStatistics: Readable<GPXStatisticsGroup>,
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>, slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>,
hoveredPoint: Writable<Coordinates | null>,
additionalDatasets: Readable<string[]>, additionalDatasets: Readable<string[]>,
elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>, elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>,
canvas: HTMLCanvasElement, canvas: HTMLCanvasElement,
@@ -68,12 +69,17 @@ export class ElevationProfile {
) { ) {
this._gpxStatistics = gpxStatistics; this._gpxStatistics = gpxStatistics;
this._slicedGPXStatistics = slicedGPXStatistics; this._slicedGPXStatistics = slicedGPXStatistics;
this._hoveredPoint = hoveredPoint;
this._additionalDatasets = additionalDatasets; this._additionalDatasets = additionalDatasets;
this._elevationFill = elevationFill; this._elevationFill = elevationFill;
this._canvas = canvas; this._canvas = canvas;
this._overlay = overlay; this._overlay = overlay;
let element = document.createElement('div');
element.className = 'h-4 w-4 rounded-full bg-cyan-500 border-2 border-white';
this._marker = new mapboxgl.Marker({
element,
});
import('chartjs-plugin-zoom').then((module) => { import('chartjs-plugin-zoom').then((module) => {
Chart.register(module.default); Chart.register(module.default);
this.initialize(); this.initialize();
@@ -156,10 +162,14 @@ export class ElevationProfile {
label: (context: TooltipItem<'line'>) => { label: (context: TooltipItem<'line'>) => {
let point = context.raw as ElevationProfilePoint; let point = context.raw as ElevationProfilePoint;
if (context.datasetIndex === 0) { if (context.datasetIndex === 0) {
const map_ = get(map);
if (map_ && this._marker) {
if (this._dragging) { if (this._dragging) {
this._hoveredPoint.set(null); this._marker.remove();
} else { } else {
this._hoveredPoint.set(point.coordinates); this._marker.setLngLat(point.coordinates);
this._marker.addTo(map_);
}
} }
return `${i18n._('quantities.elevation')}: ${getElevationWithUnits(point.y, false)}`; return `${i18n._('quantities.elevation')}: ${getElevationWithUnits(point.y, false)}`;
} else if (context.datasetIndex === 1) { } else if (context.datasetIndex === 1) {
@@ -302,7 +312,10 @@ export class ElevationProfile {
events: ['mouseout'], events: ['mouseout'],
afterEvent: (chart: Chart, args: { event: ChartEvent }) => { afterEvent: (chart: Chart, args: { event: ChartEvent }) => {
if (args.event.type === 'mouseout') { if (args.event.type === 'mouseout') {
this._hoveredPoint.set(null); const map_ = get(map);
if (map_ && this._marker) {
this._marker.remove();
}
} }
}, },
}, },
@@ -624,5 +637,8 @@ export class ElevationProfile {
this._chart.destroy(); this._chart.destroy();
this._chart = null; this._chart = null;
} }
if (this._marker) {
this._marker.remove();
}
} }
} }

View File

@@ -16,7 +16,7 @@
import { setMode } from 'mode-watcher'; import { setMode } from 'mode-watcher';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { fileStateCollection } from '$lib/logic/file-state'; import { fileStateCollection } from '$lib/logic/file-state';
import { gpxStatistics, hoveredPoint, slicedGPXStatistics } from '$lib/logic/statistics'; import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
import { loadFile } from '$lib/logic/file-actions'; import { loadFile } from '$lib/logic/file-actions';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import { untrack } from 'svelte'; import { untrack } from 'svelte';
@@ -102,7 +102,7 @@
<div class="grow relative"> <div class="grow relative">
<Map <Map
class="h-full {$fileStateCollection.size > 1 ? 'horizontal' : ''}" class="h-full {$fileStateCollection.size > 1 ? 'horizontal' : ''}"
maptilerKey={options.key} accessToken={options.token}
geocoder={false} geocoder={false}
geolocate={true} geolocate={true}
hash={useHash} hash={useHash}
@@ -130,7 +130,6 @@
<ElevationProfile <ElevationProfile
{gpxStatistics} {gpxStatistics}
{slicedGPXStatistics} {slicedGPXStatistics}
{hoveredPoint}
{additionalDatasets} {additionalDatasets}
{elevationFill} {elevationFill}
showControls={options.elevation.controls} showControls={options.elevation.controls}

View File

@@ -22,7 +22,7 @@
getCleanedEmbeddingOptions, getCleanedEmbeddingOptions,
getMergedEmbeddingOptions, getMergedEmbeddingOptions,
} from './embedding'; } from './embedding';
import { PUBLIC_MAPTILER_KEY } from '$env/static/public'; import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import Embedding from './Embedding.svelte'; import Embedding from './Embedding.svelte';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { base } from '$app/paths'; import { base } from '$app/paths';
@@ -32,7 +32,7 @@
let options = $state( let options = $state(
getMergedEmbeddingOptions( getMergedEmbeddingOptions(
{ {
key: 'YOUR_MAPTILER_KEY', token: 'YOUR_MAPBOX_TOKEN',
theme: mode.current, theme: mode.current,
}, },
defaultEmbeddingOptions defaultEmbeddingOptions
@@ -46,10 +46,10 @@
let iframeOptions = $derived( let iframeOptions = $derived(
getMergedEmbeddingOptions( getMergedEmbeddingOptions(
{ {
key: token:
options.key.length === 0 || options.key === 'YOUR_MAPTILER_KEY' options.token.length === 0 || options.token === 'YOUR_MAPBOX_TOKEN'
? PUBLIC_MAPTILER_KEY ? PUBLIC_MAPBOX_TOKEN
: options.key, : options.token,
files: files.split(',').filter((url) => url.length > 0), files: files.split(',').filter((url) => url.length > 0),
ids: driveIds.split(',').filter((id) => id.length > 0), ids: driveIds.split(',').filter((id) => id.length > 0),
elevation: { elevation: {
@@ -102,8 +102,8 @@
</Card.Header> </Card.Header>
<Card.Content> <Card.Content>
<fieldset class="flex flex-col gap-3"> <fieldset class="flex flex-col gap-3">
<Label for="key">{i18n._('embedding.maptiler_key')}</Label> <Label for="token">{i18n._('embedding.mapbox_token')}</Label>
<Input id="key" type="text" class="h-8" bind:value={options.key} /> <Input id="token" type="text" class="h-8" bind:value={options.token} />
<Label for="file_urls">{i18n._('embedding.file_urls')}</Label> <Label for="file_urls">{i18n._('embedding.file_urls')}</Label>
<Input id="file_urls" type="text" class="h-8" bind:value={files} /> <Input id="file_urls" type="text" class="h-8" bind:value={files} />
<Label for="drive_ids">{i18n._('embedding.drive_ids')}</Label> <Label for="drive_ids">{i18n._('embedding.drive_ids')}</Label>

View File

@@ -1,8 +1,8 @@
import { PUBLIC_MAPTILER_KEY } from '$env/static/public'; import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import { basemaps } from '$lib/assets/layers'; import { basemaps } from '$lib/assets/layers';
export type EmbeddingOptions = { export type EmbeddingOptions = {
key: string; token: string;
files: string[]; files: string[];
ids: string[]; ids: string[];
basemap: string; basemap: string;
@@ -26,10 +26,10 @@ export type EmbeddingOptions = {
}; };
export const defaultEmbeddingOptions = { export const defaultEmbeddingOptions = {
key: '', token: '',
files: [], files: [],
ids: [], ids: [],
basemap: 'maptilerTopo', basemap: 'mapboxOutdoors',
elevation: { elevation: {
show: true, show: true,
height: 170, height: 170,
@@ -107,7 +107,7 @@ export function getURLForGoogleDriveFile(fileId: string): string {
export function convertOldEmbeddingOptions(options: URLSearchParams): any { export function convertOldEmbeddingOptions(options: URLSearchParams): any {
let newOptions: any = { let newOptions: any = {
key: PUBLIC_MAPTILER_KEY, token: PUBLIC_MAPBOX_TOKEN,
files: [], files: [],
ids: [], ids: [],
}; };
@@ -123,7 +123,7 @@ export function convertOldEmbeddingOptions(options: URLSearchParams): any {
if (options.has('source')) { if (options.has('source')) {
let basemap = options.get('source')!; let basemap = options.get('source')!;
if (basemap === 'satellite') { if (basemap === 'satellite') {
newOptions.basemap = 'maptilerSatellite'; newOptions.basemap = 'mapboxSatellite';
} else if (basemap === 'otm') { } else if (basemap === 'otm') {
newOptions.basemap = 'openTopoMap'; newOptions.basemap = 'openTopoMap';
} else if (basemap === 'ohm') { } else if (basemap === 'ohm') {

View File

@@ -34,10 +34,11 @@
import { editStyle } from '$lib/components/file-list/style/utils.svelte'; import { editStyle } from '$lib/components/file-list/style/utils.svelte';
import { getSymbolKey, symbols } from '$lib/assets/symbols'; import { getSymbolKey, symbols } from '$lib/assets/symbols';
import { selection, copied, cut } from '$lib/logic/selection'; import { selection, copied, cut } from '$lib/logic/selection';
import { map } from '$lib/components/map/map';
import { fileActions, pasteSelection } from '$lib/logic/file-actions'; import { fileActions, pasteSelection } from '$lib/logic/file-actions';
import { allHidden } from '$lib/logic/hidden'; import { allHidden } from '$lib/logic/hidden';
import { boundsManager } from '$lib/logic/bounds'; import { boundsManager } from '$lib/logic/bounds';
import { gpxColors, gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers'; import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
import { fileStateCollection } from '$lib/logic/file-state'; import { fileStateCollection } from '$lib/logic/file-state';
import { waypointPopup } from '$lib/components/map/gpx-layer/gpx-layer-popup'; import { waypointPopup } from '$lib/components/map/gpx-layer/gpx-layer-popup';
import { allowedPastes } from './sortable-file-list'; import { allowedPastes } from './sortable-file-list';
@@ -57,11 +58,19 @@
let singleSelection = $derived($selection.size === 1); let singleSelection = $derived($selection.size === 1);
let nodeColors: string[] = $derived.by(() => { let nodeColors: string[] = $state([]);
$effect.pre(() => {
let colors: string[] = []; let colors: string[] = [];
if (node) { if (node && $map) {
if (node instanceof GPXFile) { if (node instanceof GPXFile) {
let defaultColor = $gpxColors.get(item.getFileId()); let defaultColor = undefined;
let layer = gpxLayers.getLayer(item.getFileId());
if (layer) {
defaultColor = layer.layerColor;
}
let style = node.getStyle(defaultColor); let style = node.getStyle(defaultColor);
colors = style.color; colors = style.color;
} else if (node instanceof Track) { } else if (node instanceof Track) {
@@ -74,14 +83,14 @@
colors.push(style['gpx_style:color']); colors.push(style['gpx_style:color']);
} }
if (colors.length === 0) { if (colors.length === 0) {
let defaultColor = $gpxColors.get(item.getFileId()); let layer = gpxLayers.getLayer(item.getFileId());
if (defaultColor) { if (layer) {
colors.push(defaultColor); colors.push(layer.layerColor);
} }
} }
} }
} }
return colors; nodeColors = colors;
}); });
let symbolKey = $derived(node instanceof Waypoint ? getSymbolKey(node.sym) : undefined); let symbolKey = $derived(node instanceof Waypoint ? getSymbolKey(node.sym) : undefined);

View File

@@ -5,16 +5,6 @@
map.onLoad((map_) => { map.onLoad((map_) => {
map_.on('contextmenu', (e) => { map_.on('contextmenu', (e) => {
if (
map_.queryRenderedFeatures(e.point, {
layers: map_
.getLayersOrder()
.filter((layerId) => layerId.startsWith('routing-controls')),
}).length
) {
// Clicked on routing control, ignoring
return;
}
trackpointPopup?.setItem({ trackpointPopup?.setItem({
item: new TrackPoint({ item: new TrackPoint({
attributes: { attributes: {

View File

@@ -1,25 +1,30 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from 'svelte'; 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 { Button } from '$lib/components/ui/button';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import { page } from '$app/state'; import { page } from '$app/state';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import { PUBLIC_MAPTILER_KEY } from '$env/static/public';
let { let {
maptilerKey = PUBLIC_MAPTILER_KEY, accessToken = PUBLIC_MAPBOX_TOKEN,
geolocate = true, geolocate = true,
geocoder = true, geocoder = true,
hash = true, hash = true,
class: className = '', class: className = '',
}: { }: {
maptilerKey?: string; accessToken?: string;
geolocate?: boolean; geolocate?: boolean;
geocoder?: boolean; geocoder?: boolean;
hash?: boolean; hash?: boolean;
class?: string; class?: string;
} = $props(); } = $props();
mapboxgl.accessToken = accessToken;
let webgl2Supported = $state(true); let webgl2Supported = $state(true);
let embeddedApp = $state(false); let embeddedApp = $state(false);
@@ -43,7 +48,7 @@
language = 'en'; language = 'en';
} }
map.init(maptilerKey, language, hash, geocoder, geolocate); map.init(PUBLIC_MAPBOX_TOKEN, language, hash, geocoder, geolocate);
}); });
onDestroy(() => { onDestroy(() => {
@@ -76,21 +81,21 @@
<style lang="postcss"> <style lang="postcss">
@reference "../../../app.css"; @reference "../../../app.css";
div :global(.maplibregl-map) { div :global(.mapboxgl-map) {
@apply font-sans; @apply font-sans;
} }
div :global(.maplibregl-ctrl-top-right > .maplibregl-ctrl) { div :global(.mapboxgl-ctrl-top-right > .mapboxgl-ctrl) {
@apply shadow-md; @apply shadow-md;
@apply bg-background; @apply bg-background;
@apply text-foreground; @apply text-foreground;
} }
div :global(.maplibregl-ctrl-icon) { div :global(.mapboxgl-ctrl-icon) {
@apply dark:brightness-[4.7]; @apply dark:brightness-[4.7];
} }
div :global(.maplibregl-ctrl-geocoder) { div :global(.mapboxgl-ctrl-geocoder) {
@apply flex; @apply flex;
@apply flex-row; @apply flex-row;
@apply w-fit; @apply w-fit;
@@ -105,45 +110,36 @@
@apply text-foreground; @apply text-foreground;
} }
div :global(.maplibregl-ctrl-geocoder .suggestions > li > a) { div :global(.mapboxgl-ctrl-geocoder .suggestions > li > a) {
@apply text-foreground; @apply text-foreground;
@apply hover:text-accent-foreground; @apply hover:text-accent-foreground;
@apply hover:bg-accent; @apply hover:bg-accent;
} }
div :global(.maplibregl-ctrl-geocoder .suggestions > .active > a) { div :global(.mapboxgl-ctrl-geocoder .suggestions > .active > a) {
@apply bg-background; @apply bg-background;
} }
div :global(.maplibregl-ctrl-geocoder--button) { div :global(.mapboxgl-ctrl-geocoder--button) {
@apply bg-transparent; @apply bg-transparent;
@apply hover:bg-transparent; @apply hover:bg-transparent;
} }
div :global(.maplibregl-ctrl-geocoder--icon) { div :global(.mapboxgl-ctrl-geocoder--icon) {
@apply fill-foreground; @apply fill-foreground;
@apply hover:fill-accent-foreground; @apply hover:fill-accent-foreground;
} }
div :global(.maplibregl-ctrl-geocoder--icon-search) { div :global(.mapboxgl-ctrl-geocoder--icon-search) {
@apply relative; @apply relative;
@apply top-0; @apply top-0;
@apply left-0; @apply left-0;
@apply my-2;
@apply w-[29px]; @apply w-[29px];
} }
div :global(.maplibregl-ctrl-geocoder--icon-loading) { div :global(.mapboxgl-ctrl-geocoder--input) {
@apply -mt-1;
@apply mb-0;
}
div :global(.maplibregl-ctrl-geocoder--icon-close) {
@apply my-0;
}
div :global(.maplibregl-ctrl-geocoder--input) {
@apply relative; @apply relative;
@apply h-8;
@apply w-64; @apply w-64;
@apply py-0; @apply py-0;
@apply pl-2; @apply pl-2;
@@ -153,12 +149,12 @@
@apply text-foreground; @apply text-foreground;
} }
div :global(.maplibregl-ctrl-geocoder--collapsed .maplibregl-ctrl-geocoder--input) { div :global(.mapboxgl-ctrl-geocoder--collapsed .mapboxgl-ctrl-geocoder--input) {
@apply w-0; @apply w-0;
@apply p-0; @apply p-0;
} }
div :global(.maplibregl-ctrl-top-right) { div :global(.mapboxgl-ctrl-top-right) {
@apply z-40; @apply z-40;
@apply flex; @apply flex;
@apply flex-col; @apply flex-col;
@@ -167,76 +163,77 @@
@apply overflow-hidden; @apply overflow-hidden;
} }
.horizontal :global(.maplibregl-ctrl-bottom-left) { .horizontal :global(.mapboxgl-ctrl-bottom-left) {
@apply bottom-[42px]; @apply bottom-[42px];
} }
.horizontal :global(.maplibregl-ctrl-bottom-right) { .horizontal :global(.mapboxgl-ctrl-bottom-right) {
@apply bottom-[42px]; @apply bottom-[42px];
} }
div :global(.maplibregl-ctrl-attrib) { div :global(.mapboxgl-ctrl-attrib) {
@apply dark:bg-transparent; @apply dark:bg-transparent;
} }
div :global(.maplibregl-compact-show.maplibregl-ctrl-attrib) { div :global(.mapboxgl-compact-show.mapboxgl-ctrl-attrib) {
@apply dark:bg-background; @apply dark:bg-background;
} }
div :global(.maplibregl-ctrl-attrib-button) { div :global(.mapboxgl-ctrl-attrib-button) {
@apply dark:bg-foreground; @apply dark:bg-foreground;
} }
div :global(.maplibregl-compact-show .maplibregl-ctrl-attrib-button) { div :global(.mapboxgl-compact-show .mapboxgl-ctrl-attrib-button) {
@apply dark:bg-foreground; @apply dark:bg-foreground;
} }
div :global(.maplibregl-ctrl-attrib a) { div :global(.mapboxgl-ctrl-attrib a) {
@apply text-foreground; @apply text-foreground;
} }
div :global(.maplibregl-popup) { div :global(.mapboxgl-popup) {
@apply w-fit;
@apply z-50; @apply z-50;
} }
div :global(.maplibregl-popup-content) { div :global(.mapboxgl-popup-content) {
@apply p-0; @apply p-0;
@apply bg-transparent; @apply bg-transparent;
@apply shadow-none; @apply shadow-none;
} }
div :global(.maplibregl-popup-anchor-top .maplibregl-popup-tip) { div :global(.mapboxgl-popup-anchor-top .mapboxgl-popup-tip) {
@apply border-b-background; @apply border-b-background;
} }
div :global(.maplibregl-popup-anchor-top-left .maplibregl-popup-tip) { div :global(.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip) {
@apply border-b-background; @apply border-b-background;
} }
div :global(.maplibregl-popup-anchor-top-right .maplibregl-popup-tip) { div :global(.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip) {
@apply border-b-background; @apply border-b-background;
} }
div :global(.maplibregl-popup-anchor-bottom .maplibregl-popup-tip) { div :global(.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip) {
@apply border-t-background; @apply border-t-background;
@apply drop-shadow-md; @apply drop-shadow-md;
} }
div :global(.maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip) { div :global(.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip) {
@apply border-t-background; @apply border-t-background;
@apply drop-shadow-md; @apply drop-shadow-md;
} }
div :global(.maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip) { div :global(.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip) {
@apply border-t-background; @apply border-t-background;
@apply drop-shadow-md; @apply drop-shadow-md;
} }
div :global(.maplibregl-popup-anchor-left .maplibregl-popup-tip) { div :global(.mapboxgl-popup-anchor-left .mapboxgl-popup-tip) {
@apply border-r-background; @apply border-r-background;
} }
div :global(.maplibregl-popup-anchor-right .maplibregl-popup-tip) { div :global(.mapboxgl-popup-anchor-right .mapboxgl-popup-tip) {
@apply border-l-background; @apply border-l-background;
} }
</style> </style>

View File

@@ -17,7 +17,7 @@
let control: CustomControl | null = null; let control: CustomControl | null = null;
onMount(() => { onMount(() => {
map.onLoad((map: maplibregl.Map) => { map.onLoad((map: mapboxgl.Map) => {
if (position.includes('right')) container.classList.add('float-right'); if (position.includes('right')) container.classList.add('float-right');
else container.classList.add('float-left'); else container.classList.add('float-left');
container.classList.remove('hidden'); container.classList.remove('hidden');

View File

@@ -1,4 +1,4 @@
import { type Map, type IControl } from 'maplibre-gl'; import { type Map, type IControl } from 'mapbox-gl';
export default class CustomControl implements IControl { export default class CustomControl implements IControl {
_map: Map | undefined; _map: Map | undefined;

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onDestroy } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers'; import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
import { DistanceMarkers } from '$lib/components/map/gpx-layer/distance-markers'; import { DistanceMarkers } from '$lib/components/map/gpx-layer/distance-markers';
import { StartEndMarkers } from '$lib/components/map/gpx-layer/start-end-markers'; import { StartEndMarkers } from '$lib/components/map/gpx-layer/start-end-markers';
@@ -9,10 +9,13 @@
let distanceMarkers: DistanceMarkers; let distanceMarkers: DistanceMarkers;
let startEndMarkers: StartEndMarkers; let startEndMarkers: StartEndMarkers;
map.onLoad((map_) => { onMount(() => {
gpxLayers.init(); gpxLayers.init();
startEndMarkers = new StartEndMarkers(); startEndMarkers = new StartEndMarkers();
distanceMarkers = new DistanceMarkers(); distanceMarkers = new DistanceMarkers();
});
map.onLoad((map_) => {
createPopups(map_); createPopups(map_);
}); });

View File

@@ -41,7 +41,6 @@
<Button <Button
size="sm" size="sm"
variant="outline" 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)}`} 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" target="_blank"
> >

View File

@@ -1,11 +1,10 @@
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { gpxStatistics } from '$lib/logic/statistics'; import { gpxStatistics } from '$lib/logic/statistics';
import { getConvertedDistanceToKilometers } from '$lib/units'; import { getConvertedDistanceToKilometers } from '$lib/units';
import type { GeoJSONSource } from 'mapbox-gl';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import { allHidden } from '$lib/logic/hidden'; import { allHidden } from '$lib/logic/hidden';
import type { GeoJSONSource } from 'maplibre-gl';
import { ANCHOR_LAYER_KEY } from '../style';
const { distanceMarkers, distanceUnits } = settings; const { distanceMarkers, distanceUnits } = settings;
@@ -23,7 +22,7 @@ export class DistanceMarkers {
this.unsubscribes.push( this.unsubscribes.push(
map.subscribe((map_) => { map.subscribe((map_) => {
if (map_) { if (map_) {
map_.on('style.load', this.updateBinded); map_.on('style.import.load', this.updateBinded);
} }
}) })
); );
@@ -45,8 +44,7 @@ export class DistanceMarkers {
}); });
} }
if (!map_.getLayer('distance-markers')) { if (!map_.getLayer('distance-markers')) {
map_.addLayer( map_.addLayer({
{
id: 'distance-markers', id: 'distance-markers',
type: 'symbol', type: 'symbol',
source: 'distance-markers', source: 'distance-markers',
@@ -81,9 +79,9 @@ export class DistanceMarkers {
'text-halo-width': 2, 'text-halo-width': 2,
'text-halo-color': 'white', 'text-halo-color': 'white',
}, },
}, });
ANCHOR_LAYER_KEY.distanceMarkers } else {
); map_.moveLayer('distance-markers');
} }
} else { } else {
if (map_.getLayer('distance-markers')) { if (map_.getLayer('distance-markers')) {

View File

@@ -3,14 +3,13 @@ import { MapPopup } from '$lib/components/map/map-popup';
export let waypointPopup: MapPopup | null = null; export let waypointPopup: MapPopup | null = null;
export let trackpointPopup: MapPopup | null = null; export let trackpointPopup: MapPopup | null = null;
export function createPopups(map: maplibregl.Map) { export function createPopups(map: mapboxgl.Map) {
removePopups(); removePopups();
waypointPopup = new MapPopup(map, { waypointPopup = new MapPopup(map, {
closeButton: false, closeButton: false,
focusAfterOpen: false, focusAfterOpen: false,
maxWidth: undefined, maxWidth: undefined,
offset: { offset: {
center: [0, 0],
top: [0, 0], top: [0, 0],
'top-left': [0, 0], 'top-left': [0, 0],
'top-right': [0, 0], 'top-right': [0, 0],

View File

@@ -1,10 +1,5 @@
import { get, type Readable } from 'svelte/store'; import { get, type Readable } from 'svelte/store';
import maplibregl, { import mapboxgl, { type FilterSpecification } from 'mapbox-gl';
type GeoJSONSource,
type FilterSpecification,
type MapLayerMouseEvent,
type MapLayerTouchEvent,
} from 'maplibre-gl';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import { waypointPopup, trackpointPopup } from './gpx-layer-popup'; import { waypointPopup, trackpointPopup } from './gpx-layer-popup';
import { import {
@@ -15,7 +10,7 @@ import {
ListFileItem, ListFileItem,
ListRootItem, ListRootItem,
} from '$lib/components/file-list/file-list'; } from '$lib/components/file-list/file-list';
import { getClosestLinePoint, getElevation, loadSVGIcon } from '$lib/utils'; import { getClosestLinePoint, getElevation } from '$lib/utils';
import { selectedWaypoint } from '$lib/components/toolbar/tools/waypoint/waypoint'; import { selectedWaypoint } from '$lib/components/toolbar/tools/waypoint/waypoint';
import { MapPin, Square } from 'lucide-static'; import { MapPin, Square } from 'lucide-static';
import { getSymbolKey, symbols } from '$lib/assets/symbols'; import { getSymbolKey, symbols } from '$lib/assets/symbols';
@@ -27,8 +22,6 @@ import { fileActionManager } from '$lib/logic/file-action-manager';
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors'; import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
import { gpxColors } from './gpx-layers';
const colors = [ const colors = [
'#ff0000', '#ff0000',
@@ -50,35 +43,16 @@ for (let color of colors) {
} }
// Get the color with the least amount of uses // Get the color with the least amount of uses
function getColor(fileId: string) { function getColor() {
let color = colors.reduce((a, b) => (colorCount[a] <= colorCount[b] ? a : b)); let color = colors.reduce((a, b) => (colorCount[a] <= colorCount[b] ? a : b));
colorCount[color]++; colorCount[color]++;
gpxColors.update((colors) => {
colors.set(fileId, color);
return colors;
});
return color; return color;
} }
function replaceColor(fileId: string, oldColor: string, newColor: string) { function decrementColor(color: 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)) { if (colorCount.hasOwnProperty(color)) {
colorCount[color]--; colorCount[color]--;
} }
gpxColors.update((colors) => {
colors.delete(fileId);
return colors;
});
} }
export function getSvgForSymbol(symbol?: string | undefined, layerColor?: string | undefined) { export function getSvgForSymbol(symbol?: string | undefined, layerColor?: string | undefined) {
@@ -120,38 +94,38 @@ export class GPXLayer {
selected: boolean = false; selected: boolean = false;
currentWaypointData: GeoJSON.FeatureCollection | null = null; currentWaypointData: GeoJSON.FeatureCollection | null = null;
draggedWaypointIndex: number | null = null; draggedWaypointIndex: number | null = null;
draggingStartingPosition: maplibregl.Point = new maplibregl.Point(0, 0); draggingStartingPosition: mapboxgl.Point = new mapboxgl.Point(0, 0);
unsubscribe: Function[] = []; unsubscribe: Function[] = [];
updateBinded: () => void = this.update.bind(this); updateBinded: () => void = this.update.bind(this);
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this); layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this); layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
layerOnMouseMoveBinded: (e: any) => void = this.layerOnMouseMove.bind(this); layerOnMouseMoveBinded: (e: any) => void = this.layerOnMouseMove.bind(this);
layerOnClickBinded: (e: MapLayerMouseEvent) => void = this.layerOnClick.bind(this); layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
layerOnContextMenuBinded: (e: MapLayerMouseEvent) => void = this.layerOnContextMenu.bind(this); layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this);
waypointLayerOnMouseEnterBinded: (e: MapLayerMouseEvent) => void = waypointLayerOnMouseEnterBinded: (e: mapboxgl.MapMouseEvent) => void =
this.waypointLayerOnMouseEnter.bind(this); this.waypointLayerOnMouseEnter.bind(this);
waypointLayerOnMouseLeaveBinded: (e: MapLayerMouseEvent) => void = waypointLayerOnMouseLeaveBinded: (e: mapboxgl.MapMouseEvent) => void =
this.waypointLayerOnMouseLeave.bind(this); this.waypointLayerOnMouseLeave.bind(this);
waypointLayerOnClickBinded: (e: MapLayerMouseEvent) => void = waypointLayerOnClickBinded: (e: mapboxgl.MapMouseEvent) => void =
this.waypointLayerOnClick.bind(this); this.waypointLayerOnClick.bind(this);
waypointLayerOnMouseDownBinded: (e: MapLayerMouseEvent) => void = waypointLayerOnMouseDownBinded: (e: mapboxgl.MapMouseEvent) => void =
this.waypointLayerOnMouseDown.bind(this); this.waypointLayerOnMouseDown.bind(this);
waypointLayerOnTouchStartBinded: (e: MapLayerTouchEvent) => void = waypointLayerOnTouchStartBinded: (e: mapboxgl.MapTouchEvent) => void =
this.waypointLayerOnTouchStart.bind(this); this.waypointLayerOnTouchStart.bind(this);
waypointLayerOnMouseMoveBinded: (e: MapLayerMouseEvent | MapLayerTouchEvent) => void = waypointLayerOnMouseMoveBinded: (e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) => void =
this.waypointLayerOnMouseMove.bind(this); this.waypointLayerOnMouseMove.bind(this);
waypointLayerOnMouseUpBinded: (e: MapLayerMouseEvent | MapLayerTouchEvent) => void = waypointLayerOnMouseUpBinded: (e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) => void =
this.waypointLayerOnMouseUp.bind(this); this.waypointLayerOnMouseUp.bind(this);
constructor(fileId: string, file: Readable<GPXFileWithStatistics | undefined>) { constructor(fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
this.fileId = fileId; this.fileId = fileId;
this.file = file; this.file = file;
this.layerColor = getColor(fileId); this.layerColor = getColor();
this.unsubscribe.push( this.unsubscribe.push(
map.subscribe(($map) => { map.subscribe(($map) => {
if ($map) { if ($map) {
$map.on('style.load', this.updateBinded); $map.on('style.import.load', this.updateBinded);
this.update(); this.update();
} }
}) })
@@ -174,9 +148,8 @@ export class GPXLayer {
update() { update() {
const _map = get(map); const _map = get(map);
const layerEventManager = map.layerEventManager;
let file = get(this.file)?.file; let file = get(this.file)?.file;
if (!_map || !layerEventManager || !file) { if (!_map || !file) {
return; return;
} }
@@ -185,14 +158,14 @@ export class GPXLayer {
file._data.style.color && file._data.style.color &&
this.layerColor !== `#${file._data.style.color}` this.layerColor !== `#${file._data.style.color}`
) { ) {
replaceColor(this.fileId, this.layerColor, `#${file._data.style.color}`); decrementColor(this.layerColor);
this.layerColor = `#${file._data.style.color}`; this.layerColor = `#${file._data.style.color}`;
} }
this.loadIcons(); this.loadIcons();
try { try {
let source = _map.getSource(this.fileId) as GeoJSONSource | undefined; let source = _map.getSource(this.fileId) as mapboxgl.GeoJSONSource | undefined;
if (source) { if (source) {
source.setData(this.getGeoJSON()); source.setData(this.getGeoJSON());
} else { } else {
@@ -203,8 +176,7 @@ export class GPXLayer {
} }
if (!_map.getLayer(this.fileId)) { if (!_map.getLayer(this.fileId)) {
_map.addLayer( _map.addLayer({
{
id: this.fileId, id: this.fileId,
type: 'line', type: 'line',
source: this.fileId, source: this.fileId,
@@ -217,30 +189,64 @@ export class GPXLayer {
'line-width': ['get', 'width'], 'line-width': ['get', 'width'],
'line-opacity': ['get', 'opacity'], 'line-opacity': ['get', 'opacity'],
}, },
},
ANCHOR_LAYER_KEY.tracks
);
layerEventManager.on('click', this.fileId, this.layerOnClickBinded);
layerEventManager.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
layerEventManager.on('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
layerEventManager.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
layerEventManager.on('mousemove', this.fileId, this.layerOnMouseMoveBinded);
}
let visibleTrackSegmentIds: string[] = [];
file.forEachSegment((segment, trackIndex, segmentIndex) => {
if (!segment._data.hidden) {
visibleTrackSegmentIds.push(`${trackIndex}-${segmentIndex}`);
}
}); });
const segmentFilter: FilterSpecification = [
'in',
['get', 'trackSegmentId'],
['literal', visibleTrackSegmentIds],
];
_map.setFilter(this.fileId, segmentFilter, { validate: false }); _map.on('click', this.fileId, this.layerOnClickBinded);
_map.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
_map.on('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
_map.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
_map.on('mousemove', this.fileId, this.layerOnMouseMoveBinded);
}
let waypointSource = _map.getSource(this.fileId + '-waypoints') as
| mapboxgl.GeoJSONSource
| undefined;
this.currentWaypointData = this.getWaypointsGeoJSON();
if (waypointSource) {
waypointSource.setData(this.currentWaypointData);
} else {
_map.addSource(this.fileId + '-waypoints', {
type: 'geojson',
data: this.currentWaypointData,
});
}
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.on(
'mouseenter',
this.fileId + '-waypoints',
this.waypointLayerOnMouseEnterBinded
);
_map.on(
'mouseleave',
this.fileId + '-waypoints',
this.waypointLayerOnMouseLeaveBinded
);
_map.on('click', this.fileId + '-waypoints', this.waypointLayerOnClickBinded);
_map.on(
'mousedown',
this.fileId + '-waypoints',
this.waypointLayerOnMouseDownBinded
);
_map.on(
'touchstart',
this.fileId + '-waypoints',
this.waypointLayerOnTouchStartBinded
);
}
if (get(directionMarkers)) { if (get(directionMarkers)) {
if (!_map.getLayer(this.fileId + '-direction')) { if (!_map.getLayer(this.fileId + '-direction')) {
@@ -266,73 +272,31 @@ export class GPXLayer {
'text-halo-color': 'white', 'text-halo-color': 'white',
}, },
}, },
ANCHOR_LAYER_KEY.directionMarkers _map.getLayer('distance-markers') ? 'distance-markers' : undefined
); );
} }
_map.setFilter(this.fileId + '-direction', segmentFilter, { validate: false });
} else { } else {
if (_map.getLayer(this.fileId + '-direction')) { if (_map.getLayer(this.fileId + '-direction')) {
_map.removeLayer(this.fileId + '-direction'); _map.removeLayer(this.fileId + '-direction');
} }
} }
let waypointSource = _map.getSource(this.fileId + '-waypoints') as let visibleTrackSegmentIds: string[] = [];
| GeoJSONSource file.forEachSegment((segment, trackIndex, segmentIndex) => {
| undefined; if (!segment._data.hidden) {
this.currentWaypointData = this.getWaypointsGeoJSON(); visibleTrackSegmentIds.push(`${trackIndex}-${segmentIndex}`);
if (waypointSource) {
waypointSource.setData(this.currentWaypointData);
} else {
_map.addSource(this.fileId + '-waypoints', {
type: 'geojson',
data: this.currentWaypointData,
promoteId: 'waypointIndex',
});
} }
});
const segmentFilter: FilterSpecification = [
'in',
['get', 'trackSegmentId'],
['literal', visibleTrackSegmentIds],
];
if (!_map.getLayer(this.fileId + '-waypoints')) { _map.setFilter(this.fileId, segmentFilter, { validate: false });
_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
);
layerEventManager.on( if (_map.getLayer(this.fileId + '-direction')) {
'mouseenter', _map.setFilter(this.fileId + '-direction', segmentFilter, { validate: false });
this.fileId + '-waypoints',
this.waypointLayerOnMouseEnterBinded
);
layerEventManager.on(
'mouseleave',
this.fileId + '-waypoints',
this.waypointLayerOnMouseLeaveBinded
);
layerEventManager.on(
'click',
this.fileId + '-waypoints',
this.waypointLayerOnClickBinded
);
layerEventManager.on(
'mousedown',
this.fileId + '-waypoints',
this.waypointLayerOnMouseDownBinded
);
layerEventManager.on(
'touchstart',
this.fileId + '-waypoints',
this.waypointLayerOnTouchStartBinded
);
} }
let visibleWaypoints: number[] = []; let visibleWaypoints: number[] = [];
@@ -355,47 +319,32 @@ export class GPXLayer {
remove() { remove() {
const _map = get(map); const _map = get(map);
if (_map) { if (_map) {
_map.off('style.load', this.updateBinded); _map.off('click', this.fileId, this.layerOnClickBinded);
} _map.off('contextmenu', this.fileId, this.layerOnContextMenuBinded);
_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);
const layerEventManager = map.layerEventManager; _map.off(
if (layerEventManager) {
layerEventManager.off('click', this.fileId, this.layerOnClickBinded);
layerEventManager.off('contextmenu', this.fileId, this.layerOnContextMenuBinded);
layerEventManager.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
layerEventManager.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
layerEventManager.off('mousemove', this.fileId, this.layerOnMouseMoveBinded);
layerEventManager.off(
'mouseenter', 'mouseenter',
this.fileId + '-waypoints', this.fileId + '-waypoints',
this.waypointLayerOnMouseEnterBinded this.waypointLayerOnMouseEnterBinded
); );
layerEventManager.off( _map.off(
'mouseleave', 'mouseleave',
this.fileId + '-waypoints', this.fileId + '-waypoints',
this.waypointLayerOnMouseLeaveBinded this.waypointLayerOnMouseLeaveBinded
); );
layerEventManager.off( _map.off('click', this.fileId + '-waypoints', this.waypointLayerOnClickBinded);
'click', _map.off('mousedown', this.fileId + '-waypoints', this.waypointLayerOnMouseDownBinded);
this.fileId + '-waypoints', _map.off(
this.waypointLayerOnClickBinded
);
layerEventManager.off(
'mousedown',
this.fileId + '-waypoints',
this.waypointLayerOnMouseDownBinded
);
layerEventManager.off(
'touchstart', 'touchstart',
this.fileId + '-waypoints', this.fileId + '-waypoints',
this.waypointLayerOnTouchStartBinded this.waypointLayerOnTouchStartBinded
); );
}
if (_map) {
if (_map.getLayer(this.fileId + '-direction')) { if (_map.getLayer(this.fileId + '-direction')) {
_map.removeLayer(this.fileId + '-direction'); _map.removeLayer(this.fileId + '-direction');
} }
@@ -415,7 +364,7 @@ export class GPXLayer {
this.unsubscribe.forEach((unsubscribe) => unsubscribe()); this.unsubscribe.forEach((unsubscribe) => unsubscribe());
removeColor(this.fileId, this.layerColor); decrementColor(this.layerColor);
} }
moveToFront() { moveToFront() {
@@ -424,13 +373,13 @@ export class GPXLayer {
return; return;
} }
if (_map.getLayer(this.fileId)) { if (_map.getLayer(this.fileId)) {
_map.moveLayer(this.fileId, ANCHOR_LAYER_KEY.tracks); _map.moveLayer(this.fileId);
} }
if (_map.getLayer(this.fileId + '-waypoints')) { if (_map.getLayer(this.fileId + '-waypoints')) {
_map.moveLayer(this.fileId + '-waypoints', ANCHOR_LAYER_KEY.waypoints); _map.moveLayer(this.fileId + '-waypoints');
} }
if (_map.getLayer(this.fileId + '-direction')) { if (_map.getLayer(this.fileId + '-direction')) {
_map.moveLayer(this.fileId + '-direction', ANCHOR_LAYER_KEY.directionMarkers); _map.moveLayer(this.fileId + '-direction');
} }
} }
@@ -471,7 +420,7 @@ export class GPXLayer {
} }
} }
layerOnClick(e: MapLayerMouseEvent) { layerOnClick(e: mapboxgl.MapMouseEvent) {
if ( if (
get(currentTool) === Tool.ROUTING && get(currentTool) === Tool.ROUTING &&
get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])
@@ -529,7 +478,7 @@ export class GPXLayer {
} }
} }
waypointLayerOnMouseEnter(e: MapLayerMouseEvent) { waypointLayerOnMouseEnter(e: mapboxgl.MapMouseEvent) {
if (this.draggedWaypointIndex !== null) { if (this.draggedWaypointIndex !== null) {
return; return;
} }
@@ -549,7 +498,7 @@ export class GPXLayer {
mapCursor.notify(MapCursorState.WAYPOINT_HOVER, false); mapCursor.notify(MapCursorState.WAYPOINT_HOVER, false);
} }
waypointLayerOnClick(e: MapLayerMouseEvent) { waypointLayerOnClick(e: mapboxgl.MapMouseEvent) {
e.preventDefault(); e.preventDefault();
let waypointIndex = e.features![0].properties!.waypointIndex; let waypointIndex = e.features![0].properties!.waypointIndex;
@@ -591,7 +540,7 @@ export class GPXLayer {
} }
} }
waypointLayerOnMouseDown(e: MapLayerMouseEvent) { waypointLayerOnMouseDown(e: mapboxgl.MapMouseEvent) {
if (get(currentTool) !== Tool.WAYPOINT || !this.selected) { if (get(currentTool) !== Tool.WAYPOINT || !this.selected) {
return; return;
} }
@@ -601,7 +550,6 @@ export class GPXLayer {
} }
e.preventDefault(); e.preventDefault();
_map.dragPan.disable();
this.draggedWaypointIndex = e.features![0].properties!.waypointIndex; this.draggedWaypointIndex = e.features![0].properties!.waypointIndex;
this.draggingStartingPosition = e.point; this.draggingStartingPosition = e.point;
@@ -611,7 +559,7 @@ export class GPXLayer {
_map.once('mouseup', this.waypointLayerOnMouseUpBinded); _map.once('mouseup', this.waypointLayerOnMouseUpBinded);
} }
waypointLayerOnTouchStart(e: MapLayerTouchEvent) { waypointLayerOnTouchStart(e: mapboxgl.MapTouchEvent) {
if (e.points.length !== 1 || get(currentTool) !== Tool.WAYPOINT || !this.selected) { if (e.points.length !== 1 || get(currentTool) !== Tool.WAYPOINT || !this.selected) {
return; return;
} }
@@ -625,13 +573,12 @@ export class GPXLayer {
waypointPopup?.hide(); waypointPopup?.hide();
e.preventDefault(); e.preventDefault();
_map.dragPan.disable();
_map.on('touchmove', this.waypointLayerOnMouseMoveBinded); _map.on('touchmove', this.waypointLayerOnMouseMoveBinded);
_map.once('touchend', this.waypointLayerOnMouseUpBinded); _map.once('touchend', this.waypointLayerOnMouseUpBinded);
} }
waypointLayerOnMouseMove(e: MapLayerMouseEvent | MapLayerTouchEvent) { waypointLayerOnMouseMove(e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) {
if (this.draggedWaypointIndex === null || e.point.equals(this.draggingStartingPosition)) { if (this.draggedWaypointIndex === null || e.point.equals(this.draggingStartingPosition)) {
return; return;
} }
@@ -643,35 +590,18 @@ export class GPXLayer {
).coordinates = [e.lngLat.lng, e.lngLat.lat]; ).coordinates = [e.lngLat.lng, e.lngLat.lat];
let waypointSource = get(map)?.getSource(this.fileId + '-waypoints') as let waypointSource = get(map)?.getSource(this.fileId + '-waypoints') as
| GeoJSONSource | mapboxgl.GeoJSONSource
| undefined; | undefined;
if (waypointSource) { if (waypointSource) {
waypointSource.updateData({ waypointSource.setData(this.currentWaypointData!);
update: [
{
id: this.draggedWaypointIndex,
newGeometry: {
type: 'Point',
coordinates: [e.lngLat.lng, e.lngLat.lat],
},
},
],
});
} }
} }
waypointLayerOnMouseUp(e: MapLayerMouseEvent | MapLayerTouchEvent) { waypointLayerOnMouseUp(e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) {
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false); mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
const _map = get(map); get(map)?.off('mousemove', this.waypointLayerOnMouseMoveBinded);
if (!_map) { get(map)?.off('touchmove', this.waypointLayerOnMouseMoveBinded);
return;
}
_map.dragPan.enable();
_map.off('mousemove', this.waypointLayerOnMouseMoveBinded);
_map.off('touchmove', this.waypointLayerOnMouseMoveBinded);
if (this.draggedWaypointIndex === null) { if (this.draggedWaypointIndex === null) {
return; return;
@@ -794,7 +724,20 @@ export class GPXLayer {
symbols.forEach((symbol) => { symbols.forEach((symbol) => {
const iconId = `waypoint-${symbol ?? 'default'}-${this.layerColor}`; const iconId = `waypoint-${symbol ?? 'default'}-${this.layerColor}`;
loadSVGIcon(_map, iconId, getSvgForSymbol(symbol, this.layerColor)); if (!_map.hasImage(iconId)) {
let icon = new Image(100, 100);
icon.onload = () => {
if (!_map.hasImage(iconId)) {
_map.addImage(iconId, icon);
}
};
// Lucide icons are SVG files with a 24x24 viewBox
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
icon.src =
'data:image/svg+xml,' +
encodeURIComponent(getSvgForSymbol(symbol, this.layerColor));
}
}); });
} }
} }

View File

@@ -1,5 +1,4 @@
import { GPXFileStateCollectionObserver } from '$lib/logic/file-state'; import { GPXFileStateCollectionObserver } from '$lib/logic/file-state';
import { writable } from 'svelte/store';
import { GPXLayer } from './gpx-layer'; import { GPXLayer } from './gpx-layer';
export class GPXLayerCollection { export class GPXLayerCollection {
@@ -43,4 +42,3 @@ export class GPXLayerCollection {
} }
export const gpxLayers = new GPXLayerCollection(); export const gpxLayers = new GPXLayerCollection();
export const gpxColors = writable(new Map<string, string>());

View File

@@ -1,40 +1,30 @@
import { currentTool, Tool } from '$lib/components/toolbar/tools'; import { currentTool, Tool } from '$lib/components/toolbar/tools';
import { gpxStatistics, hoveredPoint, slicedGPXStatistics } from '$lib/logic/statistics'; import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
import type { GeoJSONSource } from 'maplibre-gl'; import mapboxgl from 'mapbox-gl';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import { allHidden } from '$lib/logic/hidden'; import { allHidden } from '$lib/logic/hidden';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
import { loadSVGIcon } from '$lib/utils';
const startMarkerSVG = `<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="6" fill="#22c55e" stroke="white" stroke-width="1.5"/>
</svg>`;
const endMarkerSVG = `<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="checkerboard" x="0" y="0" width="5" height="5" patternUnits="userSpaceOnUse">
<rect x="0" y="0" width="2.5" height="2.5" fill="white"/>
<rect x="2.5" y="2.5" width="2.5" height="2.5" fill="white"/>
<rect x="2.5" y="0" width="2.5" height="2.5" fill="black"/>
<rect x="0" y="2.5" width="2.5" height="2.5" fill="black"/>
</pattern>
</defs>
<circle cx="8" cy="8" r="6" fill="url(#checkerboard)" stroke="white" stroke-width="1.5"/>
</svg>`;
const hoverMarkerSVG = `<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="6" fill="#00b8db" stroke="white" stroke-width="1.5"/>
</svg>`;
export class StartEndMarkers { export class StartEndMarkers {
start: mapboxgl.Marker;
end: mapboxgl.Marker;
updateBinded: () => void = this.update.bind(this); updateBinded: () => void = this.update.bind(this);
unsubscribes: (() => void)[] = []; unsubscribes: (() => void)[] = [];
constructor() { constructor() {
let startElement = document.createElement('div');
let endElement = document.createElement('div');
startElement.className = `h-4 w-4 rounded-full bg-green-500 border-2 border-white`;
endElement.className = `h-4 w-4 rounded-full border-2 border-white`;
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 });
map.onLoad(() => this.update()); map.onLoad(() => this.update());
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded)); this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
this.unsubscribes.push(slicedGPXStatistics.subscribe(this.updateBinded)); this.unsubscribes.push(slicedGPXStatistics.subscribe(this.updateBinded));
this.unsubscribes.push(hoveredPoint.subscribe(this.updateBinded));
this.unsubscribes.push(currentTool.subscribe(this.updateBinded)); this.unsubscribes.push(currentTool.subscribe(this.updateBinded));
this.unsubscribes.push(allHidden.subscribe(this.updateBinded)); this.unsubscribes.push(allHidden.subscribe(this.updateBinded));
} }
@@ -43,115 +33,33 @@ export class StartEndMarkers {
const map_ = get(map); const map_ = get(map);
if (!map_) return; if (!map_) return;
this.loadIcons();
const tool = get(currentTool); const tool = get(currentTool);
const statistics = get(gpxStatistics); const statistics = get(gpxStatistics);
const slicedStatistics = get(slicedGPXStatistics); const slicedStatistics = get(slicedGPXStatistics);
const hovered = get(hoveredPoint);
const hidden = get(allHidden); const hidden = get(allHidden);
if (!hidden) { if (statistics.global.length > 0 && tool !== Tool.ROUTING && !hidden) {
const data: GeoJSON.FeatureCollection = { this.start
type: 'FeatureCollection', .setLngLat(
features: [], statistics.getTrackPoint(slicedStatistics?.[1] ?? 0)!.trkpt.getCoordinates()
}; )
.addTo(map_);
if (statistics.global.length > 0 && tool !== Tool.ROUTING) { this.end
const start = statistics .setLngLat(
.getTrackPoint(slicedStatistics?.[1] ?? 0)! statistics
.trkpt.getCoordinates();
const end = statistics
.getTrackPoint(slicedStatistics?.[2] ?? statistics.global.length - 1)! .getTrackPoint(slicedStatistics?.[2] ?? statistics.global.length - 1)!
.trkpt.getCoordinates(); .trkpt.getCoordinates()
data.features.push({ )
type: 'Feature', .addTo(map_);
geometry: {
type: 'Point',
coordinates: [start.lon, start.lat],
},
properties: {
icon: 'start-marker',
},
});
data.features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [end.lon, end.lat],
},
properties: {
icon: 'end-marker',
},
});
}
if (hovered) {
data.features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [hovered.lon, hovered.lat],
},
properties: {
icon: 'hover-marker',
},
});
}
let source = map_.getSource('start-end-markers') as GeoJSONSource | undefined;
if (source) {
source.setData(data);
} else { } else {
map_.addSource('start-end-markers', { this.start.remove();
type: 'geojson', this.end.remove();
data: data,
});
}
if (!map_.getLayer('start-end-markers')) {
map_.addLayer(
{
id: 'start-end-markers',
type: 'symbol',
source: 'start-end-markers',
layout: {
'icon-image': ['get', 'icon'],
'icon-size': 0.2,
'icon-allow-overlap': true,
},
},
ANCHOR_LAYER_KEY.startEndMarkers
);
}
} else {
if (map_.getLayer('start-end-markers')) {
map_.removeLayer('start-end-markers');
}
if (map_.getSource('start-end-markers')) {
map_.removeSource('start-end-markers');
}
} }
} }
remove() { remove() {
this.unsubscribes.forEach((unsubscribe) => unsubscribe()); this.unsubscribes.forEach((unsubscribe) => unsubscribe());
const map_ = get(map); this.start.remove();
if (!map_) return; this.end.remove();
if (map_.getLayer('start-end-markers')) {
map_.removeLayer('start-end-markers');
}
if (map_.getSource('start-end-markers')) {
map_.removeSource('start-end-markers');
}
}
loadIcons() {
const map_ = get(map);
if (!map_) return;
loadSVGIcon(map_, 'start-marker', startMarkerSVG);
loadSVGIcon(map_, 'end-marker', endMarkerSVG);
loadSVGIcon(map_, 'hover-marker', hoverMarkerSVG);
} }
} }

View File

@@ -20,8 +20,9 @@
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { defaultBasemap, type CustomLayer } from '$lib/assets/layers'; import { defaultBasemap, type CustomLayer } from '$lib/assets/layers';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { remove } from './utils'; import { customBasemapUpdate, isSelected, remove } from './utils';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { map } from '$lib/components/map/map';
import { dndzone } from 'svelte-dnd-action'; import { dndzone } from 'svelte-dnd-action';
const { const {
@@ -41,9 +42,14 @@
let maxZoom: number = $state(20); let maxZoom: number = $state(20);
let layerType: 'basemap' | 'overlay' = $state('basemap'); let layerType: 'basemap' | 'overlay' = $state('basemap');
let resourceType: 'raster' | 'vector' = $derived.by(() => { let resourceType: 'raster' | 'vector' = $derived.by(() => {
if (tileUrls[0].length > 0 && tileUrls[0].includes('.json')) { if (tileUrls[0].length > 0) {
if (
tileUrls[0].includes('.json') ||
(tileUrls[0].includes('api.mapbox.com/styles') && !tileUrls[0].includes('tiles'))
) {
return 'vector'; return 'vector';
} }
}
return 'raster'; return 'raster';
}); });
@@ -128,8 +134,8 @@
], ],
}; };
} }
addLayer(layerId);
$customLayers[layerId] = layer; $customLayers[layerId] = layer;
addLayer(layerId);
selectedLayerId = undefined; selectedLayerId = undefined;
setDataFromSelectedLayer(); setDataFromSelectedLayer();
} }
@@ -152,7 +158,9 @@
return $tree; return $tree;
}); });
if ($currentBasemap !== layerId) { if ($currentBasemap === layerId) {
$customBasemapUpdate++;
} else {
$currentBasemap = layerId; $currentBasemap = layerId;
} }
@@ -168,6 +176,14 @@
return $tree; return $tree;
}); });
if ($map && $currentOverlays && isSelected($currentOverlays, layerId)) {
try {
$map.removeImport(layerId);
} catch (e) {
// No reliable way to check if the map is ready to remove sources and layers
}
}
currentOverlays.update(($overlays) => { currentOverlays.update(($overlays) => {
if (!$overlays.overlays.hasOwnProperty('custom')) { if (!$overlays.overlays.hasOwnProperty('custom')) {
$overlays.overlays['custom'] = {}; $overlays.overlays['custom'] = {};

View File

@@ -5,8 +5,12 @@
import { Separator } from '$lib/components/ui/separator'; import { Separator } from '$lib/components/ui/separator';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js'; import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import { Layers } from '@lucide/svelte'; import { Layers } from '@lucide/svelte';
import { basemaps, defaultBasemap, overlays } from '$lib/assets/layers';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { map } from '$lib/components/map/map'; 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 container: HTMLDivElement;
let overpassLayer: OverpassLayer; let overpassLayer: OverpassLayer;
@@ -19,14 +23,125 @@
selectedBasemapTree, selectedBasemapTree,
selectedOverlayTree, selectedOverlayTree,
selectedOverpassTree, selectedOverpassTree,
customLayers,
opacities,
} = settings; } = settings;
map.onLoad((_map: maplibregl.Map) => { 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) => {
if (overpassLayer) { if (overpassLayer) {
overpassLayer.remove(); overpassLayer.remove();
} }
overpassLayer = new OverpassLayer(_map, map.layerEventManager!); overpassLayer = new OverpassLayer(_map);
overpassLayer.add(); overpassLayer.add();
let first = true;
_map.on('style.import.load', () => {
if (!first) return;
first = false;
updateOverlays();
});
}); });
let open = $state(false); let open = $state(false);

View File

@@ -13,7 +13,6 @@
overlays, overlays,
overlayTree, overlayTree,
overpassTree, overpassTree,
terrainSources,
} from '$lib/assets/layers'; } from '$lib/assets/layers';
import { getLayers, isSelected, toggle } from '$lib/components/map/layer-control/utils'; import { getLayers, isSelected, toggle } from '$lib/components/map/layer-control/utils';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
@@ -32,7 +31,6 @@
currentOverpassQueries, currentOverpassQueries,
customLayers, customLayers,
opacities, opacities,
terrainSource,
} = settings; } = settings;
const { isLayerFromExtension, getLayerName } = extensionAPI; const { isLayerFromExtension, getLayerName } = extensionAPI;
@@ -167,11 +165,11 @@
{#if isSelected($selectedOverlayTree, selectedOverlay)} {#if isSelected($selectedOverlayTree, selectedOverlay)}
{#if $isLayerFromExtension(selectedOverlay)} {#if $isLayerFromExtension(selectedOverlay)}
{$getLayerName(selectedOverlay)} {$getLayerName(selectedOverlay)}
{:else if $customLayers.hasOwnProperty(selectedOverlay)}
{$customLayers[selectedOverlay].name}
{:else} {:else}
{i18n._(`layers.label.${selectedOverlay}`)} {i18n._(`layers.label.${selectedOverlay}`)}
{/if} {/if}
{:else if $customLayers.hasOwnProperty(selectedOverlay)}
{$customLayers[selectedOverlay].name}
{/if} {/if}
{/if} {/if}
</Select.Trigger> </Select.Trigger>
@@ -213,9 +211,7 @@
isSelected($currentOverlays, selectedOverlay) isSelected($currentOverlays, selectedOverlay)
) { ) {
try { try {
if ($map.getLayer(selectedOverlay)) { $map.removeImport(selectedOverlay);
$map.removeLayer(selectedOverlay);
}
} catch (e) { } catch (e) {
// No reliable way to check if the map is ready to remove sources and layers // No reliable way to check if the map is ready to remove sources and layers
} }
@@ -237,23 +233,6 @@
</ScrollArea> </ScrollArea>
</Accordion.Content> </Accordion.Content>
</Accordion.Item> </Accordion.Item>
<Accordion.Item value="terrain-source">
<Accordion.Trigger>{i18n._('layers.terrain')}</Accordion.Trigger>
<Accordion.Content class="flex flex-col gap-3 overflow-visible">
<Select.Root bind:value={$terrainSource} type="single">
<Select.Trigger class="mr-1 w-full" size="sm">
{i18n._(`layers.label.${$terrainSource}`)}
</Select.Trigger>
<Select.Content class="h-fit max-h-[40dvh] overflow-y-auto">
{#each Object.keys(terrainSources) as id}
<Select.Item value={id}>
{i18n._(`layers.label.${id}`)}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root> </Accordion.Root>
</ScrollArea> </ScrollArea>
</Sheet.Header> </Sheet.Header>

View File

@@ -54,27 +54,28 @@
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw] gap-0"> <Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw] gap-0">
<Card.Header class="p-0 gap-0"> <Card.Header class="p-0 gap-0">
<Card.Title class="text-md flex flex-row"> <Card.Title class="text-md">
<div class="flex flex-row gap-3">
<div class="flex flex-col"> <div class="flex flex-col">
<p>{name}</p> {name}
<div class="text-muted-foreground text-xs font-normal"> <div class="text-muted-foreground text-xs font-normal">
{poi.item.lat.toFixed(6)}&deg; {poi.item.lon.toFixed(6)}&deg; {poi.item.lat.toFixed(6)}&deg; {poi.item.lon.toFixed(6)}&deg;
</div> </div>
</div> </div>
<Button <Button
class="ml-auto" class="ml-auto"
variant="outline" variant="outline"
size="icon-sm" size="icon"
href="https://www.openstreetmap.org/edit?editor=id&{poi.item.type ?? 'node'}={poi href="https://www.openstreetmap.org/edit?editor=id&{poi.item.type ??
.item.id}" 'node'}={poi.item.id}"
target="_blank" target="_blank"
> >
<PencilLine size="16" /> <PencilLine size="16" />
</Button> </Button>
</div>
</Card.Title> </Card.Title>
</Card.Header> </Card.Header>
<Card.Content class="flex flex-col gap-1 p-0 text-sm whitespace-normal break-all"> <Card.Content class="flex flex-col p-0 text-sm mt-1 whitespace-normal break-all">
<ScrollArea class="flex flex-col max-h-[30dvh]"> <ScrollArea class="flex flex-col max-h-[30dvh]">
{#if tags.image || tags['image:0']} {#if tags.image || tags['image:0']}
<div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto"> <div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto">
@@ -99,14 +100,8 @@
{/each} {/each}
</div> </div>
</ScrollArea> </ScrollArea>
<Button <Button class="mt-2" variant="outline" disabled={$selection.size === 0} onclick={addToFile}>
size="sm" <MapPin size="16" />
class="mt-1 justify-start"
variant="outline"
disabled={$selection.size === 0}
onclick={addToFile}
>
<MapPin size="14" />
{i18n._('toolbar.waypoint.add')} {i18n._('toolbar.waypoint.add')}
</Button> </Button>
</Card.Content> </Card.Content>

View File

@@ -103,7 +103,7 @@ export class ExtensionAPI {
if (current && isSelected(current, overlay.id)) { if (current && isSelected(current, overlay.id)) {
show = true; show = true;
try { try {
get(map)?.removeLayer(overlay.id); get(map)?.removeImport(overlay.id);
} catch (e) { } catch (e) {
// No reliable way to check if the map is ready to remove sources and layers // No reliable way to check if the map is ready to remove sources and layers
} }

View File

@@ -6,10 +6,6 @@ import { overpassQueryData } from '$lib/assets/layers';
import { MapPopup } from '$lib/components/map/map-popup'; import { MapPopup } from '$lib/components/map/map-popup';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { db } from '$lib/db'; import { db } from '$lib/db';
import type { GeoJSONSource } from 'maplibre-gl';
import { ANCHOR_LAYER_KEY } from '../style';
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
import { loadSVGIcon } from '$lib/utils';
const { currentOverpassQueries } = settings; const { currentOverpassQueries } = settings;
@@ -24,12 +20,11 @@ liveQuery(() => db.overpassdata.toArray()).subscribe((pois) => {
}); });
export class OverpassLayer { export class OverpassLayer {
overpassUrl = 'https://maps.mail.ru/osm/tools/overpass/api/interpreter'; overpassUrl = 'https://overpass.private.coffee/api/interpreter';
minZoom = 12; minZoom = 12;
queryZoom = 12; queryZoom = 12;
expirationTime = 7 * 24 * 3600 * 1000; expirationTime = 7 * 24 * 3600 * 1000;
map: maplibregl.Map; map: mapboxgl.Map;
layerEventManager: MapLayerEventManager;
popup: MapPopup; popup: MapPopup;
currentQueries: Set<string> = new Set(); currentQueries: Set<string> = new Set();
@@ -40,9 +35,8 @@ export class OverpassLayer {
updateBinded = this.update.bind(this); updateBinded = this.update.bind(this);
onHoverBinded = this.onHover.bind(this); onHoverBinded = this.onHover.bind(this);
constructor(map: maplibregl.Map, layerEventManager: MapLayerEventManager) { constructor(map: mapboxgl.Map) {
this.map = map; this.map = map;
this.layerEventManager = layerEventManager;
this.popup = new MapPopup(map, { this.popup = new MapPopup(map, {
closeButton: false, closeButton: false,
focusAfterOpen: false, focusAfterOpen: false,
@@ -53,7 +47,7 @@ export class OverpassLayer {
add() { add() {
this.map.on('moveend', this.queryIfNeededBinded); this.map.on('moveend', this.queryIfNeededBinded);
this.map.on('style.load', this.updateBinded); this.map.on('style.import.load', this.updateBinded);
this.unsubscribes.push(data.subscribe(this.updateBinded)); this.unsubscribes.push(data.subscribe(this.updateBinded));
this.unsubscribes.push( this.unsubscribes.push(
currentOverpassQueries.subscribe(() => { currentOverpassQueries.subscribe(() => {
@@ -77,17 +71,10 @@ export class OverpassLayer {
update() { update() {
this.loadIcons(); this.loadIcons();
const fullData = get(data); let d = get(data);
const queries = getCurrentQueries();
const d: GeoJSON.FeatureCollection = {
type: 'FeatureCollection',
features: fullData.features.filter((feature) =>
queries.includes(feature.properties!.query)
),
};
try { try {
let source = this.map.getSource('overpass') as GeoJSONSource | undefined; let source = this.map.getSource('overpass') as mapboxgl.GeoJSONSource | undefined;
if (source) { if (source) {
source.setData(d); source.setData(d);
} else { } else {
@@ -98,8 +85,7 @@ export class OverpassLayer {
} }
if (!this.map.getLayer('overpass')) { if (!this.map.getLayer('overpass')) {
this.map.addLayer( this.map.addLayer({
{
id: 'overpass', id: 'overpass',
type: 'symbol', type: 'symbol',
source: 'overpass', source: 'overpass',
@@ -109,13 +95,15 @@ export class OverpassLayer {
'icon-padding': 0, 'icon-padding': 0,
'icon-allow-overlap': ['step', ['zoom'], false, 14, true], 'icon-allow-overlap': ['step', ['zoom'], false, 14, true],
}, },
}, });
ANCHOR_LAYER_KEY.overpass
);
this.layerEventManager.on('mouseenter', 'overpass', this.onHoverBinded); this.map.on('mouseenter', 'overpass', this.onHoverBinded);
this.layerEventManager.on('click', 'overpass', this.onHoverBinded); this.map.on('click', 'overpass', this.onHoverBinded);
} }
this.map.setFilter('overpass', ['in', 'query', ...getCurrentQueries()], {
validate: false,
});
} catch (e) { } catch (e) {
// No reliable way to check if the map is ready to add sources and layers // No reliable way to check if the map is ready to add sources and layers
} }
@@ -123,9 +111,7 @@ export class OverpassLayer {
remove() { remove() {
this.map.off('moveend', this.queryIfNeededBinded); this.map.off('moveend', this.queryIfNeededBinded);
this.map.off('style.load', this.updateBinded); this.map.off('style.import.load', this.updateBinded);
this.layerEventManager.off('mouseenter', 'overpass', this.onHoverBinded);
this.layerEventManager.off('click', 'overpass', this.onHoverBinded);
this.unsubscribes.forEach((unsubscribe) => unsubscribe()); this.unsubscribes.forEach((unsubscribe) => unsubscribe());
try { try {
@@ -258,16 +244,27 @@ export class OverpassLayer {
loadIcons() { loadIcons() {
let currentQueries = getCurrentQueries(); let currentQueries = getCurrentQueries();
currentQueries.forEach((query) => { currentQueries.forEach((query) => {
loadSVGIcon( if (!this.map.hasImage(`overpass-${query}`)) {
this.map, let icon = new Image(100, 100);
`overpass-${query}`, icon.onload = () => {
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"> if (!this.map.hasImage(`overpass-${query}`)) {
this.map.addImage(`overpass-${query}`, icon);
}
};
// Lucide icons are SVG files with a 24x24 viewBox
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
icon.src =
'data:image/svg+xml,' +
encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
<circle cx="20" cy="20" r="20" fill="${overpassQueryData[query].icon.color}" /> <circle cx="20" cy="20" r="20" fill="${overpassQueryData[query].icon.color}" />
<g transform="translate(8 8)"> <g transform="translate(8 8)">
${overpassQueryData[query].icon.svg.replace('stroke="currentColor"', 'stroke="white"')} ${overpassQueryData[query].icon.svg.replace('stroke="currentColor"', 'stroke="white"')}
</g> </g>
</svg>` </svg>
); `);
}
}); });
} }
} }

View File

@@ -76,3 +76,5 @@ export function removeAll(node: LayerTreeType, ids: string[]) {
}); });
return node; return node;
} }
export const customBasemapUpdate = writable(0);

View File

@@ -1,281 +0,0 @@
import { fileStateCollection } from '$lib/logic/file-state';
import maplibregl from 'maplibre-gl';
type MapLayerMouseEventListener = (e: maplibregl.MapLayerMouseEvent) => void;
type MapLayerTouchEventListener = (e: maplibregl.MapLayerTouchEvent) => void;
type MapLayerListener = {
features: maplibregl.MapGeoJSONFeature[];
mousemoves: MapLayerMouseEventListener[];
mouseenters: MapLayerMouseEventListener[];
mouseleaves: MapLayerMouseEventListener[];
mousedowns: MapLayerMouseEventListener[];
clicks: MapLayerMouseEventListener[];
contextmenus: MapLayerMouseEventListener[];
touchstarts: MapLayerTouchEventListener[];
};
export class MapLayerEventManager {
private _map: maplibregl.Map;
private _listeners: Record<string, MapLayerListener> = {};
constructor(map: maplibregl.Map) {
this._map = map;
this._map.on('mousemove', this._handleMouseMove.bind(this));
this._map.on('click', this._handleMouseClick.bind(this, 'click'));
this._map.on('contextmenu', this._handleMouseClick.bind(this, 'contextmenu'));
this._map.on('mousedown', this._handleMouseClick.bind(this, 'mousedown'));
this._map.on('touchstart', this._handleTouchStart.bind(this));
}
on(
eventType:
| 'mousemove'
| 'mouseenter'
| 'mouseleave'
| 'mousedown'
| 'click'
| 'contextmenu'
| 'touchstart',
layerId: string,
listener: MapLayerMouseEventListener | MapLayerTouchEventListener
) {
if (!this._listeners[layerId]) {
this._listeners[layerId] = {
features: [],
mousemoves: [],
mouseenters: [],
mouseleaves: [],
mousedowns: [],
clicks: [],
contextmenus: [],
touchstarts: [],
};
}
switch (eventType) {
case 'mousemove':
this._listeners[layerId].mousemoves.push(listener as MapLayerMouseEventListener);
break;
case 'mouseenter':
this._listeners[layerId].mouseenters.push(listener as MapLayerMouseEventListener);
break;
case 'mouseleave':
this._listeners[layerId].mouseleaves.push(listener as MapLayerMouseEventListener);
break;
case 'mousedown':
this._listeners[layerId].mousedowns.push(listener as MapLayerMouseEventListener);
break;
case 'click':
this._listeners[layerId].clicks.push(listener as MapLayerMouseEventListener);
break;
case 'contextmenu':
this._listeners[layerId].contextmenus.push(listener as MapLayerMouseEventListener);
break;
case 'touchstart':
this._listeners[layerId].touchstarts.push(listener as MapLayerTouchEventListener);
break;
}
}
off(
eventType:
| 'mousemove'
| 'mouseenter'
| 'mouseleave'
| 'mousedown'
| 'click'
| 'contextmenu'
| 'touchstart',
layerId: string,
listener: MapLayerMouseEventListener | MapLayerTouchEventListener
) {
if (this._listeners[layerId]) {
switch (eventType) {
case 'mousemove':
this._listeners[layerId].mousemoves = this._listeners[
layerId
].mousemoves.filter((l) => l !== listener);
break;
case 'mouseenter':
this._listeners[layerId].mouseenters = this._listeners[
layerId
].mouseenters.filter((l) => l !== listener);
break;
case 'mouseleave':
this._listeners[layerId].mouseleaves = this._listeners[
layerId
].mouseleaves.filter((l) => l !== listener);
break;
case 'mousedown':
this._listeners[layerId].mousedowns = this._listeners[
layerId
].mousedowns.filter((l) => l !== listener);
break;
case 'click':
this._listeners[layerId].clicks = this._listeners[layerId].clicks.filter(
(l) => l !== listener
);
break;
case 'contextmenu':
this._listeners[layerId].contextmenus = this._listeners[
layerId
].contextmenus.filter((l) => l !== listener);
break;
case 'touchstart':
this._listeners[layerId].touchstarts = this._listeners[
layerId
].touchstarts.filter((l) => l !== listener);
break;
}
if (
this._listeners[layerId].mousemoves.length === 0 &&
this._listeners[layerId].mouseenters.length === 0 &&
this._listeners[layerId].mouseleaves.length === 0 &&
this._listeners[layerId].mousedowns.length === 0 &&
this._listeners[layerId].clicks.length === 0 &&
this._listeners[layerId].contextmenus.length === 0 &&
this._listeners[layerId].touchstarts.length === 0
) {
delete this._listeners[layerId];
}
}
}
private _handleMouseMove(e: maplibregl.MapMouseEvent) {
const featuresByLayer = this._getRenderedFeaturesByLayer(e);
Object.keys(this._listeners).forEach((layerId) => {
const features = featuresByLayer[layerId] || [];
const listener = this._listeners[layerId];
if ((features.length == 0) != (listener.features.length == 0)) {
if (features.length > 0) {
if (listener.mouseenters.length > 0) {
const event = new maplibregl.MapMouseEvent(
'mouseenter',
e.target,
e.originalEvent,
{
features: featuresByLayer[layerId]!,
}
);
listener.mouseenters.forEach((l) => l(event));
}
} else {
if (listener.mouseleaves.length > 0) {
const event = new maplibregl.MapMouseEvent(
'mouseleave',
e.target,
e.originalEvent
);
listener.mouseleaves.forEach((l) => l(event));
}
}
}
if (features.length > 0 && listener.mousemoves.length > 0) {
const event = new maplibregl.MapMouseEvent('mousemove', e.target, e.originalEvent, {
features: featuresByLayer[layerId]!,
});
listener.mousemoves.forEach((l) => l(event));
}
listener.features = features;
});
}
private _handleMouseClick(type: string, e: maplibregl.MapMouseEvent) {
const featuresByLayer = this._getRenderedFeaturesByLayer(e);
Object.keys(this._listeners).forEach((layerId) => {
const features = featuresByLayer[layerId] || [];
const listener = this._listeners[layerId];
if (features.length > 0) {
if (type === 'click' && listener.clicks.length > 0) {
const event = new maplibregl.MapMouseEvent('click', e.target, e.originalEvent, {
features: features,
});
listener.clicks.forEach((l) => l(event));
} else if (type === 'contextmenu' && listener.contextmenus.length > 0) {
const event = new maplibregl.MapMouseEvent(
'contextmenu',
e.target,
e.originalEvent,
{
features: features,
}
);
listener.contextmenus.forEach((l) => l(event));
} else if (type === 'mousedown' && listener.mousedowns.length > 0) {
const event = new maplibregl.MapMouseEvent(
'mousedown',
e.target,
e.originalEvent,
{
features: features,
}
);
listener.mousedowns.forEach((l) => l(event));
}
}
});
}
private _handleTouchStart(e: maplibregl.MapTouchEvent) {
const featuresByLayer = this._getRenderedFeaturesByLayer(e);
Object.keys(this._listeners).forEach((layerId) => {
const features = featuresByLayer[layerId] || [];
const listener = this._listeners[layerId];
if (features.length > 0) {
const event: maplibregl.MapLayerTouchEvent = new maplibregl.MapTouchEvent(
'touchstart',
e.target,
e.originalEvent
);
event.features = featuresByLayer[layerId]!;
listener.touchstarts.forEach((l) => l(event));
}
});
}
private _getBounds(point: maplibregl.Point) {
const delta = 30;
return new maplibregl.LngLatBounds(
this._map.unproject([point.x - delta, point.y + delta]),
this._map.unproject([point.x + delta, point.y - delta])
);
}
private _filterLayersIntersectingBounds(
layerIds: string[],
bounds: maplibregl.LngLatBounds
): string[] {
let result = layerIds.filter((layerId) => {
if (!this._map.getLayer(layerId)) return false;
const fileId = layerId.replace('-waypoints', '');
if (fileId === layerId) {
return fileStateCollection.getStatistics(fileId)?.intersectsBBox(bounds) ?? true;
} else {
return (
fileStateCollection.getStatistics(fileId)?.intersectsWaypointBBox(bounds) ??
true
);
}
});
return result;
}
private _getRenderedFeaturesByLayer(e: maplibregl.MapMouseEvent | maplibregl.MapTouchEvent) {
const layerIds = this._filterLayersIntersectingBounds(
Object.keys(this._listeners),
this._getBounds(e.point)
);
const features =
layerIds.length > 0
? this._map.queryRenderedFeatures(e.point, { layers: layerIds })
: [];
const featuresByLayer: Record<string, maplibregl.MapGeoJSONFeature[]> = {};
features.forEach((f) => {
if (!featuresByLayer[f.layer.id]) {
featuresByLayer[f.layer.id] = [];
}
featuresByLayer[f.layer.id].push(f);
});
return featuresByLayer;
}
}

View File

@@ -1,5 +1,5 @@
import { TrackPoint, Waypoint } from 'gpx'; import { TrackPoint, Waypoint } from 'gpx';
import maplibregl from 'maplibre-gl'; import mapboxgl from 'mapbox-gl';
import { mount, tick, unmount } from 'svelte'; import { mount, tick, unmount } from 'svelte';
import { get, writable, type Writable } from 'svelte/store'; import { get, writable, type Writable } from 'svelte/store';
import MapPopupComponent from '$lib/components/map/MapPopup.svelte'; import MapPopupComponent from '$lib/components/map/MapPopup.svelte';
@@ -11,15 +11,15 @@ export type PopupItem<T = Waypoint | TrackPoint | any> = {
}; };
export class MapPopup { export class MapPopup {
map: maplibregl.Map; map: mapboxgl.Map;
popup: maplibregl.Popup; popup: mapboxgl.Popup;
item: Writable<PopupItem | null> = writable(null); item: Writable<PopupItem | null> = writable(null);
component: ReturnType<typeof mount>; component: ReturnType<typeof mount>;
maybeHideBinded = this.maybeHide.bind(this); maybeHideBinded = this.maybeHide.bind(this);
constructor(map: maplibregl.Map, options?: maplibregl.PopupOptions) { constructor(map: mapboxgl.Map, options?: mapboxgl.PopupOptions) {
this.map = map; this.map = map;
this.popup = new maplibregl.Popup(options); this.popup = new mapboxgl.Popup(options);
this.component = mount(MapPopupComponent, { this.component = mount(MapPopupComponent, {
target: document.body, target: document.body,
props: { props: {
@@ -51,7 +51,7 @@ export class MapPopup {
this.map.on('mousemove', this.maybeHideBinded); this.map.on('mousemove', this.maybeHideBinded);
} }
maybeHide(e: maplibregl.MapMouseEvent) { maybeHide(e: mapboxgl.MapMouseEvent) {
const item = get(this.item); const item = get(this.item);
if (item === null) { if (item === null) {
this.hide(); this.hide();
@@ -75,10 +75,10 @@ export class MapPopup {
getCoordinates() { getCoordinates() {
const item = get(this.item); const item = get(this.item);
if (item === null) { if (item === null) {
return new maplibregl.LngLat(0, 0); return new mapboxgl.LngLat(0, 0);
} }
return item.item instanceof Waypoint || item.item instanceof TrackPoint return item.item instanceof Waypoint || item.item instanceof TrackPoint
? item.item.getCoordinates() ? item.item.getCoordinates()
: new maplibregl.LngLat(item.item.lon, item.item.lat); : new mapboxgl.LngLat(item.item.lon, item.item.lat);
} }
} }

View File

@@ -1,80 +1,89 @@
import maplibregl from 'maplibre-gl'; import mapboxgl from 'mapbox-gl';
import 'maplibre-gl/dist/maplibre-gl.css'; import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
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 { get, writable, type Writable } from 'svelte/store';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { tick } from 'svelte'; import { tick } from 'svelte';
import { ANCHOR_LAYER_KEY, StyleManager } from '$lib/components/map/style';
import { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
const { treeFileView, elevationProfile, bottomPanelSize, rightPanelSize, distanceUnits } = settings; const { treeFileView, elevationProfile, bottomPanelSize, rightPanelSize, distanceUnits } = settings;
let fitBoundsOptions: maplibregl.MapOptions['fitBoundsOptions'] = { let fitBoundsOptions: mapboxgl.MapOptions['fitBoundsOptions'] = {
maxZoom: 15, maxZoom: 15,
linear: true, linear: true,
easing: () => 1, easing: () => 1,
}; };
export class MapLibreGLMap { export class MapboxGLMap {
private _maptilerKey: string = ''; private _map: Writable<mapboxgl.Map | null> = writable(null);
private _map: maplibregl.Map | null = null; private _onLoadCallbacks: ((map: mapboxgl.Map) => void)[] = [];
private _mapStore: Writable<maplibregl.Map | null> = writable(null);
private _styleManager: StyleManager | null = null;
private _onLoadCallbacks: ((map: maplibregl.Map) => void)[] = [];
private _unsubscribes: (() => void)[] = []; private _unsubscribes: (() => void)[] = [];
private callOnLoadBinded: () => void = this.callOnLoad.bind(this);
public layerEventManager: MapLayerEventManager | null = null;
subscribe(run: (value: maplibregl.Map | null) => void, invalidate?: () => void) { subscribe(run: (value: mapboxgl.Map | null) => void, invalidate?: () => void) {
return this._mapStore.subscribe(run, invalidate); return this._map.subscribe(run, invalidate);
} }
init( init(
maptilerKey: string, accessToken: string,
language: string, language: string,
hash: boolean, hash: boolean,
geocoder: boolean, geocoder: boolean,
geolocate: boolean geolocate: boolean
) { ) {
this._maptilerKey = maptilerKey; const map = new mapboxgl.Map({
this._styleManager = new StyleManager(this._mapStore, this._maptilerKey);
const map = new maplibregl.Map({
container: 'map', container: 'map',
style: { style: {
version: 8, version: 8,
projection: { sources: {},
type: 'globe', layers: [],
imports: [
{
id: 'basemap',
url: '',
}, },
{
id: 'overlays',
url: '',
data: {
version: 8,
sources: {}, sources: {},
layers: [], layers: [],
}, },
},
],
},
projection: 'globe',
zoom: 0, zoom: 0,
hash: hash, hash: hash,
language,
attributionControl: false,
logoPosition: 'bottom-right',
boxZoom: false, boxZoom: false,
maxPitch: 85,
}); });
this.layerEventManager = new MapLayerEventManager(map);
map.addControl( map.addControl(
new maplibregl.NavigationControl({ new mapboxgl.AttributionControl({
compact: true,
})
);
map.addControl(
new mapboxgl.NavigationControl({
visualizePitch: true, visualizePitch: true,
}) })
); );
if (geocoder) { if (geocoder) {
let geocoder = new MaplibreGeocoder( let geocoder = new MapboxGeocoder({
{ mapboxgl: mapboxgl,
forwardGeocode: async (config) => { enableEventLogging: false,
const results: MaplibreGeocoderFeatureResults = { collapsed: true,
features: [], flyTo: fitBoundsOptions,
type: 'FeatureCollection', language,
}; localGeocoder: () => [],
try { localGeocoderOnly: true,
const request = `https://nominatim.openstreetmap.org/search?format=json&q=${config.query}&limit=5&accept-language=${language}`; externalGeocoder: (query: string) =>
const response = await fetch(request); fetch(
const geojson = await response.json(); `https://nominatim.openstreetmap.org/search?format=json&q=${query}&limit=5&accept-language=${language}`
results.features = geojson.map((result: any) => { )
.then((response) => response.json())
.then((data) => {
return data.map((result: any) => {
return { return {
type: 'Feature', type: 'Feature',
geometry: { geometry: {
@@ -84,43 +93,80 @@ export class MapLibreGLMap {
place_name: result.display_name, place_name: result.display_name,
}; };
}); });
} catch (e) {} }),
return results; });
}, let onKeyDown = geocoder._onKeyDown;
}, geocoder._onKeyDown = (e: KeyboardEvent) => {
{ // Trigger search on Enter key only
maplibregl: maplibregl, if (e.key === 'Enter') {
enableEventLogging: false, onKeyDown.apply(geocoder, [{ target: geocoder._inputEl }]);
collapsed: true, } else if (geocoder._typeahead.data.length > 0) {
flyTo: fitBoundsOptions, geocoder._typeahead.clear();
language,
} }
); };
map.addControl(geocoder); map.addControl(geocoder);
} }
if (geolocate) { if (geolocate) {
map.addControl( map.addControl(
new maplibregl.GeolocateControl({ new mapboxgl.GeolocateControl({
positionOptions: { positionOptions: {
enableHighAccuracy: true, enableHighAccuracy: true,
}, },
fitBoundsOptions, fitBoundsOptions,
trackUserLocation: true, trackUserLocation: true,
showUserHeading: true,
}) })
); );
} }
const scaleControl = new maplibregl.ScaleControl({ const scaleControl = new mapboxgl.ScaleControl({
unit: get(distanceUnits), unit: get(distanceUnits),
}); });
map.addControl(scaleControl); 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('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', () => { map.on('load', () => {
this._map = map; this._map.set(map); // only set the store after the map has loaded
this._mapStore.set(map); // only set the store after the map has loaded
window._map = map; // entry point for extensions window._map = map; // entry point for extensions
this.resize(); this.resize();
scaleControl.setUnit(get(distanceUnits)); 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(treeFileView.subscribe(() => this.resize()));
this._unsubscribes.push(elevationProfile.subscribe(() => this.resize())); this._unsubscribes.push(elevationProfile.subscribe(() => this.resize()));
@@ -133,48 +179,44 @@ export class MapLibreGLMap {
); );
} }
onLoad(callback: (map: mapboxgl.Map) => void) {
const map = get(this._map);
if (map) {
callback(map);
} else {
this._onLoadCallbacks.push(callback);
}
}
destroy() { destroy() {
if (this._map) { const map = get(this._map);
this._map.remove(); if (map) {
this._mapStore.set(null); map.remove();
this._map.set(null);
} }
this._unsubscribes.forEach((unsubscribe) => unsubscribe()); this._unsubscribes.forEach((unsubscribe) => unsubscribe());
this._unsubscribes = []; this._unsubscribes = [];
} }
resize() { resize() {
if (this._map) { const map = get(this._map);
if (map) {
tick().then(() => { tick().then(() => {
this._map?.resize(); map.resize();
}); });
} }
} }
toggle3D() { toggle3D() {
if (this._map) { const map = get(this._map);
if (this._map.getPitch() === 0) { if (map) {
this._map.easeTo({ pitch: 70 }); if (map.getPitch() === 0) {
map.easeTo({ pitch: 70 });
} else { } else {
this._map.easeTo({ pitch: 0 }); map.easeTo({ pitch: 0 });
} }
} }
} }
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 MapLibreGLMap(); export const map = new MapboxGLMap();

View File

@@ -20,14 +20,9 @@
let container: HTMLElement; let container: HTMLElement;
onMount(() => { onMount(() => {
map.onLoad((map_: maplibregl.Map) => { map.onLoad((map: mapboxgl.Map) => {
googleRedirect = new GoogleRedirect(map_); googleRedirect = new GoogleRedirect(map);
mapillaryLayer = new MapillaryLayer( mapillaryLayer = new MapillaryLayer(map, container, mapillaryOpen);
map_,
map.layerEventManager!,
container,
mapillaryOpen
);
}); });
}); });

View File

@@ -1,10 +1,11 @@
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import type mapboxgl from 'mapbox-gl';
export class GoogleRedirect { export class GoogleRedirect {
map: maplibregl.Map; map: mapboxgl.Map;
enabled = false; enabled = false;
constructor(map: maplibregl.Map) { constructor(map: mapboxgl.Map) {
this.map = map; this.map = map;
} }
@@ -24,7 +25,7 @@ export class GoogleRedirect {
this.map.off('click', this.openStreetView); this.map.off('click', this.openStreetView);
} }
openStreetView(e: maplibregl.MapMouseEvent) { openStreetView(e: mapboxgl.MapMouseEvent) {
window.open( window.open(
`https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${e.lngLat.lat},${e.lngLat.lng}` `https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${e.lngLat.lat},${e.lngLat.lng}`
); );

View File

@@ -1,9 +1,7 @@
import maplibregl, { type LayerSpecification, type VectorSourceSpecification } from 'maplibre-gl'; import mapboxgl, { type LayerSpecification, type VectorSourceSpecification } from 'mapbox-gl';
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module'; import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
import 'mapillary-js/dist/mapillary.css'; import 'mapillary-js/dist/mapillary.css';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import { ANCHOR_LAYER_KEY } from '../style';
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
const mapillarySource: VectorSourceSpecification = { const mapillarySource: VectorSourceSpecification = {
type: 'vector', type: 'vector',
@@ -43,9 +41,8 @@ const mapillaryImageLayer: LayerSpecification = {
}; };
export class MapillaryLayer { export class MapillaryLayer {
map: maplibregl.Map; map: mapboxgl.Map;
layerEventManager: MapLayerEventManager; marker: mapboxgl.Marker;
marker: maplibregl.Marker;
viewer: Viewer; viewer: Viewer;
active = false; active = false;
@@ -55,14 +52,8 @@ export class MapillaryLayer {
onMouseEnterBinded = this.onMouseEnter.bind(this); onMouseEnterBinded = this.onMouseEnter.bind(this);
onMouseLeaveBinded = this.onMouseLeave.bind(this); onMouseLeaveBinded = this.onMouseLeave.bind(this);
constructor( constructor(map: mapboxgl.Map, container: HTMLElement, popupOpen: { value: boolean }) {
map: maplibregl.Map,
layerEventManager: MapLayerEventManager,
container: HTMLElement,
popupOpen: { value: boolean }
) {
this.map = map; this.map = map;
this.layerEventManager = layerEventManager;
this.viewer = new Viewer({ this.viewer = new Viewer({
accessToken: 'MLY|4381405525255083|3204871ec181638c3c31320490f03011', accessToken: 'MLY|4381405525255083|3204871ec181638c3c31320490f03011',
@@ -70,12 +61,15 @@ export class MapillaryLayer {
}); });
const element = document.createElement('div'); const element = document.createElement('div');
element.className = 'maplibregl-user-location maplibregl-user-location-show-heading'; element.className = 'mapboxgl-user-location mapboxgl-user-location-show-heading';
const dot = document.createElement('div'); const dot = document.createElement('div');
dot.className = 'maplibregl-user-location-dot'; dot.className = 'mapboxgl-user-location-dot';
const heading = document.createElement('div');
heading.className = 'mapboxgl-user-location-heading';
element.appendChild(dot); element.appendChild(dot);
element.appendChild(heading);
this.marker = new maplibregl.Marker({ this.marker = new mapboxgl.Marker({
rotationAlignment: 'map', rotationAlignment: 'map',
element, element,
}); });
@@ -105,20 +99,20 @@ export class MapillaryLayer {
this.map.addSource('mapillary', mapillarySource); this.map.addSource('mapillary', mapillarySource);
} }
if (!this.map.getLayer('mapillary-sequence')) { if (!this.map.getLayer('mapillary-sequence')) {
this.map.addLayer(mapillarySequenceLayer, ANCHOR_LAYER_KEY.mapillary); this.map.addLayer(mapillarySequenceLayer);
} }
if (!this.map.getLayer('mapillary-image')) { if (!this.map.getLayer('mapillary-image')) {
this.map.addLayer(mapillaryImageLayer, ANCHOR_LAYER_KEY.mapillary); this.map.addLayer(mapillaryImageLayer);
} }
this.map.on('style.load', this.addBinded); this.map.on('style.load', this.addBinded);
this.layerEventManager.on('mouseenter', 'mapillary-image', this.onMouseEnterBinded); this.map.on('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
this.layerEventManager.on('mouseleave', 'mapillary-image', this.onMouseLeaveBinded); this.map.on('mouseleave', 'mapillary-image', this.onMouseLeaveBinded);
} }
remove() { remove() {
this.map.off('style.load', this.addBinded); this.map.off('style.load', this.addBinded);
this.layerEventManager.off('mouseenter', 'mapillary-image', this.onMouseEnterBinded); this.map.off('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
this.layerEventManager.off('mouseleave', 'mapillary-image', this.onMouseLeaveBinded); this.map.off('mouseleave', 'mapillary-image', this.onMouseLeaveBinded);
if (this.map.getLayer('mapillary-image')) { if (this.map.getLayer('mapillary-image')) {
this.map.removeLayer('mapillary-image'); this.map.removeLayer('mapillary-image');
@@ -140,7 +134,7 @@ export class MapillaryLayer {
this.popupOpen.value = false; this.popupOpen.value = false;
} }
onMouseEnter(e: maplibregl.MapLayerMouseEvent) { onMouseEnter(e: mapboxgl.MapMouseEvent) {
if ( if (
e.features && e.features &&
e.features.length > 0 && e.features.length > 0 &&

View File

@@ -1,231 +0,0 @@
import { settings } from '$lib/logic/settings';
import { get, type Writable } from 'svelte/store';
import {
basemaps,
defaultBasemap,
maptilerKeyPlaceHolder,
overlays,
terrainSources,
} from '$lib/assets/layers';
import { 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',
startEndMarkers: 'start-end-markers-end',
interactions: 'interactions-end',
overpass: 'overpass-end',
waypoints: 'waypoints-end',
routingControls: 'routing-controls-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.updateBasemap();
map_.on('style.load', () => this.updateOverlays());
map_.on('pitch', () => this.updateTerrain());
}
});
currentBasemap.subscribe(() => this.updateBasemap());
currentOverlays.subscribe(() => this.updateOverlays());
opacities.subscribe(() => this.updateOverlays());
terrainSource.subscribe(() => this.updateTerrain());
customLayers.subscribe(() => this.updateBasemap());
}
updateBasemap() {
const map_ = get(this._map);
if (!map_) return;
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);
const terrain = this.getCurrentTerrain();
style.sources[terrain.source] = terrainSources[terrain.source];
style.terrain = terrain.exaggeration > 0 ? terrain : undefined;
style.layers.push(...anchorLayers);
return 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)) {
if (terrain.exaggeration > 0) {
if (!map_.getSource(terrain.source)) {
map_.addSource(terrain.source, terrainSources[terrain.source]);
}
map_.setTerrain(terrain);
} else {
map_.setTerrain(null);
}
}
}
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,
};
}
}

View File

@@ -11,7 +11,7 @@
import Clean from '$lib/components/toolbar/tools/Clean.svelte'; import Clean from '$lib/components/toolbar/tools/Clean.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte'; import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte';
import maplibregl from 'maplibre-gl'; import mapboxgl from 'mapbox-gl';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
let { let {
@@ -23,11 +23,11 @@
const { minimizeRoutingMenu } = settings; const { minimizeRoutingMenu } = settings;
let popupElement: HTMLDivElement | undefined = $state(undefined); let popupElement: HTMLDivElement | undefined = $state(undefined);
let popup: maplibregl.Popup | undefined = $derived.by(() => { let popup: mapboxgl.Popup | undefined = $derived.by(() => {
if (!popupElement) { if (!popupElement) {
return undefined; return undefined;
} }
let popup = new maplibregl.Popup({ let popup = new mapboxgl.Popup({
closeButton: false, closeButton: false,
maxWidth: undefined, maxWidth: undefined,
}); });

View File

@@ -16,11 +16,10 @@
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { Trash2 } from '@lucide/svelte'; import { Trash2 } from '@lucide/svelte';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import type { GeoJSONSource } from 'maplibre-gl'; import type { GeoJSONSource } from 'mapbox-gl';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
let props: { let props: {
class?: string; class?: string;
@@ -29,7 +28,7 @@
let cleanType = $state(CleanType.INSIDE); let cleanType = $state(CleanType.INSIDE);
let deleteTrackpoints = $state(true); let deleteTrackpoints = $state(true);
let deleteWaypoints = $state(true); let deleteWaypoints = $state(true);
let rectangleCoordinates: maplibregl.LngLat[] = $state([]); let rectangleCoordinates: mapboxgl.LngLat[] = $state([]);
$effect(() => { $effect(() => {
if ($map) { if ($map) {
@@ -64,8 +63,7 @@
}); });
} }
if (!$map.getLayer('rectangle')) { if (!$map.getLayer('rectangle')) {
$map.addLayer( $map.addLayer({
{
id: 'rectangle', id: 'rectangle',
type: 'fill', type: 'fill',
source: 'rectangle', source: 'rectangle',
@@ -73,9 +71,7 @@
'fill-color': 'SteelBlue', 'fill-color': 'SteelBlue',
'fill-opacity': 0.5, 'fill-opacity': 0.5,
}, },
}, });
ANCHOR_LAYER_KEY.interactions
);
} }
} }
} }

View File

@@ -2,6 +2,7 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { MountainSnow } from '@lucide/svelte'; import { MountainSnow } from '@lucide/svelte';
import { map } from '$lib/components/map/map';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
@@ -19,7 +20,11 @@
variant="outline" variant="outline"
class="whitespace-normal h-fit" class="whitespace-normal h-fit"
disabled={!validSelection} disabled={!validSelection}
onclick={() => fileActions.addElevationToSelection()} onclick={() => {
if ($map) {
fileActions.addElevationToSelection($map);
}
}}
> >
<MountainSnow size="16" class="shrink-0" /> <MountainSnow size="16" class="shrink-0" />
{i18n._('toolbar.elevation.button')} {i18n._('toolbar.elevation.button')}

View File

@@ -1,11 +1,10 @@
import { ListItem, ListTrackSegmentItem } from '$lib/components/file-list/file-list'; import { ListItem, ListTrackSegmentItem } from '$lib/components/file-list/file-list';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import { GPXFileStateCollectionObserver, type GPXFileState } from '$lib/logic/file-state'; import { GPXFileStateCollectionObserver, type GPXFileState } from '$lib/logic/file-state';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx'; import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
import type { GeoJSONSource } from 'maplibre-gl'; import type { GeoJSONSource } from 'mapbox-gl';
import { get, writable } from 'svelte/store'; import { get, writable } from 'svelte/store';
export const minTolerance = 0.1; export const minTolerance = 0.1;
@@ -145,8 +144,7 @@ export class ReducedGPXLayerCollection {
}); });
} }
if (!map_.getLayer('simplified')) { if (!map_.getLayer('simplified')) {
map_.addLayer( map_.addLayer({
{
id: 'simplified', id: 'simplified',
type: 'line', type: 'line',
source: 'simplified', source: 'simplified',
@@ -154,9 +152,9 @@ export class ReducedGPXLayerCollection {
'line-color': 'white', 'line-color': 'white',
'line-width': 3, 'line-width': 3,
}, },
}, });
ANCHOR_LAYER_KEY.interactions } else {
); map_.moveLayer('simplified');
} }
} }

View File

@@ -21,7 +21,7 @@
SquareArrowUpLeft, SquareArrowUpLeft,
SquareArrowOutDownRight, SquareArrowOutDownRight,
} from '@lucide/svelte'; } from '@lucide/svelte';
import { routingProfiles } from '$lib/components/toolbar/tools/routing/routing'; import { brouterProfiles } from '$lib/components/toolbar/tools/routing/routing';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { import {
@@ -51,7 +51,7 @@
}: { }: {
minimized?: boolean; minimized?: boolean;
minimizable?: boolean; minimizable?: boolean;
popup?: maplibregl.Popup; popup?: mapboxgl.Popup;
popupElement?: HTMLDivElement; popupElement?: HTMLDivElement;
class?: string; class?: string;
} = $props(); } = $props();
@@ -167,7 +167,7 @@
{i18n._(`toolbar.routing.activities.${$routingProfile}`)} {i18n._(`toolbar.routing.activities.${$routingProfile}`)}
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
{#each Object.keys(routingProfiles) as profile} {#each Object.keys(brouterProfiles) as profile}
<Select.Item value={profile} <Select.Item value={profile}
>{i18n._( >{i18n._(
`toolbar.routing.activities.${profile}` `toolbar.routing.activities.${profile}`

View File

@@ -6,7 +6,7 @@ import { get } from 'svelte/store';
const { routing, routingProfile, privateRoads } = settings; const { routing, routingProfile, privateRoads } = settings;
export const routingProfiles: { [key: string]: string } = { export const brouterProfiles: { [key: string]: string } = {
bike: 'Trekking-dry', bike: 'Trekking-dry',
racing_bike: 'fastbike', racing_bike: 'fastbike',
gravel_bike: 'gravel', gravel_bike: 'gravel',
@@ -19,7 +19,7 @@ export const routingProfiles: { [key: string]: string } = {
export function route(points: Coordinates[]): Promise<TrackPoint[]> { export function route(points: Coordinates[]): Promise<TrackPoint[]> {
if (get(routing)) { if (get(routing)) {
return getRoute(points, routingProfiles[get(routingProfile)], get(privateRoads)); return getRoute(points, brouterProfiles[get(routingProfile)], get(privateRoads));
} else { } else {
return getIntermediatePoints(points); return getIntermediatePoints(points);
} }

View File

@@ -2,21 +2,15 @@ import { ramerDouglasPeucker, type GPXFile, type TrackSegment } from 'gpx';
const earthRadius = 6371008.8; const earthRadius = 6371008.8;
export const MIN_ANCHOR_ZOOM = 0;
export const MAX_ANCHOR_ZOOM = 22;
export function getZoomLevelForDistance(latitude: number, distance?: number): number { export function getZoomLevelForDistance(latitude: number, distance?: number): number {
if (distance === undefined) { if (distance === undefined) {
return MIN_ANCHOR_ZOOM; return 0;
} }
const rad = Math.PI / 180; const rad = Math.PI / 180;
const lat = latitude * rad; const lat = latitude * rad;
return Math.min( return Math.min(22, Math.max(0, Math.log2((earthRadius * Math.cos(lat)) / distance)));
MAX_ANCHOR_ZOOM,
Math.max(MIN_ANCHOR_ZOOM, Math.round(Math.log2((earthRadius * Math.cos(lat)) / distance)))
);
} }
export function updateAnchorPoints(file: GPXFile) { export function updateAnchorPoints(file: GPXFile) {

View File

@@ -36,7 +36,7 @@
onMount(() => { onMount(() => {
if ($map) { if ($map) {
splitControls = new SplitControls($map, map.layerEventManager!); splitControls = new SplitControls($map);
} }
}); });

View File

@@ -8,33 +8,39 @@ import { get } from 'svelte/store';
import { fileStateCollection } from '$lib/logic/file-state'; import { fileStateCollection } from '$lib/logic/file-state';
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import type { GeoJSONSource } from 'maplibre-gl';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
import { loadSVGIcon } from '$lib/utils';
export class SplitControls { export class SplitControls {
map: maplibregl.Map; map: mapboxgl.Map;
layerEventManager: MapLayerEventManager;
unsubscribes: Function[] = []; unsubscribes: Function[] = [];
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this); layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this); layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this); layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
constructor(map: maplibregl.Map, layerEventManager: MapLayerEventManager) { constructor(map: mapboxgl.Map) {
this.map = map; this.map = map;
this.layerEventManager = layerEventManager;
loadSVGIcon( if (!this.map.hasImage('split-control')) {
this.map, let icon = new Image(100, 100);
'split-control', icon.onload = () => {
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"> if (!this.map.hasImage('split-control')) {
this.map.addImage('split-control', icon);
}
};
// Lucide icons are SVG files with a 24x24 viewBox
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
icon.src =
'data:image/svg+xml,' +
encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
<circle cx="20" cy="20" r="20" fill="white" /> <circle cx="20" cy="20" r="20" fill="white" />
<g transform="translate(8 8)"> <g transform="translate(8 8)">
${Scissors.replace('stroke="currentColor"', 'stroke="black"')} ${Scissors.replace('stroke="currentColor"', 'stroke="black"')}
</g> </g>
</svg>` </svg>
); `);
}
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this))); this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this))); this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
@@ -91,7 +97,7 @@ export class SplitControls {
}, false); }, false);
try { try {
let source = this.map.getSource('split-controls') as GeoJSONSource | undefined; let source = this.map.getSource('split-controls') as mapboxgl.GeoJSONSource | undefined;
if (source) { if (source) {
source.setData(data); source.setData(data);
} else { } else {
@@ -102,8 +108,7 @@ export class SplitControls {
} }
if (!this.map.getLayer('split-controls')) { if (!this.map.getLayer('split-controls')) {
this.map.addLayer( this.map.addLayer({
{
id: 'split-controls', id: 'split-controls',
type: 'symbol', type: 'symbol',
source: 'split-controls', source: 'split-controls',
@@ -113,31 +118,23 @@ export class SplitControls {
'icon-padding': 0, 'icon-padding': 0,
}, },
filter: ['<=', ['get', 'minZoom'], ['zoom']], filter: ['<=', ['get', 'minZoom'], ['zoom']],
}, });
ANCHOR_LAYER_KEY.interactions
);
this.layerEventManager.on( this.map.on('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
'mouseenter', this.map.on('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
'split-controls', this.map.on('click', 'split-controls', this.layerOnClickBinded);
this.layerOnMouseEnterBinded
);
this.layerEventManager.on(
'mouseleave',
'split-controls',
this.layerOnMouseLeaveBinded
);
this.layerEventManager.on('click', 'split-controls', this.layerOnClickBinded);
} }
this.map.moveLayer('split-controls');
} catch (e) { } catch (e) {
// No reliable way to check if the map is ready to add sources and layers // No reliable way to check if the map is ready to add sources and layers
} }
} }
remove() { remove() {
this.layerEventManager.off('mouseenter', 'split-controls', this.layerOnMouseEnterBinded); this.map.off('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
this.layerEventManager.off('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded); this.map.off('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
this.layerEventManager.off('click', 'split-controls', this.layerOnClickBinded); this.map.off('click', 'split-controls', this.layerOnClickBinded);
try { try {
if (this.map.getLayer('split-controls')) { if (this.map.getLayer('split-controls')) {
@@ -160,7 +157,7 @@ export class SplitControls {
mapCursor.notify(MapCursorState.SPLIT_CONTROL, false); mapCursor.notify(MapCursorState.SPLIT_CONTROL, false);
} }
layerOnClick(e: maplibregl.MapLayerMouseEvent) { layerOnClick(e: mapboxgl.MapMouseEvent) {
let coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates; let coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates;
fileActions.split( fileActions.split(
get(splitAs), get(splitAs),

View File

@@ -16,7 +16,7 @@
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import maplibregl from 'maplibre-gl'; import mapboxgl from 'mapbox-gl';
import { getSvgForSymbol } from '$lib/components/map/gpx-layer/gpx-layer'; import { getSvgForSymbol } from '$lib/components/map/gpx-layer/gpx-layer';
let props: { let props: {
@@ -41,7 +41,7 @@
}) })
); );
let marker: maplibregl.Marker | null = null; let marker: mapboxgl.Marker | null = null;
function reset() { function reset() {
if ($selectedWaypoint) { if ($selectedWaypoint) {
@@ -125,7 +125,7 @@
let element = document.createElement('div'); let element = document.createElement('div');
element.classList.add('w-8', 'h-8'); element.classList.add('w-8', 'h-8');
element.innerHTML = getSvgForSymbol(symbolKey); element.innerHTML = getSvgForSymbol(symbolKey);
marker = new maplibregl.Marker({ marker = new mapboxgl.Marker({
element, element,
anchor: 'bottom', anchor: 'bottom',
}) })

View File

@@ -12,7 +12,6 @@ title: Files and statistics
let gpxStatistics = writable(exampleGPXFile.getStatistics()); let gpxStatistics = writable(exampleGPXFile.getStatistics());
let slicedGPXStatistics = writable(undefined); let slicedGPXStatistics = writable(undefined);
let hoveredPoint = writable(null);
let additionalDatasets = writable(['speed', 'atemp']); let additionalDatasets = writable(['speed', 'atemp']);
let elevationFill = writable(undefined); let elevationFill = writable(undefined);
</script> </script>
@@ -85,7 +84,6 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
<ElevationProfile <ElevationProfile
{gpxStatistics} {gpxStatistics}
{slicedGPXStatistics} {slicedGPXStatistics}
{hoveredPoint}
{additionalDatasets} {additionalDatasets}
{elevationFill} {elevationFill}
/> />

View File

@@ -5,7 +5,7 @@
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free) ## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
Each time you add or move GPS points, our servers calculate the best route on the road network. Each time you add or move GPS points, our servers calculate the best route on the road network.
We also use APIs from <a href="https://maptiler.com" target="_blank">MapTiler</a> to display beautiful maps, retrieve elevation data and allow you to search for places. We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
Unfortunately, this is expensive. Unfortunately, this is expensive.
If you enjoy using this tool and find it valuable, please consider making a small donation to help keep the website free and ad-free. If you enjoy using this tool and find it valuable, please consider making a small donation to help keep the website free and ad-free.

View File

@@ -0,0 +1,5 @@
Mapbox is the company that provides some of the beautiful maps on this website.
They also develop the <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">map engine</a> which powers **gpx.studio**.
We are incredibly fortunate and grateful to be part of their <a href="https://mapbox.com/community" target="_blank">Community</a> program, which supports nonprofits, educational institutions, and positive impact organizations.
This partnership allows **gpx.studio** to benefit from Mapbox tools at discounted prices, greatly contributing to the financial viability of the project and enabling us to offer the best possible user experience.

View File

@@ -12,7 +12,7 @@ title: Integration
You can use **gpx.studio** to create maps showing your GPX files and embed them in your website. You can use **gpx.studio** to create maps showing your GPX files and embed them in your website.
All you need is: All you need is:
1. A <a href="https://cloud.maptiler.com/auth/widget?next=https://cloud.maptiler.com/maps/" target="_blank">MapTiler key</a> to load the map, and 1. A <a href="https://account.mapbox.com/auth/signup" target="_blank">Mapbox access token</a> to load the map, and
1. GPX files hosted on your server or on Google Drive, or accessible via a public URL. 1. GPX files hosted on your server or on Google Drive, or accessible via a public URL.
You can then play with the configurator below to customize your map and generate the corresponding HTML code. You can then play with the configurator below to customize your map and generate the corresponding HTML code.

View File

@@ -58,7 +58,7 @@ Only one basemap can be displayed at a time.
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<DocsLayers /> <DocsLayers />
<span class="text-sm text-center mt-2"> <span class="text-sm text-center mt-2">
Hover over the map to show the <a href="https://hiking.waymarkedtrails.org" target="_blank">Waymarked Trails hiking</a> overlay on top of the <a href="https://www.maptiler.com/maps/outdoor-topo/" target="_blank">MapTiler Topo</a> basemap. Hover over the map to show the <a href="https://hiking.waymarkedtrails.org" target="_blank">Waymarked Trails hiking</a> overlay on top of the <a href="https://www.mapbox.com/maps/outdoors" target="_blank">Mapbox Outdoors</a> basemap.
</span> </span>
</div> </div>
@@ -67,4 +67,4 @@ They can be enabled in the [map layer settings dialog](./menu/settings).
In these settings, you can also manage the opacity of the overlays. In these settings, you can also manage the opacity of the overlays.
For advanced users, it is possible to add custom basemaps and overlays by providing <a href="https://en.wikipedia.org/wiki/Web_Map_Tile_Service" target="_blank">WMTS</a>, <a href="https://en.wikipedia.org/wiki/Web_Map_Service" target="_blank">WMS</a>, or <a href="https://maplibre.org/maplibre-style-spec/" target="_blank">MapLibre style JSON</a> URLs. For advanced users, it is possible to add custom basemaps and overlays by providing <a href="https://en.wikipedia.org/wiki/Web_Map_Tile_Service" target="_blank">WMTS</a>, <a href="https://en.wikipedia.org/wiki/Web_Map_Service" target="_blank">WMS</a>, or <a href="https://docs.mapbox.com/help/glossary/style/" target="_blank">Mapbox style JSON</a> URLs.

View File

@@ -18,7 +18,7 @@ This tool allows you to add elevation data to traces and [points of interest](..
<DocsNote> <DocsNote>
Elevation data is provided by <a href="https://maptiler.com" target="_blank">MapTiler</a>. Elevation data is provided by <a href="https://mapbox.com" target="_blank">Mapbox</a>.
You can learn more about its origin and accuracy in the <a href="https://docs.maptiler.com/guides/map-tiling-hosting/data-hosting/rgb-terrain-by-maptiler/" target="_blank">documentation</a>. You can learn more about its origin and accuracy in the <a href="https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data" target="_blank">documentation</a>.
</DocsNote> </DocsNote>

View File

@@ -31,7 +31,7 @@ Create a copy of the currently selected files.
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete ### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
. Delete the currently selected files.
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all ### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all

View File

@@ -1,6 +1,6 @@
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import maplibregl from 'maplibre-gl'; import mapboxgl from 'mapbox-gl';
import { ListFileItem, ListWaypointItem } from '$lib/components/file-list/file-list'; import { ListFileItem, ListWaypointItem } from '$lib/components/file-list/file-list';
import { fileStateCollection, GPXFileStateCollectionObserver } from '$lib/logic/file-state'; import { fileStateCollection, GPXFileStateCollectionObserver } from '$lib/logic/file-state';
import { gpxStatistics } from '$lib/logic/statistics'; import { gpxStatistics } from '$lib/logic/statistics';
@@ -10,7 +10,7 @@ import type { Coordinates } from 'gpx';
import { page } from '$app/state'; import { page } from '$app/state';
export class BoundsManager { export class BoundsManager {
private _bounds: maplibregl.LngLatBounds = new maplibregl.LngLatBounds(); private _bounds: mapboxgl.LngLatBounds = new mapboxgl.LngLatBounds();
private _files: Set<string> = new Set(); private _files: Set<string> = new Set();
private _fileStateCollectionObserver: GPXFileStateCollectionObserver | null = null; private _fileStateCollectionObserver: GPXFileStateCollectionObserver | null = null;
private _unsubscribes: (() => void)[] = []; private _unsubscribes: (() => void)[] = [];
@@ -87,12 +87,12 @@ export class BoundsManager {
} }
this._unsubscribes.forEach((unsubscribe) => unsubscribe()); this._unsubscribes.forEach((unsubscribe) => unsubscribe());
this._unsubscribes = []; this._unsubscribes = [];
this._bounds = new maplibregl.LngLatBounds([180, 90, -180, -90]); this._bounds = new mapboxgl.LngLatBounds([180, 90, -180, -90]);
} }
centerMapOnSelection() { centerMapOnSelection() {
let selected = get(selection).getSelected(); let selected = get(selection).getSelected();
let bounds = new maplibregl.LngLatBounds(); let bounds = new mapboxgl.LngLatBounds();
if (selected.find((item) => item instanceof ListWaypointItem)) { if (selected.find((item) => item instanceof ListWaypointItem)) {
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {

View File

@@ -807,7 +807,7 @@ export const fileActions = {
}); });
}); });
}, },
addElevationToSelection: async () => { addElevationToSelection: async (map: mapboxgl.Map) => {
if (get(selection).size === 0) { if (get(selection).size === 0) {
return; return;
} }

View File

@@ -7,8 +7,7 @@ export enum MapCursorState {
TOOL_WITH_CROSSHAIR, TOOL_WITH_CROSSHAIR,
WAYPOINT_HOVER, WAYPOINT_HOVER,
WAYPOINT_DRAGGING, WAYPOINT_DRAGGING,
ANCHOR_HOVER, TRACKPOINT_DRAGGING,
ANCHOR_DRAGGING,
SCISSORS, SCISSORS,
SPLIT_CONTROL, SPLIT_CONTROL,
MAPILLARY_HOVER, MAPILLARY_HOVER,
@@ -21,8 +20,7 @@ const cursorStyles = {
[MapCursorState.LAYER_HOVER]: 'pointer', [MapCursorState.LAYER_HOVER]: 'pointer',
[MapCursorState.WAYPOINT_HOVER]: 'pointer', [MapCursorState.WAYPOINT_HOVER]: 'pointer',
[MapCursorState.WAYPOINT_DRAGGING]: 'grabbing', [MapCursorState.WAYPOINT_DRAGGING]: 'grabbing',
[MapCursorState.ANCHOR_HOVER]: 'pointer', [MapCursorState.TRACKPOINT_DRAGGING]: 'grabbing',
[MapCursorState.ANCHOR_DRAGGING]: 'grabbing',
[MapCursorState.TOOL_WITH_CROSSHAIR]: 'crosshair', [MapCursorState.TOOL_WITH_CROSSHAIR]: 'crosshair',
[MapCursorState.SCISSORS]: scissorsCursor, [MapCursorState.SCISSORS]: scissorsCursor,
[MapCursorState.SPLIT_CONTROL]: 'pointer', [MapCursorState.SPLIT_CONTROL]: 'pointer',

View File

@@ -1,7 +1,6 @@
import { type Database } from '$lib/db'; import { type Database } from '$lib/db';
import { liveQuery } from 'dexie'; import { liveQuery } from 'dexie';
import { import {
basemaps,
defaultBasemap, defaultBasemap,
defaultBasemapTree, defaultBasemapTree,
defaultOpacities, defaultOpacities,
@@ -9,11 +8,7 @@ import {
defaultOverlayTree, defaultOverlayTree,
defaultOverpassQueries, defaultOverpassQueries,
defaultOverpassTree, defaultOverpassTree,
defaultTerrainSource,
overlays,
overpassQueryData,
type CustomLayer, type CustomLayer,
type LayerTreeType,
} from '$lib/assets/layers'; } from '$lib/assets/layers';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { get, writable, type Writable } from 'svelte/store'; import { get, writable, type Writable } from 'svelte/store';
@@ -23,12 +18,10 @@ export class Setting<V> {
private _subscription: { unsubscribe: () => void } | null = null; private _subscription: { unsubscribe: () => void } | null = null;
private _key: string; private _key: string;
private _value: Writable<V>; private _value: Writable<V>;
private _validator?: (value: V) => V;
constructor(key: string, initial: V, validator?: (value: V) => V) { constructor(key: string, initial: V) {
this._key = key; this._key = key;
this._value = writable(initial); this._value = writable(initial);
this._validator = validator;
} }
connectToDatabase(db: Database) { connectToDatabase(db: Database) {
@@ -42,9 +35,6 @@ export class Setting<V> {
this._value.set(value); this._value.set(value);
} }
} else { } else {
if (this._validator) {
value = this._validator(value);
}
this._value.set(value); this._value.set(value);
} }
first = false; first = false;
@@ -82,13 +72,11 @@ export class SettingInitOnFirstRead<V> {
private _key: string; private _key: string;
private _value: Writable<V | undefined>; private _value: Writable<V | undefined>;
private _initial: V; private _initial: V;
private _validator?: (value: V) => V;
constructor(key: string, initial: V, validator?: (value: V) => V) { constructor(key: string, initial: V) {
this._key = key; this._key = key;
this._value = writable(undefined); this._value = writable(undefined);
this._initial = initial; this._initial = initial;
this._validator = validator;
} }
connectToDatabase(db: Database) { connectToDatabase(db: Database) {
@@ -104,9 +92,6 @@ export class SettingInitOnFirstRead<V> {
this._value.set(value); this._value.set(value);
} }
} else { } else {
if (this._validator) {
value = this._validator(value);
}
this._value.set(value); this._value.set(value);
} }
first = false; first = false;
@@ -142,166 +127,36 @@ export class SettingInitOnFirstRead<V> {
} }
} }
function getValueValidator<V>(allowed: V[], fallback: V) {
const dict = new Set<V>(allowed);
return (value: V) => (dict.has(value) ? value : fallback);
}
function getArrayValidator<V>(allowed: V[]) {
const dict = new Set<V>(allowed);
return (value: V[]) => value.filter((v) => dict.has(v));
}
function getLayerValidator(allowed: Record<string, any>, fallback: string) {
return (layer: string) =>
allowed.hasOwnProperty(layer) ||
layer.startsWith('custom-') ||
layer.startsWith('extension-')
? layer
: fallback;
}
function filterLayerTree(t: LayerTreeType, allowed: Record<string, any>): LayerTreeType {
const filtered: LayerTreeType = {};
Object.entries(t).forEach(([key, value]) => {
if (typeof value === 'object') {
filtered[key] = filterLayerTree(value, allowed);
} else if (
allowed.hasOwnProperty(key) ||
key.startsWith('custom-') ||
key.startsWith('extension-')
) {
filtered[key] = value;
}
});
return filtered;
}
function getLayerTreeValidator(allowed: Record<string, any>) {
return (value: LayerTreeType) => filterLayerTree(value, allowed);
}
type DistanceUnits = 'metric' | 'imperial' | 'nautical';
type VelocityUnits = 'speed' | 'pace';
type TemperatureUnits = 'celsius' | 'fahrenheit';
type AdditionalDataset = 'speed' | 'hr' | 'cad' | 'atemp' | 'power';
type ElevationFill = 'slope' | 'surface' | undefined;
type RoutingProfile =
| 'bike'
| 'racing_bike'
| 'gravel_bike'
| 'mountain_bike'
| 'foot'
| 'motorcycle'
| 'water'
| 'railway';
type TerrainSource = 'maptiler-dem' | 'mapterhorn';
type StreetViewSource = 'mapillary' | 'google';
export const settings = { export const settings = {
distanceUnits: new Setting<DistanceUnits>( distanceUnits: new Setting<'metric' | 'imperial' | 'nautical'>('distanceUnits', 'metric'),
'distanceUnits', velocityUnits: new Setting<'speed' | 'pace'>('velocityUnits', 'speed'),
'metric', temperatureUnits: new Setting<'celsius' | 'fahrenheit'>('temperatureUnits', 'celsius'),
getValueValidator<DistanceUnits>(['metric', 'imperial', 'nautical'], 'metric')
),
velocityUnits: new Setting<VelocityUnits>(
'velocityUnits',
'speed',
getValueValidator<VelocityUnits>(['speed', 'pace'], 'speed')
),
temperatureUnits: new Setting<TemperatureUnits>(
'temperatureUnits',
'celsius',
getValueValidator<TemperatureUnits>(['celsius', 'fahrenheit'], 'celsius')
),
elevationProfile: new Setting<boolean>('elevationProfile', true), elevationProfile: new Setting<boolean>('elevationProfile', true),
additionalDatasets: new Setting<AdditionalDataset[]>( additionalDatasets: new Setting<string[]>('additionalDatasets', []),
'additionalDatasets', elevationFill: new Setting<'slope' | 'surface' | undefined>('elevationFill', undefined),
[],
getArrayValidator<AdditionalDataset>(['speed', 'hr', 'cad', 'atemp', 'power'])
),
elevationFill: new Setting<ElevationFill>(
'elevationFill',
undefined,
getValueValidator(['slope', 'surface', undefined], undefined)
),
treeFileView: new Setting<boolean>('fileView', false), treeFileView: new Setting<boolean>('fileView', false),
minimizeRoutingMenu: new Setting('minimizeRoutingMenu', false), minimizeRoutingMenu: new Setting('minimizeRoutingMenu', false),
routing: new Setting('routing', true), routing: new Setting('routing', true),
routingProfile: new Setting<RoutingProfile>( routingProfile: new Setting('routingProfile', 'bike'),
'routingProfile',
'bike',
getValueValidator<RoutingProfile>(
[
'bike',
'racing_bike',
'gravel_bike',
'mountain_bike',
'foot',
'motorcycle',
'water',
'railway',
],
'bike'
)
),
privateRoads: new Setting('privateRoads', false), privateRoads: new Setting('privateRoads', false),
currentBasemap: new Setting( currentBasemap: new Setting('currentBasemap', defaultBasemap),
'currentBasemap', previousBasemap: new Setting('previousBasemap', defaultBasemap),
defaultBasemap, selectedBasemapTree: new Setting('selectedBasemapTree', defaultBasemapTree),
getLayerValidator(basemaps, defaultBasemap) currentOverlays: new SettingInitOnFirstRead('currentOverlays', defaultOverlays),
), previousOverlays: new Setting('previousOverlays', defaultOverlays),
previousBasemap: new Setting( selectedOverlayTree: new Setting('selectedOverlayTree', defaultOverlayTree),
'previousBasemap',
defaultBasemap,
getLayerValidator(Object.keys(basemaps), defaultBasemap)
),
selectedBasemapTree: new Setting(
'selectedBasemapTree',
defaultBasemapTree,
getLayerTreeValidator(basemaps)
),
currentOverlays: new SettingInitOnFirstRead(
'currentOverlays',
defaultOverlays,
getLayerTreeValidator(overlays)
),
previousOverlays: new Setting(
'previousOverlays',
defaultOverlays,
getLayerTreeValidator(overlays)
),
selectedOverlayTree: new Setting(
'selectedOverlayTree',
defaultOverlayTree,
getLayerTreeValidator(overlays)
),
currentOverpassQueries: new SettingInitOnFirstRead( currentOverpassQueries: new SettingInitOnFirstRead(
'currentOverpassQueries', 'currentOverpassQueries',
defaultOverpassQueries, defaultOverpassQueries
getLayerTreeValidator(overpassQueryData)
),
selectedOverpassTree: new Setting(
'selectedOverpassTree',
defaultOverpassTree,
getLayerTreeValidator(overpassQueryData)
), ),
selectedOverpassTree: new Setting('selectedOverpassTree', defaultOverpassTree),
opacities: new Setting('opacities', defaultOpacities), opacities: new Setting('opacities', defaultOpacities),
customLayers: new Setting<Record<string, CustomLayer>>('customLayers', {}), customLayers: new Setting<Record<string, CustomLayer>>('customLayers', {}),
customBasemapOrder: new Setting<string[]>('customBasemapOrder', []), customBasemapOrder: new Setting<string[]>('customBasemapOrder', []),
customOverlayOrder: new Setting<string[]>('customOverlayOrder', []), customOverlayOrder: new Setting<string[]>('customOverlayOrder', []),
terrainSource: new Setting<TerrainSource>(
'terrainSource',
defaultTerrainSource,
getValueValidator(['maptiler-dem', 'mapterhorn'], defaultTerrainSource)
),
directionMarkers: new Setting('directionMarkers', false), directionMarkers: new Setting('directionMarkers', false),
distanceMarkers: new Setting('distanceMarkers', false), distanceMarkers: new Setting('distanceMarkers', false),
streetViewSource: new Setting<StreetViewSource>( streetViewSource: new Setting('streetViewSource', 'mapillary'),
'streetViewSource',
'mapillary',
getValueValidator<StreetViewSource>(['mapillary', 'google'], 'mapillary')
),
fileOrder: new Setting<string[]>('fileOrder', []), fileOrder: new Setting<string[]>('fileOrder', []),
defaultOpacity: new Setting('defaultOpacity', 0.7), defaultOpacity: new Setting('defaultOpacity', 0.7),
defaultWidth: new Setting('defaultWidth', browser && window.innerWidth < 600 ? 8 : 5), defaultWidth: new Setting('defaultWidth', browser && window.innerWidth < 600 ? 8 : 5),

View File

@@ -1,24 +1,18 @@
import { ListItem, ListLevel } from '$lib/components/file-list/file-list'; import { ListItem, ListLevel } from '$lib/components/file-list/file-list';
import { GPXFile, GPXStatistics, GPXStatisticsGroup, type Track } from 'gpx'; import { GPXFile, GPXStatistics, GPXStatisticsGroup, type Track } from 'gpx';
import maplibregl from 'maplibre-gl';
export class GPXStatisticsTree { export class GPXStatisticsTree {
level: ListLevel; level: ListLevel;
statistics: { statistics: {
[key: string]: GPXStatisticsTree | GPXStatistics; [key: string]: GPXStatisticsTree | GPXStatistics;
} = {}; } = {};
wptBounds: maplibregl.LngLatBounds;
constructor(element: GPXFile | Track) { constructor(element: GPXFile | Track) {
this.wptBounds = new maplibregl.LngLatBounds();
if (element instanceof GPXFile) { if (element instanceof GPXFile) {
this.level = ListLevel.FILE; this.level = ListLevel.FILE;
element.children.forEach((child, index) => { element.children.forEach((child, index) => {
this.statistics[index] = new GPXStatisticsTree(child); this.statistics[index] = new GPXStatisticsTree(child);
}); });
element.wpt.forEach((wpt) => {
this.wptBounds.extend(wpt.getCoordinates());
});
} else { } else {
this.level = ListLevel.TRACK; this.level = ListLevel.TRACK;
element.children.forEach((child, index) => { element.children.forEach((child, index) => {
@@ -48,27 +42,5 @@ export class GPXStatisticsTree {
} }
return statistics; return statistics;
} }
intersectsBBox(bounds: maplibregl.LngLatBounds): boolean {
for (let key in this.statistics) {
const stats = this.statistics[key];
if (stats instanceof GPXStatistics) {
const bbox = new maplibregl.LngLatBounds(
stats.global.bounds.southWest,
stats.global.bounds.northEast
);
if (!bbox.isEmpty() && bbox.intersects(bounds)) {
return true;
}
} else if (stats.intersectsBBox(bounds)) {
return true;
}
}
return false;
}
intersectsWaypointBBox(bounds: maplibregl.LngLatBounds): boolean {
return !this.wptBounds.isEmpty() && this.wptBounds.intersects(bounds);
}
} }
export type GPXFileWithStatistics = { file: GPXFile; statistics: GPXStatisticsTree }; export type GPXFileWithStatistics = { file: GPXFile; statistics: GPXStatisticsTree };

View File

@@ -1,5 +1,5 @@
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import { GPXGlobalStatistics, GPXStatisticsGroup, type Coordinates } from 'gpx'; import { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
import { fileStateCollection, GPXFileState } from '$lib/logic/file-state'; import { fileStateCollection, GPXFileState } from '$lib/logic/file-state';
import { import {
ListFileItem, ListFileItem,
@@ -82,8 +82,6 @@ export const gpxStatistics = new SelectedGPXStatistics();
export const slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined> = export const slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined> =
writable(undefined); writable(undefined);
export const hoveredPoint: Writable<Coordinates | null> = writable(null);
gpxStatistics.subscribe(() => { gpxStatistics.subscribe(() => {
slicedGPXStatistics.set(undefined); slicedGPXStatistics.set(undefined);
}); });

View File

@@ -3,11 +3,12 @@ import { twMerge } from 'tailwind-merge';
import { base } from '$app/paths'; import { base } from '$app/paths';
import { languages } from '$lib/languages'; import { languages } from '$lib/languages';
import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance, GPXFile } from 'gpx'; import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance, GPXFile } from 'gpx';
import maplibregl from 'maplibre-gl'; import mapboxgl from 'mapbox-gl';
import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt'; import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt';
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import PNGReader from 'png.js';
import type { GPXStatisticsTree } from '$lib/logic/statistics-tree'; import type { GPXStatisticsTree } from '$lib/logic/statistics-tree';
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list'; import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
import { PUBLIC_MAPTILER_KEY } from '$env/static/public';
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
@@ -61,7 +62,7 @@ export function getClosestTrackSegments(
let segmentBounds = segmentStatistics.global.bounds; let segmentBounds = segmentStatistics.global.bounds;
let northEast = segmentBounds.northEast; let northEast = segmentBounds.northEast;
let southWest = segmentBounds.southWest; let southWest = segmentBounds.southWest;
let bounds = new maplibregl.LngLatBounds(southWest, northEast); let bounds = new mapboxgl.LngLatBounds(southWest, northEast);
if (bounds.contains(point)) { if (bounds.contains(point)) {
segmentBoundsDistances.push([0, trackIndex, segmentIndex]); segmentBoundsDistances.push([0, trackIndex, segmentIndex]);
} else { } else {
@@ -109,49 +110,33 @@ export function getElevation(
let coordinates = points.map((point) => let coordinates = points.map((point) =>
point instanceof TrackPoint || point instanceof Waypoint ? point.getCoordinates() : point point instanceof TrackPoint || point instanceof Waypoint ? point.getCoordinates() : point
); );
let bbox = new maplibregl.LngLatBounds(); let bbox = new mapboxgl.LngLatBounds();
coordinates.forEach((coord) => bbox.extend(coord)); coordinates.forEach((coord) => bbox.extend(coord));
let tiles = coordinates.map((coord) => pointToTile(coord.lon, coord.lat, ELEVATION_ZOOM)); let tiles = coordinates.map((coord) => pointToTile(coord.lon, coord.lat, ELEVATION_ZOOM));
let uniqueTiles = Array.from(new Set(tiles.map((tile) => tile.join(',')))).map((tile) => let uniqueTiles = Array.from(new Set(tiles.map((tile) => tile.join(',')))).map((tile) =>
tile.split(',').map((x) => parseInt(x)) tile.split(',').map((x) => parseInt(x))
); );
let images = new Map<string, ImageData>(); let pngs = new Map<string, any>();
const getPixelFromImageData = (imageData: ImageData, x: number, y: number): number[] => {
const index = (y * imageData.width + x) * 4;
return [imageData.data[index], imageData.data[index + 1], imageData.data[index + 2]];
};
let promises = uniqueTiles.map((tile) => let promises = uniqueTiles.map((tile) =>
fetch( fetch(
`https://api.maptiler.com/tiles/terrain-rgb-v2/${ELEVATION_ZOOM}/${tile[0]}/${tile[1]}.webp?key=${PUBLIC_MAPTILER_KEY}`, `https://api.mapbox.com/v4/mapbox.mapbox-terrain-dem-v1/${ELEVATION_ZOOM}/${tile[0]}/${tile[1]}@2x.pngraw?access_token=${PUBLIC_MAPBOX_TOKEN}`,
{ cache: 'force-cache' } { cache: 'force-cache' }
) )
.then((response) => response.blob()) .then((response) => response.arrayBuffer())
.then( .then(
(blob) => (buffer) =>
new Promise<void>((resolve) => { new Promise((resolve) => {
const url = URL.createObjectURL(blob); let png = new PNGReader(new Uint8Array(buffer));
const img = new Image(); png.parse((err, png) => {
img.onload = () => { if (err) {
const canvas = document.createElement('canvas'); resolve(false); // Also resolve so that Promise.all doesn't fail
canvas.width = img.width; } else {
canvas.height = img.height; pngs.set(tile.join(','), png);
const ctx = canvas.getContext('2d'); resolve(true);
if (ctx) {
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
images.set(tile.join(','), imageData);
} }
URL.revokeObjectURL(url); });
resolve();
};
img.onerror = () => {
URL.revokeObjectURL(url);
resolve();
};
img.src = url;
}) })
) )
); );
@@ -159,9 +144,9 @@ export function getElevation(
return Promise.all(promises).then(() => return Promise.all(promises).then(() =>
coordinates.map((coord, index) => { coordinates.map((coord, index) => {
let tile = tiles[index]; let tile = tiles[index];
let imageData = images.get(tile.join(',')); let png = pngs.get(tile.join(','));
if (!imageData) { if (!png) {
return 0; return 0;
} }
@@ -173,11 +158,10 @@ export function getElevation(
let dx = x - _x; let dx = x - _x;
let dy = y - _y; let dy = y - _y;
const p00 = getPixelFromImageData(imageData, _x, _y); const p00 = png.getPixel(_x, _y);
const p01 = getPixelFromImageData(imageData, _x, _y + (_y + 1 == tileSize ? 0 : 1)); const p01 = png.getPixel(_x, _y + (_y + 1 == tileSize ? 0 : 1));
const p10 = getPixelFromImageData(imageData, _x + (_x + 1 == tileSize ? 0 : 1), _y); const p10 = png.getPixel(_x + (_x + 1 == tileSize ? 0 : 1), _y);
const p11 = getPixelFromImageData( const p11 = png.getPixel(
imageData,
_x + (_x + 1 == tileSize ? 0 : 1), _x + (_x + 1 == tileSize ? 0 : 1),
_y + (_y + 1 == tileSize ? 0 : 1) _y + (_y + 1 == tileSize ? 0 : 1)
); );
@@ -197,18 +181,6 @@ export function getElevation(
); );
} }
export function loadSVGIcon(map: maplibregl.Map, id: string, svg: string, size: number = 100) {
if (!map.hasImage(id)) {
let icon = new Image(size, size);
icon.onload = () => {
if (!map.hasImage(id)) {
map.addImage(id, icon);
}
};
icon.src = 'data:image/svg+xml,' + encodeURIComponent(svg);
}
}
export function isMac() { export function isMac() {
return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0; return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
} }

View File

@@ -282,7 +282,6 @@
"update": "Update layer" "update": "Update layer"
}, },
"opacity": "Overlay opacity", "opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Basemaps", "basemaps": "Basemaps",
"overlays": "Overlays", "overlays": "Overlays",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -380,9 +378,7 @@
"railway-station": "Railway Station", "railway-station": "Railway Station",
"tram-stop": "Tram Stop", "tram-stop": "Tram Stop",
"bus-stop": "Bus Stop", "bus-stop": "Bus Stop",
"ferry": "Ferry", "ferry": "Ferry"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Actualitza la capa" "update": "Actualitza la capa"
}, },
"opacity": "Opacitat de la superposició", "opacity": "Opacitat de la superposició",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Mapes base", "basemaps": "Mapes base",
"overlays": "Capes", "overlays": "Capes",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -380,9 +378,7 @@
"railway-station": "Estació de tren", "railway-station": "Estació de tren",
"tram-stop": "Parada de tramvia", "tram-stop": "Parada de tramvia",
"bus-stop": "Parada d'autobús", "bus-stop": "Parada d'autobús",
"ferry": "Ferri", "ferry": "Ferri"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Aktualizovat vrstvu" "update": "Aktualizovat vrstvu"
}, },
"opacity": "Průhlednost překryvu", "opacity": "Průhlednost překryvu",
"terrain": "Zdroj terénu",
"label": { "label": {
"basemaps": "Základní mapy", "basemaps": "Základní mapy",
"overlays": "Překrytí", "overlays": "Překrytí",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Vrstevnice", "swisstopoSlope": "swisstopo Vrstevnice",
"swisstopoHiking": "swisstopo Turistická", "swisstopoHiking": "swisstopo Turistická",
"swisstopoHikingClosures": "swisstopo Turistické uzávěry", "swisstopoHikingClosures": "swisstopo Turistické uzávěry",
@@ -380,9 +378,7 @@
"railway-station": "Železniční stanice", "railway-station": "Železniční stanice",
"tram-stop": "Zastávka tramvaje", "tram-stop": "Zastávka tramvaje",
"bus-stop": "Autobusová zastávka", "bus-stop": "Autobusová zastávka",
"ferry": "Trajekt", "ferry": "Trajekt"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Update layer" "update": "Update layer"
}, },
"opacity": "Overlay opacity", "opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Basemaps", "basemaps": "Basemaps",
"overlays": "Overlays", "overlays": "Overlays",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -380,9 +378,7 @@
"railway-station": "Railway Station", "railway-station": "Railway Station",
"tram-stop": "Tram Stop", "tram-stop": "Tram Stop",
"bus-stop": "Bus Stop", "bus-stop": "Bus Stop",
"ferry": "Ferry", "ferry": "Ferry"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Layer aktualisieren" "update": "Layer aktualisieren"
}, },
"opacity": "Deckkraft der Überlagerung", "opacity": "Deckkraft der Überlagerung",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Basiskarte", "basemaps": "Basiskarte",
"overlays": "Ebenen", "overlays": "Ebenen",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Neigung", "swisstopoSlope": "swisstopo Neigung",
"swisstopoHiking": "swisstopo Wandern", "swisstopoHiking": "swisstopo Wandern",
"swisstopoHikingClosures": "swisstopo Wanderungen Schließungen", "swisstopoHikingClosures": "swisstopo Wanderungen Schließungen",
@@ -380,9 +378,7 @@
"railway-station": "Bahnhof", "railway-station": "Bahnhof",
"tram-stop": "Straßenbahnhaltestelle", "tram-stop": "Straßenbahnhaltestelle",
"bus-stop": "Bushaltestelle", "bus-stop": "Bushaltestelle",
"ferry": "Fähre", "ferry": "Fähre"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Update layer" "update": "Update layer"
}, },
"opacity": "Overlay opacity", "opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Basemaps", "basemaps": "Basemaps",
"overlays": "Overlays", "overlays": "Overlays",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -380,9 +378,7 @@
"railway-station": "Railway Station", "railway-station": "Railway Station",
"tram-stop": "Tram Stop", "tram-stop": "Tram Stop",
"bus-stop": "Bus Stop", "bus-stop": "Bus Stop",
"ferry": "Ferry", "ferry": "Ferry"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -231,7 +231,7 @@
}, },
"elevation": { "elevation": {
"button": "Request elevation data", "button": "Request elevation data",
"help": "Requesting elevation data will erase the existing elevation data, if any, and replace it with data from MapTiler.", "help": "Requesting elevation data will erase the existing elevation data, if any, and replace it with data from Mapbox.",
"help_no_selection": "Select a file item to request elevation data." "help_no_selection": "Select a file item to request elevation data."
}, },
"waypoint": { "waypoint": {
@@ -273,7 +273,7 @@
"new": "New custom layer", "new": "New custom layer",
"edit": "Edit custom layer", "edit": "Edit custom layer",
"urls": "URL(s)", "urls": "URL(s)",
"url_placeholder": "WMTS, WMS or MapLibre style JSON", "url_placeholder": "WMTS, WMS or Mapbox style JSON",
"max_zoom": "Max zoom", "max_zoom": "Max zoom",
"layer_type": "Layer type", "layer_type": "Layer type",
"basemap": "Basemap", "basemap": "Basemap",
@@ -282,7 +282,6 @@
"update": "Update layer" "update": "Update layer"
}, },
"opacity": "Overlay opacity", "opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Basemaps", "basemaps": "Basemaps",
"overlays": "Overlays", "overlays": "Overlays",
@@ -300,9 +299,8 @@
"switzerland": "Switzerland", "switzerland": "Switzerland",
"united_kingdom": "United Kingdom", "united_kingdom": "United Kingdom",
"united_states": "United States", "united_states": "United States",
"maptilerTopo": "MapTiler Topo", "mapboxOutdoors": "Mapbox Outdoors",
"maptilerOutdoors": "MapTiler Outdoors", "mapboxSatellite": "Mapbox Satellite",
"maptilerSatellite": "MapTiler Satellite",
"openStreetMap": "OpenStreetMap", "openStreetMap": "OpenStreetMap",
"openTopoMap": "OpenTopoMap", "openTopoMap": "OpenTopoMap",
"openHikingMap": "OpenHikingMap", "openHikingMap": "OpenHikingMap",
@@ -326,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -381,9 +378,7 @@
"railway-station": "Railway Station", "railway-station": "Railway Station",
"tram-stop": "Tram Stop", "tram-stop": "Tram Stop",
"bus-stop": "Bus Stop", "bus-stop": "Bus Stop",
"ferry": "Ferry", "ferry": "Ferry"
"maptiler-dem": "MapTiler DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {
@@ -524,7 +519,7 @@
}, },
"embedding": { "embedding": {
"title": "Create your own map", "title": "Create your own map",
"maptiler_key": "MapTiler key", "mapbox_token": "Mapbox access token",
"file_urls": "File URLs (separated by commas)", "file_urls": "File URLs (separated by commas)",
"drive_ids": "Google Drive file IDs (separated by commas)", "drive_ids": "Google Drive file IDs (separated by commas)",
"basemap": "Basemap", "basemap": "Basemap",

View File

@@ -282,7 +282,6 @@
"update": "Actualizar capa" "update": "Actualizar capa"
}, },
"opacity": "Opacidad de la capa superpuesta", "opacity": "Opacidad de la capa superpuesta",
"terrain": "Origen del terreno",
"label": { "label": {
"basemaps": "Mapas base", "basemaps": "Mapas base",
"overlays": "Capas", "overlays": "Capas",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "Gravel bikerouter.de", "bikerouterGravel": "Gravel bikerouter.de",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Senderismo", "swisstopoHiking": "swisstopo Senderismo",
"swisstopoHikingClosures": "swisstopo Rutas Senderismo", "swisstopoHikingClosures": "swisstopo Rutas Senderismo",
@@ -380,9 +378,7 @@
"railway-station": "Estación de tren", "railway-station": "Estación de tren",
"tram-stop": "Parada de tranvía", "tram-stop": "Parada de tranvía",
"bus-stop": "Parada de autobús", "bus-stop": "Parada de autobús",
"ferry": "Ferri", "ferry": "Ferri"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Eguneratu geruza" "update": "Eguneratu geruza"
}, },
"opacity": "Geruzaren opakutasuna", "opacity": "Geruzaren opakutasuna",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Oinarrizko mapak", "basemaps": "Oinarrizko mapak",
"overlays": "Geruzak", "overlays": "Geruzak",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Malda", "swisstopoSlope": "swisstopo Malda",
"swisstopoHiking": "swisstopo Mendi ibilaldiak", "swisstopoHiking": "swisstopo Mendi ibilaldiak",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -380,9 +378,7 @@
"railway-station": "Tren geltokia", "railway-station": "Tren geltokia",
"tram-stop": "Tranbia geltokia", "tram-stop": "Tranbia geltokia",
"bus-stop": "Autobus geltokia", "bus-stop": "Autobus geltokia",
"ferry": "Ferria", "ferry": "Ferria"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Päivitä karttataso" "update": "Päivitä karttataso"
}, },
"opacity": "Peitetason läpinäkyvyys", "opacity": "Peitetason läpinäkyvyys",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Taustakartat", "basemaps": "Taustakartat",
"overlays": "Peitetasot", "overlays": "Peitetasot",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Rinnekaltevuus", "swisstopoSlope": "swisstopo Rinnekaltevuus",
"swisstopoHiking": "swisstopo Retkeilyreitit", "swisstopoHiking": "swisstopo Retkeilyreitit",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -380,9 +378,7 @@
"railway-station": "Rautatieasemat", "railway-station": "Rautatieasemat",
"tram-stop": "Raitiovaunupysäkit", "tram-stop": "Raitiovaunupysäkit",
"bus-stop": "Linja-autopysäkit", "bus-stop": "Linja-autopysäkit",
"ferry": "Lautat", "ferry": "Lautat"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Mettre à jour la couche" "update": "Mettre à jour la couche"
}, },
"opacity": "Opacité de la surcouche", "opacity": "Opacité de la surcouche",
"terrain": "Source du relief",
"label": { "label": {
"basemaps": "Fonds de carte", "basemaps": "Fonds de carte",
"overlays": "Surcouches", "overlays": "Surcouches",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Relief",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Pente", "swisstopoSlope": "swisstopo Pente",
"swisstopoHiking": "swisstopo Randonnée", "swisstopoHiking": "swisstopo Randonnée",
"swisstopoHikingClosures": "swisstopo Fermetures de randonnée", "swisstopoHikingClosures": "swisstopo Fermetures de randonnée",
@@ -380,9 +378,7 @@
"railway-station": "Gare", "railway-station": "Gare",
"tram-stop": "Arrêt de tram", "tram-stop": "Arrêt de tram",
"bus-stop": "Arrêt de bus", "bus-stop": "Arrêt de bus",
"ferry": "Ferry", "ferry": "Ferry"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Update layer" "update": "Update layer"
}, },
"opacity": "Overlay opacity", "opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Basemaps", "basemaps": "Basemaps",
"overlays": "Overlays", "overlays": "Overlays",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -380,9 +378,7 @@
"railway-station": "Railway Station", "railway-station": "Railway Station",
"tram-stop": "Tram Stop", "tram-stop": "Tram Stop",
"bus-stop": "Bus Stop", "bus-stop": "Bus Stop",
"ferry": "Ferry", "ferry": "Ferry"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Réteg feltöltése" "update": "Réteg feltöltése"
}, },
"opacity": "Átfedés átlátszósága", "opacity": "Átfedés átlátszósága",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Alaptérkép", "basemaps": "Alaptérkép",
"overlays": "Térkép rétegek", "overlays": "Térkép rétegek",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "kerékpár és terepkerékpár út", "bikerouterGravel": "kerékpár és terepkerékpár út",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Lejtő", "swisstopoSlope": "swisstopo Lejtő",
"swisstopoHiking": "swisstopo Túra", "swisstopoHiking": "swisstopo Túra",
"swisstopoHikingClosures": "swisstopo túralezárások", "swisstopoHikingClosures": "swisstopo túralezárások",
@@ -380,9 +378,7 @@
"railway-station": "Vasútállomás", "railway-station": "Vasútállomás",
"tram-stop": "Villamos megálló", "tram-stop": "Villamos megálló",
"bus-stop": "Buszmegálló", "bus-stop": "Buszmegálló",
"ferry": "Komp", "ferry": "Komp"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Perbarui lapisan" "update": "Perbarui lapisan"
}, },
"opacity": "Opasitas Overlay", "opacity": "Opasitas Overlay",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Peta dasar", "basemaps": "Peta dasar",
"overlays": "Overlay", "overlays": "Overlay",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Kemiringan", "swisstopoSlope": "swisstopo Kemiringan",
"swisstopoHiking": "swisstopo Pendakian", "swisstopoHiking": "swisstopo Pendakian",
"swisstopoHikingClosures": "Penutupan Jalur Pendakian swisstopo", "swisstopoHikingClosures": "Penutupan Jalur Pendakian swisstopo",
@@ -380,9 +378,7 @@
"railway-station": "Stasiun kereta api", "railway-station": "Stasiun kereta api",
"tram-stop": "Halt trem", "tram-stop": "Halt trem",
"bus-stop": "Pemberhentian Bus", "bus-stop": "Pemberhentian Bus",
"ferry": "Feri", "ferry": "Feri"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Aggiorna livello" "update": "Aggiorna livello"
}, },
"opacity": "Opacità di sovrapposizione", "opacity": "Opacità di sovrapposizione",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Mappe di base", "basemaps": "Mappe di base",
"overlays": "Sovrapposizioni", "overlays": "Sovrapposizioni",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Pendenza", "swisstopoSlope": "swisstopo Pendenza",
"swisstopoHiking": "swisstopo Escursione", "swisstopoHiking": "swisstopo Escursione",
"swisstopoHikingClosures": "swisstopo Fine escursione", "swisstopoHikingClosures": "swisstopo Fine escursione",
@@ -380,9 +378,7 @@
"railway-station": "Stazione ferroviaria", "railway-station": "Stazione ferroviaria",
"tram-stop": "Fermata del tram", "tram-stop": "Fermata del tram",
"bus-stop": "Fermata dell'autobus", "bus-stop": "Fermata dell'autobus",
"ferry": "Traghetto", "ferry": "Traghetto"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "레이어 갱신" "update": "레이어 갱신"
}, },
"opacity": "오버레이 투명도", "opacity": "오버레이 투명도",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "배경 지도", "basemaps": "배경 지도",
"overlays": "오버레이", "overlays": "오버레이",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -380,9 +378,7 @@
"railway-station": "철도역", "railway-station": "철도역",
"tram-stop": "트램 정류장", "tram-stop": "트램 정류장",
"bus-stop": "버스 정류장", "bus-stop": "버스 정류장",
"ferry": "페리", "ferry": "페리"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Naujinti sluoksnį" "update": "Naujinti sluoksnį"
}, },
"opacity": "Sluoksnio skaidrumas", "opacity": "Sluoksnio skaidrumas",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Pagrindo žemėlapiai", "basemaps": "Pagrindo žemėlapiai",
"overlays": "Sluoksniai", "overlays": "Sluoksniai",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -380,9 +378,7 @@
"railway-station": "Geležinkelio stotis", "railway-station": "Geležinkelio stotis",
"tram-stop": "Tramvajaus stotelė", "tram-stop": "Tramvajaus stotelė",
"bus-stop": "Autobusų stotelė", "bus-stop": "Autobusų stotelė",
"ferry": "Keltas", "ferry": "Keltas"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Update layer" "update": "Update layer"
}, },
"opacity": "Overlay opacity", "opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Basemaps", "basemaps": "Basemaps",
"overlays": "Overlays", "overlays": "Overlays",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -380,9 +378,7 @@
"railway-station": "Railway Station", "railway-station": "Railway Station",
"tram-stop": "Tram Stop", "tram-stop": "Tram Stop",
"bus-stop": "Bus Stop", "bus-stop": "Bus Stop",
"ferry": "Ferry", "ferry": "Ferry"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Update laag" "update": "Update laag"
}, },
"opacity": "Laag Transparantie", "opacity": "Laag Transparantie",
"terrain": "Terrein bron",
"label": { "label": {
"basemaps": "Basis kaarten", "basemaps": "Basis kaarten",
"overlays": "Lagen", "overlays": "Lagen",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Grind", "bikerouterGravel": "bikerouter.de Grind",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Helling", "swisstopoSlope": "swisstopo Helling",
"swisstopoHiking": "swisstopo Wandelen", "swisstopoHiking": "swisstopo Wandelen",
"swisstopoHikingClosures": "swisstopo Hiking Sluiting", "swisstopoHikingClosures": "swisstopo Hiking Sluiting",
@@ -380,9 +378,7 @@
"railway-station": "Treinstation", "railway-station": "Treinstation",
"tram-stop": "Tramhalte", "tram-stop": "Tramhalte",
"bus-stop": "Bushalte", "bus-stop": "Bushalte",
"ferry": "Veerboot", "ferry": "Veerboot"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Oppdater lag" "update": "Oppdater lag"
}, },
"opacity": "Gjennomsiktighet for overlegg", "opacity": "Gjennomsiktighet for overlegg",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Basiskart", "basemaps": "Basiskart",
"overlays": "Overlag", "overlays": "Overlag",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "sykkelrute Grus", "bikerouterGravel": "sykkelrute Grus",
"cyclOSMlite": "SyklOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "SyklOSM Lite",
"swisstopoSlope": "swisstopografisk helningskart", "swisstopoSlope": "swisstopografisk helningskart",
"swisstopoHiking": "swisstopografisk Fottur", "swisstopoHiking": "swisstopografisk Fottur",
"swisstopoHikingClosures": "swisstopografi Stengte turstier", "swisstopoHikingClosures": "swisstopografi Stengte turstier",
@@ -380,9 +378,7 @@
"railway-station": "Jernbanestasjon", "railway-station": "Jernbanestasjon",
"tram-stop": "Trikkestopp", "tram-stop": "Trikkestopp",
"bus-stop": "Bussholdeplass", "bus-stop": "Bussholdeplass",
"ferry": "Ferge", "ferry": "Ferge"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Zaktualizuj warstwę" "update": "Zaktualizuj warstwę"
}, },
"opacity": "Przezroczystość nakładki", "opacity": "Przezroczystość nakładki",
"terrain": "Źródło danych terenowych",
"label": { "label": {
"basemaps": "Mapy bazowe", "basemaps": "Mapy bazowe",
"overlays": "Nakładki", "overlays": "Nakładki",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Stoki", "swisstopoSlope": "swisstopo Stoki",
"swisstopoHiking": "swisstopo Szlaki Turystyczne", "swisstopoHiking": "swisstopo Szlaki Turystyczne",
"swisstopoHikingClosures": "swisstopo Zamknięcia Szlaków", "swisstopoHikingClosures": "swisstopo Zamknięcia Szlaków",
@@ -380,9 +378,7 @@
"railway-station": "Stacja kolejowa", "railway-station": "Stacja kolejowa",
"tram-stop": "Przystanek tramwajowy", "tram-stop": "Przystanek tramwajowy",
"bus-stop": "Przystanek autobusowy", "bus-stop": "Przystanek autobusowy",
"ferry": "Prom", "ferry": "Prom"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Atualizar camada" "update": "Atualizar camada"
}, },
"opacity": "Opacidade de sobreposição", "opacity": "Opacidade de sobreposição",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Mapa base", "basemaps": "Mapa base",
"overlays": "Sobreposições", "overlays": "Sobreposições",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -380,9 +378,7 @@
"railway-station": "Estações ferroviárias", "railway-station": "Estações ferroviárias",
"tram-stop": "Parada de bonde", "tram-stop": "Parada de bonde",
"bus-stop": "Parada de Ônibus", "bus-stop": "Parada de Ônibus",
"ferry": "Balsa", "ferry": "Balsa"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Atualizar camada" "update": "Atualizar camada"
}, },
"opacity": "Opacidade da sobreposição", "opacity": "Opacidade da sobreposição",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Mapas base", "basemaps": "Mapas base",
"overlays": "Sobreposições", "overlays": "Sobreposições",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -380,9 +378,7 @@
"railway-station": "Estações ferroviárias", "railway-station": "Estações ferroviárias",
"tram-stop": "Parada de bonde", "tram-stop": "Parada de bonde",
"bus-stop": "Parada de Ônibus", "bus-stop": "Parada de Ônibus",
"ferry": "Balsa", "ferry": "Balsa"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Actualizează stratul" "update": "Actualizează stratul"
}, },
"opacity": "Opacitatea overlay-ului", "opacity": "Opacitatea overlay-ului",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Hărți de bază", "basemaps": "Hărți de bază",
"overlays": "Suprapuneri", "overlays": "Suprapuneri",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -380,9 +378,7 @@
"railway-station": "Gară", "railway-station": "Gară",
"tram-stop": "Tram Stop", "tram-stop": "Tram Stop",
"bus-stop": "Stație de autobuz", "bus-stop": "Stație de autobuz",
"ferry": "Feribot", "ferry": "Feribot"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Обновить слой" "update": "Обновить слой"
}, },
"opacity": "Прозрачность наложения", "opacity": "Прозрачность наложения",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Основные карты", "basemaps": "Основные карты",
"overlays": "Наложения", "overlays": "Наложения",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -380,9 +378,7 @@
"railway-station": "Железнодорожная станция", "railway-station": "Железнодорожная станция",
"tram-stop": "Трамвайная остановка", "tram-stop": "Трамвайная остановка",
"bus-stop": "Автобусная остановка", "bus-stop": "Автобусная остановка",
"ferry": "Паром", "ferry": "Паром"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Ažurirajte sloj" "update": "Ažurirajte sloj"
}, },
"opacity": "Providnost preklapanja", "opacity": "Providnost preklapanja",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Osnovne mape", "basemaps": "Osnovne mape",
"overlays": "Preklapanja", "overlays": "Preklapanja",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -380,9 +378,7 @@
"railway-station": "Železnička stanica", "railway-station": "Železnička stanica",
"tram-stop": "Tramvajsko stajalište", "tram-stop": "Tramvajsko stajalište",
"bus-stop": "Autobusko stajalište", "bus-stop": "Autobusko stajalište",
"ferry": "Trajekt", "ferry": "Trajekt"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Update layer" "update": "Update layer"
}, },
"opacity": "Overlay opacity", "opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Baskartor", "basemaps": "Baskartor",
"overlays": "Lager", "overlays": "Lager",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -380,9 +378,7 @@
"railway-station": "Järnvägsstation", "railway-station": "Järnvägsstation",
"tram-stop": "Spårvagnshållplats", "tram-stop": "Spårvagnshållplats",
"bus-stop": "Busshållplats", "bus-stop": "Busshållplats",
"ferry": "Ferry", "ferry": "Ferry"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Update layer" "update": "Update layer"
}, },
"opacity": "Overlay opacity", "opacity": "Overlay opacity",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Basemaps", "basemaps": "Basemaps",
"overlays": "Overlays", "overlays": "Overlays",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -380,9 +378,7 @@
"railway-station": "Railway Station", "railway-station": "Railway Station",
"tram-stop": "Tram Stop", "tram-stop": "Tram Stop",
"bus-stop": "Bus Stop", "bus-stop": "Bus Stop",
"ferry": "Ferry", "ferry": "Ferry"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Katman güncelle" "update": "Katman güncelle"
}, },
"opacity": "Katman şeffaflığı", "opacity": "Katman şeffaflığı",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Temel haritalar", "basemaps": "Temel haritalar",
"overlays": "Katmanlar", "overlays": "Katmanlar",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Eğim", "swisstopoSlope": "swisstopo Eğim",
"swisstopoHiking": "swisstopo Yürüyüş", "swisstopoHiking": "swisstopo Yürüyüş",
"swisstopoHikingClosures": "swisstopo Yürüyüş Sonu", "swisstopoHikingClosures": "swisstopo Yürüyüş Sonu",
@@ -380,9 +378,7 @@
"railway-station": "Tren istasyonu", "railway-station": "Tren istasyonu",
"tram-stop": "Tramvay Durağı", "tram-stop": "Tramvay Durağı",
"bus-stop": "Otobüs Durağı", "bus-stop": "Otobüs Durağı",
"ferry": "Feribot", "ferry": "Feribot"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

View File

@@ -282,7 +282,6 @@
"update": "Оновити шар" "update": "Оновити шар"
}, },
"opacity": "Непрозорість накладання", "opacity": "Непрозорість накладання",
"terrain": "Terrain source",
"label": { "label": {
"basemaps": "Базові карти", "basemaps": "Базові карти",
"overlays": "Накладання", "overlays": "Накладання",
@@ -325,9 +324,8 @@
"bgMountains": "BGMountains", "bgMountains": "BGMountains",
"usgs": "USGS", "usgs": "USGS",
"bikerouterGravel": "bikerouter.de Gravel", "bikerouterGravel": "bikerouter.de Gravel",
"cyclOSMlite": "CyclOSM Lite",
"mapterhornHillshade": "Mapterhorn Hillshade",
"openRailwayMap": "OpenRailwayMap", "openRailwayMap": "OpenRailwayMap",
"cyclOSMlite": "CyclOSM Lite",
"swisstopoSlope": "swisstopo Slope", "swisstopoSlope": "swisstopo Slope",
"swisstopoHiking": "swisstopo Hiking", "swisstopoHiking": "swisstopo Hiking",
"swisstopoHikingClosures": "swisstopo Hiking Closures", "swisstopoHikingClosures": "swisstopo Hiking Closures",
@@ -380,9 +378,7 @@
"railway-station": "Залізнична Станція", "railway-station": "Залізнична Станція",
"tram-stop": "Трамвайна Зупинка", "tram-stop": "Трамвайна Зупинка",
"bus-stop": "Автобусна Зупинка", "bus-stop": "Автобусна Зупинка",
"ferry": "Пором", "ferry": "Пором"
"mapbox-dem": "Mapbox DEM",
"mapterhorn": "Mapterhorn"
} }
}, },
"chart": { "chart": {

Some files were not shown because too many files have changed in this diff Show More