mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-09-04 01:22:32 +00:00
distance markers
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
import { type AnySourceData, type Style } from 'mapbox-gl';
|
import { type AnySourceData, type Style } from 'mapbox-gl';
|
||||||
|
|
||||||
|
export const mapboxAccessToken = 'pk.eyJ1IjoiZ3B4c3R1ZGlvIiwiYSI6ImNrdHVoM2pjNTBodmUycG1yZTNwcnJ3MzkifQ.YZnNs9s9oCQPzoXAWs_SLg';
|
||||||
|
|
||||||
export const basemaps: { [key: string]: string | Style; } = {
|
export const basemaps: { [key: string]: string | Style; } = {
|
||||||
mapboxOutdoors: 'mapbox://styles/mapbox/outdoors-v12',
|
mapboxOutdoors: 'mapbox://styles/mapbox/outdoors-v12',
|
||||||
mapboxSatellite: 'mapbox://styles/mapbox/satellite-v9',
|
mapboxSatellite: 'mapbox://styles/mapbox/satellite-streets-v12',
|
||||||
openStreetMap: {
|
openStreetMap: {
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {
|
sources: {
|
||||||
@@ -281,6 +283,7 @@ export const basemaps: { [key: string]: string | Style; } = {
|
|||||||
Object.values(basemaps).forEach((basemap) => {
|
Object.values(basemaps).forEach((basemap) => {
|
||||||
if (typeof basemap === 'object') {
|
if (typeof basemap === 'object') {
|
||||||
basemap["glyphs"] = "mapbox://fonts/mapbox/{fontstack}/{range}.pbf";
|
basemap["glyphs"] = "mapbox://fonts/mapbox/{fontstack}/{range}.pbf";
|
||||||
|
basemap["sprite"] = `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${mapboxAccessToken}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -6,20 +6,19 @@
|
|||||||
|
|
||||||
import { get, type Readable } from 'svelte/store';
|
import { get, type Readable } from 'svelte/store';
|
||||||
import { selectedFiles, selectFiles } from '$lib/stores';
|
import { selectedFiles, selectFiles } from '$lib/stores';
|
||||||
import { dbUtils } from '$lib/db';
|
import { dbUtils, type GPXFileWithStatistics } from '$lib/db';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
import type { GPXFile } from 'gpx';
|
|
||||||
|
|
||||||
export let file: Readable<GPXFile | undefined>;
|
export let file: Readable<GPXFileWithStatistics | undefined>;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
{#if $file}
|
{#if $file}
|
||||||
<div
|
<div
|
||||||
on:contextmenu={() => {
|
on:contextmenu={() => {
|
||||||
if (!get(selectedFiles).has($file._data.id)) {
|
if (!get(selectedFiles).has($file.file._data.id)) {
|
||||||
get(selectFiles).select($file._data.id);
|
get(selectFiles).select($file.file._data.id);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -33,13 +32,13 @@
|
|||||||
class="w-full h-full px-1.5 py-2"
|
class="w-full h-full px-1.5 py-2"
|
||||||
on:contextmenu={(e) => {
|
on:contextmenu={(e) => {
|
||||||
if (e.ctrlKey) {
|
if (e.ctrlKey) {
|
||||||
get(selectFiles).addSelect($file._data.id);
|
get(selectFiles).addSelect($file.file._data.id);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{$file.metadata.name}
|
{$file.file.metadata.name}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</ContextMenu.Trigger>
|
</ContextMenu.Trigger>
|
||||||
|
@@ -11,9 +11,9 @@
|
|||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
import { locale } from 'svelte-i18n';
|
import { locale } from 'svelte-i18n';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
import { mapboxAccessToken } from '$lib/assets/layers';
|
||||||
|
|
||||||
mapboxgl.accessToken =
|
mapboxgl.accessToken = mapboxAccessToken;
|
||||||
'pk.eyJ1IjoiZ3B4c3R1ZGlvIiwiYSI6ImNrdHVoM2pjNTBodmUycG1yZTNwcnJ3MzkifQ.YZnNs9s9oCQPzoXAWs_SLg';
|
|
||||||
|
|
||||||
let fitBoundsOptions: mapboxgl.FitBoundsOptions = {
|
let fitBoundsOptions: mapboxgl.FitBoundsOptions = {
|
||||||
maxZoom: 15,
|
maxZoom: 15,
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import type { GPXFile } from "gpx";
|
|
||||||
import { map, selectFiles, currentTool, Tool } from "$lib/stores";
|
import { map, selectFiles, currentTool, Tool } from "$lib/stores";
|
||||||
import { settings } from "$lib/db";
|
import { settings, type GPXFileWithStatistics } from "$lib/db";
|
||||||
import { get, type Readable } from "svelte/store";
|
import { get, type Readable } from "svelte/store";
|
||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl from "mapbox-gl";
|
||||||
|
|
||||||
@@ -37,12 +36,12 @@ function decrementColor(color: string) {
|
|||||||
colorCount[color]--;
|
colorCount[color]--;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { directionMarkers } = settings;
|
const { directionMarkers, distanceMarkers, distanceUnits } = settings;
|
||||||
|
|
||||||
export class GPXLayer {
|
export class GPXLayer {
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
fileId: string;
|
fileId: string;
|
||||||
file: Readable<GPXFile | undefined>;
|
file: Readable<GPXFileWithStatistics | undefined>;
|
||||||
layerColor: string;
|
layerColor: string;
|
||||||
popup: mapboxgl.Popup;
|
popup: mapboxgl.Popup;
|
||||||
popupElement: HTMLElement;
|
popupElement: HTMLElement;
|
||||||
@@ -52,35 +51,41 @@ export class GPXLayer {
|
|||||||
updateBinded: () => void = this.update.bind(this);
|
updateBinded: () => void = this.update.bind(this);
|
||||||
selectOnClickBinded: (e: any) => void = this.selectOnClick.bind(this);
|
selectOnClickBinded: (e: any) => void = this.selectOnClick.bind(this);
|
||||||
|
|
||||||
constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFile | undefined>, popup: mapboxgl.Popup, popupElement: HTMLElement) {
|
constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFileWithStatistics | undefined>, popup: mapboxgl.Popup, popupElement: HTMLElement) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
this.fileId = fileId;
|
this.fileId = fileId;
|
||||||
this.file = file
|
this.file = file;
|
||||||
this.layerColor = getColor();
|
this.layerColor = getColor();
|
||||||
this.popup = popup;
|
this.popup = popup;
|
||||||
this.popupElement = popupElement;
|
this.popupElement = popupElement;
|
||||||
this.unsubscribe.push(file.subscribe(this.updateBinded));
|
this.unsubscribe.push(file.subscribe(this.updateBinded));
|
||||||
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded));
|
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded));
|
||||||
|
this.unsubscribe.push(distanceMarkers.subscribe(this.updateBinded));
|
||||||
|
this.unsubscribe.push(distanceUnits.subscribe(() => {
|
||||||
|
if (get(distanceMarkers)) {
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
this.map.on('style.load', this.updateBinded);
|
this.map.on('style.load', this.updateBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
let file = get(this.file);
|
let file = get(this.file)?.file;
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let addedSource = false;
|
|
||||||
try {
|
try {
|
||||||
if (!this.map.getSource(this.fileId)) {
|
|
||||||
let data = this.getGeoJSON();
|
|
||||||
|
|
||||||
|
let source = this.map.getSource(this.fileId);
|
||||||
|
if (source) {
|
||||||
|
source.setData(this.getGeoJSON());
|
||||||
|
} else {
|
||||||
this.map.addSource(this.fileId, {
|
this.map.addSource(this.fileId, {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data
|
data: this.getGeoJSON()
|
||||||
});
|
});
|
||||||
addedSource = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.map.getLayer(this.fileId)) {
|
if (!this.map.getLayer(this.fileId)) {
|
||||||
@@ -104,7 +109,6 @@ export class GPXLayer {
|
|||||||
this.map.on('mouseleave', this.fileId, toDefaultCursor);
|
this.map.on('mouseleave', this.fileId, toDefaultCursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (get(directionMarkers)) {
|
if (get(directionMarkers)) {
|
||||||
if (!this.map.getLayer(this.fileId + '-direction')) {
|
if (!this.map.getLayer(this.fileId + '-direction')) {
|
||||||
this.map.addLayer({
|
this.map.addLayer({
|
||||||
@@ -115,6 +119,7 @@ export class GPXLayer {
|
|||||||
'text-field': '>',
|
'text-field': '>',
|
||||||
'text-keep-upright': false,
|
'text-keep-upright': false,
|
||||||
'text-max-angle': 361,
|
'text-max-angle': 361,
|
||||||
|
'text-allow-overlap': true,
|
||||||
'symbol-placement': 'line',
|
'symbol-placement': 'line',
|
||||||
'symbol-spacing': 25,
|
'symbol-spacing': 25,
|
||||||
},
|
},
|
||||||
@@ -130,17 +135,47 @@ export class GPXLayer {
|
|||||||
this.map.removeLayer(this.fileId + '-direction');
|
this.map.removeLayer(this.fileId + '-direction');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (get(distanceMarkers)) {
|
||||||
|
let distanceSource = this.map.getSource(this.fileId + '-distance');
|
||||||
|
if (distanceSource) {
|
||||||
|
distanceSource.setData(this.getDistanceMarkersGeoJSON());
|
||||||
|
} else {
|
||||||
|
this.map.addSource(this.fileId + '-distance', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: this.getDistanceMarkersGeoJSON()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!this.map.getLayer(this.fileId + '-distance')) {
|
||||||
|
this.map.addLayer({
|
||||||
|
id: this.fileId + '-distance',
|
||||||
|
type: 'symbol',
|
||||||
|
source: this.fileId + '-distance',
|
||||||
|
layout: {
|
||||||
|
'text-field': ['get', 'distance'],
|
||||||
|
'text-size': 12,
|
||||||
|
'text-font': ['Open Sans Regular'],
|
||||||
|
'icon-image': ['get', 'icon'],
|
||||||
|
'icon-padding': 50,
|
||||||
|
'icon-allow-overlap': true,
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-halo-width': 0.1,
|
||||||
|
'text-halo-color': 'black'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.map.moveLayer(this.fileId + '-distance');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.map.getLayer(this.fileId + '-distance')) {
|
||||||
|
this.map.removeLayer(this.fileId + '-distance');
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) { // No reliable way to check if the map is ready to add sources and layers
|
} catch (e) { // No reliable way to check if the map is ready to add sources and layers
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!addedSource) {
|
|
||||||
let source = this.map.getSource(this.fileId);
|
|
||||||
if (source) {
|
|
||||||
source.setData(this.getGeoJSON());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let markerIndex = 0;
|
let markerIndex = 0;
|
||||||
file.wpt.forEach((waypoint) => { // Update markers
|
file.wpt.forEach((waypoint) => { // Update markers
|
||||||
if (markerIndex < this.markers.length) {
|
if (markerIndex < this.markers.length) {
|
||||||
@@ -176,6 +211,9 @@ export class GPXLayer {
|
|||||||
if (this.map.getLayer(this.fileId + '-direction')) {
|
if (this.map.getLayer(this.fileId + '-direction')) {
|
||||||
this.map.removeLayer(this.fileId + '-direction');
|
this.map.removeLayer(this.fileId + '-direction');
|
||||||
}
|
}
|
||||||
|
if (this.map.getLayer(this.fileId + '-distance')) {
|
||||||
|
this.map.removeLayer(this.fileId + '-distance');
|
||||||
|
}
|
||||||
if (this.map.getLayer(this.fileId)) {
|
if (this.map.getLayer(this.fileId)) {
|
||||||
this.map.removeLayer(this.fileId);
|
this.map.removeLayer(this.fileId);
|
||||||
}
|
}
|
||||||
@@ -199,6 +237,9 @@ export class GPXLayer {
|
|||||||
if (this.map.getLayer(this.fileId + '-direction')) {
|
if (this.map.getLayer(this.fileId + '-direction')) {
|
||||||
this.map.moveLayer(this.fileId + '-direction');
|
this.map.moveLayer(this.fileId + '-direction');
|
||||||
}
|
}
|
||||||
|
if (this.map.getLayer(this.fileId + '-distance')) {
|
||||||
|
this.map.moveLayer(this.fileId + '-distance');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectOnClick(e: any) {
|
selectOnClick(e: any) {
|
||||||
@@ -213,7 +254,7 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getGeoJSON(): GeoJSON.FeatureCollection {
|
getGeoJSON(): GeoJSON.FeatureCollection {
|
||||||
let file = get(this.file);
|
let file = get(this.file)?.file;
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return {
|
return {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
@@ -238,6 +279,41 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
|
||||||
|
let statistics = get(this.file)?.statistics;
|
||||||
|
if (!statistics) {
|
||||||
|
return {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let features = [];
|
||||||
|
let currentTargetDistance = 1;
|
||||||
|
for (let i = 0; i < statistics.local.distance.length; i++) {
|
||||||
|
if (statistics.local.distance[i] >= currentTargetDistance * (get(distanceUnits) === 'metric' ? 1 : 1.60934)) {
|
||||||
|
let distance = currentTargetDistance.toFixed(0);
|
||||||
|
features.push({
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: [statistics.local.points[i].getLongitude(), statistics.local.points[i].getLatitude()]
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
distance,
|
||||||
|
icon: distance.length < 3 ? 'circle-white-2' : 'circle-white-3'
|
||||||
|
}
|
||||||
|
} as GeoJSON.Feature);
|
||||||
|
currentTargetDistance += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toPointerCursor() {
|
function toPointerCursor() {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { distance, type Coordinates, type GPXFile, TrackPoint, TrackSegment } from "gpx";
|
import { distance, type Coordinates, TrackPoint, TrackSegment } from "gpx";
|
||||||
import { get, type Readable, type Writable } from "svelte/store";
|
import { get, type Readable } from "svelte/store";
|
||||||
import { computeAnchorPoints } from "./Simplify";
|
import { computeAnchorPoints } from "./Simplify";
|
||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl from "mapbox-gl";
|
||||||
import { route } from "./Routing";
|
import { route } from "./Routing";
|
||||||
@@ -7,12 +7,12 @@ import { route } from "./Routing";
|
|||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
|
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import { dbUtils } from "$lib/db";
|
import { dbUtils, type GPXFileWithStatistics } from "$lib/db";
|
||||||
|
|
||||||
export class RoutingControls {
|
export class RoutingControls {
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
fileId: string = '';
|
fileId: string = '';
|
||||||
file: Readable<GPXFile | undefined>;
|
file: Readable<GPXFileWithStatistics | undefined>;
|
||||||
anchors: AnchorWithMarker[] = [];
|
anchors: AnchorWithMarker[] = [];
|
||||||
shownAnchors: AnchorWithMarker[] = [];
|
shownAnchors: AnchorWithMarker[] = [];
|
||||||
popup: mapboxgl.Popup;
|
popup: mapboxgl.Popup;
|
||||||
@@ -25,7 +25,7 @@ export class RoutingControls {
|
|||||||
updateTemporaryAnchorBinded: (e: any) => void = this.updateTemporaryAnchor.bind(this);
|
updateTemporaryAnchorBinded: (e: any) => void = this.updateTemporaryAnchor.bind(this);
|
||||||
appendAnchorBinded: (e: mapboxgl.MapMouseEvent) => void = this.appendAnchor.bind(this);
|
appendAnchorBinded: (e: mapboxgl.MapMouseEvent) => void = this.appendAnchor.bind(this);
|
||||||
|
|
||||||
constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFile | undefined>, popup: mapboxgl.Popup, popupElement: HTMLElement) {
|
constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFileWithStatistics | undefined>, popup: mapboxgl.Popup, popupElement: HTMLElement) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
this.fileId = fileId;
|
this.fileId = fileId;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
@@ -54,7 +54,7 @@ export class RoutingControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateControls() { // Update the markers when the file changes
|
updateControls() { // Update the markers when the file changes
|
||||||
let file = get(this.file);
|
let file = get(this.file)?.file;
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -263,8 +263,10 @@ export class RoutingControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getPermanentAnchor(): Anchor {
|
getPermanentAnchor(): Anchor {
|
||||||
|
let file = get(this.file)?.file;
|
||||||
|
|
||||||
// Find the closest point closest to the temporary anchor
|
// Find the closest point closest to the temporary anchor
|
||||||
let segments = get(this.file).getSegments();
|
let segments = file.getSegments();
|
||||||
let minDistance = Number.MAX_VALUE;
|
let minDistance = Number.MAX_VALUE;
|
||||||
let minAnchor = this.temporaryAnchor as Anchor;
|
let minAnchor = this.temporaryAnchor as Anchor;
|
||||||
for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) {
|
for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) {
|
||||||
@@ -350,7 +352,12 @@ export class RoutingControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
routeToStart() {
|
routeToStart() {
|
||||||
let segments = get(this.file).getSegments();
|
let file = get(this.file)?.file;
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let segments = file.getSegments();
|
||||||
if (segments.length === 0) {
|
if (segments.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -366,7 +373,12 @@ export class RoutingControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createRoundTrip() {
|
createRoundTrip() {
|
||||||
let segments = get(this.file).getSegments();
|
let file = get(this.file)?.file;
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let segments = file.getSegments();
|
||||||
if (segments.length === 0) {
|
if (segments.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import Dexie, { liveQuery } from 'dexie';
|
import Dexie, { liveQuery } from 'dexie';
|
||||||
import { GPXFile } from 'gpx';
|
import { GPXFile, GPXStatistics } from 'gpx';
|
||||||
import { enableMapSet, enablePatches, produceWithPatches, applyPatches, type Patch } from 'immer';
|
import { enableMapSet, enablePatches, produceWithPatches, applyPatches, type Patch } from 'immer';
|
||||||
import { writable, get, derived, type Readable, type Writable } from 'svelte/store';
|
import { writable, get, derived, type Readable, type Writable } from 'svelte/store';
|
||||||
import { fileOrder, selectedFiles } from './stores';
|
import { fileOrder, initTargetMapBounds, selectedFiles, updateTargetMapBounds } from './stores';
|
||||||
import { mode } from 'mode-watcher';
|
import { mode } from 'mode-watcher';
|
||||||
import { defaultBasemap, defaultBasemapTree, defaultOverlayTree, defaultOverlays } from './assets/layers';
|
import { defaultBasemap, defaultBasemapTree, defaultOverlayTree, defaultOverlays } from './assets/layers';
|
||||||
|
|
||||||
@@ -107,14 +107,23 @@ function dexieStore<T>(querier: () => T | Promise<T>, initial?: T): Readable<T>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GPXFileWithStatistics = { file: GPXFile, statistics: GPXStatistics };
|
||||||
|
|
||||||
// Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber, also takes care of the conversion to a GPXFile object
|
// Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber, also takes care of the conversion to a GPXFile object
|
||||||
function dexieGPXFileStore(querier: () => GPXFile | undefined | Promise<GPXFile | undefined>): Readable<GPXFile> {
|
function dexieGPXFileStore(querier: () => GPXFile | undefined | Promise<GPXFile | undefined>): Readable<GPXFileWithStatistics> {
|
||||||
let store = writable<GPXFile>(undefined);
|
let store = writable<GPXFileWithStatistics>(undefined);
|
||||||
liveQuery(querier).subscribe(value => {
|
liveQuery(querier).subscribe(value => {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
let gpx = new GPXFile(value);
|
let gpx = new GPXFile(value);
|
||||||
|
let statistics = gpx.getStatistics();
|
||||||
|
if (!fileState.has(gpx._data.id)) { // Update the map bounds for new files
|
||||||
|
updateTargetMapBounds(statistics.global.bounds);
|
||||||
|
}
|
||||||
fileState.set(gpx._data.id, gpx);
|
fileState.set(gpx._data.id, gpx);
|
||||||
store.set(gpx);
|
store.set({
|
||||||
|
file: gpx,
|
||||||
|
statistics
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -155,7 +164,7 @@ function commitFileStateChange(newFileState: ReadonlyMap<string, GPXFile>, patch
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fileObservers: Writable<Map<string, Readable<GPXFile | undefined>>> = writable(new Map());
|
export const fileObservers: Writable<Map<string, Readable<GPXFileWithStatistics | undefined>>> = writable(new Map());
|
||||||
const fileState: Map<string, GPXFile> = new Map(); // Used to generate patches
|
const fileState: Map<string, GPXFile> = new Map(); // Used to generate patches
|
||||||
|
|
||||||
// Observe the file ids in the database, and maintain a map of file observers for the corresponding files
|
// Observe the file ids in the database, and maintain a map of file observers for the corresponding files
|
||||||
@@ -164,6 +173,11 @@ liveQuery(() => db.fileids.toArray()).subscribe(dbFileIds => {
|
|||||||
let newFiles = dbFileIds.filter(id => !get(fileObservers).has(id)).sort((a, b) => parseInt(a.split('-')[1]) - parseInt(b.split('-')[1]));
|
let newFiles = dbFileIds.filter(id => !get(fileObservers).has(id)).sort((a, b) => parseInt(a.split('-')[1]) - parseInt(b.split('-')[1]));
|
||||||
// Find deleted files to stop observing
|
// Find deleted files to stop observing
|
||||||
let deletedFiles = Array.from(get(fileObservers).keys()).filter(id => !dbFileIds.find(fileId => fileId === id));
|
let deletedFiles = Array.from(get(fileObservers).keys()).filter(id => !dbFileIds.find(fileId => fileId === id));
|
||||||
|
|
||||||
|
if (newFiles.length > 0) { // Reset the target map bounds when new files are added
|
||||||
|
initTargetMapBounds(fileState.size === 0);
|
||||||
|
}
|
||||||
|
|
||||||
// Update the store
|
// Update the store
|
||||||
if (newFiles.length > 0 || deletedFiles.length > 0) {
|
if (newFiles.length > 0 || deletedFiles.length > 0) {
|
||||||
fileObservers.update($files => {
|
fileObservers.update($files => {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { writable, get, type Writable } from 'svelte/store';
|
import { writable, get, type Writable } from 'svelte/store';
|
||||||
|
|
||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { GPXFile, buildGPX, parseGPX, GPXStatistics } from 'gpx';
|
import { GPXFile, buildGPX, parseGPX, GPXStatistics, type Coordinates } from 'gpx';
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
import type { GPXLayer } from '$lib/components/gpx-layer/GPXLayer';
|
import type { GPXLayer } from '$lib/components/gpx-layer/GPXLayer';
|
||||||
@@ -28,86 +28,31 @@ fileObservers.subscribe((files) => { // Update selectedFiles automatically when
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const targetMapBounds = writable({
|
|
||||||
bounds: new mapboxgl.LngLatBounds([180, 90, -180, -90]),
|
|
||||||
initial: true
|
|
||||||
});
|
|
||||||
const fileStatistics: Map<string, Writable<GPXStatistics>> = new Map();
|
|
||||||
const fileUnsubscribe: Map<string, Function> = new Map();
|
|
||||||
export const gpxStatistics: Writable<GPXStatistics> = writable(new GPXStatistics());
|
export const gpxStatistics: Writable<GPXStatistics> = writable(new GPXStatistics());
|
||||||
|
|
||||||
function updateGPXData() {
|
function updateGPXData() {
|
||||||
let fileIds: string[] = get(fileOrder).filter((f) => fileStatistics.has(f) && get(selectedFiles).has(f));
|
let fileIds: string[] = get(fileOrder).filter((f) => get(selectedFiles).has(f));
|
||||||
gpxStatistics.set(fileIds.reduce((stats: GPXStatistics, fileId: string) => {
|
gpxStatistics.set(fileIds.reduce((stats: GPXStatistics, fileId: string) => {
|
||||||
let statisticsStore = fileStatistics.get(fileId);
|
let fileStore = get(fileObservers).get(fileId);
|
||||||
if (statisticsStore) {
|
if (fileStore) {
|
||||||
stats.mergeWith(get(statisticsStore));
|
let statistics = get(fileStore)?.statistics;
|
||||||
|
if (statistics) {
|
||||||
|
stats.mergeWith(statistics);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return stats;
|
return stats;
|
||||||
}, new GPXStatistics()));
|
}, new GPXStatistics()));
|
||||||
}
|
}
|
||||||
|
|
||||||
fileObservers.subscribe((files) => { // Maintain up-to-date statistics
|
|
||||||
if (files.size > fileStatistics.size) { // Files are added
|
|
||||||
let bounds = new mapboxgl.LngLatBounds([180, 90, -180, -90]);
|
|
||||||
let mapBounds = new mapboxgl.LngLatBounds([180, 90, -180, -90]);
|
|
||||||
if (fileStatistics.size > 0) { // Some files are already loaded
|
|
||||||
mapBounds = get(map)?.getBounds() ?? mapBounds;
|
|
||||||
bounds.extend(mapBounds);
|
|
||||||
}
|
|
||||||
targetMapBounds.set({
|
|
||||||
bounds: bounds,
|
|
||||||
initial: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fileStatistics.forEach((stats, fileId) => {
|
|
||||||
if (!files.has(fileId)) {
|
|
||||||
fileStatistics.delete(fileId);
|
|
||||||
let unsubscribe = fileUnsubscribe.get(fileId);
|
|
||||||
if (unsubscribe) unsubscribe();
|
|
||||||
fileUnsubscribe.delete(fileId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
files.forEach((fileObserver, fileId) => {
|
|
||||||
if (!fileStatistics.has(fileId)) {
|
|
||||||
let statisticsStore = writable(new GPXStatistics());
|
|
||||||
fileStatistics.set(fileId, statisticsStore);
|
|
||||||
let unsubscribe = fileObserver.subscribe((file) => {
|
|
||||||
if (file) {
|
|
||||||
statisticsStore.set(file.getStatistics());
|
|
||||||
if (get(selectedFiles).has(fileId)) {
|
|
||||||
updateGPXData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
fileUnsubscribe.set(fileId, unsubscribe);
|
|
||||||
|
|
||||||
let boundsUnsubscribe = statisticsStore.subscribe((stats) => {
|
|
||||||
let fileBounds = stats.global.bounds;
|
|
||||||
if (fileBounds.southWest.lat == 90 && fileBounds.southWest.lon == 180 && fileBounds.northEast.lat == -90 && fileBounds.northEast.lon == -180) { // Stats are not yet calculated
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (fileBounds.southWest.lat != fileBounds.northEast.lat || fileBounds.southWest.lon != fileBounds.northEast.lon) { // Avoid update for new files
|
|
||||||
targetMapBounds.update((target) => {
|
|
||||||
target.bounds.extend(fileBounds.southWest);
|
|
||||||
target.bounds.extend(fileBounds.northEast);
|
|
||||||
target.bounds.extend([fileBounds.southWest.lon, fileBounds.northEast.lat]);
|
|
||||||
target.bounds.extend([fileBounds.northEast.lon, fileBounds.southWest.lat]);
|
|
||||||
target.initial = false;
|
|
||||||
return target;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
boundsUnsubscribe();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
selectedFiles.subscribe((selectedFiles) => { // Maintain up-to-date statistics for the current selection
|
selectedFiles.subscribe((selectedFiles) => { // Maintain up-to-date statistics for the current selection
|
||||||
updateGPXData();
|
updateGPXData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const targetMapBounds = writable({
|
||||||
|
bounds: new mapboxgl.LngLatBounds([180, 90, -180, -90]),
|
||||||
|
initial: true
|
||||||
|
});
|
||||||
|
|
||||||
targetMapBounds.subscribe((bounds) => {
|
targetMapBounds.subscribe((bounds) => {
|
||||||
if (bounds.initial) {
|
if (bounds.initial) {
|
||||||
return;
|
return;
|
||||||
@@ -120,6 +65,36 @@ targetMapBounds.subscribe((bounds) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export function initTargetMapBounds(first: boolean) {
|
||||||
|
let bounds = new mapboxgl.LngLatBounds([180, 90, -180, -90]);
|
||||||
|
let mapBounds = new mapboxgl.LngLatBounds([180, 90, -180, -90]);
|
||||||
|
if (!first) { // Some files are already loaded
|
||||||
|
mapBounds = get(map)?.getBounds() ?? mapBounds;
|
||||||
|
bounds.extend(mapBounds);
|
||||||
|
}
|
||||||
|
targetMapBounds.set({
|
||||||
|
bounds: bounds,
|
||||||
|
initial: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTargetMapBounds(bounds: {
|
||||||
|
southWest: Coordinates,
|
||||||
|
northEast: Coordinates
|
||||||
|
}) {
|
||||||
|
if (bounds.southWest.lat == 90 && bounds.southWest.lon == 180 && bounds.northEast.lat == -90 && bounds.northEast.lon == -180) { // Avoid update for empty (new) files
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetMapBounds.update((target) => {
|
||||||
|
target.bounds.extend(bounds.southWest);
|
||||||
|
target.bounds.extend(bounds.northEast);
|
||||||
|
target.initial = false;
|
||||||
|
return target;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const gpxLayers: Writable<Map<string, GPXLayer>> = writable(new Map());
|
export const gpxLayers: Writable<Map<string, GPXLayer>> = writable(new Map());
|
||||||
|
|
||||||
export enum Tool {
|
export enum Tool {
|
||||||
|
Reference in New Issue
Block a user