This commit is contained in:
vcoppe
2024-04-25 13:48:31 +02:00
parent b5990e2d36
commit 20af7c4e45
12 changed files with 299 additions and 311 deletions

View File

@@ -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';

View File

@@ -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()})`;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}

View 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>

View 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 = '';
}

View File

@@ -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(() => {

View File

@@ -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
});
}
}
}
}