mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-11-15 07:07:07 +00:00
Migration to new Rust-based engine (#401)
* wasm first steps * parsing start * parsing continued * progress * progress * progress * progress * progress * progress * progress * progress * fixes * reorganize * change type * fix * progress * refactoring * progress * progress * progress * keep style info * progress * progress * remove (copy) in name * progress * merge methods into 1 * splice * progress * progress * progress * cleaning * progress * progress * progress * visibility * progress * progress * progress * progress * progress * fixes * progress * fix markers * fix * progress * progress * progress * progress * progress * progress * small fix * use engine info * fix popups * small fix * progress * progress * progress * clean tool * handle self closing elements * reduce data exchanges across boundary * copy on wasm side to avoid any data corruption * speed fix * routing * refactoring * reverse * reverse * crop * split * merge * progress * time tool * fix bounds for waypoints * reduce * fix reactivity * handle corrupt time data * export * progress * remove gpx js package * remove todo * update readme * cleaning * progress * fix link fields * remove unused fields * refactoring and tests * import and export of routes, ref #192 * per-chunk categories storage * cross-tab sync * outdated state * refactoring * reorder methods * refactoring * fix * improve flow * renaming * progress * progress * fix * progress * per file history * keep original flag * use history variant * fix * progress * file explorer * progress * fix * sticky folders * improved sticky folders * fix sortable * fix sortable * fix * fix tab size * no file delete on desktop * fix routing on other file * fix sorting * remember language * open links in browser * commit missing changes * avoid selecting on resize * clarify filename * explorer actions * fix folder styling * drop anywhere in folder * cleaning * fixes * remove focus-ring * full screen * small fix * consistent highlight color * improved preview * performance improvements * sparser data transfers * sparser data transfers * show popup once data is loaded * fix routing control size * fix * sparse desktop build * remove custom ign styles * use new endpoint for openhikingmap * strip native menu * reorder context menu items * route full support * route - track conversion * create route file action * route export, filter out anchors at high zoom level * clarify name handling * move hide & center to view * routes continued * update docs * fix power format, closes #378 * lint fixes * fix display of additional data with gaps, closes #361 * fix linting issues * fix import modules * fix multidrag * change base to resolve * fix type errors * fix layer typings * fix last type error * each keys * improve typings * deploy to cloudflare --------- Co-authored-by: vcoppe <vcoppe@hexaly.com>
This commit is contained in:
1 parent
6a0d434371
commit
8fe7d8d64d
484 files changed
+53949
-41152
No files matched your search
@@ -18,23 +18,25 @@
|
||||
Construction,
|
||||
} from '@lucide/svelte';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import type { Coordinates, GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import type { Coordinates } from '$lib/geo/coordinates';
|
||||
import type { SelectionStatistics } from '$lib/engine';
|
||||
import type { SlicedStatistics } from '$lib/state/selection-statistics';
|
||||
import { settings } from '$lib/state/settings';
|
||||
import { i18n } from '$lib/i18n/i18n.svelte';
|
||||
import { ElevationProfile } from '$lib/components/elevation-profile/elevation-profile';
|
||||
|
||||
const { velocityUnits } = settings;
|
||||
|
||||
let {
|
||||
gpxStatistics,
|
||||
slicedGPXStatistics,
|
||||
statistics,
|
||||
slicedStatistics,
|
||||
hoveredPoint,
|
||||
additionalDatasets,
|
||||
elevationFill,
|
||||
showControls = true,
|
||||
}: {
|
||||
gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
|
||||
statistics: Readable<SelectionStatistics>;
|
||||
slicedStatistics: Writable<SlicedStatistics | undefined>;
|
||||
hoveredPoint: Writable<Coordinates | null>;
|
||||
additionalDatasets: Writable<string[]>;
|
||||
elevationFill: Writable<'slope' | 'surface' | 'highway' | undefined>;
|
||||
@@ -47,8 +49,8 @@
|
||||
|
||||
onMount(() => {
|
||||
elevationProfile = new ElevationProfile(
|
||||
gpxStatistics,
|
||||
slicedGPXStatistics,
|
||||
statistics,
|
||||
slicedStatistics,
|
||||
hoveredPoint,
|
||||
additionalDatasets,
|
||||
elevationFill,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { i18n } from '$lib/i18n/i18n.svelte';
|
||||
import { settings } from '$lib/state/settings';
|
||||
import {
|
||||
getCadenceWithUnits,
|
||||
getConvertedDistance,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
getPowerWithUnits,
|
||||
getTemperatureWithUnits,
|
||||
getVelocityWithUnits,
|
||||
} from '$lib/units';
|
||||
} from '$lib/units/units';
|
||||
import Chart, {
|
||||
type ChartEvent,
|
||||
type ChartOptions,
|
||||
@@ -21,26 +21,37 @@ import Chart, {
|
||||
type TooltipItem,
|
||||
} from 'chart.js/auto';
|
||||
import { get, type Readable, type Writable } from 'svelte/store';
|
||||
import type { Coordinates, GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
||||
import type { Coordinates } from '$lib/geo/coordinates';
|
||||
import {
|
||||
engine,
|
||||
EMPTY_SELECTION_STATISTICS,
|
||||
type SelectionPoint,
|
||||
type SelectionStatistics,
|
||||
type StatisticsMetric,
|
||||
type StatisticsRequest,
|
||||
} from '$lib/engine';
|
||||
import type { SlicedStatistics } from '$lib/state/selection-statistics';
|
||||
import { mode } from 'mode-watcher';
|
||||
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors';
|
||||
import { categoryValue, intervalAt } from '$lib/engine/trackpoint-categories';
|
||||
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/data/colors';
|
||||
|
||||
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
|
||||
|
||||
/** `undefined` for a missing measure (NaN), so that Chart.js leaves a gap instead of a value. */
|
||||
function optional(value: number, convert: (value: number) => number = (v) => v) {
|
||||
return Number.isNaN(value) ? undefined : convert(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* A point of the chart. The rest of the data of the trackpoint is only read when it is hovered
|
||||
* (see `SelectionStatistics.point`), and the categories of the fill from their intervals.
|
||||
*/
|
||||
interface ElevationProfilePoint {
|
||||
x: number;
|
||||
y: number;
|
||||
time?: Date;
|
||||
slope: {
|
||||
at: number;
|
||||
segment: number;
|
||||
length: number;
|
||||
};
|
||||
extensions: Record<string, any>;
|
||||
coordinates: Coordinates;
|
||||
index: number;
|
||||
}
|
||||
|
||||
@@ -51,37 +62,51 @@ export class ElevationProfile {
|
||||
private _dragging = false;
|
||||
private _panning = false;
|
||||
|
||||
private _gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||
private _slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
|
||||
private _statistics: Readable<SelectionStatistics>;
|
||||
/** The statistics that the chart shows, which the fills read for each of its segments. */
|
||||
private _data: SelectionStatistics = EMPTY_SELECTION_STATISTICS;
|
||||
/**
|
||||
* The hovered trackpoint, and its data once it is read from the engine (`undefined` if the
|
||||
* statistics changed in the meantime).
|
||||
*/
|
||||
private _hovered: { index: number; loaded: boolean; point?: SelectionPoint } | null = null;
|
||||
private _loadingHovered = false;
|
||||
/** The last move over the chart, held until the data of its trackpoint is read. */
|
||||
private _heldMove: MouseEvent | null = null;
|
||||
private _slicedStatistics: Writable<SlicedStatistics | undefined>;
|
||||
private _hoveredPoint: Writable<Coordinates | null>;
|
||||
private _additionalDatasets: Readable<string[]>;
|
||||
private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>;
|
||||
/** The statistics that are only read from the engine while the profile shows them. */
|
||||
private _request: StatisticsRequest;
|
||||
|
||||
constructor(
|
||||
gpxStatistics: Readable<GPXStatisticsGroup>,
|
||||
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>,
|
||||
statistics: Readable<SelectionStatistics>,
|
||||
slicedStatistics: Writable<SlicedStatistics | undefined>,
|
||||
hoveredPoint: Writable<Coordinates | null>,
|
||||
additionalDatasets: Readable<string[]>,
|
||||
elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>,
|
||||
canvas: HTMLCanvasElement,
|
||||
overlay: HTMLCanvasElement
|
||||
) {
|
||||
this._gpxStatistics = gpxStatistics;
|
||||
this._slicedGPXStatistics = slicedGPXStatistics;
|
||||
this._statistics = statistics;
|
||||
this._slicedStatistics = slicedStatistics;
|
||||
this._hoveredPoint = hoveredPoint;
|
||||
this._additionalDatasets = additionalDatasets;
|
||||
this._elevationFill = elevationFill;
|
||||
this._canvas = canvas;
|
||||
this._overlay = overlay;
|
||||
this._request = engine.requestStatistics();
|
||||
this.updateRequest();
|
||||
|
||||
import('chartjs-plugin-zoom').then((module) => {
|
||||
Chart.register(module.default);
|
||||
this.initialize();
|
||||
|
||||
this._gpxStatistics.subscribe(() => {
|
||||
this._statistics.subscribe(() => {
|
||||
this.updateData();
|
||||
});
|
||||
this._slicedGPXStatistics.subscribe(() => {
|
||||
this._slicedStatistics.subscribe(() => {
|
||||
this.updateOverlay();
|
||||
});
|
||||
distanceUnits.subscribe(() => {
|
||||
@@ -94,16 +119,36 @@ export class ElevationProfile {
|
||||
this.updateData();
|
||||
});
|
||||
this._additionalDatasets.subscribe(() => {
|
||||
// requesting a missing metric updates the data, and then the visibility
|
||||
this.updateRequest();
|
||||
this.updateDataVisibility();
|
||||
});
|
||||
this._elevationFill.subscribe(() => {
|
||||
this.updateRequest();
|
||||
this.updateFill();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Asks the engine for what the profile currently shows, and nothing more. */
|
||||
updateRequest() {
|
||||
const metrics = new Set<StatisticsMetric>();
|
||||
for (const dataset of get(this._additionalDatasets)) {
|
||||
if (['speed', 'hr', 'cad', 'atemp', 'power'].includes(dataset)) {
|
||||
metrics.add(dataset as StatisticsMetric);
|
||||
}
|
||||
}
|
||||
const elevationFill = get(this._elevationFill);
|
||||
if (elevationFill === 'slope') {
|
||||
metrics.add('slopeSegment');
|
||||
} else if (elevationFill) {
|
||||
metrics.add(elevationFill);
|
||||
}
|
||||
this._request.set(metrics);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
let options: ChartOptions<'line'> = {
|
||||
const options: ChartOptions<'line'> = {
|
||||
animation: false,
|
||||
parsing: false,
|
||||
maintainAspectRatio: false,
|
||||
@@ -133,6 +178,7 @@ export class ElevationProfile {
|
||||
tension: 0.4,
|
||||
borderWidth: 2,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
spanGaps: true,
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
@@ -154,13 +200,16 @@ export class ElevationProfile {
|
||||
return '';
|
||||
},
|
||||
label: (context: TooltipItem<'line'>) => {
|
||||
let point = context.raw as ElevationProfilePoint;
|
||||
const point = context.raw as ElevationProfilePoint;
|
||||
if (context.datasetIndex === 0) {
|
||||
if (this._dragging) {
|
||||
this._hoveredPoint.set(null);
|
||||
} else {
|
||||
this._hoveredPoint.set(point.coordinates);
|
||||
}
|
||||
this._hoveredPoint.set(
|
||||
this._dragging
|
||||
? null
|
||||
: {
|
||||
lat: this._data.lat[point.index],
|
||||
lng: this._data.lng[point.index],
|
||||
}
|
||||
);
|
||||
return `${i18n._('quantities.elevation')}: ${getElevationWithUnits(point.y, false)}`;
|
||||
} else if (context.datasetIndex === 1) {
|
||||
return `${get(velocityUnits) === 'speed' ? i18n._('quantities.speed') : i18n._('quantities.pace')}: ${getVelocityWithUnits(point.y, false)}`;
|
||||
@@ -175,35 +224,47 @@ export class ElevationProfile {
|
||||
}
|
||||
},
|
||||
afterBody: (contexts: TooltipItem<'line'>[]) => {
|
||||
let context = contexts.filter((context) => context.datasetIndex === 0);
|
||||
const context = contexts.filter(
|
||||
(context) => context.datasetIndex === 0
|
||||
);
|
||||
if (context.length === 0) return;
|
||||
let point = context[0].raw as ElevationProfilePoint;
|
||||
let slope = {
|
||||
at: point.slope.at.toFixed(1),
|
||||
segment: point.slope.segment.toFixed(1),
|
||||
length: getDistanceWithUnits(point.slope.length),
|
||||
};
|
||||
let surface = point.extensions.surface
|
||||
? point.extensions.surface
|
||||
: 'unknown';
|
||||
let highway = point.extensions.highway
|
||||
? point.extensions.highway
|
||||
: 'unknown';
|
||||
let sacScale = point.extensions.sac_scale;
|
||||
let mtbScale = point.extensions.mtb_scale;
|
||||
const point = context[0].raw as ElevationProfilePoint;
|
||||
// the rest of the data is there once it is read (see `hover`)
|
||||
const hovered = this._hovered;
|
||||
const details =
|
||||
hovered?.index === point.index ? hovered.point : undefined;
|
||||
const elevationFill = get(this._elevationFill);
|
||||
|
||||
let labels = [
|
||||
const labels = [
|
||||
` ${i18n._('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`,
|
||||
` ${i18n._('quantities.slope')}: ${slope.at} %${get(this._elevationFill) === 'slope' ? ` (${slope.length} @${slope.segment} %)` : ''}`,
|
||||
];
|
||||
|
||||
if (get(this._elevationFill) === 'surface') {
|
||||
if (!details) {
|
||||
return labels;
|
||||
}
|
||||
if (details.slope !== undefined) {
|
||||
let slope = `${details.slope.toFixed(1)} %`;
|
||||
if (
|
||||
elevationFill === 'slope' &&
|
||||
details.slopeSegmentSlope !== undefined &&
|
||||
details.slopeSegmentDistance !== undefined
|
||||
) {
|
||||
slope += ` (${getDistanceWithUnits(details.slopeSegmentDistance)} @${details.slopeSegmentSlope.toFixed(1)} %)`;
|
||||
}
|
||||
labels.push(` ${i18n._('quantities.slope')}: ${slope}`);
|
||||
}
|
||||
const surface = details.surface ?? 'unknown';
|
||||
const highway = details.highway ?? 'unknown';
|
||||
const sacScale = details.sacScale;
|
||||
const mtbScale = details.mtbScale;
|
||||
|
||||
if (elevationFill === 'surface') {
|
||||
labels.push(
|
||||
` ${i18n._('quantities.surface')}: ${i18n._(`toolbar.routing.surface.${surface}`)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (get(this._elevationFill) === 'highway') {
|
||||
if (elevationFill === 'highway') {
|
||||
labels.push(
|
||||
` ${i18n._('quantities.highway')}: ${i18n._(`toolbar.routing.highway.${highway}`)}${
|
||||
sacScale
|
||||
@@ -218,9 +279,9 @@ export class ElevationProfile {
|
||||
}
|
||||
}
|
||||
|
||||
if (point.time) {
|
||||
if (details.time !== undefined) {
|
||||
labels.push(
|
||||
` ${i18n._('quantities.time')}: ${i18n.df.format(point.time)}`
|
||||
` ${i18n._('quantities.time')}: ${i18n.df.format(new Date(details.time))}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -235,7 +296,7 @@ export class ElevationProfile {
|
||||
modifierKey: 'shift',
|
||||
onPanStart: () => {
|
||||
this._panning = true;
|
||||
this._slicedGPXStatistics.set(undefined);
|
||||
this._slicedStatistics.set(undefined);
|
||||
return true;
|
||||
},
|
||||
onPanComplete: () => {
|
||||
@@ -247,7 +308,7 @@ export class ElevationProfile {
|
||||
enabled: true,
|
||||
},
|
||||
mode: 'x',
|
||||
onZoomStart: ({ chart, event }: { chart: Chart; event: any }) => {
|
||||
onZoomStart: ({ event }: { event: any }) => {
|
||||
if (!this._chart) {
|
||||
return false;
|
||||
}
|
||||
@@ -260,7 +321,7 @@ export class ElevationProfile {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._slicedGPXStatistics.set(undefined);
|
||||
this._slicedStatistics.set(undefined);
|
||||
},
|
||||
},
|
||||
limits: {
|
||||
@@ -277,7 +338,7 @@ export class ElevationProfile {
|
||||
},
|
||||
};
|
||||
|
||||
let datasets: string[] = ['speed', 'hr', 'cad', 'atemp', 'power'];
|
||||
const datasets: string[] = ['speed', 'hr', 'cad', 'atemp', 'power'];
|
||||
datasets.forEach((id) => {
|
||||
options.scales![`y${id}`] = {
|
||||
type: 'linear',
|
||||
@@ -285,7 +346,8 @@ export class ElevationProfile {
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
reverse: () => id === 'speed' && get(velocityUnits) === 'pace',
|
||||
// the pace axis is reversed: set with the data, which the units change
|
||||
reverse: false,
|
||||
display: false,
|
||||
};
|
||||
});
|
||||
@@ -302,10 +364,32 @@ export class ElevationProfile {
|
||||
events: ['mouseout'],
|
||||
afterEvent: (chart: Chart, args: { event: ChartEvent }) => {
|
||||
if (args.event.type === 'mouseout') {
|
||||
this._hovered = null;
|
||||
this._heldMove = null;
|
||||
this._hoveredPoint.set(null);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
// The tooltip only shows a trackpoint once all its data is read: a move to
|
||||
// another one is held until then, and replayed (see `loadHovered`).
|
||||
id: 'waitForTrackpoint',
|
||||
events: ['mousemove'],
|
||||
beforeEvent: (chart: Chart, args: { event: ChartEvent }) => {
|
||||
const index = this.indexAt(args.event);
|
||||
if (index === undefined) {
|
||||
return;
|
||||
}
|
||||
const hovered = this._hovered;
|
||||
if (hovered?.index === index && hovered.loaded) {
|
||||
this._heldMove = null;
|
||||
return;
|
||||
}
|
||||
this._heldMove = args.event.native as MouseEvent;
|
||||
this.hover(index);
|
||||
return false;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -361,14 +445,15 @@ export class ElevationProfile {
|
||||
if (startIndex === undefined) {
|
||||
startIndex = endIndex;
|
||||
} else if (startIndex !== endIndex) {
|
||||
this._slicedGPXStatistics.set([
|
||||
get(this._gpxStatistics).sliced(
|
||||
Math.min(startIndex, endIndex),
|
||||
Math.max(startIndex, endIndex)
|
||||
),
|
||||
Math.min(startIndex, endIndex),
|
||||
Math.max(startIndex, endIndex),
|
||||
]);
|
||||
const start = Math.min(startIndex, endIndex);
|
||||
const end = Math.max(startIndex, endIndex);
|
||||
get(this._statistics)
|
||||
.slice(start, end)
|
||||
.then((global) => {
|
||||
if (global) {
|
||||
this._slicedStatistics.set({ global, start, end });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -379,7 +464,7 @@ export class ElevationProfile {
|
||||
this._canvas.style.cursor = '';
|
||||
endIndex = getIndex(evt);
|
||||
if (startIndex === endIndex) {
|
||||
this._slicedGPXStatistics.set(undefined);
|
||||
this._slicedStatistics.set(undefined);
|
||||
}
|
||||
};
|
||||
this._canvas.addEventListener('pointerdown', onMouseDown);
|
||||
@@ -391,7 +476,10 @@ export class ElevationProfile {
|
||||
if (!this._chart) {
|
||||
return;
|
||||
}
|
||||
const data = get(this._gpxStatistics);
|
||||
const data = get(this._statistics);
|
||||
this._data = data;
|
||||
// the indices of the trackpoints are the ones of other statistics
|
||||
this._hovered = null;
|
||||
const units = {
|
||||
distance: get(distanceUnits),
|
||||
velocity: get(velocityUnits),
|
||||
@@ -399,52 +487,42 @@ export class ElevationProfile {
|
||||
};
|
||||
|
||||
const datasets: Array<Array<any>> = [[], [], [], [], [], []];
|
||||
data.forEachTrackPoint((trkpt, distance, speed, slope, index) => {
|
||||
const { global } = data;
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
const x = getConvertedDistance(data.totalDistance[index], units.distance);
|
||||
datasets[0].push({
|
||||
x: getConvertedDistance(distance, units.distance),
|
||||
y: trkpt.ele ? getConvertedElevation(trkpt.ele, units.distance) : 0,
|
||||
time: trkpt.time,
|
||||
slope: slope,
|
||||
extensions: trkpt.getExtensions(),
|
||||
coordinates: trkpt.getCoordinates(),
|
||||
index: index,
|
||||
x,
|
||||
y: getConvertedElevation(data.ele[index], units.distance),
|
||||
index,
|
||||
});
|
||||
if (data.global.time.total > 0) {
|
||||
if (data.speed && (global.totalTime ?? 0) > 0) {
|
||||
datasets[1].push({
|
||||
x: getConvertedDistance(distance, units.distance),
|
||||
y: getConvertedVelocity(speed, units.velocity, units.distance),
|
||||
index: index,
|
||||
x,
|
||||
y: optional(data.speed[index], (value) =>
|
||||
getConvertedVelocity(value, units.velocity, units.distance)
|
||||
),
|
||||
index,
|
||||
});
|
||||
}
|
||||
if (data.global.hr.count > 0) {
|
||||
datasets[2].push({
|
||||
x: getConvertedDistance(distance, units.distance),
|
||||
y: trkpt.getHeartRate(),
|
||||
index: index,
|
||||
});
|
||||
if (data.hr) {
|
||||
datasets[2].push({ x, y: optional(data.hr[index]), index });
|
||||
}
|
||||
if (data.global.cad.count > 0) {
|
||||
datasets[3].push({
|
||||
x: getConvertedDistance(distance, units.distance),
|
||||
y: trkpt.getCadence(),
|
||||
index: index,
|
||||
});
|
||||
if (data.cad) {
|
||||
datasets[3].push({ x, y: optional(data.cad[index]), index });
|
||||
}
|
||||
if (data.global.atemp.count > 0) {
|
||||
if (data.atemp) {
|
||||
datasets[4].push({
|
||||
x: getConvertedDistance(distance, units.distance),
|
||||
y: getConvertedTemperature(trkpt.getTemperature(), units.temperature),
|
||||
index: index,
|
||||
x,
|
||||
y: optional(data.atemp[index], (value) =>
|
||||
getConvertedTemperature(value, units.temperature)
|
||||
),
|
||||
index,
|
||||
});
|
||||
}
|
||||
if (data.global.power.count > 0) {
|
||||
datasets[5].push({
|
||||
x: getConvertedDistance(distance, units.distance),
|
||||
y: trkpt.getPower(),
|
||||
index: index,
|
||||
});
|
||||
if (data.power) {
|
||||
datasets[5].push({ x, y: optional(data.power[index]), index });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this._chart.data.datasets[0] = {
|
||||
label: i18n._('quantities.elevation'),
|
||||
@@ -482,9 +560,11 @@ export class ElevationProfile {
|
||||
|
||||
this._chart.options.scales!.x!['min'] = 0;
|
||||
this._chart.options.scales!.x!['max'] = getConvertedDistance(
|
||||
data.global.distance.total,
|
||||
global.totalDistance,
|
||||
units.distance
|
||||
);
|
||||
// a faster pace is a lower value, it goes up like a higher speed
|
||||
this._chart.options.scales!.yspeed!.reverse = units.velocity === 'pace';
|
||||
|
||||
this.setVisibility();
|
||||
this.setFill();
|
||||
@@ -506,11 +586,11 @@ export class ElevationProfile {
|
||||
}
|
||||
|
||||
const additionalDatasets = get(this._additionalDatasets);
|
||||
let includeSpeed = additionalDatasets.includes('speed');
|
||||
let includeHeartRate = additionalDatasets.includes('hr');
|
||||
let includeCadence = additionalDatasets.includes('cad');
|
||||
let includeTemperature = additionalDatasets.includes('atemp');
|
||||
let includePower = additionalDatasets.includes('power');
|
||||
const includeSpeed = additionalDatasets.includes('speed');
|
||||
const includeHeartRate = additionalDatasets.includes('hr');
|
||||
const includeCadence = additionalDatasets.includes('cad');
|
||||
const includeTemperature = additionalDatasets.includes('atemp');
|
||||
const includePower = additionalDatasets.includes('power');
|
||||
if (this._chart.data.datasets.length == 6) {
|
||||
this._chart.data.datasets[1].hidden = !includeSpeed;
|
||||
this._chart.data.datasets[2].hidden = !includeHeartRate;
|
||||
@@ -563,26 +643,24 @@ export class ElevationProfile {
|
||||
this._overlay.style.width = `${this._overlay.width}px`;
|
||||
this._overlay.style.height = `${this._overlay.height}px`;
|
||||
|
||||
const slicedGPXStatistics = get(this._slicedGPXStatistics);
|
||||
if (slicedGPXStatistics) {
|
||||
let startIndex = slicedGPXStatistics[1];
|
||||
let endIndex = slicedGPXStatistics[2];
|
||||
const slicedStatistics = get(this._slicedStatistics);
|
||||
if (slicedStatistics) {
|
||||
const startIndex = slicedStatistics.start;
|
||||
const endIndex = slicedStatistics.end;
|
||||
|
||||
// Draw selection rectangle
|
||||
let selectionContext = this._overlay.getContext('2d');
|
||||
const selectionContext = this._overlay.getContext('2d');
|
||||
if (selectionContext) {
|
||||
selectionContext.fillStyle = mode.current === 'dark' ? 'white' : 'black';
|
||||
selectionContext.globalAlpha = mode.current === 'dark' ? 0.2 : 0.1;
|
||||
selectionContext.clearRect(0, 0, this._overlay.width, this._overlay.height);
|
||||
|
||||
const gpxStatistics = get(this._gpxStatistics);
|
||||
let startPixel = this._chart.scales.x.getPixelForValue(
|
||||
getConvertedDistance(
|
||||
gpxStatistics.getTrackPoint(startIndex)?.distance.total ?? 0
|
||||
)
|
||||
const statistics = get(this._statistics);
|
||||
const startPixel = this._chart.scales.x.getPixelForValue(
|
||||
getConvertedDistance(statistics.totalDistance[startIndex] ?? 0)
|
||||
);
|
||||
let endPixel = this._chart.scales.x.getPixelForValue(
|
||||
getConvertedDistance(gpxStatistics.getTrackPoint(endIndex)?.distance.total ?? 0)
|
||||
const endPixel = this._chart.scales.x.getPixelForValue(
|
||||
getConvertedDistance(statistics.totalDistance[endIndex] ?? 0)
|
||||
);
|
||||
|
||||
selectionContext.fillRect(
|
||||
@@ -593,33 +671,93 @@ export class ElevationProfile {
|
||||
);
|
||||
}
|
||||
} else if (this._overlay) {
|
||||
let selectionContext = this._overlay.getContext('2d');
|
||||
const selectionContext = this._overlay.getContext('2d');
|
||||
if (selectionContext) {
|
||||
selectionContext.clearRect(0, 0, this._overlay.width, this._overlay.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slopeFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
||||
const point = context.p0.raw as ElevationProfilePoint;
|
||||
return getSlopeColor(point.slope.segment);
|
||||
}
|
||||
|
||||
surfaceFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
||||
const point = context.p0.raw as ElevationProfilePoint;
|
||||
return getSurfaceColor(point.extensions.surface);
|
||||
}
|
||||
|
||||
highwayFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
||||
const point = context.p0.raw as ElevationProfilePoint;
|
||||
return getHighwayColor(
|
||||
point.extensions.highway,
|
||||
point.extensions.sac_scale,
|
||||
point.extensions.mtb_scale
|
||||
/** The trackpoint that the tooltip shows for an event, `undefined` outside of the chart. */
|
||||
private indexAt(event: ChartEvent): number | undefined {
|
||||
const elements = this._chart?.getElementsAtEventForMode(
|
||||
event as unknown as Event,
|
||||
'nearest',
|
||||
{ axis: 'x', intersect: false },
|
||||
false
|
||||
);
|
||||
const element = elements?.find((element) => element.datasetIndex === 0);
|
||||
// the data of the chart can be decimated: its points are the ones of the elements
|
||||
const point = element && this._chart?.data.datasets[0].data[element.index];
|
||||
return (point as ElevationProfilePoint | undefined)?.index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the data of the hovered trackpoint from the engine, for the tooltip. One trackpoint is
|
||||
* read at a time, the last one hovered.
|
||||
*/
|
||||
hover(index: number) {
|
||||
if (this._hovered?.index === index) {
|
||||
return;
|
||||
}
|
||||
this._hovered = { index, loaded: false };
|
||||
this.loadHovered();
|
||||
}
|
||||
|
||||
private async loadHovered() {
|
||||
if (this._loadingHovered) {
|
||||
return; // the one being read is followed by the last one hovered
|
||||
}
|
||||
this._loadingHovered = true;
|
||||
try {
|
||||
while (this._hovered && !this._hovered.loaded) {
|
||||
const hovered = this._hovered;
|
||||
const point = await this._data.point(hovered.index);
|
||||
if (this._hovered !== hovered) {
|
||||
continue; // another trackpoint is hovered, or none
|
||||
}
|
||||
hovered.loaded = true;
|
||||
hovered.point = point;
|
||||
// the move that was held can now show the tooltip
|
||||
const move = this._heldMove;
|
||||
this._heldMove = null;
|
||||
if (move) {
|
||||
this._canvas.dispatchEvent(new MouseEvent(move.type, move));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this._loadingHovered = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The trackpoint that a segment of the chart starts at. */
|
||||
private static segmentStart(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
||||
return (context.p0.raw as ElevationProfilePoint).index;
|
||||
}
|
||||
|
||||
slopeFillCallback = (context: ScriptableLineSegmentContext & { p0: { raw: any } }) => {
|
||||
const segments = this._data.slopeSegments;
|
||||
const index = ElevationProfile.segmentStart(context);
|
||||
const segment = segments ? intervalAt(segments.starts, index) : -1;
|
||||
return getSlopeColor(segment >= 0 ? segments!.slopes[segment] : NaN);
|
||||
};
|
||||
|
||||
surfaceFillCallback = (context: ScriptableLineSegmentContext & { p0: { raw: any } }) => {
|
||||
const index = ElevationProfile.segmentStart(context);
|
||||
return getSurfaceColor(categoryValue(this._data.surface, index) ?? '');
|
||||
};
|
||||
|
||||
highwayFillCallback = (context: ScriptableLineSegmentContext & { p0: { raw: any } }) => {
|
||||
const index = ElevationProfile.segmentStart(context);
|
||||
return getHighwayColor(
|
||||
categoryValue(this._data.highway, index) ?? '',
|
||||
categoryValue(this._data.sacScale, index),
|
||||
categoryValue(this._data.mtbScale, index)
|
||||
);
|
||||
};
|
||||
|
||||
destroy() {
|
||||
this._request.release();
|
||||
if (this._chart) {
|
||||
this._chart.destroy();
|
||||
this._chart = null;
|
||||
|
||||
Reference in new issue
Block a user