From 763a7406ce71201c0231934d137ed87960fc4bdc Mon Sep 17 00:00:00 2001 From: vcoppe Date: Sun, 4 Oct 2026 10:15:51 +0200 Subject: [PATCH] progress --- .../lib/components/map/gpx-layer/gpx-layer.ts | 369 +++++++----------- .../components/map/gpx-layer/gpx-layers.ts | 10 +- 2 files changed, 141 insertions(+), 238 deletions(-) diff --git a/website/src/lib/components/map/gpx-layer/gpx-layer.ts b/website/src/lib/components/map/gpx-layer/gpx-layer.ts index fe2b227ab..77e0a6508 100644 --- a/website/src/lib/components/map/gpx-layer/gpx-layer.ts +++ b/website/src/lib/components/map/gpx-layer/gpx-layer.ts @@ -1,4 +1,5 @@ import { get, type Readable } from 'svelte/store'; +import type { GPXFile } from 'gpx'; import maplibregl, { type GeoJSONSource, type FilterSpecification, @@ -7,20 +8,14 @@ import maplibregl, { } from 'maplibre-gl'; import { map } from '$lib/components/map/map'; import { waypointPopup, trackpointPopup } from './gpx-layer-popup'; -import { - ListTrackSegmentItem, - ListWaypointItem, - ListWaypointsItem, - ListTrackItem, - ListFileItem, - ListRootItem, -} from '$lib/components/file-list/file-list'; import { getClosestLinePoint, getElevation, loadSVGIcon } from '$lib/utils'; import { selectedWaypoint } from '$lib/components/toolbar/tools/waypoint/waypoint'; import { MapPin, Square } from 'lucide-static'; import { getSymbolKey, symbols } from '$lib/assets/symbols'; -import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree'; -import { selection } from '$lib/logic/selection'; +import { fileStateCollection } from '$lib/logic/file-state'; +import { engine, type FileState, type Selection } from '$lib/engine'; +import { isCovered, hasSelectionWithin, type FileTreeNode } from '$lib/selection-helpers'; +import { isSegmentHidden, isWaypointHidden } from '$lib/file-visibility'; import { settings } from '$lib/logic/settings'; import { currentTool, Tool } from '$lib/components/toolbar/tools'; import { fileActionManager } from '$lib/logic/file-action-manager'; @@ -28,58 +23,6 @@ import { fileActions } from '$lib/logic/file-actions'; import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor'; import { ANCHOR_LAYER_KEY } from '$lib/components/map/style'; -import { gpxColors } from '$lib/components/map/gpx-layer/gpx-layers'; - -const colors = [ - '#ff0000', - '#0000ff', - '#46e646', - '#00ccff', - '#ff9900', - '#ff00ff', - '#ffff32', - '#288228', - '#9933ff', - '#50f0be', - '#8c645a', -]; - -const colorCount: { [key: string]: number } = {}; -for (let color of colors) { - colorCount[color] = 0; -} - -// Get the color with the least amount of uses -function getColor(fileId: string) { - let color = colors.reduce((a, b) => (colorCount[a] <= colorCount[b] ? a : b)); - colorCount[color]++; - gpxColors.update((colors) => { - colors.set(fileId, color); - return colors; - }); - return color; -} - -function replaceColor(fileId: string, oldColor: string, newColor: string) { - if (colorCount.hasOwnProperty(oldColor)) { - colorCount[oldColor]--; - } - colorCount[newColor]++; - gpxColors.update((colors) => { - colors.set(fileId, newColor); - return colors; - }); -} - -function removeColor(fileId: string, color: string) { - if (colorCount.hasOwnProperty(color)) { - colorCount[color]--; - } - gpxColors.update((colors) => { - colors.delete(fileId); - return colors; - }); -} export function getSvgForSymbol(symbol?: string | undefined, layerColor?: string | undefined) { let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined; @@ -115,8 +58,9 @@ const { directionMarkers, treeFileView, defaultOpacity, defaultWidth } = setting export class GPXLayer { fileId: string; - file: Readable; - layerColor: string; + file: Readable; + layerColor: string = ''; + selectionState: Selection = { type: 'empty' }; selected: boolean = false; currentWaypointData: GeoJSON.FeatureCollection | null = null; draggedWaypointIndex: number | null = null; @@ -144,10 +88,9 @@ export class GPXLayer { waypointLayerOnMouseUpBinded: (e: MapLayerMouseEvent | MapLayerTouchEvent) => void = this.waypointLayerOnMouseUp.bind(this); - constructor(fileId: string, file: Readable) { + constructor(fileId: string, file: Readable) { this.fileId = fileId; this.file = file; - this.layerColor = getColor(fileId); this.unsubscribe.push( map.subscribe(($map) => { if ($map) { @@ -158,8 +101,9 @@ export class GPXLayer { ); this.unsubscribe.push(file.subscribe(this.updateBinded)); this.unsubscribe.push( - selection.subscribe(($selection) => { - let newSelected = $selection.hasAnyChildren(new ListFileItem(this.fileId)); + engine.selection.subscribe(($selection) => { + this.selectionState = $selection; + let newSelected = hasSelectionWithin($selection, this.fileNode()); if (this.selected || newSelected) { this.selected = newSelected; this.update(); @@ -172,21 +116,29 @@ export class GPXLayer { this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded)); } + fileNode(): FileTreeNode { + return { type: 'file', fileId: this.fileId }; + } + + segmentNode(properties: GeoJSON.GeoJsonProperties): FileTreeNode { + return { + type: 'segment', + fileId: this.fileId, + trackId: properties!.trackId, + segmentId: properties!.segmentId, + }; + } + update() { const _map = get(map); const layerEventManager = map.layerEventManager; - let file = get(this.file)?.file; - if (!_map || !layerEventManager || !file) { + const state = get(this.file); + if (!_map || !layerEventManager) { return; } - if ( - file._data.style && - file._data.style.color && - this.layerColor !== `#${file._data.style.color}` - ) { - replaceColor(this.fileId, this.layerColor, `#${file._data.style.color}`); - this.layerColor = `#${file._data.style.color}`; + if (this.layerColor !== state.color) { + this.layerColor = state.color; } this.loadIcons(); @@ -228,16 +180,16 @@ export class GPXLayer { layerEventManager.on('mousemove', this.fileId, this.layerOnMouseMoveBinded); } - let visibleTrackSegmentIds: string[] = []; - file.forEachSegment((segment, trackIndex, segmentIndex) => { - if (!segment._data.hidden) { - visibleTrackSegmentIds.push(`${trackIndex}-${segmentIndex}`); - } - }); + const visibleSegmentIds = state.segments.features + .filter( + ({ properties }) => + !isSegmentHidden(state, properties.trackId, properties.segmentId) + ) + .map(({ properties }) => properties.segmentId); const segmentFilter: FilterSpecification = [ 'in', - ['get', 'trackSegmentId'], - ['literal', visibleTrackSegmentIds], + ['get', 'segmentId'], + ['literal', visibleSegmentIds], ]; _map.setFilter(this.fileId, segmentFilter, { validate: false }); @@ -334,12 +286,9 @@ export class GPXLayer { ); } - let visibleWaypoints: number[] = []; - file.wpt.forEach((waypoint, waypointIndex) => { - if (!waypoint._data.hidden) { - visibleWaypoints.push(waypointIndex); - } - }); + const visibleWaypoints = state.waypoints.features + .filter(({ properties }) => !isWaypointHidden(state, properties.waypointId)) + .map(({ properties }) => properties.index); _map.setFilter( this.fileId + '-waypoints', @@ -413,8 +362,6 @@ export class GPXLayer { } this.unsubscribe.forEach((unsubscribe) => unsubscribe()); - - removeColor(this.fileId, this.layerColor); } moveToFront() { @@ -434,14 +381,9 @@ export class GPXLayer { } layerOnMouseEnter(e: any) { - let trackIndex = e.features[0].properties.trackIndex; - let segmentIndex = e.features[0].properties.segmentIndex; - if ( get(currentTool) === Tool.SCISSORS && - get(selection).hasAnyParent( - new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) - ) + isCovered(this.selectionState, this.segmentNode(e.features[0].properties)) ) { mapCursor.notify(MapCursorState.SCISSORS, true); } else { @@ -459,66 +401,62 @@ export class GPXLayer { let trackIndex = e.features[0].properties.trackIndex; let segmentIndex = e.features[0].properties.segmentIndex; - const file = get(this.file)?.file; - if (file) { - const closest = getClosestLinePoint( - file.trk[trackIndex].trkseg[segmentIndex].trkpt, - { lat: e.lngLat.lat, lon: e.lngLat.lng } - ); - trackpointPopup?.setItem({ item: closest, fileId: this.fileId }); - } + // TODO needs the trackpoints of the previous implementation (see oldFile) + // const file = null; + // if (file) { + // const closest = getClosestLinePoint( + // file.trk[trackIndex].trkseg[segmentIndex].trkpt, + // { lat: e.lngLat.lat, lon: e.lngLat.lng } + // ); + // trackpointPopup?.setItem({ item: closest, fileId: this.fileId }); + // } } } layerOnClick(e: MapLayerMouseEvent) { + const properties = e.features![0].properties!; + const { trackId, segmentId, trackIndex, segmentIndex } = properties; + const selectionType = this.selectionState.type; + if ( get(currentTool) === Tool.ROUTING && - get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) + selectionType !== 'empty' && + selectionType !== 'waypoints' && + selectionType !== 'waypoint' ) { return; } - let trackIndex = e.features![0].properties!.trackIndex; - let segmentIndex = e.features![0].properties!.segmentIndex; - if ( get(currentTool) === Tool.SCISSORS && - get(selection).hasAnyParent( - new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) - ) + isCovered(this.selectionState, this.segmentNode(properties)) ) { if (get(map)?.queryRenderedFeatures(e.point, { layers: ['split-controls'] }).length) { // Clicked on split control, ignoring return; } - fileActions.split(get(splitAs), this.fileId, trackIndex, segmentIndex, { - lat: e.lngLat.lat, - lon: e.lngLat.lng, - }); + // TODO the engine can split (Command::Split) but needs the index of the trackpoint + // fileActions.split(get(splitAs), this.fileId, trackIndex, segmentIndex, { + // lat: e.lngLat.lat, + // lon: e.lngLat.lng, + // }); return; } - let file = get(this.file)?.file; - if (!file) { - return; - } - - let item = undefined; - if (get(treeFileView) && file.getSegments().length > 1) { + const add = e.originalEvent.ctrlKey || e.originalEvent.metaKey; + const { tracks } = get(this.file).structure; + const segmentCount = tracks.reduce((count, track) => count + track.segments.length, 0); + if (get(treeFileView) && segmentCount > 1) { // Select inner item - item = - file.children[trackIndex].children.length > 1 - ? new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) - : new ListTrackItem(this.fileId, trackIndex); + const track = tracks.find((track) => track.id === trackId); + if (track && track.segments.length > 1) { + engine.selectSegments(this.fileId, trackId, [segmentId], add); + } else { + engine.selectTracks(this.fileId, [trackId], add); + } } else { - item = new ListFileItem(this.fileId); - } - - if (e.originalEvent.ctrlKey || e.originalEvent.metaKey) { - selection.addSelectItem(item); - } else { - selection.selectItem(item); + engine.select([this.fileId], add); } } @@ -532,14 +470,12 @@ export class GPXLayer { if (this.draggedWaypointIndex !== null) { return; } - let file = get(this.file)?.file; - if (!file) { - return; - } - let waypointIndex = e.features![0].properties!.waypointIndex; - let waypoint = file.wpt[waypointIndex]; - waypointPopup?.setItem({ item: waypoint, fileId: this.fileId }); + // TODO the popup needs the waypoint of the previous implementation (see oldFile) + // let waypoint = this.oldFile?.wpt[e.features![0].properties!.waypointIndex]; + // if (waypoint) { + // waypointPopup?.setItem({ item: waypoint, fileId: this.fileId }); + // } mapCursor.notify(MapCursorState.WAYPOINT_HOVER, true); } @@ -551,41 +487,43 @@ export class GPXLayer { waypointLayerOnClick(e: MapLayerMouseEvent) { e.preventDefault(); - let waypointIndex = e.features![0].properties!.waypointIndex; - let file = get(this.file)?.file; - if (!file) { - return; - } - - let waypoint = file.wpt[waypointIndex]; + const { waypointIndex, waypointId } = e.features![0].properties!; + // TODO the popup and the waypoint tool need the waypoint of the previous implementation if (get(currentTool) === Tool.WAYPOINT) { if (this.selected) { if (e.originalEvent.shiftKey) { - fileActions.deleteWaypoint(this.fileId, waypointIndex); + // TODO + // fileActions.deleteWaypoint(this.fileId, waypointIndex); } else { - selection.selectItem(new ListWaypointItem(this.fileId, waypointIndex)); - selectedWaypoint.set([waypoint, this.fileId]); + engine.selectWaypoints(this.fileId, [waypointId]); + // TODO + // if (waypoint) { + // selectedWaypoint.set([waypoint, this.fileId]); + // } } } else { if (get(treeFileView)) { - selection.selectItem(new ListWaypointItem(this.fileId, waypointIndex)); + engine.selectWaypoints(this.fileId, [waypointId]); } else { - selection.selectItem(new ListFileItem(this.fileId)); + engine.select([this.fileId]); } - selectedWaypoint.set([waypoint, this.fileId]); + // TODO + // if (waypoint) { + // selectedWaypoint.set([waypoint, this.fileId]); + // } } } else { if (get(treeFileView)) { - if ((e.originalEvent.ctrlKey || e.originalEvent.metaKey) && this.selected) { - selection.addSelectItem(new ListWaypointItem(this.fileId, waypointIndex)); - } else { - selection.selectItem(new ListWaypointItem(this.fileId, waypointIndex)); - } + const add = e.originalEvent.ctrlKey || e.originalEvent.metaKey; + engine.selectWaypoints(this.fileId, [waypointId], add && this.selected); } else { if (!this.selected) { - selection.selectItem(new ListFileItem(this.fileId)); + engine.select([this.fileId]); } - waypointPopup?.setItem({ item: waypoint, fileId: this.fileId }); + // TODO + // if (waypoint) { + // waypointPopup?.setItem({ item: waypoint, fileId: this.fileId }); + // } } } } @@ -702,93 +640,58 @@ export class GPXLayer { } getGeoJSON(): GeoJSON.FeatureCollection { - let file = get(this.file)?.file; - if (!file) { - return { - type: 'FeatureCollection', - features: [], - }; - } + const state = get(this.file); + const waypointsSelected = hasSelectionWithin(this.selectionState, { + type: 'waypoints', + fileId: this.fileId, + }); - let data = file.toGeoJSON(); - - let trackIndex = 0, - segmentIndex = 0; - for (let feature of data.features) { - if (!feature.properties) { - feature.properties = {}; - } - if (!feature.properties.color) { - feature.properties.color = this.layerColor; - } - if (!feature.properties.opacity) { - feature.properties.opacity = get(defaultOpacity); - } - if (!feature.properties.width) { - feature.properties.width = get(defaultWidth); - } - if ( - get(selection).hasAnyParent( - new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) - ) || - get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), true) - ) { - feature.properties.width = feature.properties.width + 2; - feature.properties.opacity = Math.min(1, feature.properties.opacity + 0.1); - } - feature.properties.trackIndex = trackIndex; - feature.properties.segmentIndex = segmentIndex; - feature.properties.trackSegmentId = `${trackIndex}-${segmentIndex}`; - - segmentIndex++; - if (segmentIndex >= file.trk[trackIndex].trkseg.length) { - segmentIndex = 0; - trackIndex++; - } - } - return data; + return { + type: 'FeatureCollection', + features: state.segments.features.map((feature) => { + const properties = { + ...feature.properties, + opacity: feature.properties.opacity ?? get(defaultOpacity), + width: feature.properties.width ?? get(defaultWidth), + }; + if ( + isCovered(this.selectionState, this.segmentNode(properties)) || + waypointsSelected + ) { + properties.width = properties.width + 2; + properties.opacity = Math.min(1, properties.opacity + 0.1); + } + return { ...feature, properties }; + }), + }; } getWaypointsGeoJSON(): GeoJSON.FeatureCollection { - let file = get(this.file)?.file; - - let data: GeoJSON.FeatureCollection = { + return { type: 'FeatureCollection', - features: [], - }; - - if (!file) { - return data; - } - - file.wpt.forEach((waypoint, index) => { - data.features.push({ + features: get(this.file).waypoints.features.map((feature) => ({ type: 'Feature', - geometry: { - type: 'Point', - coordinates: [waypoint.getLongitude(), waypoint.getLatitude()], - }, + // copied: the coordinates are changed while a waypoint is dragged + geometry: { type: 'Point', coordinates: [...feature.geometry.coordinates] }, properties: { fileId: this.fileId, - waypointIndex: index, - icon: `waypoint-${getSymbolKey(waypoint.sym) ?? 'default'}-${this.layerColor}`, + waypointId: feature.properties.waypointId, + waypointIndex: feature.properties.index, + icon: `waypoint-${getSymbolKey(feature.properties.sym) ?? 'default'}-${this.layerColor}`, }, - }); - }); - - return data; + })), + }; } loadIcons() { const _map = get(map); - let file = get(this.file)?.file; - if (!_map || !file) { + if (!_map) { return; } let symbols = new Set(); - file.wpt.forEach((waypoint) => { - symbols.add(getSymbolKey(waypoint.sym)); + get(this.file).waypoints.features.forEach((feature) => { + symbols.add(getSymbolKey(feature.properties.sym)); }); symbols.forEach((symbol) => { diff --git a/website/src/lib/components/map/gpx-layer/gpx-layers.ts b/website/src/lib/components/map/gpx-layer/gpx-layers.ts index aa3c45e87..2b39b4e72 100644 --- a/website/src/lib/components/map/gpx-layer/gpx-layers.ts +++ b/website/src/lib/components/map/gpx-layer/gpx-layers.ts @@ -1,10 +1,10 @@ -import { GPXFileStateCollectionObserver } from '$lib/logic/file-state'; +import { FileStateCollectionObserver } from '$lib/engine'; import { writable } from 'svelte/store'; import { GPXLayer } from './gpx-layer'; export class GPXLayerCollection { private _layers: Map; - private _fileStateCollectionObserver: GPXFileStateCollectionObserver | null = null; + private _fileStateCollectionObserver: FileStateCollectionObserver | null = null; constructor() { this._layers = new Map(); @@ -14,10 +14,10 @@ export class GPXLayerCollection { if (this._fileStateCollectionObserver) { return; } - this._fileStateCollectionObserver = new GPXFileStateCollectionObserver( + this._fileStateCollectionObserver = new FileStateCollectionObserver( (newFiles) => { - newFiles.forEach((fileState, fileId) => { - const layer = new GPXLayer(fileId, fileState); + newFiles.forEach((fileStore, fileId) => { + const layer = new GPXLayer(fileId, fileStore); this._layers.set(fileId, layer); }); },