about page

This commit is contained in:
vcoppe
2024-07-05 16:08:16 +02:00
parent 9edcc5b55b
commit cb735db216
27 changed files with 44025 additions and 167 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -12,15 +12,30 @@
import { Toaster } from '$lib/components/ui/sonner'; import { Toaster } from '$lib/components/ui/sonner';
import { settings } from '$lib/db'; import { settings } from '$lib/db';
import { gpxStatistics, slicedGPXStatistics } from '$lib/stores';
const { verticalFileView, elevationProfile, bottomPanelSize, rightPanelSize } = settings; const {
verticalFileView,
elevationProfile,
bottomPanelSize,
rightPanelSize,
distanceUnits,
velocityUnits,
temperatureUnits,
additionalDatasets,
elevationFill
} = settings;
</script> </script>
<div class="fixed flex flex-row w-screen h-screen h-dvh"> <div class="fixed flex flex-row w-screen h-screen h-dvh">
<div class="flex flex-col grow h-full min-w-0"> <div class="flex flex-col grow h-full min-w-0">
<div class="grow relative"> <div class="grow relative">
<Menu /> <Menu />
<div
class="absolute top-0 bottom-0 left-0 z-20 flex flex-col justify-center pointer-events-none"
>
<Toolbar /> <Toolbar />
</div>
<Map class="h-full {$verticalFileView ? '' : 'horizontal'}" /> <Map class="h-full {$verticalFileView ? '' : 'horizontal'}" />
<StreetViewControl /> <StreetViewControl />
<LayerControl /> <LayerControl />
@@ -39,9 +54,25 @@
class="{$elevationProfile ? '' : 'h-10'} flex flex-row gap-2" class="{$elevationProfile ? '' : 'h-10'} flex flex-row gap-2"
style={$elevationProfile ? `height: ${$bottomPanelSize}px` : ''} style={$elevationProfile ? `height: ${$bottomPanelSize}px` : ''}
> >
<GPXStatistics /> <GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
panelSize={$bottomPanelSize}
orientation={$elevationProfile ? 'vertical' : 'horizontal'}
velocityUnits={$velocityUnits}
/>
{#if $elevationProfile} {#if $elevationProfile}
<ElevationProfile /> <ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
bind:additionalDatasets={$additionalDatasets}
bind:elevationFill={$elevationFill}
panelSize={$bottomPanelSize}
distanceUnits={$distanceUnits}
velocityUnits={$velocityUnits}
temperatureUnits={$temperatureUnits}
class="py-2 pr-4"
/>
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -3,8 +3,7 @@
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import Chart from 'chart.js/auto'; import Chart from 'chart.js/auto';
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import { map, gpxStatistics, slicedGPXStatistics } from '$lib/stores'; import { map } from '$lib/stores';
import { settings } from '$lib/db';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { import {
BrickWall, BrickWall,
@@ -37,8 +36,18 @@
getVelocityWithUnits, getVelocityWithUnits,
secondsToHHMMSS secondsToHHMMSS
} from '$lib/units'; } from '$lib/units';
import { get } from 'svelte/store'; import type { Writable } from 'svelte/store';
import { DateFormatter } from '@internationalized/date'; import { DateFormatter } from '@internationalized/date';
import type { GPXStatistics } from 'gpx';
export let gpxStatistics: Writable<GPXStatistics>;
export let slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
export let distanceUnits: 'metric' | 'imperial';
export let velocityUnits: 'speed' | 'pace';
export let temperatureUnits: 'celsius' | 'fahrenheit';
export let panelSize: number;
export let additionalDatasets: string[];
export let elevationFill: 'slope' | 'surface' | undefined;
let df: DateFormatter; let df: DateFormatter;
@@ -50,20 +59,19 @@
} }
let canvas: HTMLCanvasElement; let canvas: HTMLCanvasElement;
let showAdditionalScales = true;
let updateShowAdditionalScales = () => {
showAdditionalScales = canvas.width >= 1200;
};
let overlay: HTMLCanvasElement; let overlay: HTMLCanvasElement;
let chart: Chart; let chart: Chart;
Chart.defaults.font.family = 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 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; // Tailwind CSS font
let elevationFill: string;
let additionalDatasets: string[];
let marker: mapboxgl.Marker | null = null; let marker: mapboxgl.Marker | null = null;
let dragging = false; let dragging = false;
let { distanceUnits, velocityUnits, temperatureUnits, bottomPanelSize } = settings;
let options = { let options = {
animation: false, animation: false,
parsing: false, parsing: false,
@@ -127,7 +135,7 @@
} }
return `${$_('quantities.elevation')}: ${getElevationWithUnits(point.y, false)}`; return `${$_('quantities.elevation')}: ${getElevationWithUnits(point.y, false)}`;
} else if (context.datasetIndex === 1) { } else if (context.datasetIndex === 1) {
return `${$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')}: ${getVelocityWithUnits(point.y, false)}`; return `${velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')}: ${getVelocityWithUnits(point.y, false)}`;
} else if (context.datasetIndex === 2) { } else if (context.datasetIndex === 2) {
return `${$_('quantities.heartrate')}: ${getHeartRateWithUnits(point.y)}`; return `${$_('quantities.heartrate')}: ${getHeartRateWithUnits(point.y)}`;
} else if (context.datasetIndex === 3) { } else if (context.datasetIndex === 3) {
@@ -173,6 +181,7 @@
onResize: function (chart, size) { onResize: function (chart, size) {
overlay.width = size.width; overlay.width = size.width;
overlay.height = size.height; overlay.height = size.height;
updateShowAdditionalScales();
} }
}; };
@@ -185,7 +194,7 @@
} = { } = {
speed: { speed: {
id: 'speed', id: 'speed',
getLabel: () => ($velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')), getLabel: () => (velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')),
getUnits: () => getVelocityUnits() getUnits: () => getVelocityUnits()
}, },
hr: { hr: {
@@ -225,13 +234,13 @@
grid: { grid: {
display: false display: false
}, },
reverse: () => id === 'speed' && $velocityUnits === 'pace', reverse: () => id === 'speed' && velocityUnits === 'pace',
display: false display: false
}; };
} }
options.scales.yspeed['ticks'] = { options.scales.yspeed['ticks'] = {
callback: function (value: number) { callback: function (value: number) {
if ($velocityUnits === 'speed') { if (velocityUnits === 'speed') {
return value; return value;
} else { } else {
return secondsToHHMMSS(value); return secondsToHHMMSS(value);
@@ -268,6 +277,8 @@
element element
}); });
updateShowAdditionalScales();
// Overlay canvas to create a selection rectangle // Overlay canvas to create a selection rectangle
overlay.width = canvas.width; overlay.width = canvas.width;
overlay.height = canvas.height; overlay.height = canvas.height;
@@ -288,7 +299,7 @@
if (points.length === 0) { if (points.length === 0) {
return evt.x - rect.left <= chart.chartArea.left return evt.x - rect.left <= chart.chartArea.left
? 0 ? 0
: get(gpxStatistics).local.points.length - 1; : $gpxStatistics.local.points.length - 1;
} }
let point = points.find((point) => point.element.raw); let point = points.find((point) => point.element.raw);
if (point) { if (point) {
@@ -309,14 +320,11 @@
dragging = true; dragging = true;
endIndex = getIndex(evt); endIndex = getIndex(evt);
if (startIndex !== endIndex) { if (startIndex !== endIndex) {
slicedGPXStatistics.set([ $slicedGPXStatistics = [
get(gpxStatistics).slice( $gpxStatistics.slice(Math.min(startIndex, endIndex), Math.max(startIndex, endIndex)),
Math.min(startIndex, endIndex), Math.min(startIndex, endIndex),
Math.max(startIndex, endIndex) Math.max(startIndex, endIndex)
), ];
Math.min(startIndex, endIndex),
Math.max(startIndex, endIndex)
]);
} }
} }
} }
@@ -326,7 +334,7 @@
canvas.style.cursor = ''; canvas.style.cursor = '';
endIndex = getIndex(evt); endIndex = getIndex(evt);
if (startIndex === endIndex) { if (startIndex === endIndex) {
slicedGPXStatistics.set(undefined); $slicedGPXStatistics = undefined;
} }
} }
canvas.addEventListener('pointerdown', onMouseDown); canvas.addEventListener('pointerdown', onMouseDown);
@@ -334,7 +342,7 @@
canvas.addEventListener('pointerup', onMouseUp); canvas.addEventListener('pointerup', onMouseUp);
}); });
$: if (chart && $distanceUnits && $velocityUnits && $temperatureUnits) { $: if (chart && distanceUnits && velocityUnits && temperatureUnits) {
let data = $gpxStatistics; let data = $gpxStatistics;
// update data // update data
@@ -488,11 +496,12 @@
chart.data.datasets[4].hidden = !includeTemperature; chart.data.datasets[4].hidden = !includeTemperature;
chart.data.datasets[5].hidden = !includePower; chart.data.datasets[5].hidden = !includePower;
} }
chart.options.scales[`y${datasets.speed.id}`].display = includeSpeed; chart.options.scales[`y${datasets.speed.id}`].display = includeSpeed && showAdditionalScales;
chart.options.scales[`y${datasets.hr.id}`].display = includeHeartRate; chart.options.scales[`y${datasets.hr.id}`].display = includeHeartRate && showAdditionalScales;
chart.options.scales[`y${datasets.cad.id}`].display = includeCadence; chart.options.scales[`y${datasets.cad.id}`].display = includeCadence && showAdditionalScales;
chart.options.scales[`y${datasets.atemp.id}`].display = includeTemperature; chart.options.scales[`y${datasets.atemp.id}`].display =
chart.options.scales[`y${datasets.power.id}`].display = includePower; includeTemperature && showAdditionalScales;
chart.options.scales[`y${datasets.power.id}`].display = includePower && showAdditionalScales;
chart.update(); chart.update();
} }
@@ -507,10 +516,10 @@
selectionContext.clearRect(0, 0, overlay.width, overlay.height); selectionContext.clearRect(0, 0, overlay.width, overlay.height);
let startPixel = chart.scales.x.getPixelForValue( let startPixel = chart.scales.x.getPixelForValue(
getConvertedDistance(get(gpxStatistics).local.distance.total[startIndex]) getConvertedDistance($gpxStatistics.local.distance.total[startIndex])
); );
let endPixel = chart.scales.x.getPixelForValue( let endPixel = chart.scales.x.getPixelForValue(
getConvertedDistance(get(gpxStatistics).local.distance.total[endIndex]) getConvertedDistance($gpxStatistics.local.distance.total[endIndex])
); );
selectionContext.fillRect( selectionContext.fillRect(
@@ -534,17 +543,14 @@
}); });
</script> </script>
<div class="h-full grow min-w-0 flex flex-row gap-4 items-center py-2 pr-4"> <div class="h-full grow min-w-0 flex flex-row gap-4 items-center {$$props.class ?? ''}">
<div class="grow h-full min-w-0"> <div class="grow h-full min-w-0">
<canvas bind:this={overlay} class="absolute pointer-events-none"></canvas> <canvas bind:this={overlay} class="absolute pointer-events-none"></canvas>
<canvas bind:this={canvas} class="w-full h-full"></canvas> <canvas bind:this={canvas} class="w-full h-full"></canvas>
</div> </div>
<div <div class="h-full flex flex-col justify-center" style="width: {panelSize > 158 ? 22 : 42}px">
class="h-full flex flex-col justify-center"
style="width: {$bottomPanelSize > 158 ? 22 : 42}px"
>
<ToggleGroup.Root <ToggleGroup.Root
class="{$bottomPanelSize > 158 class="{panelSize > 158
? 'flex-col' ? 'flex-col'
: 'flex-row'} flex-wrap gap-0 min-h-0 content-center border rounded-t-md" : 'flex-row'} flex-wrap gap-0 min-h-0 content-center border rounded-t-md"
type="single" type="single"
@@ -564,7 +570,7 @@
</ToggleGroup.Item> </ToggleGroup.Item>
</ToggleGroup.Root> </ToggleGroup.Root>
<ToggleGroup.Root <ToggleGroup.Root
class="{$bottomPanelSize > 158 class="{panelSize > 158
? 'flex-col' ? 'flex-col'
: 'flex-row'} flex-wrap gap-0 min-h-0 content-center border rounded-b-md -mt-[1px]" : 'flex-row'} flex-wrap gap-0 min-h-0 content-center border rounded-b-md -mt-[1px]"
type="multiple" type="multiple"
@@ -574,7 +580,7 @@
<Tooltip side="left"> <Tooltip side="left">
<Zap slot="data" size="15" /> <Zap slot="data" size="15" />
<span slot="tooltip" <span slot="tooltip"
>{$velocityUnits === 'speed' ? $_('chart.show_speed') : $_('chart.show_pace')}</span >{velocityUnits === 'speed' ? $_('chart.show_speed') : $_('chart.show_pace')}</span
> >
</Tooltip> </Tooltip>
</ToggleGroup.Item> </ToggleGroup.Item>

