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
309 changed files with 3555 additions and 4118 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

@@ -27,8 +27,8 @@ Any help is greatly appreciated!
The code is split into two parts:
- `gpx`: a Typescript library for parsing and manipulating GPX files,
- `website`: the website itself, which is a [SvelteKit](https://kit.svelte.dev/) application.
- `gpx`: a Typescript library for parsing and manipulating GPX files,
- `website`: the website itself, which is a [SvelteKit](https://kit.svelte.dev/) application.
You will need [Node.js](https://nodejs.org/) to build and run these two parts.
@@ -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
```
@@ -55,25 +55,25 @@ npm run dev
This project has been made possible thanks to the following open source projects:
- Development:
- [Svelte](https://github.com/sveltejs/svelte) and [SvelteKit](https://github.com/sveltejs/kit) — seamless development experience
- [MDsveX](https://github.com/pngwn/MDsveX) — allowing a Markdown-based documentation
- Design:
- [shadcn-svelte](https://github.com/huntabyte/shadcn-svelte) — beautiful components
- [@lucide/svelte](https://github.com/lucide-icons/lucide/tree/main/packages/svelte) — beautiful icons
- [tailwindcss](https://github.com/tailwindlabs/tailwindcss) — easy styling
- [Chart.js](https://github.com/chartjs/Chart.js) — beautiful and fast charts
- Logic:
- [immer](https://github.com/immerjs/immer) — complex state management
- [Dexie.js](https://github.com/dexie/Dexie.js) — IndexedDB wrapper
- [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
- [brouter](https://github.com/abrensch/brouter) — routing engine
- [OpenStreetMap](https://www.openstreetmap.org) — map data used by Mapbox and brouter
- Search:
- [DocSearch](https://github.com/algolia/docsearch) — search engine for the documentation
- Development:
- [Svelte](https://github.com/sveltejs/svelte) and [SvelteKit](https://github.com/sveltejs/kit) — seamless development experience
- [MDsveX](https://github.com/pngwn/MDsveX) — allowing a Markdown-based documentation
- Design:
- [shadcn-svelte](https://github.com/huntabyte/shadcn-svelte) — beautiful components
- [@lucide/svelte](https://github.com/lucide-icons/lucide/tree/main/packages/svelte) — beautiful icons
- [tailwindcss](https://github.com/tailwindlabs/tailwindcss) — easy styling
- [Chart.js](https://github.com/chartjs/Chart.js) — beautiful and fast charts
- Logic:
- [immer](https://github.com/immerjs/immer) — complex state management
- [Dexie.js](https://github.com/dexie/Dexie.js) — IndexedDB wrapper
- [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:
- [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 most of the map layers, and by the routing engine
- Search:
- [DocSearch](https://github.com/algolia/docsearch) — search engine for the documentation
## License

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();
} else {
this._marker.setLngLat(point.coordinates);
this._marker.addTo(map_);
}
if (this._dragging) {
this._hoveredPoint.set(null);
} else {
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}`);
}
});
const segmentFilter: FilterSpecification = [
'in',
['get', 'trackSegmentId'],
['literal', visibleTrackSegmentIds],
];
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',
});
}
_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
.getTrackPoint(slicedStatistics?.[2] ?? statistics.global.length - 1)!
.trkpt.getCoordinates()
)
.addTo(map_);
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();
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 {
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 {
this.start.remove();
this.end.remove();
if (map_.getLayer('start-end-markers')) {
map_.removeLayer('start-end-markers');
}
if (map_.getSource('start-end-markers')) {
map_.removeSource('start-end-markers');
}
}
}
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,13 +41,8 @@
let maxZoom: number = $state(20);
let layerType: 'basemap' | 'overlay' = $state('basemap');
let resourceType: 'raster' | 'vector' = $derived.by(() => {
if (tileUrls[0].length > 0) {
if (
tileUrls[0].includes('.json') ||
(tileUrls[0].includes('api.mapbox.com/styles') && !tileUrls[0].includes('tiles'))
) {
return 'vector';
}
if (tileUrls[0].length > 0 && tileUrls[0].includes('.json')) {
return 'vector';
}
return 'raster';
});
@@ -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: '',
},
{
id: 'overlays',
url: '',
},
],
sources: {},
layers: [],
},
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,
});
} else {
map.setTerrain(null);
}
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
return;
}
onLoad(callback: (map: maplibregl.Map) => void) {
if (this._map) {
callback(this._map);
} else {
this._onLoadCallbacks.push(callback);
}
}
callOnLoad() {
if (this._map && this._map.getLayer(ANCHOR_LAYER_KEY.overlays)) {
this._onLoadCallbacks.forEach((callback) => callback(this._map!));
this._onLoadCallbacks = [];
this._map.off('style.load', this.callOnLoadBinded);
}
}
}
export const map = new MapboxGLMap();
export const map = new MapLibreGLMap();

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">
<circle cx="20" cy="20" r="20" fill="white" />
<g transform="translate(8 8)">
${Scissors.replace('stroke="currentColor"', 'stroke="black"')}
</g>
</svg>
`);
}
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>`
);
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,17 +84,19 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
<ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
{hoveredPoint}
{additionalDatasets}
{elevationFill}
/>
</div>
<div class="flex flex-col items-center w-full">
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
orientation={'horizontal'}
/>
<div class="flex flex-col items-center -mt-6">
<div class="h-10 w-fit">
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
panelSize={120}
orientation={'horizontal'}
/>
</div>
</div>
### Additional data

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,8 +13,8 @@ title: Інтэграцыя
Усё, што вам трэба, гэта:
1. GPX files hosted on your server or on Google Drive, or accessible via a public URL;
2. _Optional:_ a <a href="https://cloud.maptiler.com/auth/widget?next=https://cloud.maptiler.com/maps/" target="_blank">MapTiler key</a> to load MapTiler maps.
1. <a href="https://account.mapbox.com/auth/signup" target="_blank">Ключ доступу Mapbox</a> для загрузкі карты і
2. Файлы GPX, размешчаныя на вашым серверы або на Google Drive, або даступныя праз публічны URL.
Затым вы можаце пагуляць з канфігуратарам ніжэй, каб наладзіць сваю карту і стварыць адпаведны HTML-код.

