Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c52fa0001a | ||
|
|
dfad2ef3ef | ||
|
|
9c6e03f4a8 | ||
|
|
2a4dfe010e | ||
|
|
f42a916c25 | ||
|
|
772b810fa8 | ||
|
|
4d4d10d5c2 | ||
|
|
0e4c7dbe64 |
2
.github/workflows/deploy.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
|||||||
- name: Create env file
|
- name: Create env file
|
||||||
run: |
|
run: |
|
||||||
touch website/.env
|
touch website/.env
|
||||||
echo PUBLIC_MAPTILER_KEY=${{ secrets.PUBLIC_MAPTILER_KEY }} >> website/.env
|
echo PUBLIC_MAPBOX_TOKEN=${{ secrets.PUBLIC_MAPBOX_TOKEN }} >> website/.env
|
||||||
cat website/.env
|
cat website/.env
|
||||||
|
|
||||||
- name: Build website
|
- name: Build website
|
||||||
|
|||||||
46
README.md
@@ -27,8 +27,8 @@ Any help is greatly appreciated!
|
|||||||
|
|
||||||
The code is split into two parts:
|
The code is split into two parts:
|
||||||
|
|
||||||
- `gpx`: a Typescript library for parsing and manipulating GPX files,
|
- `gpx`: a Typescript library for parsing and manipulating GPX files,
|
||||||
- `website`: the website itself, which is a [SvelteKit](https://kit.svelte.dev/) application.
|
- `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.
|
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
|
### Running the website
|
||||||
|
|
||||||
To be able to load the map, you will need to create your own <a href="https://cloud.maptiler.com/auth/widget?next=https://cloud.maptiler.com/maps/" target="_blank">MapTiler key</a> and store it in a `.env` file in the `website` directory.
|
To be able to load the map, you will need to create your own <a href="https://account.mapbox.com/auth/signup" target="_blank">Mapbox access token</a> and store it in a `.env` file in the `website` directory.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd website
|
cd website
|
||||||
echo PUBLIC_MAPTILER_KEY={YOUR_MAPTILER_KEY} >> .env
|
echo PUBLIC_MAPBOX_TOKEN={YOUR_MAPBOX_TOKEN} >> .env
|
||||||
npm install
|
npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
@@ -55,25 +55,25 @@ npm run dev
|
|||||||
|
|
||||||
This project has been made possible thanks to the following open source projects:
|
This project has been made possible thanks to the following open source projects:
|
||||||
|
|
||||||
- Development:
|
- Development:
|
||||||
- [Svelte](https://github.com/sveltejs/svelte) and [SvelteKit](https://github.com/sveltejs/kit) — seamless development experience
|
- [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
|
- [MDsveX](https://github.com/pngwn/MDsveX) — allowing a Markdown-based documentation
|
||||||
- Design:
|
- Design:
|
||||||
- [shadcn-svelte](https://github.com/huntabyte/shadcn-svelte) — beautiful components
|
- [shadcn-svelte](https://github.com/huntabyte/shadcn-svelte) — beautiful components
|
||||||
- [@lucide/svelte](https://github.com/lucide-icons/lucide/tree/main/packages/svelte) — beautiful icons
|
- [@lucide/svelte](https://github.com/lucide-icons/lucide/tree/main/packages/svelte) — beautiful icons
|
||||||
- [tailwindcss](https://github.com/tailwindlabs/tailwindcss) — easy styling
|
- [tailwindcss](https://github.com/tailwindlabs/tailwindcss) — easy styling
|
||||||
- [Chart.js](https://github.com/chartjs/Chart.js) — beautiful and fast charts
|
- [Chart.js](https://github.com/chartjs/Chart.js) — beautiful and fast charts
|
||||||
- Logic:
|
- Logic:
|
||||||
- [immer](https://github.com/immerjs/immer) — complex state management
|
- [immer](https://github.com/immerjs/immer) — complex state management
|
||||||
- [Dexie.js](https://github.com/dexie/Dexie.js) — IndexedDB wrapper
|
- [Dexie.js](https://github.com/dexie/Dexie.js) — IndexedDB wrapper
|
||||||
- [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) — fast GPX file parsing
|
- [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) — fast GPX file parsing
|
||||||
- [SortableJS](https://github.com/SortableJS/Sortable) — creating a sortable file tree
|
- [SortableJS](https://github.com/SortableJS/Sortable) — creating a sortable file tree
|
||||||
- Mapping:
|
- Mapping:
|
||||||
- [MapLibre GL JS](https://github.com/maplibre/maplibre-gl-js) — beautiful and fast interactive maps
|
- [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js) — beautiful and fast interactive maps
|
||||||
- [brouter](https://github.com/abrensch/brouter) — routing engine
|
- [brouter](https://github.com/abrensch/brouter) — routing engine
|
||||||
- [OpenStreetMap](https://www.openstreetmap.org) — map data used by most of the map layers, and by the routing engine
|
- [OpenStreetMap](https://www.openstreetmap.org) — map data used by Mapbox and brouter
|
||||||
- Search:
|
- Search:
|
||||||
- [DocSearch](https://github.com/algolia/docsearch) — search engine for the documentation
|
- [DocSearch](https://github.com/algolia/docsearch) — search engine for the documentation
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
PUBLIC_MAPTILER_KEY=YOUR_MAPTILER_KEY
|
PUBLIC_MAPBOX_TOKEN=YOUR_MAPBOX_TOKEN
|
||||||
1222
website/package-lock.json
generated
@@ -23,9 +23,10 @@
|
|||||||
"@types/eslint": "^9.6.1",
|
"@types/eslint": "^9.6.1",
|
||||||
"@types/events": "^3.0.3",
|
"@types/events": "^3.0.3",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/mapbox__sphericalmercator": "^1.2.3",
|
|
||||||
"@types/mapbox__tilebelt": "^1.0.4",
|
"@types/mapbox__tilebelt": "^1.0.4",
|
||||||
|
"@types/mapbox-gl": "^3.4.1",
|
||||||
"@types/node": "^22.15.30",
|
"@types/node": "^22.15.30",
|
||||||
|
"@types/png.js": "^0.2.3",
|
||||||
"@types/sanitize-html": "^2.16.0",
|
"@types/sanitize-html": "^2.16.0",
|
||||||
"@types/sortablejs": "^1.15.8",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
||||||
@@ -61,9 +62,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docsearch/js": "^3.9.0",
|
"@docsearch/js": "^3.9.0",
|
||||||
"@internationalized/date": "^3.8.2",
|
"@internationalized/date": "^3.8.2",
|
||||||
|
"@mapbox/mapbox-gl-geocoder": "^5.0.3",
|
||||||
"@mapbox/sphericalmercator": "^2.0.1",
|
"@mapbox/sphericalmercator": "^2.0.1",
|
||||||
"@mapbox/tilebelt": "^2.0.2",
|
"@mapbox/tilebelt": "^2.0.2",
|
||||||
"@maplibre/maplibre-gl-geocoder": "^1.9.4",
|
"@types/mapbox__sphericalmercator": "^1.2.3",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"chartjs-plugin-zoom": "^2.2.0",
|
"chartjs-plugin-zoom": "^2.2.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -72,8 +74,9 @@
|
|||||||
"gpx": "file:../gpx",
|
"gpx": "file:../gpx",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
|
"mapbox-gl": "^3.17.0",
|
||||||
"mapillary-js": "^4.1.2",
|
"mapillary-js": "^4.1.2",
|
||||||
"maplibre-gl": "^5.16.0",
|
"png.js": "^0.2.1",
|
||||||
"sanitize-html": "^2.17.0",
|
"sanitize-html": "^2.17.0",
|
||||||
"sortablejs": "^1.15.6",
|
"sortablejs": "^1.15.6",
|
||||||
"tailwind-merge": "^3.3.0"
|
"tailwind-merge": "^3.3.0"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 3.6 MiB After Width: | Height: | Size: 3.6 MiB |
@@ -22,18 +22,15 @@ import {
|
|||||||
Binoculars,
|
Binoculars,
|
||||||
Toilet,
|
Toilet,
|
||||||
} from 'lucide-static';
|
} from 'lucide-static';
|
||||||
import { type RasterDEMSourceSpecification, type StyleSpecification } from 'maplibre-gl';
|
import { type RasterDEMSourceSpecification, type StyleSpecification } from 'mapbox-gl';
|
||||||
import ignFrTopo from './custom/ign-fr-topo.json';
|
import ignFrTopo from './custom/ign-fr-topo.json';
|
||||||
import ignFrPlan from './custom/ign-fr-plan.json';
|
import ignFrPlan from './custom/ign-fr-plan.json';
|
||||||
import ignFrSatellite from './custom/ign-fr-satellite.json';
|
import ignFrSatellite from './custom/ign-fr-satellite.json';
|
||||||
import bikerouterGravel from './custom/bikerouter-gravel.json';
|
import bikerouterGravel from './custom/bikerouter-gravel.json';
|
||||||
|
|
||||||
export const maptilerKeyPlaceHolder = 'MAPTILER_KEY';
|
|
||||||
|
|
||||||
export const basemaps: { [key: string]: string | StyleSpecification } = {
|
export const basemaps: { [key: string]: string | StyleSpecification } = {
|
||||||
maptilerTopo: `https://api.maptiler.com/maps/topo-v4/style.json?key=${maptilerKeyPlaceHolder}`,
|
mapboxOutdoors: 'mapbox://styles/mapbox/outdoors-v12',
|
||||||
maptilerOutdoors: `https://api.maptiler.com/maps/outdoor-v4/style.json?key=${maptilerKeyPlaceHolder}`,
|
mapboxSatellite: 'mapbox://styles/mapbox/satellite-streets-v12',
|
||||||
maptilerSatellite: `https://api.maptiler.com/maps/hybrid-v4/style.json?key=${maptilerKeyPlaceHolder}`,
|
|
||||||
openStreetMap: {
|
openStreetMap: {
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {
|
sources: {
|
||||||
@@ -776,9 +773,8 @@ export type LayerTreeType = { [key: string]: LayerTreeType | boolean };
|
|||||||
export const basemapTree: LayerTreeType = {
|
export const basemapTree: LayerTreeType = {
|
||||||
basemaps: {
|
basemaps: {
|
||||||
world: {
|
world: {
|
||||||
maptilerTopo: true,
|
mapboxOutdoors: true,
|
||||||
maptilerOutdoors: true,
|
mapboxSatellite: true,
|
||||||
maptilerSatellite: true,
|
|
||||||
openStreetMap: true,
|
openStreetMap: true,
|
||||||
openTopoMap: true,
|
openTopoMap: true,
|
||||||
openHikingMap: true,
|
openHikingMap: true,
|
||||||
@@ -911,7 +907,7 @@ export const overpassTree: LayerTreeType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Default basemap used
|
// Default basemap used
|
||||||
export const defaultBasemap = 'maptilerTopo';
|
export const defaultBasemap = 'mapboxOutdoors';
|
||||||
|
|
||||||
// Default overlays used (none)
|
// Default overlays used (none)
|
||||||
export const defaultOverlays: LayerTreeType = {
|
export const defaultOverlays: LayerTreeType = {
|
||||||
@@ -1000,9 +996,8 @@ export const defaultOverpassQueries: LayerTreeType = {
|
|||||||
export const defaultBasemapTree: LayerTreeType = {
|
export const defaultBasemapTree: LayerTreeType = {
|
||||||
basemaps: {
|
basemaps: {
|
||||||
world: {
|
world: {
|
||||||
maptilerTopo: true,
|
mapboxOutdoors: true,
|
||||||
maptilerOutdoors: true,
|
mapboxSatellite: true,
|
||||||
maptilerSatellite: true,
|
|
||||||
openStreetMap: true,
|
openStreetMap: true,
|
||||||
openTopoMap: true,
|
openTopoMap: true,
|
||||||
openHikingMap: true,
|
openHikingMap: true,
|
||||||
@@ -1141,7 +1136,7 @@ export type CustomLayer = {
|
|||||||
maxZoom: number;
|
maxZoom: number;
|
||||||
layerType: 'basemap' | 'overlay';
|
layerType: 'basemap' | 'overlay';
|
||||||
resourceType: 'raster' | 'vector';
|
resourceType: 'raster' | 'vector';
|
||||||
value: string | maplibregl.StyleSpecification;
|
value: string | {};
|
||||||
};
|
};
|
||||||
|
|
||||||
type OverpassQueryData = {
|
type OverpassQueryData = {
|
||||||
@@ -1460,9 +1455,11 @@ export const overpassQueryData: Record<string, OverpassQueryData> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const terrainSources: { [key: string]: RasterDEMSourceSpecification } = {
|
export const terrainSources: { [key: string]: RasterDEMSourceSpecification } = {
|
||||||
'maptiler-dem': {
|
'mapbox-dem': {
|
||||||
type: 'raster-dem',
|
type: 'raster-dem',
|
||||||
url: `https://api.maptiler.com/tiles/terrain-rgb-v2/tiles.json?key=${maptilerKeyPlaceHolder}`,
|
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
|
||||||
|
tileSize: 512,
|
||||||
|
maxzoom: 14,
|
||||||
},
|
},
|
||||||
mapterhorn: {
|
mapterhorn: {
|
||||||
type: 'raster-dem',
|
type: 'raster-dem',
|
||||||
@@ -1470,4 +1467,4 @@ export const terrainSources: { [key: string]: RasterDEMSourceSpecification } = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultTerrainSource = 'maptiler-dem';
|
export const defaultTerrainSource = 'mapbox-dem';
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
...others
|
...others
|
||||||
}: {
|
}: {
|
||||||
iconOnly?: boolean;
|
iconOnly?: boolean;
|
||||||
company?: 'gpx.studio' | 'maptiler' | 'github' | 'crowdin' | 'facebook' | 'reddit';
|
company?: 'gpx.studio' | 'mapbox' | 'github' | 'crowdin' | 'facebook' | 'reddit';
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
@@ -19,10 +19,10 @@
|
|||||||
alt="Logo of gpx.studio."
|
alt="Logo of gpx.studio."
|
||||||
{...others}
|
{...others}
|
||||||
/>
|
/>
|
||||||
{:else if company === 'maptiler'}
|
{:else if company === 'mapbox'}
|
||||||
<img
|
<img
|
||||||
src="{base}/maptiler-logo{mode.current === 'dark' ? '-dark' : ''}.svg"
|
src="{base}/mapbox-logo-{mode.current === 'dark' ? 'white' : 'black'}.svg"
|
||||||
alt="Logo of Maptiler."
|
alt="Logo of Mapbox."
|
||||||
{...others}
|
{...others}
|
||||||
/>
|
/>
|
||||||
{:else if company === 'github'}
|
{:else if company === 'github'}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import maptilerTopoMap from '$lib/assets/img/home/maptiler-topo.png?enhanced';
|
import mapboxOutdoorsMap from '$lib/assets/img/home/mapbox-outdoors.png?enhanced';
|
||||||
import waymarkedMap from '$lib/assets/img/home/waymarked.png?enhanced';
|
import waymarkedMap from '$lib/assets/img/home/waymarked.png?enhanced';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative h-80 aspect-square rounded-2xl shadow-xl overflow-clip">
|
<div class="relative h-80 aspect-square rounded-2xl shadow-xl overflow-clip">
|
||||||
<enhanced:img src={maptilerTopoMap} alt="MapTiler Topo map screenshot." class="absolute" />
|
<enhanced:img src={mapboxOutdoorsMap} alt="Mapbox Outdoors map screenshot." class="absolute" />
|
||||||
<enhanced:img
|
<enhanced:img
|
||||||
src={waymarkedMap}
|
src={waymarkedMap}
|
||||||
alt="Waymarked Trails map screenshot."
|
alt="Waymarked Trails map screenshot."
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
Construction,
|
Construction,
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import type { Readable, Writable } from 'svelte/store';
|
import type { Readable, Writable } from 'svelte/store';
|
||||||
import type { Coordinates, GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
import type { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
import { ElevationProfile } from '$lib/components/elevation-profile/elevation-profile';
|
import { ElevationProfile } from '$lib/components/elevation-profile/elevation-profile';
|
||||||
@@ -28,14 +28,12 @@
|
|||||||
let {
|
let {
|
||||||
gpxStatistics,
|
gpxStatistics,
|
||||||
slicedGPXStatistics,
|
slicedGPXStatistics,
|
||||||
hoveredPoint,
|
|
||||||
additionalDatasets,
|
additionalDatasets,
|
||||||
elevationFill,
|
elevationFill,
|
||||||
showControls = true,
|
showControls = true,
|
||||||
}: {
|
}: {
|
||||||
gpxStatistics: Readable<GPXStatisticsGroup>;
|
gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||||
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
|
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
|
||||||
hoveredPoint: Writable<Coordinates | null>;
|
|
||||||
additionalDatasets: Writable<string[]>;
|
additionalDatasets: Writable<string[]>;
|
||||||
elevationFill: Writable<'slope' | 'surface' | 'highway' | undefined>;
|
elevationFill: Writable<'slope' | 'surface' | 'highway' | undefined>;
|
||||||
showControls?: boolean;
|
showControls?: boolean;
|
||||||
@@ -49,7 +47,6 @@
|
|||||||
elevationProfile = new ElevationProfile(
|
elevationProfile = new ElevationProfile(
|
||||||
gpxStatistics,
|
gpxStatistics,
|
||||||
slicedGPXStatistics,
|
slicedGPXStatistics,
|
||||||
hoveredPoint,
|
|
||||||
additionalDatasets,
|
additionalDatasets,
|
||||||
elevationFill,
|
elevationFill,
|
||||||
canvas,
|
canvas,
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ import Chart, {
|
|||||||
type ScriptableLineSegmentContext,
|
type ScriptableLineSegmentContext,
|
||||||
type TooltipItem,
|
type TooltipItem,
|
||||||
} from 'chart.js/auto';
|
} from 'chart.js/auto';
|
||||||
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { get, type Readable, type Writable } from 'svelte/store';
|
import { get, type Readable, type Writable } from 'svelte/store';
|
||||||
import type { Coordinates, GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
import { map } from '$lib/components/map/map';
|
||||||
|
import type { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
||||||
import { mode } from 'mode-watcher';
|
import { mode } from 'mode-watcher';
|
||||||
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors';
|
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors';
|
||||||
|
|
||||||
@@ -40,7 +42,7 @@ interface ElevationProfilePoint {
|
|||||||
length: number;
|
length: number;
|
||||||
};
|
};
|
||||||
extensions: Record<string, any>;
|
extensions: Record<string, any>;
|
||||||
coordinates: Coordinates;
|
coordinates: [number, number];
|
||||||
index: number;
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,19 +50,18 @@ export class ElevationProfile {
|
|||||||
private _chart: Chart | null = null;
|
private _chart: Chart | null = null;
|
||||||
private _canvas: HTMLCanvasElement;
|
private _canvas: HTMLCanvasElement;
|
||||||
private _overlay: HTMLCanvasElement;
|
private _overlay: HTMLCanvasElement;
|
||||||
|
private _marker: mapboxgl.Marker | null = null;
|
||||||
private _dragging = false;
|
private _dragging = false;
|
||||||
private _panning = false;
|
private _panning = false;
|
||||||
|
|
||||||
private _gpxStatistics: Readable<GPXStatisticsGroup>;
|
private _gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||||
private _slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
|
private _slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
|
||||||
private _hoveredPoint: Writable<Coordinates | null>;
|
|
||||||
private _additionalDatasets: Readable<string[]>;
|
private _additionalDatasets: Readable<string[]>;
|
||||||
private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>;
|
private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
gpxStatistics: Readable<GPXStatisticsGroup>,
|
gpxStatistics: Readable<GPXStatisticsGroup>,
|
||||||
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>,
|
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>,
|
||||||
hoveredPoint: Writable<Coordinates | null>,
|
|
||||||
additionalDatasets: Readable<string[]>,
|
additionalDatasets: Readable<string[]>,
|
||||||
elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>,
|
elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>,
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
@@ -68,12 +69,17 @@ export class ElevationProfile {
|
|||||||
) {
|
) {
|
||||||
this._gpxStatistics = gpxStatistics;
|
this._gpxStatistics = gpxStatistics;
|
||||||
this._slicedGPXStatistics = slicedGPXStatistics;
|
this._slicedGPXStatistics = slicedGPXStatistics;
|
||||||
this._hoveredPoint = hoveredPoint;
|
|
||||||
this._additionalDatasets = additionalDatasets;
|
this._additionalDatasets = additionalDatasets;
|
||||||
this._elevationFill = elevationFill;
|
this._elevationFill = elevationFill;
|
||||||
this._canvas = canvas;
|
this._canvas = canvas;
|
||||||
this._overlay = overlay;
|
this._overlay = overlay;
|
||||||
|
|
||||||
|
let element = document.createElement('div');
|
||||||
|
element.className = 'h-4 w-4 rounded-full bg-cyan-500 border-2 border-white';
|
||||||
|
this._marker = new mapboxgl.Marker({
|
||||||
|
element,
|
||||||
|
});
|
||||||
|
|
||||||
import('chartjs-plugin-zoom').then((module) => {
|
import('chartjs-plugin-zoom').then((module) => {
|
||||||
Chart.register(module.default);
|
Chart.register(module.default);
|
||||||
this.initialize();
|
this.initialize();
|
||||||
@@ -156,10 +162,14 @@ export class ElevationProfile {
|
|||||||
label: (context: TooltipItem<'line'>) => {
|
label: (context: TooltipItem<'line'>) => {
|
||||||
let point = context.raw as ElevationProfilePoint;
|
let point = context.raw as ElevationProfilePoint;
|
||||||
if (context.datasetIndex === 0) {
|
if (context.datasetIndex === 0) {
|
||||||
if (this._dragging) {
|
const map_ = get(map);
|
||||||
this._hoveredPoint.set(null);
|
if (map_ && this._marker) {
|
||||||
} else {
|
if (this._dragging) {
|
||||||
this._hoveredPoint.set(point.coordinates);
|
this._marker.remove();
|
||||||
|
} else {
|
||||||
|
this._marker.setLngLat(point.coordinates);
|
||||||
|
this._marker.addTo(map_);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return `${i18n._('quantities.elevation')}: ${getElevationWithUnits(point.y, false)}`;
|
return `${i18n._('quantities.elevation')}: ${getElevationWithUnits(point.y, false)}`;
|
||||||
} else if (context.datasetIndex === 1) {
|
} else if (context.datasetIndex === 1) {
|
||||||
@@ -302,7 +312,10 @@ export class ElevationProfile {
|
|||||||
events: ['mouseout'],
|
events: ['mouseout'],
|
||||||
afterEvent: (chart: Chart, args: { event: ChartEvent }) => {
|
afterEvent: (chart: Chart, args: { event: ChartEvent }) => {
|
||||||
if (args.event.type === 'mouseout') {
|
if (args.event.type === 'mouseout') {
|
||||||
this._hoveredPoint.set(null);
|
const map_ = get(map);
|
||||||
|
if (map_ && this._marker) {
|
||||||
|
this._marker.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -624,5 +637,8 @@ export class ElevationProfile {
|
|||||||
this._chart.destroy();
|
this._chart.destroy();
|
||||||
this._chart = null;
|
this._chart = null;
|
||||||
}
|
}
|
||||||
|
if (this._marker) {
|
||||||
|
this._marker.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
import { setMode } from 'mode-watcher';
|
import { setMode } from 'mode-watcher';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { fileStateCollection } from '$lib/logic/file-state';
|
import { fileStateCollection } from '$lib/logic/file-state';
|
||||||
import { gpxStatistics, hoveredPoint, slicedGPXStatistics } from '$lib/logic/statistics';
|
import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
|
||||||
import { loadFile } from '$lib/logic/file-actions';
|
import { loadFile } from '$lib/logic/file-actions';
|
||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
<div class="grow relative">
|
<div class="grow relative">
|
||||||
<Map
|
<Map
|
||||||
class="h-full {$fileStateCollection.size > 1 ? 'horizontal' : ''}"
|
class="h-full {$fileStateCollection.size > 1 ? 'horizontal' : ''}"
|
||||||
maptilerKey={options.key}
|
accessToken={options.token}
|
||||||
geocoder={false}
|
geocoder={false}
|
||||||
geolocate={true}
|
geolocate={true}
|
||||||
hash={useHash}
|
hash={useHash}
|
||||||
@@ -130,7 +130,6 @@
|
|||||||
<ElevationProfile
|
<ElevationProfile
|
||||||
{gpxStatistics}
|
{gpxStatistics}
|
||||||
{slicedGPXStatistics}
|
{slicedGPXStatistics}
|
||||||
{hoveredPoint}
|
|
||||||
{additionalDatasets}
|
{additionalDatasets}
|
||||||
{elevationFill}
|
{elevationFill}
|
||||||
showControls={options.elevation.controls}
|
showControls={options.elevation.controls}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
getCleanedEmbeddingOptions,
|
getCleanedEmbeddingOptions,
|
||||||
getMergedEmbeddingOptions,
|
getMergedEmbeddingOptions,
|
||||||
} from './embedding';
|
} from './embedding';
|
||||||
import { PUBLIC_MAPTILER_KEY } from '$env/static/public';
|
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||||
import Embedding from './Embedding.svelte';
|
import Embedding from './Embedding.svelte';
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { base } from '$app/paths';
|
import { base } from '$app/paths';
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
let options = $state(
|
let options = $state(
|
||||||
getMergedEmbeddingOptions(
|
getMergedEmbeddingOptions(
|
||||||
{
|
{
|
||||||
key: 'YOUR_MAPTILER_KEY',
|
token: 'YOUR_MAPBOX_TOKEN',
|
||||||
theme: mode.current,
|
theme: mode.current,
|
||||||
},
|
},
|
||||||
defaultEmbeddingOptions
|
defaultEmbeddingOptions
|
||||||
@@ -46,10 +46,10 @@
|
|||||||
let iframeOptions = $derived(
|
let iframeOptions = $derived(
|
||||||
getMergedEmbeddingOptions(
|
getMergedEmbeddingOptions(
|
||||||
{
|
{
|
||||||
key:
|
token:
|
||||||
options.key.length === 0 || options.key === 'YOUR_MAPTILER_KEY'
|
options.token.length === 0 || options.token === 'YOUR_MAPBOX_TOKEN'
|
||||||
? PUBLIC_MAPTILER_KEY
|
? PUBLIC_MAPBOX_TOKEN
|
||||||
: options.key,
|
: options.token,
|
||||||
files: files.split(',').filter((url) => url.length > 0),
|
files: files.split(',').filter((url) => url.length > 0),
|
||||||
ids: driveIds.split(',').filter((id) => id.length > 0),
|
ids: driveIds.split(',').filter((id) => id.length > 0),
|
||||||
elevation: {
|
elevation: {
|
||||||
@@ -102,8 +102,8 @@
|
|||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<fieldset class="flex flex-col gap-3">
|
<fieldset class="flex flex-col gap-3">
|
||||||
<Label for="key">{i18n._('embedding.maptiler_key')}</Label>
|
<Label for="token">{i18n._('embedding.mapbox_token')}</Label>
|
||||||
<Input id="key" type="text" class="h-8" bind:value={options.key} />
|
<Input id="token" type="text" class="h-8" bind:value={options.token} />
|
||||||
<Label for="file_urls">{i18n._('embedding.file_urls')}</Label>
|
<Label for="file_urls">{i18n._('embedding.file_urls')}</Label>
|
||||||
<Input id="file_urls" type="text" class="h-8" bind:value={files} />
|
<Input id="file_urls" type="text" class="h-8" bind:value={files} />
|
||||||
<Label for="drive_ids">{i18n._('embedding.drive_ids')}</Label>
|
<Label for="drive_ids">{i18n._('embedding.drive_ids')}</Label>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { PUBLIC_MAPTILER_KEY } from '$env/static/public';
|
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||||
import { basemaps } from '$lib/assets/layers';
|
import { basemaps } from '$lib/assets/layers';
|
||||||
|
|
||||||
export type EmbeddingOptions = {
|
export type EmbeddingOptions = {
|
||||||
key: string;
|
token: string;
|
||||||
files: string[];
|
files: string[];
|
||||||
ids: string[];
|
ids: string[];
|
||||||
basemap: string;
|
basemap: string;
|
||||||
@@ -26,10 +26,10 @@ export type EmbeddingOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const defaultEmbeddingOptions = {
|
export const defaultEmbeddingOptions = {
|
||||||
key: '',
|
token: '',
|
||||||
files: [],
|
files: [],
|
||||||
ids: [],
|
ids: [],
|
||||||
basemap: 'maptilerTopo',
|
basemap: 'mapboxOutdoors',
|
||||||
elevation: {
|
elevation: {
|
||||||
show: true,
|
show: true,
|
||||||
height: 170,
|
height: 170,
|
||||||
@@ -107,7 +107,7 @@ export function getURLForGoogleDriveFile(fileId: string): string {
|
|||||||
|
|
||||||
export function convertOldEmbeddingOptions(options: URLSearchParams): any {
|
export function convertOldEmbeddingOptions(options: URLSearchParams): any {
|
||||||
let newOptions: any = {
|
let newOptions: any = {
|
||||||
key: PUBLIC_MAPTILER_KEY,
|
token: PUBLIC_MAPBOX_TOKEN,
|
||||||
files: [],
|
files: [],
|
||||||
ids: [],
|
ids: [],
|
||||||
};
|
};
|
||||||
@@ -123,7 +123,7 @@ export function convertOldEmbeddingOptions(options: URLSearchParams): any {
|
|||||||
if (options.has('source')) {
|
if (options.has('source')) {
|
||||||
let basemap = options.get('source')!;
|
let basemap = options.get('source')!;
|
||||||
if (basemap === 'satellite') {
|
if (basemap === 'satellite') {
|
||||||
newOptions.basemap = 'maptilerSatellite';
|
newOptions.basemap = 'mapboxSatellite';
|
||||||
} else if (basemap === 'otm') {
|
} else if (basemap === 'otm') {
|
||||||
newOptions.basemap = 'openTopoMap';
|
newOptions.basemap = 'openTopoMap';
|
||||||
} else if (basemap === 'ohm') {
|
} else if (basemap === 'ohm') {
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
import mapboxgl from 'mapbox-gl';
|
||||||
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
|
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
|
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { map } from '$lib/components/map/map';
|
import { map } from '$lib/components/map/map';
|
||||||
import { PUBLIC_MAPTILER_KEY } from '$env/static/public';
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
maptilerKey = PUBLIC_MAPTILER_KEY,
|
accessToken = PUBLIC_MAPBOX_TOKEN,
|
||||||
geolocate = true,
|
geolocate = true,
|
||||||
geocoder = true,
|
geocoder = true,
|
||||||
hash = true,
|
hash = true,
|
||||||
class: className = '',
|
class: className = '',
|
||||||
}: {
|
}: {
|
||||||
maptilerKey?: string;
|
accessToken?: string;
|
||||||
geolocate?: boolean;
|
geolocate?: boolean;
|
||||||
geocoder?: boolean;
|
geocoder?: boolean;
|
||||||
hash?: boolean;
|
hash?: boolean;
|
||||||
class?: string;
|
class?: string;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
|
mapboxgl.accessToken = accessToken;
|
||||||
|
|
||||||
let webgl2Supported = $state(true);
|
let webgl2Supported = $state(true);
|
||||||
let embeddedApp = $state(false);
|
let embeddedApp = $state(false);
|
||||||
|
|
||||||
@@ -43,7 +48,7 @@
|
|||||||
language = 'en';
|
language = 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
map.init(maptilerKey, language, hash, geocoder, geolocate);
|
map.init(language, hash, geocoder, geolocate);
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
@@ -76,21 +81,21 @@
|
|||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
@reference "../../../app.css";
|
@reference "../../../app.css";
|
||||||
|
|
||||||
div :global(.maplibregl-map) {
|
div :global(.mapboxgl-map) {
|
||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-ctrl-top-right > .maplibregl-ctrl) {
|
div :global(.mapboxgl-ctrl-top-right > .mapboxgl-ctrl) {
|
||||||
@apply shadow-md;
|
@apply shadow-md;
|
||||||
@apply bg-background;
|
@apply bg-background;
|
||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-ctrl-icon) {
|
div :global(.mapboxgl-ctrl-icon) {
|
||||||
@apply dark:brightness-[4.7];
|
@apply dark:brightness-[4.7];
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-ctrl-geocoder) {
|
div :global(.mapboxgl-ctrl-geocoder) {
|
||||||
@apply flex;
|
@apply flex;
|
||||||
@apply flex-row;
|
@apply flex-row;
|
||||||
@apply w-fit;
|
@apply w-fit;
|
||||||
@@ -105,27 +110,27 @@
|
|||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-ctrl-geocoder .suggestions > li > a) {
|
div :global(.mapboxgl-ctrl-geocoder .suggestions > li > a) {
|
||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
@apply hover:text-accent-foreground;
|
@apply hover:text-accent-foreground;
|
||||||
@apply hover:bg-accent;
|
@apply hover:bg-accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-ctrl-geocoder .suggestions > .active > a) {
|
div :global(.mapboxgl-ctrl-geocoder .suggestions > .active > a) {
|
||||||
@apply bg-background;
|
@apply bg-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-ctrl-geocoder--button) {
|
div :global(.mapboxgl-ctrl-geocoder--button) {
|
||||||
@apply bg-transparent;
|
@apply bg-transparent;
|
||||||
@apply hover:bg-transparent;
|
@apply hover:bg-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-ctrl-geocoder--icon) {
|
div :global(.mapboxgl-ctrl-geocoder--icon) {
|
||||||
@apply fill-foreground;
|
@apply fill-foreground;
|
||||||
@apply hover:fill-accent-foreground;
|
@apply hover:fill-accent-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-ctrl-geocoder--icon-search) {
|
div :global(.mapboxgl-ctrl-geocoder--icon-search) {
|
||||||
@apply relative;
|
@apply relative;
|
||||||
@apply top-0;
|
@apply top-0;
|
||||||
@apply left-0;
|
@apply left-0;
|
||||||
@@ -133,7 +138,7 @@
|
|||||||
@apply w-[29px];
|
@apply w-[29px];
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-ctrl-geocoder--input) {
|
div :global(.mapboxgl-ctrl-geocoder--input) {
|
||||||
@apply relative;
|
@apply relative;
|
||||||
@apply w-64;
|
@apply w-64;
|
||||||
@apply py-0;
|
@apply py-0;
|
||||||
@@ -144,12 +149,12 @@
|
|||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-ctrl-geocoder--collapsed .maplibregl-ctrl-geocoder--input) {
|
div :global(.mapboxgl-ctrl-geocoder--collapsed .mapboxgl-ctrl-geocoder--input) {
|
||||||
@apply w-0;
|
@apply w-0;
|
||||||
@apply p-0;
|
@apply p-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-ctrl-top-right) {
|
div :global(.mapboxgl-ctrl-top-right) {
|
||||||
@apply z-40;
|
@apply z-40;
|
||||||
@apply flex;
|
@apply flex;
|
||||||
@apply flex-col;
|
@apply flex-col;
|
||||||
@@ -158,76 +163,77 @@
|
|||||||
@apply overflow-hidden;
|
@apply overflow-hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.horizontal :global(.maplibregl-ctrl-bottom-left) {
|
.horizontal :global(.mapboxgl-ctrl-bottom-left) {
|
||||||
@apply bottom-[42px];
|
@apply bottom-[42px];
|
||||||
}
|
}
|
||||||
|
|
||||||
.horizontal :global(.maplibregl-ctrl-bottom-right) {
|
.horizontal :global(.mapboxgl-ctrl-bottom-right) {
|
||||||
@apply bottom-[42px];
|
@apply bottom-[42px];
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-ctrl-attrib) {
|
div :global(.mapboxgl-ctrl-attrib) {
|
||||||
@apply dark:bg-transparent;
|
@apply dark:bg-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-compact-show.maplibregl-ctrl-attrib) {
|
div :global(.mapboxgl-compact-show.mapboxgl-ctrl-attrib) {
|
||||||
@apply dark:bg-background;
|
@apply dark:bg-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-ctrl-attrib-button) {
|
div :global(.mapboxgl-ctrl-attrib-button) {
|
||||||
@apply dark:bg-foreground;
|
@apply dark:bg-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-compact-show .maplibregl-ctrl-attrib-button) {
|
div :global(.mapboxgl-compact-show .mapboxgl-ctrl-attrib-button) {
|
||||||
@apply dark:bg-foreground;
|
@apply dark:bg-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-ctrl-attrib a) {
|
div :global(.mapboxgl-ctrl-attrib a) {
|
||||||
@apply text-foreground;
|
@apply text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-popup) {
|
div :global(.mapboxgl-popup) {
|
||||||
|
@apply w-fit;
|
||||||
@apply z-50;
|
@apply z-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-popup-content) {
|
div :global(.mapboxgl-popup-content) {
|
||||||
@apply p-0;
|
@apply p-0;
|
||||||
@apply bg-transparent;
|
@apply bg-transparent;
|
||||||
@apply shadow-none;
|
@apply shadow-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-popup-anchor-top .maplibregl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-top .mapboxgl-popup-tip) {
|
||||||
@apply border-b-background;
|
@apply border-b-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-popup-anchor-top-left .maplibregl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip) {
|
||||||
@apply border-b-background;
|
@apply border-b-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-popup-anchor-top-right .maplibregl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip) {
|
||||||
@apply border-b-background;
|
@apply border-b-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-popup-anchor-bottom .maplibregl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip) {
|
||||||
@apply border-t-background;
|
@apply border-t-background;
|
||||||
@apply drop-shadow-md;
|
@apply drop-shadow-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip) {
|
||||||
@apply border-t-background;
|
@apply border-t-background;
|
||||||
@apply drop-shadow-md;
|
@apply drop-shadow-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip) {
|
||||||
@apply border-t-background;
|
@apply border-t-background;
|
||||||
@apply drop-shadow-md;
|
@apply drop-shadow-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-popup-anchor-left .maplibregl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-left .mapboxgl-popup-tip) {
|
||||||
@apply border-r-background;
|
@apply border-r-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-popup-anchor-right .maplibregl-popup-tip) {
|
div :global(.mapboxgl-popup-anchor-right .mapboxgl-popup-tip) {
|
||||||
@apply border-l-background;
|
@apply border-l-background;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
let control: CustomControl | null = null;
|
let control: CustomControl | null = null;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
map.onLoad((map: maplibregl.Map) => {
|
map.onLoad((map: mapboxgl.Map) => {
|
||||||
if (position.includes('right')) container.classList.add('float-right');
|
if (position.includes('right')) container.classList.add('float-right');
|
||||||
else container.classList.add('float-left');
|
else container.classList.add('float-left');
|
||||||
container.classList.remove('hidden');
|
container.classList.remove('hidden');
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type Map, type IControl } from 'maplibre-gl';
|
import { type Map, type IControl } from 'mapbox-gl';
|
||||||
|
|
||||||
export default class CustomControl implements IControl {
|
export default class CustomControl implements IControl {
|
||||||
_map: Map | undefined;
|
_map: Map | undefined;
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { gpxStatistics } from '$lib/logic/statistics';
|
import { gpxStatistics } from '$lib/logic/statistics';
|
||||||
import { getConvertedDistanceToKilometers } from '$lib/units';
|
import { getConvertedDistanceToKilometers } from '$lib/units';
|
||||||
|
import type { GeoJSONSource } from 'mapbox-gl';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { map } from '$lib/components/map/map';
|
import { ANCHOR_LAYER_KEY, map } from '$lib/components/map/map';
|
||||||
import { allHidden } from '$lib/logic/hidden';
|
import { allHidden } from '$lib/logic/hidden';
|
||||||
import type { GeoJSONSource } from 'maplibre-gl';
|
|
||||||
import { ANCHOR_LAYER_KEY } from '../style';
|
|
||||||
|
|
||||||
const { distanceMarkers, distanceUnits } = settings;
|
const { distanceMarkers, distanceUnits } = settings;
|
||||||
|
|
||||||
@@ -23,7 +22,7 @@ export class DistanceMarkers {
|
|||||||
this.unsubscribes.push(
|
this.unsubscribes.push(
|
||||||
map.subscribe((map_) => {
|
map.subscribe((map_) => {
|
||||||
if (map_) {
|
if (map_) {
|
||||||
map_.on('style.load', this.updateBinded);
|
map_.on('style.import.load', this.updateBinded);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ import { MapPopup } from '$lib/components/map/map-popup';
|
|||||||
export let waypointPopup: MapPopup | null = null;
|
export let waypointPopup: MapPopup | null = null;
|
||||||
export let trackpointPopup: MapPopup | null = null;
|
export let trackpointPopup: MapPopup | null = null;
|
||||||
|
|
||||||
export function createPopups(map: maplibregl.Map) {
|
export function createPopups(map: mapboxgl.Map) {
|
||||||
removePopups();
|
removePopups();
|
||||||
waypointPopup = new MapPopup(map, {
|
waypointPopup = new MapPopup(map, {
|
||||||
closeButton: false,
|
closeButton: false,
|
||||||
focusAfterOpen: false,
|
focusAfterOpen: false,
|
||||||
maxWidth: undefined,
|
maxWidth: undefined,
|
||||||
offset: {
|
offset: {
|
||||||
center: [0, 0],
|
|
||||||
top: [0, 0],
|
top: [0, 0],
|
||||||
'top-left': [0, 0],
|
'top-left': [0, 0],
|
||||||
'top-right': [0, 0],
|
'top-right': [0, 0],
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { get, type Readable } from 'svelte/store';
|
import { get, type Readable } from 'svelte/store';
|
||||||
import maplibregl, {
|
import mapboxgl, { type FilterSpecification } from 'mapbox-gl';
|
||||||
type GeoJSONSource,
|
import { ANCHOR_LAYER_KEY, map } from '$lib/components/map/map';
|
||||||
type FilterSpecification,
|
|
||||||
type MapLayerMouseEvent,
|
|
||||||
type MapLayerTouchEvent,
|
|
||||||
} from 'maplibre-gl';
|
|
||||||
import { map } from '$lib/components/map/map';
|
|
||||||
import { waypointPopup, trackpointPopup } from './gpx-layer-popup';
|
import { waypointPopup, trackpointPopup } from './gpx-layer-popup';
|
||||||
import {
|
import {
|
||||||
ListTrackSegmentItem,
|
ListTrackSegmentItem,
|
||||||
@@ -15,7 +10,7 @@ import {
|
|||||||
ListFileItem,
|
ListFileItem,
|
||||||
ListRootItem,
|
ListRootItem,
|
||||||
} from '$lib/components/file-list/file-list';
|
} from '$lib/components/file-list/file-list';
|
||||||
import { getClosestLinePoint, getElevation, loadSVGIcon } from '$lib/utils';
|
import { getClosestLinePoint, getElevation } from '$lib/utils';
|
||||||
import { selectedWaypoint } from '$lib/components/toolbar/tools/waypoint/waypoint';
|
import { selectedWaypoint } from '$lib/components/toolbar/tools/waypoint/waypoint';
|
||||||
import { MapPin, Square } from 'lucide-static';
|
import { MapPin, Square } from 'lucide-static';
|
||||||
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
||||||
@@ -27,8 +22,7 @@ import { fileActionManager } from '$lib/logic/file-action-manager';
|
|||||||
import { fileActions } from '$lib/logic/file-actions';
|
import { fileActions } from '$lib/logic/file-actions';
|
||||||
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
|
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
|
||||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
import { gpxColors } from '$lib/components/map/gpx-layer/gpx-layers';
|
||||||
import { gpxColors } from './gpx-layers';
|
|
||||||
|
|
||||||
const colors = [
|
const colors = [
|
||||||
'#ff0000',
|
'#ff0000',
|
||||||
@@ -120,28 +114,28 @@ export class GPXLayer {
|
|||||||
selected: boolean = false;
|
selected: boolean = false;
|
||||||
currentWaypointData: GeoJSON.FeatureCollection | null = null;
|
currentWaypointData: GeoJSON.FeatureCollection | null = null;
|
||||||
draggedWaypointIndex: number | null = null;
|
draggedWaypointIndex: number | null = null;
|
||||||
draggingStartingPosition: maplibregl.Point = new maplibregl.Point(0, 0);
|
draggingStartingPosition: mapboxgl.Point = new mapboxgl.Point(0, 0);
|
||||||
unsubscribe: Function[] = [];
|
unsubscribe: Function[] = [];
|
||||||
|
|
||||||
updateBinded: () => void = this.update.bind(this);
|
updateBinded: () => void = this.update.bind(this);
|
||||||
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
|
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
|
||||||
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
|
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
|
||||||
layerOnMouseMoveBinded: (e: any) => void = this.layerOnMouseMove.bind(this);
|
layerOnMouseMoveBinded: (e: any) => void = this.layerOnMouseMove.bind(this);
|
||||||
layerOnClickBinded: (e: MapLayerMouseEvent) => void = this.layerOnClick.bind(this);
|
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
|
||||||
layerOnContextMenuBinded: (e: MapLayerMouseEvent) => void = this.layerOnContextMenu.bind(this);
|
layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this);
|
||||||
waypointLayerOnMouseEnterBinded: (e: MapLayerMouseEvent) => void =
|
waypointLayerOnMouseEnterBinded: (e: mapboxgl.MapMouseEvent) => void =
|
||||||
this.waypointLayerOnMouseEnter.bind(this);
|
this.waypointLayerOnMouseEnter.bind(this);
|
||||||
waypointLayerOnMouseLeaveBinded: (e: MapLayerMouseEvent) => void =
|
waypointLayerOnMouseLeaveBinded: (e: mapboxgl.MapMouseEvent) => void =
|
||||||
this.waypointLayerOnMouseLeave.bind(this);
|
this.waypointLayerOnMouseLeave.bind(this);
|
||||||
waypointLayerOnClickBinded: (e: MapLayerMouseEvent) => void =
|
waypointLayerOnClickBinded: (e: mapboxgl.MapMouseEvent) => void =
|
||||||
this.waypointLayerOnClick.bind(this);
|
this.waypointLayerOnClick.bind(this);
|
||||||
waypointLayerOnMouseDownBinded: (e: MapLayerMouseEvent) => void =
|
waypointLayerOnMouseDownBinded: (e: mapboxgl.MapMouseEvent) => void =
|
||||||
this.waypointLayerOnMouseDown.bind(this);
|
this.waypointLayerOnMouseDown.bind(this);
|
||||||
waypointLayerOnTouchStartBinded: (e: MapLayerTouchEvent) => void =
|
waypointLayerOnTouchStartBinded: (e: mapboxgl.MapTouchEvent) => void =
|
||||||
this.waypointLayerOnTouchStart.bind(this);
|
this.waypointLayerOnTouchStart.bind(this);
|
||||||
waypointLayerOnMouseMoveBinded: (e: MapLayerMouseEvent | MapLayerTouchEvent) => void =
|
waypointLayerOnMouseMoveBinded: (e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) => void =
|
||||||
this.waypointLayerOnMouseMove.bind(this);
|
this.waypointLayerOnMouseMove.bind(this);
|
||||||
waypointLayerOnMouseUpBinded: (e: MapLayerMouseEvent | MapLayerTouchEvent) => void =
|
waypointLayerOnMouseUpBinded: (e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) => void =
|
||||||
this.waypointLayerOnMouseUp.bind(this);
|
this.waypointLayerOnMouseUp.bind(this);
|
||||||
|
|
||||||
constructor(fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
|
constructor(fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
|
||||||
@@ -151,7 +145,7 @@ export class GPXLayer {
|
|||||||
this.unsubscribe.push(
|
this.unsubscribe.push(
|
||||||
map.subscribe(($map) => {
|
map.subscribe(($map) => {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
$map.on('style.load', this.updateBinded);
|
$map.on('style.import.load', this.updateBinded);
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -174,9 +168,8 @@ export class GPXLayer {
|
|||||||
|
|
||||||
update() {
|
update() {
|
||||||
const _map = get(map);
|
const _map = get(map);
|
||||||
const layerEventManager = map.layerEventManager;
|
|
||||||
let file = get(this.file)?.file;
|
let file = get(this.file)?.file;
|
||||||
if (!_map || !layerEventManager || !file) {
|
if (!_map || !file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +185,7 @@ export class GPXLayer {
|
|||||||
this.loadIcons();
|
this.loadIcons();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let source = _map.getSource(this.fileId) as GeoJSONSource | undefined;
|
let source = _map.getSource(this.fileId) as mapboxgl.GeoJSONSource | undefined;
|
||||||
if (source) {
|
if (source) {
|
||||||
source.setData(this.getGeoJSON());
|
source.setData(this.getGeoJSON());
|
||||||
} else {
|
} else {
|
||||||
@@ -221,64 +214,15 @@ export class GPXLayer {
|
|||||||
ANCHOR_LAYER_KEY.tracks
|
ANCHOR_LAYER_KEY.tracks
|
||||||
);
|
);
|
||||||
|
|
||||||
layerEventManager.on('click', this.fileId, this.layerOnClickBinded);
|
_map.on('click', this.fileId, this.layerOnClickBinded);
|
||||||
layerEventManager.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
_map.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
||||||
layerEventManager.on('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
_map.on('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
||||||
layerEventManager.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
_map.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
||||||
layerEventManager.on('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
_map.on('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
||||||
}
|
|
||||||
|
|
||||||
let visibleTrackSegmentIds: string[] = [];
|
|
||||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
|
||||||
if (!segment._data.hidden) {
|
|
||||||
visibleTrackSegmentIds.push(`${trackIndex}-${segmentIndex}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const segmentFilter: FilterSpecification = [
|
|
||||||
'in',
|
|
||||||
['get', 'trackSegmentId'],
|
|
||||||
['literal', visibleTrackSegmentIds],
|
|
||||||
];
|
|
||||||
|
|
||||||
_map.setFilter(this.fileId, segmentFilter, { validate: false });
|
|
||||||
|
|
||||||
if (get(directionMarkers)) {
|
|
||||||
if (!_map.getLayer(this.fileId + '-direction')) {
|
|
||||||
_map.addLayer(
|
|
||||||
{
|
|
||||||
id: this.fileId + '-direction',
|
|
||||||
type: 'symbol',
|
|
||||||
source: this.fileId,
|
|
||||||
layout: {
|
|
||||||
'text-field': '»',
|
|
||||||
'text-offset': [0, -0.1],
|
|
||||||
'text-keep-upright': false,
|
|
||||||
'text-max-angle': 361,
|
|
||||||
'text-allow-overlap': true,
|
|
||||||
'text-font': ['Open Sans Bold'],
|
|
||||||
'symbol-placement': 'line',
|
|
||||||
'symbol-spacing': 20,
|
|
||||||
},
|
|
||||||
paint: {
|
|
||||||
'text-color': 'white',
|
|
||||||
'text-opacity': 0.7,
|
|
||||||
'text-halo-width': 0.2,
|
|
||||||
'text-halo-color': 'white',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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 waypointSource = _map.getSource(this.fileId + '-waypoints') as
|
let waypointSource = _map.getSource(this.fileId + '-waypoints') as
|
||||||
| GeoJSONSource
|
| mapboxgl.GeoJSONSource
|
||||||
| undefined;
|
| undefined;
|
||||||
this.currentWaypointData = this.getWaypointsGeoJSON();
|
this.currentWaypointData = this.getWaypointsGeoJSON();
|
||||||
if (waypointSource) {
|
if (waypointSource) {
|
||||||
@@ -307,33 +251,80 @@ export class GPXLayer {
|
|||||||
ANCHOR_LAYER_KEY.waypoints
|
ANCHOR_LAYER_KEY.waypoints
|
||||||
);
|
);
|
||||||
|
|
||||||
layerEventManager.on(
|
_map.on(
|
||||||
'mouseenter',
|
'mouseenter',
|
||||||
this.fileId + '-waypoints',
|
this.fileId + '-waypoints',
|
||||||
this.waypointLayerOnMouseEnterBinded
|
this.waypointLayerOnMouseEnterBinded
|
||||||
);
|
);
|
||||||
layerEventManager.on(
|
_map.on(
|
||||||
'mouseleave',
|
'mouseleave',
|
||||||
this.fileId + '-waypoints',
|
this.fileId + '-waypoints',
|
||||||
this.waypointLayerOnMouseLeaveBinded
|
this.waypointLayerOnMouseLeaveBinded
|
||||||
);
|
);
|
||||||
layerEventManager.on(
|
_map.on('click', this.fileId + '-waypoints', this.waypointLayerOnClickBinded);
|
||||||
'click',
|
_map.on(
|
||||||
this.fileId + '-waypoints',
|
|
||||||
this.waypointLayerOnClickBinded
|
|
||||||
);
|
|
||||||
layerEventManager.on(
|
|
||||||
'mousedown',
|
'mousedown',
|
||||||
this.fileId + '-waypoints',
|
this.fileId + '-waypoints',
|
||||||
this.waypointLayerOnMouseDownBinded
|
this.waypointLayerOnMouseDownBinded
|
||||||
);
|
);
|
||||||
layerEventManager.on(
|
_map.on(
|
||||||
'touchstart',
|
'touchstart',
|
||||||
this.fileId + '-waypoints',
|
this.fileId + '-waypoints',
|
||||||
this.waypointLayerOnTouchStartBinded
|
this.waypointLayerOnTouchStartBinded
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (get(directionMarkers)) {
|
||||||
|
if (!_map.getLayer(this.fileId + '-direction')) {
|
||||||
|
_map.addLayer(
|
||||||
|
{
|
||||||
|
id: this.fileId + '-direction',
|
||||||
|
type: 'symbol',
|
||||||
|
source: this.fileId,
|
||||||
|
layout: {
|
||||||
|
'text-field': '»',
|
||||||
|
'text-offset': [0, -0.1],
|
||||||
|
'text-keep-upright': false,
|
||||||
|
'text-max-angle': 361,
|
||||||
|
'text-allow-overlap': true,
|
||||||
|
'text-font': ['Open Sans Bold'],
|
||||||
|
'symbol-placement': 'line',
|
||||||
|
'symbol-spacing': 20,
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-color': 'white',
|
||||||
|
'text-opacity': 0.7,
|
||||||
|
'text-halo-width': 0.2,
|
||||||
|
'text-halo-color': 'white',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ANCHOR_LAYER_KEY.directionMarkers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} 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],
|
||||||
|
];
|
||||||
|
|
||||||
|
_map.setFilter(this.fileId, segmentFilter, { validate: false });
|
||||||
|
|
||||||
|
if (_map.getLayer(this.fileId + '-direction')) {
|
||||||
|
_map.setFilter(this.fileId + '-direction', segmentFilter, { validate: false });
|
||||||
|
}
|
||||||
|
|
||||||
let visibleWaypoints: number[] = [];
|
let visibleWaypoints: number[] = [];
|
||||||
file.wpt.forEach((waypoint, waypointIndex) => {
|
file.wpt.forEach((waypoint, waypointIndex) => {
|
||||||
if (!waypoint._data.hidden) {
|
if (!waypoint._data.hidden) {
|
||||||
@@ -354,47 +345,32 @@ export class GPXLayer {
|
|||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
const _map = get(map);
|
const _map = get(map);
|
||||||
|
|
||||||
if (_map) {
|
if (_map) {
|
||||||
_map.off('style.load', this.updateBinded);
|
_map.off('click', this.fileId, this.layerOnClickBinded);
|
||||||
}
|
_map.off('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
||||||
|
_map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
||||||
|
_map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
||||||
|
_map.off('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
||||||
|
_map.off('style.import.load', this.updateBinded);
|
||||||
|
|
||||||
const layerEventManager = map.layerEventManager;
|
_map.off(
|
||||||
if (layerEventManager) {
|
|
||||||
layerEventManager.off('click', this.fileId, this.layerOnClickBinded);
|
|
||||||
layerEventManager.off('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
|
||||||
layerEventManager.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
|
||||||
layerEventManager.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
|
||||||
layerEventManager.off('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
|
||||||
|
|
||||||
layerEventManager.off(
|
|
||||||
'mouseenter',
|
'mouseenter',
|
||||||
this.fileId + '-waypoints',
|
this.fileId + '-waypoints',
|
||||||
this.waypointLayerOnMouseEnterBinded
|
this.waypointLayerOnMouseEnterBinded
|
||||||
);
|
);
|
||||||
layerEventManager.off(
|
_map.off(
|
||||||
'mouseleave',
|
'mouseleave',
|
||||||
this.fileId + '-waypoints',
|
this.fileId + '-waypoints',
|
||||||
this.waypointLayerOnMouseLeaveBinded
|
this.waypointLayerOnMouseLeaveBinded
|
||||||
);
|
);
|
||||||
layerEventManager.off(
|
_map.off('click', this.fileId + '-waypoints', this.waypointLayerOnClickBinded);
|
||||||
'click',
|
_map.off('mousedown', this.fileId + '-waypoints', this.waypointLayerOnMouseDownBinded);
|
||||||
this.fileId + '-waypoints',
|
_map.off(
|
||||||
this.waypointLayerOnClickBinded
|
|
||||||
);
|
|
||||||
layerEventManager.off(
|
|
||||||
'mousedown',
|
|
||||||
this.fileId + '-waypoints',
|
|
||||||
this.waypointLayerOnMouseDownBinded
|
|
||||||
);
|
|
||||||
layerEventManager.off(
|
|
||||||
'touchstart',
|
'touchstart',
|
||||||
this.fileId + '-waypoints',
|
this.fileId + '-waypoints',
|
||||||
this.waypointLayerOnTouchStartBinded
|
this.waypointLayerOnTouchStartBinded
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (_map) {
|
|
||||||
if (_map.getLayer(this.fileId + '-direction')) {
|
if (_map.getLayer(this.fileId + '-direction')) {
|
||||||
_map.removeLayer(this.fileId + '-direction');
|
_map.removeLayer(this.fileId + '-direction');
|
||||||
}
|
}
|
||||||
@@ -470,7 +446,7 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
layerOnClick(e: MapLayerMouseEvent) {
|
layerOnClick(e: mapboxgl.MapMouseEvent) {
|
||||||
if (
|
if (
|
||||||
get(currentTool) === Tool.ROUTING &&
|
get(currentTool) === Tool.ROUTING &&
|
||||||
get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])
|
get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])
|
||||||
@@ -528,7 +504,7 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
waypointLayerOnMouseEnter(e: MapLayerMouseEvent) {
|
waypointLayerOnMouseEnter(e: mapboxgl.MapMouseEvent) {
|
||||||
if (this.draggedWaypointIndex !== null) {
|
if (this.draggedWaypointIndex !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -548,7 +524,7 @@ export class GPXLayer {
|
|||||||
mapCursor.notify(MapCursorState.WAYPOINT_HOVER, false);
|
mapCursor.notify(MapCursorState.WAYPOINT_HOVER, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
waypointLayerOnClick(e: MapLayerMouseEvent) {
|
waypointLayerOnClick(e: mapboxgl.MapMouseEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
let waypointIndex = e.features![0].properties!.waypointIndex;
|
let waypointIndex = e.features![0].properties!.waypointIndex;
|
||||||
@@ -590,7 +566,7 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
waypointLayerOnMouseDown(e: MapLayerMouseEvent) {
|
waypointLayerOnMouseDown(e: mapboxgl.MapMouseEvent) {
|
||||||
if (get(currentTool) !== Tool.WAYPOINT || !this.selected) {
|
if (get(currentTool) !== Tool.WAYPOINT || !this.selected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -600,7 +576,6 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
_map.dragPan.disable();
|
|
||||||
|
|
||||||
this.draggedWaypointIndex = e.features![0].properties!.waypointIndex;
|
this.draggedWaypointIndex = e.features![0].properties!.waypointIndex;
|
||||||
this.draggingStartingPosition = e.point;
|
this.draggingStartingPosition = e.point;
|
||||||
@@ -610,7 +585,7 @@ export class GPXLayer {
|
|||||||
_map.once('mouseup', this.waypointLayerOnMouseUpBinded);
|
_map.once('mouseup', this.waypointLayerOnMouseUpBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
waypointLayerOnTouchStart(e: MapLayerTouchEvent) {
|
waypointLayerOnTouchStart(e: mapboxgl.MapTouchEvent) {
|
||||||
if (e.points.length !== 1 || get(currentTool) !== Tool.WAYPOINT || !this.selected) {
|
if (e.points.length !== 1 || get(currentTool) !== Tool.WAYPOINT || !this.selected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -624,13 +599,12 @@ export class GPXLayer {
|
|||||||
waypointPopup?.hide();
|
waypointPopup?.hide();
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
_map.dragPan.disable();
|
|
||||||
|
|
||||||
_map.on('touchmove', this.waypointLayerOnMouseMoveBinded);
|
_map.on('touchmove', this.waypointLayerOnMouseMoveBinded);
|
||||||
_map.once('touchend', this.waypointLayerOnMouseUpBinded);
|
_map.once('touchend', this.waypointLayerOnMouseUpBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
waypointLayerOnMouseMove(e: MapLayerMouseEvent | MapLayerTouchEvent) {
|
waypointLayerOnMouseMove(e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) {
|
||||||
if (this.draggedWaypointIndex === null || e.point.equals(this.draggingStartingPosition)) {
|
if (this.draggedWaypointIndex === null || e.point.equals(this.draggingStartingPosition)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -642,25 +616,18 @@ export class GPXLayer {
|
|||||||
).coordinates = [e.lngLat.lng, e.lngLat.lat];
|
).coordinates = [e.lngLat.lng, e.lngLat.lat];
|
||||||
|
|
||||||
let waypointSource = get(map)?.getSource(this.fileId + '-waypoints') as
|
let waypointSource = get(map)?.getSource(this.fileId + '-waypoints') as
|
||||||
| GeoJSONSource
|
| mapboxgl.GeoJSONSource
|
||||||
| undefined;
|
| undefined;
|
||||||
if (waypointSource) {
|
if (waypointSource) {
|
||||||
waypointSource.setData(this.currentWaypointData!);
|
waypointSource.setData(this.currentWaypointData!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
waypointLayerOnMouseUp(e: MapLayerMouseEvent | MapLayerTouchEvent) {
|
waypointLayerOnMouseUp(e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) {
|
||||||
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
|
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
|
||||||
|
|
||||||
const _map = get(map);
|
get(map)?.off('mousemove', this.waypointLayerOnMouseMoveBinded);
|
||||||
if (!_map) {
|
get(map)?.off('touchmove', this.waypointLayerOnMouseMoveBinded);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_map.dragPan.enable();
|
|
||||||
|
|
||||||
_map.off('mousemove', this.waypointLayerOnMouseMoveBinded);
|
|
||||||
_map.off('touchmove', this.waypointLayerOnMouseMoveBinded);
|
|
||||||
|
|
||||||
if (this.draggedWaypointIndex === null) {
|
if (this.draggedWaypointIndex === null) {
|
||||||
return;
|
return;
|
||||||
@@ -783,7 +750,20 @@ export class GPXLayer {
|
|||||||
|
|
||||||
symbols.forEach((symbol) => {
|
symbols.forEach((symbol) => {
|
||||||
const iconId = `waypoint-${symbol ?? 'default'}-${this.layerColor}`;
|
const iconId = `waypoint-${symbol ?? 'default'}-${this.layerColor}`;
|
||||||
loadSVGIcon(_map, iconId, getSvgForSymbol(symbol, this.layerColor));
|
if (!_map.hasImage(iconId)) {
|
||||||
|
let icon = new Image(100, 100);
|
||||||
|
icon.onload = () => {
|
||||||
|
if (!_map.hasImage(iconId)) {
|
||||||
|
_map.addImage(iconId, icon);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lucide icons are SVG files with a 24x24 viewBox
|
||||||
|
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
|
||||||
|
icon.src =
|
||||||
|
'data:image/svg+xml,' +
|
||||||
|
encodeURIComponent(getSvgForSymbol(symbol, this.layerColor));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,30 @@
|
|||||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
||||||
import { gpxStatistics, hoveredPoint, slicedGPXStatistics } from '$lib/logic/statistics';
|
import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
|
||||||
import type { GeoJSONSource } from 'maplibre-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { map } from '$lib/components/map/map';
|
import { map } from '$lib/components/map/map';
|
||||||
import { allHidden } from '$lib/logic/hidden';
|
import { allHidden } from '$lib/logic/hidden';
|
||||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
|
||||||
import { loadSVGIcon } from '$lib/utils';
|
|
||||||
|
|
||||||
const startMarkerSVG = `<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="8" cy="8" r="6" fill="#22c55e" stroke="white" stroke-width="1.5"/>
|
|
||||||
</svg>`;
|
|
||||||
|
|
||||||
const endMarkerSVG = `<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<defs>
|
|
||||||
<pattern id="checkerboard" x="0" y="0" width="5" height="5" patternUnits="userSpaceOnUse">
|
|
||||||
<rect x="0" y="0" width="2.5" height="2.5" fill="white"/>
|
|
||||||
<rect x="2.5" y="2.5" width="2.5" height="2.5" fill="white"/>
|
|
||||||
<rect x="2.5" y="0" width="2.5" height="2.5" fill="black"/>
|
|
||||||
<rect x="0" y="2.5" width="2.5" height="2.5" fill="black"/>
|
|
||||||
</pattern>
|
|
||||||
</defs>
|
|
||||||
<circle cx="8" cy="8" r="6" fill="url(#checkerboard)" stroke="white" stroke-width="1.5"/>
|
|
||||||
</svg>`;
|
|
||||||
const hoverMarkerSVG = `<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="8" cy="8" r="6" fill="#00b8db" stroke="white" stroke-width="1.5"/>
|
|
||||||
</svg>`;
|
|
||||||
|
|
||||||
export class StartEndMarkers {
|
export class StartEndMarkers {
|
||||||
|
start: mapboxgl.Marker;
|
||||||
|
end: mapboxgl.Marker;
|
||||||
updateBinded: () => void = this.update.bind(this);
|
updateBinded: () => void = this.update.bind(this);
|
||||||
unsubscribes: (() => void)[] = [];
|
unsubscribes: (() => void)[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
let startElement = document.createElement('div');
|
||||||
|
let endElement = document.createElement('div');
|
||||||
|
startElement.className = `h-4 w-4 rounded-full bg-green-500 border-2 border-white`;
|
||||||
|
endElement.className = `h-4 w-4 rounded-full border-2 border-white`;
|
||||||
|
endElement.style.background =
|
||||||
|
'repeating-conic-gradient(#fff 0 90deg, #000 0 180deg) 0 0/8px 8px round';
|
||||||
|
|
||||||
|
this.start = new mapboxgl.Marker({ element: startElement });
|
||||||
|
this.end = new mapboxgl.Marker({ element: endElement });
|
||||||
|
|
||||||
map.onLoad(() => this.update());
|
map.onLoad(() => this.update());
|
||||||
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
|
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
|
||||||
this.unsubscribes.push(slicedGPXStatistics.subscribe(this.updateBinded));
|
this.unsubscribes.push(slicedGPXStatistics.subscribe(this.updateBinded));
|
||||||
this.unsubscribes.push(hoveredPoint.subscribe(this.updateBinded));
|
|
||||||
this.unsubscribes.push(currentTool.subscribe(this.updateBinded));
|
this.unsubscribes.push(currentTool.subscribe(this.updateBinded));
|
||||||
this.unsubscribes.push(allHidden.subscribe(this.updateBinded));
|
this.unsubscribes.push(allHidden.subscribe(this.updateBinded));
|
||||||
}
|
}
|
||||||
@@ -43,113 +33,33 @@ export class StartEndMarkers {
|
|||||||
const map_ = get(map);
|
const map_ = get(map);
|
||||||
if (!map_) return;
|
if (!map_) return;
|
||||||
|
|
||||||
this.loadIcons();
|
|
||||||
|
|
||||||
const tool = get(currentTool);
|
const tool = get(currentTool);
|
||||||
const statistics = get(gpxStatistics);
|
const statistics = get(gpxStatistics);
|
||||||
const slicedStatistics = get(slicedGPXStatistics);
|
const slicedStatistics = get(slicedGPXStatistics);
|
||||||
const hovered = get(hoveredPoint);
|
|
||||||
const hidden = get(allHidden);
|
const hidden = get(allHidden);
|
||||||
if (statistics.global.length > 0 && tool !== Tool.ROUTING && !hidden) {
|
if (statistics.global.length > 0 && tool !== Tool.ROUTING && !hidden) {
|
||||||
const start = statistics
|
this.start
|
||||||
.getTrackPoint(slicedStatistics?.[1] ?? 0)!
|
.setLngLat(
|
||||||
.trkpt.getCoordinates();
|
statistics.getTrackPoint(slicedStatistics?.[1] ?? 0)!.trkpt.getCoordinates()
|
||||||
const end = statistics
|
)
|
||||||
.getTrackPoint(slicedStatistics?.[2] ?? statistics.global.length - 1)!
|
.addTo(map_);
|
||||||
.trkpt.getCoordinates();
|
this.end
|
||||||
const data: GeoJSON.FeatureCollection = {
|
.setLngLat(
|
||||||
type: 'FeatureCollection',
|
statistics
|
||||||
features: [
|
.getTrackPoint(slicedStatistics?.[2] ?? statistics.global.length - 1)!
|
||||||
{
|
.trkpt.getCoordinates()
|
||||||
type: 'Feature',
|
)
|
||||||
geometry: {
|
.addTo(map_);
|
||||||
type: 'Point',
|
|
||||||
coordinates: [start.lon, start.lat],
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
icon: 'start-marker',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 {
|
} else {
|
||||||
if (map_.getLayer('start-end-markers')) {
|
this.start.remove();
|
||||||
map_.removeLayer('start-end-markers');
|
this.end.remove();
|
||||||
}
|
|
||||||
if (map_.getSource('start-end-markers')) {
|
|
||||||
map_.removeSource('start-end-markers');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
|
||||||
const map_ = get(map);
|
this.start.remove();
|
||||||
if (!map_) return;
|
this.end.remove();
|
||||||
|
|
||||||
if (map_.getLayer('start-end-markers')) {
|
|
||||||
map_.removeLayer('start-end-markers');
|
|
||||||
}
|
|
||||||
if (map_.getSource('start-end-markers')) {
|
|
||||||
map_.removeSource('start-end-markers');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadIcons() {
|
|
||||||
const map_ = get(map);
|
|
||||||
if (!map_) return;
|
|
||||||
loadSVGIcon(map_, 'start-marker', startMarkerSVG);
|
|
||||||
loadSVGIcon(map_, 'end-marker', endMarkerSVG);
|
|
||||||
loadSVGIcon(map_, 'hover-marker', hoverMarkerSVG);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,9 @@
|
|||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
import { defaultBasemap, type CustomLayer } from '$lib/assets/layers';
|
import { defaultBasemap, type CustomLayer } from '$lib/assets/layers';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { remove } from './utils';
|
import { customBasemapUpdate, isSelected, remove } from './utils';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
|
import { map } from '$lib/components/map/map';
|
||||||
import { dndzone } from 'svelte-dnd-action';
|
import { dndzone } from 'svelte-dnd-action';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -41,8 +42,13 @@
|
|||||||
let maxZoom: number = $state(20);
|
let maxZoom: number = $state(20);
|
||||||
let layerType: 'basemap' | 'overlay' = $state('basemap');
|
let layerType: 'basemap' | 'overlay' = $state('basemap');
|
||||||
let resourceType: 'raster' | 'vector' = $derived.by(() => {
|
let resourceType: 'raster' | 'vector' = $derived.by(() => {
|
||||||
if (tileUrls[0].length > 0 && tileUrls[0].includes('.json')) {
|
if (tileUrls[0].length > 0) {
|
||||||
return 'vector';
|
if (
|
||||||
|
tileUrls[0].includes('.json') ||
|
||||||
|
(tileUrls[0].includes('api.mapbox.com/styles') && !tileUrls[0].includes('tiles'))
|
||||||
|
) {
|
||||||
|
return 'vector';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 'raster';
|
return 'raster';
|
||||||
});
|
});
|
||||||
@@ -128,8 +134,8 @@
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
addLayer(layerId);
|
|
||||||
$customLayers[layerId] = layer;
|
$customLayers[layerId] = layer;
|
||||||
|
addLayer(layerId);
|
||||||
selectedLayerId = undefined;
|
selectedLayerId = undefined;
|
||||||
setDataFromSelectedLayer();
|
setDataFromSelectedLayer();
|
||||||
}
|
}
|
||||||
@@ -152,7 +158,9 @@
|
|||||||
return $tree;
|
return $tree;
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($currentBasemap !== layerId) {
|
if ($currentBasemap === layerId) {
|
||||||
|
$customBasemapUpdate++;
|
||||||
|
} else {
|
||||||
$currentBasemap = layerId;
|
$currentBasemap = layerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +176,14 @@
|
|||||||
return $tree;
|
return $tree;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if ($map && $currentOverlays && isSelected($currentOverlays, layerId)) {
|
||||||
|
try {
|
||||||
|
$map.removeImport(layerId);
|
||||||
|
} catch (e) {
|
||||||
|
// No reliable way to check if the map is ready to remove sources and layers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
currentOverlays.update(($overlays) => {
|
currentOverlays.update(($overlays) => {
|
||||||
if (!$overlays.overlays.hasOwnProperty('custom')) {
|
if (!$overlays.overlays.hasOwnProperty('custom')) {
|
||||||
$overlays.overlays['custom'] = {};
|
$overlays.overlays['custom'] = {};
|
||||||
|
|||||||
@@ -5,8 +5,12 @@
|
|||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||||
import { Layers } from '@lucide/svelte';
|
import { Layers } from '@lucide/svelte';
|
||||||
|
import { basemaps, defaultBasemap, overlays } from '$lib/assets/layers';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { map } from '$lib/components/map/map';
|
import { map } from '$lib/components/map/map';
|
||||||
|
import { customBasemapUpdate, getLayers } from './utils';
|
||||||
|
import type { ImportSpecification, StyleSpecification } from 'mapbox-gl';
|
||||||
|
import { untrack } from 'svelte';
|
||||||
|
|
||||||
let container: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
let overpassLayer: OverpassLayer;
|
let overpassLayer: OverpassLayer;
|
||||||
@@ -19,14 +23,125 @@
|
|||||||
selectedBasemapTree,
|
selectedBasemapTree,
|
||||||
selectedOverlayTree,
|
selectedOverlayTree,
|
||||||
selectedOverpassTree,
|
selectedOverpassTree,
|
||||||
|
customLayers,
|
||||||
|
opacities,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
map.onLoad((_map: maplibregl.Map) => {
|
function setStyle() {
|
||||||
|
if (!$map) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let basemap = basemaps.hasOwnProperty($currentBasemap)
|
||||||
|
? basemaps[$currentBasemap]
|
||||||
|
: ($customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap]);
|
||||||
|
$map.removeImport('basemap');
|
||||||
|
if (typeof basemap === 'string') {
|
||||||
|
$map.addImport({ id: 'basemap', url: basemap }, 'overlays');
|
||||||
|
} else {
|
||||||
|
$map.addImport(
|
||||||
|
{
|
||||||
|
id: 'basemap',
|
||||||
|
url: '',
|
||||||
|
data: basemap as StyleSpecification,
|
||||||
|
},
|
||||||
|
'overlays'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if ($map && ($currentBasemap || $customBasemapUpdate)) {
|
||||||
|
untrack(() => setStyle());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function addOverlay(id: string) {
|
||||||
|
if (!$map) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id];
|
||||||
|
if (typeof overlay === 'string') {
|
||||||
|
$map.addImport({ id, url: overlay });
|
||||||
|
} else {
|
||||||
|
if ($opacities.hasOwnProperty(id)) {
|
||||||
|
overlay = {
|
||||||
|
...overlay,
|
||||||
|
layers: (overlay as StyleSpecification).layers.map((layer) => {
|
||||||
|
if (layer.type === 'raster') {
|
||||||
|
if (!layer.paint) {
|
||||||
|
layer.paint = {};
|
||||||
|
}
|
||||||
|
layer.paint['raster-opacity'] = $opacities[id];
|
||||||
|
}
|
||||||
|
return layer;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
$map.addImport({
|
||||||
|
id,
|
||||||
|
url: '',
|
||||||
|
data: overlay as StyleSpecification,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// No reliable way to check if the map is ready to add sources and layers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOverlays() {
|
||||||
|
if ($map && $currentOverlays && $opacities) {
|
||||||
|
let overlayLayers = getLayers($currentOverlays);
|
||||||
|
try {
|
||||||
|
let activeOverlays =
|
||||||
|
$map
|
||||||
|
.getStyle()
|
||||||
|
.imports?.reduce(
|
||||||
|
(
|
||||||
|
acc: Record<string, ImportSpecification>,
|
||||||
|
imprt: ImportSpecification
|
||||||
|
) => {
|
||||||
|
if (!['basemap', 'overlays'].includes(imprt.id)) {
|
||||||
|
acc[imprt.id] = imprt;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
) || {};
|
||||||
|
let toRemove = Object.keys(activeOverlays).filter((id) => !overlayLayers[id]);
|
||||||
|
toRemove.forEach((id) => {
|
||||||
|
$map?.removeImport(id);
|
||||||
|
});
|
||||||
|
let toAdd = Object.entries(overlayLayers)
|
||||||
|
.filter(([id, selected]) => selected && !activeOverlays.hasOwnProperty(id))
|
||||||
|
.map(([id]) => id);
|
||||||
|
toAdd.forEach((id) => {
|
||||||
|
addOverlay(id);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// No reliable way to check if the map is ready to add sources and layers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if ($map && $currentOverlays && $opacities) {
|
||||||
|
untrack(() => updateOverlays());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
map.onLoad((_map: mapboxgl.Map) => {
|
||||||
if (overpassLayer) {
|
if (overpassLayer) {
|
||||||
overpassLayer.remove();
|
overpassLayer.remove();
|
||||||
}
|
}
|
||||||
overpassLayer = new OverpassLayer(_map, map.layerEventManager!);
|
overpassLayer = new OverpassLayer(_map);
|
||||||
overpassLayer.add();
|
overpassLayer.add();
|
||||||
|
let first = true;
|
||||||
|
_map.on('style.import.load', () => {
|
||||||
|
if (!first) return;
|
||||||
|
first = false;
|
||||||
|
updateOverlays();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
let open = $state(false);
|
let open = $state(false);
|
||||||
|
|||||||
@@ -167,11 +167,11 @@
|
|||||||
{#if isSelected($selectedOverlayTree, selectedOverlay)}
|
{#if isSelected($selectedOverlayTree, selectedOverlay)}
|
||||||
{#if $isLayerFromExtension(selectedOverlay)}
|
{#if $isLayerFromExtension(selectedOverlay)}
|
||||||
{$getLayerName(selectedOverlay)}
|
{$getLayerName(selectedOverlay)}
|
||||||
{:else if $customLayers.hasOwnProperty(selectedOverlay)}
|
|
||||||
{$customLayers[selectedOverlay].name}
|
|
||||||
{:else}
|
{:else}
|
||||||
{i18n._(`layers.label.${selectedOverlay}`)}
|
{i18n._(`layers.label.${selectedOverlay}`)}
|
||||||
{/if}
|
{/if}
|
||||||
|
{:else if $customLayers.hasOwnProperty(selectedOverlay)}
|
||||||
|
{$customLayers[selectedOverlay].name}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
@@ -213,9 +213,7 @@
|
|||||||
isSelected($currentOverlays, selectedOverlay)
|
isSelected($currentOverlays, selectedOverlay)
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
if ($map.getLayer(selectedOverlay)) {
|
$map.removeImport(selectedOverlay);
|
||||||
$map.removeLayer(selectedOverlay);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// No reliable way to check if the map is ready to remove sources and layers
|
// No reliable way to check if the map is ready to remove sources and layers
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export class ExtensionAPI {
|
|||||||
if (current && isSelected(current, overlay.id)) {
|
if (current && isSelected(current, overlay.id)) {
|
||||||
show = true;
|
show = true;
|
||||||
try {
|
try {
|
||||||
get(map)?.removeLayer(overlay.id);
|
get(map)?.removeImport(overlay.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// No reliable way to check if the map is ready to remove sources and layers
|
// No reliable way to check if the map is ready to remove sources and layers
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ import { overpassQueryData } from '$lib/assets/layers';
|
|||||||
import { MapPopup } from '$lib/components/map/map-popup';
|
import { MapPopup } from '$lib/components/map/map-popup';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { db } from '$lib/db';
|
import { db } from '$lib/db';
|
||||||
import type { GeoJSONSource } from 'maplibre-gl';
|
import { ANCHOR_LAYER_KEY } from '$lib/components/map/map';
|
||||||
import { ANCHOR_LAYER_KEY } from '../style';
|
|
||||||
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
|
||||||
import { loadSVGIcon } from '$lib/utils';
|
|
||||||
|
|
||||||
const { currentOverpassQueries } = settings;
|
const { currentOverpassQueries } = settings;
|
||||||
|
|
||||||
@@ -24,12 +21,11 @@ liveQuery(() => db.overpassdata.toArray()).subscribe((pois) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export class OverpassLayer {
|
export class OverpassLayer {
|
||||||
overpassUrl = 'https://maps.mail.ru/osm/tools/overpass/api/interpreter';
|
overpassUrl = 'https://overpass.private.coffee/api/interpreter';
|
||||||
minZoom = 12;
|
minZoom = 12;
|
||||||
queryZoom = 12;
|
queryZoom = 12;
|
||||||
expirationTime = 7 * 24 * 3600 * 1000;
|
expirationTime = 7 * 24 * 3600 * 1000;
|
||||||
map: maplibregl.Map;
|
map: mapboxgl.Map;
|
||||||
layerEventManager: MapLayerEventManager;
|
|
||||||
popup: MapPopup;
|
popup: MapPopup;
|
||||||
|
|
||||||
currentQueries: Set<string> = new Set();
|
currentQueries: Set<string> = new Set();
|
||||||
@@ -40,9 +36,8 @@ export class OverpassLayer {
|
|||||||
updateBinded = this.update.bind(this);
|
updateBinded = this.update.bind(this);
|
||||||
onHoverBinded = this.onHover.bind(this);
|
onHoverBinded = this.onHover.bind(this);
|
||||||
|
|
||||||
constructor(map: maplibregl.Map, layerEventManager: MapLayerEventManager) {
|
constructor(map: mapboxgl.Map) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
this.layerEventManager = layerEventManager;
|
|
||||||
this.popup = new MapPopup(map, {
|
this.popup = new MapPopup(map, {
|
||||||
closeButton: false,
|
closeButton: false,
|
||||||
focusAfterOpen: false,
|
focusAfterOpen: false,
|
||||||
@@ -53,7 +48,7 @@ export class OverpassLayer {
|
|||||||
|
|
||||||
add() {
|
add() {
|
||||||
this.map.on('moveend', this.queryIfNeededBinded);
|
this.map.on('moveend', this.queryIfNeededBinded);
|
||||||
this.map.on('style.load', this.updateBinded);
|
this.map.on('style.import.load', this.updateBinded);
|
||||||
this.unsubscribes.push(data.subscribe(this.updateBinded));
|
this.unsubscribes.push(data.subscribe(this.updateBinded));
|
||||||
this.unsubscribes.push(
|
this.unsubscribes.push(
|
||||||
currentOverpassQueries.subscribe(() => {
|
currentOverpassQueries.subscribe(() => {
|
||||||
@@ -77,17 +72,10 @@ export class OverpassLayer {
|
|||||||
update() {
|
update() {
|
||||||
this.loadIcons();
|
this.loadIcons();
|
||||||
|
|
||||||
const fullData = get(data);
|
let d = get(data);
|
||||||
const queries = getCurrentQueries();
|
|
||||||
const d: GeoJSON.FeatureCollection = {
|
|
||||||
type: 'FeatureCollection',
|
|
||||||
features: fullData.features.filter((feature) =>
|
|
||||||
queries.includes(feature.properties!.query)
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let source = this.map.getSource('overpass') as GeoJSONSource | undefined;
|
let source = this.map.getSource('overpass') as mapboxgl.GeoJSONSource | undefined;
|
||||||
if (source) {
|
if (source) {
|
||||||
source.setData(d);
|
source.setData(d);
|
||||||
} else {
|
} else {
|
||||||
@@ -113,9 +101,13 @@ export class OverpassLayer {
|
|||||||
ANCHOR_LAYER_KEY.overpass
|
ANCHOR_LAYER_KEY.overpass
|
||||||
);
|
);
|
||||||
|
|
||||||
this.layerEventManager.on('mouseenter', 'overpass', this.onHoverBinded);
|
this.map.on('mouseenter', 'overpass', this.onHoverBinded);
|
||||||
this.layerEventManager.on('click', 'overpass', this.onHoverBinded);
|
this.map.on('click', 'overpass', this.onHoverBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.map.setFilter('overpass', ['in', 'query', ...getCurrentQueries()], {
|
||||||
|
validate: false,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// No reliable way to check if the map is ready to add sources and layers
|
// No reliable way to check if the map is ready to add sources and layers
|
||||||
}
|
}
|
||||||
@@ -123,9 +115,7 @@ export class OverpassLayer {
|
|||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.map.off('moveend', this.queryIfNeededBinded);
|
this.map.off('moveend', this.queryIfNeededBinded);
|
||||||
this.map.off('style.load', this.updateBinded);
|
this.map.off('style.import.load', this.updateBinded);
|
||||||
this.layerEventManager.off('mouseenter', 'overpass', this.onHoverBinded);
|
|
||||||
this.layerEventManager.off('click', 'overpass', this.onHoverBinded);
|
|
||||||
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -258,16 +248,27 @@ export class OverpassLayer {
|
|||||||
loadIcons() {
|
loadIcons() {
|
||||||
let currentQueries = getCurrentQueries();
|
let currentQueries = getCurrentQueries();
|
||||||
currentQueries.forEach((query) => {
|
currentQueries.forEach((query) => {
|
||||||
loadSVGIcon(
|
if (!this.map.hasImage(`overpass-${query}`)) {
|
||||||
this.map,
|
let icon = new Image(100, 100);
|
||||||
`overpass-${query}`,
|
icon.onload = () => {
|
||||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
if (!this.map.hasImage(`overpass-${query}`)) {
|
||||||
|
this.map.addImage(`overpass-${query}`, icon);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lucide icons are SVG files with a 24x24 viewBox
|
||||||
|
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
|
||||||
|
icon.src =
|
||||||
|
'data:image/svg+xml,' +
|
||||||
|
encodeURIComponent(`
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
||||||
<circle cx="20" cy="20" r="20" fill="${overpassQueryData[query].icon.color}" />
|
<circle cx="20" cy="20" r="20" fill="${overpassQueryData[query].icon.color}" />
|
||||||
<g transform="translate(8 8)">
|
<g transform="translate(8 8)">
|
||||||
${overpassQueryData[query].icon.svg.replace('stroke="currentColor"', 'stroke="white"')}
|
${overpassQueryData[query].icon.svg.replace('stroke="currentColor"', 'stroke="white"')}
|
||||||
</g>
|
</g>
|
||||||
</svg>`
|
</svg>
|
||||||
);
|
`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,3 +76,5 @@ export function removeAll(node: LayerTreeType, ids: string[]) {
|
|||||||
});
|
});
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const customBasemapUpdate = writable(0);
|
||||||
|
|||||||
@@ -1,287 +0,0 @@
|
|||||||
import { fileStateCollection } from '$lib/logic/file-state';
|
|
||||||
import maplibregl from 'maplibre-gl';
|
|
||||||
|
|
||||||
type MapLayerMouseEventListener = (e: maplibregl.MapLayerMouseEvent) => void;
|
|
||||||
type MapLayerTouchEventListener = (e: maplibregl.MapLayerTouchEvent) => void;
|
|
||||||
type MapLayerListener = {
|
|
||||||
features: maplibregl.MapGeoJSONFeature[];
|
|
||||||
mousemoves: MapLayerMouseEventListener[];
|
|
||||||
mouseenters: MapLayerMouseEventListener[];
|
|
||||||
mouseleaves: MapLayerMouseEventListener[];
|
|
||||||
mousedowns: MapLayerMouseEventListener[];
|
|
||||||
clicks: MapLayerMouseEventListener[];
|
|
||||||
contextmenus: MapLayerMouseEventListener[];
|
|
||||||
touchstarts: MapLayerTouchEventListener[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export class MapLayerEventManager {
|
|
||||||
private _map: maplibregl.Map;
|
|
||||||
private _listeners: Record<string, MapLayerListener> = {};
|
|
||||||
|
|
||||||
constructor(map: maplibregl.Map) {
|
|
||||||
this._map = map;
|
|
||||||
this._map.on('mousemove', this._handleMouseMove.bind(this));
|
|
||||||
this._map.on('click', this._handleMouseClick.bind(this, 'click'));
|
|
||||||
this._map.on('contextmenu', this._handleMouseClick.bind(this, 'contextmenu'));
|
|
||||||
this._map.on('mousedown', this._handleMouseClick.bind(this, 'mousedown'));
|
|
||||||
this._map.on('touchstart', this._handleTouchStart.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
on(
|
|
||||||
eventType:
|
|
||||||
| 'mousemove'
|
|
||||||
| 'mouseenter'
|
|
||||||
| 'mouseleave'
|
|
||||||
| 'mousedown'
|
|
||||||
| 'click'
|
|
||||||
| 'contextmenu'
|
|
||||||
| 'touchstart',
|
|
||||||
|
|
||||||
layerId: string,
|
|
||||||
listener: MapLayerMouseEventListener | MapLayerTouchEventListener
|
|
||||||
) {
|
|
||||||
if (!this._listeners[layerId]) {
|
|
||||||
this._listeners[layerId] = {
|
|
||||||
features: [],
|
|
||||||
mousemoves: [],
|
|
||||||
mouseenters: [],
|
|
||||||
mouseleaves: [],
|
|
||||||
mousedowns: [],
|
|
||||||
clicks: [],
|
|
||||||
contextmenus: [],
|
|
||||||
touchstarts: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
switch (eventType) {
|
|
||||||
case 'mousemove':
|
|
||||||
this._listeners[layerId].mousemoves.push(listener as MapLayerMouseEventListener);
|
|
||||||
break;
|
|
||||||
case 'mouseenter':
|
|
||||||
this._listeners[layerId].mouseenters.push(listener as MapLayerMouseEventListener);
|
|
||||||
break;
|
|
||||||
case 'mouseleave':
|
|
||||||
this._listeners[layerId].mouseleaves.push(listener as MapLayerMouseEventListener);
|
|
||||||
break;
|
|
||||||
case 'mousedown':
|
|
||||||
this._listeners[layerId].mousedowns.push(listener as MapLayerMouseEventListener);
|
|
||||||
break;
|
|
||||||
case 'click':
|
|
||||||
this._listeners[layerId].clicks.push(listener as MapLayerMouseEventListener);
|
|
||||||
break;
|
|
||||||
case 'contextmenu':
|
|
||||||
this._listeners[layerId].contextmenus.push(listener as MapLayerMouseEventListener);
|
|
||||||
break;
|
|
||||||
case 'touchstart':
|
|
||||||
this._listeners[layerId].touchstarts.push(listener as MapLayerTouchEventListener);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
off(
|
|
||||||
eventType:
|
|
||||||
| 'mousemove'
|
|
||||||
| 'mouseenter'
|
|
||||||
| 'mouseleave'
|
|
||||||
| 'mousedown'
|
|
||||||
| 'click'
|
|
||||||
| 'contextmenu'
|
|
||||||
| 'touchstart',
|
|
||||||
layerId: string,
|
|
||||||
listener: MapLayerMouseEventListener | MapLayerTouchEventListener
|
|
||||||
) {
|
|
||||||
if (this._listeners[layerId]) {
|
|
||||||
switch (eventType) {
|
|
||||||
case 'mousemove':
|
|
||||||
this._listeners[layerId].mousemoves = this._listeners[
|
|
||||||
layerId
|
|
||||||
].mousemoves.filter((l) => l !== listener);
|
|
||||||
break;
|
|
||||||
case 'mouseenter':
|
|
||||||
this._listeners[layerId].mouseenters = this._listeners[
|
|
||||||
layerId
|
|
||||||
].mouseenters.filter((l) => l !== listener);
|
|
||||||
break;
|
|
||||||
case 'mouseleave':
|
|
||||||
this._listeners[layerId].mouseleaves = this._listeners[
|
|
||||||
layerId
|
|
||||||
].mouseleaves.filter((l) => l !== listener);
|
|
||||||
break;
|
|
||||||
case 'mousedown':
|
|
||||||
this._listeners[layerId].mousedowns = this._listeners[
|
|
||||||
layerId
|
|
||||||
].mousedowns.filter((l) => l !== listener);
|
|
||||||
break;
|
|
||||||
case 'click':
|
|
||||||
this._listeners[layerId].clicks = this._listeners[layerId].clicks.filter(
|
|
||||||
(l) => l !== listener
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'contextmenu':
|
|
||||||
this._listeners[layerId].contextmenus = this._listeners[
|
|
||||||
layerId
|
|
||||||
].contextmenus.filter((l) => l !== listener);
|
|
||||||
break;
|
|
||||||
case 'touchstart':
|
|
||||||
this._listeners[layerId].touchstarts = this._listeners[
|
|
||||||
layerId
|
|
||||||
].touchstarts.filter((l) => l !== listener);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
this._listeners[layerId].mousemoves.length === 0 &&
|
|
||||||
this._listeners[layerId].mouseenters.length === 0 &&
|
|
||||||
this._listeners[layerId].mouseleaves.length === 0 &&
|
|
||||||
this._listeners[layerId].mousedowns.length === 0 &&
|
|
||||||
this._listeners[layerId].clicks.length === 0 &&
|
|
||||||
this._listeners[layerId].contextmenus.length === 0 &&
|
|
||||||
this._listeners[layerId].touchstarts.length === 0
|
|
||||||
) {
|
|
||||||
delete this._listeners[layerId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleMouseMove(e: maplibregl.MapMouseEvent) {
|
|
||||||
const 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);
|
|
||||||
});
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
listener.features = features;
|
|
||||||
}
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleMouseClick(type: string, e: maplibregl.MapMouseEvent) {
|
|
||||||
Object.values(this._listeners).forEach((listener) => {
|
|
||||||
if (listener.features.length > 0) {
|
|
||||||
if (type === 'click' && listener.clicks.length > 0) {
|
|
||||||
const event = new maplibregl.MapMouseEvent('click', e.target, e.originalEvent, {
|
|
||||||
features: listener.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: listener.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: listener.features,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
listener.mousedowns.forEach((l) => l(event));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleTouchStart(e: maplibregl.MapTouchEvent) {
|
|
||||||
const layerIds = this._filterLayersIntersectingBounds(
|
|
||||||
Object.keys(this._listeners).filter(
|
|
||||||
(layerId) => this._listeners[layerId].touchstarts.length > 0
|
|
||||||
),
|
|
||||||
this._getBounds(e.point)
|
|
||||||
);
|
|
||||||
if (layerIds.length === 0) return;
|
|
||||||
const features = this._map.queryRenderedFeatures(e.points[0], { 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);
|
|
||||||
});
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TrackPoint, Waypoint } from 'gpx';
|
import { TrackPoint, Waypoint } from 'gpx';
|
||||||
import maplibregl from 'maplibre-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { mount, tick, unmount } from 'svelte';
|
import { mount, tick, unmount } from 'svelte';
|
||||||
import { get, writable, type Writable } from 'svelte/store';
|
import { get, writable, type Writable } from 'svelte/store';
|
||||||
import MapPopupComponent from '$lib/components/map/MapPopup.svelte';
|
import MapPopupComponent from '$lib/components/map/MapPopup.svelte';
|
||||||
@@ -11,15 +11,15 @@ export type PopupItem<T = Waypoint | TrackPoint | any> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class MapPopup {
|
export class MapPopup {
|
||||||
map: maplibregl.Map;
|
map: mapboxgl.Map;
|
||||||
popup: maplibregl.Popup;
|
popup: mapboxgl.Popup;
|
||||||
item: Writable<PopupItem | null> = writable(null);
|
item: Writable<PopupItem | null> = writable(null);
|
||||||
component: ReturnType<typeof mount>;
|
component: ReturnType<typeof mount>;
|
||||||
maybeHideBinded = this.maybeHide.bind(this);
|
maybeHideBinded = this.maybeHide.bind(this);
|
||||||
|
|
||||||
constructor(map: maplibregl.Map, options?: maplibregl.PopupOptions) {
|
constructor(map: mapboxgl.Map, options?: mapboxgl.PopupOptions) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
this.popup = new maplibregl.Popup(options);
|
this.popup = new mapboxgl.Popup(options);
|
||||||
this.component = mount(MapPopupComponent, {
|
this.component = mount(MapPopupComponent, {
|
||||||
target: document.body,
|
target: document.body,
|
||||||
props: {
|
props: {
|
||||||
@@ -51,7 +51,7 @@ export class MapPopup {
|
|||||||
this.map.on('mousemove', this.maybeHideBinded);
|
this.map.on('mousemove', this.maybeHideBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
maybeHide(e: maplibregl.MapMouseEvent) {
|
maybeHide(e: mapboxgl.MapMouseEvent) {
|
||||||
const item = get(this.item);
|
const item = get(this.item);
|
||||||
if (item === null) {
|
if (item === null) {
|
||||||
this.hide();
|
this.hide();
|
||||||
@@ -75,10 +75,10 @@ export class MapPopup {
|
|||||||
getCoordinates() {
|
getCoordinates() {
|
||||||
const item = get(this.item);
|
const item = get(this.item);
|
||||||
if (item === null) {
|
if (item === null) {
|
||||||
return new maplibregl.LngLat(0, 0);
|
return new mapboxgl.LngLat(0, 0);
|
||||||
}
|
}
|
||||||
return item.item instanceof Waypoint || item.item instanceof TrackPoint
|
return item.item instanceof Waypoint || item.item instanceof TrackPoint
|
||||||
? item.item.getCoordinates()
|
? item.item.getCoordinates()
|
||||||
: new maplibregl.LngLat(item.item.lon, item.item.lat);
|
: new mapboxgl.LngLat(item.item.lon, item.item.lat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,80 +1,110 @@
|
|||||||
import maplibregl from 'maplibre-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
|
||||||
import MaplibreGeocoder, {
|
|
||||||
type MaplibreGeocoderFeatureResults,
|
|
||||||
} from '@maplibre/maplibre-gl-geocoder';
|
|
||||||
import '@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css';
|
|
||||||
import { get, writable, type Writable } from 'svelte/store';
|
import { get, writable, type Writable } from 'svelte/store';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import { ANCHOR_LAYER_KEY, StyleManager } from '$lib/components/map/style';
|
import { terrainSources } from '$lib/assets/layers';
|
||||||
import { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
|
||||||
|
|
||||||
const { treeFileView, elevationProfile, bottomPanelSize, rightPanelSize, distanceUnits } = settings;
|
const {
|
||||||
|
treeFileView,
|
||||||
|
elevationProfile,
|
||||||
|
bottomPanelSize,
|
||||||
|
rightPanelSize,
|
||||||
|
distanceUnits,
|
||||||
|
terrainSource,
|
||||||
|
} = settings;
|
||||||
|
|
||||||
let fitBoundsOptions: maplibregl.MapOptions['fitBoundsOptions'] = {
|
let fitBoundsOptions: mapboxgl.MapOptions['fitBoundsOptions'] = {
|
||||||
maxZoom: 15,
|
maxZoom: 15,
|
||||||
linear: true,
|
linear: true,
|
||||||
easing: () => 1,
|
easing: () => 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class MapLibreGLMap {
|
const emptySource: mapboxgl.GeoJSONSourceSpecification = {
|
||||||
private _maptilerKey: string = '';
|
type: 'geojson',
|
||||||
private _map: maplibregl.Map | null = null;
|
data: {
|
||||||
private _mapStore: Writable<maplibregl.Map | null> = writable(null);
|
type: 'FeatureCollection',
|
||||||
private _styleManager: StyleManager | null = null;
|
features: [],
|
||||||
private _onLoadCallbacks: ((map: maplibregl.Map) => void)[] = [];
|
},
|
||||||
private _unsubscribes: (() => void)[] = [];
|
};
|
||||||
private callOnLoadBinded: () => void = this.callOnLoad.bind(this);
|
export const ANCHOR_LAYER_KEY = {
|
||||||
public layerEventManager: MapLayerEventManager | null = null;
|
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',
|
||||||
|
}));
|
||||||
|
|
||||||
subscribe(run: (value: maplibregl.Map | null) => void, invalidate?: () => void) {
|
export class MapboxGLMap {
|
||||||
return this._mapStore.subscribe(run, invalidate);
|
private _map: Writable<mapboxgl.Map | null> = writable(null);
|
||||||
|
private _onLoadCallbacks: ((map: mapboxgl.Map) => void)[] = [];
|
||||||
|
private _unsubscribes: (() => void)[] = [];
|
||||||
|
|
||||||
|
subscribe(run: (value: mapboxgl.Map | null) => void, invalidate?: () => void) {
|
||||||
|
return this._map.subscribe(run, invalidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
init(
|
init(language: string, hash: boolean, geocoder: boolean, geolocate: boolean) {
|
||||||
maptilerKey: string,
|
const map = new mapboxgl.Map({
|
||||||
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',
|
container: 'map',
|
||||||
style: {
|
style: {
|
||||||
version: 8,
|
version: 8,
|
||||||
projection: {
|
sources: {
|
||||||
type: 'globe',
|
'empty-source': emptySource,
|
||||||
},
|
},
|
||||||
sources: {},
|
layers: anchorLayers,
|
||||||
layers: [],
|
imports: [
|
||||||
|
{
|
||||||
|
id: 'basemap',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'overlays',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
projection: 'globe',
|
||||||
zoom: 0,
|
zoom: 0,
|
||||||
hash: hash,
|
hash: hash,
|
||||||
|
language,
|
||||||
|
attributionControl: false,
|
||||||
|
logoPosition: 'bottom-right',
|
||||||
boxZoom: false,
|
boxZoom: false,
|
||||||
maxPitch: 85,
|
|
||||||
});
|
});
|
||||||
this.layerEventManager = new MapLayerEventManager(map);
|
|
||||||
map.addControl(
|
map.addControl(
|
||||||
new maplibregl.NavigationControl({
|
new mapboxgl.AttributionControl({
|
||||||
|
compact: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
map.addControl(
|
||||||
|
new mapboxgl.NavigationControl({
|
||||||
visualizePitch: true,
|
visualizePitch: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
if (geocoder) {
|
if (geocoder) {
|
||||||
let geocoder = new MaplibreGeocoder(
|
let geocoder = new MapboxGeocoder({
|
||||||
{
|
mapboxgl: mapboxgl,
|
||||||
forwardGeocode: async (config) => {
|
enableEventLogging: false,
|
||||||
const results: MaplibreGeocoderFeatureResults = {
|
collapsed: true,
|
||||||
features: [],
|
flyTo: fitBoundsOptions,
|
||||||
type: 'FeatureCollection',
|
language,
|
||||||
};
|
localGeocoder: () => [],
|
||||||
try {
|
localGeocoderOnly: true,
|
||||||
const request = `https://nominatim.openstreetmap.org/search?format=json&q=${config.query}&limit=5&accept-language=${language}`;
|
externalGeocoder: (query: string) =>
|
||||||
const response = await fetch(request);
|
fetch(
|
||||||
const geojson = await response.json();
|
`https://nominatim.openstreetmap.org/search?format=json&q=${query}&limit=5&accept-language=${language}`
|
||||||
results.features = geojson.map((result: any) => {
|
)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
return data.map((result: any) => {
|
||||||
return {
|
return {
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
geometry: {
|
geometry: {
|
||||||
@@ -84,43 +114,61 @@ export class MapLibreGLMap {
|
|||||||
place_name: result.display_name,
|
place_name: result.display_name,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (e) {}
|
}),
|
||||||
return results;
|
});
|
||||||
},
|
let onKeyDown = geocoder._onKeyDown;
|
||||||
},
|
geocoder._onKeyDown = (e: KeyboardEvent) => {
|
||||||
{
|
// Trigger search on Enter key only
|
||||||
maplibregl: maplibregl,
|
if (e.key === 'Enter') {
|
||||||
enableEventLogging: false,
|
onKeyDown.apply(geocoder, [{ target: geocoder._inputEl }]);
|
||||||
collapsed: true,
|
} else if (geocoder._typeahead.data.length > 0) {
|
||||||
flyTo: fitBoundsOptions,
|
geocoder._typeahead.clear();
|
||||||
language,
|
|
||||||
}
|
}
|
||||||
);
|
};
|
||||||
map.addControl(geocoder);
|
map.addControl(geocoder);
|
||||||
}
|
}
|
||||||
if (geolocate) {
|
if (geolocate) {
|
||||||
map.addControl(
|
map.addControl(
|
||||||
new maplibregl.GeolocateControl({
|
new mapboxgl.GeolocateControl({
|
||||||
positionOptions: {
|
positionOptions: {
|
||||||
enableHighAccuracy: true,
|
enableHighAccuracy: true,
|
||||||
},
|
},
|
||||||
fitBoundsOptions,
|
fitBoundsOptions,
|
||||||
trackUserLocation: true,
|
trackUserLocation: true,
|
||||||
|
showUserHeading: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const scaleControl = new maplibregl.ScaleControl({
|
const scaleControl = new mapboxgl.ScaleControl({
|
||||||
unit: get(distanceUnits),
|
unit: get(distanceUnits),
|
||||||
});
|
});
|
||||||
map.addControl(scaleControl);
|
map.addControl(scaleControl);
|
||||||
|
map.on('style.load', () => {
|
||||||
|
map.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', () => {
|
map.on('load', () => {
|
||||||
this._map = map;
|
this._map.set(map); // only set the store after the map has loaded
|
||||||
this._mapStore.set(map); // only set the store after the map has loaded
|
|
||||||
window._map = map; // entry point for extensions
|
window._map = map; // entry point for extensions
|
||||||
this.resize();
|
this.resize();
|
||||||
|
this.setTerrain();
|
||||||
scaleControl.setUnit(get(distanceUnits));
|
scaleControl.setUnit(get(distanceUnits));
|
||||||
|
|
||||||
|
this._onLoadCallbacks.forEach((callback) => callback(map));
|
||||||
|
this._onLoadCallbacks = [];
|
||||||
});
|
});
|
||||||
map.on('style.load', this.callOnLoadBinded);
|
|
||||||
|
|
||||||
this._unsubscribes.push(treeFileView.subscribe(() => this.resize()));
|
this._unsubscribes.push(treeFileView.subscribe(() => this.resize()));
|
||||||
this._unsubscribes.push(elevationProfile.subscribe(() => this.resize()));
|
this._unsubscribes.push(elevationProfile.subscribe(() => this.resize()));
|
||||||
@@ -131,50 +179,70 @@ export class MapLibreGLMap {
|
|||||||
scaleControl.setUnit(units);
|
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() {
|
destroy() {
|
||||||
if (this._map) {
|
const map = get(this._map);
|
||||||
this._map.remove();
|
if (map) {
|
||||||
this._mapStore.set(null);
|
map.remove();
|
||||||
|
this._map.set(null);
|
||||||
}
|
}
|
||||||
this._unsubscribes.forEach((unsubscribe) => unsubscribe());
|
this._unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
this._unsubscribes = [];
|
this._unsubscribes = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
resize() {
|
resize() {
|
||||||
if (this._map) {
|
const map = get(this._map);
|
||||||
|
if (map) {
|
||||||
tick().then(() => {
|
tick().then(() => {
|
||||||
this._map?.resize();
|
map.resize();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle3D() {
|
toggle3D() {
|
||||||
if (this._map) {
|
const map = get(this._map);
|
||||||
if (this._map.getPitch() === 0) {
|
if (map) {
|
||||||
this._map.easeTo({ pitch: 70 });
|
if (map.getPitch() === 0) {
|
||||||
|
map.easeTo({ pitch: 70 });
|
||||||
} else {
|
} else {
|
||||||
this._map.easeTo({ pitch: 0 });
|
map.easeTo({ pitch: 0 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoad(callback: (map: maplibregl.Map) => void) {
|
setTerrain() {
|
||||||
if (this._map) {
|
const map = get(this._map);
|
||||||
callback(this._map);
|
if (map) {
|
||||||
} else {
|
const source = get(terrainSource);
|
||||||
this._onLoadCallbacks.push(callback);
|
try {
|
||||||
}
|
if (!map.getSource(source)) {
|
||||||
}
|
map.addSource(source, terrainSources[source]);
|
||||||
|
}
|
||||||
callOnLoad() {
|
if (map.getPitch() > 0) {
|
||||||
if (this._map && this._map.getLayer(ANCHOR_LAYER_KEY.overlays)) {
|
map.setTerrain({
|
||||||
this._onLoadCallbacks.forEach((callback) => callback(this._map!));
|
source: source,
|
||||||
this._onLoadCallbacks = [];
|
exaggeration: 1,
|
||||||
this._map.off('style.load', this.callOnLoadBinded);
|
});
|
||||||
|
} else {
|
||||||
|
map.setTerrain(null);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// No reliable way to check if the map is ready to add sources and layers
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const map = new MapLibreGLMap();
|
export const map = new MapboxGLMap();
|
||||||
|
|||||||
@@ -20,14 +20,9 @@
|
|||||||
let container: HTMLElement;
|
let container: HTMLElement;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
map.onLoad((map_: maplibregl.Map) => {
|
map.onLoad((map: mapboxgl.Map) => {
|
||||||
googleRedirect = new GoogleRedirect(map_);
|
googleRedirect = new GoogleRedirect(map);
|
||||||
mapillaryLayer = new MapillaryLayer(
|
mapillaryLayer = new MapillaryLayer(map, container, mapillaryOpen);
|
||||||
map_,
|
|
||||||
map.layerEventManager!,
|
|
||||||
container,
|
|
||||||
mapillaryOpen
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||||
|
import type mapboxgl from 'mapbox-gl';
|
||||||
|
|
||||||
export class GoogleRedirect {
|
export class GoogleRedirect {
|
||||||
map: maplibregl.Map;
|
map: mapboxgl.Map;
|
||||||
enabled = false;
|
enabled = false;
|
||||||
|
|
||||||
constructor(map: maplibregl.Map) {
|
constructor(map: mapboxgl.Map) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ export class GoogleRedirect {
|
|||||||
this.map.off('click', this.openStreetView);
|
this.map.off('click', this.openStreetView);
|
||||||
}
|
}
|
||||||
|
|
||||||
openStreetView(e: maplibregl.MapMouseEvent) {
|
openStreetView(e: mapboxgl.MapMouseEvent) {
|
||||||
window.open(
|
window.open(
|
||||||
`https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${e.lngLat.lat},${e.lngLat.lng}`
|
`https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${e.lngLat.lat},${e.lngLat.lng}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import maplibregl, { type LayerSpecification, type VectorSourceSpecification } from 'maplibre-gl';
|
import mapboxgl, { type LayerSpecification, type VectorSourceSpecification } from 'mapbox-gl';
|
||||||
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
|
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
|
||||||
import 'mapillary-js/dist/mapillary.css';
|
import 'mapillary-js/dist/mapillary.css';
|
||||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||||
import { ANCHOR_LAYER_KEY } from '../style';
|
import { ANCHOR_LAYER_KEY } from '$lib/components/map/map';
|
||||||
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
|
||||||
|
|
||||||
const mapillarySource: VectorSourceSpecification = {
|
const mapillarySource: VectorSourceSpecification = {
|
||||||
type: 'vector',
|
type: 'vector',
|
||||||
@@ -43,9 +42,8 @@ const mapillaryImageLayer: LayerSpecification = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class MapillaryLayer {
|
export class MapillaryLayer {
|
||||||
map: maplibregl.Map;
|
map: mapboxgl.Map;
|
||||||
layerEventManager: MapLayerEventManager;
|
marker: mapboxgl.Marker;
|
||||||
marker: maplibregl.Marker;
|
|
||||||
viewer: Viewer;
|
viewer: Viewer;
|
||||||
|
|
||||||
active = false;
|
active = false;
|
||||||
@@ -55,14 +53,8 @@ export class MapillaryLayer {
|
|||||||
onMouseEnterBinded = this.onMouseEnter.bind(this);
|
onMouseEnterBinded = this.onMouseEnter.bind(this);
|
||||||
onMouseLeaveBinded = this.onMouseLeave.bind(this);
|
onMouseLeaveBinded = this.onMouseLeave.bind(this);
|
||||||
|
|
||||||
constructor(
|
constructor(map: mapboxgl.Map, container: HTMLElement, popupOpen: { value: boolean }) {
|
||||||
map: maplibregl.Map,
|
|
||||||
layerEventManager: MapLayerEventManager,
|
|
||||||
container: HTMLElement,
|
|
||||||
popupOpen: { value: boolean }
|
|
||||||
) {
|
|
||||||
this.map = map;
|
this.map = map;
|
||||||
this.layerEventManager = layerEventManager;
|
|
||||||
|
|
||||||
this.viewer = new Viewer({
|
this.viewer = new Viewer({
|
||||||
accessToken: 'MLY|4381405525255083|3204871ec181638c3c31320490f03011',
|
accessToken: 'MLY|4381405525255083|3204871ec181638c3c31320490f03011',
|
||||||
@@ -70,12 +62,15 @@ export class MapillaryLayer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const element = document.createElement('div');
|
const element = document.createElement('div');
|
||||||
element.className = 'maplibregl-user-location maplibregl-user-location-show-heading';
|
element.className = 'mapboxgl-user-location mapboxgl-user-location-show-heading';
|
||||||
const dot = document.createElement('div');
|
const dot = document.createElement('div');
|
||||||
dot.className = 'maplibregl-user-location-dot';
|
dot.className = 'mapboxgl-user-location-dot';
|
||||||
|
const heading = document.createElement('div');
|
||||||
|
heading.className = 'mapboxgl-user-location-heading';
|
||||||
element.appendChild(dot);
|
element.appendChild(dot);
|
||||||
|
element.appendChild(heading);
|
||||||
|
|
||||||
this.marker = new maplibregl.Marker({
|
this.marker = new mapboxgl.Marker({
|
||||||
rotationAlignment: 'map',
|
rotationAlignment: 'map',
|
||||||
element,
|
element,
|
||||||
});
|
});
|
||||||
@@ -111,14 +106,14 @@ export class MapillaryLayer {
|
|||||||
this.map.addLayer(mapillaryImageLayer, ANCHOR_LAYER_KEY.mapillary);
|
this.map.addLayer(mapillaryImageLayer, ANCHOR_LAYER_KEY.mapillary);
|
||||||
}
|
}
|
||||||
this.map.on('style.load', this.addBinded);
|
this.map.on('style.load', this.addBinded);
|
||||||
this.layerEventManager.on('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
this.map.on('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
||||||
this.layerEventManager.on('mouseleave', 'mapillary-image', this.onMouseLeaveBinded);
|
this.map.on('mouseleave', 'mapillary-image', this.onMouseLeaveBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.map.off('style.load', this.addBinded);
|
this.map.off('style.load', this.addBinded);
|
||||||
this.layerEventManager.off('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
this.map.off('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
||||||
this.layerEventManager.off('mouseleave', 'mapillary-image', this.onMouseLeaveBinded);
|
this.map.off('mouseleave', 'mapillary-image', this.onMouseLeaveBinded);
|
||||||
|
|
||||||
if (this.map.getLayer('mapillary-image')) {
|
if (this.map.getLayer('mapillary-image')) {
|
||||||
this.map.removeLayer('mapillary-image');
|
this.map.removeLayer('mapillary-image');
|
||||||
@@ -140,7 +135,7 @@ export class MapillaryLayer {
|
|||||||
this.popupOpen.value = false;
|
this.popupOpen.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseEnter(e: maplibregl.MapLayerMouseEvent) {
|
onMouseEnter(e: mapboxgl.MapMouseEvent) {
|
||||||
if (
|
if (
|
||||||
e.features &&
|
e.features &&
|
||||||
e.features.length > 0 &&
|
e.features.length > 0 &&
|
||||||
|
|||||||
@@ -1,230 +0,0 @@
|
|||||||
import { settings } from '$lib/logic/settings';
|
|
||||||
import { get, type Writable } from 'svelte/store';
|
|
||||||
import {
|
|
||||||
basemaps,
|
|
||||||
defaultBasemap,
|
|
||||||
maptilerKeyPlaceHolder,
|
|
||||||
overlays,
|
|
||||||
terrainSources,
|
|
||||||
} from '$lib/assets/layers';
|
|
||||||
import { getLayers } from '$lib/components/map/layer-control/utils';
|
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
|
||||||
|
|
||||||
const { currentBasemap, currentOverlays, customLayers, opacities, terrainSource } = settings;
|
|
||||||
|
|
||||||
const emptySource: maplibregl.GeoJSONSourceSpecification = {
|
|
||||||
type: 'geojson',
|
|
||||||
data: {
|
|
||||||
type: 'FeatureCollection',
|
|
||||||
features: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export const ANCHOR_LAYER_KEY = {
|
|
||||||
overlays: 'overlays-end',
|
|
||||||
mapillary: 'mapillary-end',
|
|
||||||
tracks: 'tracks-end',
|
|
||||||
directionMarkers: 'direction-markers-end',
|
|
||||||
distanceMarkers: 'distance-markers-end',
|
|
||||||
startEndMarkers: 'start-end-markers-end',
|
|
||||||
interactions: 'interactions-end',
|
|
||||||
overpass: 'overpass-end',
|
|
||||||
waypoints: 'waypoints-end',
|
|
||||||
};
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
import Clean from '$lib/components/toolbar/tools/Clean.svelte';
|
import Clean from '$lib/components/toolbar/tools/Clean.svelte';
|
||||||
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
|
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
|
||||||
import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte';
|
import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte';
|
||||||
import maplibregl from 'maplibre-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -23,11 +23,11 @@
|
|||||||
const { minimizeRoutingMenu } = settings;
|
const { minimizeRoutingMenu } = settings;
|
||||||
|
|
||||||
let popupElement: HTMLDivElement | undefined = $state(undefined);
|
let popupElement: HTMLDivElement | undefined = $state(undefined);
|
||||||
let popup: maplibregl.Popup | undefined = $derived.by(() => {
|
let popup: mapboxgl.Popup | undefined = $derived.by(() => {
|
||||||
if (!popupElement) {
|
if (!popupElement) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
let popup = new maplibregl.Popup({
|
let popup = new mapboxgl.Popup({
|
||||||
closeButton: false,
|
closeButton: false,
|
||||||
maxWidth: undefined,
|
maxWidth: undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,12 +15,11 @@
|
|||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
import { Trash2 } from '@lucide/svelte';
|
import { Trash2 } from '@lucide/svelte';
|
||||||
import { map } from '$lib/components/map/map';
|
import { ANCHOR_LAYER_KEY, map } from '$lib/components/map/map';
|
||||||
import type { GeoJSONSource } from 'maplibre-gl';
|
import type { GeoJSONSource } from 'mapbox-gl';
|
||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import { fileActions } from '$lib/logic/file-actions';
|
import { fileActions } from '$lib/logic/file-actions';
|
||||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
|
||||||
|
|
||||||
let props: {
|
let props: {
|
||||||
class?: string;
|
class?: string;
|
||||||
@@ -29,7 +28,7 @@
|
|||||||
let cleanType = $state(CleanType.INSIDE);
|
let cleanType = $state(CleanType.INSIDE);
|
||||||
let deleteTrackpoints = $state(true);
|
let deleteTrackpoints = $state(true);
|
||||||
let deleteWaypoints = $state(true);
|
let deleteWaypoints = $state(true);
|
||||||
let rectangleCoordinates: maplibregl.LngLat[] = $state([]);
|
let rectangleCoordinates: mapboxgl.LngLat[] = $state([]);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { ListItem, ListTrackSegmentItem } from '$lib/components/file-list/file-list';
|
import { ListItem, ListTrackSegmentItem } from '$lib/components/file-list/file-list';
|
||||||
import { map } from '$lib/components/map/map';
|
import { ANCHOR_LAYER_KEY, map } from '$lib/components/map/map';
|
||||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
|
||||||
import { fileActions } from '$lib/logic/file-actions';
|
import { fileActions } from '$lib/logic/file-actions';
|
||||||
import { GPXFileStateCollectionObserver, type GPXFileState } from '$lib/logic/file-state';
|
import { GPXFileStateCollectionObserver, type GPXFileState } from '$lib/logic/file-state';
|
||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
|
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
|
||||||
import type { GeoJSONSource } from 'maplibre-gl';
|
import type { GeoJSONSource } from 'mapbox-gl';
|
||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
|
|
||||||
export const minTolerance = 0.1;
|
export const minTolerance = 0.1;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
SquareArrowUpLeft,
|
SquareArrowUpLeft,
|
||||||
SquareArrowOutDownRight,
|
SquareArrowOutDownRight,
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import { routingProfiles } from '$lib/components/toolbar/tools/routing/routing';
|
import { brouterProfiles } from '$lib/components/toolbar/tools/routing/routing';
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import {
|
import {
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
}: {
|
}: {
|
||||||
minimized?: boolean;
|
minimized?: boolean;
|
||||||
minimizable?: boolean;
|
minimizable?: boolean;
|
||||||
popup?: maplibregl.Popup;
|
popup?: mapboxgl.Popup;
|
||||||
popupElement?: HTMLDivElement;
|
popupElement?: HTMLDivElement;
|
||||||
class?: string;
|
class?: string;
|
||||||
} = $props();
|
} = $props();
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
{i18n._(`toolbar.routing.activities.${$routingProfile}`)}
|
{i18n._(`toolbar.routing.activities.${$routingProfile}`)}
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
{#each Object.keys(routingProfiles) as profile}
|
{#each Object.keys(brouterProfiles) as profile}
|
||||||
<Select.Item value={profile}
|
<Select.Item value={profile}
|
||||||
>{i18n._(
|
>{i18n._(
|
||||||
`toolbar.routing.activities.${profile}`
|
`toolbar.routing.activities.${profile}`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from 'gpx';
|
import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from 'gpx';
|
||||||
import { get, writable, type Readable } from 'svelte/store';
|
import { get, writable, type Readable } from 'svelte/store';
|
||||||
import maplibregl from 'maplibre-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { route } from './routing';
|
import { route } from './routing';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import {
|
import {
|
||||||
@@ -32,7 +32,7 @@ export class RoutingControls {
|
|||||||
file: Readable<GPXFileWithStatistics | undefined>;
|
file: Readable<GPXFileWithStatistics | undefined>;
|
||||||
anchors: AnchorWithMarker[] = [];
|
anchors: AnchorWithMarker[] = [];
|
||||||
shownAnchors: AnchorWithMarker[] = [];
|
shownAnchors: AnchorWithMarker[] = [];
|
||||||
popup: maplibregl.Popup;
|
popup: mapboxgl.Popup;
|
||||||
popupElement: HTMLElement;
|
popupElement: HTMLElement;
|
||||||
temporaryAnchor: AnchorWithMarker;
|
temporaryAnchor: AnchorWithMarker;
|
||||||
lastDragEvent = 0;
|
lastDragEvent = 0;
|
||||||
@@ -43,12 +43,12 @@ export class RoutingControls {
|
|||||||
this.toggleAnchorsForZoomLevelAndBounds.bind(this);
|
this.toggleAnchorsForZoomLevelAndBounds.bind(this);
|
||||||
showTemporaryAnchorBinded: (e: any) => void = this.showTemporaryAnchor.bind(this);
|
showTemporaryAnchorBinded: (e: any) => void = this.showTemporaryAnchor.bind(this);
|
||||||
updateTemporaryAnchorBinded: (e: any) => void = this.updateTemporaryAnchor.bind(this);
|
updateTemporaryAnchorBinded: (e: any) => void = this.updateTemporaryAnchor.bind(this);
|
||||||
appendAnchorBinded: (e: maplibregl.MapMouseEvent) => void = this.appendAnchor.bind(this);
|
appendAnchorBinded: (e: mapboxgl.MapMouseEvent) => void = this.appendAnchor.bind(this);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
fileId: string,
|
fileId: string,
|
||||||
file: Readable<GPXFileWithStatistics | undefined>,
|
file: Readable<GPXFileWithStatistics | undefined>,
|
||||||
popup: maplibregl.Popup,
|
popup: mapboxgl.Popup,
|
||||||
popupElement: HTMLElement
|
popupElement: HTMLElement
|
||||||
) {
|
) {
|
||||||
this.fileId = fileId;
|
this.fileId = fileId;
|
||||||
@@ -94,8 +94,7 @@ export class RoutingControls {
|
|||||||
|
|
||||||
add() {
|
add() {
|
||||||
const map_ = get(map);
|
const map_ = get(map);
|
||||||
const layerEventManager = map.layerEventManager;
|
if (!map_) {
|
||||||
if (!map_ || !layerEventManager) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,8 +102,8 @@ export class RoutingControls {
|
|||||||
|
|
||||||
map_.on('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
|
map_.on('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
|
||||||
map_.on('click', this.appendAnchorBinded);
|
map_.on('click', this.appendAnchorBinded);
|
||||||
layerEventManager.on('mousemove', this.fileId, this.showTemporaryAnchorBinded);
|
map_.on('mousemove', this.fileId, this.showTemporaryAnchorBinded);
|
||||||
layerEventManager.on('click', this.fileId, stopPropagation);
|
map_.on('click', this.fileId, stopPropagation);
|
||||||
|
|
||||||
this.fileUnsubscribe = this.file.subscribe(this.updateControls.bind(this));
|
this.fileUnsubscribe = this.file.subscribe(this.updateControls.bind(this));
|
||||||
}
|
}
|
||||||
@@ -153,18 +152,20 @@ export class RoutingControls {
|
|||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
const map_ = get(map);
|
const map_ = get(map);
|
||||||
const layerEventManager = map.layerEventManager;
|
if (!map_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.active = false;
|
this.active = false;
|
||||||
|
|
||||||
for (let anchor of this.anchors) {
|
for (let anchor of this.anchors) {
|
||||||
anchor.marker.remove();
|
anchor.marker.remove();
|
||||||
}
|
}
|
||||||
map_?.off('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
|
map_.off('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
|
||||||
map_?.off('click', this.appendAnchorBinded);
|
map_.off('click', this.appendAnchorBinded);
|
||||||
layerEventManager?.off('mousemove', this.fileId, this.showTemporaryAnchorBinded);
|
map_.off('mousemove', this.fileId, this.showTemporaryAnchorBinded);
|
||||||
layerEventManager?.off('click', this.fileId, stopPropagation);
|
map_.off('click', this.fileId, stopPropagation);
|
||||||
map_?.off('mousemove', this.updateTemporaryAnchorBinded);
|
map_.off('mousemove', this.updateTemporaryAnchorBinded);
|
||||||
this.temporaryAnchor.marker.remove();
|
this.temporaryAnchor.marker.remove();
|
||||||
|
|
||||||
this.fileUnsubscribe();
|
this.fileUnsubscribe();
|
||||||
@@ -179,7 +180,7 @@ export class RoutingControls {
|
|||||||
let element = document.createElement('div');
|
let element = document.createElement('div');
|
||||||
element.className = `h-5 w-5 xs:h-4 xs:w-4 md:h-3 md:w-3 rounded-full bg-white border-2 border-black cursor-pointer`;
|
element.className = `h-5 w-5 xs:h-4 xs:w-4 md:h-3 md:w-3 rounded-full bg-white border-2 border-black cursor-pointer`;
|
||||||
|
|
||||||
let marker = new maplibregl.Marker({
|
let marker = new mapboxgl.Marker({
|
||||||
draggable: true,
|
draggable: true,
|
||||||
className: 'z-10',
|
className: 'z-10',
|
||||||
element,
|
element,
|
||||||
@@ -214,7 +215,7 @@ export class RoutingControls {
|
|||||||
return anchor;
|
return anchor;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClickForAnchor(anchor: Anchor, marker: maplibregl.Marker) {
|
handleClickForAnchor(anchor: Anchor, marker: mapboxgl.Marker) {
|
||||||
return (e: any) => {
|
return (e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -606,7 +607,7 @@ export class RoutingControls {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async appendAnchor(e: maplibregl.MapMouseEvent) {
|
async appendAnchor(e: mapboxgl.MapMouseEvent) {
|
||||||
// Add a new anchor to the end of the last segment
|
// Add a new anchor to the end of the last segment
|
||||||
if (get(streetViewEnabled) && get(streetViewSource) === 'google') {
|
if (get(streetViewEnabled) && get(streetViewSource) === 'google') {
|
||||||
return;
|
return;
|
||||||
@@ -857,6 +858,6 @@ type Anchor = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type AnchorWithMarker = Anchor & {
|
type AnchorWithMarker = Anchor & {
|
||||||
marker: maplibregl.Marker;
|
marker: mapboxgl.Marker;
|
||||||
inZoom: boolean;
|
inZoom: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { get } from 'svelte/store';
|
|||||||
|
|
||||||
const { routing, routingProfile, privateRoads } = settings;
|
const { routing, routingProfile, privateRoads } = settings;
|
||||||
|
|
||||||
export const routingProfiles: { [key: string]: string } = {
|
export const brouterProfiles: { [key: string]: string } = {
|
||||||
bike: 'Trekking-dry',
|
bike: 'Trekking-dry',
|
||||||
racing_bike: 'fastbike',
|
racing_bike: 'fastbike',
|
||||||
gravel_bike: 'gravel',
|
gravel_bike: 'gravel',
|
||||||
@@ -19,7 +19,7 @@ export const routingProfiles: { [key: string]: string } = {
|
|||||||
|
|
||||||
export function route(points: Coordinates[]): Promise<TrackPoint[]> {
|
export function route(points: Coordinates[]): Promise<TrackPoint[]> {
|
||||||
if (get(routing)) {
|
if (get(routing)) {
|
||||||
return getRoute(points, routingProfiles[get(routingProfile)], get(privateRoads));
|
return getRoute(points, brouterProfiles[get(routingProfile)], get(privateRoads));
|
||||||
} else {
|
} else {
|
||||||
return getIntermediatePoints(points);
|
return getIntermediatePoints(points);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
splitControls = new SplitControls($map, map.layerEventManager!);
|
splitControls = new SplitControls($map);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,33 +8,40 @@ import { get } from 'svelte/store';
|
|||||||
import { fileStateCollection } from '$lib/logic/file-state';
|
import { fileStateCollection } from '$lib/logic/file-state';
|
||||||
import { fileActions } from '$lib/logic/file-actions';
|
import { fileActions } from '$lib/logic/file-actions';
|
||||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||||
import type { GeoJSONSource } from 'maplibre-gl';
|
import { ANCHOR_LAYER_KEY } from '$lib/components/map/map';
|
||||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
|
||||||
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
|
||||||
import { loadSVGIcon } from '$lib/utils';
|
|
||||||
|
|
||||||
export class SplitControls {
|
export class SplitControls {
|
||||||
map: maplibregl.Map;
|
map: mapboxgl.Map;
|
||||||
layerEventManager: MapLayerEventManager;
|
|
||||||
unsubscribes: Function[] = [];
|
unsubscribes: Function[] = [];
|
||||||
|
|
||||||
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
|
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
|
||||||
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
|
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
|
||||||
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
|
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
|
||||||
|
|
||||||
constructor(map: maplibregl.Map, layerEventManager: MapLayerEventManager) {
|
constructor(map: mapboxgl.Map) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
this.layerEventManager = layerEventManager;
|
|
||||||
loadSVGIcon(
|
if (!this.map.hasImage('split-control')) {
|
||||||
this.map,
|
let icon = new Image(100, 100);
|
||||||
'split-control',
|
icon.onload = () => {
|
||||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
if (!this.map.hasImage('split-control')) {
|
||||||
<circle cx="20" cy="20" r="20" fill="white" />
|
this.map.addImage('split-control', icon);
|
||||||
<g transform="translate(8 8)">
|
}
|
||||||
${Scissors.replace('stroke="currentColor"', 'stroke="black"')}
|
};
|
||||||
</g>
|
|
||||||
</svg>`
|
// 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.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
|
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
|
||||||
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
|
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
|
||||||
@@ -91,7 +98,7 @@ export class SplitControls {
|
|||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let source = this.map.getSource('split-controls') as GeoJSONSource | undefined;
|
let source = this.map.getSource('split-controls') as mapboxgl.GeoJSONSource | undefined;
|
||||||
if (source) {
|
if (source) {
|
||||||
source.setData(data);
|
source.setData(data);
|
||||||
} else {
|
} else {
|
||||||
@@ -117,17 +124,9 @@ export class SplitControls {
|
|||||||
ANCHOR_LAYER_KEY.interactions
|
ANCHOR_LAYER_KEY.interactions
|
||||||
);
|
);
|
||||||
|
|
||||||
this.layerEventManager.on(
|
this.map.on('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
|
||||||
'mouseenter',
|
this.map.on('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
|
||||||
'split-controls',
|
this.map.on('click', 'split-controls', this.layerOnClickBinded);
|
||||||
this.layerOnMouseEnterBinded
|
|
||||||
);
|
|
||||||
this.layerEventManager.on(
|
|
||||||
'mouseleave',
|
|
||||||
'split-controls',
|
|
||||||
this.layerOnMouseLeaveBinded
|
|
||||||
);
|
|
||||||
this.layerEventManager.on('click', 'split-controls', this.layerOnClickBinded);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// No reliable way to check if the map is ready to add sources and layers
|
// No reliable way to check if the map is ready to add sources and layers
|
||||||
@@ -135,9 +134,9 @@ export class SplitControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.layerEventManager.off('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
|
this.map.off('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
|
||||||
this.layerEventManager.off('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
|
this.map.off('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
|
||||||
this.layerEventManager.off('click', 'split-controls', this.layerOnClickBinded);
|
this.map.off('click', 'split-controls', this.layerOnClickBinded);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.map.getLayer('split-controls')) {
|
if (this.map.getLayer('split-controls')) {
|
||||||
@@ -160,7 +159,7 @@ export class SplitControls {
|
|||||||
mapCursor.notify(MapCursorState.SPLIT_CONTROL, false);
|
mapCursor.notify(MapCursorState.SPLIT_CONTROL, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
layerOnClick(e: maplibregl.MapLayerMouseEvent) {
|
layerOnClick(e: mapboxgl.MapMouseEvent) {
|
||||||
let coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates;
|
let coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates;
|
||||||
fileActions.split(
|
fileActions.split(
|
||||||
get(splitAs),
|
get(splitAs),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
import { fileActions } from '$lib/logic/file-actions';
|
import { fileActions } from '$lib/logic/file-actions';
|
||||||
import { map } from '$lib/components/map/map';
|
import { map } from '$lib/components/map/map';
|
||||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||||
import maplibregl from 'maplibre-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { getSvgForSymbol } from '$lib/components/map/gpx-layer/gpx-layer';
|
import { getSvgForSymbol } from '$lib/components/map/gpx-layer/gpx-layer';
|
||||||
|
|
||||||
let props: {
|
let props: {
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
let marker: maplibregl.Marker | null = null;
|
let marker: mapboxgl.Marker | null = null;
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
if ($selectedWaypoint) {
|
if ($selectedWaypoint) {
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
let element = document.createElement('div');
|
let element = document.createElement('div');
|
||||||
element.classList.add('w-8', 'h-8');
|
element.classList.add('w-8', 'h-8');
|
||||||
element.innerHTML = getSvgForSymbol(symbolKey);
|
element.innerHTML = getSvgForSymbol(symbolKey);
|
||||||
marker = new maplibregl.Marker({
|
marker = new mapboxgl.Marker({
|
||||||
element,
|
element,
|
||||||
anchor: 'bottom',
|
anchor: 'bottom',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Mapbox ist das Unternehmen, das einige der schönen Karten auf dieser Website zur Verfügung stellt.
|
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> welche **gpx.studio** unterstützt.
|
Sie entwickeln auch die <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">Karten-Engine</a>, die **gpx.studio** 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.
|
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.
|
||||||
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.
|
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.
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ title: Files and statistics
|
|||||||
|
|
||||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||||
let slicedGPXStatistics = writable(undefined);
|
let slicedGPXStatistics = writable(undefined);
|
||||||
let hoveredPoint = writable(null);
|
|
||||||
let additionalDatasets = writable(['speed', 'atemp']);
|
let additionalDatasets = writable(['speed', 'atemp']);
|
||||||
let elevationFill = writable(undefined);
|
let elevationFill = writable(undefined);
|
||||||
</script>
|
</script>
|
||||||
@@ -85,7 +84,6 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
|||||||
<ElevationProfile
|
<ElevationProfile
|
||||||
{gpxStatistics}
|
{gpxStatistics}
|
||||||
{slicedGPXStatistics}
|
{slicedGPXStatistics}
|
||||||
{hoveredPoint}
|
|
||||||
{additionalDatasets}
|
{additionalDatasets}
|
||||||
{elevationFill}
|
{elevationFill}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||||
|
|
||||||
Each time you add or move GPS points, our servers calculate the best route on the road network.
|
Each time you add or move GPS points, our servers calculate the best route on the road network.
|
||||||
We also use APIs from <a href="https://maptiler.com" target="_blank">MapTiler</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
||||||
|
|
||||||
Unfortunately, this is expensive.
|
Unfortunately, this is expensive.
|
||||||
If you enjoy using this tool and find it valuable, please consider making a small donation to help keep the website free and ad-free.
|
If you enjoy using this tool and find it valuable, please consider making a small donation to help keep the website free and ad-free.
|
||||||
|
|||||||
5
website/src/lib/docs/en/home/mapbox.mdx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Mapbox is the company that provides some of the beautiful maps on this website.
|
||||||
|
They also develop the <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">map engine</a> which powers **gpx.studio**.
|
||||||
|
|
||||||
|
We are incredibly fortunate and grateful to be part of their <a href="https://mapbox.com/community" target="_blank">Community</a> program, which supports nonprofits, educational institutions, and positive impact organizations.
|
||||||
|
This partnership allows **gpx.studio** to benefit from Mapbox tools at discounted prices, greatly contributing to the financial viability of the project and enabling us to offer the best possible user experience.
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
MapTiler is the company that provides some of the beautiful maps on this website.
|
|
||||||
This partnership allows **gpx.studio** to benefit from MapTiler tools at discounted prices, greatly contributing to the financial viability of the project and enabling us to offer the best possible user experience.
|
|
||||||
@@ -12,7 +12,7 @@ title: Integration
|
|||||||
You can use **gpx.studio** to create maps showing your GPX files and embed them in your website.
|
You can use **gpx.studio** to create maps showing your GPX files and embed them in your website.
|
||||||
|
|
||||||
All you need is:
|
All you need is:
|
||||||
1. A <a href="https://cloud.maptiler.com/auth/widget?next=https://cloud.maptiler.com/maps/" target="_blank">MapTiler key</a> to load the map, and
|
1. A <a href="https://account.mapbox.com/auth/signup" target="_blank">Mapbox access token</a> to load the map, and
|
||||||
1. GPX files hosted on your server or on Google Drive, or accessible via a public URL.
|
1. GPX files hosted on your server or on Google Drive, or accessible via a public URL.
|
||||||
|
|
||||||
You can then play with the configurator below to customize your map and generate the corresponding HTML code.
|
You can then play with the configurator below to customize your map and generate the corresponding HTML code.
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ Only one basemap can be displayed at a time.
|
|||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<DocsLayers />
|
<DocsLayers />
|
||||||
<span class="text-sm text-center mt-2">
|
<span class="text-sm text-center mt-2">
|
||||||
Hover over the map to show the <a href="https://hiking.waymarkedtrails.org" target="_blank">Waymarked Trails hiking</a> overlay on top of the <a href="https://www.maptiler.com/maps/outdoor-topo/" target="_blank">MapTiler Topo</a> basemap.
|
Hover over the map to show the <a href="https://hiking.waymarkedtrails.org" target="_blank">Waymarked Trails hiking</a> overlay on top of the <a href="https://www.mapbox.com/maps/outdoors" target="_blank">Mapbox Outdoors</a> basemap.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -67,4 +67,4 @@ They can be enabled in the [map layer settings dialog](./menu/settings).
|
|||||||
|
|
||||||
In these settings, you can also manage the opacity of the overlays.
|
In these settings, you can also manage the opacity of the overlays.
|
||||||
|
|
||||||
For advanced users, it is possible to add custom basemaps and overlays by providing <a href="https://en.wikipedia.org/wiki/Web_Map_Tile_Service" target="_blank">WMTS</a>, <a href="https://en.wikipedia.org/wiki/Web_Map_Service" target="_blank">WMS</a>, or <a href="https://maplibre.org/maplibre-style-spec/" target="_blank">MapLibre style JSON</a> URLs.
|
For advanced users, it is possible to add custom basemaps and overlays by providing <a href="https://en.wikipedia.org/wiki/Web_Map_Tile_Service" target="_blank">WMTS</a>, <a href="https://en.wikipedia.org/wiki/Web_Map_Service" target="_blank">WMS</a>, or <a href="https://docs.mapbox.com/help/glossary/style/" target="_blank">Mapbox style JSON</a> URLs.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ This tool allows you to add elevation data to traces and [points of interest](..
|
|||||||
|
|
||||||
<DocsNote>
|
<DocsNote>
|
||||||
|
|
||||||
Elevation data is provided by <a href="https://maptiler.com" target="_blank">MapTiler</a>.
|
Elevation data is provided by <a href="https://mapbox.com" target="_blank">Mapbox</a>.
|
||||||
You can learn more about its origin and accuracy in the <a href="https://docs.maptiler.com/guides/map-tiling-hosting/data-hosting/rgb-terrain-by-maptiler/" target="_blank">documentation</a>.
|
You can learn more about its origin and accuracy in the <a href="https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data" target="_blank">documentation</a>.
|
||||||
|
|
||||||
</DocsNote>
|
</DocsNote>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import maplibregl from 'maplibre-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { ListFileItem, ListWaypointItem } from '$lib/components/file-list/file-list';
|
import { ListFileItem, ListWaypointItem } from '$lib/components/file-list/file-list';
|
||||||
import { fileStateCollection, GPXFileStateCollectionObserver } from '$lib/logic/file-state';
|
import { fileStateCollection, GPXFileStateCollectionObserver } from '$lib/logic/file-state';
|
||||||
import { gpxStatistics } from '$lib/logic/statistics';
|
import { gpxStatistics } from '$lib/logic/statistics';
|
||||||
@@ -10,7 +10,7 @@ import type { Coordinates } from 'gpx';
|
|||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
|
|
||||||
export class BoundsManager {
|
export class BoundsManager {
|
||||||
private _bounds: maplibregl.LngLatBounds = new maplibregl.LngLatBounds();
|
private _bounds: mapboxgl.LngLatBounds = new mapboxgl.LngLatBounds();
|
||||||
private _files: Set<string> = new Set();
|
private _files: Set<string> = new Set();
|
||||||
private _fileStateCollectionObserver: GPXFileStateCollectionObserver | null = null;
|
private _fileStateCollectionObserver: GPXFileStateCollectionObserver | null = null;
|
||||||
private _unsubscribes: (() => void)[] = [];
|
private _unsubscribes: (() => void)[] = [];
|
||||||
@@ -87,12 +87,12 @@ export class BoundsManager {
|
|||||||
}
|
}
|
||||||
this._unsubscribes.forEach((unsubscribe) => unsubscribe());
|
this._unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
this._unsubscribes = [];
|
this._unsubscribes = [];
|
||||||
this._bounds = new maplibregl.LngLatBounds([180, 90, -180, -90]);
|
this._bounds = new mapboxgl.LngLatBounds([180, 90, -180, -90]);
|
||||||
}
|
}
|
||||||
|
|
||||||
centerMapOnSelection() {
|
centerMapOnSelection() {
|
||||||
let selected = get(selection).getSelected();
|
let selected = get(selection).getSelected();
|
||||||
let bounds = new maplibregl.LngLatBounds();
|
let bounds = new mapboxgl.LngLatBounds();
|
||||||
|
|
||||||
if (selected.find((item) => item instanceof ListWaypointItem)) {
|
if (selected.find((item) => item instanceof ListWaypointItem)) {
|
||||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { type Database } from '$lib/db';
|
import { type Database } from '$lib/db';
|
||||||
import { liveQuery } from 'dexie';
|
import { liveQuery } from 'dexie';
|
||||||
import {
|
import {
|
||||||
basemaps,
|
|
||||||
defaultBasemap,
|
defaultBasemap,
|
||||||
defaultBasemapTree,
|
defaultBasemapTree,
|
||||||
defaultOpacities,
|
defaultOpacities,
|
||||||
@@ -10,10 +9,7 @@ import {
|
|||||||
defaultOverpassQueries,
|
defaultOverpassQueries,
|
||||||
defaultOverpassTree,
|
defaultOverpassTree,
|
||||||
defaultTerrainSource,
|
defaultTerrainSource,
|
||||||
overlays,
|
|
||||||
overpassQueryData,
|
|
||||||
type CustomLayer,
|
type CustomLayer,
|
||||||
type LayerTreeType,
|
|
||||||
} from '$lib/assets/layers';
|
} from '$lib/assets/layers';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { get, writable, type Writable } from 'svelte/store';
|
import { get, writable, type Writable } from 'svelte/store';
|
||||||
@@ -23,12 +19,10 @@ export class Setting<V> {
|
|||||||
private _subscription: { unsubscribe: () => void } | null = null;
|
private _subscription: { unsubscribe: () => void } | null = null;
|
||||||
private _key: string;
|
private _key: string;
|
||||||
private _value: Writable<V>;
|
private _value: Writable<V>;
|
||||||
private _validator?: (value: V) => V;
|
|
||||||
|
|
||||||
constructor(key: string, initial: V, validator?: (value: V) => V) {
|
constructor(key: string, initial: V) {
|
||||||
this._key = key;
|
this._key = key;
|
||||||
this._value = writable(initial);
|
this._value = writable(initial);
|
||||||
this._validator = validator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectToDatabase(db: Database) {
|
connectToDatabase(db: Database) {
|
||||||
@@ -42,9 +36,6 @@ export class Setting<V> {
|
|||||||
this._value.set(value);
|
this._value.set(value);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this._validator) {
|
|
||||||
value = this._validator(value);
|
|
||||||
}
|
|
||||||
this._value.set(value);
|
this._value.set(value);
|
||||||
}
|
}
|
||||||
first = false;
|
first = false;
|
||||||
@@ -82,13 +73,11 @@ export class SettingInitOnFirstRead<V> {
|
|||||||
private _key: string;
|
private _key: string;
|
||||||
private _value: Writable<V | undefined>;
|
private _value: Writable<V | undefined>;
|
||||||
private _initial: V;
|
private _initial: V;
|
||||||
private _validator?: (value: V) => V;
|
|
||||||
|
|
||||||
constructor(key: string, initial: V, validator?: (value: V) => V) {
|
constructor(key: string, initial: V) {
|
||||||
this._key = key;
|
this._key = key;
|
||||||
this._value = writable(undefined);
|
this._value = writable(undefined);
|
||||||
this._initial = initial;
|
this._initial = initial;
|
||||||
this._validator = validator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectToDatabase(db: Database) {
|
connectToDatabase(db: Database) {
|
||||||
@@ -104,9 +93,6 @@ export class SettingInitOnFirstRead<V> {
|
|||||||
this._value.set(value);
|
this._value.set(value);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this._validator) {
|
|
||||||
value = this._validator(value);
|
|
||||||
}
|
|
||||||
this._value.set(value);
|
this._value.set(value);
|
||||||
}
|
}
|
||||||
first = false;
|
first = false;
|
||||||
@@ -142,166 +128,37 @@ export class SettingInitOnFirstRead<V> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getValueValidator<V>(allowed: V[], fallback: V) {
|
|
||||||
const dict = new Set<V>(allowed);
|
|
||||||
return (value: V) => (dict.has(value) ? value : fallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getArrayValidator<V>(allowed: V[]) {
|
|
||||||
const dict = new Set<V>(allowed);
|
|
||||||
return (value: V[]) => value.filter((v) => dict.has(v));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLayerValidator(allowed: Record<string, any>, fallback: string) {
|
|
||||||
return (layer: string) =>
|
|
||||||
allowed.hasOwnProperty(layer) ||
|
|
||||||
layer.startsWith('custom-') ||
|
|
||||||
layer.startsWith('extension-')
|
|
||||||
? layer
|
|
||||||
: fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterLayerTree(t: LayerTreeType, allowed: Record<string, any>): LayerTreeType {
|
|
||||||
const filtered: LayerTreeType = {};
|
|
||||||
Object.entries(t).forEach(([key, value]) => {
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
filtered[key] = filterLayerTree(value, allowed);
|
|
||||||
} else if (
|
|
||||||
allowed.hasOwnProperty(key) ||
|
|
||||||
key.startsWith('custom-') ||
|
|
||||||
key.startsWith('extension-')
|
|
||||||
) {
|
|
||||||
filtered[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLayerTreeValidator(allowed: Record<string, any>) {
|
|
||||||
return (value: LayerTreeType) => filterLayerTree(value, allowed);
|
|
||||||
}
|
|
||||||
|
|
||||||
type DistanceUnits = 'metric' | 'imperial' | 'nautical';
|
|
||||||
type VelocityUnits = 'speed' | 'pace';
|
|
||||||
type TemperatureUnits = 'celsius' | 'fahrenheit';
|
|
||||||
type AdditionalDataset = 'speed' | 'hr' | 'cad' | 'atemp' | 'power';
|
|
||||||
type ElevationFill = 'slope' | 'surface' | undefined;
|
|
||||||
type RoutingProfile =
|
|
||||||
| 'bike'
|
|
||||||
| 'racing_bike'
|
|
||||||
| 'gravel_bike'
|
|
||||||
| 'mountain_bike'
|
|
||||||
| 'foot'
|
|
||||||
| 'motorcycle'
|
|
||||||
| 'water'
|
|
||||||
| 'railway';
|
|
||||||
type TerrainSource = 'maptiler-dem' | 'mapterhorn';
|
|
||||||
type StreetViewSource = 'mapillary' | 'google';
|
|
||||||
|
|
||||||
export const settings = {
|
export const settings = {
|
||||||
distanceUnits: new Setting<DistanceUnits>(
|
distanceUnits: new Setting<'metric' | 'imperial' | 'nautical'>('distanceUnits', 'metric'),
|
||||||
'distanceUnits',
|
velocityUnits: new Setting<'speed' | 'pace'>('velocityUnits', 'speed'),
|
||||||
'metric',
|
temperatureUnits: new Setting<'celsius' | 'fahrenheit'>('temperatureUnits', 'celsius'),
|
||||||
getValueValidator<DistanceUnits>(['metric', 'imperial', 'nautical'], 'metric')
|
|
||||||
),
|
|
||||||
velocityUnits: new Setting<VelocityUnits>(
|
|
||||||
'velocityUnits',
|
|
||||||
'speed',
|
|
||||||
getValueValidator<VelocityUnits>(['speed', 'pace'], 'speed')
|
|
||||||
),
|
|
||||||
temperatureUnits: new Setting<TemperatureUnits>(
|
|
||||||
'temperatureUnits',
|
|
||||||
'celsius',
|
|
||||||
getValueValidator<TemperatureUnits>(['celsius', 'fahrenheit'], 'celsius')
|
|
||||||
),
|
|
||||||
elevationProfile: new Setting<boolean>('elevationProfile', true),
|
elevationProfile: new Setting<boolean>('elevationProfile', true),
|
||||||
additionalDatasets: new Setting<AdditionalDataset[]>(
|
additionalDatasets: new Setting<string[]>('additionalDatasets', []),
|
||||||
'additionalDatasets',
|
elevationFill: new Setting<'slope' | 'surface' | undefined>('elevationFill', undefined),
|
||||||
[],
|
|
||||||
getArrayValidator<AdditionalDataset>(['speed', 'hr', 'cad', 'atemp', 'power'])
|
|
||||||
),
|
|
||||||
elevationFill: new Setting<ElevationFill>(
|
|
||||||
'elevationFill',
|
|
||||||
undefined,
|
|
||||||
getValueValidator(['slope', 'surface', undefined], undefined)
|
|
||||||
),
|
|
||||||
treeFileView: new Setting<boolean>('fileView', false),
|
treeFileView: new Setting<boolean>('fileView', false),
|
||||||
minimizeRoutingMenu: new Setting('minimizeRoutingMenu', false),
|
minimizeRoutingMenu: new Setting('minimizeRoutingMenu', false),
|
||||||
routing: new Setting('routing', true),
|
routing: new Setting('routing', true),
|
||||||
routingProfile: new Setting<RoutingProfile>(
|
routingProfile: new Setting('routingProfile', 'bike'),
|
||||||
'routingProfile',
|
|
||||||
'bike',
|
|
||||||
getValueValidator<RoutingProfile>(
|
|
||||||
[
|
|
||||||
'bike',
|
|
||||||
'racing_bike',
|
|
||||||
'gravel_bike',
|
|
||||||
'mountain_bike',
|
|
||||||
'foot',
|
|
||||||
'motorcycle',
|
|
||||||
'water',
|
|
||||||
'railway',
|
|
||||||
],
|
|
||||||
'bike'
|
|
||||||
)
|
|
||||||
),
|
|
||||||
privateRoads: new Setting('privateRoads', false),
|
privateRoads: new Setting('privateRoads', false),
|
||||||
currentBasemap: new Setting(
|
currentBasemap: new Setting('currentBasemap', defaultBasemap),
|
||||||
'currentBasemap',
|
previousBasemap: new Setting('previousBasemap', defaultBasemap),
|
||||||
defaultBasemap,
|
selectedBasemapTree: new Setting('selectedBasemapTree', defaultBasemapTree),
|
||||||
getLayerValidator(basemaps, defaultBasemap)
|
currentOverlays: new SettingInitOnFirstRead('currentOverlays', defaultOverlays),
|
||||||
),
|
previousOverlays: new Setting('previousOverlays', defaultOverlays),
|
||||||
previousBasemap: new Setting(
|
selectedOverlayTree: new Setting('selectedOverlayTree', defaultOverlayTree),
|
||||||
'previousBasemap',
|
|
||||||
defaultBasemap,
|
|
||||||
getLayerValidator(Object.keys(basemaps), defaultBasemap)
|
|
||||||
),
|
|
||||||
selectedBasemapTree: new Setting(
|
|
||||||
'selectedBasemapTree',
|
|
||||||
defaultBasemapTree,
|
|
||||||
getLayerTreeValidator(basemaps)
|
|
||||||
),
|
|
||||||
currentOverlays: new SettingInitOnFirstRead(
|
|
||||||
'currentOverlays',
|
|
||||||
defaultOverlays,
|
|
||||||
getLayerTreeValidator(overlays)
|
|
||||||
),
|
|
||||||
previousOverlays: new Setting(
|
|
||||||
'previousOverlays',
|
|
||||||
defaultOverlays,
|
|
||||||
getLayerTreeValidator(overlays)
|
|
||||||
),
|
|
||||||
selectedOverlayTree: new Setting(
|
|
||||||
'selectedOverlayTree',
|
|
||||||
defaultOverlayTree,
|
|
||||||
getLayerTreeValidator(overlays)
|
|
||||||
),
|
|
||||||
currentOverpassQueries: new SettingInitOnFirstRead(
|
currentOverpassQueries: new SettingInitOnFirstRead(
|
||||||
'currentOverpassQueries',
|
'currentOverpassQueries',
|
||||||
defaultOverpassQueries,
|
defaultOverpassQueries
|
||||||
getLayerTreeValidator(overpassQueryData)
|
|
||||||
),
|
|
||||||
selectedOverpassTree: new Setting(
|
|
||||||
'selectedOverpassTree',
|
|
||||||
defaultOverpassTree,
|
|
||||||
getLayerTreeValidator(overpassQueryData)
|
|
||||||
),
|
),
|
||||||
|
selectedOverpassTree: new Setting('selectedOverpassTree', defaultOverpassTree),
|
||||||
opacities: new Setting('opacities', defaultOpacities),
|
opacities: new Setting('opacities', defaultOpacities),
|
||||||
customLayers: new Setting<Record<string, CustomLayer>>('customLayers', {}),
|
customLayers: new Setting<Record<string, CustomLayer>>('customLayers', {}),
|
||||||
customBasemapOrder: new Setting<string[]>('customBasemapOrder', []),
|
customBasemapOrder: new Setting<string[]>('customBasemapOrder', []),
|
||||||
customOverlayOrder: new Setting<string[]>('customOverlayOrder', []),
|
customOverlayOrder: new Setting<string[]>('customOverlayOrder', []),
|
||||||
terrainSource: new Setting<TerrainSource>(
|
terrainSource: new Setting('terrainSource', defaultTerrainSource),
|
||||||
'terrainSource',
|
|
||||||
defaultTerrainSource,
|
|
||||||
getValueValidator(['maptiler-dem', 'mapterhorn'], defaultTerrainSource)
|
|
||||||
),
|
|
||||||
directionMarkers: new Setting('directionMarkers', false),
|
directionMarkers: new Setting('directionMarkers', false),
|
||||||
distanceMarkers: new Setting('distanceMarkers', false),
|
distanceMarkers: new Setting('distanceMarkers', false),
|
||||||
streetViewSource: new Setting<StreetViewSource>(
|
streetViewSource: new Setting('streetViewSource', 'mapillary'),
|
||||||
'streetViewSource',
|
|
||||||
'mapillary',
|
|
||||||
getValueValidator<StreetViewSource>(['mapillary', 'google'], 'mapillary')
|
|
||||||
),
|
|
||||||
fileOrder: new Setting<string[]>('fileOrder', []),
|
fileOrder: new Setting<string[]>('fileOrder', []),
|
||||||
defaultOpacity: new Setting('defaultOpacity', 0.7),
|
defaultOpacity: new Setting('defaultOpacity', 0.7),
|
||||||
defaultWidth: new Setting('defaultWidth', browser && window.innerWidth < 600 ? 8 : 5),
|
defaultWidth: new Setting('defaultWidth', browser && window.innerWidth < 600 ? 8 : 5),
|
||||||
|
|||||||
@@ -1,24 +1,18 @@
|
|||||||
import { ListItem, ListLevel } from '$lib/components/file-list/file-list';
|
import { ListItem, ListLevel } from '$lib/components/file-list/file-list';
|
||||||
import { GPXFile, GPXStatistics, GPXStatisticsGroup, type Track } from 'gpx';
|
import { GPXFile, GPXStatistics, GPXStatisticsGroup, type Track } from 'gpx';
|
||||||
import maplibregl from 'maplibre-gl';
|
|
||||||
|
|
||||||
export class GPXStatisticsTree {
|
export class GPXStatisticsTree {
|
||||||
level: ListLevel;
|
level: ListLevel;
|
||||||
statistics: {
|
statistics: {
|
||||||
[key: string]: GPXStatisticsTree | GPXStatistics;
|
[key: string]: GPXStatisticsTree | GPXStatistics;
|
||||||
} = {};
|
} = {};
|
||||||
wptBounds: maplibregl.LngLatBounds;
|
|
||||||
|
|
||||||
constructor(element: GPXFile | Track) {
|
constructor(element: GPXFile | Track) {
|
||||||
this.wptBounds = new maplibregl.LngLatBounds();
|
|
||||||
if (element instanceof GPXFile) {
|
if (element instanceof GPXFile) {
|
||||||
this.level = ListLevel.FILE;
|
this.level = ListLevel.FILE;
|
||||||
element.children.forEach((child, index) => {
|
element.children.forEach((child, index) => {
|
||||||
this.statistics[index] = new GPXStatisticsTree(child);
|
this.statistics[index] = new GPXStatisticsTree(child);
|
||||||
});
|
});
|
||||||
element.wpt.forEach((wpt) => {
|
|
||||||
this.wptBounds.extend(wpt.getCoordinates());
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.level = ListLevel.TRACK;
|
this.level = ListLevel.TRACK;
|
||||||
element.children.forEach((child, index) => {
|
element.children.forEach((child, index) => {
|
||||||
@@ -48,27 +42,5 @@ export class GPXStatisticsTree {
|
|||||||
}
|
}
|
||||||
return statistics;
|
return statistics;
|
||||||
}
|
}
|
||||||
|
|
||||||
intersectsBBox(bounds: maplibregl.LngLatBounds): boolean {
|
|
||||||
for (let key in this.statistics) {
|
|
||||||
const stats = this.statistics[key];
|
|
||||||
if (stats instanceof GPXStatistics) {
|
|
||||||
const bbox = new maplibregl.LngLatBounds(
|
|
||||||
stats.global.bounds.southWest,
|
|
||||||
stats.global.bounds.northEast
|
|
||||||
);
|
|
||||||
if (!bbox.isEmpty() && bbox.intersects(bounds)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (stats.intersectsBBox(bounds)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
intersectsWaypointBBox(bounds: maplibregl.LngLatBounds): boolean {
|
|
||||||
return !this.wptBounds.isEmpty() && this.wptBounds.intersects(bounds);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export type GPXFileWithStatistics = { file: GPXFile; statistics: GPXStatisticsTree };
|
export type GPXFileWithStatistics = { file: GPXFile; statistics: GPXStatisticsTree };
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import { GPXGlobalStatistics, GPXStatisticsGroup, type Coordinates } from 'gpx';
|
import { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
||||||
import { fileStateCollection, GPXFileState } from '$lib/logic/file-state';
|
import { fileStateCollection, GPXFileState } from '$lib/logic/file-state';
|
||||||
import {
|
import {
|
||||||
ListFileItem,
|
ListFileItem,
|
||||||
@@ -82,8 +82,6 @@ export const gpxStatistics = new SelectedGPXStatistics();
|
|||||||
export const slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined> =
|
export const slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined> =
|
||||||
writable(undefined);
|
writable(undefined);
|
||||||
|
|
||||||
export const hoveredPoint: Writable<Coordinates | null> = writable(null);
|
|
||||||
|
|
||||||
gpxStatistics.subscribe(() => {
|
gpxStatistics.subscribe(() => {
|
||||||
slicedGPXStatistics.set(undefined);
|
slicedGPXStatistics.set(undefined);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import { twMerge } from 'tailwind-merge';
|
|||||||
import { base } from '$app/paths';
|
import { base } from '$app/paths';
|
||||||
import { languages } from '$lib/languages';
|
import { languages } from '$lib/languages';
|
||||||
import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance, GPXFile } from 'gpx';
|
import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance, GPXFile } from 'gpx';
|
||||||
import maplibregl from 'maplibre-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt';
|
import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt';
|
||||||
|
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||||
|
import PNGReader from 'png.js';
|
||||||
import type { GPXStatisticsTree } from '$lib/logic/statistics-tree';
|
import type { GPXStatisticsTree } from '$lib/logic/statistics-tree';
|
||||||
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
|
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
|
||||||
import { PUBLIC_MAPTILER_KEY } from '$env/static/public';
|
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
@@ -61,7 +62,7 @@ export function getClosestTrackSegments(
|
|||||||
let segmentBounds = segmentStatistics.global.bounds;
|
let segmentBounds = segmentStatistics.global.bounds;
|
||||||
let northEast = segmentBounds.northEast;
|
let northEast = segmentBounds.northEast;
|
||||||
let southWest = segmentBounds.southWest;
|
let southWest = segmentBounds.southWest;
|
||||||
let bounds = new maplibregl.LngLatBounds(southWest, northEast);
|
let bounds = new mapboxgl.LngLatBounds(southWest, northEast);
|
||||||
if (bounds.contains(point)) {
|
if (bounds.contains(point)) {
|
||||||
segmentBoundsDistances.push([0, trackIndex, segmentIndex]);
|
segmentBoundsDistances.push([0, trackIndex, segmentIndex]);
|
||||||
} else {
|
} else {
|
||||||
@@ -109,49 +110,33 @@ export function getElevation(
|
|||||||
let coordinates = points.map((point) =>
|
let coordinates = points.map((point) =>
|
||||||
point instanceof TrackPoint || point instanceof Waypoint ? point.getCoordinates() : point
|
point instanceof TrackPoint || point instanceof Waypoint ? point.getCoordinates() : point
|
||||||
);
|
);
|
||||||
let bbox = new maplibregl.LngLatBounds();
|
let bbox = new mapboxgl.LngLatBounds();
|
||||||
coordinates.forEach((coord) => bbox.extend(coord));
|
coordinates.forEach((coord) => bbox.extend(coord));
|
||||||
|
|
||||||
let tiles = coordinates.map((coord) => pointToTile(coord.lon, coord.lat, ELEVATION_ZOOM));
|
let tiles = coordinates.map((coord) => pointToTile(coord.lon, coord.lat, ELEVATION_ZOOM));
|
||||||
let uniqueTiles = Array.from(new Set(tiles.map((tile) => tile.join(',')))).map((tile) =>
|
let uniqueTiles = Array.from(new Set(tiles.map((tile) => tile.join(',')))).map((tile) =>
|
||||||
tile.split(',').map((x) => parseInt(x))
|
tile.split(',').map((x) => parseInt(x))
|
||||||
);
|
);
|
||||||
let images = new Map<string, ImageData>();
|
let pngs = new Map<string, any>();
|
||||||
|
|
||||||
const getPixelFromImageData = (imageData: ImageData, x: number, y: number): number[] => {
|
|
||||||
const index = (y * imageData.width + x) * 4;
|
|
||||||
return [imageData.data[index], imageData.data[index + 1], imageData.data[index + 2]];
|
|
||||||
};
|
|
||||||
|
|
||||||
let promises = uniqueTiles.map((tile) =>
|
let promises = uniqueTiles.map((tile) =>
|
||||||
fetch(
|
fetch(
|
||||||
`https://api.maptiler.com/tiles/terrain-rgb-v2/${ELEVATION_ZOOM}/${tile[0]}/${tile[1]}.webp?key=${PUBLIC_MAPTILER_KEY}`,
|
`https://api.mapbox.com/v4/mapbox.mapbox-terrain-dem-v1/${ELEVATION_ZOOM}/${tile[0]}/${tile[1]}@2x.pngraw?access_token=${PUBLIC_MAPBOX_TOKEN}`,
|
||||||
{ cache: 'force-cache' }
|
{ cache: 'force-cache' }
|
||||||
)
|
)
|
||||||
.then((response) => response.blob())
|
.then((response) => response.arrayBuffer())
|
||||||
.then(
|
.then(
|
||||||
(blob) =>
|
(buffer) =>
|
||||||
new Promise<void>((resolve) => {
|
new Promise((resolve) => {
|
||||||
const url = URL.createObjectURL(blob);
|
let png = new PNGReader(new Uint8Array(buffer));
|
||||||
const img = new Image();
|
png.parse((err, png) => {
|
||||||
img.onload = () => {
|
if (err) {
|
||||||
const canvas = document.createElement('canvas');
|
resolve(false); // Also resolve so that Promise.all doesn't fail
|
||||||
canvas.width = img.width;
|
} else {
|
||||||
canvas.height = img.height;
|
pngs.set(tile.join(','), png);
|
||||||
const ctx = canvas.getContext('2d');
|
resolve(true);
|
||||||
if (ctx) {
|
|
||||||
ctx.drawImage(img, 0, 0);
|
|
||||||
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
|
||||||
images.set(tile.join(','), imageData);
|
|
||||||
}
|
}
|
||||||
URL.revokeObjectURL(url);
|
});
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
img.onerror = () => {
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
img.src = url;
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -159,9 +144,9 @@ export function getElevation(
|
|||||||
return Promise.all(promises).then(() =>
|
return Promise.all(promises).then(() =>
|
||||||
coordinates.map((coord, index) => {
|
coordinates.map((coord, index) => {
|
||||||
let tile = tiles[index];
|
let tile = tiles[index];
|
||||||
let imageData = images.get(tile.join(','));
|
let png = pngs.get(tile.join(','));
|
||||||
|
|
||||||
if (!imageData) {
|
if (!png) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,11 +158,10 @@ export function getElevation(
|
|||||||
let dx = x - _x;
|
let dx = x - _x;
|
||||||
let dy = y - _y;
|
let dy = y - _y;
|
||||||
|
|
||||||
const p00 = getPixelFromImageData(imageData, _x, _y);
|
const p00 = png.getPixel(_x, _y);
|
||||||
const p01 = getPixelFromImageData(imageData, _x, _y + (_y + 1 == tileSize ? 0 : 1));
|
const p01 = png.getPixel(_x, _y + (_y + 1 == tileSize ? 0 : 1));
|
||||||
const p10 = getPixelFromImageData(imageData, _x + (_x + 1 == tileSize ? 0 : 1), _y);
|
const p10 = png.getPixel(_x + (_x + 1 == tileSize ? 0 : 1), _y);
|
||||||
const p11 = getPixelFromImageData(
|
const p11 = png.getPixel(
|
||||||
imageData,
|
|
||||||
_x + (_x + 1 == tileSize ? 0 : 1),
|
_x + (_x + 1 == tileSize ? 0 : 1),
|
||||||
_y + (_y + 1 == tileSize ? 0 : 1)
|
_y + (_y + 1 == tileSize ? 0 : 1)
|
||||||
);
|
);
|
||||||
@@ -197,18 +181,6 @@ export function getElevation(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadSVGIcon(map: maplibregl.Map, id: string, svg: string) {
|
|
||||||
if (!map.hasImage(id)) {
|
|
||||||
let icon = new Image(100, 100);
|
|
||||||
icon.onload = () => {
|
|
||||||
if (!map.hasImage(id)) {
|
|
||||||
map.addImage(id, icon);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
icon.src = 'data:image/svg+xml,' + encodeURIComponent(svg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isMac() {
|
export function isMac() {
|
||||||
return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
|
return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,14 @@
|
|||||||
"export_all": "Alle exportieren...",
|
"export_all": "Alle exportieren...",
|
||||||
"export_options": "Export-Einstellungen",
|
"export_options": "Export-Einstellungen",
|
||||||
"support_message": "Das Tool darf frei benutzt werden, aber es darf nicht woanders aufgesetzt werden. Bitte unterstützen Sie die Website, wenn Sie sie häufig benutzen. Vielen Dank!",
|
"support_message": "Das Tool darf frei benutzt werden, aber es darf nicht woanders aufgesetzt werden. Bitte unterstützen Sie die Website, wenn Sie sie häufig benutzen. Vielen Dank!",
|
||||||
"support_button": "Hilf dabei, die Webseite kostenlos zu belassen",
|
"support_button": "Hilf uns, die Website weiterhin kostenlos bereitzustellen",
|
||||||
"download_file": "Datei herunterladen",
|
"download_file": "Datei herunterladen",
|
||||||
"download_files": "Dateien herunterladen",
|
"download_files": "Dateien herunterladen",
|
||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
"undo": "Rückgängig",
|
"undo": "Rückgängig",
|
||||||
"redo": "Wiederholen",
|
"redo": "Wiederholen",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"delete_all": "Delete all",
|
"delete_all": "Alle löschen",
|
||||||
"select_all": "Alle auswählen",
|
"select_all": "Alle auswählen",
|
||||||
"view": "Ansicht",
|
"view": "Ansicht",
|
||||||
"elevation_profile": "Höhenprofil",
|
"elevation_profile": "Höhenprofil",
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
"center": "Zentrieren",
|
"center": "Zentrieren",
|
||||||
"open_in": "Öffnen in",
|
"open_in": "Öffnen in",
|
||||||
"copy_coordinates": "Koordinaten kopieren",
|
"copy_coordinates": "Koordinaten kopieren",
|
||||||
"edit_osm": "Edit in OpenStreetMap"
|
"edit_osm": "In OpenStreetMap bearbeiten"
|
||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"routing": {
|
"routing": {
|
||||||
@@ -282,7 +282,7 @@
|
|||||||
"update": "Layer aktualisieren"
|
"update": "Layer aktualisieren"
|
||||||
},
|
},
|
||||||
"opacity": "Deckkraft der Überlagerung",
|
"opacity": "Deckkraft der Überlagerung",
|
||||||
"terrain": "Terrain source",
|
"terrain": "Geländequelle",
|
||||||
"label": {
|
"label": {
|
||||||
"basemaps": "Basiskarte",
|
"basemaps": "Basiskarte",
|
||||||
"overlays": "Ebenen",
|
"overlays": "Ebenen",
|
||||||
@@ -326,7 +326,7 @@
|
|||||||
"usgs": "USGS",
|
"usgs": "USGS",
|
||||||
"bikerouterGravel": "bikerouter.de Gravel",
|
"bikerouterGravel": "bikerouter.de Gravel",
|
||||||
"cyclOSMlite": "CyclOSM Lite",
|
"cyclOSMlite": "CyclOSM Lite",
|
||||||
"mapterhornHillshade": "Mapterhorn Hillshade",
|
"mapterhornHillshade": "MapTiler Hillshade",
|
||||||
"openRailwayMap": "OpenRailwayMap",
|
"openRailwayMap": "OpenRailwayMap",
|
||||||
"swisstopoSlope": "swisstopo Neigung",
|
"swisstopoSlope": "swisstopo Neigung",
|
||||||
"swisstopoHiking": "swisstopo Wandern",
|
"swisstopoHiking": "swisstopo Wandern",
|
||||||
@@ -356,7 +356,7 @@
|
|||||||
"water": "Trinkwasser",
|
"water": "Trinkwasser",
|
||||||
"shower": "Dusche",
|
"shower": "Dusche",
|
||||||
"shelter": "Unterstand",
|
"shelter": "Unterstand",
|
||||||
"cemetery": "Cemetery",
|
"cemetery": "Friedhof",
|
||||||
"motorized": "Autos und Motorräder",
|
"motorized": "Autos und Motorräder",
|
||||||
"fuel-station": "Tankstelle",
|
"fuel-station": "Tankstelle",
|
||||||
"parking": "Parken",
|
"parking": "Parken",
|
||||||
|
|||||||
@@ -231,7 +231,7 @@
|
|||||||
},
|
},
|
||||||
"elevation": {
|
"elevation": {
|
||||||
"button": "Request elevation data",
|
"button": "Request elevation data",
|
||||||
"help": "Requesting elevation data will erase the existing elevation data, if any, and replace it with data from MapTiler.",
|
"help": "Requesting elevation data will erase the existing elevation data, if any, and replace it with data from Mapbox.",
|
||||||
"help_no_selection": "Select a file item to request elevation data."
|
"help_no_selection": "Select a file item to request elevation data."
|
||||||
},
|
},
|
||||||
"waypoint": {
|
"waypoint": {
|
||||||
@@ -273,7 +273,7 @@
|
|||||||
"new": "New custom layer",
|
"new": "New custom layer",
|
||||||
"edit": "Edit custom layer",
|
"edit": "Edit custom layer",
|
||||||
"urls": "URL(s)",
|
"urls": "URL(s)",
|
||||||
"url_placeholder": "WMTS, WMS or MapLibre style JSON",
|
"url_placeholder": "WMTS, WMS or Mapbox style JSON",
|
||||||
"max_zoom": "Max zoom",
|
"max_zoom": "Max zoom",
|
||||||
"layer_type": "Layer type",
|
"layer_type": "Layer type",
|
||||||
"basemap": "Basemap",
|
"basemap": "Basemap",
|
||||||
@@ -300,9 +300,8 @@
|
|||||||
"switzerland": "Switzerland",
|
"switzerland": "Switzerland",
|
||||||
"united_kingdom": "United Kingdom",
|
"united_kingdom": "United Kingdom",
|
||||||
"united_states": "United States",
|
"united_states": "United States",
|
||||||
"maptilerTopo": "MapTiler Topo",
|
"mapboxOutdoors": "Mapbox Outdoors",
|
||||||
"maptilerOutdoors": "MapTiler Outdoors",
|
"mapboxSatellite": "Mapbox Satellite",
|
||||||
"maptilerSatellite": "MapTiler Satellite",
|
|
||||||
"openStreetMap": "OpenStreetMap",
|
"openStreetMap": "OpenStreetMap",
|
||||||
"openTopoMap": "OpenTopoMap",
|
"openTopoMap": "OpenTopoMap",
|
||||||
"openHikingMap": "OpenHikingMap",
|
"openHikingMap": "OpenHikingMap",
|
||||||
@@ -382,7 +381,7 @@
|
|||||||
"tram-stop": "Tram Stop",
|
"tram-stop": "Tram Stop",
|
||||||
"bus-stop": "Bus Stop",
|
"bus-stop": "Bus Stop",
|
||||||
"ferry": "Ferry",
|
"ferry": "Ferry",
|
||||||
"maptiler-dem": "MapTiler DEM",
|
"mapbox-dem": "Mapbox DEM",
|
||||||
"mapterhorn": "Mapterhorn"
|
"mapterhorn": "Mapterhorn"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -524,7 +523,7 @@
|
|||||||
},
|
},
|
||||||
"embedding": {
|
"embedding": {
|
||||||
"title": "Create your own map",
|
"title": "Create your own map",
|
||||||
"maptiler_key": "MapTiler key",
|
"mapbox_token": "Mapbox access token",
|
||||||
"file_urls": "File URLs (separated by commas)",
|
"file_urls": "File URLs (separated by commas)",
|
||||||
"drive_ids": "Google Drive file IDs (separated by commas)",
|
"drive_ids": "Google Drive file IDs (separated by commas)",
|
||||||
"basemap": "Basemap",
|
"basemap": "Basemap",
|
||||||
|
|||||||
@@ -282,7 +282,7 @@
|
|||||||
"update": "更新图层"
|
"update": "更新图层"
|
||||||
},
|
},
|
||||||
"opacity": "图层透明度",
|
"opacity": "图层透明度",
|
||||||
"terrain": "Terrain source",
|
"terrain": "地形来源",
|
||||||
"label": {
|
"label": {
|
||||||
"basemaps": "底图",
|
"basemaps": "底图",
|
||||||
"overlays": "叠加层",
|
"overlays": "叠加层",
|
||||||
@@ -326,7 +326,7 @@
|
|||||||
"usgs": "USGS",
|
"usgs": "USGS",
|
||||||
"bikerouterGravel": "bikerouter.de Gravel",
|
"bikerouterGravel": "bikerouter.de Gravel",
|
||||||
"cyclOSMlite": "CyclOSM Lite",
|
"cyclOSMlite": "CyclOSM Lite",
|
||||||
"mapterhornHillshade": "Mapterhorn Hillshade",
|
"mapterhornHillshade": "山体阴影",
|
||||||
"openRailwayMap": "OpenRailwayMap",
|
"openRailwayMap": "OpenRailwayMap",
|
||||||
"swisstopoSlope": "Swisstopo Slope",
|
"swisstopoSlope": "Swisstopo Slope",
|
||||||
"swisstopoHiking": "Swisstopo Hiking",
|
"swisstopoHiking": "Swisstopo Hiking",
|
||||||
|
|||||||
@@ -29,13 +29,12 @@
|
|||||||
data: {
|
data: {
|
||||||
fundingModule: Promise<any>;
|
fundingModule: Promise<any>;
|
||||||
translationModule: Promise<any>;
|
translationModule: Promise<any>;
|
||||||
maptilerModule: Promise<any>;
|
mapboxModule: Promise<any>;
|
||||||
};
|
};
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||||
let slicedGPXStatistics = writable(undefined);
|
let slicedGPXStatistics = writable(undefined);
|
||||||
let hoveredPoint = writable(null);
|
|
||||||
let additionalDatasets = writable(['speed', 'atemp']);
|
let additionalDatasets = writable(['speed', 'atemp']);
|
||||||
let elevationFill = writable(undefined);
|
let elevationFill = writable(undefined);
|
||||||
|
|
||||||
@@ -153,14 +152,14 @@
|
|||||||
class="relative w-full max-w-[320px] aspect-square rounded-2xl shadow-xl overflow-clip"
|
class="relative w-full max-w-[320px] aspect-square rounded-2xl shadow-xl overflow-clip"
|
||||||
>
|
>
|
||||||
<enhanced:img
|
<enhanced:img
|
||||||
src="/src/lib/assets/img/home/maptiler-topo.png"
|
src="/src/lib/assets/img/home/mapbox-outdoors.png"
|
||||||
alt="MapTiler Topo map screenshot."
|
alt="Mapbox Outdoors map screenshot."
|
||||||
class="absolute"
|
class="absolute"
|
||||||
style="clip-path: inset(0 50% 50% 0);"
|
style="clip-path: inset(0 50% 50% 0);"
|
||||||
/>
|
/>
|
||||||
<enhanced:img
|
<enhanced:img
|
||||||
src="/src/lib/assets/img/home/maptiler-satellite.png"
|
src="/src/lib/assets/img/home/mapbox-satellite.png"
|
||||||
alt="MapTiler Satellite map screenshot."
|
alt="Mapbox Satellite map screenshot."
|
||||||
class="absolute"
|
class="absolute"
|
||||||
style="clip-path: inset(0 0 50% 50%);"
|
style="clip-path: inset(0 0 50% 50%);"
|
||||||
/>
|
/>
|
||||||
@@ -198,7 +197,6 @@
|
|||||||
<ElevationProfile
|
<ElevationProfile
|
||||||
{gpxStatistics}
|
{gpxStatistics}
|
||||||
{slicedGPXStatistics}
|
{slicedGPXStatistics}
|
||||||
{hoveredPoint}
|
|
||||||
{additionalDatasets}
|
{additionalDatasets}
|
||||||
{elevationFill}
|
{elevationFill}
|
||||||
/>
|
/>
|
||||||
@@ -282,12 +280,12 @@
|
|||||||
<div class="text-lg font-semibold text-muted-foreground">
|
<div class="text-lg font-semibold text-muted-foreground">
|
||||||
❤️ {i18n._('homepage.supported_by')}
|
❤️ {i18n._('homepage.supported_by')}
|
||||||
</div>
|
</div>
|
||||||
<a href="https://www.maptiler.com/" target="_blank">
|
<a href="https://www.mapbox.com/" target="_blank">
|
||||||
<Logo company="maptiler" class="w-60" />
|
<Logo company="mapbox" class="w-60" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{#await data.maptilerModule then maptilerModule}
|
{#await data.mapboxModule then mapboxModule}
|
||||||
<DocsContainer module={maptilerModule.default} />
|
<DocsContainer module={mapboxModule.default} />
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ export async function load({ params }) {
|
|||||||
return {
|
return {
|
||||||
fundingModule: getModule(language, 'funding'),
|
fundingModule: getModule(language, 'funding'),
|
||||||
translationModule: getModule(language, 'translation'),
|
translationModule: getModule(language, 'translation'),
|
||||||
maptilerModule: getModule(language, 'maptiler'),
|
mapboxModule: getModule(language, 'mapbox'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
import { loadFiles } from '$lib/logic/file-actions';
|
import { loadFiles } from '$lib/logic/file-actions';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { gpxStatistics, hoveredPoint, slicedGPXStatistics } from '$lib/logic/statistics';
|
import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
|
||||||
import { getURLForGoogleDriveFile } from '$lib/components/embedding/embedding';
|
import { getURLForGoogleDriveFile } from '$lib/components/embedding/embedding';
|
||||||
import { db } from '$lib/db';
|
import { db } from '$lib/db';
|
||||||
import { fileStateCollection } from '$lib/logic/file-state';
|
import { fileStateCollection } from '$lib/logic/file-state';
|
||||||
@@ -140,7 +140,6 @@
|
|||||||
<ElevationProfile
|
<ElevationProfile
|
||||||
{gpxStatistics}
|
{gpxStatistics}
|
||||||
{slicedGPXStatistics}
|
{slicedGPXStatistics}
|
||||||
{hoveredPoint}
|
|
||||||
{additionalDatasets}
|
{additionalDatasets}
|
||||||
{elevationFill}
|
{elevationFill}
|
||||||
/>
|
/>
|
||||||
|
|||||||
38
website/static/mapbox-logo-black.svg
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 21.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="new" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 800 180" style="enable-background:new 0 0 800 180;" xml:space="preserve">
|
||||||
|
<title>Mapbox_Logo_08</title>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M594.6,49.8c-9.9,0-19.4,4.1-26.3,11.3V23c0-1.2-1-2.2-2.2-2.2l0,0h-13.4c-1.2,0-2.2,1-2.2,2.2v103c0,1.2,1,2.2,2.2,2.2
|
||||||
|
h13.4c1.2,0,2.2-1,2.2-2.2v0v-7.1c6.9,7.2,16.3,11.3,26.3,11.3c20.9,0,37.8-18,37.8-40.2S615.5,49.8,594.6,49.8z M591.5,114.1
|
||||||
|
c-12.7,0-23-10.6-23.1-23.8v-0.6c0.2-13.2,10.4-23.8,23.1-23.8c12.8,0,23.1,10.8,23.1,24.1S604.2,114.1,591.5,114.1L591.5,114.1z"
|
||||||
|
/>
|
||||||
|
<path d="M681.7,49.8c-22.6,0-40.9,18-40.9,40.2s18.3,40.2,40.9,40.2c22.6,0,40.9-18,40.9-40.2S704.3,49.8,681.7,49.8z
|
||||||
|
M681.6,114.1c-12.8,0-23.1-10.8-23.1-24.1s10.4-24.1,23.1-24.1s23.1,10.8,23.1,24.1S694.3,114.1,681.6,114.1L681.6,114.1z"/>
|
||||||
|
<path d="M431.6,51.8h-13.4c-1.2,0-2.2,1-2.2,2.2c0,0,0,0,0,0v7.1c-6.9-7.2-16.3-11.3-26.3-11.3c-20.9,0-37.8,18-37.8,40.2
|
||||||
|
s16.9,40.2,37.8,40.2c9.9,0,19.4-4.1,26.3-11.3v7.1c0,1.2,1,2.2,2.2,2.2l0,0h13.4c1.2,0,2.2-1,2.2-2.2v0V54
|
||||||
|
C433.8,52.8,432.8,51.8,431.6,51.8z M392.8,114.1c-12.8,0-23.1-10.8-23.1-24.1s10.4-24.1,23.1-24.1c12.7,0,23,10.6,23.1,23.8v0.6
|
||||||
|
C415.8,103.5,405.5,114.1,392.8,114.1L392.8,114.1z"/>
|
||||||
|
<path d="M498.5,49.8c-9.9,0-19.4,4.1-26.3,11.3V54c0-1.2-1-2.2-2.2-2.2l0,0h-13.4c-1.2,0-2.2,1-2.2,2.2c0,0,0,0,0,0v103
|
||||||
|
c0,1.2,1,2.2,2.2,2.2l0,0h13.4c1.2,0,2.2-1,2.2-2.2v0v-38.1c6.9,7.2,16.3,11.3,26.3,11.3c20.9,0,37.8-18,37.8-40.2
|
||||||
|
S519.4,49.8,498.5,49.8z M495.4,114.1c-12.7,0-23-10.6-23.1-23.8v-0.6c0.2-13.2,10.4-23.8,23.1-23.8c12.8,0,23.1,10.8,23.1,24.1
|
||||||
|
S508.2,114.1,495.4,114.1L495.4,114.1z"/>
|
||||||
|
<path d="M311.8,49.8c-10,0.1-19.1,5.9-23.4,15c-4.9-9.3-14.7-15.1-25.2-15c-8.2,0-15.9,4-20.7,10.6V54c0-1.2-1-2.2-2.2-2.2l0,0
|
||||||
|
h-13.4c-1.2,0-2.2,1-2.2,2.2c0,0,0,0,0,0v72c0,1.2,1,2.2,2.2,2.2h0h13.4c1.2,0,2.2-1,2.2-2.2v0V82.9c0.5-9.6,7.2-17.3,15.4-17.3
|
||||||
|
c8.5,0,15.6,7.1,15.6,16.4v44c0,1.2,1,2.2,2.2,2.2l13.5,0c1.2,0,2.2-1,2.2-2.2c0,0,0,0,0,0l-0.1-44.8c1.2-8.8,7.5-15.6,15.2-15.6
|
||||||
|
c8.5,0,15.6,7.1,15.6,16.4v44c0,1.2,1,2.2,2.2,2.2l13.5,0c1.2,0,2.2-1,2.2-2.2c0,0,0,0,0,0l-0.1-49.5
|
||||||
|
C339.9,61.7,327.3,49.8,311.8,49.8z"/>
|
||||||
|
<path d="M794.7,125.1l-23.2-35.3l23-35c0.6-0.9,0.3-2.2-0.6-2.8c-0.3-0.2-0.7-0.3-1.1-0.3h-15.5c-1.2,0-2.3,0.6-2.9,1.6L760.9,76
|
||||||
|
l-13.5-22.6c-0.6-1-1.7-1.6-2.9-1.6h-15.5c-1.1,0-2,0.9-2,2c0,0.4,0.1,0.8,0.3,1.1l23,35l-23.2,35.3c-0.6,0.9-0.3,2.2,0.6,2.8
|
||||||
|
c0.3,0.2,0.7,0.3,1.1,0.3h15.5c1.2,0,2.3-0.6,2.9-1.6l13.8-23l13.8,23c0.6,1,1.7,1.6,2.9,1.6H793c1.1,0,2-0.9,2-2
|
||||||
|
C795,125.9,794.9,125.5,794.7,125.1z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path d="M93.9,1.1C44.8,1.1,5,40.9,5,90s39.8,88.9,88.9,88.9s88.9-39.8,88.9-88.9C182.8,40.9,143,1.1,93.9,1.1z M136.1,111.8
|
||||||
|
c-30.4,30.4-84.7,20.7-84.7,20.7s-9.8-54.2,20.7-84.7C89,30.9,117,31.6,134.7,49.2S153,94.9,136.1,111.8L136.1,111.8z"/>
|
||||||
|
<polygon points="104.1,53.2 95.4,71.1 77.5,79.8 95.4,88.5 104.1,106.4 112.8,88.5 130.7,79.8 112.8,71.1 "/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
42
website/static/mapbox-logo-white.svg
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 21.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="new" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 800 180" style="enable-background:new 0 0 800 180;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<title>Mapbox_Logo_08</title>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M594.6,49.8c-9.9,0-19.4,4.1-26.3,11.3V23c0-1.2-1-2.2-2.2-2.2l0,0h-13.4c-1.2,0-2.2,1-2.2,2.2v103
|
||||||
|
c0,1.2,1,2.2,2.2,2.2h13.4c1.2,0,2.2-1,2.2-2.2v0v-7.1c6.9,7.2,16.3,11.3,26.3,11.3c20.9,0,37.8-18,37.8-40.2
|
||||||
|
S615.5,49.8,594.6,49.8z M591.5,114.1c-12.7,0-23-10.6-23.1-23.8v-0.6c0.2-13.2,10.4-23.8,23.1-23.8c12.8,0,23.1,10.8,23.1,24.1
|
||||||
|
S604.2,114.1,591.5,114.1L591.5,114.1z"/>
|
||||||
|
<path class="st0" d="M681.7,49.8c-22.6,0-40.9,18-40.9,40.2s18.3,40.2,40.9,40.2c22.6,0,40.9-18,40.9-40.2S704.3,49.8,681.7,49.8z
|
||||||
|
M681.6,114.1c-12.8,0-23.1-10.8-23.1-24.1s10.4-24.1,23.1-24.1s23.1,10.8,23.1,24.1S694.3,114.1,681.6,114.1L681.6,114.1z"/>
|
||||||
|
<path class="st0" d="M431.6,51.8h-13.4c-1.2,0-2.2,1-2.2,2.2c0,0,0,0,0,0v7.1c-6.9-7.2-16.3-11.3-26.3-11.3
|
||||||
|
c-20.9,0-37.8,18-37.8,40.2s16.9,40.2,37.8,40.2c9.9,0,19.4-4.1,26.3-11.3v7.1c0,1.2,1,2.2,2.2,2.2l0,0h13.4c1.2,0,2.2-1,2.2-2.2
|
||||||
|
v0V54C433.8,52.8,432.8,51.8,431.6,51.8z M392.8,114.1c-12.8,0-23.1-10.8-23.1-24.1s10.4-24.1,23.1-24.1c12.7,0,23,10.6,23.1,23.8
|
||||||
|
v0.6C415.8,103.5,405.5,114.1,392.8,114.1L392.8,114.1z"/>
|
||||||
|
<path class="st0" d="M498.5,49.8c-9.9,0-19.4,4.1-26.3,11.3V54c0-1.2-1-2.2-2.2-2.2l0,0h-13.4c-1.2,0-2.2,1-2.2,2.2c0,0,0,0,0,0
|
||||||
|
v103c0,1.2,1,2.2,2.2,2.2l0,0h13.4c1.2,0,2.2-1,2.2-2.2v0v-38.1c6.9,7.2,16.3,11.3,26.3,11.3c20.9,0,37.8-18,37.8-40.2
|
||||||
|
S519.4,49.8,498.5,49.8z M495.4,114.1c-12.7,0-23-10.6-23.1-23.8v-0.6c0.2-13.2,10.4-23.8,23.1-23.8c12.8,0,23.1,10.8,23.1,24.1
|
||||||
|
S508.2,114.1,495.4,114.1L495.4,114.1z"/>
|
||||||
|
<path class="st0" d="M311.8,49.8c-10,0.1-19.1,5.9-23.4,15c-4.9-9.3-14.7-15.1-25.2-15c-8.2,0-15.9,4-20.7,10.6V54
|
||||||
|
c0-1.2-1-2.2-2.2-2.2l0,0h-13.4c-1.2,0-2.2,1-2.2,2.2c0,0,0,0,0,0v72c0,1.2,1,2.2,2.2,2.2h0h13.4c1.2,0,2.2-1,2.2-2.2v0V82.9
|
||||||
|
c0.5-9.6,7.2-17.3,15.4-17.3c8.5,0,15.6,7.1,15.6,16.4v44c0,1.2,1,2.2,2.2,2.2l13.5,0c1.2,0,2.2-1,2.2-2.2c0,0,0,0,0,0l-0.1-44.8
|
||||||
|
c1.2-8.8,7.5-15.6,15.2-15.6c8.5,0,15.6,7.1,15.6,16.4v44c0,1.2,1,2.2,2.2,2.2l13.5,0c1.2,0,2.2-1,2.2-2.2c0,0,0,0,0,0l-0.1-49.5
|
||||||
|
C339.9,61.7,327.3,49.8,311.8,49.8z"/>
|
||||||
|
<path class="st0" d="M794.7,125.1l-23.2-35.3l23-35c0.6-0.9,0.3-2.2-0.6-2.8c-0.3-0.2-0.7-0.3-1.1-0.3h-15.5
|
||||||
|
c-1.2,0-2.3,0.6-2.9,1.6L760.9,76l-13.5-22.6c-0.6-1-1.7-1.6-2.9-1.6h-15.5c-1.1,0-2,0.9-2,2c0,0.4,0.1,0.8,0.3,1.1l23,35
|
||||||
|
l-23.2,35.3c-0.6,0.9-0.3,2.2,0.6,2.8c0.3,0.2,0.7,0.3,1.1,0.3h15.5c1.2,0,2.3-0.6,2.9-1.6l13.8-23l13.8,23c0.6,1,1.7,1.6,2.9,1.6
|
||||||
|
H793c1.1,0,2-0.9,2-2C795,125.9,794.9,125.5,794.7,125.1z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M93.9,1.1C44.8,1.1,5,40.9,5,90s39.8,88.9,88.9,88.9s88.9-39.8,88.9-88.9C182.8,40.9,143,1.1,93.9,1.1z
|
||||||
|
M136.1,111.8c-30.4,30.4-84.7,20.7-84.7,20.7s-9.8-54.2,20.7-84.7C89,30.9,117,31.6,134.7,49.2S153,94.9,136.1,111.8L136.1,111.8
|
||||||
|
z"/>
|
||||||
|
<polygon class="st0" points="104.1,53.2 95.4,71.1 77.5,79.8 95.4,88.5 104.1,106.4 112.8,88.5 130.7,79.8 112.8,71.1 "/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -1,47 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 23.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Vrstva_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
viewBox="0 0 812 212" style="enable-background:new 0 0 812 212;" xml:space="preserve">
|
|
||||||
<style type="text/css">
|
|
||||||
.st0{fill:#3A1888;}
|
|
||||||
.st1{fill:#03A1C4;}
|
|
||||||
.st2{fill:#05D0DF;}
|
|
||||||
.st3{fill:#761FE8;}
|
|
||||||
.st4{fill:#FFAA01;}
|
|
||||||
.st5{fill:#F1175D;}
|
|
||||||
.st6{fill:#FB3A1B;}
|
|
||||||
.st7{fill:#FBC935;}
|
|
||||||
.st8{fill:#FFFFFF;}
|
|
||||||
</style>
|
|
||||||
<g>
|
|
||||||
<path class="st0" d="M94.3,164.2c9.2,9.2,33.8,34.3,33.8,34.3c-0.1,0.2,24.4-24.5,34.2-34.2l-34.1-34.1L94.3,164.2z"/>
|
|
||||||
<path class="st1" d="M128.3,130.2l34.1,34.1c0.1-0.1,0.1-0.1,0.2-0.2l34-34L162.5,96L128.3,130.2z"/>
|
|
||||||
<path class="st2" d="M196.6,130.1L196.6,130.1c18.9-18.9,18.9-49.4,0.1-68.3L162.5,96L196.6,130.1z"/>
|
|
||||||
<path class="st3" d="M94.1,96l-34,34c0,0,0,0,0,0l34.1,34.1c0,0,0,0,0.1,0.1l34-34L94.1,96z"/>
|
|
||||||
<path class="st4" d="M128.3,61.8L162.5,96l34.2-34.2c0,0,0,0-0.1-0.1l-34.1-34.1c0,0,0,0,0,0L128.3,61.8z"/>
|
|
||||||
<path class="st5" d="M60,61.9c-18.7,18.9-18.6,49.3,0.1,68.1l34-34L60,61.9z"/>
|
|
||||||
<path class="st6" d="M128.3,61.8L94.2,27.7l-34,34c-0.1,0.1-0.1,0.1-0.2,0.2L94.1,96L128.3,61.8z"/>
|
|
||||||
<path class="st7" d="M162.5,27.6c-18.9-18.8-49.4-18.8-68.2,0l-0.1,0.1l34.1,34.1L162.5,27.6z"/>
|
|
||||||
</g>
|
|
||||||
<path class="st8" d="M303.8,138.6v-34.9c0-8.6-4.5-16.4-13.3-16.4c-8.7,0-13.9,7.8-13.9,16.4v34.9h-16.1V73.4h14.9l1.2,7.9
|
|
||||||
c3.4-6.6,11-9,17.2-9c7.8,0,15.6,3.2,19.3,12.2c5.8-9.2,13.3-11.9,21.8-11.9c18.5,0,27.6,11.4,27.6,30.9v35.1h-16.1v-35.1
|
|
||||||
c0-8.6-3.6-15.9-12.3-15.9c-8.7,0-14.1,7.5-14.1,16.1v34.9H303.8z"/>
|
|
||||||
<path class="st8" d="M430.5,73.5h15.5v65.1h-15.2l-0.8-9.5c-3.7,7.7-13.9,11.4-21.1,11.5c-19.3,0.1-33.6-11.8-33.6-34.6
|
|
||||||
c0-22.5,14.9-34.2,34-34.1c8.7,0,17,4.1,20.7,10.6L430.5,73.5z M391.4,106c0,12.4,8.6,19.8,19.3,19.8c25.4,0,25.4-39.5,0-39.5
|
|
||||||
C399.9,86.3,391.4,93.6,391.4,106z"/>
|
|
||||||
<path class="st8" d="M459.5,165.8V73.5h15.1l1.1,9c5-7.3,13.7-10.4,21.1-10.4c20.1,0,33.4,14.9,33.4,34.1c0,19-12,34.1-32.9,34.1
|
|
||||||
c-6.9,0-17-2.1-21.7-9.3v34.9H459.5z M514.1,106.1c0-10.2-6.9-18.5-18.5-18.5c-11.6,0-18.5,8.3-18.5,18.5c0,10.2,7.5,18.5,18.5,18.5
|
|
||||||
C506.6,124.6,514.1,116.3,514.1,106.1z"/>
|
|
||||||
<path class="st8" d="M559,53.7v19.7h22.2v5.4H559v39.8c0,8.8,1.9,15.1,12,15.1c3.2,0,6.7-1.1,10-2.6l2.2,5.3
|
|
||||||
c-4.1,2-8.2,3.3-12.3,3.3c-13.9,0-18.4-8.2-18.4-21V78.8h-13.9v-5.4h13.9v-19L559,53.7z"/>
|
|
||||||
<path class="st8" d="M604.7,52.1c0,6.9-10.4,6.9-10.4,0C594.3,45.2,604.7,45.2,604.7,52.1z M596.1,73.1v65.5h6.5V73.1H596.1z"/>
|
|
||||||
<path class="st8" d="M627.6,46.2v92.5h-6.5V46.2H627.6z"/>
|
|
||||||
<path class="st8" d="M730.2,73.4l0.3,11.6c4.1-8.9,13.3-12.3,21.7-12.3c4.9-0.1,9.6,1.2,14,3.8l-2.9,5.3c-3.4-2.1-7.3-3-11.1-3
|
|
||||||
c-12.2,0.1-21.5,9.9-21.5,21.8v38h-6.5V73.4H730.2z"/>
|
|
||||||
<g>
|
|
||||||
<path class="st8" d="M675.1,134.7c-11.5,0-21.4-7.2-25.5-17.4l0,0l0,0c0,0,0,0,0,0l52.8-14c0,0,0,0,0,0.1l5.6-1.5
|
|
||||||
c-2.3-16.5-16.2-29.3-33-29.3c-18.4,0-33.3,15.2-33.3,34c0,18.8,14.9,34,33.3,34c13.8,0,25.6-8.5,30.6-20.7l-5.3-2.3
|
|
||||||
C696.2,127.6,686.4,134.7,675.1,134.7z M647.5,106.6c0-15.5,12.3-28.1,27.5-28.1c11.9,0,22,7.7,25.9,18.5L647.9,111
|
|
||||||
C647.6,109.5,647.5,108.1,647.5,106.6z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -1,45 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 23.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Vrstva_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
viewBox="0 0 812 212" style="enable-background:new 0 0 812 212;" xml:space="preserve">
|
|
||||||
<style type="text/css">
|
|
||||||
.st0{fill:#3A1888;}
|
|
||||||
.st1{fill:#03A1C4;}
|
|
||||||
.st2{fill:#05D0DF;}
|
|
||||||
.st3{fill:#761FE8;}
|
|
||||||
.st4{fill:#FFAA01;}
|
|
||||||
.st5{fill:#F1175D;}
|
|
||||||
.st6{fill:#FB3A1B;}
|
|
||||||
.st7{fill:#FBC935;}
|
|
||||||
.st8{fill:#333359;}
|
|
||||||
</style>
|
|
||||||
<path class="st0" d="M94.3,164.2c9.2,9.2,33.8,34.3,33.8,34.3c-0.1,0.2,24.4-24.5,34.2-34.2l-34.1-34.1L94.3,164.2z"/>
|
|
||||||
<path class="st1" d="M128.3,130.2l34.1,34.1c0.1-0.1,0.1-0.1,0.2-0.2l34-34L162.5,96L128.3,130.2z"/>
|
|
||||||
<path class="st2" d="M196.6,130.1L196.6,130.1c18.9-18.9,18.9-49.4,0.1-68.3L162.5,96L196.6,130.1z"/>
|
|
||||||
<path class="st3" d="M94.1,96l-34,34c0,0,0,0,0,0l34.1,34.1c0,0,0,0,0.1,0.1l34-34L94.1,96z"/>
|
|
||||||
<path class="st4" d="M128.3,61.8L162.5,96l34.2-34.2c0,0,0,0-0.1-0.1l-34.1-34.1c0,0,0,0,0,0L128.3,61.8z"/>
|
|
||||||
<path class="st5" d="M60,61.9c-18.7,18.9-18.6,49.3,0.1,68.1l34-34L60,61.9z"/>
|
|
||||||
<path class="st6" d="M128.3,61.8L94.2,27.7l-34,34c-0.1,0.1-0.1,0.1-0.2,0.2L94.1,96L128.3,61.8z"/>
|
|
||||||
<path class="st7" d="M162.5,27.6c-18.9-18.8-49.4-18.8-68.2,0l-0.1,0.1l34.1,34.1L162.5,27.6z"/>
|
|
||||||
<path class="st8" d="M303.7,138.6v-34.9c0-8.6-4.5-16.4-13.3-16.4c-8.7,0-13.9,7.8-13.9,16.4v34.9h-16.1V73.4h14.9l1.2,7.9
|
|
||||||
c3.4-6.6,11-9,17.2-9c7.8,0,15.6,3.2,19.3,12.2c5.8-9.2,13.3-11.9,21.8-11.9c18.5,0,27.6,11.4,27.6,30.9v35.1h-16.1v-35.1
|
|
||||||
c0-8.6-3.6-15.9-12.3-15.9c-8.7,0-14.1,7.5-14.1,16.1v34.9H303.7z"/>
|
|
||||||
<path class="st8" d="M430.3,73.5h15.5v65.1h-15.2l-0.8-9.5c-3.7,7.7-13.9,11.4-21.1,11.5c-19.3,0.1-33.6-11.8-33.6-34.6
|
|
||||||
c0-22.5,14.9-34.2,34-34.1c8.7,0,17,4.1,20.7,10.6L430.3,73.5z M391.2,106c0,12.4,8.6,19.8,19.3,19.8c25.4,0,25.4-39.5,0-39.5
|
|
||||||
C399.8,86.3,391.2,93.6,391.2,106z"/>
|
|
||||||
<path class="st8" d="M459.4,165.8V73.5h15.1l1.1,9c5-7.3,13.7-10.4,21.1-10.4c20.1,0,33.4,14.9,33.4,34.1c0,19-12,34.1-32.9,34.1
|
|
||||||
c-6.9,0-17-2.1-21.7-9.3v34.9H459.4z M514,106.1c0-10.2-6.9-18.5-18.5-18.5c-11.6,0-18.5,8.3-18.5,18.5c0,10.2,7.5,18.5,18.5,18.5
|
|
||||||
C506.4,124.6,514,116.3,514,106.1z"/>
|
|
||||||
<path class="st8" d="M558.9,53.7v19.7h22.2v5.4h-22.2v39.8c0,8.8,1.9,15.1,12,15.1c3.2,0,6.7-1.1,10-2.6l2.2,5.3
|
|
||||||
c-4.1,2-8.2,3.3-12.3,3.3c-13.9,0-18.4-8.2-18.4-21V78.8h-13.9v-5.4h13.9v-19L558.9,53.7z"/>
|
|
||||||
<path class="st8" d="M604.6,52.1c0,6.9-10.4,6.9-10.4,0C594.1,45.2,604.6,45.2,604.6,52.1z M596,73.1v65.5h6.5V73.1H596z"/>
|
|
||||||
<path class="st8" d="M627.4,46.2v92.5H621V46.2H627.4z"/>
|
|
||||||
<path class="st8" d="M730.1,73.4l0.3,11.6c4.1-8.9,13.3-12.3,21.7-12.3c4.9-0.1,9.6,1.2,14,3.8l-2.9,5.3c-3.4-2.1-7.3-3-11.1-3
|
|
||||||
c-12.2,0.1-21.5,9.9-21.5,21.8v38H724V73.4H730.1z"/>
|
|
||||||
<g>
|
|
||||||
<path class="st8" d="M674.9,134.7c-11.5,0-21.4-7.2-25.5-17.4l0,0l0,0c0,0,0,0,0,0l52.8-14c0,0,0,0,0,0.1l5.6-1.5
|
|
||||||
c-2.3-16.5-16.2-29.3-33-29.3c-18.4,0-33.3,15.2-33.3,34c0,18.8,14.9,34,33.3,34c13.8,0,25.6-8.5,30.6-20.7l-5.3-2.3
|
|
||||||
C696.1,127.6,686.3,134.7,674.9,134.7z M647.4,106.6c0-15.5,12.3-28.1,27.5-28.1c11.9,0,22,7.7,25.9,18.5L647.7,111
|
|
||||||
C647.5,109.5,647.4,108.1,647.4,106.6z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.1 KiB |