4 Commits

Author SHA1 Message Date
vcoppe
05df3ca064 start fixing elevation profile 2025-10-18 20:12:19 +02:00
vcoppe
356884cf58 starting to fix time tool 2025-10-18 19:21:10 +02:00
vcoppe
e68da7354e update shadcn components 2025-10-18 18:51:11 +02:00
vcoppe
c59cd66141 fix tools 2025-10-18 16:10:08 +02:00
88 changed files with 1931 additions and 1682 deletions

View File

@@ -1,5 +1,6 @@
{ {
"$schema": "https://next.shadcn-svelte.com/schema.json", "$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": { "tailwind": {
"css": "src/app.css", "css": "src/app.css",
"baseColor": "slate" "baseColor": "slate"
@@ -12,5 +13,5 @@
"lib": "$lib" "lib": "$lib"
}, },
"typescript": true, "typescript": true,
"registry": "https://next.shadcn-svelte.com/registry" "registry": "https://shadcn-svelte.com/registry"
} }

View File

@@ -27,11 +27,10 @@
"png.js": "^0.2.1", "png.js": "^0.2.1",
"sanitize-html": "^2.17.0", "sanitize-html": "^2.17.0",
"sortablejs": "^1.15.6", "sortablejs": "^1.15.6",
"tailwind-merge": "^3.3.0", "tailwind-merge": "^3.3.0"
"tailwind-variants": "^1.0.0"
}, },
"devDependencies": { "devDependencies": {
"@lucide/svelte": "^0.513.0", "@lucide/svelte": "^0.544.0",
"@sveltejs/adapter-static": "^3.0.8", "@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/enhanced-img": "^0.6.0", "@sveltejs/enhanced-img": "^0.6.0",
"@sveltejs/kit": "^2.21.2", "@sveltejs/kit": "^2.21.2",
@@ -48,7 +47,7 @@
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^8.33.1", "@typescript-eslint/eslint-plugin": "^8.33.1",
"@typescript-eslint/parser": "^8.33.1", "@typescript-eslint/parser": "^8.33.1",
"bits-ui": "^2.5.0", "bits-ui": "^2.12.0",
"eslint": "^9.28.0", "eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "^3.9.1", "eslint-plugin-svelte": "^3.9.1",
@@ -56,14 +55,15 @@
"glob": "^11.0.2", "glob": "^11.0.2",
"lucide-static": "^0.513.0", "lucide-static": "^0.513.0",
"mdsvex": "^0.12.6", "mdsvex": "^0.12.6",
"mode-watcher": "^1.0.7", "mode-watcher": "^1.1.0",
"paneforge": "^1.0.0-next.5", "paneforge": "^1.0.0-next.5",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.4.0", "prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.33.18", "svelte": "^5.33.18",
"svelte-check": "^4.0.0", "svelte-check": "^4.0.0",
"svelte-sonner": "^1.0.4", "svelte-sonner": "^1.0.5",
"tailwind-variants": "^3.1.1",
"tailwindcss": "^4.1.8", "tailwindcss": "^4.1.8",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"tsx": "^4.19.1", "tsx": "^4.19.1",
@@ -1625,9 +1625,9 @@
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
}, },
"node_modules/@lucide/svelte": { "node_modules/@lucide/svelte": {
"version": "0.513.0", "version": "0.544.0",
"resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.513.0.tgz", "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.544.0.tgz",
"integrity": "sha512-XwBQMQkMlr9qp9yVg+epx5MzhBBrqul8atO00y/ZfhlKRJuQZVmq3ELibApqyBtj9ys0Ai4FH/SZcODTUFYXig==", "integrity": "sha512-9f9O6uxng2pLB01sxNySHduJN3HTl5p0HDu4H26VR51vhZfiMzyOMe9Mhof3XAk4l813eTtl+/DYRvGyoRR+yw==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peerDependencies": { "peerDependencies": {
@@ -3233,23 +3233,21 @@
] ]
}, },
"node_modules/bits-ui": { "node_modules/bits-ui": {
"version": "2.5.0", "version": "2.12.0",
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.5.0.tgz", "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.12.0.tgz",
"integrity": "sha512-PbjylA1UWd4A/c5AYqie/EVxQ1/8uugmJKLg9whLoBBHbfPEBGhK09dCPrahK9kA6DRHhMmij0XXIUGIfrmNow==", "integrity": "sha512-8NF4ILNyAJlIxDXpl/akGXGBV5QmZAe+8gTfPttM5P6/+LrijumcSfFXY5cr4QkXwTmLA7H5stYpbgJf2XFJvg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@floating-ui/core": "^1.7.0", "@floating-ui/core": "^1.7.1",
"@floating-ui/dom": "^1.7.0", "@floating-ui/dom": "^1.7.1",
"css.escape": "^1.5.1",
"esm-env": "^1.1.2", "esm-env": "^1.1.2",
"runed": "^0.28.0", "runed": "^0.35.1",
"svelte-toolbelt": "^0.9.1", "svelte-toolbelt": "^0.10.6",
"tabbable": "^6.2.0" "tabbable": "^6.2.0"
}, },
"engines": { "engines": {
"node": ">=20", "node": ">=20"
"pnpm": ">=8.7.0"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/huntabyte" "url": "https://github.com/sponsors/huntabyte"
@@ -3260,9 +3258,9 @@
} }
}, },
"node_modules/bits-ui/node_modules/runed": { "node_modules/bits-ui/node_modules/runed": {
"version": "0.28.0", "version": "0.35.1",
"resolved": "https://registry.npmjs.org/runed/-/runed-0.28.0.tgz", "resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz",
"integrity": "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==", "integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==",
"dev": true, "dev": true,
"funding": [ "funding": [
"https://github.com/sponsors/huntabyte", "https://github.com/sponsors/huntabyte",
@@ -3270,23 +3268,31 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esm-env": "^1.0.0" "dequal": "^2.0.3",
"esm-env": "^1.0.0",
"lz-string": "^1.5.0"
}, },
"peerDependencies": { "peerDependencies": {
"@sveltejs/kit": "^2.21.0",
"svelte": "^5.7.0" "svelte": "^5.7.0"
},
"peerDependenciesMeta": {
"@sveltejs/kit": {
"optional": true
}
} }
}, },
"node_modules/bits-ui/node_modules/svelte-toolbelt": { "node_modules/bits-ui/node_modules/svelte-toolbelt": {
"version": "0.9.1", "version": "0.10.6",
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.9.1.tgz", "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz",
"integrity": "sha512-wBX6MtYw/kpht80j5zLpxJyR9soLizXPIAIWEVd9llAi17SR44ZdG291bldjB7r/K5duC0opDFcuhk2cA1hb8g==", "integrity": "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
"https://github.com/sponsors/huntabyte" "https://github.com/sponsors/huntabyte"
], ],
"dependencies": { "dependencies": {
"clsx": "^2.1.1", "clsx": "^2.1.1",
"runed": "^0.28.0", "runed": "^0.35.1",
"style-to-object": "^1.0.8" "style-to-object": "^1.0.8"
}, },
"engines": { "engines": {
@@ -3891,13 +3897,6 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
"dev": true,
"license": "MIT"
},
"node_modules/csscolorparser": { "node_modules/csscolorparser": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz",
@@ -4053,6 +4052,16 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/des.js": { "node_modules/des.js": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
@@ -6019,6 +6028,16 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"bin": {
"lz-string": "bin/bin.js"
}
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.17", "version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
@@ -6369,9 +6388,9 @@
} }
}, },
"node_modules/mode-watcher": { "node_modules/mode-watcher": {
"version": "1.0.7", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.0.7.tgz", "resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.1.0.tgz",
"integrity": "sha512-ZGA7ZGdOvBJeTQkzdBOnXSgTkO6U6iIFWJoyGCTt6oHNg9XP9NBvS26De+V4W2aqI+B0yYXUskFG2VnEo3zyMQ==", "integrity": "sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -8292,22 +8311,22 @@
} }
}, },
"node_modules/svelte-sonner": { "node_modules/svelte-sonner": {
"version": "1.0.4", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.0.4.tgz", "resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.0.5.tgz",
"integrity": "sha512-ctm9jeV0Rf3im2J6RU1emccrJFjRSdNSPsLlxaF62TLZw9bB1D40U/U7+wqEgohJY/X7FBdghdj0BFQF/IqKPQ==", "integrity": "sha512-9dpGPFqKb/QWudYqGnEz93vuY+NgCEvyNvxoCLMVGw6sDN/3oVeKV1xiEirW2E1N3vJEyj5imSBNOGltQHA7mg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"runed": "^0.26.0" "runed": "^0.28.0"
}, },
"peerDependencies": { "peerDependencies": {
"svelte": "^5.0.0" "svelte": "^5.0.0"
} }
}, },
"node_modules/svelte-sonner/node_modules/runed": { "node_modules/svelte-sonner/node_modules/runed": {
"version": "0.26.0", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/runed/-/runed-0.26.0.tgz", "resolved": "https://registry.npmjs.org/runed/-/runed-0.28.0.tgz",
"integrity": "sha512-qWFv0cvLVRd8pdl/AslqzvtQyEn5KaIugEernwg9G98uJVSZcs/ygvPBvF80LA46V8pwRvSKnaVLDI3+i2wubw==", "integrity": "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
"https://github.com/sponsors/huntabyte", "https://github.com/sponsors/huntabyte",
@@ -8376,35 +8395,30 @@
} }
}, },
"node_modules/tailwind-variants": { "node_modules/tailwind-variants": {
"version": "1.0.0", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-1.0.0.tgz", "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.1.1.tgz",
"integrity": "sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA==", "integrity": "sha512-ftLXe3krnqkMHsuBTEmaVUXYovXtPyTK7ckEfDRXS8PBZx0bAUas+A0jYxuKA5b8qg++wvQ3d2MQ7l/xeZxbZQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": {
"tailwind-merge": "3.0.2"
},
"engines": { "engines": {
"node": ">=16.x", "node": ">=16.x",
"pnpm": ">=7.x" "pnpm": ">=7.x"
}, },
"peerDependencies": { "peerDependencies": {
"tailwind-merge": ">=3.0.0",
"tailwindcss": "*" "tailwindcss": "*"
}
}, },
"node_modules/tailwind-variants/node_modules/tailwind-merge": { "peerDependenciesMeta": {
"version": "3.0.2", "tailwind-merge": {
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz", "optional": true
"integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==", }
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
} }
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.1.8", "version": "4.1.8",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz",
"integrity": "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og==", "integrity": "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/tapable": { "node_modules/tapable": {

View File

@@ -14,7 +14,7 @@
"format": "prettier --write ." "format": "prettier --write ."
}, },
"devDependencies": { "devDependencies": {
"@lucide/svelte": "^0.513.0", "@lucide/svelte": "^0.544.0",
"@sveltejs/adapter-static": "^3.0.8", "@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/enhanced-img": "^0.6.0", "@sveltejs/enhanced-img": "^0.6.0",
"@sveltejs/kit": "^2.21.2", "@sveltejs/kit": "^2.21.2",
@@ -31,7 +31,7 @@
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^8.33.1", "@typescript-eslint/eslint-plugin": "^8.33.1",
"@typescript-eslint/parser": "^8.33.1", "@typescript-eslint/parser": "^8.33.1",
"bits-ui": "^2.5.0", "bits-ui": "^2.12.0",
"eslint": "^9.28.0", "eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "^3.9.1", "eslint-plugin-svelte": "^3.9.1",
@@ -39,14 +39,15 @@
"glob": "^11.0.2", "glob": "^11.0.2",
"lucide-static": "^0.513.0", "lucide-static": "^0.513.0",
"mdsvex": "^0.12.6", "mdsvex": "^0.12.6",
"mode-watcher": "^1.0.7", "mode-watcher": "^1.1.0",
"paneforge": "^1.0.0-next.5", "paneforge": "^1.0.0-next.5",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.4.0", "prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.33.18", "svelte": "^5.33.18",
"svelte-check": "^4.0.0", "svelte-check": "^4.0.0",
"svelte-sonner": "^1.0.4", "svelte-sonner": "^1.0.5",
"tailwind-variants": "^3.1.1",
"tailwindcss": "^4.1.8", "tailwindcss": "^4.1.8",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"tsx": "^4.19.1", "tsx": "^4.19.1",
@@ -77,7 +78,6 @@
"png.js": "^0.2.1", "png.js": "^0.2.1",
"sanitize-html": "^2.17.0", "sanitize-html": "^2.17.0",
"sortablejs": "^1.15.6", "sortablejs": "^1.15.6",
"tailwind-merge": "^3.3.0", "tailwind-merge": "^3.3.0"
"tailwind-variants": "^1.0.0"
} }
} }

View File