View File

@@ -56,12 +56,10 @@ 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.
</span>
<DocsLayers />
<span class="text-sm text-center mt-2">
Навядзіце курсор мышы на карту, каб паказаць накладанне <a href="https://hiking.waymarkedtrails.org" target="_blank">Пешаходных Сцежак</a> на базавай карце <a href="https://www.mapbox.com/maps/outdoors" target="_blank">Mapbox Outdoors</a>.
</span>
</div>
Вялікая калекцыя глабальных і лакальных базавых карт і накладанняў даступная ў **gpx.studio**, а таксама выбар катэгорый кропак цікавасці.
@@ -69,4 +67,4 @@ Hover over the map to show the <a href="https://hiking.waymarkedtrails.org" targ
У гэтых наладах вы таксама можаце кіраваць непразрыстасцю накладанняў.
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

@@ -5,7 +5,6 @@ title: Merge
<script>
import { Group } from '@lucide/svelte';
import Merge from '$lib/components/toolbar/tools/Merge.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
# <Group size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
@@ -16,13 +15,6 @@ To use this tool, you need to [select](../files-and-stats) multiple files, [trac
- The second option can be used to create or manage files with multiple [tracks or segments](../gpx).
Merging files (or tracks) will result in a single file (or track) containing all tracks (or segments) from the selection.
<DocsNote>
Selected items are merged in the order they appear in the files list.
Reorder items by drag-and-drop if needed.
</DocsNote>
<div class="flex flex-row justify-center">
<Merge class="text-foreground p-3 border rounded-md shadow-lg" />
</div>

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,17 +84,19 @@ També pots utilitzar la rodeta del ratolí per apropar o allunyar el perfil d'e
<ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
{hoveredPoint}
{additionalDatasets}
{elevationFill}
/>
</div>
<div class="flex flex-col items-center w-full">
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
orientation={'horizontal'}
/>
<div class="flex flex-col items-center -mt-6">
<div class="h-10 w-fit">
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
panelSize={120}
orientation={'horizontal'}
/>
</div>
</div>
### Dades addicionals

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,8 +13,8 @@ Pots utilitzar **gpx.studio** per crear mapes que mostrin els teus arxius GPX i
Tot el que necessites és:
1. GPX files hosted on your server or on Google Drive, or accessible via a public URL;
2. _Optional:_ a <a href="https://cloud.maptiler.com/auth/widget?next=https://cloud.maptiler.com/maps/" target="_blank">MapTiler key</a> to load MapTiler maps.
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,13 +55,8 @@ 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.
</span>
<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>
Una gran col·lecció de mapes i capes sobreposades globals i locals està disponible a **gpx.studio**, així com una selecció de categories de punts d'interès.
@@ -69,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

@@ -5,7 +5,6 @@ title: Fusionar
<script>
import { Group } from '@lucide/svelte';
import Merge from '$lib/components/toolbar/tools/Merge.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
# <Group size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
@@ -16,13 +15,6 @@ Per utilitzar aquesta eina, s'ha de [seleccionar](../files-and-stats) múltiples
- La segona opció es pot utilitzar per a crear o gestionar arxius amb múltiples [tracs o segments](../gpx).
Fusionar arxius (o tracs) crearà un sol arxiu (o trac) que contindrà tots els tracs (o segments) seleccionats.
<DocsNote>
Selected items are merged in the order they appear in the files list.
Reorder items by drag-and-drop if needed.
</DocsNote>
<div class="flex flex-row justify-center">
<Merge class="text-foreground p-3 border rounded-md shadow-lg" />
</div>

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,17 +84,19 @@ Pomocí kolečka myši můžete také výškový profil přiblížit a oddálit
<ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
{hoveredPoint}
{additionalDatasets}
{elevationFill}
/>
</div>
<div class="flex flex-col items-center w-full">
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
orientation={'horizontal'}
/>
<div class="flex flex-col items-center -mt-6">
<div class="h-10 w-fit">
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
panelSize={120}
orientation={'horizontal'}
/>
</div>
</div>
### Doplňující údaje

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,8 +13,8 @@ Pomocí **gpx.studio** můžete vytvářet mapy se zobrazením souborů GPX a vk
Vše, co potřebujete, je:
1. GPX files hosted on your server or on Google Drive, or accessible via a public URL;
2. _Optional:_ a <a href="https://cloud.maptiler.com/auth/widget?next=https://cloud.maptiler.com/maps/" target="_blank">MapTiler key</a> to load MapTiler maps.
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

@@ -56,12 +56,10 @@ Tlačítko mapové vrstvy umožňuje přepínat mezi různými podkladovými map
- **Body zájmu** lze přidat do mapy a zobrazit tak různé kategorie míst, jako jsou obchody, restaurace nebo ubytování.
<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.
</span>
<DocsLayers />
<span class="text-sm text-center mt-2">
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>
V **gpx.studio** je k dispozici rozsáhlá sbírka globálních a místních podkladových map a překryvů a také řada kategorií bodů zájmu.
@@ -69,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

@@ -5,7 +5,6 @@ title: Sloučit
<script>
import { Group } from '@lucide/svelte';
import Merge from '$lib/components/toolbar/tools/Merge.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
# <Group size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
@@ -16,13 +15,6 @@ Chcete-li použít tento nástroj, musíte [vybrat](../files-and-stats) více so
- Druhá možnost může být použita k vytvoření nebo správě souborů s více [trasami nebo segmenty](../gpx).
Sloučením souborů (nebo tras) vznikne jeden soubor (nebo trasa) obsahující všechny trasy (nebo úseky) z výběru.
<DocsNote>
Selected items are merged in the order they appear in the files list.
Reorder items by drag-and-drop if needed.
</DocsNote>
<div class="flex flex-row justify-center">
<Merge class="text-foreground p-3 border rounded-md shadow-lg" />
</div>

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,17 +84,19 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
<ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
{hoveredPoint}
{additionalDatasets}
{elevationFill}
/>
</div>
<div class="flex flex-col items-center w-full">
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
orientation={'horizontal'}
/>
<div class="flex flex-col items-center -mt-6">
<div class="h-10 w-fit">
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
panelSize={120}
orientation={'horizontal'}
/>
</div>
</div>
### Additional data

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,8 +13,8 @@ You can use **gpx.studio** to create maps showing your GPX files and embed them
All you need is:
1. GPX files hosted on your server or on Google Drive, or accessible via a public URL;
2. _Optional:_ a <a href="https://cloud.maptiler.com/auth/widget?next=https://cloud.maptiler.com/maps/" target="_blank">MapTiler key</a> to load MapTiler maps.
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

@@ -56,12 +56,10 @@ The map layers button allows you to switch between different basemaps, and toggl
- **Points of interest** can be added to the map to show different categories of places, such as shops, restaurants, or accommodations.
<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.
</span>
<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.
</span>
</div>
A large collection of global and local basemaps and overlays is available in **gpx.studio**, as well as a selection of point-of-interest categories.
@@ -69,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

@@ -5,7 +5,6 @@ title: Merge
<script>
import { Group } from '@lucide/svelte';
import Merge from '$lib/components/toolbar/tools/Merge.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
# <Group size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
@@ -16,13 +15,6 @@ To use this tool, you need to [select](../files-and-stats) multiple files, [trac
- The second option can be used to create or manage files with multiple [tracks or segments](../gpx).
Merging files (or tracks) will result in a single file (or track) containing all tracks (or segments) from the selection.
<DocsNote>
Selected items are merged in the order they appear in the files list.
Reorder items by drag-and-drop if needed.
</DocsNote>
<div class="flex flex-row justify-center">
<Merge class="text-foreground p-3 border rounded-md shadow-lg" />
</div>

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,17 +84,19 @@ Sie können auch das Mausrad verwenden, um auf dem Höhenprofil heranzuzoomen un
<ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
{hoveredPoint}
{additionalDatasets}
{elevationFill}
/>
</div>
<div class="flex flex-col items-center w-full">
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
orientation={'horizontal'}
/>
<div class="flex flex-col items-center -mt-6">
<div class="h-10 w-fit">
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
panelSize={120}
orientation={'horizontal'}
/>
</div>
</div>
### Zusätzliche Daten

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,8 +13,8 @@ Du kannst **gpx.studio** verwenden, um Karten zu erstellen, die deine GPX-Dateie
Alles was Sie brauchen:
1. GPX files hosted on your server or on Google Drive, or accessible via a public URL;
2. _Optional:_ a <a href="https://cloud.maptiler.com/auth/widget?next=https://cloud.maptiler.com/maps/" target="_blank">MapTiler key</a> to load MapTiler maps.
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

@@ -56,12 +56,10 @@ Mit der Schaltfläche Karten-Ebenen können Sie zwischen verschiedenen Basemaps
- **Punkte von Interesse** können auf der Karte angezeigt werden, um verschiedene Kategorien von Orten wie Geschäfte, Restaurants oder Unterkünfte anzuzeigen.
<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.
</span>
<DocsLayers />
<span class="text-sm text-center mt-2">
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>
Eine große Sammlung globaler und lokaler Basemaps und Overlays ist im **gpx.studio** sowie eine Auswahl von Point-of-Interest Kategorien verfügbar.
@@ -69,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

@@ -5,7 +5,6 @@ title: Zusammenfügen
<script>
import { Group } from '@lucide/svelte';
import Merge from '$lib/components/toolbar/tools/Merge.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
# <Group size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
@@ -16,13 +15,6 @@ Um dieses Tool nutzen zu können, musst du [select](../files-and-stats) mehrere
- Die zweite Option kann verwendet werden, um Dateien mit mehreren [Tracks oder Segments](../gpx) zu erstellen oder zu verwalten.
Das Zusammenführen von Dateien (oder Tracks) führt zu einer einzigen Datei (oder Track), die alle Tracks (oder Segmente) aus der Auswahl enthält.
<DocsNote>
Selected items are merged in the order they appear in the files list.
Reorder items by drag-and-drop if needed.
</DocsNote>
<div class="flex flex-row justify-center">
<Merge class="text-foreground p-3 border rounded-md shadow-lg" />
</div>

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,17 +84,19 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
<ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
{hoveredPoint}
{additionalDatasets}
{elevationFill}
/>
</div>
<div class="flex flex-col items-center w-full">
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
orientation={'horizontal'}
/>
<div class="flex flex-col items-center -mt-6">
<div class="h-10 w-fit">
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
panelSize={120}
orientation={'horizontal'}
/>
</div>
</div>
### Additional data

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,8 +13,8 @@ You can use **gpx.studio** to create maps showing your GPX files and embed them
All you need is:
1. GPX files hosted on your server or on Google Drive, or accessible via a public URL;
2. _Optional:_ a <a href="https://cloud.maptiler.com/auth/widget?next=https://cloud.maptiler.com/maps/" target="_blank">MapTiler key</a> to load MapTiler maps.
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

@@ -56,12 +56,10 @@ The map layers button allows you to switch between different basemaps, and toggl
- **Points of interest** can be added to the map to show different categories of places, such as shops, restaurants, or accommodations.
<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.
</span>
<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.
</span>
</div>
A large collection of global and local basemaps and overlays is available in **gpx.studio**, as well as a selection of point-of-interest categories.
@@ -69,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

@@ -5,7 +5,6 @@ title: Merge
<script>
import { Group } from '@lucide/svelte';
import Merge from '$lib/components/toolbar/tools/Merge.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
# <Group size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
@@ -16,13 +15,6 @@ To use this tool, you need to [select](../files-and-stats) multiple files, [trac
- The second option can be used to create or manage files with multiple [tracks or segments](../gpx).
Merging files (or tracks) will result in a single file (or track) containing all tracks (or segments) from the selection.
<DocsNote>
Selected items are merged in the order they appear in the files list.
Reorder items by drag-and-drop if needed.
</DocsNote>
<div class="flex flex-row justify-center">
<Merge class="text-foreground p-3 border rounded-md shadow-lg" />
</div>

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,17 +84,19 @@ Puede usar el ratón para acercar o alejar el perfil de elevación y moverse hac
<ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
{hoveredPoint}
{additionalDatasets}
{elevationFill}
/>
</div>
<div class="flex flex-col items-center w-full">
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
orientation={'horizontal'}
/>
<div class="flex flex-col items-center -mt-6">
<div class="h-10 w-fit">
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
panelSize={120}
orientation={'horizontal'}
/>
</div>
</div>
### Datos adicionales

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,8 +13,8 @@ Puede usar **gpx.studio** para crear mapas que muestren sus archivos GPX e integ
Todo lo que necesita es:
1. GPX files hosted on your server or on Google Drive, or accessible via a public URL;
2. _Optional:_ a <a href="https://cloud.maptiler.com/auth/widget?next=https://cloud.maptiler.com/maps/" target="_blank">MapTiler key</a> to load MapTiler maps.
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

@@ -56,12 +56,10 @@ El botón de capas de mapa le permite cambiar entre diferentes mapas bases y alt
- Los **puntos de interés** se pueden añadir al mapa para mostrar diferentes categorías de lugares, como tiendas, restaurantes o alojamientos.
<div class="flex flex-col items-center">
<DocsLayers />
<span class="text-sm text-center mt-2">
Pasa el ratón sobre el mapa para que la capa de <a href="https://hiking.waymarkedtrails.org" target="_blank">Caminatas por Sendas Marcadas</a> aparezca superpuesta al mapa base <a href="https://www.maptiler.com/maps/outdoor-topo/" target="_blank">MapTiler</a>.
</span>
<DocsLayers />
<span class="text-sm text-center mt-2">
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>
Una gran colección de mapas y capas superpuestas globales y locales está disponible en **gpx.studio**, así como una selección de categorías de puntos de interés.
@@ -69,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>

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