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
vite.config.js.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,
ignFrTopo: ignFrTopo,
ignFrPlan: ignFrPlan as StyleSpecification,
ignFrTopo: ignFrTopo as StyleSpecification,
ignFrScan25: {
version: 8,
sources: {
@@ -209,7 +209,7 @@ export const basemaps: { [key: string]: string | StyleSpecification } = {
},
],
},
ignFrSatellite: ignFrSatellite,
ignFrSatellite: ignFrSatellite as StyleSpecification,
ignEs: {
version: 8,
sources: {
@@ -366,7 +366,7 @@ export const overlays: { [key: string]: string | StyleSpecification } = {
},
],
},
bikerouterGravel: bikerouterGravel,
bikerouterGravel: bikerouterGravel as StyleSpecification,
swisstopoSlope: {
version: 8,
sources: {

View File

@@ -12,7 +12,7 @@ import {
DoorOpen,
Trees,
Fuel,
Home,
House,
Info,
TreeDeciduous,
CircleParking,
@@ -44,7 +44,7 @@ import {
DoorOpen as DoorOpenSvg,
Trees as TreesSvg,
Fuel as FuelSvg,
Home as HomeSvg,
House as HouseSvg,
Info as InfoSvg,
TreeDeciduous as TreeDeciduousSvg,
CircleParking as CircleParkingSvg,
@@ -95,7 +95,7 @@ export const symbols: { [key: string]: Symbol } = {
},
drinking_water: { value: 'Drinking Water', icon: Droplet, iconSvg: DropletSvg },
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 },
forest: { value: 'Forest', icon: Trees, iconSvg: TreesSvg },
gas_station: { value: 'Gas Station', icon: Fuel, iconSvg: FuelSvg },
@@ -105,7 +105,7 @@ export const symbols: { [key: string]: Symbol } = {
iconSvg: TrainFrontSvg,
},
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 },
park: { value: 'Park', icon: TreeDeciduous, iconSvg: TreeDeciduousSvg },
parking_area: { value: 'Parking Area', icon: CircleParking, iconSvg: CircleParkingSvg },

View File

@@ -2,7 +2,7 @@
import { Button } from '$lib/components/ui/button';
import LanguageSelect from '$lib/components/LanguageSelect.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 { getURLForLanguage } from '$lib/utils';
</script>
@@ -14,7 +14,7 @@
<Logo class="h-8" width="153" />
<Button
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"
target="_blank"
>
@@ -27,15 +27,15 @@
<span class="font-semibold">{i18n._('homepage.website')}</span>
<Button
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, '/')}
>
<Home size="16" />
<House size="16" />
{i18n._('homepage.home')}
</Button>
<Button
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')}
>
<Map size="16" />
@@ -43,7 +43,7 @@
</Button>
<Button
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')}
>
<BookOpenText size="16" />
@@ -54,7 +54,7 @@
<span class="font-semibold">{i18n._('homepage.contact')}</span>
<Button
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/"
target="_blank"
>
@@ -63,7 +63,7 @@
</Button>
<Button
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"
target="_blank"
>
@@ -72,7 +72,7 @@
</Button>
<Button
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"
target="_blank"
>
@@ -81,7 +81,7 @@
</Button>
<Button
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"
target="_blank"
>
@@ -93,7 +93,7 @@
<span class="font-semibold">{i18n._('homepage.contribute')}</span>
<Button
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"
target="_blank"
>
@@ -102,7 +102,7 @@
</Button>
<Button
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"
target="_blank"
>
@@ -111,7 +111,7 @@
</Button>
<Button
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"
target="_blank"
>

View File

@@ -5,10 +5,16 @@
import { getURLForLanguage } from '$lib/utils';
import { Languages } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte';
let {
class: className = '',
}: {
class?: string;
} = $props();
</script>
<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" />
<span class="ml-2 mr-auto">
{languages[i18n.lang]}

View File

