start localization

This commit is contained in:
vcoppe
2024-04-24 16:12:50 +02:00
parent 9bde53a4e2
commit 78b7612171
14 changed files with 1001 additions and 94 deletions

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import Data from '$lib/components/Data.svelte';
import ElevationProfile from '$lib/components/ElevationProfile.svelte';
import FileList from '$lib/components/FileList.svelte';
import GPXData from '$lib/components/GPXData.svelte';
import Map from '$lib/components/Map.svelte';
import Menu from '$lib/components/Menu.svelte';
import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
import LayerControl from '$lib/components/layer-control/LayerControl.svelte';
</script>
<div class="flex flex-col w-screen h-screen">
<div class="grow relative">
<Menu />
<Toolbar />
<Map class="h-full" />
<LayerControl />
<Data />
<FileList />
</div>
<div class="h-60 flex flex-row gap-2 overflow-hidden border">
<GPXData />
<ElevationProfile />
</div>
</div>

View File

@@ -6,7 +6,7 @@
import Chart from 'chart.js/auto';
import mapboxgl from 'mapbox-gl';
import { map, fileCollection, fileOrder, selectedFiles } from '$lib/stores';
import { map, fileCollection, fileOrder, selectedFiles, settings } from '$lib/stores';
import { onDestroy, onMount } from 'svelte';
import {
@@ -21,6 +21,28 @@
import { GPXFiles } from 'gpx';
import { surfaceColors } from '$lib/assets/surfaces';
import { _ } from 'svelte-i18n';
import {
getCadenceUnits,
getCadenceWithUnits,
getConvertedDistance,
getConvertedElevation,
getConvertedTemperature,
getConvertedVelocity,
getDistanceUnits,
getDistanceWithUnits,
getElevationUnits,
getElevationWithUnits,
getHeartRateUnits,
getHeartRateWithUnits,
getPowerUnits,
getPowerWithUnits,
getTemperatureUnits,
getTemperatureWithUnits,
getVelocityUnits,
getVelocityWithUnits
} from '$lib/units';
let canvas: HTMLCanvasElement;
let chart: Chart;
@@ -41,7 +63,7 @@
type: 'linear',
title: {
display: true,
text: 'Distance (km)',
text: `${$_('quantities.distance')} (${getDistanceUnits()})`,
padding: 0,
align: 'end'
}
@@ -50,7 +72,7 @@
type: 'linear',
title: {
display: true,
text: 'Elevation (m)',
text: `${$_('quantities.elevation')} (${getElevationUnits()})`,
padding: 0
}
}
@@ -82,41 +104,34 @@
label: function (context: Chart.TooltipContext) {
let point = context.raw;
if (context.datasetIndex === 0) {
let elevation = point.y.toFixed(0);
if ($map && marker) {
marker.addTo($map);
marker.setLngLat(point.coordinates);
}
return `Elevation: ${elevation} m`;
return `${$_('quantities.elevation')}: ${getElevationWithUnits(point.y, false)}`;
} else if (context.datasetIndex === 1) {
let speed = point.y.toFixed(2);
return `Speed: ${speed} km/h`;
return `${$settings.velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')}: ${getVelocityWithUnits(point.y, false)}`;
} else if (context.datasetIndex === 2) {
let hr = point.y;
return `Heart Rate: ${hr} bpm`;
return `${$_('quantities.heartrate')}: ${getHeartRateWithUnits(point.y)}`;
} else if (context.datasetIndex === 3) {
let cad = point.y;
return `Cadence: ${cad} rpm`;
return `${$_('quantities.cadence')}: ${getCadenceWithUnits(point.y)}`;
} else if (context.datasetIndex === 4) {
let atemp = point.y.toFixed(1);
return `Temperature: ${atemp} °C`;
return `${$_('quantities.temperature')}: ${getTemperatureWithUnits(point.y, false)}`;
} else if (context.datasetIndex === 5) {
let power = point.y;
return `Power: ${power} W`;
return `${$_('quantities.power')}: ${getPowerWithUnits(point.y)}`;
}
},
afterBody: function (contexts: Chart.TooltipContext[]) {
let context = contexts.filter((context) => context.datasetIndex === 0);
if (context.length === 0) return;
let point = context[0].raw;
let distance = point.x.toFixed(2);
let slope = point.slope.toFixed(1);
let surface = point.surface ? point.surface : 'unknown';
return [
` Distance: ${distance} km`,
` Slope: ${slope} %`,
` Surface: ${surface}`
` ${$_('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`,
` ${$_('quantities.slope')}: ${slope} %`,
` ${$_('quantities.surface')}: ${surface}`
];
}
}
@@ -134,28 +149,28 @@
} = {
speed: {
id: 'speed',
label: 'Speed',
units: 'km/h'
label: $_('quantities.speed'),
units: getVelocityUnits()
},
hr: {
id: 'hr',
label: 'Heart Rate',
units: 'bpm'
label: $_('quantities.heartrate'),
units: getHeartRateUnits()
},
cad: {
id: 'cad',
label: 'Cadence',
units: 'rpm'
label: $_('quantities.cadence'),
units: getCadenceUnits()
},
atemp: {
id: 'atemp',
label: 'Temperature',
units: '°C'
label: $_('quantities.temperature'),
units: getTemperatureUnits()
},
power: {
id: 'power',
label: 'Power',
units: 'W'
label: $_('quantities.power'),
units: getPowerUnits()
}
};
@@ -211,11 +226,11 @@
let trackPointsAndStatistics = gpxFiles.getTrackPointsAndStatistics();
chart.data.datasets[0] = {
label: 'Elevation',
label: $_('quantities.elevation'),
data: trackPointsAndStatistics.points.map((point, index) => {
return {
x: trackPointsAndStatistics.statistics.distance[index],
y: point.ele ? point.ele : 0,
x: getConvertedDistance(trackPointsAndStatistics.statistics.distance[index]),
y: point.ele ? getConvertedElevation(point.ele) : 0,
slope: trackPointsAndStatistics.statistics.slope[index],
surface: point.getSurface(),
coordinates: point.getCoordinates()
@@ -229,8 +244,8 @@
label: datasets.speed.label,
data: trackPointsAndStatistics.points.map((point, index) => {
return {
x: trackPointsAndStatistics.statistics.distance[index],
y: trackPointsAndStatistics.statistics.speed[index]
x: getConvertedDistance(trackPointsAndStatistics.statistics.distance[index]),
y: getConvertedVelocity(trackPointsAndStatistics.statistics.speed[index])
};
}),
normalized: true,
@@ -241,7 +256,7 @@
label: datasets.hr.label,
data: trackPointsAndStatistics.points.map((point, index) => {
return {
x: trackPointsAndStatistics.statistics.distance[index],
x: getConvertedDistance(trackPointsAndStatistics.statistics.distance[index]),
y: point.getHeartRate()
};
}),
@@ -253,7 +268,7 @@
label: datasets.cad.label,
data: trackPointsAndStatistics.points.map((point, index) => {
return {
x: trackPointsAndStatistics.statistics.distance[index],
x: getConvertedDistance(trackPointsAndStatistics.statistics.distance[index]),
y: point.getCadence()
};
}),
@@ -265,8 +280,8 @@
label: datasets.atemp.label,
data: trackPointsAndStatistics.points.map((point, index) => {
return {
x: trackPointsAndStatistics.statistics.distance[index],
y: point.getTemperature()
x: getConvertedDistance(trackPointsAndStatistics.statistics.distance[index]),
y: getConvertedTemperature(point.getTemperature())
};
}),
normalized: true,
@@ -277,7 +292,7 @@
label: datasets.power.label,
data: trackPointsAndStatistics.points.map((point, index) => {
return {
x: trackPointsAndStatistics.statistics.distance[index],
x: getConvertedDistance(trackPointsAndStatistics.statistics.distance[index]),
y: point.getPower()
};
}),
@@ -286,7 +301,8 @@
hidden: true
};
chart.options.scales.x['min'] = 0;
chart.options.scales.x['max'] = gpxFiles.statistics.distance.total;
chart.options.scales.x['max'] = getConvertedDistance(gpxFiles.statistics.distance.total);
chart.update();
}

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import Tooltip from '$lib/components/Tooltip.svelte';
import WithUnits from '$lib/components/WithUnits.svelte';
import { GPXStatistics } from 'gpx';
@@ -17,17 +18,6 @@
}
});
}
function toHHMMSS(seconds: number) {
var hours = Math.floor(seconds / 3600);
var minutes = Math.floor(seconds / 60) % 60;
var seconds = Math.round(seconds % 60);
return [hours, minutes, seconds]
.map((v) => (v < 10 ? '0' + v : v))
.filter((v, i) => v !== '00' || i > 0)
.join(':');
}
</script>
<Card.Root class="h-full overflow-hidden border-none min-w-48 pl-4">
@@ -35,30 +25,32 @@
<Tooltip>
<span slot="data" class="flex flex-row items-center">
<Ruler size="18" class="mr-1" />
{gpxData.distance.total.toFixed(2)} km
<WithUnits value={gpxData.distance.total} type="distance" />
</span>
<span slot="tooltip">Distance</span>
</Tooltip>
<Tooltip>
<span slot="data" class="flex flex-row items-center">
<MoveUpRight size="18" class="mr-1" />
{gpxData.elevation.gain.toFixed(0)} m
<WithUnits value={gpxData.elevation.gain} type="elevation" />
<MoveDownRight size="18" class="mx-1" />
{gpxData.elevation.loss.toFixed(0)} m
<WithUnits value={gpxData.elevation.loss} type="elevation" />
</span>
<span slot="tooltip">Elevation</span>
</Tooltip>
<Tooltip>
<span slot="data" class="flex flex-row items-center">
<Zap size="18" class="mr-1" />
{gpxData.speed.moving.toFixed(2)} km/h
<WithUnits value={gpxData.speed.moving} type="speed" />
</span>
<span slot="tooltip">Speed</span>
</Tooltip>
<Tooltip>
<span slot="data" class="flex flex-row items-center">
<Timer size="18" class="mr-1" />
{toHHMMSS(gpxData.time.moving)} / {toHHMMSS(gpxData.time.total)}
<WithUnits value={gpxData.time.moving} type="time" />
<span class="mx-1">/</span>
<WithUnits value={gpxData.time.total} type="time" />
</span>
<span slot="tooltip">Moving time / Total time</span>
</Tooltip>

