finishes trail printing

This commit is contained in:
Christian Beutel
2024-04-12 00:25:26 +02:00
parent 71d548b9d6
commit f9f8998617
20 changed files with 643 additions and 39 deletions

216
web/package-lock.json generated
View File

@@ -14,8 +14,10 @@
"@threlte/extras": "^8.7.5", "@threlte/extras": "^8.7.5",
"@types/leaflet.awesome-markers": "^2.0.28", "@types/leaflet.awesome-markers": "^2.0.28",
"@types/three": "^0.161.2", "@types/three": "^0.161.2",
"canvg": "^4.0.1",
"crypto-random-string": "^5.0.0", "crypto-random-string": "^5.0.0",
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
"jspdf": "^2.5.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"leaflet-gpx": "^1.7.0", "leaflet-gpx": "^1.7.0",
"leaflet.awesome-markers": "^2.0.5", "leaflet.awesome-markers": "^2.0.5",
@@ -73,6 +75,17 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.24.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz",
"integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.19.12", "version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
@@ -1105,12 +1118,22 @@
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
}, },
"node_modules/@types/offscreencanvas": {
"version": "2019.7.3",
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
"integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="
},
"node_modules/@types/pug": { "node_modules/@types/pug": {
"version": "2.0.10", "version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
"dev": true "dev": true
}, },
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw=="
},
"node_modules/@types/resolve": { "node_modules/@types/resolve": {
"version": "1.20.2", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@@ -1329,6 +1352,17 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
}, },
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"bin": {
"atob": "bin/atob.js"
},
"engines": {
"node": ">= 4.5.0"
}
},
"node_modules/autoprefixer": { "node_modules/autoprefixer": {
"version": "10.4.17", "version": "10.4.17",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz",
@@ -1393,6 +1427,15 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
}, },
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -1491,6 +1534,17 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/btoa": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
"bin": {
"btoa": "bin/btoa.js"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/buffer-crc32": { "node_modules/buffer-crc32": {
"version": "0.2.13", "version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
@@ -1584,6 +1638,22 @@
} }
] ]
}, },
"node_modules/canvg": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-4.0.1.tgz",
"integrity": "sha512-5gD/d6SiCCT7baLnVr0hokYe93DfcHW2rSqdKOuOQD84YMlyfttnZ8iQsThTdX6koYam+PROz/FuQTo500zqGw==",
"dependencies": {
"@types/offscreencanvas": "^2019.6.4",
"@types/raf": "^3.4.0",
"raf": "^3.4.1",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/chai": { "node_modules/chai": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz",
@@ -1805,6 +1875,17 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/core-js": {
"version": "3.36.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz",
"integrity": "sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==",
"hasInstallScript": true,
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cross-fetch": { "node_modules/cross-fetch": {
"version": "3.1.8", "version": "3.1.8",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz",
@@ -1846,6 +1927,15 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css-tree": { "node_modules/css-tree": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
@@ -2106,6 +2196,12 @@
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true "dev": true
}, },
"node_modules/dompurify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.0.tgz",
"integrity": "sha512-5RXhAXSCrKTqt9pSbobT9PVRX+oPpENplTZqCiK1l0ya+ZOzwo9kqsGLbYRsAhzIiLCwKEy99XKSSrqnRTLVcw==",
"optional": true
},
"node_modules/eastasianwidth": { "node_modules/eastasianwidth": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -2689,6 +2785,19 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/http-proxy-agent": { "node_modules/http-proxy-agent": {
"version": "7.0.2", "version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
@@ -3225,6 +3334,53 @@
"integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==",
"dev": true "dev": true
}, },
"node_modules/jspdf": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz",
"integrity": "sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==",
"dependencies": {
"@babel/runtime": "^7.14.0",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"fflate": "^0.4.8"
},
"optionalDependencies": {
"canvg": "^3.0.6",
"core-js": "^3.6.0",
"dompurify": "^2.2.0",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jspdf/node_modules/canvg": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/jspdf/node_modules/fflate": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="
},
"node_modules/jspdf/node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"optional": true
},
"node_modules/kleur": { "node_modules/kleur": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -3882,6 +4038,11 @@
"png-js": "^1.0.0" "png-js": "^1.0.0"
} }
}, },
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
},
"node_modules/periscopic": { "node_modules/periscopic": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
@@ -4228,6 +4389,14 @@
} }
] ]
}, },
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "18.2.0", "version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
@@ -4255,6 +4424,11 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.2", "version": "1.5.2",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
@@ -4338,6 +4512,14 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rimraf": { "node_modules/rimraf": {
"version": "2.7.1", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
@@ -4586,6 +4768,14 @@
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
"dev": true "dev": true
}, },
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/std-env": { "node_modules/std-env": {
"version": "3.7.0", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
@@ -4962,6 +5152,14 @@
} }
} }
}, },
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/symbol-tree": { "node_modules/symbol-tree": {
"version": "3.2.4", "version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -5016,6 +5214,15 @@
"node": ">=10.13.0" "node": ">=10.13.0"
} }
}, },
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/thenify": { "node_modules/thenify": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@@ -5305,6 +5512,15 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true "dev": true
}, },
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "5.0.12", "version": "5.0.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",

View File