@@ -1,29 +1,36 @@
<script lang="ts">
import { base } from '$app/paths';
import { mode } from 'mode-watcher';
import { base } from '$app/paths';
export let iconOnly = false;
export let company = 'gpx.studio';
let {
iconOnly = false,
company = 'gpx.studio',
...others
}: {
iconOnly?: boolean;
company?: 'gpx.studio' | 'mapbox' | 'github' | 'crowdin' | 'facebook' | 'x' | 'reddit';
[key: string]: any;
} = $props();
</script>
{#if company === 'gpx.studio'}
<img
src="{base}/{iconOnly ? 'icon' : 'logo'}{mode.current === 'dark' ? '-dark' : ''}.svg"
alt="Logo of gpx.studio."
{...$$restProps}
{...others}
/>
{:else if company === 'mapbox'}
<img
src="{base}/mapbox-logo-{mode.current === 'dark' ? 'white' : 'black'}.svg"
alt="Logo of Mapbox."
{...$$restProps}
{...others}
/>
{:else if company === 'github'}
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}"
class="fill-foreground {others.class ?? ''}"
><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"
/></svg
@@ -33,7 +40,7 @@
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}"
class="fill-foreground {others.class ?? ''}"
><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"
/></svg
@@ -43,7 +50,7 @@
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}"
class="fill-foreground {others.class ?? ''}"
><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"
/></svg
@@ -53,7 +60,7 @@
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}"
class="fill-foreground {others.class ?? ''}"
><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"
/></svg
@@ -63,7 +70,7 @@
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}"
class="fill-foreground {others.class ?? ''}"
><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"
/></svg

View File

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

View File

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

View File

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

View File

