mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-03-13 16:22:59 +00:00
Compare commits
4 Commits
9fa8fe5767
...
05df3ca064
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05df3ca064 | ||
|
|
356884cf58 | ||
|
|
e68da7354e | ||
|
|
c59cd66141 |
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "https://next.shadcn-svelte.com/schema.json",
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"style": "default",
|
||||
"tailwind": {
|
||||
"css": "src/app.css",
|
||||
"baseColor": "slate"
|
||||
@@ -12,5 +13,5 @@
|
||||
"lib": "$lib"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://next.shadcn-svelte.com/registry"
|
||||
"registry": "https://shadcn-svelte.com/registry"
|
||||
}
|
||||
|
||||
134
website/package-lock.json
generated
134
website/package-lock.json
generated
@@ -27,11 +27,10 @@
|
||||
"png.js": "^0.2.1",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"sortablejs": "^1.15.6",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwind-variants": "^1.0.0"
|
||||
"tailwind-merge": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lucide/svelte": "^0.513.0",
|
||||
"@lucide/svelte": "^0.544.0",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/enhanced-img": "^0.6.0",
|
||||
"@sveltejs/kit": "^2.21.2",
|
||||
@@ -48,7 +47,7 @@
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
||||
"@typescript-eslint/parser": "^8.33.1",
|
||||
"bits-ui": "^2.5.0",
|
||||
"bits-ui": "^2.12.0",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-svelte": "^3.9.1",
|
||||
@@ -56,14 +55,15 @@
|
||||
"glob": "^11.0.2",
|
||||
"lucide-static": "^0.513.0",
|
||||
"mdsvex": "^0.12.6",
|
||||
"mode-watcher": "^1.0.7",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"paneforge": "^1.0.0-next.5",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"svelte": "^5.33.18",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-sonner": "^1.0.4",
|
||||
"svelte-sonner": "^1.0.5",
|
||||
"tailwind-variants": "^3.1.1",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"tslib": "^2.8.1",
|
||||
"tsx": "^4.19.1",
|
||||
@@ -1625,9 +1625,9 @@
|
||||
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
|
||||
},
|
||||
"node_modules/@lucide/svelte": {
|
||||
"version": "0.513.0",
|
||||
"resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.513.0.tgz",
|
||||
"integrity": "sha512-XwBQMQkMlr9qp9yVg+epx5MzhBBrqul8atO00y/ZfhlKRJuQZVmq3ELibApqyBtj9ys0Ai4FH/SZcODTUFYXig==",
|
||||
"version": "0.544.0",
|
||||
"resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.544.0.tgz",
|
||||
"integrity": "sha512-9f9O6uxng2pLB01sxNySHduJN3HTl5p0HDu4H26VR51vhZfiMzyOMe9Mhof3XAk4l813eTtl+/DYRvGyoRR+yw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
@@ -3233,23 +3233,21 @@
|
||||
]
|
||||
},
|
||||
"node_modules/bits-ui": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.5.0.tgz",
|
||||
"integrity": "sha512-PbjylA1UWd4A/c5AYqie/EVxQ1/8uugmJKLg9whLoBBHbfPEBGhK09dCPrahK9kA6DRHhMmij0XXIUGIfrmNow==",
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.12.0.tgz",
|
||||
"integrity": "sha512-8NF4ILNyAJlIxDXpl/akGXGBV5QmZAe+8gTfPttM5P6/+LrijumcSfFXY5cr4QkXwTmLA7H5stYpbgJf2XFJvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.0",
|
||||
"@floating-ui/dom": "^1.7.0",
|
||||
"css.escape": "^1.5.1",
|
||||
"@floating-ui/core": "^1.7.1",
|
||||
"@floating-ui/dom": "^1.7.1",
|
||||
"esm-env": "^1.1.2",
|
||||
"runed": "^0.28.0",
|
||||
"svelte-toolbelt": "^0.9.1",
|
||||
"runed": "^0.35.1",
|
||||
"svelte-toolbelt": "^0.10.6",
|
||||
"tabbable": "^6.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20",
|
||||
"pnpm": ">=8.7.0"
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/huntabyte"
|
||||
@@ -3260,9 +3258,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bits-ui/node_modules/runed": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/runed/-/runed-0.28.0.tgz",
|
||||
"integrity": "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==",
|
||||
"version": "0.35.1",
|
||||
"resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz",
|
||||
"integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/huntabyte",
|
||||
@@ -3270,23 +3268,31 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esm-env": "^1.0.0"
|
||||
"dequal": "^2.0.3",
|
||||
"esm-env": "^1.0.0",
|
||||
"lz-string": "^1.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@sveltejs/kit": "^2.21.0",
|
||||
"svelte": "^5.7.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@sveltejs/kit": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bits-ui/node_modules/svelte-toolbelt": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.9.1.tgz",
|
||||
"integrity": "sha512-wBX6MtYw/kpht80j5zLpxJyR9soLizXPIAIWEVd9llAi17SR44ZdG291bldjB7r/K5duC0opDFcuhk2cA1hb8g==",
|
||||
"version": "0.10.6",
|
||||
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz",
|
||||
"integrity": "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/huntabyte"
|
||||
],
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"runed": "^0.28.0",
|
||||
"runed": "^0.35.1",
|
||||
"style-to-object": "^1.0.8"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3891,13 +3897,6 @@
|
||||
"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": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz",
|
||||
@@ -4053,6 +4052,16 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
|
||||
@@ -6019,6 +6028,16 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.30.17",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||
@@ -6369,9 +6388,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mode-watcher": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.0.7.tgz",
|
||||
"integrity": "sha512-ZGA7ZGdOvBJeTQkzdBOnXSgTkO6U6iIFWJoyGCTt6oHNg9XP9NBvS26De+V4W2aqI+B0yYXUskFG2VnEo3zyMQ==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.1.0.tgz",
|
||||
"integrity": "sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -8292,22 +8311,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-sonner": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.0.4.tgz",
|
||||
"integrity": "sha512-ctm9jeV0Rf3im2J6RU1emccrJFjRSdNSPsLlxaF62TLZw9bB1D40U/U7+wqEgohJY/X7FBdghdj0BFQF/IqKPQ==",
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.0.5.tgz",
|
||||
"integrity": "sha512-9dpGPFqKb/QWudYqGnEz93vuY+NgCEvyNvxoCLMVGw6sDN/3oVeKV1xiEirW2E1N3vJEyj5imSBNOGltQHA7mg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"runed": "^0.26.0"
|
||||
"runed": "^0.28.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-sonner/node_modules/runed": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/runed/-/runed-0.26.0.tgz",
|
||||
"integrity": "sha512-qWFv0cvLVRd8pdl/AslqzvtQyEn5KaIugEernwg9G98uJVSZcs/ygvPBvF80LA46V8pwRvSKnaVLDI3+i2wubw==",
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/runed/-/runed-0.28.0.tgz",
|
||||
"integrity": "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/huntabyte",
|
||||
@@ -8376,35 +8395,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-variants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-1.0.0.tgz",
|
||||
"integrity": "sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.1.1.tgz",
|
||||
"integrity": "sha512-ftLXe3krnqkMHsuBTEmaVUXYovXtPyTK7ckEfDRXS8PBZx0bAUas+A0jYxuKA5b8qg++wvQ3d2MQ7l/xeZxbZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tailwind-merge": "3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.x",
|
||||
"pnpm": ">=7.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwind-merge": ">=3.0.0",
|
||||
"tailwindcss": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-variants/node_modules/tailwind-merge": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz",
|
||||
"integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"tailwind-merge": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz",
|
||||
"integrity": "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lucide/svelte": "^0.513.0",
|
||||
"@lucide/svelte": "^0.544.0",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/enhanced-img": "^0.6.0",
|
||||
"@sveltejs/kit": "^2.21.2",
|
||||
@@ -31,7 +31,7 @@
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
||||
"@typescript-eslint/parser": "^8.33.1",
|
||||
"bits-ui": "^2.5.0",
|
||||
"bits-ui": "^2.12.0",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-svelte": "^3.9.1",
|
||||
@@ -39,14 +39,15 @@
|
||||
"glob": "^11.0.2",
|
||||
"lucide-static": "^0.513.0",
|
||||
"mdsvex": "^0.12.6",
|
||||
"mode-watcher": "^1.0.7",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"paneforge": "^1.0.0-next.5",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"svelte": "^5.33.18",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-sonner": "^1.0.4",
|
||||
"svelte-sonner": "^1.0.5",
|
||||
"tailwind-variants": "^3.1.1",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"tslib": "^2.8.1",
|
||||
"tsx": "^4.19.1",
|
||||
@@ -77,7 +78,6 @@
|
||||
"png.js": "^0.2.1",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"sortablejs": "^1.15.6",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwind-variants": "^1.0.0"
|
||||
"tailwind-merge": "^3.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script lang="ts">
|
||||
import ButtonWithTooltip from '$lib/components/ButtonWithTooltip.svelte';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import * as ToggleGroup from '$lib/components/ui/toggle-group';
|
||||
import * as Popover from '$lib/components/ui/popover/index.js';
|
||||
import * as ToggleGroup from '$lib/components/ui/toggle-group/index.js';
|
||||
import Chart from 'chart.js/auto';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { map } from '$lib/stores';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import {
|
||||
BrickWall,
|
||||
@@ -20,7 +19,6 @@
|
||||
Construction,
|
||||
} from '@lucide/svelte';
|
||||
import { getSlopeColor, getSurfaceColor, getHighwayColor } from '$lib/assets/colors';
|
||||
import { _, df } from '$lib/i18n.svelte';
|
||||
import {
|
||||
getCadenceWithUnits,
|
||||
getConvertedDistance,
|
||||
@@ -35,19 +33,29 @@
|
||||
getTemperatureWithUnits,
|
||||
getVelocityWithUnits,
|
||||
} from '$lib/units';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import type { GPXStatistics } from 'gpx';
|
||||
import { settings } from '$lib/db';
|
||||
import { mode } from 'mode-watcher';
|
||||
|
||||
export let gpxStatistics: Writable<GPXStatistics>;
|
||||
export let slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
|
||||
export let additionalDatasets: string[];
|
||||
export let elevationFill: 'slope' | 'surface' | 'highway' | undefined;
|
||||
export let showControls: boolean = true;
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { map } from '$lib/components/map/map';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
|
||||
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 overlay: HTMLCanvasElement;
|
||||
let chart: Chart;
|
||||
@@ -179,7 +187,7 @@
|
||||
|
||||
if (point.time) {
|
||||
labels.push(
|
||||
` ${i18n._('quantities.time')}: ${$df.format(point.time)}`
|
||||
` ${i18n._('quantities.time')}: ${i18n.df.format(point.time)}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -356,96 +364,97 @@
|
||||
canvas.addEventListener('pointerup', onMouseUp);
|
||||
});
|
||||
|
||||
$: if (chart && $distanceUnits && $velocityUnits && $temperatureUnits) {
|
||||
$effect(() => {
|
||||
let data = $gpxStatistics;
|
||||
if (chart && $distanceUnits && $velocityUnits && $temperatureUnits) {
|
||||
// update data
|
||||
chart.data.datasets[0] = {
|
||||
label: i18n._('quantities.elevation'),
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.ele ? getConvertedElevation(point.ele) : 0,
|
||||
time: point.time,
|
||||
slope: {
|
||||
at: data.local.slope.at[index],
|
||||
segment: data.local.slope.segment[index],
|
||||
length: data.local.slope.length[index],
|
||||
},
|
||||
extensions: point.getExtensions(),
|
||||
coordinates: point.getCoordinates(),
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
fill: 'start',
|
||||
order: 1,
|
||||
};
|
||||
chart.data.datasets[1] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: getConvertedVelocity(data.local.speed[index]),
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
yAxisID: 'yspeed',
|
||||
hidden: true,
|
||||
};
|
||||
chart.data.datasets[2] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.getHeartRate(),
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
yAxisID: 'yhr',
|
||||
hidden: true,
|
||||
};
|
||||
chart.data.datasets[3] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.getCadence(),
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
yAxisID: 'ycad',
|
||||
hidden: true,
|
||||
};
|
||||
chart.data.datasets[4] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: getConvertedTemperature(point.getTemperature()),
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
yAxisID: 'yatemp',
|
||||
hidden: true,
|
||||
};
|
||||
chart.data.datasets[5] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.getPower(),
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
yAxisID: 'ypower',
|
||||
hidden: true,
|
||||
};
|
||||
chart.options.scales.x['min'] = 0;
|
||||
chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total);
|
||||
|
||||
// update data
|
||||
chart.data.datasets[0] = {
|
||||
label: i18n._('quantities.elevation'),
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.ele ? getConvertedElevation(point.ele) : 0,
|
||||
time: point.time,
|
||||
slope: {
|
||||
at: data.local.slope.at[index],
|
||||
segment: data.local.slope.segment[index],
|
||||
length: data.local.slope.length[index],
|
||||
},
|
||||
extensions: point.getExtensions(),
|
||||
coordinates: point.getCoordinates(),
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
fill: 'start',
|
||||
order: 1,
|
||||
};
|
||||
chart.data.datasets[1] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: getConvertedVelocity(data.local.speed[index]),
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
yAxisID: 'yspeed',
|
||||
hidden: true,
|
||||
};
|
||||
chart.data.datasets[2] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.getHeartRate(),
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
yAxisID: 'yhr',
|
||||
hidden: true,
|
||||
};
|
||||
chart.data.datasets[3] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.getCadence(),
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
yAxisID: 'ycad',
|
||||
hidden: true,
|
||||
};
|
||||
chart.data.datasets[4] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: getConvertedTemperature(point.getTemperature()),
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
yAxisID: 'yatemp',
|
||||
hidden: true,
|
||||
};
|
||||
chart.data.datasets[5] = {
|
||||
data: data.local.points.map((point, index) => {
|
||||
return {
|
||||
x: getConvertedDistance(data.local.distance.total[index]),
|
||||
y: point.getPower(),
|
||||
index: index,
|
||||
};
|
||||
}),
|
||||
normalized: true,
|
||||
yAxisID: 'ypower',
|
||||
hidden: true,
|
||||
};
|
||||
chart.options.scales.x['min'] = 0;
|
||||
chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total);
|
||||
|
||||
chart.update();
|
||||
}
|
||||
chart.update();
|
||||
}
|
||||
});
|
||||
|
||||
function slopeFillCallback(context) {
|
||||
return getSlopeColor(context.p0.raw.slope.segment);
|
||||
@@ -463,40 +472,44 @@
|
||||
);
|
||||
}
|
||||
|
||||
$: if (chart) {
|
||||
if (elevationFill === 'slope') {
|
||||
chart.data.datasets[0]['segment'] = {
|
||||
backgroundColor: slopeFillCallback,
|
||||
};
|
||||
} else if (elevationFill === 'surface') {
|
||||
chart.data.datasets[0]['segment'] = {
|
||||
backgroundColor: surfaceFillCallback,
|
||||
};
|
||||
} else if (elevationFill === 'highway') {
|
||||
chart.data.datasets[0]['segment'] = {
|
||||
backgroundColor: highwayFillCallback,
|
||||
};
|
||||
} else {
|
||||
chart.data.datasets[0]['segment'] = {};
|
||||
$effect(() => {
|
||||
if (elevationFill && chart) {
|
||||
if (elevationFill === 'slope') {
|
||||
chart.data.datasets[0]['segment'] = {
|
||||
backgroundColor: slopeFillCallback,
|
||||
};
|
||||
} else if (elevationFill === 'surface') {
|
||||
chart.data.datasets[0]['segment'] = {
|
||||
backgroundColor: surfaceFillCallback,
|
||||
};
|
||||
} else if (elevationFill === 'highway') {
|
||||
chart.data.datasets[0]['segment'] = {
|
||||
backgroundColor: highwayFillCallback,
|
||||
};
|
||||
} else {
|
||||
chart.data.datasets[0]['segment'] = {};
|
||||
}
|
||||
chart.update();
|
||||
}
|
||||
chart.update();
|
||||
}
|
||||
});
|
||||
|
||||
$: if (additionalDatasets && chart) {
|
||||
let includeSpeed = additionalDatasets.includes('speed');
|
||||
let includeHeartRate = additionalDatasets.includes('hr');
|
||||
let includeCadence = additionalDatasets.includes('cad');
|
||||
let includeTemperature = additionalDatasets.includes('atemp');
|
||||
let includePower = additionalDatasets.includes('power');
|
||||
if (chart.data.datasets.length > 0) {
|
||||
chart.data.datasets[1].hidden = !includeSpeed;
|
||||
chart.data.datasets[2].hidden = !includeHeartRate;
|
||||
chart.data.datasets[3].hidden = !includeCadence;
|
||||
chart.data.datasets[4].hidden = !includeTemperature;
|
||||
chart.data.datasets[5].hidden = !includePower;
|
||||
$effect(() => {
|
||||
if (additionalDatasets && chart) {
|
||||
let includeSpeed = additionalDatasets.includes('speed');
|
||||
let includeHeartRate = additionalDatasets.includes('hr');
|
||||
let includeCadence = additionalDatasets.includes('cad');
|
||||
let includeTemperature = additionalDatasets.includes('atemp');
|
||||
let includePower = additionalDatasets.includes('power');
|
||||
if (chart.data.datasets.length > 0) {
|
||||
chart.data.datasets[1].hidden = !includeSpeed;
|
||||
chart.data.datasets[2].hidden = !includeHeartRate;
|
||||
chart.data.datasets[3].hidden = !includeCadence;
|
||||
chart.data.datasets[4].hidden = !includeTemperature;
|
||||
chart.data.datasets[5].hidden = !includePower;
|
||||
}
|
||||
chart.update();
|
||||
}
|
||||
chart.update();
|
||||
}
|
||||
});
|
||||
|
||||
function updateOverlay() {
|
||||
if (!canvas) {
|
||||
@@ -541,7 +554,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: $slicedGPXStatistics, mode.current, updateOverlay();
|
||||
$effect(() => {
|
||||
if ($slicedGPXStatistics || mode.current) {
|
||||
updateOverlay();
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (chart) {
|
||||
@@ -557,63 +574,62 @@
|
||||
<div class="absolute bottom-10 right-1.5">
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<ButtonWithTooltip
|
||||
{...props}
|
||||
label={i18n._('chart.settings')}
|
||||
variant="outline"
|
||||
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" />
|
||||
</ButtonWithTooltip>
|
||||
{/snippet}
|
||||
<ButtonWithTooltip
|
||||
label={i18n._('chart.settings')}
|
||||
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"
|
||||
>
|
||||
<ChartNoAxesColumn size="18" />
|
||||
</ButtonWithTooltip>
|
||||
</Popover.Trigger>
|
||||
<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"
|
||||
align="end"
|
||||
sideOffset={-32}
|
||||
>
|
||||
<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"
|
||||
bind:value={elevationFill}
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<div class="w-6 flex justify-center items-center">
|
||||
{#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}
|
||||
</div>
|
||||
<TriangleRight size="15" class="mr-1" />
|
||||
<TriangleRight size="15" />
|
||||
{i18n._('quantities.slope')}
|
||||
</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"
|
||||
variant="outline"
|
||||
>
|
||||
<div class="w-6 flex justify-center items-center">
|
||||
{#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}
|
||||
</div>
|
||||
<BrickWall size="15" class="mr-1" />
|
||||
<BrickWall size="15" />
|
||||
{i18n._('quantities.surface')}
|
||||
</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"
|
||||
variant="outline"
|
||||
>
|
||||
<div class="w-6 flex justify-center items-center">
|
||||
{#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}
|
||||
</div>
|
||||
<Construction size="15" class="mr-1" />
|
||||
<Construction size="15" />
|
||||
{i18n._('quantities.highway')}
|
||||
</ToggleGroup.Item>
|
||||
</ToggleGroup.Root>
|
||||
@@ -623,7 +639,7 @@
|
||||
bind:value={additionalDatasets}
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<div class="w-6 flex justify-center items-center">
|
||||
@@ -631,13 +647,13 @@
|
||||
<Check size="14" />
|
||||
{/if}
|
||||
</div>
|
||||
<Zap size="15" class="mr-1" />
|
||||
<Zap size="15" />
|
||||
{$velocityUnits === 'speed'
|
||||
? i18n._('quantities.speed')
|
||||
: i18n._('quantities.pace')}
|
||||
</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"
|
||||
>
|
||||
<div class="w-6 flex justify-center items-center">
|
||||
@@ -645,11 +661,11 @@
|
||||
<Check size="14" />
|
||||
{/if}
|
||||
</div>
|
||||
<HeartPulse size="15" class="mr-1" />
|
||||
<HeartPulse size="15" />
|
||||
{i18n._('quantities.heartrate')}
|
||||
</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"
|
||||
>
|
||||
<div class="w-6 flex justify-center items-center">
|
||||
@@ -657,11 +673,11 @@
|
||||
<Check size="14" />
|
||||
{/if}
|
||||
</div>
|
||||
<Orbit size="15" class="mr-1" />
|
||||
<Orbit size="15" />
|
||||
{i18n._('quantities.cadence')}
|
||||
</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"
|
||||
>
|
||||
<div class="w-6 flex justify-center items-center">
|
||||
@@ -669,11 +685,11 @@
|
||||
<Check size="14" />
|
||||
{/if}
|
||||
</div>
|
||||
<Thermometer size="15" class="mr-1" />
|
||||
<Thermometer size="15" />
|
||||
{i18n._('quantities.temperature')}
|
||||
</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"
|
||||
>
|
||||
<div class="w-6 flex justify-center items-center">
|
||||
@@ -681,7 +697,7 @@
|
||||
<Check size="14" />
|
||||
{/if}
|
||||
</div>
|
||||
<SquareActivity size="15" class="mr-1" />
|
||||
<SquareActivity size="15" />
|
||||
{i18n._('quantities.power')}
|
||||
</ToggleGroup.Item>
|
||||
</ToggleGroup.Root>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { CircleHelp } from '@lucide/svelte';
|
||||
import { CircleQuestionMark } from '@lucide/svelte';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
|
||||
export let link: string | undefined = undefined;
|
||||
@@ -8,7 +8,7 @@
|
||||
<div
|
||||
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>
|
||||
<slot />
|
||||
{#if link}
|
||||
|
||||
@@ -120,13 +120,13 @@
|
||||
</Menubar.Trigger>
|
||||
<Menubar.Content class="border-none">
|
||||
<Menubar.Item onclick={createFile}>
|
||||
<Plus size="16" class="mr-1" />
|
||||
<Plus size="16" />
|
||||
{i18n._('menu.new')}
|
||||
<Shortcut key="+" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item onclick={triggerFileInput}>
|
||||
<FolderOpen size="16" class="mr-1" />
|
||||
<FolderOpen size="16" />
|
||||
{i18n._('menu.open')}
|
||||
<Shortcut key="O" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
@@ -135,7 +135,7 @@
|
||||
onclick={fileActions.duplicateSelection}
|
||||
disabled={$selection.size == 0}
|
||||
>
|
||||
<Copy size="16" class="mr-1" />
|
||||
<Copy size="16" />
|
||||
{i18n._('menu.duplicate')}
|
||||
<Shortcut key="D" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
@@ -144,7 +144,7 @@
|
||||
onclick={fileActions.deleteSelectedFiles}
|
||||
disabled={$selection.size == 0}
|
||||
>
|
||||
<FileX size="16" class="mr-1" />
|
||||
<FileX size="16" />
|
||||
{i18n._('menu.close')}
|
||||
<Shortcut key="⌫" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
@@ -152,7 +152,7 @@
|
||||
onclick={fileActions.deleteAllFiles}
|
||||
disabled={fileStateCollection.size == 0}
|
||||
>
|
||||
<FileX size="16" class="mr-1" />
|
||||
<FileX size="16" />
|
||||
{i18n._('menu.close_all')}
|
||||
<Shortcut key="⌫" ctrl={true} shift={true} />
|
||||
</Menubar.Item>
|
||||
@@ -161,7 +161,7 @@
|
||||
onclick={() => (exportState.current = ExportState.SELECTION)}
|
||||
disabled={$selection.size == 0}
|
||||
>
|
||||
<Download size="16" class="mr-1" />
|
||||
<Download size="16" />
|
||||
{i18n._('menu.export')}
|
||||
<Shortcut key="S" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
@@ -169,7 +169,7 @@
|
||||
onclick={() => (exportState.current = ExportState.ALL)}
|
||||
disabled={fileStateCollection.size == 0}
|
||||
>
|
||||
<Download size="16" class="mr-1" />
|
||||
<Download size="16" />
|
||||
{i18n._('menu.export_all')}
|
||||
<Shortcut key="S" ctrl={true} shift={true} />
|
||||
</Menubar.Item>
|
||||
@@ -185,7 +185,7 @@
|
||||
onclick={() => fileActionManager.undo()}
|
||||
disabled={!fileActionManager.canUndo}
|
||||
>
|
||||
<Undo2 size="16" class="mr-1" />
|
||||
<Undo2 size="16" />
|
||||
{i18n._('menu.undo')}
|
||||
<Shortcut key="Z" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
@@ -193,7 +193,7 @@
|
||||
onclick={() => fileActionManager.redo()}
|
||||
disabled={!fileActionManager.canRedo}
|
||||
>
|
||||
<Redo2 size="16" class="mr-1" />
|
||||
<Redo2 size="16" />
|
||||
{i18n._('menu.redo')}
|
||||
<Shortcut key="Z" ctrl={true} shift={true} />
|
||||
</Menubar.Item>
|
||||
@@ -209,7 +209,7 @@
|
||||
)}
|
||||
onclick={() => (editMetadata.current = true)}
|
||||
>
|
||||
<Info size="16" class="mr-1" />
|
||||
<Info size="16" />
|
||||
{i18n._('menu.metadata.button')}
|
||||
<Shortcut key="I" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
@@ -224,7 +224,7 @@
|
||||
)}
|
||||
onclick={() => (editStyle.current = true)}
|
||||
>
|
||||
<PaintBucket size="16" class="mr-1" />
|
||||
<PaintBucket size="16" />
|
||||
{i18n._('menu.style.button')}
|
||||
</Menubar.Item>
|
||||
<Menubar.Item
|
||||
@@ -238,10 +238,10 @@
|
||||
disabled={$selection.size == 0}
|
||||
>
|
||||
<!-- {#if $allHidden}
|
||||
<Eye size="16" class="mr-1" />
|
||||
<Eye size="16" />
|
||||
{i18n._('menu.unhide')}
|
||||
{:else}
|
||||
<EyeOff size="16" class="mr-1" />
|
||||
<EyeOff size="16" />
|
||||
{i18n._('menu.hide')}
|
||||
{/if} -->
|
||||
<Shortcut key="H" ctrl={true} />
|
||||
@@ -256,7 +256,7 @@
|
||||
)}
|
||||
disabled={$selection.size !== 1}
|
||||
>
|
||||
<Plus size="16" class="mr-1" />
|
||||
<Plus size="16" />
|
||||
{i18n._('menu.new_track')}
|
||||
</Menubar.Item>
|
||||
{:else if $selection
|
||||
@@ -273,7 +273,7 @@
|
||||
}}
|
||||
disabled={$selection.size !== 1}
|
||||
>
|
||||
<Plus size="16" class="mr-1" />
|
||||
<Plus size="16" />
|
||||
{i18n._('menu.new_segment')}
|
||||
</Menubar.Item>
|
||||
{/if}
|
||||
@@ -283,7 +283,7 @@
|
||||
onclick={selection.selectAll}
|
||||
disabled={fileStateCollection.size == 0}
|
||||
>
|
||||
<FileStack size="16" class="mr-1" />
|
||||
<FileStack size="16" />
|
||||
{i18n._('menu.select_all')}
|
||||
<Shortcut key="A" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
@@ -294,7 +294,7 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Maximize size="16" class="mr-1" />
|
||||
<Maximize size="16" />
|
||||
{i18n._('menu.center')}
|
||||
<Shortcut key="⏎" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
@@ -304,7 +304,7 @@
|
||||
onclick={selection.copySelection}
|
||||
disabled={$selection.size === 0}
|
||||
>
|
||||
<ClipboardCopy size="16" class="mr-1" />
|
||||
<ClipboardCopy size="16" />
|
||||
{i18n._('menu.copy')}
|
||||
<Shortcut key="C" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
@@ -312,7 +312,7 @@
|
||||
onclick={selection.cutSelection}
|
||||
disabled={$selection.size === 0}
|
||||
>
|
||||
<Scissors size="16" class="mr-1" />
|
||||
<Scissors size="16" />
|
||||
{i18n._('menu.cut')}
|
||||
<Shortcut key="X" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
@@ -325,7 +325,7 @@
|
||||
))}
|
||||
onclick={pasteSelection}
|
||||
>
|
||||
<ClipboardPaste size="16" class="mr-1" />
|
||||
<ClipboardPaste size="16" />
|
||||
{i18n._('menu.paste')}
|
||||
<Shortcut key="V" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
@@ -335,7 +335,7 @@
|
||||
onclick={fileActions.deleteSelection}
|
||||
disabled={$selection.size == 0}
|
||||
>
|
||||
<Trash2 size="16" class="mr-1" />
|
||||
<Trash2 size="16" />
|
||||
{i18n._('menu.delete')}
|
||||
<Shortcut key="⌫" ctrl={true} />
|
||||
</Menubar.Item>
|
||||
@@ -348,42 +348,36 @@
|
||||
</Menubar.Trigger>
|
||||
<Menubar.Content class="border-none">
|
||||
<Menubar.CheckboxItem bind:checked={$elevationProfile}>
|
||||
<ChartArea size="16" class="mr-1" />
|
||||
<ChartArea size="16" />
|
||||
{i18n._('menu.elevation_profile')}
|
||||
<Shortcut key="P" ctrl={true} />
|
||||
</Menubar.CheckboxItem>
|
||||
<Menubar.CheckboxItem bind:checked={$treeFileView}>
|
||||
<ListTree size="16" class="mr-1" />
|
||||
<ListTree size="16" />
|
||||
{i18n._('menu.tree_file_view')}
|
||||
<Shortcut key="L" ctrl={true} />
|
||||
</Menubar.CheckboxItem>
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item inset onclick={switchBasemaps}>
|
||||
<Map size="16" class="mr-1" />{i18n._('menu.switch_basemap')}<Shortcut
|
||||
key="F1"
|
||||
/>
|
||||
<Map size="16" />{i18n._('menu.switch_basemap')}<Shortcut key="F1" />
|
||||
</Menubar.Item>
|
||||
<Menubar.Item inset onclick={toggleOverlays}>
|
||||
<Layers2 size="16" class="mr-1" />{i18n._('menu.toggle_overlays')}<Shortcut
|
||||
key="F2"
|
||||
/>
|
||||
<Layers2 size="16" />{i18n._('menu.toggle_overlays')}<Shortcut key="F2" />
|
||||
</Menubar.Item>
|
||||
<Menubar.Separator />
|
||||
<Menubar.CheckboxItem bind:checked={$distanceMarkers}>
|
||||
<Coins size="16" class="mr-1" />{i18n._('menu.distance_markers')}<Shortcut
|
||||
key="F3"
|
||||
/>
|
||||
<Coins size="16" />{i18n._('menu.distance_markers')}<Shortcut key="F3" />
|
||||
</Menubar.CheckboxItem>
|
||||
<Menubar.CheckboxItem bind:checked={$directionMarkers}>
|
||||
<Milestone size="16" class="mr-1" />{i18n._(
|
||||
'menu.direction_markers'
|
||||
)}<Shortcut key="F4" />
|
||||
<Milestone size="16" />{i18n._('menu.direction_markers')}<Shortcut
|
||||
key="F4"
|
||||
/>
|
||||
</Menubar.CheckboxItem>
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item inset onclick={map.toggle3D}>
|
||||
<Box size="16" class="mr-1" />
|
||||
<Box size="16" />
|
||||
{i18n._('menu.toggle_3d')}
|
||||
<Shortcut key="{i18n._('menu.ctrl')}+{i18n._('menu.drag')}" />
|
||||
<Shortcut key="{i18n._('menu.ctrl')} {i18n._('menu.drag')}" />
|
||||
</Menubar.Item>
|
||||
</Menubar.Content>
|
||||
</Menubar.Menu>
|
||||
@@ -397,7 +391,7 @@
|
||||
<Menubar.Content class="border-none">
|
||||
<Menubar.Sub>
|
||||
<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.SubContent>
|
||||
<Menubar.RadioGroup bind:value={$distanceUnits}>
|
||||
@@ -415,7 +409,7 @@
|
||||
</Menubar.Sub>
|
||||
<Menubar.Sub>
|
||||
<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.SubContent>
|
||||
<Menubar.RadioGroup bind:value={$velocityUnits}>
|
||||
@@ -430,7 +424,7 @@
|
||||
</Menubar.Sub>
|
||||
<Menubar.Sub>
|
||||
<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.SubContent>
|
||||
<Menubar.RadioGroup bind:value={$temperatureUnits}>
|
||||
@@ -446,7 +440,7 @@
|
||||
<Menubar.Separator />
|
||||
<Menubar.Sub>
|
||||
<Menubar.SubTrigger>
|
||||
<Languages size="16" class="mr-1" />
|
||||
<Languages size="16" class="mr-2" />
|
||||
{i18n._('menu.language')}
|
||||
</Menubar.SubTrigger>
|
||||
<Menubar.SubContent>
|
||||
@@ -462,9 +456,9 @@
|
||||
<Menubar.Sub>
|
||||
<Menubar.SubTrigger>
|
||||
{#if mode.current === 'light' || !mode.current}
|
||||
<Sun size="16" class="mr-1" />
|
||||
<Sun size="16" class="mr-2" />
|
||||
{:else}
|
||||
<Moon size="16" class="mr-1" />
|
||||
<Moon size="16" class="mr-2" />
|
||||
{/if}
|
||||
{i18n._('menu.mode')}
|
||||
</Menubar.SubTrigger>
|
||||
@@ -487,7 +481,7 @@
|
||||
<Menubar.Separator />
|
||||
<Menubar.Sub>
|
||||
<Menubar.SubTrigger>
|
||||
<PersonStanding size="16" class="mr-1" />
|
||||
<PersonStanding size="16" class="mr-2" />
|
||||
{i18n._('menu.street_view_source')}
|
||||
</Menubar.SubTrigger>
|
||||
<Menubar.SubContent>
|
||||
@@ -502,7 +496,7 @@
|
||||
</Menubar.SubContent>
|
||||
</Menubar.Sub>
|
||||
<Menubar.Item onclick={() => (layerSettingsOpen = true)}>
|
||||
<Layers size="16" class="mr-1" />
|
||||
<Layers size="16" />
|
||||
{i18n._('menu.layers')}
|
||||
</Menubar.Item>
|
||||
</Menubar.Content>
|
||||
@@ -544,15 +538,17 @@
|
||||
<svelte:window
|
||||
on:keydown={(e) => {
|
||||
let targetInput =
|
||||
e.target.tagName === 'INPUT' ||
|
||||
e.target.tagName === 'TEXTAREA' ||
|
||||
e.target.tagName === 'SELECT' ||
|
||||
e.target.role === 'combobox' ||
|
||||
e.target.role === 'radio' ||
|
||||
e.target.role === 'menu' ||
|
||||
e.target.role === 'menuitem' ||
|
||||
e.target.role === 'menuitemradio' ||
|
||||
e.target.role === 'menuitemcheckbox';
|
||||
e &&
|
||||
e.target &&
|
||||
(e.target.tagName === 'INPUT' ||
|
||||
e.target.tagName === 'TEXTAREA' ||
|
||||
e.target.tagName === 'SELECT' ||
|
||||
e.target.role === 'combobox' ||
|
||||
e.target.role === 'radio' ||
|
||||
e.target.role === 'menu' ||
|
||||
e.target.role === 'menuitem' ||
|
||||
e.target.role === 'menuitemradio' ||
|
||||
e.target.role === 'menuitemcheckbox');
|
||||
|
||||
if (e.key === '+' && (e.metaKey || e.ctrlKey)) {
|
||||
createFile();
|
||||
|
||||
@@ -2,14 +2,24 @@
|
||||
import { isMac, isSafari } from '$lib/utils';
|
||||
import { onMount } from 'svelte';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import * as Kbd from '$lib/components/ui/kbd/index.js';
|
||||
|
||||
export let key: string | undefined = undefined;
|
||||
export let shift: boolean = false;
|
||||
export let ctrl: boolean = false;
|
||||
export let click: boolean = false;
|
||||
let {
|
||||
key = undefined,
|
||||
shift = false,
|
||||
ctrl = false,
|
||||
click = false,
|
||||
class: className = '',
|
||||
}: {
|
||||
key?: string;
|
||||
shift?: boolean;
|
||||
ctrl?: boolean;
|
||||
click?: boolean;
|
||||
class?: string;
|
||||
} = $props();
|
||||
|
||||
let mac = false;
|
||||
let safari = false;
|
||||
let mac = $state(false);
|
||||
let safari = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
mac = isMac();
|
||||
@@ -17,20 +27,17 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="ml-auto pl-2 text-xs tracking-widest text-muted-foreground flex flex-row gap-0 items-baseline"
|
||||
{...$$props}
|
||||
>
|
||||
<Kbd.Root class="ml-auto {className}">
|
||||
{#if shift}
|
||||
<span>⇧</span>
|
||||
⇧
|
||||
{/if}
|
||||
{#if ctrl}
|
||||
<span>{mac && !safari ? '⌘' : i18n._('menu.ctrl') + '+'}</span>
|
||||
{mac && !safari ? '⌘' : i18n._('menu.ctrl')}
|
||||
{/if}
|
||||
{#if key}
|
||||
<span class={key === '+' ? 'font-medium text-sm/4' : ''}>{key}</span>
|
||||
{key}
|
||||
{/if}
|
||||
{#if click}
|
||||
<span>{i18n._('menu.click')}</span>
|
||||
{i18n._('menu.click')}
|
||||
{/if}
|
||||
</div>
|
||||
</Kbd.Root>
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
<script lang="ts">
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
export let label: string;
|
||||
export let side: 'top' | 'right' | 'bottom' | 'left' = 'top';
|
||||
let {
|
||||
label,
|
||||
side = 'top',
|
||||
children,
|
||||
extra,
|
||||
class: className = '',
|
||||
}: {
|
||||
label: string;
|
||||
side?: 'top' | 'right' | 'bottom' | 'left';
|
||||
children: Snippet;
|
||||
extra?: Snippet;
|
||||
class?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger {...$$restProps} aria-label={label}>
|
||||
<slot />
|
||||
<Tooltip.Trigger class={className} aria-label={label}>
|
||||
{@render children()}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content {side}>
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<span>{label}</span>
|
||||
<slot name="extra" />
|
||||
{@render extra?.()}
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
variant="ghost"
|
||||
class="w-full flex flex-row {side === 'right'
|
||||
? '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'
|
||||
: ''} pointer-events-none"
|
||||
>
|
||||
@@ -62,7 +62,7 @@
|
||||
variant="ghost"
|
||||
class="w-full flex flex-row {side === 'right'
|
||||
? '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'}
|
||||
<Collapsible.Trigger>
|
||||
@@ -86,7 +86,7 @@
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
<Collapsible.Content class="ml-2">
|
||||
<Collapsible.Content>
|
||||
{@render props.content()}
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
|
||||
@@ -10,14 +10,7 @@ import {
|
||||
ListFileItem,
|
||||
ListRootItem,
|
||||
} from '$lib/components/file-list/file-list';
|
||||
import {
|
||||
getClosestLinePoint,
|
||||
getElevation,
|
||||
resetCursor,
|
||||
setGrabbingCursor,
|
||||
setPointerCursor,
|
||||
setScissorsCursor,
|
||||
} from '$lib/utils';
|
||||
import { getClosestLinePoint, getElevation } from '$lib/utils';
|
||||
import { selectedWaypoint } from '$lib/components/toolbar/tools/waypoint/waypoint';
|
||||
import { MapPin, Square } from 'lucide-static';
|
||||
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 { fileActions } from '$lib/logic/file-actions';
|
||||
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
|
||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||
|
||||
const colors = [
|
||||
'#ff0000',
|
||||
@@ -335,12 +329,12 @@ export class GPXLayer {
|
||||
e.stopPropagation();
|
||||
});
|
||||
marker.on('dragstart', () => {
|
||||
setGrabbingCursor();
|
||||
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, true);
|
||||
marker.getElement().style.cursor = 'grabbing';
|
||||
waypointPopup?.hide();
|
||||
});
|
||||
marker.on('dragend', (e) => {
|
||||
resetCursor();
|
||||
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
|
||||
marker.getElement().style.cursor = '';
|
||||
getElevation([marker._waypoint]).then((ele) => {
|
||||
fileActionManager.applyToFile(this.fileId, (file) => {
|
||||
@@ -431,14 +425,15 @@ export class GPXLayer {
|
||||
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
|
||||
)
|
||||
) {
|
||||
setScissorsCursor();
|
||||
mapCursor.notify(MapCursorState.SCISSORS, true);
|
||||
} else {
|
||||
setPointerCursor();
|
||||
mapCursor.notify(MapCursorState.LAYER_HOVER, true);
|
||||
}
|
||||
}
|
||||
|
||||
layerOnMouseLeave() {
|
||||
resetCursor();
|
||||
mapCursor.notify(MapCursorState.SCISSORS, false);
|
||||
mapCursor.notify(MapCursorState.LAYER_HOVER, false);
|
||||
}
|
||||
|
||||
layerOnMouseMove(e: any) {
|
||||
|
||||
@@ -312,10 +312,20 @@
|
||||
<div class="flex flex-row items-center gap-2" data-id={id}>
|
||||
<Move size="12" />
|
||||
<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" />
|
||||
</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" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -338,17 +348,26 @@
|
||||
<div class="flex flex-row items-center gap-2" data-id={id}>
|
||||
<Move size="12" />
|
||||
<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" />
|
||||
</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" />
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Root class="py-0 gap-0 shadow-none">
|
||||
<Card.Header class="p-3">
|
||||
<Card.Title class="text-base">
|
||||
{#if selectedLayerId}
|
||||
|
||||
@@ -179,9 +179,9 @@
|
||||
? 'grid-rows-[1fr] grid-cols-[1fr]'
|
||||
: ''} {cancelEvents ? 'pointer-events-none' : ''}"
|
||||
>
|
||||
<ScrollArea>
|
||||
<ScrollArea class="overflow-hidden">
|
||||
<div class="h-fit">
|
||||
<div class="p-2">
|
||||
<div class="p-2 ml-1">
|
||||
<LayerTree
|
||||
layerTree={$selectedBasemapTree}
|
||||
name="basemaps"
|
||||
@@ -193,7 +193,7 @@
|
||||
/>
|
||||
</div>
|
||||
<Separator class="w-full" />
|
||||
<div class="p-2">
|
||||
<div class="p-2 ml-1">
|
||||
{#if $currentOverlays}
|
||||
<LayerTree
|
||||
layerTree={$selectedOverlayTree}
|
||||
@@ -204,7 +204,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<Separator class="w-full" />
|
||||
<div class="p-2">
|
||||
<div class="p-2 ml-1">
|
||||
{#if $currentOverpassQueries}
|
||||
<LayerTree
|
||||
layerTree={$selectedOverpassTree}
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
<Accordion.Item value="layer-selection" class="flex flex-col">
|
||||
<Accordion.Trigger>{i18n._('layers.selection')}</Accordion.Trigger>
|
||||
<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={basemapTree}
|
||||
name="basemapSettings"
|
||||
@@ -99,7 +99,7 @@
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="py-2 pl-1 pr-2">
|
||||
<div class="py-2 pl-3 pr-2">
|
||||
<LayerTree
|
||||
layerTree={overlayTree}
|
||||
name="overlaySettings"
|
||||
@@ -108,7 +108,7 @@
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="py-2 pl-1 pr-2">
|
||||
<div class="py-2 pl-3 pr-2">
|
||||
<LayerTree
|
||||
layerTree={overpassTree}
|
||||
name="overpassSettings"
|
||||
@@ -130,7 +130,7 @@
|
||||
type="single"
|
||||
onValueChange={setOpacityFromSelection}
|
||||
>
|
||||
<Select.Trigger class="h-8 mr-1">
|
||||
<Select.Trigger class="h-8 mr-1 w-full">
|
||||
{#if selectedOverlay}
|
||||
{#if isSelected($selectedOverlayTree, selectedOverlay)}
|
||||
{i18n._(`layers.label.${selectedOverlay}`)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||
import type mapboxgl from 'mapbox-gl';
|
||||
|
||||
export class GoogleRedirect {
|
||||
@@ -13,7 +13,7 @@ export class GoogleRedirect {
|
||||
if (this.enabled) return;
|
||||
|
||||
this.enabled = true;
|
||||
setCrosshairCursor();
|
||||
mapCursor.notify(MapCursorState.STREET_VIEW_CROSSHAIR, true);
|
||||
this.map.on('click', this.openStreetView);
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ export class GoogleRedirect {
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.enabled = false;
|
||||
resetCursor();
|
||||
mapCursor.notify(MapCursorState.STREET_VIEW_CROSSHAIR, false);
|
||||
this.map.off('click', this.openStreetView);
|
||||
}
|
||||
|
||||
openStreetView(e) {
|
||||
openStreetView(e: mapboxgl.MapMouseEvent) {
|
||||
window.open(
|
||||
`https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${e.lngLat.lat},${e.lngLat.lng}`
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import mapboxgl, { type LayerSpecification, type VectorSourceSpecification } from 'mapbox-gl';
|
||||
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
|
||||
import 'mapillary-js/dist/mapillary.css';
|
||||
import { resetCursor, setPointerCursor } from '$lib/utils';
|
||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||
|
||||
const mapillarySource: VectorSourceSpecification = {
|
||||
type: 'vector',
|
||||
@@ -140,10 +140,10 @@ export class MapillaryLayer {
|
||||
this.viewer.resize();
|
||||
this.viewer.moveTo(e.features[0].properties.id);
|
||||
|
||||
setPointerCursor();
|
||||
mapCursor.notify(MapCursorState.MAPILLARY_HOVER, true);
|
||||
}
|
||||
|
||||
onMouseLeave() {
|
||||
resetCursor();
|
||||
mapCursor.notify(MapCursorState.MAPILLARY_HOVER, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import ButtonWithTooltip from '$lib/components/ButtonWithTooltip.svelte';
|
||||
|
||||
const { streetViewSource } = settings;
|
||||
|
||||
@@ -47,15 +48,21 @@
|
||||
</script>
|
||||
|
||||
<CustomControl class="w-[29px] h-[29px] shrink-0">
|
||||
<Tooltip class="w-full h-full" side="left" label={i18n._('menu.toggle_street_view')}>
|
||||
<Toggle
|
||||
bind:pressed={$streetViewEnabled}
|
||||
class="w-full h-full rounded p-0"
|
||||
aria-label={i18n._('menu.toggle_street_view')}
|
||||
>
|
||||
<PersonStanding size="22" />
|
||||
</Toggle>
|
||||
</Tooltip>
|
||||
<ButtonWithTooltip
|
||||
variant="ghost"
|
||||
class="w-full h-full"
|
||||
side="left"
|
||||
label={i18n._('menu.toggle_street_view')}
|
||||
onclick={() => {
|
||||
$streetViewEnabled = !$streetViewEnabled;
|
||||
}}
|
||||
>
|
||||
<PersonStanding
|
||||
size="22"
|
||||
class="size-5.5"
|
||||
color={$streetViewEnabled ? '#33b5e5' : 'currentColor'}
|
||||
/>
|
||||
</ButtonWithTooltip>
|
||||
</CustomControl>
|
||||
|
||||
<div
|
||||
|
||||
@@ -26,31 +26,31 @@
|
||||
''}"
|
||||
>
|
||||
<ToolbarItem itemTool={Tool.ROUTING} label={i18n._('toolbar.routing.tooltip')}>
|
||||
<Pencil size="18" />
|
||||
<Pencil size="18" class="size-4.5" />
|
||||
</ToolbarItem>
|
||||
<ToolbarItem itemTool={Tool.WAYPOINT} label={i18n._('toolbar.waypoint.tooltip')}>
|
||||
<MapPin size="18" />
|
||||
<MapPin size="18" class="size-4.5" />
|
||||
</ToolbarItem>
|
||||
<ToolbarItem itemTool={Tool.SCISSORS} label={i18n._('toolbar.scissors.tooltip')}>
|
||||
<Scissors size="18" />
|
||||
<Scissors size="18" class="size-4.5" />
|
||||
</ToolbarItem>
|
||||
<ToolbarItem itemTool={Tool.TIME} label={i18n._('toolbar.time.tooltip')}>
|
||||
<CalendarClock size="18" />
|
||||
<CalendarClock size="18" class="size-4.5" />
|
||||
</ToolbarItem>
|
||||
<ToolbarItem itemTool={Tool.MERGE} label={i18n._('toolbar.merge.tooltip')}>
|
||||
<Group size="18" />
|
||||
<Group size="18" class="size-4.5" />
|
||||
</ToolbarItem>
|
||||
<ToolbarItem itemTool={Tool.EXTRACT} label={i18n._('toolbar.extract.tooltip')}>
|
||||
<Ungroup size="18" />
|
||||
<Ungroup size="18" class="size-4.5" />
|
||||
</ToolbarItem>
|
||||
<ToolbarItem itemTool={Tool.ELEVATION} label={i18n._('toolbar.elevation.button')}>
|
||||
<MountainSnow size="18" />
|
||||
<MountainSnow size="18" class="size-4.5" />
|
||||
</ToolbarItem>
|
||||
<ToolbarItem itemTool={Tool.REDUCE} label={i18n._('toolbar.reduce.tooltip')}>
|
||||
<Funnel size="18" />
|
||||
<Funnel size="18" class="size-4.5" />
|
||||
</ToolbarItem>
|
||||
<ToolbarItem itemTool={Tool.CLEAN} label={i18n._('toolbar.clean.tooltip')}>
|
||||
<SquareDashedMousePointer size="18" />
|
||||
<SquareDashedMousePointer size="18" class="size-4.5" />
|
||||
</ToolbarItem>
|
||||
</div>
|
||||
<ToolbarItemMenu class={props.class ?? ''} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
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';
|
||||
|
||||
let {
|
||||
@@ -15,22 +15,22 @@
|
||||
} = $props();
|
||||
|
||||
function toggleTool() {
|
||||
if (tool.current === itemTool) {
|
||||
tool.current = null;
|
||||
if ($currentTool === itemTool) {
|
||||
$currentTool = null;
|
||||
} else {
|
||||
tool.current = itemTool;
|
||||
$currentTool = itemTool;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root delayDuration={300}>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button
|
||||
{...props}
|
||||
variant="ghost"
|
||||
class="h-[26px] px-1 py-1.5 {tool.current === itemTool ? 'bg-accent' : ''}"
|
||||
class="size-[24px] {$currentTool === itemTool ? 'bg-accent' : ''}"
|
||||
onclick={toggleTool}
|
||||
aria-label={label}
|
||||
>
|
||||
|
||||
@@ -1,68 +1,64 @@
|
||||
<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 Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||
import Scissors from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
|
||||
import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
|
||||
import Time from '$lib/components/toolbar/tools/Time.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 Extract from '$lib/components/toolbar/tools/Extract.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 { onMount } from 'svelte';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
|
||||
let {
|
||||
popupElement,
|
||||
popup,
|
||||
class: className = '',
|
||||
}: {
|
||||
popupElement: HTMLDivElement;
|
||||
popup: mapboxgl.Popup;
|
||||
class: string;
|
||||
} = $props();
|
||||
|
||||
const { minimizeRoutingMenu } = settings;
|
||||
|
||||
onMount(() => {
|
||||
popup = new mapboxgl.Popup({
|
||||
let popupElement: HTMLDivElement | undefined = $state(undefined);
|
||||
let popup: mapboxgl.Popup | undefined = $derived.by(() => {
|
||||
if (!popupElement) {
|
||||
return undefined;
|
||||
}
|
||||
let popup = new mapboxgl.Popup({
|
||||
closeButton: false,
|
||||
maxWidth: undefined,
|
||||
});
|
||||
popup.setDOMContent(popupElement);
|
||||
popupElement.classList.remove('hidden');
|
||||
return popup;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if tool.current !== null}
|
||||
{#if $currentTool !== null}
|
||||
<div class="translate-x-1 h-full animate-in animate-out {className}">
|
||||
<div class="rounded-md shadow-md pointer-events-auto">
|
||||
<Card.Root class="rounded-md border-none">
|
||||
<Card.Content class="p-2.5">
|
||||
{#if tool.current === Tool.ROUTING}
|
||||
<Routing
|
||||
{popup}
|
||||
{popupElement}
|
||||
bind:minimized={minimizeRoutingMenu.value}
|
||||
/>
|
||||
{:else if tool.current === Tool.SCISSORS}
|
||||
<Card.Root class="rounded-md border-none py-2.5">
|
||||
<Card.Content class="px-2.5">
|
||||
{#if $currentTool === Tool.ROUTING}
|
||||
<Routing {popup} {popupElement} bind:minimized={$minimizeRoutingMenu} />
|
||||
{:else if $currentTool === Tool.SCISSORS}
|
||||
<Scissors />
|
||||
{:else if tool.current === Tool.WAYPOINT}
|
||||
{:else if $currentTool === Tool.WAYPOINT}
|
||||
<Waypoint />
|
||||
{:else if tool.current === Tool.TIME}
|
||||
{:else if $currentTool === Tool.TIME}
|
||||
<Time />
|
||||
{:else if tool.current === Tool.MERGE}
|
||||
{:else if $currentTool === Tool.MERGE}
|
||||
<Merge />
|
||||
{:else if tool.current === Tool.ELEVATION}
|
||||
{:else if $currentTool === Tool.ELEVATION}
|
||||
<Elevation />
|
||||
{:else if tool.current === Tool.EXTRACT}
|
||||
{:else if $currentTool === Tool.EXTRACT}
|
||||
<Extract />
|
||||
{:else if tool.current === Tool.CLEAN}
|
||||
{:else if $currentTool === Tool.CLEAN}
|
||||
<Clean />
|
||||
{:else if tool.current === Tool.REDUCE}
|
||||
{:else if $currentTool === Tool.REDUCE}
|
||||
<Reduce />
|
||||
{/if}
|
||||
</Card.Content>
|
||||
@@ -73,8 +69,8 @@
|
||||
|
||||
<svelte:window
|
||||
on:keydown={(e) => {
|
||||
if (tool.current !== null && e.key === 'Escape') {
|
||||
tool.current = null;
|
||||
if ($currentTool !== null && e.key === 'Escape') {
|
||||
$currentTool = null;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -13,12 +13,13 @@
|
||||
import Help from '$lib/components/Help.svelte';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
import { Trash2 } from '@lucide/svelte';
|
||||
import { map } from '$lib/components/map/map';
|
||||
import type { GeoJSONSource } from 'mapbox-gl';
|
||||
import { selection } from '$lib/logic/selection';
|
||||
import { fileActions } from '$lib/logic/file-actions';
|
||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||
|
||||
let props: {
|
||||
class?: string;
|
||||
@@ -30,10 +31,10 @@
|
||||
let rectangleCoordinates: mapboxgl.LngLat[] = $state([]);
|
||||
|
||||
$effect(() => {
|
||||
if (map.value) {
|
||||
if ($map) {
|
||||
if (rectangleCoordinates.length != 2) {
|
||||
if (map.value.getLayer('rectangle')) {
|
||||
map.value.removeLayer('rectangle');
|
||||
if ($map.getLayer('rectangle')) {
|
||||
$map.removeLayer('rectangle');
|
||||
}
|
||||
} else {
|
||||
let data: GeoJSON.Feature = {
|
||||
@@ -52,17 +53,17 @@
|
||||
},
|
||||
properties: {},
|
||||
};
|
||||
let source: GeoJSONSource | undefined = map.value.getSource('rectangle');
|
||||
let source: GeoJSONSource | undefined = $map.getSource('rectangle');
|
||||
if (source) {
|
||||
source.setData(data);
|
||||
} else {
|
||||
map.value.addSource('rectangle', {
|
||||
$map.addSource('rectangle', {
|
||||
type: 'geojson',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
if (!map.value.getLayer('rectangle')) {
|
||||
map.value.addLayer({
|
||||
if (!$map.getLayer('rectangle')) {
|
||||
$map.addLayer({
|
||||
id: 'rectangle',
|
||||
type: 'fill',
|
||||
source: 'rectangle',
|
||||
@@ -93,39 +94,39 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (map.value) {
|
||||
setCrosshairCursor(map.value.getCanvas());
|
||||
map.value.on('mousedown', onMouseDown);
|
||||
map.value.on('mousemove', onMouseMove);
|
||||
map.value.on('mouseup', onMouseUp);
|
||||
map.value.on('touchstart', onMouseDown);
|
||||
map.value.on('touchmove', onMouseMove);
|
||||
map.value.on('touchend', onMouseUp);
|
||||
map.value.dragPan.disable();
|
||||
if ($map) {
|
||||
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, true);
|
||||
$map.on('mousedown', onMouseDown);
|
||||
$map.on('mousemove', onMouseMove);
|
||||
$map.on('mouseup', onMouseUp);
|
||||
$map.on('touchstart', onMouseDown);
|
||||
$map.on('touchmove', onMouseMove);
|
||||
$map.on('touchend', onMouseUp);
|
||||
$map.dragPan.disable();
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (map.value) {
|
||||
resetCursor(map.value.getCanvas());
|
||||
map.value.off('mousedown', onMouseDown);
|
||||
map.value.off('mousemove', onMouseMove);
|
||||
map.value.off('mouseup', onMouseUp);
|
||||
map.value.off('touchstart', onMouseDown);
|
||||
map.value.off('touchmove', onMouseMove);
|
||||
map.value.off('touchend', onMouseUp);
|
||||
map.value.dragPan.enable();
|
||||
if ($map) {
|
||||
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false);
|
||||
$map.off('mousedown', onMouseDown);
|
||||
$map.off('mousemove', onMouseMove);
|
||||
$map.off('mouseup', onMouseUp);
|
||||
$map.off('touchstart', onMouseDown);
|
||||
$map.off('touchmove', onMouseMove);
|
||||
$map.off('touchend', onMouseUp);
|
||||
$map.dragPan.enable();
|
||||
|
||||
if (map.value.getLayer('rectangle')) {
|
||||
map.value.removeLayer('rectangle');
|
||||
if ($map.getLayer('rectangle')) {
|
||||
$map.removeLayer('rectangle');
|
||||
}
|
||||
if (map.value.getSource('rectangle')) {
|
||||
map.value.removeSource('rectangle');
|
||||
if ($map.getSource('rectangle')) {
|
||||
$map.removeSource('rectangle');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let validSelection = $derived(selection.value.size > 0);
|
||||
let validSelection = $derived($selection.size > 0);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-3 w-full max-w-80 items-center {props.class ?? ''}">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
class?: string;
|
||||
} = $props();
|
||||
|
||||
let validSelection = $derived(selection.value.size > 0);
|
||||
let validSelection = $derived($selection.size > 0);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
|
||||
@@ -21,8 +21,8 @@
|
||||
class="whitespace-normal h-fit"
|
||||
disabled={!validSelection}
|
||||
onclick={async () => {
|
||||
if (map.value) {
|
||||
fileActions.addElevationToSelection(map.value);
|
||||
if ($map) {
|
||||
fileActions.addElevationToSelection($map);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
} = $props();
|
||||
|
||||
let validSelection = $derived(
|
||||
selection.value.size > 0 &&
|
||||
selection.value.getSelected().every((item) => {
|
||||
$selection.size > 0 &&
|
||||
$selection.getSelected().every((item) => {
|
||||
if (
|
||||
item instanceof ListWaypointsItem ||
|
||||
item instanceof ListWaypointItem ||
|
||||
|
||||
@@ -16,20 +16,20 @@
|
||||
import { Group } from '@lucide/svelte';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||
import { gpxStatistics } from '$lib/stores';
|
||||
import { selection } from '$lib/logic/selection';
|
||||
import { fileStateCollection } from '$lib/logic/file-state';
|
||||
import { fileActions } from '$lib/logic/file-actions';
|
||||
import { gpxStatistics } from '$lib/logic/statistics';
|
||||
|
||||
let props: {
|
||||
class?: string;
|
||||
} = $props();
|
||||
|
||||
let canMergeTraces = $derived.by(() => {
|
||||
if (selection.value.size > 1) {
|
||||
if ($selection.size > 1) {
|
||||
return true;
|
||||
} else if (selection.value.size === 1) {
|
||||
let selected = selection.value.getSelected()[0];
|
||||
} else if ($selection.size === 1) {
|
||||
let selected = $selection.getSelected()[0];
|
||||
if (selected instanceof ListFileItem) {
|
||||
let file = fileStateCollection.getFile(selected.getFileId());
|
||||
if (file) {
|
||||
@@ -47,8 +47,8 @@
|
||||
});
|
||||
|
||||
let canMergeContents = $derived(
|
||||
selection.value.size > 1 &&
|
||||
selection.value
|
||||
$selection.size > 1 &&
|
||||
$selection
|
||||
.getSelected()
|
||||
.some((item) => item instanceof ListFileItem || item instanceof ListTrackItem)
|
||||
);
|
||||
@@ -95,22 +95,14 @@
|
||||
{:else if mergeType === MergeType.TRACES && !canMergeTraces}
|
||||
{i18n._('toolbar.merge.help_cannot_merge_traces')}
|
||||
{i18n._('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[0]}
|
||||
<Shortcut
|
||||
ctrl={true}
|
||||
click={true}
|
||||
class="inline-flex text-muted-foreground text-xs border rounded p-0.5 gap-0"
|
||||
/>
|
||||
<Shortcut ctrl={true} click={true} class="border" />
|
||||
{i18n._('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[1]}
|
||||
{:else if mergeType === MergeType.CONTENTS && canMergeContents}
|
||||
{i18n._('toolbar.merge.help_merge_contents')}
|
||||
{:else if mergeType === MergeType.CONTENTS && !canMergeContents}
|
||||
{i18n._('toolbar.merge.help_cannot_merge_contents')}
|
||||
{i18n._('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[0]}
|
||||
<Shortcut
|
||||
ctrl={true}
|
||||
click={true}
|
||||
class="inline-flex text-muted-foreground text-xs border rounded p-0.5 gap-0"
|
||||
/>
|
||||
<Shortcut ctrl={true} click={true} class="border" />
|
||||
{i18n._('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[1]}
|
||||
{/if}
|
||||
</Help>
|
||||
|
||||
@@ -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>
|
||||
@@ -5,7 +5,6 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import TimePicker from '$lib/components/ui/time-picker/TimePicker.svelte';
|
||||
import { gpxStatistics } from '$lib/stores';
|
||||
import {
|
||||
distancePerHourToSecondsPerDistance,
|
||||
getConvertedVelocity,
|
||||
@@ -26,20 +25,20 @@
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
import { selection } from '$lib/logic/selection';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { fileActions } from '$lib/logic/file-actions';
|
||||
import { fileActionManager } from '$lib/logic/file-action-manager';
|
||||
import { gpxStatistics } from '$lib/logic/statistics';
|
||||
|
||||
let props: {
|
||||
class?: string;
|
||||
} = $props();
|
||||
|
||||
let startDate: DateValue | undefined = undefined;
|
||||
let startTime: string | undefined = undefined;
|
||||
let endDate: DateValue | undefined = undefined;
|
||||
let endTime: string | undefined = undefined;
|
||||
let movingTime: number | undefined = undefined;
|
||||
let speed: number | undefined = undefined;
|
||||
let artificial = false;
|
||||
let startDate: DateValue | undefined = $state(undefined);
|
||||
let startTime: string | undefined = $state(undefined);
|
||||
let endDate: DateValue | undefined = $state(undefined);
|
||||
let endTime: string | undefined = $state(undefined);
|
||||
let movingTime: number | undefined = $state(undefined);
|
||||
let speed: number | undefined = $state(undefined);
|
||||
let artificial = $state(false);
|
||||
|
||||
function toCalendarDate(date: Date): CalendarDate {
|
||||
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
||||
@@ -53,7 +52,7 @@
|
||||
|
||||
function setSpeed(value: number) {
|
||||
let speedValue = getConvertedVelocity(value);
|
||||
if (velocityUnits.value === 'speed') {
|
||||
if ($velocityUnits === 'speed') {
|
||||
speedValue = parseFloat(speedValue.toFixed(2));
|
||||
}
|
||||
speed = speedValue;
|
||||
@@ -86,9 +85,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
// $: if ($gpxStatistics && $velocityUnits && $distanceUnits) {
|
||||
// setGPXData();
|
||||
// }
|
||||
$effect(() => {
|
||||
if ($gpxStatistics && $velocityUnits && $distanceUnits) {
|
||||
setGPXData();
|
||||
}
|
||||
});
|
||||
|
||||
function getDate(date: DateValue, time: string): Date {
|
||||
if (date === undefined) {
|
||||
@@ -139,12 +140,12 @@
|
||||
}
|
||||
|
||||
let speedValue = speed;
|
||||
if (velocityUnits.value === 'pace') {
|
||||
if ($velocityUnits === 'pace') {
|
||||
speedValue = distancePerHourToSecondsPerDistance(speed);
|
||||
}
|
||||
if (distanceUnits.value === 'imperial') {
|
||||
if ($distanceUnits === 'imperial') {
|
||||
speedValue = milesToKilometers(speedValue);
|
||||
} else if (distanceUnits.value === 'nautical') {
|
||||
} else if ($distanceUnits === 'nautical') {
|
||||
speedValue = nauticalMilesToKilometers(speedValue);
|
||||
}
|
||||
return speedValue;
|
||||
@@ -178,8 +179,7 @@
|
||||
}
|
||||
|
||||
let canUpdate = $derived(
|
||||
selection.value.size === 1 &&
|
||||
selection.value.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
|
||||
$selection.size === 1 && $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -189,14 +189,14 @@
|
||||
<div class="flex flex-col gap-2 grow">
|
||||
<Label for="speed" class="flex flex-row">
|
||||
<Zap size="16" class="mr-1" />
|
||||
{#if velocityUnits.value === 'speed'}
|
||||
{#if $velocityUnits === 'speed'}
|
||||
{i18n._('quantities.speed')}
|
||||
{:else}
|
||||
{i18n._('quantities.pace')}
|
||||
{/if}
|
||||
</Label>
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
{#if velocityUnits.value === 'speed'}
|
||||
{#if $velocityUnits === 'speed'}
|
||||
<Input
|
||||
id="speed"
|
||||
type="number"
|
||||
@@ -205,13 +205,14 @@
|
||||
disabled={!canUpdate}
|
||||
bind:value={speed}
|
||||
onchange={updateDataFromSpeed}
|
||||
class="text-sm"
|
||||
/>
|
||||
<span class="text-sm shrink-0">
|
||||
{#if distanceUnits.value === 'imperial'}
|
||||
{#if $distanceUnits === 'imperial'}
|
||||
{i18n._('units.miles_per_hour')}
|
||||
{:else if distanceUnits.value === 'metric'}
|
||||
{:else if $distanceUnits === 'metric'}
|
||||
{i18n._('units.kilometers_per_hour')}
|
||||
{:else if distanceUnits.value === 'nautical'}
|
||||
{:else if $distanceUnits === 'nautical'}
|
||||
{i18n._('units.knots')}
|
||||
{/if}
|
||||
</span>
|
||||
@@ -223,11 +224,11 @@
|
||||
onChange={updateDataFromSpeed}
|
||||
/>
|
||||
<span class="text-sm shrink-0">
|
||||
{#if distanceUnits.value === 'imperial'}
|
||||
{#if $distanceUnits === 'imperial'}
|
||||
{i18n._('units.minutes_per_mile')}
|
||||
{:else if distanceUnits.value === 'metric'}
|
||||
{:else if $distanceUnits === 'metric'}
|
||||
{i18n._('units.minutes_per_kilometer')}
|
||||
{:else if distanceUnits.value === 'nautical'}
|
||||
{:else if $distanceUnits === 'nautical'}
|
||||
{i18n._('units.minutes_per_nautical_mile')}
|
||||
{/if}
|
||||
</span>
|
||||
@@ -332,7 +333,7 @@
|
||||
ratio = $gpxStatistics.global.speed.moving / effectiveSpeed;
|
||||
}
|
||||
|
||||
let item = selection.value.getSelected()[0];
|
||||
let item = $selection.getSelected()[0];
|
||||
let fileId = item.getFileId();
|
||||
fileActionManager.applyToFile(fileId, (file) => {
|
||||
if (item instanceof ListFileItem) {
|
||||
|
||||
@@ -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>
|
||||
187
website/src/lib/components/toolbar/tools/reduce/reduce.ts
Normal file
187
website/src/lib/components/toolbar/tools/reduce/reduce.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,8 @@
|
||||
SquareArrowUpLeft,
|
||||
SquareArrowOutDownRight,
|
||||
} 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 { RoutingControls } from './RoutingControls';
|
||||
import { slide } from 'svelte/transition';
|
||||
import {
|
||||
ListFileItem,
|
||||
@@ -32,14 +31,16 @@
|
||||
ListTrackSegmentItem,
|
||||
type ListItem,
|
||||
} 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 { TrackPoint } from 'gpx';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
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 { fileActions, getFileIds, newGPXFile } from '$lib/logic/file-actions';
|
||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||
import { RoutingControls, routingControls } from './RoutingControls';
|
||||
|
||||
let {
|
||||
minimized = $bindable(false),
|
||||
@@ -55,34 +56,9 @@
|
||||
class?: string;
|
||||
} = $props();
|
||||
|
||||
let selectedItem: ListItem | null = null;
|
||||
|
||||
const { privateRoads, routing, routingProfile } = settings;
|
||||
|
||||
// $: if (map && popup && popupElement) {
|
||||
// // 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 fileStateCollectionObserver: GPXFileStateCollectionObserver;
|
||||
|
||||
let validSelection = $derived(
|
||||
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
|
||||
@@ -101,36 +77,61 @@
|
||||
]);
|
||||
file._data.id = getFileIds(1)[0];
|
||||
fileActions.add(file);
|
||||
// selectFileWhenLoaded(file._data.id);
|
||||
selection.selectFileWhenLoaded(file._data.id);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// setCrosshairCursor();
|
||||
$map?.on('click', createFileWithPoint);
|
||||
if ($map && popup && popupElement) {
|
||||
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(() => {
|
||||
// resetCursor();
|
||||
$map?.off('click', createFileWithPoint);
|
||||
if ($map) {
|
||||
if (fileStateCollectionObserver) {
|
||||
fileStateCollectionObserver.destroy();
|
||||
}
|
||||
|
||||
// routingControls.forEach((controls) => controls.destroy());
|
||||
// routingControls.clear();
|
||||
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false);
|
||||
$map.off('click', createFileWithPoint);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if minimizable && minimized}
|
||||
<div class="-m-1.5 -mb-2">
|
||||
<Button variant="ghost" class="px-1 h-[26px]" onclick={() => (minimized = false)}>
|
||||
<SquareArrowOutDownRight size="18" />
|
||||
<Button variant="ghost" size="icon-sm" class="size-6" onclick={() => (minimized = false)}>
|
||||
<SquareArrowOutDownRight size="18" class="size-4.5" />
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-3 w-full max-w-80 animate-in animate-out {className ?? ''}">
|
||||
<div class="flex flex-col gap-3">
|
||||
<Label class="flex flex-row justify-between items-center gap-2">
|
||||
<Label class="justify-between">
|
||||
<span class="flex flex-row items-center gap-1">
|
||||
{#if routing.value}
|
||||
{#if $routing}
|
||||
<Route size="16" />
|
||||
{:else}
|
||||
<RouteOff size="16" />
|
||||
@@ -138,28 +139,30 @@
|
||||
{i18n._('toolbar.routing.use_routing')}
|
||||
</span>
|
||||
<Tooltip label={i18n._('toolbar.routing.use_routing_tooltip')}>
|
||||
<Switch class="scale-90" bind:checked={routing.value} />
|
||||
<Shortcut slot="extra" key="F5" />
|
||||
<Switch bind:checked={$routing} />
|
||||
{#snippet extra()}
|
||||
<Shortcut key="F5" />
|
||||
{/snippet}
|
||||
</Tooltip>
|
||||
</Label>
|
||||
{#if routing.value}
|
||||
{#if $routing}
|
||||
<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">
|
||||
{#if routingProfile.value.includes('bike') || routingProfile.value.includes('motorcycle')}
|
||||
{#if $routingProfile.includes('bike') || $routingProfile.includes('motorcycle')}
|
||||
<Bike size="16" />
|
||||
{:else if routingProfile.value.includes('foot')}
|
||||
{:else if $routingProfile.includes('foot')}
|
||||
<Footprints size="16" />
|
||||
{:else if routingProfile.value.includes('water')}
|
||||
{:else if $routingProfile.includes('water')}
|
||||
<Waves size="16" />
|
||||
{:else if routingProfile.value.includes('railway')}
|
||||
{:else if $routingProfile.includes('railway')}
|
||||
<TrainFront size="16" />
|
||||
{/if}
|
||||
{i18n._('toolbar.routing.activity')}
|
||||
</span>
|
||||
<Select.Root type="single" bind:value={routingProfile.value}>
|
||||
<Select.Root type="single" bind:value={$routingProfile}>
|
||||
<Select.Trigger class="h-8 grow">
|
||||
{i18n._(`toolbar.routing.activities.${routingProfile.value}`)}
|
||||
{i18n._(`toolbar.routing.activities.${$routingProfile}`)}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each Object.keys(brouterProfiles) as profile}
|
||||
@@ -172,12 +175,12 @@
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Label>
|
||||
<Label class="flex flex-row justify-between items-center gap-2">
|
||||
<Label class="justify-between">
|
||||
<span class="flex flex-row gap-1">
|
||||
<TriangleAlert size="16" />
|
||||
{i18n._('toolbar.routing.allow_private')}
|
||||
</span>
|
||||
<Switch class="scale-90" bind:checked={privateRoads.value} />
|
||||
<Switch bind:checked={$privateRoads} />
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -186,7 +189,7 @@
|
||||
<ButtonWithTooltip
|
||||
label={i18n._('toolbar.routing.reverse.tooltip')}
|
||||
variant="outline"
|
||||
class="flex flex-row gap-1 text-xs px-2"
|
||||
class="gap-1 text-xs"
|
||||
disabled={!validSelection}
|
||||
onclick={fileActions.reverseSelection}
|
||||
>
|
||||
@@ -195,7 +198,7 @@
|
||||
<ButtonWithTooltip
|
||||
label={i18n._('toolbar.routing.route_back_to_start.tooltip')}
|
||||
variant="outline"
|
||||
class="flex flex-row gap-1 text-xs px-2"
|
||||
class="gap-1 text-xs"
|
||||
disabled={!validSelection}
|
||||
onclick={() => {
|
||||
const selected = selection.getOrderedSelection();
|
||||
@@ -218,9 +221,9 @@
|
||||
|
||||
if (start !== undefined) {
|
||||
const lastFileId = selected[selected.length - 1].getFileId();
|
||||
// routingControls
|
||||
// .get(lastFileId)
|
||||
// ?.appendAnchorWithCoordinates(start.getCoordinates());
|
||||
routingControls
|
||||
.get(lastFileId)
|
||||
?.appendAnchorWithCoordinates(start.getCoordinates());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,7 +234,7 @@
|
||||
<ButtonWithTooltip
|
||||
label={i18n._('toolbar.routing.round_trip.tooltip')}
|
||||
variant="outline"
|
||||
class="flex flex-row gap-1 text-xs px-2"
|
||||
class="gap-1 text-xs"
|
||||
disabled={!validSelection}
|
||||
onclick={fileActions.createRoundTripForSelection}
|
||||
>
|
||||
@@ -248,7 +251,8 @@
|
||||
</Help>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="px-1 h-6"
|
||||
size="icon-sm"
|
||||
class="size-6"
|
||||
onclick={() => {
|
||||
if (minimizable) {
|
||||
minimized = true;
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
|
||||
export let element: HTMLElement;
|
||||
let {
|
||||
element = $bindable(),
|
||||
}: {
|
||||
element: HTMLElement | undefined;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div bind:this={element} class="hidden">
|
||||
@@ -17,7 +21,7 @@
|
||||
<Button
|
||||
class="w-full px-2 py-1 h-6 justify-start"
|
||||
variant="ghost"
|
||||
onclick={() => element.dispatchEvent(new CustomEvent('change-start'))}
|
||||
onclick={() => element?.dispatchEvent(new CustomEvent('change-start'))}
|
||||
>
|
||||
<CirclePlay size="16" class="mr-1" />
|
||||
{i18n._('toolbar.routing.start_loop_here')}
|
||||
@@ -26,7 +30,7 @@
|
||||
<Button
|
||||
class="w-full px-2 py-1 h-6 justify-start"
|
||||
variant="ghost"
|
||||
onclick={() => element.dispatchEvent(new CustomEvent('delete'))}
|
||||
onclick={() => element?.dispatchEvent(new CustomEvent('delete'))}
|
||||
>
|
||||
<Trash2 size="16" class="mr-1" />
|
||||
{i18n._('menu.delete')}
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from 'gpx';
|
||||
import { get, writable, type Readable } from 'svelte/store';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { route } from './utils.svelte';
|
||||
import { route } from './routing';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import {
|
||||
ListFileItem,
|
||||
ListTrackItem,
|
||||
ListTrackSegmentItem,
|
||||
} 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 { 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);
|
||||
|
||||
function stopPropagation(e: any) {
|
||||
@@ -20,7 +28,6 @@ function stopPropagation(e: any) {
|
||||
|
||||
export class RoutingControls {
|
||||
active: boolean = false;
|
||||
map: mapboxgl.Map;
|
||||
fileId: string = '';
|
||||
file: Readable<GPXFileWithStatistics | undefined>;
|
||||
anchors: AnchorWithMarker[] = [];
|
||||
@@ -39,13 +46,11 @@ export class RoutingControls {
|
||||
appendAnchorBinded: (e: mapboxgl.MapMouseEvent) => void = this.appendAnchor.bind(this);
|
||||
|
||||
constructor(
|
||||
map: mapboxgl.Map,
|
||||
fileId: string,
|
||||
file: Readable<GPXFileWithStatistics | undefined>,
|
||||
popup: mapboxgl.Popup,
|
||||
popupElement: HTMLElement
|
||||
) {
|
||||
this.map = map;
|
||||
this.fileId = fileId;
|
||||
this.file = file;
|
||||
this.popup = popup;
|
||||
@@ -88,12 +93,17 @@ export class RoutingControls {
|
||||
}
|
||||
|
||||
add() {
|
||||
const map_ = get(map);
|
||||
if (!map_) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.active = true;
|
||||
|
||||
this.map.on('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
|
||||
this.map.on('click', this.appendAnchorBinded);
|
||||
this.map.on('mousemove', this.fileId, this.showTemporaryAnchorBinded);
|
||||
this.map.on('click', this.fileId, stopPropagation);
|
||||
map_.on('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
|
||||
map_.on('click', this.appendAnchorBinded);
|
||||
map_.on('mousemove', this.fileId, this.showTemporaryAnchorBinded);
|
||||
map_.on('click', this.fileId, stopPropagation);
|
||||
|
||||
this.fileUnsubscribe = this.file.subscribe(this.updateControls.bind(this));
|
||||
}
|
||||
@@ -141,25 +151,26 @@ export class RoutingControls {
|
||||
}
|
||||
|
||||
remove() {
|
||||
const map_ = get(map);
|
||||
if (!map_) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.active = false;
|
||||
|
||||
for (let anchor of this.anchors) {
|
||||
anchor.marker.remove();
|
||||
}
|
||||
this.map.off('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
|
||||
this.map.off('click', this.appendAnchorBinded);
|
||||
this.map.off('mousemove', this.fileId, this.showTemporaryAnchorBinded);
|
||||
this.map.off('click', this.fileId, stopPropagation);
|
||||
this.map.off('mousemove', this.updateTemporaryAnchorBinded);
|
||||
map_.off('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
|
||||
map_.off('click', this.appendAnchorBinded);
|
||||
map_.off('mousemove', this.fileId, this.showTemporaryAnchorBinded);
|
||||
map_.off('click', this.fileId, stopPropagation);
|
||||
map_.off('mousemove', this.updateTemporaryAnchorBinded);
|
||||
this.temporaryAnchor.marker.remove();
|
||||
|
||||
this.fileUnsubscribe();
|
||||
}
|
||||
|
||||
updateMap(map: mapboxgl.Map) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
createAnchor(
|
||||
point: TrackPoint,
|
||||
segment: TrackSegment,
|
||||
@@ -186,13 +197,13 @@ export class RoutingControls {
|
||||
|
||||
marker.on('dragstart', (e) => {
|
||||
this.lastDragEvent = Date.now();
|
||||
setGrabbingCursor();
|
||||
mapCursor.notify(MapCursorState.TRACKPOINT_DRAGGING, true);
|
||||
element.classList.remove('cursor-pointer');
|
||||
element.classList.add('cursor-grabbing');
|
||||
});
|
||||
marker.on('dragend', (e) => {
|
||||
this.lastDragEvent = Date.now();
|
||||
resetCursor();
|
||||
mapCursor.notify(MapCursorState.TRACKPOINT_DRAGGING, false);
|
||||
element.classList.remove('cursor-grabbing');
|
||||
element.classList.add('cursor-pointer');
|
||||
this.moveAnchor(anchor);
|
||||
@@ -255,19 +266,24 @@ export class RoutingControls {
|
||||
}
|
||||
|
||||
toggleAnchorsForZoomLevelAndBounds() {
|
||||
const map_ = get(map);
|
||||
if (!map_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show markers only if they are in the current zoom level and bounds
|
||||
this.shownAnchors.splice(0, this.shownAnchors.length);
|
||||
|
||||
let center = this.map.getCenter();
|
||||
let bottomLeft = this.map.unproject([0, this.map.getCanvas().height]);
|
||||
let topRight = this.map.unproject([this.map.getCanvas().width, 0]);
|
||||
let center = map_.getCenter();
|
||||
let bottomLeft = map_.unproject([0, map_.getCanvas().height]);
|
||||
let topRight = map_.unproject([map_.getCanvas().width, 0]);
|
||||
let diagonal = bottomLeft.distanceTo(topRight);
|
||||
|
||||
let zoom = this.map.getZoom();
|
||||
let zoom = map_.getZoom();
|
||||
this.anchors.forEach((anchor) => {
|
||||
anchor.inZoom = anchor.point._data.zoom <= zoom;
|
||||
if (anchor.inZoom && center.distanceTo(anchor.marker.getLngLat()) < diagonal) {
|
||||
anchor.marker.addTo(this.map);
|
||||
anchor.marker.addTo(map_);
|
||||
this.shownAnchors.push(anchor);
|
||||
} else {
|
||||
anchor.marker.remove();
|
||||
@@ -276,6 +292,11 @@ export class RoutingControls {
|
||||
}
|
||||
|
||||
showTemporaryAnchor(e: any) {
|
||||
const map_ = get(map);
|
||||
if (!map_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) {
|
||||
// Do not not change the source point if it is already being dragged
|
||||
return;
|
||||
@@ -305,25 +326,30 @@ export class RoutingControls {
|
||||
lat: e.lngLat.lat,
|
||||
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) {
|
||||
const map_ = get(map);
|
||||
if (!map_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
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)
|
||||
) {
|
||||
// Hide if too far from the layer
|
||||
this.temporaryAnchor.marker.remove();
|
||||
this.map.off('mousemove', this.updateTemporaryAnchorBinded);
|
||||
map_.off('mousemove', this.updateTemporaryAnchorBinded);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -331,8 +357,13 @@ export class RoutingControls {
|
||||
}
|
||||
|
||||
temporaryAnchorCloseToOtherAnchor(e: any) {
|
||||
const map_ = get(map);
|
||||
if (!map_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -482,7 +513,7 @@ export class RoutingControls {
|
||||
});
|
||||
|
||||
if (minInfo.trackIndex !== -1) {
|
||||
dbUtils.applyToFile(this.fileId, (file) =>
|
||||
fileActionManager.applyToFile(this.fileId, (file) =>
|
||||
file.replaceTrackPoints(
|
||||
minInfo.trackIndex,
|
||||
minInfo.segmentIndex,
|
||||
@@ -506,12 +537,12 @@ export class RoutingControls {
|
||||
|
||||
if (previousAnchor === null && nextAnchor === null) {
|
||||
// Only one point, remove it
|
||||
dbUtils.applyToFile(this.fileId, (file) =>
|
||||
fileActionManager.applyToFile(this.fileId, (file) =>
|
||||
file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, 0, [])
|
||||
);
|
||||
} else if (previousAnchor === null) {
|
||||
// First point, remove trackpoints until nextAnchor
|
||||
dbUtils.applyToFile(this.fileId, (file) =>
|
||||
fileActionManager.applyToFile(this.fileId, (file) =>
|
||||
file.replaceTrackPoints(
|
||||
anchor.trackIndex,
|
||||
anchor.segmentIndex,
|
||||
@@ -522,7 +553,7 @@ export class RoutingControls {
|
||||
);
|
||||
} else if (nextAnchor === null) {
|
||||
// Last point, remove trackpoints from previousAnchor
|
||||
dbUtils.applyToFile(this.fileId, (file) => {
|
||||
fileActionManager.applyToFile(this.fileId, (file) => {
|
||||
let segment = file.getSegment(anchor.trackIndex, anchor.segmentIndex);
|
||||
file.replaceTrackPoints(
|
||||
anchor.trackIndex,
|
||||
@@ -558,7 +589,7 @@ export class RoutingControls {
|
||||
).global.speed.moving;
|
||||
|
||||
let segment = anchor.segment;
|
||||
dbUtils.applyToFile(this.fileId, (file) => {
|
||||
fileActionManager.applyToFile(this.fileId, (file) => {
|
||||
file.replaceTrackPoints(
|
||||
anchor.trackIndex,
|
||||
anchor.segmentIndex,
|
||||
@@ -590,7 +621,7 @@ export class RoutingControls {
|
||||
|
||||
async appendAnchorWithCoordinates(coordinates: Coordinates) {
|
||||
// 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) {
|
||||
return;
|
||||
}
|
||||
@@ -605,7 +636,7 @@ export class RoutingControls {
|
||||
newPoint._data.zoom = 0;
|
||||
|
||||
if (!lastAnchor) {
|
||||
dbUtils.applyToFile(this.fileId, (file) => {
|
||||
fileActionManager.applyToFile(this.fileId, (file) => {
|
||||
let trackIndex = file.trk.length > 0 ? file.trk.length - 1 : 0;
|
||||
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
|
||||
trackIndex = item.getTrackIndex();
|
||||
@@ -686,7 +717,7 @@ export class RoutingControls {
|
||||
|
||||
if (anchors.length === 1) {
|
||||
// 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, [
|
||||
new TrackPoint({
|
||||
attributes: targetCoordinates[0],
|
||||
@@ -701,13 +732,13 @@ export class RoutingControls {
|
||||
response = await route(targetCoordinates);
|
||||
} catch (e: any) {
|
||||
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')) {
|
||||
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')) {
|
||||
toast.error(get(_)('toolbar.routing.error.to'));
|
||||
toast.error(i18n._('toolbar.routing.error.to'));
|
||||
} else if (e.message.includes('Time-out')) {
|
||||
toast.error(get(_)('toolbar.routing.error.timeout'));
|
||||
toast.error(i18n._('toolbar.routing.error.timeout'));
|
||||
} else {
|
||||
toast.error(e.message);
|
||||
}
|
||||
@@ -797,7 +828,7 @@ export class RoutingControls {
|
||||
}
|
||||
}
|
||||
|
||||
dbUtils.applyToFile(this.fileId, (file) =>
|
||||
fileActionManager.applyToFile(this.fileId, (file) =>
|
||||
file.replaceTrackPoints(
|
||||
anchors[0].trackIndex,
|
||||
anchors[0].segmentIndex,
|
||||
@@ -818,6 +849,8 @@ export class RoutingControls {
|
||||
}
|
||||
}
|
||||
|
||||
export const routingControls: Map<string, RoutingControls> = new Map();
|
||||
|
||||
type Anchor = {
|
||||
segment: TrackSegment;
|
||||
trackIndex: number;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Coordinates } from 'gpx';
|
||||
import { TrackPoint, distance } from 'gpx';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { getElevation } from '$lib/utils';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
const { routing, routingProfile, privateRoads } = settings;
|
||||
|
||||
@@ -17,8 +18,8 @@ export const brouterProfiles: { [key: string]: string } = {
|
||||
};
|
||||
|
||||
export function route(points: Coordinates[]): Promise<TrackPoint[]> {
|
||||
if (routing.value) {
|
||||
return getRoute(points, brouterProfiles[routingProfile.value], privateRoads.value);
|
||||
if (get(routing)) {
|
||||
return getRoute(points, brouterProfiles[get(routingProfile)], get(privateRoads));
|
||||
} else {
|
||||
return getIntermediatePoints(points);
|
||||
}
|
||||
@@ -7,16 +7,16 @@
|
||||
import { Slider } from '$lib/components/ui/slider';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import { gpxStatistics, slicedGPXStatistics } from '$lib/stores';
|
||||
import { map } from '$lib/components/map/map';
|
||||
import { get } from 'svelte/store';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { onDestroy, tick } from 'svelte';
|
||||
import { Crop } from '@lucide/svelte';
|
||||
import { dbUtils } from '$lib/db';
|
||||
import { SplitControls } from './split-controls';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
import { selection } from '$lib/logic/selection';
|
||||
import { fileActions } from '$lib/logic/file-actions';
|
||||
import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
|
||||
|
||||
let props: {
|
||||
class?: string;
|
||||
@@ -26,16 +26,16 @@
|
||||
let canCrop = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (map.current) {
|
||||
if ($map) {
|
||||
if (splitControls) {
|
||||
splitControls.destroy();
|
||||
}
|
||||
splitControls = new SplitControls(map.current);
|
||||
splitControls = new SplitControls($map);
|
||||
}
|
||||
});
|
||||
|
||||
let validSelection = $derived(
|
||||
selection.value.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
|
||||
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
|
||||
$gpxStatistics.local.points.length > 0
|
||||
);
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
<Button
|
||||
variant="outline"
|
||||
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')}
|
||||
</Button>
|
||||
@@ -129,9 +129,9 @@
|
||||
<span class="shrink-0">
|
||||
{i18n._('toolbar.scissors.split_as')}
|
||||
</span>
|
||||
<Select.Root bind:value={splitAs.current} type="single">
|
||||
<Select.Root bind:value={$splitAs} type="single">
|
||||
<Select.Trigger class="h-8 w-fit grow">
|
||||
{i18n._('gpx.' + splitAs)}
|
||||
{i18n._('gpx.' + $splitAs)}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each Object.values(SplitType) as splitType}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { TrackPoint, TrackSegment } from 'gpx';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { dbUtils, getFile } from '$lib/db';
|
||||
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
|
||||
import { gpxStatistics } from '$lib/stores';
|
||||
import { tool, Tool } from '$lib/components/toolbar/tools';
|
||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
||||
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
|
||||
import { Scissors } from 'lucide-static';
|
||||
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 {
|
||||
active: boolean = false;
|
||||
@@ -22,13 +24,12 @@ export class SplitControls {
|
||||
this.map = map;
|
||||
|
||||
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
|
||||
$effect(() => {
|
||||
tool.current, selection.value, this.addIfNeeded.bind(this);
|
||||
});
|
||||
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
|
||||
this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this)));
|
||||
}
|
||||
|
||||
addIfNeeded() {
|
||||
let scissors = tool.current === Tool.SCISSORS;
|
||||
let scissors = get(currentTool) === Tool.SCISSORS;
|
||||
if (!scissors) {
|
||||
if (this.active) {
|
||||
this.remove();
|
||||
@@ -54,12 +55,12 @@ export class SplitControls {
|
||||
// Update the markers when the files change
|
||||
let controlIndex = 0;
|
||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||
let file = getFile(fileId);
|
||||
let file = fileStateCollection.getFile(fileId);
|
||||
|
||||
if (file) {
|
||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||
if (
|
||||
selection.value.hasAnyParent(
|
||||
get(selection).hasAnyParent(
|
||||
new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
|
||||
)
|
||||
) {
|
||||
@@ -163,8 +164,8 @@ export class SplitControls {
|
||||
|
||||
marker.getElement().addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
dbUtils.split(
|
||||
splitAs.current,
|
||||
fileActions.split(
|
||||
get(splitAs),
|
||||
control.fileId,
|
||||
control.trackIndex,
|
||||
control.segmentIndex,
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
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 { onDestroy, onMount } from 'svelte';
|
||||
import { map } from '$lib/stores';
|
||||
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||
import { onDestroy, onMount, untrack } from 'svelte';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
import { MapPin, CircleX, Save } from '@lucide/svelte';
|
||||
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
||||
import { selection } from '$lib/logic/selection';
|
||||
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: {
|
||||
class?: string;
|
||||
@@ -24,20 +24,46 @@
|
||||
let name = $state('');
|
||||
let description = $state('');
|
||||
let link = $state('');
|
||||
let symbolKey = $state('');
|
||||
let sym = $state('');
|
||||
let longitude = $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() {
|
||||
name = '';
|
||||
description = '';
|
||||
link = '';
|
||||
symbolKey = '';
|
||||
longitude = 0;
|
||||
latitude = 0;
|
||||
}
|
||||
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 = '';
|
||||
description = '';
|
||||
link = '';
|
||||
sym = '';
|
||||
longitude = 0;
|
||||
latitude = 0;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function createOrUpdateWaypoint() {
|
||||
if (typeof latitude === 'string') {
|
||||
@@ -49,7 +75,7 @@
|
||||
latitude = parseFloat(latitude.toFixed(6));
|
||||
longitude = parseFloat(longitude.toFixed(6));
|
||||
|
||||
dbUtils.addOrUpdateWaypoint(
|
||||
fileActions.addOrUpdateWaypoint(
|
||||
{
|
||||
attributes: {
|
||||
lat: latitude,
|
||||
@@ -59,7 +85,7 @@
|
||||
desc: description.length > 0 ? description : undefined,
|
||||
cmt: description.length > 0 ? description : undefined,
|
||||
link: link.length > 0 ? { attributes: { href: link } } : undefined,
|
||||
sym: symbols[symbolKey]?.value ?? '',
|
||||
sym: sym,
|
||||
},
|
||||
selectedWaypoint.wpt && selectedWaypoint.fileId
|
||||
? new ListWaypointItem(selectedWaypoint.fileId, selectedWaypoint.wpt._data.index)
|
||||
@@ -67,7 +93,6 @@
|
||||
);
|
||||
|
||||
selectedWaypoint.reset();
|
||||
resetWaypointData();
|
||||
}
|
||||
|
||||
function setCoordinates(e: any) {
|
||||
@@ -75,22 +100,18 @@
|
||||
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(() => {
|
||||
map.value?.on('click', setCoordinates);
|
||||
// setCrosshairCursor();
|
||||
if ($map) {
|
||||
$map.on('click', setCoordinates);
|
||||
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, true);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
map.value?.off('click', setCoordinates);
|
||||
// resetCursor();
|
||||
if ($map) {
|
||||
$map.off('click', setCoordinates);
|
||||
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -101,25 +122,25 @@
|
||||
bind:value={name}
|
||||
id="name"
|
||||
class="font-semibold h-8"
|
||||
disabled={!canCreate && !selectedWaypoint.wpt}
|
||||
disabled={!canCreate && !$selectedWaypoint}
|
||||
/>
|
||||
<Label for="description">{i18n._('menu.metadata.description')}</Label>
|
||||
<Textarea
|
||||
bind:value={description}
|
||||
id="description"
|
||||
disabled={!canCreate && !selectedWaypoint.wpt}
|
||||
disabled={!canCreate && !$selectedWaypoint}
|
||||
/>
|
||||
<Label for="symbol">{i18n._('toolbar.waypoint.icon')}</Label>
|
||||
<Select.Root bind:value={symbolKey} type="single">
|
||||
<Select.Root bind:value={sym} type="single">
|
||||
<Select.Trigger
|
||||
id="symbol"
|
||||
class="w-full h-8"
|
||||
disabled={!canCreate && !selectedWaypoint.wpt}
|
||||
disabled={!canCreate && !$selectedWaypoint}
|
||||
>
|
||||
{#if symbolKey in symbols}
|
||||
{#if symbolKey}
|
||||
{i18n._(`gpx.symbol.${symbolKey}`)}
|
||||
{:else}
|
||||
{symbolKey}
|
||||
{sym}
|
||||
{/if}
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-60 overflow-y-scroll">
|
||||
@@ -127,11 +148,8 @@
|
||||
<Select.Item value={symbol.value}>
|
||||
<span>
|
||||
{#if symbol.icon}
|
||||
<svelte:component
|
||||
this={symbol.icon}
|
||||
size="14"
|
||||
class="inline-block align-sub mr-0.5"
|
||||
/>
|
||||
{@const Component = symbol.icon}
|
||||
<Component size="14" class="inline-block align-sub mr-0.5" />
|
||||
{:else}
|
||||
<span class="w-4 inline-block"></span>
|
||||
{/if}
|
||||
@@ -146,7 +164,7 @@
|
||||
bind:value={link}
|
||||
id="link"
|
||||
class="h-8"
|
||||
disabled={!canCreate && !selectedWaypoint.wpt}
|
||||
disabled={!canCreate && !$selectedWaypoint}
|
||||
/>
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="grow">
|
||||
@@ -159,7 +177,7 @@
|
||||
min={-90}
|
||||
max={90}
|
||||
class="text-xs h-8"
|
||||
disabled={!canCreate && !selectedWaypoint.wpt}
|
||||
disabled={!canCreate && !$selectedWaypoint}
|
||||
/>
|
||||
</div>
|
||||
<div class="grow">
|
||||
@@ -172,7 +190,7 @@
|
||||
min={-180}
|
||||
max={180}
|
||||
class="text-xs h-8"
|
||||
disabled={!canCreate && !selectedWaypoint.wpt}
|
||||
disabled={!canCreate && !$selectedWaypoint}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,11 +198,11 @@
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={!canCreate && !selectedWaypoint.wpt}
|
||||
disabled={!canCreate && !$selectedWaypoint}
|
||||
class="grow whitespace-normal h-fit"
|
||||
onclick={createOrUpdateWaypoint}
|
||||
>
|
||||
{#if selectedWaypoint.wpt}
|
||||
{#if $selectedWaypoint}
|
||||
<Save size="16" class="mr-1 shrink-0" />
|
||||
{i18n._('menu.metadata.save')}
|
||||
{:else}
|
||||
@@ -192,18 +210,12 @@
|
||||
{i18n._('toolbar.waypoint.create')}
|
||||
{/if}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
selectedWaypoint.reset();
|
||||
resetWaypointData();
|
||||
}}
|
||||
>
|
||||
<Button variant="outline" onclick={() => selectedWaypoint.reset()}>
|
||||
<CircleX size="16" />
|
||||
</Button>
|
||||
</div>
|
||||
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/poi')}>
|
||||
{#if selectedWaypoint.wpt || canCreate}
|
||||
{#if $selectedWaypoint || canCreate}
|
||||
{i18n._('toolbar.waypoint.help')}
|
||||
{:else}
|
||||
{i18n._('toolbar.waypoint.help_no_selection')}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { get, writable, type Writable } from 'svelte/store';
|
||||
|
||||
export class WaypointSelection {
|
||||
private _selection: Writable<[Waypoint, string] | undefined>;
|
||||
private _fileUnsubscribe: (() => void) | undefined;
|
||||
|
||||
constructor() {
|
||||
this._selection = writable(undefined);
|
||||
@@ -18,15 +19,37 @@ 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() {
|
||||
if (this._fileUnsubscribe) {
|
||||
this._fileUnsubscribe();
|
||||
this._fileUnsubscribe = undefined;
|
||||
}
|
||||
this._selection.update(() => {
|
||||
if (get(settings.treeFileView) && get(selection).size === 1) {
|
||||
let item = get(selection).getSelected()[0];
|
||||
if (item instanceof ListWaypointItem) {
|
||||
let file = fileStateCollection.getFile(item.getFileId());
|
||||
let waypoint = file?.wpt[item.getWaypointIndex()];
|
||||
if (waypoint) {
|
||||
return [waypoint, item.getFileId()];
|
||||
let fileState = fileStateCollection.getFileState(item.getFileId());
|
||||
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) {
|
||||
return [waypoint, item.getFileId()];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,34 +70,6 @@ export class WaypointSelection {
|
||||
const selection = get(this._selection);
|
||||
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();
|
||||
|
||||
@@ -1,80 +1,82 @@
|
||||
<script lang="ts" module>
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||
import { type VariantProps, tv } from 'tailwind-variants';
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
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",
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
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',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
});
|
||||
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",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
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",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
let {
|
||||
class: className,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = 'button',
|
||||
disabled,
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
let {
|
||||
class: className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = "button",
|
||||
disabled,
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
href={disabled ? undefined : href}
|
||||
aria-disabled={disabled}
|
||||
role={disabled ? 'link' : undefined}
|
||||
tabindex={disabled ? -1 : undefined}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
<a
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
href={disabled ? undefined : href}
|
||||
aria-disabled={disabled}
|
||||
role={disabled ? "link" : undefined}
|
||||
tabindex={disabled ? -1 : undefined}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{disabled}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
<button
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{disabled}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -1,39 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { CalendarIcon } from '@lucide/svelte';
|
||||
import CalendarIcon from '@lucide/svelte/icons/calendar';
|
||||
import { DateFormatter, type DateValue, getLocalTimeZone } from '@internationalized/date';
|
||||
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 * as Popover from '$lib/components/ui/popover/index.js';
|
||||
|
||||
export let value: DateValue | undefined = undefined;
|
||||
export let placeholder: string = 'Pick a date';
|
||||
export let locale = 'en';
|
||||
export let disabled: boolean = false;
|
||||
export let onValueChange: any;
|
||||
let {
|
||||
value = $bindable<DateValue | undefined>(),
|
||||
placeholder = 'Pick a date',
|
||||
disabled = false,
|
||||
locale,
|
||||
class: className = '',
|
||||
}: {
|
||||
value?: DateValue;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
locale: string;
|
||||
class?: string;
|
||||
} = $props();
|
||||
|
||||
const df = new DateFormatter(locale, {
|
||||
dateStyle: 'long',
|
||||
});
|
||||
|
||||
let contentRef = $state<HTMLElement | null>(null);
|
||||
</script>
|
||||
|
||||
<Popover.Root>
|
||||
<Popover.Trigger asChild let:builder>
|
||||
<Button
|
||||
variant="outline"
|
||||
class={cn(
|
||||
'w-[280px] justify-start text-left font-normal',
|
||||
!value && 'text-muted-foreground',
|
||||
$$props.class
|
||||
)}
|
||||
{disabled}
|
||||
builders={[builder]}
|
||||
>
|
||||
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||
{value ? df.format(value.toDate(getLocalTimeZone())) : placeholder}
|
||||
</Button>
|
||||
<Popover.Trigger
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
variant: 'outline',
|
||||
class: 'justify-start text-left font-normal',
|
||||
}),
|
||||
!value && 'text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{disabled}
|
||||
>
|
||||
<CalendarIcon />
|
||||
{value ? df.format(value.toDate(getLocalTimeZone())) : placeholder}
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="w-auto p-0">
|
||||
<Calendar bind:value initialFocus {locale} {onValueChange} />
|
||||
<Popover.Content bind:ref={contentRef} class="w-auto p-0">
|
||||
<Calendar type="single" captionLayout="dropdown" bind:value />
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
{@render children?.()}
|
||||
{#if showCloseButton}
|
||||
<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 />
|
||||
<span class="sr-only">Close</span>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
data-slot="dropdown-menu-content"
|
||||
{sideOffset}
|
||||
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
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
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
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
type,
|
||||
files = $bindable(),
|
||||
class: className,
|
||||
"data-slot": dataSlot = "input",
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
</script>
|
||||
@@ -22,9 +23,9 @@
|
||||
{#if type === "file"}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot="input"
|
||||
data-slot={dataSlot}
|
||||
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]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
@@ -37,7 +38,7 @@
|
||||
{:else}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot="input"
|
||||
data-slot={dataSlot}
|
||||
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",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
|
||||
10
website/src/lib/components/ui/kbd/index.ts
Normal file
10
website/src/lib/components/ui/kbd/index.ts
Normal 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,
|
||||
};
|
||||
10
website/src/lib/components/ui/kbd/kbd-group.svelte
Normal file
10
website/src/lib/components/ui/kbd/kbd-group.svelte
Normal 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>
|
||||
19
website/src/lib/components/ui/kbd/kbd.svelte
Normal file
19
website/src/lib/components/ui/kbd/kbd.svelte
Normal 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>
|
||||
@@ -1,40 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { ScrollArea as ScrollAreaPrimitive } from 'bits-ui';
|
||||
import { Scrollbar } from './index.js';
|
||||
import { cn, type WithoutChild } from '$lib/utils.js';
|
||||
import { ScrollArea as ScrollAreaPrimitive } from "bits-ui";
|
||||
import { Scrollbar } from "./index.js";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
orientation = 'vertical',
|
||||
scrollbarXClasses = '',
|
||||
scrollbarYClasses = '',
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<ScrollAreaPrimitive.RootProps> & {
|
||||
orientation?: 'vertical' | 'horizontal' | 'both' | undefined;
|
||||
scrollbarXClasses?: string | undefined;
|
||||
scrollbarYClasses?: string | undefined;
|
||||
} = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
orientation = "vertical",
|
||||
scrollbarXClasses = "",
|
||||
scrollbarYClasses = "",
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChild<ScrollAreaPrimitive.RootProps> & {
|
||||
orientation?: "vertical" | "horizontal" | "both" | undefined;
|
||||
scrollbarXClasses?: string | undefined;
|
||||
scrollbarYClasses?: string | undefined;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<ScrollAreaPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="scroll-area"
|
||||
class={cn('relative overflow-hidden', className)}
|
||||
{...restProps}
|
||||
bind:ref
|
||||
data-slot="scroll-area"
|
||||
class={cn("relative", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
class="ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:outline-1 focus-visible:ring-4"
|
||||
>
|
||||
{@render children?.()}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
{#if orientation === 'vertical' || orientation === 'both'}
|
||||
<Scrollbar orientation="vertical" class={scrollbarYClasses} />
|
||||
{/if}
|
||||
{#if orientation === 'horizontal' || orientation === 'both'}
|
||||
<Scrollbar orientation="horizontal" class={scrollbarXClasses} />
|
||||
{/if}
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
class="ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:outline-1 focus-visible:ring-4"
|
||||
>
|
||||
{@render children?.()}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
{#if orientation === "vertical" || orientation === "both"}
|
||||
<Scrollbar orientation="vertical" class={scrollbarYClasses} />
|
||||
{/if}
|
||||
{#if orientation === "horizontal" || orientation === "both"}
|
||||
<Scrollbar orientation="horizontal" class={scrollbarXClasses} />
|
||||
{/if}
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
"data-slot": dataSlot = "separator",
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SeparatorPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="separator"
|
||||
data-slot={dataSlot}
|
||||
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",
|
||||
className
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
<Sonner
|
||||
theme={mode.current}
|
||||
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}
|
||||
/>
|
||||
|
||||
@@ -6,13 +6,14 @@
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
class: className,
|
||||
"data-slot": dataSlot = "textarea",
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLTextareaAttributes>> = $props();
|
||||
</script>
|
||||
|
||||
<textarea
|
||||
bind:this={ref}
|
||||
data-slot="textarea"
|
||||
data-slot={dataSlot}
|
||||
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",
|
||||
className
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
class={cn(
|
||||
"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=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=left]:translate-y-[calc(50%_-_3px)]",
|
||||
"data-[side=left]:-translate-y-[calc(50%_-_3px)]",
|
||||
arrowClasses
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minify
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minimitzar
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Zjednodušit
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minify
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minimieren
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minify
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minify
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minimizar
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Txikiagotu
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minify
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minifier
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minify
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minimalizálás
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minimizza
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minify
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minify
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minify
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Verkleinen
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minify
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minify
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minificar
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minify
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minify
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minify
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minify
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minimera
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Küçült
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minify
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Minify
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: 精简 GPS 点数量
|
||||
|
||||
<script>
|
||||
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';
|
||||
</script>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -151,6 +151,7 @@ export class GPXFileStateCollectionObserver {
|
||||
private _onFileAdded: GPXFileStateCallback;
|
||||
private _onFileRemoved: (fileId: string) => void;
|
||||
private _onDestroy: () => void;
|
||||
private _unsubscribe: () => void;
|
||||
|
||||
constructor(
|
||||
onFileAdded: GPXFileStateCallback,
|
||||
@@ -162,7 +163,7 @@ export class GPXFileStateCollectionObserver {
|
||||
this._onFileRemoved = onFileRemoved;
|
||||
this._onDestroy = onDestroy;
|
||||
|
||||
fileStateCollection.subscribe((files) => {
|
||||
this._unsubscribe = fileStateCollection.subscribe((files) => {
|
||||
this._fileIds.forEach((fileId) => {
|
||||
if (!files.has(fileId)) {
|
||||
this._onFileRemoved(fileId);
|
||||
@@ -180,5 +181,6 @@ export class GPXFileStateCollectionObserver {
|
||||
|
||||
destroy() {
|
||||
this._onDestroy();
|
||||
this._unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
55
website/src/lib/logic/map-cursor.ts
Normal file
55
website/src/lib/logic/map-cursor.ts
Normal 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();
|
||||
@@ -14,6 +14,7 @@ import { settings } from '$lib/logic/settings';
|
||||
import type { GPXFile } from 'gpx';
|
||||
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
||||
import { SelectionTreeType } from '$lib/logic/selection-tree';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
export class Selection {
|
||||
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[]) {
|
||||
this._selection.update(($selection) => {
|
||||
$selection.clear();
|
||||
|
||||
@@ -7,6 +7,9 @@ import {
|
||||
ListWaypointsItem,
|
||||
} from '$lib/components/file-list/file-list';
|
||||
import { get, writable, type Writable } from 'svelte/store';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
|
||||
const { fileOrder } = settings;
|
||||
|
||||
export class SelectedGPXStatistics {
|
||||
private _statistics: Writable<GPXStatistics>;
|
||||
@@ -22,6 +25,7 @@ export class SelectedGPXStatistics {
|
||||
this._statistics = writable(new GPXStatistics());
|
||||
this._files = new Map();
|
||||
selection.subscribe(() => this.update());
|
||||
fileOrder.subscribe(() => this.update());
|
||||
}
|
||||
|
||||
subscribe(run: (value: GPXStatistics) => void, invalidate?: (value?: GPXStatistics) => void) {
|
||||
|
||||
@@ -24,17 +24,6 @@
|
||||
|
||||
// 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 function updateAllHidden() {
|
||||
|
||||
@@ -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() {
|
||||
return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import DocsContainer from '$lib/components/docs/DocsContainer.svelte';
|
||||
import Logo from '$lib/components/Logo.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 {
|
||||
BookOpenText,
|
||||
@@ -20,7 +20,7 @@
|
||||
import { exampleGPXFile } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
// 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';
|
||||
|
||||
let {
|
||||
@@ -38,19 +38,19 @@
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable<'slope' | 'surface' | undefined>(undefined);
|
||||
|
||||
// onMount(() => {
|
||||
// tool.current = Tool.SCISSORS;
|
||||
// });
|
||||
onMount(() => {
|
||||
$currentTool = Tool.SCISSORS;
|
||||
});
|
||||
|
||||
// $effect(() => {
|
||||
// if (tool.current !== Tool.SCISSORS) {
|
||||
// tool.current = Tool.SCISSORS;
|
||||
// }
|
||||
// });
|
||||
$effect(() => {
|
||||
if ($currentTool !== Tool.SCISSORS) {
|
||||
$currentTool = Tool.SCISSORS;
|
||||
}
|
||||
});
|
||||
|
||||
// onDestroy(() => {
|
||||
// tool.current = null;
|
||||
// });
|
||||
onDestroy(() => {
|
||||
$currentTool = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="space-y-24 my-24">
|
||||
@@ -199,12 +199,12 @@
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="h-10 w-fit">
|
||||
<!-- <GPXStatistics
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
panelSize={192}
|
||||
orientation={'horizontal'}
|
||||
/> -->
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
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 GPXStatistics from '$lib/components/GPXStatistics.svelte';
|
||||
import Map from '$lib/components/map/Map.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 LayerControl from '$lib/components/map/layer-control/LayerControl.svelte';
|
||||
// import CoordinatesPopup from '$lib/components/map/CoordinatesPopup.svelte';
|
||||
@@ -101,7 +101,7 @@
|
||||
<div
|
||||
class="absolute top-0 bottom-0 left-0 z-20 flex flex-col justify-center pointer-events-none"
|
||||
>
|
||||
<!-- <Toolbar /> -->
|
||||
<Toolbar />
|
||||
</div>
|
||||
<Map class="h-full {$treeFileView ? '' : 'horizontal'}" />
|
||||
<StreetViewControl />
|
||||
@@ -133,14 +133,14 @@
|
||||
panelSize={$bottomPanelSize}
|
||||
orientation={$elevationProfile ? 'vertical' : 'horizontal'}
|
||||
/>
|
||||
<!-- {#if $elevationProfile}
|
||||
{#if $elevationProfile}
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
bind:additionalDatasets={$additionalDatasets}
|
||||
bind:elevationFill={$elevationFill}
|
||||
/>
|
||||
{/if} -->
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if $treeFileView}
|
||||
|
||||
Reference in New Issue
Block a user