15 Commits

Author SHA1 Message Date
vcoppe
c9ca75e2e8 small ui improvements 2026-02-17 22:24:14 +01:00
vcoppe
091f6a3ed0 adapt routing control size to canvas width 2026-02-17 21:12:04 +01:00
vcoppe
d6c9fb1025 split routing controls in zoom-specific layers to improve performance 2026-02-14 15:05:23 +01:00
vcoppe
88abd72a41 layer instead of markers for routing controls 2026-02-14 14:35:35 +01:00
vcoppe
1137e851ce remove company support section until clarified 2026-02-11 18:31:08 +01:00
vcoppe
b8c1500aad fix layer filtering in event manager 2026-02-02 21:50:01 +01:00
vcoppe
bfd0d90abc validate settings 2026-02-01 18:45:40 +01:00
vcoppe
dba01e1826 finish renaming 2026-02-01 18:06:16 +01:00
vcoppe
2189c76edd renaming 2026-02-01 17:46:24 +01:00
vcoppe
6f8c9d66db use map layers for start/end/hover markers 2026-02-01 17:18:17 +01:00
vcoppe
9408ce10c7 check that map contains the layer 2026-02-01 16:26:17 +01:00
vcoppe
9895c3c304 further improve event listener performance 2026-02-01 15:57:18 +01:00
vcoppe
0ab3b77db8 centralized map layer event listener for better performance 2026-01-31 12:57:08 +01:00
vcoppe
d13e7e7a0a update package-lock 2026-01-30 23:13:02 +01:00
vcoppe
e96b544a75 switch to maplibre, but laggy 2026-01-30 21:01:24 +01:00
277 changed files with 3079 additions and 3194 deletions

View File

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

View File