@@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import ButtonWithTooltip from '$lib/components/ButtonWithTooltip.svelte'; import ButtonWithTooltip from '$lib/components/ButtonWithTooltip.svelte';
import * as Popover from '$lib/components/ui/popover'; import * as Popover from '$lib/components/ui/popover/index.js';
import * as ToggleGroup from '$lib/components/ui/toggle-group'; import * as ToggleGroup from '$lib/components/ui/toggle-group/index.js';
import Chart from 'chart.js/auto'; import Chart from 'chart.js/auto';
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import { map } from '$lib/stores';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { import {
BrickWall, BrickWall,
@@ -20,7 +19,6 @@
Construction, Construction,
} from '@lucide/svelte'; } from '@lucide/svelte';
import { getSlopeColor, getSurfaceColor, getHighwayColor } from '$lib/assets/colors'; import { getSlopeColor, getSurfaceColor, getHighwayColor } from '$lib/assets/colors';
import { _, df } from '$lib/i18n.svelte';
import { import {
getCadenceWithUnits, getCadenceWithUnits,
getConvertedDistance, getConvertedDistance,
@@ -35,19 +33,29 @@
getTemperatureWithUnits, getTemperatureWithUnits,
getVelocityWithUnits, getVelocityWithUnits,
} from '$lib/units'; } from '$lib/units';
import type { Writable } from 'svelte/store'; import type { Readable, Writable } from 'svelte/store';
import type { GPXStatistics } from 'gpx'; import type { GPXStatistics } from 'gpx';
import { settings } from '$lib/db';
import { mode } from 'mode-watcher'; import { mode } from 'mode-watcher';
import { settings } from '$lib/logic/settings';
export let gpxStatistics: Writable<GPXStatistics>; import { map } from '$lib/components/map/map';
export let slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>; import { i18n } from '$lib/i18n.svelte';
export let additionalDatasets: string[];
export let elevationFill: 'slope' | 'surface' | 'highway' | undefined;
export let showControls: boolean = true;
const { distanceUnits, velocityUnits, temperatureUnits } = settings; const { distanceUnits, velocityUnits, temperatureUnits } = settings;
let {
gpxStatistics,
slicedGPXStatistics,
additionalDatasets = $bindable(),
elevationFill = $bindable(),
showControls = true,
}: {
gpxStatistics: Readable<GPXStatistics>;
slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
additionalDatasets: string[];
elevationFill: 'slope' | 'surface' | 'highway' | undefined;
showControls?: boolean;
} = $props();
let canvas: HTMLCanvasElement; let canvas: HTMLCanvasElement;
let overlay: HTMLCanvasElement; let overlay: HTMLCanvasElement;
let chart: Chart; let chart: Chart;
@@ -179,7 +187,7 @@
if (point.time) { if (point.time) {
labels.push( labels.push(
` ${i18n._('quantities.time')}: ${$df.format(point.time)}` ` ${i18n._('quantities.time')}: ${i18n.df.format(point.time)}`
); );
} }
@@ -356,9 +364,9 @@
canvas.addEventListener('pointerup', onMouseUp); canvas.addEventListener('pointerup', onMouseUp);
}); });
$: if (chart && $distanceUnits && $velocityUnits && $temperatureUnits) { $effect(() => {
let data = $gpxStatistics; let data = $gpxStatistics;
if (chart && $distanceUnits && $velocityUnits && $temperatureUnits) {
// update data // update data
chart.data.datasets[0] = { chart.data.datasets[0] = {
label: i18n._('quantities.elevation'), label: i18n._('quantities.elevation'),
@@ -446,6 +454,7 @@
chart.update(); chart.update();
} }
});
function slopeFillCallback(context) { function slopeFillCallback(context) {
return getSlopeColor(context.p0.raw.slope.segment); return getSlopeColor(context.p0.raw.slope.segment);
@@ -463,7 +472,8 @@
); );
} }
$: if (chart) { $effect(() => {
if (elevationFill && chart) {
if (elevationFill === 'slope') { if (elevationFill === 'slope') {
chart.data.datasets[0]['segment'] = { chart.data.datasets[0]['segment'] = {
backgroundColor: slopeFillCallback, backgroundColor: slopeFillCallback,
@@ -481,8 +491,10 @@
} }
chart.update(); chart.update();
} }
});
$: if (additionalDatasets && chart) { $effect(() => {
if (additionalDatasets && chart) {
let includeSpeed = additionalDatasets.includes('speed'); let includeSpeed = additionalDatasets.includes('speed');
let includeHeartRate = additionalDatasets.includes('hr'); let includeHeartRate = additionalDatasets.includes('hr');
let includeCadence = additionalDatasets.includes('cad'); let includeCadence = additionalDatasets.includes('cad');
@@ -497,6 +509,7 @@
} }
chart.update(); chart.update();
} }
});
function updateOverlay() { function updateOverlay() {
if (!canvas) { if (!canvas) {
@@ -541,7 +554,11 @@
} }
} }
$: $slicedGPXStatistics, mode.current, updateOverlay(); $effect(() => {
if ($slicedGPXStatistics || mode.current) {
updateOverlay();
}
});
onDestroy(() => { onDestroy(() => {
if (chart) { if (chart) {
@@ -557,63 +574,62 @@
<div class="absolute bottom-10 right-1.5"> <div class="absolute bottom-10 right-1.5">
<Popover.Root> <Popover.Root>
<Popover.Trigger> <Popover.Trigger>
{#snippet child({ props })}
<ButtonWithTooltip <ButtonWithTooltip
{...props}
label={i18n._('chart.settings')} label={i18n._('chart.settings')}
variant="outline" variant="outline"
side="left"
class="w-7 h-7 p-0 flex justify-center opacity-70 hover:opacity-100 transition-opacity duration-300 hover:bg-background" class="w-7 h-7 p-0 flex justify-center opacity-70 hover:opacity-100 transition-opacity duration-300 hover:bg-background"
> >
<ChartNoAxesColumn size="18" /> <ChartNoAxesColumn size="18" />
</ButtonWithTooltip> </ButtonWithTooltip>
{/snippet}
</Popover.Trigger> </Popover.Trigger>
<Popover.Content <Popover.Content
class="w-fit p-0 flex flex-col divide-y" class="w-fit p-0 flex flex-col divide-y-2 divide-solid divide-gray-500"
side="top" side="top"
align="end"
sideOffset={-32} sideOffset={-32}
> >
<ToggleGroup.Root <ToggleGroup.Root
class="flex flex-col items-start gap-0 p-1" class="flex flex-col items-start gap-0 p-1 w-full border-none"
type="single" type="single"
bind:value={elevationFill} bind:value={elevationFill}
> >
<ToggleGroup.Item <ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground" class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="slope" value="slope"
> >
<div class="w-6 flex justify-center items-center"> <div class="w-6 flex justify-center items-center">
{#if elevationFill === 'slope'} {#if elevationFill === 'slope'}
<Circle class="h-1.5 w-1.5 fill-current text-current" /> <Circle class="size-1.5 fill-current text-current" />
{/if} {/if}
</div> </div>
<TriangleRight size="15" class="mr-1" /> <TriangleRight size="15" />
{i18n._('quantities.slope')} {i18n._('quantities.slope')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item <ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground" class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="surface" value="surface"
variant="outline" variant="outline"
> >
<div class="w-6 flex justify-center items-center"> <div class="w-6 flex justify-center items-center">
{#if elevationFill === 'surface'} {#if elevationFill === 'surface'}
<Circle class="h-1.5 w-1.5 fill-current text-current" /> <Circle class="size-1.5 fill-current text-current" />
{/if} {/if}
</div> </div>
<BrickWall size="15" class="mr-1" /> <BrickWall size="15" />
{i18n._('quantities.surface')} {i18n._('quantities.surface')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item <ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground" class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="highway" value="highway"
variant="outline" variant="outline"
> >
<div class="w-6 flex justify-center items-center"> <div class="w-6 flex justify-center items-center">
{#if elevationFill === 'highway'} {#if elevationFill === 'highway'}
<Circle class="h-1.5 w-1.5 fill-current text-current" /> <Circle class="size-1.5 fill-current text-current" />
{/if} {/if}
</div> </div>
<Construction size="15" class="mr-1" /> <Construction size="15" />
{i18n._('quantities.highway')} {i18n._('quantities.highway')}
</ToggleGroup.Item> </ToggleGroup.Item>
</ToggleGroup.Root> </ToggleGroup.Root>
@@ -623,7 +639,7 @@
bind:value={additionalDatasets} bind:value={additionalDatasets}
> >
<ToggleGroup.Item <ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground" class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="speed" value="speed"
> >
<div class="w-6 flex justify-center items-center"> <div class="w-6 flex justify-center items-center">
@@ -631,13 +647,13 @@
<Check size="14" /> <Check size="14" />
{/if} {/if}
</div> </div>
<Zap size="15" class="mr-1" /> <Zap size="15" />
{$velocityUnits === 'speed' {$velocityUnits === 'speed'
? i18n._('quantities.speed') ? i18n._('quantities.speed')
: i18n._('quantities.pace')} : i18n._('quantities.pace')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item <ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground" class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="hr" value="hr"
> >
<div class="w-6 flex justify-center items-center"> <div class="w-6 flex justify-center items-center">
@@ -645,11 +661,11 @@
<Check size="14" /> <Check size="14" />
{/if} {/if}
</div> </div>
<HeartPulse size="15" class="mr-1" /> <HeartPulse size="15" />
{i18n._('quantities.heartrate')} {i18n._('quantities.heartrate')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item <ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground" class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="cad" value="cad"
> >
<div class="w-6 flex justify-center items-center"> <div class="w-6 flex justify-center items-center">
@@ -657,11 +673,11 @@
<Check size="14" /> <Check size="14" />
{/if} {/if}
</div> </div>
<Orbit size="15" class="mr-1" /> <Orbit size="15" />
{i18n._('quantities.cadence')} {i18n._('quantities.cadence')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item <ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground" class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="atemp" value="atemp"
> >
<div class="w-6 flex justify-center items-center"> <div class="w-6 flex justify-center items-center">
@@ -669,11 +685,11 @@
<Check size="14" /> <Check size="14" />
{/if} {/if}
</div> </div>
<Thermometer size="15" class="mr-1" /> <Thermometer size="15" />
{i18n._('quantities.temperature')} {i18n._('quantities.temperature')}
</ToggleGroup.Item> </ToggleGroup.Item>
<ToggleGroup.Item <ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground" class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="power" value="power"
> >
<div class="w-6 flex justify-center items-center"> <div class="w-6 flex justify-center items-center">
@@ -681,7 +697,7 @@
<Check size="14" /> <Check size="14" />
{/if} {/if}
</div> </div>
<SquareActivity size="15" class="mr-1" /> <SquareActivity size="15" />
{i18n._('quantities.power')} {i18n._('quantities.power')}
</ToggleGroup.Item> </ToggleGroup.Item>
</ToggleGroup.Root> </ToggleGroup.Root>

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { CircleHelp } from '@lucide/svelte'; import { CircleQuestionMark } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
export let link: string | undefined = undefined; export let link: string | undefined = undefined;
@@ -8,7 +8,7 @@
<div <div
class="text-sm bg-secondary rounded border flex flex-row items-center p-2 {$$props.class || ''}" class="text-sm bg-secondary rounded border flex flex-row items-center p-2 {$$props.class || ''}"
> >
<CircleHelp size="16" class="w-4 mr-2 shrink-0 grow-0" /> <CircleQuestionMark size="16" class="w-4 mr-2 shrink-0 grow-0" />
<div> <div>
<slot /> <slot />
{#if link} {#if link}

View File

@@ -120,13 +120,13 @@
</Menubar.Trigger> </Menubar.Trigger>
<Menubar.Content class="border-none"> <Menubar.Content class="border-none">
<Menubar.Item onclick={createFile}> <Menubar.Item onclick={createFile}>
<Plus size="16" class="mr-1" /> <Plus size="16" />
{i18n._('menu.new')} {i18n._('menu.new')}
<Shortcut key="+" ctrl={true} /> <Shortcut key="+" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item onclick={triggerFileInput}> <Menubar.Item onclick={triggerFileInput}>
<FolderOpen size="16" class="mr-1" /> <FolderOpen size="16" />
{i18n._('menu.open')} {i18n._('menu.open')}
<Shortcut key="O" ctrl={true} /> <Shortcut key="O" ctrl={true} />
</Menubar.Item> </Menubar.Item>
@@ -135,7 +135,7 @@
onclick={fileActions.duplicateSelection} onclick={fileActions.duplicateSelection}
disabled={$selection.size == 0} disabled={$selection.size == 0}
> >
<Copy size="16" class="mr-1" /> <Copy size="16" />
{i18n._('menu.duplicate')} {i18n._('menu.duplicate')}
<Shortcut key="D" ctrl={true} /> <Shortcut key="D" ctrl={true} />
</Menubar.Item> </Menubar.Item>
@@ -144,7 +144,7 @@
onclick={fileActions.deleteSelectedFiles} onclick={fileActions.deleteSelectedFiles}
disabled={$selection.size == 0} disabled={$selection.size == 0}
> >
<FileX size="16" class="mr-1" /> <FileX size="16" />
{i18n._('menu.close')} {i18n._('menu.close')}
<Shortcut key="⌫" ctrl={true} /> <Shortcut key="⌫" ctrl={true} />
</Menubar.Item> </Menubar.Item>
@@ -152,7 +152,7 @@
onclick={fileActions.deleteAllFiles} onclick={fileActions.deleteAllFiles}
disabled={fileStateCollection.size == 0} disabled={fileStateCollection.size == 0}
> >
<FileX size="16" class="mr-1" /> <FileX size="16" />
{i18n._('menu.close_all')} {i18n._('menu.close_all')}
<Shortcut key="⌫" ctrl={true} shift={true} /> <Shortcut key="⌫" ctrl={true} shift={true} />
</Menubar.Item> </Menubar.Item>
@@ -161,7 +161,7 @@
onclick={() => (exportState.current = ExportState.SELECTION)} onclick={() => (exportState.current = ExportState.SELECTION)}
disabled={$selection.size == 0} disabled={$selection.size == 0}
> >
<Download size="16" class="mr-1" /> <Download size="16" />
{i18n._('menu.export')} {i18n._('menu.export')}
<Shortcut key="S" ctrl={true} /> <Shortcut key="S" ctrl={true} />
</Menubar.Item> </Menubar.Item>
@@ -169,7 +169,7 @@
onclick={() => (exportState.current = ExportState.ALL)} onclick={() => (exportState.current = ExportState.ALL)}
disabled={fileStateCollection.size == 0} disabled={fileStateCollection.size == 0}
> >
<Download size="16" class="mr-1" /> <Download size="16" />
{i18n._('menu.export_all')} {i18n._('menu.export_all')}
<Shortcut key="S" ctrl={true} shift={true} /> <Shortcut key="S" ctrl={true} shift={true} />
</Menubar.Item> </Menubar.Item>
@@ -185,7 +185,7 @@
onclick={() => fileActionManager.undo()} onclick={() => fileActionManager.undo()}
disabled={!fileActionManager.canUndo} disabled={!fileActionManager.canUndo}
> >
<Undo2 size="16" class="mr-1" /> <Undo2 size="16" />
{i18n._('menu.undo')} {i18n._('menu.undo')}
<Shortcut key="Z" ctrl={true} /> <Shortcut key="Z" ctrl={true} />
</Menubar.Item> </Menubar.Item>
@@ -193,7 +193,7 @@
onclick={() => fileActionManager.redo()} onclick={() => fileActionManager.redo()}
disabled={!fileActionManager.canRedo} disabled={!fileActionManager.canRedo}
> >
<Redo2 size="16" class="mr-1" /> <Redo2 size="16" />
{i18n._('menu.redo')} {i18n._('menu.redo')}
<Shortcut key="Z" ctrl={true} shift={true} /> <Shortcut key="Z" ctrl={true} shift={true} />
</Menubar.Item> </Menubar.Item>
@@ -209,7 +209,7 @@
)} )}
onclick={() => (editMetadata.current = true)} onclick={() => (editMetadata.current = true)}
> >
<Info size="16" class="mr-1" /> <Info size="16" />
{i18n._('menu.metadata.button')} {i18n._('menu.metadata.button')}
<Shortcut key="I" ctrl={true} /> <Shortcut key="I" ctrl={true} />
</Menubar.Item> </Menubar.Item>
@@ -224,7 +224,7 @@
)} )}
onclick={() => (editStyle.current = true)} onclick={() => (editStyle.current = true)}
> >
<PaintBucket size="16" class="mr-1" /> <PaintBucket size="16" />
{i18n._('menu.style.button')} {i18n._('menu.style.button')}
</Menubar.Item> </Menubar.Item>
<Menubar.Item <Menubar.Item
@@ -238,10 +238,10 @@
disabled={$selection.size == 0} disabled={$selection.size == 0}
> >
<!-- {#if $allHidden} <!-- {#if $allHidden}
<Eye size="16" class="mr-1" /> <Eye size="16" />
{i18n._('menu.unhide')} {i18n._('menu.unhide')}
{:else} {:else}
<EyeOff size="16" class="mr-1" /> <EyeOff size="16" />
{i18n._('menu.hide')} {i18n._('menu.hide')}
{/if} --> {/if} -->
<Shortcut key="H" ctrl={true} /> <Shortcut key="H" ctrl={true} />
@@ -256,7 +256,7 @@
)} )}
disabled={$selection.size !== 1} disabled={$selection.size !== 1}
> >
<Plus size="16" class="mr-1" /> <Plus size="16" />
{i18n._('menu.new_track')} {i18n._('menu.new_track')}
</Menubar.Item> </Menubar.Item>
{:else if $selection {:else if $selection
@@ -273,7 +273,7 @@
}} }}
disabled={$selection.size !== 1} disabled={$selection.size !== 1}
> >
<Plus size="16" class="mr-1" /> <Plus size="16" />
{i18n._('menu.new_segment')} {i18n._('menu.new_segment')}
</Menubar.Item> </Menubar.Item>
{/if} {/if}
@@ -283,7 +283,7 @@
onclick={selection.selectAll} onclick={selection.selectAll}
disabled={fileStateCollection.size == 0} disabled={fileStateCollection.size == 0}
> >
<FileStack size="16" class="mr-1" /> <FileStack size="16" />
{i18n._('menu.select_all')} {i18n._('menu.select_all')}
<Shortcut key="A" ctrl={true} /> <Shortcut key="A" ctrl={true} />
</Menubar.Item> </Menubar.Item>
@@ -294,7 +294,7 @@
} }
}} }}
> >
<Maximize size="16" class="mr-1" /> <Maximize size="16" />
{i18n._('menu.center')} {i18n._('menu.center')}
<Shortcut key="⏎" ctrl={true} /> <Shortcut key="⏎" ctrl={true} />
</Menubar.Item> </Menubar.Item>
@@ -304,7 +304,7 @@
onclick={selection.copySelection} onclick={selection.copySelection}
disabled={$selection.size === 0} disabled={$selection.size === 0}
> >
<ClipboardCopy size="16" class="mr-1" /> <ClipboardCopy size="16" />
{i18n._('menu.copy')} {i18n._('menu.copy')}
<Shortcut key="C" ctrl={true} /> <Shortcut key="C" ctrl={true} />
</Menubar.Item> </Menubar.Item>
@@ -312,7 +312,7 @@
onclick={selection.cutSelection} onclick={selection.cutSelection}
disabled={$selection.size === 0} disabled={$selection.size === 0}
> >
<Scissors size="16" class="mr-1" /> <Scissors size="16" />
{i18n._('menu.cut')} {i18n._('menu.cut')}
<Shortcut key="X" ctrl={true} /> <Shortcut key="X" ctrl={true} />
</Menubar.Item> </Menubar.Item>
@@ -325,7 +325,7 @@
))} ))}
onclick={pasteSelection} onclick={pasteSelection}
> >
<ClipboardPaste size="16" class="mr-1" /> <ClipboardPaste size="16" />
{i18n._('menu.paste')} {i18n._('menu.paste')}
<Shortcut key="V" ctrl={true} /> <Shortcut key="V" ctrl={true} />
</Menubar.Item> </Menubar.Item>
@@ -335,7 +335,7 @@
onclick={fileActions.deleteSelection} onclick={fileActions.deleteSelection}
disabled={$selection.size == 0} disabled={$selection.size == 0}
> >
<Trash2 size="16" class="mr-1" /> <Trash2 size="16" />
{i18n._('menu.delete')} {i18n._('menu.delete')}
<Shortcut key="⌫" ctrl={true} /> <Shortcut key="⌫" ctrl={true} />
</Menubar.Item> </Menubar.Item>
@@ -348,42 +348,36 @@
</Menubar.Trigger> </Menubar.Trigger>
<Menubar.Content class="border-none"> <Menubar.Content class="border-none">
<Menubar.CheckboxItem bind:checked={$elevationProfile}> <Menubar.CheckboxItem bind:checked={$elevationProfile}>
<ChartArea size="16" class="mr-1" /> <ChartArea size="16" />
{i18n._('menu.elevation_profile')} {i18n._('menu.elevation_profile')}
<Shortcut key="P" ctrl={true} /> <Shortcut key="P" ctrl={true} />
</Menubar.CheckboxItem> </Menubar.CheckboxItem>
<Menubar.CheckboxItem bind:checked={$treeFileView}> <Menubar.CheckboxItem bind:checked={$treeFileView}>
<ListTree size="16" class="mr-1" /> <ListTree size="16" />
{i18n._('menu.tree_file_view')} {i18n._('menu.tree_file_view')}
<Shortcut key="L" ctrl={true} /> <Shortcut key="L" ctrl={true} />
</Menubar.CheckboxItem> </Menubar.CheckboxItem>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item inset onclick={switchBasemaps}> <Menubar.Item inset onclick={switchBasemaps}>
<Map size="16" class="mr-1" />{i18n._('menu.switch_basemap')}<Shortcut <Map size="16" />{i18n._('menu.switch_basemap')}<Shortcut key="F1" />
key="F1"
/>
</Menubar.Item> </Menubar.Item>
<Menubar.Item inset onclick={toggleOverlays}> <Menubar.Item inset onclick={toggleOverlays}>
<Layers2 size="16" class="mr-1" />{i18n._('menu.toggle_overlays')}<Shortcut <Layers2 size="16" />{i18n._('menu.toggle_overlays')}<Shortcut key="F2" />
key="F2"
/>
</Menubar.Item> </Menubar.Item>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.CheckboxItem bind:checked={$distanceMarkers}> <Menubar.CheckboxItem bind:checked={$distanceMarkers}>
<Coins size="16" class="mr-1" />{i18n._('menu.distance_markers')}<Shortcut <Coins size="16" />{i18n._('menu.distance_markers')}<Shortcut key="F3" />
key="F3"
/>
</Menubar.CheckboxItem> </Menubar.CheckboxItem>
<Menubar.CheckboxItem bind:checked={$directionMarkers}> <Menubar.CheckboxItem bind:checked={$directionMarkers}>
<Milestone size="16" class="mr-1" />{i18n._( <Milestone size="16" />{i18n._('menu.direction_markers')}<Shortcut
'menu.direction_markers' key="F4"
)}<Shortcut key="F4" /> />
</Menubar.CheckboxItem> </Menubar.CheckboxItem>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item inset onclick={map.toggle3D}> <Menubar.Item inset onclick={map.toggle3D}>
<Box size="16" class="mr-1" /> <Box size="16" />
{i18n._('menu.toggle_3d')} {i18n._('menu.toggle_3d')}
<Shortcut key="{i18n._('menu.ctrl')}+{i18n._('menu.drag')}" /> <Shortcut key="{i18n._('menu.ctrl')} {i18n._('menu.drag')}" />
</Menubar.Item> </Menubar.Item>
</Menubar.Content> </Menubar.Content>
</Menubar.Menu> </Menubar.Menu>
@@ -397,7 +391,7 @@
<Menubar.Content class="border-none"> <Menubar.Content class="border-none">
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
<Ruler size="16" class="mr-1" />{i18n._('menu.distance_units')} <Ruler size="16" class="mr-2" />{i18n._('menu.distance_units')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$distanceUnits}> <Menubar.RadioGroup bind:value={$distanceUnits}>
@@ -415,7 +409,7 @@
</Menubar.Sub> </Menubar.Sub>
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
<Zap size="16" class="mr-1" />{i18n._('menu.velocity_units')} <Zap size="16" class="mr-2" />{i18n._('menu.velocity_units')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$velocityUnits}> <Menubar.RadioGroup bind:value={$velocityUnits}>
@@ -430,7 +424,7 @@
</Menubar.Sub> </Menubar.Sub>
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
<Thermometer size="16" class="mr-1" />{i18n._('menu.temperature_units')} <Thermometer size="16" class="mr-2" />{i18n._('menu.temperature_units')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$temperatureUnits}> <Menubar.RadioGroup bind:value={$temperatureUnits}>
@@ -446,7 +440,7 @@
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
<Languages size="16" class="mr-1" /> <Languages size="16" class="mr-2" />
{i18n._('menu.language')} {i18n._('menu.language')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
@@ -462,9 +456,9 @@
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
{#if mode.current === 'light' || !mode.current} {#if mode.current === 'light' || !mode.current}
<Sun size="16" class="mr-1" /> <Sun size="16" class="mr-2" />
{:else} {:else}
<Moon size="16" class="mr-1" /> <Moon size="16" class="mr-2" />
{/if} {/if}
{i18n._('menu.mode')} {i18n._('menu.mode')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
@@ -487,7 +481,7 @@
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
<PersonStanding size="16" class="mr-1" /> <PersonStanding size="16" class="mr-2" />
{i18n._('menu.street_view_source')} {i18n._('menu.street_view_source')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
@@ -502,7 +496,7 @@
</Menubar.SubContent> </Menubar.SubContent>
</Menubar.Sub> </Menubar.Sub>
<Menubar.Item onclick={() => (layerSettingsOpen = true)}> <Menubar.Item onclick={() => (layerSettingsOpen = true)}>
<Layers size="16" class="mr-1" /> <Layers size="16" />
{i18n._('menu.layers')} {i18n._('menu.layers')}
</Menubar.Item> </Menubar.Item>
</Menubar.Content> </Menubar.Content>
@@ -544,7 +538,9 @@
<svelte:window <svelte:window
on:keydown={(e) => { on:keydown={(e) => {
let targetInput = let targetInput =
e.target.tagName === 'INPUT' || e &&
e.target &&
(e.target.tagName === 'INPUT' ||
e.target.tagName === 'TEXTAREA' || e.target.tagName === 'TEXTAREA' ||
e.target.tagName === 'SELECT' || e.target.tagName === 'SELECT' ||
e.target.role === 'combobox' || e.target.role === 'combobox' ||
@@ -552,7 +548,7 @@
e.target.role === 'menu' || e.target.role === 'menu' ||
e.target.role === 'menuitem' || e.target.role === 'menuitem' ||
e.target.role === 'menuitemradio' || e.target.role === 'menuitemradio' ||
e.target.role === 'menuitemcheckbox'; e.target.role === 'menuitemcheckbox');
if (e.key === '+' && (e.metaKey || e.ctrlKey)) { if (e.key === '+' && (e.metaKey || e.ctrlKey)) {
createFile(); createFile();

View File

@@ -2,14 +2,24 @@
import { isMac, isSafari } from '$lib/utils'; import { isMac, isSafari } from '$lib/utils';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import * as Kbd from '$lib/components/ui/kbd/index.js';
export let key: string | undefined = undefined; let {
export let shift: boolean = false; key = undefined,
export let ctrl: boolean = false; shift = false,
export let click: boolean = false; ctrl = false,
click = false,
class: className = '',
}: {
key?: string;
shift?: boolean;
ctrl?: boolean;
click?: boolean;
class?: string;
} = $props();
let mac = false; let mac = $state(false);
let safari = false; let safari = $state(false);
onMount(() => { onMount(() => {
mac = isMac(); mac = isMac();
@@ -17,20 +27,17 @@
}); });
</script> </script>
<div <Kbd.Root class="ml-auto {className}">
class="ml-auto pl-2 text-xs tracking-widest text-muted-foreground flex flex-row gap-0 items-baseline"
{...$$props}
>
{#if shift} {#if shift}
<span></span>
{/if} {/if}
{#if ctrl} {#if ctrl}
<span>{mac && !safari ? '⌘' : i18n._('menu.ctrl') + '+'}</span> {mac && !safari ? '⌘' : i18n._('menu.ctrl')}
{/if} {/if}
{#if key} {#if key}
<span class={key === '+' ? 'font-medium text-sm/4' : ''}>{key}</span> {key}
{/if} {/if}
{#if click} {#if click}
<span>{i18n._('menu.click')}</span> {i18n._('menu.click')}
{/if} {/if}
</div> </Kbd.Root>

View File

@@ -1,19 +1,31 @@
<script lang="ts"> <script lang="ts">
import * as Tooltip from '$lib/components/ui/tooltip/index.js'; import * as Tooltip from '$lib/components/ui/tooltip/index.js';
import type { Snippet } from 'svelte';
export let label: string; let {
export let side: 'top' | 'right' | 'bottom' | 'left' = 'top'; label,
side = 'top',
children,
extra,
class: className = '',
}: {
label: string;
side?: 'top' | 'right' | 'bottom' | 'left';
children: Snippet;
extra?: Snippet;
class?: string;
} = $props();
</script> </script>
<Tooltip.Provider> <Tooltip.Provider>
<Tooltip.Root> <Tooltip.Root>
<Tooltip.Trigger {...$$restProps} aria-label={label}> <Tooltip.Trigger class={className} aria-label={label}>
<slot /> {@render children()}
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Content {side}> <Tooltip.Content {side}>
<div class="flex flex-row items-center"> <div class="flex flex-row items-center gap-2">
<span>{label}</span> <span>{label}</span>
<slot name="extra" /> {@render extra?.()}
</div> </div>
</Tooltip.Content> </Tooltip.Content>
</Tooltip.Root> </Tooltip.Root>

View File

@@ -36,7 +36,7 @@
variant="ghost" variant="ghost"
class="w-full flex flex-row {side === 'right' class="w-full flex flex-row {side === 'right'
? 'justify-between' ? 'justify-between'
: 'justify-start'} py-0 px-1 h-fit {nohover : 'justify-start'} p-0 has-[>svg]:px-0 h-fit {nohover
? 'hover:bg-background' ? 'hover:bg-background'
: ''} pointer-events-none" : ''} pointer-events-none"
> >
@@ -62,7 +62,7 @@
variant="ghost" variant="ghost"
class="w-full flex flex-row {side === 'right' class="w-full flex flex-row {side === 'right'
? 'justify-between' ? 'justify-between'
: 'justify-start'} py-0 px-1 h-fit {nohover ? 'hover:bg-background' : ''}" : 'justify-start'} p-0 has-[>svg]:px-0 h-fit {nohover ? 'hover:bg-background' : ''}"
> >
{#if side === 'left'} {#if side === 'left'}
<Collapsible.Trigger> <Collapsible.Trigger>
@@ -86,7 +86,7 @@
</Button> </Button>
{/if} {/if}
<Collapsible.Content class="ml-2"> <Collapsible.Content>
{@render props.content()} {@render props.content()}
</Collapsible.Content> </Collapsible.Content>
</Collapsible.Root> </Collapsible.Root>

View File

@@ -10,14 +10,7 @@ import {
ListFileItem, ListFileItem,
ListRootItem, ListRootItem,
} from '$lib/components/file-list/file-list'; } from '$lib/components/file-list/file-list';
import { import { getClosestLinePoint, getElevation } from '$lib/utils';
getClosestLinePoint,
getElevation,
resetCursor,
setGrabbingCursor,
setPointerCursor,
setScissorsCursor,
} from '$lib/utils';
import { selectedWaypoint } from '$lib/components/toolbar/tools/waypoint/waypoint'; import { selectedWaypoint } from '$lib/components/toolbar/tools/waypoint/waypoint';
import { MapPin, Square } from 'lucide-static'; import { MapPin, Square } from 'lucide-static';
import { getSymbolKey, symbols } from '$lib/assets/symbols'; import { getSymbolKey, symbols } from '$lib/assets/symbols';
@@ -28,6 +21,7 @@ import { currentTool, Tool } from '$lib/components/toolbar/tools';
import { fileActionManager } from '$lib/logic/file-action-manager'; import { fileActionManager } from '$lib/logic/file-action-manager';
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors'; import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
const colors = [ const colors = [
'#ff0000', '#ff0000',
@@ -335,12 +329,12 @@ export class GPXLayer {
e.stopPropagation(); e.stopPropagation();
}); });
marker.on('dragstart', () => { marker.on('dragstart', () => {
setGrabbingCursor(); mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, true);
marker.getElement().style.cursor = 'grabbing'; marker.getElement().style.cursor = 'grabbing';
waypointPopup?.hide(); waypointPopup?.hide();
}); });
marker.on('dragend', (e) => { marker.on('dragend', (e) => {
resetCursor(); mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
marker.getElement().style.cursor = ''; marker.getElement().style.cursor = '';
getElevation([marker._waypoint]).then((ele) => { getElevation([marker._waypoint]).then((ele) => {
fileActionManager.applyToFile(this.fileId, (file) => { fileActionManager.applyToFile(this.fileId, (file) => {
@@ -431,14 +425,15 @@ export class GPXLayer {
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
) )
) { ) {
setScissorsCursor(); mapCursor.notify(MapCursorState.SCISSORS, true);
} else { } else {
setPointerCursor(); mapCursor.notify(MapCursorState.LAYER_HOVER, true);
} }
} }
layerOnMouseLeave() { layerOnMouseLeave() {
resetCursor(); mapCursor.notify(MapCursorState.SCISSORS, false);
mapCursor.notify(MapCursorState.LAYER_HOVER, false);
} }
layerOnMouseMove(e: any) { layerOnMouseMove(e: any) {

View File

@@ -312,10 +312,20 @@
<div class="flex flex-row items-center gap-2" data-id={id}> <div class="flex flex-row items-center gap-2" data-id={id}>
<Move size="12" /> <Move size="12" />
<span class="grow">{$customLayers[id].name}</span> <span class="grow">{$customLayers[id].name}</span>
<Button variant="outline" onclick={() => (selectedLayerId = id)} class="p-1 h-7"> <Button
variant="outline"
size="icon-sm"
onclick={() => (selectedLayerId = id)}
class="p-1 h-7"
>
<Pencil size="16" /> <Pencil size="16" />
</Button> </Button>
<Button variant="outline" onclick={() => deleteLayer(id)} class="p-1 h-7"> <Button
variant="outline"
size="icon-sm"
onclick={() => deleteLayer(id)}
class="p-1 h-7"
>
<Trash2 size="16" /> <Trash2 size="16" />
</Button> </Button>
</div> </div>
@@ -338,17 +348,26 @@
<div class="flex flex-row items-center gap-2" data-id={id}> <div class="flex flex-row items-center gap-2" data-id={id}>
<Move size="12" /> <Move size="12" />
<span class="grow">{$customLayers[id].name}</span> <span class="grow">{$customLayers[id].name}</span>
<Button variant="outline" onclick={() => (selectedLayerId = id)} class="p-1 h-7"> <Button
variant="outline"
size="icon-sm"
onclick={() => (selectedLayerId = id)}
class="p-1 h-7"
>
<Pencil size="16" /> <Pencil size="16" />
</Button> </Button>
<Button variant="outline" onclick={() => deleteLayer(id)} class="p-1 h-7"> <Button
variant="outline"
size="icon-sm"
onclick={() => deleteLayer(id)}
class="p-1 h-7"
>
<Trash2 size="16" /> <Trash2 size="16" />
</Button> </Button>
</div> </div>
{/each} {/each}
</div> </div>
<Card.Root class="py-0 gap-0 shadow-none">
<Card.Root>
<Card.Header class="p-3"> <Card.Header class="p-3">
<Card.Title class="text-base"> <Card.Title class="text-base">
{#if selectedLayerId} {#if selectedLayerId}

View File

@@ -179,9 +179,9 @@
? 'grid-rows-[1fr] grid-cols-[1fr]' ? 'grid-rows-[1fr] grid-cols-[1fr]'
: ''} {cancelEvents ? 'pointer-events-none' : ''}" : ''} {cancelEvents ? 'pointer-events-none' : ''}"
> >
<ScrollArea> <ScrollArea class="overflow-hidden">
<div class="h-fit"> <div class="h-fit">
<div class="p-2"> <div class="p-2 ml-1">
<LayerTree <LayerTree
layerTree={$selectedBasemapTree} layerTree={$selectedBasemapTree}
name="basemaps" name="basemaps"
@@ -193,7 +193,7 @@
/> />
</div> </div>
<Separator class="w-full" /> <Separator class="w-full" />
<div class="p-2"> <div class="p-2 ml-1">
{#if $currentOverlays} {#if $currentOverlays}
<LayerTree <LayerTree
layerTree={$selectedOverlayTree} layerTree={$selectedOverlayTree}
@@ -204,7 +204,7 @@
{/if} {/if}
</div> </div>
<Separator class="w-full" /> <Separator class="w-full" />
<div class="p-2"> <div class="p-2 ml-1">
{#if $currentOverpassQueries} {#if $currentOverpassQueries}
<LayerTree <LayerTree
layerTree={$selectedOverpassTree} layerTree={$selectedOverpassTree}

View File

@@ -90,7 +90,7 @@
<Accordion.Item value="layer-selection" class="flex flex-col"> <Accordion.Item value="layer-selection" class="flex flex-col">
<Accordion.Trigger>{i18n._('layers.selection')}</Accordion.Trigger> <Accordion.Trigger>{i18n._('layers.selection')}</Accordion.Trigger>
<Accordion.Content class="grow flex flex-col border rounded"> <Accordion.Content class="grow flex flex-col border rounded">
<div class="py-2 pl-1 pr-2"> <div class="py-2 pl-3 pr-2">
<LayerTree <LayerTree
layerTree={basemapTree} layerTree={basemapTree}
name="basemapSettings" name="basemapSettings"
@@ -99,7 +99,7 @@
/> />
</div> </div>
<Separator /> <Separator />
<div class="py-2 pl-1 pr-2"> <div class="py-2 pl-3 pr-2">
<LayerTree <LayerTree
layerTree={overlayTree} layerTree={overlayTree}
name="overlaySettings" name="overlaySettings"
@@ -108,7 +108,7 @@
/> />
</div> </div>
<Separator /> <Separator />
<div class="py-2 pl-1 pr-2"> <div class="py-2 pl-3 pr-2">
<LayerTree <LayerTree
layerTree={overpassTree} layerTree={overpassTree}
name="overpassSettings" name="overpassSettings"
@@ -130,7 +130,7 @@
type="single" type="single"
onValueChange={setOpacityFromSelection} onValueChange={setOpacityFromSelection}
> >
<Select.Trigger class="h-8 mr-1"> <Select.Trigger class="h-8 mr-1 w-full">
{#if selectedOverlay} {#if selectedOverlay}
{#if isSelected($selectedOverlayTree, selectedOverlay)} {#if isSelected($selectedOverlayTree, selectedOverlay)}
{i18n._(`layers.label.${selectedOverlay}`)} {i18n._(`layers.label.${selectedOverlay}`)}

View File

@@ -1,4 +1,4 @@
import { resetCursor, setCrosshairCursor } from '$lib/utils'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import type mapboxgl from 'mapbox-gl'; import type mapboxgl from 'mapbox-gl';
export class GoogleRedirect { export class GoogleRedirect {
@@ -13,7 +13,7 @@ export class GoogleRedirect {
if (this.enabled) return; if (this.enabled) return;
this.enabled = true; this.enabled = true;
setCrosshairCursor(); mapCursor.notify(MapCursorState.STREET_VIEW_CROSSHAIR, true);
this.map.on('click', this.openStreetView); this.map.on('click', this.openStreetView);
} }
@@ -21,11 +21,11 @@ export class GoogleRedirect {
if (!this.enabled) return; if (!this.enabled) return;
this.enabled = false; this.enabled = false;
resetCursor(); mapCursor.notify(MapCursorState.STREET_VIEW_CROSSHAIR, false);
this.map.off('click', this.openStreetView); this.map.off('click', this.openStreetView);
} }
openStreetView(e) { openStreetView(e: mapboxgl.MapMouseEvent) {
window.open( window.open(
`https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${e.lngLat.lat},${e.lngLat.lng}` `https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${e.lngLat.lat},${e.lngLat.lng}`
); );

View File

@@ -1,7 +1,7 @@
import mapboxgl, { type LayerSpecification, type VectorSourceSpecification } from 'mapbox-gl'; import mapboxgl, { type LayerSpecification, type VectorSourceSpecification } from 'mapbox-gl';
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module'; import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
import 'mapillary-js/dist/mapillary.css'; import 'mapillary-js/dist/mapillary.css';
import { resetCursor, setPointerCursor } from '$lib/utils'; import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
const mapillarySource: VectorSourceSpecification = { const mapillarySource: VectorSourceSpecification = {
type: 'vector', type: 'vector',
@@ -140,10 +140,10 @@ export class MapillaryLayer {
this.viewer.resize(); this.viewer.resize();
this.viewer.moveTo(e.features[0].properties.id); this.viewer.moveTo(e.features[0].properties.id);
setPointerCursor(); mapCursor.notify(MapCursorState.MAPILLARY_HOVER, true);
} }
onMouseLeave() { onMouseLeave() {
resetCursor(); mapCursor.notify(MapCursorState.MAPILLARY_HOVER, false);
} }
} }

View File

@@ -10,6 +10,7 @@
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import ButtonWithTooltip from '$lib/components/ButtonWithTooltip.svelte';
const { streetViewSource } = settings; const { streetViewSource } = settings;
@@ -47,15 +48,21 @@
</script> </script>
<CustomControl class="w-[29px] h-[29px] shrink-0"> <CustomControl class="w-[29px] h-[29px] shrink-0">
<Tooltip class="w-full h-full" side="left" label={i18n._('menu.toggle_street_view')}> <ButtonWithTooltip
<Toggle variant="ghost"
bind:pressed={$streetViewEnabled} class="w-full h-full"
class="w-full h-full rounded p-0" side="left"
aria-label={i18n._('menu.toggle_street_view')} label={i18n._('menu.toggle_street_view')}
onclick={() => {
$streetViewEnabled = !$streetViewEnabled;
}}
> >
<PersonStanding size="22" /> <PersonStanding
</Toggle> size="22"
</Tooltip> class="size-5.5"
color={$streetViewEnabled ? '#33b5e5' : 'currentColor'}
/>
</ButtonWithTooltip>
</CustomControl> </CustomControl>
<div <div

View File

@@ -26,31 +26,31 @@
''}" ''}"
> >
<ToolbarItem itemTool={Tool.ROUTING} label={i18n._('toolbar.routing.tooltip')}> <ToolbarItem itemTool={Tool.ROUTING} label={i18n._('toolbar.routing.tooltip')}>
<Pencil size="18" /> <Pencil size="18" class="size-4.5" />
</ToolbarItem> </ToolbarItem>
<ToolbarItem itemTool={Tool.WAYPOINT} label={i18n._('toolbar.waypoint.tooltip')}> <ToolbarItem itemTool={Tool.WAYPOINT} label={i18n._('toolbar.waypoint.tooltip')}>
<MapPin size="18" /> <MapPin size="18" class="size-4.5" />
</ToolbarItem> </ToolbarItem>
<ToolbarItem itemTool={Tool.SCISSORS} label={i18n._('toolbar.scissors.tooltip')}> <ToolbarItem itemTool={Tool.SCISSORS} label={i18n._('toolbar.scissors.tooltip')}>
<Scissors size="18" /> <Scissors size="18" class="size-4.5" />
</ToolbarItem> </ToolbarItem>
<ToolbarItem itemTool={Tool.TIME} label={i18n._('toolbar.time.tooltip')}> <ToolbarItem itemTool={Tool.TIME} label={i18n._('toolbar.time.tooltip')}>
<CalendarClock size="18" /> <CalendarClock size="18" class="size-4.5" />
</ToolbarItem> </ToolbarItem>
<ToolbarItem itemTool={Tool.MERGE} label={i18n._('toolbar.merge.tooltip')}> <ToolbarItem itemTool={Tool.MERGE} label={i18n._('toolbar.merge.tooltip')}>
<Group size="18" /> <Group size="18" class="size-4.5" />
</ToolbarItem> </ToolbarItem>
<ToolbarItem itemTool={Tool.EXTRACT} label={i18n._('toolbar.extract.tooltip')}> <ToolbarItem itemTool={Tool.EXTRACT} label={i18n._('toolbar.extract.tooltip')}>
<Ungroup size="18" /> <Ungroup size="18" class="size-4.5" />
</ToolbarItem> </ToolbarItem>
<ToolbarItem itemTool={Tool.ELEVATION} label={i18n._('toolbar.elevation.button')}> <ToolbarItem itemTool={Tool.ELEVATION} label={i18n._('toolbar.elevation.button')}>
<MountainSnow size="18" /> <MountainSnow size="18" class="size-4.5" />
</ToolbarItem> </ToolbarItem>
<ToolbarItem itemTool={Tool.REDUCE} label={i18n._('toolbar.reduce.tooltip')}> <ToolbarItem itemTool={Tool.REDUCE} label={i18n._('toolbar.reduce.tooltip')}>
<Funnel size="18" /> <Funnel size="18" class="size-4.5" />
</ToolbarItem> </ToolbarItem>
<ToolbarItem itemTool={Tool.CLEAN} label={i18n._('toolbar.clean.tooltip')}> <ToolbarItem itemTool={Tool.CLEAN} label={i18n._('toolbar.clean.tooltip')}>
<SquareDashedMousePointer size="18" /> <SquareDashedMousePointer size="18" class="size-4.5" />
</ToolbarItem> </ToolbarItem>
</div> </div>
<ToolbarItemMenu class={props.class ?? ''} /> <ToolbarItemMenu class={props.class ?? ''} />

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Tooltip from '$lib/components/ui/tooltip/index.js'; import * as Tooltip from '$lib/components/ui/tooltip/index.js';
import { tool, Tool } from '$lib/components/toolbar/tools'; import { currentTool, Tool } from '$lib/components/toolbar/tools';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
let { let {
@@ -15,22 +15,22 @@
} = $props(); } = $props();
function toggleTool() { function toggleTool() {
if (tool.current === itemTool) { if ($currentTool === itemTool) {
tool.current = null; $currentTool = null;
} else { } else {
tool.current = itemTool; $currentTool = itemTool;
} }
} }
</script> </script>
<Tooltip.Provider> <Tooltip.Provider>
<Tooltip.Root delayDuration={300}> <Tooltip.Root>
<Tooltip.Trigger> <Tooltip.Trigger>
{#snippet child({ props })} {#snippet child({ props })}
<Button <Button
{...props} {...props}
variant="ghost" variant="ghost"
class="h-[26px] px-1 py-1.5 {tool.current === itemTool ? 'bg-accent' : ''}" class="size-[24px] {$currentTool === itemTool ? 'bg-accent' : ''}"
onclick={toggleTool} onclick={toggleTool}
aria-label={label} aria-label={label}
> >

View File

@@ -1,68 +1,64 @@
<script lang="ts"> <script lang="ts">
import { Tool, tool } from '$lib/components/toolbar/tools'; import { Tool, currentTool } from '$lib/components/toolbar/tools';
import * as Card from '$lib/components/ui/card'; import * as Card from '$lib/components/ui/card';
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte'; import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
import Scissors from '$lib/components/toolbar/tools/scissors/Scissors.svelte'; import Scissors from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte'; import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
import Time from '$lib/components/toolbar/tools/Time.svelte'; import Time from '$lib/components/toolbar/tools/Time.svelte';
import Merge from '$lib/components/toolbar/tools/Merge.svelte'; import Merge from '$lib/components/toolbar/tools/Merge.svelte';
import Extract from '$lib/components/toolbar/tools/Extract.svelte';
import Elevation from '$lib/components/toolbar/tools/Elevation.svelte'; import Elevation from '$lib/components/toolbar/tools/Elevation.svelte';
import Extract from '$lib/components/toolbar/tools/Extract.svelte';
import Clean from '$lib/components/toolbar/tools/Clean.svelte'; import Clean from '$lib/components/toolbar/tools/Clean.svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte'; import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte';
import { onMount } from 'svelte';
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
let { let {
popupElement,
popup,
class: className = '', class: className = '',
}: { }: {
popupElement: HTMLDivElement;
popup: mapboxgl.Popup;
class: string; class: string;
} = $props(); } = $props();
const { minimizeRoutingMenu } = settings; const { minimizeRoutingMenu } = settings;
onMount(() => { let popupElement: HTMLDivElement | undefined = $state(undefined);
popup = new mapboxgl.Popup({ let popup: mapboxgl.Popup | undefined = $derived.by(() => {
if (!popupElement) {
return undefined;
}
let popup = new mapboxgl.Popup({
closeButton: false, closeButton: false,
maxWidth: undefined, maxWidth: undefined,
}); });
popup.setDOMContent(popupElement); popup.setDOMContent(popupElement);
popupElement.classList.remove('hidden'); popupElement.classList.remove('hidden');
return popup;
}); });
</script> </script>
{#if tool.current !== null} {#if $currentTool !== null}
<div class="translate-x-1 h-full animate-in animate-out {className}"> <div class="translate-x-1 h-full animate-in animate-out {className}">
<div class="rounded-md shadow-md pointer-events-auto"> <div class="rounded-md shadow-md pointer-events-auto">
<Card.Root class="rounded-md border-none"> <Card.Root class="rounded-md border-none py-2.5">
<Card.Content class="p-2.5"> <Card.Content class="px-2.5">
{#if tool.current === Tool.ROUTING} {#if $currentTool === Tool.ROUTING}
<Routing <Routing {popup} {popupElement} bind:minimized={$minimizeRoutingMenu} />
{popup} {:else if $currentTool === Tool.SCISSORS}
{popupElement}
bind:minimized={minimizeRoutingMenu.value}
/>
{:else if tool.current === Tool.SCISSORS}
<Scissors /> <Scissors />
{:else if tool.current === Tool.WAYPOINT} {:else if $currentTool === Tool.WAYPOINT}
<Waypoint /> <Waypoint />
{:else if tool.current === Tool.TIME} {:else if $currentTool === Tool.TIME}
<Time /> <Time />
{:else if tool.current === Tool.MERGE} {:else if $currentTool === Tool.MERGE}
<Merge /> <Merge />
{:else if tool.current === Tool.ELEVATION} {:else if $currentTool === Tool.ELEVATION}
<Elevation /> <Elevation />
{:else if tool.current === Tool.EXTRACT} {:else if $currentTool === Tool.EXTRACT}
<Extract /> <Extract />
{:else if tool.current === Tool.CLEAN} {:else if $currentTool === Tool.CLEAN}
<Clean /> <Clean />
{:else if tool.current === Tool.REDUCE} {:else if $currentTool === Tool.REDUCE}
<Reduce /> <Reduce />
{/if} {/if}
</Card.Content> </Card.Content>
@@ -73,8 +69,8 @@
<svelte:window <svelte:window
on:keydown={(e) => { on:keydown={(e) => {
if (tool.current !== null && e.key === 'Escape') { if ($currentTool !== null && e.key === 'Escape') {
tool.current = null; $currentTool = null;
} }
}} }}
/> />

View File

@@ -13,12 +13,13 @@
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { Trash2 } from '@lucide/svelte'; import { Trash2 } from '@lucide/svelte';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import type { GeoJSONSource } from 'mapbox-gl'; import type { GeoJSONSource } from 'mapbox-gl';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
let props: { let props: {
class?: string; class?: string;
@@ -30,10 +31,10 @@
let rectangleCoordinates: mapboxgl.LngLat[] = $state([]); let rectangleCoordinates: mapboxgl.LngLat[] = $state([]);
$effect(() => { $effect(() => {
if (map.value) { if ($map) {
if (rectangleCoordinates.length != 2) { if (rectangleCoordinates.length != 2) {
if (map.value.getLayer('rectangle')) { if ($map.getLayer('rectangle')) {
map.value.removeLayer('rectangle'); $map.removeLayer('rectangle');
} }
} else { } else {
let data: GeoJSON.Feature = { let data: GeoJSON.Feature = {
@@ -52,17 +53,17 @@
}, },
properties: {}, properties: {},
}; };
let source: GeoJSONSource | undefined = map.value.getSource('rectangle'); let source: GeoJSONSource | undefined = $map.getSource('rectangle');
if (source) { if (source) {
source.setData(data); source.setData(data);
} else { } else {
map.value.addSource('rectangle', { $map.addSource('rectangle', {
type: 'geojson', type: 'geojson',
data: data, data: data,
}); });
} }
if (!map.value.getLayer('rectangle')) { if (!$map.getLayer('rectangle')) {
map.value.addLayer({ $map.addLayer({
id: 'rectangle', id: 'rectangle',
type: 'fill', type: 'fill',
source: 'rectangle', source: 'rectangle',
@@ -93,39 +94,39 @@
} }
onMount(() => { onMount(() => {
if (map.value) { if ($map) {
setCrosshairCursor(map.value.getCanvas()); mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, true);
map.value.on('mousedown', onMouseDown); $map.on('mousedown', onMouseDown);
map.value.on('mousemove', onMouseMove); $map.on('mousemove', onMouseMove);
map.value.on('mouseup', onMouseUp); $map.on('mouseup', onMouseUp);
map.value.on('touchstart', onMouseDown); $map.on('touchstart', onMouseDown);
map.value.on('touchmove', onMouseMove); $map.on('touchmove', onMouseMove);
map.value.on('touchend', onMouseUp); $map.on('touchend', onMouseUp);
map.value.dragPan.disable(); $map.dragPan.disable();
} }
}); });
onDestroy(() => { onDestroy(() => {
if (map.value) { if ($map) {
resetCursor(map.value.getCanvas()); mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false);
map.value.off('mousedown', onMouseDown); $map.off('mousedown', onMouseDown);
map.value.off('mousemove', onMouseMove); $map.off('mousemove', onMouseMove);
map.value.off('mouseup', onMouseUp); $map.off('mouseup', onMouseUp);
map.value.off('touchstart', onMouseDown); $map.off('touchstart', onMouseDown);
map.value.off('touchmove', onMouseMove); $map.off('touchmove', onMouseMove);
map.value.off('touchend', onMouseUp); $map.off('touchend', onMouseUp);
map.value.dragPan.enable(); $map.dragPan.enable();
if (map.value.getLayer('rectangle')) { if ($map.getLayer('rectangle')) {
map.value.removeLayer('rectangle'); $map.removeLayer('rectangle');
} }
if (map.value.getSource('rectangle')) { if ($map.getSource('rectangle')) {
map.value.removeSource('rectangle'); $map.removeSource('rectangle');
} }
} }
}); });
let validSelection = $derived(selection.value.size > 0); let validSelection = $derived($selection.size > 0);
</script> </script>
<div class="flex flex-col gap-3 w-full max-w-80 items-center {props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 items-center {props.class ?? ''}">

View File

@@ -12,7 +12,7 @@
class?: string; class?: string;
} = $props(); } = $props();
let validSelection = $derived(selection.value.size > 0); let validSelection = $derived($selection.size > 0);
</script> </script>
<div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
@@ -21,8 +21,8 @@
class="whitespace-normal h-fit" class="whitespace-normal h-fit"
disabled={!validSelection} disabled={!validSelection}
onclick={async () => { onclick={async () => {
if (map.value) { if ($map) {
fileActions.addElevationToSelection(map.value); fileActions.addElevationToSelection($map);
} }
}} }}
> >

View File

@@ -20,8 +20,8 @@
} = $props(); } = $props();
let validSelection = $derived( let validSelection = $derived(
selection.value.size > 0 && $selection.size > 0 &&
selection.value.getSelected().every((item) => { $selection.getSelected().every((item) => {
if ( if (
item instanceof ListWaypointsItem || item instanceof ListWaypointsItem ||
item instanceof ListWaypointItem || item instanceof ListWaypointItem ||

View File

@@ -16,20 +16,20 @@
import { Group } from '@lucide/svelte'; import { Group } from '@lucide/svelte';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import Shortcut from '$lib/components/Shortcut.svelte'; import Shortcut from '$lib/components/Shortcut.svelte';
import { gpxStatistics } from '$lib/stores';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import { fileStateCollection } from '$lib/logic/file-state'; import { fileStateCollection } from '$lib/logic/file-state';
import { fileActions } from '$lib/logic/file-actions'; import { fileActions } from '$lib/logic/file-actions';
import { gpxStatistics } from '$lib/logic/statistics';
let props: { let props: {
class?: string; class?: string;
} = $props(); } = $props();
let canMergeTraces = $derived.by(() => { let canMergeTraces = $derived.by(() => {
if (selection.value.size > 1) { if ($selection.size > 1) {
return true; return true;
} else if (selection.value.size === 1) { } else if ($selection.size === 1) {
let selected = selection.value.getSelected()[0]; let selected = $selection.getSelected()[0];
if (selected instanceof ListFileItem) { if (selected instanceof ListFileItem) {
let file = fileStateCollection.getFile(selected.getFileId()); let file = fileStateCollection.getFile(selected.getFileId());
if (file) { if (file) {
@@ -47,8 +47,8 @@
}); });
let canMergeContents = $derived( let canMergeContents = $derived(
selection.value.size > 1 && $selection.size > 1 &&
selection.value $selection
.getSelected() .getSelected()
.some((item) => item instanceof ListFileItem || item instanceof ListTrackItem) .some((item) => item instanceof ListFileItem || item instanceof ListTrackItem)
); );
@@ -95,22 +95,14 @@
{:else if mergeType === MergeType.TRACES && !canMergeTraces} {:else if mergeType === MergeType.TRACES && !canMergeTraces}
{i18n._('toolbar.merge.help_cannot_merge_traces')} {i18n._('toolbar.merge.help_cannot_merge_traces')}
{i18n._('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[0]} {i18n._('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[0]}
<Shortcut <Shortcut ctrl={true} click={true} class="border" />
ctrl={true}
click={true}
class="inline-flex text-muted-foreground text-xs border rounded p-0.5 gap-0"
/>
{i18n._('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[1]} {i18n._('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[1]}
{:else if mergeType === MergeType.CONTENTS && canMergeContents} {:else if mergeType === MergeType.CONTENTS && canMergeContents}
{i18n._('toolbar.merge.help_merge_contents')} {i18n._('toolbar.merge.help_merge_contents')}
{:else if mergeType === MergeType.CONTENTS && !canMergeContents} {:else if mergeType === MergeType.CONTENTS && !canMergeContents}
{i18n._('toolbar.merge.help_cannot_merge_contents')} {i18n._('toolbar.merge.help_cannot_merge_contents')}
{i18n._('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[0]} {i18n._('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[0]}
<Shortcut <Shortcut ctrl={true} click={true} class="border" />
ctrl={true}
click={true}
class="inline-flex text-muted-foreground text-xs border rounded p-0.5 gap-0"
/>
{i18n._('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[1]} {i18n._('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[1]}
{/if} {/if}
</Help> </Help>

View File

@@ -1,195 +0,0 @@
<script lang="ts">
import { Label } from '$lib/components/ui/label/index.js';
import { Button } from '$lib/components/ui/button';
import { Slider } from '$lib/components/ui/slider';
import {
ListItem,
ListRootItem,
ListTrackSegmentItem,
} from '$lib/components/file-list/file-list';
import Help from '$lib/components/Help.svelte';
import { Funnel } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte';
import WithUnits from '$lib/components/WithUnits.svelte';
import { map } from '$lib/components/map/map';
import { onDestroy } from 'svelte';
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
import { getURLForLanguage } from '$lib/utils';
import type { GeoJSONSource } from 'mapbox-gl';
import { selection } from '$lib/logic/selection';
import { fileActions } from '$lib/logic/file-actions';
let props: { class?: string } = $props();
let sliderValue = $state([50]);
let maxPoints = $state(0);
let currentPoints = $state(0);
const minTolerance = 0.1;
const maxTolerance = 10000;
let validSelection = $derived(
selection.value.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
);
let tolerance = $derived(
minTolerance * 2 ** (sliderValue[0] / (100 / Math.log2(maxTolerance / minTolerance)))
);
let simplified = new Map<string, [ListItem, number, SimplifiedTrackPoint[]]>();
let unsubscribes = new Map<string, () => void>();
function update() {
maxPoints = 0;
currentPoints = 0;
let data: GeoJSON.FeatureCollection = {
type: 'FeatureCollection',
features: [],
};
simplified.forEach(([item, maxPts, points], itemFullId) => {
maxPoints += maxPts;
let current = points.filter(
(point) => point.distance === undefined || point.distance >= tolerance
);
currentPoints += current.length;
data.features.push({
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: current.map((point) => [
point.point.getLongitude(),
point.point.getLatitude(),
]),
},
properties: {},
});
});
if (map.value) {
let source: GeoJSONSource | undefined = map.value.getSource('simplified');
if (source) {
source.setData(data);
} else {
map.value.addSource('simplified', {
type: 'geojson',
data: data,
});
}
if (!map.value.getLayer('simplified')) {
map.value.addLayer({
id: 'simplified',
type: 'line',
source: 'simplified',
paint: {
'line-color': 'white',
'line-width': 3,
},
});
} else {
map.value.moveLayer('simplified');
}
}
}
// $effect(() => {
// if ($fileObservers) {
// unsubscribes.forEach((unsubscribe, fileId) => {
// if (!$fileObservers.has(fileId)) {
// unsubscribe();
// unsubscribes.delete(fileId);
// }
// });
// $fileObservers.forEach((fileStore, fileId) => {
// if (!unsubscribes.has(fileId)) {
// let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [
// fs,
// sel,
// ]).subscribe(([fs, sel]) => {
// if (fs) {
// fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
// let segmentItem = new ListTrackSegmentItem(
// fileId,
// trackIndex,
// segmentIndex
// );
// if (sel.hasAnyParent(segmentItem)) {
// let statistics = fs.statistics.getStatisticsFor(segmentItem);
// simplified.set(segmentItem.getFullId(), [
// segmentItem,
// statistics.local.points.length,
// ramerDouglasPeucker(statistics.local.points, minTolerance),
// ]);
// update();
// } else if (simplified.has(segmentItem.getFullId())) {
// simplified.delete(segmentItem.getFullId());
// update();
// }
// });
// }
// });
// unsubscribes.set(fileId, unsubscribe);
// }
// });
// }
// });
$effect(() => {
if (tolerance) {
update();
}
});
onDestroy(() => {
if (map.value) {
if (map.value.getLayer('simplified')) {
map.value.removeLayer('simplified');
}
if (map.value.getSource('simplified')) {
map.value.removeSource('simplified');
}
}
unsubscribes.forEach((unsubscribe) => unsubscribe());
simplified.clear();
});
function reduce() {
let itemsAndPoints = new Map<ListItem, TrackPoint[]>();
simplified.forEach(([item, maxPts, points], itemFullId) => {
itemsAndPoints.set(
item,
points
.filter((point) => point.distance === undefined || point.distance >= tolerance)
.map((point) => point.point)
);
});
fileActions.reduce(itemsAndPoints);
}
</script>
<div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
<div class="p-2">
<Slider bind:value={sliderValue} min={0} max={100} step={1} type="multiple" />
</div>
<Label class="flex flex-row justify-between">
<span>{i18n._('toolbar.reduce.tolerance')}</span>
<WithUnits value={tolerance / 1000} type="distance" decimals={4} class="font-normal" />
</Label>
<Label class="flex flex-row justify-between">
<span>{i18n._('toolbar.reduce.number_of_points')}</span>
<span class="font-normal">{currentPoints}/{maxPoints}</span>
</Label>
<Button variant="outline" disabled={!validSelection} onclick={reduce}>
<Funnel size="16" class="mr-1" />
{i18n._('toolbar.reduce.button')}
</Button>
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/minify')}>
{#if validSelection}
{i18n._('toolbar.reduce.help')}
{:else}
{i18n._('toolbar.reduce.help_no_selection')}
{/if}
</Help>
</div>

View File

@@ -5,7 +5,6 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Checkbox } from '$lib/components/ui/checkbox'; import { Checkbox } from '$lib/components/ui/checkbox';
import TimePicker from '$lib/components/ui/time-picker/TimePicker.svelte'; import TimePicker from '$lib/components/ui/time-picker/TimePicker.svelte';
import { gpxStatistics } from '$lib/stores';
import { import {
distancePerHourToSecondsPerDistance, distancePerHourToSecondsPerDistance,
getConvertedVelocity, getConvertedVelocity,
@@ -26,20 +25,20 @@
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { fileActions } from '$lib/logic/file-actions';
import { fileActionManager } from '$lib/logic/file-action-manager'; import { fileActionManager } from '$lib/logic/file-action-manager';
import { gpxStatistics } from '$lib/logic/statistics';
let props: { let props: {
class?: string; class?: string;
} = $props(); } = $props();
let startDate: DateValue | undefined = undefined; let startDate: DateValue | undefined = $state(undefined);
let startTime: string | undefined = undefined; let startTime: string | undefined = $state(undefined);
let endDate: DateValue | undefined = undefined; let endDate: DateValue | undefined = $state(undefined);
let endTime: string | undefined = undefined; let endTime: string | undefined = $state(undefined);
let movingTime: number | undefined = undefined; let movingTime: number | undefined = $state(undefined);
let speed: number | undefined = undefined; let speed: number | undefined = $state(undefined);
let artificial = false; let artificial = $state(false);
function toCalendarDate(date: Date): CalendarDate { function toCalendarDate(date: Date): CalendarDate {
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate()); return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
@@ -53,7 +52,7 @@
function setSpeed(value: number) { function setSpeed(value: number) {
let speedValue = getConvertedVelocity(value); let speedValue = getConvertedVelocity(value);
if (velocityUnits.value === 'speed') { if ($velocityUnits === 'speed') {
speedValue = parseFloat(speedValue.toFixed(2)); speedValue = parseFloat(speedValue.toFixed(2));
} }
speed = speedValue; speed = speedValue;
@@ -86,9 +85,11 @@
} }
} }
// $: if ($gpxStatistics && $velocityUnits && $distanceUnits) { $effect(() => {
// setGPXData(); if ($gpxStatistics && $velocityUnits && $distanceUnits) {
// } setGPXData();
}
});
function getDate(date: DateValue, time: string): Date { function getDate(date: DateValue, time: string): Date {
if (date === undefined) { if (date === undefined) {
@@ -139,12 +140,12 @@
} }
let speedValue = speed; let speedValue = speed;
if (velocityUnits.value === 'pace') { if ($velocityUnits === 'pace') {
speedValue = distancePerHourToSecondsPerDistance(speed); speedValue = distancePerHourToSecondsPerDistance(speed);
} }
if (distanceUnits.value === 'imperial') { if ($distanceUnits === 'imperial') {
speedValue = milesToKilometers(speedValue); speedValue = milesToKilometers(speedValue);
} else if (distanceUnits.value === 'nautical') { } else if ($distanceUnits === 'nautical') {
speedValue = nauticalMilesToKilometers(speedValue); speedValue = nauticalMilesToKilometers(speedValue);
} }
return speedValue; return speedValue;
@@ -178,8 +179,7 @@
} }
let canUpdate = $derived( let canUpdate = $derived(
selection.value.size === 1 && $selection.size === 1 && $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
selection.value.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
); );
</script> </script>
@@ -189,14 +189,14 @@
<div class="flex flex-col gap-2 grow"> <div class="flex flex-col gap-2 grow">
<Label for="speed" class="flex flex-row"> <Label for="speed" class="flex flex-row">
<Zap size="16" class="mr-1" /> <Zap size="16" class="mr-1" />
{#if velocityUnits.value === 'speed'} {#if $velocityUnits === 'speed'}
{i18n._('quantities.speed')} {i18n._('quantities.speed')}
{:else} {:else}
{i18n._('quantities.pace')} {i18n._('quantities.pace')}
{/if} {/if}
</Label> </Label>
<div class="flex flex-row gap-1 items-center"> <div class="flex flex-row gap-1 items-center">
{#if velocityUnits.value === 'speed'} {#if $velocityUnits === 'speed'}
<Input <Input
id="speed" id="speed"
type="number" type="number"
@@ -205,13 +205,14 @@
disabled={!canUpdate} disabled={!canUpdate}
bind:value={speed} bind:value={speed}
onchange={updateDataFromSpeed} onchange={updateDataFromSpeed}
class="text-sm"
/> />
<span class="text-sm shrink-0"> <span class="text-sm shrink-0">
{#if distanceUnits.value === 'imperial'} {#if $distanceUnits === 'imperial'}
{i18n._('units.miles_per_hour')} {i18n._('units.miles_per_hour')}
{:else if distanceUnits.value === 'metric'} {:else if $distanceUnits === 'metric'}
{i18n._('units.kilometers_per_hour')} {i18n._('units.kilometers_per_hour')}
{:else if distanceUnits.value === 'nautical'} {:else if $distanceUnits === 'nautical'}
{i18n._('units.knots')} {i18n._('units.knots')}
{/if} {/if}
</span> </span>
@@ -223,11 +224,11 @@
onChange={updateDataFromSpeed} onChange={updateDataFromSpeed}
/> />
<span class="text-sm shrink-0"> <span class="text-sm shrink-0">
{#if distanceUnits.value === 'imperial'} {#if $distanceUnits === 'imperial'}
{i18n._('units.minutes_per_mile')} {i18n._('units.minutes_per_mile')}
{:else if distanceUnits.value === 'metric'} {:else if $distanceUnits === 'metric'}
{i18n._('units.minutes_per_kilometer')} {i18n._('units.minutes_per_kilometer')}
{:else if distanceUnits.value === 'nautical'} {:else if $distanceUnits === 'nautical'}
{i18n._('units.minutes_per_nautical_mile')} {i18n._('units.minutes_per_nautical_mile')}
{/if} {/if}
</span> </span>
@@ -332,7 +333,7 @@
ratio = $gpxStatistics.global.speed.moving / effectiveSpeed; ratio = $gpxStatistics.global.speed.moving / effectiveSpeed;
} }
let item = selection.value.getSelected()[0]; let item = $selection.getSelected()[0];
let fileId = item.getFileId(); let fileId = item.getFileId();
fileActionManager.applyToFile(fileId, (file) => { fileActionManager.applyToFile(fileId, (file) => {
if (item instanceof ListFileItem) { if (item instanceof ListFileItem) {

View File

@@ -0,0 +1,63 @@
<script lang="ts">
import { Label } from '$lib/components/ui/label/index.js';
import { Button } from '$lib/components/ui/button';
import { Slider } from '$lib/components/ui/slider';
import { ListItem, ListRootItem } from '$lib/components/file-list/file-list';
import Help from '$lib/components/Help.svelte';
import { Funnel } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte';
import WithUnits from '$lib/components/WithUnits.svelte';
import { onDestroy } from 'svelte';
import { getURLForLanguage } from '$lib/utils';
import { selection } from '$lib/logic/selection';
import { minTolerance, ReducedGPXLayerCollection, tolerance } from './reduce';
let props: { class?: string } = $props();
let sliderValue = $state([50]);
let maxPoints = $state(0);
let currentPoints = $state(0);
const maxTolerance = 10000;
let validSelection = $derived(
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
);
let reducedLayers = new ReducedGPXLayerCollection();
$effect(() => {
tolerance.set(
minTolerance * 2 ** (sliderValue[0] / (100 / Math.log2(maxTolerance / minTolerance)))
);
});
onDestroy(() => {
reducedLayers.destroy();
});
</script>
<div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
<div class="p-2">
<Slider bind:value={sliderValue} min={0} max={100} step={1} type="multiple" />
</div>
<Label class="flex flex-row justify-between">
<span>{i18n._('toolbar.reduce.tolerance')}</span>
<WithUnits value={$tolerance / 1000} type="distance" decimals={4} class="font-normal" />
</Label>
<Label class="flex flex-row justify-between">
<span>{i18n._('toolbar.reduce.number_of_points')}</span>
<span class="font-normal">{currentPoints}/{maxPoints}</span>
</Label>
<Button variant="outline" disabled={!validSelection} onclick={() => reducedLayers.reduce()}>
<Funnel size="16" class="mr-1" />
{i18n._('toolbar.reduce.button')}
</Button>
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/minify')}>
{#if validSelection}
{i18n._('toolbar.reduce.help')}
{:else}
{i18n._('toolbar.reduce.help_no_selection')}
{/if}
</Help>
</div>

View File

@@ -0,0 +1,187 @@
import { ListItem, ListTrackSegmentItem } from '$lib/components/file-list/file-list';
import { map } from '$lib/components/map/map';
import { fileActions } from '$lib/logic/file-actions';
import { GPXFileStateCollectionObserver, type GPXFileState } from '$lib/logic/file-state';
import { selection } from '$lib/logic/selection';
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
import type { GeoJSONSource } from 'mapbox-gl';
import { get, writable } from 'svelte/store';
export const minTolerance = 0.1;
export class ReducedGPXLayer {
private _fileState: GPXFileState;
private _updateSimplified: (
itemId: string,
data: [ListItem, number, SimplifiedTrackPoint[]]
) => void;
private _unsubscribes: (() => void)[] = [];
constructor(
fileState: GPXFileState,
updateSimplified: (itemId: string, data: [ListItem, number, SimplifiedTrackPoint[]]) => void
) {
this._fileState = fileState;
this._updateSimplified = updateSimplified;
this._unsubscribes.push(this._fileState.subscribe(() => this.update()));
}
update() {
const file = this._fileState.file;
const stats = this._fileState.statistics;
if (!file || !stats) {
return;
}
file.forEachSegment((segment, trackIndex, segmentIndex) => {
let segmentItem = new ListTrackSegmentItem(file._data.id, trackIndex, segmentIndex);
let statistics = stats.getStatisticsFor(segmentItem);
this._updateSimplified(segmentItem.getFullId(), [
segmentItem,
statistics.local.points.length,
ramerDouglasPeucker(statistics.local.points, minTolerance),
]);
});
}
destroy() {
this._unsubscribes.forEach((unsubscribe) => unsubscribe());
}
}
export const tolerance = writable<number>(0);
export class ReducedGPXLayerCollection {
private _layers: Map<string, ReducedGPXLayer> = new Map();
private _simplified: Map<string, [ListItem, number, SimplifiedTrackPoint[]]>;
private _fileStateCollectionOberver: GPXFileStateCollectionObserver;
private _updateSimplified = this.updateSimplified.bind(this);
private _unsubscribes: (() => void)[] = [];
constructor() {
this._layers = new Map();
this._simplified = new Map();
this._fileStateCollectionOberver = new GPXFileStateCollectionObserver(
(fileId, fileState) => {
this._layers.set(fileId, new ReducedGPXLayer(fileState, this._updateSimplified));
},
(fileId) => {
this._layers.get(fileId)?.destroy();
this._layers.delete(fileId);
},
() => {
this._layers.forEach((layer) => layer.destroy());
this._layers.clear();
}
);
this._unsubscribes.push(selection.subscribe(() => this.update()));
this._unsubscribes.push(tolerance.subscribe(() => this.update()));
}
updateSimplified(itemId: string, data: [ListItem, number, SimplifiedTrackPoint[]]) {
this._simplified.set(itemId, data);
if (get(selection).hasAnyParent(data[0])) {
this.update();
}
}
removeSimplified(itemId: string) {
if (this._simplified.delete(itemId)) {
this.update();
}
}
update() {
let maxPoints = 0;
let currentPoints = 0;
let data: GeoJSON.FeatureCollection = {
type: 'FeatureCollection',
features: [],
};
this._simplified.forEach(([item, maxPts, points], itemFullId) => {
if (!get(selection).hasAnyParent(item)) {
return;
}
maxPoints += maxPts;
let current = points.filter(
(point) => point.distance === undefined || point.distance >= get(tolerance)
);
currentPoints += current.length;
data.features.push({
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: current.map((point) => [
point.point.getLongitude(),
point.point.getLatitude(),
]),
},
properties: {},
});
});
const map_ = get(map);
if (!map_) {
return;
}
let source: GeoJSONSource | undefined = map_.getSource('simplified');
if (source) {
source.setData(data);
} else {
map_.addSource('simplified', {
type: 'geojson',
data: data,
});
}
if (!map_.getLayer('simplified')) {
map_.addLayer({
id: 'simplified',
type: 'line',
source: 'simplified',
paint: {
'line-color': 'white',
'line-width': 3,
},
});
} else {
map_.moveLayer('simplified');
}
}
reduce() {
let itemsAndPoints = new Map<ListItem, TrackPoint[]>();
this._simplified.forEach(([item, maxPts, points], itemFullId) => {
itemsAndPoints.set(
item,
points
.filter(
(point) => point.distance === undefined || point.distance >= get(tolerance)
)
.map((point) => point.point)
);
});
fileActions.reduce(itemsAndPoints);
}
destroy() {
this._fileStateCollectionOberver.destroy();
this._unsubscribes.forEach((unsubscribe) => unsubscribe());
const map_ = get(map);
if (!map_) {
return;
}
if (map_.getLayer('simplified')) {
map_.removeLayer('simplified');
}
if (map_.getSource('simplified')) {
map_.removeSource('simplified');
}
}
}

View File

@@ -21,9 +21,8 @@
SquareArrowUpLeft, SquareArrowUpLeft,
SquareArrowOutDownRight, SquareArrowOutDownRight,
} from '@lucide/svelte'; } from '@lucide/svelte';
import { brouterProfiles } from '$lib/components/toolbar/tools/routing/utils.svelte'; import { brouterProfiles } from '$lib/components/toolbar/tools/routing/routing';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
// import { RoutingControls } from './RoutingControls';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { import {
ListFileItem, ListFileItem,
@@ -32,14 +31,16 @@
ListTrackSegmentItem, ListTrackSegmentItem,
type ListItem, type ListItem,
} from '$lib/components/file-list/file-list'; } from '$lib/components/file-list/file-list';
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { TrackPoint } from 'gpx'; import { TrackPoint } from 'gpx';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import { fileStateCollection } from '$lib/logic/file-state'; import { fileStateCollection, GPXFileStateCollectionObserver } from '$lib/logic/file-state';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import { fileActions, getFileIds, newGPXFile } from '$lib/logic/file-actions'; import { fileActions, getFileIds, newGPXFile } from '$lib/logic/file-actions';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import { RoutingControls, routingControls } from './RoutingControls';
let { let {
minimized = $bindable(false), minimized = $bindable(false),
@@ -55,34 +56,9 @@
class?: string; class?: string;
} = $props(); } = $props();
let selectedItem: ListItem | null = null;
const { privateRoads, routing, routingProfile } = settings; const { privateRoads, routing, routingProfile } = settings;
// $: if (map && popup && popupElement) { let fileStateCollectionObserver: GPXFileStateCollectionObserver;
// // remove controls for deleted files
// routingControls.forEach((controls, fileId) => {
// if (!$fileObservers.has(fileId)) {
// controls.destroy();
// routingControls.delete(fileId);
// if (selectedItem && selectedItem.getFileId() === fileId) {
// selectedItem = null;
// }
// } else if ($map !== controls.map) {
// controls.updateMap($map);
// }
// });
// // add controls for new files
// fileStateCollection.files.forEach((file, fileId) => {
// if (!routingControls.has(fileId)) {
// routingControls.set(
// fileId,
// new RoutingControls($map, fileId, file, popup, popupElement)
// );
// }
// });
// }
let validSelection = $derived( let validSelection = $derived(
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
@@ -101,36 +77,61 @@
]); ]);
file._data.id = getFileIds(1)[0]; file._data.id = getFileIds(1)[0];
fileActions.add(file); fileActions.add(file);
// selectFileWhenLoaded(file._data.id); selection.selectFileWhenLoaded(file._data.id);
} }
} }
onMount(() => { onMount(() => {
// setCrosshairCursor(); if ($map && popup && popupElement) {
$map?.on('click', createFileWithPoint); fileStateCollectionObserver = new GPXFileStateCollectionObserver(
(fileId, fileState) => {
routingControls.set(
fileId,
new RoutingControls(fileId, fileState, popup, popupElement)
);
},
(fileId) => {
const controls = routingControls.get(fileId);
if (controls) {
controls.destroy();
routingControls.delete(fileId);
}
},
() => {
routingControls.forEach((controls) => controls.destroy());
routingControls.clear();
}
);
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, true);
$map.on('click', createFileWithPoint);
}
}); });
onDestroy(() => { onDestroy(() => {
// resetCursor(); if ($map) {
$map?.off('click', createFileWithPoint); if (fileStateCollectionObserver) {
fileStateCollectionObserver.destroy();
}
// routingControls.forEach((controls) => controls.destroy()); mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false);
// routingControls.clear(); $map.off('click', createFileWithPoint);
}
}); });
</script> </script>
{#if minimizable && minimized} {#if minimizable && minimized}
<div class="-m-1.5 -mb-2"> <div class="-m-1.5 -mb-2">
<Button variant="ghost" class="px-1 h-[26px]" onclick={() => (minimized = false)}> <Button variant="ghost" size="icon-sm" class="size-6" onclick={() => (minimized = false)}>
<SquareArrowOutDownRight size="18" /> <SquareArrowOutDownRight size="18" class="size-4.5" />
</Button> </Button>
</div> </div>
{:else} {:else}
<div class="flex flex-col gap-3 w-full max-w-80 animate-in animate-out {className ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 animate-in animate-out {className ?? ''}">
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<Label class="flex flex-row justify-between items-center gap-2"> <Label class="justify-between">
<span class="flex flex-row items-center gap-1"> <span class="flex flex-row items-center gap-1">
{#if routing.value} {#if $routing}
<Route size="16" /> <Route size="16" />
{:else} {:else}
<RouteOff size="16" /> <RouteOff size="16" />
@@ -138,28 +139,30 @@
{i18n._('toolbar.routing.use_routing')} {i18n._('toolbar.routing.use_routing')}
</span> </span>
<Tooltip label={i18n._('toolbar.routing.use_routing_tooltip')}> <Tooltip label={i18n._('toolbar.routing.use_routing_tooltip')}>
<Switch class="scale-90" bind:checked={routing.value} /> <Switch bind:checked={$routing} />
<Shortcut slot="extra" key="F5" /> {#snippet extra()}
<Shortcut key="F5" />
{/snippet}
</Tooltip> </Tooltip>
</Label> </Label>
{#if routing.value} {#if $routing}
<div class="flex flex-col gap-3" in:slide> <div class="flex flex-col gap-3" in:slide>
<Label class="flex flex-row justify-between items-center gap-2"> <Label class="justify-between">
<span class="shrink-0 flex flex-row items-center gap-1"> <span class="shrink-0 flex flex-row items-center gap-1">
{#if routingProfile.value.includes('bike') || routingProfile.value.includes('motorcycle')} {#if $routingProfile.includes('bike') || $routingProfile.includes('motorcycle')}
<Bike size="16" /> <Bike size="16" />
{:else if routingProfile.value.includes('foot')} {:else if $routingProfile.includes('foot')}
<Footprints size="16" /> <Footprints size="16" />
{:else if routingProfile.value.includes('water')} {:else if $routingProfile.includes('water')}
<Waves size="16" /> <Waves size="16" />
{:else if routingProfile.value.includes('railway')} {:else if $routingProfile.includes('railway')}
<TrainFront size="16" /> <TrainFront size="16" />
{/if} {/if}
{i18n._('toolbar.routing.activity')} {i18n._('toolbar.routing.activity')}
</span> </span>
<Select.Root type="single" bind:value={routingProfile.value}> <Select.Root type="single" bind:value={$routingProfile}>
<Select.Trigger class="h-8 grow"> <Select.Trigger class="h-8 grow">
{i18n._(`toolbar.routing.activities.${routingProfile.value}`)} {i18n._(`toolbar.routing.activities.${$routingProfile}`)}
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
{#each Object.keys(brouterProfiles) as profile} {#each Object.keys(brouterProfiles) as profile}
@@ -172,12 +175,12 @@
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
</Label> </Label>
<Label class="flex flex-row justify-between items-center gap-2"> <Label class="justify-between">
<span class="flex flex-row gap-1"> <span class="flex flex-row gap-1">
<TriangleAlert size="16" /> <TriangleAlert size="16" />
{i18n._('toolbar.routing.allow_private')} {i18n._('toolbar.routing.allow_private')}
</span> </span>
<Switch class="scale-90" bind:checked={privateRoads.value} /> <Switch bind:checked={$privateRoads} />
</Label> </Label>
</div> </div>
{/if} {/if}
@@ -186,7 +189,7 @@
<ButtonWithTooltip <ButtonWithTooltip
label={i18n._('toolbar.routing.reverse.tooltip')} label={i18n._('toolbar.routing.reverse.tooltip')}
variant="outline" variant="outline"
class="flex flex-row gap-1 text-xs px-2" class="gap-1 text-xs"
disabled={!validSelection} disabled={!validSelection}
onclick={fileActions.reverseSelection} onclick={fileActions.reverseSelection}
> >
@@ -195,7 +198,7 @@
<ButtonWithTooltip <ButtonWithTooltip
label={i18n._('toolbar.routing.route_back_to_start.tooltip')} label={i18n._('toolbar.routing.route_back_to_start.tooltip')}
variant="outline" variant="outline"
class="flex flex-row gap-1 text-xs px-2" class="gap-1 text-xs"
disabled={!validSelection} disabled={!validSelection}
onclick={() => { onclick={() => {
const selected = selection.getOrderedSelection(); const selected = selection.getOrderedSelection();
@@ -218,9 +221,9 @@
if (start !== undefined) { if (start !== undefined) {
const lastFileId = selected[selected.length - 1].getFileId(); const lastFileId = selected[selected.length - 1].getFileId();
// routingControls routingControls
// .get(lastFileId) .get(lastFileId)
// ?.appendAnchorWithCoordinates(start.getCoordinates()); ?.appendAnchorWithCoordinates(start.getCoordinates());
} }
} }
} }
@@ -231,7 +234,7 @@
<ButtonWithTooltip <ButtonWithTooltip
label={i18n._('toolbar.routing.round_trip.tooltip')} label={i18n._('toolbar.routing.round_trip.tooltip')}
variant="outline" variant="outline"
class="flex flex-row gap-1 text-xs px-2" class="gap-1 text-xs"
disabled={!validSelection} disabled={!validSelection}
onclick={fileActions.createRoundTripForSelection} onclick={fileActions.createRoundTripForSelection}
> >
@@ -248,7 +251,8 @@
</Help> </Help>
<Button <Button
variant="ghost" variant="ghost"
class="px-1 h-6" size="icon-sm"
class="size-6"
onclick={() => { onclick={() => {
if (minimizable) { if (minimizable) {
minimized = true; minimized = true;

View File

@@ -7,7 +7,11 @@
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
export let element: HTMLElement; let {
element = $bindable(),
}: {
element: HTMLElement | undefined;
} = $props();
</script> </script>
<div bind:this={element} class="hidden"> <div bind:this={element} class="hidden">
@@ -17,7 +21,7 @@
<Button <Button
class="w-full px-2 py-1 h-6 justify-start" class="w-full px-2 py-1 h-6 justify-start"
variant="ghost" variant="ghost"
onclick={() => element.dispatchEvent(new CustomEvent('change-start'))} onclick={() => element?.dispatchEvent(new CustomEvent('change-start'))}
> >
<CirclePlay size="16" class="mr-1" /> <CirclePlay size="16" class="mr-1" />
{i18n._('toolbar.routing.start_loop_here')} {i18n._('toolbar.routing.start_loop_here')}
@@ -26,7 +30,7 @@
<Button <Button
class="w-full px-2 py-1 h-6 justify-start" class="w-full px-2 py-1 h-6 justify-start"
variant="ghost" variant="ghost"
onclick={() => element.dispatchEvent(new CustomEvent('delete'))} onclick={() => element?.dispatchEvent(new CustomEvent('delete'))}
> >
<Trash2 size="16" class="mr-1" /> <Trash2 size="16" class="mr-1" />
{i18n._('menu.delete')} {i18n._('menu.delete')}

View File

@@ -1,17 +1,25 @@
import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from 'gpx'; import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from 'gpx';
import { get, writable, type Readable } from 'svelte/store'; import { get, writable, type Readable } from 'svelte/store';
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import { route } from './utils.svelte'; import { route } from './routing';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { import {
ListFileItem, ListFileItem,
ListTrackItem, ListTrackItem,
ListTrackSegmentItem, ListTrackSegmentItem,
} from '$lib/components/file-list/file-list'; } from '$lib/components/file-list/file-list';
import { getClosestLinePoint, resetCursor, setGrabbingCursor } from '$lib/utils'; import { getClosestLinePoint } from '$lib/utils';
import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree'; import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import { settings } from '$lib/logic/settings';
import { selection } from '$lib/logic/selection';
import { currentTool, Tool } from '$lib/components/toolbar/tools';
import { streetViewEnabled } from '$lib/components/map/street-view-control/utils';
import { fileActionManager } from '$lib/logic/file-action-manager';
import { i18n } from '$lib/i18n.svelte';
import { map } from '$lib/components/map/map';
// const { streetViewSource } = settings; const { streetViewSource } = settings;
export const canChangeStart = writable(false); export const canChangeStart = writable(false);
function stopPropagation(e: any) { function stopPropagation(e: any) {
@@ -20,7 +28,6 @@ function stopPropagation(e: any) {
export class RoutingControls { export class RoutingControls {
active: boolean = false; active: boolean = false;
map: mapboxgl.Map;
fileId: string = ''; fileId: string = '';
file: Readable<GPXFileWithStatistics | undefined>; file: Readable<GPXFileWithStatistics | undefined>;
anchors: AnchorWithMarker[] = []; anchors: AnchorWithMarker[] = [];
@@ -39,13 +46,11 @@ export class RoutingControls {
appendAnchorBinded: (e: mapboxgl.MapMouseEvent) => void = this.appendAnchor.bind(this); appendAnchorBinded: (e: mapboxgl.MapMouseEvent) => void = this.appendAnchor.bind(this);
constructor( constructor(
map: mapboxgl.Map,
fileId: string, fileId: string,
file: Readable<GPXFileWithStatistics | undefined>, file: Readable<GPXFileWithStatistics | undefined>,
popup: mapboxgl.Popup, popup: mapboxgl.Popup,
popupElement: HTMLElement popupElement: HTMLElement
) { ) {
this.map = map;
this.fileId = fileId; this.fileId = fileId;
this.file = file; this.file = file;
this.popup = popup; this.popup = popup;
@@ -88,12 +93,17 @@ export class RoutingControls {
} }
add() { add() {
const map_ = get(map);
if (!map_) {
return;
}
this.active = true; this.active = true;
this.map.on('move', this.toggleAnchorsForZoomLevelAndBoundsBinded); map_.on('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
this.map.on('click', this.appendAnchorBinded); map_.on('click', this.appendAnchorBinded);
this.map.on('mousemove', this.fileId, this.showTemporaryAnchorBinded); map_.on('mousemove', this.fileId, this.showTemporaryAnchorBinded);
this.map.on('click', this.fileId, stopPropagation); map_.on('click', this.fileId, stopPropagation);
this.fileUnsubscribe = this.file.subscribe(this.updateControls.bind(this)); this.fileUnsubscribe = this.file.subscribe(this.updateControls.bind(this));
} }
@@ -141,25 +151,26 @@ export class RoutingControls {
} }
remove() { remove() {
const map_ = get(map);
if (!map_) {
return;
}
this.active = false; this.active = false;
for (let anchor of this.anchors) { for (let anchor of this.anchors) {
anchor.marker.remove(); anchor.marker.remove();
} }
this.map.off('move', this.toggleAnchorsForZoomLevelAndBoundsBinded); map_.off('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
this.map.off('click', this.appendAnchorBinded); map_.off('click', this.appendAnchorBinded);
this.map.off('mousemove', this.fileId, this.showTemporaryAnchorBinded); map_.off('mousemove', this.fileId, this.showTemporaryAnchorBinded);
this.map.off('click', this.fileId, stopPropagation); map_.off('click', this.fileId, stopPropagation);
this.map.off('mousemove', this.updateTemporaryAnchorBinded); map_.off('mousemove', this.updateTemporaryAnchorBinded);
this.temporaryAnchor.marker.remove(); this.temporaryAnchor.marker.remove();
this.fileUnsubscribe(); this.fileUnsubscribe();
} }
updateMap(map: mapboxgl.Map) {
this.map = map;
}
createAnchor( createAnchor(
point: TrackPoint, point: TrackPoint,
segment: TrackSegment, segment: TrackSegment,
@@ -186,13 +197,13 @@ export class RoutingControls {
marker.on('dragstart', (e) => { marker.on('dragstart', (e) => {
this.lastDragEvent = Date.now(); this.lastDragEvent = Date.now();
setGrabbingCursor(); mapCursor.notify(MapCursorState.TRACKPOINT_DRAGGING, true);
element.classList.remove('cursor-pointer'); element.classList.remove('cursor-pointer');
element.classList.add('cursor-grabbing'); element.classList.add('cursor-grabbing');
}); });
marker.on('dragend', (e) => { marker.on('dragend', (e) => {
this.lastDragEvent = Date.now(); this.lastDragEvent = Date.now();
resetCursor(); mapCursor.notify(MapCursorState.TRACKPOINT_DRAGGING, false);
element.classList.remove('cursor-grabbing'); element.classList.remove('cursor-grabbing');
element.classList.add('cursor-pointer'); element.classList.add('cursor-pointer');
this.moveAnchor(anchor); this.moveAnchor(anchor);
@@ -255,19 +266,24 @@ export class RoutingControls {
} }
toggleAnchorsForZoomLevelAndBounds() { toggleAnchorsForZoomLevelAndBounds() {
const map_ = get(map);
if (!map_) {
return;
}
// Show markers only if they are in the current zoom level and bounds // Show markers only if they are in the current zoom level and bounds
this.shownAnchors.splice(0, this.shownAnchors.length); this.shownAnchors.splice(0, this.shownAnchors.length);
let center = this.map.getCenter(); let center = map_.getCenter();
let bottomLeft = this.map.unproject([0, this.map.getCanvas().height]); let bottomLeft = map_.unproject([0, map_.getCanvas().height]);
let topRight = this.map.unproject([this.map.getCanvas().width, 0]); let topRight = map_.unproject([map_.getCanvas().width, 0]);
let diagonal = bottomLeft.distanceTo(topRight); let diagonal = bottomLeft.distanceTo(topRight);
let zoom = this.map.getZoom(); let zoom = map_.getZoom();
this.anchors.forEach((anchor) => { this.anchors.forEach((anchor) => {
anchor.inZoom = anchor.point._data.zoom <= zoom; anchor.inZoom = anchor.point._data.zoom <= zoom;
if (anchor.inZoom && center.distanceTo(anchor.marker.getLngLat()) < diagonal) { if (anchor.inZoom && center.distanceTo(anchor.marker.getLngLat()) < diagonal) {
anchor.marker.addTo(this.map); anchor.marker.addTo(map_);
this.shownAnchors.push(anchor); this.shownAnchors.push(anchor);
} else { } else {
anchor.marker.remove(); anchor.marker.remove();
@@ -276,6 +292,11 @@ export class RoutingControls {
} }
showTemporaryAnchor(e: any) { showTemporaryAnchor(e: any) {
const map_ = get(map);
if (!map_) {
return;
}
if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) { if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) {
// Do not not change the source point if it is already being dragged // Do not not change the source point if it is already being dragged
return; return;
@@ -305,25 +326,30 @@ export class RoutingControls {
lat: e.lngLat.lat, lat: e.lngLat.lat,
lon: e.lngLat.lng, lon: e.lngLat.lng,
}); });
this.temporaryAnchor.marker.setLngLat(e.lngLat).addTo(this.map); this.temporaryAnchor.marker.setLngLat(e.lngLat).addTo(map_);
this.map.on('mousemove', this.updateTemporaryAnchorBinded); map_.on('mousemove', this.updateTemporaryAnchorBinded);
} }
updateTemporaryAnchor(e: any) { updateTemporaryAnchor(e: any) {
const map_ = get(map);
if (!map_) {
return;
}
if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) { if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) {
// Do not hide if it is being dragged, and stop listening for mousemove // Do not hide if it is being dragged, and stop listening for mousemove
this.map.off('mousemove', this.updateTemporaryAnchorBinded); map_.off('mousemove', this.updateTemporaryAnchorBinded);
return; return;
} }
if ( if (
e.point.dist(this.map.project(this.temporaryAnchor.point.getCoordinates())) > 20 || e.point.dist(map_.project(this.temporaryAnchor.point.getCoordinates())) > 20 ||
this.temporaryAnchorCloseToOtherAnchor(e) this.temporaryAnchorCloseToOtherAnchor(e)
) { ) {
// Hide if too far from the layer // Hide if too far from the layer
this.temporaryAnchor.marker.remove(); this.temporaryAnchor.marker.remove();
this.map.off('mousemove', this.updateTemporaryAnchorBinded); map_.off('mousemove', this.updateTemporaryAnchorBinded);
return; return;
} }
@@ -331,8 +357,13 @@ export class RoutingControls {
} }
temporaryAnchorCloseToOtherAnchor(e: any) { temporaryAnchorCloseToOtherAnchor(e: any) {
const map_ = get(map);
if (!map_) {
return false;
}
for (let anchor of this.shownAnchors) { for (let anchor of this.shownAnchors) {
if (e.point.dist(this.map.project(anchor.marker.getLngLat())) < 10) { if (e.point.dist(map_.project(anchor.marker.getLngLat())) < 10) {
return true; return true;
} }
} }
@@ -482,7 +513,7 @@ export class RoutingControls {
}); });
if (minInfo.trackIndex !== -1) { if (minInfo.trackIndex !== -1) {
dbUtils.applyToFile(this.fileId, (file) => fileActionManager.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints( file.replaceTrackPoints(
minInfo.trackIndex, minInfo.trackIndex,
minInfo.segmentIndex, minInfo.segmentIndex,
@@ -506,12 +537,12 @@ export class RoutingControls {
if (previousAnchor === null && nextAnchor === null) { if (previousAnchor === null && nextAnchor === null) {
// Only one point, remove it // Only one point, remove it
dbUtils.applyToFile(this.fileId, (file) => fileActionManager.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, 0, []) file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, 0, [])
); );
} else if (previousAnchor === null) { } else if (previousAnchor === null) {
// First point, remove trackpoints until nextAnchor // First point, remove trackpoints until nextAnchor
dbUtils.applyToFile(this.fileId, (file) => fileActionManager.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints( file.replaceTrackPoints(
anchor.trackIndex, anchor.trackIndex,
anchor.segmentIndex, anchor.segmentIndex,
@@ -522,7 +553,7 @@ export class RoutingControls {
); );
} else if (nextAnchor === null) { } else if (nextAnchor === null) {
// Last point, remove trackpoints from previousAnchor // Last point, remove trackpoints from previousAnchor
dbUtils.applyToFile(this.fileId, (file) => { fileActionManager.applyToFile(this.fileId, (file) => {
let segment = file.getSegment(anchor.trackIndex, anchor.segmentIndex); let segment = file.getSegment(anchor.trackIndex, anchor.segmentIndex);
file.replaceTrackPoints( file.replaceTrackPoints(
anchor.trackIndex, anchor.trackIndex,
@@ -558,7 +589,7 @@ export class RoutingControls {
).global.speed.moving; ).global.speed.moving;
let segment = anchor.segment; let segment = anchor.segment;
dbUtils.applyToFile(this.fileId, (file) => { fileActionManager.applyToFile(this.fileId, (file) => {
file.replaceTrackPoints( file.replaceTrackPoints(
anchor.trackIndex, anchor.trackIndex,
anchor.segmentIndex, anchor.segmentIndex,
@@ -590,7 +621,7 @@ export class RoutingControls {
async appendAnchorWithCoordinates(coordinates: Coordinates) { async appendAnchorWithCoordinates(coordinates: Coordinates) {
// Add a new anchor to the end of the last segment // Add a new anchor to the end of the last segment
let selected = getOrderedSelection(); let selected = selection.getOrderedSelection();
if (selected.length === 0 || selected[selected.length - 1].getFileId() !== this.fileId) { if (selected.length === 0 || selected[selected.length - 1].getFileId() !== this.fileId) {
return; return;
} }
@@ -605,7 +636,7 @@ export class RoutingControls {
newPoint._data.zoom = 0; newPoint._data.zoom = 0;
if (!lastAnchor) { if (!lastAnchor) {
dbUtils.applyToFile(this.fileId, (file) => { fileActionManager.applyToFile(this.fileId, (file) => {
let trackIndex = file.trk.length > 0 ? file.trk.length - 1 : 0; let trackIndex = file.trk.length > 0 ? file.trk.length - 1 : 0;
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) { if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
trackIndex = item.getTrackIndex(); trackIndex = item.getTrackIndex();
@@ -686,7 +717,7 @@ export class RoutingControls {
if (anchors.length === 1) { if (anchors.length === 1) {
// Only one anchor, update the point in the segment // Only one anchor, update the point in the segment
dbUtils.applyToFile(this.fileId, (file) => fileActionManager.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints(anchors[0].trackIndex, anchors[0].segmentIndex, 0, 0, [ file.replaceTrackPoints(anchors[0].trackIndex, anchors[0].segmentIndex, 0, 0, [
new TrackPoint({ new TrackPoint({
attributes: targetCoordinates[0], attributes: targetCoordinates[0],
@@ -701,13 +732,13 @@ export class RoutingControls {
response = await route(targetCoordinates); response = await route(targetCoordinates);
} catch (e: any) { } catch (e: any) {
if (e.message.includes('from-position not mapped in existing datafile')) { if (e.message.includes('from-position not mapped in existing datafile')) {
toast.error(get(_)('toolbar.routing.error.from')); toast.error(i18n._('toolbar.routing.error.from'));
} else if (e.message.includes('via1-position not mapped in existing datafile')) { } else if (e.message.includes('via1-position not mapped in existing datafile')) {
toast.error(get(_)('toolbar.routing.error.via')); toast.error(i18n._('toolbar.routing.error.via'));
} else if (e.message.includes('to-position not mapped in existing datafile')) { } else if (e.message.includes('to-position not mapped in existing datafile')) {
toast.error(get(_)('toolbar.routing.error.to')); toast.error(i18n._('toolbar.routing.error.to'));
} else if (e.message.includes('Time-out')) { } else if (e.message.includes('Time-out')) {
toast.error(get(_)('toolbar.routing.error.timeout')); toast.error(i18n._('toolbar.routing.error.timeout'));
} else { } else {
toast.error(e.message); toast.error(e.message);
} }
@@ -797,7 +828,7 @@ export class RoutingControls {
} }
} }
dbUtils.applyToFile(this.fileId, (file) => fileActionManager.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints( file.replaceTrackPoints(
anchors[0].trackIndex, anchors[0].trackIndex,
anchors[0].segmentIndex, anchors[0].segmentIndex,
@@ -818,6 +849,8 @@ export class RoutingControls {
} }
} }
export const routingControls: Map<string, RoutingControls> = new Map();
type Anchor = { type Anchor = {
segment: TrackSegment; segment: TrackSegment;
trackIndex: number; trackIndex: number;

View File

@@ -2,6 +2,7 @@ import type { Coordinates } from 'gpx';
import { TrackPoint, distance } from 'gpx'; import { TrackPoint, distance } from 'gpx';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { getElevation } from '$lib/utils'; import { getElevation } from '$lib/utils';
import { get } from 'svelte/store';
const { routing, routingProfile, privateRoads } = settings; const { routing, routingProfile, privateRoads } = settings;
@@ -17,8 +18,8 @@ export const brouterProfiles: { [key: string]: string } = {
}; };
export function route(points: Coordinates[]): Promise<TrackPoint[]> { export function route(points: Coordinates[]): Promise<TrackPoint[]> {
if (routing.value) { if (get(routing)) {
return getRoute(points, brouterProfiles[routingProfile.value], privateRoads.value); return getRoute(points, brouterProfiles[get(routingProfile)], get(privateRoads));
} else { } else {
return getIntermediatePoints(points); return getIntermediatePoints(points);
} }

View File

@@ -7,16 +7,16 @@
import { Slider } from '$lib/components/ui/slider'; import { Slider } from '$lib/components/ui/slider';
import * as Select from '$lib/components/ui/select'; import * as Select from '$lib/components/ui/select';
import { Separator } from '$lib/components/ui/separator'; import { Separator } from '$lib/components/ui/separator';
import { gpxStatistics, slicedGPXStatistics } from '$lib/stores';
import { map } from '$lib/components/map/map'; import { map } from '$lib/components/map/map';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { onDestroy, tick } from 'svelte'; import { onDestroy, tick } from 'svelte';
import { Crop } from '@lucide/svelte'; import { Crop } from '@lucide/svelte';
import { dbUtils } from '$lib/db';
import { SplitControls } from './split-controls'; import { SplitControls } from './split-controls';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import { fileActions } from '$lib/logic/file-actions';
import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
let props: { let props: {
class?: string; class?: string;
@@ -26,16 +26,16 @@
let canCrop = $state(false); let canCrop = $state(false);
$effect(() => { $effect(() => {
if (map.current) { if ($map) {
if (splitControls) { if (splitControls) {
splitControls.destroy(); splitControls.destroy();
} }
splitControls = new SplitControls(map.current); splitControls = new SplitControls($map);
} }
}); });
let validSelection = $derived( let validSelection = $derived(
selection.value.hasAnyChildren(new ListRootItem(), true, ['waypoints']) && $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
$gpxStatistics.local.points.length > 0 $gpxStatistics.local.points.length > 0
); );
@@ -120,7 +120,7 @@
<Button <Button
variant="outline" variant="outline"
disabled={!validSelection || !canCrop} disabled={!validSelection || !canCrop}
onclick={() => dbUtils.cropSelection(sliderValues[0], sliderValues[1])} onclick={() => fileActions.cropSelection(sliderValues[0], sliderValues[1])}
> >
<Crop size="16" class="mr-1" />{i18n._('toolbar.scissors.crop')} <Crop size="16" class="mr-1" />{i18n._('toolbar.scissors.crop')}
</Button> </Button>
@@ -129,9 +129,9 @@
<span class="shrink-0"> <span class="shrink-0">
{i18n._('toolbar.scissors.split_as')} {i18n._('toolbar.scissors.split_as')}
</span> </span>
<Select.Root bind:value={splitAs.current} type="single"> <Select.Root bind:value={$splitAs} type="single">
<Select.Trigger class="h-8 w-fit grow"> <Select.Trigger class="h-8 w-fit grow">
{i18n._('gpx.' + splitAs)} {i18n._('gpx.' + $splitAs)}
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
{#each Object.values(SplitType) as splitType} {#each Object.values(SplitType) as splitType}

View File

@@ -1,12 +1,14 @@
import { TrackPoint, TrackSegment } from 'gpx'; import { TrackPoint, TrackSegment } from 'gpx';
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import { dbUtils, getFile } from '$lib/db';
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list'; import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
import { gpxStatistics } from '$lib/stores'; import { currentTool, Tool } from '$lib/components/toolbar/tools';
import { tool, Tool } from '$lib/components/toolbar/tools';
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors'; import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
import { Scissors } from 'lucide-static'; import { Scissors } from 'lucide-static';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import { gpxStatistics } from '$lib/logic/statistics';
import { get } from 'svelte/store';
import { fileStateCollection } from '$lib/logic/file-state';
import { fileActions } from '$lib/logic/file-actions';
export class SplitControls { export class SplitControls {
active: boolean = false; active: boolean = false;
@@ -22,13 +24,12 @@ export class SplitControls {
this.map = map; this.map = map;
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this))); this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
$effect(() => { this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
tool.current, selection.value, this.addIfNeeded.bind(this); this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this)));
});
} }
addIfNeeded() { addIfNeeded() {
let scissors = tool.current === Tool.SCISSORS; let scissors = get(currentTool) === Tool.SCISSORS;
if (!scissors) { if (!scissors) {
if (this.active) { if (this.active) {
this.remove(); this.remove();
@@ -54,12 +55,12 @@ export class SplitControls {
// Update the markers when the files change // Update the markers when the files change
let controlIndex = 0; let controlIndex = 0;
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => { selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
let file = getFile(fileId); let file = fileStateCollection.getFile(fileId);
if (file) { if (file) {
file.forEachSegment((segment, trackIndex, segmentIndex) => { file.forEachSegment((segment, trackIndex, segmentIndex) => {
if ( if (
selection.value.hasAnyParent( get(selection).hasAnyParent(
new ListTrackSegmentItem(fileId, trackIndex, segmentIndex) new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
) )
) { ) {
@@ -163,8 +164,8 @@ export class SplitControls {
marker.getElement().addEventListener('click', (e) => { marker.getElement().addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
dbUtils.split( fileActions.split(
splitAs.current, get(splitAs),
control.fileId, control.fileId,
control.trackIndex, control.trackIndex,
control.segmentIndex, control.segmentIndex,

View File

@@ -6,16 +6,16 @@
import * as Select from '$lib/components/ui/select'; import * as Select from '$lib/components/ui/select';
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { ListWaypointItem } from '$lib/components/file-list/file-list'; import { ListWaypointItem } from '$lib/components/file-list/file-list';
import { dbUtils, fileObservers, getFile, settings, type GPXFileWithStatistics } from '$lib/db';
import { get } from 'svelte/store';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount, untrack } from 'svelte';
import { map } from '$lib/stores'; import { getURLForLanguage } from '$lib/utils';
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
import { MapPin, CircleX, Save } from '@lucide/svelte'; import { MapPin, CircleX, Save } from '@lucide/svelte';
import { getSymbolKey, symbols } from '$lib/assets/symbols'; import { getSymbolKey, symbols } from '$lib/assets/symbols';
import { selection } from '$lib/logic/selection'; import { selection } from '$lib/logic/selection';
import { selectedWaypoint } from './waypoint'; import { selectedWaypoint } from './waypoint';
import { fileActions } from '$lib/logic/file-actions';
import { map } from '$lib/components/map/map';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
let props: { let props: {
class?: string; class?: string;
@@ -24,20 +24,46 @@
let name = $state(''); let name = $state('');
let description = $state(''); let description = $state('');
let link = $state(''); let link = $state('');
let symbolKey = $state(''); let sym = $state('');
let longitude = $state(0); let longitude = $state(0);
let latitude = $state(0); let latitude = $state(0);
let symbolKey = $derived(getSymbolKey(sym));
let canCreate = $derived(selection.value.size > 0); let canCreate = $derived($selection.size > 0);
function resetWaypointData() { let sortedSymbols = $derived(
Object.entries(symbols).sort((a, b) => {
return i18n
._(`gpx.symbol.${a[0]}`)
.localeCompare(i18n._(`gpx.symbol.${b[0]}`), i18n.lang);
})
);
$effect(() => {
if ($selectedWaypoint) {
const wpt = $selectedWaypoint[0];
untrack(() => {
name = wpt.name ?? '';
description = wpt.desc ?? '';
if (wpt.cmt !== undefined && wpt.cmt !== wpt.desc) {
description += '\n\n' + wpt.cmt;
}
link = wpt.link?.attributes?.href ?? '';
sym = wpt.sym ?? '';
longitude = parseFloat(wpt.getLongitude().toFixed(6));
latitude = parseFloat(wpt.getLatitude().toFixed(6));
});
} else {
untrack(() => {
name = ''; name = '';
description = ''; description = '';
link = ''; link = '';
symbolKey = ''; sym = '';
longitude = 0; longitude = 0;
latitude = 0; latitude = 0;
});
} }
});
function createOrUpdateWaypoint() { function createOrUpdateWaypoint() {
if (typeof latitude === 'string') { if (typeof latitude === 'string') {
@@ -49,7 +75,7 @@
latitude = parseFloat(latitude.toFixed(6)); latitude = parseFloat(latitude.toFixed(6));
longitude = parseFloat(longitude.toFixed(6)); longitude = parseFloat(longitude.toFixed(6));
dbUtils.addOrUpdateWaypoint( fileActions.addOrUpdateWaypoint(
{ {
attributes: { attributes: {
lat: latitude, lat: latitude,
@@ -59,7 +85,7 @@
desc: description.length > 0 ? description : undefined, desc: description.length > 0 ? description : undefined,
cmt: description.length > 0 ? description : undefined, cmt: description.length > 0 ? description : undefined,
link: link.length > 0 ? { attributes: { href: link } } : undefined, link: link.length > 0 ? { attributes: { href: link } } : undefined,
sym: symbols[symbolKey]?.value ?? '', sym: sym,
}, },
selectedWaypoint.wpt && selectedWaypoint.fileId selectedWaypoint.wpt && selectedWaypoint.fileId
? new ListWaypointItem(selectedWaypoint.fileId, selectedWaypoint.wpt._data.index) ? new ListWaypointItem(selectedWaypoint.fileId, selectedWaypoint.wpt._data.index)
@@ -67,7 +93,6 @@
); );
selectedWaypoint.reset(); selectedWaypoint.reset();
resetWaypointData();
} }
function setCoordinates(e: any) { function setCoordinates(e: any) {
@@ -75,22 +100,18 @@
longitude = e.lngLat.lng.toFixed(6); longitude = e.lngLat.lng.toFixed(6);
} }
let sortedSymbols = $derived(
Object.entries(symbols).sort((a, b) => {
return i18n
._(`gpx.symbol.${a[0]}`)
.localeCompare(i18n._(`gpx.symbol.${b[0]}`), i18n.lang);
})
);
onMount(() => { onMount(() => {
map.value?.on('click', setCoordinates); if ($map) {
// setCrosshairCursor(); $map.on('click', setCoordinates);
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, true);
}
}); });
onDestroy(() => { onDestroy(() => {
map.value?.off('click', setCoordinates); if ($map) {
// resetCursor(); $map.off('click', setCoordinates);
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false);
}
}); });
</script> </script>
@@ -101,25 +122,25 @@
bind:value={name} bind:value={name}
id="name" id="name"
class="font-semibold h-8" class="font-semibold h-8"
disabled={!canCreate && !selectedWaypoint.wpt} disabled={!canCreate && !$selectedWaypoint}
/> />
<Label for="description">{i18n._('menu.metadata.description')}</Label> <Label for="description">{i18n._('menu.metadata.description')}</Label>
<Textarea <Textarea
bind:value={description} bind:value={description}
id="description" id="description"
disabled={!canCreate && !selectedWaypoint.wpt} disabled={!canCreate && !$selectedWaypoint}
/> />
<Label for="symbol">{i18n._('toolbar.waypoint.icon')}</Label> <Label for="symbol">{i18n._('toolbar.waypoint.icon')}</Label>
<Select.Root bind:value={symbolKey} type="single"> <Select.Root bind:value={sym} type="single">
<Select.Trigger <Select.Trigger
id="symbol" id="symbol"
class="w-full h-8" class="w-full h-8"
disabled={!canCreate && !selectedWaypoint.wpt} disabled={!canCreate && !$selectedWaypoint}
> >
{#if symbolKey in symbols} {#if symbolKey}
{i18n._(`gpx.symbol.${symbolKey}`)} {i18n._(`gpx.symbol.${symbolKey}`)}
{:else} {:else}
{symbolKey} {sym}
{/if} {/if}
</Select.Trigger> </Select.Trigger>
<Select.Content class="max-h-60 overflow-y-scroll"> <Select.Content class="max-h-60 overflow-y-scroll">
@@ -127,11 +148,8 @@
<Select.Item value={symbol.value}> <Select.Item value={symbol.value}>
<span> <span>
{#if symbol.icon} {#if symbol.icon}
<svelte:component {@const Component = symbol.icon}
this={symbol.icon} <Component size="14" class="inline-block align-sub mr-0.5" />
size="14"
class="inline-block align-sub mr-0.5"
/>
{:else} {:else}
<span class="w-4 inline-block"></span> <span class="w-4 inline-block"></span>
{/if} {/if}
@@ -146,7 +164,7 @@
bind:value={link} bind:value={link}
id="link" id="link"
class="h-8" class="h-8"
disabled={!canCreate && !selectedWaypoint.wpt} disabled={!canCreate && !$selectedWaypoint}
/> />
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<div class="grow"> <div class="grow">
@@ -159,7 +177,7 @@
min={-90} min={-90}
max={90} max={90}
class="text-xs h-8" class="text-xs h-8"
disabled={!canCreate && !selectedWaypoint.wpt} disabled={!canCreate && !$selectedWaypoint}
/> />
</div> </div>
<div class="grow"> <div class="grow">
@@ -172,7 +190,7 @@
min={-180} min={-180}
max={180} max={180}
class="text-xs h-8" class="text-xs h-8"
disabled={!canCreate && !selectedWaypoint.wpt} disabled={!canCreate && !$selectedWaypoint}
/> />
</div> </div>
</div> </div>
@@ -180,11 +198,11 @@
<div class="flex flex-row gap-2 items-center"> <div class="flex flex-row gap-2 items-center">
<Button <Button
variant="outline" variant="outline"
disabled={!canCreate && !selectedWaypoint.wpt} disabled={!canCreate && !$selectedWaypoint}
class="grow whitespace-normal h-fit" class="grow whitespace-normal h-fit"
onclick={createOrUpdateWaypoint} onclick={createOrUpdateWaypoint}
> >
{#if selectedWaypoint.wpt} {#if $selectedWaypoint}
<Save size="16" class="mr-1 shrink-0" /> <Save size="16" class="mr-1 shrink-0" />
{i18n._('menu.metadata.save')} {i18n._('menu.metadata.save')}
{:else} {:else}
@@ -192,18 +210,12 @@
{i18n._('toolbar.waypoint.create')} {i18n._('toolbar.waypoint.create')}
{/if} {/if}
</Button> </Button>
<Button <Button variant="outline" onclick={() => selectedWaypoint.reset()}>
variant="outline"
onclick={() => {
selectedWaypoint.reset();
resetWaypointData();
}}
>
<CircleX size="16" /> <CircleX size="16" />
</Button> </Button>
</div> </div>
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/poi')}> <Help link={getURLForLanguage(i18n.lang, '/help/toolbar/poi')}>
{#if selectedWaypoint.wpt || canCreate} {#if $selectedWaypoint || canCreate}
{i18n._('toolbar.waypoint.help')} {i18n._('toolbar.waypoint.help')}
{:else} {:else}
{i18n._('toolbar.waypoint.help_no_selection')} {i18n._('toolbar.waypoint.help_no_selection')}

View File

@@ -7,6 +7,7 @@ import { get, writable, type Writable } from 'svelte/store';
export class WaypointSelection { export class WaypointSelection {
private _selection: Writable<[Waypoint, string] | undefined>; private _selection: Writable<[Waypoint, string] | undefined>;
private _fileUnsubscribe: (() => void) | undefined;
constructor() { constructor() {
this._selection = writable(undefined); this._selection = writable(undefined);
@@ -18,18 +19,40 @@ export class WaypointSelection {
}); });
} }
subscribe(
run: (value: [Waypoint, string] | undefined) => void,
invalidate?: (value?: [Waypoint, string] | undefined) => void
) {
return this._selection.subscribe(run, invalidate);
}
set(value: [Waypoint, string] | undefined) {
this._selection.set(value);
}
update() { update() {
if (this._fileUnsubscribe) {
this._fileUnsubscribe();
this._fileUnsubscribe = undefined;
}
this._selection.update(() => { this._selection.update(() => {
if (get(settings.treeFileView) && get(selection).size === 1) { if (get(settings.treeFileView) && get(selection).size === 1) {
let item = get(selection).getSelected()[0]; let item = get(selection).getSelected()[0];
if (item instanceof ListWaypointItem) { if (item instanceof ListWaypointItem) {
let file = fileStateCollection.getFile(item.getFileId()); let fileState = fileStateCollection.getFileState(item.getFileId());
let waypoint = file?.wpt[item.getWaypointIndex()]; if (fileState) {
let first = true;
this._fileUnsubscribe = fileState.subscribe(() => {
if (first) first = false;
else this.update();
});
let waypoint = fileState.file?.wpt[item.getWaypointIndex()];
if (waypoint) { if (waypoint) {
return [waypoint, item.getFileId()]; return [waypoint, item.getFileId()];
} }
} }
} }
}
return undefined; return undefined;
}); });
} }
@@ -47,34 +70,6 @@ export class WaypointSelection {
const selection = get(this._selection); const selection = get(this._selection);
return selection ? selection[1] : undefined; return selection ? selection[1] : undefined;
} }
// TODO update the waypoint data if the file changes
// function updateWaypointData(fileStore: GPXFileWithStatistics | undefined) {
// if (selectedWaypoint.wpt) {
// if (fileStore) {
// if ($selectedWaypoint[0]._data.index < fileStore.file.wpt.length) {
// $selectedWaypoint[0] = fileStore.file.wpt[$selectedWaypoint[0]._data.index];
// name = $selectedWaypoint[0].name ?? '';
// description = $selectedWaypoint[0].desc ?? '';
// if (
// $selectedWaypoint[0].cmt !== undefined &&
// $selectedWaypoint[0].cmt !== $selectedWaypoint[0].desc
// ) {
// description += '\n\n' + $selectedWaypoint[0].cmt;
// }
// link = $selectedWaypoint[0].link?.attributes?.href ?? '';
// let symbol = $selectedWaypoint[0].sym ?? '';
// symbolKey = getSymbolKey(symbol) ?? symbol ?? '';
// longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6));
// latitude = parseFloat($selectedWaypoint[0].getLatitude().toFixed(6));
// } else {
// selectedWaypoint.reset();
// }
// } else {
// selectedWaypoint.reset();
// }
// }
// }
} }
export const selectedWaypoint = new WaypointSelection(); export const selectedWaypoint = new WaypointSelection();

View File

@@ -1,36 +1,38 @@
<script lang="ts" module> <script lang="ts" module>
import { cn, type WithElementRef } from '$lib/utils.js'; import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements'; import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from 'tailwind-variants'; import { type VariantProps, tv } from "tailwind-variants";
export const buttonVariants = tv({ export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: { variants: {
variant: { variant: {
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive: destructive:
'bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white', "bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white",
outline: outline:
'bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border', "bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border",
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: 'text-primary underline-offset-4 hover:underline', link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3', default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5', sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: 'size-9', icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: 'default', variant: "default",
size: 'default', size: "default",
}, },
}); });
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant']; export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>['size']; export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> & export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & { WithElementRef<HTMLAnchorAttributes> & {
@@ -42,11 +44,11 @@
<script lang="ts"> <script lang="ts">
let { let {
class: className, class: className,
variant = 'default', variant = "default",
size = 'default', size = "default",
ref = $bindable(null), ref = $bindable(null),
href = undefined, href = undefined,
type = 'button', type = "button",
disabled, disabled,
children, children,
...restProps ...restProps
@@ -60,7 +62,7 @@
class={cn(buttonVariants({ variant, size }), className)} class={cn(buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href} href={disabled ? undefined : href}
aria-disabled={disabled} aria-disabled={disabled}
role={disabled ? 'link' : undefined} role={disabled ? "link" : undefined}
tabindex={disabled ? -1 : undefined} tabindex={disabled ? -1 : undefined}
{...restProps} {...restProps}
> >

View File

@@ -1,39 +1,48 @@
<script lang="ts"> <script lang="ts">
import { CalendarIcon } from '@lucide/svelte'; import CalendarIcon from '@lucide/svelte/icons/calendar';
import { DateFormatter, type DateValue, getLocalTimeZone } from '@internationalized/date'; import { DateFormatter, type DateValue, getLocalTimeZone } from '@internationalized/date';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import { Button } from '$lib/components/ui/button/index.js'; import { buttonVariants } from '$lib/components/ui/button/index.js';
import { Calendar } from '$lib/components/ui/calendar/index.js'; import { Calendar } from '$lib/components/ui/calendar/index.js';
import * as Popover from '$lib/components/ui/popover/index.js'; import * as Popover from '$lib/components/ui/popover/index.js';
export let value: DateValue | undefined = undefined; let {
export let placeholder: string = 'Pick a date'; value = $bindable<DateValue | undefined>(),
export let locale = 'en'; placeholder = 'Pick a date',
export let disabled: boolean = false; disabled = false,
export let onValueChange: any; locale,
class: className = '',
}: {
value?: DateValue;
placeholder?: string;
disabled?: boolean;
locale: string;
class?: string;
} = $props();
const df = new DateFormatter(locale, { const df = new DateFormatter(locale, {
dateStyle: 'long', dateStyle: 'long',
}); });
let contentRef = $state<HTMLElement | null>(null);
</script> </script>
<Popover.Root> <Popover.Root>
<Popover.Trigger asChild let:builder> <Popover.Trigger
<Button
variant="outline"
class={cn( class={cn(
'w-[280px] justify-start text-left font-normal', buttonVariants({
variant: 'outline',
class: 'justify-start text-left font-normal',
}),
!value && 'text-muted-foreground', !value && 'text-muted-foreground',
$$props.class className
)} )}
{disabled} {disabled}
builders={[builder]}
> >
<CalendarIcon class="mr-2 h-4 w-4" /> <CalendarIcon />
{value ? df.format(value.toDate(getLocalTimeZone())) : placeholder} {value ? df.format(value.toDate(getLocalTimeZone())) : placeholder}
</Button>
</Popover.Trigger> </Popover.Trigger>
<Popover.Content class="w-auto p-0"> <Popover.Content bind:ref={contentRef} class="w-auto p-0">
<Calendar bind:value initialFocus {locale} {onValueChange} /> <Calendar type="single" captionLayout="dropdown" bind:value />
</Popover.Content> </Popover.Content>
</Popover.Root> </Popover.Root>

View File

@@ -33,7 +33,7 @@
{@render children?.()} {@render children?.()}
{#if showCloseButton} {#if showCloseButton}
<DialogPrimitive.Close <DialogPrimitive.Close
class="ring-offset-background focus:ring-ring rounded-xs focus:outline-hidden absolute right-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0" class="ring-offset-background focus:ring-ring rounded-xs focus:outline-hidden absolute end-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
> >
<XIcon /> <XIcon />
<span class="sr-only">Close</span> <span class="sr-only">Close</span>

View File

@@ -19,7 +19,7 @@
data-slot="dropdown-menu-content" data-slot="dropdown-menu-content"
{sideOffset} {sideOffset}
class={cn( class={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md", "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--bits-dropdown-menu-content-available-height) origin-(--bits-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md outline-none",
className className
)} )}
{...restProps} {...restProps}

View File

@@ -13,7 +13,7 @@
bind:ref bind:ref
data-slot="dropdown-menu-sub-content" data-slot="dropdown-menu-sub-content"
class={cn( class={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg", "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
className className
)} )}
{...restProps} {...restProps}

View File

@@ -15,6 +15,7 @@
type, type,
files = $bindable(), files = $bindable(),
class: className, class: className,
"data-slot": dataSlot = "input",
...restProps ...restProps
}: Props = $props(); }: Props = $props();
</script> </script>
@@ -22,9 +23,9 @@
{#if type === "file"} {#if type === "file"}
<input <input
bind:this={ref} bind:this={ref}
data-slot="input" data-slot={dataSlot}
class={cn( class={cn(
"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className className
@@ -37,7 +38,7 @@
{:else} {:else}
<input <input
bind:this={ref} bind:this={ref}
data-slot="input" data-slot={dataSlot}
class={cn( class={cn(
"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",

View File

@@ -0,0 +1,10 @@
import Root from "./kbd.svelte";
import Group from "./kbd-group.svelte";
export {
Root,
Group,
//
Root as Kbd,
Group as KbdGroup,
};

View File

@@ -0,0 +1,10 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLElement> = $props();
</script>
<kbd data-slot="kbd-group" class={cn("inline-flex items-center gap-1", className)} {...restProps}>
{@render children?.()}
</kbd>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLElement> = $props();
</script>
<kbd
data-slot="kbd"
class={cn(
"bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 select-none items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium",
"[&_svg:not([class*='size-'])]:size-3",
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
className
)}
{...restProps}
>
{@render children?.()}
</kbd>

View File

@@ -1,18 +1,18 @@
<script lang="ts"> <script lang="ts">
import { ScrollArea as ScrollAreaPrimitive } from 'bits-ui'; import { ScrollArea as ScrollAreaPrimitive } from "bits-ui";
import { Scrollbar } from './index.js'; import { Scrollbar } from "./index.js";
import { cn, type WithoutChild } from '$lib/utils.js'; import { cn, type WithoutChild } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
orientation = 'vertical', orientation = "vertical",
scrollbarXClasses = '', scrollbarXClasses = "",
scrollbarYClasses = '', scrollbarYClasses = "",
children, children,
...restProps ...restProps
}: WithoutChild<ScrollAreaPrimitive.RootProps> & { }: WithoutChild<ScrollAreaPrimitive.RootProps> & {
orientation?: 'vertical' | 'horizontal' | 'both' | undefined; orientation?: "vertical" | "horizontal" | "both" | undefined;
scrollbarXClasses?: string | undefined; scrollbarXClasses?: string | undefined;
scrollbarYClasses?: string | undefined; scrollbarYClasses?: string | undefined;
} = $props(); } = $props();
@@ -21,7 +21,7 @@
<ScrollAreaPrimitive.Root <ScrollAreaPrimitive.Root
bind:ref bind:ref
data-slot="scroll-area" data-slot="scroll-area"
class={cn('relative overflow-hidden', className)} class={cn("relative", className)}
{...restProps} {...restProps}
> >
<ScrollAreaPrimitive.Viewport <ScrollAreaPrimitive.Viewport
@@ -30,10 +30,10 @@
> >
{@render children?.()} {@render children?.()}
</ScrollAreaPrimitive.Viewport> </ScrollAreaPrimitive.Viewport>
{#if orientation === 'vertical' || orientation === 'both'} {#if orientation === "vertical" || orientation === "both"}
<Scrollbar orientation="vertical" class={scrollbarYClasses} /> <Scrollbar orientation="vertical" class={scrollbarYClasses} />
{/if} {/if}
{#if orientation === 'horizontal' || orientation === 'both'} {#if orientation === "horizontal" || orientation === "both"}
<Scrollbar orientation="horizontal" class={scrollbarXClasses} /> <Scrollbar orientation="horizontal" class={scrollbarXClasses} />
{/if} {/if}
<ScrollAreaPrimitive.Corner /> <ScrollAreaPrimitive.Corner />

View File

@@ -5,13 +5,14 @@
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
"data-slot": dataSlot = "separator",
...restProps ...restProps
}: SeparatorPrimitive.RootProps = $props(); }: SeparatorPrimitive.RootProps = $props();
</script> </script>
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
bind:ref bind:ref
data-slot="separator" data-slot={dataSlot}
class={cn( class={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px", "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px",
className className

View File

@@ -8,6 +8,6 @@
<Sonner <Sonner
theme={mode.current} theme={mode.current}
class="toaster group" class="toaster group"
style="--normal-bg: var(--popover); --normal-text: var(--popover-foreground); --normal-border: var(--border);" style="--normal-bg: var(--color-popover); --normal-text: var(--color-popover-foreground); --normal-border: var(--color-border);"
{...restProps} {...restProps}
/> />

View File

@@ -6,13 +6,14 @@
ref = $bindable(null), ref = $bindable(null),
value = $bindable(), value = $bindable(),
class: className, class: className,
"data-slot": dataSlot = "textarea",
...restProps ...restProps
}: WithoutChildren<WithElementRef<HTMLTextareaAttributes>> = $props(); }: WithoutChildren<WithElementRef<HTMLTextareaAttributes>> = $props();
</script> </script>
<textarea <textarea
bind:this={ref} bind:this={ref}
data-slot="textarea" data-slot={dataSlot}
class={cn( class={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 field-sizing-content shadow-xs flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 field-sizing-content shadow-xs flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className className

View File

@@ -34,9 +34,9 @@
class={cn( class={cn(
"bg-primary z-50 size-2.5 rotate-45 rounded-[2px]", "bg-primary z-50 size-2.5 rotate-45 rounded-[2px]",
"data-[side=top]:translate-x-1/2 data-[side=top]:translate-y-[calc(-50%_+_2px)]", "data-[side=top]:translate-x-1/2 data-[side=top]:translate-y-[calc(-50%_+_2px)]",
"data-[side=bottom]:-translate-y-[calc(-50%_+_1px)] data-[side=bottom]:translate-x-1/2", "data-[side=bottom]:-translate-x-1/2 data-[side=bottom]:-translate-y-[calc(-50%_+_1px)]",
"data-[side=right]:translate-x-[calc(50%_+_2px)] data-[side=right]:translate-y-1/2", "data-[side=right]:translate-x-[calc(50%_+_2px)] data-[side=right]:translate-y-1/2",
"data-[side=left]:translate-y-[calc(50%_-_3px)]", "data-[side=left]:-translate-y-[calc(50%_-_3px)]",
arrowClasses arrowClasses
)} )}
{...props} {...props}

View File

@@ -4,7 +4,7 @@ title: Minify
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minimitzar
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Zjednodušit
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minimieren
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minimizar
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Txikiagotu
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minifier
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minimalizálás
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minimizza
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Verkleinen
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minificar
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minimera
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Küçült
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

View File

@@ -4,7 +4,7 @@ title: 精简 GPS 点数量
<script> <script>
import { Funnel } from '@lucide/svelte'; import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte'; import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script> </script>

File diff suppressed because it is too large Load Diff

View File

@@ -151,6 +151,7 @@ export class GPXFileStateCollectionObserver {
private _onFileAdded: GPXFileStateCallback; private _onFileAdded: GPXFileStateCallback;
private _onFileRemoved: (fileId: string) => void; private _onFileRemoved: (fileId: string) => void;
private _onDestroy: () => void; private _onDestroy: () => void;
private _unsubscribe: () => void;
constructor( constructor(
onFileAdded: GPXFileStateCallback, onFileAdded: GPXFileStateCallback,
@@ -162,7 +163,7 @@ export class GPXFileStateCollectionObserver {
this._onFileRemoved = onFileRemoved; this._onFileRemoved = onFileRemoved;
this._onDestroy = onDestroy; this._onDestroy = onDestroy;
fileStateCollection.subscribe((files) => { this._unsubscribe = fileStateCollection.subscribe((files) => {
this._fileIds.forEach((fileId) => { this._fileIds.forEach((fileId) => {
if (!files.has(fileId)) { if (!files.has(fileId)) {
this._onFileRemoved(fileId); this._onFileRemoved(fileId);
@@ -180,5 +181,6 @@ export class GPXFileStateCollectionObserver {
destroy() { destroy() {
this._onDestroy(); this._onDestroy();
this._unsubscribe();
} }
} }

View File

@@ -0,0 +1,55 @@
import { map } from '$lib/components/map/map';
import { get, writable, type Writable } from 'svelte/store';
export enum MapCursorState {
DEFAULT,
LAYER_HOVER,
WAYPOINT_DRAGGING,
TRACKPOINT_DRAGGING,
TOOL_WITH_CROSSHAIR,
SCISSORS,
MAPILLARY_HOVER,
STREET_VIEW_CROSSHAIR,
}
const scissorsCursor = `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" version="1.1"><path d="M 3.200 3.200 C 0.441 5.959, 2.384 9.516, 7 10.154 C 10.466 10.634, 10.187 13.359, 6.607 13.990 C 2.934 14.637, 1.078 17.314, 2.612 19.750 C 4.899 23.380, 10 21.935, 10 17.657 C 10 16.445, 12.405 13.128, 15.693 9.805 C 18.824 6.641, 21.066 3.732, 20.674 3.341 C 20.283 2.950, 18.212 4.340, 16.072 6.430 C 12.019 10.388, 10 10.458, 10 6.641 C 10 2.602, 5.882 0.518, 3.200 3.200 M 4.446 5.087 C 3.416 6.755, 5.733 8.667, 7.113 7.287 C 8.267 6.133, 7.545 4, 6 4 C 5.515 4, 4.816 4.489, 4.446 5.087 M 14 14.813 C 14 16.187, 19.935 21.398, 20.667 20.667 C 21.045 20.289, 20.065 18.634, 18.490 16.990 C 15.661 14.036, 14 13.231, 14 14.813 M 4.446 17.087 C 3.416 18.755, 5.733 20.667, 7.113 19.287 C 8.267 18.133, 7.545 16, 6 16 C 5.515 16, 4.816 16.489, 4.446 17.087" stroke="black" stroke-width="1.2" fill="white" fill-rule="evenodd"/></svg>') 12 12, auto`;
const cursorStyles = {
[MapCursorState.DEFAULT]: 'default',
[MapCursorState.LAYER_HOVER]: 'pointer',
[MapCursorState.WAYPOINT_DRAGGING]: 'grabbing',
[MapCursorState.TRACKPOINT_DRAGGING]: 'grabbing',
[MapCursorState.TOOL_WITH_CROSSHAIR]: 'crosshair',
[MapCursorState.SCISSORS]: scissorsCursor,
[MapCursorState.MAPILLARY_HOVER]: 'pointer',
[MapCursorState.STREET_VIEW_CROSSHAIR]: 'crosshair',
};
export class MapCursor {
private _states: Writable<Set<MapCursorState>>;
constructor() {
this._states = writable(new Set());
this._states.subscribe((states) => {
let state = states.entries().reduce((max, entry) => {
return entry[0] > max ? entry[0] : max;
}, MapCursorState.DEFAULT);
let canvas = get(map)?.getCanvas();
if (canvas) {
canvas.style.cursor = cursorStyles[state];
}
});
}
notify(cursorState: MapCursorState, isActive: boolean) {
this._states.update((states) => {
if (isActive) {
states.add(cursorState);
} else {
states.delete(cursorState);
}
return states;
});
}
}
export const mapCursor = new MapCursor();

View File

@@ -14,6 +14,7 @@ import { settings } from '$lib/logic/settings';
import type { GPXFile } from 'gpx'; import type { GPXFile } from 'gpx';
import { get, writable, type Readable, type Writable } from 'svelte/store'; import { get, writable, type Readable, type Writable } from 'svelte/store';
import { SelectionTreeType } from '$lib/logic/selection-tree'; import { SelectionTreeType } from '$lib/logic/selection-tree';
import { tick } from 'svelte';
export class Selection { export class Selection {
private _selection: Writable<SelectionTreeType>; private _selection: Writable<SelectionTreeType>;
@@ -100,6 +101,15 @@ export class Selection {
}); });
} }
selectFileWhenLoaded(fileId: string) {
const unsubscribe = fileStateCollection.subscribe((files) => {
if (files.has(fileId)) {
this.selectFile(fileId);
unsubscribe();
}
});
}
set(items: ListItem[]) { set(items: ListItem[]) {
this._selection.update(($selection) => { this._selection.update(($selection) => {
$selection.clear(); $selection.clear();

View File

@@ -7,6 +7,9 @@ import {
ListWaypointsItem, ListWaypointsItem,
} from '$lib/components/file-list/file-list'; } from '$lib/components/file-list/file-list';
import { get, writable, type Writable } from 'svelte/store'; import { get, writable, type Writable } from 'svelte/store';
import { settings } from '$lib/logic/settings';
const { fileOrder } = settings;
export class SelectedGPXStatistics { export class SelectedGPXStatistics {
private _statistics: Writable<GPXStatistics>; private _statistics: Writable<GPXStatistics>;
@@ -22,6 +25,7 @@ export class SelectedGPXStatistics {
this._statistics = writable(new GPXStatistics()); this._statistics = writable(new GPXStatistics());
this._files = new Map(); this._files = new Map();
selection.subscribe(() => this.update()); selection.subscribe(() => this.update());
fileOrder.subscribe(() => this.update());
} }
subscribe(run: (value: GPXStatistics) => void, invalidate?: (value?: GPXStatistics) => void) { subscribe(run: (value: GPXStatistics) => void, invalidate?: (value?: GPXStatistics) => void) {

View File

@@ -24,17 +24,6 @@
// export const routingControls: Map<string, RoutingControls> = new Map(); // export const routingControls: Map<string, RoutingControls> = new Map();
// export function selectFileWhenLoaded(fileId: string) {
// const unsubscribe = fileObservers.subscribe((files) => {
// if (files.has(fileId)) {
// tick().then(() => {
// selectFile(fileId);
// });
// unsubscribe();
// }
// });
// }
// export const allHidden = writable(false); // export const allHidden = writable(false);
// export function updateAllHidden() { // export function updateAllHidden() {

View File

@@ -126,34 +126,6 @@ export function getElevation(
); );
} }
let previousCursors: string[] = [];
export function setCursor(canvas: HTMLCanvasElement, cursor: string) {
previousCursors.push(canvas.style.cursor);
canvas.style.cursor = cursor;
}
export function resetCursor(canvas: HTMLCanvasElement) {
canvas.style.cursor = previousCursors.pop() ?? '';
}
export function setPointerCursor(canvas: HTMLCanvasElement) {
setCursor(canvas, 'pointer');
}
export function setGrabbingCursor(canvas: HTMLCanvasElement) {
setCursor(canvas, 'grabbing');
}
export function setCrosshairCursor(canvas: HTMLCanvasElement) {
setCursor(canvas, 'crosshair');
}
export const scissorsCursor = `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" version="1.1"><path d="M 3.200 3.200 C 0.441 5.959, 2.384 9.516, 7 10.154 C 10.466 10.634, 10.187 13.359, 6.607 13.990 C 2.934 14.637, 1.078 17.314, 2.612 19.750 C 4.899 23.380, 10 21.935, 10 17.657 C 10 16.445, 12.405 13.128, 15.693 9.805 C 18.824 6.641, 21.066 3.732, 20.674 3.341 C 20.283 2.950, 18.212 4.340, 16.072 6.430 C 12.019 10.388, 10 10.458, 10 6.641 C 10 2.602, 5.882 0.518, 3.200 3.200 M 4.446 5.087 C 3.416 6.755, 5.733 8.667, 7.113 7.287 C 8.267 6.133, 7.545 4, 6 4 C 5.515 4, 4.816 4.489, 4.446 5.087 M 14 14.813 C 14 16.187, 19.935 21.398, 20.667 20.667 C 21.045 20.289, 20.065 18.634, 18.490 16.990 C 15.661 14.036, 14 13.231, 14 14.813 M 4.446 17.087 C 3.416 18.755, 5.733 20.667, 7.113 19.287 C 8.267 18.133, 7.545 16, 6 16 C 5.515 16, 4.816 16.489, 4.446 17.087" stroke="black" stroke-width="1.2" fill="white" fill-rule="evenodd"/></svg>') 12 12, auto`;
export function setScissorsCursor(canvas: HTMLCanvasElement) {
setCursor(canvas, scissorsCursor);
}
export function isMac() { export function isMac() {
return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0; return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
} }

View File

@@ -3,7 +3,7 @@
import DocsContainer from '$lib/components/docs/DocsContainer.svelte'; import DocsContainer from '$lib/components/docs/DocsContainer.svelte';
import Logo from '$lib/components/Logo.svelte'; import Logo from '$lib/components/Logo.svelte';
// import ElevationProfile from '$lib/components/ElevationProfile.svelte'; // import ElevationProfile from '$lib/components/ElevationProfile.svelte';
// import GPXStatistics from '$lib/components/GPXStatistics.svelte'; import GPXStatistics from '$lib/components/GPXStatistics.svelte';
// import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte'; // import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
import { import {
BookOpenText, BookOpenText,
@@ -20,7 +20,7 @@
import { exampleGPXFile } from '$lib/assets/example'; import { exampleGPXFile } from '$lib/assets/example';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
// import Toolbar from '$lib/components/toolbar/Toolbar.svelte'; // import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
// import { tool, Tool } from '$lib/components/toolbar/utils.svelte'; import { currentTool, Tool } from '$lib/components/toolbar/tools';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
let { let {
@@ -38,19 +38,19 @@
let additionalDatasets = writable(['speed', 'atemp']); let additionalDatasets = writable(['speed', 'atemp']);
let elevationFill = writable<'slope' | 'surface' | undefined>(undefined); let elevationFill = writable<'slope' | 'surface' | undefined>(undefined);
// onMount(() => { onMount(() => {
// tool.current = Tool.SCISSORS; $currentTool = Tool.SCISSORS;
// }); });
// $effect(() => { $effect(() => {
// if (tool.current !== Tool.SCISSORS) { if ($currentTool !== Tool.SCISSORS) {
// tool.current = Tool.SCISSORS; $currentTool = Tool.SCISSORS;
// } }
// }); });
// onDestroy(() => { onDestroy(() => {
// tool.current = null; $currentTool = null;
// }); });
</script> </script>
<div class="space-y-24 my-24"> <div class="space-y-24 my-24">
@@ -199,12 +199,12 @@
</div> </div>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<div class="h-10 w-fit"> <div class="h-10 w-fit">
<!-- <GPXStatistics <GPXStatistics
{gpxStatistics} {gpxStatistics}
{slicedGPXStatistics} {slicedGPXStatistics}
panelSize={192} panelSize={192}
orientation={'horizontal'} orientation={'horizontal'}
/> --> />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import GPXLayers from '$lib/components/map/gpx-layer/GPXLayers.svelte'; import GPXLayers from '$lib/components/map/gpx-layer/GPXLayers.svelte';
// import ElevationProfile from '$lib/components/ElevationProfile.svelte'; import ElevationProfile from '$lib/components/ElevationProfile.svelte';
// import FileList from '$lib/components/file-list/FileList.svelte'; // import FileList from '$lib/components/file-list/FileList.svelte';
import GPXStatistics from '$lib/components/GPXStatistics.svelte'; import GPXStatistics from '$lib/components/GPXStatistics.svelte';
import Map from '$lib/components/map/Map.svelte'; import Map from '$lib/components/map/Map.svelte';
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 StreetViewControl from '$lib/components/map/street-view-control/StreetViewControl.svelte'; import StreetViewControl from '$lib/components/map/street-view-control/StreetViewControl.svelte';
import LayerControl from '$lib/components/map/layer-control/LayerControl.svelte'; import LayerControl from '$lib/components/map/layer-control/LayerControl.svelte';
// import CoordinatesPopup from '$lib/components/map/CoordinatesPopup.svelte'; // import CoordinatesPopup from '$lib/components/map/CoordinatesPopup.svelte';
@@ -101,7 +101,7 @@
<div <div
class="absolute top-0 bottom-0 left-0 z-20 flex flex-col justify-center pointer-events-none" class="absolute top-0 bottom-0 left-0 z-20 flex flex-col justify-center pointer-events-none"
> >
<!-- <Toolbar /> --> <Toolbar />
</div> </div>
<Map class="h-full {$treeFileView ? '' : 'horizontal'}" /> <Map class="h-full {$treeFileView ? '' : 'horizontal'}" />
<StreetViewControl /> <StreetViewControl />
@@ -133,14 +133,14 @@
panelSize={$bottomPanelSize} panelSize={$bottomPanelSize}
orientation={$elevationProfile ? 'vertical' : 'horizontal'} orientation={$elevationProfile ? 'vertical' : 'horizontal'}
/> />
<!-- {#if $elevationProfile} {#if $elevationProfile}
<ElevationProfile <ElevationProfile
{gpxStatistics} {gpxStatistics}
{slicedGPXStatistics} {slicedGPXStatistics}
bind:additionalDatasets={$additionalDatasets} bind:additionalDatasets={$additionalDatasets}
bind:elevationFill={$elevationFill} bind:elevationFill={$elevationFill}
/> />
{/if} --> {/if}
</div> </div>
</div> </div>
{#if $treeFileView} {#if $treeFileView}