@@ -39,8 +39,10 @@
"@threlte/extras": "^8.7.5", "@threlte/extras": "^8.7.5",
"@types/leaflet.awesome-markers": "^2.0.28", "@types/leaflet.awesome-markers": "^2.0.28",
"@types/three": "^0.161.2", "@types/three": "^0.161.2",
"canvg": "^4.0.1",
"crypto-random-string": "^5.0.0", "crypto-random-string": "^5.0.0",
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
"jspdf": "^2.5.1",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"leaflet-gpx": "^1.7.0", "leaflet-gpx": "^1.7.0",
"leaflet.awesome-markers": "^2.0.5", "leaflet.awesome-markers": "^2.0.5",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,7 @@
import type { Trail } from "$lib/models/trail"; import type { Trail } from "$lib/models/trail";
import { currentUser } from "$lib/stores/user_store"; import { currentUser } from "$lib/stores/user_store";
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util"; import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
import type AutoGraticule from "$lib/vendor/leaflet-graticule/leaflet-auto-graticule";
import type { Map, Marker } from "leaflet"; import type { Map, Marker } from "leaflet";
import { createEventDispatcher, onMount } from "svelte"; import { createEventDispatcher, onMount } from "svelte";
@@ -9,6 +10,7 @@
export let markers: Marker[] = []; export let markers: Marker[] = [];
export let map: Map | null = null; export let map: Map | null = null;
export let options = {}; export let options = {};
export let graticule: AutoGraticule | null = null;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -110,7 +112,8 @@
} }
if(elevation_options.graticule) { if(elevation_options.graticule) {
new AutoGraticule().addTo(map!); graticule = new AutoGraticule();
graticule.addTo(map!);
} }
}); });
@@ -120,6 +123,6 @@
<div id="map" class="rounded-xl z-0 basis-full"></div> <div id="map" class="rounded-xl z-0 basis-full"></div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<slot /> <slot />
<div class="basis-full min-w-[300px]" id="elevation"></div> <div class="basis-[300px] flex-grow flex-shrink-0" id="elevation"></div>
</div> </div>
</div> </div>

View File