@@ -42,11 +42,11 @@ npm run build
### Running the website
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.
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.
```bash
cd website
echo PUBLIC_MAPBOX_TOKEN={YOUR_MAPBOX_TOKEN} >> .env
echo PUBLIC_MAPTILER_KEY={YOUR_MAPTILER_KEY} >> .env
npm install
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
- [SortableJS](https://github.com/SortableJS/Sortable) — creating a sortable file tree
- Mapping:
- [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js) — beautiful and fast interactive maps
- [MapLibre GL JS](https://github.com/maplibre/maplibre-gl-js) — beautiful and fast interactive maps
- [brouter](https://github.com/abrensch/brouter) — routing engine
- [OpenStreetMap](https://www.openstreetmap.org) — map data used by Mapbox and brouter
- [OpenStreetMap](https://www.openstreetmap.org) — map data used by most of the map layers, and by the routing engine
- Search:
- [DocSearch](https://github.com/algolia/docsearch) — search engine for the documentation

View File

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

1222
website/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

Before

Width:  |  Height:  |  Size: 3.6 MiB

After

Width:  |  Height:  |  Size: 3.6 MiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -22,15 +22,18 @@ import {
Binoculars,
Toilet,
} from 'lucide-static';
import { type RasterDEMSourceSpecification, type StyleSpecification } from 'mapbox-gl';
import { type RasterDEMSourceSpecification, type StyleSpecification } from 'maplibre-gl';
import ignFrTopo from './custom/ign-fr-topo.json';
import ignFrPlan from './custom/ign-fr-plan.json';
import ignFrSatellite from './custom/ign-fr-satellite.json';
import bikerouterGravel from './custom/bikerouter-gravel.json';
export const maptilerKeyPlaceHolder = 'MAPTILER_KEY';
export const basemaps: { [key: string]: string | StyleSpecification } = {
mapboxOutdoors: 'mapbox://styles/mapbox/outdoors-v12',
mapboxSatellite: 'mapbox://styles/mapbox/satellite-streets-v12',
maptilerTopo: `https://api.maptiler.com/maps/topo-v4/style.json?key=${maptilerKeyPlaceHolder}`,
maptilerOutdoors: `https://api.maptiler.com/maps/outdoor-v4/style.json?key=${maptilerKeyPlaceHolder}`,
maptilerSatellite: `https://api.maptiler.com/maps/hybrid-v4/style.json?key=${maptilerKeyPlaceHolder}`,
openStreetMap: {
version: 8,
sources: {
@@ -773,8 +776,9 @@ export type LayerTreeType = { [key: string]: LayerTreeType | boolean };
export const basemapTree: LayerTreeType = {
basemaps: {
world: {
mapboxOutdoors: true,
mapboxSatellite: true,
maptilerTopo: true,
maptilerOutdoors: true,
maptilerSatellite: true,
openStreetMap: true,
openTopoMap: true,
openHikingMap: true,
@@ -907,7 +911,7 @@ export const overpassTree: LayerTreeType = {
};
// Default basemap used
export const defaultBasemap = 'mapboxOutdoors';
export const defaultBasemap = 'maptilerTopo';
// Default overlays used (none)
export const defaultOverlays: LayerTreeType = {
@@ -996,8 +1000,9 @@ export const defaultOverpassQueries: LayerTreeType = {
export const defaultBasemapTree: LayerTreeType = {
basemaps: {
world: {
mapboxOutdoors: true,
mapboxSatellite: true,
maptilerTopo: true,
maptilerOutdoors: true,
maptilerSatellite: true,
openStreetMap: true,
openTopoMap: true,
openHikingMap: true,
@@ -1136,7 +1141,7 @@ export type CustomLayer = {
maxZoom: number;
layerType: 'basemap' | 'overlay';
resourceType: 'raster' | 'vector';
value: string | {};
value: string | maplibregl.StyleSpecification;
};
type OverpassQueryData = {
@@ -1455,11 +1460,9 @@ export const overpassQueryData: Record<string, OverpassQueryData> = {
};
export const terrainSources: { [key: string]: RasterDEMSourceSpecification } = {
'mapbox-dem': {
'maptiler-dem': {
type: 'raster-dem',
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
tileSize: 512,
maxzoom: 14,
url: `https://api.maptiler.com/tiles/terrain-rgb-v2/tiles.json?key=${maptilerKeyPlaceHolder}`,
},
mapterhorn: {
type: 'raster-dem',
@@ -1467,4 +1470,4 @@ export const terrainSources: { [key: string]: RasterDEMSourceSpecification } = {
},
};
export const defaultTerrainSource = 'mapbox-dem';
export const defaultTerrainSource = 'maptiler-dem';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,16 @@
map.onLoad((map_) => {
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({
item: new TrackPoint({
attributes: {

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,11 @@
import { settings } from '$lib/logic/settings';
import { gpxStatistics } from '$lib/logic/statistics';
import { getConvertedDistanceToKilometers } from '$lib/units';
import type { GeoJSONSource } from 'mapbox-gl';
import { get } from 'svelte/store';
import { ANCHOR_LAYER_KEY, map } from '$lib/components/map/map';
import { map } from '$lib/components/map/map';
import { allHidden } from '$lib/logic/hidden';
import type { GeoJSONSource } from 'maplibre-gl';
import { ANCHOR_LAYER_KEY } from '../style';
const { distanceMarkers, distanceUnits } = settings;
@@ -22,7 +23,7 @@ export class DistanceMarkers {
this.unsubscribes.push(
map.subscribe((map_) => {
if (map_) {
map_.on('style.import.load', this.updateBinded);
map_.on('style.load', this.updateBinded);
}
})
);

View File

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

View File

@@ -1,6 +1,11 @@
import { get, type Readable } from 'svelte/store';
import mapboxgl, { type FilterSpecification } from 'mapbox-gl';
import { ANCHOR_LAYER_KEY, map } from '$lib/components/map/map';
import maplibregl, {
type GeoJSONSource,
type FilterSpecification,
type MapLayerMouseEvent,
type MapLayerTouchEvent,
} from 'maplibre-gl';
import { map } from '$lib/components/map/map';
import { waypointPopup, trackpointPopup } from './gpx-layer-popup';
import {
ListTrackSegmentItem,
@@ -10,7 +15,7 @@ import {
ListFileItem,
ListRootItem,
} from '$lib/components/file-list/file-list';
import { getClosestLinePoint, getElevation } from '$lib/utils';
import { getClosestLinePoint, getElevation, loadSVGIcon } from '$lib/utils';
import { selectedWaypoint } from '$lib/components/toolbar/tools/waypoint/waypoint';
import { MapPin, Square } from 'lucide-static';
import { getSymbolKey, symbols } from '$lib/assets/symbols';
@@ -22,7 +27,8 @@ import { fileActionManager } from '$lib/logic/file-action-manager';
import { fileActions } from '$lib/logic/file-actions';
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import { gpxColors } from '$lib/components/map/gpx-layer/gpx-layers';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
import { gpxColors } from './gpx-layers';
const colors = [
'#ff0000',
@@ -114,28 +120,28 @@ export class GPXLayer {
selected: boolean = false;
currentWaypointData: GeoJSON.FeatureCollection | null = null;
draggedWaypointIndex: number | null = null;
draggingStartingPosition: mapboxgl.Point = new mapboxgl.Point(0, 0);
draggingStartingPosition: maplibregl.Point = new maplibregl.Point(0, 0);
unsubscribe: Function[] = [];
updateBinded: () => void = this.update.bind(this);
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
layerOnMouseMoveBinded: (e: any) => void = this.layerOnMouseMove.bind(this);
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this);
waypointLayerOnMouseEnterBinded: (e: mapboxgl.MapMouseEvent) => void =
layerOnClickBinded: (e: MapLayerMouseEvent) => void = this.layerOnClick.bind(this);
layerOnContextMenuBinded: (e: MapLayerMouseEvent) => void = this.layerOnContextMenu.bind(this);
waypointLayerOnMouseEnterBinded: (e: MapLayerMouseEvent) => void =
this.waypointLayerOnMouseEnter.bind(this);
waypointLayerOnMouseLeaveBinded: (e: mapboxgl.MapMouseEvent) => void =
waypointLayerOnMouseLeaveBinded: (e: MapLayerMouseEvent) => void =
this.waypointLayerOnMouseLeave.bind(this);
waypointLayerOnClickBinded: (e: mapboxgl.MapMouseEvent) => void =
waypointLayerOnClickBinded: (e: MapLayerMouseEvent) => void =
this.waypointLayerOnClick.bind(this);
waypointLayerOnMouseDownBinded: (e: mapboxgl.MapMouseEvent) => void =
waypointLayerOnMouseDownBinded: (e: MapLayerMouseEvent) => void =
this.waypointLayerOnMouseDown.bind(this);
waypointLayerOnTouchStartBinded: (e: mapboxgl.MapTouchEvent) => void =
waypointLayerOnTouchStartBinded: (e: MapLayerTouchEvent) => void =
this.waypointLayerOnTouchStart.bind(this);
waypointLayerOnMouseMoveBinded: (e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) => void =
waypointLayerOnMouseMoveBinded: (e: MapLayerMouseEvent | MapLayerTouchEvent) => void =
this.waypointLayerOnMouseMove.bind(this);
waypointLayerOnMouseUpBinded: (e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) => void =
waypointLayerOnMouseUpBinded: (e: MapLayerMouseEvent | MapLayerTouchEvent) => void =
this.waypointLayerOnMouseUp.bind(this);
constructor(fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
@@ -145,7 +151,7 @@ export class GPXLayer {
this.unsubscribe.push(
map.subscribe(($map) => {
if ($map) {
$map.on('style.import.load', this.updateBinded);
$map.on('style.load', this.updateBinded);
this.update();
}
})
@@ -168,8 +174,9 @@ export class GPXLayer {
update() {
const _map = get(map);
const layerEventManager = map.layerEventManager;
let file = get(this.file)?.file;
if (!_map || !file) {
if (!_map || !layerEventManager || !file) {
return;
}
@@ -185,7 +192,7 @@ export class GPXLayer {
this.loadIcons();
try {
let source = _map.getSource(this.fileId) as mapboxgl.GeoJSONSource | undefined;
let source = _map.getSource(this.fileId) as GeoJSONSource | undefined;
if (source) {
source.setData(this.getGeoJSON());
} else {
@@ -214,65 +221,26 @@ export class GPXLayer {
ANCHOR_LAYER_KEY.tracks
);
_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);
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 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,
let visibleTrackSegmentIds: string[] = [];
file.forEachSegment((segment, trackIndex, segmentIndex) => {
if (!segment._data.hidden) {
visibleTrackSegmentIds.push(`${trackIndex}-${segmentIndex}`);
}
});
}
const segmentFilter: FilterSpecification = [
'in',
['get', 'trackSegmentId'],
['literal', visibleTrackSegmentIds],
];
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,
},
},
ANCHOR_LAYER_KEY.waypoints
);
_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
);
}
_map.setFilter(this.fileId, segmentFilter, { validate: false });
if (get(directionMarkers)) {
if (!_map.getLayer(this.fileId + '-direction')) {
@@ -301,28 +269,70 @@ export class GPXLayer {
ANCHOR_LAYER_KEY.directionMarkers
);
}
_map.setFilter(this.fileId + '-direction', segmentFilter, { validate: false });
} else {
if (_map.getLayer(this.fileId + '-direction')) {
_map.removeLayer(this.fileId + '-direction');
}
}
let visibleTrackSegmentIds: string[] = [];
file.forEachSegment((segment, trackIndex, segmentIndex) => {
if (!segment._data.hidden) {
visibleTrackSegmentIds.push(`${trackIndex}-${segmentIndex}`);
}
let waypointSource = _map.getSource(this.fileId + '-waypoints') as
| GeoJSONSource
| undefined;
this.currentWaypointData = this.getWaypointsGeoJSON();
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],
];
}
_map.setFilter(this.fileId, segmentFilter, { validate: false });
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,
},
},
ANCHOR_LAYER_KEY.waypoints
);
if (_map.getLayer(this.fileId + '-direction')) {
_map.setFilter(this.fileId + '-direction', segmentFilter, { validate: false });
layerEventManager.on(
'mouseenter',
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[] = [];
@@ -345,32 +355,47 @@ export class GPXLayer {
remove() {
const _map = get(map);
if (_map) {
_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);
_map.off(
if (_map) {
_map.off('style.load', this.updateBinded);
}
const layerEventManager = map.layerEventManager;
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',
this.fileId + '-waypoints',
this.waypointLayerOnMouseEnterBinded
);
_map.off(
layerEventManager.off(
'mouseleave',
this.fileId + '-waypoints',
this.waypointLayerOnMouseLeaveBinded
);
_map.off('click', this.fileId + '-waypoints', this.waypointLayerOnClickBinded);
_map.off('mousedown', this.fileId + '-waypoints', this.waypointLayerOnMouseDownBinded);
_map.off(
layerEventManager.off(
'click',
this.fileId + '-waypoints',
this.waypointLayerOnClickBinded
);
layerEventManager.off(
'mousedown',
this.fileId + '-waypoints',
this.waypointLayerOnMouseDownBinded
);
layerEventManager.off(
'touchstart',
this.fileId + '-waypoints',
this.waypointLayerOnTouchStartBinded
);
}
if (_map) {
if (_map.getLayer(this.fileId + '-direction')) {
_map.removeLayer(this.fileId + '-direction');
}
@@ -446,7 +471,7 @@ export class GPXLayer {
}
}
layerOnClick(e: mapboxgl.MapMouseEvent) {
layerOnClick(e: MapLayerMouseEvent) {
if (
get(currentTool) === Tool.ROUTING &&
get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])
@@ -504,7 +529,7 @@ export class GPXLayer {
}
}
waypointLayerOnMouseEnter(e: mapboxgl.MapMouseEvent) {
waypointLayerOnMouseEnter(e: MapLayerMouseEvent) {
if (this.draggedWaypointIndex !== null) {
return;
}
@@ -524,7 +549,7 @@ export class GPXLayer {
mapCursor.notify(MapCursorState.WAYPOINT_HOVER, false);
}
waypointLayerOnClick(e: mapboxgl.MapMouseEvent) {
waypointLayerOnClick(e: MapLayerMouseEvent) {
e.preventDefault();
let waypointIndex = e.features![0].properties!.waypointIndex;
@@ -566,7 +591,7 @@ export class GPXLayer {
}
}
waypointLayerOnMouseDown(e: mapboxgl.MapMouseEvent) {
waypointLayerOnMouseDown(e: MapLayerMouseEvent) {
if (get(currentTool) !== Tool.WAYPOINT || !this.selected) {
return;
}
@@ -576,6 +601,7 @@ export class GPXLayer {
}
e.preventDefault();
_map.dragPan.disable();
this.draggedWaypointIndex = e.features![0].properties!.waypointIndex;
this.draggingStartingPosition = e.point;
@@ -585,7 +611,7 @@ export class GPXLayer {
_map.once('mouseup', this.waypointLayerOnMouseUpBinded);
}
waypointLayerOnTouchStart(e: mapboxgl.MapTouchEvent) {
waypointLayerOnTouchStart(e: MapLayerTouchEvent) {
if (e.points.length !== 1 || get(currentTool) !== Tool.WAYPOINT || !this.selected) {
return;
}
@@ -599,12 +625,13 @@ export class GPXLayer {
waypointPopup?.hide();
e.preventDefault();
_map.dragPan.disable();
_map.on('touchmove', this.waypointLayerOnMouseMoveBinded);
_map.once('touchend', this.waypointLayerOnMouseUpBinded);
}
waypointLayerOnMouseMove(e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) {
waypointLayerOnMouseMove(e: MapLayerMouseEvent | MapLayerTouchEvent) {
if (this.draggedWaypointIndex === null || e.point.equals(this.draggingStartingPosition)) {
return;
}
@@ -616,18 +643,35 @@ export class GPXLayer {
).coordinates = [e.lngLat.lng, e.lngLat.lat];
let waypointSource = get(map)?.getSource(this.fileId + '-waypoints') as
| mapboxgl.GeoJSONSource
| GeoJSONSource
| undefined;
if (waypointSource) {
waypointSource.setData(this.currentWaypointData!);
waypointSource.updateData({
update: [
{
id: this.draggedWaypointIndex,
newGeometry: {
type: 'Point',
coordinates: [e.lngLat.lng, e.lngLat.lat],
},
},
],
});
}
}
waypointLayerOnMouseUp(e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) {
waypointLayerOnMouseUp(e: MapLayerMouseEvent | MapLayerTouchEvent) {
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
get(map)?.off('mousemove', this.waypointLayerOnMouseMoveBinded);
get(map)?.off('touchmove', this.waypointLayerOnMouseMoveBinded);
const _map = get(map);
if (!_map) {
return;
}
_map.dragPan.enable();
_map.off('mousemove', this.waypointLayerOnMouseMoveBinded);
_map.off('touchmove', this.waypointLayerOnMouseMoveBinded);
if (this.draggedWaypointIndex === null) {
return;
@@ -750,20 +794,7 @@ export class GPXLayer {
symbols.forEach((symbol) => {
const iconId = `waypoint-${symbol ?? 'default'}-${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));
}
loadSVGIcon(_map, iconId, getSvgForSymbol(symbol, this.layerColor));
});
}
}

View File

@@ -1,30 +1,40 @@
import { currentTool, Tool } from '$lib/components/toolbar/tools';
import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
import mapboxgl from 'mapbox-gl';
import { gpxStatistics, hoveredPoint, slicedGPXStatistics } from '$lib/logic/statistics';
import type { GeoJSONSource } from 'maplibre-gl';
import { get } from 'svelte/store';
import { map } from '$lib/components/map/map';
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 {
start: mapboxgl.Marker;
end: mapboxgl.Marker;
updateBinded: () => void = this.update.bind(this);
unsubscribes: (() => void)[] = [];
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());
this.unsubscribes.push(gpxStatistics.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(allHidden.subscribe(this.updateBinded));
}
@@ -33,33 +43,115 @@ export class StartEndMarkers {
const map_ = get(map);
if (!map_) return;
this.loadIcons();
const tool = get(currentTool);
const statistics = get(gpxStatistics);
const slicedStatistics = get(slicedGPXStatistics);
const hovered = get(hoveredPoint);
const hidden = get(allHidden);
if (statistics.global.length > 0 && tool !== Tool.ROUTING && !hidden) {
this.start
.setLngLat(
statistics.getTrackPoint(slicedStatistics?.[1] ?? 0)!.trkpt.getCoordinates()
)
.addTo(map_);
this.end
.setLngLat(
statistics
if (!hidden) {
const data: GeoJSON.FeatureCollection = {
type: 'FeatureCollection',
features: [],
};
if (statistics.global.length > 0 && tool !== Tool.ROUTING) {
const start = statistics
.getTrackPoint(slicedStatistics?.[1] ?? 0)!
.trkpt.getCoordinates();
const end = statistics
.getTrackPoint(slicedStatistics?.[2] ?? statistics.global.length - 1)!
.trkpt.getCoordinates()
)
.addTo(map_);
.trkpt.getCoordinates();
data.features.push({
type: 'Feature',
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 {
this.start.remove();
this.end.remove();
map_.addSource('start-end-markers', {
type: 'geojson',
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() {
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
this.start.remove();
this.end.remove();
const map_ = get(map);
if (!map_) return;
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,9 +20,8 @@
import { i18n } from '$lib/i18n.svelte';
import { defaultBasemap, type CustomLayer } from '$lib/assets/layers';
import { onMount } from 'svelte';
import { customBasemapUpdate, isSelected, remove } from './utils';
import { remove } from './utils';
import { settings } from '$lib/logic/settings';
import { map } from '$lib/components/map/map';
import { dndzone } from 'svelte-dnd-action';
const {
@@ -42,14 +41,9 @@
let maxZoom: number = $state(20);
let layerType: 'basemap' | 'overlay' = $state('basemap');
let resourceType: 'raster' | 'vector' = $derived.by(() => {
if (tileUrls[0].length > 0) {
if (
tileUrls[0].includes('.json') ||
(tileUrls[0].includes('api.mapbox.com/styles') && !tileUrls[0].includes('tiles'))
) {
if (tileUrls[0].length > 0 && tileUrls[0].includes('.json')) {
return 'vector';
}
}
return 'raster';
});
@@ -134,8 +128,8 @@
],
};
}
$customLayers[layerId] = layer;
addLayer(layerId);
$customLayers[layerId] = layer;
selectedLayerId = undefined;
setDataFromSelectedLayer();
}
@@ -158,9 +152,7 @@
return $tree;
});
if ($currentBasemap === layerId) {
$customBasemapUpdate++;
} else {
if ($currentBasemap !== layerId) {
$currentBasemap = layerId;
}
@@ -176,14 +168,6 @@
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) => {
if (!$overlays.overlays.hasOwnProperty('custom')) {
$overlays.overlays['custom'] = {};

View File

@@ -5,12 +5,8 @@
import { Separator } from '$lib/components/ui/separator';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import { Layers } from '@lucide/svelte';
import { basemaps, defaultBasemap, overlays } from '$lib/assets/layers';
import { settings } from '$lib/logic/settings';
import { map } from '$lib/components/map/map';
import { customBasemapUpdate, getLayers } from './utils';
import type { ImportSpecification, StyleSpecification } from 'mapbox-gl';
import { untrack } from 'svelte';
let container: HTMLDivElement;
let overpassLayer: OverpassLayer;
@@ -23,125 +19,14 @@
selectedBasemapTree,
selectedOverlayTree,
selectedOverpassTree,
customLayers,
opacities,
} = settings;
function setStyle() {
if (!$map) {
return;
}
let basemap = basemaps.hasOwnProperty($currentBasemap)
? basemaps[$currentBasemap]
: ($customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap]);
$map.removeImport('basemap');
if (typeof basemap === 'string') {
$map.addImport({ id: 'basemap', url: basemap }, 'overlays');
} else {
$map.addImport(
{
id: 'basemap',
url: '',
data: basemap as StyleSpecification,
},
'overlays'
);
}
}
$effect(() => {
if ($map && ($currentBasemap || $customBasemapUpdate)) {
untrack(() => setStyle());
}
});
function addOverlay(id: string) {
if (!$map) {
return;
}
try {
let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id];
if (typeof overlay === 'string') {
$map.addImport({ id, url: overlay });
} else {
if ($opacities.hasOwnProperty(id)) {
overlay = {
...overlay,
layers: (overlay as StyleSpecification).layers.map((layer) => {
if (layer.type === 'raster') {
if (!layer.paint) {
layer.paint = {};
}
layer.paint['raster-opacity'] = $opacities[id];
}
return layer;
}),
};
}
$map.addImport({
id,
url: '',
data: overlay as StyleSpecification,
});
}
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
}
}
function updateOverlays() {
if ($map && $currentOverlays && $opacities) {
let overlayLayers = getLayers($currentOverlays);
try {
let activeOverlays =
$map
.getStyle()
.imports?.reduce(
(
acc: Record<string, ImportSpecification>,
imprt: ImportSpecification
) => {
if (!['basemap', 'overlays'].includes(imprt.id)) {
acc[imprt.id] = imprt;
}
return acc;
},
{}
) || {};
let toRemove = Object.keys(activeOverlays).filter((id) => !overlayLayers[id]);
toRemove.forEach((id) => {
$map?.removeImport(id);
});
let toAdd = Object.entries(overlayLayers)
.filter(([id, selected]) => selected && !activeOverlays.hasOwnProperty(id))
.map(([id]) => id);
toAdd.forEach((id) => {
addOverlay(id);
});
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
}
}
}
$effect(() => {
if ($map && $currentOverlays && $opacities) {
untrack(() => updateOverlays());
}
});
map.onLoad((_map: mapboxgl.Map) => {
map.onLoad((_map: maplibregl.Map) => {
if (overpassLayer) {
overpassLayer.remove();
}
overpassLayer = new OverpassLayer(_map);
overpassLayer = new OverpassLayer(_map, map.layerEventManager!);
overpassLayer.add();
let first = true;
_map.on('style.import.load', () => {
if (!first) return;
first = false;
updateOverlays();
});
});
let open = $state(false);

View File

@@ -167,11 +167,11 @@
{#if isSelected($selectedOverlayTree, selectedOverlay)}
{#if $isLayerFromExtension(selectedOverlay)}
{$getLayerName(selectedOverlay)}
{:else if $customLayers.hasOwnProperty(selectedOverlay)}
{$customLayers[selectedOverlay].name}
{:else}
{i18n._(`layers.label.${selectedOverlay}`)}
{/if}
{:else if $customLayers.hasOwnProperty(selectedOverlay)}
{$customLayers[selectedOverlay].name}
{/if}
{/if}
</Select.Trigger>
@@ -213,7 +213,9 @@
isSelected($currentOverlays, selectedOverlay)
) {
try {
$map.removeImport(selectedOverlay);
if ($map.getLayer(selectedOverlay)) {
$map.removeLayer(selectedOverlay);
}
} catch (e) {
// No reliable way to check if the map is ready to remove sources and layers
}

View File

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

View File

@@ -6,7 +6,10 @@ import { overpassQueryData } from '$lib/assets/layers';
import { MapPopup } from '$lib/components/map/map-popup';
import { settings } from '$lib/logic/settings';
import { db } from '$lib/db';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/map';
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;
@@ -21,11 +24,12 @@ liveQuery(() => db.overpassdata.toArray()).subscribe((pois) => {
});
export class OverpassLayer {
overpassUrl = 'https://overpass.private.coffee/api/interpreter';
overpassUrl = 'https://maps.mail.ru/osm/tools/overpass/api/interpreter';
minZoom = 12;
queryZoom = 12;
expirationTime = 7 * 24 * 3600 * 1000;
map: mapboxgl.Map;
map: maplibregl.Map;
layerEventManager: MapLayerEventManager;
popup: MapPopup;
currentQueries: Set<string> = new Set();
@@ -36,8 +40,9 @@ export class OverpassLayer {
updateBinded = this.update.bind(this);
onHoverBinded = this.onHover.bind(this);
constructor(map: mapboxgl.Map) {
constructor(map: maplibregl.Map, layerEventManager: MapLayerEventManager) {
this.map = map;
this.layerEventManager = layerEventManager;
this.popup = new MapPopup(map, {
closeButton: false,
focusAfterOpen: false,
@@ -48,7 +53,7 @@ export class OverpassLayer {
add() {
this.map.on('moveend', this.queryIfNeededBinded);
this.map.on('style.import.load', this.updateBinded);
this.map.on('style.load', this.updateBinded);
this.unsubscribes.push(data.subscribe(this.updateBinded));
this.unsubscribes.push(
currentOverpassQueries.subscribe(() => {
@@ -72,10 +77,17 @@ export class OverpassLayer {
update() {
this.loadIcons();
let d = get(data);
const fullData = get(data);
const queries = getCurrentQueries();
const d: GeoJSON.FeatureCollection = {
type: 'FeatureCollection',
features: fullData.features.filter((feature) =>
queries.includes(feature.properties!.query)
),
};
try {
let source = this.map.getSource('overpass') as mapboxgl.GeoJSONSource | undefined;
let source = this.map.getSource('overpass') as GeoJSONSource | undefined;
if (source) {
source.setData(d);
} else {
@@ -101,13 +113,9 @@ export class OverpassLayer {
ANCHOR_LAYER_KEY.overpass
);
this.map.on('mouseenter', 'overpass', this.onHoverBinded);
this.map.on('click', 'overpass', this.onHoverBinded);
this.layerEventManager.on('mouseenter', 'overpass', this.onHoverBinded);
this.layerEventManager.on('click', 'overpass', this.onHoverBinded);
}
this.map.setFilter('overpass', ['in', 'query', ...getCurrentQueries()], {
validate: false,
});
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
}
@@ -115,7 +123,9 @@ export class OverpassLayer {
remove() {
this.map.off('moveend', this.queryIfNeededBinded);
this.map.off('style.import.load', this.updateBinded);
this.map.off('style.load', this.updateBinded);
this.layerEventManager.off('mouseenter', 'overpass', this.onHoverBinded);
this.layerEventManager.off('click', 'overpass', this.onHoverBinded);
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
try {
@@ -248,27 +258,16 @@ export class OverpassLayer {
loadIcons() {
let currentQueries = getCurrentQueries();
currentQueries.forEach((query) => {
if (!this.map.hasImage(`overpass-${query}`)) {
let icon = new Image(100, 100);
icon.onload = () => {
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">
loadSVGIcon(
this.map,
`overpass-${query}`,
`<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}" />
<g transform="translate(8 8)">
${overpassQueryData[query].icon.svg.replace('stroke="currentColor"', 'stroke="white"')}
</g>
</svg>
`);
}
</svg>`
);
});
}
}

View File

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

View File

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

View File

@@ -1,110 +1,80 @@
import mapboxgl from 'mapbox-gl';
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import MaplibreGeocoder, {
type MaplibreGeocoderFeatureResults,
} from '@maplibre/maplibre-gl-geocoder';
import '@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css';
import { get, writable, type Writable } from 'svelte/store';
import { settings } from '$lib/logic/settings';
import { tick } from 'svelte';
import { terrainSources } from '$lib/assets/layers';
import { ANCHOR_LAYER_KEY, StyleManager } from '$lib/components/map/style';
import { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
const {
treeFileView,
elevationProfile,
bottomPanelSize,
rightPanelSize,
distanceUnits,
terrainSource,
} = settings;
const { treeFileView, elevationProfile, bottomPanelSize, rightPanelSize, distanceUnits } = settings;
let fitBoundsOptions: mapboxgl.MapOptions['fitBoundsOptions'] = {
let fitBoundsOptions: maplibregl.MapOptions['fitBoundsOptions'] = {
maxZoom: 15,
linear: true,
easing: () => 1,
};
const emptySource: mapboxgl.GeoJSONSourceSpecification = {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [],
},
};
export const ANCHOR_LAYER_KEY = {
mapillary: 'mapillary-end',
tracks: 'tracks-end',
directionMarkers: 'direction-markers-end',
distanceMarkers: 'distance-markers-end',
interactions: 'interactions-end',
overpass: 'overpass-end',
waypoints: 'waypoints-end',
};
const anchorLayers: mapboxgl.LayerSpecification[] = Object.values(ANCHOR_LAYER_KEY).map((id) => ({
id: id,
type: 'symbol',
source: 'empty-source',
}));
export class MapboxGLMap {
private _map: Writable<mapboxgl.Map | null> = writable(null);
private _onLoadCallbacks: ((map: mapboxgl.Map) => void)[] = [];
export class MapLibreGLMap {
private _maptilerKey: string = '';
private _map: maplibregl.Map | null = null;
private _mapStore: Writable<maplibregl.Map | null> = writable(null);
private _styleManager: StyleManager | null = null;
private _onLoadCallbacks: ((map: maplibregl.Map) => void)[] = [];
private _unsubscribes: (() => void)[] = [];
private callOnLoadBinded: () => void = this.callOnLoad.bind(this);
public layerEventManager: MapLayerEventManager | null = null;
subscribe(run: (value: mapboxgl.Map | null) => void, invalidate?: () => void) {
return this._map.subscribe(run, invalidate);
subscribe(run: (value: maplibregl.Map | null) => void, invalidate?: () => void) {
return this._mapStore.subscribe(run, invalidate);
}
init(language: string, hash: boolean, geocoder: boolean, geolocate: boolean) {
const map = new mapboxgl.Map({
init(
maptilerKey: string,
language: string,
hash: boolean,
geocoder: boolean,
geolocate: boolean
) {
this._maptilerKey = maptilerKey;
this._styleManager = new StyleManager(this._mapStore, this._maptilerKey);
const map = new maplibregl.Map({
container: 'map',
style: {
version: 8,
sources: {
'empty-source': emptySource,
projection: {
type: 'globe',
},
layers: anchorLayers,
imports: [
{
id: 'basemap',
url: '',
sources: {},
layers: [],
},
{
id: 'overlays',
url: '',
},
],
},
projection: 'globe',
zoom: 0,
hash: hash,
language,
attributionControl: false,
logoPosition: 'bottom-right',
boxZoom: false,
maxPitch: 85,
});
this.layerEventManager = new MapLayerEventManager(map);
map.addControl(
new mapboxgl.AttributionControl({
compact: true,
})
);
map.addControl(
new mapboxgl.NavigationControl({
new maplibregl.NavigationControl({
visualizePitch: true,
})
);
if (geocoder) {
let geocoder = new MapboxGeocoder({
mapboxgl: mapboxgl,
enableEventLogging: false,
collapsed: true,
flyTo: fitBoundsOptions,
language,
localGeocoder: () => [],
localGeocoderOnly: true,
externalGeocoder: (query: string) =>
fetch(
`https://nominatim.openstreetmap.org/search?format=json&q=${query}&limit=5&accept-language=${language}`
)
.then((response) => response.json())
.then((data) => {
return data.map((result: any) => {
let geocoder = new MaplibreGeocoder(
{
forwardGeocode: async (config) => {
const results: MaplibreGeocoderFeatureResults = {
features: [],
type: 'FeatureCollection',
};
try {
const request = `https://nominatim.openstreetmap.org/search?format=json&q=${config.query}&limit=5&accept-language=${language}`;
const response = await fetch(request);
const geojson = await response.json();
results.features = geojson.map((result: any) => {
return {
type: 'Feature',
geometry: {
@@ -114,61 +84,43 @@ export class MapboxGLMap {
place_name: result.display_name,
};
});
}),
});
let onKeyDown = geocoder._onKeyDown;
geocoder._onKeyDown = (e: KeyboardEvent) => {
// Trigger search on Enter key only
if (e.key === 'Enter') {
onKeyDown.apply(geocoder, [{ target: geocoder._inputEl }]);
} else if (geocoder._typeahead.data.length > 0) {
geocoder._typeahead.clear();
} catch (e) {}
return results;
},
},
{
maplibregl: maplibregl,
enableEventLogging: false,
collapsed: true,
flyTo: fitBoundsOptions,
language,
}
};
);
map.addControl(geocoder);
}
if (geolocate) {
map.addControl(
new mapboxgl.GeolocateControl({
new maplibregl.GeolocateControl({
positionOptions: {
enableHighAccuracy: true,
},
fitBoundsOptions,
trackUserLocation: true,
showUserHeading: true,
})
);
}
const scaleControl = new mapboxgl.ScaleControl({
const scaleControl = new maplibregl.ScaleControl({
unit: get(distanceUnits),
});
map.addControl(scaleControl);
map.on('style.load', () => {
map.setFog({
color: 'rgb(186, 210, 235)',
'high-color': 'rgb(36, 92, 223)',
'horizon-blend': 0.1,
'space-color': 'rgb(156, 240, 255)',
});
map.on('pitch', this.setTerrain.bind(this));
this.setTerrain();
});
map.on('style.import.load', () => {
const basemap = map.getStyle().imports?.find((imprt) => imprt.id === 'basemap');
if (basemap && basemap.data && basemap.data.glyphs) {
map.setGlyphsUrl(basemap.data.glyphs);
}
});
map.on('load', () => {
this._map.set(map); // only set the store after the map has loaded
this._map = map;
this._mapStore.set(map); // only set the store after the map has loaded
window._map = map; // entry point for extensions
this.resize();
this.setTerrain();
scaleControl.setUnit(get(distanceUnits));
this._onLoadCallbacks.forEach((callback) => callback(map));
this._onLoadCallbacks = [];
});
map.on('style.load', this.callOnLoadBinded);
this._unsubscribes.push(treeFileView.subscribe(() => this.resize()));
this._unsubscribes.push(elevationProfile.subscribe(() => this.resize()));
@@ -179,70 +131,50 @@ export class MapboxGLMap {
scaleControl.setUnit(units);
})
);
this._unsubscribes.push(terrainSource.subscribe(() => this.setTerrain()));
}
onLoad(callback: (map: mapboxgl.Map) => void) {
const map = get(this._map);
if (map) {
callback(map);
} else {
this._onLoadCallbacks.push(callback);
}
}
destroy() {
const map = get(this._map);
if (map) {
map.remove();
this._map.set(null);
if (this._map) {
this._map.remove();
this._mapStore.set(null);
}
this._unsubscribes.forEach((unsubscribe) => unsubscribe());
this._unsubscribes = [];
}
resize() {
const map = get(this._map);
if (map) {
if (this._map) {
tick().then(() => {
map.resize();
this._map?.resize();
});
}
}
toggle3D() {
const map = get(this._map);
if (map) {
if (map.getPitch() === 0) {
map.easeTo({ pitch: 70 });
if (this._map) {
if (this._map.getPitch() === 0) {
this._map.easeTo({ pitch: 70 });
} else {
map.easeTo({ pitch: 0 });
this._map.easeTo({ pitch: 0 });
}
}
}
setTerrain() {
const map = get(this._map);
if (map) {
const source = get(terrainSource);
try {
if (!map.getSource(source)) {
map.addSource(source, terrainSources[source]);
}
if (map.getPitch() > 0) {
map.setTerrain({
source: source,
exaggeration: 1,
});
onLoad(callback: (map: maplibregl.Map) => void) {
if (this._map) {
callback(this._map);
} else {
map.setTerrain(null);
this._onLoadCallbacks.push(callback);
}
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
return;
}
callOnLoad() {
if (this._map && this._map.getLayer(ANCHOR_LAYER_KEY.overlays)) {
this._onLoadCallbacks.forEach((callback) => callback(this._map!));
this._onLoadCallbacks = [];
this._map.off('style.load', this.callOnLoadBinded);
}
}
}
export const map = new MapboxGLMap();
export const map = new MapLibreGLMap();

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,11 +15,12 @@
import { onDestroy, onMount } from 'svelte';
import { getURLForLanguage } from '$lib/utils';
import { Trash2 } from '@lucide/svelte';
import { ANCHOR_LAYER_KEY, map } from '$lib/components/map/map';
import type { GeoJSONSource } from 'mapbox-gl';
import { map } from '$lib/components/map/map';
import type { GeoJSONSource } from 'maplibre-gl';
import { selection } from '$lib/logic/selection';
import { fileActions } from '$lib/logic/file-actions';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
let props: {
class?: string;
@@ -28,7 +29,7 @@
let cleanType = $state(CleanType.INSIDE);
let deleteTrackpoints = $state(true);
let deleteWaypoints = $state(true);
let rectangleCoordinates: mapboxgl.LngLat[] = $state([]);
let rectangleCoordinates: maplibregl.LngLat[] = $state([]);
$effect(() => {
if ($map) {

View File

@@ -1,10 +1,11 @@
import { ListItem, ListTrackSegmentItem } from '$lib/components/file-list/file-list';
import { ANCHOR_LAYER_KEY, 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 { GPXFileStateCollectionObserver, type GPXFileState } from '$lib/logic/file-state';
import { selection } from '$lib/logic/selection';
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
import type { GeoJSONSource } from 'mapbox-gl';
import type { GeoJSONSource } from 'maplibre-gl';
import { get, writable } from 'svelte/store';
export const minTolerance = 0.1;

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,40 +8,33 @@ import { get } from 'svelte/store';
import { fileStateCollection } from '$lib/logic/file-state';
import { fileActions } from '$lib/logic/file-actions';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import { ANCHOR_LAYER_KEY } from '$lib/components/map/map';
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 {
map: mapboxgl.Map;
map: maplibregl.Map;
layerEventManager: MapLayerEventManager;
unsubscribes: Function[] = [];
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
constructor(map: mapboxgl.Map) {
constructor(map: maplibregl.Map, layerEventManager: MapLayerEventManager) {
this.map = map;
if (!this.map.hasImage('split-control')) {
let icon = new Image(100, 100);
icon.onload = () => {
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">
this.layerEventManager = layerEventManager;
loadSVGIcon(
this.map,
'split-control',
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
<circle cx="20" cy="20" r="20" fill="white" />
<g transform="translate(8 8)">
${Scissors.replace('stroke="currentColor"', 'stroke="black"')}
</g>
</svg>
`);
}
</svg>`
);
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
@@ -98,7 +91,7 @@ export class SplitControls {
}, false);
try {
let source = this.map.getSource('split-controls') as mapboxgl.GeoJSONSource | undefined;
let source = this.map.getSource('split-controls') as GeoJSONSource | undefined;
if (source) {
source.setData(data);
} else {
@@ -124,9 +117,17 @@ export class SplitControls {
ANCHOR_LAYER_KEY.interactions
);
this.map.on('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
this.map.on('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
this.map.on('click', 'split-controls', this.layerOnClickBinded);
this.layerEventManager.on(
'mouseenter',
'split-controls',
this.layerOnMouseEnterBinded
);
this.layerEventManager.on(
'mouseleave',
'split-controls',
this.layerOnMouseLeaveBinded
);
this.layerEventManager.on('click', 'split-controls', this.layerOnClickBinded);
}
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
@@ -134,9 +135,9 @@ export class SplitControls {
}
remove() {
this.map.off('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
this.map.off('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
this.map.off('click', 'split-controls', this.layerOnClickBinded);
this.layerEventManager.off('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
this.layerEventManager.off('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
this.layerEventManager.off('click', 'split-controls', this.layerOnClickBinded);
try {
if (this.map.getLayer('split-controls')) {
@@ -159,7 +160,7 @@ export class SplitControls {
mapCursor.notify(MapCursorState.SPLIT_CONTROL, false);
}
layerOnClick(e: mapboxgl.MapMouseEvent) {
layerOnClick(e: maplibregl.MapLayerMouseEvent) {
let coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates;
fileActions.split(
get(splitAs),

View File

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

View File

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

View File

@@ -5,7 +5,7 @@
## <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.
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.
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

@@ -13,7 +13,7 @@ title: Інтэграцыя
Усё, што вам трэба, гэта:
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 href="https://account.mapbox.com/auth/signup" target="_blank">Ключ доступу Mapbox</a> для загрузкі карты і
2. Файлы GPX, размешчаныя на вашым серверы або на Google Drive, або даступныя праз публічны URL.
Затым вы можаце пагуляць з канфігуратарам ніжэй, каб наладзіць сваю карту і стварыць адпаведны HTML-код.

View File

@@ -58,7 +58,7 @@ These controls allow you to navigate the map, zoom in and out, and switch betwee
<div class="flex flex-col items-center">
<DocsLayers />
<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.
Навядзіце курсор мышы на карту, каб паказаць накладанне <a href="https://hiking.waymarkedtrails.org" target="_blank">Пешаходных Сцежак</a> на базавай карце <a href="https://www.mapbox.com/maps/outdoors" target="_blank">Mapbox Outdoors</a>.
</span>
</div>
@@ -67,4 +67,4 @@ These controls allow you to navigate the map, zoom in and out, and switch betwee
У гэтых наладах вы таксама можаце кіраваць непразрыстасцю накладанняў.
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.
Для прасунутых карыстальнікаў можна дадаваць карыстальніцкія базавыя карты і накладкі, дадаўшы URL-адрасы <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> або <a href="https://docs.mapbox.com/help/glossary/style/" target="_blank">JSON у стылі Mapbox</a>.

View File

@@ -18,7 +18,7 @@ This tool allows you to add elevation data to traces and [points of interest](..
<DocsNote>
Elevation data is provided by <a href="https://maptiler.com" target="_blank">MapTiler</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>.
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.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data" target="_blank">documentation</a>.
</DocsNote>

View File

@@ -12,7 +12,6 @@ title: Fitxers i estadístiques
let gpxStatistics = writable(exampleGPXFile.getStatistics());
let slicedGPXStatistics = writable(undefined);
let hoveredPoint = writable(null);
let additionalDatasets = writable(['speed', 'atemp']);
let elevationFill = writable(undefined);
</script>
@@ -85,7 +84,6 @@ També pots utilitzar la rodeta del ratolí per apropar o allunyar el perfil d'e
<ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
{hoveredPoint}
{additionalDatasets}
{elevationFill}
/>

View File

@@ -5,7 +5,7 @@
## <HeartHandshake size="18" class="inline-block align-baseline" /> Ajuda a mantenir aquesta pàgina web gratuïta (i sense anuncis)
Cada cop que afegeixes o mous un punt GPS, els nostres servidors calculen la millor ruta possible.
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.
També utilitzen l'API de <a href="https://mapbox.com" target="_blank">Mapbox</a> per ensenyar mapes bonics, donar informació sobre l'altitud i permetre la cerca de llocs d'interès.
Desafortunadament, això és car.
Si gaudeixes aquesta eina i la trobes valuosa, si us plau, considera fer una petita donació per ajudar a mantenir la pàgina web gratuïta i sense anuncis.

View File

@@ -13,7 +13,7 @@ Pots utilitzar **gpx.studio** per crear mapes que mostrin els teus arxius GPX i
Tot el que necessites és:
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. Un <a href="https://account.mapbox.com/auth/signup" target="_blank"> token d'accés a Mapbox</a> per carregar el mapa i
2. Arxius GPX allotjats en el teu servidor, a Google Drive o accessibles a través d'una URL pública.
Aleshores pots jugar amb el configurador de sota per personalitzar el teu mapa i generar el corresponent codi HTML.

View File

@@ -55,10 +55,7 @@ El botó de capa de mapa permet canviar entre diferents mapes base i alternar ca
- Les **Capes sobreposades** són capes addicionals que es poden mostrar sobre el mapa base per proporcionar informació complementària.
- Els **Punts d'interès** es poden afegir al mapa per mostrar diferents categories de llocs, com botigues, restaurants o allotjaments.
<div class="flex flex-col items-center">
<DocsLayers />
<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.
<div class="flex flex-col items-center"><DocsLayers /><span class="text-sm text-center mt-2">Situa el cursor sobre el mapa per mostrar la capa <a href="https://hiking.waymarkedtrails.org" target="_blank">Waymarked Trails hiking</a> sobreposada sobre del <a href="https://www.mapbox.com/maps/outdoors" target="_blank">Mapbox Outdoors</a> mapa base.
</span>
</div>
@@ -67,4 +64,4 @@ Poden activar-se en el [configuració de capes del mapa](./menu/settings).
En aquests ajustaments pots gestionar l'opacitat de les capes sobreposades.
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.
Per a usuaris avançats és possible afegir mapes base i sobreposicions personalitzades proporcionant <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>, o <a href="https://docs.mapbox.com/help/glossary/style/" target="_blank">JSON tipus Mapbox</a> URLs.

View File

@@ -29,13 +29,13 @@ Pots arrossegar y deixar arxius directament des del seu sistema d'arxius cap a l
Crear una còpia dels arxius seleccionats.
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Esborra
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
Esborra l'arxiu seleccinat.
Delete the currently selected files.
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Esborra-ho tot
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
Esborra tots els fitxers.
Delete all files.
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportar...

View File

@@ -18,7 +18,7 @@ Aquesta eina permet afegir dades d'elevació a traces i [punts d'interès](../gp
<DocsNote>
Elevation data is provided by <a href="https://maptiler.com" target="_blank">MapTiler</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>.
Dades d'elevació subministrades per <a href="https://mapbox.com" target="_blank">Mapbox</a>.
Pots aprendre més sobre els seus orígens i precisió en la <a href="https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data" target="_blank">documentació</a>.
</DocsNote>

View File

@@ -12,7 +12,6 @@ title: Soubory a statistiky
let gpxStatistics = writable(exampleGPXFile.getStatistics());
let slicedGPXStatistics = writable(undefined);
let hoveredPoint = writable(null);
let additionalDatasets = writable(['speed', 'atemp']);
let elevationFill = writable(undefined);
</script>
@@ -85,7 +84,6 @@ Pomocí kolečka myši můžete také výškový profil přiblížit a oddálit
<ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
{hoveredPoint}
{additionalDatasets}
{elevationFill}
/>

View File

@@ -5,7 +5,7 @@
## <HeartHandshake size="18" class="inline-block align-baseline" /> Pomozte udržet web zdarma (a bez reklam)
Vždy, když přidáte nebo přesunete GPS body, naše servery vypočítají nejlepší cestu po silniční síti.
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.
Používáme také API z <a href="https://mapbox.com" target="_blank">Mapboxu</a> pro zobrazení krásných map, získání dat o nadmořské výšce a vyhledávání míst.
Bohužel, to vše je nákladné.
Pokud rádi používáte tento nástroj a zdá se vám hodnotný, zvažte prosím malý příspěvek k udržení webu zdarma a bez reklam.

View File

@@ -13,7 +13,7 @@ Pomocí **gpx.studio** můžete vytvářet mapy se zobrazením souborů GPX a vk
Vše, co potřebujete, je:
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 href="https://account.mapbox.com/auth/signup" target="_blank">Přístupový token Mapboxu</a> k načtení mapy,
2. Soubory GPX umístěné na vašem serveru nebo na Disku Google, nebo přístupné prostřednictvím veřejné adresy URL.
V níže zobrazeném konfigurátoru si pak můžete mapu přizpůsobit a vygenerovat odpovídající kód HTML.

View File

@@ -58,7 +58,7 @@ Tlačítko mapové vrstvy umožňuje přepínat mezi různými podkladovými map
<div class="flex flex-col items-center">
<DocsLayers />
<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.
Po najetí myší nad mapu se zobrazí překryv<a href="https://hiking.waymarkedtrails.org" target="_blank">značených stezek pro pěší turistiku</a> na podkladové mapě <a href="https://www.mapbox.com/maps/outdoors" target="_blank">Mapbox Outdoors</a>.
</span>
</div>
@@ -67,4 +67,4 @@ Lze je povolit v nabídce [nastavení mapových vrstev](./menu/settings).
V tomto nastavení můžete také spravovat neprůhlednost překryvů.
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.
Pokročilí uživatelé mohou přidávat vlastní podkladové mapy a překryvy pomocí <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> nebo URL <a href="https://docs.mapbox.com/help/glossary/style/" target="_blank">Mapbox stylu JSON</a>.

View File

@@ -18,7 +18,7 @@ Tento nástroj umožňuje přidat údaje o nadmořské výšce ke stopám a [bod
<DocsNote>
Elevation data is provided by <a href="https://maptiler.com" target="_blank">MapTiler</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>.
Údaje o nadmořské výšce poskytuje <a href="https://mapbox.com" target="_blank">Mapbox</a>.
Více informací o jejich původu a přesnosti najdete v <a href="https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data" target="_blank">dokumentaci</a>.
</DocsNote>

View File

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

View File

@@ -5,7 +5,7 @@
## <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.
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.
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

@@ -13,7 +13,7 @@ You can use **gpx.studio** to create maps showing your GPX files and embed them
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
2. 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.

View File

@@ -58,7 +58,7 @@ The map layers button allows you to switch between different basemaps, and toggl
<div class="flex flex-col items-center">
<DocsLayers />
<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>
</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.
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>
Elevation data is provided by <a href="https://maptiler.com" target="_blank">MapTiler</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>.
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.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data" target="_blank">documentation</a>.
</DocsNote>

View File

@@ -12,7 +12,6 @@ title: Dateien und Statistiken
let gpxStatistics = writable(exampleGPXFile.getStatistics());
let slicedGPXStatistics = writable(undefined);
let hoveredPoint = writable(null);
let additionalDatasets = writable(['speed', 'atemp']);
let elevationFill = writable(undefined);
</script>
@@ -85,7 +84,6 @@ Sie können auch das Mausrad verwenden, um auf dem Höhenprofil heranzuzoomen un
<ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
{hoveredPoint}
{additionalDatasets}
{elevationFill}
/>

View File

@@ -5,7 +5,7 @@
## <HeartHandshake size="18" class="inline-block align-baseline" /> Helfen Sie, die Website kostenlos (und werbefrei) zu erhalten
Jedes Mal, wenn Sie GPS-Punkte hinzufügen oder verschieben, berechnen unsere Server die beste Route im Straßennetz.
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.
Wir verwenden auch APIs von <a href="https://mapbox.com" target="_blank">Mapbox</a>, um schöne Karten anzuzeigen, Höhendaten abzurufen und Ihnen die Suche nach Orten zu ermöglichen.
Leider ist dies mit hohen Kosten verbunden.
Wenn Sie dieses Tool gerne verwenden und es wertvoll finden, erwägen Sie bitte eine kleine Spende, um die Website kostenlos und werbefrei zu halten.

View File

@@ -1,5 +1,5 @@
Mapbox stellt einige der auf dieser Website verwendeten Karten bereit.
Sie entwickeln auch die <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">Karten-Engine</a>, die **gpx.studio** unterstützt.
Mapbox ist das Unternehmen, das einige der schönen Karten auf dieser Website zur Verfügung stellt.
Sie entwickeln auch die <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">Karten-Engine</a> welche **gpx.studio** unterstützt.
Wir sind froh und dankbar, Teil ihres <a href="https://mapbox.com/community" target="_blank">Community</a> Programms zu sein, das gemeinnützige Organisationen, Bildungseinrichtungen und Organisationen unterstützt.
Wir sind äußerst glücklich und dankbar, Teil ihres <a href="https://mapbox.com/community" target="_blank">Community</a> Programms zu sein, das gemeinnützige Organisationen, Bildungseinrichtungen und Organisationen mit positivem Einfluss unterstützt.
Diese Partnerschaft ermöglicht es **gpx.studio**, von den Mapbox-Tools zu ermäßigten Preisen zu profitieren, was erheblich zur finanziellen Tragfähigkeit des Projekts beiträgt und es uns ermöglicht, die bestmögliche Benutzererfahrung zu bieten.

View File

@@ -13,7 +13,7 @@ Du kannst **gpx.studio** verwenden, um Karten zu erstellen, die deine GPX-Dateie
Alles was Sie brauchen:
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. Eine <a href="https://account.mapbox.com/auth/signup" target="_blank">Mapbox Zugriffstoken</a> zum Laden der Karte, und
2. GPX-Dateien, die auf Ihrem Server oder Google Drive gehostet werden oder über eine öffentliche URL erreichbar sind.
Sie können dann mit dem Konfigurator unten spielen, um Ihre Karte anzupassen und den entsprechenden HTML-Code zu generieren.

View File

@@ -58,7 +58,7 @@ Mit der Schaltfläche Karten-Ebenen können Sie zwischen verschiedenen Basemaps
<div class="flex flex-col items-center">
<DocsLayers />
<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.
Fahren Sie über der Karte, um die <a href="https://hiking.waymarkedtrails.org" target="_blank">Wegmarkierte Wanderwege</a> Overlay oben auf die <a href="https://www.mapbox.com/maps/outdoors" target="_blank">Mapbox Outdoors</a> Basemap anzuzeigen.
</span>
</div>
@@ -67,4 +67,4 @@ Sie können im [Einstellungsdialog für die Kartenlayer Einstellungen](./menu/se
In diesen Einstellungen können Sie auch die Deckkraft der Overlays verwalten.
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.
Für fortgeschrittene Benutzer ist es möglich, benutzerdefinierte Basemaps und Overlays durch die Bereitstellung von <a href="https://en.wikipedia.org/wiki/Web_Map_Tile_Service" target="_blank">WMTS</a>hinzuzufügen, <a href="https://en.wikipedia.org/wiki/Web_Map_Service" target="_blank">WMS</a>oder <a href="https://docs.mapbox.com/help/glossary/style/" target="_blank">Mapbox Stil JSON</a> URLs.

View File

@@ -33,7 +33,7 @@ Erstelle eine Kopie der aktuell ausgewählten Dateien.
Delete the currently selected files.
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Lösche alles
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
Delete all files.

View File

@@ -18,7 +18,7 @@ Mit diesem Tool kannst du Höhendaten zu Routen und [Points of Interest] (../gpx
<DocsNote>
Elevation data is provided by <a href="https://maptiler.com" target="_blank">MapTiler</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>.
Höhendaten werden von <a href="https://mapbox.com" target="_blank">Mapbox</a> bereitgestellt.
Du kannst mehr über den Ursprung und die Genauigkeit des Tools in der <a href="https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data" target="_blank">Dokumentation</a> erfahren.
</DocsNote>

View File

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

View File

@@ -5,7 +5,7 @@
## <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.
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.
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

@@ -2,11 +2,11 @@
import { Languages } from '@lucide/svelte';
</script>
## <Languages size="18" class="inline-block align-baseline" />Μετάφραση
## <Languages size="18" class="inline-block align-baseline" /> Translation
Αυτός ο ιστότοπος μεταφράζεται από εθελοντές μέσω μια πλατφόρμας συνεργατικής μετάφρασης.
Μπορείτε να συνεισφέρετε προσθέτοντας ή βελτιώνοντας μεταφράσεις στο <a href="https://crowdin.com/project/gpxstudio" target="_blank"> Crowdin έργο</a>.
The website is translated by volunteers using a collaborative translation platform.
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
Αν θέλετε να ξεκινήσετε μετάφραση μιας νέας γλώσσας, παρακαλώ <a href="#contact">επικοινωνήστε μαζί μας<a href="#contact">.
If you would like to start translating into a new language, please <a href="#contact">get in touch</a>.
Οποιαδήποτε βοήθεια εκτιμάται ιδιαίτερα!
Any help is greatly appreciated!

View File

@@ -13,7 +13,7 @@ You can use **gpx.studio** to create maps showing your GPX files and embed them
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
2. 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.

View File

@@ -58,7 +58,7 @@ The map layers button allows you to switch between different basemaps, and toggl
<div class="flex flex-col items-center">
<DocsLayers />
<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>
</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.
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

@@ -28,9 +28,9 @@ Change the language used in the interface.
<DocsNote>
Μπορείτε να συνεισφέρετε προσθέτοντας ή βελτιώνοντας μεταφράσεις στο <a href="https://crowdin.com/project/gpxstudio" target="_blank"> Crowdin έργο</a>.
Αν θέλετε να ξεκινήσετε μετάφραση μιας νέας γλώσσας, παρακαλώ <a href="#contact">επικοινωνήστε μαζί μας<a href="#contact">.
Οποιαδήποτε βοήθεια εκτιμάται ιδιαίτερα!
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
If you would like to start translating into a new language, please <a href="#contact">get in touch</a>.
Any help is greatly appreciated!
</DocsNote>

View File

@@ -1,5 +1,5 @@
---
title: Καθαρισμός
title: Clean
---
<script>
@@ -9,9 +9,9 @@ title: Καθαρισμός
# <SquareDashedMousePointer size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
Όταν επιλεχθεί το εργαλείο καθαρισμού, σέρνοντας το χάρτη δημιουργείται μια ορθογώνια επιλογή.
When the clean tool is selected, dragging the map will create a rectangular selection.
Ανάλογα με τις επιλεγμένες ρυθμίσεις στο παράθυρο παρακάτω, πατώντας το κουμπί διαγραφής θα αφαιρεθούν σημεία GPS και/ή [σημεία ενδιαφέροντος](../gpx) που βρίσκονται είτε μέσα είτε έξω από την επιλογή.
Depending on the options selected in the dialog shown below, clicking the delete button will remove GPS points and/or [points of interest](../gpx) located either inside or outside the selection.
<div class="flex flex-row justify-center">
<Clean class="text-foreground p-3 border rounded-md shadow-lg" />

View File

@@ -18,7 +18,7 @@ This tool allows you to add elevation data to traces and [points of interest](..
<DocsNote>
Elevation data is provided by <a href="https://maptiler.com" target="_blank">MapTiler</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>.
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.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data" target="_blank">documentation</a>.
</DocsNote>

View File

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

View File

@@ -5,7 +5,7 @@
## <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.
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.
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.
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.

View File

@@ -1,5 +0,0 @@
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.
All you need is:
1. A <a href="https://account.mapbox.com/auth/signup" target="_blank">Mapbox access token</a> to load the map, and
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. 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.

View File

@@ -58,7 +58,7 @@ Only one basemap can be displayed at a time.
<div class="flex flex-col items-center">
<DocsLayers />
<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.mapbox.com/maps/outdoors" target="_blank">Mapbox Outdoors</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.maptiler.com/maps/outdoor-topo/" target="_blank">MapTiler Topo</a> basemap.
</span>
</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.
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.
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.

View File

@@ -18,7 +18,7 @@ This tool allows you to add elevation data to traces and [points of interest](..
<DocsNote>
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.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data" target="_blank">documentation</a>.
Elevation data is provided by <a href="https://maptiler.com" target="_blank">MapTiler</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>.
</DocsNote>

View File

@@ -12,7 +12,6 @@ title: Archivos y estadísticas
let gpxStatistics = writable(exampleGPXFile.getStatistics());
let slicedGPXStatistics = writable(undefined);
let hoveredPoint = writable(null);
let additionalDatasets = writable(['speed', 'atemp']);
let elevationFill = writable(undefined);
</script>
@@ -85,7 +84,6 @@ Puede usar el ratón para acercar o alejar el perfil de elevación y moverse hac
<ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
{hoveredPoint}
{additionalDatasets}
{elevationFill}
/>

View File

@@ -5,7 +5,7 @@
## <HeartHandshake size="18" class="inline-block align-baseline" /> Ayude a mantener este sitio gratis (y libre de anuncios)
Cada vez que añade o mueve puntos GPS, nuestros servidores calculan la mejor ruta en la red de carreteras.
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.
También usamos APIs de <a href="https://mapbox.com" target="_blank">Mapbox</a> para mostrar hermosos mapas, obtener datos de elevación y permitirle buscar lugares.
Por desgracia, esto tiene un coste económico.
Si disfruta usando esta herramienta y la encuentra valiosa, por favor considere hacer una pequeña donación para ayudar a mantener este sitio gratis y libre de anuncios.

View File

@@ -13,7 +13,7 @@ Puede usar **gpx.studio** para crear mapas que muestren sus archivos GPX e integ
Todo lo que necesita es:
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. Un <a href="https://account.mapbox.com/auth/signup" target="_blank">token de acceso a Mapbox</a> para cargar el mapa, y
2. Archivos GPX alojados en su servidor, en Google Drive o accesibles a través de una URL pública.
Luego puede jugar con el configurador de abajo para personalizar su mapa y generar el código HTML correspondiente.

View File

@@ -58,7 +58,7 @@ El botón de capas de mapa le permite cambiar entre diferentes mapas bases y alt
<div class="flex flex-col items-center">
<DocsLayers />
<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.
Sitúe el cursor sobre el mapa para mostrar la capa <a href="https://hiking.waymarkedtrails.org" target="_blank">Rutas Señalizadas de senderismo</a> sobre el mapa base <a href="https://www.mapbox.com/maps/outdoors" target="_blank">Mapbox Exteriores</a>.
</span>
</div>
@@ -67,4 +67,4 @@ Pueden activarse en la [configuración de capas del mapa](./menu/settings).
En estos ajustes, también puede administrar la opacidad de las capas superpuestas.
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.
Para los usuarios avanzados, es posible añadir mapas base y superposiciones personalizadas proporcionando <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> o URLs <a href="https://docs.mapbox.com/help/glossary/style/" target="_blank">JSON estilo Mapbox</a>.

View File

@@ -18,7 +18,7 @@ Le permite añadir datos de desnivel a trazas y [puntos de interés](../gpx), o
<DocsNote>
Elevation data is provided by <a href="https://maptiler.com" target="_blank">MapTiler</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>.
Los datos de desnivel son proporcionados por <a href="https://mapbox.com" target="_blank">Mapbox</a>.
Puede aprender más sobre su origen y precisión en la <a href="https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data" target="_blank">documentación</a>.
</DocsNote>

View File

@@ -12,7 +12,6 @@ title: Fitxategiak eta estatistikak
let gpxStatistics = writable(exampleGPXFile.getStatistics());
let slicedGPXStatistics = writable(undefined);
let hoveredPoint = writable(null);
let additionalDatasets = writable(['speed', 'atemp']);
let elevationFill = writable(undefined);
</script>
@@ -84,7 +83,6 @@ Saguaren gurpila ere erabil dezakezu altueren profila handitzeko eta mugitzeko e
<ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
{hoveredPoint}
{additionalDatasets}
{elevationFill}
/>

View File

@@ -4,8 +4,7 @@
## <HeartHandshake size="18" class="inline-block align-baseline" /> Lagundu mantentzen webgunea doan (eta propagandarik gabe)
GPS puntuak gehitzen edo mugitzen dituzun bakoitzean, gure zerbitzariek bide sareko ibilbide onena kalkulatzen dute.
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.
GPS puntuak gehitzen edo mugitzen dituzun bakoitzean, gure zerbitzariek bide sareko ibilbide onena kalkulatzen dute.<a href="https://mapbox.com" target="_blank">Mapbox</a>en APIak erabiltzen ditugu erakusteko mapa argiak, kota-datuak eskaintzeko eta ahalbidetzeko lekuen bilaketa.
Tamalez, hau garestia da.
Tresna hau erabiltzen baduzu eta baliotsua suertatzen bazaizu, kontuan hartu dohaintza txiki bat egitea webgunea doakoa eta iragarkirik gabe mantentzen laguntzeko.

View File

@@ -13,7 +13,7 @@ title: Integrazioa
Behar duzun guztia hau da:
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 href="https://account.mapbox.com/auth/signup" target="_blank">Mapbox sarbiderako tokena</a> mapa kargatzeko, eta
2. GPX fitxategiak zure zerbitzarian, hodeian, Google Drive-n edo eskuragarri URL publiko baten bidez.
Ondoren, konfiguratzailearekin jolastu dezakezu zure mapa pertsonalizatzeko eta dagokion HTML kodea sortzeko.

View File

@@ -58,7 +58,7 @@ Maparen geruzen botoiak mapa-oinarri desberdinen artean aldatzeko aukera ematen
<div class="flex flex-col items-center">
<DocsLayers />
<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.
Pasa maparen gainetik <a href="https://hiking.waymarkedtrails.org" target="_blank">mendiko bideak</a> ikusteko <a href="https://www.mapbox.com/maps/outdoors" target="_blank">Mapbox Outdoors</a> mapa oinarriaren gainean.
</span>
</div>
@@ -67,4 +67,4 @@ Global eta tokiko mapa oinarri bilduma handia dago **gpx.studio**-n eskuragarri,
Ezarpen horietan, gainjartzeen opakutasuna ere kudeatu dezakezu.
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.
Erabiltzaile aurreratuetarako, mapa oinarri pertsonalizatuak gehitu daitezke <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>, edo <a href="https://docs.mapbox.com/help/glossary/style/" target="_blank">Mapbox Style Json</a> URLak gehituz.

View File

@@ -18,7 +18,7 @@ Tresna honen bidez elebazio datuak gehitzen ahal dira ibilbidetan eta [interesgu
<DocsNote>
Elevation data is provided by <a href="https://maptiler.com" target="_blank">MapTiler</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>.
Altuera datuen iturria <a href="https://mapbox.com" target="_blank">Mapbox</a> da.
Hauen jatorriaz edo xehetasunez informazio gehiago <a href="https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data" target="_blank">dokumentazioan</a>.
</DocsNote>

View File

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

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