View File

@@ -3,15 +3,17 @@
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import WithUnits from '$lib/components/WithUnits.svelte'; import WithUnits from '$lib/components/WithUnits.svelte';
import { gpxStatistics, slicedGPXStatistics } from '$lib/stores';
import { settings } from '$lib/db';
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from 'lucide-svelte'; import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from 'lucide-svelte';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import type { GPXStatistics } from 'gpx'; import type { GPXStatistics } from 'gpx';
import type { Writable } from 'svelte/store';
const { velocityUnits, elevationProfile, bottomPanelSize } = settings; export let gpxStatistics: Writable<GPXStatistics>;
export let slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
export let velocityUnits: 'speed' | 'pace';
export let orientation: 'horizontal' | 'vertical';
export let panelSize: number;
let statistics: GPXStatistics; let statistics: GPXStatistics;
@@ -23,14 +25,14 @@
</script> </script>
<Card.Root <Card.Root
class="h-full {$elevationProfile class="h-full {orientation === 'vertical'
? '' ? 'min-w-52'
: 'w-full pr-4'} overflow-hidden border-none shadow-none min-w-52 pl-4" : 'w-full pr-4'} border-none shadow-none pl-4"
> >
<Card.Content <Card.Content
class="h-full flex {$elevationProfile class="h-full flex {orientation === 'vertical'
? 'flex-col justify-center' ? 'flex-col justify-center'
: 'flex-row w-full justify-between'} flex-wrap gap-4 p-0" : 'flex-row w-full justify-between'} gap-4 p-0"
> >
<Tooltip> <Tooltip>
<span slot="data" class="flex flex-row items-center"> <span slot="data" class="flex flex-row items-center">
@@ -48,8 +50,8 @@
</span> </span>
<span slot="tooltip">{$_('quantities.elevation')}</span> <span slot="tooltip">{$_('quantities.elevation')}</span>
</Tooltip> </Tooltip>
{#if $bottomPanelSize > 120 || !$elevationProfile} {#if panelSize > 120 || orientation === 'horizontal'}
<Tooltip> <Tooltip class={orientation === 'horizontal' ? 'hidden xs:block' : ''}>
<span slot="data" class="flex flex-row items-center"> <span slot="data" class="flex flex-row items-center">
<Zap size="18" class="mr-1" /> <Zap size="18" class="mr-1" />
<WithUnits value={statistics.global.speed.moving} type="speed" showUnits={false} /> <WithUnits value={statistics.global.speed.moving} type="speed" showUnits={false} />
@@ -57,14 +59,14 @@
<WithUnits value={statistics.global.speed.total} type="speed" /> <WithUnits value={statistics.global.speed.total} type="speed" />
</span> </span>
<span slot="tooltip" <span slot="tooltip"
>{$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_( >{velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_(
'quantities.moving' 'quantities.moving'
)} / {$_('quantities.total')})</span )} / {$_('quantities.total')})</span
> >
</Tooltip> </Tooltip>
{/if} {/if}
{#if $bottomPanelSize > 160 || !$elevationProfile} {#if panelSize > 160 || orientation === 'horizontal'}
<Tooltip> <Tooltip class={orientation === 'horizontal' ? 'hidden md:block' : ''}>
<span slot="data" class="flex flex-row items-center"> <span slot="data" class="flex flex-row items-center">
<Timer size="18" class="mr-1" /> <Timer size="18" class="mr-1" />
<WithUnits value={statistics.global.time.moving} type="time" /> <WithUnits value={statistics.global.time.moving} type="time" />

View File

@@ -5,7 +5,7 @@
</script> </script>
<Tooltip.Root> <Tooltip.Root>
<Tooltip.Trigger> <Tooltip.Trigger {...$$restProps}>
<slot name="data" /> <slot name="data" />
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Content {side}> <Tooltip.Content {side}>

View File

@@ -15,7 +15,7 @@
</script> </script>
{#if module !== undefined} {#if module !== undefined}
<div class="markdown"> <div class="markdown space-y-3">
<svelte:component this={module} /> <svelte:component this={module} />
</div> </div>
{/if} {/if}
@@ -26,13 +26,13 @@
:global(.markdown h1) { :global(.markdown h1) {
@apply text-3xl; @apply text-3xl;
@apply font-bold; @apply font-semibold;
@apply mb-3; @apply mb-6;
} }
:global(.markdown h2) { :global(.markdown h2) {
@apply text-2xl; @apply text-2xl;
@apply font-bold; @apply font-semibold;
@apply mb-3; @apply mb-3;
} }
@@ -41,6 +41,11 @@
@apply hover:underline; @apply hover:underline;
} }
:global(.markdown p > a) {
@apply text-blue-500;
@apply hover:underline;
}
:global(.markdown ul) { :global(.markdown ul) {
@apply list-disc; @apply list-disc;
@apply pl-4; @apply pl-4;

View File

@@ -16,8 +16,7 @@
import ToolbarItemMenu from './ToolbarItemMenu.svelte'; import ToolbarItemMenu from './ToolbarItemMenu.svelte';
</script> </script>
<div class="absolute top-0 bottom-0 left-0 z-20 flex flex-col justify-center pointer-events-none"> <div class="flex flex-row w-full items-center">
<div class="flex flex-row w-screen items-center">
<div <div
class="h-fit flex flex-col p-1 gap-1.5 bg-background rounded-r-md pointer-events-auto shadow-md" class="h-fit flex flex-col p-1 gap-1.5 bg-background rounded-r-md pointer-events-auto shadow-md"
> >
@@ -56,4 +55,3 @@
</div> </div>
<ToolbarItemMenu /> <ToolbarItemMenu />
</div> </div>
</div>

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Tool, currentTool } from '$lib/stores'; import { Tool, currentTool } from '$lib/stores';
import { settings } from '$lib/db';
import { flyAndScale } from '$lib/utils'; import { flyAndScale } from '$lib/utils';
import * as Card from '$lib/components/ui/card'; import * as Card from '$lib/components/ui/card';
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte'; import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
@@ -14,6 +15,8 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
const { minimizeRoutingMenu } = settings;
let popupElement: HTMLElement; let popupElement: HTMLElement;
let popup: mapboxgl.Popup; let popup: mapboxgl.Popup;
@@ -36,7 +39,7 @@
<Card.Root class="rounded-md border-none"> <Card.Root class="rounded-md border-none">
<Card.Content class="p-2.5"> <Card.Content class="p-2.5">
{#if $currentTool === Tool.ROUTING} {#if $currentTool === Tool.ROUTING}
<Routing {popup} {popupElement} /> <Routing {popup} {popupElement} bind:minimized={$minimizeRoutingMenu} />
{:else if $currentTool === Tool.SCISSORS} {:else if $currentTool === Tool.SCISSORS}
<Scissors /> <Scissors />
{:else if $currentTool === Tool.WAYPOINT} {:else if $currentTool === Tool.WAYPOINT}

View File

@@ -36,13 +36,15 @@
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { TrackPoint } from 'gpx'; import { TrackPoint } from 'gpx';
export let popup: mapboxgl.Popup; export let minimized = false;
export let popupElement: HTMLElement; export let minimizable = true;
export let popup: mapboxgl.Popup | undefined = undefined;
export let popupElement: HTMLElement | undefined = undefined;
let selectedItem: ListItem | null = null; let selectedItem: ListItem | null = null;
const { minimizeRoutingMenu, privateRoads, routing } = settings; const { privateRoads, routing } = settings;
$: if ($map) { $: if ($map && popup && popupElement) {
// remove controls for deleted files // remove controls for deleted files
routingControls.forEach((controls, fileId) => { routingControls.forEach((controls, fileId) => {
if (!$fileObservers.has(fileId)) { if (!$fileObservers.has(fileId)) {
@@ -93,9 +95,9 @@
}); });
</script> </script>
{#if $minimizeRoutingMenu} {#if minimized}
<div class="-m-1.5 -mb-2"> <div class="-m-1.5 -mb-2">
<Button variant="ghost" class="px-1 h-[26px]" on:click={() => ($minimizeRoutingMenu = false)}> <Button variant="ghost" class="px-1 h-[26px]" on:click={() => (minimized = false)}>
<SquareArrowOutDownRight size="18" /> <SquareArrowOutDownRight size="18" />
</Button> </Button>
</div> </div>
@@ -207,9 +209,11 @@
<div>{$_('toolbar.routing.help')}</div> <div>{$_('toolbar.routing.help')}</div>
{/if} {/if}
</Help> </Help>
<Button variant="ghost" class="px-1 h-6" on:click={() => ($minimizeRoutingMenu = true)}> {#if minimizable}
<Button variant="ghost" class="px-1 h-6" on:click={() => (minimized = true)}>
<SquareArrowUpLeft size="18" /> <SquareArrowUpLeft size="18" />
</Button> </Button>
{/if}
</div> </div>
</div> </div>
{/if} {/if}

View File

@@ -82,9 +82,11 @@ function dexieUninitializedSettingStore(setting: string, initial: any): Writable
export const settings = { export const settings = {
distanceUnits: dexieSettingStore<'metric' | 'imperial'>('distanceUnits', 'metric'), distanceUnits: dexieSettingStore<'metric' | 'imperial'>('distanceUnits', 'metric'),
velocityUnits: dexieSettingStore('velocityUnits', 'speed'), velocityUnits: dexieSettingStore<'speed' | 'pace'>('velocityUnits', 'speed'),
temperatureUnits: dexieSettingStore('temperatureUnits', 'celsius'), temperatureUnits: dexieSettingStore<'celsius' | 'fahrenheit'>('temperatureUnits', 'celsius'),
elevationProfile: dexieSettingStore('elevationProfile', true), elevationProfile: dexieSettingStore('elevationProfile', true),
additionalDatasets: dexieSettingStore<string[]>('additionalDatasets', []),
elevationFill: dexieSettingStore<'slope' | 'surface' | undefined>('elevationFill', undefined),
verticalFileView: dexieSettingStore<boolean>('fileView', false), verticalFileView: dexieSettingStore<boolean>('fileView', false),
mode: dexieSettingStore('mode', (() => { mode: dexieSettingStore('mode', (() => {
let currentMode: string | undefined = get(mode); let currentMode: string | undefined = get(mode);

View File

@@ -1,7 +1,7 @@
# Help keep the website free (and ad-free) 🙏 # Help keep the website free (and ad-free) 🙏
Each time you add or move a track point, we make a request to our servers to retrieve a route on the road network. Each time you add or move GPS points, we make a request to our servers to retrieve a route on the road network.
We also rely on APIs from [Mapbox](https://mapbox.com) to load beautiful maps, retrieve elevation data and process geocoding requests (looking for a place in the search bar). We also rely on APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to load beautiful maps, retrieve elevation data and process geocoding requests (looking for a place in the search bar).
Unfortunately this is very costly so if you like the tool and use it frequently, please consider making even a small donation so that this website can stay **free** and **ad-free**. Unfortunately this is very costly so if you like the tool and use it frequently, please consider making even a small donation so that this website can stay **free** and **ad-free**.
Thank you very much for your support! ❤️ Thank you very much for your support! ❤️

View File

@@ -1,4 +1,5 @@
[Mapbox](https://mapbox.com) is the company that provides some of the beautiful maps on this website. Mapbox is the company that provides some of the beautiful maps on this website.
They also develop the [map engine](https://github.com/mapbox/mapbox-gl-js) which powers *gpx.studio*. They also develop the <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">map engine</a> which powers *gpx.studio*.
We are incredibly lucky and grateful to have joined their [Community](https://www.mapbox.com/community) program, which supports nonprofits, educational institutions, and positive impact organizations.
We are incredibly lucky and grateful to have joined their <a href="https://mapbox.com/community" target="_blank">Community</a> program, which supports nonprofits, educational institutions, and positive impact organizations.
This means that *gpx.studio* can benefit from Mapbox tools at discounted prices, which greatly contributes to the financial viability of the project and to offering the best possible user experience. This means that *gpx.studio* can benefit from Mapbox tools at discounted prices, which greatly contributes to the financial viability of the project and to offering the best possible user experience.

View File

@@ -0,0 +1,3 @@
# Global and local maps
A large collection of basemaps and overlays to help you craft your next outdoor adventure, or visualize your latest achievement.

View File

@@ -0,0 +1,3 @@
# Data visualization
An interactive elevation profile with detailed statistics to analyze recorded activities and future objectives.

View File

@@ -0,0 +1,3 @@
# Route planning
An intuitive interface to create itineraries tailored to each sport, based on <a href="https://www.openstreetmap.org" target="_blank">OpenStreetMap</a> data.

View File

@@ -0,0 +1,3 @@
# Advanced file processing tools
A suite of tools for performing all common file processing tasks, and which can be applied to multiple files at once.

View File

@@ -1,6 +1,7 @@
# Translation 🗣 # Translation 🗣
The website is translated by volunteers on a collaborative translation platform. The website is translated by volunteers on a collaborative translation platform.
You can help complete and improve the translations by joining the [Crowdin project](https://crowdin.com/project/gpxstudio). You can help complete and improve the translations by joining the <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
[Get in touch](#contact) if you would like to start the translation in a new language. [Get in touch](#contact) if you would like to start the translation in a new language.
Any help is greatly appreciated! Any help is greatly appreciated!

View File

@@ -2,7 +2,7 @@
"metadata": { "metadata": {
"app_title": "the online GPX file editor", "app_title": "the online GPX file editor",
"about_title": "about", "about_title": "about",
"description": "View, edit and create GPX files online with advanced route planning capabilities, file processing tools and beautiful maps." "description": "View, edit and create GPX files online with advanced route planning capabilities and file processing tools, beautiful maps and detailed data visualizations."
}, },
"menu": { "menu": {
"new": "New", "new": "New",

View File

@@ -3,10 +3,34 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import DocsLoader from '$lib/components/docs/DocsLoader.svelte'; import DocsLoader from '$lib/components/docs/DocsLoader.svelte';
import Logo from '$lib/components/Logo.svelte'; import Logo from '$lib/components/Logo.svelte';
import ElevationProfile from '$lib/components/ElevationProfile.svelte';
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
import { languages } from '$lib/languages'; import { languages } from '$lib/languages';
import { Heart } from 'lucide-svelte'; import { settings } from '$lib/db';
import { mode } from 'mode-watcher'; import { BookOpenText, Heart, Map } from 'lucide-svelte';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import { exampleGPXFile } from '$lib/assets/example';
import { writable } from 'svelte/store';
import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
import { currentTool, Tool } from '$lib/stores';
import { onDestroy, onMount } from 'svelte';
let gpxStatistics = writable(exampleGPXFile.getStatistics());
let slicedGPXStatistics = writable(undefined);
let additionalDatasets = writable(['speed', 'atemp']);
let elevationFill = writable<'slope' | 'surface' | undefined>(undefined);
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
onMount(() => {
currentTool.set(Tool.SCISSORS);
});
onDestroy(() => {
currentTool.set(null);
});
</script> </script>
<svelte:head> <svelte:head>
@@ -27,45 +51,136 @@
{/each} {/each}
</svelte:head> </svelte:head>
<div> <div class="flex flex-col gap-y-24 my-24">
<div class="p-12">TODO hero section</div> <div class="px-12 w-full flex flex-col items-center">
<div class="p-12"> <div class="flex flex-col gap-6 items-center max-w-3xl">
<div>TODO show toolbar, advanced route planning and file editing tools</div> <div class="text-6xl font-black text-center">{$_('metadata.app_title')}</div>
<div class="text-xl text-muted-foreground text-center">
{$_('metadata.description')}
</div>
<div class="w-full flex flex-row justify-center gap-3">
<Button href="./" class="w-1/3 min-w-fit">
<Map size="18" class="mr-1.5" />
{$_('homepage.app')}
</Button>
<Button variant="secondary" href="./documentation" class="w-1/3 min-w-fit">
<BookOpenText size="18" class="mr-1.5" />
<span>{$_('homepage.documentation')}</span>
</Button>
</div>
</div>
</div>
<div class="px-24 w-full flex flex-col items-center">
<div class="flex flex-col md:flex-row gap-x-12 gap-y-6 items-center justify-between max-w-5xl">
<div class="text-center [&>*>p]:text-muted-foreground">
<DocsLoader path="about/routing.md" />
</div>
<div class="p-3 w-fit rounded-md border shadow-xl">
<Routing minimizable={false} />
</div>
</div>
</div>
<div class="px-24 w-full flex flex-col items-center">
<div class="flex flex-col md:flex-row gap-x-12 gap-y-6 items-center justify-between max-w-5xl">
<div class="text-center md:hidden [&>*>p]:text-muted-foreground">
<DocsLoader path="about/tools.md" />
</div> </div>
<div class="relative"> <div class="relative">
<img src="{base}/map.png" alt="Screenshot of the gpx.studio app." class="w-full" /> <Toolbar />
</div>
<div class="text-center hidden md:block [&>*>p]:text-muted-foreground">
<DocsLoader path="about/tools.md" />
</div>
</div>
</div>
<div class="px-24 w-full flex flex-col items-center">
<div
class="markdown flex flex-col md:flex-row gap-x-12 gap-y-6 items-center justify-between max-w-5xl"
>
<div class="text-center [&>*>p]:text-muted-foreground">
<DocsLoader path="about/maps.md" />
</div>
<div
class="relative grow min-w-1/2 min-h-96 aspect-square rounded-2xl shadow-xl overflow-clip"
>
<img
src="{base}/mapbox-outdoors.png"
alt="Mapbox Outdoors map screenshot."
class="absolute"
style="clip-path: inset(0 50% 50% 0);"
/>
<img
src="{base}/mapbox-satellite.png"
alt="Mapbox Satellite map screenshot."
class="absolute"
style="clip-path: inset(0 0 50% 50%);"
/>
<img
src="{base}/ign.png"
alt="IGN map screenshot."
class="absolute"
style="clip-path: inset(50% 50% 0 0);"
/>
<img
src="{base}/cyclosm.png"
alt="CyclOSM map screenshot."
class="absolute"
style="clip-path: inset(50% 0 0 50%);"
/>
<img src="{base}/waymarked.png" alt="Waymarked Trails map screenshot." class="absolute" />
</div>
</div>
</div>
<div class="p-6 md:p-12">
<div class="text-center mb-6 [&>*>p]:text-muted-foreground">
<DocsLoader path="about/plot.md" />
</div>
<div class="flex flex-col items-center">
<div class="h-48 w-full">
<ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
additionalDatasets={$additionalDatasets}
elevationFill={$elevationFill}
panelSize={200}
distanceUnits={$distanceUnits}
velocityUnits={$velocityUnits}
temperatureUnits={$temperatureUnits}
/>
</div>
<div class="h-10 w-fit">
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
panelSize={192}
orientation={'horizontal'}
velocityUnits={$velocityUnits}
/>
</div>
</div>
</div>
<div class="relative">
<img src="{base}/map.png" alt="Screenshot of the gpx.studio map in 3D." class="w-full" />
<div <div
class="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-background via-transparent to-background" class="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-background via-transparent to-background"
/> />
</div> </div>
<div class="p-12"> <div class="px-12 flex flex-col items-center gap-6">
<div>TODO elevation profile (use component?)</div>
<img
src="{base}/profile-{$mode ?? 'light'}.png"
alt="Screenshot of an elevation profile in the gpx.studio app."
class="w-full"
/>
</div>
<div class="p-12">
<div>TODO show several squares with different basemaps of the same place</div>
</div>
<div class="p-12 flex flex-col gap-12">
<div class="flex flex-col items-center gap-6">
<DocsLoader path="about/funding.md" /> <DocsLoader path="about/funding.md" />
<Button <Button
href="https://ko-fi.com/gpxstudio" href="https://ko-fi.com/gpxstudio"
target="_blank" target="_blank"
class="w-1/3 min-w-fit bg-support text-base" class="w-1/3 min-w-fit bg-support text-base"
> >
<Heart size="16" class="ml-1 mr-1" fill="rgb(var(--support))" /> <Heart size="16" class="mr-1" fill="rgb(var(--support))" />
<span>{$_('homepage.support_button')}</span> <span>{$_('homepage.support_button')}</span>
</Button> </Button>
</div> </div>
<div class="flex flex-col items-center gap-6"> <div class="px-12">
<DocsLoader path="about/translation.md" /> <DocsLoader path="about/translation.md" />
</div> </div>
<div <div
class="flex flex-col md:flex-row items-center justify-center gap-x-12 gap-y-6 p-6 mx-6 border rounded-md shadow-lg" class="mx-24 flex flex-col md:flex-row items-center justify-center gap-x-12 gap-y-6 p-6 border rounded-md shadow-xl"
> >
<div class="shrink-0 flex flex-col sm:flex-row md:flex-col items-center gap-x-4 gap-y-2"> <div class="shrink-0 flex flex-col sm:flex-row md:flex-col items-center gap-x-4 gap-y-2">
<div class="text-lg font-semibold text-muted-foreground"> <div class="text-lg font-semibold text-muted-foreground">
@@ -78,4 +193,3 @@
<DocsLoader path="about/mapbox.md" /> <DocsLoader path="about/mapbox.md" />
</div> </div>
</div> </div>
</div>

BIN
website/static/cyclosm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

BIN
website/static/ign.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

View File

@@ -6,13 +6,6 @@ const config = {
content: ["./src/**/*.{html,js,svelte,ts}"], content: ["./src/**/*.{html,js,svelte,ts}"],
safelist: ["dark"], safelist: ["dark"],
theme: { theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px"
}
},
extend: { extend: {
colors: { colors: {
border: "hsl(var(--border) / <alpha-value>)", border: "hsl(var(--border) / <alpha-value>)",
@@ -57,6 +50,9 @@ const config = {
}, },
fontFamily: { fontFamily: {
sans: [...fontFamily.sans] sans: [...fontFamily.sans]
},
screens: {
"xs": "500px",
} }
} }
}, },