mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-01-15 14:18:41 +00:00
Compare commits
5 Commits
graphhoppe
...
e7a1d0488b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7a1d0488b | ||
|
|
22b8e0edb4 | ||
|
|
d062a38e8f | ||
|
|
affa59130f | ||
|
|
3c816567bc |
@@ -1,6 +0,0 @@
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
src/lib/components/ui
|
||||
*.mdx
|
||||
@@ -25,7 +25,7 @@
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"postinstall": "npm run build",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write ."
|
||||
"lint": "prettier --check . --config ../.prettierrc && eslint .",
|
||||
"format": "prettier --write . --config ../.prettierrc"
|
||||
}
|
||||
}
|
||||
|
||||
3
website/.prettierignore
Normal file
3
website/.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
||||
src/lib/components/ui
|
||||
src/lib/docs/**/*.mdx
|
||||
**/*.webmanifest
|
||||
27
website/package-lock.json
generated
27
website/package-lock.json
generated
@@ -14,7 +14,7 @@
|
||||
"@mapbox/sphericalmercator": "^2.0.1",
|
||||
"@mapbox/tilebelt": "^2.0.2",
|
||||
"@types/mapbox__sphericalmercator": "^1.2.3",
|
||||
"chart.js": "^4.4.9",
|
||||
"chart.js": "^4.5.1",
|
||||
"chartjs-plugin-zoom": "^2.2.0",
|
||||
"clsx": "^2.1.1",
|
||||
"dexie": "^4.0.11",
|
||||
@@ -22,7 +22,7 @@
|
||||
"gpx": "file:../gpx",
|
||||
"immer": "^10.1.1",
|
||||
"jszip": "^3.10.1",
|
||||
"mapbox-gl": "^3.16.0",
|
||||
"mapbox-gl": "^3.17.0",
|
||||
"mapillary-js": "^4.1.2",
|
||||
"png.js": "^0.2.1",
|
||||
"sanitize-html": "^2.17.0",
|
||||
@@ -3664,9 +3664,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.4.9",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz",
|
||||
"integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==",
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
@@ -6069,12 +6069,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mapbox-gl": {
|
||||
"version": "3.16.0",
|
||||
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.16.0.tgz",
|
||||
"integrity": "sha512-rluV1Zp/0oHf1Y9BV+nePRNnKyTdljko3E19CzO5rBqtQaNUYS0ePCMPRtxOuWRwSdKp3f9NWJkOCjemM8nmjw==",
|
||||
"version": "3.17.0",
|
||||
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.17.0.tgz",
|
||||
"integrity": "sha512-nCrDKRlr5di6xUksUDslNWwxroJ5yv1hT8pyVFtcpWJOOKsYQxF/wOFTMie8oxMnXeFkrz1Tl1TwA1XN1yX0KA==",
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"workspaces": [
|
||||
"src/style-spec",
|
||||
"test/build/vite",
|
||||
"test/build/webpack",
|
||||
"test/build/typings"
|
||||
],
|
||||
"dependencies": {
|
||||
@@ -6102,7 +6104,6 @@
|
||||
"pbf": "^4.0.1",
|
||||
"potpack": "^2.0.0",
|
||||
"quickselect": "^3.0.0",
|
||||
"serialize-to-js": "^3.1.2",
|
||||
"supercluster": "^8.0.1",
|
||||
"tinyqueue": "^3.0.0"
|
||||
}
|
||||
@@ -7634,14 +7635,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/serialize-to-js": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-to-js/-/serialize-to-js-3.1.2.tgz",
|
||||
"integrity": "sha512-owllqNuDDEimQat7EPG0tH7JjO090xKNzUtYz6X+Sk2BXDnOCilDdNLwjWeFywG9xkJul1ULvtUQa9O4pUaY0w==",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write ."
|
||||
"lint": "prettier --check . --config ../.prettierrc && eslint .",
|
||||
"format": "prettier --write . --config ../.prettierrc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lucide/svelte": "^0.544.0",
|
||||
@@ -66,7 +66,7 @@
|
||||
"@mapbox/sphericalmercator": "^2.0.1",
|
||||
"@mapbox/tilebelt": "^2.0.2",
|
||||
"@types/mapbox__sphericalmercator": "^1.2.3",
|
||||
"chart.js": "^4.4.9",
|
||||
"chart.js": "^4.5.1",
|
||||
"chartjs-plugin-zoom": "^2.2.0",
|
||||
"clsx": "^2.1.1",
|
||||
"dexie": "^4.0.11",
|
||||
@@ -74,7 +74,7 @@
|
||||
"gpx": "file:../gpx",
|
||||
"immer": "^10.1.1",
|
||||
"jszip": "^3.10.1",
|
||||
"mapbox-gl": "^3.16.0",
|
||||
"mapbox-gl": "^3.17.0",
|
||||
"mapillary-js": "^4.1.2",
|
||||
"png.js": "^0.2.1",
|
||||
"sanitize-html": "^2.17.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
|
||||
@@ -538,6 +538,7 @@
|
||||
let targetInput =
|
||||
e &&
|
||||
e.target &&
|
||||
e.target instanceof HTMLElement &&
|
||||
(e.target.tagName === 'INPUT' ||
|
||||
e.target.tagName === 'TEXTAREA' ||
|
||||
e.target.tagName === 'SELECT' ||
|
||||
|
||||
@@ -14,7 +14,12 @@ import {
|
||||
getTemperatureWithUnits,
|
||||
getVelocityWithUnits,
|
||||
} from '$lib/units';
|
||||
import Chart from 'chart.js/auto';
|
||||
import Chart, {
|
||||
type ChartEvent,
|
||||
type ChartOptions,
|
||||
type ScriptableLineSegmentContext,
|
||||
type TooltipItem,
|
||||
} from 'chart.js/auto';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { get, type Readable, type Writable } from 'svelte/store';
|
||||
import { map } from '$lib/components/map/map';
|
||||
@@ -27,6 +32,20 @@ const { distanceUnits, velocityUnits, temperatureUnits } = settings;
|
||||
Chart.defaults.font.family =
|
||||
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; // Tailwind CSS font
|
||||
|
||||
interface ElevationProfilePoint {
|
||||
x: number;
|
||||
y: number;
|
||||
time?: Date;
|
||||
slope: {
|
||||
at: number;
|
||||
segment: number;
|
||||
length: number;
|
||||
};
|
||||
extensions: Record<string, any>;
|
||||
coordinates: [number, number];
|
||||
index: number;
|
||||
}
|
||||
|
||||
export class ElevationProfile {
|
||||
private _chart: Chart | null = null;
|
||||
private _canvas: HTMLCanvasElement;
|
||||
@@ -90,7 +109,7 @@ export class ElevationProfile {
|
||||
}
|
||||
|
||||
initialize() {
|
||||
let options = {
|
||||
let options: ChartOptions<'line'> = {
|
||||
animation: false,
|
||||
parsing: false,
|
||||
maintainAspectRatio: false,
|
||||
@@ -98,8 +117,8 @@ export class ElevationProfile {
|
||||
x: {
|
||||
type: 'linear',
|
||||
ticks: {
|
||||
callback: function (value: number) {
|
||||
return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
|
||||
callback: function (value: number | string) {
|
||||
return `${(value as number).toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
|
||||
},
|
||||
align: 'inner',
|
||||
maxRotation: 0,
|
||||
@@ -108,8 +127,8 @@ export class ElevationProfile {
|
||||
y: {
|
||||
type: 'linear',
|
||||
ticks: {
|
||||
callback: function (value: number) {
|
||||
return getElevationWithUnits(value, false);
|
||||
callback: function (value: number | string) {
|
||||
return getElevationWithUnits(value as number, false);
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -140,8 +159,8 @@ export class ElevationProfile {
|
||||
title: () => {
|
||||
return '';
|
||||
},
|
||||
label: (context: Chart.TooltipContext) => {
|
||||
let point = context.raw;
|
||||
label: (context: TooltipItem<'line'>) => {
|
||||
let point = context.raw as ElevationProfilePoint;
|
||||
if (context.datasetIndex === 0) {
|
||||
const map_ = get(map);
|
||||
if (map_ && this._marker) {
|
||||
@@ -165,10 +184,10 @@ export class ElevationProfile {
|
||||
return `${i18n._('quantities.power')}: ${getPowerWithUnits(point.y)}`;
|
||||
}
|
||||
},
|
||||
afterBody: (contexts: Chart.TooltipContext[]) => {
|
||||
afterBody: (contexts: TooltipItem<'line'>[]) => {
|
||||
let context = contexts.filter((context) => context.datasetIndex === 0);
|
||||
if (context.length === 0) return;
|
||||
let point = context[0].raw;
|
||||
let point = context[0].raw as ElevationProfilePoint;
|
||||
let slope = {
|
||||
at: point.slope.at.toFixed(1),
|
||||
segment: point.slope.segment.toFixed(1),
|
||||
@@ -227,6 +246,7 @@ export class ElevationProfile {
|
||||
onPanStart: () => {
|
||||
this._panning = true;
|
||||
this._slicedGPXStatistics.set(undefined);
|
||||
return true;
|
||||
},
|
||||
onPanComplete: () => {
|
||||
this._panning = false;
|
||||
@@ -238,13 +258,13 @@ export class ElevationProfile {
|
||||
},
|
||||
mode: 'x',
|
||||
onZoomStart: ({ chart, event }: { chart: Chart; event: any }) => {
|
||||
if (!this._chart) {
|
||||
return false;
|
||||
}
|
||||
const maxZoom = this._chart.getInitialScaleBounds()?.x?.max ?? 0;
|
||||
if (
|
||||
event.deltaY < 0 &&
|
||||
Math.abs(
|
||||
this._chart.getInitialScaleBounds().x.max /
|
||||
this._chart.options.plugins.zoom.limits.x.minRange -
|
||||
this._chart.getZoomLevel()
|
||||
) < 0.01
|
||||
Math.abs(maxZoom / this._chart.getZoomLevel()) < 0.01
|
||||
) {
|
||||
// Disable wheel pan if zoomed in to the max, and zooming in
|
||||
return false;
|
||||
@@ -262,7 +282,6 @@ export class ElevationProfile {
|
||||
},
|
||||
},
|
||||
},
|
||||
stacked: false,
|
||||
onResize: () => {
|
||||
this.updateOverlay();
|
||||
},
|
||||
@@ -270,7 +289,7 @@ export class ElevationProfile {
|
||||
|
||||
let datasets: string[] = ['speed', 'hr', 'cad', 'atemp', 'power'];
|
||||
datasets.forEach((id) => {
|
||||
options.scales[`y${id}`] = {
|
||||
options.scales![`y${id}`] = {
|
||||
type: 'linear',
|
||||
position: 'right',
|
||||
grid: {
|
||||
@@ -291,7 +310,7 @@ export class ElevationProfile {
|
||||
{
|
||||
id: 'toggleMarker',
|
||||
events: ['mouseout'],
|
||||
afterEvent: (chart: Chart, args: { event: Chart.ChartEvent }) => {
|
||||
afterEvent: (chart: Chart, args: { event: ChartEvent }) => {
|
||||
if (args.event.type === 'mouseout') {
|
||||
const map_ = get(map);
|
||||
if (map_ && this._marker) {
|
||||
@@ -305,7 +324,7 @@ export class ElevationProfile {
|
||||
|
||||
let startIndex = 0;
|
||||
let endIndex = 0;
|
||||
const getIndex = (evt) => {
|
||||
const getIndex = (evt: PointerEvent) => {
|
||||
if (!this._chart) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -329,16 +348,16 @@ export class ElevationProfile {
|
||||
}
|
||||
}
|
||||
|
||||
let point = points.find((point) => point.element.raw);
|
||||
const point = points.find((point) => (point.element as any).raw);
|
||||
if (point) {
|
||||
return point.element.raw.index;
|
||||
return (point.element as any).raw.index;
|
||||
} else {
|
||||
return points[0].index;
|
||||
}
|
||||
};
|
||||
|
||||
let dragStarted = false;
|
||||
const onMouseDown = (evt) => {
|
||||
const onMouseDown = (evt: PointerEvent) => {
|
||||
if (evt.shiftKey) {
|
||||
// Panning interaction
|
||||
return;
|
||||
@@ -347,7 +366,7 @@ export class ElevationProfile {
|
||||
this._canvas.style.cursor = 'col-resize';
|
||||
startIndex = getIndex(evt);
|
||||
};
|
||||
const onMouseMove = (evt) => {
|
||||
const onMouseMove = (evt: PointerEvent) => {
|
||||
if (dragStarted) {
|
||||
this._dragging = true;
|
||||
endIndex = getIndex(evt);
|
||||
@@ -367,7 +386,7 @@ export class ElevationProfile {
|
||||
}
|
||||
}
|
||||
};
|
||||
const onMouseUp = (evt) => {
|
||||
const onMouseUp = (evt: PointerEvent) => {
|
||||
dragStarted = false;
|
||||
this._dragging = false;
|
||||
this._canvas.style.cursor = '';
|
||||
@@ -463,8 +482,8 @@ export class ElevationProfile {
|
||||
normalized: true,
|
||||
yAxisID: 'ypower',
|
||||
};
|
||||
this._chart.options.scales.x['min'] = 0;
|
||||
this._chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total);
|
||||
this._chart.options.scales!.x!['min'] = 0;
|
||||
this._chart.options.scales!.x!['max'] = getConvertedDistance(data.global.distance.total);
|
||||
|
||||
this.setVisibility();
|
||||
this.setFill();
|
||||
@@ -513,21 +532,24 @@ export class ElevationProfile {
|
||||
return;
|
||||
}
|
||||
const elevationFill = get(this._elevationFill);
|
||||
const dataset = this._chart.data.datasets[0];
|
||||
let segment: any = {};
|
||||
if (elevationFill === 'slope') {
|
||||
this._chart.data.datasets[0]['segment'] = {
|
||||
segment = {
|
||||
backgroundColor: this.slopeFillCallback,
|
||||
};
|
||||
} else if (elevationFill === 'surface') {
|
||||
this._chart.data.datasets[0]['segment'] = {
|
||||
segment = {
|
||||
backgroundColor: this.surfaceFillCallback,
|
||||
};
|
||||
} else if (elevationFill === 'highway') {
|
||||
this._chart.data.datasets[0]['segment'] = {
|
||||
segment = {
|
||||
backgroundColor: this.highwayFillCallback,
|
||||
};
|
||||
} else {
|
||||
this._chart.data.datasets[0]['segment'] = {};
|
||||
segment = {};
|
||||
}
|
||||
Object.assign(dataset, { segment });
|
||||
}
|
||||
|
||||
updateOverlay() {
|
||||
@@ -575,19 +597,22 @@ export class ElevationProfile {
|
||||
}
|
||||
}
|
||||
|
||||
slopeFillCallback(context) {
|
||||
return getSlopeColor(context.p0.raw.slope.segment);
|
||||
slopeFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
||||
const point = context.p0.raw as ElevationProfilePoint;
|
||||
return getSlopeColor(point.slope.segment);
|
||||
}
|
||||
|
||||
surfaceFillCallback(context) {
|
||||
return getSurfaceColor(context.p0.raw.extensions.surface);
|
||||
surfaceFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
||||
const point = context.p0.raw as ElevationProfilePoint;
|
||||
return getSurfaceColor(point.extensions.surface);
|
||||
}
|
||||
|
||||
highwayFillCallback(context) {
|
||||
highwayFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
||||
const point = context.p0.raw as ElevationProfilePoint;
|
||||
return getHighwayColor(
|
||||
context.p0.raw.extensions.highway,
|
||||
context.p0.raw.extensions.sac_scale,
|
||||
context.p0.raw.extensions.mtb_scale
|
||||
point.extensions.highway,
|
||||
point.extensions.sac_scale,
|
||||
point.extensions.mtb_scale
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { get, type Readable } from 'svelte/store';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import mapboxgl, { type FilterSpecification } from 'mapbox-gl';
|
||||
import { map } from '$lib/components/map/map';
|
||||
import { waypointPopup, trackpointPopup } from './gpx-layer-popup';
|
||||
import {
|
||||
@@ -281,25 +281,23 @@ export class GPXLayer {
|
||||
}
|
||||
}
|
||||
|
||||
let visibleSegments: [number, number][] = [];
|
||||
let visibleTrackSegmentIds: string[] = [];
|
||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||
if (!segment._data.hidden) {
|
||||
visibleSegments.push([trackIndex, segmentIndex]);
|
||||
visibleTrackSegmentIds.push(`${trackIndex}-${segmentIndex}`);
|
||||
}
|
||||
});
|
||||
const segmentFilter: FilterSpecification = [
|
||||
'in',
|
||||
['get', 'trackSegmentId'],
|
||||
['literal', visibleTrackSegmentIds],
|
||||
];
|
||||
|
||||
_map.setFilter(
|
||||
this.fileId,
|
||||
[
|
||||
'any',
|
||||
...visibleSegments.map(([trackIndex, segmentIndex]) => [
|
||||
'all',
|
||||
['==', 'trackIndex', trackIndex],
|
||||
['==', 'segmentIndex', segmentIndex],
|
||||
]),
|
||||
],
|
||||
{ validate: false }
|
||||
);
|
||||
_map.setFilter(this.fileId, segmentFilter, { validate: false });
|
||||
|
||||
if (_map.getLayer(this.fileId + '-direction')) {
|
||||
_map.setFilter(this.fileId + '-direction', segmentFilter, { validate: false });
|
||||
}
|
||||
|
||||
let visibleWaypoints: number[] = [];
|
||||
file.wpt.forEach((waypoint, waypointIndex) => {
|
||||
@@ -313,21 +311,6 @@ export class GPXLayer {
|
||||
['in', ['get', 'waypointIndex'], ['literal', visibleWaypoints]],
|
||||
{ validate: false }
|
||||
);
|
||||
|
||||
if (_map.getLayer(this.fileId + '-direction')) {
|
||||
_map.setFilter(
|
||||
this.fileId + '-direction',
|
||||
[
|
||||
'any',
|
||||
...visibleSegments.map(([trackIndex, segmentIndex]) => [
|
||||
'all',
|
||||
['==', 'trackIndex', trackIndex],
|
||||
['==', 'segmentIndex', segmentIndex],
|
||||
]),
|
||||
],
|
||||
{ validate: false }
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// No reliable way to check if the map is ready to add sources and layers
|
||||
return;
|
||||
@@ -686,6 +669,7 @@ export class GPXLayer {
|
||||
}
|
||||
feature.properties.trackIndex = trackIndex;
|
||||
feature.properties.segmentIndex = segmentIndex;
|
||||
feature.properties.trackSegmentId = `${trackIndex}-${segmentIndex}`;
|
||||
|
||||
segmentIndex++;
|
||||
if (segmentIndex >= file.trk[trackIndex].trkseg.length) {
|
||||
|
||||
Reference in New Issue
Block a user