12 Commits

Author SHA1 Message Date
vcoppe
01240c4f2a fix spacing 2025-11-10 16:47:16 +01:00
vcoppe
431a9ce827 migrate component 2025-11-10 16:45:50 +01:00
vcoppe
20ab41c3b4 update lucid icon name 2025-11-10 16:23:35 +01:00
vcoppe
3f4ea27be2 update gitignore 2025-11-10 16:07:06 +01:00
vcoppe
5bb5b2f8c8 fix destroy 2025-11-10 16:03:03 +01:00
vcoppe
e9bb9e27bb fix spacing 2025-11-10 16:02:54 +01:00
vcoppe
ee1dd1fae7 migrate component 2025-11-10 15:47:43 +01:00
vcoppe
738530a960 remove active layers when removed from selection 2025-11-10 15:26:12 +01:00
vcoppe
16023b0688 fix some typescript errors 2025-11-10 13:11:44 +01:00
vcoppe
bce7b5984f fix footer spacing 2025-11-10 11:56:28 +01:00
vcoppe
4e5d7d391a small style fixes 2025-11-10 11:51:16 +01:00
vcoppe
0554a85e01 fix toolbar animation 2025-11-10 11:11:37 +01:00
41 changed files with 241 additions and 151 deletions

2
website/.gitignore vendored
View File

