From a8ef6dcae19406b184bfb36a0d75b2a72ddb3d65 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Wed, 19 Jun 2024 16:15:21 +0200 Subject: [PATCH] manage file style --- gpx/src/gpx.ts | 78 +++++- gpx/src/index.ts | 2 +- .../file-list/FileListNodeLabel.svelte | 222 +++++++++++++++++- .../src/lib/components/gpx-layer/GPXLayer.ts | 21 +- .../layer-control/LayerControlSettings.svelte | 2 +- website/src/lib/db.ts | 29 ++- website/src/lib/stores.ts | 17 +- website/src/locales/en.json | 21 +- 8 files changed, 356 insertions(+), 36 deletions(-) diff --git a/gpx/src/gpx.ts b/gpx/src/gpx.ts index f5969109..8070801c 100644 --- a/gpx/src/gpx.ts +++ b/gpx/src/gpx.ts @@ -1,4 +1,4 @@ -import { Coordinates, GPXFileAttributes, GPXFileType, Link, Metadata, TrackExtensions, TrackPointExtensions, TrackPointType, TrackSegmentType, TrackType, WaypointType } from "./types"; +import { Coordinates, GPXFileAttributes, GPXFileType, LineStyleExtension, Link, Metadata, TrackExtensions, TrackPointExtensions, TrackPointType, TrackSegmentType, TrackType, WaypointType } from "./types"; import { Draft, immerable, isDraft, original, produce, freeze } from "immer"; function cloneJSON(obj: T): T { @@ -154,6 +154,27 @@ export class GPXFile extends GPXTreeNode{ }); } + getStyle(): MergedLineStyles { + return this.trk.map((track) => track.getStyle()).reduce((acc, style) => { + if (style) { + if (style.color && !acc.color.includes(style.color)) { + acc.color.push(style.color); + } + if (style.opacity && !acc.opacity.includes(style.opacity)) { + acc.opacity.push(style.opacity); + } + if (style.weight && !acc.weight.includes(style.weight)) { + acc.weight.push(style.weight); + } + } + return acc; + }, { + color: [], + opacity: [], + weight: [] + }); + } + clone(): GPXFile { return new GPXFile({ attributes: cloneJSON(this.attributes), @@ -185,6 +206,9 @@ export class GPXFile extends GPXTreeNode{ let removed = []; let result = produce(this, (draft) => { let og = getOriginal(draft); // Read as much as possible from the original object because it is faster + if (og._data.style) { + tracks = tracks.map((track) => track.setStyle(og._data.style, false)); + } let trk = og.trk.slice(); removed = trk.splice(start, end - start + 1, ...tracks); draft.trk = freeze(trk); // Pre-freeze the array, faster as well @@ -325,6 +349,26 @@ export class GPXFile extends GPXTreeNode{ draft.trk = freeze(trk); // Pre-freeze the array, faster as well }); } + + setStyle(style: LineStyleExtension) { + return produce(this, (draft) => { + let og = getOriginal(draft); // Read as much as possible from the original object because it is faster + let trk = og.trk.map((track) => track.setStyle(style)); + draft.trk = freeze(trk); // Pre-freeze the array, faster as well + if (!draft._data.style) { + draft._data.style = {}; + } + if (style.color) { + draft._data.style.color = style.color; + } + if (style.opacity) { + draft._data.style.opacity = style.opacity; + } + if (style.weight) { + draft._data.style.weight = style.weight; + } + }); + } }; // A class that represents a Track in a GPX file @@ -377,6 +421,10 @@ export class Track extends GPXTreeNode { }); } + getStyle(): LineStyleExtension | undefined { + return this.extensions && this.extensions['gpx_style:line']; + } + toGeoJSON(): GeoJSON.Feature[] { return this.children.map((child) => { let geoJSON = child.toGeoJSON(); @@ -507,6 +555,26 @@ export class Track extends GPXTreeNode { draft.trkseg = freeze(trkseg); // Pre-freeze the array, faster as well }); } + + setStyle(style: LineStyleExtension, force: boolean = true) { + return produce(this, (draft) => { + if (!draft.extensions) { + draft.extensions = {}; + } + if (!draft.extensions['gpx_style:line']) { + draft.extensions['gpx_style:line'] = {}; + } + if (style.color !== undefined && (force || draft.extensions['gpx_style:line'].color === undefined)) { + draft.extensions['gpx_style:line'].color = style.color; + } + if (style.opacity !== undefined && (force || draft.extensions['gpx_style:line'].opacity === undefined)) { + draft.extensions['gpx_style:line'].opacity = style.opacity; + } + if (style.weight !== undefined && (force || draft.extensions['gpx_style:line'].weight === undefined)) { + draft.extensions['gpx_style:line'].weight = style.weight; + } + }); + } }; // A class that represents a TrackSegment in a GPX file @@ -1146,4 +1214,10 @@ function getOriginal(obj: any): any { obj = original(obj); } return obj; -} \ No newline at end of file +} + +export type MergedLineStyles = { + color: string[] + opacity: number[], + weight: number[], +}; \ No newline at end of file diff --git a/gpx/src/index.ts b/gpx/src/index.ts index a5e98cdb..0a263eac 100644 --- a/gpx/src/index.ts +++ b/gpx/src/index.ts @@ -1,5 +1,5 @@ export * from './gpx'; -export { Coordinates } from './types'; +export { Coordinates, LineStyleExtension } from './types'; export { parseGPX, buildGPX } from './io'; diff --git a/website/src/lib/components/file-list/FileListNodeLabel.svelte b/website/src/lib/components/file-list/FileListNodeLabel.svelte index b50f770e..9982f163 100644 --- a/website/src/lib/components/file-list/FileListNodeLabel.svelte +++ b/website/src/lib/components/file-list/FileListNodeLabel.svelte @@ -3,11 +3,12 @@ import { Input } from '$lib/components/ui/input'; import { Textarea } from '$lib/components/ui/textarea'; import { Label } from '$lib/components/ui/label/index.js'; + import { Slider } from '$lib/components/ui/slider'; import * as ContextMenu from '$lib/components/ui/context-menu'; import * as Popover from '$lib/components/ui/popover'; import Shortcut from '$lib/components/Shortcut.svelte'; import { dbUtils, getFile, settings } from '$lib/db'; - import { Copy, Info, MapPin, Plus, Save, Trash2, Waypoints } from 'lucide-svelte'; + import { Copy, Info, MapPin, PaintBucket, Plus, Save, Trash2, Waypoints } from 'lucide-svelte'; import { ListFileItem, ListLevel, @@ -16,10 +17,9 @@ type ListItem } from './FileList'; import { selectItem, selection } from './Selection'; - import { _ } from 'svelte-i18n'; - import { getContext, onMount } from 'svelte'; + import { getContext } from 'svelte'; import { get } from 'svelte/store'; - import { gpxLayers } from '$lib/stores'; + import { gpxLayers, map } from '$lib/stores'; import { GPXTreeElement, Track, @@ -28,6 +28,7 @@ Waypoint, GPXFile } from 'gpx'; + import { _ } from 'svelte-i18n'; export let node: | GPXTreeElement @@ -35,14 +36,16 @@ | Readonly; export let item: ListItem; export let label: string | undefined; + let nodeColors: string[] = []; let orientation = getContext<'vertical' | 'horizontal'>('orientation'); - const { verticalFileView } = settings; + const { verticalFileView, defaultOpacity, defaultWeight } = settings; $: singleSelection = $selection.size === 1; let openEditMetadata: boolean = false; + let openEditStyle: boolean = false; let name: string = node instanceof GPXFile ? node.metadata.name ?? '' @@ -55,6 +58,111 @@ : node instanceof Track ? node.desc ?? '' : ''; + let colors: string[] = []; + let color: string | undefined = undefined; + let opacity: number[] = []; + let weight: number[] = []; + let colorChanged = false; + let opacityChanged = false; + let weightChanged = false; + + $: if (node && $map) { + nodeColors = []; + + if (node instanceof GPXFile) { + let style = node.getStyle(); + + let layer = gpxLayers.get(item.getFileId()); + if (layer) { + style.color.push(layer.layerColor); + } + + style.color.forEach((c) => { + if (!nodeColors.includes(c)) { + nodeColors.push(c); + } + }); + } else if (node instanceof Track) { + let style = node.getStyle(); + if (style) { + if (style.color && !nodeColors.includes(style.color)) { + nodeColors.push(style.color); + } + } + if (nodeColors.length === 0) { + let layer = gpxLayers.get(item.getFileId()); + if (layer) { + nodeColors.push(layer.layerColor); + } + } + } + } + + function setStyleInputs() { + colors = []; + opacity = []; + weight = []; + + $selection.forEach((item) => { + if (item instanceof ListFileItem) { + let file = getFile(item.getFileId()); + let layer = gpxLayers.get(item.getFileId()); + if (file && layer) { + let style = file.getStyle(); + style.color.push(layer.layerColor); + + style.color.forEach((c) => { + if (!colors.includes(c)) { + colors.push(c); + } + }); + style.opacity.forEach((o) => { + if (!opacity.includes(o)) { + opacity.push(o); + } + }); + style.weight.forEach((w) => { + if (!weight.includes(w)) { + weight.push(w); + } + }); + } + } else if (item instanceof ListTrackItem) { + let file = getFile(item.getFileId()); + let layer = gpxLayers.get(item.getFileId()); + if (file && layer) { + let track = file.trk[item.getTrackIndex()]; + let style = track.getStyle(); + if (style) { + if (style.color && !colors.includes(style.color)) { + colors.push(style.color); + } + if (style.opacity && !opacity.includes(style.opacity)) { + opacity.push(style.opacity); + } + if (style.weight && !weight.includes(style.weight)) { + weight.push(style.weight); + } + } + if (!colors.includes(layer.layerColor)) { + colors.push(layer.layerColor); + } + } + } + }); + + color = colors[0]; + opacity = [opacity[0] ?? $defaultOpacity]; + weight = [weight[0] ?? $defaultWeight]; + + colorChanged = false; + opacityChanged = false; + weightChanged = false; + } + + $: if ($selection && openEditStyle) { + setStyleInputs(); + } @@ -103,6 +211,76 @@ + + + + + + + + + {/if} - {#if item.level === ListLevel.SEGMENT} + {#if item.level === ListLevel.FILE} +
+ {:else if item.level === ListLevel.TRACK} +
+ {:else if item.level === ListLevel.SEGMENT} {:else if item.level === ListLevel.WAYPOINT} @@ -147,6 +337,17 @@ + {#if item instanceof ListFileItem || item instanceof ListTrackItem} + (openEditMetadata = true)}> + + {$_('menu.metadata.button')} + + (openEditStyle = true)}> + + {$_('menu.style.button')} + + + {/if} {#if $verticalFileView} {#if item instanceof ListFileItem} {$_('menu.new_track')} + {:else if item instanceof ListTrackItem} {$_('menu.new_segment')} + {/if} {/if} - {#if item instanceof ListFileItem || item instanceof ListTrackItem} - (openEditMetadata = true)}> - - {$_('menu.metadata.button')} - - - {/if} {#if item.level !== ListLevel.WAYPOINTS} diff --git a/website/src/lib/components/gpx-layer/GPXLayer.ts b/website/src/lib/components/gpx-layer/GPXLayer.ts index 9c4f3afa..ab29cbe3 100644 --- a/website/src/lib/components/gpx-layer/GPXLayer.ts +++ b/website/src/lib/components/gpx-layer/GPXLayer.ts @@ -11,9 +11,6 @@ import { resetCursor, setCursor, setGrabbingCursor, setPointerCursor } from "$li import { font } from "$lib/assets/layers"; import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte"; -let defaultWeight = 5; -let defaultOpacity = 0.6; - const colors = [ '#ff0000', '#0000ff', @@ -41,10 +38,12 @@ function getColor() { } function decrementColor(color: string) { - colorCount[color]--; + if (colorCount.hasOwnProperty(color)) { + colorCount[color]--; + } } -const { directionMarkers, verticalFileView, currentBasemap } = settings; +const { directionMarkers, verticalFileView, currentBasemap, defaultOpacity, defaultWeight } = settings; export class GPXLayer { map: mapboxgl.Map; @@ -99,6 +98,11 @@ export class GPXLayer { return; } + if (file._data.style && file._data.style.color && this.layerColor !== file._data.style.color) { + decrementColor(this.layerColor); + this.layerColor = file._data.style.color; + } + try { let source = this.map.getSource(this.fileId); if (source) { @@ -358,7 +362,6 @@ export class GPXLayer { } } - getGeoJSON(): GeoJSON.FeatureCollection { let file = get(this.file)?.file; if (!file) { @@ -379,14 +382,14 @@ export class GPXLayer { feature.properties.color = this.layerColor; } if (!feature.properties.weight) { - feature.properties.weight = defaultWeight; + feature.properties.weight = get(defaultWeight); } if (!feature.properties.opacity) { - feature.properties.opacity = defaultOpacity; + feature.properties.opacity = get(defaultOpacity); } if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)) || get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), true)) { feature.properties.weight = feature.properties.weight + 2; - feature.properties.opacity = (feature.properties.opacity + 2) / 3; + feature.properties.opacity = Math.min(1, feature.properties.opacity + 0.1); } feature.properties.trackIndex = trackIndex; feature.properties.segmentIndex = segmentIndex; diff --git a/website/src/lib/components/layer-control/LayerControlSettings.svelte b/website/src/lib/components/layer-control/LayerControlSettings.svelte index 868bd185..cae4be14 100644 --- a/website/src/lib/components/layer-control/LayerControlSettings.svelte +++ b/website/src/lib/components/layer-control/LayerControlSettings.svelte @@ -120,7 +120,7 @@ {$_('layers.heatmap')}