View File

@@ -23,11 +23,12 @@
removeAllFiles,
removeSelectedFiles,
triggerFileInput,
selectFiles
selectFiles,
settings
} from '$lib/stores';
let distanceUnits = 'metric';
let velocityUnits = 'speed';
import { _ } from 'svelte-i18n';
let showDistanceMarkers = false;
let showDirectionMarkers = false;
</script>
@@ -39,10 +40,12 @@
<Logo class="h-5 mt-0.5 mx-2" />
<Menubar.Root class="border-none h-fit p-0">
<Menubar.Menu>
<Menubar.Trigger>File</Menubar.Trigger>
<Menubar.Trigger>{$_('menu.file')}</Menubar.Trigger>
<Menubar.Content>
<Menubar.Item>
<Plus size="16" class="mr-1" /> New <Menubar.Shortcut>⌘N</Menubar.Shortcut>
<Plus size="16" class="mr-1" />
{$_('menu.new')}
<Menubar.Shortcut>⌘N</Menubar.Shortcut>
</Menubar.Item>
<Menubar.Separator />
<Menubar.Item on:click={triggerFileInput}>
@@ -108,7 +111,7 @@
><Menubar.Sub>
<Menubar.SubTrigger inset>Distance units</Menubar.SubTrigger>
<Menubar.SubContent>
<Menubar.RadioGroup bind:value={distanceUnits}>
<Menubar.RadioGroup bind:value={$settings.distanceUnits}>
<Menubar.RadioItem value="metric">Metric</Menubar.RadioItem>
<Menubar.RadioItem value="imperial">Imperial</Menubar.RadioItem>
</Menubar.RadioGroup>
@@ -117,12 +120,21 @@
<Menubar.Sub>
<Menubar.SubTrigger inset>Velocity units</Menubar.SubTrigger>
<Menubar.SubContent>
<Menubar.RadioGroup bind:value={velocityUnits}>
<Menubar.RadioGroup bind:value={$settings.velocityUnits}>
<Menubar.RadioItem value="speed">Speed</Menubar.RadioItem>
<Menubar.RadioItem value="pace">Pace</Menubar.RadioItem>
</Menubar.RadioGroup>
</Menubar.SubContent>
</Menubar.Sub>
<Menubar.Sub>
<Menubar.SubTrigger inset>Temperature units</Menubar.SubTrigger>
<Menubar.SubContent>
<Menubar.RadioGroup bind:value={$settings.temperatureUnits}>
<Menubar.RadioItem value="celsius">Celsius</Menubar.RadioItem>
<Menubar.RadioItem value="fahrenheit">Fahrenheit</Menubar.RadioItem>
</Menubar.RadioGroup>
</Menubar.SubContent>
</Menubar.Sub>
<Menubar.Separator />
<Menubar.CheckboxItem bind:checked={showDistanceMarkers}>
Show distance markers