@@ -40,6 +40,7 @@
}, },
] ]
: []), : []),
{ text: $_("print"), value: "print", icon: "print" },
{ text: $_("add-to-list"), value: "list", icon: "bookmark" }, { text: $_("add-to-list"), value: "list", icon: "bookmark" },
{ text: $_("edit"), value: "edit", icon: "pen" }, { text: $_("edit"), value: "edit", icon: "pen" },
{ text: $_("delete"), value: "delete", icon: "trash" }, { text: $_("delete"), value: "delete", icon: "trash" },
@@ -61,6 +62,8 @@
"_blank", "_blank",
) )
?.focus(); ?.focus();
} else if (item.value == "print") {
goto(`/map/trail/${trail.id}/print`);
} else if (item.value == "download") { } else if (item.value == "download") {
downloadURI(getFileURL(trail, trail.gpx), trail.gpx!); downloadURI(getFileURL(trail, trail.gpx), trail.gpx!);
} else if (item.value == "edit") { } else if (item.value == "edit") {

View File

@@ -52,6 +52,7 @@
"entry": "Eintrag", "entry": "Eintrag",
"error-creating-user": "Fehler beim Erstellen des Nutzers", "error-creating-user": "Fehler beim Erstellen des Nutzers",
"error-during-login": "Fehler beim Login", "error-during-login": "Fehler beim Login",
"error-printing-map": "",
"error-reading-file": "Fehler beim Lesen der Datei", "error-reading-file": "Fehler beim Lesen der Datei",
"est-duration": "Gesch. Dauer", "est-duration": "Gesch. Dauer",
"explore": "Erkunden", "explore": "Erkunden",
@@ -59,6 +60,7 @@
"features": "Features", "features": "Features",
"french": "Französisch", "french": "Französisch",
"german": "Deutsch", "german": "Deutsch",
"grid": "",
"hero_section_0_text": "Entdecke spannende Routen, speichere deine Favoriten und erlebe die Schönheit der Natur. Finde dein nächstes Abenteuer!", "hero_section_0_text": "Entdecke spannende Routen, speichere deine Favoriten und erlebe die Schönheit der Natur. Finde dein nächstes Abenteuer!",
"hero_section_1_heading": "Hier gibt es noch keine Routen", "hero_section_1_heading": "Hier gibt es noch keine Routen",
"hero_section_1_text": "Hier sind einige Routen, die dir gefallen könnten. Oder du wirfst einen Blick auf die vollständige Liste.", "hero_section_1_text": "Hier sind einige Routen, die dir gefallen könnten. Oder du wirfst einen Blick auf die vollständige Liste.",
@@ -67,6 +69,7 @@
"hungarian": "Ungarisch", "hungarian": "Ungarisch",
"icon": "Icon", "icon": "Icon",
"imperial": "Amerikanisch", "imperial": "Amerikanisch",
"include-description": "",
"invalid-date": "Ungültiges Datum", "invalid-date": "Ungültiges Datum",
"invalid-username": "Ungültiger Nutzername", "invalid-username": "Ungültiger Nutzername",
"language": "Sprache", "language": "Sprache",
@@ -99,6 +102,8 @@
"not-a-valid-email-address": "Keine gültige Email-Adresse", "not-a-valid-email-address": "Keine gültige Email-Adresse",
"not-completed": "Nicht abgeschlossen", "not-completed": "Nicht abgeschlossen",
"or": "oder", "or": "oder",
"orientation": "",
"paper-size": "",
"password": "Passwort", "password": "Passwort",
"photos": "Fotos", "photos": "Fotos",
"pick-a-trail": "Route auswählen", "pick-a-trail": "Route auswählen",

View File

@@ -52,6 +52,7 @@
"entry": "Entry", "entry": "Entry",
"error-creating-user": "Error creating user", "error-creating-user": "Error creating user",
"error-during-login": "Error during login", "error-during-login": "Error during login",
"error-printing-map": "Error printing map",
"error-reading-file": "Error reading file", "error-reading-file": "Error reading file",
"est-duration": "Est. duration", "est-duration": "Est. duration",
"explore": "Explore", "explore": "Explore",
@@ -59,6 +60,7 @@
"features": "Features", "features": "Features",
"french": "French", "french": "French",
"german": "German", "german": "German",
"grid": "Grid",
"hero_section_0_text": "Explore exciting trails, save your favorites, and experience the beauty of nature. Find your next adventure!", "hero_section_0_text": "Explore exciting trails, save your favorites, and experience the beauty of nature. Find your next adventure!",
"hero_section_1_heading": "It seems there are no trails here yet.", "hero_section_1_heading": "It seems there are no trails here yet.",
"hero_section_1_text": "Here are some trails you might like. Or you can just go to the full list right now.", "hero_section_1_text": "Here are some trails you might like. Or you can just go to the full list right now.",
@@ -67,6 +69,7 @@
"hungarian": "Hungarian", "hungarian": "Hungarian",
"icon": "Icon", "icon": "Icon",
"imperial": "Imperial", "imperial": "Imperial",
"include-description": "Include description",
"invalid-date": "Invalid Date", "invalid-date": "Invalid Date",
"invalid-username": "Invalid username", "invalid-username": "Invalid username",
"language": "Language", "language": "Language",
@@ -99,6 +102,8 @@
"not-a-valid-email-address": "Not a valid email address", "not-a-valid-email-address": "Not a valid email address",
"not-completed": "Not completed", "not-completed": "Not completed",
"or": "or", "or": "or",
"orientation": "Orientation",
"paper-size": "Paper size",
"password": "Password", "password": "Password",
"photos": "Photos", "photos": "Photos",
"pick-a-trail": "Pick a trail", "pick-a-trail": "Pick a trail",

View File

@@ -52,6 +52,7 @@
"entry": "Entrée", "entry": "Entrée",
"error-creating-user": "Erreur durant la création de l'utilisateur", "error-creating-user": "Erreur durant la création de l'utilisateur",
"error-during-login": "Erreur durant la connexion", "error-during-login": "Erreur durant la connexion",
"error-printing-map": "",
"error-reading-file": "Erreur de lecture du fichier", "error-reading-file": "Erreur de lecture du fichier",
"est-duration": "Temps estimé", "est-duration": "Temps estimé",
"explore": "Explorer", "explore": "Explorer",
@@ -59,6 +60,7 @@
"features": "Fonctionnalités", "features": "Fonctionnalités",
"french": "Français", "french": "Français",
"german": "Allemand", "german": "Allemand",
"grid": "",
"hero_section_0_text": "Explorez des itinéraires passionnants, sauvegardez vos favoris et découvrez la beauté de la nature. Trouvez votre prochaine aventure !", "hero_section_0_text": "Explorez des itinéraires passionnants, sauvegardez vos favoris et découvrez la beauté de la nature. Trouvez votre prochaine aventure !",
"hero_section_1_heading": "Il semble qu'il n'y ait pas encore d'itinéraire ici.", "hero_section_1_heading": "Il semble qu'il n'y ait pas encore d'itinéraire ici.",
"hero_section_1_text": "Voici quelques itinéraires qui pourraient vous plaire. Vous pouvez également consulter la liste complète dès maintenant.", "hero_section_1_text": "Voici quelques itinéraires qui pourraient vous plaire. Vous pouvez également consulter la liste complète dès maintenant.",
@@ -67,6 +69,7 @@
"hungarian": "Hongrois", "hungarian": "Hongrois",
"icon": "Icône", "icon": "Icône",
"imperial": "Impérial", "imperial": "Impérial",
"include-description": "",
"invalid-date": "Date invalide", "invalid-date": "Date invalide",
"invalid-username": "Nom d'utilisateur invalide", "invalid-username": "Nom d'utilisateur invalide",
"language": "Langue", "language": "Langue",
@@ -99,6 +102,8 @@
"not-a-valid-email-address": "Adresse email invalide", "not-a-valid-email-address": "Adresse email invalide",
"not-completed": "Pas terminé", "not-completed": "Pas terminé",
"or": "ou", "or": "ou",
"orientation": "",
"paper-size": "",
"password": "Mot de passe", "password": "Mot de passe",
"photos": "Photos", "photos": "Photos",
"pick-a-trail": "Choisir un itinéraire", "pick-a-trail": "Choisir un itinéraire",

View File

@@ -52,6 +52,7 @@
"entry": "Bejegyzés", "entry": "Bejegyzés",
"error-creating-user": "Hiba felhasználó hozzáadása közben", "error-creating-user": "Hiba felhasználó hozzáadása közben",
"error-during-login": "Hiba bejelentkezés közben", "error-during-login": "Hiba bejelentkezés közben",
"error-printing-map": "",
"error-reading-file": "Hiba a fájl olvasása közben", "error-reading-file": "Hiba a fájl olvasása közben",
"est-duration": "Becsült időtartam", "est-duration": "Becsült időtartam",
"explore": "Felfedezés", "explore": "Felfedezés",
@@ -59,6 +60,7 @@
"features": "Jellemzők", "features": "Jellemzők",
"french": "Francia", "french": "Francia",
"german": "Német", "german": "Német",
"grid": "",
"hero_section_0_text": "Fedezze fel az izgalmas útvonalakat, mentse el kedvenceit, és tapasztalja meg a természet szépségét. Találja meg a következő kalandját!", "hero_section_0_text": "Fedezze fel az izgalmas útvonalakat, mentse el kedvenceit, és tapasztalja meg a természet szépségét. Találja meg a következő kalandját!",
"hero_section_1_heading": "Úgy tűnik, itt még nincsenek ösvények.", "hero_section_1_heading": "Úgy tűnik, itt még nincsenek ösvények.",
"hero_section_1_text": "Íme néhány nyomvonal, amely tetszeni fog. Vagy csak menj a teljes listára most rögtön.", "hero_section_1_text": "Íme néhány nyomvonal, amely tetszeni fog. Vagy csak menj a teljes listára most rögtön.",
@@ -67,6 +69,7 @@
"hungarian": "Magyar", "hungarian": "Magyar",
"icon": "Ikon", "icon": "Ikon",
"imperial": "Angolszász", "imperial": "Angolszász",
"include-description": "",
"invalid-date": "Érvénytelen dátum", "invalid-date": "Érvénytelen dátum",
"invalid-username": "Érvénytelen felhasználó", "invalid-username": "Érvénytelen felhasználó",
"language": "Nyelf", "language": "Nyelf",
@@ -99,6 +102,8 @@
"not-a-valid-email-address": "Érvénytelen e-mail cím", "not-a-valid-email-address": "Érvénytelen e-mail cím",
"not-completed": "Nem teljesített", "not-completed": "Nem teljesített",
"or": "vagy", "or": "vagy",
"orientation": "",
"paper-size": "",
"password": "Jelszó", "password": "Jelszó",
"photos": "Képek", "photos": "Képek",
"pick-a-trail": "Válasszon útvonalat", "pick-a-trail": "Válasszon útvonalat",

View File

@@ -52,6 +52,7 @@
"entry": "Item", "entry": "Item",
"error-creating-user": "Het account kan niet worden aangemaakt", "error-creating-user": "Het account kan niet worden aangemaakt",
"error-during-login": "Het inloggen is mislukt", "error-during-login": "Het inloggen is mislukt",
"error-printing-map": "",
"error-reading-file": "Het bestand kan niet worden ingelezen", "error-reading-file": "Het bestand kan niet worden ingelezen",
"est-duration": "Geschatte duur", "est-duration": "Geschatte duur",
"explore": "Verkennen", "explore": "Verkennen",
@@ -59,6 +60,7 @@
"features": "Kenmerken", "features": "Kenmerken",
"french": "Frans", "french": "Frans",
"german": "Duits", "german": "Duits",
"grid": "",
"hero_section_0_text": "Verken uitdagende wandelroutes, bewaar je favoriete en ervaar de pracht van de natuur. Op naar het volgende avontuur!", "hero_section_0_text": "Verken uitdagende wandelroutes, bewaar je favoriete en ervaar de pracht van de natuur. Op naar het volgende avontuur!",
"hero_section_1_heading": "Je hebt nog geen wandelroutes.", "hero_section_1_heading": "Je hebt nog geen wandelroutes.",
"hero_section_1_text": "Wellicht vind je deze routes interessant. Ook kun je de volledige lijst bekijken.", "hero_section_1_text": "Wellicht vind je deze routes interessant. Ook kun je de volledige lijst bekijken.",
@@ -67,6 +69,7 @@
"hungarian": "Hongaars", "hungarian": "Hongaars",
"icon": "Pictogram", "icon": "Pictogram",
"imperial": "Imperiaal", "imperial": "Imperiaal",
"include-description": "",
"invalid-date": "Ongeldige datum", "invalid-date": "Ongeldige datum",
"invalid-username": "Ongeldige gebruikersnaam", "invalid-username": "Ongeldige gebruikersnaam",
"language": "Taal", "language": "Taal",
@@ -99,6 +102,8 @@
"not-a-valid-email-address": "Het e-mailadres is ongeldig", "not-a-valid-email-address": "Het e-mailadres is ongeldig",
"not-completed": "Niet voltooid", "not-completed": "Niet voltooid",
"or": "of", "or": "of",
"orientation": "",
"paper-size": "",
"password": "Wachtwoord", "password": "Wachtwoord",
"photos": "Foto's", "photos": "Foto's",
"pick-a-trail": "Kies een wandelroute", "pick-a-trail": "Kies een wandelroute",

View File

@@ -52,6 +52,7 @@
"entry": "Pozycja", "entry": "Pozycja",
"error-creating-user": "Błąd tworzenia użytkownika", "error-creating-user": "Błąd tworzenia użytkownika",
"error-during-login": "Błąd podczas logowania", "error-during-login": "Błąd podczas logowania",
"error-printing-map": "",
"error-reading-file": "Błąd wczytywania pliku", "error-reading-file": "Błąd wczytywania pliku",
"est-duration": "Szacowany czas", "est-duration": "Szacowany czas",
"explore": "Eksploruj", "explore": "Eksploruj",
@@ -59,6 +60,7 @@
"features": "Funkcje", "features": "Funkcje",
"french": "Francuski", "french": "Francuski",
"german": "Niemiecki", "german": "Niemiecki",
"grid": "",
"hero_section_0_text": "Eksploruj ekscytujące szlaki, zapisz swoje ulubione, doświadcz piękna naturalnego świata. Znajdź swoją następną przygodę!", "hero_section_0_text": "Eksploruj ekscytujące szlaki, zapisz swoje ulubione, doświadcz piękna naturalnego świata. Znajdź swoją następną przygodę!",
"hero_section_1_heading": "Wygląda na to że nie ma tutaj ścieżek.", "hero_section_1_heading": "Wygląda na to że nie ma tutaj ścieżek.",
"hero_section_1_text": "Oto kila ścieżek które możesz zobaczyć lub możesz przejść do pełnej listy.", "hero_section_1_text": "Oto kila ścieżek które możesz zobaczyć lub możesz przejść do pełnej listy.",
@@ -67,6 +69,7 @@
"hungarian": "Język węgierski", "hungarian": "Język węgierski",
"icon": "Ikona", "icon": "Ikona",
"imperial": "Anglosaskie", "imperial": "Anglosaskie",
"include-description": "",
"invalid-date": "Nieprawidłowa data", "invalid-date": "Nieprawidłowa data",
"invalid-username": "Błędna nazwa użytkownika", "invalid-username": "Błędna nazwa użytkownika",
"language": "Język", "language": "Język",
@@ -99,6 +102,8 @@
"not-a-valid-email-address": "Nieprawidłowy adres email", "not-a-valid-email-address": "Nieprawidłowy adres email",
"not-completed": "Nie dokończono", "not-completed": "Nie dokończono",
"or": "lub", "or": "lub",
"orientation": "",
"paper-size": "",
"password": "Hasło", "password": "Hasło",
"photos": "Zdjęcia", "photos": "Zdjęcia",
"pick-a-trail": "Wybierz ścieżkę", "pick-a-trail": "Wybierz ścieżkę",

View File

@@ -52,6 +52,7 @@
"entry": "Entrada", "entry": "Entrada",
"error-creating-user": "Erro ao criar usuário", "error-creating-user": "Erro ao criar usuário",
"error-during-login": "Erro durante o login", "error-during-login": "Erro durante o login",
"error-printing-map": "",
"error-reading-file": "Erro ao ler o arquivo", "error-reading-file": "Erro ao ler o arquivo",
"est-duration": "Duração prevista", "est-duration": "Duração prevista",
"explore": "Explorar", "explore": "Explorar",
@@ -59,6 +60,7 @@
"features": "Características", "features": "Características",
"french": "Francês", "french": "Francês",
"german": "Alemão", "german": "Alemão",
"grid": "",
"hero_section_0_text": "Explore trilhas emocionantes, salve seus favoritos e experimente a beleza da natureza. Encontre sua próxima aventura!", "hero_section_0_text": "Explore trilhas emocionantes, salve seus favoritos e experimente a beleza da natureza. Encontre sua próxima aventura!",
"hero_section_1_heading": "Parece que ainda não há trilhas aqui.", "hero_section_1_heading": "Parece que ainda não há trilhas aqui.",
"hero_section_1_text": "Aqui estão algumas trilhas que você pode gostar. Ou podes ir à lista completa agora.", "hero_section_1_text": "Aqui estão algumas trilhas que você pode gostar. Ou podes ir à lista completa agora.",
@@ -67,6 +69,7 @@
"hungarian": "Húngaro", "hungarian": "Húngaro",
"icon": "Ícone", "icon": "Ícone",
"imperial": "Imperial", "imperial": "Imperial",
"include-description": "",
"invalid-date": "Data inválida", "invalid-date": "Data inválida",
"invalid-username": "Nome de usuário inválido", "invalid-username": "Nome de usuário inválido",
"language": "Língua", "language": "Língua",
@@ -99,6 +102,8 @@
"not-a-valid-email-address": "Não um endereço de e-mail válido", "not-a-valid-email-address": "Não um endereço de e-mail válido",
"not-completed": "Não preenchido", "not-completed": "Não preenchido",
"or": "ou", "or": "ou",
"orientation": "",
"paper-size": "",
"password": "Senha", "password": "Senha",
"photos": "Fotos", "photos": "Fotos",
"pick-a-trail": "Escolha uma trilha", "pick-a-trail": "Escolha uma trilha",

View File

@@ -52,6 +52,7 @@
"entry": "日程", "entry": "日程",
"error-creating-user": "创建用户错误", "error-creating-user": "创建用户错误",
"error-during-login": "登录错误", "error-during-login": "登录错误",
"error-printing-map": "",
"error-reading-file": "读取文件错误", "error-reading-file": "读取文件错误",
"est-duration": "预计时长", "est-duration": "预计时长",
"explore": "探索", "explore": "探索",
@@ -59,6 +60,7 @@
"features": "特性", "features": "特性",
"french": "法语", "french": "法语",
"german": "德语", "german": "德语",
"grid": "",
"hero_section_0_text": "探索激动人心的探险,收藏最爱行程,,体验美丽地大自然。找到你的下一次冒险!", "hero_section_0_text": "探索激动人心的探险,收藏最爱行程,,体验美丽地大自然。找到你的下一次冒险!",
"hero_section_1_heading": "这儿似乎还没有行程。", "hero_section_1_heading": "这儿似乎还没有行程。",
"hero_section_1_text": "以下有你可能喜欢的线路,或者立刻浏览全部列表。", "hero_section_1_text": "以下有你可能喜欢的线路,或者立刻浏览全部列表。",
@@ -67,6 +69,7 @@
"hungarian": "匈牙利语", "hungarian": "匈牙利语",
"icon": "图标", "icon": "图标",
"imperial": "英制", "imperial": "英制",
"include-description": "",
"invalid-date": "无效日期", "invalid-date": "无效日期",
"invalid-username": "无效用户名", "invalid-username": "无效用户名",
"language": "语言", "language": "语言",
@@ -99,6 +102,8 @@
"not-a-valid-email-address": "无效电子邮箱地址", "not-a-valid-email-address": "无效电子邮箱地址",
"not-completed": "未完成", "not-completed": "未完成",
"or": "或", "or": "或",
"orientation": "",
"paper-size": "",
"password": "密码", "password": "密码",
"photos": "图片", "photos": "图片",
"pick-a-trail": "选择一个路线", "pick-a-trail": "选择一个路线",

View File

@@ -4,6 +4,7 @@ export function createRect(
width: number, width: number,
height: number, height: number,
fill: string, fill: string,
border?: string,
) { ) {
const svgns = "http://www.w3.org/2000/svg"; const svgns = "http://www.w3.org/2000/svg";
@@ -12,7 +13,7 @@ export function createRect(
rect.setAttribute("y", y.toString()); rect.setAttribute("y", y.toString());
rect.setAttribute("width", width.toString()); rect.setAttribute("width", width.toString());
rect.setAttribute("height", height.toString()); rect.setAttribute("height", height.toString());
rect.setAttribute("fill", fill); rect.setAttribute("style", `fill:${fill};stroke-width:1;stroke:${border ?? ''}`);
return rect; return rect;
} }

View File

@@ -327,7 +327,7 @@
--ele-stroke: #7b7b7b; --ele-stroke: #7b7b7b;
--ele-circle: #fff; --ele-circle: #fff;
--ele-line: #000; --ele-line: #000;
--ele-grid: rgba(var(--input-border)); --ele-grid: #d1d5db;
} }
.elevation-detached.lightblue-theme .area { .elevation-detached.lightblue-theme .area {

View File

@@ -215,7 +215,7 @@ export default function leafletImage(map, callback) {
im.onload = function () { im.onload = function () {
if (marker._icon.classList.contains("leaflet-grid-label")) { if (marker._icon.classList.contains("leaflet-grid-label")) {
const label = marker._icon.children[0].textContent; const label = marker._icon.children[0].textContent;
ctx.font = '600 8px "Font Awesome 6 Free"'; ctx.font = '600 10px "IBMPlexSans"';
ctx.fillStyle = "#000"; ctx.fillStyle = "#000";
ctx.fillText(label, x + 4, y + 12); ctx.fillText(label, x + 4, y + 12);
} else { } else {
@@ -224,14 +224,16 @@ export default function leafletImage(map, callback) {
ctx.drawImage(this, x, y, size[0], size[1]); ctx.drawImage(this, x, y, size[0], size[1]);
ctx.font = '600 14px "Font Awesome 6 Free"'; ctx.font = '600 14px "Font Awesome 6 Free"';
ctx.fillStyle = "#ffffff"; ctx.fillStyle = "#ffffff";
ctx.fillText(icon, x + 10.2, y + 24); const iconWidth = ctx.measureText(icon).width
ctx.fillText(icon, x + size[0] / 2 - iconWidth / 2, y + 24);
if (waypointName) { if (waypointName) {
ctx.fillStyle = "#313131" ctx.fillStyle = "rgba(49, 49, 49, 0.5)"
ctx.roundRect(x - 8, y - 18, waypointName.length * 7, 20, 5)
ctx.fill();
ctx.font = '600 12px "IBMPlexSans"'; ctx.font = '600 12px "IBMPlexSans"';
const textWidth = ctx.measureText(waypointName).width
ctx.roundRect(size[0] / 2 + x - (textWidth + 16) / 2, y - 18, textWidth + 16, 20, 5)
ctx.fill();
ctx.fillStyle = "#ffffff"; ctx.fillStyle = "#ffffff";
ctx.fillText(waypointName, x, y - 4); ctx.fillText(waypointName, size[0] / 2 + x - (textWidth + 16) / 2 + 8, y - 4);
} }
} }

View File

@@ -1,7 +1,12 @@
<script lang="ts"> <script lang="ts">
import { page } from "$app/stores"; import { page } from "$app/stores";
import "$lib/assets/fonts/IBMPlexSans-Regular-normal";
import "$lib/assets/fonts/IBMPlexSans-SemiBold-bold";
import "$lib/assets/fonts/fa-solid-900-normal";
import Button from "$lib/components/base/button.svelte"; import Button from "$lib/components/base/button.svelte";
import Select from "$lib/components/base/select.svelte"; import Select, {
type SelectItem,
} from "$lib/components/base/select.svelte";
import LogoText from "$lib/components/logo/logo_text.svelte"; import LogoText from "$lib/components/logo/logo_text.svelte";
import MapWithElevation from "$lib/components/trail/map_with_elevation.svelte"; import MapWithElevation from "$lib/components/trail/map_with_elevation.svelte";
import { trail } from "$lib/stores/trail_store"; import { trail } from "$lib/stores/trail_store";
@@ -18,14 +23,19 @@
import { createRect, createText } from "$lib/util/svg_util"; import { createRect, createText } from "$lib/util/svg_util";
import "$lib/vendor/leaflet-elevation/src/index.css"; import "$lib/vendor/leaflet-elevation/src/index.css";
import leafletImage from "$lib/vendor/leaflet-image/leaflet-image.js"; import leafletImage from "$lib/vendor/leaflet-image/leaflet-image.js";
import { jsPDF } from "jspdf";
import type { Map } from "leaflet"; import type { Map } from "leaflet";
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css"; import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import QrCodeWithLogo from "qrcode-with-logos"; import QrCodeWithLogo from "qrcode-with-logos";
import { onMount, tick } from "svelte"; import { onMount, tick } from "svelte";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { Canvg } from "canvg";
import type AutoGraticule from "$lib/vendor/leaflet-graticule/leaflet-auto-graticule";
import { show_toast } from "$lib/stores/toast_store";
let map: Map; let map: Map;
let graticule: AutoGraticule;
const paperSizes: { text: string; value: keyof typeof paperDimensions }[] = const paperSizes: { text: string; value: keyof typeof paperDimensions }[] =
[ [
@@ -34,12 +44,24 @@
]; ];
const paperDimensions = { const paperDimensions = {
a4: { a4: {
width: "21cm", css: {
height: "29.7cm", width: "21cm",
height: "29.7cm",
},
pdf: {
width: 210,
height: 297,
},
}, },
letter: { letter: {
width: "21.59cm", css: {
height: "27.94cm", width: "21.59cm",
height: "27.94cm",
},
pdf: {
width: 215.9,
height: 279.4,
},
}, },
}; };
let selectedPaperSize = paperSizes[0].value; let selectedPaperSize = paperSizes[0].value;
@@ -50,7 +72,17 @@
]; ];
let selectedOrientation: "portrait" | "landscape" = orientations[0].value; let selectedOrientation: "portrait" | "landscape" = orientations[0].value;
let scale = 1; const gridOptions = [
{ text: "Degrees", value: "degree" },
{ text: "No Grid", value: "off" },
];
let selectedGrid = gridOptions[0].value;
let scale: number = 1;
let printLoading: boolean = false;
let includeDescription: boolean = false;
onMount(() => { onMount(() => {
let qrcode = new QrCodeWithLogo({ let qrcode = new QrCodeWithLogo({
@@ -63,16 +95,262 @@
}); });
function print() { function print() {
leafletImage(map, function (err: string, canvas: HTMLCanvasElement) { printLoading = true;
var img = document.createElement("img"); const doc = new jsPDF(selectedOrientation, "mm", [
var dimensions = map.getSize(); paperDimensions[selectedPaperSize].pdf[
img.width = dimensions.x; selectedOrientation == "portrait" ? "width" : "height"
img.height = dimensions.y; ],
img.src = canvas.toDataURL(); paperDimensions[selectedPaperSize].pdf[
selectedOrientation == "portrait" ? "height" : "width"
],
]);
doc.setFont("IBMPlexSans-Regular", "normal");
var width = doc.internal.pageSize.getWidth();
var height = doc.internal.pageSize.getHeight();
let currentHeight = 0;
const rulerElement = document.getElementById("ruler") as HTMLElement;
const rulerSegments = rulerElement.getElementsByTagName("rect");
const rulerLabels = rulerElement.getElementsByTagName("text");
document.getElementById("images")!.innerHTML = ""; const pageWidth = document
document.getElementById("images")!.appendChild(img); .getElementById("paper")
}); ?.getBoundingClientRect().width;
try {
leafletImage(
map,
async function (err: string, canvas: HTMLCanvasElement) {
// Map
var img = document.createElement("img");
var dimensions = map.getSize();
img.width = dimensions.x;
img.height = dimensions.y;
const ratio = img.height / img.width;
img.src = canvas.toDataURL();
doc.addImage(img.src, "png", 8, 8, 0, (width - 16) * ratio);
currentHeight += (width - 16) * ratio + 11;
const elevationProfileHeight = currentHeight - 4;
// QR Code
doc.addImage(
document.getElementById("qrcode") as HTMLImageElement,
"png",
8,
currentHeight,
24,
24,
);
// Distance
currentHeight += 5;
let currentWidth = 8 + 24 + 4;
doc.setFontSize(9);
doc.setTextColor(107, 114, 128);
doc.setFont("fa-solid-900", "normal");
doc.text("\uf337", currentWidth, currentHeight);
currentWidth += doc.getTextWidth("\uf337") + 3;
doc.setFont("IBMPlexSans-Regular", "normal");
doc.text(
formatDistance($trail.distance),
currentWidth,
currentHeight,
);
currentWidth +=
doc.getTextWidth(formatDistance($trail.distance)) + 4;
// Elevation gain
doc.setFont("fa-solid-900", "normal");
doc.text("\uf338", currentWidth, currentHeight);
currentWidth += doc.getTextWidth("\uf338") + 3;
doc.setFont("IBMPlexSans-Regular", "normal");
doc.text(
formatElevation($trail.elevation_gain),
currentWidth,
currentHeight,
);
currentWidth +=
doc.getTextWidth(
formatDistance($trail.elevation_gain),
) + 4;
// Duration
doc.setFont("fa-solid-900", "normal");
doc.text("\uf017", currentWidth, currentHeight);
currentWidth += doc.getTextWidth("\uf337") + 3;
doc.setFont("IBMPlexSans-Regular", "normal");
doc.text(
formatTimeHHMM($trail.duration),
currentWidth,
currentHeight,
);
// Ruler
currentHeight += 6;
currentWidth = 8 + 24 + 4;
doc.setTextColor(0, 0, 0);
for (let i = 0; i < rulerSegments.length; i++) {
const segment = rulerSegments[i];
const label = rulerLabels[i].textContent ?? "";
const segmentWidth =
segment.getBoundingClientRect().width;
const segmentWidthPercent =
segmentWidth / (pageWidth ?? 0);
const segmentPDFWidth = width * segmentWidthPercent;
let textWidth = doc.getTextWidth(label);
if (i % 2 == 0) {
doc.setFillColor(255, 255, 255);
} else {
doc.setFillColor(93, 93, 93);
}
doc.rect(
currentWidth,
currentHeight,
segmentPDFWidth,
2,
"FD",
);
doc.text(
label,
currentWidth - textWidth / 2,
currentHeight + 6,
);
currentWidth += segmentPDFWidth;
if (i == rulerSegments.length - 1) {
const endLabel =
rulerLabels[i + 1].textContent ?? "";
textWidth = doc.getTextWidth(endLabel);
doc.text(
endLabel,
currentWidth - textWidth / 2,
currentHeight + 6,
);
textWidth = doc.getTextWidth("KM");
doc.text(
"KM",
currentWidth - textWidth,
currentHeight - 2,
);
}
}
// Scale
currentHeight += 12;
doc.setFillColor(255, 255, 255);
doc.text(
`Scale: 1 : ${Math.round(scale)}`,
8 + 24 + 4,
currentHeight,
);
// Elevation profile
const gridLines = document.querySelectorAll(
"#elevation .grid line",
);
gridLines.forEach((l) =>
l.setAttribute("stroke", "#d1d5db"),
);
const gridBorders = document.querySelectorAll(
"#elevation .grid path",
);
gridBorders.forEach((l) =>
l.setAttribute("stroke", "#fff"),
);
const tooltip = document.querySelector(
"#elevation .tooltip",
) as HTMLElement;
tooltip.style.display = "none";
const units = document.querySelectorAll(
"#elevation .axis text",
);
units.forEach((l) => l.setAttribute("fill", "#000"));
const elevationCanvas = document.createElement("canvas");
const ctx = elevationCanvas.getContext("2d")!;
const v = await Canvg.from(
ctx,
document.querySelector("#elevation svg")!.outerHTML,
);
v.start();
const profileRatio =
elevationCanvas.height / elevationCanvas.width;
doc.addImage(
elevationCanvas.toDataURL("image/png"),
width - 79.3 - 8,
elevationProfileHeight,
79.3,
79.3 * profileRatio,
);
const elevationPlot = (
document.querySelector(
".canvas-plot",
) as HTMLCanvasElement
).toDataURL("image/png");
doc.addImage(
elevationPlot,
width - 77.3,
elevationProfileHeight + 8,
0,
0,
);
tooltip.style.display = "block";
// Separator
currentHeight += 5;
doc.setDrawColor(232, 234, 237);
doc.line(8, currentHeight, width - 8, currentHeight);
currentHeight += 6;
// Trail name & location
doc.setTextColor(0, 0, 0);
doc.setFont("IBMPlexSans-SemiBold", "bold");
doc.text($trail.name, 8, currentHeight);
doc.setFont("fa-solid-900", "normal");
currentHeight += 5;
doc.text("\uf3c5", 8, currentHeight);
doc.setFont("IBMPlexSans-Regular", "normal");
doc.text($trail.location || "-", 12, currentHeight);
// Logo
const logo = new Image();
logo.src = "/imgs/logo_text_dark.png";
const logoRatio = 64 / 212;
const logoWidth = 32;
const logoHeight = logoWidth * logoRatio;
doc.addImage(
logo,
width - 8 - logoWidth,
height - 4 - logoHeight,
logoWidth,
logoHeight,
);
if (includeDescription && $trail.description) {
doc.addPage();
doc.text($trail.description, 16, 16, {
maxWidth: width - 32,
});
doc.addImage(
logo,
width - 8 - logoWidth,
height - 4 - logoHeight,
logoWidth,
logoHeight,
);
}
doc.save($trail.name + ".pdf");
printLoading = false;
},
);
} catch (e) {
show_toast({
icon: "close",
type: "error",
text: $_("error-printing-map"),
});
printLoading = false;
}
} }
async function updateMapSize() { async function updateMapSize() {
@@ -107,8 +385,7 @@
label: string, label: string,
fill: string, fill: string,
) { ) {
const bar = createRect(x, height / 2, width - 4, 6, fill); const bar = createRect(x, height / 2, width, 6, fill, "#000");
const tick = createRect(x + width - 4, height / 2, 4, 6, "#000");
const text = createText( const text = createText(
label, label,
x + width - 5 * label.length, x + width - 5 * label.length,
@@ -116,7 +393,6 @@
"0.75rem", "0.75rem",
); );
svg.appendChild(bar); svg.appendChild(bar);
svg.appendChild(tick);
svg.append(text); svg.append(text);
} }
let multiplier = 1; let multiplier = 1;
@@ -148,7 +424,7 @@
oneUnitInPixels * i * multiplier, oneUnitInPixels * i * multiplier,
oneUnitInPixels * multiplier, oneUnitInPixels * multiplier,
`${multiplier < 1 ? ((i + 1) * multiplier).toFixed(1) : (i + 1) * multiplier}`, `${multiplier < 1 ? ((i + 1) * multiplier).toFixed(1) : (i + 1) * multiplier}`,
i % 2 == 0 ? "#7c7c7c" : "#5d5d5d", i % 2 == 0 ? "#fff" : "#5d5d5d",
); );
} }
@@ -162,40 +438,72 @@
scale = calculateScaleFactor(map); scale = calculateScaleFactor(map);
} }
function toggleGrid() {
if (selectedGrid == "off") {
graticule?.remove();
} else {
graticule.addTo(map);
}
}
</script> </script>
<svelte:head> <svelte:head>
<title>{$_("print")} | wanderer</title> <title>{$_("print")} | wanderer</title>
</svelte:head> </svelte:head>
<main class="grid grid-cols-1 md:grid-cols-[400px_1fr] gap-x-1 gap-y-4 items-start"> <main
class="grid grid-cols-1 md:grid-cols-[400px_1fr] gap-x-1 gap-y-4 items-start"
>
<div <div
class="print-details px-6 space-y-4 pb-4 border border-input-border rounded-3xl" class="print-details md:sticky w-80 md:top-8 mx-auto md:ml-8 p-6 flex flex-col gap-y-4 border border-input-border rounded-3xl"
> >
<div id="images"></div>
<h1 class="text-4xl font-bold">Print Trail</h1> <h1 class="text-4xl font-bold">Print Trail</h1>
<Select <Select
bind:value={selectedPaperSize} bind:value={selectedPaperSize}
items={paperSizes} items={paperSizes}
label="Paper size" label={$_("paper-size")}
on:change={updateMapSize} on:change={updateMapSize}
></Select> ></Select>
<Select <Select
bind:value={selectedOrientation} bind:value={selectedOrientation}
items={orientations} items={orientations}
label="Orientation" label={$_("orientation")}
on:change={updateMapSize} on:change={updateMapSize}
></Select> ></Select>
<Select
bind:value={selectedGrid}
items={gridOptions}
label={$_("grid")}
on:change={toggleGrid}
></Select>
<div>
<input
id="description-checkbox"
type="checkbox"
bind:value={includeDescription}
class="w-4 h-4 bg-input-background accent-primary border-input-border focus:ring-input-ring focus:ring-2"
/>
<label for="description-checkbox" class="ms-2 text-sm"
>{$_("include-description")}</label
>
</div>
<Button primary={true} on:click={print}>Print!</Button> <Button
loading={printLoading}
extraClasses="mt-2"
primary={true}
on:click={print}>Print!</Button
>
</div> </div>
<div class="paper-container overflow-scroll"> <div class="paper-container overflow-scroll">
<div <div
class="paper flex flex-col mx-auto shadow-xl mb-8 p-8 bg-white" id="paper"
style="width: {paperDimensions[selectedPaperSize][ class="flex flex-col mx-auto shadow-xl mb-8 p-8 bg-white"
style="width: {paperDimensions[selectedPaperSize].css[
selectedOrientation == 'portrait' ? 'width' : 'height' selectedOrientation == 'portrait' ? 'width' : 'height'
]}; height: {paperDimensions[selectedPaperSize][ ]}; height: {paperDimensions[selectedPaperSize].css[
selectedOrientation == 'portrait' ? 'height' : 'width' selectedOrientation == 'portrait' ? 'height' : 'width'
]}" ]}"
> >
@@ -214,6 +522,7 @@
}} }}
on:zoomend={(e) => updateScale(e.detail)} on:zoomend={(e) => updateScale(e.detail)}
bind:map bind:map
bind:graticule
> >
<img <img
class="w-[100px] h-[100px]" class="w-[100px] h-[100px]"
@@ -239,7 +548,14 @@
></i>{formatTimeHHMM($trail.duration)}</span ></i>{formatTimeHHMM($trail.duration)}</span
> >
</div> </div>
<svg width="100%" height="50" id="ruler"> </svg> <svg
width="100%"
height="50"
id="ruler"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
>
</svg>
<span class="text-sm" <span class="text-sm"
>Scale: &nbsp 1 : {Math.round(scale)}</span >Scale: &nbsp 1 : {Math.round(scale)}</span
> >

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB