mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-07 06:14:07 +00:00
progress
This commit is contained in:
+46
-15
@@ -1,5 +1,6 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
||||
import { FileColorAllocator, normalizeColor } from '$lib/file-colors';
|
||||
import type { Feature, FeatureCollection, LineString, Point } from 'geojson';
|
||||
import type { FileStatistics, FileStructure, Selection } from 'gpx-rs';
|
||||
|
||||
@@ -23,6 +24,11 @@ export type SegmentProperties = {
|
||||
segmentIndex: number;
|
||||
/** Changes when the trackpoints of the segment change. */
|
||||
rev: string;
|
||||
/** Color of the track, or else the base color of the file. */
|
||||
color: string;
|
||||
/** Only when the track defines it. */
|
||||
opacity?: number;
|
||||
width?: number;
|
||||
};
|
||||
|
||||
export type WaypointProperties = {
|
||||
@@ -30,11 +36,14 @@ export type WaypointProperties = {
|
||||
waypointId: string;
|
||||
index: number;
|
||||
name?: string;
|
||||
sym?: string;
|
||||
};
|
||||
|
||||
/** Everything the UI knows about one file. */
|
||||
export type FileState = {
|
||||
structure: FileStructure;
|
||||
/** Color of the file: the one defined by its tracks if any, otherwise one from the palette. */
|
||||
color: string;
|
||||
/** Global statistics of the file. */
|
||||
statistics: FileStatistics;
|
||||
/** One LineString feature per track segment, in file order. */
|
||||
@@ -79,6 +88,7 @@ class Engine {
|
||||
readonly ready: Promise<void>;
|
||||
|
||||
private _order = writable<string[]>([]);
|
||||
private _colors = new FileColorAllocator();
|
||||
private _files = writable<Map<string, Writable<FileState>>>(new Map());
|
||||
private _selection = writable<Selection>({ type: 'empty' });
|
||||
|
||||
@@ -188,7 +198,10 @@ class Engine {
|
||||
if (added.length > 0 || update.removed.length > 0) {
|
||||
this._files.update((files) => {
|
||||
const next = new Map(files);
|
||||
update.removed.forEach((id) => next.delete(id));
|
||||
update.removed.forEach((id) => {
|
||||
next.delete(id);
|
||||
this._colors.release(id);
|
||||
});
|
||||
added.forEach(([id, state]) => next.set(id, writable(state)));
|
||||
return next;
|
||||
});
|
||||
@@ -212,18 +225,34 @@ class Engine {
|
||||
return null;
|
||||
}
|
||||
|
||||
const color = this._colors.resolve(
|
||||
id,
|
||||
structure.tracks.map((track) => track.color)
|
||||
);
|
||||
|
||||
const previousSegments = new Map(
|
||||
previous?.segments.features.map((f) => [f.properties.segmentId, f]) ?? []
|
||||
);
|
||||
const segments: Feature<LineString, SegmentProperties>[] = [];
|
||||
structure.tracks.forEach((track, trackIndex) => {
|
||||
track.segments.forEach((segment, segmentIndex) => {
|
||||
const properties: SegmentProperties = {
|
||||
fileId: id,
|
||||
trackId: track.id,
|
||||
segmentId: segment.id,
|
||||
trackIndex,
|
||||
segmentIndex,
|
||||
rev: segment.rev,
|
||||
color: track.color !== undefined ? normalizeColor(track.color) : color,
|
||||
opacity: track.opacity,
|
||||
width: track.width,
|
||||
};
|
||||
const old = previousSegments.get(segment.id);
|
||||
const unchanged =
|
||||
old?.properties.rev === segment.rev &&
|
||||
old.properties.trackId === track.id &&
|
||||
old.properties.trackIndex === trackIndex &&
|
||||
old.properties.segmentIndex === segmentIndex;
|
||||
old !== undefined &&
|
||||
(Object.keys(properties) as (keyof SegmentProperties)[]).every(
|
||||
(key) => old.properties[key] === properties[key]
|
||||
);
|
||||
segments.push(
|
||||
unchanged
|
||||
? old
|
||||
@@ -233,14 +262,7 @@ class Engine {
|
||||
type: 'LineString',
|
||||
coordinates: toPositions(wasm.segment_coordinates(segment.id)),
|
||||
},
|
||||
properties: {
|
||||
fileId: id,
|
||||
trackId: track.id,
|
||||
segmentId: segment.id,
|
||||
trackIndex,
|
||||
segmentIndex,
|
||||
rev: segment.rev,
|
||||
},
|
||||
properties,
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -253,7 +275,9 @@ class Engine {
|
||||
previousStructure.waypoints.length === structure.waypoints.length &&
|
||||
previousStructure.waypoints.every(
|
||||
(w, i) =>
|
||||
w.id === structure.waypoints[i].id && w.name === structure.waypoints[i].name
|
||||
w.id === structure.waypoints[i].id &&
|
||||
w.name === structure.waypoints[i].name &&
|
||||
w.sym === structure.waypoints[i].sym
|
||||
);
|
||||
let waypoints = previous?.waypoints;
|
||||
if (!waypoints || !waypointsUnchanged) {
|
||||
@@ -263,13 +287,20 @@ class Engine {
|
||||
features: structure.waypoints.map((waypoint, index) => ({
|
||||
type: 'Feature',
|
||||
geometry: { type: 'Point', coordinates: coordinates[index] },
|
||||
properties: { fileId: id, waypointId: waypoint.id, index, name: waypoint.name },
|
||||
properties: {
|
||||
fileId: id,
|
||||
waypointId: waypoint.id,
|
||||
index,
|
||||
name: waypoint.name,
|
||||
sym: waypoint.sym,
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
structure,
|
||||
color,
|
||||
statistics: wasm.file_statistics(id) ?? EMPTY_STATISTICS,
|
||||
segments: { type: 'FeatureCollection', features: segments },
|
||||
waypoints,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
const PALETTE = [
|
||||
'#ff0000',
|
||||
'#0000ff',
|
||||
'#46e646',
|
||||
'#00ccff',
|
||||
'#ff9900',
|
||||
'#ff00ff',
|
||||
'#ffff32',
|
||||
'#288228',
|
||||
'#9933ff',
|
||||
'#50f0be',
|
||||
'#8c645a',
|
||||
];
|
||||
|
||||
/** GPX style colors are hex digits, with or without the leading `#`. */
|
||||
export function normalizeColor(color: string): string {
|
||||
return /^[0-9a-f]{6}$/i.test(color) ? `#${color}` : color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks the base color of each file: the color defined by the file itself if any, otherwise a
|
||||
* palette color, the least used one when the file first shows up. A file keeps its palette color
|
||||
* for its whole lifetime, so that it does not change when style information comes and goes
|
||||
* (edits, undo, redo).
|
||||
*/
|
||||
export class FileColorAllocator {
|
||||
/** Palette color reserved for each file. */
|
||||
private _reserved = new Map<string, string>();
|
||||
/** Color currently displayed for each file. */
|
||||
private _base = new Map<string, string>();
|
||||
|
||||
/** Updates (and returns) the base color of a file, given the colors defined by its tracks. */
|
||||
resolve(fileId: string, trackColors: (string | undefined)[]): string {
|
||||
const explicit = trackColors.find((color) => color !== undefined);
|
||||
if (explicit === undefined && !this._reserved.has(fileId)) {
|
||||
this._reserved.set(fileId, this.leastUsed());
|
||||
}
|
||||
const color =
|
||||
explicit !== undefined ? normalizeColor(explicit) : this._reserved.get(fileId)!;
|
||||
this._base.set(fileId, color);
|
||||
return color;
|
||||
}
|
||||
|
||||
release(fileId: string) {
|
||||
this._reserved.delete(fileId);
|
||||
this._base.delete(fileId);
|
||||
}
|
||||
|
||||
private leastUsed(): string {
|
||||
const usage = new Map(PALETTE.map((color) => [color, 0]));
|
||||
this._base.forEach((color) => {
|
||||
const count = usage.get(color);
|
||||
if (count !== undefined) {
|
||||
usage.set(color, count + 1);
|
||||
}
|
||||
});
|
||||
return PALETTE.reduce((a, b) => (usage.get(a)! <= usage.get(b)! ? a : b));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user