@@ -41,7 +41,7 @@
let canvas: HTMLCanvasElement;
let overlay: HTMLCanvasElement;
let elevationProfile: ElevationProfile;
let elevationProfile: ElevationProfile | null = null;
onMount(() => {
elevationProfile = new ElevationProfile(
@@ -55,7 +55,9 @@
});
onDestroy(() => {
if (elevationProfile) {
elevationProfile.destroy();
}
});
</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"
>
<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="max-w-[80%] text-sm">
<span class="w-12 shrink-0 text-center text-xl">⚠️</span>
<span class="text-sm">
{i18n._('menu.support_message')}
</span>
</div>
<div class="w-full flex flex-row flex-wrap gap-2">
<Button class="bg-support grow" href="https://ko-fi.com/gpxstudio" target="_blank">
{i18n._('menu.support_button')}
<span class="ml-2">🙏</span>
<span>🙏</span>
</Button>
<Button
variant="outline"
@@ -117,7 +117,7 @@
exportState.current = ExportState.NONE;
}}
>
<Download size="16" class="mr-1" />
<Download size="16" />
{#if $fileStateCollection.size === 1 || (exportState.current === ExportState.SELECTION && $selection.size === 1)}
{i18n._('menu.download_file')}
{:else}

View File

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

View File

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

View File

@@ -16,7 +16,7 @@
</script>
<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"
onclick={() => {
navigator.clipboard.writeText(
@@ -25,6 +25,6 @@
onCopy();
}}
>
<ClipboardCopy size="16" class="mr-1" />
<ClipboardCopy size="16" />
{i18n._('menu.copy_coordinates')}
</Button>

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@
import { map } from '$lib/components/map/map';
import CustomLayers from './CustomLayers.svelte';
import { settings } from '$lib/logic/settings';
import { untrack } from 'svelte';
const {
selectedBasemapTree,
@@ -26,6 +27,7 @@
selectedOverpassTree,
currentBasemap,
currentOverlays,
currentOverpassQueries,
customLayers,
opacities,
} = settings;
@@ -60,7 +62,9 @@
});
$effect(() => {
if ($selectedOverlayTree && $currentOverlays) {
if ($selectedOverlayTree) {
untrack(() => {
if ($currentOverlays) {
let overlayLayers = getLayers($currentOverlays);
let toRemove = Object.entries(overlayLayers).filter(
([id, checked]) => checked && !isSelected($selectedOverlayTree, id)
@@ -75,6 +79,29 @@
}
}
});
}
});
$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>
<Sheet.Root bind:open>

View File

@@ -5,7 +5,7 @@
import { i18n } from '$lib/i18n.svelte';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
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 { selection } from '$lib/logic/selection';
@@ -53,13 +53,14 @@
<div class="flex flex-row gap-3">
<div class="flex flex-col">
{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;
</div>
</div>
<Button
class="ml-auto p-1.5 h-8"
class="ml-auto"
variant="outline"
size="icon"
href="https://www.openstreetmap.org/edit?editor=id&{poi.item.type ??
'node'}={poi.item.id}"
target="_blank"
@@ -95,7 +96,7 @@
</div>
</ScrollArea>
<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')}
</Button>
</Card.Content>

View File

@@ -74,7 +74,7 @@ export class OverpassLayer {
let d = get(data);
try {
let source = this.map.getSource('overpass');
let source = this.map.getSource('overpass') as mapboxgl.GeoJSONSource | undefined;
if (source) {
source.setData(d);
} else {
@@ -284,9 +284,9 @@ function getQuery(query: 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) {
return arrayEntry[1]
return arrayEntry
.map(
(val) =>
`nwr${Object.entries(tags)

View File

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

View File

@@ -38,7 +38,9 @@
</script>
{#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">
<Card.Root class="rounded-md border-none py-2.5">
<Card.Content class="px-2.5">

View File

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

View File

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

View File

@@ -129,7 +129,7 @@
</Button>
</div>
{: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">
<Label class="justify-between">
<span class="flex flex-row items-center gap-1">

View File

@@ -15,7 +15,7 @@
</script>
<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">
{#if $canChangeStart}
<Button
@@ -23,7 +23,7 @@
variant="ghost"
onclick={() => element?.dispatchEvent(new CustomEvent('change-start'))}
>
<CirclePlay size="16" class="mr-1" />
<CirclePlay size="16" />
{i18n._('toolbar.routing.start_loop_here')}
</Button>
{/if}
@@ -32,7 +32,7 @@
variant="ghost"
onclick={() => element?.dispatchEvent(new CustomEvent('delete'))}
>
<Trash2 size="16" class="mr-1" />
<Trash2 size="16" />
{i18n._('menu.delete')}
<Shortcut shift={true} click={true} />
</Button>

View File

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

View File

@@ -99,7 +99,7 @@
disabled={!validSelection || !canCrop}
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>
<Separator />
<Label class="flex flex-row flex-wrap gap-3 items-center">

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
import { HeartHandshake } from '@lucide/svelte';
</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.
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';
</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.
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>
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 Routing from '$lib/components/toolbar/tools/routing/Routing.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.
### <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.

View File

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

View File

@@ -65,7 +65,7 @@
</div>
<div class="w-full flex flex-row justify-center gap-3">
<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')}
</Button>
<Button
@@ -73,7 +73,7 @@
href={getURLForLanguage(i18n.lang, '/help')}
class="w-1/3 min-w-fit"
>
<BookOpenText size="18" class="mr-1.5" />
<BookOpenText size="18" />
<span>{i18n._('menu.help')}</span>
</Button>
</div>
@@ -96,7 +96,7 @@
>
<div class="markdown text-center">
<h1>
<Route size="24" class="mr-1 inline-block align-baseline" />
<Route size="24" class="inline-block align-baseline" />
{i18n._('homepage.route_planning')}
</h1>
<p class="text-muted-foreground">{i18n._('homepage.route_planning_description')}</p>
@@ -112,7 +112,7 @@
>
<div class="markdown text-center md:hidden">
<h1>
<PencilRuler size="24" class="mr-1 inline-block align-baseline" />
<PencilRuler size="24" class="inline-block align-baseline" />
{i18n._('homepage.file_processing')}
</h1>
<p class="text-muted-foreground">
@@ -124,7 +124,7 @@
</div>
<div class="markdown text-center hidden md:block">
<h1>
<PencilRuler size="24" class="mr-1 inline-block align-baseline" />
<PencilRuler size="24" class="inline-block align-baseline" />
{i18n._('homepage.file_processing')}
</h1>
<p class="text-muted-foreground">
@@ -139,7 +139,7 @@
>
<div class="markdown text-center">
<h1>
<Map size="24" class="mr-1 inline-block align-baseline" />
<Map size="24" class="inline-block align-baseline" />
{i18n._('homepage.maps')}
</h1>
<p class="text-muted-foreground">{i18n._('homepage.maps_description')}</p>
@@ -182,7 +182,7 @@
<div class="px-8 md:px-12">
<div class="markdown text-center px-4 md:px-12">
<h1>
<ChartArea size="24" class="mr-1 inline-block align-baseline" />
<ChartArea size="24" class="inline-block align-baseline" />
{i18n._('homepage.data_visualization')}
</h1>
<p class="text-muted-foreground mb-6">
@@ -214,7 +214,7 @@
>
<div class="markdown text-center md:hidden">
<h1>
<Scale size="24" class="mr-1 inline-block align-baseline" />
<Scale size="24" class="inline-block align-baseline" />
{i18n._('homepage.identity')}
</h1>
<p class="text-muted-foreground">{i18n._('homepage.identity_description')}</p>
@@ -224,7 +224,7 @@
</a>
<div class="markdown text-center hidden md:block">
<h1>
<Scale size="24" class="mr-1 inline-block align-baseline" />
<Scale size="24" class="inline-block align-baseline" />
{i18n._('homepage.identity')}
</h1>
<p class="text-muted-foreground">{i18n._('homepage.identity_description')}</p>
@@ -241,7 +241,7 @@
</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
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} />
{/await}
<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>
</Button>
</div>
@@ -261,7 +261,7 @@
<DocsContainer module={translationModule.default} />
{/await}
<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>
</Button>
</div>

View File

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