@@ -8,3 +8,5 @@ node_modules
!.env.example !.env.example
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
static/*.webmanifest
!static/en.manifest.webmanifest

View File

@@ -186,8 +186,8 @@ export const basemaps: { [key: string]: string | StyleSpecification } = {
}, },
], ],
}, },
ignFrPlan: ignFrPlan, ignFrPlan: ignFrPlan as StyleSpecification,
ignFrTopo: ignFrTopo, ignFrTopo: ignFrTopo as StyleSpecification,
ignFrScan25: { ignFrScan25: {
version: 8, version: 8,
sources: { sources: {
@@ -209,7 +209,7 @@ export const basemaps: { [key: string]: string | StyleSpecification } = {
}, },
], ],
}, },
ignFrSatellite: ignFrSatellite, ignFrSatellite: ignFrSatellite as StyleSpecification,
ignEs: { ignEs: {
version: 8, version: 8,
sources: { sources: {
@@ -366,7 +366,7 @@ export const overlays: { [key: string]: string | StyleSpecification } = {
}, },
], ],
}, },
bikerouterGravel: bikerouterGravel, bikerouterGravel: bikerouterGravel as StyleSpecification,
swisstopoSlope: { swisstopoSlope: {
version: 8, version: 8,
sources: { sources: {

View File

@@ -12,7 +12,7 @@ import {
DoorOpen, DoorOpen,
Trees, Trees,
Fuel, Fuel,
Home, House,
Info, Info,
TreeDeciduous, TreeDeciduous,
CircleParking, CircleParking,
@@ -44,7 +44,7 @@ import {
DoorOpen as DoorOpenSvg, DoorOpen as DoorOpenSvg,
Trees as TreesSvg, Trees as TreesSvg,
Fuel as FuelSvg, Fuel as FuelSvg,
Home as HomeSvg, House as HouseSvg,
Info as InfoSvg, Info as InfoSvg,
TreeDeciduous as TreeDeciduousSvg, TreeDeciduous as TreeDeciduousSvg,
CircleParking as CircleParkingSvg, CircleParking as CircleParkingSvg,
@@ -95,7 +95,7 @@ export const symbols: { [key: string]: Symbol } = {
}, },
drinking_water: { value: 'Drinking Water', icon: Droplet, iconSvg: DropletSvg }, drinking_water: { value: 'Drinking Water', icon: Droplet, iconSvg: DropletSvg },
exit: { value: 'Exit', icon: DoorOpen, iconSvg: DoorOpenSvg }, exit: { value: 'Exit', icon: DoorOpen, iconSvg: DoorOpenSvg },
lodge: { value: 'Lodge', icon: Home, iconSvg: HomeSvg }, lodge: { value: 'Lodge', icon: House, iconSvg: HouseSvg },
lodging: { value: 'Lodging', icon: Bed, iconSvg: BedSvg }, lodging: { value: 'Lodging', icon: Bed, iconSvg: BedSvg },
forest: { value: 'Forest', icon: Trees, iconSvg: TreesSvg }, forest: { value: 'Forest', icon: Trees, iconSvg: TreesSvg },
gas_station: { value: 'Gas Station', icon: Fuel, iconSvg: FuelSvg }, gas_station: { value: 'Gas Station', icon: Fuel, iconSvg: FuelSvg },
@@ -105,7 +105,7 @@ export const symbols: { [key: string]: Symbol } = {
iconSvg: TrainFrontSvg, iconSvg: TrainFrontSvg,
}, },
hotel: { value: 'Hotel', icon: Bed, iconSvg: BedSvg }, hotel: { value: 'Hotel', icon: Bed, iconSvg: BedSvg },
house: { value: 'House', icon: Home, iconSvg: HomeSvg }, house: { value: 'House', icon: House, iconSvg: HouseSvg },
information: { value: 'Information', icon: Info, iconSvg: InfoSvg }, information: { value: 'Information', icon: Info, iconSvg: InfoSvg },
park: { value: 'Park', icon: TreeDeciduous, iconSvg: TreeDeciduousSvg }, park: { value: 'Park', icon: TreeDeciduous, iconSvg: TreeDeciduousSvg },
parking_area: { value: 'Parking Area', icon: CircleParking, iconSvg: CircleParkingSvg }, parking_area: { value: 'Parking Area', icon: CircleParking, iconSvg: CircleParkingSvg },

View File

@@ -2,7 +2,7 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import LanguageSelect from '$lib/components/LanguageSelect.svelte'; import LanguageSelect from '$lib/components/LanguageSelect.svelte';
import Logo from '$lib/components/Logo.svelte'; import Logo from '$lib/components/Logo.svelte';
import { AtSign, BookOpenText, Heart, Home, Map } from '@lucide/svelte'; import { AtSign, BookOpenText, Heart, House, Map } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
</script> </script>
@@ -14,7 +14,7 @@
<Logo class="h-8" width="153" /> <Logo class="h-8" width="153" />
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://github.com/gpxstudio/gpx.studio/blob/main/LICENSE" href="https://github.com/gpxstudio/gpx.studio/blob/main/LICENSE"
target="_blank" target="_blank"
> >
@@ -27,15 +27,15 @@
<span class="font-semibold">{i18n._('homepage.website')}</span> <span class="font-semibold">{i18n._('homepage.website')}</span>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href={getURLForLanguage(i18n.lang, '/')} href={getURLForLanguage(i18n.lang, '/')}
> >
<Home size="16" /> <House size="16" />
{i18n._('homepage.home')} {i18n._('homepage.home')}
</Button> </Button>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href={getURLForLanguage(i18n.lang, '/app')} href={getURLForLanguage(i18n.lang, '/app')}
> >
<Map size="16" /> <Map size="16" />
@@ -43,7 +43,7 @@
</Button> </Button>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href={getURLForLanguage(i18n.lang, '/help')} href={getURLForLanguage(i18n.lang, '/help')}
> >
<BookOpenText size="16" /> <BookOpenText size="16" />
@@ -54,7 +54,7 @@
<span class="font-semibold">{i18n._('homepage.contact')}</span> <span class="font-semibold">{i18n._('homepage.contact')}</span>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://www.reddit.com/r/gpxstudio/" href="https://www.reddit.com/r/gpxstudio/"
target="_blank" target="_blank"
> >
@@ -63,7 +63,7 @@
</Button> </Button>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://facebook.com/gpx.studio" href="https://facebook.com/gpx.studio"
target="_blank" target="_blank"
> >
@@ -72,7 +72,7 @@
</Button> </Button>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://x.com/gpxstudio" href="https://x.com/gpxstudio"
target="_blank" target="_blank"
> >
@@ -81,7 +81,7 @@
</Button> </Button>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="mailto:hello@gpx.studio" href="mailto:hello@gpx.studio"
target="_blank" target="_blank"
> >
@@ -93,7 +93,7 @@
<span class="font-semibold">{i18n._('homepage.contribute')}</span> <span class="font-semibold">{i18n._('homepage.contribute')}</span>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://ko-fi.com/gpxstudio" href="https://ko-fi.com/gpxstudio"
target="_blank" target="_blank"
> >
@@ -102,7 +102,7 @@
</Button> </Button>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://crowdin.com/project/gpxstudio" href="https://crowdin.com/project/gpxstudio"
target="_blank" target="_blank"
> >
@@ -111,7 +111,7 @@
</Button> </Button>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://github.com/gpxstudio/gpx.studio" href="https://github.com/gpxstudio/gpx.studio"
target="_blank" target="_blank"
> >

View File

@@ -5,10 +5,16 @@
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { Languages } from '@lucide/svelte'; import { Languages } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
let {
class: className = '',
}: {
class?: string;
} = $props();
</script> </script>
<Select.Root type="single" value={i18n.lang}> <Select.Root type="single" value={i18n.lang}>
<Select.Trigger class="w-[180px] {$$props.class ?? ''}" aria-label={i18n._('menu.language')}> <Select.Trigger class="w-[180px] {className}" aria-label={i18n._('menu.language')}>
<Languages size="16" /> <Languages size="16" />
<span class="ml-2 mr-auto"> <span class="ml-2 mr-auto">
{languages[i18n.lang]} {languages[i18n.lang]}

View File

@@ -1,29 +1,36 @@
<script lang="ts"> <script lang="ts">
import { base } from '$app/paths';
import { mode } from 'mode-watcher'; import { mode } from 'mode-watcher';
import { base } from '$app/paths';
export let iconOnly = false; let {
export let company = 'gpx.studio'; iconOnly = false,
company = 'gpx.studio',
...others
}: {
iconOnly?: boolean;
company?: 'gpx.studio' | 'mapbox' | 'github' | 'crowdin' | 'facebook' | 'x' | 'reddit';
[key: string]: any;
} = $props();
</script> </script>
{#if company === 'gpx.studio'} {#if company === 'gpx.studio'}
<img <img
src="{base}/{iconOnly ? 'icon' : 'logo'}{mode.current === 'dark' ? '-dark' : ''}.svg" src="{base}/{iconOnly ? 'icon' : 'logo'}{mode.current === 'dark' ? '-dark' : ''}.svg"
alt="Logo of gpx.studio." alt="Logo of gpx.studio."
{...$$restProps} {...others}
/> />
{:else if company === 'mapbox'} {:else if company === 'mapbox'}
<img <img
src="{base}/mapbox-logo-{mode.current === 'dark' ? 'white' : 'black'}.svg" src="{base}/mapbox-logo-{mode.current === 'dark' ? 'white' : 'black'}.svg"
alt="Logo of Mapbox." alt="Logo of Mapbox."
{...$$restProps} {...others}
/> />
{:else if company === 'github'} {:else if company === 'github'}
<svg <svg
role="img" role="img"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}" class="fill-foreground {others.class ?? ''}"
><title>GitHub</title><path ><title>GitHub</title><path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
/></svg /></svg
@@ -33,7 +40,7 @@
role="img" role="img"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}" class="fill-foreground {others.class ?? ''}"
><title>Crowdin</title><path ><title>Crowdin</title><path
d="M16.119 17.793a2.619 2.619 0 0 1-1.667-.562c-.546-.436-1.004-1.09-1.018-1.858-.008-.388.414-.388.414-.388l1.018-.008c.332.008.43.47.445.586.128 1.04.717 1.495 1.168 1.702.273.123.204.513-.362.528zm-5.695-5.287L8.5 12.252c-.867-.214-.844-.982-.807-1.247a5.119 5.119 0 0 1 .814-2.125c.545-.804 1.303-1.508 2.29-2.073 1.856-1.074 4.45-1.673 7.31-1.673 2.09 0 4.256.27 4.29.27.197.025.328.213.333.437a.377.377 0 0 1-.355.393l-.92-.01c-2.902 0-4.968.394-6.506 1.248-1.527.837-2.57 2.117-3.287 4.012-.076.163-.335 1.12-1.24 1.022zm2.533 7.823c-1.44 0-2.797-.622-3.825-1.746-.87-.96-1.397-1.931-1.493-3.164-.06-.813.3-1.094.788-1.044l1.988.218c.45.092.75.34.825.854.397 2.736 2.122 3.814 3.15 4.046.18.042.292.157.283.365a.412.412 0 0 1-.322.398c-.458.074-.936.073-1.394.073zm-4.101 2.418a14.216 14.216 0 0 1-2.307-.214c-1.202-.214-2.208-.582-3.072-1.13C1.41 20.095.163 17.786.014 15.048c-.037-.65-.11-1.89 1.427-1.797.638.033 1.653.343 2.368.548.887.247 1.314.933 1.314 1.608 0 3.858 3.494 6.408 5.02 6.408.654 0 .414.701.127.779-.502.136-1.15.153-1.413.153zM3.525 11.419c-.605-.109-1.194-.358-1.768-.5C-.018 10.479.284 8.688.45 8.196c1.617-4.757 6.746-6.35 10.887-6.773 3.898-.4 7.978-.092 11.778.967.31.083 1.269.327.718.891-.35.358-1.7-.016-2.073-.041-2.23-.167-4.434-.192-6.656.15-2.349.357-4.768 1.099-6.71 2.665-.938.758-1.76 1.723-2.313 2.866-.144.3-.256.6-.354.9-.11.327-.47 1.91-2.215 1.6zm9.94.917c.332-1.488 1.81-3.848 6.385-3.686 1.05.033.57.749.052.731-2.586-.09-3.815 1.578-4.457 3.27-.219.546-.68.626-1.271.53-.415-.074-.866-.123-.71-.846Z" d="M16.119 17.793a2.619 2.619 0 0 1-1.667-.562c-.546-.436-1.004-1.09-1.018-1.858-.008-.388.414-.388.414-.388l1.018-.008c.332.008.43.47.445.586.128 1.04.717 1.495 1.168 1.702.273.123.204.513-.362.528zm-5.695-5.287L8.5 12.252c-.867-.214-.844-.982-.807-1.247a5.119 5.119 0 0 1 .814-2.125c.545-.804 1.303-1.508 2.29-2.073 1.856-1.074 4.45-1.673 7.31-1.673 2.09 0 4.256.27 4.29.27.197.025.328.213.333.437a.377.377 0 0 1-.355.393l-.92-.01c-2.902 0-4.968.394-6.506 1.248-1.527.837-2.57 2.117-3.287 4.012-.076.163-.335 1.12-1.24 1.022zm2.533 7.823c-1.44 0-2.797-.622-3.825-1.746-.87-.96-1.397-1.931-1.493-3.164-.06-.813.3-1.094.788-1.044l1.988.218c.45.092.75.34.825.854.397 2.736 2.122 3.814 3.15 4.046.18.042.292.157.283.365a.412.412 0 0 1-.322.398c-.458.074-.936.073-1.394.073zm-4.101 2.418a14.216 14.216 0 0 1-2.307-.214c-1.202-.214-2.208-.582-3.072-1.13C1.41 20.095.163 17.786.014 15.048c-.037-.65-.11-1.89 1.427-1.797.638.033 1.653.343 2.368.548.887.247 1.314.933 1.314 1.608 0 3.858 3.494 6.408 5.02 6.408.654 0 .414.701.127.779-.502.136-1.15.153-1.413.153zM3.525 11.419c-.605-.109-1.194-.358-1.768-.5C-.018 10.479.284 8.688.45 8.196c1.617-4.757 6.746-6.35 10.887-6.773 3.898-.4 7.978-.092 11.778.967.31.083 1.269.327.718.891-.35.358-1.7-.016-2.073-.041-2.23-.167-4.434-.192-6.656.15-2.349.357-4.768 1.099-6.71 2.665-.938.758-1.76 1.723-2.313 2.866-.144.3-.256.6-.354.9-.11.327-.47 1.91-2.215 1.6zm9.94.917c.332-1.488 1.81-3.848 6.385-3.686 1.05.033.57.749.052.731-2.586-.09-3.815 1.578-4.457 3.27-.219.546-.68.626-1.271.53-.415-.074-.866-.123-.71-.846Z"
/></svg /></svg
@@ -43,7 +50,7 @@
role="img" role="img"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}" class="fill-foreground {others.class ?? ''}"
><title>Facebook</title><path ><title>Facebook</title><path
d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z" d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"
/></svg /></svg
@@ -53,7 +60,7 @@
role="img" role="img"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}" class="fill-foreground {others.class ?? ''}"
><title>X</title><path ><title>X</title><path
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/></svg /></svg
@@ -63,7 +70,7 @@
role="img" role="img"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}" class="fill-foreground {others.class ?? ''}"
><title>Reddit</title><path ><title>Reddit</title><path
d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z" d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z"
/></svg /></svg

View File

@@ -319,7 +319,7 @@
$copied.length === 0 || $copied.length === 0 ||
($selection.size > 0 && ($selection.size > 0 &&
!allowedPastes[$copied[0].level].includes( !allowedPastes[$copied[0].level].includes(
$selection.getSelected().pop()?.level $selection.getSelected().pop()!.level
))} ))}
onclick={pasteSelection} onclick={pasteSelection}
> >
@@ -659,7 +659,7 @@
on:dragover={(e) => e.preventDefault()} on:dragover={(e) => e.preventDefault()}
on:drop={(e) => { on:drop={(e) => {
e.preventDefault(); e.preventDefault();
if (e.dataTransfer.files.length > 0) { if (e.dataTransfer && e.dataTransfer.files.length > 0) {
loadFiles(e.dataTransfer.files); loadFiles(e.dataTransfer.files);
} }
}} }}

View File

@@ -3,11 +3,18 @@
import { Moon, Sun } from '@lucide/svelte'; import { Moon, Sun } from '@lucide/svelte';
import { mode, setMode } from 'mode-watcher'; import { mode, setMode } from 'mode-watcher';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
let {
class: className = '',
}: {
class?: string;
} = $props();
</script> </script>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
class={className}
onclick={() => { onclick={() => {
setMode(mode.current === 'light' ? 'dark' : 'light'); setMode(mode.current === 'light' ? 'dark' : 'light');
}} }}

View File

@@ -3,7 +3,7 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import AlgoliaDocSearch from '$lib/components/AlgoliaDocSearch.svelte'; import AlgoliaDocSearch from '$lib/components/AlgoliaDocSearch.svelte';
import ModeSwitch from '$lib/components/ModeSwitch.svelte'; import ModeSwitch from '$lib/components/ModeSwitch.svelte';
import { BookOpenText, Home, Map } from '@lucide/svelte'; import { BookOpenText, House, Map } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
</script> </script>
@@ -14,19 +14,31 @@
<Logo class="h-8 sm:hidden" iconOnly={true} width="26" /> <Logo class="h-8 sm:hidden" iconOnly={true} width="26" />
<Logo class="h-8 hidden sm:block" width="153" /> <Logo class="h-8 hidden sm:block" width="153" />
</a> </a>
<Button variant="link" class="text-base px-0" href={getURLForLanguage(i18n.lang, '/')}> <Button
<Home size="18" /> variant="link"
class="text-base px-0 has-[>svg]:px-0"
href={getURLForLanguage(i18n.lang, '/')}
>
<House size="18" />
{i18n._('homepage.home')} {i18n._('homepage.home')}
</Button> </Button>
<Button variant="link" class="text-base px-0" href={getURLForLanguage(i18n.lang, '/app')}> <Button
variant="link"
class="text-base px-0 has-[>svg]:px-0"
href={getURLForLanguage(i18n.lang, '/app')}
>
<Map size="18" /> <Map size="18" />
{i18n._('homepage.app')} {i18n._('homepage.app')}
</Button> </Button>
<Button variant="link" class="text-base px-0" href={getURLForLanguage(i18n.lang, '/help')}> <Button
variant="link"
class="text-base px-0 has-[>svg]:px-0"
href={getURLForLanguage(i18n.lang, '/help')}
>
<BookOpenText size="18" /> <BookOpenText size="18" />
{i18n._('menu.help')} {i18n._('menu.help')}
</Button> </Button>
<AlgoliaDocSearch class="ml-auto" /> <AlgoliaDocSearch class="ml-auto" />
<ModeSwitch class="hidden xs:block" /> <ModeSwitch class="hidden xs:inline-flex" />
</div> </div>
</nav> </nav>

View File

@@ -41,7 +41,7 @@
let canvas: HTMLCanvasElement; let canvas: HTMLCanvasElement;
let overlay: HTMLCanvasElement; let overlay: HTMLCanvasElement;
let elevationProfile: ElevationProfile; let elevationProfile: ElevationProfile | null = null;
onMount(() => { onMount(() => {
elevationProfile = new ElevationProfile( elevationProfile = new ElevationProfile(
@@ -55,7 +55,9 @@
}); });
onDestroy(() => { onDestroy(() => {
elevationProfile.destroy(); if (elevationProfile) {
elevationProfile.destroy();
}
}); });
</script> </script>

View File

@@ -92,17 +92,17 @@
class="fixed left-[50%] top-[50%] z-50 w-fit max-w-full translate-x-[-50%] translate-y-[-50%] flex flex-col items-center gap-3 border bg-background p-3 shadow-lg rounded-md" class="fixed left-[50%] top-[50%] z-50 w-fit max-w-full translate-x-[-50%] translate-y-[-50%] flex flex-col items-center gap-3 border bg-background p-3 shadow-lg rounded-md"
> >
<div <div
class="w-full flex flex-row items-center justify-center gap-4 border rounded-md p-2 bg-secondary" class="w-full flex flex-col sm:flex-row items-center justify-center gap-1 sm:gap-2 border rounded-md p-2 bg-secondary"
> >
<span>⚠️</span> <span class="w-12 shrink-0 text-center text-xl">⚠️</span>
<span class="max-w-[80%] text-sm"> <span class="text-sm">
{i18n._('menu.support_message')} {i18n._('menu.support_message')}
</span> </span>
</div> </div>
<div class="w-full flex flex-row flex-wrap gap-2"> <div class="w-full flex flex-row flex-wrap gap-2">
<Button class="bg-support grow" href="https://ko-fi.com/gpxstudio" target="_blank"> <Button class="bg-support grow" href="https://ko-fi.com/gpxstudio" target="_blank">
{i18n._('menu.support_button')} {i18n._('menu.support_button')}
<span class="ml-2">🙏</span> <span>🙏</span>
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
@@ -117,7 +117,7 @@
exportState.current = ExportState.NONE; exportState.current = ExportState.NONE;
}} }}
> >
<Download size="16" class="mr-1" /> <Download size="16" />
{#if $fileStateCollection.size === 1 || (exportState.current === ExportState.SELECTION && $selection.size === 1)} {#if $fileStateCollection.size === 1 || (exportState.current === ExportState.SELECTION && $selection.size === 1)}
{i18n._('menu.download_file')} {i18n._('menu.download_file')}
{:else} {:else}

View File

@@ -68,7 +68,7 @@
open = false; open = false;
}} }}
> >
<Save size="16" class="mr-1" /> <Save size="16" />
{i18n._('menu.metadata.save')} {i18n._('menu.metadata.save')}
</Button> </Button>
</Popover.Content> </Popover.Content>

View File

@@ -164,7 +164,7 @@
disabled={!colorChanged && !opacityChanged && !widthChanged} disabled={!colorChanged && !opacityChanged && !widthChanged}
onclick={applyStyle} onclick={applyStyle}
> >
<Save size="16" class="mr-1" /> <Save size="16" />
{i18n._('menu.metadata.save')} {i18n._('menu.metadata.save')}
</Button> </Button>
</Popover.Content> </Popover.Content>

View File

@@ -16,7 +16,7 @@
</script> </script>
<Button <Button
class="w-full px-2 py-1 h-8 justify-start {className}" class="p-1 has-[>svg]:px-2 h-8 justify-start {className}"
variant="outline" variant="outline"
onclick={() => { onclick={() => {
navigator.clipboard.writeText( navigator.clipboard.writeText(
@@ -25,6 +25,6 @@
onCopy(); onCopy();
}} }}
> >
<ClipboardCopy size="16" class="mr-1" /> <ClipboardCopy size="16" />
{i18n._('menu.copy_coordinates')} {i18n._('menu.copy_coordinates')}
</Button> </Button>

View File

@@ -11,12 +11,16 @@
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from 'sanitize-html';
import type { Waypoint } from 'gpx'; import type { Waypoint } from 'gpx';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js'; import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import type { PopupItem } from '$lib/components/map/map';
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import type { PopupItem } from '$lib/components/map/map-popup';
export let waypoint: PopupItem<Waypoint>; let {
waypoint,
}: {
waypoint: PopupItem<Waypoint>;
} = $props();
$: symbolKey = waypoint ? getSymbolKey(waypoint.item.sym) : undefined; let symbolKey = $derived(waypoint ? getSymbolKey(waypoint.item.sym) : undefined);
function sanitize(text: string | undefined): string { function sanitize(text: string | undefined): string {
if (text === undefined) { if (text === undefined) {
@@ -50,11 +54,8 @@
{#if symbolKey} {#if symbolKey}
<span> <span>
{#if symbols[symbolKey].icon} {#if symbols[symbolKey].icon}
<svelte:component {@const Icon = symbols[symbolKey].icon}
this={symbols[symbolKey].icon} <Icon size="12" class="inline-block mb-1" />
size="12"
class="inline-block mb-0.5"
/>
{:else} {:else}
<span class="w-4 inline-block"></span> <span class="w-4 inline-block"></span>
{/if} {/if}
@@ -82,15 +83,16 @@
<CopyCoordinates coordinates={waypoint.item.attributes} /> <CopyCoordinates coordinates={waypoint.item.attributes} />
{#if $currentTool === Tool.WAYPOINT} {#if $currentTool === Tool.WAYPOINT}
<Button <Button
class="w-full px-2 py-1 h-8 justify-start" class="p-1 has-[>svg]:px-2 h-8"
variant="outline" variant="outline"
onclick={() => { onclick={() => {
if (waypoint.fileId) { if (waypoint.fileId) {
fileActions.deleteWaypoint(waypoint.fileId, waypoint.item._data.index); fileActions.deleteWaypoint(waypoint.fileId, waypoint.item._data.index);
waypoint.hide?.();
} }
}} }}
> >
<Trash2 size="16" class="mr-1" /> <Trash2 size="16" />
{i18n._('menu.delete')} {i18n._('menu.delete')}
<Shortcut shift={true} click={true} /> <Shortcut shift={true} click={true} />
</Button> </Button>

View File

@@ -156,7 +156,7 @@ export class GPXLayer {
} }
try { try {
let source = _map.getSource(this.fileId); let source = _map.getSource(this.fileId) as mapboxgl.GeoJSONSource | undefined;
if (source) { if (source) {
source.setData(this.getGeoJSON()); source.setData(this.getGeoJSON());
} else { } else {

View File

@@ -463,7 +463,7 @@
{#if selectedLayerId} {#if selectedLayerId}
<div class="mt-2 flex flex-row gap-2"> <div class="mt-2 flex flex-row gap-2">
<Button variant="outline" onclick={createLayer} class="grow"> <Button variant="outline" onclick={createLayer} class="grow">
<Save size="16" class="mr-1" /> <Save size="16" />
{i18n._('layers.custom_layers.update')} {i18n._('layers.custom_layers.update')}
</Button> </Button>
<Button variant="outline" onclick={() => (selectedLayerId = undefined)}> <Button variant="outline" onclick={() => (selectedLayerId = undefined)}>
@@ -472,7 +472,7 @@
</div> </div>
{:else} {:else}
<Button variant="outline" class="mt-2" onclick={createLayer}> <Button variant="outline" class="mt-2" onclick={createLayer}>
<CirclePlus size="16" class="mr-1" /> <CirclePlus size="16" />
{i18n._('layers.custom_layers.create')} {i18n._('layers.custom_layers.create')}
</Button> </Button>
{/if} {/if}

View File

@@ -227,8 +227,9 @@
</CustomControl> </CustomControl>
<svelte:window <svelte:window
on:click={(e) => { on:click={(e: MouseEvent) => {
if (open && !cancelEvents && !container.contains(e.target)) { const target = e.target as Node | null;
if (open && !cancelEvents && target && container && !container.contains(target)) {
closeLayerControl(); closeLayerControl();
} }
}} }}

View File

@@ -19,6 +19,7 @@
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import CustomLayers from './CustomLayers.svelte'; import CustomLayers from './CustomLayers.svelte';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { untrack } from 'svelte';
const { const {
selectedBasemapTree, selectedBasemapTree,
@@ -26,6 +27,7 @@
selectedOverpassTree, selectedOverpassTree,
currentBasemap, currentBasemap,
currentOverlays, currentOverlays,
currentOverpassQueries,
customLayers, customLayers,
opacities, opacities,
} = settings; } = settings;
@@ -60,19 +62,44 @@
}); });
$effect(() => { $effect(() => {
if ($selectedOverlayTree && $currentOverlays) { if ($selectedOverlayTree) {
let overlayLayers = getLayers($currentOverlays); untrack(() => {
let toRemove = Object.entries(overlayLayers).filter( if ($currentOverlays) {
([id, checked]) => checked && !isSelected($selectedOverlayTree, id) let overlayLayers = getLayers($currentOverlays);
); let toRemove = Object.entries(overlayLayers).filter(
if (toRemove.length > 0) { ([id, checked]) => checked && !isSelected($selectedOverlayTree, id)
currentOverlays.update((tree) => { );
toRemove.forEach(([id]) => { if (toRemove.length > 0) {
toggle(tree, id); currentOverlays.update((tree) => {
}); toRemove.forEach(([id]) => {
return tree; toggle(tree, id);
}); });
} return tree;
});
}
}
});
}
});
$effect(() => {
if ($selectedOverpassTree) {
untrack(() => {
if ($currentOverpassQueries) {
let overlayLayers = getLayers($currentOverpassQueries);
let toRemove = Object.entries(overlayLayers).filter(
([id, checked]) => checked && !isSelected($selectedOverpassTree, id)
);
if (toRemove.length > 0) {
currentOverpassQueries.update((tree) => {
toRemove.forEach(([id]) => {
toggle(tree, id);
});
return tree;
});
}
}
});
} }
}); });
</script> </script>

View File

@@ -5,7 +5,7 @@
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js'; import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import type { WaypointType } from 'gpx'; import type { WaypointType } from 'gpx';
import type { PopupItem } from '$lib/components/map/map'; import type { PopupItem } from '$lib/components/map/map-popup';
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
@@ -53,13 +53,14 @@
<div class="flex flex-row gap-3"> <div class="flex flex-row gap-3">
<div class="flex flex-col"> <div class="flex flex-col">
{name} {name}
<div class="text-muted-foreground text-sm font-normal"> <div class="text-muted-foreground text-xs font-normal">
{poi.item.lat.toFixed(6)}&deg; {poi.item.lon.toFixed(6)}&deg; {poi.item.lat.toFixed(6)}&deg; {poi.item.lon.toFixed(6)}&deg;
</div> </div>
</div> </div>
<Button <Button
class="ml-auto p-1.5 h-8" class="ml-auto"
variant="outline" variant="outline"
size="icon"
href="https://www.openstreetmap.org/edit?editor=id&{poi.item.type ?? href="https://www.openstreetmap.org/edit?editor=id&{poi.item.type ??
'node'}={poi.item.id}" 'node'}={poi.item.id}"
target="_blank" target="_blank"
@@ -95,7 +96,7 @@
</div> </div>
</ScrollArea> </ScrollArea>
<Button class="mt-2" variant="outline" disabled={$selection.size === 0} onclick={addToFile}> <Button class="mt-2" variant="outline" disabled={$selection.size === 0} onclick={addToFile}>
<MapPin size="16" class="mr-1" /> <MapPin size="16" />
{i18n._('toolbar.waypoint.add')} {i18n._('toolbar.waypoint.add')}
</Button> </Button>
</Card.Content> </Card.Content>

View File

@@ -74,7 +74,7 @@ export class OverpassLayer {
let d = get(data); let d = get(data);
try { try {
let source = this.map.getSource('overpass'); let source = this.map.getSource('overpass') as mapboxgl.GeoJSONSource | undefined;
if (source) { if (source) {
source.setData(d); source.setData(d);
} else { } else {
@@ -284,9 +284,9 @@ function getQuery(query: string) {
} }
function getQueryItem(tags: Record<string, string | boolean | string[]>) { function getQueryItem(tags: Record<string, string | boolean | string[]>) {
let arrayEntry = Object.entries(tags).find(([_, value]) => Array.isArray(value)); let arrayEntry = Object.values(tags).find((value) => Array.isArray(value));
if (arrayEntry !== undefined) { if (arrayEntry !== undefined) {
return arrayEntry[1] return arrayEntry
.map( .map(
(val) => (val) =>
`nwr${Object.entries(tags) `nwr${Object.entries(tags)

View File

@@ -135,12 +135,19 @@ export class MapillaryLayer {
} }
onMouseEnter(e: mapboxgl.MapMouseEvent) { onMouseEnter(e: mapboxgl.MapMouseEvent) {
this.active = true; if (
e.features &&
e.features.length > 0 &&
e.features[0].properties &&
e.features[0].properties.id
) {
this.active = true;
this.viewer.resize(); this.viewer.resize();
this.viewer.moveTo(e.features[0].properties.id); this.viewer.moveTo(e.features[0].properties.id);
mapCursor.notify(MapCursorState.MAPILLARY_HOVER, true); mapCursor.notify(MapCursorState.MAPILLARY_HOVER, true);
}
} }
onMouseLeave() { onMouseLeave() {

View File

@@ -38,7 +38,9 @@
</script> </script>
{#if $currentTool !== null} {#if $currentTool !== null}
<div class="translate-x-1 h-full animate-in animate-out {className}"> <div
class="translate-x-1 h-full animate-in fade-in-0 zoom-in-95 slide-in-from-left-2 {className}"
>
<div class="rounded-md shadow-md pointer-events-auto"> <div class="rounded-md shadow-md pointer-events-auto">
<Card.Root class="rounded-md border-none py-2.5"> <Card.Root class="rounded-md border-none py-2.5">
<Card.Content class="px-2.5"> <Card.Content class="px-2.5">

View File

@@ -177,7 +177,7 @@
rectangleCoordinates = []; rectangleCoordinates = [];
}} }}
> >
<Trash2 size="16" class="mr-1" /> <Trash2 size="16" />
{i18n._('toolbar.clean.button')} {i18n._('toolbar.clean.button')}
</Button> </Button>
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/clean')}> <Help link={getURLForLanguage(i18n.lang, '/help/toolbar/clean')}>

View File

@@ -26,7 +26,7 @@
} }
}} }}
> >
<MountainSnow size="16" class="mr-1 shrink-0" /> <MountainSnow size="16" class="shrink-0" />
{i18n._('toolbar.elevation.button')} {i18n._('toolbar.elevation.button')}
</Button> </Button>
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/elevation')}> <Help link={getURLForLanguage(i18n.lang, '/help/toolbar/elevation')}>

View File

@@ -46,7 +46,7 @@
<div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
<Button variant="outline" disabled={!validSelection} onclick={fileActions.extractSelection}> <Button variant="outline" disabled={!validSelection} onclick={fileActions.extractSelection}>
<Ungroup size="16" class="mr-1" /> <Ungroup size="16" />
{i18n._('toolbar.extract.button')} {i18n._('toolbar.extract.button')}
</Button> </Button>
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/extract')}> <Help link={getURLForLanguage(i18n.lang, '/help/toolbar/extract')}>

View File

@@ -86,7 +86,7 @@
); );
}} }}
> >
<Group size="16" class="mr-1 shrink-0" /> <Group size="16" class="shrink-0" />
{i18n._('toolbar.merge.merge_selection')} {i18n._('toolbar.merge.merge_selection')}
</Button> </Button>
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/merge')}> <Help link={getURLForLanguage(i18n.lang, '/help/toolbar/merge')}>

View File

@@ -188,7 +188,7 @@
<div class="flex flex-row gap-2 justify-center"> <div class="flex flex-row gap-2 justify-center">
<div class="flex flex-col gap-2 grow"> <div class="flex flex-col gap-2 grow">
<Label for="speed" class="flex flex-row"> <Label for="speed" class="flex flex-row">
<Zap size="16" class="mr-1" /> <Zap size="16" />
{#if $velocityUnits === 'speed'} {#if $velocityUnits === 'speed'}
{i18n._('quantities.speed')} {i18n._('quantities.speed')}
{:else} {:else}
@@ -241,7 +241,7 @@
</div> </div>
<div class="flex flex-col gap-2 grow"> <div class="flex flex-col gap-2 grow">
<Label for="duration" class="flex flex-row"> <Label for="duration" class="flex flex-row">
<Timer size="16" class="mr-1" /> <Timer size="16" />
{i18n._('toolbar.time.total_time')} {i18n._('toolbar.time.total_time')}
</Label> </Label>
<TimePicker <TimePicker
@@ -254,7 +254,7 @@
</div> </div>
</div> </div>
<Label class="flex flex-row"> <Label class="flex flex-row">
<CirclePlay size="16" class="mr-1" /> <CirclePlay size="16" />
{i18n._('toolbar.time.start')} {i18n._('toolbar.time.start')}
</Label> </Label>
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
@@ -280,7 +280,7 @@
/> />
</div> </div>
<Label class="flex flex-row"> <Label class="flex flex-row">
<CircleStop size="16" class="mr-1" /> <CircleStop size="16" />
{i18n._('toolbar.time.end')} {i18n._('toolbar.time.end')}
</Label> </Label>
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
@@ -324,7 +324,8 @@
if ( if (
startDate === undefined || startDate === undefined ||
startTime === undefined || startTime === undefined ||
effectiveSpeed === undefined effectiveSpeed === undefined ||
movingTime === undefined
) { ) {
return; return;
} }
@@ -347,12 +348,12 @@
if (item instanceof ListFileItem) { if (item instanceof ListFileItem) {
if (artificial || !$gpxStatistics.global.time.moving) { if (artificial || !$gpxStatistics.global.time.moving) {
file.createArtificialTimestamps( file.createArtificialTimestamps(
getDate(startDate, startTime), getDate(startDate!, startTime!),
movingTime movingTime!
); );
} else { } else {
file.changeTimestamps( file.changeTimestamps(
getDate(startDate, startTime), getDate(startDate!, startTime!),
effectiveSpeed, effectiveSpeed,
ratio ratio
); );
@@ -360,13 +361,13 @@
} else if (item instanceof ListTrackItem) { } else if (item instanceof ListTrackItem) {
if (artificial || !$gpxStatistics.global.time.moving) { if (artificial || !$gpxStatistics.global.time.moving) {
file.createArtificialTimestamps( file.createArtificialTimestamps(
getDate(startDate, startTime), getDate(startDate!, startTime!),
movingTime, movingTime!,
item.getTrackIndex() item.getTrackIndex()
); );
} else { } else {
file.changeTimestamps( file.changeTimestamps(
getDate(startDate, startTime), getDate(startDate!, startTime!),
effectiveSpeed, effectiveSpeed,
ratio, ratio,
item.getTrackIndex() item.getTrackIndex()
@@ -375,14 +376,14 @@
} else if (item instanceof ListTrackSegmentItem) { } else if (item instanceof ListTrackSegmentItem) {
if (artificial || !$gpxStatistics.global.time.moving) { if (artificial || !$gpxStatistics.global.time.moving) {
file.createArtificialTimestamps( file.createArtificialTimestamps(
getDate(startDate, startTime), getDate(startDate!, startTime!),
movingTime, movingTime!,
item.getTrackIndex(), item.getTrackIndex(),
item.getSegmentIndex() item.getSegmentIndex()
); );
} else { } else {
file.changeTimestamps( file.changeTimestamps(
getDate(startDate, startTime), getDate(startDate!, startTime!),
effectiveSpeed, effectiveSpeed,
ratio, ratio,
item.getTrackIndex(), item.getTrackIndex(),
@@ -393,10 +394,10 @@
}); });
}} }}
> >
<CalendarClock size="16" class="mr-1 shrink-0" /> <CalendarClock size="16" class="shrink-0" />
{i18n._('toolbar.time.update')} {i18n._('toolbar.time.update')}
</Button> </Button>
<Button variant="outline" onclick={setGPXData}> <Button variant="outline" size="icon" onclick={setGPXData}>
<CircleX size="16" /> <CircleX size="16" />
</Button> </Button>
</div> </div>

View File

@@ -47,7 +47,7 @@
<span class="font-normal">{reducedLayers.currentPoints}/{reducedLayers.maxPoints}</span> <span class="font-normal">{reducedLayers.currentPoints}/{reducedLayers.maxPoints}</span>
</Label> </Label>
<Button variant="outline" disabled={!validSelection} onclick={() => reducedLayers.reduce()}> <Button variant="outline" disabled={!validSelection} onclick={() => reducedLayers.reduce()}>
<Funnel size="16" class="mr-1" /> <Funnel size="16" />
{i18n._('toolbar.reduce.button')} {i18n._('toolbar.reduce.button')}
</Button> </Button>

View File

@@ -129,7 +129,7 @@
</Button> </Button>
</div> </div>
{:else} {:else}
<div class="flex flex-col gap-3 w-full max-w-80 animate-in animate-out {className ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 {className ?? ''}">
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<Label class="justify-between"> <Label class="justify-between">
<span class="flex flex-row items-center gap-1"> <span class="flex flex-row items-center gap-1">

View File

@@ -15,7 +15,7 @@
</script> </script>
<div bind:this={element} class="hidden"> <div bind:this={element} class="hidden">
<Card.Root class="border-none shadow-md text-base"> <Card.Root class="border-none shadow-md text-base p-0 gap-0 rounded-lg">
<Card.Content class="flex flex-col p-1"> <Card.Content class="flex flex-col p-1">
{#if $canChangeStart} {#if $canChangeStart}
<Button <Button
@@ -23,7 +23,7 @@
variant="ghost" variant="ghost"
onclick={() => element?.dispatchEvent(new CustomEvent('change-start'))} onclick={() => element?.dispatchEvent(new CustomEvent('change-start'))}
> >
<CirclePlay size="16" class="mr-1" /> <CirclePlay size="16" />
{i18n._('toolbar.routing.start_loop_here')} {i18n._('toolbar.routing.start_loop_here')}
</Button> </Button>
{/if} {/if}
@@ -32,7 +32,7 @@
variant="ghost" variant="ghost"
onclick={() => element?.dispatchEvent(new CustomEvent('delete'))} onclick={() => element?.dispatchEvent(new CustomEvent('delete'))}
> >
<Trash2 size="16" class="mr-1" /> <Trash2 size="16" />
{i18n._('menu.delete')} {i18n._('menu.delete')}
<Shortcut shift={true} click={true} /> <Shortcut shift={true} click={true} />
</Button> </Button>

View File

@@ -494,7 +494,7 @@ export class RoutingControls {
segment.trkpt[before].time && segment.trkpt[before + 1].time segment.trkpt[before].time && segment.trkpt[before + 1].time
? new Date( ? new Date(
(1 - ratio) * segment.trkpt[before].time.getTime() + (1 - ratio) * segment.trkpt[before].time.getTime() +
ratio * segment.trkpt[before + 1].time.getTime() ratio * segment.trkpt[before + 1].time!.getTime()
) )
: undefined; : undefined;
point._data = { point._data = {
@@ -540,7 +540,7 @@ export class RoutingControls {
fileActionManager.applyToFile(this.fileId, (file) => fileActionManager.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, 0, []) file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, 0, [])
); );
} else if (previousAnchor === null) { } else if (previousAnchor === null && nextAnchor !== null) {
// First point, remove trackpoints until nextAnchor // First point, remove trackpoints until nextAnchor
fileActionManager.applyToFile(this.fileId, (file) => fileActionManager.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints( file.replaceTrackPoints(
@@ -551,7 +551,7 @@ export class RoutingControls {
[] []
) )
); );
} else if (nextAnchor === null) { } else if (nextAnchor === null && previousAnchor !== null) {
// Last point, remove trackpoints from previousAnchor // Last point, remove trackpoints from previousAnchor
fileActionManager.applyToFile(this.fileId, (file) => { fileActionManager.applyToFile(this.fileId, (file) => {
let segment = file.getSegment(anchor.trackIndex, anchor.segmentIndex); let segment = file.getSegment(anchor.trackIndex, anchor.segmentIndex);
@@ -563,7 +563,7 @@ export class RoutingControls {
[] []
); );
}); });
} else { } else if (previousAnchor !== null && nextAnchor !== null) {
// Route between previousAnchor and nextAnchor // Route between previousAnchor and nextAnchor
this.routeBetweenAnchors( this.routeBetweenAnchors(
[previousAnchor, nextAnchor], [previousAnchor, nextAnchor],

View File

@@ -99,7 +99,7 @@
disabled={!validSelection || !canCrop} disabled={!validSelection || !canCrop}
onclick={() => fileActions.cropSelection(sliderValues[0], sliderValues[1])} onclick={() => fileActions.cropSelection(sliderValues[0], sliderValues[1])}
> >
<Crop size="16" class="mr-1" />{i18n._('toolbar.scissors.crop')} <Crop size="16" />{i18n._('toolbar.scissors.crop')}
</Button> </Button>
<Separator /> <Separator />
<Label class="flex flex-row flex-wrap gap-3 items-center"> <Label class="flex flex-row flex-wrap gap-3 items-center">

View File

@@ -203,14 +203,14 @@
onclick={createOrUpdateWaypoint} onclick={createOrUpdateWaypoint}
> >
{#if $selectedWaypoint} {#if $selectedWaypoint}
<Save size="16" class="mr-1 shrink-0" /> <Save size="16" class="shrink-0" />
{i18n._('menu.metadata.save')} {i18n._('menu.metadata.save')}
{:else} {:else}
<MapPin size="16" class="mr-1 shrink-0" /> <MapPin size="16" class="shrink-0" />
{i18n._('toolbar.waypoint.create')} {i18n._('toolbar.waypoint.create')}
{/if} {/if}
</Button> </Button>
<Button variant="outline" onclick={() => selectedWaypoint.reset()}> <Button variant="outline" size="icon" onclick={() => selectedWaypoint.reset()}>
<CircleX size="16" /> <CircleX size="16" />
</Button> </Button>
</div> </div>

View File

@@ -49,7 +49,7 @@
} else { } else {
untrack(() => { untrack(() => {
if (value != computeValue()) { if (value != computeValue()) {
let rounded = Math.max(Math.round(value), 1); let rounded = Math.max(Math.round(value!), 1);
if (showHours) { if (showHours) {
hours = Math.floor(rounded / 3600); hours = Math.floor(rounded / 3600);
minutes = Math.floor((rounded % 3600) / 60) minutes = Math.floor((rounded % 3600) / 60)
@@ -66,14 +66,19 @@
let container: HTMLDivElement; let container: HTMLDivElement;
let countKeyPress = 0; let countKeyPress = 0;
function onKeyPress(e) { function onKeyPress(e: KeyboardEvent) {
if (['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].includes(e.key)) { const target = e.target as HTMLInputElement | null;
if (target && ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].includes(e.key)) {
countKeyPress++; countKeyPress++;
if (countKeyPress === 2) { if (countKeyPress === 2) {
if (e.target.id === 'hours') { const nextInput =
container.querySelector('#minutes')?.focus(); target.id === 'hours'
} else if (e.target.id === 'minutes') { ? (container.querySelector('#minutes') as HTMLInputElement)
container.querySelector('#seconds')?.focus(); : target.id === 'minutes'
? (container.querySelector('#seconds') as HTMLInputElement)
: null;
if (nextInput) {
nextInput.focus();
} }
} }
} }
@@ -172,6 +177,7 @@
margin: 0; margin: 0;
} }
div :global(input[type='number']) { div :global(input[type='number']) {
appearance: textfield;
-moz-appearance: textfield; -moz-appearance: textfield;
} }
</style> </style>

View File

@@ -2,7 +2,7 @@
import { HeartHandshake } from '@lucide/svelte'; import { HeartHandshake } from '@lucide/svelte';
</script> </script>
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Help keep the website free (and ad-free) ## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
Each time you add or move GPS points, our servers calculate the best route on the road network. Each time you add or move GPS points, our servers calculate the best route on the road network.
We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places. We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.

View File

@@ -2,7 +2,7 @@
import { Languages } from '@lucide/svelte'; import { Languages } from '@lucide/svelte';
</script> </script>
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Translation ## <Languages size="18" class="inline-block align-baseline" /> Translation
The website is translated by volunteers using a collaborative translation platform. The website is translated by volunteers using a collaborative translation platform.
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>. You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.

View File

@@ -3,7 +3,7 @@ title: Route planning and editing
--- ---
<script> <script>
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte'; import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte'; import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
import DocsImage from '$lib/components/docs/DocsImage.svelte'; import DocsImage from '$lib/components/docs/DocsImage.svelte';
@@ -71,7 +71,7 @@ The following tools automate some common route modification operations.
Reverse the direction of the route. Reverse the direction of the route.
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start ### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
Connect the last point of the route with the starting point, using the chosen routing settings. Connect the last point of the route with the starting point, using the chosen routing settings.

View File

@@ -15,7 +15,7 @@ import {
type ListItem, type ListItem,
} from '$lib/components/file-list/file-list'; } from '$lib/components/file-list/file-list';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { freeze } from 'immer'; import { freeze, type WritableDraft } from 'immer';
import { import {
distance, distance,
GPXFile, GPXFile,
@@ -395,6 +395,7 @@ export const fileActions = {
} }
} }
if (targetFile) { if (targetFile) {
targetFile = targetFile as GPXFile;
if (target instanceof ListFileItem) { if (target instanceof ListFileItem) {
targetFile.replaceTracks(0, targetFile.trk.length - 1, toMerge.trk); targetFile.replaceTracks(0, targetFile.trk.length - 1, toMerge.trk);
targetFile.replaceWaypoints(0, targetFile.wpt.length - 1, toMerge.wpt); targetFile.replaceWaypoints(0, targetFile.wpt.length - 1, toMerge.wpt);
@@ -1059,7 +1060,10 @@ export function moveItems(
let files = [fromParent.getFileId(), toParent.getFileId()]; let files = [fromParent.getFileId(), toParent.getFileId()];
let callbacks = [ let callbacks = [
(file, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => { (
file: WritableDraft<GPXFile>,
context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]
) => {
fromItems.forEach((item) => { fromItems.forEach((item) => {
if (item instanceof ListTrackItem) { if (item instanceof ListTrackItem) {
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []); file.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []);
@@ -1077,7 +1081,10 @@ export function moveItems(
} }
}); });
}, },
(file, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => { (
file: WritableDraft<GPXFile>,
context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]
) => {
toItems.forEach((item, i) => { toItems.forEach((item, i) => {
if (item instanceof ListTrackItem) { if (item instanceof ListTrackItem) {
if (context[i] instanceof Track) { if (context[i] instanceof Track) {

View File

@@ -65,7 +65,7 @@
</div> </div>
<div class="w-full flex flex-row justify-center gap-3"> <div class="w-full flex flex-row justify-center gap-3">
<Button href={getURLForLanguage(i18n.lang, '/app')} class="w-1/3 min-w-fit"> <Button href={getURLForLanguage(i18n.lang, '/app')} class="w-1/3 min-w-fit">
<Map size="18" class="mr-1.5" /> <Map size="18" />
{i18n._('homepage.app')} {i18n._('homepage.app')}
</Button> </Button>
<Button <Button
@@ -73,7 +73,7 @@
href={getURLForLanguage(i18n.lang, '/help')} href={getURLForLanguage(i18n.lang, '/help')}
class="w-1/3 min-w-fit" class="w-1/3 min-w-fit"
> >
<BookOpenText size="18" class="mr-1.5" /> <BookOpenText size="18" />
<span>{i18n._('menu.help')}</span> <span>{i18n._('menu.help')}</span>
</Button> </Button>
</div> </div>
@@ -96,7 +96,7 @@
> >
<div class="markdown text-center"> <div class="markdown text-center">
<h1> <h1>
<Route size="24" class="mr-1 inline-block align-baseline" /> <Route size="24" class="inline-block align-baseline" />
{i18n._('homepage.route_planning')} {i18n._('homepage.route_planning')}
</h1> </h1>
<p class="text-muted-foreground">{i18n._('homepage.route_planning_description')}</p> <p class="text-muted-foreground">{i18n._('homepage.route_planning_description')}</p>
@@ -112,7 +112,7 @@
> >
<div class="markdown text-center md:hidden"> <div class="markdown text-center md:hidden">
<h1> <h1>
<PencilRuler size="24" class="mr-1 inline-block align-baseline" /> <PencilRuler size="24" class="inline-block align-baseline" />
{i18n._('homepage.file_processing')} {i18n._('homepage.file_processing')}
</h1> </h1>
<p class="text-muted-foreground"> <p class="text-muted-foreground">
@@ -124,7 +124,7 @@
</div> </div>
<div class="markdown text-center hidden md:block"> <div class="markdown text-center hidden md:block">
<h1> <h1>
<PencilRuler size="24" class="mr-1 inline-block align-baseline" /> <PencilRuler size="24" class="inline-block align-baseline" />
{i18n._('homepage.file_processing')} {i18n._('homepage.file_processing')}
</h1> </h1>
<p class="text-muted-foreground"> <p class="text-muted-foreground">
@@ -139,7 +139,7 @@
> >
<div class="markdown text-center"> <div class="markdown text-center">
<h1> <h1>
<Map size="24" class="mr-1 inline-block align-baseline" /> <Map size="24" class="inline-block align-baseline" />
{i18n._('homepage.maps')} {i18n._('homepage.maps')}
</h1> </h1>
<p class="text-muted-foreground">{i18n._('homepage.maps_description')}</p> <p class="text-muted-foreground">{i18n._('homepage.maps_description')}</p>
@@ -182,7 +182,7 @@
<div class="px-8 md:px-12"> <div class="px-8 md:px-12">
<div class="markdown text-center px-4 md:px-12"> <div class="markdown text-center px-4 md:px-12">
<h1> <h1>
<ChartArea size="24" class="mr-1 inline-block align-baseline" /> <ChartArea size="24" class="inline-block align-baseline" />
{i18n._('homepage.data_visualization')} {i18n._('homepage.data_visualization')}
</h1> </h1>
<p class="text-muted-foreground mb-6"> <p class="text-muted-foreground mb-6">
@@ -214,7 +214,7 @@
> >
<div class="markdown text-center md:hidden"> <div class="markdown text-center md:hidden">
<h1> <h1>
<Scale size="24" class="mr-1 inline-block align-baseline" /> <Scale size="24" class="inline-block align-baseline" />
{i18n._('homepage.identity')} {i18n._('homepage.identity')}
</h1> </h1>
<p class="text-muted-foreground">{i18n._('homepage.identity_description')}</p> <p class="text-muted-foreground">{i18n._('homepage.identity_description')}</p>
@@ -224,7 +224,7 @@
</a> </a>
<div class="markdown text-center hidden md:block"> <div class="markdown text-center hidden md:block">
<h1> <h1>
<Scale size="24" class="mr-1 inline-block align-baseline" /> <Scale size="24" class="inline-block align-baseline" />
{i18n._('homepage.identity')} {i18n._('homepage.identity')}
</h1> </h1>
<p class="text-muted-foreground">{i18n._('homepage.identity_description')}</p> <p class="text-muted-foreground">{i18n._('homepage.identity_description')}</p>
@@ -241,7 +241,7 @@
</div> </div>
</div> </div>
<div <div
class="px-12 md:px-24 flex flex-row flex-wrap lg:flex-nowrap items-center justify-center -space-y-0.5 lg:-space-x-0.5" class="px-12 md:px-24 flex flex-row flex-wrap lg:flex-nowrap items-center justify-center -space-y-0.5 lg:-space-x-6"
> >
<div <div
class="grow max-w-xl flex flex-col items-center gap-6 p-8 border rounded-2xl shadow-xl -rotate-1 lg:rotate-1" class="grow max-w-xl flex flex-col items-center gap-6 p-8 border rounded-2xl shadow-xl -rotate-1 lg:rotate-1"
@@ -250,7 +250,7 @@
<DocsContainer module={fundingModule.default} /> <DocsContainer module={fundingModule.default} />
{/await} {/await}
<Button href="https://ko-fi.com/gpxstudio" target="_blank" class="text-base"> <Button href="https://ko-fi.com/gpxstudio" target="_blank" class="text-base">
<Heart size="16" class="mr-1" fill="var(--support)" color="var(--support)" /> <Heart size="16" fill="var(--support)" color="var(--support)" />
<span>{i18n._('homepage.support_button')}</span> <span>{i18n._('homepage.support_button')}</span>
</Button> </Button>
</div> </div>
@@ -261,7 +261,7 @@
<DocsContainer module={translationModule.default} /> <DocsContainer module={translationModule.default} />
{/await} {/await}
<Button href="https://crowdin.com/project/gpxstudio" target="_blank" class="text-base"> <Button href="https://crowdin.com/project/gpxstudio" target="_blank" class="text-base">
<PenLine size="16" class="mr-1" /> <PenLine size="16" />
<span>{i18n._('homepage.contribute')}</span> <span>{i18n._('homepage.contribute')}</span>
</Button> </Button>
</div> </div>

View File

@@ -38,7 +38,7 @@
{guideIcons[subGuide]} {guideIcons[subGuide]}
{:else} {:else}
{@const GuideIcon = guideIcons[subGuide]} {@const GuideIcon = guideIcons[subGuide]}
<GuideIcon size="16" class="mr-1 shrink-0" /> <GuideIcon size="16" class="shrink-0" />
{/if} {/if}
{data.guideTitles[`${guide}/${subGuide}`]} {data.guideTitles[`${guide}/${subGuide}`]}
</Button> </Button>