mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-12-02 01:52:12 +00:00
Compare commits
12 Commits
b2a5462372
...
01240c4f2a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01240c4f2a | ||
|
|
431a9ce827 | ||
|
|
20ab41c3b4 | ||
|
|
3f4ea27be2 | ||
|
|
5bb5b2f8c8 | ||
|
|
e9bb9e27bb | ||
|
|
ee1dd1fae7 | ||
|
|
738530a960 | ||
|
|
16023b0688 | ||
|
|
bce7b5984f | ||
|
|
4e5d7d391a | ||
|
|
0554a85e01 |
2
website/.gitignore
vendored
2
website/.gitignore
vendored
@@ -8,3 +8,5 @@ node_modules
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
static/*.webmanifest
|
||||
!static/en.manifest.webmanifest
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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');
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
elevationProfile.destroy();
|
||||
if (elevationProfile) {
|
||||
elevationProfile.destroy();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
open = false;
|
||||
}}
|
||||
>
|
||||
<Save size="16" class="mr-1" />
|
||||
<Save size="16" />
|
||||
{i18n._('menu.metadata.save')}
|
||||
</Button>
|
||||
</Popover.Content>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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,19 +62,44 @@
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if ($selectedOverlayTree && $currentOverlays) {
|
||||
let overlayLayers = getLayers($currentOverlays);
|
||||
let toRemove = Object.entries(overlayLayers).filter(
|
||||
([id, checked]) => checked && !isSelected($selectedOverlayTree, id)
|
||||
);
|
||||
if (toRemove.length > 0) {
|
||||
currentOverlays.update((tree) => {
|
||||
toRemove.forEach(([id]) => {
|
||||
toggle(tree, id);
|
||||
});
|
||||
return tree;
|
||||
});
|
||||
}
|
||||
if ($selectedOverlayTree) {
|
||||
untrack(() => {
|
||||
if ($currentOverlays) {
|
||||
let overlayLayers = getLayers($currentOverlays);
|
||||
let toRemove = Object.entries(overlayLayers).filter(
|
||||
([id, checked]) => checked && !isSelected($selectedOverlayTree, id)
|
||||
);
|
||||
if (toRemove.length > 0) {
|
||||
currentOverlays.update((tree) => {
|
||||
toRemove.forEach(([id]) => {
|
||||
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>
|
||||
|
||||
@@ -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)}° {poi.item.lon.toFixed(6)}°
|
||||
</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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -135,12 +135,19 @@ export class MapillaryLayer {
|
||||
}
|
||||
|
||||
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.moveTo(e.features[0].properties.id);
|
||||
this.viewer.resize();
|
||||
this.viewer.moveTo(e.features[0].properties.id);
|
||||
|
||||
mapCursor.notify(MapCursorState.MAPILLARY_HOVER, true);
|
||||
mapCursor.notify(MapCursorState.MAPILLARY_HOVER, true);
|
||||
}
|
||||
}
|
||||
|
||||
onMouseLeave() {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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')}>
|
||||
|
||||
@@ -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')}>
|
||||
|
||||
@@ -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')}>
|
||||
|
||||
@@ -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')}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user