View File

@@ -0,0 +1,51 @@
<script lang="ts">
import { settings } from '$lib/stores';
import {
celsiusToFahrenheit,
distancePerHourToSecondsPerDistance,
kilometersToMiles,
metersToFeet,
secondsToHHMMSS
} from '$lib/units';
import { _ } from 'svelte-i18n';
export let value: number;
export let type: 'distance' | 'elevation' | 'speed' | 'temperature' | 'time';
</script>
{#if type === 'distance'}
{#if $settings.distanceUnits === 'metric'}
{value.toFixed(2)} {$_('units.kilometers')}
{:else}
{kilometersToMiles(value).toFixed(2)} {$_('units.miles')}
{/if}
{:else if type === 'elevation'}
{#if $settings.distanceUnits === 'metric'}
{value.toFixed(0)} {$_('units.meters')}
{:else}
{metersToFeet(value).toFixed(0)} {$_('units.feet')}
{/if}
{:else if type === 'speed'}
{#if $settings.distanceUnits === 'metric'}
{#if $settings.velocityUnits === 'speed'}
{value.toFixed(2)} {$_('units.kilometers_per_hour')}
{:else}
{secondsToHHMMSS(distancePerHourToSecondsPerDistance(value))}
{$_('units.minutes_per_kilometer')}
{/if}
{:else if $settings.velocityUnits === 'speed'}
{kilometersToMiles(value).toFixed(2)} {$_('units.miles_per_hour')}
{:else}
{secondsToHHMMSS(distancePerHourToSecondsPerDistance(kilometersToMiles(value)))}
{$_('units.minutes_per_mile')}
{/if}
{:else if type === 'temperature'}
{#if $settings.temperatureUnits === 'celsius'}
{value} {$_('units.celsius')}
{:else}
{celsiusToFahrenheit(value)} {$_('units.fahrenheit')}
{/if}
{:else if type === 'time'}
{secondsToHHMMSS(value)}
{/if}

View File

@@ -8,6 +8,11 @@ export const fileCollection = writable<GPXFiles>(new GPXFiles([]));
export const fileOrder = writable<GPXFile[]>([]);
export const selectedFiles = writable<Set<GPXFile>>(new Set());
export const selectFiles = writable<{ [key: string]: (file?: GPXFile) => void }>({});
export const settings = writable<{ [key: string]: any }>({
distanceUnits: 'metric',
velocityUnits: 'speed',
temperatureUnits: 'celsius',
});
export function addFile(file: GPXFile) {
fileCollection.update($files => {

144
website/src/lib/units.ts Normal file
View File

@@ -0,0 +1,144 @@
import { get } from 'svelte/store';
import { settings } from './stores';
import { _ } from 'svelte-i18n';
export function kilometersToMiles(value: number) {
return value * 0.621371;
}
export function metersToFeet(value: number) {
return value * 3.28084;
}
export function celsiusToFahrenheit(value: number) {
return value * 1.8 + 32;
}
export function distancePerHourToSecondsPerDistance(value: number) {
return 3600 / value;
}
export function secondsToHHMMSS(value: number) {
var hours = Math.floor(value / 3600);
var minutes = Math.floor(value / 60) % 60;
var seconds = Math.round(value % 60);
return [hours, minutes, seconds]
.map((v) => (v < 10 ? '0' + v : v))
.filter((v, i) => v !== '00' || i > 0)
.join(':');
}
// Get a string representation of the value with units
export function getDistanceWithUnits(value: number, convert: boolean = true) {
if (convert) {
return getConvertedDistance(value).toFixed(2) + ' ' + getDistanceUnits();
} else {
return value.toFixed(2) + ' ' + getDistanceUnits();
}
}
export function getVelocityWithUnits(value: number, convert: boolean = true) {
const velocityUnits = get(settings).velocityUnits;
const distanceUnits = get(settings).distanceUnits;
if (velocityUnits === 'speed') {
if (convert) {
return getConvertedVelocity(value).toFixed(2) + ' ' + getVelocityUnits();
} else {
return value.toFixed(2) + ' ' + getVelocityUnits();
}
} else {
if (convert) {
return secondsToHHMMSS(getConvertedVelocity(value));
} else {
return secondsToHHMMSS(value);
}
}
}
export function getElevationWithUnits(value: number, convert: boolean = true) {
if (convert) {
return getConvertedElevation(value).toFixed(0) + ' ' + getElevationUnits();
} else {
return value.toFixed(0) + ' ' + getElevationUnits();
}
}
export function getHeartRateWithUnits(value: number) {
return value.toFixed(0) + ' ' + getHeartRateUnits();
}
export function getCadenceWithUnits(value: number) {
return value.toFixed(0) + ' ' + getCadenceUnits();
}
export function getPowerWithUnits(value: number) {
return value.toFixed(0) + ' ' + getPowerUnits();
}
export function getTemperatureWithUnits(value: number, convert: boolean = true) {
if (convert) {
return getConvertedTemperature(value).toFixed(0) + ' ' + getTemperatureUnits();
} else {
return value.toFixed(0) + ' ' + getTemperatureUnits();
}
}
// Get the units
export function getDistanceUnits() {
return get(settings).distanceUnits === 'metric' ? get(_)('units.kilometers') : get(_)('units.miles');
}
export function getVelocityUnits() {
const velocityUnits = get(settings).velocityUnits;
const distanceUnits = get(settings).distanceUnits;
if (velocityUnits === 'speed') {
return distanceUnits === 'metric' ? get(_)('units.kilometers_per_hour') : get(_)('units.miles_per_hour');
} else {
return distanceUnits === 'metric' ? get(_)('units.minutes_per_kilometer') : get(_)('units.minutes_per_mile');
}
}
export function getElevationUnits() {
return get(settings).distanceUnits === 'metric' ? get(_)('units.meters') : get(_)('units.feet');
}
export function getHeartRateUnits() {
return get(_)('units.heartrate');
}
export function getCadenceUnits() {
return get(_)('units.cadence');
}
export function getPowerUnits() {
return get(_)('units.power');
}
export function getTemperatureUnits() {
return get(settings).temperatureUnits === 'celsius' ? get(_)('units.celsius') : get(_)('units.fahrenheit');
}
// Convert only the value
export function getConvertedDistance(value: number) {
return get(settings).distanceUnits === 'metric' ? value : kilometersToMiles(value);
}
export function getConvertedElevation(value: number) {
return get(settings).distanceUnits === 'metric' ? value : metersToFeet(value);
}
export function getConvertedVelocity(value: number) {
const velocityUnits = get(settings).velocityUnits;
const distanceUnits = get(settings).distanceUnits;
if (velocityUnits === 'speed') {
return distanceUnits === 'metric' ? value : kilometersToMiles(value);
} else {
return distanceUnits === 'metric' ? distancePerHourToSecondsPerDistance(value) : distancePerHourToSecondsPerDistance(kilometersToMiles(value));
}
}
export function getConvertedTemperature(value: number) {
return get(settings).temperatureUnits === 'celsius' ? value : celsiusToFahrenheit(value);
}

View File

@@ -0,0 +1,33 @@
{
"menu": {
"file": "File",
"new": "New"
},
"quantities": {
"distance": "Distance",
"elevation": "Elevation",
"temperature": "Temperature",
"speed": "Speed",
"pace": "Pace",
"heartrate": "Heart rate",
"cadence": "Cadence",
"power": "Power",
"slope": "Slope",
"surface": "Surface"
},
"units": {
"meters": "m",
"feet": "ft",
"kilometers": "km",
"miles": "mi",
"celsius": "°C",
"fahrenheit": "°F",
"kilometers_per_hour": "km/h",
"miles_per_hour": "mph",
"minutes_per_kilometer": "min/km",
"minutes_per_mile": "min/mi",
"heartrate": "bpm",
"cadence": "rpm",
"power": "W"
}
}

View File

@@ -0,0 +1,10 @@
export const prerender = true;
import { register, init } from 'svelte-i18n';
register('en', () => import('../locales/en.json'));
init({
fallbackLocale: 'en',
initialLocale: 'en',
});

View File

@@ -1 +0,0 @@
export const prerender = true;

View File

@@ -1,25 +1,5 @@
<script lang="ts">
import Data from '$lib/components/Data.svelte';
import ElevationProfile from '$lib/components/ElevationProfile.svelte';
import FileList from '$lib/components/FileList.svelte';
import GPXData from '$lib/components/GPXData.svelte';
import Map from '$lib/components/Map.svelte';
import Menu from '$lib/components/Menu.svelte';
import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
import LayerControl from '$lib/components/layer-control/LayerControl.svelte';
import App from '$lib/components/App.svelte';
</script>
<div class="flex flex-col w-screen h-screen">
<div class="grow relative">
<Menu />
<Toolbar />
<Map class="h-full" />
<LayerControl />
<Data />
<FileList />
</div>
<div class="h-60 flex flex-row gap-2 overflow-hidden border">
<GPXData />
<ElevationProfile />
</div>
</div>
<App />

View File

@@ -0,0 +1,10 @@
<script lang="ts">
import App from '$lib/components/App.svelte';
import { locale } from 'svelte-i18n';
import { page } from '$app/stores';
locale.set($page.params.language);
</script>
<App />