mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-03-13 16:22:59 +00:00
Compare commits
14 Commits
e96b544a75
...
maplibre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9ca75e2e8 | ||
|
|
091f6a3ed0 | ||
|
|
d6c9fb1025 | ||
|
|
88abd72a41 | ||
|
|
1137e851ce | ||
|
|
b8c1500aad | ||
|
|
bfd0d90abc | ||
|
|
dba01e1826 | ||
|
|
2189c76edd | ||
|
|
6f8c9d66db | ||
|
|
9408ce10c7 | ||
|
|
9895c3c304 | ||
|
|
0ab3b77db8 | ||
|
|
d13e7e7a0a |
12
website/package-lock.json
generated
12
website/package-lock.json
generated
@@ -13,7 +13,6 @@
|
|||||||
"@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",
|
"@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",
|
||||||
@@ -38,9 +37,9 @@
|
|||||||
"@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/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",
|
||||||
@@ -2631,7 +2630,8 @@
|
|||||||
"node_modules/@types/mapbox__sphericalmercator": {
|
"node_modules/@types/mapbox__sphericalmercator": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mapbox__sphericalmercator/-/mapbox__sphericalmercator-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mapbox__sphericalmercator/-/mapbox__sphericalmercator-1.2.3.tgz",
|
||||||
"integrity": "sha512-gBXMMNhRTA8HzAzLdBzVYET0dH1p8jDPYZoT9+KnfFRYIRwHnbW+3IyiSlwS7kvr97PMn501QY+Dd3kjxb2dAA=="
|
"integrity": "sha512-gBXMMNhRTA8HzAzLdBzVYET0dH1p8jDPYZoT9+KnfFRYIRwHnbW+3IyiSlwS7kvr97PMn501QY+Dd3kjxb2dAA==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/mapbox__tilebelt": {
|
"node_modules/@types/mapbox__tilebelt": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
@@ -2672,12 +2672,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
|
||||||
"integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA=="
|
"integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/png.js": {
|
|
||||||
"version": "0.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/png.js/-/png.js-0.2.3.tgz",
|
|
||||||
"integrity": "sha512-7F2LTRf/WJDpw9n9cPKi096a+gAdY2LS7WGlaEbGD7YZ7F7+t5tHRNHgvNvvigP6cZyjNW2Qsi/aR6AC2XN9xQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/polylabel": {
|
"node_modules/@types/polylabel": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/polylabel/-/polylabel-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/polylabel/-/polylabel-1.1.3.tgz",
|
||||||
|
|||||||
@@ -31,13 +31,13 @@
|
|||||||
|
|
||||||
<Card.Root
|
<Card.Root
|
||||||
class="h-full {orientation === 'vertical'
|
class="h-full {orientation === 'vertical'
|
||||||
? 'min-w-40 sm:min-w-44 text-sm sm:text-base'
|
? 'min-w-40 sm:min-w-44'
|
||||||
: 'w-full'} border-none shadow-none p-0"
|
: 'w-full h-10'} border-none shadow-none p-0 text-sm sm:text-base"
|
||||||
>
|
>
|
||||||
<Card.Content
|
<Card.Content
|
||||||
class="h-full flex {orientation === 'vertical'
|
class="h-full flex {orientation === 'vertical'
|
||||||
? 'flex-col justify-center'
|
? 'flex-col justify-center'
|
||||||
: 'flex-row w-full justify-between'} gap-4 p-0"
|
: 'flex-row w-full justify-evenly'} gap-4 p-0"
|
||||||
>
|
>
|
||||||
<Tooltip label={i18n._('quantities.distance')}>
|
<Tooltip label={i18n._('quantities.distance')}>
|
||||||
<span class="flex flex-row items-center">
|
<span class="flex flex-row items-center">
|
||||||
|
|||||||
@@ -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 { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
import type { Coordinates, 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,12 +28,14 @@
|
|||||||
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;
|
||||||
@@ -47,6 +49,7 @@
|
|||||||
elevationProfile = new ElevationProfile(
|
elevationProfile = new ElevationProfile(
|
||||||
gpxStatistics,
|
gpxStatistics,
|
||||||
slicedGPXStatistics,
|
slicedGPXStatistics,
|
||||||
|
hoveredPoint,
|
||||||
additionalDatasets,
|
additionalDatasets,
|
||||||
elevationFill,
|
elevationFill,
|
||||||
canvas,
|
canvas,
|
||||||
@@ -61,7 +64,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="h-full grow min-w-0 relative py-2">
|
<div class="h-full grow min-w-0 min-h-0 relative">
|
||||||
<canvas bind:this={overlay} class="w-full h-full absolute pointer-events-none"></canvas>
|
<canvas bind:this={overlay} class="w-full h-full absolute pointer-events-none"></canvas>
|
||||||
<canvas bind:this={canvas} class="w-full h-full absolute"></canvas>
|
<canvas bind:this={canvas} class="w-full h-full absolute"></canvas>
|
||||||
{#if showControls}
|
{#if showControls}
|
||||||
|
|||||||
@@ -20,10 +20,8 @@ import Chart, {
|
|||||||
type ScriptableLineSegmentContext,
|
type ScriptableLineSegmentContext,
|
||||||
type TooltipItem,
|
type TooltipItem,
|
||||||
} from 'chart.js/auto';
|
} from 'chart.js/auto';
|
||||||
import maplibregl from 'maplibre-gl';
|
|
||||||
import { get, type Readable, type Writable } from 'svelte/store';
|
import { get, type Readable, type Writable } from 'svelte/store';
|
||||||
import { map } from '$lib/components/map/map';
|
import type { Coordinates, GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
||||||
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';
|
||||||
|
|
||||||
@@ -42,7 +40,7 @@ interface ElevationProfilePoint {
|
|||||||
length: number;
|
length: number;
|
||||||
};
|
};
|
||||||
extensions: Record<string, any>;
|
extensions: Record<string, any>;
|
||||||
coordinates: [number, number];
|
coordinates: Coordinates;
|
||||||
index: number;
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,18 +48,19 @@ 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: maplibregl.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,
|
||||||
@@ -69,17 +68,12 @@ 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 maplibregl.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();
|
||||||
@@ -162,14 +156,10 @@ export class ElevationProfile {
|
|||||||
label: (context: TooltipItem<'line'>) => {
|
label: (context: TooltipItem<'line'>) => {
|
||||||
let point = context.raw as ElevationProfilePoint;
|
let point = context.raw as ElevationProfilePoint;
|
||||||
if (context.datasetIndex === 0) {
|
if (context.datasetIndex === 0) {
|
||||||
const map_ = get(map);
|
|
||||||
if (map_ && this._marker) {
|
|
||||||
if (this._dragging) {
|
if (this._dragging) {
|
||||||
this._marker.remove();
|
this._hoveredPoint.set(null);
|
||||||
} else {
|
} else {
|
||||||
this._marker.setLngLat(point.coordinates);
|
this._hoveredPoint.set(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) {
|
||||||
@@ -312,10 +302,7 @@ 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') {
|
||||||
const map_ = get(map);
|
this._hoveredPoint.set(null);
|
||||||
if (map_ && this._marker) {
|
|
||||||
this._marker.remove();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -637,8 +624,5 @@ 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, slicedGPXStatistics } from '$lib/logic/statistics';
|
import { gpxStatistics, hoveredPoint, 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' : ''}"
|
||||||
accessToken={options.token}
|
maptilerKey={options.key}
|
||||||
geocoder={false}
|
geocoder={false}
|
||||||
geolocate={true}
|
geolocate={true}
|
||||||
hash={useHash}
|
hash={useHash}
|
||||||
@@ -130,6 +130,7 @@
|
|||||||
<ElevationProfile
|
<ElevationProfile
|
||||||
{gpxStatistics}
|
{gpxStatistics}
|
||||||
{slicedGPXStatistics}
|
{slicedGPXStatistics}
|
||||||
|
{hoveredPoint}
|
||||||
{additionalDatasets}
|
{additionalDatasets}
|
||||||
{elevationFill}
|
{elevationFill}
|
||||||
showControls={options.elevation.controls}
|
showControls={options.elevation.controls}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
let options = $state(
|
let options = $state(
|
||||||
getMergedEmbeddingOptions(
|
getMergedEmbeddingOptions(
|
||||||
{
|
{
|
||||||
token: 'YOUR_MAPTILER_KEY',
|
key: 'YOUR_MAPTILER_KEY',
|
||||||
theme: mode.current,
|
theme: mode.current,
|
||||||
},
|
},
|
||||||
defaultEmbeddingOptions
|
defaultEmbeddingOptions
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
let iframeOptions = $derived(
|
let iframeOptions = $derived(
|
||||||
getMergedEmbeddingOptions(
|
getMergedEmbeddingOptions(
|
||||||
{
|
{
|
||||||
token:
|
key:
|
||||||
options.key.length === 0 || options.key === 'YOUR_MAPTILER_KEY'
|
options.key.length === 0 || options.key === 'YOUR_MAPTILER_KEY'
|
||||||
? PUBLIC_MAPTILER_KEY
|
? PUBLIC_MAPTILER_KEY
|
||||||
: options.key,
|
: options.key,
|
||||||
|
|||||||
@@ -5,6 +5,16 @@
|
|||||||
|
|
||||||
map.onLoad((map_) => {
|
map.onLoad((map_) => {
|
||||||
map_.on('contextmenu', (e) => {
|
map_.on('contextmenu', (e) => {
|
||||||
|
if (
|
||||||
|
map_.queryRenderedFeatures(e.point, {
|
||||||
|
layers: map_
|
||||||
|
.getLayersOrder()
|
||||||
|
.filter((layerId) => layerId.startsWith('routing-controls')),
|
||||||
|
}).length
|
||||||
|
) {
|
||||||
|
// Clicked on routing control, ignoring
|
||||||
|
return;
|
||||||
|
}
|
||||||
trackpointPopup?.setItem({
|
trackpointPopup?.setItem({
|
||||||
item: new TrackPoint({
|
item: new TrackPoint({
|
||||||
attributes: {
|
attributes: {
|
||||||
|
|||||||
@@ -129,12 +129,21 @@
|
|||||||
@apply relative;
|
@apply relative;
|
||||||
@apply top-0;
|
@apply top-0;
|
||||||
@apply left-0;
|
@apply left-0;
|
||||||
@apply my-2;
|
|
||||||
@apply w-[29px];
|
@apply w-[29px];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div :global(.maplibregl-ctrl-geocoder--icon-loading) {
|
||||||
|
@apply -mt-1;
|
||||||
|
@apply mb-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.maplibregl-ctrl-geocoder--icon-close) {
|
||||||
|
@apply my-0;
|
||||||
|
}
|
||||||
|
|
||||||
div :global(.maplibregl-ctrl-geocoder--input) {
|
div :global(.maplibregl-ctrl-geocoder--input) {
|
||||||
@apply relative;
|
@apply relative;
|
||||||
|
@apply h-8;
|
||||||
@apply w-64;
|
@apply w-64;
|
||||||
@apply py-0;
|
@apply py-0;
|
||||||
@apply pl-2;
|
@apply pl-2;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
ListFileItem,
|
ListFileItem,
|
||||||
ListRootItem,
|
ListRootItem,
|
||||||
} from '$lib/components/file-list/file-list';
|
} from '$lib/components/file-list/file-list';
|
||||||
import { getClosestLinePoint, getElevation } from '$lib/utils';
|
import { getClosestLinePoint, getElevation, loadSVGIcon } from '$lib/utils';
|
||||||
import { selectedWaypoint } from '$lib/components/toolbar/tools/waypoint/waypoint';
|
import { 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';
|
||||||
@@ -174,8 +174,9 @@ 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 || !file) {
|
if (!_map || !layerEventManager || !file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,64 +221,26 @@ export class GPXLayer {
|
|||||||
ANCHOR_LAYER_KEY.tracks
|
ANCHOR_LAYER_KEY.tracks
|
||||||
);
|
);
|
||||||
|
|
||||||
_map.on('click', this.fileId, this.layerOnClickBinded);
|
layerEventManager.on('click', this.fileId, this.layerOnClickBinded);
|
||||||
_map.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
layerEventManager.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
||||||
_map.on('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
layerEventManager.on('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
||||||
_map.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
layerEventManager.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
||||||
_map.on('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
layerEventManager.on('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
||||||
|
}
|
||||||
|
|
||||||
|
let visibleTrackSegmentIds: string[] = [];
|
||||||
|
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||||
|
if (!segment._data.hidden) {
|
||||||
|
visibleTrackSegmentIds.push(`${trackIndex}-${segmentIndex}`);
|
||||||
}
|
}
|
||||||
let waypointSource = _map.getSource(this.fileId + '-waypoints') as
|
|
||||||
| GeoJSONSource
|
|
||||||
| undefined;
|
|
||||||
this.currentWaypointData = this.getWaypointsGeoJSON();
|
|
||||||
if (waypointSource) {
|
|
||||||
waypointSource.setData(this.currentWaypointData);
|
|
||||||
} else {
|
|
||||||
_map.addSource(this.fileId + '-waypoints', {
|
|
||||||
type: 'geojson',
|
|
||||||
data: this.currentWaypointData,
|
|
||||||
});
|
});
|
||||||
}
|
const segmentFilter: FilterSpecification = [
|
||||||
|
'in',
|
||||||
|
['get', 'trackSegmentId'],
|
||||||
|
['literal', visibleTrackSegmentIds],
|
||||||
|
];
|
||||||
|
|
||||||
if (!_map.getLayer(this.fileId + '-waypoints')) {
|
_map.setFilter(this.fileId, segmentFilter, { validate: false });
|
||||||
_map.addLayer(
|
|
||||||
{
|
|
||||||
id: this.fileId + '-waypoints',
|
|
||||||
type: 'symbol',
|
|
||||||
source: this.fileId + '-waypoints',
|
|
||||||
layout: {
|
|
||||||
'icon-image': ['get', 'icon'],
|
|
||||||
'icon-size': 0.3,
|
|
||||||
'icon-anchor': 'bottom',
|
|
||||||
'icon-padding': 0,
|
|
||||||
'icon-allow-overlap': true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ANCHOR_LAYER_KEY.waypoints
|
|
||||||
);
|
|
||||||
|
|
||||||
_map.on(
|
|
||||||
'mouseenter',
|
|
||||||
this.fileId + '-waypoints',
|
|
||||||
this.waypointLayerOnMouseEnterBinded
|
|
||||||
);
|
|
||||||
_map.on(
|
|
||||||
'mouseleave',
|
|
||||||
this.fileId + '-waypoints',
|
|
||||||
this.waypointLayerOnMouseLeaveBinded
|
|
||||||
);
|
|
||||||
_map.on('click', this.fileId + '-waypoints', this.waypointLayerOnClickBinded);
|
|
||||||
_map.on(
|
|
||||||
'mousedown',
|
|
||||||
this.fileId + '-waypoints',
|
|
||||||
this.waypointLayerOnMouseDownBinded
|
|
||||||
);
|
|
||||||
_map.on(
|
|
||||||
'touchstart',
|
|
||||||
this.fileId + '-waypoints',
|
|
||||||
this.waypointLayerOnTouchStartBinded
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (get(directionMarkers)) {
|
if (get(directionMarkers)) {
|
||||||
if (!_map.getLayer(this.fileId + '-direction')) {
|
if (!_map.getLayer(this.fileId + '-direction')) {
|
||||||
@@ -306,28 +269,70 @@ export class GPXLayer {
|
|||||||
ANCHOR_LAYER_KEY.directionMarkers
|
ANCHOR_LAYER_KEY.directionMarkers
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_map.setFilter(this.fileId + '-direction', segmentFilter, { validate: false });
|
||||||
} else {
|
} else {
|
||||||
if (_map.getLayer(this.fileId + '-direction')) {
|
if (_map.getLayer(this.fileId + '-direction')) {
|
||||||
_map.removeLayer(this.fileId + '-direction');
|
_map.removeLayer(this.fileId + '-direction');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let visibleTrackSegmentIds: string[] = [];
|
let waypointSource = _map.getSource(this.fileId + '-waypoints') as
|
||||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
| GeoJSONSource
|
||||||
if (!segment._data.hidden) {
|
| undefined;
|
||||||
visibleTrackSegmentIds.push(`${trackIndex}-${segmentIndex}`);
|
this.currentWaypointData = this.getWaypointsGeoJSON();
|
||||||
}
|
if (waypointSource) {
|
||||||
|
waypointSource.setData(this.currentWaypointData);
|
||||||
|
} else {
|
||||||
|
_map.addSource(this.fileId + '-waypoints', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: this.currentWaypointData,
|
||||||
|
promoteId: 'waypointIndex',
|
||||||
});
|
});
|
||||||
const segmentFilter: FilterSpecification = [
|
}
|
||||||
'in',
|
|
||||||
['get', 'trackSegmentId'],
|
|
||||||
['literal', visibleTrackSegmentIds],
|
|
||||||
];
|
|
||||||
|
|
||||||
_map.setFilter(this.fileId, segmentFilter, { validate: false });
|
if (!_map.getLayer(this.fileId + '-waypoints')) {
|
||||||
|
_map.addLayer(
|
||||||
|
{
|
||||||
|
id: this.fileId + '-waypoints',
|
||||||
|
type: 'symbol',
|
||||||
|
source: this.fileId + '-waypoints',
|
||||||
|
layout: {
|
||||||
|
'icon-image': ['get', 'icon'],
|
||||||
|
'icon-size': 0.3,
|
||||||
|
'icon-anchor': 'bottom',
|
||||||
|
'icon-padding': 0,
|
||||||
|
'icon-allow-overlap': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ANCHOR_LAYER_KEY.waypoints
|
||||||
|
);
|
||||||
|
|
||||||
if (_map.getLayer(this.fileId + '-direction')) {
|
layerEventManager.on(
|
||||||
_map.setFilter(this.fileId + '-direction', segmentFilter, { validate: false });
|
'mouseenter',
|
||||||
|
this.fileId + '-waypoints',
|
||||||
|
this.waypointLayerOnMouseEnterBinded
|
||||||
|
);
|
||||||
|
layerEventManager.on(
|
||||||
|
'mouseleave',
|
||||||
|
this.fileId + '-waypoints',
|
||||||
|
this.waypointLayerOnMouseLeaveBinded
|
||||||
|
);
|
||||||
|
layerEventManager.on(
|
||||||
|
'click',
|
||||||
|
this.fileId + '-waypoints',
|
||||||
|
this.waypointLayerOnClickBinded
|
||||||
|
);
|
||||||
|
layerEventManager.on(
|
||||||
|
'mousedown',
|
||||||
|
this.fileId + '-waypoints',
|
||||||
|
this.waypointLayerOnMouseDownBinded
|
||||||
|
);
|
||||||
|
layerEventManager.on(
|
||||||
|
'touchstart',
|
||||||
|
this.fileId + '-waypoints',
|
||||||
|
this.waypointLayerOnTouchStartBinded
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let visibleWaypoints: number[] = [];
|
let visibleWaypoints: number[] = [];
|
||||||
@@ -350,32 +355,47 @@ export class GPXLayer {
|
|||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
const _map = get(map);
|
const _map = get(map);
|
||||||
if (_map) {
|
|
||||||
_map.off('click', this.fileId, this.layerOnClickBinded);
|
|
||||||
_map.off('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
|
||||||
_map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
|
||||||
_map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
|
||||||
_map.off('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
|
||||||
_map.off('style.load', this.updateBinded);
|
|
||||||
|
|
||||||
_map.off(
|
if (_map) {
|
||||||
|
_map.off('style.load', this.updateBinded);
|
||||||
|
}
|
||||||
|
|
||||||
|
const layerEventManager = map.layerEventManager;
|
||||||
|
if (layerEventManager) {
|
||||||
|
layerEventManager.off('click', this.fileId, this.layerOnClickBinded);
|
||||||
|
layerEventManager.off('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
||||||
|
layerEventManager.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
||||||
|
layerEventManager.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
||||||
|
layerEventManager.off('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
||||||
|
|
||||||
|
layerEventManager.off(
|
||||||
'mouseenter',
|
'mouseenter',
|
||||||
this.fileId + '-waypoints',
|
this.fileId + '-waypoints',
|
||||||
this.waypointLayerOnMouseEnterBinded
|
this.waypointLayerOnMouseEnterBinded
|
||||||
);
|
);
|
||||||
_map.off(
|
layerEventManager.off(
|
||||||
'mouseleave',
|
'mouseleave',
|
||||||
this.fileId + '-waypoints',
|
this.fileId + '-waypoints',
|
||||||
this.waypointLayerOnMouseLeaveBinded
|
this.waypointLayerOnMouseLeaveBinded
|
||||||
);
|
);
|
||||||
_map.off('click', this.fileId + '-waypoints', this.waypointLayerOnClickBinded);
|
layerEventManager.off(
|
||||||
_map.off('mousedown', this.fileId + '-waypoints', this.waypointLayerOnMouseDownBinded);
|
'click',
|
||||||
_map.off(
|
this.fileId + '-waypoints',
|
||||||
|
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');
|
||||||
}
|
}
|
||||||
@@ -581,6 +601,7 @@ 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;
|
||||||
@@ -604,6 +625,7 @@ 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);
|
||||||
@@ -624,15 +646,32 @@ export class GPXLayer {
|
|||||||
| GeoJSONSource
|
| GeoJSONSource
|
||||||
| undefined;
|
| undefined;
|
||||||
if (waypointSource) {
|
if (waypointSource) {
|
||||||
waypointSource.setData(this.currentWaypointData!);
|
waypointSource.updateData({
|
||||||
|
update: [
|
||||||
|
{
|
||||||
|
id: this.draggedWaypointIndex,
|
||||||
|
newGeometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: [e.lngLat.lng, e.lngLat.lat],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
waypointLayerOnMouseUp(e: MapLayerMouseEvent | MapLayerTouchEvent) {
|
waypointLayerOnMouseUp(e: MapLayerMouseEvent | MapLayerTouchEvent) {
|
||||||
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
|
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
|
||||||
|
|
||||||
get(map)?.off('mousemove', this.waypointLayerOnMouseMoveBinded);
|
const _map = get(map);
|
||||||
get(map)?.off('touchmove', this.waypointLayerOnMouseMoveBinded);
|
if (!_map) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_map.dragPan.enable();
|
||||||
|
|
||||||
|
_map.off('mousemove', this.waypointLayerOnMouseMoveBinded);
|
||||||
|
_map.off('touchmove', this.waypointLayerOnMouseMoveBinded);
|
||||||
|
|
||||||
if (this.draggedWaypointIndex === null) {
|
if (this.draggedWaypointIndex === null) {
|
||||||
return;
|
return;
|
||||||
@@ -755,20 +794,7 @@ export class GPXLayer {
|
|||||||
|
|
||||||
symbols.forEach((symbol) => {
|
symbols.forEach((symbol) => {
|
||||||
const iconId = `waypoint-${symbol ?? 'default'}-${this.layerColor}`;
|
const iconId = `waypoint-${symbol ?? 'default'}-${this.layerColor}`;
|
||||||
if (!_map.hasImage(iconId)) {
|
loadSVGIcon(_map, iconId, getSvgForSymbol(symbol, this.layerColor));
|
||||||
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,30 +1,40 @@
|
|||||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
||||||
import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
|
import { gpxStatistics, hoveredPoint, slicedGPXStatistics } from '$lib/logic/statistics';
|
||||||
import maplibregl from 'maplibre-gl';
|
import type { GeoJSONSource } from 'maplibre-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: maplibregl.Marker;
|
|
||||||
end: maplibregl.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 maplibregl.Marker({ element: startElement });
|
|
||||||
this.end = new maplibregl.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));
|
||||||
}
|
}
|
||||||
@@ -33,33 +43,115 @@ 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 (!hidden) {
|
||||||
this.start
|
const data: GeoJSON.FeatureCollection = {
|
||||||
.setLngLat(
|
type: 'FeatureCollection',
|
||||||
statistics.getTrackPoint(slicedStatistics?.[1] ?? 0)!.trkpt.getCoordinates()
|
features: [],
|
||||||
)
|
};
|
||||||
.addTo(map_);
|
|
||||||
this.end
|
if (statistics.global.length > 0 && tool !== Tool.ROUTING) {
|
||||||
.setLngLat(
|
const start = statistics
|
||||||
statistics
|
.getTrackPoint(slicedStatistics?.[1] ?? 0)!
|
||||||
|
.trkpt.getCoordinates();
|
||||||
|
const end = statistics
|
||||||
.getTrackPoint(slicedStatistics?.[2] ?? statistics.global.length - 1)!
|
.getTrackPoint(slicedStatistics?.[2] ?? statistics.global.length - 1)!
|
||||||
.trkpt.getCoordinates()
|
.trkpt.getCoordinates();
|
||||||
)
|
data.features.push({
|
||||||
.addTo(map_);
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: [start.lon, start.lat],
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
icon: 'start-marker',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
data.features.push({
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: [end.lon, end.lat],
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
icon: 'end-marker',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hovered) {
|
||||||
|
data.features.push({
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: [hovered.lon, hovered.lat],
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
icon: 'hover-marker',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = map_.getSource('start-end-markers') as GeoJSONSource | undefined;
|
||||||
|
if (source) {
|
||||||
|
source.setData(data);
|
||||||
} else {
|
} else {
|
||||||
this.start.remove();
|
map_.addSource('start-end-markers', {
|
||||||
this.end.remove();
|
type: 'geojson',
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map_.getLayer('start-end-markers')) {
|
||||||
|
map_.addLayer(
|
||||||
|
{
|
||||||
|
id: 'start-end-markers',
|
||||||
|
type: 'symbol',
|
||||||
|
source: 'start-end-markers',
|
||||||
|
layout: {
|
||||||
|
'icon-image': ['get', 'icon'],
|
||||||
|
'icon-size': 0.2,
|
||||||
|
'icon-allow-overlap': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ANCHOR_LAYER_KEY.startEndMarkers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (map_.getLayer('start-end-markers')) {
|
||||||
|
map_.removeLayer('start-end-markers');
|
||||||
|
}
|
||||||
|
if (map_.getSource('start-end-markers')) {
|
||||||
|
map_.removeSource('start-end-markers');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
|
||||||
this.start.remove();
|
const map_ = get(map);
|
||||||
this.end.remove();
|
if (!map_) return;
|
||||||
|
|
||||||
|
if (map_.getLayer('start-end-markers')) {
|
||||||
|
map_.removeLayer('start-end-markers');
|
||||||
|
}
|
||||||
|
if (map_.getSource('start-end-markers')) {
|
||||||
|
map_.removeSource('start-end-markers');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadIcons() {
|
||||||
|
const map_ = get(map);
|
||||||
|
if (!map_) return;
|
||||||
|
loadSVGIcon(map_, 'start-marker', startMarkerSVG);
|
||||||
|
loadSVGIcon(map_, 'end-marker', endMarkerSVG);
|
||||||
|
loadSVGIcon(map_, 'hover-marker', hoverMarkerSVG);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,8 @@
|
|||||||
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 { customBasemapUpdate, isSelected, remove } from './utils';
|
import { 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 {
|
||||||
@@ -129,8 +128,8 @@
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
$customLayers[layerId] = layer;
|
|
||||||
addLayer(layerId);
|
addLayer(layerId);
|
||||||
|
$customLayers[layerId] = layer;
|
||||||
selectedLayerId = undefined;
|
selectedLayerId = undefined;
|
||||||
setDataFromSelectedLayer();
|
setDataFromSelectedLayer();
|
||||||
}
|
}
|
||||||
@@ -153,9 +152,7 @@
|
|||||||
return $tree;
|
return $tree;
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($currentBasemap === layerId) {
|
if ($currentBasemap !== layerId) {
|
||||||
$customBasemapUpdate++;
|
|
||||||
} else {
|
|
||||||
$currentBasemap = layerId;
|
$currentBasemap = layerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,14 +168,6 @@
|
|||||||
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'] = {};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
if (overpassLayer) {
|
if (overpassLayer) {
|
||||||
overpassLayer.remove();
|
overpassLayer.remove();
|
||||||
}
|
}
|
||||||
overpassLayer = new OverpassLayer(_map);
|
overpassLayer = new OverpassLayer(_map, map.layerEventManager!);
|
||||||
overpassLayer.add();
|
overpassLayer.add();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { settings } from '$lib/logic/settings';
|
|||||||
import { db } from '$lib/db';
|
import { db } from '$lib/db';
|
||||||
import type { GeoJSONSource } from 'maplibre-gl';
|
import type { GeoJSONSource } from 'maplibre-gl';
|
||||||
import { ANCHOR_LAYER_KEY } from '../style';
|
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;
|
||||||
|
|
||||||
@@ -27,6 +29,7 @@ export class OverpassLayer {
|
|||||||
queryZoom = 12;
|
queryZoom = 12;
|
||||||
expirationTime = 7 * 24 * 3600 * 1000;
|
expirationTime = 7 * 24 * 3600 * 1000;
|
||||||
map: maplibregl.Map;
|
map: maplibregl.Map;
|
||||||
|
layerEventManager: MapLayerEventManager;
|
||||||
popup: MapPopup;
|
popup: MapPopup;
|
||||||
|
|
||||||
currentQueries: Set<string> = new Set();
|
currentQueries: Set<string> = new Set();
|
||||||
@@ -37,8 +40,9 @@ 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) {
|
constructor(map: maplibregl.Map, layerEventManager: MapLayerEventManager) {
|
||||||
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,
|
||||||
@@ -73,7 +77,14 @@ export class OverpassLayer {
|
|||||||
update() {
|
update() {
|
||||||
this.loadIcons();
|
this.loadIcons();
|
||||||
|
|
||||||
let d = get(data);
|
const fullData = get(data);
|
||||||
|
const queries = getCurrentQueries();
|
||||||
|
const d: GeoJSON.FeatureCollection = {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: fullData.features.filter((feature) =>
|
||||||
|
queries.includes(feature.properties!.query)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let source = this.map.getSource('overpass') as GeoJSONSource | undefined;
|
let source = this.map.getSource('overpass') as GeoJSONSource | undefined;
|
||||||
@@ -102,13 +113,9 @@ export class OverpassLayer {
|
|||||||
ANCHOR_LAYER_KEY.overpass
|
ANCHOR_LAYER_KEY.overpass
|
||||||
);
|
);
|
||||||
|
|
||||||
this.map.on('mouseenter', 'overpass', this.onHoverBinded);
|
this.layerEventManager.on('mouseenter', 'overpass', this.onHoverBinded);
|
||||||
this.map.on('click', 'overpass', this.onHoverBinded);
|
this.layerEventManager.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
|
||||||
}
|
}
|
||||||
@@ -117,6 +124,8 @@ 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.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 {
|
||||||
@@ -249,27 +258,16 @@ export class OverpassLayer {
|
|||||||
loadIcons() {
|
loadIcons() {
|
||||||
let currentQueries = getCurrentQueries();
|
let currentQueries = getCurrentQueries();
|
||||||
currentQueries.forEach((query) => {
|
currentQueries.forEach((query) => {
|
||||||
if (!this.map.hasImage(`overpass-${query}`)) {
|
loadSVGIcon(
|
||||||
let icon = new Image(100, 100);
|
this.map,
|
||||||
icon.onload = () => {
|
`overpass-${query}`,
|
||||||
if (!this.map.hasImage(`overpass-${query}`)) {
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
||||||
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,5 +76,3 @@ export function removeAll(node: LayerTreeType, ids: string[]) {
|
|||||||
});
|
});
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const customBasemapUpdate = writable(0);
|
|
||||||
|
|||||||
281
website/src/lib/components/map/map-layer-event-manager.ts
Normal file
281
website/src/lib/components/map/map-layer-event-manager.ts
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import { fileStateCollection } from '$lib/logic/file-state';
|
||||||
|
import maplibregl from 'maplibre-gl';
|
||||||
|
|
||||||
|
type MapLayerMouseEventListener = (e: maplibregl.MapLayerMouseEvent) => void;
|
||||||
|
type MapLayerTouchEventListener = (e: maplibregl.MapLayerTouchEvent) => void;
|
||||||
|
type MapLayerListener = {
|
||||||
|
features: maplibregl.MapGeoJSONFeature[];
|
||||||
|
mousemoves: MapLayerMouseEventListener[];
|
||||||
|
mouseenters: MapLayerMouseEventListener[];
|
||||||
|
mouseleaves: MapLayerMouseEventListener[];
|
||||||
|
mousedowns: MapLayerMouseEventListener[];
|
||||||
|
clicks: MapLayerMouseEventListener[];
|
||||||
|
contextmenus: MapLayerMouseEventListener[];
|
||||||
|
touchstarts: MapLayerTouchEventListener[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export class MapLayerEventManager {
|
||||||
|
private _map: maplibregl.Map;
|
||||||
|
private _listeners: Record<string, MapLayerListener> = {};
|
||||||
|
|
||||||
|
constructor(map: maplibregl.Map) {
|
||||||
|
this._map = map;
|
||||||
|
this._map.on('mousemove', this._handleMouseMove.bind(this));
|
||||||
|
this._map.on('click', this._handleMouseClick.bind(this, 'click'));
|
||||||
|
this._map.on('contextmenu', this._handleMouseClick.bind(this, 'contextmenu'));
|
||||||
|
this._map.on('mousedown', this._handleMouseClick.bind(this, 'mousedown'));
|
||||||
|
this._map.on('touchstart', this._handleTouchStart.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
on(
|
||||||
|
eventType:
|
||||||
|
| 'mousemove'
|
||||||
|
| 'mouseenter'
|
||||||
|
| 'mouseleave'
|
||||||
|
| 'mousedown'
|
||||||
|
| 'click'
|
||||||
|
| 'contextmenu'
|
||||||
|
| 'touchstart',
|
||||||
|
|
||||||
|
layerId: string,
|
||||||
|
listener: MapLayerMouseEventListener | MapLayerTouchEventListener
|
||||||
|
) {
|
||||||
|
if (!this._listeners[layerId]) {
|
||||||
|
this._listeners[layerId] = {
|
||||||
|
features: [],
|
||||||
|
mousemoves: [],
|
||||||
|
mouseenters: [],
|
||||||
|
mouseleaves: [],
|
||||||
|
mousedowns: [],
|
||||||
|
clicks: [],
|
||||||
|
contextmenus: [],
|
||||||
|
touchstarts: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
switch (eventType) {
|
||||||
|
case 'mousemove':
|
||||||
|
this._listeners[layerId].mousemoves.push(listener as MapLayerMouseEventListener);
|
||||||
|
break;
|
||||||
|
case 'mouseenter':
|
||||||
|
this._listeners[layerId].mouseenters.push(listener as MapLayerMouseEventListener);
|
||||||
|
break;
|
||||||
|
case 'mouseleave':
|
||||||
|
this._listeners[layerId].mouseleaves.push(listener as MapLayerMouseEventListener);
|
||||||
|
break;
|
||||||
|
case 'mousedown':
|
||||||
|
this._listeners[layerId].mousedowns.push(listener as MapLayerMouseEventListener);
|
||||||
|
break;
|
||||||
|
case 'click':
|
||||||
|
this._listeners[layerId].clicks.push(listener as MapLayerMouseEventListener);
|
||||||
|
break;
|
||||||
|
case 'contextmenu':
|
||||||
|
this._listeners[layerId].contextmenus.push(listener as MapLayerMouseEventListener);
|
||||||
|
break;
|
||||||
|
case 'touchstart':
|
||||||
|
this._listeners[layerId].touchstarts.push(listener as MapLayerTouchEventListener);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
off(
|
||||||
|
eventType:
|
||||||
|
| 'mousemove'
|
||||||
|
| 'mouseenter'
|
||||||
|
| 'mouseleave'
|
||||||
|
| 'mousedown'
|
||||||
|
| 'click'
|
||||||
|
| 'contextmenu'
|
||||||
|
| 'touchstart',
|
||||||
|
layerId: string,
|
||||||
|
listener: MapLayerMouseEventListener | MapLayerTouchEventListener
|
||||||
|
) {
|
||||||
|
if (this._listeners[layerId]) {
|
||||||
|
switch (eventType) {
|
||||||
|
case 'mousemove':
|
||||||
|
this._listeners[layerId].mousemoves = this._listeners[
|
||||||
|
layerId
|
||||||
|
].mousemoves.filter((l) => l !== listener);
|
||||||
|
break;
|
||||||
|
case 'mouseenter':
|
||||||
|
this._listeners[layerId].mouseenters = this._listeners[
|
||||||
|
layerId
|
||||||
|
].mouseenters.filter((l) => l !== listener);
|
||||||
|
break;
|
||||||
|
case 'mouseleave':
|
||||||
|
this._listeners[layerId].mouseleaves = this._listeners[
|
||||||
|
layerId
|
||||||
|
].mouseleaves.filter((l) => l !== listener);
|
||||||
|
break;
|
||||||
|
case 'mousedown':
|
||||||
|
this._listeners[layerId].mousedowns = this._listeners[
|
||||||
|
layerId
|
||||||
|
].mousedowns.filter((l) => l !== listener);
|
||||||
|
break;
|
||||||
|
case 'click':
|
||||||
|
this._listeners[layerId].clicks = this._listeners[layerId].clicks.filter(
|
||||||
|
(l) => l !== listener
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'contextmenu':
|
||||||
|
this._listeners[layerId].contextmenus = this._listeners[
|
||||||
|
layerId
|
||||||
|
].contextmenus.filter((l) => l !== listener);
|
||||||
|
break;
|
||||||
|
case 'touchstart':
|
||||||
|
this._listeners[layerId].touchstarts = this._listeners[
|
||||||
|
layerId
|
||||||
|
].touchstarts.filter((l) => l !== listener);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this._listeners[layerId].mousemoves.length === 0 &&
|
||||||
|
this._listeners[layerId].mouseenters.length === 0 &&
|
||||||
|
this._listeners[layerId].mouseleaves.length === 0 &&
|
||||||
|
this._listeners[layerId].mousedowns.length === 0 &&
|
||||||
|
this._listeners[layerId].clicks.length === 0 &&
|
||||||
|
this._listeners[layerId].contextmenus.length === 0 &&
|
||||||
|
this._listeners[layerId].touchstarts.length === 0
|
||||||
|
) {
|
||||||
|
delete this._listeners[layerId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleMouseMove(e: maplibregl.MapMouseEvent) {
|
||||||
|
const featuresByLayer = this._getRenderedFeaturesByLayer(e);
|
||||||
|
Object.keys(this._listeners).forEach((layerId) => {
|
||||||
|
const features = featuresByLayer[layerId] || [];
|
||||||
|
const listener = this._listeners[layerId];
|
||||||
|
if ((features.length == 0) != (listener.features.length == 0)) {
|
||||||
|
if (features.length > 0) {
|
||||||
|
if (listener.mouseenters.length > 0) {
|
||||||
|
const event = new maplibregl.MapMouseEvent(
|
||||||
|
'mouseenter',
|
||||||
|
e.target,
|
||||||
|
e.originalEvent,
|
||||||
|
{
|
||||||
|
features: featuresByLayer[layerId]!,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
listener.mouseenters.forEach((l) => l(event));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (listener.mouseleaves.length > 0) {
|
||||||
|
const event = new maplibregl.MapMouseEvent(
|
||||||
|
'mouseleave',
|
||||||
|
e.target,
|
||||||
|
e.originalEvent
|
||||||
|
);
|
||||||
|
listener.mouseleaves.forEach((l) => l(event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (features.length > 0 && listener.mousemoves.length > 0) {
|
||||||
|
const event = new maplibregl.MapMouseEvent('mousemove', e.target, e.originalEvent, {
|
||||||
|
features: featuresByLayer[layerId]!,
|
||||||
|
});
|
||||||
|
listener.mousemoves.forEach((l) => l(event));
|
||||||
|
}
|
||||||
|
listener.features = features;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleMouseClick(type: string, e: maplibregl.MapMouseEvent) {
|
||||||
|
const featuresByLayer = this._getRenderedFeaturesByLayer(e);
|
||||||
|
Object.keys(this._listeners).forEach((layerId) => {
|
||||||
|
const features = featuresByLayer[layerId] || [];
|
||||||
|
const listener = this._listeners[layerId];
|
||||||
|
if (features.length > 0) {
|
||||||
|
if (type === 'click' && listener.clicks.length > 0) {
|
||||||
|
const event = new maplibregl.MapMouseEvent('click', e.target, e.originalEvent, {
|
||||||
|
features: features,
|
||||||
|
});
|
||||||
|
listener.clicks.forEach((l) => l(event));
|
||||||
|
} else if (type === 'contextmenu' && listener.contextmenus.length > 0) {
|
||||||
|
const event = new maplibregl.MapMouseEvent(
|
||||||
|
'contextmenu',
|
||||||
|
e.target,
|
||||||
|
e.originalEvent,
|
||||||
|
{
|
||||||
|
features: features,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
listener.contextmenus.forEach((l) => l(event));
|
||||||
|
} else if (type === 'mousedown' && listener.mousedowns.length > 0) {
|
||||||
|
const event = new maplibregl.MapMouseEvent(
|
||||||
|
'mousedown',
|
||||||
|
e.target,
|
||||||
|
e.originalEvent,
|
||||||
|
{
|
||||||
|
features: features,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
listener.mousedowns.forEach((l) => l(event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleTouchStart(e: maplibregl.MapTouchEvent) {
|
||||||
|
const featuresByLayer = this._getRenderedFeaturesByLayer(e);
|
||||||
|
Object.keys(this._listeners).forEach((layerId) => {
|
||||||
|
const features = featuresByLayer[layerId] || [];
|
||||||
|
const listener = this._listeners[layerId];
|
||||||
|
if (features.length > 0) {
|
||||||
|
const event: maplibregl.MapLayerTouchEvent = new maplibregl.MapTouchEvent(
|
||||||
|
'touchstart',
|
||||||
|
e.target,
|
||||||
|
e.originalEvent
|
||||||
|
);
|
||||||
|
event.features = featuresByLayer[layerId]!;
|
||||||
|
listener.touchstarts.forEach((l) => l(event));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getBounds(point: maplibregl.Point) {
|
||||||
|
const delta = 30;
|
||||||
|
return new maplibregl.LngLatBounds(
|
||||||
|
this._map.unproject([point.x - delta, point.y + delta]),
|
||||||
|
this._map.unproject([point.x + delta, point.y - delta])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _filterLayersIntersectingBounds(
|
||||||
|
layerIds: string[],
|
||||||
|
bounds: maplibregl.LngLatBounds
|
||||||
|
): string[] {
|
||||||
|
let result = layerIds.filter((layerId) => {
|
||||||
|
if (!this._map.getLayer(layerId)) return false;
|
||||||
|
const fileId = layerId.replace('-waypoints', '');
|
||||||
|
if (fileId === layerId) {
|
||||||
|
return fileStateCollection.getStatistics(fileId)?.intersectsBBox(bounds) ?? true;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
fileStateCollection.getStatistics(fileId)?.intersectsWaypointBBox(bounds) ??
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getRenderedFeaturesByLayer(e: maplibregl.MapMouseEvent | maplibregl.MapTouchEvent) {
|
||||||
|
const layerIds = this._filterLayersIntersectingBounds(
|
||||||
|
Object.keys(this._listeners),
|
||||||
|
this._getBounds(e.point)
|
||||||
|
);
|
||||||
|
const features =
|
||||||
|
layerIds.length > 0
|
||||||
|
? this._map.queryRenderedFeatures(e.point, { layers: layerIds })
|
||||||
|
: [];
|
||||||
|
const featuresByLayer: Record<string, maplibregl.MapGeoJSONFeature[]> = {};
|
||||||
|
features.forEach((f) => {
|
||||||
|
if (!featuresByLayer[f.layer.id]) {
|
||||||
|
featuresByLayer[f.layer.id] = [];
|
||||||
|
}
|
||||||
|
featuresByLayer[f.layer.id].push(f);
|
||||||
|
});
|
||||||
|
return featuresByLayer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ 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 { ANCHOR_LAYER_KEY, StyleManager } from '$lib/components/map/style';
|
||||||
|
import { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
||||||
|
|
||||||
const { treeFileView, elevationProfile, bottomPanelSize, rightPanelSize, distanceUnits } = settings;
|
const { treeFileView, elevationProfile, bottomPanelSize, rightPanelSize, distanceUnits } = settings;
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ export class MapLibreGLMap {
|
|||||||
private _onLoadCallbacks: ((map: maplibregl.Map) => void)[] = [];
|
private _onLoadCallbacks: ((map: maplibregl.Map) => void)[] = [];
|
||||||
private _unsubscribes: (() => void)[] = [];
|
private _unsubscribes: (() => void)[] = [];
|
||||||
private callOnLoadBinded: () => void = this.callOnLoad.bind(this);
|
private callOnLoadBinded: () => void = this.callOnLoad.bind(this);
|
||||||
|
public layerEventManager: MapLayerEventManager | null = null;
|
||||||
|
|
||||||
subscribe(run: (value: maplibregl.Map | null) => void, invalidate?: () => void) {
|
subscribe(run: (value: maplibregl.Map | null) => void, invalidate?: () => void) {
|
||||||
return this._mapStore.subscribe(run, invalidate);
|
return this._mapStore.subscribe(run, invalidate);
|
||||||
@@ -54,6 +56,7 @@ export class MapLibreGLMap {
|
|||||||
boxZoom: false,
|
boxZoom: false,
|
||||||
maxPitch: 85,
|
maxPitch: 85,
|
||||||
});
|
});
|
||||||
|
this.layerEventManager = new MapLayerEventManager(map);
|
||||||
map.addControl(
|
map.addControl(
|
||||||
new maplibregl.NavigationControl({
|
new maplibregl.NavigationControl({
|
||||||
visualizePitch: true,
|
visualizePitch: true,
|
||||||
|
|||||||
@@ -20,9 +20,14 @@
|
|||||||
let container: HTMLElement;
|
let container: HTMLElement;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
map.onLoad((map: maplibregl.Map) => {
|
map.onLoad((map_: maplibregl.Map) => {
|
||||||
googleRedirect = new GoogleRedirect(map);
|
googleRedirect = new GoogleRedirect(map_);
|
||||||
mapillaryLayer = new MapillaryLayer(map, container, mapillaryOpen);
|
mapillaryLayer = new MapillaryLayer(
|
||||||
|
map_,
|
||||||
|
map.layerEventManager!,
|
||||||
|
container,
|
||||||
|
mapillaryOpen
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.mod
|
|||||||
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 '../style';
|
||||||
|
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
||||||
|
|
||||||
const mapillarySource: VectorSourceSpecification = {
|
const mapillarySource: VectorSourceSpecification = {
|
||||||
type: 'vector',
|
type: 'vector',
|
||||||
@@ -43,6 +44,7 @@ const mapillaryImageLayer: LayerSpecification = {
|
|||||||
|
|
||||||
export class MapillaryLayer {
|
export class MapillaryLayer {
|
||||||
map: maplibregl.Map;
|
map: maplibregl.Map;
|
||||||
|
layerEventManager: MapLayerEventManager;
|
||||||
marker: maplibregl.Marker;
|
marker: maplibregl.Marker;
|
||||||
viewer: Viewer;
|
viewer: Viewer;
|
||||||
|
|
||||||
@@ -53,8 +55,14 @@ export class MapillaryLayer {
|
|||||||
onMouseEnterBinded = this.onMouseEnter.bind(this);
|
onMouseEnterBinded = this.onMouseEnter.bind(this);
|
||||||
onMouseLeaveBinded = this.onMouseLeave.bind(this);
|
onMouseLeaveBinded = this.onMouseLeave.bind(this);
|
||||||
|
|
||||||
constructor(map: maplibregl.Map, container: HTMLElement, popupOpen: { value: boolean }) {
|
constructor(
|
||||||
|
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',
|
||||||
@@ -103,14 +111,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.map.on('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
this.layerEventManager.on('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
||||||
this.map.on('mouseleave', 'mapillary-image', this.onMouseLeaveBinded);
|
this.layerEventManager.on('mouseleave', 'mapillary-image', this.onMouseLeaveBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.map.off('style.load', this.addBinded);
|
this.map.off('style.load', this.addBinded);
|
||||||
this.map.off('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
this.layerEventManager.off('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
||||||
this.map.off('mouseleave', 'mapillary-image', this.onMouseLeaveBinded);
|
this.layerEventManager.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');
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
overlays,
|
overlays,
|
||||||
terrainSources,
|
terrainSources,
|
||||||
} from '$lib/assets/layers';
|
} from '$lib/assets/layers';
|
||||||
import { customBasemapUpdate, getLayers } from '$lib/components/map/layer-control/utils';
|
import { getLayers } from '$lib/components/map/layer-control/utils';
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
|
|
||||||
const { currentBasemap, currentOverlays, customLayers, opacities, terrainSource } = settings;
|
const { currentBasemap, currentOverlays, customLayers, opacities, terrainSource } = settings;
|
||||||
@@ -25,9 +25,11 @@ export const ANCHOR_LAYER_KEY = {
|
|||||||
tracks: 'tracks-end',
|
tracks: 'tracks-end',
|
||||||
directionMarkers: 'direction-markers-end',
|
directionMarkers: 'direction-markers-end',
|
||||||
distanceMarkers: 'distance-markers-end',
|
distanceMarkers: 'distance-markers-end',
|
||||||
|
startEndMarkers: 'start-end-markers-end',
|
||||||
interactions: 'interactions-end',
|
interactions: 'interactions-end',
|
||||||
overpass: 'overpass-end',
|
overpass: 'overpass-end',
|
||||||
waypoints: 'waypoints-end',
|
waypoints: 'waypoints-end',
|
||||||
|
routingControls: 'routing-controls-end',
|
||||||
};
|
};
|
||||||
const anchorLayers: maplibregl.LayerSpecification[] = Object.values(ANCHOR_LAYER_KEY).map((id) => ({
|
const anchorLayers: maplibregl.LayerSpecification[] = Object.values(ANCHOR_LAYER_KEY).map((id) => ({
|
||||||
id: id,
|
id: id,
|
||||||
@@ -45,22 +47,51 @@ export class StyleManager {
|
|||||||
this._maptilerKey = maptilerKey;
|
this._maptilerKey = maptilerKey;
|
||||||
this._map.subscribe((map_) => {
|
this._map.subscribe((map_) => {
|
||||||
if (map_) {
|
if (map_) {
|
||||||
this.update();
|
this.updateBasemap();
|
||||||
map_.on('style.load', () => this.updateOverlays());
|
map_.on('style.load', () => this.updateOverlays());
|
||||||
map_.on('pitch', () => this.updateTerrain());
|
map_.on('pitch', () => this.updateTerrain());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
currentBasemap.subscribe(() => this.update());
|
currentBasemap.subscribe(() => this.updateBasemap());
|
||||||
customBasemapUpdate.subscribe(() => this.update());
|
|
||||||
currentOverlays.subscribe(() => this.updateOverlays());
|
currentOverlays.subscribe(() => this.updateOverlays());
|
||||||
opacities.subscribe(() => this.updateOverlays());
|
opacities.subscribe(() => this.updateOverlays());
|
||||||
terrainSource.subscribe(() => this.updateTerrain());
|
terrainSource.subscribe(() => this.updateTerrain());
|
||||||
|
customLayers.subscribe(() => this.updateBasemap());
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
updateBasemap() {
|
||||||
const map_ = get(this._map);
|
const map_ = get(this._map);
|
||||||
if (!map_) return;
|
if (!map_) return;
|
||||||
this.build().then((style) => map_.setStyle(style));
|
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() {
|
async updateOverlays() {
|
||||||
@@ -127,36 +158,15 @@ export class StyleManager {
|
|||||||
const mapTerrain = map_.getTerrain();
|
const mapTerrain = map_.getTerrain();
|
||||||
const terrain = this.getCurrentTerrain();
|
const terrain = this.getCurrentTerrain();
|
||||||
if (JSON.stringify(mapTerrain) !== JSON.stringify(terrain)) {
|
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);
|
map_.setTerrain(terrain);
|
||||||
|
} else {
|
||||||
|
map_.setTerrain(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async build(): Promise<maplibregl.StyleSpecification> {
|
|
||||||
const custom = get(customLayers);
|
|
||||||
|
|
||||||
const style: maplibregl.StyleSpecification = {
|
|
||||||
version: 8,
|
|
||||||
projection: {
|
|
||||||
type: 'globe',
|
|
||||||
},
|
|
||||||
sources: {
|
|
||||||
'empty-source': emptySource,
|
|
||||||
},
|
|
||||||
layers: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
let basemap = get(currentBasemap);
|
|
||||||
const basemapInfo = basemaps[basemap] ?? custom[basemap]?.value ?? basemaps[defaultBasemap];
|
|
||||||
const basemapStyle = await this.get(basemapInfo);
|
|
||||||
|
|
||||||
this.merge(style, basemapStyle);
|
|
||||||
|
|
||||||
style.terrain = this.getCurrentTerrain();
|
|
||||||
style.sources[style.terrain.source] = terrainSources[style.terrain.source];
|
|
||||||
|
|
||||||
style.layers.push(...anchorLayers);
|
|
||||||
|
|
||||||
return style;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(
|
async get(
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
SquareArrowUpLeft,
|
SquareArrowUpLeft,
|
||||||
SquareArrowOutDownRight,
|
SquareArrowOutDownRight,
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import { brouterProfiles } from '$lib/components/toolbar/tools/routing/routing';
|
import { routingProfiles } from '$lib/components/toolbar/tools/routing/routing';
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import {
|
import {
|
||||||
@@ -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(brouterProfiles) as profile}
|
{#each Object.keys(routingProfiles) as profile}
|
||||||
<Select.Item value={profile}
|
<Select.Item value={profile}
|
||||||
>{i18n._(
|
>{i18n._(
|
||||||
`toolbar.routing.activities.${profile}`
|
`toolbar.routing.activities.${profile}`
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ import { get } from 'svelte/store';
|
|||||||
|
|
||||||
const { routing, routingProfile, privateRoads } = settings;
|
const { routing, routingProfile, privateRoads } = settings;
|
||||||
|
|
||||||
export const brouterProfiles: { [key: string]: string } = {
|
export const routingProfiles: { [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 brouterProfiles: { [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, brouterProfiles[get(routingProfile)], get(privateRoads));
|
return getRoute(points, routingProfiles[get(routingProfile)], get(privateRoads));
|
||||||
} else {
|
} else {
|
||||||
return getIntermediatePoints(points);
|
return getIntermediatePoints(points);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,21 @@ import { ramerDouglasPeucker, type GPXFile, type TrackSegment } from 'gpx';
|
|||||||
|
|
||||||
const earthRadius = 6371008.8;
|
const earthRadius = 6371008.8;
|
||||||
|
|
||||||
|
export const MIN_ANCHOR_ZOOM = 0;
|
||||||
|
export const MAX_ANCHOR_ZOOM = 22;
|
||||||
|
|
||||||
export function getZoomLevelForDistance(latitude: number, distance?: number): number {
|
export function getZoomLevelForDistance(latitude: number, distance?: number): number {
|
||||||
if (distance === undefined) {
|
if (distance === undefined) {
|
||||||
return 0;
|
return MIN_ANCHOR_ZOOM;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rad = Math.PI / 180;
|
const rad = Math.PI / 180;
|
||||||
const lat = latitude * rad;
|
const lat = latitude * rad;
|
||||||
|
|
||||||
return Math.min(22, Math.max(0, Math.log2((earthRadius * Math.cos(lat)) / distance)));
|
return Math.min(
|
||||||
|
MAX_ANCHOR_ZOOM,
|
||||||
|
Math.max(MIN_ANCHOR_ZOOM, Math.round(Math.log2((earthRadius * Math.cos(lat)) / distance)))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateAnchorPoints(file: GPXFile) {
|
export function updateAnchorPoints(file: GPXFile) {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
splitControls = new SplitControls($map);
|
splitControls = new SplitControls($map, map.layerEventManager!);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,39 +10,31 @@ 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 type { GeoJSONSource } from 'maplibre-gl';
|
||||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
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: maplibregl.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) {
|
constructor(map: maplibregl.Map, layerEventManager: MapLayerEventManager) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
|
this.layerEventManager = layerEventManager;
|
||||||
if (!this.map.hasImage('split-control')) {
|
loadSVGIcon(
|
||||||
let icon = new Image(100, 100);
|
this.map,
|
||||||
icon.onload = () => {
|
'split-control',
|
||||||
if (!this.map.hasImage('split-control')) {
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
||||||
this.map.addImage('split-control', icon);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Lucide icons are SVG files with a 24x24 viewBox
|
|
||||||
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
|
|
||||||
icon.src =
|
|
||||||
'data:image/svg+xml,' +
|
|
||||||
encodeURIComponent(`
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
|
||||||
<circle cx="20" cy="20" r="20" fill="white" />
|
<circle cx="20" cy="20" r="20" fill="white" />
|
||||||
<g transform="translate(8 8)">
|
<g transform="translate(8 8)">
|
||||||
${Scissors.replace('stroke="currentColor"', 'stroke="black"')}
|
${Scissors.replace('stroke="currentColor"', 'stroke="black"')}
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>`
|
||||||
`);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
|
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
|
||||||
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
|
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
|
||||||
@@ -125,9 +117,17 @@ export class SplitControls {
|
|||||||
ANCHOR_LAYER_KEY.interactions
|
ANCHOR_LAYER_KEY.interactions
|
||||||
);
|
);
|
||||||
|
|
||||||
this.map.on('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
|
this.layerEventManager.on(
|
||||||
this.map.on('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
|
'mouseenter',
|
||||||
this.map.on('click', 'split-controls', this.layerOnClickBinded);
|
'split-controls',
|
||||||
|
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 +135,9 @@ export class SplitControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.map.off('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
|
this.layerEventManager.off('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
|
||||||
this.map.off('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
|
this.layerEventManager.off('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
|
||||||
this.map.off('click', 'split-controls', this.layerOnClickBinded);
|
this.layerEventManager.off('click', 'split-controls', this.layerOnClickBinded);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.map.getLayer('split-controls')) {
|
if (this.map.getLayer('split-controls')) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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>
|
||||||
@@ -84,6 +85,7 @@ 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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -7,7 +7,8 @@ export enum MapCursorState {
|
|||||||
TOOL_WITH_CROSSHAIR,
|
TOOL_WITH_CROSSHAIR,
|
||||||
WAYPOINT_HOVER,
|
WAYPOINT_HOVER,
|
||||||
WAYPOINT_DRAGGING,
|
WAYPOINT_DRAGGING,
|
||||||
TRACKPOINT_DRAGGING,
|
ANCHOR_HOVER,
|
||||||
|
ANCHOR_DRAGGING,
|
||||||
SCISSORS,
|
SCISSORS,
|
||||||
SPLIT_CONTROL,
|
SPLIT_CONTROL,
|
||||||
MAPILLARY_HOVER,
|
MAPILLARY_HOVER,
|
||||||
@@ -20,7 +21,8 @@ const cursorStyles = {
|
|||||||
[MapCursorState.LAYER_HOVER]: 'pointer',
|
[MapCursorState.LAYER_HOVER]: 'pointer',
|
||||||
[MapCursorState.WAYPOINT_HOVER]: 'pointer',
|
[MapCursorState.WAYPOINT_HOVER]: 'pointer',
|
||||||
[MapCursorState.WAYPOINT_DRAGGING]: 'grabbing',
|
[MapCursorState.WAYPOINT_DRAGGING]: 'grabbing',
|
||||||
[MapCursorState.TRACKPOINT_DRAGGING]: 'grabbing',
|
[MapCursorState.ANCHOR_HOVER]: 'pointer',
|
||||||
|
[MapCursorState.ANCHOR_DRAGGING]: 'grabbing',
|
||||||
[MapCursorState.TOOL_WITH_CROSSHAIR]: 'crosshair',
|
[MapCursorState.TOOL_WITH_CROSSHAIR]: 'crosshair',
|
||||||
[MapCursorState.SCISSORS]: scissorsCursor,
|
[MapCursorState.SCISSORS]: scissorsCursor,
|
||||||
[MapCursorState.SPLIT_CONTROL]: 'pointer',
|
[MapCursorState.SPLIT_CONTROL]: 'pointer',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { type Database } from '$lib/db';
|
import { type Database } from '$lib/db';
|
||||||
import { liveQuery } from 'dexie';
|
import { liveQuery } from 'dexie';
|
||||||
import {
|
import {
|
||||||
|
basemaps,
|
||||||
defaultBasemap,
|
defaultBasemap,
|
||||||
defaultBasemapTree,
|
defaultBasemapTree,
|
||||||
defaultOpacities,
|
defaultOpacities,
|
||||||
@@ -9,7 +10,10 @@ 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';
|
||||||
@@ -19,10 +23,12 @@ 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) {
|
constructor(key: string, initial: V, validator?: (value: V) => V) {
|
||||||
this._key = key;
|
this._key = key;
|
||||||
this._value = writable(initial);
|
this._value = writable(initial);
|
||||||
|
this._validator = validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectToDatabase(db: Database) {
|
connectToDatabase(db: Database) {
|
||||||
@@ -36,6 +42,9 @@ 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;
|
||||||
@@ -73,11 +82,13 @@ 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) {
|
constructor(key: string, initial: V, validator?: (value: V) => 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) {
|
||||||
@@ -93,6 +104,9 @@ 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;
|
||||||
@@ -128,37 +142,166 @@ 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<'metric' | 'imperial' | 'nautical'>('distanceUnits', 'metric'),
|
distanceUnits: new Setting<DistanceUnits>(
|
||||||
velocityUnits: new Setting<'speed' | 'pace'>('velocityUnits', 'speed'),
|
'distanceUnits',
|
||||||
temperatureUnits: new Setting<'celsius' | 'fahrenheit'>('temperatureUnits', 'celsius'),
|
'metric',
|
||||||
|
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<string[]>('additionalDatasets', []),
|
additionalDatasets: new Setting<AdditionalDataset[]>(
|
||||||
elevationFill: new Setting<'slope' | 'surface' | undefined>('elevationFill', undefined),
|
'additionalDatasets',
|
||||||
|
[],
|
||||||
|
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', 'bike'),
|
routingProfile: new Setting<RoutingProfile>(
|
||||||
|
'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', defaultBasemap),
|
currentBasemap: new Setting(
|
||||||
previousBasemap: new Setting('previousBasemap', defaultBasemap),
|
'currentBasemap',
|
||||||
selectedBasemapTree: new Setting('selectedBasemapTree', defaultBasemapTree),
|
defaultBasemap,
|
||||||
currentOverlays: new SettingInitOnFirstRead('currentOverlays', defaultOverlays),
|
getLayerValidator(basemaps, defaultBasemap)
|
||||||
previousOverlays: new Setting('previousOverlays', defaultOverlays),
|
),
|
||||||
selectedOverlayTree: new Setting('selectedOverlayTree', defaultOverlayTree),
|
previousBasemap: new Setting(
|
||||||
|
'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', defaultTerrainSource),
|
terrainSource: new Setting<TerrainSource>(
|
||||||
|
'terrainSource',
|
||||||
|
defaultTerrainSource,
|
||||||
|
getValueValidator(['maptiler-dem', 'mapterhorn'], defaultTerrainSource)
|
||||||
|
),
|
||||||
directionMarkers: new Setting('directionMarkers', false),
|
directionMarkers: new Setting('directionMarkers', false),
|
||||||
distanceMarkers: new Setting('distanceMarkers', false),
|
distanceMarkers: new Setting('distanceMarkers', false),
|
||||||
streetViewSource: new Setting('streetViewSource', 'mapillary'),
|
streetViewSource: new Setting<StreetViewSource>(
|
||||||
|
'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,18 +1,24 @@
|
|||||||
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) => {
|
||||||
@@ -42,5 +48,27 @@ 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 } from 'gpx';
|
import { GPXGlobalStatistics, GPXStatisticsGroup, type Coordinates } from 'gpx';
|
||||||
import { fileStateCollection, GPXFileState } from '$lib/logic/file-state';
|
import { fileStateCollection, GPXFileState } from '$lib/logic/file-state';
|
||||||
import {
|
import {
|
||||||
ListFileItem,
|
ListFileItem,
|
||||||
@@ -82,6 +82,8 @@ 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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -197,6 +197,18 @@ export function getElevation(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loadSVGIcon(map: maplibregl.Map, id: string, svg: string, size: number = 100) {
|
||||||
|
if (!map.hasImage(id)) {
|
||||||
|
let icon = new Image(size, size);
|
||||||
|
icon.onload = () => {
|
||||||
|
if (!map.hasImage(id)) {
|
||||||
|
map.addImage(id, icon);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
icon.src = 'data:image/svg+xml,' + encodeURIComponent(svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function isMac() {
|
export function isMac() {
|
||||||
return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
|
return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,12 @@
|
|||||||
data: {
|
data: {
|
||||||
fundingModule: Promise<any>;
|
fundingModule: Promise<any>;
|
||||||
translationModule: Promise<any>;
|
translationModule: Promise<any>;
|
||||||
maptilerModule: 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);
|
||||||
|
|
||||||
@@ -197,6 +197,7 @@
|
|||||||
<ElevationProfile
|
<ElevationProfile
|
||||||
{gpxStatistics}
|
{gpxStatistics}
|
||||||
{slicedGPXStatistics}
|
{slicedGPXStatistics}
|
||||||
|
{hoveredPoint}
|
||||||
{additionalDatasets}
|
{additionalDatasets}
|
||||||
{elevationFill}
|
{elevationFill}
|
||||||
/>
|
/>
|
||||||
@@ -270,23 +271,4 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-12 md:px-24 flex flex-col items-center">
|
|
||||||
<div
|
|
||||||
class="max-w-4xl flex flex-col lg:flex-row items-center justify-center gap-x-12 gap-y-6 p-6 border rounded-2xl shadow-xl bg-secondary"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="shrink-0 flex flex-col sm:flex-row lg:flex-col items-center gap-x-4 gap-y-2"
|
|
||||||
>
|
|
||||||
<div class="text-lg font-semibold text-muted-foreground">
|
|
||||||
❤️ {i18n._('homepage.supported_by')}
|
|
||||||
</div>
|
|
||||||
<a href="https://www.maptiler.com/" target="_blank">
|
|
||||||
<Logo company="maptiler" class="w-60" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{#await data.maptilerModule then maptilerModule}
|
|
||||||
<DocsContainer module={maptilerModule.default} />
|
|
||||||
{/await}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,5 @@ 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'),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, slicedGPXStatistics } from '$lib/logic/statistics';
|
import { gpxStatistics, hoveredPoint, 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';
|
||||||
@@ -30,6 +30,11 @@
|
|||||||
elevationFill,
|
elevationFill,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
|
let bottomPanelWidth: number | undefined = $state();
|
||||||
|
let bottomPanelOrientation = $derived(
|
||||||
|
bottomPanelWidth && bottomPanelWidth >= 540 && $elevationProfile ? 'horizontal' : 'vertical'
|
||||||
|
);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
settings.connectToDatabase(db);
|
settings.connectToDatabase(db);
|
||||||
fileStateCollection.connectToDatabase(db).then(() => {
|
fileStateCollection.connectToDatabase(db).then(() => {
|
||||||
@@ -127,19 +132,23 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
class="{$elevationProfile ? '' : 'h-10'} flex flex-row gap-2 px-2 sm:px-4"
|
bind:offsetWidth={bottomPanelWidth}
|
||||||
|
class="flex {bottomPanelOrientation == 'vertical'
|
||||||
|
? 'flex-col'
|
||||||
|
: 'flex-row py-2'} gap-1 px-2"
|
||||||
style={$elevationProfile ? `height: ${$bottomPanelSize}px` : ''}
|
style={$elevationProfile ? `height: ${$bottomPanelSize}px` : ''}
|
||||||
>
|
>
|
||||||
<GPXStatistics
|
<GPXStatistics
|
||||||
{gpxStatistics}
|
{gpxStatistics}
|
||||||
{slicedGPXStatistics}
|
{slicedGPXStatistics}
|
||||||
panelSize={$bottomPanelSize}
|
panelSize={$bottomPanelSize}
|
||||||
orientation={$elevationProfile ? 'vertical' : 'horizontal'}
|
orientation={bottomPanelOrientation == 'horizontal' ? 'vertical' : 'horizontal'}
|
||||||
/>
|
/>
|
||||||
{#if $elevationProfile}
|
{#if $elevationProfile}
|
||||||
<ElevationProfile
|
<ElevationProfile
|
||||||
{gpxStatistics}
|
{gpxStatistics}
|
||||||
{slicedGPXStatistics}
|
{slicedGPXStatistics}
|
||||||
|
{hoveredPoint}
|
||||||
{additionalDatasets}
|
{additionalDatasets}
|
||||||
{elevationFill}
|
{elevationFill}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user