mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-01-01 15:54:44 +00:00
Compare commits
17 Commits
867b6a6ac7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
595ea8e2d3 | ||
|
|
d3e733aa3e | ||
|
|
a011768d2d | ||
|
|
4b45b5d716 | ||
|
|
ebe9681c12 | ||
|
|
51c85e4cd5 | ||
|
|
2e171dfbee | ||
|
|
a6a3917986 | ||
|
|
21f2448213 | ||
|
|
e7a1d0488b | ||
|
|
22b8e0edb4 | ||
|
|
d062a38e8f | ||
|
|
affa59130f | ||
|
|
3c816567bc | ||
|
|
e92e48ffde | ||
|
|
4ce7777b86 | ||
|
|
bc130ad867 |
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,9 @@ export class GPXFile extends GPXTreeNode<Track> {
|
||||
},
|
||||
},
|
||||
};
|
||||
this.wpt = gpx.wpt ? gpx.wpt.map((waypoint) => new Waypoint(waypoint)) : [];
|
||||
this.wpt = gpx.wpt
|
||||
? gpx.wpt.map((waypoint, index) => new Waypoint(waypoint, index))
|
||||
: [];
|
||||
this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : [];
|
||||
if (gpx.rte && gpx.rte.length > 0) {
|
||||
this.trk = this.trk.concat(gpx.rte.map((route) => convertRouteToTrack(route)));
|
||||
@@ -186,9 +188,6 @@ export class GPXFile extends GPXTreeNode<Track> {
|
||||
segment._data['segmentIndex'] = segmentIndex;
|
||||
});
|
||||
});
|
||||
this.wpt.forEach((waypoint, waypointIndex) => {
|
||||
waypoint._data['index'] = waypointIndex;
|
||||
});
|
||||
}
|
||||
|
||||
get children(): Array<Track> {
|
||||
@@ -807,7 +806,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
constructor(segment?: (TrackSegmentType & { _data?: any }) | TrackSegment) {
|
||||
super();
|
||||
if (segment) {
|
||||
this.trkpt = segment.trkpt.map((point) => new TrackPoint(point));
|
||||
this.trkpt = segment.trkpt.map((point, index) => new TrackPoint(point, index));
|
||||
if (segment.hasOwnProperty('_data')) {
|
||||
this._data = segment._data;
|
||||
}
|
||||
@@ -819,12 +818,10 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
_computeStatistics(): GPXStatistics {
|
||||
let statistics = new GPXStatistics();
|
||||
|
||||
statistics.local.points = this.trkpt.map((point) => point);
|
||||
statistics.local.points = this.trkpt.slice(0);
|
||||
|
||||
const points = this.trkpt;
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
points[i]._data['index'] = i;
|
||||
|
||||
// distance
|
||||
let dist = 0;
|
||||
if (i > 0) {
|
||||
@@ -1317,7 +1314,7 @@ export class TrackPoint {
|
||||
|
||||
_data: { [key: string]: any } = {};
|
||||
|
||||
constructor(point: (TrackPointType & { _data?: any }) | TrackPoint) {
|
||||
constructor(point: (TrackPointType & { _data?: any }) | TrackPoint, index?: number) {
|
||||
this.attributes = point.attributes;
|
||||
this.ele = point.ele;
|
||||
this.time = point.time;
|
||||
@@ -1325,6 +1322,9 @@ export class TrackPoint {
|
||||
if (point.hasOwnProperty('_data')) {
|
||||
this._data = point._data;
|
||||
}
|
||||
if (index !== undefined) {
|
||||
this._data.index = index;
|
||||
}
|
||||
}
|
||||
|
||||
getCoordinates(): Coordinates {
|
||||
@@ -1468,11 +1468,18 @@ export class TrackPoint {
|
||||
|
||||
clone(): TrackPoint {
|
||||
return new TrackPoint({
|
||||
attributes: cloneJSON(this.attributes),
|
||||
attributes: {
|
||||
lat: this.attributes.lat,
|
||||
lon: this.attributes.lon,
|
||||
},
|
||||
ele: this.ele,
|
||||
time: this.time ? new Date(this.time.getTime()) : undefined,
|
||||
extensions: cloneJSON(this.extensions),
|
||||
_data: cloneJSON(this._data),
|
||||
extensions: this.extensions ? cloneJSON(this.extensions) : undefined,
|
||||
_data: {
|
||||
index: this._data?.index,
|
||||
anchor: this._data?.anchor,
|
||||
zoom: this._data?.zoom,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1491,7 +1498,7 @@ export class Waypoint {
|
||||
type?: string;
|
||||
_data: { [key: string]: any } = {};
|
||||
|
||||
constructor(waypoint: (WaypointType & { _data?: any }) | Waypoint) {
|
||||
constructor(waypoint: (WaypointType & { _data?: any }) | Waypoint, index?: number) {
|
||||
this.attributes = waypoint.attributes;
|
||||
this.ele = waypoint.ele;
|
||||
this.time = waypoint.time;
|
||||
@@ -1510,6 +1517,9 @@ export class Waypoint {
|
||||
if (waypoint.hasOwnProperty('_data')) {
|
||||
this._data = waypoint._data;
|
||||
}
|
||||
if (index !== undefined) {
|
||||
this._data.index = index;
|
||||
}
|
||||
}
|
||||
|
||||
getCoordinates(): Coordinates {
|
||||
@@ -1557,7 +1567,10 @@ export class Waypoint {
|
||||
|
||||
clone(): Waypoint {
|
||||
return new Waypoint({
|
||||
attributes: cloneJSON(this.attributes),
|
||||
attributes: {
|
||||
lat: this.attributes.lat,
|
||||
lon: this.attributes.lon,
|
||||
},
|
||||
ele: this.ele,
|
||||
time: this.time ? new Date(this.time.getTime()) : undefined,
|
||||
name: this.name,
|
||||
|
||||
@@ -59,13 +59,13 @@ function ramerDouglasPeuckerRecursive(
|
||||
}
|
||||
|
||||
export function crossarcDistance(
|
||||
point1: TrackPoint,
|
||||
point2: TrackPoint,
|
||||
point1: TrackPoint | Coordinates,
|
||||
point2: TrackPoint | Coordinates,
|
||||
point3: TrackPoint | Coordinates
|
||||
): number {
|
||||
return crossarc(
|
||||
point1.getCoordinates(),
|
||||
point2.getCoordinates(),
|
||||
point1 instanceof TrackPoint ? point1.getCoordinates() : point1,
|
||||
point2 instanceof TrackPoint ? point2.getCoordinates() : point2,
|
||||
point3 instanceof TrackPoint ? point3.getCoordinates() : point3
|
||||
);
|
||||
}
|
||||
|
||||
3
website/.prettierignore
Normal file
3
website/.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
||||
src/lib/components/ui
|
||||
src/lib/docs/**/*.mdx
|
||||
**/*.webmanifest
|
||||
35
website/package-lock.json
generated
35
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",
|
||||
@@ -47,7 +47,7 @@
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
||||
"@typescript-eslint/parser": "^8.33.1",
|
||||
"bits-ui": "^2.12.0",
|
||||
"bits-ui": "^2.14.4",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-svelte": "^3.9.1",
|
||||
@@ -3241,9 +3241,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/bits-ui": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.12.0.tgz",
|
||||
"integrity": "sha512-8NF4ILNyAJlIxDXpl/akGXGBV5QmZAe+8gTfPttM5P6/+LrijumcSfFXY5cr4QkXwTmLA7H5stYpbgJf2XFJvg==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.14.4.tgz",
|
||||
"integrity": "sha512-W6kenhnbd/YVvur+DKkaVJ6GldE53eLewur5AhUCqslYQ0vjZr8eWlOfwZnMiPB+PF5HMVqf61vXBvmyrAmPWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -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",
|
||||
@@ -31,7 +31,7 @@
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
||||
"@typescript-eslint/parser": "^8.33.1",
|
||||
"bits-ui": "^2.12.0",
|
||||
"bits-ui": "^2.14.4",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-svelte": "^3.9.1",
|
||||
@@ -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 = '';
|
||||
@@ -409,62 +428,77 @@ export class ElevationProfile {
|
||||
segment: {},
|
||||
};
|
||||
this._chart.data.datasets[1] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
data:
|
||||
data.global.time.total > 0
|
||||
? data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: getConvertedVelocity(data.local.speed[index]),
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
})
|
||||
: [],
|
||||
normalized: true,
|
||||
yAxisID: 'yspeed',
|
||||
};
|
||||
this._chart.data.datasets[2] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
data:
|
||||
data.global.hr.count > 0
|
||||
? data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.getHeartRate(),
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
})
|
||||
: [],
|
||||
normalized: true,
|
||||
yAxisID: 'yhr',
|
||||
};
|
||||
this._chart.data.datasets[3] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
data:
|
||||
data.global.cad.count > 0
|
||||
? data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.getCadence(),
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
})
|
||||
: [],
|
||||
normalized: true,
|
||||
yAxisID: 'ycad',
|
||||
};
|
||||
this._chart.data.datasets[4] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
data:
|
||||
data.global.atemp.count > 0
|
||||
? data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: getConvertedTemperature(point.getTemperature()),
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
})
|
||||
: [],
|
||||
normalized: true,
|
||||
yAxisID: 'yatemp',
|
||||
};
|
||||
this._chart.data.datasets[5] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
data:
|
||||
data.global.power.count > 0
|
||||
? data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.getPower(),
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
})
|
||||
: [],
|
||||
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 +547,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 +612,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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { Compass, Earth, Mountain, Timer } from '@lucide/svelte';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import type { PopupItem } from '$lib/components/map/map-popup';
|
||||
import { map } from '$lib/components/map/map';
|
||||
|
||||
let { trackpoint }: { trackpoint: PopupItem<TrackPoint> } = $props();
|
||||
</script>
|
||||
@@ -40,7 +41,7 @@
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
href={`https://www.openstreetmap.org/edit?#map=18/${trackpoint.item.getLatitude().toFixed(5)}/${trackpoint.item.getLongitude().toFixed(5)}`}
|
||||
href={`https://www.openstreetmap.org/edit?#map=${(($map?.getZoom() ?? 17) + 1).toFixed(0)}/${trackpoint.item.getLatitude().toFixed(5)}/${trackpoint.item.getLongitude().toFixed(5)}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Earth size="14" />
|
||||
|
||||
@@ -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 {
|
||||
@@ -153,8 +153,6 @@ export class GPXLayer {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadIcons();
|
||||
|
||||
if (
|
||||
file._data.style &&
|
||||
file._data.style.color &&
|
||||
@@ -164,6 +162,8 @@ export class GPXLayer {
|
||||
this.layerColor = `#${file._data.style.color}`;
|
||||
}
|
||||
|
||||
this.loadIcons();
|
||||
|
||||
try {
|
||||
let source = _map.getSource(this.fileId) as mapboxgl.GeoJSONSource | undefined;
|
||||
if (source) {
|
||||
@@ -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) {
|
||||
@@ -718,7 +702,7 @@ export class GPXLayer {
|
||||
properties: {
|
||||
fileId: this.fileId,
|
||||
waypointIndex: index,
|
||||
icon: `${this.fileId}-waypoint-${getSymbolKey(waypoint.sym) ?? 'default'}`,
|
||||
icon: `waypoint-${getSymbolKey(waypoint.sym) ?? 'default'}-${this.layerColor}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -739,7 +723,7 @@ export class GPXLayer {
|
||||
});
|
||||
|
||||
symbols.forEach((symbol) => {
|
||||
const iconId = `${this.fileId}-waypoint-${symbol ?? 'default'}`;
|
||||
const iconId = `waypoint-${symbol ?? 'default'}-${this.layerColor}`;
|
||||
if (!_map.hasImage(iconId)) {
|
||||
let icon = new Image(100, 100);
|
||||
icon.onload = () => {
|
||||
|
||||
@@ -29,13 +29,13 @@ Beste era batez, fitxategiak zuzenean arrastatu eta jaregin ditzakezu zure fitxa
|
||||
|
||||
Sortu hautatutako fitxategien kopia bat.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Ezabatu
|
||||
|
||||
Delete the currently selected files.
|
||||
Ezabatu hautatutako fitxategiak.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Ezabatu guztiak
|
||||
|
||||
Delete all files.
|
||||
Ezabatu fitxategi guztiak.
|
||||
|
||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Esportatu...
|
||||
|
||||
|
||||
@@ -29,13 +29,13 @@ cÈ inoltre possibile trascinare i file direttamente dal file system del tuo Pc
|
||||
|
||||
Crea una copia dei file attualmente selezionati.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" />Elimina
|
||||
|
||||
Delete the currently selected files.
|
||||
Elimina i file attualmente selezionati.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" />Cancella tutto
|
||||
|
||||
Delete all files.
|
||||
Elimina tutti i file.
|
||||
|
||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Esporta...
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Deze handleiding zal je door alle componenten en gereedschappen van de interface
|
||||
<DocsImage src="getting-started/interface" alt="De gpx.studio interface." />
|
||||
|
||||
Zoals weergegeven in bovenstaande scherm, is de interface verdeeld in vier hoofddelen rond de kaart.
|
||||
Voordat we in de details van elke sectie duiken, hebben we een snel overzicht van de interface.
|
||||
Voordat we in de details van elke sectie duiken, eerst een snel overzicht van de interface.
|
||||
|
||||
## Menu
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ Deze actie is alleen beschikbaar wanneer de verticale indeling van de bestandsli
|
||||
|
||||
### <ClipboardPaste size="16" class="inline-block" style="margin-bottom: 2px" /> Plakken
|
||||
|
||||
Plak de bestandsitems van het klembord naar het huidige hiërarchie niveau indien compatibel.
|
||||
Plak de bestandsitems van het klembord naar het huidige hiërarchieniveau indien compatibel.
|
||||
|
||||
<DocsNote>
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ title: 文件
|
||||
|
||||
Delete the currently selected files.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> 删除全部
|
||||
|
||||
Delete all files.
|
||||
删除全部文件。
|
||||
|
||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> 导出...
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { freeze, type WritableDraft } from 'immer';
|
||||
import {
|
||||
distance,
|
||||
GPXFile,
|
||||
parseGPX,
|
||||
Track,
|
||||
@@ -30,7 +29,7 @@ import {
|
||||
} from 'gpx';
|
||||
import { get } from 'svelte/store';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { getClosestLinePoint, getElevation } from '$lib/utils';
|
||||
import { getClosestLinePoint, getClosestTrackSegments, getElevation } from '$lib/utils';
|
||||
import { gpxStatistics } from '$lib/logic/statistics';
|
||||
import { boundsManager } from './bounds';
|
||||
|
||||
@@ -453,34 +452,13 @@ export const fileActions = {
|
||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||
if (level === ListLevel.FILE) {
|
||||
let file = fileStateCollection.getFile(fileId);
|
||||
if (file) {
|
||||
let statistics = fileStateCollection.getStatistics(fileId);
|
||||
if (file && statistics) {
|
||||
if (file.trk.length > 1) {
|
||||
let fileIds = getFileIds(file.trk.length);
|
||||
let closest = file.wpt.map((wpt, wptIndex) => {
|
||||
return {
|
||||
wptIndex: wptIndex,
|
||||
index: [0],
|
||||
distance: Number.MAX_VALUE,
|
||||
};
|
||||
});
|
||||
file.trk.forEach((track, index) => {
|
||||
track.getSegments().forEach((segment) => {
|
||||
segment.trkpt.forEach((point) => {
|
||||
file.wpt.forEach((wpt, wptIndex) => {
|
||||
let dist = distance(
|
||||
point.getCoordinates(),
|
||||
wpt.getCoordinates()
|
||||
let closest = file.wpt.map((wpt) =>
|
||||
getClosestTrackSegments(file, statistics, wpt.getCoordinates())
|
||||
);
|
||||
if (dist < closest[wptIndex].distance) {
|
||||
closest[wptIndex].distance = dist;
|
||||
closest[wptIndex].index = [index];
|
||||
} else if (dist === closest[wptIndex].distance) {
|
||||
closest[wptIndex].index.push(index);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
file.trk.forEach((track, index) => {
|
||||
let newFile = file.clone();
|
||||
let tracks = track.trkseg.map((segment, segmentIndex) => {
|
||||
@@ -495,9 +473,11 @@ export const fileActions = {
|
||||
newFile.replaceWaypoints(
|
||||
0,
|
||||
file.wpt.length - 1,
|
||||
closest
|
||||
.filter((c) => c.index.includes(index))
|
||||
.map((c) => file.wpt[c.wptIndex])
|
||||
file.wpt.filter((wpt, wptIndex) =>
|
||||
closest[wptIndex].some(
|
||||
([trackIndex, segmentIndex]) => trackIndex === index
|
||||
)
|
||||
)
|
||||
);
|
||||
newFile._data.id = fileIds[index];
|
||||
newFile.metadata.name =
|
||||
@@ -506,29 +486,9 @@ export const fileActions = {
|
||||
});
|
||||
} else if (file.trk.length === 1) {
|
||||
let fileIds = getFileIds(file.trk[0].trkseg.length);
|
||||
let closest = file.wpt.map((wpt, wptIndex) => {
|
||||
return {
|
||||
wptIndex: wptIndex,
|
||||
index: [0],
|
||||
distance: Number.MAX_VALUE,
|
||||
};
|
||||
});
|
||||
file.trk[0].trkseg.forEach((segment, index) => {
|
||||
segment.trkpt.forEach((point) => {
|
||||
file.wpt.forEach((wpt, wptIndex) => {
|
||||
let dist = distance(
|
||||
point.getCoordinates(),
|
||||
wpt.getCoordinates()
|
||||
let closest = file.wpt.map((wpt) =>
|
||||
getClosestTrackSegments(file, statistics, wpt.getCoordinates())
|
||||
);
|
||||
if (dist < closest[wptIndex].distance) {
|
||||
closest[wptIndex].distance = dist;
|
||||
closest[wptIndex].index = [index];
|
||||
} else if (dist === closest[wptIndex].distance) {
|
||||
closest[wptIndex].index.push(index);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
file.trk[0].trkseg.forEach((segment, index) => {
|
||||
let newFile = file.clone();
|
||||
newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [
|
||||
@@ -537,9 +497,11 @@ export const fileActions = {
|
||||
newFile.replaceWaypoints(
|
||||
0,
|
||||
file.wpt.length - 1,
|
||||
closest
|
||||
.filter((c) => c.index.includes(index))
|
||||
.map((c) => file.wpt[c.wptIndex])
|
||||
file.wpt.filter((wpt, wptIndex) =>
|
||||
closest[wptIndex].some(
|
||||
([trackIndex, segmentIndex]) => segmentIndex === index
|
||||
)
|
||||
)
|
||||
);
|
||||
newFile._data.id = fileIds[index];
|
||||
newFile.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`;
|
||||
|
||||
@@ -22,25 +22,34 @@ export class GPXStatisticsTree {
|
||||
}
|
||||
|
||||
getStatisticsFor(item: ListItem): GPXStatistics {
|
||||
let statistics = new GPXStatistics();
|
||||
let statistics = [];
|
||||
let id = item.getIdAtLevel(this.level);
|
||||
if (id === undefined || id === 'waypoints') {
|
||||
Object.keys(this.statistics).forEach((key) => {
|
||||
if (this.statistics[key] instanceof GPXStatistics) {
|
||||
statistics.mergeWith(this.statistics[key]);
|
||||
statistics.push(this.statistics[key]);
|
||||
} else {
|
||||
statistics.mergeWith(this.statistics[key].getStatisticsFor(item));
|
||||
statistics.push(this.statistics[key].getStatisticsFor(item));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let child = this.statistics[id];
|
||||
if (child instanceof GPXStatistics) {
|
||||
statistics.mergeWith(child);
|
||||
statistics.push(child);
|
||||
} else if (child !== undefined) {
|
||||
statistics.mergeWith(child.getStatisticsFor(item));
|
||||
statistics.push(child.getStatisticsFor(item));
|
||||
}
|
||||
}
|
||||
return statistics;
|
||||
if (statistics.length === 0) {
|
||||
return new GPXStatistics();
|
||||
} else if (statistics.length === 1) {
|
||||
return statistics[0];
|
||||
} else {
|
||||
return statistics.reduce((acc, curr) => {
|
||||
acc.mergeWith(curr);
|
||||
return acc;
|
||||
}, new GPXStatistics());
|
||||
}
|
||||
}
|
||||
}
|
||||
export type GPXFileWithStatistics = { file: GPXFile; statistics: GPXStatisticsTree };
|
||||
|
||||
@@ -2,11 +2,13 @@ import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { base } from '$app/paths';
|
||||
import { languages } from '$lib/languages';
|
||||
import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance } from 'gpx';
|
||||
import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance, GPXFile } from 'gpx';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt';
|
||||
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||
import PNGReader from 'png.js';
|
||||
import type { GPXStatisticsTree } from '$lib/logic/statistics-tree';
|
||||
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
@@ -47,6 +49,59 @@ export function getClosestLinePoint(
|
||||
return closest;
|
||||
}
|
||||
|
||||
export function getClosestTrackSegments(
|
||||
file: GPXFile,
|
||||
statistics: GPXStatisticsTree,
|
||||
point: Coordinates
|
||||
): [number, number][] {
|
||||
let segmentBoundsDistances: [number, number, number][] = [];
|
||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||
let segmentStatistics = statistics.getStatisticsFor(
|
||||
new ListTrackSegmentItem(file._data.id, trackIndex, segmentIndex)
|
||||
);
|
||||
let segmentBounds = segmentStatistics.global.bounds;
|
||||
let northEast = segmentBounds.northEast;
|
||||
let southWest = segmentBounds.southWest;
|
||||
let bounds = new mapboxgl.LngLatBounds(southWest, northEast);
|
||||
if (bounds.contains(point)) {
|
||||
segmentBoundsDistances.push([0, trackIndex, segmentIndex]);
|
||||
} else {
|
||||
let northWest: Coordinates = { lat: northEast.lat, lon: southWest.lon };
|
||||
let southEast: Coordinates = { lat: southWest.lat, lon: northEast.lon };
|
||||
let distanceToBounds = Math.min(
|
||||
crossarcDistance(northWest, northEast, point),
|
||||
crossarcDistance(northEast, southEast, point),
|
||||
crossarcDistance(southEast, southWest, point),
|
||||
crossarcDistance(southWest, northWest, point)
|
||||
);
|
||||
segmentBoundsDistances.push([distanceToBounds, trackIndex, segmentIndex]);
|
||||
}
|
||||
});
|
||||
segmentBoundsDistances.sort((a, b) => a[0] - b[0]);
|
||||
|
||||
let closest: { distance: number; indices: [number, number][] } = {
|
||||
distance: Number.MAX_VALUE,
|
||||
indices: [],
|
||||
};
|
||||
for (let s = 0; s < segmentBoundsDistances.length; s++) {
|
||||
if (segmentBoundsDistances[s][0] > closest.distance) {
|
||||
break;
|
||||
}
|
||||
const segment = file.getSegment(segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]);
|
||||
segment.trkpt.forEach((pt) => {
|
||||
let dist = distance(pt.getCoordinates(), point);
|
||||
if (dist < closest.distance) {
|
||||
closest.distance = dist;
|
||||
closest.indices = [[segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]]];
|
||||
} else if (dist === closest.distance) {
|
||||
closest.indices.push([segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return closest.indices;
|
||||
}
|
||||
|
||||
export function getElevation(
|
||||
points: (TrackPoint | Waypoint | Coordinates)[],
|
||||
ELEVATION_ZOOM: number = 13,
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
"center": "Centrar",
|
||||
"open_in": "Abrir en",
|
||||
"copy_coordinates": "Copiar coordenadas",
|
||||
"edit_osm": "Edit in OpenStreetMap"
|
||||
"edit_osm": "Editar en OpenStreetMap"
|
||||
},
|
||||
"toolbar": {
|
||||
"routing": {
|
||||
@@ -353,7 +353,7 @@
|
||||
"water": "Agua",
|
||||
"shower": "Ducha",
|
||||
"shelter": "Refugio",
|
||||
"cemetery": "Cemetery",
|
||||
"cemetery": "Cementerio",
|
||||
"motorized": "Coches y motos",
|
||||
"fuel-station": "Gasolinera",
|
||||
"parking": "Aparcamiento",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"undo": "Desegin",
|
||||
"redo": "Berregin",
|
||||
"delete": "Ezabatu",
|
||||
"delete_all": "Delete all",
|
||||
"delete_all": "Ezabatu guztiak",
|
||||
"select_all": "Hautatu dena",
|
||||
"view": "Ikusi",
|
||||
"elevation_profile": "Altuera profila",
|
||||
@@ -80,7 +80,7 @@
|
||||
"center": "Erdiratu",
|
||||
"open_in": "Ireki hemen",
|
||||
"copy_coordinates": "Kopiatu koordenatuak",
|
||||
"edit_osm": "Edit in OpenStreetMap"
|
||||
"edit_osm": "Editatu OpenStreeMapen"
|
||||
},
|
||||
"toolbar": {
|
||||
"routing": {
|
||||
@@ -353,7 +353,7 @@
|
||||
"water": "Ura",
|
||||
"shower": "Dutxa",
|
||||
"shelter": "Babeslekua",
|
||||
"cemetery": "Cemetery",
|
||||
"cemetery": "Hilerria",
|
||||
"motorized": "Kotxeak eta motorrak",
|
||||
"fuel-station": "Gasolindegia",
|
||||
"parking": "Aparkalekua",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"undo": "Annulla",
|
||||
"redo": "Ripeti",
|
||||
"delete": "Elimina",
|
||||
"delete_all": "Delete all",
|
||||
"delete_all": "Cancella tutto",
|
||||
"select_all": "Seleziona tutto",
|
||||
"view": "Visualizza",
|
||||
"elevation_profile": "Profilo altimetrico",
|
||||
@@ -80,7 +80,7 @@
|
||||
"center": "Centra",
|
||||
"open_in": "Apri con",
|
||||
"copy_coordinates": "Copia le coordinate",
|
||||
"edit_osm": "Edit in OpenStreetMap"
|
||||
"edit_osm": "Modifica in OpenStreetMap"
|
||||
},
|
||||
"toolbar": {
|
||||
"routing": {
|
||||
@@ -353,7 +353,7 @@
|
||||
"water": "Acqua",
|
||||
"shower": "Doccia",
|
||||
"shelter": "Riparo",
|
||||
"cemetery": "Cemetery",
|
||||
"cemetery": "Cimitero",
|
||||
"motorized": "Auto e Motocicli",
|
||||
"fuel-station": "Stazione di Rifornimento",
|
||||
"parking": "Parcheggio",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"undo": "撤销",
|
||||
"redo": "恢复",
|
||||
"delete": "删除",
|
||||
"delete_all": "",
|
||||
"delete_all": "全部删除",
|
||||
"select_all": "全选",
|
||||
"view": "显示",
|
||||
"elevation_profile": "海拔剖面图",
|
||||
@@ -80,7 +80,7 @@
|
||||
"center": "居中",
|
||||
"open_in": "打开于",
|
||||
"copy_coordinates": "复制坐标",
|
||||
"edit_osm": "Edit in OpenStreetMap"
|
||||
"edit_osm": "在 OpenStreetMap 中编辑"
|
||||
},
|
||||
"toolbar": {
|
||||
"routing": {
|
||||
@@ -353,7 +353,7 @@
|
||||
"water": "饮用水",
|
||||
"shower": "淋浴",
|
||||
"shelter": "庇护所",
|
||||
"cemetery": "Cemetery",
|
||||
"cemetery": "墓地",
|
||||
"motorized": "汽车和摩托车",
|
||||
"fuel-station": "加油站",
|
||||
"parking": "停车场",
|
||||
|
||||
Reference in New Issue
Block a user