mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-09-02 08:42:31 +00:00
progress
This commit is contained in:
7
gpx/package-lock.json
generated
7
gpx/package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/geojson": "^7946.0.14",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.12.7",
|
||||
"jest": "^29.7.0",
|
||||
@@ -1038,6 +1039,12 @@
|
||||
"@babel/types": "^7.20.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz",
|
||||
"integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/graceful-fs": {
|
||||
"version": "4.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
||||
|
@@ -18,10 +18,11 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/geojson": "^7946.0.14",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.12.7",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.2",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
108
gpx/src/gpx.ts
108
gpx/src/gpx.ts
@@ -9,23 +9,21 @@ function cloneJSON<T>(obj: T): T {
|
||||
|
||||
// An abstract class that groups functions that need to be computed recursively in the GPX file hierarchy
|
||||
abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
|
||||
statistics: GPXStatistics;
|
||||
_data: { [key: string]: any } = {};
|
||||
|
||||
abstract isLeaf(): boolean;
|
||||
abstract getChildren(): T[];
|
||||
|
||||
abstract computeStatistics(): GPXStatistics;
|
||||
abstract refreshStatistics(): void;
|
||||
|
||||
abstract append(points: TrackPoint[]): void;
|
||||
abstract reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date): void;
|
||||
|
||||
abstract getStartTimestamp(): Date;
|
||||
abstract getEndTimestamp(): Date;
|
||||
abstract getStatistics(): GPXStatistics;
|
||||
abstract getTrackPoints(): TrackPoint[];
|
||||
abstract getTrackPointsAndStatistics(): { points: TrackPoint[], statistics: TrackPointStatistics };
|
||||
abstract getTrackPointsAndStatistics(): { points: TrackPoint[], point_statistics: TrackPointStatistics, statistics: GPXStatistics };
|
||||
|
||||
abstract toGeoJSON(): any;
|
||||
abstract toGeoJSON(): GeoJSON.Feature | GeoJSON.Feature[] | GeoJSON.FeatureCollection | GeoJSON.FeatureCollection[];
|
||||
}
|
||||
|
||||
// An abstract class that can be extended to facilitate functions working similarly with Tracks and TrackSegments
|
||||
@@ -34,22 +32,6 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement
|
||||
return false;
|
||||
}
|
||||
|
||||
computeStatistics(): GPXStatistics {
|
||||
for (let child of this.getChildren()) {
|
||||
child.computeStatistics();
|
||||
}
|
||||
this.refreshStatistics();
|
||||
return this.statistics;
|
||||
}
|
||||
|
||||
refreshStatistics(): void {
|
||||
this.statistics = new GPXStatistics();
|
||||
for (let child of this.getChildren()) {
|
||||
child.refreshStatistics();
|
||||
this.statistics.mergeWith(child.statistics);
|
||||
}
|
||||
}
|
||||
|
||||
append(points: TrackPoint[]): void {
|
||||
let children = this.getChildren();
|
||||
|
||||
@@ -58,8 +40,6 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement
|
||||
}
|
||||
|
||||
children[children.length - 1].append(points);
|
||||
|
||||
this.refreshStatistics();
|
||||
}
|
||||
|
||||
reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date): void {
|
||||
@@ -80,8 +60,6 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement
|
||||
originalNextTimestamp = originalStartTimestamp;
|
||||
newPreviousTimestamp = children[i].getEndTimestamp();
|
||||
}
|
||||
|
||||
this.refreshStatistics();
|
||||
}
|
||||
|
||||
getStartTimestamp(): Date {
|
||||
@@ -96,9 +74,17 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement
|
||||
return this.getChildren().flatMap((child) => child.getTrackPoints());
|
||||
}
|
||||
|
||||
getTrackPointsAndStatistics(): { points: TrackPoint[]; statistics: TrackPointStatistics; } {
|
||||
getStatistics(): GPXStatistics {
|
||||
let statistics = new GPXStatistics();
|
||||
for (let child of this.getChildren()) {
|
||||
statistics.mergeWith(child.getStatistics());
|
||||
}
|
||||
return statistics;
|
||||
}
|
||||
|
||||
getTrackPointsAndStatistics(): { points: TrackPoint[], point_statistics: TrackPointStatistics, statistics: GPXStatistics } {
|
||||
let points: TrackPoint[] = [];
|
||||
let statistics: TrackPointStatistics = {
|
||||
let point_statistics: TrackPointStatistics = {
|
||||
distance: [],
|
||||
time: [],
|
||||
speed: [],
|
||||
@@ -109,25 +95,25 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement
|
||||
},
|
||||
slope: [],
|
||||
};
|
||||
let statistics = new GPXStatistics();
|
||||
|
||||
let current = new GPXStatistics();
|
||||
for (let child of this.getChildren()) {
|
||||
let childData = child.getTrackPointsAndStatistics();
|
||||
points = points.concat(childData.points);
|
||||
|
||||
statistics.distance = statistics.distance.concat(childData.statistics.distance.map((distance) => distance + current.distance.total));
|
||||
statistics.time = statistics.time.concat(childData.statistics.time.map((time) => time + current.time.total));
|
||||
statistics.elevation.gain = statistics.elevation.gain.concat(childData.statistics.elevation.gain.map((gain) => gain + current.elevation.gain));
|
||||
statistics.elevation.loss = statistics.elevation.loss.concat(childData.statistics.elevation.loss.map((loss) => loss + current.elevation.loss));
|
||||
point_statistics.distance = point_statistics.distance.concat(childData.point_statistics.distance.map((distance) => distance + statistics.distance.total));
|
||||
point_statistics.time = point_statistics.time.concat(childData.point_statistics.time.map((time) => time + statistics.time.total));
|
||||
point_statistics.elevation.gain = point_statistics.elevation.gain.concat(childData.point_statistics.elevation.gain.map((gain) => gain + statistics.elevation.gain));
|
||||
point_statistics.elevation.loss = point_statistics.elevation.loss.concat(childData.point_statistics.elevation.loss.map((loss) => loss + statistics.elevation.loss));
|
||||
|
||||
statistics.speed = statistics.speed.concat(childData.statistics.speed);
|
||||
statistics.elevation.smoothed = statistics.elevation.smoothed.concat(childData.statistics.elevation.smoothed);
|
||||
statistics.slope = statistics.slope.concat(childData.statistics.slope);
|
||||
point_statistics.speed = point_statistics.speed.concat(childData.point_statistics.speed);
|
||||
point_statistics.elevation.smoothed = point_statistics.elevation.smoothed.concat(childData.point_statistics.elevation.smoothed);
|
||||
point_statistics.slope = point_statistics.slope.concat(childData.point_statistics.slope);
|
||||
|
||||
current.mergeWith(child.statistics);
|
||||
statistics.mergeWith(childData.statistics);
|
||||
}
|
||||
|
||||
return { points, statistics };
|
||||
return { points, point_statistics, statistics };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,18 +135,13 @@ export class GPXFiles extends GPXTreeNode<GPXFile> {
|
||||
constructor(files: GPXFile[]) {
|
||||
super();
|
||||
this.files = files;
|
||||
|
||||
this.statistics = new GPXStatistics();
|
||||
for (let file of files) {
|
||||
this.statistics.mergeWith(file.statistics);
|
||||
}
|
||||
}
|
||||
|
||||
getChildren(): GPXFile[] {
|
||||
return this.files;
|
||||
}
|
||||
|
||||
toGeoJSON(): any {
|
||||
toGeoJSON(): GeoJSON.FeatureCollection[] {
|
||||
return this.getChildren().map((child) => child.toGeoJSON());
|
||||
}
|
||||
}
|
||||
@@ -178,8 +159,6 @@ export class GPXFile extends GPXTreeNode<Track>{
|
||||
this.metadata = cloneJSON(gpx.metadata);
|
||||
this.wpt = gpx.wpt ? gpx.wpt.map((waypoint) => new Waypoint(waypoint)) : [];
|
||||
this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : [];
|
||||
|
||||
this.computeStatistics();
|
||||
}
|
||||
|
||||
getChildren(): Track[] {
|
||||
@@ -190,7 +169,7 @@ export class GPXFile extends GPXTreeNode<Track>{
|
||||
return new GPXFile(structuredClone(this));
|
||||
}
|
||||
|
||||
toGeoJSON(): any {
|
||||
toGeoJSON(): GeoJSON.FeatureCollection {
|
||||
return {
|
||||
type: "FeatureCollection",
|
||||
features: this.getChildren().flatMap((child) => child.toGeoJSON())
|
||||
@@ -234,7 +213,7 @@ export class Track extends GPXTreeNode<TrackSegment> {
|
||||
return this.trkseg;
|
||||
}
|
||||
|
||||
toGeoJSON(): any {
|
||||
toGeoJSON(): GeoJSON.Feature[] {
|
||||
return this.getChildren().map((child) => {
|
||||
let geoJSON = child.toGeoJSON();
|
||||
if (this.extensions && this.extensions['gpx_style:line']) {
|
||||
@@ -274,13 +253,15 @@ export class Track extends GPXTreeNode<TrackSegment> {
|
||||
export class TrackSegment extends GPXTreeLeaf {
|
||||
trkpt: TrackPoint[];
|
||||
trkptStatistics: TrackPointStatistics;
|
||||
statistics: GPXStatistics;
|
||||
|
||||
constructor(segment: TrackSegmentType | TrackSegment) {
|
||||
super();
|
||||
this.trkpt = segment.trkpt.map((point) => new TrackPoint(point));
|
||||
this._computeStatistics();
|
||||
}
|
||||
|
||||
computeStatistics(): GPXStatistics {
|
||||
_computeStatistics(): void {
|
||||
let statistics = new GPXStatistics();
|
||||
let trkptStatistics: TrackPointStatistics = {
|
||||
distance: [],
|
||||
@@ -294,8 +275,8 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
slope: [],
|
||||
};
|
||||
|
||||
trkptStatistics.elevation.smoothed = this.computeSmoothedElevation();
|
||||
trkptStatistics.slope = this.computeSlope();
|
||||
trkptStatistics.elevation.smoothed = this._computeSmoothedElevation();
|
||||
trkptStatistics.slope = this._computeSlope();
|
||||
|
||||
const points = this.trkpt;
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
@@ -357,14 +338,9 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
|
||||
this.statistics = statistics;
|
||||
this.trkptStatistics = trkptStatistics;
|
||||
|
||||
return statistics;
|
||||
}
|
||||
|
||||
// Do nothing, recompute statistics after modifying the segment only
|
||||
refreshStatistics(): void { }
|
||||
|
||||
computeSmoothedElevation(): number[] {
|
||||
_computeSmoothedElevation(): number[] {
|
||||
const points = this.trkpt;
|
||||
|
||||
let smoothed = distanceWindowSmoothing(points, 100, (index) => points[index].ele, (accumulated, start, end) => accumulated / (end - start + 1));
|
||||
@@ -377,7 +353,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
return smoothed;
|
||||
}
|
||||
|
||||
computeSlope(): number[] {
|
||||
_computeSlope(): number[] {
|
||||
const points = this.trkpt;
|
||||
|
||||
return distanceWindowSmoothingWithDistanceAccumulator(points, 50, (accumulated, start, end) => 100 * (points[end].ele - points[start].ele) / accumulated);
|
||||
@@ -385,7 +361,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
|
||||
append(points: TrackPoint[]): void {
|
||||
this.trkpt = this.trkpt.concat(points);
|
||||
this.computeStatistics();
|
||||
this._computeStatistics();
|
||||
}
|
||||
|
||||
reverse(originalNextTimestamp: Date | undefined, newPreviousTimestamp: Date | undefined): void {
|
||||
@@ -405,7 +381,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
} else {
|
||||
this.trkpt.reverse();
|
||||
}
|
||||
this.computeStatistics();
|
||||
this._computeStatistics();
|
||||
}
|
||||
|
||||
getStartTimestamp(): Date {
|
||||
@@ -420,14 +396,19 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
return this.trkpt;
|
||||
}
|
||||
|
||||
getTrackPointsAndStatistics(): { points: TrackPoint[], statistics: TrackPointStatistics } {
|
||||
getStatistics(): GPXStatistics {
|
||||
return this.statistics;
|
||||
}
|
||||
|
||||
getTrackPointsAndStatistics(): { points: TrackPoint[], point_statistics: TrackPointStatistics, statistics: GPXStatistics } {
|
||||
return {
|
||||
points: this.trkpt,
|
||||
statistics: this.trkptStatistics
|
||||
point_statistics: this.trkptStatistics,
|
||||
statistics: this.statistics,
|
||||
};
|
||||
}
|
||||
|
||||
toGeoJSON(): any {
|
||||
toGeoJSON(): GeoJSON.Feature {
|
||||
return {
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
@@ -454,6 +435,7 @@ export class TrackPoint {
|
||||
ele?: number;
|
||||
time?: Date;
|
||||
extensions?: TrackPointExtensions;
|
||||
_data: {} = {};
|
||||
|
||||
constructor(point: TrackPointType | TrackPoint) {
|
||||
this.attributes = cloneJSON(point.attributes);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import GPXMapLayers from '$lib/components/GPXMapLayers.svelte';
|
||||
import GPXMapLayers from '$lib/components/gpx-layer/GPXMapLayers.svelte';
|
||||
import ElevationProfile from '$lib/components/ElevationProfile.svelte';
|
||||
import FileList from '$lib/components/FileList.svelte';
|
||||
import GPXData from '$lib/components/GPXData.svelte';
|
||||
|
@@ -231,16 +231,16 @@
|
||||
|
||||
$: if (chart && $settings) {
|
||||
let gpxFiles = new GPXFiles(get(fileOrder).filter((f) => $selectedFiles.has(f)));
|
||||
let data = gpxFiles.getTrackPointsAndStatistics();
|
||||
|
||||
// update data
|
||||
let trackPointsAndStatistics = gpxFiles.getTrackPointsAndStatistics();
|
||||
chart.data.datasets[0] = {
|
||||
label: $_('quantities.elevation'),
|
||||
data: trackPointsAndStatistics.points.map((point, index) => {
|
||||
data: data.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(trackPointsAndStatistics.statistics.distance[index]),
|
||||
x: getConvertedDistance(data.point_statistics.distance[index]),
|
||||
y: point.ele ? getConvertedElevation(point.ele) : 0,
|
||||
slope: trackPointsAndStatistics.statistics.slope[index],
|
||||
slope: data.point_statistics.slope[index],
|
||||
surface: point.getSurface(),
|
||||
coordinates: point.getCoordinates()
|
||||
};
|
||||
@@ -251,10 +251,10 @@
|
||||
};
|
||||
chart.data.datasets[1] = {
|
||||
label: datasets.speed.getLabel(),
|
||||
data: trackPointsAndStatistics.points.map((point, index) => {
|
||||
data: data.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(trackPointsAndStatistics.statistics.distance[index]),
|
||||
y: getConvertedVelocity(trackPointsAndStatistics.statistics.speed[index])
|
||||
x: getConvertedDistance(data.point_statistics.distance[index]),
|
||||
y: getConvertedVelocity(data.point_statistics.speed[index])
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
@@ -263,9 +263,9 @@
|
||||
};
|
||||
chart.data.datasets[2] = {
|
||||
label: datasets.hr.getLabel(),
|
||||
data: trackPointsAndStatistics.points.map((point, index) => {
|
||||
data: data.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(trackPointsAndStatistics.statistics.distance[index]),
|
||||
x: getConvertedDistance(data.point_statistics.distance[index]),
|
||||
y: point.getHeartRate()
|
||||
};
|
||||
}),
|
||||
@@ -275,9 +275,9 @@
|
||||
};
|
||||
chart.data.datasets[3] = {
|
||||
label: datasets.cad.getLabel(),
|
||||
data: trackPointsAndStatistics.points.map((point, index) => {
|
||||
data: data.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(trackPointsAndStatistics.statistics.distance[index]),
|
||||
x: getConvertedDistance(data.point_statistics.distance[index]),
|
||||
y: point.getCadence()
|
||||
};
|
||||
}),
|
||||
@@ -287,9 +287,9 @@
|
||||
};
|
||||
chart.data.datasets[4] = {
|
||||
label: datasets.atemp.getLabel(),
|
||||
data: trackPointsAndStatistics.points.map((point, index) => {
|
||||
data: data.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(trackPointsAndStatistics.statistics.distance[index]),
|
||||
x: getConvertedDistance(data.point_statistics.distance[index]),
|
||||
y: getConvertedTemperature(point.getTemperature())
|
||||
};
|
||||
}),
|
||||
@@ -299,9 +299,9 @@
|
||||
};
|
||||
chart.data.datasets[5] = {
|
||||
label: datasets.power.getLabel(),
|
||||
data: trackPointsAndStatistics.points.map((point, index) => {
|
||||
data: data.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(trackPointsAndStatistics.statistics.distance[index]),
|
||||
x: getConvertedDistance(data.point_statistics.distance[index]),
|
||||
y: point.getPower()
|
||||
};
|
||||
}),
|
||||
@@ -310,7 +310,7 @@
|
||||
hidden: true
|
||||
};
|
||||
chart.options.scales.x['min'] = 0;
|
||||
chart.options.scales.x['max'] = getConvertedDistance(gpxFiles.statistics.distance.total);
|
||||
chart.options.scales.x['max'] = getConvertedDistance(data.statistics.distance.total);
|
||||
|
||||
// update units
|
||||
chart.options.scales.x.title.text = `${$_('quantities.distance')} (${getDistanceUnits()})`;
|
||||
|
@@ -3,9 +3,9 @@
|
||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||
import WithUnits from '$lib/components/WithUnits.svelte';
|
||||
|
||||
import { GPXStatistics } from 'gpx';
|
||||
import { GPXFiles, GPXStatistics } from 'gpx';
|
||||
|
||||
import { getFileStore, selectedFiles, settings } from '$lib/stores';
|
||||
import { selectedFiles, settings } from '$lib/stores';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from 'lucide-svelte';
|
||||
@@ -15,10 +15,7 @@
|
||||
let gpxData: GPXStatistics = new GPXStatistics();
|
||||
|
||||
function updateGPXData() {
|
||||
gpxData = new GPXStatistics();
|
||||
$selectedFiles.forEach((file) => {
|
||||
gpxData.mergeWith(file.statistics);
|
||||
});
|
||||
gpxData = new GPXFiles(Array.from(get(selectedFiles))).getStatistics();
|
||||
}
|
||||
|
||||
$: if ($selectedFiles) {
|
||||
@@ -47,24 +44,25 @@
|
||||
<Tooltip>
|
||||
<span slot="data" class="flex flex-row items-center">
|
||||
<Zap size="18" class="mr-1" />
|
||||
<WithUnits value={gpxData.speed.moving} type="speed" showUnits={false} /> /
|
||||
<WithUnits value={gpxData.speed.total} type="speed" />
|
||||
<WithUnits value={gpxData.speed.total} type="speed" showUnits={false} />
|
||||
<span class="mx-1">/</span>
|
||||
<WithUnits value={gpxData.speed.moving} type="speed" />
|
||||
</span>
|
||||
<span slot="tooltip"
|
||||
>{$settings.velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_(
|
||||
'quantities.moving'
|
||||
)} / {$_('quantities.total')})</span
|
||||
'quantities.total'
|
||||
)} / {$_('quantities.moving')})</span
|
||||
>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<span slot="data" class="flex flex-row items-center">
|
||||
<Timer size="18" class="mr-1" />
|
||||
<WithUnits value={gpxData.time.moving} type="time" />
|
||||
<span class="mx-1">/</span>
|
||||
<WithUnits value={gpxData.time.total} type="time" />
|
||||
<span class="mx-1">/</span>
|
||||
<WithUnits value={gpxData.time.moving} type="time" />
|
||||
</span>
|
||||
<span slot="tooltip"
|
||||
>{$_('quantities.time')} ({$_('quantities.moving')} / {$_('quantities.total')})</span
|
||||
>{$_('quantities.time')} ({$_('quantities.total')} / {$_('quantities.moving')})</span
|
||||
>
|
||||
</Tooltip>
|
||||
</Card.Content>
|
||||
|
@@ -1,201 +0,0 @@
|
||||
<script context="module" lang="ts">
|
||||
let id = 0;
|
||||
function getLayerId() {
|
||||
return `gpx-${id++}`;
|
||||
}
|
||||
|
||||
let defaultWeight = 6;
|
||||
let defaultOpacity = 1;
|
||||
|
||||
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() {
|
||||
let color = colors.reduce((a, b) => (colorCount[a] <= colorCount[b] ? a : b));
|
||||
colorCount[color]++;
|
||||
return color;
|
||||
}
|
||||
|
||||
function decrementColor(color: string) {
|
||||
colorCount[color]--;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { GPXFile } from 'gpx';
|
||||
import { map, selectedFiles, selectFiles, files } from '$lib/stores';
|
||||
import { get, type Writable } from 'svelte/store';
|
||||
|
||||
export let file: Writable<GPXFile>;
|
||||
|
||||
let layerId = getLayerId();
|
||||
let layerColor = getColor();
|
||||
|
||||
file.update((f) => {
|
||||
Object.defineProperty(f, 'layerId', {
|
||||
value: layerId,
|
||||
writable: false
|
||||
});
|
||||
return f;
|
||||
});
|
||||
|
||||
function selectOnClick(e: any) {
|
||||
if (e.originalEvent.shiftKey) {
|
||||
get(selectFiles).addSelect(get(file));
|
||||
} else {
|
||||
get(selectFiles).select(get(file));
|
||||
}
|
||||
}
|
||||
|
||||
function toPointerCursor() {
|
||||
if ($map) {
|
||||
$map.getCanvas().style.cursor = 'pointer';
|
||||
}
|
||||
}
|
||||
|
||||
function toDefaultCursor() {
|
||||
if ($map) {
|
||||
$map.getCanvas().style.cursor = '';
|
||||
}
|
||||
}
|
||||
|
||||
function extendGeoJSON(data: any) {
|
||||
for (let feature of data.features) {
|
||||
if (!feature.properties.color) {
|
||||
feature.properties.color = layerColor;
|
||||
}
|
||||
if (!feature.properties.weight) {
|
||||
feature.properties.weight = defaultWeight;
|
||||
}
|
||||
if (!feature.properties.opacity) {
|
||||
feature.properties.opacity = defaultOpacity;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function addGPXLayer() {
|
||||
if ($map) {
|
||||
if (!$map.getSource(layerId)) {
|
||||
let data = extendGeoJSON($file.toGeoJSON());
|
||||
|
||||
$map.addSource(layerId, {
|
||||
type: 'geojson',
|
||||
data
|
||||
});
|
||||
}
|
||||
if (!$map.getLayer(layerId)) {
|
||||
$map.addLayer({
|
||||
id: layerId,
|
||||
type: 'line',
|
||||
source: layerId,
|
||||
layout: {
|
||||
'line-join': 'round',
|
||||
'line-cap': 'round'
|
||||
},
|
||||
paint: {
|
||||
'line-color': ['get', 'color'],
|
||||
'line-width': ['get', 'weight'],
|
||||
'line-opacity': ['get', 'opacity']
|
||||
}
|
||||
});
|
||||
|
||||
$map.on('click', layerId, selectOnClick);
|
||||
$map.on('mouseenter', layerId, toPointerCursor);
|
||||
$map.on('mouseleave', layerId, toDefaultCursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: if ($selectedFiles.has(get(file))) {
|
||||
if ($map) {
|
||||
$map.moveLayer(layerId);
|
||||
}
|
||||
}
|
||||
|
||||
$: if ($map) {
|
||||
let source = $map.getSource(layerId);
|
||||
if (source) {
|
||||
source.setData(extendGeoJSON($file.toGeoJSON()));
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
addGPXLayer();
|
||||
if ($map) {
|
||||
if ($files.length == 1) {
|
||||
$map.fitBounds(
|
||||
[get(file).statistics.bounds.southWest, get(file).statistics.bounds.northEast],
|
||||
{
|
||||
padding: 60,
|
||||
linear: true,
|
||||
easing: () => 1
|
||||
}
|
||||
);
|
||||
} else {
|
||||
let mapBounds = $map.getBounds();
|
||||
if (
|
||||
mapBounds.contains(get(file).statistics.bounds.southWest) &&
|
||||
mapBounds.contains(get(file).statistics.bounds.northEast) &&
|
||||
mapBounds.contains([
|
||||
get(file).statistics.bounds.southWest.lon,
|
||||
get(file).statistics.bounds.northEast.lat
|
||||
]) &&
|
||||
mapBounds.contains([
|
||||
get(file).statistics.bounds.northEast.lon,
|
||||
get(file).statistics.bounds.southWest.lat
|
||||
])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$map.fitBounds(
|
||||
$map
|
||||
.getBounds()
|
||||
.extend([
|
||||
get(file).statistics.bounds.southWest.lon,
|
||||
get(file).statistics.bounds.southWest.lat,
|
||||
get(file).statistics.bounds.northEast.lon,
|
||||
get(file).statistics.bounds.northEast.lat
|
||||
]),
|
||||
{
|
||||
padding: 60
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$map.on('style.load', addGPXLayer);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if ($map) {
|
||||
$map.off('click', layerId, selectOnClick);
|
||||
$map.off('mouseenter', layerId, toPointerCursor);
|
||||
$map.off('mouseleave', layerId, toDefaultCursor);
|
||||
$map.off('style.load', addGPXLayer);
|
||||
|
||||
$map.removeLayer(layerId);
|
||||
$map.removeSource(layerId);
|
||||
}
|
||||
decrementColor(layerColor);
|
||||
});
|
||||
</script>
|
@@ -1,9 +0,0 @@
|
||||
<script lang="ts">
|
||||
import GPXMapLayer from './GPXMapLayer.svelte';
|
||||
|
||||
import { files } from '$lib/stores';
|
||||
</script>
|
||||
|
||||
{#each $files as file}
|
||||
<GPXMapLayer {file} />
|
||||
{/each}
|
29
website/src/lib/components/gpx-layer/GPXMapLayers.svelte
Normal file
29
website/src/lib/components/gpx-layer/GPXMapLayers.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { map, files, selectedFiles, getFileStore } from '$lib/stores';
|
||||
import type { GPXFile } from 'gpx';
|
||||
import { GPXMapLayer } from './GPXMapLayers';
|
||||
import { get, type Writable } from 'svelte/store';
|
||||
|
||||
let gpxLayers: Map<Writable<GPXFile>, GPXMapLayer> = new Map();
|
||||
|
||||
$: if ($map) {
|
||||
gpxLayers.forEach((layer, file) => {
|
||||
if (!get(files).includes(file)) {
|
||||
layer.remove();
|
||||
gpxLayers.delete(file);
|
||||
}
|
||||
});
|
||||
$files.forEach((file) => {
|
||||
if (!gpxLayers.has(file)) {
|
||||
gpxLayers.set(file, new GPXMapLayer(get(map), file));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$: $selectedFiles.forEach((file) => {
|
||||
let fileStore = getFileStore(file);
|
||||
if (gpxLayers.has(fileStore)) {
|
||||
gpxLayers.get(fileStore)?.moveToFront();
|
||||
}
|
||||
});
|
||||
</script>
|
158
website/src/lib/components/gpx-layer/GPXMapLayers.ts
Normal file
158
website/src/lib/components/gpx-layer/GPXMapLayers.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import type { GPXFile } from "gpx";
|
||||
import { map, selectFiles } from "$lib/stores";
|
||||
import { get, type Writable } from "svelte/store";
|
||||
import type mapboxgl from "mapbox-gl";
|
||||
|
||||
let id = 0;
|
||||
function getLayerId() {
|
||||
return `gpx-${id++}`;
|
||||
}
|
||||
|
||||
let defaultWeight = 6;
|
||||
let defaultOpacity = 1;
|
||||
|
||||
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() {
|
||||
let color = colors.reduce((a, b) => (colorCount[a] <= colorCount[b] ? a : b));
|
||||
colorCount[color]++;
|
||||
return color;
|
||||
}
|
||||
|
||||
function decrementColor(color: string) {
|
||||
colorCount[color]--;
|
||||
}
|
||||
|
||||
export class GPXMapLayer {
|
||||
map: mapboxgl.Map;
|
||||
file: Writable<GPXFile>;
|
||||
layerId: string;
|
||||
layerColor: string;
|
||||
unsubscribe: () => void;
|
||||
|
||||
constructor(map: mapboxgl.Map, file: Writable<GPXFile>) {
|
||||
this.map = map;
|
||||
this.file = file;
|
||||
this.layerId = getLayerId();
|
||||
this.layerColor = getColor();
|
||||
this.unsubscribe = file.subscribe(this.updateData.bind(this));
|
||||
|
||||
get(this.file)._data = {
|
||||
layerId: this.layerId,
|
||||
layerColor: this.layerColor
|
||||
};
|
||||
|
||||
this.add();
|
||||
this.map.on('style.load', this.add.bind(this));
|
||||
}
|
||||
|
||||
add() {
|
||||
if (!this.map.getSource(this.layerId)) {
|
||||
let data = this.getGeoJSON();
|
||||
|
||||
this.map.addSource(this.layerId, {
|
||||
type: 'geojson',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.map.getLayer(this.layerId)) {
|
||||
this.map.addLayer({
|
||||
id: this.layerId,
|
||||
type: 'line',
|
||||
source: this.layerId,
|
||||
layout: {
|
||||
'line-join': 'round',
|
||||
'line-cap': 'round'
|
||||
},
|
||||
paint: {
|
||||
'line-color': ['get', 'color'],
|
||||
'line-width': ['get', 'weight'],
|
||||
'line-opacity': ['get', 'opacity']
|
||||
}
|
||||
});
|
||||
|
||||
this.map.on('click', this.layerId, this.selectOnClick.bind(this));
|
||||
this.map.on('mouseenter', this.layerId, toPointerCursor);
|
||||
this.map.on('mouseleave', this.layerId, toDefaultCursor);
|
||||
}
|
||||
}
|
||||
|
||||
updateData() {
|
||||
let source = this.map.getSource(this.layerId);
|
||||
if (source) {
|
||||
source.setData(this.getGeoJSON());
|
||||
}
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.map.off('click', this.layerId, this.selectOnClick.bind(this));
|
||||
this.map.off('mouseenter', this.layerId, toPointerCursor);
|
||||
this.map.off('mouseleave', this.layerId, toDefaultCursor);
|
||||
this.map.off('style.load', this.add.bind(this));
|
||||
|
||||
this.map.removeLayer(this.layerId);
|
||||
this.map.removeSource(this.layerId);
|
||||
|
||||
this.unsubscribe();
|
||||
|
||||
decrementColor(this.layerColor);
|
||||
}
|
||||
|
||||
moveToFront() {
|
||||
this.map.moveLayer(this.layerId);
|
||||
}
|
||||
|
||||
selectOnClick(e: any) {
|
||||
if (e.originalEvent.shiftKey) {
|
||||
get(selectFiles).addSelect(get(this.file));
|
||||
} else {
|
||||
get(selectFiles).select(get(this.file));
|
||||
}
|
||||
}
|
||||
|
||||
getGeoJSON(): GeoJSON.FeatureCollection {
|
||||
let data = get(this.file).toGeoJSON();
|
||||
for (let feature of data.features) {
|
||||
if (!feature.properties) {
|
||||
feature.properties = {};
|
||||
}
|
||||
if (!feature.properties.color) {
|
||||
feature.properties.color = this.layerColor;
|
||||
}
|
||||
if (!feature.properties.weight) {
|
||||
feature.properties.weight = defaultWeight;
|
||||
}
|
||||
if (!feature.properties.opacity) {
|
||||
feature.properties.opacity = defaultOpacity;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
function toPointerCursor() {
|
||||
get(map).getCanvas().style.cursor = 'pointer';
|
||||
}
|
||||
|
||||
function toDefaultCursor() {
|
||||
get(map).getCanvas().style.cursor = '';
|
||||
}
|
@@ -106,7 +106,7 @@
|
||||
$map.off('move', toggleMarkersForZoomLevelAndBounds);
|
||||
$map.off('click', extendFile);
|
||||
if (file) {
|
||||
$map.off('mouseover', file.layerId, showInsertableMarker);
|
||||
$map.off('mouseover', file._data.layerId, showInsertableMarker);
|
||||
}
|
||||
if (insertableMarker) {
|
||||
insertableMarker.remove();
|
||||
@@ -115,7 +115,7 @@
|
||||
kdbush = null;
|
||||
}
|
||||
|
||||
$: if ($selectedFiles.size == 1 && $map) {
|
||||
$: if ($selectedFiles.size == 1) {
|
||||
let selectedFile = $selectedFiles.values().next().value;
|
||||
|
||||
if (selectedFile !== file) {
|
||||
@@ -124,6 +124,9 @@
|
||||
} else {
|
||||
// update markers
|
||||
}
|
||||
} else {
|
||||
clean();
|
||||
file = null;
|
||||
}
|
||||
|
||||
$: if ($map && file) {
|
||||
@@ -140,7 +143,7 @@
|
||||
$map.on('zoom', toggleMarkersForZoomLevelAndBounds);
|
||||
$map.on('move', toggleMarkersForZoomLevelAndBounds);
|
||||
$map.on('click', extendFile);
|
||||
$map.on('mouseover', file.layerId, showInsertableMarker);
|
||||
$map.on('mouseover', file._data.layerId, showInsertableMarker);
|
||||
|
||||
let points = file.getTrackPoints();
|
||||
|
||||
@@ -152,8 +155,6 @@
|
||||
kdbush.finish();
|
||||
end = performance.now();
|
||||
console.log('Time to create kdbush: ' + (end - start) + 'ms');
|
||||
} else {
|
||||
clean();
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
|
@@ -68,10 +68,32 @@ export function triggerFileInput() {
|
||||
}
|
||||
|
||||
export async function loadFiles(list: FileList) {
|
||||
let bounds = new mapboxgl.LngLatBounds();
|
||||
let mapBounds = new mapboxgl.LngLatBounds([180, 90, -180, -90]);
|
||||
if (get(files).length > 0) {
|
||||
mapBounds = get(map)?.getBounds() ?? mapBounds;
|
||||
bounds.extend(mapBounds);
|
||||
}
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
let file = await loadFile(list[i]);
|
||||
if (i == 0 && file) {
|
||||
get(selectFiles).select(get(file));
|
||||
if (file) {
|
||||
if (i == 0) {
|
||||
get(selectFiles).select(get(file));
|
||||
}
|
||||
|
||||
let fileBounds = get(file).getStatistics().bounds;
|
||||
bounds.extend(fileBounds.southWest);
|
||||
bounds.extend(fileBounds.northEast);
|
||||
bounds.extend([fileBounds.southWest.lon, fileBounds.northEast.lat]);
|
||||
bounds.extend([fileBounds.northEast.lon, fileBounds.southWest.lat]);
|
||||
|
||||
if (!mapBounds.contains(bounds.getSouthWest()) || !mapBounds.contains(bounds.getNorthEast()) || !mapBounds.contains(bounds.getSouthEast()) || !mapBounds.contains(bounds.getNorthWest())) {
|
||||
get(map)?.fitBounds(bounds, {
|
||||
padding: 80,
|
||||
linear: true,
|
||||
easing: () => 1
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user