diff --git a/docker-compose.yml b/docker-compose.yml index f3814097..1fc47c02 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -71,7 +71,7 @@ services: volumes: - ./data/valhalla:/custom_files environment: - - tile_urls=https://download.geofabrik.de/europe/germany-latest.osm.pbf https://download.geofabrik.de/europe/germany/bayern/oberbayern-latest.osm.pbf + - tile_urls=https://download.geofabrik.de/europe/germany/bayern/oberbayern-latest.osm.pbf - use_tiles_ignore_pbf=True - force_rebuild=False - force_rebuild_elevation=False diff --git a/web/package-lock.json b/web/package-lock.json index ad062b10..55fd021a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "wanderer", - "version": "0.4.0s", + "version": "0.4.0", "dependencies": { "@fortawesome/fontawesome-free": "^6.5.1", "@sveltejs/adapter-node": "^4.0.1", @@ -16,7 +16,7 @@ "@types/three": "^0.161.2", "canvg": "^4.0.1", "crypto-random-string": "^5.0.0", - "jsdom": "^24.0.0", + "isomorphic-xml2js": "^0.1.3", "jspdf": "^2.5.1", "leaflet": "^1.9.4", "leaflet-gpx": "^1.7.0", @@ -36,7 +36,6 @@ "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", - "@types/jsdom": "^21.1.6", "@types/leaflet": "^1.9.8", "@types/leaflet-gpx": "^1.3.7", "@types/node": "^20.11.25", @@ -1073,17 +1072,6 @@ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.13.tgz", "integrity": "sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ==" }, - "node_modules/@types/jsdom": { - "version": "21.1.6", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.6.tgz", - "integrity": "sha512-/7kkMsC+/kMs7gAYmmBR9P0vGTnOoLhQhyhQJSlXGI5bzTHp6xdo0TtKWQAsz6pmSAeVqKSbqeyP6hytqr9FDw==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^7.0.0" - } - }, "node_modules/@types/leaflet": { "version": "1.9.8", "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.8.tgz", @@ -1113,7 +1101,6 @@ "version": "20.11.25", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz", "integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==", - "devOptional": true, "dependencies": { "undici-types": "~5.26.4" } @@ -1155,17 +1142,19 @@ "meshoptimizer": "~0.18.1" } }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", - "dev": true - }, "node_modules/@types/webxr": { "version": "0.5.14", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.14.tgz", "integrity": "sha512-UEMMm/Xn3DtEa+gpzUrOcDj+SJS1tk5YodjwOxcqStNhCfPcwgyC5Srg2ToVKyg2Fhq16Ffpb0UWUQHqoT9AMA==" }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@vitest/expect": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.1.tgz", @@ -1259,6 +1248,9 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "debug": "^4.3.4" }, @@ -1350,7 +1342,10 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "optional": true, + "peer": true }, "node_modules/atob": { "version": "2.1.2", @@ -1840,6 +1835,9 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -1964,6 +1962,9 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz", "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "rrweb-cssom": "^0.6.0" }, @@ -1984,6 +1985,9 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" @@ -1996,6 +2000,9 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "punycode": "^2.3.1" }, @@ -2007,6 +2014,9 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=12" } @@ -2015,6 +2025,9 @@ "version": "14.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "tr46": "^5.0.0", "webidl-conversions": "^7.0.0" @@ -2050,7 +2063,10 @@ "node_modules/decimal.js": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true, + "optional": true, + "peer": true }, "node_modules/deep-eql": { "version": "4.1.3", @@ -2139,6 +2155,9 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=0.4.0" } @@ -2229,6 +2248,9 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=0.12" }, @@ -2546,6 +2568,9 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -2778,6 +2803,9 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "whatwg-encoding": "^3.1.1" }, @@ -2802,6 +2830,9 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -2814,6 +2845,9 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "agent-base": "^7.0.2", "debug": "4" @@ -2835,6 +2869,9 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -3094,7 +3131,10 @@ "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "optional": true, + "peer": true }, "node_modules/is-promise": { "version": "2.2.2", @@ -3226,6 +3266,15 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isomorphic-xml2js": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/isomorphic-xml2js/-/isomorphic-xml2js-0.1.3.tgz", + "integrity": "sha512-dIkT2U9ritKVWF/HfHfGwm5tTnlMnknYsv7l12oJlQQgOV2CNV65pX+FHy6HFL9YP8q0JcrlNQAFRJIN2agUmQ==", + "dependencies": { + "@types/xml2js": "^0.4.2", + "xml2js": "^0.4.19" + } + }, "node_modules/jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", @@ -3262,6 +3311,9 @@ "version": "24.0.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.0.0.tgz", "integrity": "sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "cssstyle": "^4.0.1", "data-urls": "^5.0.0", @@ -3301,6 +3353,9 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "punycode": "^2.3.1" }, @@ -3312,6 +3367,9 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=12" } @@ -3320,6 +3378,9 @@ "version": "14.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "tr46": "^5.0.0", "webidl-conversions": "^7.0.0" @@ -3570,6 +3631,9 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -3578,6 +3642,9 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -3797,7 +3864,10 @@ "node_modules/nwsapi": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", - "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==" + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "dev": true, + "optional": true, + "peer": true }, "node_modules/object-assign": { "version": "4.1.1", @@ -3957,6 +4027,9 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "entities": "^4.4.0" }, @@ -4329,12 +4402,18 @@ "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true, + "optional": true, + "peer": true }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=6" } @@ -4359,7 +4438,10 @@ "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "optional": true, + "peer": true }, "node_modules/queue-microtask": { "version": "1.2.3", @@ -4462,7 +4544,10 @@ "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "optional": true, + "peer": true }, "node_modules/resolve": { "version": "1.22.8", @@ -4558,7 +4643,10 @@ "node_modules/rrweb-cssom": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", - "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==" + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true, + "optional": true, + "peer": true }, "node_modules/run-parallel": { "version": "1.2.0", @@ -4597,7 +4685,10 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "optional": true, + "peer": true }, "node_modules/sander": { "version": "0.5.1", @@ -4611,10 +4702,18 @@ "rimraf": "^2.5.2" } }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "xmlchars": "^2.2.0" }, @@ -5155,7 +5254,10 @@ "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "optional": true, + "peer": true }, "node_modules/tailwindcss": { "version": "3.4.1", @@ -5330,6 +5432,9 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -5430,8 +5535,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "devOptional": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/unicode-properties": { "version": "1.4.1", @@ -5455,6 +5559,9 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -5493,6 +5600,9 @@ "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -5685,6 +5795,9 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "xml-name-validator": "^5.0.0" }, @@ -5706,6 +5819,9 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "optional": true, + "peer": true, "dependencies": { "iconv-lite": "0.6.3" }, @@ -5717,6 +5833,9 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=18" } @@ -5928,6 +6047,9 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -5948,14 +6070,40 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=18" } }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "optional": true, + "peer": true }, "node_modules/y18n": { "version": "4.0.3", diff --git a/web/package.json b/web/package.json index 8ff52264..04a5709c 100644 --- a/web/package.json +++ b/web/package.json @@ -17,7 +17,6 @@ "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", - "@types/jsdom": "^21.1.6", "@types/leaflet": "^1.9.8", "@types/leaflet-gpx": "^1.3.7", "@types/node": "^20.11.25", @@ -41,7 +40,7 @@ "@types/three": "^0.161.2", "canvg": "^4.0.1", "crypto-random-string": "^5.0.0", - "jsdom": "^24.0.0", + "isomorphic-xml2js": "^0.1.3", "jspdf": "^2.5.1", "leaflet": "^1.9.4", "leaflet-gpx": "^1.7.0", diff --git a/web/src/lib/components/trail/map_with_elevation.svelte b/web/src/lib/components/trail/map_with_elevation.svelte index 4db80dca..c69071fa 100644 --- a/web/src/lib/components/trail/map_with_elevation.svelte +++ b/web/src/lib/components/trail/map_with_elevation.svelte @@ -38,7 +38,7 @@ map = L.map("map", { preferCanvas: true }).setView( [trail?.lat ?? 0, trail?.lon ?? 0], - 3, + 16, ); map!.attributionControl.setPrefix(false); diff --git a/web/src/lib/models/gpx/bounds.ts b/web/src/lib/models/gpx/bounds.ts new file mode 100644 index 00000000..aea9c2f4 --- /dev/null +++ b/web/src/lib/models/gpx/bounds.ts @@ -0,0 +1,12 @@ +export default class Bounds { + minlat: number; + minlon: number; + maxlat: number; + maxlon: number; + constructor(object: {minlat: number, minlon: number, maxlat: number, maxlon: number}) { + this.minlat = object.minlat; + this.minlon = object.minlon; + this.maxlat = object.maxlat; + this.maxlon = object.maxlon; + } +} diff --git a/web/src/lib/models/gpx/copyright.ts b/web/src/lib/models/gpx/copyright.ts new file mode 100644 index 00000000..4052b085 --- /dev/null +++ b/web/src/lib/models/gpx/copyright.ts @@ -0,0 +1,10 @@ +export default class Copyright { + author: string; + year: number; + license: string; + constructor(object: { author: string, year: number, license: string }) { + this.author = object.author; + this.year = object.year; + this.license = object.license; + } +} \ No newline at end of file diff --git a/web/src/lib/models/gpx/gpx.ts b/web/src/lib/models/gpx/gpx.ts new file mode 100644 index 00000000..0ff39900 --- /dev/null +++ b/web/src/lib/models/gpx/gpx.ts @@ -0,0 +1,101 @@ +import * as xml2js from 'isomorphic-xml2js'; +import Metadata from './metadata'; +import Waypoint from './waypoint'; +import Route from './route'; +import Track from './track'; +import { removeEmpty, allDatesToISOString } from './utils'; + +const defaultAttributes = { + version: '1.1', + creator: 'wanderer', + xmlns: 'http://www.topografix.com/GPX/1/1', + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:schemaLocation': + 'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd' +} + +export default class GPX { + $: { + version: string; + creator: string; + xmlns: string; + 'xmlns:xsi': string; + 'xsi:schemaLocation': string; + } + extensions?: string; + metadata?: Metadata; + wpt?: Waypoint[]; + rte?: Route[]; + trk?: Track[]; + + constructor(object: { + $?: { + version: string, + creator: string, + xmlns: string, + 'xmlns:xsi': string, + 'xsi:schemaLocation': string, + } + extensions?: string, + metadata?: Metadata, + wpt?: Waypoint[] | Waypoint, + rte?: Route[] | Route, + trk?: Track[], + }) { + this.$ = Object.assign({}, defaultAttributes, object.$ || {}); + if (object.extensions) { + this.extensions = object.extensions; + } + + if (object.metadata) { + this.metadata = object.metadata; + } + if (object.wpt) { + if (!Array.isArray(object.wpt)) { + object.wpt = [object.wpt]; + } + this.wpt = object.wpt.map(wpt => new Waypoint(wpt)) + } + if (object.rte) { + if (!Array.isArray(object.rte)) { + object.rte = [object.rte]; + } + this.rte = object.rte.map(rte => new Route(rte)) + } + if (object.trk) { + if (!Array.isArray(object.trk)) { + object.trk = [object.trk]; + } + this.trk = object.trk.map(trk => new Track(trk)) + } + + removeEmpty(this); + } + + static parse(gpxString: string): Promise { + return new Promise((resolve, reject) => xml2js.parseString(gpxString, { + explicitArray: false + }, (err, xml) => { + if (err) { + reject(err); + } + const gpx = new GPX({ + $: xml.gpx.$, + metadata: xml.gpx.metadata, + wpt: xml.gpx.wpt, + rte: xml.gpx.rte, + trk: xml.gpx.trk + }); + resolve(gpx) + })); + } + + toString(options?: xml2js.BuilderOptions) { + options = options || {}; + options.rootName = 'gpx'; + + const builder = new xml2js.Builder(options), gpx = new GPX(this); + allDatesToISOString(gpx); + return builder.buildObject(gpx); + } +} \ No newline at end of file diff --git a/web/src/lib/models/gpx/link.ts b/web/src/lib/models/gpx/link.ts new file mode 100644 index 00000000..844ea861 --- /dev/null +++ b/web/src/lib/models/gpx/link.ts @@ -0,0 +1,13 @@ +export default class Link { + $: { + href?: string; + } + text: string; + type: string; + constructor(object: any) { + this.$ = {}; + this.$.href = object.$.href || object.href; + this.text = object.text; + this.type = object.type; + } +} \ No newline at end of file diff --git a/web/src/lib/models/gpx/metadata.ts b/web/src/lib/models/gpx/metadata.ts new file mode 100644 index 00000000..89516201 --- /dev/null +++ b/web/src/lib/models/gpx/metadata.ts @@ -0,0 +1,48 @@ +import Copyright from './copyright'; +import Link from './link'; +import Person from './person'; +import Bounds from './bounds'; + +export default class Metadata { + name: string; + desc: string; + time: Date; + keywords: string; + extensions: string; + author?: Person; + link?: Link[]; + bounds?: Bounds; + copyright?: Copyright; + constructor(object: { + name: string, + desc: string, + time: string, + keywords: string, + extensions: string, + author?: Person, + link?: Link | Link[], + bounds?: Bounds, + copyright?: Copyright + }) { + this.name = object.name; + this.desc = object.desc; + this.time = object.time ? new Date(object.time) : new Date(); + this.keywords = object.keywords; + this.extensions = object.extensions; + if (object.author) { + this.author = new Person(object.author); + } + if (object.link) { + if (!Array.isArray(object.link)) { + object.link = [object.link]; + } + this.link = object.link.map(l => new Link(l)); + } + if (object.bounds) { + this.bounds = new Bounds(object.bounds); + } + if (object.copyright) { + this.copyright = new Copyright(object.copyright); + } + } +} \ No newline at end of file diff --git a/web/src/lib/models/gpx/person.ts b/web/src/lib/models/gpx/person.ts new file mode 100644 index 00000000..ac72167e --- /dev/null +++ b/web/src/lib/models/gpx/person.ts @@ -0,0 +1,14 @@ +import Link from './link'; + +export default class Person { + name: string; + email: string; + link?: Link; + constructor(object: { name: string, email: string, link?: Link }) { + this.name = object.name; + this.email = object.email; + if (object.link) { + this.link = new Link(object.link); + } + } +} \ No newline at end of file diff --git a/web/src/lib/models/gpx/route.ts b/web/src/lib/models/gpx/route.ts new file mode 100644 index 00000000..869a9d11 --- /dev/null +++ b/web/src/lib/models/gpx/route.ts @@ -0,0 +1,45 @@ +import Waypoint from './waypoint'; +import Link from './link'; + +export default class Route { + name: string; + cmt: string; + desc: string; + src: string; + number: number; + type: string; + extensions: string; + link?: Link[]; + rtept?: Waypoint[]; + constructor(object: { + name: string, + cmt: string, desc: string, + src: string, + number: number, + type: string, + extensions: string, + link?: Link | Link[], + rtept?: Waypoint | Waypoint[] + }) { + this.name = object.name; + this.cmt = object.cmt; + this.desc = object.desc; + this.src = object.src; + this.number = object.number; + this.type = object.type; + this.extensions = object.extensions; + if (object.link) { + if (!Array.isArray(object.link)) { + object.link = [object.link]; + } + this.link = object.link.map(l => new Link(l)); + } + + if (object.rtept) { + if (!Array.isArray(object.rtept)) { + this.rtept = [object.rtept]; + } + this.rtept = (object.rtept as Waypoint[]).map(rtept => new Waypoint(rtept)) + } + } +} diff --git a/web/src/lib/models/gpx/track-segment.ts b/web/src/lib/models/gpx/track-segment.ts new file mode 100644 index 00000000..08d7d985 --- /dev/null +++ b/web/src/lib/models/gpx/track-segment.ts @@ -0,0 +1,15 @@ +import Waypoint from './waypoint'; + +export default class TrackSegment { + trkpt?: Waypoint[]; + extensions?: string; + constructor(object: { trkpt?: Waypoint[], extensions?: string }) { + if (object.trkpt) { + if (!Array.isArray(object.trkpt)) { + object.trkpt = [object.trkpt]; + } + this.trkpt = object.trkpt.map(trkpt => new Waypoint(trkpt)) + } + this.extensions = object.extensions; + } +} \ No newline at end of file diff --git a/web/src/lib/models/gpx/track.ts b/web/src/lib/models/gpx/track.ts new file mode 100644 index 00000000..efe5cd9c --- /dev/null +++ b/web/src/lib/models/gpx/track.ts @@ -0,0 +1,45 @@ +import TrackSegment from './track-segment'; +import Link from './link'; + +export default class Track { + name?: string; + cmt?: string; + desc?: string; + src?: string; + number?: number; + type?: string; + extensions?: string; + link?: Link[]; + trkseg?: TrackSegment[]; + constructor(object: { + name?: string, + cmt?: string, + desc?: string, + src?: string, + number?: number, + type?: string, + extensions?: string, + link?: Link | Link[], + trkseg?: TrackSegment | TrackSegment[] + }) { + this.name = object.name; + this.cmt = object.cmt; + this.desc = object.desc; + this.src = object.src; + this.number = object.number; + this.type = object.type; + this.extensions = object.extensions; + if (object.link) { + if (!Array.isArray(object.link)) { + object.link = [object.link]; + } + this.link = object.link.map(l => new Link(l)); + } + if (object.trkseg) { + if (!Array.isArray(object.trkseg)) { + object.trkseg = [object.trkseg]; + } + this.trkseg = object.trkseg.map(trkseg => new TrackSegment(trkseg));; + } + } +} \ No newline at end of file diff --git a/web/src/lib/models/gpx/utils.ts b/web/src/lib/models/gpx/utils.ts new file mode 100644 index 00000000..e7be40dc --- /dev/null +++ b/web/src/lib/models/gpx/utils.ts @@ -0,0 +1,23 @@ +function removeEmpty(obj: Record) { + Object.entries(obj).forEach(([key, val]) => { + if (val && val instanceof Object) { + removeEmpty(val); + } else if (val == null) { + delete obj[key]; + } + }); +} + +function allDatesToISOString(obj: Record) { + Object.entries(obj).forEach(([key, val]) => { + if (val) { + if (val instanceof Date) { + obj[key] = val.toISOString().split('.')[0] + 'Z'; + } else if (val instanceof Object) { + allDatesToISOString(val); + } + } + }); +} + +export { removeEmpty, allDatesToISOString }; diff --git a/web/src/lib/models/gpx/waypoint.ts b/web/src/lib/models/gpx/waypoint.ts new file mode 100644 index 00000000..5223230c --- /dev/null +++ b/web/src/lib/models/gpx/waypoint.ts @@ -0,0 +1,81 @@ +import Link from './link'; + +export default class Waypoint { + $: { + lat?: number; + lon?: number; + } + ele?: number; + time?: Date; + magvar?: string; + geoidheight?: string; + name?: string; + cmt?: string + desc?: string; + src?: string; + sym?: string; + type?: string; + sat?: string; + hdop?: string; + vdop?: string; + pdop?: string; + ageofdgpsdata?: string; + dgpsid?: string; + extensions?: string; + link?: Link[]; + constructor(object: { + lat?: number, + lon?: number, + $: { + lat?: number, + lon?: number + }, + ele?: number, + time?: Date, + magvar?: string, + geoidheight?: string, + name?: string, + cmt?: string, + desc?: string, + src?: string, + sym?: string, + type?: string, + sat?: string, + hdop?: string, + vdop?: string, + pdop?: string, + ageofdgpsdata?: string, + dgpsid?: string, + extensions?: string, + link?: Link[] + }) { + this.$ = {}; + this.$.lat = object.$.lat === 0 || object.lat === 0 ? 0 : object.$.lat || object.lat || -1; + this.$.lon = object.$.lon === 0 || object.lon === 0 ? 0 : object.$.lon || object.lon || -1; + this.ele = object.ele; + if (object.time) { + this.time = new Date(object.time); + } + this.magvar = object.magvar; + this.geoidheight = object.geoidheight; + this.name = object.name; + this.cmt = object.cmt; + this.desc = object.desc; + this.src = object.src; + this.sym = object.sym; + this.type = object.type; + this.sat = object.sat; + this.hdop = object.hdop; + this.vdop = object.vdop; + this.pdop = object.pdop; + this.ageofdgpsdata = object.ageofdgpsdata; + this.dgpsid = object.dgpsid; + this.extensions = object.extensions; + if (object.link) { + if (!Array.isArray(object.link)) { + object.link = [object.link]; + } + this.link = object.link.map(l => new Link(l)); + } + } +} \ No newline at end of file diff --git a/web/src/lib/models/valhalla.ts b/web/src/lib/models/valhalla.ts index 9350d6b7..265828de 100644 --- a/web/src/lib/models/valhalla.ts +++ b/web/src/lib/models/valhalla.ts @@ -1,31 +1,9 @@ interface ValhallaResponse { trip: { legs: { - shape: string[]; + shape: string; }[]; }; } -class ValhallaRoute { - type: "Feature"; - properties: {}; - geometry: { - type: "LineString"; - coordinates: number[][]; - }; - - constructor() { - this.type = "Feature" - this.properties = {} - this.geometry = { - type: "LineString", - coordinates: [] - } - } - - toString() { - return JSON.stringify(this); - } -} - -export {type ValhallaResponse, ValhallaRoute} \ No newline at end of file +export { type ValhallaResponse } \ No newline at end of file diff --git a/web/src/lib/stores/valhalla_store.ts b/web/src/lib/stores/valhalla_store.ts index 8ed1001b..dab4e6d4 100644 --- a/web/src/lib/stores/valhalla_store.ts +++ b/web/src/lib/stores/valhalla_store.ts @@ -1,13 +1,17 @@ -import { ValhallaRoute, type ValhallaResponse } from "$lib/models/valhalla"; +import GPX from "$lib/models/gpx/gpx"; +import type Track from "$lib/models/gpx/track"; +import TrackSegment from "$lib/models/gpx/track-segment"; +import Waypoint from "$lib/models/gpx/waypoint"; +import { type ValhallaResponse } from "$lib/models/valhalla"; import { ClientResponseError } from "pocketbase"; - -export const route: ValhallaRoute = new ValhallaRoute(); +const emtpyTrack: Track = { trkseg: [] } +export let route: GPX = new GPX({ trk: [emtpyTrack] }); export function clearRoute() { - route.geometry.coordinates = []; + route = new GPX({ trk: [emtpyTrack] }); } export async function calculateRouteBetween(startLat: number, startLon: number, endLat: number, endLon: number) { @@ -21,16 +25,30 @@ export async function calculateRouteBetween(startLat: number, startLon: number, throw new ClientResponseError(await r.json()) } const response: ValhallaResponse = await r.json(); + const points = decodeShape(response.trip.legs[0].shape); + const waypoints = points.map((p) => new Waypoint({ $: { lat: p[0], lon: p[1] } })) - appendToRoute(response); + return waypoints } -function appendToRoute(valhallaResponse: any) { - const routeGeometry = decodeShape(valhallaResponse.trip.legs[0].shape); +export async function appendToRoute(waypoints: Waypoint[]) { + const segment = new TrackSegment({ trkpt: [] }) - for (const point of routeGeometry) { - route.geometry.coordinates.push([point[1], point[0]]) + for (const wpt of waypoints) { + segment.trkpt!.push(wpt) } + route.trk?.at(0)?.trkseg?.push(segment); +} + +export async function editRoute(index: number, waypoints: Waypoint[]) { + const segment = route.trk?.at(0)?.trkseg?.at(index) + if (segment) { + segment.trkpt = waypoints + } +} + +export function deleteFromRoute(index: number) { + route.trk?.at(0)?.trkseg?.splice(index, 1); } diff --git a/web/src/lib/util/gpx_util.ts b/web/src/lib/util/gpx_util.ts index 2e5e5d36..b101fa92 100644 --- a/web/src/lib/util/gpx_util.ts +++ b/web/src/lib/util/gpx_util.ts @@ -1,8 +1,8 @@ +import GPX from "$lib/models/gpx/gpx"; import { Trail } from "$lib/models/trail"; import { Waypoint } from "$lib/models/waypoint"; -import { kml, tcx } from "$lib/vendor/toGeoJSON/toGeoJSON" -import GeoJsonToGpx from "$lib/vendor/geoJSONToGPX" -import { browser } from "$app/environment"; +import GeoJsonToGpx from "$lib/vendor/geoJSONToGPX"; +import { kml, tcx } from "$lib/vendor/toGeoJSON/toGeoJSON"; function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number { const R = 6371; // Radius of the Earth in km @@ -17,38 +17,23 @@ function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: numbe return distance; } -export async function gpx2trail(gpx: string) { - let xml; - if (browser) { - const parser = new DOMParser(); - xml = parser.parseFromString(gpx, "application/xml") - } else { - const JSDOM = (await import("jsdom")).JSDOM - xml = new JSDOM(gpx).window.document +export async function gpx2trail(gpxString: string) { + const gpx = await GPX.parse(gpxString); + + if (gpx instanceof Error) { + throw gpx; } const trail = new Trail(""); - let name = xml.getElementsByTagName('name'); - if (name.length > 0) { - trail.name = name[0].textContent ?? ""; - } - let desc = xml.getElementsByTagName('desc'); - if (desc.length > 0) { - trail.description = desc[0].textContent ?? ""; - } + trail.name = gpx.metadata?.name ?? ""; - const el = xml.getElementsByTagName('wpt'); - const elLength = el.length - for (let i = 0; i < elLength; i++) { - const wp = new Waypoint(parseFloat(el[i].getAttribute('lat')!), parseFloat(el[i].getAttribute('lon')!)); - - let nameEl = el[i].getElementsByTagName('name'); - wp.name = nameEl.length > 0 ? nameEl[0].textContent ?? "" : ''; - - let descEl = el[i].getElementsByTagName('desc'); - wp.description = descEl.length > 0 ? descEl[0].textContent ?? "" : ''; + trail.description = gpx.metadata?.desc; + for (const wpt of gpx.wpt ?? []) { + const wp = new Waypoint(wpt.$.lat ?? 0, wpt.$.lon ?? 0); + wp.name = wp.name + wp.description = wp.description; trail.expand.waypoints.push(wp); } @@ -56,17 +41,13 @@ export async function gpx2trail(gpx: string) { let totalDuration = 0; let totalDistance = 0; - const tracks = xml.getElementsByTagName("trk") - for (const track of tracks) { - const segments = track.getElementsByTagName("trkseg") - for (const segment of segments) { - const points = segment.getElementsByTagName("trkpt") + for (const track of gpx.trk ?? []) { + for (const segment of track.trkseg ?? []) { + const points = segment.trkpt ?? []; if (points.length >= 2) { - const startTimeString = points[0].querySelector('time')?.textContent; - const startTime = startTimeString ? new Date(startTimeString) : undefined; - const endTimeString = points[points.length - 1].querySelector('time')?.textContent; - const endTime = endTimeString ? new Date(endTimeString) : undefined; + const startTime = points[0].time; + const endTime = points[points.length - 1].time if (startTime && endTime) { totalDuration += endTime.getTime() - startTime.getTime(); @@ -79,30 +60,30 @@ export async function gpx2trail(gpx: string) { const pointLength = points.length for (let i = 1; i < pointLength; i++) { - const elevation = parseFloat(points[i].querySelector('ele')?.textContent || '0') - const previousElevation = parseFloat(points[i - 1].querySelector('ele')?.textContent || '0') + const prevPoint = points[i - 1]; + const point = points[i]; + const elevation = point.ele ?? 0 + const previousElevation = prevPoint.ele ?? 0 const elevationDiff = elevation - previousElevation; if (elevationDiff > 0) { totalElevationGain += elevationDiff; } - const prevPoint = points[i - 1]; - const point = points[i]; const distance = calculateDistance( - parseFloat(prevPoint.getAttribute("lat") || '0'), - parseFloat(prevPoint.getAttribute("lon") || '0'), - parseFloat(point.getAttribute("lat") || '0'), - parseFloat(point.getAttribute("lon") || '0') + prevPoint.$.lat ?? 0, + prevPoint.$.lon ?? 0, + point.$.lat ?? 0, + point.$.lon ?? 0, ); totalDistance += distance; } } } - const startPoint = xml.getElementsByTagName("trk")[0].getElementsByTagName("trkseg")[0].getElementsByTagName("trkpt")[0]; + const startPoint = gpx.trk?.at(0)?.trkseg?.at(0)?.trkpt?.at(0); if (startPoint) { - trail.lat = parseFloat(startPoint.getAttribute('lat')!) - trail.lon = parseFloat(startPoint.getAttribute('lon')!) + trail.lat = startPoint.$.lat + trail.lon = startPoint.$.lon } trail.duration = totalDuration / 1000 / 60 diff --git a/web/src/routes/trail/edit/[id]/+page.svelte b/web/src/routes/trail/edit/[id]/+page.svelte index 744e04cf..fde1422b 100644 --- a/web/src/routes/trail/edit/[id]/+page.svelte +++ b/web/src/routes/trail/edit/[id]/+page.svelte @@ -32,8 +32,11 @@ trails_update, } from "$lib/stores/trail_store"; import { + appendToRoute, calculateRouteBetween, clearRoute, + deleteFromRoute, + editRoute, route, } from "$lib/stores/valhalla_store"; import { waypoint } from "$lib/stores/waypoint_store"; @@ -47,7 +50,7 @@ import { createMarkerFromWaypoint } from "$lib/util/leaflet_util"; import { createForm } from "$lib/vendor/svelte-form-lib"; import cryptoRandomString from "crypto-random-string"; - import type { LatLng, LeafletMouseEvent, Map } from "leaflet"; + import type { LeafletMouseEvent, Map } from "leaflet"; import { onMount } from "svelte"; import { _ } from "svelte-i18n"; import { array, number, object, string } from "yup"; @@ -161,6 +164,9 @@ onMount(async () => { L = (await import("leaflet")).default; await import("leaflet.awesome-markers"); + + $form.lat = 47.74604748696788; + $form.lon = 11.588988304138185; }); function openFileBrowser() { @@ -242,9 +248,7 @@ openWaypointModal(); } else if (e.detail.value === "delete") { currentWaypoint.marker?.remove(); - $form.expand.waypoints.splice(index, 1); - $form.waypoints.splice(index, 1); - $form.expand.waypoints = $form.expand.waypoints; + deleteWaypoint(index); } } @@ -254,6 +258,34 @@ openWaypointModal(); } + function deleteWaypoint(index: number) { + $form.expand.waypoints.splice(index, 1); + $form.waypoints.splice(index, 1); + $form.expand.waypoints = $form.expand.waypoints; + + if (drawingActive) { + if (index == 0) { + deleteFromRoute(index); + $form.expand.gpx_data = route.toString(); + const nextWaypoint = $form.expand.waypoints[index]; + nextWaypoint.icon = "circle-half-stroke"; + saveWaypoint(nextWaypoint); + } else if (index == $form.expand.waypoints.length) { + deleteFromRoute(index - 1); + $form.expand.gpx_data = route.toString(); + + if ($form.expand.waypoints.length > 1) { + const previousWaypoint = $form.expand.waypoints[index - 1]; + previousWaypoint.icon = "flag-checkered"; + saveWaypoint(previousWaypoint); + } + } else { + deleteFromRoute(index - 1); + recalculateRoute(index); + } + } + } + function saveWaypoint(savedWaypoint: Waypoint) { let editedWaypointIndex = $form.expand.waypoints.findIndex( (s) => s.id == savedWaypoint.id, @@ -270,12 +302,18 @@ const marker = createMarkerFromWaypoint(L, savedWaypoint, (event) => { var marker = event.target; var position = marker.getLatLng(); - const editableWaypoint = $form.expand.waypoints.find( + const editableWaypointIndex = $form.expand.waypoints.findIndex( (w) => w.id == savedWaypoint.id, ); + const editableWaypoint = + $form.expand.waypoints[editableWaypointIndex]; editableWaypoint!.lat = position.lat; editableWaypoint!.lon = position.lng; $form.expand.waypoints = [...$form.expand.waypoints]; + + if (drawingActive) { + recalculateRoute(editableWaypointIndex); + } }); marker.addTo(map); @@ -327,7 +365,6 @@ await lists_index(); } catch (e) { console.error(e); - show_toast({ type: "error", icon: "close", @@ -353,22 +390,29 @@ const waypointCount = $form.expand.waypoints.length; const wp = new Waypoint(e.latlng.lat, e.latlng.lng, { name: `${$_("waypoints", { values: { n: 1 } })} ${waypointCount + 1}`, - icon: waypointCount ? undefined : "circle-half-stroke", + icon: waypointCount ? "flag-checkered" : "circle-half-stroke", }); - saveWaypoint(wp); - - if (waypointCount >= 1) { + if (waypointCount == 0) { + saveWaypoint(wp); + } else { const previousWaypoint = $form.expand.waypoints[waypointCount - 1]; try { - await calculateRouteBetween( + const routeWaypoints = await calculateRouteBetween( previousWaypoint.lat, previousWaypoint.lon, e.latlng.lat, e.latlng.lng, ); + appendToRoute(routeWaypoints); + saveWaypoint(wp); + if (waypointCount > 1) { + previousWaypoint.icon = "circle"; + saveWaypoint(previousWaypoint); + } $form.expand.gpx_data = route.toString(); } catch (e) { + console.error(e); show_toast({ text: "Error calculating route", icon: "close", @@ -377,6 +421,38 @@ } } } + + async function recalculateRoute(waypointIndex: number) { + const waypoint = $form.expand.waypoints[waypointIndex]; + let nextWaypoints; + let previousWaypoints; + if (waypointIndex < $form.expand.waypoints.length - 1) { + const nextWaypoint = $form.expand.waypoints[waypointIndex + 1]; + + nextWaypoints = await calculateRouteBetween( + waypoint.lat, + waypoint.lon, + nextWaypoint.lat, + nextWaypoint.lon, + ); + } + if (waypointIndex > 0) { + const previousWaypoint = $form.expand.waypoints[waypointIndex - 1]; + previousWaypoints = await calculateRouteBetween( + previousWaypoint.lat, + previousWaypoint.lon, + waypoint.lat, + waypoint.lon, + ); + } + if (nextWaypoints) { + editRoute(waypointIndex, nextWaypoints); + } + if (previousWaypoints) { + editRoute(waypointIndex - 1, previousWaypoints); + } + $form.expand.gpx_data = route.toString(); + } @@ -616,7 +692,12 @@ handleMapClick(e.detail)} >