style: add eslint and prettier
Some checks failed
TrafficCue CI / check (push) Successful in 26s
TrafficCue CI / build (push) Has been cancelled

This commit is contained in:
Cfp
2025-06-22 17:53:32 +02:00
parent 16c0f0c399
commit f2348873fd
100 changed files with 5110 additions and 7344 deletions

1
.gitignore vendored
View File

@ -15,6 +15,7 @@ dist-ssr
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
.idea
.DS_Store
*.suo

2
.prettierignore Normal file
View File

@ -0,0 +1,2 @@
android/
dist/

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"useTabs": true,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"eslint.validate": ["javascript", "typescript", "svelte"]
}

View File

@ -10,7 +10,7 @@
**TrafficCue** is a FOSS navigation app built for vehicles that max out at 45 km/h, so mopeds, microcars, LEVs, and the like (but not limited to them!).
Mainstream navigation often routes these vehicles onto roads they're not legally allowed to use. **We dont.**
Built with **Svelte + Vite**, powered by **Capacitor** for mobile, and focused on *respectful routing*, **TrafficCue** fills the gap.
Built with **Svelte + Vite**, powered by **Capacitor** for mobile, and focused on _respectful routing_, **TrafficCue** fills the gap.
---

259
bun.lock
View File

@ -7,8 +7,13 @@
"@capacitor/android": "^7.3.0",
"@capacitor/cli": "^7.3.0",
"@capacitor/core": "^7.3.0",
"@eslint/js": "^9.29.0",
"eslint": "^9.29.0",
"eslint-plugin-svelte": "^3.9.3",
"globals": "^16.2.0",
"opening_hours": "^3.8.0",
"svelte-maplibre-gl": "^0.1.8",
"typescript-eslint": "^8.34.1",
},
"devDependencies": {
"@internationalized/date": "^3.8.1",
@ -19,13 +24,15 @@
"@types/node": "^22.15.24",
"bits-ui": "^2.7.0",
"clsx": "^2.1.1",
"svelte": "^5.28.1",
"eslint-config-prettier": "^10.1.5",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.34.7",
"svelte-check": "^4.1.6",
"tailwind-merge": "^3.0.2",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.0",
"tw-animate-css": "^1.3.2",
"typescript": "~5.8.3",
"typescript": "^5.8.3",
"vaul-svelte": "^1.0.0-next.7",
"vite": "^6.3.5",
},
@ -98,12 +105,38 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
"@eslint/config-array": ["@eslint/config-array@0.20.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.2.3", "", {}, "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg=="],
"@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
"@eslint/js": ["@eslint/js@9.29.0", "", {}, "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.2", "", { "dependencies": { "@eslint/core": "^0.15.0", "levn": "^0.4.1" } }, "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg=="],
"@floating-ui/core": ["@floating-ui/core@1.7.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.0", "", { "dependencies": { "@floating-ui/core": "^1.7.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@internationalized/date": ["@internationalized/date@3.8.1", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-PgVE6B6eIZtzf9Gu5HvJxRK3ufUFz9DhspELuhW/N0GuMGMTLvPQNRkHP2hTuP9lblOk+f+1xi96sPiPXANXAA=="],
"@ionic/cli-framework-output": ["@ionic/cli-framework-output@2.2.8", "", { "dependencies": { "@ionic/utils-terminal": "2.3.5", "debug": "^4.0.0", "tslib": "^2.0.1" } }, "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g=="],
@ -184,6 +217,12 @@
"@math.gl/web-mercator": ["@math.gl/web-mercator@4.1.0", "", { "dependencies": { "@math.gl/core": "4.1.0" } }, "sha512-HZo3vO5GCMkXJThxRJ5/QYUYRr3XumfT8CzNNCwoJfinxy5NtKUd7dusNTXn7yJ40UoB8FMIwkVwNlqaiRZZAw=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@probe.gl/env": ["@probe.gl/env@4.1.0", "", {}, "sha512-5ac2Jm2K72VCs4eSMsM7ykVRrV47w32xOGMvcgqn8vQdEMF9PRXyBGYEV9YbqRKWNKpNKmQJVi4AHM/fkCxs9w=="],
"@probe.gl/log": ["@probe.gl/log@4.1.0", "", { "dependencies": { "@probe.gl/env": "4.1.0" } }, "sha512-r4gRReNY6f+OZEMgfWEXrAE2qJEt8rX0HsDJQXUBMoc+5H47bdB7f/5HBHAmapK8UydwPKL9wCDoS22rJ0yq7Q=="],
@ -278,6 +317,8 @@
"@types/geojson-vt": ["@types/geojson-vt@3.2.5", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/mapbox__point-geometry": ["@types/mapbox__point-geometry@0.1.4", "", {}, "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA=="],
"@types/mapbox__vector-tile": ["@types/mapbox__vector-tile@1.3.4", "", { "dependencies": { "@types/geojson": "*", "@types/mapbox__point-geometry": "*", "@types/pbf": "*" } }, "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg=="],
@ -292,14 +333,40 @@
"@types/supercluster": ["@types/supercluster@7.1.3", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.34.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.34.1", "@typescript-eslint/type-utils": "8.34.1", "@typescript-eslint/utils": "8.34.1", "@typescript-eslint/visitor-keys": "8.34.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.34.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.34.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.34.1", "@typescript-eslint/types": "8.34.1", "@typescript-eslint/typescript-estree": "8.34.1", "@typescript-eslint/visitor-keys": "8.34.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.34.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.34.1", "@typescript-eslint/types": "^8.34.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.34.1", "", { "dependencies": { "@typescript-eslint/types": "8.34.1", "@typescript-eslint/visitor-keys": "8.34.1" } }, "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.34.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.34.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.34.1", "@typescript-eslint/utils": "8.34.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.34.1", "", {}, "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.34.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.34.1", "@typescript-eslint/tsconfig-utils": "8.34.1", "@typescript-eslint/types": "8.34.1", "@typescript-eslint/visitor-keys": "8.34.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.34.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.34.1", "@typescript-eslint/types": "8.34.1", "@typescript-eslint/typescript-estree": "8.34.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.34.1", "", { "dependencies": { "@typescript-eslint/types": "8.34.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw=="],
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.10", "", {}, "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw=="],
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
"astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="],
@ -318,10 +385,16 @@
"bplist-parser": ["bplist-parser@0.3.2", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ=="],
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
@ -334,12 +407,18 @@
"commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
@ -360,9 +439,41 @@
"esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.29.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.1", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.29.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ=="],
"eslint-config-prettier": ["eslint-config-prettier@10.1.5", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw=="],
"eslint-plugin-svelte": ["eslint-plugin-svelte@3.9.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.6.1", "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", "globals": "^16.0.0", "known-css-properties": "^0.37.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", "svelte-eslint-parser": "^1.2.0" }, "peerDependencies": { "eslint": "^8.57.1 || ^9.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-PlcyK80sqAZ43IITeZkgl3zPFWJytx/Joup9iKGqIOsXM2m3pWfPbWuXPr5PN3loXFEypqTY/JyZwNqlSpSvRw=="],
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
"esrap": ["esrap@1.4.6", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw=="],
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
"esrap": ["esrap@1.4.9", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
@ -370,6 +481,16 @@
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="],
@ -386,16 +507,30 @@
"glob": ["glob@11.0.2", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^4.0.1", "minimatch": "^10.0.0", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"global-prefix": ["global-prefix@4.0.0", "", { "dependencies": { "ini": "^4.1.3", "kind-of": "^6.0.3", "which": "^4.0.0" } }, "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA=="],
"globals": ["globals@16.2.0", "", {}, "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"i18next": ["i18next@21.10.0", "", { "dependencies": { "@babel/runtime": "^7.17.2" } }, "sha512-YeuIBmFsGjUfO3qBmMOc0rQaun4mIpGKET5WDwvu8lU7gvwpcariZLNtL0Fzj+zazcHUrlXHiptcFhBMFaxzfg=="],
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@6.1.8", "", { "dependencies": { "@babel/runtime": "^7.19.0" } }, "sha512-Svm+MduCElO0Meqpj1kJAriTC6OhI41VhlT/A0UPjGoPZBhAHIaGE5EfsHlTpgdH09UVX7rcc72pSDDBeKSQQA=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="],
@ -404,8 +539,14 @@
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
@ -416,16 +557,30 @@
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"json-stringify-pretty-compact": ["json-stringify-pretty-compact@4.0.0", "", {}, "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q=="],
"jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
"kdbush": ["kdbush@4.0.2", "", {}, "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"known-css-properties": ["known-css-properties@0.37.0", "", {}, "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
@ -448,8 +603,14 @@
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"lru-cache": ["lru-cache@11.1.0", "", {}, "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A=="],
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
@ -458,7 +619,11 @@
"maplibre-gl": ["maplibre-gl@5.5.0", "", { "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", "@mapbox/point-geometry": "^0.1.0", "@mapbox/tiny-sdf": "^2.0.6", "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^1.3.1", "@mapbox/whoots-js": "^3.1.0", "@maplibre/maplibre-gl-style-spec": "^23.2.2", "@types/geojson": "^7946.0.16", "@types/geojson-vt": "3.2.5", "@types/mapbox__point-geometry": "^0.1.4", "@types/mapbox__vector-tile": "^1.3.4", "@types/pbf": "^3.0.5", "@types/supercluster": "^7.1.3", "earcut": "^3.0.1", "geojson-vt": "^4.0.2", "gl-matrix": "^3.4.3", "global-prefix": "^4.0.0", "kdbush": "^4.0.2", "murmurhash-js": "^1.0.0", "pbf": "^3.3.0", "potpack": "^2.0.0", "quickselect": "^3.0.0", "supercluster": "^8.0.1", "tinyqueue": "^3.0.0", "vt-pbf": "^3.1.3" } }, "sha512-p8AOPuzzqn1ZA9gcXxKw0IED715we/2Owa/YUr6PANmgMvNMe/JG+V/C1hRra43Wm62Biz+Aa8AgbOLJimA8tA=="],
"minimatch": ["minimatch@10.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
@ -480,12 +645,24 @@
"native-run": ["native-run@2.0.1", "", { "dependencies": { "@ionic/utils-fs": "^3.1.7", "@ionic/utils-terminal": "^2.3.4", "bplist-parser": "^0.3.2", "debug": "^4.3.4", "elementtree": "^0.1.7", "ini": "^4.1.1", "plist": "^3.1.0", "split2": "^4.2.0", "through2": "^4.0.2", "tslib": "^2.6.2", "yauzl": "^2.10.0" }, "bin": { "native-run": "bin/native-run" } }, "sha512-XfG1FBZLM50J10xH9361whJRC9SHZ0Bub4iNRhhI61C8Jv0e1ud19muex6sNKB51ibQNUJNuYn25MuYET/rE6w=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
"opening_hours": ["opening_hours@3.8.0", "", { "dependencies": { "i18next": "^21.8.3", "i18next-browser-languagedetector": "^6.1.4", "suncalc": "^1.9.0" } }, "sha512-bRJroECQSe/itVcNmC3j9PPicxn/LBowdd1Hi+4Aa7hCswdt7w81WHfUwrEMbtk1BBYmGJEbSepl8oYYPviSuA=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-scurry": ["path-scurry@2.0.0", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg=="],
@ -504,24 +681,48 @@
"postcss": ["postcss@8.5.4", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w=="],
"postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="],
"postcss-safe-parser": ["postcss-safe-parser@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A=="],
"postcss-scss": ["postcss-scss@4.0.9", "", { "peerDependencies": { "postcss": "^8.4.29" } }, "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A=="],
"postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
"potpack": ["potpack@2.0.0", "", {}, "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
"prettier-plugin-svelte": ["prettier-plugin-svelte@3.4.0", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ=="],
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
"protocol-buffers-schema": ["protocol-buffers-schema@3.6.0", "", {}, "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"quickselect": ["quickselect@3.0.0", "", {}, "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"resolve-protobuf-schema": ["resolve-protobuf-schema@2.1.0", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rimraf": ["rimraf@6.0.1", "", { "dependencies": { "glob": "^11.0.0", "package-json-from-dist": "^1.0.0" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A=="],
"rollup": ["rollup@4.41.1", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.1", "@rollup/rollup-android-arm64": "4.41.1", "@rollup/rollup-darwin-arm64": "4.41.1", "@rollup/rollup-darwin-x64": "4.41.1", "@rollup/rollup-freebsd-arm64": "4.41.1", "@rollup/rollup-freebsd-x64": "4.41.1", "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", "@rollup/rollup-linux-arm-musleabihf": "4.41.1", "@rollup/rollup-linux-arm64-gnu": "4.41.1", "@rollup/rollup-linux-arm64-musl": "4.41.1", "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-musl": "4.41.1", "@rollup/rollup-linux-s390x-gnu": "4.41.1", "@rollup/rollup-linux-x64-gnu": "4.41.1", "@rollup/rollup-linux-x64-musl": "4.41.1", "@rollup/rollup-win32-arm64-msvc": "4.41.1", "@rollup/rollup-win32-ia32-msvc": "4.41.1", "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="],
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
@ -558,16 +759,22 @@
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="],
"suncalc": ["suncalc@1.9.0", "", {}, "sha512-vMJ8Byp1uIPoj+wb9c1AdK4jpkSKVAywgHX0lqY7zt6+EWRRC3Z+0Ucfjy/0yxTVO1hwwchZe4uoFNqrIC24+A=="],
"supercluster": ["supercluster@8.0.1", "", { "dependencies": { "kdbush": "^4.0.2" } }, "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ=="],
"svelte": ["svelte@5.33.7", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^1.4.6", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-qQYkNHE3L26ahV4VvW4DhEHOrAT6LfZHjIIW7htZs2bFP0/ayep/t4RvA6Km7Yosxo0nR+o2vZHf99553l4ZtQ=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"svelte": ["svelte@5.34.7", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^1.4.8", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-5PEg+QQKce4t1qiOtVUhUS3AQRTtxJyGBTpxLcNWnr0Ve8q4r06bMo0Gv8uhtCPWlztZHoi3Ye7elLhu+PCTMg=="],
"svelte-check": ["svelte-check@4.2.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-e49SU1RStvQhoipkQ/aonDhHnG3qxHSBtNfBRb9pxVXoa+N7qybAo32KgA9wEb2PCYFNaDg7bZCdhLD1vHpdYA=="],
"svelte-eslint-parser": ["svelte-eslint-parser@1.2.0", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-mbPtajIeuiyU80BEyGvwAktBeTX7KCr5/0l+uRGLq1dafwRNrjfM5kHGJScEBlPG3ipu6dJqfW/k0/fujvIEVw=="],
"svelte-maplibre-gl": ["svelte-maplibre-gl@0.1.8", "", { "peerDependencies": { "@deck.gl/core": "^9.1.0", "@deck.gl/layers": "^9.1.0", "@deck.gl/mapbox": "^9.1.0", "maplibre-contour": ">=0.1.0", "maplibre-gl": "^5.0.0 || ^4.0.0", "pmtiles": "^4.0.0", "svelte": ">=5.0.0", "terra-draw": "^1.0.0", "terra-draw-maplibre-gl-adapter": "^1.0.3" } }, "sha512-YvMo25q/rpNDNE4iBvOuYYt+E+6jT+PBLxX7vR20LE5ZD2K3cLV9cR34S4SX7w81E00lP7InD2+CvFr7T0vBxg=="],
"svelte-toolbelt": ["svelte-toolbelt@0.9.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.28.0", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-wBX6MtYw/kpht80j5zLpxJyR9soLizXPIAIWEVd9llAi17SR44ZdG291bldjB7r/K5duC0opDFcuhk2cA1hb8g=="],
@ -594,20 +801,30 @@
"tinyqueue": ["tinyqueue@3.0.0", "", {}, "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tw-animate-css": ["tw-animate-css@1.3.2", "", {}, "sha512-khGYcg4sHWFWcjpiWvy0KN0Bd6yVy6Ecc4r9ZP2u7FV+n4/Fp8MQscCWJkM0KMIRvrpGyKpIQnIbEd1hrewdeg=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"typescript-eslint": ["typescript-eslint@8.34.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.34.1", "@typescript-eslint/parser": "8.34.1", "@typescript-eslint/utils": "8.34.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"untildify": ["untildify@4.0.0", "", {}, "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vaul-svelte": ["vaul-svelte@1.0.0-next.7", "", { "dependencies": { "runed": "^0.23.2", "svelte-toolbelt": "^0.7.1" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-7zN7Bi3dFQixvvbUJY9uGDe7Ws/dGZeBQR2pXdXmzQiakjrxBvWo0QrmsX3HK+VH+SZOltz378cmgmCS9f9rSg=="],
@ -622,6 +839,8 @@
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
@ -632,10 +851,22 @@
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.15.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw=="],
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
"@ionic/utils-fs/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
@ -660,18 +891,28 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"elementtree/sax": ["sax@1.1.4", "", {}, "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"glob/minimatch": ["minimatch@10.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ=="],
"glob/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"global-prefix/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
"maplibre-gl/earcut": ["earcut@3.0.1", "", {}, "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"path-scurry/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
@ -702,6 +943,10 @@
"@tailwindcss/oxide/tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"global-prefix/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
}
}

View File

@ -1,9 +1,9 @@
import type { CapacitorConfig } from '@capacitor/cli';
import type { CapacitorConfig } from "@capacitor/cli";
const config: CapacitorConfig = {
appId: 'de.trafficcue.trafficcue',
appName: 'TrafficCue',
webDir: 'dist'
appId: "de.trafficcue.trafficcue",
appName: "TrafficCue",
webDir: "dist",
};
export default config;

55
eslint.config.mjs Normal file
View File

@ -0,0 +1,55 @@
// @ts-check
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import eslintConfigPrettier from "eslint-config-prettier/flat";
import { globalIgnores } from "eslint/config";
import svelte from "eslint-plugin-svelte";
import globals from "globals";
import svelteConfig from "./svelte.config.js";
export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.recommended,
tseslint.configs.stylistic,
...svelte.configs.recommended,
eslintConfigPrettier,
{
rules: {
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
},
},
[globalIgnores(["./android", "./dist"])],
{
languageOptions: {
globals: {
...globals.browser,
},
},
},
{
files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: [".svelte"], // Add support for additional file extensions, such as .svelte
parser: tseslint.parser,
// We recommend importing and specifying svelte.config.js.
// By doing so, some rules in eslint-plugin-svelte will automatically read the configuration and adjust their behavior accordingly.
// While certain Svelte settings may be statically loaded from svelte.config.js even if you dont specify it,
// explicitly specifying it ensures better compatibility and functionality.
svelteConfig,
},
},
rules: {
"no-undef": "off",
},
},
);

View File

@ -10,13 +10,15 @@
"@types/node": "^22.15.24",
"bits-ui": "^2.7.0",
"clsx": "^2.1.1",
"svelte": "^5.28.1",
"eslint-config-prettier": "^10.1.5",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.34.7",
"svelte-check": "^4.1.6",
"tailwind-merge": "^3.0.2",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.0",
"tw-animate-css": "^1.3.2",
"typescript": "~5.8.3",
"typescript": "^5.8.3",
"vaul-svelte": "^1.0.0-next.7",
"vite": "^6.3.5"
},
@ -32,7 +34,12 @@
"@capacitor/android": "^7.3.0",
"@capacitor/cli": "^7.3.0",
"@capacitor/core": "^7.3.0",
"@eslint/js": "^9.29.0",
"eslint": "^9.29.0",
"eslint-plugin-svelte": "^3.9.3",
"globals": "^16.2.0",
"opening_hours": "^3.8.0",
"svelte-maplibre-gl": "^0.1.8"
"svelte-maplibre-gl": "^0.1.8",
"typescript-eslint": "^8.34.1"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,5 @@
<script lang="ts">
import "./app.css";
import { GeolocateControl, Hash, MapLibre } from "svelte-maplibre-gl";
import Sidebar from "./lib/components/lnv/Sidebar.svelte";
import { onMount } from "svelte";
import Map from "$lib/components/lnv/Map.svelte";
@ -9,8 +8,10 @@
import RoutingInfo from "$lib/components/lnv/RoutingInfo.svelte";
onMount(() => {
if(!checkWebGL()) {
alert("WebGL is not supported in your browser. Please try a different browser.");
if (!checkWebGL()) {
alert(
"WebGL is not supported in your browser. Please try a different browser.",
);
return;
}
});

View File

@ -5,22 +5,22 @@
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1.0000 0 0);
--background: oklch(1 0 0);
--foreground: oklch(0.2644 0 0);
--card: oklch(1.0000 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.2644 0 0);
--popover: oklch(1.0000 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.2644 0 0);
--primary: oklch(0.3261 0 0);
--primary-foreground: oklch(0.9886 0 0);
--secondary: oklch(0.9772 0 0);
--secondary-foreground: oklch(0.3261 0 0);
--muted: oklch(0.9772 0 0);
--muted-foreground: oklch(0.6460 0 0);
--muted-foreground: oklch(0.646 0 0);
--accent: oklch(0.9772 0 0);
--accent-foreground: oklch(0.3261 0 0);
--destructive: oklch(0.6201 0.2092 25.7747);
--destructive-foreground: oklch(1.0000 0 0);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.9404 0 0);
--input: oklch(0.9404 0 0);
--ring: oklch(0.7716 0 0);
@ -37,17 +37,26 @@
--sidebar-accent-foreground: oklch(0.3261 0 0);
--sidebar-border: oklch(0.9404 0 0);
--sidebar-ring: oklch(0.7716 0 0);
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-sans:
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--font-mono:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--radius: 0.625rem;
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-sm:
0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow-md:
0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1);
--shadow-lg:
0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1);
--shadow-xl:
0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
}
@ -59,7 +68,7 @@
--popover: oklch(0.1405 0.0044 285.8238);
--popover-foreground: oklch(0.9848 0 0);
--primary: oklch(0.5111 0.2152 266.7098);
--primary-foreground: oklch(1.0000 0 0);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.2741 0.0055 286.0329);
--secondary-foreground: oklch(0.9848 0 0);
--muted: oklch(0.2741 0.0055 286.0329);
@ -67,34 +76,43 @@
--accent: oklch(0.5111 0.2152 266.7098);
--accent-foreground: oklch(0.9848 0 0);
--destructive: oklch(0.3959 0.1331 25.7205);
--destructive-foreground: oklch(0.9710 0.0127 17.3758);
--destructive-foreground: oklch(0.971 0.0127 17.3758);
--border: oklch(0.2741 0.0055 286.0329);
--input: oklch(0.2741 0.0055 286.0329);
--ring: oklch(0.8709 0.0055 286.2853);
--chart-1: oklch(0.5292 0.1931 262.1292);
--chart-2: oklch(0.6983 0.1337 165.4626);
--chart-3: oklch(0.7232 0.1500 60.6307);
--chart-3: oklch(0.7232 0.15 60.6307);
--chart-4: oklch(0.6192 0.2037 312.7283);
--chart-5: oklch(0.6123 0.2093 6.3856);
--sidebar: oklch(0.2103 0.0059 285.8835);
--sidebar-foreground: oklch(0.9676 0.0013 286.3752);
--sidebar-primary: oklch(0.4878 0.2170 264.3876);
--sidebar-primary-foreground: oklch(1.0000 0 0);
--sidebar-primary: oklch(0.4878 0.217 264.3876);
--sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.2741 0.0055 286.0329);
--sidebar-accent-foreground: oklch(0.9676 0.0013 286.3752);
--sidebar-border: oklch(0.2741 0.0055 286.0329);
--sidebar-ring: oklch(0.8709 0.0055 286.2853);
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-sans:
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--font-mono:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--radius: 0.625rem;
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-sm:
0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 1px 2px -1px hsl(0 0% 0% / 0.1);
--shadow-md:
0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 2px 4px -1px hsl(0 0% 0% / 0.1);
--shadow-lg:
0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 4px 6px -1px hsl(0 0% 0% / 0.1);
--shadow-xl:
0 1px 3px 0px hsl(0 0% 0% / 0.1), 0 8px 10px -1px hsl(0 0% 0% / 0.1);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
}

View File

@ -1,4 +1,17 @@
import { BriefcaseMedicalIcon, CarIcon, ChefHatIcon, CroissantIcon, DrillIcon, FlameIcon, FuelIcon, HamburgerIcon, PackageIcon, SchoolIcon, SquareParkingIcon, StoreIcon } from "@lucide/svelte";
import {
BriefcaseMedicalIcon,
CarIcon,
ChefHatIcon,
CroissantIcon,
DrillIcon,
FlameIcon,
FuelIcon,
HamburgerIcon,
PackageIcon,
SchoolIcon,
SquareParkingIcon,
StoreIcon,
} from "@lucide/svelte";
import type { Component } from "svelte";
export const POIIcons: Record<string, Component> = {
@ -14,5 +27,5 @@ export const POIIcons: Record<string, Component> = {
"shop=kiosk": StoreIcon,
"amenity=restaurant": ChefHatIcon,
"amenity=fast_food": HamburgerIcon,
"shop=bakery": CroissantIcon
"shop=bakery": CroissantIcon,
};

View File

@ -1,8 +1,22 @@
<script lang="ts">
import * as Drawer from "$lib/components/ui/drawer/index.js";
import { BikeIcon, CarIcon, PlusCircleIcon, SaveIcon, TractorIcon, TruckIcon, XIcon } from "@lucide/svelte";
import {
BikeIcon,
CarIcon,
SaveIcon,
TractorIcon,
TruckIcon,
XIcon,
} from "@lucide/svelte";
import Button, { buttonVariants } from "../ui/button/button.svelte";
import { DefaultVehicle, isValidFuel, selectVehicle, setVehicles, vehicles, type Vehicle, type VehicleType } from "$lib/vehicles/vehicles.svelte";
import {
isValidFuel,
selectVehicle,
setVehicles,
vehicles,
type Vehicle,
type VehicleType,
} from "$lib/vehicles/vehicles.svelte";
import type { Snippet } from "svelte";
import * as Select from "../ui/select";
import Input from "../ui/input/input.svelte";
@ -36,12 +50,14 @@
actualMaxSpeed: 45,
emissionClass: "euro_5",
fuelType: "diesel",
preferredFuel: "Diesel"
preferredFuel: "Diesel",
});
</script>
<Drawer.Root bind:open={open}>
<Drawer.Trigger class={buttonVariants({ variant: "secondary", class: "w-full p-5" })}>
<Drawer.Root bind:open>
<Drawer.Trigger
class={buttonVariants({ variant: "secondary", class: "w-full p-5" })}
>
{@render children()}
</Drawer.Trigger>
<Drawer.Content>
@ -54,7 +70,11 @@
<Select.Trigger class="w-[180px]">
{@const Icon = getVehicleIcon(vehicle.type)}
<Icon />
{vehicle.type === "car" ? "Car" : vehicle.type === "motor_scooter" ? "Moped" : "?"}
{vehicle.type === "car"
? "Car"
: vehicle.type === "motor_scooter"
? "Moped"
: "?"}
</Select.Trigger>
<Select.Content>
<Select.Item value="car">
@ -110,7 +130,11 @@
<div class="flex gap-2">
<Select.Root type="single" bind:value={vehicle.fuelType}>
<Select.Trigger class="w-full">
{vehicle.fuelType === "diesel" ? "Diesel" : vehicle.fuelType === "petrol" ? "Petrol" : "Electric"}
{vehicle.fuelType === "diesel"
? "Diesel"
: vehicle.fuelType === "petrol"
? "Petrol"
: "Electric"}
</Select.Trigger>
<Select.Content>
<Select.Item value="diesel">Diesel</Select.Item>
@ -137,7 +161,8 @@
</div>
</div>
<Drawer.Footer>
<Button onclick={() => {
<Button
onclick={() => {
open = false;
if (vehicle.name.trim() === "") {
alert("Please enter a vehicle name.");
@ -147,20 +172,24 @@
alert("Please enter valid speeds.");
return;
}
if(!isValidFuel(vehicle)) {
if (!isValidFuel(vehicle)) {
alert("Please select a valid fuel type and preferred fuel.");
return;
}
setVehicles([...vehicles, vehicle]);
selectVehicle(vehicle);
location.reload(); // TODO
}}>
}}
>
<SaveIcon />
Save
</Button>
<Button variant="secondary" onclick={() => {
<Button
variant="secondary"
onclick={() => {
open = false;
}}>
}}
>
<XIcon />
Cancel
</Button>

View File

@ -3,7 +3,7 @@
import * as Select from "../ui/select";
</script>
{#each EVConnectors as connector}
{#each EVConnectors as connector (connector)}
<Select.Item value={connector}>
{connector}
</Select.Item>

View File

@ -34,9 +34,7 @@
let value = $state("");
let triggerRef = $state<HTMLButtonElement>(null!);
const selectedValue = $derived(
value === "location" ? "My Location" : value
);
const selectedValue = $derived(value === "location" ? "My Location" : value);
// We want to refocus the trigger button when the user selects
// an item from the list so users can continue navigating the
@ -51,7 +49,7 @@
<Popover.Root bind:open>
<Popover.Trigger bind:ref={triggerRef}>
{#snippet child({ props }: { props: Record<string, any> })}
{#snippet child({ props }: { props: Record<string, unknown> })}
<Button
variant="outline"
class="justify-between"
@ -71,7 +69,7 @@
<Command.Empty>No location found.</Command.Empty>
<Command.Group>
<Command.Item
value={"location"}
value="location"
onSelect={() => {
value = "location";
closeAndFocusTrigger();
@ -80,14 +78,14 @@
<CheckIcon
class={cn(
"mr-2 size-4",
value !== "location" && "text-transparent"
value !== "location" && "text-transparent",
)}
/>
My Location
</Command.Item>
</Command.Group>
<Command.Group>
{#each frameworks as framework}
{#each frameworks as framework (framework.value)}
<Command.Item
value={framework.value}
onSelect={() => {
@ -98,7 +96,7 @@
<CheckIcon
class={cn(
"mr-2 size-4",
value !== framework.value && "text-transparent"
value !== framework.value && "text-transparent",
)}
/>
{framework.label}

View File

@ -6,7 +6,7 @@
let name = $derived(maneuverTypes[maneuver] || "none");
</script>
<img src="/img/maneuver/{name}.svg" alt={name}>
<img src="/img/maneuver/{name}.svg" alt={name} />
<style>
img {

View File

@ -2,8 +2,6 @@
import { onMount } from "svelte";
import {
GeoJSONSource,
GeolocateControl,
Hash,
LineLayer,
MapLibre,
Marker,
@ -11,13 +9,7 @@
} from "svelte-maplibre-gl";
import { view } from "./sidebar.svelte";
import { map, pin } from "./map.svelte";
import {
drawAllRoutes,
fetchRoute,
routing,
} from "$lib/services/navigation/routing.svelte";
import { createValhallaRequest } from "$lib/vehicles/ValhallaVehicles";
import { ROUTING_SERVER } from "$lib/services/hosts";
import { routing } from "$lib/services/navigation/routing.svelte";
import { location } from "./location.svelte";
onMount(() => {
@ -33,7 +25,9 @@
scheme="tiles"
loadFn={async (params) => {
console.log(params.url);
const url = params.url.replace("tiles://", "").replace("tiles.openfreemap.org/", "");
const url = params.url
.replace("tiles://", "")
.replace("tiles.openfreemap.org/", "");
const path = url.split("/")[0];
if (path == "natural_earth") {
const t = await fetch("https://tiles.openfreemap.org/" + url);
@ -73,7 +67,7 @@
}
}}
onmove={(e) => {
// @ts-ignore
// @ts-expect-error - not typed
if (e.reason !== "location") {
location.locked = false;
}
@ -191,7 +185,10 @@
{#if location.available}
<div class="maplibregl-user-location-dot" bind:this={locationDot}></div>
<div class="maplibregl-user-location-accuracy-circle" bind:this={locationAccuracyCircle}></div>
<div
class="maplibregl-user-location-accuracy-circle"
bind:this={locationAccuracyCircle}
></div>
<Marker
lnglat={{ lat: location.lat, lng: location.lng }}
element={locationDot}

View File

@ -2,7 +2,10 @@
import { hasCapability, type Capabilities } from "$lib/services/lnv";
import type { Snippet } from "svelte";
let { capability, children }: {
let {
capability,
children,
}: {
capability: Capabilities[number];
children: Snippet;
} = $props();
@ -12,6 +15,6 @@
{#if has}
{@render children()}
{/if}
{:catch error}
{:catch _error}
<!-- user is likely offline -->
{/await}

View File

@ -1,36 +1,51 @@
<script lang="ts">
import LanesDisplay from "$lib/services/navigation/LanesDisplay.svelte";
import { decodePolyline, routing } from "$lib/services/navigation/routing.svelte";
import {
decodePolyline,
routing,
} from "$lib/services/navigation/routing.svelte";
import { location } from "./location.svelte";
import ManeuverIcon from "./ManeuverIcon.svelte";
// Helper: Haversine distance in meters
function haversine(a: { lat: number; lon: number }, b: { lat: number; lon: number }) {
function haversine(
a: { lat: number; lon: number },
b: { lat: number; lon: number },
) {
const R = 6371000;
const toRad = (d: number) => d * Math.PI / 180;
const toRad = (d: number) => (d * Math.PI) / 180;
const dLat = toRad(b.lat - a.lat);
const dLon = toRad(b.lon - a.lon);
const lat1 = toRad(a.lat);
const lat2 = toRad(b.lat);
const aVal = Math.sin(dLat / 2) ** 2 +
const aVal =
Math.sin(dLat / 2) ** 2 +
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
return 2 * R * Math.atan2(Math.sqrt(aVal), Math.sqrt(1 - aVal));
}
// Helper: Project point onto segment AB, return projected point and distance along segment
function projectPointToSegment(p: WorldLocation, a: WorldLocation, b: WorldLocation) {
const toRad = (deg: number) => deg * Math.PI / 180;
const toDeg = (rad: number) => rad * 180 / Math.PI;
function projectPointToSegment(
p: WorldLocation,
a: WorldLocation,
b: WorldLocation,
) {
const toRad = (deg: number) => (deg * Math.PI) / 180;
const toDeg = (rad: number) => (rad * 180) / Math.PI;
const lat1 = toRad(a.lat), lon1 = toRad(a.lon);
const lat2 = toRad(b.lat), lon2 = toRad(b.lon);
const lat3 = toRad(p.lat), lon3 = toRad(p.lon);
const lat1 = toRad(a.lat),
lon1 = toRad(a.lon);
const lat2 = toRad(b.lat),
lon2 = toRad(b.lon);
const lat3 = toRad(p.lat),
lon3 = toRad(p.lon);
const dLon = lon2 - lon1;
const dLat = lat2 - lat1;
const t = ((lat3 - lat1) * dLat + (lon3 - lon1) * dLon) /
const t =
((lat3 - lat1) * dLat + (lon3 - lon1) * dLon) /
(dLat * dLat + dLon * dLon);
// Clamp to [0,1]
@ -49,7 +64,9 @@
// const point = $derived(decodePolyline(routing.currentTrip?.legs[0].shape || "")[routing.currentTripInfo.currentManeuver?.end_shape_index || 0]);
// const distance = $derived(Math.sqrt(Math.pow(point.lat - location.lat, 2) + Math.pow(point.lon - location.lng, 2)) * 111000); // Approximate conversion to meters
const shape = $derived(decodePolyline(routing.currentTrip?.legs[0].shape || ""));
const shape = $derived(
decodePolyline(routing.currentTrip?.legs[0].shape || ""),
);
const maneuver = $derived(routing.currentTripInfo.currentManeuver);
const distance = $derived.by(() => {
@ -76,24 +93,27 @@
} else {
return Math.round(dist / 5000) * 5000;
}
})
});
const distanceText = $derived.by(() => {
const dist = roundDistance;
if (dist < 1000) return `${dist} m`;
return `${(dist / 1000)} km`;
})
return `${dist / 1000} km`;
});
</script>
<div class="fixed top-4 left-4 z-50 w-[calc(100%-32px)] bg-background/60 text-white rounded-lg overflow-hidden" style="backdrop-filter: blur(5px);">
<div
class="fixed top-4 left-4 z-50 w-[calc(100%-32px)] bg-background/60 text-white rounded-lg overflow-hidden"
style="backdrop-filter: blur(5px);"
>
<div class="p-2 flex gap-2">
<ManeuverIcon maneuver={routing.currentTripInfo.currentManeuver?.type ?? 0} />
<ManeuverIcon
maneuver={routing.currentTripInfo.currentManeuver?.type ?? 0}
/>
<div class="flex gap-1 flex-col">
<span class="text-xl font-bold">{distanceText}</span>
<span>{routing.currentTripInfo.currentManeuver?.instruction}</span>
</div>
</div>
<LanesDisplay
lanes={routing.currentTripInfo.currentManeuver?.lanes}
/>
<LanesDisplay lanes={routing.currentTripInfo.currentManeuver?.lanes} />
</div>

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { onMount, type Component } from "svelte";
import { type Component } from "svelte";
import InvalidSidebar from "./sidebar/InvalidSidebar.svelte";
import { searchbar, view } from "./sidebar.svelte";
import MainSidebar from "./sidebar/MainSidebar.svelte";
@ -8,18 +8,28 @@
import { map } from "./map.svelte";
import TripSidebar from "./sidebar/TripSidebar.svelte";
import Input from "../ui/input/input.svelte";
import { EllipsisIcon, HomeIcon, SettingsIcon, UserIcon } from "@lucide/svelte";
import {
EllipsisIcon,
HomeIcon,
SettingsIcon,
UserIcon,
} from "@lucide/svelte";
import Button from "../ui/button/button.svelte";
import { search, type Feature } from "$lib/services/Search";
import SearchSidebar from "./sidebar/SearchSidebar.svelte";
import RequiresCapability from "./RequiresCapability.svelte";
import UserSidebar from "./sidebar/UserSidebar.svelte";
import { advertiseRemoteLocation, location, remoteLocation } from "./location.svelte";
import {
advertiseRemoteLocation,
location,
remoteLocation,
} from "./location.svelte";
import * as Popover from "../ui/popover";
import { routing } from "$lib/services/navigation/routing.svelte";
import InRouteSidebar from "./sidebar/InRouteSidebar.svelte";
const views: {[key: string]: Component<any>} = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const views: Record<string, Component<any>> = {
main: MainSidebar,
info: InfoSidebar,
route: RouteSidebar,
@ -41,12 +51,14 @@
$effect(() => {
const newValue = getter(); // read here to subscribe to it
clearTimeout(timer);
timer = setTimeout(() => value = newValue, delay);
timer = setTimeout(() => (value = newValue), delay);
return () => clearTimeout(timer);
});
return () => value;
}
// TODO: implement loading state
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let loading = $state(false);
let searchText = $derived.by(debounce(() => searchbar.text, 300));
@ -54,20 +66,20 @@
let mobileView = $derived(window.innerWidth < 768 || routing.currentTrip);
$effect(() => {
if(!searchText) {
if (!searchText) {
searchResults = [];
if(view.current.type == "search") view.switch("main");
if (view.current.type == "search") view.switch("main");
return;
}
if (searchText.length > 0) {
loading = true;
search(searchText, 0, 0).then(results => {
search(searchText, 0, 0).then((results) => {
searchResults = results;
loading = false;
view.switch("search", {
results: searchResults,
query: searchText
})
query: searchText,
});
});
} else {
searchResults = [];
@ -77,20 +89,27 @@
{#if !routing.currentTrip}
<div id="floating-search" class={mobileView ? "mobileView" : ""}>
<Input class="h-10"
placeholder="Search..." bind:value={searchbar.text} />
<Input class="h-10" placeholder="Search..." bind:value={searchbar.text} />
</div>
{/if}
<div id="sidebar" class={mobileView ? "mobileView" : ""} style={(mobileView ? `height: ${sidebarHeight}px;` : "") + (routing.currentTrip ? "bottom: 0 !important;" : "")}>
<div
id="sidebar"
class={mobileView ? "mobileView" : ""}
style={(mobileView ? `height: ${sidebarHeight}px;` : "") +
(routing.currentTrip ? "bottom: 0 !important;" : "")}
>
{#if mobileView}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_interactive_supports_focus -->
<div role="button" id="grabber" style="height: 10px; cursor: grab; display: flex; justify-content: center; align-items: center; touch-action: none;" ontouchstart={(e) => {
<div
role="button"
id="grabber"
style="height: 10px; cursor: grab; display: flex; justify-content: center; align-items: center; touch-action: none;"
ontouchstart={(e) => {
isDragging = true;
startY = e.touches[0].clientY;
startHeight = sidebarHeight;
}} ontouchmove={(e) => {
if(!isDragging) return;
}}
ontouchmove={(e) => {
if (!isDragging) return;
e.preventDefault();
const deltaY = e.touches[0].clientY - startY;
let newHeight = Math.max(100, startHeight - deltaY);
@ -103,11 +122,15 @@
sidebarHeight = newHeight;
map.updateMapPadding();
}} ontouchend={() => {
if(!isDragging) return;
}}
ontouchend={() => {
if (!isDragging) return;
isDragging = false;
}}>
<div style="height: 8px; background-color: #acacac; width: 40%; border-radius: 15px;"></div>
}}
>
<div
style="height: 8px; background-color: #acacac; width: 40%; border-radius: 15px;"
></div>
</div>
{/if}
@ -123,9 +146,11 @@
<HomeIcon />
</button>
<RequiresCapability capability="auth">
<button onclick={async () => {
<button
onclick={async () => {
view.switch("user");
}}>
}}
>
<UserIcon />
</button>
</RequiresCapability>
@ -145,22 +170,31 @@
</Popover.Trigger>
<Popover.Content>
<div class="flex flex-col gap-2">
<Button variant="outline" onclick={() => {
<Button
variant="outline"
onclick={() => {
location.toggleLock();
}}>
}}
>
{location.locked ? "Unlock Location" : "Lock Location"}
</Button>
{#if location.code}
<span>Advertise code: {location.code}</span>
{/if}
<Button variant="outline" onclick={() => {
<Button
variant="outline"
onclick={() => {
advertiseRemoteLocation();
}}>
}}
>
Advertise Location
</Button>
<Button variant="outline" onclick={() => {
<Button
variant="outline"
onclick={() => {
remoteLocation(prompt("Code?") || "");
}}>
}}
>
Join Remote Location
</Button>
</div>

View File

@ -1,8 +1,20 @@
<script lang="ts">
import * as Drawer from "$lib/components/ui/drawer/index.js";
import { BikeIcon, CarIcon, PlusCircleIcon, TractorIcon, TruckIcon } from "@lucide/svelte";
import {
BikeIcon,
CarIcon,
PlusCircleIcon,
TractorIcon,
TruckIcon,
} from "@lucide/svelte";
import Button, { buttonVariants } from "../ui/button/button.svelte";
import { DefaultVehicle, selectedVehicle, selectVehicle, vehicles, type VehicleType } from "$lib/vehicles/vehicles.svelte";
import {
DefaultVehicle,
selectedVehicle,
selectVehicle,
vehicles,
type VehicleType,
} from "$lib/vehicles/vehicles.svelte";
import AddVehicleDrawer from "./AddVehicleDrawer.svelte";
let open = $state(false);
@ -25,8 +37,10 @@
}
</script>
<Drawer.Root bind:open={open}>
<Drawer.Trigger class={buttonVariants({ variant: "secondary", class: "w-full p-5" })}>
<Drawer.Root bind:open>
<Drawer.Trigger
class={buttonVariants({ variant: "secondary", class: "w-full p-5" })}
>
{@const vehicle = selectedVehicle() ?? DefaultVehicle}
{@const Icon = getVehicleIcon(vehicle.type)}
<Icon />
@ -35,11 +49,20 @@
<Drawer.Content>
<Drawer.Header>
<Drawer.Title>Vehicle Selector</Drawer.Title>
<Drawer.Description>Select your vehicle to customize routing just for you.</Drawer.Description>
<Drawer.Description
>Select your vehicle to customize routing just for you.</Drawer.Description
>
</Drawer.Header>
<div class="p-4 pt-0 flex flex-col gap-2">
{#each vehicles as vehicle}
<Button variant={selectedVehicle() === vehicle ? "default" : "secondary"} class="w-full p-5" onclick={() => {selectVehicle(vehicle); open = false;}}>
{#each vehicles as vehicle (vehicle.name)}
<Button
variant={selectedVehicle() === vehicle ? "default" : "secondary"}
class="w-full p-5"
onclick={() => {
selectVehicle(vehicle);
open = false;
}}
>
{@const Icon = getVehicleIcon(vehicle.type)}
<Icon />
{vehicle.name}

View File

@ -7,7 +7,7 @@
<h3 class="text-lg font-bold mt-2">Fuel Types</h3>
<ul class="flex gap-2 flex-wrap">
{#each Object.entries(tags).filter(([key]) => key.startsWith("fuel:")) as [key, tag]}
{#each Object.entries(tags).filter( ([key]) => key.startsWith("fuel:"), ) as [key, tag] (key)}
<!-- <li>{key.replace("fuel:", "")}: {tag}</li> -->
<Badge>
{key.replace("fuel:", "")}

View File

@ -1,6 +1,5 @@
<script lang="ts">
import Input from "$lib/components/ui/input/input.svelte";
import { LNV_SERVER } from "$lib/services/hosts";
import { ai } from "$lib/services/lnv";
import { SparklesIcon } from "@lucide/svelte";
@ -12,7 +11,7 @@
const chunks = res.split("\n");
let text = "";
for (const chunk of chunks) {
if(chunk.startsWith("0:")) {
if (chunk.startsWith("0:")) {
text += JSON.parse(chunk.substring(2).trim());
}
}
@ -33,9 +32,11 @@
{/await}
<Input
type="text"
value={""}
placeholder="Ask a question about this place..." onchange={(e) => {
question = (e.target! as any).value;
}} />
value=""
placeholder="Ask a question about this place..."
onchange={(e) => {
question = (e.target! as HTMLInputElement).value;
}}
/>
</div>
</div>

View File

@ -2,16 +2,19 @@
import Badge from "$lib/components/ui/badge/badge.svelte";
import opening_hours from "opening_hours";
let { hours, lat, lon }: { hours: string, lat: number, lon: number } = $props();
let { hours, lat, lon }: { hours: string; lat: number; lon: number } =
$props();
const oh = $derived.by(() => {
return new opening_hours(hours, {
lat, lon, address: {
lat,
lon,
address: {
country_code: "de", // Default to Germany, can be overridden if needed
state: "NRW", // Default to North Rhine-Westphalia, can be overridden if needed
}
},
});
});
})
</script>
<h3 class="text-lg font-bold mt-2">

View File

@ -4,14 +4,14 @@
import { getReviews, postReview } from "$lib/services/lnv";
import Stars from "./Stars.svelte";
let { lat, lng }: { lat: number, lng: number } = $props();
let { lat, lng }: { lat: number; lng: number } = $props();
</script>
<h3 class="text-lg font-bold mt-2">Reviews</h3>
{#await getReviews({lat, lon: lng}) then reviews}
{#await getReviews({ lat, lon: lng }) then reviews}
{#if reviews.length > 0}
<ul class="list-disc pl-5">
{#each reviews as review}
{#each reviews as review (review)}
<li class="flex justify-center gap-2 mb-2 flex-col">
<div class="flex items-center gap-2">
<Avatar.Root>
@ -27,20 +27,27 @@
{:else}
<p>No reviews available.</p>
{/if}
<Button variant="secondary" onclick={async () => {
<Button
variant="secondary"
onclick={async () => {
const rating = prompt("Enter your rating (1-5):");
const comment = prompt("Enter your review comment:");
if (rating && comment) {
console.log(`Rating: ${rating}, Comment: ${comment}`);
await postReview({ lat, lon: lng }, {
await postReview(
{ lat, lon: lng },
{
rating: parseInt(rating, 10),
comment
})
comment,
},
);
alert("Thank you for your review!");
} else {
alert("Review submission cancelled.");
}
}} disabled>Write a review</Button><br>
}}
disabled>Write a review</Button
><br />
{:catch error}
<p>Error loading reviews: {error.message}</p>
{/await}

View File

@ -1,6 +1,6 @@
import { LNV_SERVER } from "$lib/services/hosts"
import { routing } from "$lib/services/navigation/routing.svelte"
import { map } from "./map.svelte"
import { LNV_SERVER } from "$lib/services/hosts";
import { routing } from "$lib/services/navigation/routing.svelte";
import { map } from "./map.svelte";
export const location = $state({
available: false,
@ -12,100 +12,117 @@ export const location = $state({
provider: "gps" as "gps" | "remote" | "simulated",
locked: true,
toggleLock: () => {
location.locked = !location.locked
console.log("Location lock toggled:", location.locked)
location.locked = !location.locked;
console.log("Location lock toggled:", location.locked);
if (location.locked) {
map.value?.flyTo({
map.value?.flyTo(
{
center: [location.lng, location.lat],
zoom: 16,
duration: 1000,
// bearing: location.heading !== null ? location.heading : undefined
}, {
reason: "location"
})
},
{
reason: "location",
},
);
}
},
advertiser: null as WebSocket | null,
code: null as string | null,
lastUpdate: null as Date | null
})
lastUpdate: null as Date | null,
});
export function watchLocation() {
if(navigator.geolocation) {
navigator.geolocation.watchPosition((pos) => {
if(location.provider !== "gps") return;
if (navigator.geolocation) {
navigator.geolocation.watchPosition(
(pos) => {
if (location.provider !== "gps") return;
// console.log("Geolocation update:", pos)
location.lat = pos.coords.latitude
location.lng = pos.coords.longitude
location.accuracy = pos.coords.accuracy
location.speed = pos.coords.speed || 0
location.available = true
location.heading = pos.coords.heading
location.lastUpdate = new Date()
location.lat = pos.coords.latitude;
location.lng = pos.coords.longitude;
location.accuracy = pos.coords.accuracy;
location.speed = pos.coords.speed || 0;
location.available = true;
location.heading = pos.coords.heading;
location.lastUpdate = new Date();
if (location.locked) {
map.value?.flyTo({
map.value?.flyTo(
{
center: [location.lng, location.lat],
zoom: 16,
duration: 1000,
// bearing: location.heading !== null ? location.heading : undefined
}, {
reason: "location"
})
},
{
reason: "location",
},
);
}
// console.log(location.advertiser);
if (location.advertiser) {
location.advertiser.send(JSON.stringify({
location.advertiser.send(
JSON.stringify({
type: "location",
location: {
lat: location.lat,
lng: location.lng,
accuracy: location.accuracy,
speed: location.speed,
heading: location.heading
heading: location.heading,
},
route: {
trip: routing.currentTrip,
info: routing.currentTripInfo,
geojson: routing.geojson
geojson: routing.geojson,
},
}),
);
}
}))
}
}, (err) => {
console.error("Geolocation error:", err)
}, {
enableHighAccuracy: true
})
},
(err) => {
console.error("Geolocation error:", err);
},
{
enableHighAccuracy: true,
},
);
}
}
let checkRunning = false;
if(!checkRunning) {
if (!checkRunning) {
setInterval(() => {
checkRunning = true;
if(location.provider !== "gps") return;
if (location.provider !== "gps") return;
// If the last update was more than 5 seconds ago, recall watchPosition
// console.log("Checking location update status")
if (location.lastUpdate && (new Date().getTime() - location.lastUpdate.getTime()) > 10000) {
console.warn("Location update is stale, rewatching position")
if (
location.lastUpdate &&
new Date().getTime() - location.lastUpdate.getTime() > 10000
) {
console.warn("Location update is stale, rewatching position");
watchLocation();
}
}, 1000)
}, 1000);
checkRunning = true;
}
watchLocation()
watchLocation();
export function advertiseRemoteLocation(code?: string) {
const ws = new WebSocket(
`${LNV_SERVER.replace("https", "wss").replace("http", "ws")}/ws`
`${LNV_SERVER.replace("https", "wss").replace("http", "ws")}/ws`,
);
ws.addEventListener("open", () => {
console.log("WebSocket connection established for remote location advertisement")
ws.send(JSON.stringify({ type: "advertise", code }))
console.log(
"WebSocket connection established for remote location advertisement",
);
ws.send(JSON.stringify({ type: "advertise", code }));
});
ws.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
@ -127,40 +144,43 @@ export function remoteLocation(code: string) {
// Open websocket connection
// Use LNV_SERVER, change to ws or wss based on protocol
const ws = new WebSocket(
`${LNV_SERVER.replace("https", "wss").replace("http", "ws")}/ws`
`${LNV_SERVER.replace("https", "wss").replace("http", "ws")}/ws`,
);
ws.addEventListener("open", () => {
console.log("WebSocket connection established for remote location")
ws.send(JSON.stringify({ type: "subscribe", code }))
location.provider = "remote"
location.code = code
})
console.log("WebSocket connection established for remote location");
ws.send(JSON.stringify({ type: "subscribe", code }));
location.provider = "remote";
location.code = code;
});
ws.addEventListener("message", (event) => {
const data = JSON.parse(event.data)
const data = JSON.parse(event.data);
if (data.type === "location") {
console.log("Remote location update:", data.location)
location.lat = data.location.lat
location.lng = data.location.lng
location.accuracy = data.location.accuracy
location.speed = data.location.speed || 0
location.available = true
location.heading = data.location.heading || null
routing.currentTrip = data.route.trip || null
routing.currentTripInfo = data.route.info || null
routing.geojson = data.route.geojson || null
console.log("Remote location update:", data.location);
location.lat = data.location.lat;
location.lng = data.location.lng;
location.accuracy = data.location.accuracy;
location.speed = data.location.speed || 0;
location.available = true;
location.heading = data.location.heading || null;
routing.currentTrip = data.route.trip || null;
routing.currentTripInfo = data.route.info || null;
routing.geojson = data.route.geojson || null;
if (location.locked) {
map.value?.flyTo({
map.value?.flyTo(
{
center: [location.lng, location.lat],
zoom: 16,
duration: 1000,
// bearing: location.heading !== null ? location.heading : undefined
}, {
reason: "location"
})
},
{
reason: "location",
},
);
}
}
})
});
}
// setInterval(() => {

View File

@ -1,5 +1,4 @@
import { routing } from "$lib/services/navigation/routing.svelte";
import { reverseGeocode } from "$lib/services/Search";
import { view } from "./sidebar.svelte";
// export const geolocate = $state({
@ -9,27 +8,31 @@ import { view } from "./sidebar.svelte";
export const map = $state({
value: undefined as maplibregl.Map | undefined,
updateMapPadding: () => {
if(document.querySelector<HTMLDivElement>("#sidebar") == null) {
if (document.querySelector<HTMLDivElement>("#sidebar") == null) {
map._setPadding({
top: 0,
right: 0,
bottom: 0,
left: 0
left: 0,
});
return;
}
console.log("Updating map padding");
if (window.innerWidth < 768 || routing.currentTrip) {
const calculatedSidebarHeight = document.querySelector<HTMLDivElement>("#sidebar")!.getBoundingClientRect().height;
const calculatedSidebarHeight = document
.querySelector<HTMLDivElement>("#sidebar")!
.getBoundingClientRect().height;
map._setPadding({
top: routing.currentTrip ? 64 : 0,
right: 0,
bottom: calculatedSidebarHeight,
left: 0
left: 0,
});
return;
}
const calculatedSidebarWidth = document.querySelector<HTMLDivElement>("#sidebar")!.getBoundingClientRect().width;
const calculatedSidebarWidth = document
.querySelector<HTMLDivElement>("#sidebar")!
.getBoundingClientRect().width;
map._setPadding({
top: 0,
right: 0,
@ -41,14 +44,19 @@ export const map = $state({
top: 0,
right: 0,
bottom: 0,
left: 0
left: 0,
},
_setPadding: (_padding: { top: number, right: number, bottom: number, left: number }) => {
_setPadding: (_padding: {
top: number;
right: number;
bottom: number;
left: number;
}) => {
map.padding = _padding;
if (map.value) {
map.value.setPadding(map.padding);
}
}
},
});
export const pin = $state({
@ -66,12 +74,12 @@ export const pin = $state({
pin.lng = 0;
},
showInfo: async () => {
if(!pin.isDropped) return;
if (!pin.isDropped) return;
// const res = await reverseGeocode({ lat: pin.lat, lon: pin.lng });
// if(res.length > 0) {
// const feature = res[0];
// view.switch("info", { feature });
// }
view.switch("info", { lat: pin.lat, lng: pin.lng });
}
})
},
});

View File

@ -1,6 +1,6 @@
export type View = {
export interface View {
type: string;
props?: Record<string, any>;
props?: Record<string, unknown>;
}
export const view = $state({
@ -13,14 +13,14 @@ export const view = $state({
view.current = { type: "main" } as View; // Reset to main view if history is empty
}
},
switch: (to: string, props?: Record<string, any>) => {
switch: (to: string, props?: Record<string, unknown>) => {
if (view.current.type !== to) {
view.history.push(view.current);
}
view.current = { type: to, props } as View;
}
},
});
export const searchbar = $state({
text: ""
})
text: "",
});

View File

@ -1,35 +1,51 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { decodePolyline, routing, stopNavigation } from "$lib/services/navigation/routing.svelte";
import {
decodePolyline,
routing,
stopNavigation,
} from "$lib/services/navigation/routing.svelte";
import { advertiseRemoteLocation, location } from "../location.svelte";
// Helper: Haversine distance in meters
function haversine(a: { lat: number; lon: number }, b: { lat: number; lon: number }) {
function haversine(
a: { lat: number; lon: number },
b: { lat: number; lon: number },
) {
const R = 6371000;
const toRad = (d: number) => d * Math.PI / 180;
const toRad = (d: number) => (d * Math.PI) / 180;
const dLat = toRad(b.lat - a.lat);
const dLon = toRad(b.lon - a.lon);
const lat1 = toRad(a.lat);
const lat2 = toRad(b.lat);
const aVal = Math.sin(dLat / 2) ** 2 +
const aVal =
Math.sin(dLat / 2) ** 2 +
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
return 2 * R * Math.atan2(Math.sqrt(aVal), Math.sqrt(1 - aVal));
}
// Helper: Project point onto segment AB, return projected point and distance along segment
function projectPointToSegment(p: WorldLocation, a: WorldLocation, b: WorldLocation) {
const toRad = (deg: number) => deg * Math.PI / 180;
const toDeg = (rad: number) => rad * 180 / Math.PI;
function projectPointToSegment(
p: WorldLocation,
a: WorldLocation,
b: WorldLocation,
) {
const toRad = (deg: number) => (deg * Math.PI) / 180;
const toDeg = (rad: number) => (rad * 180) / Math.PI;
const lat1 = toRad(a.lat), lon1 = toRad(a.lon);
const lat2 = toRad(b.lat), lon2 = toRad(b.lon);
const lat3 = toRad(p.lat), lon3 = toRad(p.lon);
const lat1 = toRad(a.lat),
lon1 = toRad(a.lon);
const lat2 = toRad(b.lat),
lon2 = toRad(b.lon);
const lat3 = toRad(p.lat),
lon3 = toRad(p.lon);
const dLon = lon2 - lon1;
const dLat = lat2 - lat1;
const t = ((lat3 - lat1) * dLat + (lon3 - lon1) * dLon) /
const t =
((lat3 - lat1) * dLat + (lon3 - lon1) * dLon) /
(dLat * dLat + dLon * dLon);
// Clamp to [0,1]
@ -46,8 +62,10 @@
};
}
const shape = $derived(decodePolyline(routing.currentTrip?.legs[0].shape || ""));
const maneuver = $derived(routing.currentTripInfo.currentManeuver);
const shape = $derived(
decodePolyline(routing.currentTrip?.legs[0].shape || ""),
);
// const maneuver = $derived(routing.currentTripInfo.currentManeuver);
const fullDistance = $derived.by(() => {
const lat = location.lat;
@ -61,7 +79,11 @@
const b = shape[i + 1];
const proj = projectPointToSegment({ lat, lon }, a, b);
if (proj.distToUser < best.dist) {
best = { idx: i, proj: { lat: proj.lat, lon: proj.lon }, dist: proj.distToUser };
best = {
idx: i,
proj: { lat: proj.lat, lon: proj.lon },
dist: proj.distToUser,
};
}
}
@ -91,39 +113,49 @@
} else {
return Math.round(dist / 5000) * 5000;
}
})
});
const fullDistanceText = $derived.by(() => {
const dist = roundFullDistance;
if (dist < 1000) return `${dist} m`;
return `${(dist / 1000)} km`;
})
return `${dist / 1000} km`;
});
</script>
{fullDistanceText} left
<Button onclick={() => {
<Button
onclick={() => {
location.toggleLock();
}}>LOCK</Button>
}}>LOCK</Button
>
<Button onclick={() => {
<Button
onclick={() => {
stopNavigation();
}}>End Trip</Button>
}}>End Trip</Button
>
<div class="flex flex-col gap-2 mt-5">
{#if location.code}
<span>Share Code: {location.code}</span>
<Button variant="secondary" onclick={() => {
<Button
variant="secondary"
onclick={() => {
location.advertiser?.close();
location.advertiser = null;
location.code = null;
}}>
}}
>
Stop Sharing Location
</Button>
{:else}
<Button variant="secondary" onclick={() => {
<Button
variant="secondary"
onclick={() => {
advertiseRemoteLocation();
}}>
}}
>
Share Trip Status & Location
</Button>
{/if}

View File

@ -1,8 +1,15 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { POIIcons } from "$lib/POIIcons";
import { OVERPASS_SERVER } from "$lib/services/hosts";
import { BriefcaseIcon, EllipsisIcon, GlobeIcon, HomeIcon, MailIcon, PhoneIcon, RouteIcon } from "@lucide/svelte";
import {
BriefcaseIcon,
EllipsisIcon,
GlobeIcon,
HomeIcon,
MailIcon,
PhoneIcon,
RouteIcon,
} from "@lucide/svelte";
import { pin } from "../map.svelte";
import SidebarHeader from "./SidebarHeader.svelte";
import { fetchPOI, type OverpassElement } from "$lib/services/Overpass";
@ -13,57 +20,78 @@
import * as Popover from "$lib/components/ui/popover";
import Reviews from "../info/Reviews.svelte";
import MapAi from "../info/MapAI.svelte";
import { hasCapability } from "$lib/services/lnv";
import RequiresCapability from "../RequiresCapability.svelte";
// let { feature }: { feature: Feature } = $props();
// let Icon = $derived(POIIcons[feature.properties.osm_key + "=" + feature.properties.osm_value]);
let { lat, lng }: { lat: number, lng: number } = $props();
let { lat, lng }: { lat: number; lng: number } = $props();
function getIcon(tags: Record<string, string>): typeof POIIcons[keyof typeof POIIcons] | null {
const key = Object.keys(tags).find(k => k.startsWith("amenity") || k.startsWith("shop"));
function getIcon(
tags: Record<string, string>,
): (typeof POIIcons)[keyof typeof POIIcons] | null {
const key = Object.keys(tags).find(
(k) => k.startsWith("amenity") || k.startsWith("shop"),
);
if (key && POIIcons[key + "=" + tags[key]]) {
return POIIcons[key + "=" + tags[key]];
}
return null;
}
function getDistance(aLat: number, aLon: number, lat: number, lon: number): number {
function getDistance(
aLat: number,
aLon: number,
lat: number,
lon: number,
): number {
const R = 6371e3; // Earth radius in meters
const φ1 = lat * Math.PI / 180;
const φ2 = aLat * Math.PI / 180;
const Δφ = (aLat - lat) * Math.PI / 180;
const Δλ = (aLon - lon) * Math.PI / 180;
const φ1 = (lat * Math.PI) / 180;
const φ2 = (aLat * Math.PI) / 180;
const Δφ = ((aLat - lat) * Math.PI) / 180;
const Δλ = ((aLon - lon) * Math.PI) / 180;
const a = Math.sin(Δφ/2)**2 + Math.cos(φ1)*Math.cos(φ2)*Math.sin(Δλ/2)**2;
const a =
Math.sin(Δφ / 2) ** 2 +
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) ** 2;
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
function sortByDistance(elements: OverpassElement[], lat: number, lng: number): OverpassElement[] {
function sortByDistance(
elements: OverpassElement[],
lat: number,
lng: number,
): OverpassElement[] {
return elements.sort((a: OverpassElement, b: OverpassElement) => {
const aLoc = a.center || a;
const bLoc = b.center || b;
return getDistance(aLoc.lat!, aLoc.lon!, lat, lng) - getDistance(bLoc.lat!, bLoc.lon!, lat, lng);
return (
getDistance(aLoc.lat!, aLoc.lon!, lat, lng) -
getDistance(bLoc.lat!, bLoc.lon!, lat, lng)
);
});
}
</script>
{#await fetchPOI(lat, lng, 20)}
<SidebarHeader onback={() => {
<SidebarHeader
onback={() => {
pin.liftPin();
}}>
}}
>
Dropped Pin
</SidebarHeader>
<p>Loading...</p>
{:then res}
{#if res.elements.length === 0}
<SidebarHeader onback={() => {
<SidebarHeader
onback={() => {
pin.liftPin();
}}>
}}
>
Dropped Pin
</SidebarHeader>
<span style="color: #acacac;">&copy; OpenStreetMap</span>
@ -75,38 +103,57 @@
{@const ellat = firstElement.center?.lat || firstElement.lat!}
{@const ellng = firstElement.center?.lon || firstElement.lon!}
<SidebarHeader onback={() => {
<SidebarHeader
onback={() => {
pin.liftPin();
}}>
}}
>
{#if getIcon(tags)}
{@const Icon = getIcon(tags)}
<Icon />
{/if}
{tags.name || (tags["addr:street"] ? (tags["addr:street"] + " " + tags["addr:housenumber"]) : "")}
{tags.name ||
(tags["addr:street"]
? tags["addr:street"] + " " + tags["addr:housenumber"]
: "")}
</SidebarHeader>
<div id="actions">
<Button onclick={() => {
<Button
onclick={() => {
view.switch("route", {
to: lat + "," + lng,
})
}}>
});
}}
>
<RouteIcon />
Route
</Button>
{#if tags.email || tags["contact:email"]}
<Button variant="secondary" href={`mailto:${tags.email || tags["contact:email"]}`} target="_blank">
<Button
variant="secondary"
href={`mailto:${tags.email || tags["contact:email"]}`}
target="_blank"
>
<MailIcon />
Email
</Button>
{/if}
{#if tags.website || tags["contact:website"]}
<Button variant="secondary" href={tags.website || tags["contact:website"]} target="_blank">
<Button
variant="secondary"
href={tags.website || tags["contact:website"]}
target="_blank"
>
<GlobeIcon />
Website
</Button>
{/if}
{#if tags.phone || tags["contact:phone"]}
<Button variant="secondary" href={`tel:${tags.phone || tags["contact:phone"]}`} target="_blank">
<Button
variant="secondary"
href={`tel:${tags.phone || tags["contact:phone"]}`}
target="_blank"
>
<PhoneIcon />
Call
</Button>
@ -120,15 +167,27 @@
</Popover.Trigger>
<Popover.Content>
<div class="flex flex-col gap-2">
<Button variant="outline" onclick={() => {
localStorage.setItem("saved.home", JSON.stringify({ lat, lon: lng }));
}}>
<Button
variant="outline"
onclick={() => {
localStorage.setItem(
"saved.home",
JSON.stringify({ lat, lon: lng }),
);
}}
>
<HomeIcon />
Set as Home
</Button>
<Button variant="outline" onclick={() => {
localStorage.setItem("saved.work", JSON.stringify({ lat, lon: lng }));
}}>
<Button
variant="outline"
onclick={() => {
localStorage.setItem(
"saved.work",
JSON.stringify({ lat, lon: lng }),
);
}}
>
<BriefcaseIcon />
Set as Work
</Button>
@ -160,12 +219,11 @@
{/if}
<!-- any payment:* tag -->
{#if Object.keys(tags).some(key => key.startsWith("payment:"))}
{#if Object.keys(tags).some((key) => key.startsWith("payment:"))}
<h3 class="text-lg font-bold mt-2">Payment Methods</h3>
<ul style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
{#each Object.entries(tags).filter(([key]) => key.startsWith("payment:")) as [key, value]}
<!-- <li>{key.replace("payment:", "")}: {value}</li> -->
<Badge>{key.replace("payment:", "")}</Badge>
{#each Object.entries(tags).filter( ([key]) => key.startsWith("payment:"), ) as [key, value] (key)}
<Badge>{key.replace("payment:", "")}: {value}</Badge>
{/each}
</ul>
{/if}
@ -179,9 +237,11 @@
<pre>{JSON.stringify(elements, null, 2)}</pre>
{/if}
{:catch err}
<SidebarHeader onback={() => {
<SidebarHeader
onback={() => {
pin.liftPin();
}}>
}}
>
Dropped Pin
</SidebarHeader>
<p>Error: {err.message}</p>

View File

@ -1,117 +0,0 @@
<script lang="ts">
import { BriefcaseIcon, HomeIcon } from "@lucide/svelte";
import { Button } from "../../ui/button";
import { Input } from "../../ui/input";
import { fly } from "svelte/transition";
import { circInOut } from "svelte/easing";
import { search, type Feature } from "$lib/services/Search";
import { view } from "../sidebar.svelte";
import { map, pin } from "../map.svelte";
function debounce<T>(getter: () => T, delay: number): () => T | undefined {
let value = $state<T>();
let timer: NodeJS.Timeout;
$effect(() => {
const newValue = getter(); // read here to subscribe to it
clearTimeout(timer);
timer = setTimeout(() => value = newValue, delay);
return () => clearTimeout(timer);
});
return () => value;
}
let typedText = $state("");
let loading = $state(false);
let searchText = $derived.by(debounce(() => typedText, 300));
let searchResults: Feature[] = $state([]);
$effect(() => {
if(!searchText) {
searchResults = [];
return;
}
if (searchText.length > 0) {
loading = true;
search(searchText, 0, 0).then(results => {
searchResults = results;
loading = false;
});
} else {
searchResults = [];
}
});
$inspect("searchText", searchText);
</script>
<div id="search-progress" style="min-height: calc(3px + 3px); width: 100%; min-height: 3ch;">
{#if loading}
LOADING
{/if}
</div>
<Input placeholder="Search..." bind:value={typedText} class="mb-2" />
{#if searchResults.length == 0}
<div id="saved" in:fly={{ y: 20, duration: 200, easing: circInOut }}>
<Button variant="secondary" class="flex-1" onclick={() => {
const home = localStorage.getItem("saved.home");
if(!home) {
alert("No home location saved.");
return;
}
const {lat, lon} = JSON.parse(home);
pin.dropPin(lat, lon);
pin.showInfo();
map.value?.flyTo({
center: [lon, lat],
zoom: 19
});
}}>
<HomeIcon />
Home
</Button>
<Button variant="secondary" class="flex-1" onclick={() => {
const work = localStorage.getItem("saved.work");
if(!work) {
alert("No home location saved.");
return;
}
const {lat, lon} = JSON.parse(work);
pin.dropPin(lat, lon);
pin.showInfo();
map.value?.flyTo({
center: [lon, lat],
zoom: 19
});
}}>
<BriefcaseIcon />
Work
</Button>
</div>
{:else}
<div id="results" in:fly={{ y: 20, duration: 200, easing: circInOut }}>
{#each searchResults as result}
<Button variant="secondary" class="flex-1" onclick={() => {
// view.switch("info", { feature: result });
pin.dropPin(result.geometry.coordinates[1], result.geometry.coordinates[0]);
pin.showInfo();
map.value?.flyTo({
center: [result.geometry.coordinates[0], result.geometry.coordinates[1]],
zoom: 19
});
}}>
{result.properties.name}
</Button>
{/each}
</div>
{/if}
<style>
#saved {
display: flex;
gap: 0.5rem;
/* justify-content: space-evenly; */
width: 100%;
max-width: 100%;
}
</style>

View File

@ -9,38 +9,50 @@
import RequiresCapability from "../RequiresCapability.svelte";
</script>
<div id="saved" class="mt-2 mb-2" in:fly={{ y: 20, duration: 200, easing: circInOut }}>
<Button variant="secondary" class="flex-1" onclick={() => {
<div
id="saved"
class="mt-2 mb-2"
in:fly={{ y: 20, duration: 200, easing: circInOut }}
>
<Button
variant="secondary"
class="flex-1"
onclick={() => {
const home = localStorage.getItem("saved.home");
if(!home) {
if (!home) {
alert("No home location saved.");
return;
}
const {lat, lon} = JSON.parse(home);
const { lat, lon } = JSON.parse(home);
pin.dropPin(lat, lon);
pin.showInfo();
map.value?.flyTo({
center: [lon, lat],
zoom: 19
zoom: 19,
});
}}>
}}
>
<HomeIcon />
Home
</Button>
<Button variant="secondary" class="flex-1" onclick={() => {
<Button
variant="secondary"
class="flex-1"
onclick={() => {
const work = localStorage.getItem("saved.work");
if(!work) {
if (!work) {
alert("No work location saved.");
return;
}
const {lat, lon} = JSON.parse(work);
const { lat, lon } = JSON.parse(work);
pin.dropPin(lat, lon);
pin.showInfo();
map.value?.flyTo({
center: [lon, lat],
zoom: 19
zoom: 19,
});
}}>
}}
>
<BriefcaseIcon />
Work
</Button>

View File

@ -1,19 +1,29 @@
<script lang="ts">
import { CircleArrowDown, CircleDotIcon, StarIcon } from "@lucide/svelte";
import LocationSelect from "../LocationSelect.svelte";
import Input from "$lib/components/ui/input/input.svelte";
import SidebarHeader from "./SidebarHeader.svelte";
import { Button } from "$lib/components/ui/button";
import { createValhallaRequest } from "$lib/vehicles/ValhallaVehicles";
import { drawAllRoutes, fetchRoute, removeAllRoutes, zoomToPoints } from "$lib/services/navigation/routing.svelte";
import {
drawAllRoutes,
fetchRoute,
removeAllRoutes,
zoomToPoints,
} from "$lib/services/navigation/routing.svelte";
import { ROUTING_SERVER } from "$lib/services/hosts";
import { map } from "../map.svelte";
import { view } from "../sidebar.svelte";
import { DefaultVehicle, selectedVehicle } from "$lib/vehicles/vehicles.svelte";
import {
DefaultVehicle,
selectedVehicle,
} from "$lib/vehicles/vehicles.svelte";
let { from, to }: {
from?: string,
to?: string
let {
from,
to,
}: {
from?: string;
to?: string;
} = $props();
let fromLocation = $state(from || "");
@ -29,13 +39,18 @@
}
</script>
<SidebarHeader onback={() => {
<SidebarHeader
onback={() => {
removeAllRoutes();
}}>
}}
>
Route
</SidebarHeader>
<span>Driving with <strong>{(selectedVehicle() ?? DefaultVehicle).name}</strong></span>
<span
>Driving with <strong>{(selectedVehicle() ?? DefaultVehicle).name}</strong
></span
>
<div class="flex flex-col gap-2 w-full mb-2">
<div class="flex gap-2 items-center w-full">
<CircleDotIcon />
@ -49,39 +64,51 @@
<Input bind:value={toLocation} />
</div>
</div>
<Button onclick={async () => {
const FROM: WorldLocation = fromLocation == "home" ? JSON.parse(localStorage.getItem("saved.home")!)
: fromLocation == "work" ? JSON.parse(localStorage.getItem("saved.work")!)
<Button
onclick={async () => {
const FROM: WorldLocation =
fromLocation == "home"
? JSON.parse(localStorage.getItem("saved.home")!)
: fromLocation == "work"
? JSON.parse(localStorage.getItem("saved.work")!)
: {
lat: parseFloat(fromLocation.split(",")[0]),
lon: parseFloat(fromLocation.split(",")[1])
lon: parseFloat(fromLocation.split(",")[1]),
};
const TO: WorldLocation = toLocation == "home" ? JSON.parse(localStorage.getItem("saved.home")!)
: toLocation == "work" ? JSON.parse(localStorage.getItem("saved.work")!)
const TO: WorldLocation =
toLocation == "home"
? JSON.parse(localStorage.getItem("saved.home")!)
: toLocation == "work"
? JSON.parse(localStorage.getItem("saved.work")!)
: {
lat: parseFloat(toLocation.split(",")[0]),
lon: parseFloat(toLocation.split(",")[1])
lon: parseFloat(toLocation.split(",")[1]),
};
const req = createValhallaRequest(selectedVehicle() ?? DefaultVehicle, [FROM, TO]);
const req = createValhallaRequest(selectedVehicle() ?? DefaultVehicle, [
FROM,
TO,
]);
const res = await fetchRoute(ROUTING_SERVER, req);
routes = [
res.trip,
];
for(const alternate of res.alternates) {
if(alternate.trip) {
routes = [res.trip];
for (const alternate of res.alternates) {
if (alternate.trip) {
routes.push(alternate.trip);
}
}
drawAllRoutes(routes);
zoomToPoints(FROM, TO, map.value!);
}}>Calculate</Button>
}}>Calculate</Button
>
{#if routes}
<div class="mt-2 flex gap-2 flex-col">
{#each routes as route, i (route?.summary?.length)}
<Button variant="secondary" onclick={() => {
<Button
variant="secondary"
onclick={() => {
view.switch("trip", { route });
}}>
}}
>
{#if i == 0}
<StarIcon />
{/if}

View File

@ -7,28 +7,47 @@
import SidebarHeader from "./SidebarHeader.svelte";
import { searchbar } from "../sidebar.svelte";
let { results, query }: {
results: Feature[],
query: string
let {
results,
query,
}: {
results: Feature[];
query: string;
} = $props();
</script>
<SidebarHeader onback={() => {
<SidebarHeader
onback={() => {
searchbar.text = "";
}}>
Search Results
}}
>
Search Results for "{query}"
</SidebarHeader>
<div id="results" class="mt-2" in:fly={{ y: 20, duration: 200, easing: circInOut }}>
{#each results as result}
<Button variant="secondary" class="flex-1" onclick={() => {
<div
id="results"
class="mt-2"
in:fly={{ y: 20, duration: 200, easing: circInOut }}
>
{#each results as result (result.properties.osm_id)}
<Button
variant="secondary"
class="flex-1"
onclick={() => {
// view.switch("info", { feature: result });
pin.dropPin(result.geometry.coordinates[1], result.geometry.coordinates[0]);
pin.dropPin(
result.geometry.coordinates[1],
result.geometry.coordinates[0],
);
pin.showInfo();
map.value?.flyTo({
center: [result.geometry.coordinates[0], result.geometry.coordinates[1]],
zoom: 19
center: [
result.geometry.coordinates[0],
result.geometry.coordinates[1],
],
zoom: 19,
});
}}>
}}
>
{result.properties.name}
</Button>
{/each}

View File

@ -3,18 +3,26 @@
import type { Snippet } from "svelte";
import { view } from "../sidebar.svelte";
let { children, onback }: {
children: Snippet,
onback?: () => void,
let {
children,
onback,
}: {
children: Snippet;
onback?: () => void;
} = $props();
</script>
<div class="flex gap-2 items-center mb-2">
<Button variant="outline" onclick={() => {
<Button
variant="outline"
onclick={() => {
view.back();
if (onback) {
onback();
}
}}>&lt;</Button>
<h2 class="text-lg font-bold flex gap-2 items-center">{@render children?.()}</h2>
}}>&lt;</Button
>
<h2 class="text-lg font-bold flex gap-2 items-center">
{@render children?.()}
</h2>
</div>

View File

@ -1,34 +1,44 @@
<script lang="ts">
import { onMount } from "svelte";
import SidebarHeader from "./SidebarHeader.svelte";
import { drawRoute, removeAllRoutes, startRoute } from "$lib/services/navigation/routing.svelte";
import {
drawRoute,
removeAllRoutes,
startRoute,
} from "$lib/services/navigation/routing.svelte";
import { Button } from "$lib/components/ui/button";
import { RouteIcon, SaveIcon, SendIcon } from "@lucide/svelte";
import { map } from "../map.svelte";
let { route }: {
route: Trip
let {
route,
}: {
route: Trip;
} = $props();
onMount(() => {
removeAllRoutes();
drawRoute(route);
})
});
</script>
<SidebarHeader onback={() => {
<SidebarHeader
onback={() => {
removeAllRoutes();
}}>
}}
>
Trip Details
</SidebarHeader>
<div id="actions" class="flex gap-2">
<Button onclick={async () => {
<Button
onclick={async () => {
await startRoute(route);
requestAnimationFrame(() => {
map.updateMapPadding();
})
}}>
});
}}
>
<RouteIcon />
Start Navigation
</Button>
@ -43,7 +53,7 @@
</div>
<div class="flex flex-col gap-2 mt-2">
{#each route.legs[0].maneuvers as maneuver}
{#each route.legs[0].maneuvers as maneuver (maneuver)}
<li>
{maneuver.instruction}
</li>

View File

@ -5,38 +5,46 @@
import { getAuthURL, getOIDCUser } from "$lib/services/oidc";
import * as Avatar from "$lib/components/ui/avatar";
let user: any = $state(null);
interface OIDCUser {
sub: string;
preferred_username: string;
name?: string;
picture?: string;
}
let user: OIDCUser | null = $state(null);
onMount(() => {
if(!localStorage.getItem("lnv-token")) {
if (!localStorage.getItem("lnv-token")) {
user = null;
} else {
user = JSON.parse(atob((localStorage.getItem("lnv-id") || "").split(".")[1]));
user = JSON.parse(
atob((localStorage.getItem("lnv-id") || "").split(".")[1]),
);
}
})
});
</script>
{#if !user}
<SidebarHeader>
User
</SidebarHeader>
<SidebarHeader>User</SidebarHeader>
<Button onclick={async () => {
<Button
onclick={async () => {
const auth = await getAuthURL();
// localStorage.setItem("lnv-codeVerifier", auth.codeVerifier);
// localStorage.setItem("lnv-oidcstate", auth.state);
const popup = window.open(auth.url, "Login", "width=500,height=600");
window.addEventListener("message", async (e) => {
if(e.origin !== window.location.origin) return;
if (e.origin !== window.location.origin) return;
const { code, state } = e.data;
console.log("Received data from popup:", e.data);
if(!code || !state) {
if (!code || !state) {
console.error("Invalid response from popup");
return;
}
popup?.close();
if(state !== auth.state) {
if (state !== auth.state) {
alert("State mismatch. Please try again.");
return;
}
@ -45,9 +53,12 @@
localStorage.setItem("lnv-id", token.id_token);
localStorage.setItem("lnv-token", token.access_token);
localStorage.setItem("lnv-refresh", token.refresh_token);
user = JSON.parse(atob((localStorage.getItem("lnv-id") || "").split(".")[1]));
})
}}>Login</Button>
user = JSON.parse(
atob((localStorage.getItem("lnv-id") || "").split(".")[1]),
);
});
}}>Login</Button
>
{:else}
<SidebarHeader>
<Avatar.Root>

View File

@ -12,6 +12,9 @@
<AvatarPrimitive.Fallback
bind:ref
data-slot="avatar-fallback"
class={cn("bg-muted flex size-full items-center justify-center rounded-full", className)}
class={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className,
)}
{...restProps}
/>

View File

@ -12,6 +12,9 @@
<AvatarPrimitive.Root
bind:ref
data-slot="avatar"
class={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
class={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className,
)}
{...restProps}
/>

View File

@ -11,7 +11,8 @@
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
destructive:
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {

View File

@ -1,19 +1,25 @@
<script lang="ts" module>
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import type {
HTMLAnchorAttributes,
HTMLButtonAttributes,
} from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
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",
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",
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: {

View File

@ -13,7 +13,10 @@
<div
bind:this={ref}
data-slot="card-action"
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
class={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
{...restProps}
>
{@render children?.()}

View File

@ -10,6 +10,11 @@
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
<div
bind:this={ref}
data-slot="card-content"
class={cn("px-6", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@ -15,7 +15,7 @@
data-slot="card-header"
class={cn(
"@container/card-header has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
className
className,
)}
{...restProps}
>

View File

@ -15,7 +15,7 @@
data-slot="card"
class={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
className,
)}
{...restProps}
>

View File

@ -1,5 +1,8 @@
<script lang="ts">
import type { Command as CommandPrimitive, Dialog as DialogPrimitive } from "bits-ui";
import type {
Command as CommandPrimitive,
Dialog as DialogPrimitive,
} from "bits-ui";
import type { Snippet } from "svelte";
import Command from "./command.svelte";
import * as Dialog from "$lib/components/ui/dialog/index.js";

View File

@ -11,13 +11,16 @@
}: CommandPrimitive.InputProps = $props();
</script>
<div class="flex h-9 items-center gap-2 border-b px-3" data-slot="command-input-wrapper">
<div
class="flex h-9 items-center gap-2 border-b px-3"
data-slot="command-input-wrapper"
>
<SearchIcon class="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
class={cn(
"placeholder:text-muted-foreground outline-hidden flex h-10 w-full rounded-md bg-transparent py-3 text-sm disabled:cursor-not-allowed disabled:opacity-50",
className
className,
)}
bind:ref
{...restProps}

View File

@ -14,7 +14,7 @@
data-slot="command-item"
class={cn(
"aria-selected:bg-accent aria-selected:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
className,
)}
{...restProps}
/>

View File

@ -14,7 +14,7 @@
data-slot="command-item"
class={cn(
"aria-selected:bg-accent aria-selected:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
className,
)}
{...restProps}
/>

View File

@ -12,6 +12,9 @@
<CommandPrimitive.List
bind:ref
data-slot="command-list"
class={cn("max-h-[300px] scroll-py-1 overflow-y-auto overflow-x-hidden", className)}
class={cn(
"max-h-[300px] scroll-py-1 overflow-y-auto overflow-x-hidden",
className,
)}
{...restProps}
/>

View File

@ -16,7 +16,7 @@
data-slot="command"
class={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
className,
)}
{...restProps}
/>

View File

@ -1,7 +1,8 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps = $props();
let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps =
$props();
</script>
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />

View File

@ -24,7 +24,7 @@
data-slot="dialog-content"
class={cn(
"bg-background 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 fixed left-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
className,
)}
{...restProps}
>

View File

@ -13,7 +13,10 @@
<div
bind:this={ref}
data-slot="dialog-footer"
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
class={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...restProps}
>
{@render children?.()}

View File

@ -14,7 +14,7 @@
data-slot="dialog-overlay"
class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
className,
)}
{...restProps}
/>

View File

@ -1,7 +1,8 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: DialogPrimitive.TriggerProps = $props();
let { ref = $bindable(null), ...restProps }: DialogPrimitive.TriggerProps =
$props();
</script>
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />

View File

@ -1,7 +1,8 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.CloseProps = $props();
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.CloseProps =
$props();
</script>
<DrawerPrimitive.Close bind:ref data-slot="drawer-close" {...restProps} />

View File

@ -25,7 +25,7 @@
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className
className,
)}
{...restProps}
>

View File

@ -9,4 +9,9 @@
}: DrawerPrimitive.RootProps = $props();
</script>
<DrawerPrimitive.NestedRoot {shouldScaleBackground} bind:open bind:activeSnapPoint {...restProps} />
<DrawerPrimitive.NestedRoot
{shouldScaleBackground}
bind:open
bind:activeSnapPoint
{...restProps}
/>

View File

@ -14,7 +14,7 @@
data-slot="drawer-overlay"
class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
className,
)}
{...restProps}
/>

View File

@ -1,7 +1,8 @@
<script lang="ts">
import { Drawer as DrawerPrimitive } from "vaul-svelte";
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.TriggerProps = $props();
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.TriggerProps =
$props();
</script>
<DrawerPrimitive.Trigger bind:ref data-slot="drawer-trigger" {...restProps} />

View File

@ -9,4 +9,9 @@
}: DrawerPrimitive.RootProps = $props();
</script>
<DrawerPrimitive.Root {shouldScaleBackground} bind:open bind:activeSnapPoint {...restProps} />
<DrawerPrimitive.Root
{shouldScaleBackground}
bind:open
bind:activeSnapPoint
{...restProps}
/>

View File

@ -1,12 +1,18 @@
<script lang="ts">
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
import type {
HTMLInputAttributes,
HTMLInputTypeAttribute,
} from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
type Props = WithElementRef<
Omit<HTMLInputAttributes, "type"> &
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
(
| { type: "file"; files?: FileList }
| { type?: InputType; files?: undefined }
)
>;
let {
@ -27,7 +33,7 @@
"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 py-2 text-sm font-medium 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]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
className,
)}
type="file"
bind:files
@ -42,7 +48,7 @@
"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]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
className,
)}
{type}
bind:value

View File

@ -22,7 +22,7 @@
{align}
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-(--bits-popover-content-transform-origin) outline-hidden z-50 w-72 rounded-md border p-4 shadow-md",
className
className,
)}
{...restProps}
/>

View File

@ -23,14 +23,14 @@
data-slot="select-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 max-h-(--bits-select-content-available-height) origin-(--bits-select-content-transform-origin) relative z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
className,
)}
{...restProps}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
class={cn(
"h-(--bits-select-anchor-height) min-w-(--bits-select-anchor-width) w-full scroll-my-1 p-1"
"h-(--bits-select-anchor-height) min-w-(--bits-select-anchor-width) w-full scroll-my-1 p-1",
)}
>
{@render children?.()}

View File

@ -1,7 +1,8 @@
<script lang="ts">
import { Select as SelectPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps = $props();
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps =
$props();
</script>
<SelectPrimitive.Group data-slot="select-group" {...restProps} />

View File

@ -19,7 +19,7 @@
data-slot="select-item"
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
className,
)}
{...restProps}
>

View File

@ -20,7 +20,7 @@
data-size={size}
class={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]: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 dark:hover:bg-input/50 shadow-xs flex w-fit select-none items-center justify-between gap-2 whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
className,
)}
{...restProps}
>

View File

@ -14,7 +14,7 @@
data-slot="separator"
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
className,
)}
{...restProps}
/>

View File

@ -1,7 +1,7 @@
import { LNV_SERVER } from "./hosts";
import { hasCapability } from "./lnv";
type Station = {
interface Station {
id: string;
name: string;
brand: string;
@ -18,13 +18,13 @@ type Station = {
postCode: number;
}
type StationsResponse = {
interface StationsResponse {
ok: boolean;
license: "CC BY 4.0 - https:\/\/creativecommons.tankerkoenig.de";
license: "CC BY 4.0 - https://creativecommons.tankerkoenig.de";
data: "MTS-K";
status: string;
stations: Station[];
};
}
type StationDetails = {
openingTimes: StationOpeningTime[];
@ -32,37 +32,49 @@ type StationDetails = {
wholeDay: boolean;
} & Station;
type StationOpeningTime = {
interface StationOpeningTime {
text: string;
start: string;
end: string;
};
}
type StationDetailsResponse = {
interface StationDetailsResponse {
ok: boolean;
license: "CC BY 4.0 - https:\/\/creativecommons.tankerkoenig.de";
license: "CC BY 4.0 - https://creativecommons.tankerkoenig.de";
data: "MTS-K";
status: string;
station: StationDetails;
};
export async function getStations(lat: number, lon: number): Promise<StationsResponse> {
if(!await hasCapability("fuel")) {
throw new Error("Fuel capability is not available");
}
return await fetch(`${LNV_SERVER}/fuel/list?lat=${lat}&lng=${lon}&rad=1&sort=dist&type=all`).then(res => res.json());
}
export async function getPrices(id: string) { // TODO: add type
if(!await hasCapability("fuel")) {
export async function getStations(
lat: number,
lon: number,
): Promise<StationsResponse> {
if (!(await hasCapability("fuel"))) {
throw new Error("Fuel capability is not available");
}
return await fetch(`${LNV_SERVER}/fuel/prices?ids=${id}`).then(res => res.json());
return await fetch(
`${LNV_SERVER}/fuel/list?lat=${lat}&lng=${lon}&rad=1&sort=dist&type=all`,
).then((res) => res.json());
}
export async function getStationDetails(id: string): Promise<StationDetailsResponse> {
if(!await hasCapability("fuel")) {
export async function getPrices(id: string) {
// TODO: add type
if (!(await hasCapability("fuel"))) {
throw new Error("Fuel capability is not available");
}
return await fetch(`${LNV_SERVER}/fuel/detail?id=${id}`).then(res => res.json());
return await fetch(`${LNV_SERVER}/fuel/prices?ids=${id}`).then((res) =>
res.json(),
);
}
export async function getStationDetails(
id: string,
): Promise<StationDetailsResponse> {
if (!(await hasCapability("fuel"))) {
throw new Error("Fuel capability is not available");
}
return await fetch(`${LNV_SERVER}/fuel/detail?id=${id}`).then((res) =>
res.json(),
);
}

View File

@ -1,10 +1,10 @@
import { OVERPASS_SERVER } from "./hosts";
export type OverpassResult = {
export interface OverpassResult {
elements: OverpassElement[];
};
}
export type OverpassElement = {
export interface OverpassElement {
type: "node" | "way" | "relation";
id: number;
tags: Record<string, string>;
@ -15,7 +15,7 @@ export type OverpassElement = {
lat: number; // Only for relations
lon: number; // Only for relations
};
};
}
/**
[out:json];
@ -40,11 +40,7 @@ out geom;
out geom;
*/
export async function fetchPOI(
lat: number,
lon: number,
radius: number,
) {
export async function fetchPOI(lat: number, lon: number, radius: number) {
return await fetch(OVERPASS_SERVER, {
method: "POST",
body: `[out:json];
@ -60,6 +56,6 @@ export async function fetchPOI(
node(around:${radius}, ${lat}, ${lon})["amenity"="parking"];
way(around:${radius}, ${lat}, ${lon})["amenity"="parking"];
);
out center tags;`
}).then(res => res.json() as Promise<OverpassResult>);
out center tags;`,
}).then((res) => res.json() as Promise<OverpassResult>);
}

View File

@ -2,38 +2,50 @@
import { SEARCH_SERVER } from "./hosts";
// import { Capacitor } from "@capacitor/core";
export type Feature = {
type: "Feature",
export interface Feature {
type: "Feature";
geometry: {
coordinates: [number, number],
type: "Point"
},
coordinates: [number, number];
type: "Point";
};
properties: {
osm_key: string;
osm_value: string;
osm_id: number,
city: string,
country: string,
name: string,
street: string,
housenumber: string,
type: string,
osm_id: number;
city: string;
country: string;
name: string;
street: string;
housenumber: string;
type: string;
// There is more, but not needed atm
}
};
}
export async function searchPlaces(query: string, lat: number, lon: number): Promise<Feature[]> {
const res = await fetch(SEARCH_SERVER + "/api/?q=" + query + "&lat=" + lat + "&lon=" + lon).then((res) => res.json());
export async function searchPlaces(
query: string,
lat: number,
lon: number,
): Promise<Feature[]> {
const res = await fetch(
SEARCH_SERVER + "/api/?q=" + query + "&lat=" + lat + "&lon=" + lon,
).then((res) => res.json());
return res.features;
}
export async function reverseGeocode(coord: WorldLocation): Promise<Feature[]> {
const res = await fetch(SEARCH_SERVER + "/reverse?lat=" + coord.lat + "&lon=" + coord.lon).then((res) => res.json());
const res = await fetch(
SEARCH_SERVER + "/reverse?lat=" + coord.lat + "&lon=" + coord.lon,
).then((res) => res.json());
return res.features;
}
export async function search(query: string, lat: number, lon: number): Promise<Feature[]> {
if(query.startsWith("@")) {
export async function search(
query: string,
lat: number,
lon: number,
): Promise<Feature[]> {
if (query.startsWith("@")) {
// if(Capacitor.isNativePlatform()) {
// return await searchContacts(query, lat, lon);
// }

View File

@ -4,4 +4,7 @@ export const ROUTING_SERVER = "https://valhalla1.openstreetmap.de/";
// export const ROUTING_SERVER = "https://routing.map.picoscratch.de";
export const SEARCH_SERVER = "https://photon.komoot.io/";
export const OVERPASS_SERVER = "https://overpass-api.de/api/interpreter";
export const LNV_SERVER = location.hostname == "localhost" ? "http://localhost:3000/api" : "https://trafficcue-api.picoscratch.de/api";
export const LNV_SERVER =
location.hostname == "localhost"
? "http://localhost:3000/api"
: "https://trafficcue-api.picoscratch.de/api";

View File

@ -2,7 +2,11 @@ import { LNV_SERVER } from "./hosts";
export type Capabilities = ("auth" | "reviews" | "ai" | "fuel" | "post")[];
export let capabilities: Capabilities = [];
export let oidcConfig: { AUTH_URL: string; CLIENT_ID: string; TOKEN_URL: string } | null = null;
export let oidcConfig: {
AUTH_URL: string;
CLIENT_ID: string;
TOKEN_URL: string;
} | null = null;
export async function fetchConfig() {
const res = await fetch(LNV_SERVER + "/config");
@ -10,7 +14,12 @@ export async function fetchConfig() {
throw new Error(`Failed to fetch capabilities: ${res.statusText}`);
}
const data = await res.json();
return data as { name: string; version: string; capabilities: Capabilities; oidc?: { AUTH_URL: string; CLIENT_ID: string; TOKEN_URL: string } };
return data as {
name: string;
version: string;
capabilities: Capabilities;
oidc?: { AUTH_URL: string; CLIENT_ID: string; TOKEN_URL: string };
};
}
export async function getCapabilities() {
@ -30,28 +39,32 @@ export async function getOIDCConfig() {
oidcConfig = {
AUTH_URL: config.oidc.AUTH_URL,
CLIENT_ID: config.oidc.CLIENT_ID,
TOKEN_URL: config.oidc.TOKEN_URL
TOKEN_URL: config.oidc.TOKEN_URL,
};
}
return oidcConfig;
}
export async function hasCapability(capability: Capabilities[number]): Promise<boolean> {
export async function hasCapability(
capability: Capabilities[number],
): Promise<boolean> {
const caps = await getCapabilities();
return caps.includes(capability);
}
export type Review = {
export interface Review {
user_id: string;
username: string;
rating: number;
comment: string;
}
export async function getReviews(location: WorldLocation) {
if(!await hasCapability("reviews")) {
if (!(await hasCapability("reviews"))) {
throw new Error("Reviews capability is not available");
}
const res = await fetch(LNV_SERVER + `/reviews?lat=${location.lat}&lon=${location.lon}`);
const res = await fetch(
LNV_SERVER + `/reviews?lat=${location.lat}&lon=${location.lon}`,
);
if (!res.ok) {
throw new Error(`Failed to fetch reviews: ${res.statusText}`);
}
@ -59,8 +72,11 @@ export async function getReviews(location: WorldLocation) {
return data as Review[];
}
export async function postReview(location: WorldLocation, review: Omit<Review, 'user_id' | 'username'>) {
if(!await hasCapability("reviews")) {
export async function postReview(
location: WorldLocation,
review: Omit<Review, "user_id" | "username">,
) {
if (!(await hasCapability("reviews"))) {
throw new Error("Reviews capability is not available");
}
const token = localStorage.getItem("lnv-token");
@ -71,13 +87,13 @@ export async function postReview(location: WorldLocation, review: Omit<Review, '
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
...review,
lat: location.lat,
lon: location.lon
})
lon: location.lon,
}),
});
if (!res.ok) {
throw new Error(`Failed to post review: ${res.statusText}`);
@ -86,7 +102,7 @@ export async function postReview(location: WorldLocation, review: Omit<Review, '
}
export async function ai(query: string, location?: WorldLocation) {
if(!await hasCapability("ai")) {
if (!(await hasCapability("ai"))) {
throw new Error("AI capability is not available");
}
const res = await fetch(LNV_SERVER + `/ai`, {
@ -96,8 +112,8 @@ export async function ai(query: string, location?: WorldLocation) {
},
body: JSON.stringify({
text: query,
coords: location
})
coords: location,
}),
});
if (!res.ok) {
throw new Error(`Failed to get AI response: ${res.statusText}`);

View File

@ -4,24 +4,28 @@
async function fetchImage(bit: number) {
if (knownDirections.includes(bit)) {
return await fetch(`/img/lanes/${bit}.svg`).then(res => res.text());
return await fetch(`/img/lanes/${bit}.svg`).then((res) => res.text());
} else {
return `<span>${bit}</span>`;
}
}
function loadImage(node: HTMLElement, bit: number) {
fetchImage(bit).then(img => {
fetchImage(bit).then((img) => {
node.innerHTML = img;
});
}
</script>
<div class="lane">
{#each Array(10).fill(0).map((_, i) => 1 << i) as bit}
{#each Array(10)
.fill(0)
.map((_, i) => 1 << i) as bit (bit)}
{#if lane.directions & bit}
<div
class="lane-image {lane.valid & bit ? 'valid' : ''} {lane.active & bit ? 'active' : ''}"
class="lane-image {lane.valid & bit ? 'valid' : ''} {lane.active & bit
? 'active'
: ''}"
use:loadImage={bit}
></div>
{/if}

View File

@ -10,7 +10,7 @@ export async function displayLane(lane: Lane) {
// Check if the bit is in the known directions
let img = "";
if (knownDirections.includes(bit)) {
img = await fetch(`/img/lanes/${bit}.svg`).then(res => res.text());
img = await fetch(`/img/lanes/${bit}.svg`).then((res) => res.text());
} else {
img = `<span>${bit}</span>`;
}

View File

@ -6,7 +6,7 @@
{#if lanes}
<div id="lanes">
{#each lanes as lane}
{#each lanes as lane (lane)}
<LaneDisplay {lane} />
{/each}
</div>

View File

@ -1,19 +1,38 @@
/* eslint-disable no-empty */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/prefer-for-of */
/* eslint-disable prefer-const */
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import maplibregl from "maplibre-gl";
// import { maneuverTypes } from "./Maneuver";
import { hideRouteStatus, updateRouteStatus } from "../../components/routestatus";
import { NavigationLayer, removeAllNavigationLayers, updateNavigationLayer } from "./NavigationLayers";
import {
hideRouteStatus,
updateRouteStatus,
} from "../../components/routestatus";
import {
NavigationLayer,
removeAllNavigationLayers,
updateNavigationLayer,
} from "./NavigationLayers";
import { updateMapPadding } from "../../main";
import say from "../TTS";
import { ROUTING_SERVER } from "../servers";
import { createValhallaRequest } from "./ValhallaRequest";
import { Vehicle } from "../../components/vehicles";
import { KeepAwake } from "@capacitor-community/keep-awake";
import { getCurrentViewName, getSidebarView } from "../../components/sidebar/SidebarRegistry";
import {
getCurrentViewName,
getSidebarView,
} from "../../components/sidebar/SidebarRegistry";
// import { displayLane } from "./LaneDisplay";
export async function fetchRoute(vehicle: Vehicle, from: WorldLocation, to: WorldLocation): Promise<RouteResult> {
export async function fetchRoute(
vehicle: Vehicle,
from: WorldLocation,
to: WorldLocation,
): Promise<RouteResult> {
// const req = {
// locations: [
// from,
@ -65,7 +84,11 @@ function drawRoute(trip: Trip, name: NavigationLayer) {
updateNavigationLayer(name, geometry);
}
export async function findRoute(vehicle: Vehicle, from: WorldLocation, to: WorldLocation): Promise<Trip[]> {
export async function findRoute(
vehicle: Vehicle,
from: WorldLocation,
to: WorldLocation,
): Promise<Trip[]> {
fromMarker = new maplibregl.Marker()
.setLngLat([from.lon, from.lat])
.addTo(window.glmap);
@ -75,8 +98,8 @@ export async function findRoute(vehicle: Vehicle, from: WorldLocation, to: World
const route = await fetchRoute(vehicle, from, to);
let routes = [route.trip];
if(route.alternates) {
for(let i = 0; i < route.alternates.length; i++) {
if (route.alternates) {
for (let i = 0; i < route.alternates.length; i++) {
routes.push(route.alternates[i].trip);
}
}
@ -117,7 +140,11 @@ function getUserLocation(): WorldLocation {
let pastRoute: WorldLocation[] = [];
// Check if the location is on the line between from and to (3 meter tolerance)
function isOnLine(location: WorldLocation, from: WorldLocation, to: WorldLocation) {
function isOnLine(
location: WorldLocation,
from: WorldLocation,
to: WorldLocation,
) {
// Convert the 6-meter tolerance to degrees (approximation)
const tolerance = 6 / 111320; // 1 degree latitude ≈ 111.32 km
@ -126,7 +153,9 @@ function isOnLine(location: WorldLocation, from: WorldLocation, to: WorldLocatio
const dy = to.lat - from.lat;
// Calculate the projection of the location onto the line segment
const t = ((location.lon - from.lon) * dx + (location.lat - from.lat) * dy) / (dx * dx + dy * dy);
const t =
((location.lon - from.lon) * dx + (location.lat - from.lat) * dy) /
(dx * dx + dy * dy);
// Clamp t to the range [0, 1] to ensure the projection is on the segment
const clampedT = Math.max(0, Math.min(1, t));
@ -139,7 +168,8 @@ function isOnLine(location: WorldLocation, from: WorldLocation, to: WorldLocatio
// Calculate the distance from the location to the closest point
const distance = Math.sqrt(
Math.pow(location.lon - closestPoint.lon, 2) + Math.pow(location.lat - closestPoint.lat, 2)
Math.pow(location.lon - closestPoint.lon, 2) +
Math.pow(location.lat - closestPoint.lat, 2),
);
// Check if the distance is within the tolerance
@ -175,9 +205,9 @@ export async function startNavigation(trip: Trip) {
// @ts-ignore The types are not correct
int = setInterval(() => {
if(instructionIdx != 0) {
if (instructionIdx != 0) {
// Only continue if the user location is at the end shape index of the current maneuver
if(currentManeuver == null) {
if (currentManeuver == null) {
return;
}
const bgi = currentManeuver.begin_shape_index;
@ -212,7 +242,7 @@ export async function startNavigation(trip: Trip) {
updateRouteStatus({
time: trip.summary.time,
distance: trip.summary.length,
currentManeuver
currentManeuver,
});
}
@ -228,7 +258,7 @@ export async function startNavigation(trip: Trip) {
// pastRoute.push(...polyline.slice(0, bgi + 1));
pastRoute = polyline.slice(0, bgi + 1);
updateNavigationLayer("route-past", pastRoute.flat())
updateNavigationLayer("route-past", pastRoute.flat());
// Remove from shape begin to end from the route line
const newShape = polyline.slice(bgi);
@ -242,11 +272,14 @@ export async function startNavigation(trip: Trip) {
return;
}
const maneuver = trip.legs[0].maneuvers[instructionIdx];
updateRouteStatus({
updateRouteStatus(
{
time: trip.summary.time,
distance: trip.summary.length,
currentManeuver: trip.legs[0].maneuvers[instructionIdx],
}, maneuver.lanes);
},
maneuver.lanes,
);
currentManeuver = maneuver;
// document.querySelector<HTMLDivElement>("#lanes")!.innerHTML = "";
@ -266,22 +299,27 @@ export async function startNavigation(trip: Trip) {
if (instructionIdx > 0) {
const prevManeuver = trip.legs[0].maneuvers[instructionIdx - 1];
if (prevManeuver.verbal_post_transition_instruction) {
console.log("Saying: " + prevManeuver.verbal_post_transition_instruction);
console.log(
"Saying: " + prevManeuver.verbal_post_transition_instruction,
);
say(prevManeuver.verbal_post_transition_instruction);
}
}
}, 1000);
updateRouteStatus({
updateRouteStatus(
{
time: trip.summary.time,
distance: trip.summary.length,
currentManeuver: trip.legs[0].maneuvers[0],
}, trip.legs[0].maneuvers[0].lanes);
},
trip.legs[0].maneuvers[0].lanes,
);
currentTrip = trip;
}
export async function stopNavigation() {
if(int) clearInterval(int);
if (int) clearInterval(int);
await KeepAwake.allowSleep();
hideRouteStatus();
document.querySelector<HTMLBodyElement>("body")!.classList.remove("isInTrip");
@ -320,7 +358,7 @@ export function decodePolyline(encoded: string): WorldLocation[] {
shift += 5;
} while (byte >= 0x20);
let deltaLat = (result & 1) ? ~(result >> 1) : (result >> 1);
let deltaLat = result & 1 ? ~(result >> 1) : result >> 1;
lat += deltaLat;
shift = 0;
@ -331,7 +369,7 @@ export function decodePolyline(encoded: string): WorldLocation[] {
shift += 5;
} while (byte >= 0x20);
let deltaLng = (result & 1) ? ~(result >> 1) : (result >> 1);
let deltaLng = result & 1 ? ~(result >> 1) : result >> 1;
lng += deltaLng;
// Convert the latitude and longitude to decimal format with six digits of precision

View File

@ -8,7 +8,7 @@ export type ValhallaCosting =
| "motor_scooter"
| "multimodal"
| "pedestrian";
export type ValhallaRequest = {
export interface ValhallaRequest {
locations: WorldLocation[];
costing: ValhallaCosting;
units: "miles" | "kilometers";
@ -16,8 +16,8 @@ export type ValhallaRequest = {
alternates: number;
costing_options: ValhallaCostingOptions;
turn_lanes: boolean;
};
export type GeneralCostingOptions = {
}
export interface GeneralCostingOptions {
/**
* A penalty applied when transitioning between roads that do not have consistent
* naming - in other words, no road names in common. This penalty can be used to
@ -59,7 +59,7 @@ export type GeneralCostingOptions = {
* @default 0 for trucks, 15 for cars, buses, motor scooters and motorcycles
*/
service_penalty?: number;
};
}
export type AutomobileCostingOptions = {
/**
* A penalty applied when a gate or bollard with access=private is encountered.
@ -226,7 +226,7 @@ export type AutomobileCostingOptions = {
*/
hierarchy_limits?: void;
} & GeneralCostingOptions;
export type OtherCostingOptions = {
export interface OtherCostingOptions {
/**
* The height of the vehicle (in meters).
* @default 1.9 for car, bus, taxi and 4.11 for truck
@ -267,7 +267,7 @@ export type OtherCostingOptions = {
* @default false
*/
include_hot?: boolean;
};
}
/**
* The type of the bicycle.
* Road: a road-style bicycle with narrow tires that is generally lightweight and designed for speed on paved surfaces.
@ -276,7 +276,7 @@ export type OtherCostingOptions = {
* Mountain: a mountain bicycle suitable for most surfaces but generally heavier and slower on paved surfaces.
*/
export type BicycleType = "Road" | "Hybrid" | "City" | "Mountain";
export type BicycleCostingOptions = {
export interface BicycleCostingOptions {
/**
* @default "Hybrid"
*/
@ -367,9 +367,8 @@ export type BicycleCostingOptions = {
* @default false
*/
shortest?: boolean;
};
export type BikeshareCostingOptions = {}; // TODO
}
export type BikeshareCostingOptions = unknown; // TODO
export type MotorScooterCostingOptions = {
/**
* A rider's propensity to use primary roads.
@ -398,9 +397,9 @@ export type MotorScooterCostingOptions = {
*/
use_hills?: boolean;
} & AutomobileCostingOptions;
export type MultimodalCostingOptions = {}; // TODO
export type PedestrianCostingOptions = {}; // TODO
export type TruckCostingOptions = {
export type MultimodalCostingOptions = unknown; // TODO
export type PedestrianCostingOptions = unknown; // TODO
export interface TruckCostingOptions {
/**
* The length of the truck (in meters).
* @default 21.64
@ -448,8 +447,8 @@ export type TruckCostingOptions = {
* @default 0
*/
use_truck_route?: boolean;
};
export type ValhallaCostingOptions = {
}
export interface ValhallaCostingOptions {
auto?: AutomobileCostingOptions & OtherCostingOptions;
bicycle?: BicycleCostingOptions;
bus?: AutomobileCostingOptions & OtherCostingOptions;
@ -459,4 +458,4 @@ export type ValhallaCostingOptions = {
motor_scooter?: MotorScooterCostingOptions;
multimodal?: MultimodalCostingOptions;
pedestrian?: PedestrianCostingOptions;
};
}

View File

@ -1,15 +1,18 @@
type Language = "de-DE" | "en-US";
type WorldLocation = { lat: number; lon: number };
interface WorldLocation {
lat: number;
lon: number;
}
type Units = "kilometers" | "miles";
type RouteResult = {
interface RouteResult {
alternates?: {
trip: Trip;
}[];
trip: Trip;
}
type Trip = {
interface Trip {
language: Language;
legs: Leg[];
status: number;
@ -17,16 +20,16 @@ type Trip = {
summary: Summary;
units: Units;
locations: WorldLocation[];
};
}
type Leg = {
interface Leg {
maneuvers: Maneuver[];
shape: string;
summary: Summary;
locations: WorldLocation[];
}
type Summary = {
interface Summary {
cost: number;
has_ferry: boolean;
has_highway: boolean;
@ -55,13 +58,13 @@ type Summary = {
* 512 = MergeToLeft
* 1024 = MergeToRight
*/
type Lane = {
interface Lane {
directions: number;
valid: number;
active: number;
};
}
type Maneuver = {
interface Maneuver {
bearing_after: number;
begin_shape_index: number;
cost: number;
@ -78,4 +81,4 @@ type Maneuver = {
verbal_pre_transition_instruction: string;
verbal_succinct_transition_instruction: string;
lanes?: Lane[];
};
}

View File

@ -18,8 +18,8 @@ export const routing = $state({
int: null as NodeJS.Timeout | null,
isOffRoute: false,
currentManeuver: null as Maneuver | null,
}
})
},
});
export function resetRouting() {
routing.geojson.route = null;
@ -30,8 +30,9 @@ export function resetRouting() {
export async function fetchRoute(server: string, request: ValhallaRequest) {
try {
const res = await fetch(server + "/route?json=" + JSON.stringify(request))
.then((res) => res.json());
const res = await fetch(
server + "/route?json=" + JSON.stringify(request),
).then((res) => res.json());
console.log(res);
return res;
@ -65,9 +66,9 @@ function geometryToGeoJSON(polyline: WorldLocation[]): GeoJSON.Feature {
}
export function decodePolyline(encoded: string): WorldLocation[] {
let points = [];
const points = [];
let index = 0;
let len = encoded.length;
const len = encoded.length;
let lat = 0;
let lng = 0;
@ -81,7 +82,7 @@ export function decodePolyline(encoded: string): WorldLocation[] {
shift += 5;
} while (byte >= 0x20);
let deltaLat = (result & 1) ? ~(result >> 1) : (result >> 1);
const deltaLat = result & 1 ? ~(result >> 1) : result >> 1;
lat += deltaLat;
shift = 0;
@ -92,7 +93,7 @@ export function decodePolyline(encoded: string): WorldLocation[] {
shift += 5;
} while (byte >= 0x20);
let deltaLng = (result & 1) ? ~(result >> 1) : (result >> 1);
const deltaLng = result & 1 ? ~(result >> 1) : result >> 1;
lng += deltaLng;
// Convert the latitude and longitude to decimal format with six digits of precision
@ -108,8 +109,8 @@ export function decodePolyline(encoded: string): WorldLocation[] {
export function drawAllRoutes(trips: Trip[]) {
routing.geojson.routePast = null;
routing.geojson.route = tripToGeoJSON(trips[0]);
if(trips[1]) routing.geojson.al0 = tripToGeoJSON(trips[1]);
if(trips[2]) routing.geojson.al1 = tripToGeoJSON(trips[2]);
if (trips[1]) routing.geojson.al0 = tripToGeoJSON(trips[1]);
if (trips[2]) routing.geojson.al1 = tripToGeoJSON(trips[2]);
}
export function drawRoute(trip: Trip) {
@ -119,7 +120,9 @@ export function drawRoute(trip: Trip) {
function drawCurrentTrip() {
if (!routing.currentTrip) return;
routing.geojson.route = geometryToGeoJSON(routing.currentTripInfo.route);
routing.geojson.routePast = geometryToGeoJSON(routing.currentTripInfo.past.flat());
routing.geojson.routePast = geometryToGeoJSON(
routing.currentTripInfo.past.flat(),
);
}
export async function startRoute(trip: Trip) {
@ -131,7 +134,8 @@ export async function startRoute(trip: Trip) {
routing.currentTripInfo.isOffRoute = false;
drawRoute(trip);
routing.currentTripInfo.currentManeuver = routing.currentTrip.legs[0].maneuvers[0];
routing.currentTripInfo.currentManeuver =
routing.currentTrip.legs[0].maneuvers[0];
routing.currentTripInfo.int = setInterval(tickRoute, 500);
}
@ -143,7 +147,8 @@ async function tickRoute() {
const info = routing.currentTripInfo;
if (!trip) return;
const currentManeuver = trip.legs[0].maneuvers[routing.currentTripInfo.maneuverIdx];
const currentManeuver =
trip.legs[0].maneuvers[routing.currentTripInfo.maneuverIdx];
if (!currentManeuver) {
// No more maneuvers, stop navigation
stopNavigation();
@ -155,14 +160,14 @@ async function tickRoute() {
const polyline = decodePolyline(trip.legs[0].shape);
// Check if the user location is on the last point of the entire route
if(isOnPoint(location, polyline[polyline.length - 1])) {
if (isOnPoint(location, polyline[polyline.length - 1])) {
console.log("Reached destination!");
stopNavigation();
return;
}
// Check if the user is on the route
if(!isOnShape(location, polyline)) {
if (!isOnShape(location, polyline)) {
console.log("Off route!");
info.isOffRoute = true;
// TODO: Implement re-routing logic
@ -171,18 +176,24 @@ async function tickRoute() {
info.isOffRoute = false;
}
if (currentManeuver.verbal_pre_transition_instruction && !hasAnnouncedPreInstruction) {
if (
currentManeuver.verbal_pre_transition_instruction &&
!hasAnnouncedPreInstruction
) {
const distanceToEnd = calculateDistance(location, polyline[bgi]);
// console.log("Distance to end of current maneuver: ", distanceToEnd, " meters");
if (distanceToEnd <= 100) {
hasAnnouncedPreInstruction = true;
console.log("[Verbal instruction] ", currentManeuver.verbal_pre_transition_instruction);
console.log(
"[Verbal instruction] ",
currentManeuver.verbal_pre_transition_instruction,
);
}
}
// Check if the user is past the current maneuver
// Checks if the user is still on the current maneuver's polyline
if(!isOnShape(location, polyline.slice(bgi))) {
if (!isOnShape(location, polyline.slice(bgi))) {
return; // User is not on the current maneuver's polyline, do not update
}
@ -196,15 +207,25 @@ async function tickRoute() {
// announce the "verbal_post_transition_instruction"
if (currentManeuver.verbal_post_transition_instruction) {
hasAnnouncedPreInstruction = false;
const distanceToEnd = calculateDistance(location, polyline[trip.legs[0].maneuvers[routing.currentTripInfo.maneuverIdx + 1].begin_shape_index]);
const distanceToEnd = calculateDistance(
location,
polyline[
trip.legs[0].maneuvers[routing.currentTripInfo.maneuverIdx + 1]
.begin_shape_index
],
);
if (distanceToEnd >= 200) {
console.log("[Verbal instruction] ", currentManeuver.verbal_post_transition_instruction);
console.log(
"[Verbal instruction] ",
currentManeuver.verbal_post_transition_instruction,
);
}
}
// Advance to the next maneuver
info.maneuverIdx++;
if(info.maneuverIdx >= trip.legs[0].maneuvers.length) { // No more maneuvers
if (info.maneuverIdx >= trip.legs[0].maneuvers.length) {
// No more maneuvers
stopNavigation();
return;
}
@ -228,8 +249,8 @@ function getUserLocation(): WorldLocation {
// return geolocate.currentLocation!;
return {
lat: location.lat,
lon: location.lng
}
lon: location.lng,
};
// const lnglat = window.geolocate._userLocationDotMarker.getLngLat();
// return { lat: lnglat.lat, lon: lnglat.lng };
// console.log(map.value!)
@ -239,7 +260,11 @@ function getUserLocation(): WorldLocation {
// }
}
function isOnLine(location: WorldLocation, from: WorldLocation, to: WorldLocation) {
function isOnLine(
location: WorldLocation,
from: WorldLocation,
to: WorldLocation,
) {
// Convert the 12-meter tolerance to degrees (approximation)
const tolerance = 12 / 111320; // 1 degree latitude ≈ 111.32 km
@ -248,7 +273,9 @@ function isOnLine(location: WorldLocation, from: WorldLocation, to: WorldLocatio
const dy = to.lat - from.lat;
// Calculate the projection of the location onto the line segment
const t = ((location.lon - from.lon) * dx + (location.lat - from.lat) * dy) / (dx * dx + dy * dy);
const t =
((location.lon - from.lon) * dx + (location.lat - from.lat) * dy) /
(dx * dx + dy * dy);
// Clamp t to the range [0, 1] to ensure the projection is on the segment
const clampedT = Math.max(0, Math.min(1, t));
@ -261,7 +288,8 @@ function isOnLine(location: WorldLocation, from: WorldLocation, to: WorldLocatio
// Calculate the distance from the location to the closest point
const distance = Math.sqrt(
Math.pow(location.lon - closestPoint.lon, 2) + Math.pow(location.lat - closestPoint.lat, 2)
Math.pow(location.lon - closestPoint.lon, 2) +
Math.pow(location.lat - closestPoint.lat, 2),
);
// Check if the distance is within the tolerance
@ -273,7 +301,8 @@ function isOnPoint(location: WorldLocation, point: WorldLocation) {
const tolerance = 6 / 111320; // 1 degree latitude ≈ 111.32 km
// Calculate the distance from the location to the point
const distance = Math.sqrt(
Math.pow(location.lon - point.lon, 2) + Math.pow(location.lat - point.lat, 2)
Math.pow(location.lon - point.lon, 2) +
Math.pow(location.lat - point.lat, 2),
);
// Check if the distance is within the tolerance
return distance <= tolerance;
@ -289,23 +318,36 @@ function isOnShape(location: WorldLocation, shape: WorldLocation[]) {
return false;
}
function calculateDistance(point1: WorldLocation, point2: WorldLocation): number {
function calculateDistance(
point1: WorldLocation,
point2: WorldLocation,
): number {
const R = 6371000; // Earth's radius in meters
const lat1 = point1.lat * (Math.PI / 180);
const lat2 = point2.lat * (Math.PI / 180);
const deltaLat = (point2.lat - point1.lat) * (Math.PI / 180);
const deltaLon = (point2.lon - point1.lon) * (Math.PI / 180);
const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
Math.cos(lat1) * Math.cos(lat2) *
Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
const a =
Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
Math.cos(lat1) *
Math.cos(lat2) *
Math.sin(deltaLon / 2) *
Math.sin(deltaLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // Distance in meters
}
export function zoomToPoints(from: WorldLocation, to: WorldLocation, map: maplibregl.Map) {
const getBoundingBox = (point1: [number, number], point2: [number, number]): LngLatBoundsLike => {
export function zoomToPoints(
from: WorldLocation,
to: WorldLocation,
map: maplibregl.Map,
) {
const getBoundingBox = (
point1: [number, number],
point2: [number, number],
): LngLatBoundsLike => {
const [lng1, lat1] = point1;
const [lng2, lat2] = point2;
@ -316,7 +358,7 @@ export function zoomToPoints(from: WorldLocation, to: WorldLocation, map: maplib
};
map.fitBounds(getBoundingBox([from.lon, from.lat], [to.lon, to.lat]), {
padding: 40
padding: 40,
});
}

View File

@ -3,7 +3,7 @@
import { getOIDCConfig, hasCapability } from "./lnv";
export async function getAuthURL() {
if(!await hasCapability("auth")) {
if (!(await hasCapability("auth"))) {
throw new Error("Server does not support OIDC authentication");
}
const oidcConfig = await getOIDCConfig();
@ -17,8 +17,7 @@ export async function getAuthURL() {
const state = generateRandomString(16);
return {
url:
`${AUTH_URL}?client_id=${CLIENT_ID}&response_type=code&redirect_uri=${window.location.origin}/login/callback&scope=openid%20profile&code_challenge=${pkce.codeChallenge}&code_challenge_method=S256&state=${state}`,
url: `${AUTH_URL}?client_id=${CLIENT_ID}&response_type=code&redirect_uri=${window.location.origin}/login/callback&scope=openid%20profile&code_challenge=${pkce.codeChallenge}&code_challenge_method=S256&state=${state}`,
codeVerifier: pkce.codeVerifier,
state,
};
@ -41,7 +40,9 @@ function generateRandomString(length: number) {
window.crypto.getRandomValues(array);
return Array.from(array, (dec) => ("0" + dec.toString(16)).substr(-2)).join("");
return Array.from(array, (dec) => ("0" + dec.toString(16)).substr(-2)).join(
"",
);
}
// Encodes a string to base64url (no padding)
@ -60,7 +61,7 @@ async function sha256(input: string | undefined): Promise<ArrayBuffer> {
}
export async function getOIDCUser(code: string, codeVerifier: string) {
if(!await hasCapability("auth")) {
if (!(await hasCapability("auth"))) {
throw new Error("Server does not support OIDC authentication");
}
const oidcConfig = await getOIDCConfig();
@ -81,8 +82,8 @@ export async function getOIDCUser(code: string, codeVerifier: string) {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: params
}).then(res => res.json());
body: params,
}).then((res) => res.json());
return res;
// return JSON.parse(atob(id_token.split(".")[1]));

View File

@ -8,6 +8,10 @@ export function cn(...inputs: ClassValue[]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
export type WithoutChildren<T> = T extends { children?: any }
? Omit<T, "children">
: T;
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & {
ref?: U | null;
};

View File

@ -1,4 +1,8 @@
import type { ValhallaCosting, ValhallaCostingOptions, ValhallaRequest } from "$lib/services/navigation/ValhallaRequest";
import type {
ValhallaCosting,
ValhallaCostingOptions,
ValhallaRequest,
} from "$lib/services/navigation/ValhallaRequest";
import type { Vehicle } from "./vehicles.svelte";
function getVehicleCosting(vehicle: Vehicle): ValhallaCosting {
@ -17,31 +21,43 @@ function getVehicleCosting(vehicle: Vehicle): ValhallaCosting {
}
}
export function createValhallaRequest(vehicle: Vehicle, locations: WorldLocation[]): ValhallaRequest {
export function createValhallaRequest(
vehicle: Vehicle,
locations: WorldLocation[],
): ValhallaRequest {
const costing = getVehicleCosting(vehicle);
let costingOptions: ValhallaCostingOptions = costing == "auto" ? {
const costingOptions: ValhallaCostingOptions =
costing == "auto"
? {
auto: {
top_speed: vehicle.legalMaxSpeed,
fixed_speed: vehicle.actualMaxSpeed
fixed_speed: vehicle.actualMaxSpeed,
},
}
} : costing == "motor_scooter" ? {
: costing == "motor_scooter"
? {
motor_scooter: {
top_speed: vehicle.legalMaxSpeed,
fixed_speed: vehicle.actualMaxSpeed
fixed_speed: vehicle.actualMaxSpeed,
},
}
} : costing == "truck" ? {
: costing == "truck"
? {
truck: {
top_speed: vehicle.legalMaxSpeed,
fixed_speed: vehicle.actualMaxSpeed,
length: vehicle.length,
weight: vehicle.weight,
axle_load: vehicle.axisLoad
axle_load: vehicle.axisLoad,
},
}
} : costing == "bicycle" ? {
: costing == "bicycle"
? {
bicycle: {
cycling_speed: vehicle.actualMaxSpeed
cycling_speed: vehicle.actualMaxSpeed,
},
}
} : {};
: {};
return {
locations,
costing,
@ -49,6 +65,6 @@ export function createValhallaRequest(vehicle: Vehicle, locations: WorldLocation
alternates: 2,
language: "de-DE",
costing_options: costingOptions,
turn_lanes: true
}
turn_lanes: true,
};
}

View File

@ -6,19 +6,42 @@ bicycle, prefer cycleways and bicycle lanes = bicycle
truck, prioritizes truck routes = truck
motor_scooter = motor scooter, moped, lkfz
*/
export const VehicleTypes = ["car", "truck", "motorcycle", "bicycle", "motor_scooter"] as const;
export type VehicleType = typeof VehicleTypes[number];
export const FuelTypes = ["petrol", "diesel", "electric"] as const;
export type FuelType = typeof FuelTypes[number];
export const EVConnectors = [
"Type 2", "CCS", "CHAdeMO", "Tesla Supercharger", "Type 1", "Type 3",
"SEV 1011 (Type 13)", "SEV 1011 (Type 15)", "SEV 1011 (Type 23)", "SEV 1011 (Type 25)",
"CEE (red)", "CEE (blue)", "Schuko", "CCS Type 2", "Other"
export const VehicleTypes = [
"car",
"truck",
"motorcycle",
"bicycle",
"motor_scooter",
] as const;
export type EVConnector = typeof EVConnectors[number];
export const PreferredFuels = ["Super", "Super E10", "Diesel", ...EVConnectors] as const;
export type PreferredFuel = typeof PreferredFuels[number];
export type Vehicle = {
export type VehicleType = (typeof VehicleTypes)[number];
export const FuelTypes = ["petrol", "diesel", "electric"] as const;
export type FuelType = (typeof FuelTypes)[number];
export const EVConnectors = [
"Type 2",
"CCS",
"CHAdeMO",
"Tesla Supercharger",
"Type 1",
"Type 3",
"SEV 1011 (Type 13)",
"SEV 1011 (Type 15)",
"SEV 1011 (Type 23)",
"SEV 1011 (Type 25)",
"CEE (red)",
"CEE (blue)",
"Schuko",
"CCS Type 2",
"Other",
] as const;
export type EVConnector = (typeof EVConnectors)[number];
export const PreferredFuels = [
"Super",
"Super E10",
"Diesel",
...EVConnectors,
] as const;
export type PreferredFuel = (typeof PreferredFuels)[number];
export interface Vehicle {
name: string;
legalMaxSpeed: number;
actualMaxSpeed: number;
@ -31,7 +54,7 @@ export type Vehicle = {
emissionClass: string;
fuelType: FuelType;
preferredFuel: PreferredFuel;
};
}
export const DefaultVehicle: Vehicle = {
name: "Default Vehicle",
@ -40,49 +63,64 @@ export const DefaultVehicle: Vehicle = {
type: "motor_scooter",
emissionClass: "Euro 4",
fuelType: "diesel",
preferredFuel: "Diesel"
}
preferredFuel: "Diesel",
};
type StateValue<T> = {v: T};
export const vehicles: Vehicle[] = $state(localStorage.getItem("vehicles") ? JSON.parse(localStorage.getItem("vehicles")!) : []);
interface StateValue<T> {
v: T;
}
export const vehicles: Vehicle[] = $state(
localStorage.getItem("vehicles")
? JSON.parse(localStorage.getItem("vehicles")!)
: [],
);
export const selectedVehicleIdx: StateValue<number | null> = $state({
v: localStorage.getItem("selectedVehicle") ? parseInt(localStorage.getItem("selectedVehicle")!) : null
v: localStorage.getItem("selectedVehicle")
? parseInt(localStorage.getItem("selectedVehicle")!)
: null,
});
export const selectedVehicle: () => Vehicle | null = () => {
return vehicles[selectedVehicleIdx.v !== null ? selectedVehicleIdx.v : 0] || null
return (
vehicles[selectedVehicleIdx.v !== null ? selectedVehicleIdx.v : 0] || null
);
};
export function setVehicles(_vehicles: Vehicle[]) {
// vehicles = _vehicles;
// Hack to update without reassigning the array
vehicles.length = 0;
_vehicles.forEach(vehicle => vehicles.push(vehicle));
_vehicles.forEach((vehicle) => vehicles.push(vehicle));
localStorage.setItem("vehicles", JSON.stringify(vehicles));
}
export function selectVehicle(vehicle: Vehicle | null) {
if(vehicle == null) {
if (vehicle == null) {
selectedVehicleIdx.v = null;
} else {
selectedVehicleIdx.v = vehicles.findIndex(v => v.name === vehicle.name);
if(selectedVehicleIdx.v === -1) {
selectedVehicleIdx.v = vehicles.findIndex((v) => v.name === vehicle.name);
if (selectedVehicleIdx.v === -1) {
selectedVehicleIdx.v = null;
}
}
localStorage.setItem("selectedVehicle", selectedVehicleIdx.v !== null ? selectedVehicleIdx.v.toString() : "");
localStorage.setItem(
"selectedVehicle",
selectedVehicleIdx.v !== null ? selectedVehicleIdx.v.toString() : "",
);
}
/**
* Check if the vehicle uses the correct preferred fuel type
*/
export function isValidFuel(vehicle: Vehicle): boolean {
if(vehicle.fuelType == "petrol") {
return vehicle.preferredFuel == "Super" || vehicle.preferredFuel == "Super E10";
if (vehicle.fuelType == "petrol") {
return (
vehicle.preferredFuel == "Super" || vehicle.preferredFuel == "Super E10"
);
}
if(vehicle.fuelType == "diesel") {
if (vehicle.fuelType == "diesel") {
return vehicle.preferredFuel == "Diesel";
}
if(vehicle.fuelType == "electric") {
if (vehicle.fuelType == "electric") {
return EVConnectors.includes(vehicle.preferredFuel as EVConnector);
}
return false;

View File

@ -1,18 +1,22 @@
export function checkWebGL() {
if(window.WebGLRenderingContext) {
if (window.WebGLRenderingContext) {
const canvas = document.createElement("canvas");
try {
const ctx = canvas.getContext("webgl2") || canvas.getContext("webgl");
if(ctx && typeof ctx.getParameter == "function") {
if (ctx && typeof ctx.getParameter == "function") {
return true;
}
} catch(e) {
} catch (_e) {
// Supported, but disabled
alert("WebGL is supported but disabled in your browser. Please enable it in your settings.")
alert(
"WebGL is supported but disabled in your browser. Please enable it in your settings.",
);
}
return false;
}
// WebGL is not supported
alert("WebGL is not supported in your browser. Please try a different browser.");
alert(
"WebGL is not supported in your browser. Please try a different browser.",
);
return false;
}

View File

@ -1,21 +1,25 @@
import { mount } from 'svelte'
import './app.css'
import App from './App.svelte'
import { mount } from "svelte";
import "./app.css";
import App from "./App.svelte";
if(location.href.includes("/login/callback")) {
if (location.href.includes("/login/callback")) {
const url = new URL(location.href);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
if(code && state) {
window.opener.postMessage({
code, state
}, window.location.origin);
if (code && state) {
window.opener.postMessage(
{
code,
state,
},
window.location.origin,
);
window.close();
}
}
const app = mount(App, {
target: document.getElementById('app')!,
})
target: document.getElementById("app")!,
});
export default app
export default app;

View File

@ -1,10 +1,10 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
compilerOptions: {
runes: true
}
}
runes: true,
},
};

View File

@ -1,6 +1,6 @@
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import path from "path";
export default defineConfig({