mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-09-02 16:52:31 +00:00
sonner on routing error, and dark mode
This commit is contained in:
18
website/package-lock.json
generated
18
website/package-lock.json
generated
@@ -15,8 +15,10 @@
|
|||||||
"gpx": "file:../gpx",
|
"gpx": "file:../gpx",
|
||||||
"lucide-svelte": "^0.365.0",
|
"lucide-svelte": "^0.365.0",
|
||||||
"mapbox-gl": "^3.2.0",
|
"mapbox-gl": "^3.2.0",
|
||||||
|
"mode-watcher": "^0.3.0",
|
||||||
"sortablejs": "^1.15.2",
|
"sortablejs": "^1.15.2",
|
||||||
"svelte-i18n": "^4.0.0",
|
"svelte-i18n": "^4.0.0",
|
||||||
|
"svelte-sonner": "^0.3.22",
|
||||||
"tailwind-merge": "^2.2.2",
|
"tailwind-merge": "^2.2.2",
|
||||||
"tailwind-variants": "^0.2.1"
|
"tailwind-variants": "^0.2.1"
|
||||||
},
|
},
|
||||||
@@ -3939,6 +3941,14 @@
|
|||||||
"mkdirp": "bin/cmd.js"
|
"mkdirp": "bin/cmd.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mode-watcher": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-k8jjuTx94HaaRKWO6JDf8wL761hFatrTIHJKl+E+3JWcnv+GnMBH062zcLsy0lbCI3n7RZxxHaWi66auFnUO4g==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mri": {
|
"node_modules/mri": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||||
@@ -5745,6 +5755,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svelte-sonner": {
|
||||||
|
"version": "0.3.22",
|
||||||
|
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.22.tgz",
|
||||||
|
"integrity": "sha512-1AEBl7rTP4oeMAmBmkcvoHNOwB8gPzz73RYApcY8pyDwbjBewU8ATnXV8N42omV1sQvtSX/X0o5A1nfkN3T6cg==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"svelte": ">=3 <5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tabbable": {
|
"node_modules/tabbable": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||||
|
@@ -48,8 +48,10 @@
|
|||||||
"gpx": "file:../gpx",
|
"gpx": "file:../gpx",
|
||||||
"lucide-svelte": "^0.365.0",
|
"lucide-svelte": "^0.365.0",
|
||||||
"mapbox-gl": "^3.2.0",
|
"mapbox-gl": "^3.2.0",
|
||||||
|
"mode-watcher": "^0.3.0",
|
||||||
"sortablejs": "^1.15.2",
|
"sortablejs": "^1.15.2",
|
||||||
"svelte-i18n": "^4.0.0",
|
"svelte-i18n": "^4.0.0",
|
||||||
|
"svelte-sonner": "^0.3.22",
|
||||||
"tailwind-merge": "^2.2.2",
|
"tailwind-merge": "^2.2.2",
|
||||||
"tailwind-variants": "^0.2.1"
|
"tailwind-variants": "^0.2.1"
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
import Menu from '$lib/components/Menu.svelte';
|
import Menu from '$lib/components/Menu.svelte';
|
||||||
import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
|
import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
|
||||||
import LayerControl from '$lib/components/layer-control/LayerControl.svelte';
|
import LayerControl from '$lib/components/layer-control/LayerControl.svelte';
|
||||||
|
import { Toaster } from '$lib/components/ui/sonner';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col w-screen h-screen">
|
<div class="flex flex-col w-screen h-screen">
|
||||||
@@ -17,9 +18,18 @@
|
|||||||
<LayerControl />
|
<LayerControl />
|
||||||
<GPXLayers />
|
<GPXLayers />
|
||||||
<FileList />
|
<FileList />
|
||||||
|
<Toaster richColors />
|
||||||
</div>
|
</div>
|
||||||
<div class="h-48 flex flex-row gap-2 overflow-hidden">
|
<div class="h-48 flex flex-row gap-2 overflow-hidden">
|
||||||
<GPXData />
|
<GPXData />
|
||||||
<ElevationProfile />
|
<ElevationProfile />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
div :global(.toaster.group) {
|
||||||
|
@apply absolute;
|
||||||
|
@apply right-2;
|
||||||
|
--offset: 50px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fileOrder, files, getFileIndex, selectedFiles, selectFiles } from '$lib/stores';
|
import { fileOrder, files, getFileIndex, selectedFiles, selectFiles } from '$lib/stores';
|
||||||
|
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index';
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index';
|
||||||
import Sortable from 'sortablejs/Sortable';
|
import Sortable from 'sortablejs/Sortable';
|
||||||
|
|
||||||
@@ -9,8 +10,8 @@
|
|||||||
import { afterUpdate, onMount } from 'svelte';
|
import { afterUpdate, onMount } from 'svelte';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
let tabs: HTMLDivElement;
|
let container: HTMLDivElement;
|
||||||
let buttons: HTMLButtonElement[] = [];
|
let buttons: HTMLDivElement[] = [];
|
||||||
let sortable: Sortable;
|
let sortable: Sortable;
|
||||||
|
|
||||||
function selectFile(file: GPXFile) {
|
function selectFile(file: GPXFile) {
|
||||||
@@ -60,7 +61,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
sortable = Sortable.create(tabs, {
|
sortable = Sortable.create(container, {
|
||||||
forceAutoScrollFallback: true,
|
forceAutoScrollFallback: true,
|
||||||
multiDrag: true,
|
multiDrag: true,
|
||||||
multiDragKey: 'shift',
|
multiDragKey: 'shift',
|
||||||
@@ -125,22 +126,29 @@
|
|||||||
|
|
||||||
<div class="h-10 -translate-y-10 w-full pointer-events-none">
|
<div class="h-10 -translate-y-10 w-full pointer-events-none">
|
||||||
<ScrollArea orientation="horizontal" class="w-full h-full" scrollbarXClasses="h-2">
|
<ScrollArea orientation="horizontal" class="w-full h-full" scrollbarXClasses="h-2">
|
||||||
<div bind:this={tabs} class="flex flex-row gap-1">
|
<div bind:this={container} class="flex flex-row gap-1">
|
||||||
{#each $files as file, index}
|
{#each $files as file, index}
|
||||||
<button
|
<div
|
||||||
bind:this={buttons[index]}
|
bind:this={buttons[index]}
|
||||||
data-id={index}
|
data-id={index}
|
||||||
class="my-1 px-1.5 py-1 rounded bg-secondary hover:bg-gray-200 shadow-none first:ml-1 last:mr-1 pointer-events-auto"
|
class="pointer-events-auto first:ml-1 last:mr-1 mb-1 bg-transparent"
|
||||||
>
|
>
|
||||||
|
<Button variant="outline" class="h-9 px-1.5 py-1 border-none shadow-md">
|
||||||
{get(file).metadata.name}
|
{get(file).metadata.name}
|
||||||
</button>
|
</Button>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
div :global(.sortable-selected) {
|
div :global(button) {
|
||||||
@apply bg-background shadow;
|
@apply bg-accent;
|
||||||
|
@apply hover:bg-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.sortable-selected > button) {
|
||||||
|
@apply bg-background;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -162,11 +162,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-bottom-left) {
|
div :global(.mapboxgl-ctrl-bottom-left) {
|
||||||
@apply bottom-9;
|
@apply bottom-[42px];
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-bottom-right) {
|
div :global(.mapboxgl-ctrl-bottom-right) {
|
||||||
@apply bottom-9;
|
@apply bottom-[42px];
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-popup) {
|
div :global(.mapboxgl-popup) {
|
||||||
|
@@ -17,10 +17,29 @@
|
|||||||
settings
|
settings
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
|
|
||||||
|
import { mode, resetMode, setMode } from 'mode-watcher';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
|
import { derived, get } from 'svelte/store';
|
||||||
|
|
||||||
let showDistanceMarkers = false;
|
let showDistanceMarkers = false;
|
||||||
let showDirectionMarkers = false;
|
let showDirectionMarkers = false;
|
||||||
|
|
||||||
|
let currentMode = derived(mode, ($mode) => {
|
||||||
|
if (!$mode) {
|
||||||
|
return 'system';
|
||||||
|
} else {
|
||||||
|
return $mode;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$: if ($settings.mode !== get(currentMode)) {
|
||||||
|
if ($settings.mode === 'system') {
|
||||||
|
resetMode();
|
||||||
|
} else {
|
||||||
|
setMode($settings.mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="absolute top-2 left-0 right-0 z-20 flex flex-row justify-center pointer-events-none">
|
<div class="absolute top-2 left-0 right-0 z-20 flex flex-row justify-center pointer-events-none">
|
||||||
@@ -126,6 +145,17 @@
|
|||||||
</Menubar.SubContent>
|
</Menubar.SubContent>
|
||||||
</Menubar.Sub>
|
</Menubar.Sub>
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
|
<Menubar.Sub>
|
||||||
|
<Menubar.SubTrigger inset>{$_('menu.mode')}</Menubar.SubTrigger>
|
||||||
|
<Menubar.SubContent>
|
||||||
|
<Menubar.RadioGroup bind:value={$settings.mode}>
|
||||||
|
<Menubar.RadioItem value="light">{$_('menu.light')}</Menubar.RadioItem>
|
||||||
|
<Menubar.RadioItem value="dark">{$_('menu.dark')}</Menubar.RadioItem>
|
||||||
|
<Menubar.RadioItem value="system">{$_('menu.system')}</Menubar.RadioItem>
|
||||||
|
</Menubar.RadioGroup>
|
||||||
|
</Menubar.SubContent>
|
||||||
|
</Menubar.Sub>
|
||||||
|
<Menubar.Separator />
|
||||||
<Menubar.CheckboxItem bind:checked={showDistanceMarkers}>
|
<Menubar.CheckboxItem bind:checked={showDistanceMarkers}>
|
||||||
{$_('menu.distance_markers')}
|
{$_('menu.distance_markers')}
|
||||||
</Menubar.CheckboxItem>
|
</Menubar.CheckboxItem>
|
||||||
|
@@ -38,6 +38,12 @@ async function getRoute(points: Coordinates[], brouterProfile: string, privateRo
|
|||||||
let url = `https://routing.gpx.studio?lonlats=${points.map(point => `${point.lon.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile + (privateRoads ? '-private' : '')}&format=geojson&alternativeidx=0`;
|
let url = `https://routing.gpx.studio?lonlats=${points.map(point => `${point.lon.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile + (privateRoads ? '-private' : '')}&format=geojson&alternativeidx=0`;
|
||||||
|
|
||||||
let response = await fetch(url);
|
let response = await fetch(url);
|
||||||
|
|
||||||
|
// Check if the response is ok
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`${await response.text()}`);
|
||||||
|
}
|
||||||
|
|
||||||
let geojson = await response.json();
|
let geojson = await response.json();
|
||||||
|
|
||||||
let route: TrackPoint[] = [];
|
let route: TrackPoint[] = [];
|
||||||
@@ -66,7 +72,7 @@ async function getRoute(points: Coordinates[], brouterProfile: string, privateRo
|
|||||||
coordinates[i][1] == Number(messages[messageIdx][latIdx]) / 1000000) {
|
coordinates[i][1] == Number(messages[messageIdx][latIdx]) / 1000000) {
|
||||||
messageIdx++;
|
messageIdx++;
|
||||||
|
|
||||||
if (messageIdx == messages.length) surface = "missing";
|
if (messageIdx == messages.length) surface = "unknown";
|
||||||
else surface = getSurface(messages[messageIdx][tagIdx]);
|
else surface = getSurface(messages[messageIdx][tagIdx]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,11 @@ import { get, type Writable } from "svelte/store";
|
|||||||
import { computeAnchorPoints, type SimplifiedTrackPoint } from "./Simplify";
|
import { computeAnchorPoints, type SimplifiedTrackPoint } from "./Simplify";
|
||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl from "mapbox-gl";
|
||||||
import { route } from "./Routing";
|
import { route } from "./Routing";
|
||||||
import { applyToFileElement, applyToFileStore } from "$lib/stores";
|
import { applyToFileElement } from "$lib/stores";
|
||||||
|
|
||||||
|
import { toast } from "svelte-sonner";
|
||||||
|
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
export class RoutingControls {
|
export class RoutingControls {
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
@@ -100,7 +104,7 @@ export class RoutingControls {
|
|||||||
|
|
||||||
createMarker(anchor: SimplifiedTrackPoint) {
|
createMarker(anchor: SimplifiedTrackPoint) {
|
||||||
let element = document.createElement('div');
|
let element = document.createElement('div');
|
||||||
element.className = `h-3 w-3 rounded-full bg-background border-2 border-black cursor-pointer`;
|
element.className = `h-3 w-3 rounded-full bg-white border-2 border-black cursor-pointer`;
|
||||||
|
|
||||||
let marker = new mapboxgl.Marker({
|
let marker = new mapboxgl.Marker({
|
||||||
draggable: true,
|
draggable: true,
|
||||||
@@ -380,7 +384,14 @@ export class RoutingControls {
|
|||||||
try {
|
try {
|
||||||
response = await route(targetCoordinates);
|
response = await route(targetCoordinates);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.log(e);
|
||||||
|
if (e.message.includes('from-position not mapped in existing datafile')) {
|
||||||
|
toast.error(get(_)("toolbar.routing.error.from"));
|
||||||
|
} else if (e.message.includes('to-position not mapped in existing datafile')) {
|
||||||
|
toast.error(get(_)("toolbar.routing.error.to"));
|
||||||
|
} else {
|
||||||
|
toast.error(e.message);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1
website/src/lib/components/ui/sonner/index.ts
Normal file
1
website/src/lib/components/ui/sonner/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as Toaster } from "./sonner.svelte";
|
20
website/src/lib/components/ui/sonner/sonner.svelte
Normal file
20
website/src/lib/components/ui/sonner/sonner.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
|
||||||
|
import { mode } from "mode-watcher";
|
||||||
|
|
||||||
|
type $$Props = SonnerProps;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sonner
|
||||||
|
theme={$mode}
|
||||||
|
class="toaster group"
|
||||||
|
toastOptions={{
|
||||||
|
classes: {
|
||||||
|
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||||
|
description: "group-[.toast]:text-muted-foreground",
|
||||||
|
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||||
|
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...$$restProps}
|
||||||
|
/>
|
@@ -12,6 +12,7 @@ export const settings = writable<{ [key: string]: any }>({
|
|||||||
distanceUnits: 'metric',
|
distanceUnits: 'metric',
|
||||||
velocityUnits: 'speed',
|
velocityUnits: 'speed',
|
||||||
temperatureUnits: 'celsius',
|
temperatureUnits: 'celsius',
|
||||||
|
mode: 'system'
|
||||||
});
|
});
|
||||||
export enum Tool {
|
export enum Tool {
|
||||||
ROUTING
|
ROUTING
|
||||||
|
@@ -20,6 +20,10 @@
|
|||||||
"temperature_units": "Temperature units",
|
"temperature_units": "Temperature units",
|
||||||
"celsius": "Celsius",
|
"celsius": "Celsius",
|
||||||
"fahrenheit": "Fahrenheit",
|
"fahrenheit": "Fahrenheit",
|
||||||
|
"mode": "Mode",
|
||||||
|
"light": "Light",
|
||||||
|
"dark": "Dark",
|
||||||
|
"system": "System",
|
||||||
"distance_markers": "Show distance markers",
|
"distance_markers": "Show distance markers",
|
||||||
"direction_markers": "Show direction markers",
|
"direction_markers": "Show direction markers",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
@@ -42,6 +46,10 @@
|
|||||||
"motorcycle": "Motorcycle",
|
"motorcycle": "Motorcycle",
|
||||||
"water": "Water",
|
"water": "Water",
|
||||||
"railway": "Railway"
|
"railway": "Railway"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"from": "The start point is too far from the nearest road",
|
||||||
|
"to": "The end point is too far from the nearest road"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"time_tooltip": "Change the time and speed data",
|
"time_tooltip": "Change the time and speed data",
|
||||||
|
@@ -1,5 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import '../app.pcss';
|
import '../app.pcss';
|
||||||
|
|
||||||
|
import { ModeWatcher } from 'mode-watcher';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<ModeWatcher />
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
Reference in New Issue
Block a user