mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-03-20 11:36:33 +00:00
Compare commits
1505 Commits
l10n
...
cfb629dcfb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfb629dcfb | ||
|
|
268afe5f84 | ||
|
|
6832b7b1ca | ||
|
|
042c900f80 | ||
|
|
6524864dea | ||
|
|
3ad29a2f8b | ||
|
|
f40c01109a | ||
|
|
2c875bd96b | ||
|
|
ace184c5f4 | ||
|
|
3857649baa | ||
|
|
db32681bcf | ||
|
|
0c551a5991 | ||
|
|
be39bc80a0 | ||
|
|
1798cbcc1f | ||
|
|
054abd8555 | ||
|
|
febf949e83 | ||
|
|
712ca963d2 | ||
|
|
41c06e9c98 | ||
|
|
7bf411672d | ||
|
|
3d7f98903f | ||
|
|
ea744f4979 | ||
|
|
3ffec22e5e | ||
|
|
243b6cbe79 | ||
|
|
79872ac8e4 | ||
|
|
0214cad3dd | ||
|
|
818b3e0d44 | ||
|
|
b61313ce6c | ||
|
|
f511da811b | ||
|
|
b7b1662383 | ||
|
|
716a28352d | ||
|
|
bf05c28e32 | ||
|
|
0c8f516e39 | ||
|
|
ad7355e976 | ||
|
|
9734bb251b | ||
|
|
b1986f68d6 | ||
|
|
957faced0a | ||
|
|
4e43867ab3 | ||
|
|
0b53016675 | ||
|
|
04c2411432 | ||
|
|
237e673963 | ||
|
|
d48b7c1927 | ||
|
|
39602fa0e9 | ||
|
|
08704e2eff | ||
|
|
79178a6514 | ||
|
|
05cfeca5b4 | ||
|
|
5b93749051 | ||
|
|
355ef01a38 | ||
|
|
c382298c7e | ||
|
|
b083c6e0a2 | ||
|
|
96275b9e8e | ||
|
|
91ae587254 | ||
|
|
18a8c02e7c | ||
|
|
daa2d662de | ||
|
|
c0d310fc1d | ||
|
|
78c4553171 | ||
|
|
3906fe45e0 | ||
|
|
b449397914 | ||
|
|
c0fa6e34c5 | ||
|
|
c5bb302a15 | ||
|
|
05012179f9 | ||
|
|
a53c934a1e | ||
|
|
da73f9426a | ||
|
|
b5383122bc | ||
|
|
0a059c345b | ||
|
|
07c979df5d | ||
|
|
52a5654923 | ||
|
|
3a86303865 | ||
|
|
7cc06efda1 | ||
|
|
b467b69478 | ||
|
|
c289c316d7 | ||
|
|
a30c9ae81a | ||
|
|
cb9e316a8c | ||
|
|
f6abbab561 | ||
|
|
aae0626a22 | ||
|
|
b39f30fec6 | ||
|
|
f323bc2e25 | ||
|
|
67abd55120 | ||
|
|
5f52964672 | ||
|
|
084587fcc1 | ||
|
|
619f129f0c | ||
|
|
acd2962427 | ||
|
|
c9ddb5f3d1 | ||
|
|
480056ce86 | ||
|
|
cc47635a60 | ||
|
|
631b0cd3d3 | ||
|
|
447d416bbb | ||
|
|
05b8dff0a6 | ||
|
|
c44e9be42d | ||
|
|
366f26ff0b | ||
|
|
dd1865a2cf | ||
|
|
359ce2fedc | ||
|
|
dc94a33e64 | ||
|
|
8be176b57e | ||
|
|
984e518163 | ||
|
|
219afd5f1c | ||
|
|
4d0e0c8c95 | ||
|
|
1bfc19cc74 | ||
|
|
48f0466e5e | ||
|
|
6c004740c9 | ||
|
|
4582ddd1db | ||
|
|
ea254f89a5 | ||
|
|
6d5880ee09 | ||
|
|
f230c5fd84 | ||
|
|
2326fb3809 | ||
|
|
270df17c67 | ||
|
|
cbb386e799 | ||
|
|
6c7f4933eb | ||
|
|
c572570517 | ||
|
|
72264b76aa | ||
|
|
3cfe444f8d | ||
|
|
d9b38bab4c | ||
|
|
b39e09bd0c | ||
|
|
cd32631a71 | ||
|
|
ec8e1455a1 | ||
|
|
cfd9b9946b | ||
|
|
961daf76f0 | ||
|
|
256ca75851 | ||
|
|
6fa63cca0d | ||
|
|
a8730722dd | ||
|
|
ededaebd39 | ||
|
|
80d43a85cd | ||
|
|
6b2a946ed7 | ||
|
|
6822e3bed3 | ||
|
|
5c9ae9cde6 | ||
|
|
335505bb6e | ||
|
|
a153b3379f | ||
|
|
04c280560e | ||
|
|
31385fb34f | ||
|
|
3844360614 | ||
|
|
e9f4487035 | ||
|
|
b5c50fbbdf | ||
|
|
ace8928c99 | ||
|
|
de8c3dbcce | ||
|
|
b6df3ad67f | ||
|
|
edb9950e38 | ||
|
|
2dc06ddbd4 | ||
|
|
2cca65730f | ||
|
|
ff6090f859 | ||
|
|
105a705302 | ||
|
|
2620dcd2f8 | ||
|
|
74e0f260ae | ||
|
|
64beb4cc52 | ||
|
|
b531fc5df4 | ||
|
|
300ea4b77f | ||
|
|
309d60fb46 | ||
|
|
f8606a7453 | ||
|
|
d7fc332d91 | ||
|
|
bc14935be3 | ||
|
|
6b38430822 | ||
|
|
4fb2cd1805 | ||
|
|
e265d6b561 | ||
|
|
b6cb891858 | ||
|
|
09a9c2ef17 | ||
|
|
3cb9498c9b | ||
|
|
1991d88c03 | ||
|
|
9e5fa15630 | ||
|
|
d3be51fdcd | ||
|
|
8ae62558be | ||
|
|
cf0d82a4d2 | ||
|
|
b6aa4738cf | ||
|
|
f73de02f87 | ||
|
|
383bf649c3 | ||
|
|
7fc4826f43 | ||
|
|
4b8fd2f25c | ||
|
|
9dee07a534 | ||
|
|
d476f0991e | ||
|
|
8da76ebf95 | ||
|
|
0db93bf1f7 | ||
|
|
7480d5e2d5 | ||
|
|
88da695b9f | ||
|
|
54cc725550 | ||
|
|
bf299220fe | ||
|
|
c726aefa87 | ||
|
|
15a5d9a457 | ||
|
|
e01223f985 | ||
|
|
ab6389901e | ||
|
|
281fec42cb | ||
|
|
ec0f463be9 | ||
|
|
a51a616181 | ||
|
|
11a8ed3388 | ||
|
|
1870ca9a6c | ||
|
|
4bc6ec4eea | ||
|
|
82ba54e592 | ||
|
|
3ef4a6d9b7 | ||
|
|
d724799258 | ||
|
|
e0cda2a2c0 | ||
|
|
118584debb | ||
|
|
fb9c242972 | ||
|
|
448838fde4 | ||
|
|
a6c4332854 | ||
|
|
f55e2eb1f3 | ||
|
|
cfc5a00248 | ||
|
|
8c508b0ca5 | ||
|
|
67d6fe326f | ||
|
|
eadc9a62ce | ||
|
|
b2b68316f2 | ||
|
|
fac8938d0a | ||
|
|
0f8c37ff70 | ||
|
|
fc004fd212 | ||
|
|
680a18f737 | ||
|
|
9b8533352b | ||
|
|
0761e6249d | ||
|
|
6b6636bc16 | ||
|
|
f2da0e9baf | ||
|
|
e82683a5e5 | ||
|
|
9c59f27849 | ||
|
|
6113af1ff5 | ||
|
|
1d535c4bba | ||
|
|
8cc022db94 | ||
|
|
4a6e795595 | ||
|
|
9453f65991 | ||
|
|
7d5fa67aba | ||
|
|
c5488d2f9b | ||
|
|
f1740d122e | ||
|
|
d54eb810ae | ||
|
|
cc77fbf7d1 | ||
|
|
7e79a1af45 | ||
|
|
0fd5929d33 | ||
|
|
bfc2e87cbd | ||
|
|
6de7b71e5e | ||
|
|
6773b21e70 | ||
|
|
da2538de26 | ||
|
|
5f055d3c5e | ||
|
|
a53df616be | ||
|
|
7d55a86209 | ||
|
|
d0d0115ee7 | ||
|
|
630efd4ca2 | ||
|
|
95f956e12c | ||
|
|
26602696ce | ||
|
|
65c5d4d950 | ||
|
|
765f79b7fd | ||
|
|
f822939633 | ||
|
|
d186843ba2 | ||
|
|
ba7ec47a07 | ||
|
|
7ce0adc245 | ||
|
|
6c9a93bc75 | ||
|
|
a09bf4dfa9 | ||
|
|
6de834b55a | ||
|
|
b2447718e3 | ||
|
|
e3e1746d42 | ||
|
|
e013c3aa92 | ||
|
|
8d38cc7efe | ||
|
|
37f17968b7 | ||
|
|
2a2789dcba | ||
|
|
b36e2b8d82 | ||
|
|
22dbe9d07d | ||
|
|
2fb28a5a4a | ||
|
|
0ea0e9992e | ||
|
|
0f2f9703b2 | ||
|
|
403f018b43 | ||
|
|
35033bf439 | ||
|
|
be30315718 | ||
|
|
046e7382cf | ||
|
|
ad4a0485c4 | ||
|
|
c6aface832 | ||
|
|
651e1e9203 | ||
|
|
e3dcf6eb87 | ||
|
|
15d8b22025 | ||
|
|
700c8b565f | ||
|
|
a0a888b31f | ||
|
|
32f99c6c73 | ||
|
|
d646a05222 | ||
|
|
3b2e49b524 | ||
|
|
5b1d415149 | ||
|
|
03cb33ed04 | ||
|
|
3aff104429 | ||
|
|
50c4a27ce8 | ||
|
|
00ca6db8fc | ||
|
|
1f43ccd2f2 | ||
|
|
d86bd0a26b | ||
|
|
f85bc96f73 | ||
|
|
96f89e5329 | ||
|
|
d2fea652e4 | ||
|
|
ffb1e23ac0 | ||
|
|
5ef0316099 | ||
|
|
42168c2cd5 | ||
|
|
5133bf3768 | ||
|
|
f1497a0caa | ||
|
|
0fd7cd5c07 | ||
|
|
48c35dcf17 | ||
|
|
c5982f725c | ||
|
|
063e567a09 | ||
|
|
a0b067d92e | ||
|
|
47d4c9cb20 | ||
|
|
585ef679f4 | ||
|
|
62f5851e25 | ||
|
|
a6f81c03d2 | ||
|
|
70192b2cb9 | ||
|
|
9578aee988 | ||
|
|
f91dee0074 | ||
|
|
fd2ebd10f5 | ||
|
|
d8db4d2a73 | ||
|
|
ebccd487be | ||
|
|
42a1a73dbf | ||
|
|
7b543b65a6 | ||
|
|
9d870faa1e | ||
|
|
b514595b24 | ||
|
|
ea6d9783fd | ||
|
|
6686d48249 | ||
|
|
94a243b3b9 | ||
|
|
284fec9857 | ||
|
|
36b0b82625 | ||
|
|
0f0628024f | ||
|
|
3633448af5 | ||
|
|
ef500ac6f7 | ||
|
|
2678ce6948 | ||
|
|
1f2bca7271 | ||
|
|
ff31897603 | ||
|
|
930930fc43 | ||
|
|
158120e396 | ||
|
|
ae61d76e3f | ||
|
|
3df66d5653 | ||
|
|
f10a27f882 | ||
|
|
88b776b415 | ||
|
|
d3e0f65e9e | ||
|
|
666554bf18 | ||
|
|
fc722177e8 | ||
|
|
d5a3f32811 | ||
|
|
7bbbe8d861 | ||
|
|
ddc6f2e10b | ||
|
|
ff78e98ae2 | ||
|
|
ecf54ea6da | ||
|
|
ca0e1110ca | ||
|
|
ca128d332f | ||
|
|
ad027cb58c | ||
|
|
ff7f1d7f67 | ||
|
|
ded9f33313 | ||
|
|
ccbc9211ac | ||
|
|
85684fe415 | ||
|
|
aeb60c50ee | ||
|
|
f81405e409 | ||
|
|
294d85e33b | ||
|
|
2d52865487 | ||
|
|
cdba2179ff | ||
|
|
60a521373d | ||
|
|
1bb65ea66f | ||
|
|
d0ceecbf51 | ||
|
|
57af9b9ece | ||
|
|
27d10bf9fe | ||
|
|
0ee0814779 | ||
|
|
384af64599 | ||
|
|
a4f06e5196 | ||
|
|
150b7878fc | ||
|
|
da83cc1649 | ||
|
|
5be4e6816d | ||
|
|
9769d7f9ec | ||
|
|
d7b843db26 | ||
|
|
23ee01a8e9 | ||
|
|
adc2197d98 | ||
|
|
f5c95fe0f4 | ||
|
|
38523f1c42 | ||
|
|
747d3b28ce | ||
|
|
a63683d894 | ||
|
|
13907f2b5c | ||
|
|
9d448cb8d4 | ||
|
|
7647ca0762 | ||
|
|
da9021c3ff | ||
|
|
5a8232167a | ||
|
|
f937c9750d | ||
|
|
65a10b2daa | ||
|
|
20c87a14aa | ||
|
|
f77571b7ef | ||
|
|
9e95c4b3e1 | ||
|
|
92e46ec540 | ||
|
|
1713437813 | ||
|
|
5bf88a34f7 | ||
|
|
75c6a2facf | ||
|
|
3c728115b8 | ||
|
|
51d56dcf7a | ||
|
|
6fe0aa3ee6 | ||
|
|
670bccd2aa | ||
|
|
7cb253565e | ||
|
|
54e1900e6d | ||
|
|
d83194b225 | ||
|
|
82eb9a1dc9 | ||
|
|
d298e51a6c | ||
|
|
2b7e3a2b5d | ||
|
|
0644952db3 | ||
|
|
08e5dc382f | ||
|
|
afd3d5e8dc | ||
|
|
b2fa2d8685 | ||
|
|
f8e5ccc5bb | ||
|
|
63b56166cb | ||
|
|
357791d17c | ||
|
|
d095f6734d | ||
|
|
ea8771d6f5 | ||
|
|
c8e28127c3 | ||
|
|
cae013a68d | ||
|
|
caa56ee16b | ||
|
|
80e11a7723 | ||
|
|
87b3126eb0 | ||
|
|
8388c8db23 | ||
|
|
2bce1dca84 | ||
|
|
749be1eb73 | ||
|
|
23b74a1d63 | ||
|
|
b2f10c2f68 | ||
|
|
2aa7f12514 | ||
|
|
cea8cfbfa6 | ||
|
|
e15275d96d | ||
|
|
3b6fb4d170 | ||
|
|
05c83c63e7 | ||
|
|
470ca982a1 | ||
|
|
ec62477937 | ||
|
|
fed8474dce | ||
|
|
7dc93302bf | ||
|
|
3105c444ce | ||
|
|
a2cae2e086 | ||
|
|
cd4320fe08 | ||
|
|
32b07b0e32 | ||
|
|
3d6234d5b9 | ||
|
|
2463c8b40a | ||
|
|
0a9206b1d3 | ||
|
|
7cd1c0dd69 | ||
|
|
3111927f02 | ||
|
|
c471eb160c | ||
|
|
c983abb49f | ||
|
|
5195e4f591 | ||
|
|
538d04824d | ||
|
|
4666fa418d | ||
|
|
87ce67c326 | ||
|
|
6eaec189e6 | ||
|
|
6f342951ce | ||
|
|
aacab6e62c | ||
|
|
cc7a07e872 | ||
|
|
9127031ac9 | ||
|
|
9aa654e69d | ||
|
|
573d4e58bb | ||
|
|
e7c96e3b19 | ||
|
|
c9bd41e58e | ||
|
|
5027903820 | ||
|
|
767ebc573a | ||
|
|
5a3d66c885 | ||
|
|
e84e6bd6c5 | ||
|
|
bee6a7dacc | ||
|
|
9876c7506c | ||
|
|
afb477d155 | ||
|
|
2c9a8491ca | ||
|
|
f6a9b37cc2 | ||
|
|
1852214758 | ||
|
|
a404591c57 | ||
|
|
406f8012fc | ||
|
|
9de5fa1819 | ||
|
|
da7862546c | ||
|
|
8314ee0c30 | ||
|
|
ba0da25c6c | ||
|
|
9b432cef1b | ||
|
|
ada6adaadf | ||
|
|
4fe7d72e65 | ||
|
|
aafc5df915 | ||
|
|
d4cbaeb1e7 | ||
|
|
36300aaa45 | ||
|
|
693800e0b8 | ||
|
|
245c0eecc6 | ||
|
|
25fe02602f | ||
|
|
5524ddc6a0 | ||
|
|
2fabbec217 | ||
|
|
3c080873e5 | ||
|
|
f252afb29f | ||
|
|
3109c0002f | ||
|
|
059d7d6ae5 | ||
|
|
58f159c0c9 | ||
|
|
5a6c115121 | ||
|
|
4cb85a543c | ||
|
|
d99c3e8f43 | ||
|
|
a2908b00ef | ||
|
|
7b88e161e4 | ||
|
|
96ea0d6ec0 | ||
|
|
bbfed01367 | ||
|
|
65d81a0d58 | ||
|
|
186908fd3b | ||
|
|
ab43314a53 | ||
|
|
ed1c76879d | ||
|
|
38af47f2eb | ||
|
|
302d84baf7 | ||
|
|
2b43f5f74f | ||
|
|
e4630052d1 | ||
|
|
8711f942c0 | ||
|
|
5282a6e045 | ||
|
|
55cbbfa920 | ||
|
|
68371d716b | ||
|
|
027c0b856f | ||
|
|
422978e404 | ||
|
|
8b5ce045e4 | ||
|
|
86f126e735 | ||
|
|
0e3898c065 | ||
|
|
1e3a74643e | ||
|
|
4792689998 | ||
|
|
0eec7f0813 | ||
|
|
2c1bf56638 | ||
|
|
57a4279647 | ||
|
|
f9146d62cf | ||
|
|
694528a294 | ||
|
|
f11968d2c6 | ||
|
|
b64b354f36 | ||
|
|
0cb7b61043 | ||
|
|
b45fb37b02 | ||
|
|
bfbb25c71e | ||
|
|
8a0e89c14d | ||
|
|
8c7573b669 | ||
|
|
0cae837e07 | ||
|
|
ef36fb567a | ||
|
|
5431fe1eac | ||
|
|
07b4e4b59d | ||
|
|
fa06da7f03 | ||
|
|
6cc6a34b74 | ||
|
|
7187c9ce71 | ||
|
|
b209ce2524 | ||
|
|
c37dd605cc | ||
|
|
d0a3ea99bd | ||
|
|
80077b92c4 | ||
|
|
dcf919e440 | ||
|
|
48569e05e8 | ||
|
|
60b24e9c3b | ||
|
|
fd4a97f547 | ||
|
|
8b348aeea9 | ||
|
|
6d0400e85e | ||
|
|
d60cb68430 | ||
|
|
5834940965 | ||
|
|
2680d3eb5c | ||
|
|
149a1e5b0d | ||
|
|
1e4a0bfb8e | ||
|
|
ba4c29c44d | ||
|
|
a632bedb4c | ||
|
|
fc8deeb0c5 | ||
|
|
e9e462b020 | ||
|
|
713d2cd7ee | ||
|
|
dea37b2e83 | ||
|
|
95e02956fb | ||
|
|
87ef568de0 | ||
|
|
a4c407e3ab | ||
|
|
543af45daa | ||
|
|
aca0442928 | ||
|
|
af1cc42b0d | ||
|
|
1bca40eb6a | ||
|
|
2c099c3a2a | ||
|
|
1afd9fdab2 | ||
|
|
522eea1371 | ||
|
|
e9fc115bdd | ||
|
|
711e4a84f8 | ||
|
|
ac6b9c855a | ||
|
|
eb1a9f8b9f | ||
|
|
4fc08faa48 | ||
|
|
4dcf6b672c | ||
|
|
36c8d8668a | ||
|
|
ffe3d1f5bb | ||
|
|
260cad776e | ||
|
|
3cb6fd1f44 | ||
|
|
8f0d0b24c6 | ||
|
|
f80a06b258 | ||
|
|
de1ce49ba3 | ||
|
|
f20b26809d | ||
|
|
dbf788976f | ||
|
|
2f8cd71c51 | ||
|
|
ea9757a288 | ||
|
|
782ca0c11a | ||
|
|
56d5474bd7 | ||
|
|
683c3a6122 | ||
|
|
b89dc25959 | ||
|
|
ab09fe19d8 | ||
|
|
d16cd4bf8e | ||
|
|
2e853006a5 | ||
|
|
fbfaf858e6 | ||
|
|
70511c3ece | ||
|
|
7c529eefb3 | ||
|
|
bb794cf8f3 | ||
|
|
20ae712719 | ||
|
|
607cd21f41 | ||
|
|
754394e1a8 | ||
|
|
7a8fcb46c7 | ||
|
|
59b5a5068d | ||
|
|
d65bdc103d | ||
|
|
aefd924f05 | ||
|
|
96018b9e6a | ||
|
|
3865d52c29 | ||
|
|
20c8133301 | ||
|
|
9705f56e2f | ||
|
|
f4d2459a10 | ||
|
|
7073baea6e | ||
|
|
7834e6294c | ||
|
|
a2d2d79ba4 | ||
|
|
087132a8b3 | ||
|
|
e62eca1fdf | ||
|
|
f0333d0f68 | ||
|
|
08e39ae6e0 | ||
|
|
128484dd73 | ||
|
|
1e2d14af4c | ||
|
|
37e3b86a36 | ||
|
|
13e49b7f2c | ||
|
|
8c3e556071 | ||
|
|
7a24f95566 | ||
|
|
dbc14f071a | ||
|
|
b75b5dc107 | ||
|
|
f1f6f3b0a8 | ||
|
|
a3f556a136 | ||
|
|
04ec1d3df4 | ||
|
|
da8000787d | ||
|
|
faa4e917f1 | ||
|
|
99dc05f55b | ||
|
|
0f2c0b8cc7 | ||
|
|
c4b1e75b0c | ||
|
|
b2eb932a06 | ||
|
|
aac6b15c77 | ||
|
|
2b4f1e3203 | ||
|
|
386320e12c | ||
|
|
e775e7918d | ||
|
|
e0f60ddf28 | ||
|
|
7e9b9500d5 | ||
|
|
29b143fb50 | ||
|
|
0a6f454bb4 | ||
|
|
ad21ab0a45 | ||
|
|
7958d32b90 | ||
|
|
81bfb6184e | ||
|
|
79bc30cbe0 | ||
|
|
076d741453 | ||
|
|
dc36e2e9d7 | ||
|
|
0a3da42af9 | ||
|
|
019d2b0c1b | ||
|
|
cf5c4bcd32 | ||
|
|
8c813103f2 | ||
|
|
ed97c83a4c | ||
|
|
a7dfb0b6d3 | ||
|
|
be544e6051 | ||
|
|
e457526e8f | ||
|
|
539e0a2045 | ||
|
|
fdcfca7633 | ||
|
|
bd0f0cabca | ||
|
|
a94edc6fe2 | ||
|
|
295ea93340 | ||
|
|
4af58fa882 | ||
|
|
9b4b10e3b6 | ||
|
|
24f27fcfb4 | ||
|
|
725a8b7959 | ||
|
|
c7b5b4b7dd | ||
|
|
04ffafb3b7 | ||
|
|
485b6903e5 | ||
|
|
b55d79564c | ||
|
|
e23de3adbc | ||
|
|
b97aa933a2 | ||
|
|
f0f7d5ea2f | ||
|
|
30af13cf67 | ||
|
|
45e8a2ffd8 | ||
|
|
b41bb21fba | ||
|
|
e828d82a5a | ||
|
|
563ac5e44c | ||
|
|
fbc46cf7bc | ||
|
|
5efc0dc11b | ||
|
|
d16ce01efd | ||
|
|
8ff0ca1855 | ||
|
|
6e49714e80 | ||
|
|
f402ef278f | ||
|
|
4fb32bc11b | ||
|
|
684b29d479 | ||
|
|
5b2f253f10 | ||
|
|
c88ad2c97b | ||
|
|
3e0f670e02 | ||
|
|
737e8167b4 | ||
|
|
23be635323 | ||
|
|
8d7d63a7fa | ||
|
|
8ad6c77c92 | ||
|
|
7d1f862185 | ||
|
|
2d9561de38 | ||
|
|
1c09d67846 | ||
|
|
5f5775d201 | ||
|
|
db64d97ef1 | ||
|
|
19b0f7e572 | ||
|
|
2fb0ccc752 | ||
|
|
e6f1abad32 | ||
|
|
816f36de50 | ||
|
|
193056531c | ||
|
|
ce356ce3ff | ||
|
|
334b6d7588 | ||
|
|
2c38082cec | ||
|
|
5dfc5d35ea | ||
|
|
9d6782e4eb | ||
|
|
81cf903e5f | ||
|
|
3d02a377ba | ||
|
|
e80659cf38 | ||
|
|
3c0eeba130 | ||
|
|
6911bc624f | ||
|
|
d732745e2d | ||
|
|
9cd0fb8420 | ||
|
|
e3ed704119 | ||
|
|
7ff575de97 | ||
|
|
c0083be420 | ||
|
|
4272a25ffd | ||
|
|
3e39e93999 | ||
|
|
b0dce71d1e | ||
|
|
fd66246ec5 | ||
|
|
0dc1598eca | ||
|
|
d5108acc99 | ||
|
|
d87facb618 | ||
|
|
e4c4b3f07d | ||
|
|
70c4a52321 | ||
|
|
7102ef0d26 | ||
|
|
e91383d558 | ||
|
|
a4708a5fce | ||
|
|
3d06200b71 | ||
|
|
4d173d7d76 | ||
|
|
1fb2f03835 | ||
|
|
15d010d895 | ||
|
|
7367b19515 | ||
|
|
9202ee5289 | ||
|
|
a4bb04786d | ||
|
|
94591fdd4b | ||
|
|
3df7b27d9b | ||
|
|
bbf1bcbf58 | ||
|
|
383c3ec00c | ||
|
|
e328cdc21a | ||
|
|
c80b48e9c8 | ||
|
|
4128ae3aed | ||
|
|
e34f5b01bb | ||
|
|
ef80df69ca | ||
|
|
4a34d6c599 | ||
|
|
55bb191268 | ||
|
|
36ae02a44d | ||
|
|
43f282dded | ||
|
|
639418692e | ||
|
|
12e3a0cddd | ||
|
|
c6a6e9511f | ||
|
|
5f34726b22 | ||
|
|
f9f16ec609 | ||
|
|
ae7f14a975 | ||
|
|
1a2652855a | ||
|
|
bfb52060e7 | ||
|
|
371998cef4 | ||
|
|
af379947b8 | ||
|
|
09bddc0958 | ||
|
|
b4e971596d | ||
|
|
57bc7251cf | ||
|
|
576142a25a | ||
|
|
76a7f168af | ||
|
|
5b1ae587c0 | ||
|
|
c9cdcaeb94 | ||
|
|
39581801c6 | ||
|
|
3983d39f0c | ||
|
|
4fc41816ca | ||
|
|
d102ff4de7 | ||
|
|
adf34ee85a | ||
|
|
870f1c11da | ||
|
|
11aedd1274 | ||
|
|
ef04c874f0 | ||
|
|
d478f74252 | ||
|
|
3d140ae9c2 | ||
|
|
9dc652322f | ||
|
|
0ae7f59444 | ||
|
|
e215d473c6 | ||
|
|
96cbd24d4f | ||
|
|
665f12785d | ||
|
|
7a933ff297 | ||
|
|
9f59cce7c9 | ||
|
|
2c04528c12 | ||
|
|
aad22e2698 | ||
|
|
3714deac89 | ||
|
|
37c4c9538b | ||
|
|
5ca5473c1b | ||
|
|
4b5bc7b2d7 | ||
|
|
72aa9fe46f | ||
|
|
ce08c5165b | ||
|
|
61e6b9476d | ||
|
|
4466787c94 | ||
|
|
0df0e891cb | ||
|
|
89d608b2ac | ||
|
|
5b0c09f75d | ||
|
|
a9e114388f | ||
|
|
31c428ccbc | ||
|
|
9d4d70ee76 | ||
|
|
5258327694 | ||
|
|
e80875e3b0 | ||
|
|
cb42dcf2c4 | ||
|
|
fe9f3f0a55 | ||
|
|
712d193f7c | ||
|
|
c546185dda | ||
|
|
5945a1c063 | ||
|
|
79a17b4720 | ||
|
|
0c69eb2910 | ||
|
|
9f5b865949 | ||
|
|
fccbe1c700 | ||
|
|
a67f503b15 | ||
|
|
b9d37a87d3 | ||
|
|
103580954b | ||
|
|
a46a5cb723 | ||
|
|
7c9bb0786f | ||
|
|
a10e365fe3 | ||
|
|
bb11d417aa | ||
|
|
1014c5b8fc | ||
|
|
0f52beeda0 | ||
|
|
b34c87a4a8 | ||
|
|
c4ea8c8902 | ||
|
|
67f79e447e | ||
|
|
c55e2b5223 | ||
|
|
58020a2ddd | ||
|
|
263fb75e40 | ||
|
|
e2253b07f8 | ||
|
|
fd9a23a2b4 | ||
|
|
1f175e3693 | ||
|
|
cb36ceff9e | ||
|
|
3634071686 | ||
|
|
5e09d7cafd | ||
|
|
9fc574a1da | ||
|
|
f57a3770e6 | ||
|
|
f3de70d69e | ||
|
|
5bf5ebc279 | ||
|
|
0ffbf02ba0 | ||
|
|
73ffaaecd7 | ||
|
|
b68e5f8b03 | ||
|
|
79586b2d3f | ||
|
|
5307e9e4a6 | ||
|
|
900225fb8d | ||
|
|
0d2993ea16 | ||
|
|
019857bc61 | ||
|
|
aec642d6f3 | ||
|
|
0ce25946d4 | ||
|
|
d4d897b53a | ||
|
|
e7967c441c | ||
|
|
9122e8bf0e | ||
|
|
3ac4e30a5f | ||
|
|
668b77c91f | ||
|
|
5dae658d4b | ||
|
|
adc132609b | ||
|
|
c9853a4058 | ||
|
|
336ad925aa | ||
|
|
624dd408c1 | ||
|
|
aa7ffbe88d | ||
|
|
bb6ae3ec00 | ||
|
|
e568f77de3 | ||
|
|
ec23dfba68 | ||
|
|
817a67a118 | ||
|
|
bf5aa1321c | ||
|
|
85d1be85a3 | ||
|
|
ecc15cb462 | ||
|
|
4bbf813617 | ||
|
|
28fd8b104c | ||
|
|
578c3732b3 | ||
|
|
7568f1581f | ||
|
|
0d07ef8eab | ||
|
|
301bbf4206 | ||
|
|
14057bf3b4 | ||
|
|
01527c1dba | ||
|
|
3350b7e8e8 | ||
|
|
673d6d776e | ||
|
|
4169f3880c | ||
|
|
a21b17be89 | ||
|
|
dad9f4f790 | ||
|
|
f6b5fa83af | ||
|
|
a3bcef09d4 | ||
|
|
545fb1368a | ||
|
|
d737131f27 | ||
|
|
ead2477400 | ||
|
|
aa7cc66a81 | ||
|
|
d82882c98a | ||
|
|
4e0584177f | ||
|
|
bca62140d0 | ||
|
|
1e80cfe7b3 | ||
|
|
a27863e67a | ||
|
|
d491eeb314 | ||
|
|
c943d0c796 | ||
|
|
bf749e02e2 | ||
|
|
ea7a17fbc6 | ||
|
|
7da23271b9 | ||
|
|
49204ac5b0 | ||
|
|
d38fb3c30b | ||
|
|
9a2f75d33e | ||
|
|
ce64e201ce | ||
|
|
49ac3859a6 | ||
|
|
5e63f53b4e | ||
|
|
a24a1d55fa | ||
|
|
7e69310202 | ||
|
|
9d65bd006d | ||
|
|
552cb854e6 | ||
|
|
7cc9f23792 | ||
|
|
8b3a852508 | ||
|
|
1bbbbffd8d | ||
|
|
52906e8041 | ||
|
|
6e402f3c97 | ||
|
|
549d8debb8 | ||
|
|
52027b028a | ||
|
|
8d922e55d9 | ||
|
|
5eea175b76 | ||
|
|
be843d056e | ||
|
|
7a58268c25 | ||
|
|
eb23baed5d | ||
|
|
afb699bc12 | ||
|
|
3112f4158b | ||
|
|
2cce3eb452 | ||
|
|
bf58de7c1f | ||
|
|
fa797f219f | ||
|
|
f6f859a343 | ||
|
|
baf43cefe9 | ||
|
|
7c4e3cfe32 | ||
|
|
ed6b41823d | ||
|
|
32f141aa2b | ||
|
|
9f76ece5cf | ||
|
|
ba08346083 | ||
|
|
fe2d418b3a | ||
|
|
e4e6e0f4ad | ||
|
|
cb2214ef9b | ||
|
|
ca1289460d | ||
|
|
0540b9eb12 | ||
|
|
0a6cf92f54 | ||
|
|
835ff9ec19 | ||
|
|
4ba0f4bba3 | ||
|
|
654769b67b | ||
|
|
4d64ae5944 | ||
|
|
c6f1890055 | ||
|
|
55f4a6f021 | ||
|
|
f8c8b4d368 | ||
|
|
be5b248f21 | ||
|
|
6a11f8bd41 | ||
|
|
2cc6d5ad03 | ||
|
|
a21c80f843 | ||
|
|
90060a36c6 | ||
|
|
ac5c2372fa | ||
|
|
f6efcd160f | ||
|
|
83ec2b97d6 | ||
|
|
3220550633 | ||
|
|
bafafc281a | ||
|
|
b58a0d22c6 | ||
|
|
55c25efca7 | ||
|
|
967a5bf612 | ||
|
|
fcb762ee8f | ||
|
|
38a941e986 | ||
|
|
3544e1bac3 | ||
|
|
9c81714568 | ||
|
|
dff93c491c | ||
|
|
0b7ffdc45a | ||
|
|
71cfc65f68 | ||
|
|
005548bdd0 | ||
|
|
aa285d8409 | ||
|
|
a801166692 | ||
|
|
8259c57fad | ||
|
|
0b47fadef7 | ||
|
|
4453001b30 | ||
|
|
58999c28fe | ||
|
|
74d06e5aad | ||
|
|
662c8f5ca2 | ||
|
|
b5b5f82464 | ||
|
|
7593ec5587 | ||
|
|
94ac0ef90c | ||
|
|
36093288f7 | ||
|
|
d226b3c3c1 | ||
|
|
13215e5c16 | ||
|
|
f6c6eb441f | ||
|
|
e883ce7c43 | ||
|
|
4836a99fcd | ||
|
|
a7075067bf | ||
|
|
c8edea25ae | ||
|
|
07731fc2de | ||
|
|
8c436d5c0e | ||
|
|
16a2b1979e | ||
|
|
7753613cf4 | ||
|
|
d156a6ffee | ||
|
|
f54b614ece | ||
|
|
24ed769eac | ||
|
|
ef8387f7c7 | ||
|
|
c76c37c874 | ||
|
|
bf41cb70f8 | ||
|
|
f7e759c69f | ||
|
|
376360caac | ||
|
|
66fa02c90a | ||
|
|
26cc2e3c9e | ||
|
|
09ceb1b5ce | ||
|
|
1204e971c2 | ||
|
|
9833df2110 | ||
|
|
7843170978 | ||
|
|
93c1535b76 | ||
|
|
c3c6e2e757 | ||
|
|
2d4b3514bc | ||
|
|
a27fce18ed | ||
|
|
1adc1833dc | ||
|
|
9a1c721a46 | ||
|
|
9bd7d49fe7 | ||
|
|
e94f128de8 | ||
|
|
9005a58e43 | ||
|
|
12f07c57f2 | ||
|
|
3c6066f047 | ||
|
|
99d39c6d71 | ||
|
|
24d088ac79 | ||
|
|
268ddd9aae | ||
|
|
ace28b2051 | ||
|
|
beb9b8c19c | ||
|
|
9f1c23ea67 | ||
|
|
901a41dab1 | ||
|
|
8777cb3a72 | ||
|
|
c34a9719d9 | ||
|
|
54a3535373 | ||
|
|
7e2cecc838 | ||
|
|
33a61c41d4 | ||
|
|
e21d5ea4ef | ||
|
|
96c12b38ed | ||
|
|
2faa17a608 | ||
|
|
5386d3b19f | ||
|
|
56b219ed0d | ||
|
|
5bfd82272d | ||
|
|
9af0480418 | ||
|
|
bcbd9535e5 | ||
|
|
e4bf263fe2 | ||
|
|
a5da416a54 | ||
|
|
1dbc9c968f | ||
|
|
b5b6b293ca | ||
|
|
b7df027c05 | ||
|
|
6890c2b182 | ||
|
|
2f90728b82 | ||
|
|
d8e445315b | ||
|
|
26d99cc790 | ||
|
|
52c273d47f | ||
|
|
9ebca23a96 | ||
|
|
0775c745dd | ||
|
|
f4111a0185 | ||
|
|
6ad88accf4 | ||
|
|
9669b7bd16 | ||
|
|
a37e27a82f | ||
|
|
85e347dfe2 | ||
|
|
afcaa89cc3 | ||
|
|
c87c8952c4 | ||
|
|
d08d8dacb4 | ||
|
|
2fbad4fa91 | ||
|
|
08731d2708 | ||
|
|
a684b7c101 | ||
|
|
6000d8c939 | ||
|
|
4a30157638 | ||
|
|
16fc317543 | ||
|
|
40e6b801ef | ||
|
|
e908b4d996 | ||
|
|
a4230df2be | ||
|
|
37894a26e7 | ||
|
|
d9ee36076c | ||
|
|
f9f07de8ef | ||
|
|
141560e159 | ||
|
|
f7a9697779 | ||
|
|
e778e3e85f | ||
|
|
c227b5c37b | ||
|
|
bdf03ab962 | ||
|
|
07e2cdbac1 | ||
|
|
ec0c27c421 | ||
|
|
5670d47c5f | ||
|
|
c8a3bf2d8a | ||
|
|
cc28f877fd | ||
|
|
39c746068a | ||
|
|
f37ad668be | ||
|
|
e87561fb17 | ||
|
|
96084ca57f | ||
|
|
186b60dcfe | ||
|
|
aa10fbaf9f | ||
|
|
2954b353c9 | ||
|
|
63614e5e0c | ||
|
|
899ac29ec1 | ||
|
|
722683fae2 | ||
|
|
5eb82b5ef9 | ||
|
|
89e8f0ba2d | ||
|
|
f6cfde92c4 | ||
|
|
35b7063ca8 | ||
|
|
23357de4db | ||
|
|
0850db9f0d | ||
|
|
0206bb94be | ||
|
|
d25bbd216d | ||
|
|
aedd584fbc | ||
|
|
4a68e2c8be | ||
|
|
926fbc7fe7 | ||
|
|
7cf4ced17d | ||
|
|
b415c8b3da | ||
|
|
b58273b98c | ||
|
|
bd5f66b5d5 | ||
|
|
3abe640640 | ||
|
|
95bc685459 | ||
|
|
e65c8ceace | ||
|
|
d41080c4e3 | ||
|
|
95e4169108 | ||
|
|
09ed3ffd3f | ||
|
|
0cd8ac88d6 | ||
|
|
99a022f647 | ||
|
|
6aab3d842b | ||
|
|
430e004c60 | ||
|
|
022a2a9f78 | ||
|
|
821657c447 | ||
|
|
c03e7517a5 | ||
|
|
a89032b5e4 | ||
|
|
d6c55cf555 | ||
|
|
2da2f63503 | ||
|
|
ab1bb2ee5d | ||
|
|
326e9a82c9 | ||
|
|
fbdba3ef69 | ||
|
|
312c4e7211 | ||
|
|
3386e1e6b0 | ||
|
|
a11565e94c | ||
|
|
0c0e6d5017 | ||
|
|
4c33e88c63 | ||
|
|
aa582fb0d8 | ||
|
|
c38fdf8ccc | ||
|
|
305c2743ba | ||
|
|
5cc085c6a6 | ||
|
|
3859779f0a | ||
|
|
9857469cd1 | ||
|
|
ff805d32fb | ||
|
|
5a94e58236 | ||
|
|
9ff7da58ea | ||
|
|
30d974ff4a | ||
|
|
11709dc594 | ||
|
|
246c31bac4 | ||
|
|
2a8cc9ad70 | ||
|
|
098844bb4a | ||
|
|
11a064a05b | ||
|
|
b2adf99c2a | ||
|
|
937d9fab36 | ||
|
|
3c4d5a8324 | ||
|
|
d66ae036e5 | ||
|
|
28dcb92f4c | ||
|
|
162059df91 | ||
|
|
439085c873 | ||
|
|
e3c34b6e8b | ||
|
|
8a6e60f961 | ||
|
|
eedcf5b2e8 | ||
|
|
bad3cb5c7e | ||
|
|
2351a16fae | ||
|
|
5779c11097 | ||
|
|
ab34ab3429 | ||
|
|
98487a18e5 | ||
|
|
fae46c2af0 | ||
|
|
2725d3a386 | ||
|
|
4601d61385 | ||
|
|
2b1081b523 | ||
|
|
c2bcbd3597 | ||
|
|
d4367475fb | ||
|
|
e62d7b2132 | ||
|
|
13486e3dfa | ||
|
|
34e4069b2f | ||
|
|
5ed921d8dc | ||
|
|
6e9c2191eb | ||
|
|
afb9f1ba26 | ||
|
|
acc9fd8101 | ||
|
|
60f9b4ae33 | ||
|
|
8d3e241869 | ||
|
|
0cfd35e74f | ||
|
|
dba8fa3a42 | ||
|
|
bf4d7101b8 | ||
|
|
bff1d5fa6f | ||
|
|
f8f5a59520 | ||
|
|
dc2bd3a5d6 | ||
|
|
5b7cf32314 | ||
|
|
5f99680b01 | ||
|
|
f76c4b0405 | ||
|
|
d8425ce831 | ||
|
|
7e42e5bb54 | ||
|
|
7c2533b6fb | ||
|
|
ae05034927 | ||
|
|
1c331d57b3 | ||
|
|
db5cda70ad | ||
|
|
8196ac451d | ||
|
|
e2cfeec10a | ||
|
|
9923c1ae65 | ||
|
|
957634f90c | ||
|
|
6be78b2a54 | ||
|
|
f04972666c | ||
|
|
e25caa6e03 | ||
|
|
1525bc447d | ||
|
|
700dc9e81d | ||
|
|
6aafd74ddf | ||
|
|
f519ff427b | ||
|
|
a89ec4df27 | ||
|
|
c4cb648a2a | ||
|
|
0c8c3feb0c | ||
|
|
5529eeaa72 | ||
|
|
8b1055bdda | ||
|
|
df23d9fe63 | ||
|
|
18328711b2 | ||
|
|
dc996ed44c | ||
|
|
e2bc6e44e2 | ||
|
|
b667718fc2 | ||
|
|
4ae5707068 | ||
|
|
a14e8dbc52 | ||
|
|
a9344e0069 | ||
|
|
9cdf831dd1 | ||
|
|
0857e2fee3 | ||
|
|
69553c3bf3 | ||
|
|
5ce5da1443 | ||
|
|
ede95247c7 | ||
|
|
9b7294c34c | ||
|
|
58ebe8c5eb | ||
|
|
67ed742858 | ||
|
|
538a6680e7 | ||
|
|
e16d42af87 | ||
|
|
6ee62a7e1f | ||
|
|
3adfe54788 | ||
|
|
673ac6503f | ||
|
|
b226b87b4b | ||
|
|
a62c0c623a | ||
|
|
e7981f61bb | ||
|
|
cfe131815e | ||
|
|
bee1b32f4c | ||
|
|
5861d61444 | ||
|
|
9852f41683 | ||
|
|
d1f8ae2c3d | ||
|
|
bdc1c9d6c2 | ||
|
|
2b7a7b1da8 | ||
|
|
9ef115b74f | ||
|
|
3e1395466c | ||
|
|
54c667cbed | ||
|
|
1e224c588d | ||
|
|
5bd410cc4c | ||
|
|
67ad2676a1 | ||
|
|
0910f4c002 | ||
|
|
ea8e91c0cc | ||
|
|
db5517e0c9 | ||
|
|
77d787d5b3 | ||
|
|
69f3180b63 | ||
|
|
ee2abb0517 | ||
|
|
d7a938a90b | ||
|
|
4d292a077a | ||
|
|
526c9d674d | ||
|
|
36f624da04 | ||
|
|
9d2a77599e | ||
|
|
e9fbe7d303 | ||
|
|
47757bde94 | ||
|
|
f71c410d36 | ||
|
|
8f8a3f8de6 | ||
|
|
a338e87b2e | ||
|
|
5801c5ddd0 | ||
|
|
5b71da0223 | ||
|
|
ed85728729 | ||
|
|
9b5b661f1c | ||
|
|
a5790aaba3 | ||
|
|
698b149271 | ||
|
|
15c28b0ecb | ||
|
|
b38f533ccc | ||
|
|
5d61376486 | ||
|
|
28c9e5ff78 | ||
|
|
a35f978a08 | ||
|
|
8fb8ecf9f9 | ||
|
|
c8be4d1e4a | ||
|
|
8a13c908f8 | ||
|
|
0ab9cab46d | ||
|
|
ae9e31a127 | ||
|
|
ea74d066ac | ||
|
|
3a8da3a818 | ||
|
|
7b2273d180 | ||
|
|
2bba112bc0 | ||
|
|
f747ac2fc9 | ||
|
|
50112ef89f | ||
|
|
c5cce0a87b | ||
|
|
bae21c87f4 | ||
|
|
edf77a6c0f | ||
|
|
232a0b2acf | ||
|
|
06381cb094 | ||
|
|
33a16bdd51 | ||
|
|
1b1276cc90 | ||
|
|
8d2d7a8884 | ||
|
|
3894c12505 | ||
|
|
504d952581 | ||
|
|
e3777ac18a | ||
|
|
4b087d8943 | ||
|
|
d383db5874 | ||
|
|
fa75a76ed8 | ||
|
|
bc28507ea6 | ||
|
|
e470df06ec | ||
|
|
9b81157d5d | ||
|
|
40bf919931 | ||
|
|
e55f7af9b3 | ||
|
|
40495dc86f | ||
|
|
75f01ca497 | ||
|
|
6ebdf327e3 | ||
|
|
9741c45e52 | ||
|
|
d193929dd7 | ||
|
|
19cdc8b3cf | ||
|
|
8ac97c332d | ||
|
|
1b61bbafa9 | ||
|
|
9745b308d2 | ||
|
|
3f2e162523 | ||
|
|
9669afb772 | ||
|
|
105842023b | ||
|
|
224ce99d79 | ||
|
|
1d696edf56 | ||
|
|
ae8e244418 | ||
|
|
a1f51e9400 | ||
|
|
d0c5392cf9 | ||
|
|
998c597e17 | ||
|
|
c943ffd30a | ||
|
|
1f54884a38 | ||
|
|
df0c91a448 | ||
|
|
cceaaa5106 | ||
|
|
380aa3d178 | ||
|
|
e1624b4fe6 | ||
|
|
e3bf1e8ca1 | ||
|
|
6cb2553cc3 | ||
|
|
5ad4e86b8a | ||
|
|
4d392ff48e | ||
|
|
16e542d302 | ||
|
|
3a252c911c | ||
|
|
128ce34a41 | ||
|
|
b65f7231f8 | ||
|
|
7412faf534 | ||
|
|
ac4dd28927 | ||
|
|
1672451def | ||
|
|
96269ba437 | ||
|
|
320a6311c5 | ||
|
|
7c2a96b04a | ||
|
|
704359cdba | ||
|
|
87a1a893bf | ||
|
|
66421250b4 | ||
|
|
374f2b22b5 | ||
|
|
5b3a287bf4 | ||
|
|
56bc9ca01a | ||
|
|
18dcbf3568 | ||
|
|
bfc8dd681d | ||
|
|
86412f34f3 | ||
|
|
fc416db742 | ||
|
|
a6a5092abd | ||
|
|
522360bbe7 | ||
|
|
ed5b6e4b1d | ||
|
|
4224fe3211 | ||
|
|
cb99d5b007 | ||
|
|
be1ba18f7f | ||
|
|
52b54d8584 | ||
|
|
665822d2f8 | ||
|
|
fbfa3b2974 | ||
|
|
b3b0ef8f0b | ||
|
|
d255b29832 | ||
|
|
5a0198d10d | ||
|
|
3781a118a1 | ||
|
|
ca3262d554 | ||
|
|
c5194b254a | ||
|
|
4a369b8af7 | ||
|
|
93bddd469d | ||
|
|
8cf3d028b7 | ||
|
|
23af660dc0 | ||
|
|
11119654eb | ||
|
|
6f9d3b7fa9 | ||
|
|
0c64ffd362 | ||
|
|
5d316d4d94 | ||
|
|
aa3df50a30 | ||
|
|
8308a78834 | ||
|
|
c02a0f27ca | ||
|
|
d36d798c1b | ||
|
|
7eb5ed7bcd | ||
|
|
584aa6bf5f | ||
|
|
d90915e25b | ||
|
|
e787abb6de | ||
|
|
7e4c2f2cf3 | ||
|
|
fb709389c4 | ||
|
|
f22fe9eec1 | ||
|
|
4352309f9f | ||
|
|
687f3533c8 | ||
|
|
2b99240be3 | ||
|
|
e03fa42d63 | ||
|
|
8639ed4d00 | ||
|
|
cf9dca228a | ||
|
|
be66de7584 | ||
|
|
f47b73ee01 | ||
|
|
d5703b0e43 | ||
|
|
293e10e944 | ||
|
|
328baea33b | ||
|
|
6c0b38d946 | ||
|
|
c0221fc0e5 | ||
|
|
f1418ee99d | ||
|
|
111bd40b94 | ||
|
|
ef477f5642 | ||
|
|
c785fc957c | ||
|
|
eeaeed9d83 | ||
|
|
483d66b2cd | ||
|
|
e2332ee760 | ||
|
|
14e3cb049e | ||
|
|
c021d2fa5b | ||
|
|
20e365b0c0 | ||
|
|
1ef4c9878d | ||
|
|
7e47a68acf | ||
|
|
fe5502e0dd | ||
|
|
392cc2ba43 | ||
|
|
b93c15bc38 | ||
|
|
5d86c6f0e1 | ||
|
|
dbfca7c677 | ||
|
|
714c8d2e1c | ||
|
|
e6e0f259f4 | ||
|
|
a03580a64d | ||
|
|
588f80c282 | ||
|
|
4e3bedac31 | ||
|
|
953c1eb639 | ||
|
|
aa4f14c461 | ||
|
|
6937c86e66 | ||
|
|
c1ea4741eb | ||
|
|
d4d5edd0f3 | ||
|
|
3d8dd63fad | ||
|
|
8bc6477e88 | ||
|
|
5743814d90 | ||
|
|
d631b3d799 | ||
|
|
6b4cf58454 | ||
|
|
e864e7c2bf | ||
|
|
ebca3f0f5f | ||
|
|
8216ba11b8 | ||
|
|
ae67f84476 | ||
|
|
59f1313bb2 | ||
|
|
04341c8c3c | ||
|
|
0a315b50b4 | ||
|
|
36a453f6c0 | ||
|
|
991f079deb | ||
|
|
5110b4449c | ||
|
|
0e8fb9d56d | ||
|
|
868dd01952 | ||
|
|
47be4ed8dc | ||
|
|
392c00a635 | ||
|
|
aaa99ded43 | ||
|
|
838a6be13d | ||
|
|
b546e130bc | ||
|
|
3254afce29 | ||
|
|
417a113ca7 | ||
|
|
aef814b094 | ||
|
|
47075dadec | ||
|
|
c1e9e0c55f | ||
|
|
70b0698504 | ||
|
|
3f00534549 | ||
|
|
392fbf6af4 | ||
|
|
552b18148f | ||
|
|
08007ad3b2 | ||
|
|
e8890e6ba3 | ||
|
|
b10e4fc487 | ||
|
|
1ce92bcfbc | ||
|
|
7e9cd2f672 | ||
|
|
bc1c06b01f | ||
|
|
2be6d36fc6 | ||
|
|
75f7aeb588 | ||
|
|
2944694492 | ||
|
|
53b29b5bdd | ||
|
|
0d4f73b269 | ||
|
|
f5f2d2734d | ||
|
|
4b5507333c | ||
|
|
7d6d4d4cec | ||
|
|
3c86cb0e69 | ||
|
|
73f5d46e76 | ||
|
|
8cd02ac3ed | ||
|
|
8b208f9dbd | ||
|
|
11065519aa | ||
|
|
5124f9a054 | ||
|
|
6c0646acc6 | ||
|
|
60bfc8f856 | ||
|
|
c57f0f84af | ||
|
|
cd1a5bc169 | ||
|
|
27f0fe30e7 | ||
|
|
c24e008865 | ||
|
|
a852ed88f7 | ||
|
|
57236e7010 | ||
|
|
1f7389d1e1 | ||
|
|
3a213a273d | ||
|
|
7602c70d98 | ||
|
|
54fda40fcf | ||
|
|
ce038fde8c | ||
|
|
dbe2c2be9b | ||
|
|
85c66f1b1c | ||
|
|
4b72468f0c | ||
|
|
ecaedf15d0 | ||
|
|
1d9bee9fe7 | ||
|
|
22a30cef11 | ||
|
|
def7a36f70 | ||
|
|
064adcfc2a | ||
|
|
c84eedbce0 | ||
|
|
10babaa9e6 | ||
|
|
2f8f3264df | ||
|
|
5f58762dda | ||
|
|
e293000db9 | ||
|
|
cff344cda5 | ||
|
|
ba9c622d7a | ||
|
|
8b44770072 | ||
|
|
371c5c8c32 | ||
|
|
ddac691e8d | ||
|
|
71b658612c | ||
|
|
6f3bba910e | ||
|
|
4e197885c8 | ||
|
|
8802375dc0 | ||
|
|
5298879240 | ||
|
|
2f8f40df0d | ||
|
|
3c3216f384 | ||
|
|
346c9308f2 | ||
|
|
7468c52865 | ||
|
|
49c7fb5d67 | ||
|
|
ad3d11daf4 | ||
|
|
4e5d485157 | ||
|
|
e9097670f8 | ||
|
|
53ba24a087 | ||
|
|
271d046248 | ||
|
|
beffe9cdcc | ||
|
|
e7a375d912 | ||
|
|
663dd96019 | ||
|
|
7eeb33a973 | ||
|
|
e95195151d | ||
|
|
d81c2e190a | ||
|
|
6628a52b66 | ||
|
|
e5ff306200 | ||
|
|
43f4023370 | ||
|
|
5f6956dd47 | ||
|
|
91247b1bb5 | ||
|
|
f4056986fa | ||
|
|
30a752928f | ||
|
|
bff66cf0dd | ||
|
|
1abf0bc157 | ||
|
|
6e845e765d | ||
|
|
ef18c1a673 | ||
|
|
a32460b8d0 | ||
|
|
aedfaa05ca | ||
|
|
78a30a7f51 | ||
|
|
a59215e082 | ||
|
|
a41f084fad | ||
|
|
28edec8158 | ||
|
|
9870a631c4 | ||
|
|
5027f188c9 | ||
|
|
df157a5940 | ||
|
|
1a50843ee0 | ||
|
|
fb1b365fcf | ||
|
|
393568499d | ||
|
|
b24bf11192 |
98
.github/workflows/deploy.yml
vendored
98
.github/workflows/deploy.yml
vendored
@@ -1,63 +1,63 @@
|
|||||||
name: Deploy to GitHub Pages
|
name: Deploy to GitHub Pages
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: 'main'
|
branches: 'main'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_site:
|
build_site:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 20
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
gpx/package-lock.json
|
gpx/package-lock.json
|
||||||
website/package-lock.json
|
website/package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies for gpx
|
||||||
|
run: npm install --prefix gpx
|
||||||
|
|
||||||
- name: Install dependencies for gpx
|
- name: Build gpx
|
||||||
run: npm install --prefix gpx
|
run: npm run build --prefix gpx
|
||||||
|
|
||||||
- name: Build gpx
|
- name: Install dependencies for website
|
||||||
run: npm run build --prefix gpx
|
run: npm install --prefix website
|
||||||
|
|
||||||
- name: Install dependencies for website
|
- name: Create env file
|
||||||
run: npm install --prefix website
|
run: |
|
||||||
|
touch website/.env
|
||||||
|
echo PUBLIC_MAPBOX_TOKEN=${{ secrets.PUBLIC_MAPBOX_TOKEN }} >> website/.env
|
||||||
|
cat website/.env
|
||||||
|
|
||||||
- name: Create env file
|
- name: Build website
|
||||||
run: |
|
env:
|
||||||
touch website/.env
|
BASE_PATH: ''
|
||||||
echo PUBLIC_MAPBOX_TOKEN=${{ secrets.PUBLIC_MAPBOX_TOKEN }} >> website/.env
|
run: |
|
||||||
cat website/.env
|
npm run build --prefix website
|
||||||
|
|
||||||
- name: Build website
|
- name: Upload Artifacts
|
||||||
env:
|
uses: actions/upload-pages-artifact@v3
|
||||||
BASE_PATH: ''
|
with:
|
||||||
run: |
|
path: 'website/build/'
|
||||||
npm run build --prefix website
|
|
||||||
|
|
||||||
- name: Upload Artifacts
|
deploy:
|
||||||
uses: actions/upload-pages-artifact@v4
|
needs: build_site
|
||||||
with:
|
runs-on: ubuntu-latest
|
||||||
path: 'website/build/'
|
|
||||||
|
|
||||||
deploy:
|
permissions:
|
||||||
needs: build_site
|
pages: write
|
||||||
runs-on: ubuntu-latest
|
id-token: write
|
||||||
|
|
||||||
permissions:
|
environment:
|
||||||
pages: write
|
name: github-pages
|
||||||
id-token: write
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
|
||||||
environment:
|
steps:
|
||||||
name: github-pages
|
- name: Deploy
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
steps:
|
|
||||||
- name: Deploy
|
|
||||||
id: deployment
|
|
||||||
uses: actions/deploy-pages@v4
|
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
website/src/lib/components/ui
|
# Ignore files for PNPM, NPM and YARN
|
||||||
website/src/lib/docs/**/*.mdx
|
pnpm-lock.yaml
|
||||||
**/*.webmanifest
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
src/lib/components/ui
|
||||||
|
*.mdx
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2026 gpx.studio
|
Copyright (c) 2024 gpx.studio
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"postinstall": "npm run build",
|
"postinstall": "npm run build",
|
||||||
"lint": "prettier --check . --config ../.prettierrc && eslint .",
|
"lint": "prettier --check . && eslint .",
|
||||||
"format": "prettier --write . --config ../.prettierrc"
|
"format": "prettier --write ."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
675
gpx/src/gpx.ts
675
gpx/src/gpx.ts
@@ -1,5 +1,4 @@
|
|||||||
import { ramerDouglasPeucker } from './simplify';
|
import { ramerDouglasPeucker } from './simplify';
|
||||||
import { GPXStatistics, GPXStatisticsGroup, TrackPointLocalStatistics } from './statistics';
|
|
||||||
import {
|
import {
|
||||||
Coordinates,
|
Coordinates,
|
||||||
GPXFileAttributes,
|
GPXFileAttributes,
|
||||||
@@ -18,9 +17,6 @@ import {
|
|||||||
import { immerable, isDraft, original, freeze } from 'immer';
|
import { immerable, isDraft, original, freeze } from 'immer';
|
||||||
|
|
||||||
function cloneJSON<T>(obj: T): T {
|
function cloneJSON<T>(obj: T): T {
|
||||||
if (obj === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (obj === null || typeof obj !== 'object') {
|
if (obj === null || typeof obj !== 'object') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -37,6 +33,7 @@ export abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
|
|||||||
abstract getNumberOfTrackPoints(): number;
|
abstract getNumberOfTrackPoints(): number;
|
||||||
abstract getStartTimestamp(): Date | undefined;
|
abstract getStartTimestamp(): Date | undefined;
|
||||||
abstract getEndTimestamp(): Date | undefined;
|
abstract getEndTimestamp(): Date | undefined;
|
||||||
|
abstract getStatistics(): GPXStatistics;
|
||||||
abstract getSegments(): TrackSegment[];
|
abstract getSegments(): TrackSegment[];
|
||||||
abstract getTrackPoints(): TrackPoint[];
|
abstract getTrackPoints(): TrackPoint[];
|
||||||
|
|
||||||
@@ -76,6 +73,14 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement
|
|||||||
return this.children[this.children.length - 1].getEndTimestamp();
|
return this.children[this.children.length - 1].getEndTimestamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStatistics(): GPXStatistics {
|
||||||
|
let statistics = new GPXStatistics();
|
||||||
|
for (let child of this.children) {
|
||||||
|
statistics.mergeWith(child.getStatistics());
|
||||||
|
}
|
||||||
|
return statistics;
|
||||||
|
}
|
||||||
|
|
||||||
getSegments(): TrackSegment[] {
|
getSegments(): TrackSegment[] {
|
||||||
return this.children.flatMap((child) => child.getSegments());
|
return this.children.flatMap((child) => child.getSegments());
|
||||||
}
|
}
|
||||||
@@ -140,9 +145,7 @@ export class GPXFile extends GPXTreeNode<Track> {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.wpt = gpx.wpt
|
this.wpt = gpx.wpt ? gpx.wpt.map((waypoint) => new Waypoint(waypoint)) : [];
|
||||||
? gpx.wpt.map((waypoint, index) => new Waypoint(waypoint, index))
|
|
||||||
: [];
|
|
||||||
this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : [];
|
this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : [];
|
||||||
if (gpx.rte && gpx.rte.length > 0) {
|
if (gpx.rte && gpx.rte.length > 0) {
|
||||||
this.trk = this.trk.concat(gpx.rte.map((route) => convertRouteToTrack(route)));
|
this.trk = this.trk.concat(gpx.rte.map((route) => convertRouteToTrack(route)));
|
||||||
@@ -180,6 +183,9 @@ export class GPXFile extends GPXTreeNode<Track> {
|
|||||||
segment._data['segmentIndex'] = segmentIndex;
|
segment._data['segmentIndex'] = segmentIndex;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
this.wpt.forEach((waypoint, waypointIndex) => {
|
||||||
|
waypoint._data['index'] = waypointIndex;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get children(): Array<Track> {
|
get children(): Array<Track> {
|
||||||
@@ -200,16 +206,8 @@ export class GPXFile extends GPXTreeNode<Track> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatistics(): GPXStatisticsGroup {
|
|
||||||
let statistics = new GPXStatisticsGroup();
|
|
||||||
this.forEachSegment((segment) => {
|
|
||||||
statistics.add(segment.getStatistics());
|
|
||||||
});
|
|
||||||
return statistics;
|
|
||||||
}
|
|
||||||
|
|
||||||
getStyle(defaultColor?: string): MergedLineStyles {
|
getStyle(defaultColor?: string): MergedLineStyles {
|
||||||
const style = this.trk
|
return this.trk
|
||||||
.map((track) => track.getStyle())
|
.map((track) => track.getStyle())
|
||||||
.reduce(
|
.reduce(
|
||||||
(acc, style) => {
|
(acc, style) => {
|
||||||
@@ -219,6 +217,8 @@ export class GPXFile extends GPXTreeNode<Track> {
|
|||||||
!acc.color.includes(style['gpx_style:color'])
|
!acc.color.includes(style['gpx_style:color'])
|
||||||
) {
|
) {
|
||||||
acc.color.push(style['gpx_style:color']);
|
acc.color.push(style['gpx_style:color']);
|
||||||
|
} else if (defaultColor && !acc.color.includes(defaultColor)) {
|
||||||
|
acc.color.push(defaultColor);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
style &&
|
style &&
|
||||||
@@ -242,10 +242,6 @@ export class GPXFile extends GPXTreeNode<Track> {
|
|||||||
width: [],
|
width: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (style.color.length === 0 && defaultColor) {
|
|
||||||
style.color.push(defaultColor);
|
|
||||||
}
|
|
||||||
return style;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clone(): GPXFile {
|
clone(): GPXFile {
|
||||||
@@ -808,7 +804,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
constructor(segment?: (TrackSegmentType & { _data?: any }) | TrackSegment) {
|
constructor(segment?: (TrackSegmentType & { _data?: any }) | TrackSegment) {
|
||||||
super();
|
super();
|
||||||
if (segment) {
|
if (segment) {
|
||||||
this.trkpt = segment.trkpt.map((point, index) => new TrackPoint(point, index));
|
this.trkpt = segment.trkpt.map((point) => new TrackPoint(point));
|
||||||
if (segment.hasOwnProperty('_data')) {
|
if (segment.hasOwnProperty('_data')) {
|
||||||
this._data = segment._data;
|
this._data = segment._data;
|
||||||
}
|
}
|
||||||
@@ -820,12 +816,15 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
_computeStatistics(): GPXStatistics {
|
_computeStatistics(): GPXStatistics {
|
||||||
let statistics = new GPXStatistics();
|
let statistics = new GPXStatistics();
|
||||||
|
|
||||||
statistics.global.length = this.trkpt.length;
|
statistics.local.points = this.trkpt.map((point) => point);
|
||||||
statistics.local.points = this.trkpt.slice(0);
|
|
||||||
statistics.local.data = this.trkpt.map(() => new TrackPointLocalStatistics());
|
statistics.local.elevation.smoothed = this._computeSmoothedElevation();
|
||||||
|
statistics.local.slope.at = this._computeSlope();
|
||||||
|
|
||||||
const points = this.trkpt;
|
const points = this.trkpt;
|
||||||
for (let i = 0; i < points.length; i++) {
|
for (let i = 0; i < points.length; i++) {
|
||||||
|
points[i]._data['index'] = i;
|
||||||
|
|
||||||
// distance
|
// distance
|
||||||
let dist = 0;
|
let dist = 0;
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
@@ -834,18 +833,34 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
statistics.global.distance.total += dist;
|
statistics.global.distance.total += dist;
|
||||||
}
|
}
|
||||||
|
|
||||||
statistics.local.data[i].distance.total = statistics.global.distance.total;
|
statistics.local.distance.total.push(statistics.global.distance.total);
|
||||||
|
|
||||||
|
// elevation
|
||||||
|
if (i > 0) {
|
||||||
|
const ele =
|
||||||
|
statistics.local.elevation.smoothed[i] -
|
||||||
|
statistics.local.elevation.smoothed[i - 1];
|
||||||
|
if (ele > 0) {
|
||||||
|
statistics.global.elevation.gain += ele;
|
||||||
|
} else if (ele < 0) {
|
||||||
|
statistics.global.elevation.loss -= ele;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
|
||||||
|
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
|
||||||
|
|
||||||
// time
|
// time
|
||||||
if (points[i].time === undefined) {
|
if (points[i].time === undefined) {
|
||||||
statistics.local.data[i].time.total = 0;
|
statistics.local.time.total.push(0);
|
||||||
} else {
|
} else {
|
||||||
if (statistics.global.time.start === undefined) {
|
if (statistics.global.time.start === undefined) {
|
||||||
statistics.global.time.start = points[i].time;
|
statistics.global.time.start = points[i].time;
|
||||||
}
|
}
|
||||||
statistics.global.time.end = points[i].time;
|
statistics.global.time.end = points[i].time;
|
||||||
statistics.local.data[i].time.total =
|
statistics.local.time.total.push(
|
||||||
(points[i].time.getTime() - statistics.global.time.start.getTime()) / 1000;
|
(points[i].time.getTime() - statistics.global.time.start.getTime()) / 1000
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// speed
|
// speed
|
||||||
@@ -860,8 +875,8 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statistics.local.data[i].distance.moving = statistics.global.distance.moving;
|
statistics.local.distance.moving.push(statistics.global.distance.moving);
|
||||||
statistics.local.data[i].time.moving = statistics.global.time.moving;
|
statistics.local.time.moving.push(statistics.global.time.moving);
|
||||||
|
|
||||||
// bounds
|
// bounds
|
||||||
statistics.global.bounds.southWest.lat = Math.min(
|
statistics.global.bounds.southWest.lat = Math.min(
|
||||||
@@ -945,7 +960,8 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._elevationComputation(statistics);
|
[statistics.local.slope.segment, statistics.local.slope.length] =
|
||||||
|
this._computeSlopeSegments(statistics);
|
||||||
|
|
||||||
statistics.global.time.total =
|
statistics.global.time.total =
|
||||||
statistics.global.time.start && statistics.global.time.end
|
statistics.global.time.start && statistics.global.time.end
|
||||||
@@ -961,115 +977,73 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
? statistics.global.distance.moving / (statistics.global.time.moving / 3600)
|
? statistics.global.distance.moving / (statistics.global.time.moving / 3600)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
timeWindowSmoothing(
|
statistics.local.speed = distanceWindowSmoothingWithDistanceAccumulator(
|
||||||
points,
|
points,
|
||||||
10000,
|
200,
|
||||||
(start, end) =>
|
(accumulated, start, end) =>
|
||||||
points[start].time && points[end].time
|
points[start].time && points[end].time
|
||||||
? (3600 *
|
? (3600 * accumulated) /
|
||||||
(statistics.local.data[end].distance.total -
|
(points[end].time.getTime() - points[start].time.getTime())
|
||||||
statistics.local.data[start].distance.total)) /
|
: undefined
|
||||||
Math.max(
|
|
||||||
(points[end].time.getTime() - points[start].time.getTime()) / 1000,
|
|
||||||
1
|
|
||||||
)
|
|
||||||
: undefined,
|
|
||||||
(value, index) => {
|
|
||||||
statistics.local.data[index].speed = value;
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return statistics;
|
return statistics;
|
||||||
}
|
}
|
||||||
|
|
||||||
_elevationComputation(statistics: GPXStatistics) {
|
_computeSmoothedElevation(): number[] {
|
||||||
|
const points = this.trkpt;
|
||||||
|
|
||||||
|
let smoothed = distanceWindowSmoothing(
|
||||||
|
points,
|
||||||
|
100,
|
||||||
|
(index) => points[index].ele ?? 0,
|
||||||
|
(accumulated, start, end) => accumulated / (end - start + 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (points.length > 0) {
|
||||||
|
smoothed[0] = points[0].ele ?? 0;
|
||||||
|
smoothed[points.length - 1] = points[points.length - 1].ele ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return smoothed;
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeSlope(): number[] {
|
||||||
|
const points = this.trkpt;
|
||||||
|
|
||||||
|
return distanceWindowSmoothingWithDistanceAccumulator(
|
||||||
|
points,
|
||||||
|
50,
|
||||||
|
(accumulated, start, end) =>
|
||||||
|
(100 * ((points[end].ele ?? 0) - (points[start].ele ?? 0))) /
|
||||||
|
(accumulated > 0 ? accumulated : 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_computeSlopeSegments(statistics: GPXStatistics): [number[], number[]] {
|
||||||
let simplified = ramerDouglasPeucker(
|
let simplified = ramerDouglasPeucker(
|
||||||
this.trkpt,
|
this.trkpt,
|
||||||
20,
|
20,
|
||||||
getElevationDistanceFunction(statistics)
|
getElevationDistanceFunction(statistics)
|
||||||
);
|
);
|
||||||
|
|
||||||
for (let i = 0; i < simplified.length - 1; i++) {
|
let slope = [];
|
||||||
let start = simplified[i].point._data.index;
|
let length = [];
|
||||||
let end = simplified[i + 1].point._data.index;
|
|
||||||
|
|
||||||
let cumulEle = 0;
|
|
||||||
let currentStart = start;
|
|
||||||
let currentEnd = start;
|
|
||||||
let prevSmoothedEle = 0;
|
|
||||||
distanceWindowSmoothing(
|
|
||||||
start,
|
|
||||||
end + 1,
|
|
||||||
statistics,
|
|
||||||
0.1,
|
|
||||||
(s, e) => {
|
|
||||||
for (let i = currentStart; i < s; i++) {
|
|
||||||
cumulEle -= this.trkpt[i].ele ?? 0;
|
|
||||||
}
|
|
||||||
for (let i = currentEnd; i <= e; i++) {
|
|
||||||
cumulEle += this.trkpt[i].ele ?? 0;
|
|
||||||
}
|
|
||||||
currentStart = s;
|
|
||||||
currentEnd = e + 1;
|
|
||||||
return cumulEle / (e - s + 1);
|
|
||||||
},
|
|
||||||
(smoothedEle, j) => {
|
|
||||||
if (j === start) {
|
|
||||||
smoothedEle = this.trkpt[start].ele ?? 0;
|
|
||||||
prevSmoothedEle = smoothedEle;
|
|
||||||
} else if (j === end) {
|
|
||||||
smoothedEle = this.trkpt[end].ele ?? 0;
|
|
||||||
}
|
|
||||||
const ele = smoothedEle - prevSmoothedEle;
|
|
||||||
if (ele > 0) {
|
|
||||||
statistics.global.elevation.gain += ele;
|
|
||||||
} else if (ele < 0) {
|
|
||||||
statistics.global.elevation.loss -= ele;
|
|
||||||
}
|
|
||||||
prevSmoothedEle = smoothedEle;
|
|
||||||
if (j < end) {
|
|
||||||
statistics.local.data[j].elevation.gain = statistics.global.elevation.gain;
|
|
||||||
statistics.local.data[j].elevation.loss = statistics.global.elevation.loss;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (statistics.global.length > 0) {
|
|
||||||
statistics.local.data[statistics.global.length - 1].elevation.gain =
|
|
||||||
statistics.global.elevation.gain;
|
|
||||||
statistics.local.data[statistics.global.length - 1].elevation.loss =
|
|
||||||
statistics.global.elevation.loss;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < simplified.length - 1; i++) {
|
for (let i = 0; i < simplified.length - 1; i++) {
|
||||||
let start = simplified[i].point._data.index;
|
let start = simplified[i].point._data.index;
|
||||||
let end = simplified[i + 1].point._data.index;
|
let end = simplified[i + 1].point._data.index;
|
||||||
let dist =
|
let dist =
|
||||||
statistics.local.data[end].distance.total -
|
statistics.local.distance.total[end] - statistics.local.distance.total[start];
|
||||||
statistics.local.data[start].distance.total;
|
|
||||||
let ele = (simplified[i + 1].point.ele ?? 0) - (simplified[i].point.ele ?? 0);
|
let ele = (simplified[i + 1].point.ele ?? 0) - (simplified[i].point.ele ?? 0);
|
||||||
|
|
||||||
for (let j = start; j < end + (i + 1 === simplified.length - 1 ? 1 : 0); j++) {
|
for (let j = start; j < end + (i + 1 === simplified.length - 1 ? 1 : 0); j++) {
|
||||||
statistics.local.data[j].slope.segment = (0.1 * ele) / dist;
|
slope.push((0.1 * ele) / dist);
|
||||||
statistics.local.data[j].slope.length = dist;
|
length.push(dist);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
distanceWindowSmoothing(
|
return [slope, length];
|
||||||
0,
|
|
||||||
this.trkpt.length,
|
|
||||||
statistics,
|
|
||||||
0.05,
|
|
||||||
(start, end) => {
|
|
||||||
const ele = this.trkpt[end].ele - this.trkpt[start].ele || 0;
|
|
||||||
const dist =
|
|
||||||
statistics.local.data[end].distance.total -
|
|
||||||
statistics.local.data[start].distance.total;
|
|
||||||
return dist > 0 ? (0.1 * ele) / dist : 0;
|
|
||||||
},
|
|
||||||
(value, index) => {
|
|
||||||
statistics.local.data[index].slope.at = value;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getNumberOfTrackPoints(): number {
|
getNumberOfTrackPoints(): number {
|
||||||
@@ -1316,8 +1290,8 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
lastPoint: TrackPoint | undefined
|
lastPoint: TrackPoint | undefined
|
||||||
) {
|
) {
|
||||||
let og = getOriginal(this); // Read as much as possible from the original object because it is faster
|
let og = getOriginal(this); // Read as much as possible from the original object because it is faster
|
||||||
let statistics = og._computeStatistics();
|
let slope = og._computeSlope();
|
||||||
let trkpt = withArtificialTimestamps(og.trkpt, totalTime, lastPoint, startTime, statistics);
|
let trkpt = withArtificialTimestamps(og.trkpt, totalTime, lastPoint, startTime, slope);
|
||||||
this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1326,7 +1300,6 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyExtensions: Record<string, string> = {};
|
|
||||||
export class TrackPoint {
|
export class TrackPoint {
|
||||||
[immerable] = true;
|
[immerable] = true;
|
||||||
|
|
||||||
@@ -1337,7 +1310,7 @@ export class TrackPoint {
|
|||||||
|
|
||||||
_data: { [key: string]: any } = {};
|
_data: { [key: string]: any } = {};
|
||||||
|
|
||||||
constructor(point: (TrackPointType & { _data?: any }) | TrackPoint, index?: number) {
|
constructor(point: (TrackPointType & { _data?: any }) | TrackPoint) {
|
||||||
this.attributes = point.attributes;
|
this.attributes = point.attributes;
|
||||||
this.ele = point.ele;
|
this.ele = point.ele;
|
||||||
this.time = point.time;
|
this.time = point.time;
|
||||||
@@ -1345,9 +1318,6 @@ export class TrackPoint {
|
|||||||
if (point.hasOwnProperty('_data')) {
|
if (point.hasOwnProperty('_data')) {
|
||||||
this._data = point._data;
|
this._data = point._data;
|
||||||
}
|
}
|
||||||
if (index !== undefined) {
|
|
||||||
this._data.index = index;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCoordinates(): Coordinates {
|
getCoordinates(): Coordinates {
|
||||||
@@ -1421,7 +1391,7 @@ export class TrackPoint {
|
|||||||
this.extensions['gpxtpx:TrackPointExtension'] &&
|
this.extensions['gpxtpx:TrackPointExtension'] &&
|
||||||
this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions']
|
this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions']
|
||||||
? this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions']
|
? this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions']
|
||||||
: emptyExtensions;
|
: {};
|
||||||
}
|
}
|
||||||
|
|
||||||
toTrackPointType(exclude: string[] = []): TrackPointType {
|
toTrackPointType(exclude: string[] = []): TrackPointType {
|
||||||
@@ -1491,18 +1461,11 @@ export class TrackPoint {
|
|||||||
|
|
||||||
clone(): TrackPoint {
|
clone(): TrackPoint {
|
||||||
return new TrackPoint({
|
return new TrackPoint({
|
||||||
attributes: {
|
attributes: cloneJSON(this.attributes),
|
||||||
lat: this.attributes.lat,
|
|
||||||
lon: this.attributes.lon,
|
|
||||||
},
|
|
||||||
ele: this.ele,
|
ele: this.ele,
|
||||||
time: this.time ? new Date(this.time.getTime()) : undefined,
|
time: this.time ? new Date(this.time.getTime()) : undefined,
|
||||||
extensions: this.extensions ? cloneJSON(this.extensions) : undefined,
|
extensions: cloneJSON(this.extensions),
|
||||||
_data: {
|
_data: cloneJSON(this._data),
|
||||||
index: this._data?.index,
|
|
||||||
anchor: this._data?.anchor,
|
|
||||||
zoom: this._data?.zoom,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1521,28 +1484,19 @@ export class Waypoint {
|
|||||||
type?: string;
|
type?: string;
|
||||||
_data: { [key: string]: any } = {};
|
_data: { [key: string]: any } = {};
|
||||||
|
|
||||||
constructor(waypoint: (WaypointType & { _data?: any }) | Waypoint, index?: number) {
|
constructor(waypoint: (WaypointType & { _data?: any }) | Waypoint) {
|
||||||
this.attributes = waypoint.attributes;
|
this.attributes = waypoint.attributes;
|
||||||
this.ele = waypoint.ele;
|
this.ele = waypoint.ele;
|
||||||
this.time = waypoint.time;
|
this.time = waypoint.time;
|
||||||
this.name = waypoint.name === '' ? undefined : waypoint.name;
|
this.name = waypoint.name;
|
||||||
this.cmt = waypoint.cmt === '' ? undefined : waypoint.cmt;
|
this.cmt = waypoint.cmt;
|
||||||
this.desc = waypoint.desc === '' ? undefined : waypoint.desc;
|
this.desc = waypoint.desc;
|
||||||
this.link =
|
this.link = waypoint.link;
|
||||||
!waypoint.link ||
|
this.sym = waypoint.sym;
|
||||||
!waypoint.link.attributes ||
|
this.type = waypoint.type;
|
||||||
!waypoint.link.attributes.href ||
|
|
||||||
waypoint.link.attributes.href === ''
|
|
||||||
? undefined
|
|
||||||
: waypoint.link;
|
|
||||||
this.sym = waypoint.sym === '' ? undefined : waypoint.sym;
|
|
||||||
this.type = waypoint.type === '' ? undefined : waypoint.type;
|
|
||||||
if (waypoint.hasOwnProperty('_data')) {
|
if (waypoint.hasOwnProperty('_data')) {
|
||||||
this._data = waypoint._data;
|
this._data = waypoint._data;
|
||||||
}
|
}
|
||||||
if (index !== undefined) {
|
|
||||||
this._data.index = index;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCoordinates(): Coordinates {
|
getCoordinates(): Coordinates {
|
||||||
@@ -1590,10 +1544,7 @@ export class Waypoint {
|
|||||||
|
|
||||||
clone(): Waypoint {
|
clone(): Waypoint {
|
||||||
return new Waypoint({
|
return new Waypoint({
|
||||||
attributes: {
|
attributes: cloneJSON(this.attributes),
|
||||||
lat: this.attributes.lat,
|
|
||||||
lon: this.attributes.lon,
|
|
||||||
},
|
|
||||||
ele: this.ele,
|
ele: this.ele,
|
||||||
time: this.time ? new Date(this.time.getTime()) : undefined,
|
time: this.time ? new Date(this.time.getTime()) : undefined,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
@@ -1642,6 +1593,310 @@ export class Waypoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class GPXStatistics {
|
||||||
|
global: {
|
||||||
|
distance: {
|
||||||
|
moving: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
time: {
|
||||||
|
start: Date | undefined;
|
||||||
|
end: Date | undefined;
|
||||||
|
moving: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
speed: {
|
||||||
|
moving: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
elevation: {
|
||||||
|
gain: number;
|
||||||
|
loss: number;
|
||||||
|
};
|
||||||
|
bounds: {
|
||||||
|
southWest: Coordinates;
|
||||||
|
northEast: Coordinates;
|
||||||
|
};
|
||||||
|
atemp: {
|
||||||
|
avg: number;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
hr: {
|
||||||
|
avg: number;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
cad: {
|
||||||
|
avg: number;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
power: {
|
||||||
|
avg: number;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
extensions: Record<string, Record<string, number>>;
|
||||||
|
};
|
||||||
|
local: {
|
||||||
|
points: TrackPoint[];
|
||||||
|
distance: {
|
||||||
|
moving: number[];
|
||||||
|
total: number[];
|
||||||
|
};
|
||||||
|
time: {
|
||||||
|
moving: number[];
|
||||||
|
total: number[];
|
||||||
|
};
|
||||||
|
speed: number[];
|
||||||
|
elevation: {
|
||||||
|
smoothed: number[];
|
||||||
|
gain: number[];
|
||||||
|
loss: number[];
|
||||||
|
};
|
||||||
|
slope: {
|
||||||
|
at: number[];
|
||||||
|
segment: number[];
|
||||||
|
length: number[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.global = {
|
||||||
|
distance: {
|
||||||
|
moving: 0,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
start: undefined,
|
||||||
|
end: undefined,
|
||||||
|
moving: 0,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
speed: {
|
||||||
|
moving: 0,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
elevation: {
|
||||||
|
gain: 0,
|
||||||
|
loss: 0,
|
||||||
|
},
|
||||||
|
bounds: {
|
||||||
|
southWest: {
|
||||||
|
lat: 90,
|
||||||
|
lon: 180,
|
||||||
|
},
|
||||||
|
northEast: {
|
||||||
|
lat: -90,
|
||||||
|
lon: -180,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
atemp: {
|
||||||
|
avg: 0,
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
hr: {
|
||||||
|
avg: 0,
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
cad: {
|
||||||
|
avg: 0,
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
power: {
|
||||||
|
avg: 0,
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
extensions: {},
|
||||||
|
};
|
||||||
|
this.local = {
|
||||||
|
points: [],
|
||||||
|
distance: {
|
||||||
|
moving: [],
|
||||||
|
total: [],
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
moving: [],
|
||||||
|
total: [],
|
||||||
|
},
|
||||||
|
speed: [],
|
||||||
|
elevation: {
|
||||||
|
smoothed: [],
|
||||||
|
gain: [],
|
||||||
|
loss: [],
|
||||||
|
},
|
||||||
|
slope: {
|
||||||
|
at: [],
|
||||||
|
segment: [],
|
||||||
|
length: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeWith(other: GPXStatistics): void {
|
||||||
|
this.local.points = this.local.points.concat(other.local.points);
|
||||||
|
|
||||||
|
this.local.distance.total = this.local.distance.total.concat(
|
||||||
|
other.local.distance.total.map((distance) => distance + this.global.distance.total)
|
||||||
|
);
|
||||||
|
this.local.distance.moving = this.local.distance.moving.concat(
|
||||||
|
other.local.distance.moving.map((distance) => distance + this.global.distance.moving)
|
||||||
|
);
|
||||||
|
this.local.time.total = this.local.time.total.concat(
|
||||||
|
other.local.time.total.map((time) => time + this.global.time.total)
|
||||||
|
);
|
||||||
|
this.local.time.moving = this.local.time.moving.concat(
|
||||||
|
other.local.time.moving.map((time) => time + this.global.time.moving)
|
||||||
|
);
|
||||||
|
this.local.elevation.gain = this.local.elevation.gain.concat(
|
||||||
|
other.local.elevation.gain.map((gain) => gain + this.global.elevation.gain)
|
||||||
|
);
|
||||||
|
this.local.elevation.loss = this.local.elevation.loss.concat(
|
||||||
|
other.local.elevation.loss.map((loss) => loss + this.global.elevation.loss)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.local.speed = this.local.speed.concat(other.local.speed);
|
||||||
|
this.local.elevation.smoothed = this.local.elevation.smoothed.concat(
|
||||||
|
other.local.elevation.smoothed
|
||||||
|
);
|
||||||
|
this.local.slope.at = this.local.slope.at.concat(other.local.slope.at);
|
||||||
|
this.local.slope.segment = this.local.slope.segment.concat(other.local.slope.segment);
|
||||||
|
this.local.slope.length = this.local.slope.length.concat(other.local.slope.length);
|
||||||
|
|
||||||
|
this.global.distance.total += other.global.distance.total;
|
||||||
|
this.global.distance.moving += other.global.distance.moving;
|
||||||
|
|
||||||
|
this.global.time.start =
|
||||||
|
this.global.time.start !== undefined && other.global.time.start !== undefined
|
||||||
|
? new Date(
|
||||||
|
Math.min(this.global.time.start.getTime(), other.global.time.start.getTime())
|
||||||
|
)
|
||||||
|
: (this.global.time.start ?? other.global.time.start);
|
||||||
|
this.global.time.end =
|
||||||
|
this.global.time.end !== undefined && other.global.time.end !== undefined
|
||||||
|
? new Date(
|
||||||
|
Math.max(this.global.time.end.getTime(), other.global.time.end.getTime())
|
||||||
|
)
|
||||||
|
: (this.global.time.end ?? other.global.time.end);
|
||||||
|
|
||||||
|
this.global.time.total += other.global.time.total;
|
||||||
|
this.global.time.moving += other.global.time.moving;
|
||||||
|
|
||||||
|
this.global.speed.moving =
|
||||||
|
this.global.time.moving > 0
|
||||||
|
? this.global.distance.moving / (this.global.time.moving / 3600)
|
||||||
|
: 0;
|
||||||
|
this.global.speed.total =
|
||||||
|
this.global.time.total > 0
|
||||||
|
? this.global.distance.total / (this.global.time.total / 3600)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
this.global.elevation.gain += other.global.elevation.gain;
|
||||||
|
this.global.elevation.loss += other.global.elevation.loss;
|
||||||
|
|
||||||
|
this.global.bounds.southWest.lat = Math.min(
|
||||||
|
this.global.bounds.southWest.lat,
|
||||||
|
other.global.bounds.southWest.lat
|
||||||
|
);
|
||||||
|
this.global.bounds.southWest.lon = Math.min(
|
||||||
|
this.global.bounds.southWest.lon,
|
||||||
|
other.global.bounds.southWest.lon
|
||||||
|
);
|
||||||
|
this.global.bounds.northEast.lat = Math.max(
|
||||||
|
this.global.bounds.northEast.lat,
|
||||||
|
other.global.bounds.northEast.lat
|
||||||
|
);
|
||||||
|
this.global.bounds.northEast.lon = Math.max(
|
||||||
|
this.global.bounds.northEast.lon,
|
||||||
|
other.global.bounds.northEast.lon
|
||||||
|
);
|
||||||
|
|
||||||
|
this.global.atemp.avg =
|
||||||
|
(this.global.atemp.count * this.global.atemp.avg +
|
||||||
|
other.global.atemp.count * other.global.atemp.avg) /
|
||||||
|
Math.max(1, this.global.atemp.count + other.global.atemp.count);
|
||||||
|
this.global.atemp.count += other.global.atemp.count;
|
||||||
|
this.global.hr.avg =
|
||||||
|
(this.global.hr.count * this.global.hr.avg +
|
||||||
|
other.global.hr.count * other.global.hr.avg) /
|
||||||
|
Math.max(1, this.global.hr.count + other.global.hr.count);
|
||||||
|
this.global.hr.count += other.global.hr.count;
|
||||||
|
this.global.cad.avg =
|
||||||
|
(this.global.cad.count * this.global.cad.avg +
|
||||||
|
other.global.cad.count * other.global.cad.avg) /
|
||||||
|
Math.max(1, this.global.cad.count + other.global.cad.count);
|
||||||
|
this.global.cad.count += other.global.cad.count;
|
||||||
|
this.global.power.avg =
|
||||||
|
(this.global.power.count * this.global.power.avg +
|
||||||
|
other.global.power.count * other.global.power.avg) /
|
||||||
|
Math.max(1, this.global.power.count + other.global.power.count);
|
||||||
|
this.global.power.count += other.global.power.count;
|
||||||
|
Object.keys(other.global.extensions).forEach((extension) => {
|
||||||
|
if (this.global.extensions[extension] === undefined) {
|
||||||
|
this.global.extensions[extension] = {};
|
||||||
|
}
|
||||||
|
Object.keys(other.global.extensions[extension]).forEach((value) => {
|
||||||
|
if (this.global.extensions[extension][value] === undefined) {
|
||||||
|
this.global.extensions[extension][value] = 0;
|
||||||
|
}
|
||||||
|
this.global.extensions[extension][value] +=
|
||||||
|
other.global.extensions[extension][value];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
slice(start: number, end: number): GPXStatistics {
|
||||||
|
if (start < 0) {
|
||||||
|
start = 0;
|
||||||
|
} else if (start >= this.local.points.length) {
|
||||||
|
return new GPXStatistics();
|
||||||
|
}
|
||||||
|
if (end < start) {
|
||||||
|
return new GPXStatistics();
|
||||||
|
} else if (end >= this.local.points.length) {
|
||||||
|
end = this.local.points.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let statistics = new GPXStatistics();
|
||||||
|
|
||||||
|
statistics.local.points = this.local.points.slice(start, end + 1);
|
||||||
|
|
||||||
|
statistics.global.distance.total =
|
||||||
|
this.local.distance.total[end] - this.local.distance.total[start];
|
||||||
|
statistics.global.distance.moving =
|
||||||
|
this.local.distance.moving[end] - this.local.distance.moving[start];
|
||||||
|
|
||||||
|
statistics.global.time.start = this.local.points[start].time;
|
||||||
|
statistics.global.time.end = this.local.points[end].time;
|
||||||
|
|
||||||
|
statistics.global.time.total = this.local.time.total[end] - this.local.time.total[start];
|
||||||
|
statistics.global.time.moving = this.local.time.moving[end] - this.local.time.moving[start];
|
||||||
|
|
||||||
|
statistics.global.speed.moving =
|
||||||
|
statistics.global.time.moving > 0
|
||||||
|
? statistics.global.distance.moving / (statistics.global.time.moving / 3600)
|
||||||
|
: 0;
|
||||||
|
statistics.global.speed.total =
|
||||||
|
statistics.global.time.total > 0
|
||||||
|
? statistics.global.distance.total / (statistics.global.time.total / 3600)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
statistics.global.elevation.gain =
|
||||||
|
this.local.elevation.gain[end] - this.local.elevation.gain[start];
|
||||||
|
statistics.global.elevation.loss =
|
||||||
|
this.local.elevation.loss[end] - this.local.elevation.loss[start];
|
||||||
|
|
||||||
|
statistics.global.bounds.southWest.lat = this.global.bounds.southWest.lat;
|
||||||
|
statistics.global.bounds.southWest.lon = this.global.bounds.southWest.lon;
|
||||||
|
statistics.global.bounds.northEast.lat = this.global.bounds.northEast.lat;
|
||||||
|
statistics.global.bounds.northEast.lon = this.global.bounds.northEast.lon;
|
||||||
|
|
||||||
|
statistics.global.atemp = this.global.atemp;
|
||||||
|
statistics.global.hr = this.global.hr;
|
||||||
|
statistics.global.cad = this.global.cad;
|
||||||
|
statistics.global.power = this.global.power;
|
||||||
|
|
||||||
|
return statistics;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const earthRadius = 6371008.8;
|
const earthRadius = 6371008.8;
|
||||||
export function distance(
|
export function distance(
|
||||||
coord1: TrackPoint | Coordinates,
|
coord1: TrackPoint | Coordinates,
|
||||||
@@ -1656,15 +1911,11 @@ export function distance(
|
|||||||
const rad = Math.PI / 180;
|
const rad = Math.PI / 180;
|
||||||
const lat1 = coord1.lat * rad;
|
const lat1 = coord1.lat * rad;
|
||||||
const lat2 = coord2.lat * rad;
|
const lat2 = coord2.lat * rad;
|
||||||
const dLat = lat2 - lat1;
|
|
||||||
const dLon = (coord2.lon - coord1.lon) * rad;
|
|
||||||
|
|
||||||
// Haversine formula - better numerical stability for small distances
|
|
||||||
const a =
|
const a =
|
||||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
Math.sin(lat1) * Math.sin(lat2) +
|
||||||
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
Math.cos(lat1) * Math.cos(lat2) * Math.cos((coord2.lon - coord1.lon) * rad);
|
||||||
const c = 2 * Math.asin(Math.sqrt(Math.min(a, 1)));
|
const maxMeters = earthRadius * Math.acos(Math.min(a, 1));
|
||||||
return earthRadius * c;
|
return maxMeters;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
||||||
@@ -1675,9 +1926,9 @@ export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
|||||||
if (point1.ele === undefined || point2.ele === undefined || point3.ele === undefined) {
|
if (point1.ele === undefined || point2.ele === undefined || point3.ele === undefined) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let x1 = statistics.local.data[point1._data.index].distance.total * 1000;
|
let x1 = statistics.local.distance.total[point1._data.index] * 1000;
|
||||||
let x2 = statistics.local.data[point2._data.index].distance.total * 1000;
|
let x2 = statistics.local.distance.total[point2._data.index] * 1000;
|
||||||
let x3 = statistics.local.data[point3._data.index].distance.total * 1000;
|
let x3 = statistics.local.distance.total[point3._data.index] * 1000;
|
||||||
let y1 = point1.ele;
|
let y1 = point1.ele;
|
||||||
let y2 = point2.ele;
|
let y2 = point2.ele;
|
||||||
let y3 = point3.ele;
|
let y3 = point3.ele;
|
||||||
@@ -1691,61 +1942,57 @@ export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function windowSmoothing(
|
function distanceWindowSmoothing(
|
||||||
left: number,
|
points: TrackPoint[],
|
||||||
right: number,
|
distanceWindow: number,
|
||||||
distance: (index1: number, index2: number) => number,
|
accumulate: (index: number) => number,
|
||||||
window: number,
|
compute: (accumulated: number, start: number, end: number) => number,
|
||||||
compute: (start: number, end: number) => number,
|
remove?: (index: number) => number
|
||||||
callback: (value: number, index: number) => void
|
): number[] {
|
||||||
): void {
|
let result = [];
|
||||||
let start = left;
|
|
||||||
for (var i = left; i < right; i++) {
|
let start = 0,
|
||||||
while (start + 1 < i && distance(start, i) > window) {
|
end = 0,
|
||||||
|
accumulated = 0;
|
||||||
|
for (var i = 0; i < points.length; i++) {
|
||||||
|
while (
|
||||||
|
start + 1 < i &&
|
||||||
|
distance(points[start].getCoordinates(), points[i].getCoordinates()) > distanceWindow
|
||||||
|
) {
|
||||||
|
if (remove) {
|
||||||
|
accumulated -= remove(start);
|
||||||
|
} else {
|
||||||
|
accumulated -= accumulate(start);
|
||||||
|
}
|
||||||
start++;
|
start++;
|
||||||
}
|
}
|
||||||
let end = Math.min(i + 2, right);
|
while (
|
||||||
while (end < right && distance(i, end) <= window) {
|
end < points.length &&
|
||||||
|
distance(points[i].getCoordinates(), points[end].getCoordinates()) <= distanceWindow
|
||||||
|
) {
|
||||||
|
accumulated += accumulate(end);
|
||||||
end++;
|
end++;
|
||||||
}
|
}
|
||||||
callback(compute(start, end - 1), i);
|
result[i] = compute(accumulated, start, end - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function distanceWindowSmoothing(
|
function distanceWindowSmoothingWithDistanceAccumulator(
|
||||||
left: number,
|
|
||||||
right: number,
|
|
||||||
statistics: GPXStatistics,
|
|
||||||
window: number,
|
|
||||||
compute: (start: number, end: number) => number,
|
|
||||||
callback: (value: number, index: number) => void
|
|
||||||
): void {
|
|
||||||
windowSmoothing(
|
|
||||||
left,
|
|
||||||
right,
|
|
||||||
(index1, index2) =>
|
|
||||||
statistics.local.data[index2].distance.total -
|
|
||||||
statistics.local.data[index1].distance.total,
|
|
||||||
window,
|
|
||||||
compute,
|
|
||||||
callback
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function timeWindowSmoothing(
|
|
||||||
points: TrackPoint[],
|
points: TrackPoint[],
|
||||||
window: number,
|
distanceWindow: number,
|
||||||
compute: (start: number, end: number) => number,
|
compute: (accumulated: number, start: number, end: number) => number
|
||||||
callback: (value: number, index: number) => void
|
): number[] {
|
||||||
): void {
|
return distanceWindowSmoothing(
|
||||||
windowSmoothing(
|
points,
|
||||||
0,
|
distanceWindow,
|
||||||
points.length,
|
(index) =>
|
||||||
(index1, index2) =>
|
index > 0
|
||||||
points[index2].time?.getTime() - points[index1].time?.getTime() || 2 * window,
|
? distance(points[index - 1].getCoordinates(), points[index].getCoordinates())
|
||||||
window,
|
: 0,
|
||||||
compute,
|
compute,
|
||||||
callback
|
(index) => distance(points[index].getCoordinates(), points[index + 1].getCoordinates())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1797,14 +2044,14 @@ function withArtificialTimestamps(
|
|||||||
totalTime: number,
|
totalTime: number,
|
||||||
lastPoint: TrackPoint | undefined,
|
lastPoint: TrackPoint | undefined,
|
||||||
startTime: Date,
|
startTime: Date,
|
||||||
statistics: GPXStatistics
|
slope: number[]
|
||||||
): TrackPoint[] {
|
): TrackPoint[] {
|
||||||
let weight = [];
|
let weight = [];
|
||||||
let totalWeight = 0;
|
let totalWeight = 0;
|
||||||
|
|
||||||
for (let i = 0; i < points.length - 1; i++) {
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
let dist = distance(points[i].getCoordinates(), points[i + 1].getCoordinates());
|
let dist = distance(points[i].getCoordinates(), points[i + 1].getCoordinates());
|
||||||
let w = dist * (0.5 + 1 / (1 + Math.exp(-0.2 * statistics.local.data[i].slope.at)));
|
let w = dist * (0.5 + 1 / (1 + Math.exp(-0.2 * slope[i])));
|
||||||
weight.push(w);
|
weight.push(w);
|
||||||
totalWeight += w;
|
totalWeight += w;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export * from './gpx';
|
export * from './gpx';
|
||||||
export * from './statistics';
|
|
||||||
export { Coordinates, LineStyleExtension, WaypointType } from './types';
|
export { Coordinates, LineStyleExtension, WaypointType } from './types';
|
||||||
export { parseGPX, buildGPX } from './io';
|
export { parseGPX, buildGPX } from './io';
|
||||||
export * from './simplify';
|
export * from './simplify';
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { Coordinates } from './types';
|
|||||||
|
|
||||||
export type SimplifiedTrackPoint = { point: TrackPoint; distance?: number };
|
export type SimplifiedTrackPoint = { point: TrackPoint; distance?: number };
|
||||||
|
|
||||||
|
const earthRadius = 6371008.8;
|
||||||
|
|
||||||
export function ramerDouglasPeucker(
|
export function ramerDouglasPeucker(
|
||||||
points: TrackPoint[],
|
points: TrackPoint[],
|
||||||
epsilon: number = 50,
|
epsilon: number = 50,
|
||||||
@@ -59,56 +61,76 @@ function ramerDouglasPeuckerRecursive(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function crossarcDistance(
|
export function crossarcDistance(
|
||||||
point1: TrackPoint | Coordinates,
|
point1: TrackPoint,
|
||||||
point2: TrackPoint | Coordinates,
|
point2: TrackPoint,
|
||||||
point3: TrackPoint | Coordinates
|
point3: TrackPoint | Coordinates
|
||||||
): number {
|
): number {
|
||||||
return crossarc(
|
return crossarc(
|
||||||
point1 instanceof TrackPoint ? point1.getCoordinates() : point1,
|
point1.getCoordinates(),
|
||||||
point2 instanceof TrackPoint ? point2.getCoordinates() : point2,
|
point2.getCoordinates(),
|
||||||
point3 instanceof TrackPoint ? point3.getCoordinates() : point3
|
point3 instanceof TrackPoint ? point3.getCoordinates() : point3
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const metersPerLatitudeDegree = 111320;
|
|
||||||
|
|
||||||
function getMetersPerLongitudeDegree(latitude: number): number {
|
|
||||||
return Math.cos((latitude * Math.PI) / 180) * metersPerLatitudeDegree;
|
|
||||||
}
|
|
||||||
|
|
||||||
function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number {
|
function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number {
|
||||||
// Calculates the perpendicular distance in meters
|
// Calculates the shortest distance in meters
|
||||||
// between a line segment (defined by p1 and p2) and a third point, p3.
|
// between an arc (defined by p1 and p2) and a third point, p3.
|
||||||
// Uses simple planar geometry (ignores earth curvature).
|
// Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees.
|
||||||
|
|
||||||
// Convert to meters using approximate scaling
|
const rad = Math.PI / 180;
|
||||||
const metersPerLongitudeDegree = getMetersPerLongitudeDegree(coord1.lat);
|
const lat1 = coord1.lat * rad;
|
||||||
|
const lat2 = coord2.lat * rad;
|
||||||
|
const lat3 = coord3.lat * rad;
|
||||||
|
|
||||||
const x1 = coord1.lon * metersPerLongitudeDegree;
|
const lon1 = coord1.lon * rad;
|
||||||
const y1 = coord1.lat * metersPerLatitudeDegree;
|
const lon2 = coord2.lon * rad;
|
||||||
const x2 = coord2.lon * metersPerLongitudeDegree;
|
const lon3 = coord3.lon * rad;
|
||||||
const y2 = coord2.lat * metersPerLatitudeDegree;
|
|
||||||
const x3 = coord3.lon * metersPerLongitudeDegree;
|
|
||||||
const y3 = coord3.lat * metersPerLatitudeDegree;
|
|
||||||
|
|
||||||
const dx = x2 - x1;
|
// Prerequisites for the formulas
|
||||||
const dy = y2 - y1;
|
const bear12 = bearing(lat1, lon1, lat2, lon2);
|
||||||
const segmentLengthSquared = dx * dx + dy * dy;
|
const bear13 = bearing(lat1, lon1, lat3, lon3);
|
||||||
|
let dis13 = distance(lat1, lon1, lat3, lon3);
|
||||||
|
|
||||||
if (segmentLengthSquared === 0) {
|
let diff = Math.abs(bear13 - bear12);
|
||||||
// p1 and p2 are the same point
|
if (diff > Math.PI) {
|
||||||
return Math.sqrt((x3 - x1) * (x3 - x1) + (y3 - y1) * (y3 - y1));
|
diff = 2 * Math.PI - diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project p3 onto the line defined by p1-p2
|
// Is relative bearing obtuse?
|
||||||
const t = Math.max(0, Math.min(1, ((x3 - x1) * dx + (y3 - y1) * dy) / segmentLengthSquared));
|
if (diff > Math.PI / 2) {
|
||||||
|
return dis13;
|
||||||
|
}
|
||||||
|
|
||||||
// Find the closest point on the segment
|
// Find the cross-track distance.
|
||||||
const projX = x1 + t * dx;
|
let dxt = Math.asin(Math.sin(dis13 / earthRadius) * Math.sin(bear13 - bear12)) * earthRadius;
|
||||||
const projY = y1 + t * dy;
|
|
||||||
|
|
||||||
// Return distance from p3 to the projected point
|
// Is p4 beyond the arc?
|
||||||
return Math.sqrt((x3 - projX) * (x3 - projX) + (y3 - projY) * (y3 - projY));
|
let dis12 = distance(lat1, lon1, lat2, lon2);
|
||||||
|
let dis14 =
|
||||||
|
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
|
||||||
|
if (dis14 > dis12) {
|
||||||
|
return distance(lat2, lon2, lat3, lon3);
|
||||||
|
} else {
|
||||||
|
return Math.abs(dxt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function distance(latA: number, lonA: number, latB: number, lonB: number): number {
|
||||||
|
// Finds the distance between two lat / lon points.
|
||||||
|
return (
|
||||||
|
Math.acos(
|
||||||
|
Math.sin(latA) * Math.sin(latB) +
|
||||||
|
Math.cos(latA) * Math.cos(latB) * Math.cos(lonB - lonA)
|
||||||
|
) * earthRadius
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bearing(latA: number, lonA: number, latB: number, lonB: number): number {
|
||||||
|
// Finds the bearing from one lat / lon point to another.
|
||||||
|
return Math.atan2(
|
||||||
|
Math.sin(lonB - lonA) * Math.cos(latB),
|
||||||
|
Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function projectedPoint(
|
export function projectedPoint(
|
||||||
@@ -124,39 +146,56 @@ export function projectedPoint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates {
|
function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates {
|
||||||
// Calculates the point on the line segment defined by p1 and p2
|
// Calculates the point on the line defined by p1 and p2
|
||||||
// that is closest to the third point, p3.
|
// that is closest to the third point, p3.
|
||||||
// Uses simple planar geometry (ignores earth curvature).
|
// Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees.
|
||||||
|
|
||||||
// Convert to meters using approximate scaling
|
const rad = Math.PI / 180;
|
||||||
const metersPerLongitudeDegree = getMetersPerLongitudeDegree(coord1.lat);
|
const lat1 = coord1.lat * rad;
|
||||||
|
const lat2 = coord2.lat * rad;
|
||||||
|
const lat3 = coord3.lat * rad;
|
||||||
|
|
||||||
const x1 = coord1.lon * metersPerLongitudeDegree;
|
const lon1 = coord1.lon * rad;
|
||||||
const y1 = coord1.lat * metersPerLatitudeDegree;
|
const lon2 = coord2.lon * rad;
|
||||||
const x2 = coord2.lon * metersPerLongitudeDegree;
|
const lon3 = coord3.lon * rad;
|
||||||
const y2 = coord2.lat * metersPerLatitudeDegree;
|
|
||||||
const x3 = coord3.lon * metersPerLongitudeDegree;
|
|
||||||
const y3 = coord3.lat * metersPerLatitudeDegree;
|
|
||||||
|
|
||||||
const dx = x2 - x1;
|
// Prerequisites for the formulas
|
||||||
const dy = y2 - y1;
|
const bear12 = bearing(lat1, lon1, lat2, lon2);
|
||||||
const segmentLengthSquared = dx * dx + dy * dy;
|
const bear13 = bearing(lat1, lon1, lat3, lon3);
|
||||||
|
let dis13 = distance(lat1, lon1, lat3, lon3);
|
||||||
|
|
||||||
if (segmentLengthSquared === 0) {
|
let diff = Math.abs(bear13 - bear12);
|
||||||
// p1 and p2 are the same point
|
if (diff > Math.PI) {
|
||||||
|
diff = 2 * Math.PI - diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is relative bearing obtuse?
|
||||||
|
if (diff > Math.PI / 2) {
|
||||||
return coord1;
|
return coord1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project p3 onto the line defined by p1-p2
|
// Find the cross-track distance.
|
||||||
const t = Math.max(0, Math.min(1, ((x3 - x1) * dx + (y3 - y1) * dy) / segmentLengthSquared));
|
let dxt = Math.asin(Math.sin(dis13 / earthRadius) * Math.sin(bear13 - bear12)) * earthRadius;
|
||||||
|
|
||||||
// Find the closest point on the segment
|
// Is p4 beyond the arc?
|
||||||
const projX = x1 + t * dx;
|
let dis12 = distance(lat1, lon1, lat2, lon2);
|
||||||
const projY = y1 + t * dy;
|
let dis14 =
|
||||||
|
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
|
||||||
|
if (dis14 > dis12) {
|
||||||
|
return coord2;
|
||||||
|
} else {
|
||||||
|
// Determine the closest point (p4) on the great circle
|
||||||
|
const f = dis14 / earthRadius;
|
||||||
|
const lat4 = Math.asin(
|
||||||
|
Math.sin(lat1) * Math.cos(f) + Math.cos(lat1) * Math.sin(f) * Math.cos(bear12)
|
||||||
|
);
|
||||||
|
const lon4 =
|
||||||
|
lon1 +
|
||||||
|
Math.atan2(
|
||||||
|
Math.sin(bear12) * Math.sin(f) * Math.cos(lat1),
|
||||||
|
Math.cos(f) - Math.sin(lat1) * Math.sin(lat4)
|
||||||
|
);
|
||||||
|
|
||||||
// Convert back to degrees
|
return { lat: lat4 / rad, lon: lon4 / rad };
|
||||||
return {
|
}
|
||||||
lat: projY / metersPerLatitudeDegree,
|
|
||||||
lon: projX / metersPerLongitudeDegree,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,391 +0,0 @@
|
|||||||
import { TrackPoint } from './gpx';
|
|
||||||
import { Coordinates } from './types';
|
|
||||||
|
|
||||||
export class GPXGlobalStatistics {
|
|
||||||
length: number;
|
|
||||||
distance: {
|
|
||||||
moving: number;
|
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
time: {
|
|
||||||
start: Date | undefined;
|
|
||||||
end: Date | undefined;
|
|
||||||
moving: number;
|
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
speed: {
|
|
||||||
moving: number;
|
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
elevation: {
|
|
||||||
gain: number;
|
|
||||||
loss: number;
|
|
||||||
};
|
|
||||||
bounds: {
|
|
||||||
southWest: Coordinates;
|
|
||||||
northEast: Coordinates;
|
|
||||||
};
|
|
||||||
atemp: {
|
|
||||||
avg: number;
|
|
||||||
count: number;
|
|
||||||
};
|
|
||||||
hr: {
|
|
||||||
avg: number;
|
|
||||||
count: number;
|
|
||||||
};
|
|
||||||
cad: {
|
|
||||||
avg: number;
|
|
||||||
count: number;
|
|
||||||
};
|
|
||||||
power: {
|
|
||||||
avg: number;
|
|
||||||
count: number;
|
|
||||||
};
|
|
||||||
extensions: Record<string, Record<string, number>>;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.length = 0;
|
|
||||||
this.distance = {
|
|
||||||
moving: 0,
|
|
||||||
total: 0,
|
|
||||||
};
|
|
||||||
this.time = {
|
|
||||||
start: undefined,
|
|
||||||
end: undefined,
|
|
||||||
moving: 0,
|
|
||||||
total: 0,
|
|
||||||
};
|
|
||||||
this.speed = {
|
|
||||||
moving: 0,
|
|
||||||
total: 0,
|
|
||||||
};
|
|
||||||
this.elevation = {
|
|
||||||
gain: 0,
|
|
||||||
loss: 0,
|
|
||||||
};
|
|
||||||
this.bounds = {
|
|
||||||
southWest: {
|
|
||||||
lat: 90,
|
|
||||||
lon: 180,
|
|
||||||
},
|
|
||||||
northEast: {
|
|
||||||
lat: -90,
|
|
||||||
lon: -180,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
this.atemp = {
|
|
||||||
avg: 0,
|
|
||||||
count: 0,
|
|
||||||
};
|
|
||||||
this.hr = {
|
|
||||||
avg: 0,
|
|
||||||
count: 0,
|
|
||||||
};
|
|
||||||
this.cad = {
|
|
||||||
avg: 0,
|
|
||||||
count: 0,
|
|
||||||
};
|
|
||||||
this.power = {
|
|
||||||
avg: 0,
|
|
||||||
count: 0,
|
|
||||||
};
|
|
||||||
this.extensions = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeWith(other: GPXGlobalStatistics): void {
|
|
||||||
this.length += other.length;
|
|
||||||
|
|
||||||
this.distance.total += other.distance.total;
|
|
||||||
this.distance.moving += other.distance.moving;
|
|
||||||
|
|
||||||
this.time.start =
|
|
||||||
this.time.start !== undefined && other.time.start !== undefined
|
|
||||||
? new Date(Math.min(this.time.start.getTime(), other.time.start.getTime()))
|
|
||||||
: (this.time.start ?? other.time.start);
|
|
||||||
this.time.end =
|
|
||||||
this.time.end !== undefined && other.time.end !== undefined
|
|
||||||
? new Date(Math.max(this.time.end.getTime(), other.time.end.getTime()))
|
|
||||||
: (this.time.end ?? other.time.end);
|
|
||||||
|
|
||||||
this.time.total += other.time.total;
|
|
||||||
this.time.moving += other.time.moving;
|
|
||||||
|
|
||||||
this.speed.moving =
|
|
||||||
this.time.moving > 0 ? this.distance.moving / (this.time.moving / 3600) : 0;
|
|
||||||
this.speed.total = this.time.total > 0 ? this.distance.total / (this.time.total / 3600) : 0;
|
|
||||||
|
|
||||||
this.elevation.gain += other.elevation.gain;
|
|
||||||
this.elevation.loss += other.elevation.loss;
|
|
||||||
|
|
||||||
this.bounds.southWest.lat = Math.min(this.bounds.southWest.lat, other.bounds.southWest.lat);
|
|
||||||
this.bounds.southWest.lon = Math.min(this.bounds.southWest.lon, other.bounds.southWest.lon);
|
|
||||||
this.bounds.northEast.lat = Math.max(this.bounds.northEast.lat, other.bounds.northEast.lat);
|
|
||||||
this.bounds.northEast.lon = Math.max(this.bounds.northEast.lon, other.bounds.northEast.lon);
|
|
||||||
|
|
||||||
this.atemp.avg =
|
|
||||||
(this.atemp.count * this.atemp.avg + other.atemp.count * other.atemp.avg) /
|
|
||||||
Math.max(1, this.atemp.count + other.atemp.count);
|
|
||||||
this.atemp.count += other.atemp.count;
|
|
||||||
this.hr.avg =
|
|
||||||
(this.hr.count * this.hr.avg + other.hr.count * other.hr.avg) /
|
|
||||||
Math.max(1, this.hr.count + other.hr.count);
|
|
||||||
this.hr.count += other.hr.count;
|
|
||||||
this.cad.avg =
|
|
||||||
(this.cad.count * this.cad.avg + other.cad.count * other.cad.avg) /
|
|
||||||
Math.max(1, this.cad.count + other.cad.count);
|
|
||||||
this.cad.count += other.cad.count;
|
|
||||||
this.power.avg =
|
|
||||||
(this.power.count * this.power.avg + other.power.count * other.power.avg) /
|
|
||||||
Math.max(1, this.power.count + other.power.count);
|
|
||||||
this.power.count += other.power.count;
|
|
||||||
|
|
||||||
Object.keys(other.extensions).forEach((extension) => {
|
|
||||||
if (this.extensions[extension] === undefined) {
|
|
||||||
this.extensions[extension] = {};
|
|
||||||
}
|
|
||||||
Object.keys(other.extensions[extension]).forEach((value) => {
|
|
||||||
if (this.extensions[extension][value] === undefined) {
|
|
||||||
this.extensions[extension][value] = 0;
|
|
||||||
}
|
|
||||||
this.extensions[extension][value] += other.extensions[extension][value];
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TrackPointLocalStatistics {
|
|
||||||
distance: {
|
|
||||||
moving: number;
|
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
time: {
|
|
||||||
moving: number;
|
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
speed: number;
|
|
||||||
elevation: {
|
|
||||||
gain: number;
|
|
||||||
loss: number;
|
|
||||||
};
|
|
||||||
slope: {
|
|
||||||
at: number;
|
|
||||||
segment: number;
|
|
||||||
length: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.distance = {
|
|
||||||
moving: 0,
|
|
||||||
total: 0,
|
|
||||||
};
|
|
||||||
this.time = {
|
|
||||||
moving: 0,
|
|
||||||
total: 0,
|
|
||||||
};
|
|
||||||
this.speed = 0;
|
|
||||||
this.elevation = {
|
|
||||||
gain: 0,
|
|
||||||
loss: 0,
|
|
||||||
};
|
|
||||||
this.slope = {
|
|
||||||
at: 0,
|
|
||||||
segment: 0,
|
|
||||||
length: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GPXLocalStatistics {
|
|
||||||
points: TrackPoint[];
|
|
||||||
data: TrackPointLocalStatistics[];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.points = [];
|
|
||||||
this.data = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TrackPointWithLocalStatistics = {
|
|
||||||
trkpt: TrackPoint;
|
|
||||||
} & TrackPointLocalStatistics;
|
|
||||||
|
|
||||||
export class GPXStatistics {
|
|
||||||
global: GPXGlobalStatistics;
|
|
||||||
local: GPXLocalStatistics;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.global = new GPXGlobalStatistics();
|
|
||||||
this.local = new GPXLocalStatistics();
|
|
||||||
}
|
|
||||||
|
|
||||||
sliced(start: number, end: number): GPXGlobalStatistics {
|
|
||||||
if (start < 0) {
|
|
||||||
start = 0;
|
|
||||||
} else if (start >= this.global.length) {
|
|
||||||
return new GPXGlobalStatistics();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (end < start) {
|
|
||||||
return new GPXGlobalStatistics();
|
|
||||||
} else if (end >= this.global.length) {
|
|
||||||
end = this.global.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (start === 0 && end === this.global.length - 1) {
|
|
||||||
return this.global;
|
|
||||||
}
|
|
||||||
|
|
||||||
let statistics = new GPXGlobalStatistics();
|
|
||||||
|
|
||||||
statistics.length = end - start + 1;
|
|
||||||
|
|
||||||
statistics.distance.total =
|
|
||||||
this.local.data[end].distance.total - this.local.data[start].distance.total;
|
|
||||||
statistics.distance.moving =
|
|
||||||
this.local.data[end].distance.moving - this.local.data[start].distance.moving;
|
|
||||||
|
|
||||||
statistics.time.start = this.local.points[start].time;
|
|
||||||
statistics.time.end = this.local.points[end].time;
|
|
||||||
|
|
||||||
statistics.time.total = this.local.data[end].time.total - this.local.data[start].time.total;
|
|
||||||
statistics.time.moving =
|
|
||||||
this.local.data[end].time.moving - this.local.data[start].time.moving;
|
|
||||||
|
|
||||||
statistics.speed.moving =
|
|
||||||
statistics.time.moving > 0
|
|
||||||
? statistics.distance.moving / (statistics.time.moving / 3600)
|
|
||||||
: 0;
|
|
||||||
statistics.speed.total =
|
|
||||||
statistics.time.total > 0
|
|
||||||
? statistics.distance.total / (statistics.time.total / 3600)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
statistics.elevation.gain =
|
|
||||||
this.local.data[end].elevation.gain - this.local.data[start].elevation.gain;
|
|
||||||
statistics.elevation.loss =
|
|
||||||
this.local.data[end].elevation.loss - this.local.data[start].elevation.loss;
|
|
||||||
|
|
||||||
statistics.bounds.southWest.lat = this.global.bounds.southWest.lat;
|
|
||||||
statistics.bounds.southWest.lon = this.global.bounds.southWest.lon;
|
|
||||||
statistics.bounds.northEast.lat = this.global.bounds.northEast.lat;
|
|
||||||
statistics.bounds.northEast.lon = this.global.bounds.northEast.lon;
|
|
||||||
|
|
||||||
statistics.atemp = this.global.atemp;
|
|
||||||
statistics.hr = this.global.hr;
|
|
||||||
statistics.cad = this.global.cad;
|
|
||||||
statistics.power = this.global.power;
|
|
||||||
|
|
||||||
return statistics;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GPXStatisticsGroup {
|
|
||||||
private _statistics: GPXStatistics[];
|
|
||||||
private _cumulative: GPXGlobalStatistics[];
|
|
||||||
private _slice: [number, number] | null = null;
|
|
||||||
global: GPXGlobalStatistics;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this._statistics = [];
|
|
||||||
this._cumulative = [new GPXGlobalStatistics()];
|
|
||||||
this.global = new GPXGlobalStatistics();
|
|
||||||
}
|
|
||||||
|
|
||||||
add(statistics: GPXStatistics | GPXStatisticsGroup): void {
|
|
||||||
if (statistics instanceof GPXStatisticsGroup) {
|
|
||||||
statistics._statistics.forEach((stats) => this._add(stats));
|
|
||||||
} else {
|
|
||||||
this._add(statistics);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_add(statistics: GPXStatistics): void {
|
|
||||||
this._statistics.push(statistics);
|
|
||||||
const cumulative = new GPXGlobalStatistics();
|
|
||||||
cumulative.mergeWith(this._cumulative[this._cumulative.length - 1]);
|
|
||||||
cumulative.mergeWith(statistics.global);
|
|
||||||
this._cumulative.push(cumulative);
|
|
||||||
this.global.mergeWith(statistics.global);
|
|
||||||
}
|
|
||||||
|
|
||||||
sliced(start: number, end: number): GPXGlobalStatistics {
|
|
||||||
let sliced = new GPXGlobalStatistics();
|
|
||||||
for (let i = 0; i < this._statistics.length; i++) {
|
|
||||||
const statistics = this._statistics[i];
|
|
||||||
const cumulative = this._cumulative[i];
|
|
||||||
if (start < cumulative.length + statistics.global.length && end >= cumulative.length) {
|
|
||||||
const localStart = Math.max(0, start - cumulative.length);
|
|
||||||
const localEnd = Math.min(statistics.global.length - 1, end - cumulative.length);
|
|
||||||
sliced.mergeWith(statistics.sliced(localStart, localEnd));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sliced;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTrackPoint(index: number): TrackPointWithLocalStatistics | undefined {
|
|
||||||
if (this._slice !== null) {
|
|
||||||
index += this._slice[0];
|
|
||||||
}
|
|
||||||
for (let i = 0; i < this._statistics.length; i++) {
|
|
||||||
const statistics = this._statistics[i];
|
|
||||||
const cumulative = this._cumulative[i];
|
|
||||||
if (index < cumulative.length + statistics.global.length) {
|
|
||||||
return this._getTrackPoint(cumulative, statistics, index - cumulative.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
_getTrackPoint(
|
|
||||||
cumulative: GPXGlobalStatistics,
|
|
||||||
statistics: GPXStatistics,
|
|
||||||
index: number
|
|
||||||
): TrackPointWithLocalStatistics {
|
|
||||||
const point = statistics.local.points[index];
|
|
||||||
return {
|
|
||||||
trkpt: point,
|
|
||||||
distance: {
|
|
||||||
moving: statistics.local.data[index].distance.moving + cumulative.distance.moving,
|
|
||||||
total: statistics.local.data[index].distance.total + cumulative.distance.total,
|
|
||||||
},
|
|
||||||
time: {
|
|
||||||
moving: statistics.local.data[index].time.moving + cumulative.time.moving,
|
|
||||||
total: statistics.local.data[index].time.total + cumulative.time.total,
|
|
||||||
},
|
|
||||||
speed: statistics.local.data[index].speed,
|
|
||||||
elevation: {
|
|
||||||
gain: statistics.local.data[index].elevation.gain + cumulative.elevation.gain,
|
|
||||||
loss: statistics.local.data[index].elevation.loss + cumulative.elevation.loss,
|
|
||||||
},
|
|
||||||
slope: {
|
|
||||||
at: statistics.local.data[index].slope.at,
|
|
||||||
segment: statistics.local.data[index].slope.segment,
|
|
||||||
length: statistics.local.data[index].slope.length,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
forEachTrackPoint(
|
|
||||||
callback: (
|
|
||||||
point: TrackPoint,
|
|
||||||
distance: number,
|
|
||||||
speed: number,
|
|
||||||
slope: { at: number; segment: number; length: number },
|
|
||||||
index: number
|
|
||||||
) => void
|
|
||||||
): void {
|
|
||||||
for (let i = 0; i < this._statistics.length; i++) {
|
|
||||||
const statistics = this._statistics[i];
|
|
||||||
const cumulative = this._cumulative[i];
|
|
||||||
statistics.local.points.forEach((point, index) =>
|
|
||||||
callback(
|
|
||||||
point,
|
|
||||||
cumulative.distance.total + statistics.local.data[index].distance.total,
|
|
||||||
statistics.local.data[index].speed,
|
|
||||||
statistics.local.data[index].slope,
|
|
||||||
cumulative.length + index
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "gpx.studio",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
"style": "default",
|
"style": "default",
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"css": "src/app.css",
|
"css": "src/app.css",
|
||||||
"baseColor": "slate"
|
"baseColor": "slate"
|
||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "$lib/components",
|
"components": "$lib/components",
|
||||||
"utils": "$lib/utils",
|
"utils": "$lib/utils",
|
||||||
"ui": "$lib/components/ui",
|
"ui": "$lib/components/ui",
|
||||||
"hooks": "$lib/hooks",
|
"hooks": "$lib/hooks",
|
||||||
"lib": "$lib"
|
"lib": "$lib"
|
||||||
},
|
},
|
||||||
"typescript": true,
|
"typescript": true,
|
||||||
"registry": "https://shadcn-svelte.com/registry"
|
"registry": "https://shadcn-svelte.com/registry"
|
||||||
}
|
}
|
||||||
|
|||||||
119
website/package-lock.json
generated
119
website/package-lock.json
generated
@@ -14,7 +14,7 @@
|
|||||||
"@mapbox/sphericalmercator": "^2.0.1",
|
"@mapbox/sphericalmercator": "^2.0.1",
|
||||||
"@mapbox/tilebelt": "^2.0.2",
|
"@mapbox/tilebelt": "^2.0.2",
|
||||||
"@types/mapbox__sphericalmercator": "^1.2.3",
|
"@types/mapbox__sphericalmercator": "^1.2.3",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.4.9",
|
||||||
"chartjs-plugin-zoom": "^2.2.0",
|
"chartjs-plugin-zoom": "^2.2.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dexie": "^4.0.11",
|
"dexie": "^4.0.11",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"gpx": "file:../gpx",
|
"gpx": "file:../gpx",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"mapbox-gl": "^3.17.0",
|
"mapbox-gl": "^3.12.0",
|
||||||
"mapillary-js": "^4.1.2",
|
"mapillary-js": "^4.1.2",
|
||||||
"png.js": "^0.2.1",
|
"png.js": "^0.2.1",
|
||||||
"sanitize-html": "^2.17.0",
|
"sanitize-html": "^2.17.0",
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
"@types/sortablejs": "^1.15.8",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
||||||
"@typescript-eslint/parser": "^8.33.1",
|
"@typescript-eslint/parser": "^8.33.1",
|
||||||
"bits-ui": "^2.14.4",
|
"bits-ui": "^2.12.0",
|
||||||
"eslint": "^9.28.0",
|
"eslint": "^9.28.0",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"eslint-plugin-svelte": "^3.9.1",
|
"eslint-plugin-svelte": "^3.9.1",
|
||||||
@@ -1701,10 +1701,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mapbox/point-geometry": {
|
"node_modules/@mapbox/point-geometry": {
|
||||||
"version": "1.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
|
||||||
"integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==",
|
"integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ=="
|
||||||
"license": "ISC"
|
|
||||||
},
|
},
|
||||||
"node_modules/@mapbox/polyline": {
|
"node_modules/@mapbox/polyline": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
@@ -1739,26 +1738,11 @@
|
|||||||
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw=="
|
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw=="
|
||||||
},
|
},
|
||||||
"node_modules/@mapbox/vector-tile": {
|
"node_modules/@mapbox/vector-tile": {
|
||||||
"version": "2.0.4",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz",
|
||||||
"integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==",
|
"integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/point-geometry": "~1.1.0",
|
"@mapbox/point-geometry": "~0.1.0"
|
||||||
"@types/geojson": "^7946.0.16",
|
|
||||||
"pbf": "^4.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@mapbox/vector-tile/node_modules/pbf": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"resolve-protobuf-schema": "^2.1.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"pbf": "bin/pbf"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mapbox/whoots-js": {
|
"node_modules/@mapbox/whoots-js": {
|
||||||
@@ -2660,8 +2644,7 @@
|
|||||||
"node_modules/@types/mapbox__point-geometry": {
|
"node_modules/@types/mapbox__point-geometry": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
|
||||||
"integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==",
|
"integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA=="
|
||||||
"license": "MIT"
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/mapbox__sphericalmercator": {
|
"node_modules/@types/mapbox__sphericalmercator": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
@@ -2677,6 +2660,16 @@
|
|||||||
"@types/geojson": "*"
|
"@types/geojson": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/mapbox__vector-tile": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*",
|
||||||
|
"@types/mapbox__point-geometry": "*",
|
||||||
|
"@types/pbf": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/mapbox-gl": {
|
"node_modules/@types/mapbox-gl": {
|
||||||
"version": "3.4.1",
|
"version": "3.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz",
|
||||||
@@ -3241,9 +3234,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/bits-ui": {
|
"node_modules/bits-ui": {
|
||||||
"version": "2.14.4",
|
"version": "2.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.14.4.tgz",
|
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.12.0.tgz",
|
||||||
"integrity": "sha512-W6kenhnbd/YVvur+DKkaVJ6GldE53eLewur5AhUCqslYQ0vjZr8eWlOfwZnMiPB+PF5HMVqf61vXBvmyrAmPWg==",
|
"integrity": "sha512-8NF4ILNyAJlIxDXpl/akGXGBV5QmZAe+8gTfPttM5P6/+LrijumcSfFXY5cr4QkXwTmLA7H5stYpbgJf2XFJvg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3664,9 +3657,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chart.js": {
|
"node_modules/chart.js": {
|
||||||
"version": "4.5.1",
|
"version": "4.4.9",
|
||||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz",
|
||||||
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
"integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kurkle/color": "^0.3.0"
|
"@kurkle/color": "^0.3.0"
|
||||||
@@ -4954,10 +4947,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/gl-matrix": {
|
"node_modules/gl-matrix": {
|
||||||
"version": "3.4.4",
|
"version": "3.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
|
||||||
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
"integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="
|
||||||
"license": "MIT"
|
|
||||||
},
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "11.0.2",
|
"version": "11.0.2",
|
||||||
@@ -6069,55 +6061,44 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mapbox-gl": {
|
"node_modules/mapbox-gl": {
|
||||||
"version": "3.17.0",
|
"version": "3.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.12.0.tgz",
|
||||||
"integrity": "sha512-nCrDKRlr5di6xUksUDslNWwxroJ5yv1hT8pyVFtcpWJOOKsYQxF/wOFTMie8oxMnXeFkrz1Tl1TwA1XN1yX0KA==",
|
"integrity": "sha512-DV6TRr+xoPrLSKuGiUcbyLVkoLdNaNNpn6O7+ZC27yQH7BOOIF7l6JKbTCMhfMJuZBVJfL8YRJjlMJ6MZCTggA==",
|
||||||
"license": "SEE LICENSE IN LICENSE.txt",
|
"license": "SEE LICENSE IN LICENSE.txt",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"src/style-spec",
|
"src/style-spec",
|
||||||
"test/build/vite",
|
|
||||||
"test/build/webpack",
|
|
||||||
"test/build/typings"
|
"test/build/typings"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
||||||
"@mapbox/mapbox-gl-supported": "^3.0.0",
|
"@mapbox/mapbox-gl-supported": "^3.0.0",
|
||||||
"@mapbox/point-geometry": "^1.1.0",
|
"@mapbox/point-geometry": "^0.1.0",
|
||||||
"@mapbox/tiny-sdf": "^2.0.6",
|
"@mapbox/tiny-sdf": "^2.0.6",
|
||||||
"@mapbox/unitbezier": "^0.0.1",
|
"@mapbox/unitbezier": "^0.0.1",
|
||||||
"@mapbox/vector-tile": "^2.0.4",
|
"@mapbox/vector-tile": "^1.3.1",
|
||||||
"@mapbox/whoots-js": "^3.1.0",
|
"@mapbox/whoots-js": "^3.1.0",
|
||||||
"@types/geojson": "^7946.0.16",
|
"@types/geojson": "^7946.0.16",
|
||||||
"@types/geojson-vt": "^3.2.5",
|
"@types/geojson-vt": "^3.2.5",
|
||||||
"@types/mapbox__point-geometry": "^0.1.4",
|
"@types/mapbox__point-geometry": "^0.1.4",
|
||||||
|
"@types/mapbox__vector-tile": "^1.3.4",
|
||||||
"@types/pbf": "^3.0.5",
|
"@types/pbf": "^3.0.5",
|
||||||
"@types/supercluster": "^7.1.3",
|
"@types/supercluster": "^7.1.3",
|
||||||
"cheap-ruler": "^4.0.0",
|
"cheap-ruler": "^4.0.0",
|
||||||
"csscolorparser": "~1.0.3",
|
"csscolorparser": "~1.0.3",
|
||||||
"earcut": "^3.0.1",
|
"earcut": "^3.0.1",
|
||||||
"geojson-vt": "^4.0.2",
|
"geojson-vt": "^4.0.2",
|
||||||
"gl-matrix": "^3.4.4",
|
"gl-matrix": "^3.4.3",
|
||||||
"grid-index": "^1.1.0",
|
"grid-index": "^1.1.0",
|
||||||
"kdbush": "^4.0.2",
|
"kdbush": "^4.0.2",
|
||||||
"martinez-polygon-clipping": "^0.7.4",
|
"martinez-polygon-clipping": "^0.7.4",
|
||||||
"murmurhash-js": "^1.0.0",
|
"murmurhash-js": "^1.0.0",
|
||||||
"pbf": "^4.0.1",
|
"pbf": "^3.2.1",
|
||||||
"potpack": "^2.0.0",
|
"potpack": "^2.0.0",
|
||||||
"quickselect": "^3.0.0",
|
"quickselect": "^3.0.0",
|
||||||
|
"serialize-to-js": "^3.1.2",
|
||||||
"supercluster": "^8.0.1",
|
"supercluster": "^8.0.1",
|
||||||
"tinyqueue": "^3.0.0"
|
"tinyqueue": "^3.0.0",
|
||||||
}
|
"vt-pbf": "^3.1.3"
|
||||||
},
|
|
||||||
"node_modules/mapbox-gl/node_modules/pbf": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"resolve-protobuf-schema": "^2.1.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"pbf": "bin/pbf"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mapillary-js": {
|
"node_modules/mapillary-js": {
|
||||||
@@ -7635,6 +7616,14 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/serialize-to-js": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/serialize-to-js/-/serialize-to-js-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-owllqNuDDEimQat7EPG0tH7JjO090xKNzUtYz6X+Sk2BXDnOCilDdNLwjWeFywG9xkJul1ULvtUQa9O4pUaY0w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/set-cookie-parser": {
|
"node_modules/set-cookie-parser": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz",
|
||||||
@@ -9032,6 +9021,16 @@
|
|||||||
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
|
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/vt-pbf": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/point-geometry": "0.1.0",
|
||||||
|
"@mapbox/vector-tile": "^1.3.1",
|
||||||
|
"pbf": "^3.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"lint": "prettier --check . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore && eslint .",
|
"lint": "prettier --check . && eslint .",
|
||||||
"format": "prettier --write . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore"
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lucide/svelte": "^0.544.0",
|
"@lucide/svelte": "^0.544.0",
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"@types/sortablejs": "^1.15.8",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
||||||
"@typescript-eslint/parser": "^8.33.1",
|
"@typescript-eslint/parser": "^8.33.1",
|
||||||
"bits-ui": "^2.14.4",
|
"bits-ui": "^2.12.0",
|
||||||
"eslint": "^9.28.0",
|
"eslint": "^9.28.0",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"eslint-plugin-svelte": "^3.9.1",
|
"eslint-plugin-svelte": "^3.9.1",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"@mapbox/sphericalmercator": "^2.0.1",
|
"@mapbox/sphericalmercator": "^2.0.1",
|
||||||
"@mapbox/tilebelt": "^2.0.2",
|
"@mapbox/tilebelt": "^2.0.2",
|
||||||
"@types/mapbox__sphericalmercator": "^1.2.3",
|
"@types/mapbox__sphericalmercator": "^1.2.3",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.4.9",
|
||||||
"chartjs-plugin-zoom": "^2.2.0",
|
"chartjs-plugin-zoom": "^2.2.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dexie": "^4.0.11",
|
"dexie": "^4.0.11",
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
"gpx": "file:../gpx",
|
"gpx": "file:../gpx",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"mapbox-gl": "^3.17.0",
|
"mapbox-gl": "^3.12.0",
|
||||||
"mapillary-js": "^4.1.2",
|
"mapillary-js": "^4.1.2",
|
||||||
"png.js": "^0.2.1",
|
"png.js": "^0.2.1",
|
||||||
"sanitize-html": "^2.17.0",
|
"sanitize-html": "^2.17.0",
|
||||||
|
|||||||
@@ -1,126 +1,124 @@
|
|||||||
@import 'tailwindcss';
|
@import "tailwindcss";
|
||||||
@import 'tw-animate-css';
|
@import "tw-animate-css";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: hsl(0 0% 100%) /* <- Wrap in HSL */;
|
--background: hsl(0 0% 100%) /* <- Wrap in HSL */;
|
||||||
--foreground: hsl(240 10% 3.9%);
|
--foreground: hsl(240 10% 3.9%);
|
||||||
--muted: hsl(240 4.8% 95.9%);
|
--muted: hsl(240 4.8% 95.9%);
|
||||||
--muted-foreground: hsl(240 3.8% 46.1%);
|
--muted-foreground: hsl(240 3.8% 46.1%);
|
||||||
--popover: hsl(0 0% 100%);
|
--popover: hsl(0 0% 100%);
|
||||||
--popover-foreground: hsl(240 10% 3.9%);
|
--popover-foreground: hsl(240 10% 3.9%);
|
||||||
--card: hsl(0 0% 100%);
|
--card: hsl(0 0% 100%);
|
||||||
--card-foreground: hsl(240 10% 3.9%);
|
--card-foreground: hsl(240 10% 3.9%);
|
||||||
--border: hsl(240 5.9% 90%);
|
--border: hsl(240 5.9% 90%);
|
||||||
--input: hsl(240 5.9% 90%);
|
--input: hsl(240 5.9% 90%);
|
||||||
--primary: hsl(240 5.9% 10%);
|
--primary: hsl(240 5.9% 10%);
|
||||||
--primary-foreground: hsl(0 0% 98%);
|
--primary-foreground: hsl(0 0% 98%);
|
||||||
--secondary: hsl(240 4.8% 95.9%);
|
--secondary: hsl(240 4.8% 95.9%);
|
||||||
--secondary-foreground: hsl(240 5.9% 10%);
|
--secondary-foreground: hsl(240 5.9% 10%);
|
||||||
--accent: hsl(240 4.8% 95.9%);
|
--accent: hsl(240 4.8% 95.9%);
|
||||||
--accent-foreground: hsl(240 5.9% 10%);
|
--accent-foreground: hsl(240 5.9% 10%);
|
||||||
--destructive: hsl(0 72.2% 50.6%);
|
--destructive: hsl(0 72.2% 50.6%);
|
||||||
--destructive-foreground: hsl(0 0% 98%);
|
--destructive-foreground: hsl(0 0% 98%);
|
||||||
--ring: hsl(240 10% 3.9%);
|
--ring: hsl(240 10% 3.9%);
|
||||||
--sidebar: hsl(0 0% 98%);
|
--sidebar: hsl(0 0% 98%);
|
||||||
--sidebar-foreground: hsl(240 5.3% 26.1%);
|
--sidebar-foreground: hsl(240 5.3% 26.1%);
|
||||||
--sidebar-primary: hsl(240 5.9% 10%);
|
--sidebar-primary: hsl(240 5.9% 10%);
|
||||||
--sidebar-primary-foreground: hsl(0 0% 98%);
|
--sidebar-primary-foreground: hsl(0 0% 98%);
|
||||||
--sidebar-accent: hsl(240 4.8% 95.9%);
|
--sidebar-accent: hsl(240 4.8% 95.9%);
|
||||||
--sidebar-accent-foreground: hsl(240 5.9% 10%);
|
--sidebar-accent-foreground: hsl(240 5.9% 10%);
|
||||||
--sidebar-border: hsl(220 13% 91%);
|
--sidebar-border: hsl(220 13% 91%);
|
||||||
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||||
|
|
||||||
--support: rgb(220 15 130);
|
--support: rgb(220 15 130);
|
||||||
--link: rgb(0 110 180);
|
--link: rgb(0 110 180);
|
||||||
--selection: hsl(240 4.8% 93%);
|
|
||||||
|
--radius: 0.5rem;
|
||||||
--radius: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: hsl(240 10% 3.9%);
|
--background: hsl(240 10% 3.9%);
|
||||||
--foreground: hsl(0 0% 98%);
|
--foreground: hsl(0 0% 98%);
|
||||||
--muted: hsl(240 3.7% 15.9%);
|
--muted: hsl(240 3.7% 15.9%);
|
||||||
--muted-foreground: hsl(240 5% 64.9%);
|
--muted-foreground: hsl(240 5% 64.9%);
|
||||||
--popover: hsl(240 10% 3.9%);
|
--popover: hsl(240 10% 3.9%);
|
||||||
--popover-foreground: hsl(0 0% 98%);
|
--popover-foreground: hsl(0 0% 98%);
|
||||||
--card: hsl(240 10% 3.9%);
|
--card: hsl(240 10% 3.9%);
|
||||||
--card-foreground: hsl(0 0% 98%);
|
--card-foreground: hsl(0 0% 98%);
|
||||||
--border: hsl(240 3.7% 15.9%);
|
--border: hsl(240 3.7% 15.9%);
|
||||||
--input: hsl(240 3.7% 15.9%);
|
--input: hsl(240 3.7% 15.9%);
|
||||||
--primary: hsl(0 0% 98%);
|
--primary: hsl(0 0% 98%);
|
||||||
--primary-foreground: hsl(240 5.9% 10%);
|
--primary-foreground: hsl(240 5.9% 10%);
|
||||||
--secondary: hsl(240 3.7% 15.9%);
|
--secondary: hsl(240 3.7% 15.9%);
|
||||||
--secondary-foreground: hsl(0 0% 98%);
|
--secondary-foreground: hsl(0 0% 98%);
|
||||||
--accent: hsl(240 3.7% 15.9%);
|
--accent: hsl(240 3.7% 15.9%);
|
||||||
--accent-foreground: hsl(0 0% 98%);
|
--accent-foreground: hsl(0 0% 98%);
|
||||||
--destructive: hsl(0 62.8% 30.6%);
|
--destructive: hsl(0 62.8% 30.6%);
|
||||||
--destructive-foreground: hsl(0 0% 98%);
|
--destructive-foreground: hsl(0 0% 98%);
|
||||||
--ring: hsl(240 4.9% 83.9%);
|
--ring: hsl(240 4.9% 83.9%);
|
||||||
--sidebar: hsl(240 5.9% 10%);
|
--sidebar: hsl(240 5.9% 10%);
|
||||||
--sidebar-foreground: hsl(240 4.8% 95.9%);
|
--sidebar-foreground: hsl(240 4.8% 95.9%);
|
||||||
--sidebar-primary: hsl(224.3 76.3% 48%);
|
--sidebar-primary: hsl(224.3 76.3% 48%);
|
||||||
--sidebar-primary-foreground: hsl(0 0% 100%);
|
--sidebar-primary-foreground: hsl(0 0% 100%);
|
||||||
--sidebar-accent: hsl(240 3.7% 15.9%);
|
--sidebar-accent: hsl(240 3.7% 15.9%);
|
||||||
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
|
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
|
||||||
--sidebar-border: hsl(240 3.7% 15.9%);
|
--sidebar-border: hsl(240 3.7% 15.9%);
|
||||||
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||||
|
|
||||||
--support: rgb(255 110 190);
|
--support: rgb(255 110 190);
|
||||||
--link: rgb(80 190 255);
|
--link: rgb(80 190 255);
|
||||||
--selection: hsl(240 3.7% 22%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
/* Radius (for rounded-*) */
|
/* Radius (for rounded-*) */
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
|
||||||
|
/* Colors */
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-radius: var(--radius);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-support: var(--support);
|
||||||
|
--color-link: var(--link);
|
||||||
|
|
||||||
/* Colors */
|
--breakpoint-xs: 540px;
|
||||||
--color-background: var(--background);
|
|
||||||
--color-foreground: var(--foreground);
|
|
||||||
--color-muted: var(--muted);
|
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
|
||||||
--color-popover: var(--popover);
|
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
|
||||||
--color-card: var(--card);
|
|
||||||
--color-card-foreground: var(--card-foreground);
|
|
||||||
--color-border: var(--border);
|
|
||||||
--color-input: var(--input);
|
|
||||||
--color-primary: var(--primary);
|
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
|
||||||
--color-secondary: var(--secondary);
|
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
|
||||||
--color-accent: var(--accent);
|
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
|
||||||
--color-destructive: var(--destructive);
|
|
||||||
--color-destructive-foreground: var(--destructive-foreground);
|
|
||||||
--color-ring: var(--ring);
|
|
||||||
--color-radius: var(--radius);
|
|
||||||
--color-sidebar: var(--sidebar);
|
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
|
||||||
--color-support: var(--support);
|
|
||||||
--color-link: var(--link);
|
|
||||||
|
|
||||||
--breakpoint-xs: 540px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,14 +24,6 @@ export async function handle({ event, resolve }) {
|
|||||||
|
|
||||||
let headTag = `<head>
|
let headTag = `<head>
|
||||||
<title>gpx.studio — ${title}</title>
|
<title>gpx.studio — ${title}</title>
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "WebSite",
|
|
||||||
"name": "gpx.studio",
|
|
||||||
"url": "https://gpx.studio"
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<meta name="description" content="${description}" />
|
<meta name="description" content="${description}" />
|
||||||
<meta property="og:title" content="gpx.studio — ${title}" />
|
<meta property="og:title" content="gpx.studio — ${title}" />
|
||||||
<meta property="og:description" content="${description}" />
|
<meta property="og:description" content="${description}" />
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sprite": "https://demotiles.maplibre.org/styles/osm-bright-gl-style/sprite",
|
"sprite": "https://demotiles.maplibre.org/styles/osm-bright-gl-style/sprite",
|
||||||
|
"glyphs": "https://api.maptiler.com/fonts/{fontstack}/{range}.pbf?key={key}",
|
||||||
"layers": [
|
"layers": [
|
||||||
{
|
{
|
||||||
"id": "background",
|
"id": "background",
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
Binoculars,
|
Binoculars,
|
||||||
Toilet,
|
Toilet,
|
||||||
} from 'lucide-static';
|
} from 'lucide-static';
|
||||||
import { type RasterDEMSourceSpecification, type StyleSpecification } from 'mapbox-gl';
|
import { type StyleSpecification } from 'mapbox-gl';
|
||||||
import ignFrTopo from './custom/ign-fr-topo.json';
|
import ignFrTopo from './custom/ign-fr-topo.json';
|
||||||
import ignFrPlan from './custom/ign-fr-plan.json';
|
import ignFrPlan from './custom/ign-fr-plan.json';
|
||||||
import ignFrSatellite from './custom/ign-fr-satellite.json';
|
import ignFrSatellite from './custom/ign-fr-satellite.json';
|
||||||
@@ -145,7 +145,7 @@ export const basemaps: { [key: string]: string | StyleSpecification } = {
|
|||||||
swisstopoVector: 'https://vectortiles.geo.admin.ch/styles/ch.swisstopo.basemap.vt/style.json',
|
swisstopoVector: 'https://vectortiles.geo.admin.ch/styles/ch.swisstopo.basemap.vt/style.json',
|
||||||
swisstopoSatellite:
|
swisstopoSatellite:
|
||||||
'https://vectortiles.geo.admin.ch/styles/ch.swisstopo.imagerybasemap.vt/style.json',
|
'https://vectortiles.geo.admin.ch/styles/ch.swisstopo.imagerybasemap.vt/style.json',
|
||||||
linz: 'https://basemaps.linz.govt.nz/v1/styles/topographic-v2.json?api=d01fbtg0ar23gctac5m0jgyy2ds',
|
linz: 'https://basemaps.linz.govt.nz/v1/tiles/topographic/EPSG:3857/style/topographic.json?api=d01fbtg0ar23gctac5m0jgyy2ds',
|
||||||
linzTopo: {
|
linzTopo: {
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {
|
sources: {
|
||||||
@@ -368,42 +368,6 @@ export const overlays: { [key: string]: string | StyleSpecification } = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
bikerouterGravel: bikerouterGravel as StyleSpecification,
|
bikerouterGravel: bikerouterGravel as StyleSpecification,
|
||||||
openRailwayMap: {
|
|
||||||
version: 8,
|
|
||||||
sources: {
|
|
||||||
openRailwayMap: {
|
|
||||||
type: 'raster',
|
|
||||||
tiles: ['https://tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png'],
|
|
||||||
tileSize: 256,
|
|
||||||
maxzoom: 19,
|
|
||||||
attribution:
|
|
||||||
'Data <a href="https://www.openstreetmap.org/copyright">© OpenStreetMap contributors</a>, Style: <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA 2.0</a> <a href="http://www.openrailwaymap.org/">OpenRailwayMap</a>',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: 'openRailwayMap',
|
|
||||||
type: 'raster',
|
|
||||||
source: 'openRailwayMap',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
mapterhornHillshade: {
|
|
||||||
version: 8,
|
|
||||||
sources: {
|
|
||||||
mapterhornHillshade: {
|
|
||||||
type: 'raster-dem',
|
|
||||||
url: 'https://tiles.mapterhorn.com/tilejson.json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: 'mapterhornHillshade',
|
|
||||||
type: 'hillshade',
|
|
||||||
source: 'mapterhornHillshade',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
swisstopoSlope: {
|
swisstopoSlope: {
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {
|
sources: {
|
||||||
@@ -835,10 +799,8 @@ export const overlayTree: LayerTreeType = {
|
|||||||
waymarkedTrailsHorseRiding: true,
|
waymarkedTrailsHorseRiding: true,
|
||||||
waymarkedTrailsWinter: true,
|
waymarkedTrailsWinter: true,
|
||||||
},
|
},
|
||||||
bikerouterGravel: true,
|
|
||||||
cyclOSMlite: true,
|
cyclOSMlite: true,
|
||||||
mapterhornHillshade: true,
|
bikerouterGravel: true,
|
||||||
openRailwayMap: true,
|
|
||||||
},
|
},
|
||||||
countries: {
|
countries: {
|
||||||
france: {
|
france: {
|
||||||
@@ -874,7 +836,6 @@ export const overpassTree: LayerTreeType = {
|
|||||||
shower: true,
|
shower: true,
|
||||||
shelter: true,
|
shelter: true,
|
||||||
barrier: true,
|
barrier: true,
|
||||||
cemetery: true,
|
|
||||||
},
|
},
|
||||||
tourism: {
|
tourism: {
|
||||||
attraction: true,
|
attraction: true,
|
||||||
@@ -921,10 +882,8 @@ export const defaultOverlays: LayerTreeType = {
|
|||||||
waymarkedTrailsHorseRiding: false,
|
waymarkedTrailsHorseRiding: false,
|
||||||
waymarkedTrailsWinter: false,
|
waymarkedTrailsWinter: false,
|
||||||
},
|
},
|
||||||
bikerouterGravel: false,
|
|
||||||
cyclOSMlite: false,
|
cyclOSMlite: false,
|
||||||
mapterhornHillshade: false,
|
bikerouterGravel: false,
|
||||||
openRailwayMap: false,
|
|
||||||
},
|
},
|
||||||
countries: {
|
countries: {
|
||||||
france: {
|
france: {
|
||||||
@@ -960,7 +919,6 @@ export const defaultOverpassQueries: LayerTreeType = {
|
|||||||
shower: false,
|
shower: false,
|
||||||
shelter: false,
|
shelter: false,
|
||||||
barrier: false,
|
barrier: false,
|
||||||
cemetery: false,
|
|
||||||
},
|
},
|
||||||
tourism: {
|
tourism: {
|
||||||
attraction: false,
|
attraction: false,
|
||||||
@@ -1058,10 +1016,8 @@ export const defaultOverlayTree: LayerTreeType = {
|
|||||||
waymarkedTrailsHorseRiding: false,
|
waymarkedTrailsHorseRiding: false,
|
||||||
waymarkedTrailsWinter: false,
|
waymarkedTrailsWinter: false,
|
||||||
},
|
},
|
||||||
bikerouterGravel: false,
|
|
||||||
cyclOSMlite: false,
|
cyclOSMlite: false,
|
||||||
mapterhornHillshade: false,
|
bikerouterGravel: false,
|
||||||
openRailwayMap: false,
|
|
||||||
},
|
},
|
||||||
countries: {
|
countries: {
|
||||||
france: {
|
france: {
|
||||||
@@ -1097,7 +1053,6 @@ export const defaultOverpassTree: LayerTreeType = {
|
|||||||
shower: false,
|
shower: false,
|
||||||
shelter: false,
|
shelter: false,
|
||||||
barrier: false,
|
barrier: false,
|
||||||
cemetery: false,
|
|
||||||
},
|
},
|
||||||
tourism: {
|
tourism: {
|
||||||
attraction: false,
|
attraction: false,
|
||||||
@@ -1144,7 +1099,9 @@ type OverpassQueryData = {
|
|||||||
svg: string;
|
svg: string;
|
||||||
color: string;
|
color: string;
|
||||||
};
|
};
|
||||||
tags: Record<string, string | string[]> | Record<string, string | string[]>[];
|
tags:
|
||||||
|
| Record<string, string | boolean | string[]>
|
||||||
|
| Record<string, string | boolean | string[]>[];
|
||||||
symbol?: string;
|
symbol?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1225,20 +1182,6 @@ export const overpassQueryData: Record<string, OverpassQueryData> = {
|
|||||||
},
|
},
|
||||||
symbol: 'Shelter',
|
symbol: 'Shelter',
|
||||||
},
|
},
|
||||||
cemetery: {
|
|
||||||
icon: {
|
|
||||||
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 17v-10a6 5 0 1 1 12 0v10"/><path d="M 4 21 a 1 1 0 0 0 1 1 h 14 a 1 1 0 0 0 1-1 v -1 a 2 2 0 0 0-2-2 H6 a 2 2 0 0 0-2 2 z"/></svg>',
|
|
||||||
color: '#000000',
|
|
||||||
},
|
|
||||||
tags: [
|
|
||||||
{
|
|
||||||
landuse: 'cemetery',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
amenity: 'grave_yard',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'fuel-station': {
|
'fuel-station': {
|
||||||
icon: {
|
icon: {
|
||||||
svg: Fuel,
|
svg: Fuel,
|
||||||
@@ -1275,25 +1218,7 @@ export const overpassQueryData: Record<string, OverpassQueryData> = {
|
|||||||
color: '#000000',
|
color: '#000000',
|
||||||
},
|
},
|
||||||
tags: {
|
tags: {
|
||||||
barrier: [
|
barrier: true,
|
||||||
'bar',
|
|
||||||
'barrier_board',
|
|
||||||
'block',
|
|
||||||
'chain',
|
|
||||||
'cycle_barrier',
|
|
||||||
'gate',
|
|
||||||
'hampshire_gate',
|
|
||||||
'horse_stile',
|
|
||||||
'kissing_gate',
|
|
||||||
'lift_gate',
|
|
||||||
'motorcycle_barrier',
|
|
||||||
'sliding_beam',
|
|
||||||
'sliding_gate',
|
|
||||||
'stile',
|
|
||||||
'swing_gate',
|
|
||||||
'turnstile',
|
|
||||||
'wicket_gate',
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
attraction: {
|
attraction: {
|
||||||
@@ -1453,18 +1378,3 @@ export const overpassQueryData: Record<string, OverpassQueryData> = {
|
|||||||
symbol: 'Anchor',
|
symbol: 'Anchor',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const terrainSources: { [key: string]: RasterDEMSourceSpecification } = {
|
|
||||||
'mapbox-dem': {
|
|
||||||
type: 'raster-dem',
|
|
||||||
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
|
|
||||||
tileSize: 512,
|
|
||||||
maxzoom: 14,
|
|
||||||
},
|
|
||||||
mapterhorn: {
|
|
||||||
type: 'raster-dem',
|
|
||||||
url: 'https://tiles.mapterhorn.com/tilejson.json',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const defaultTerrainSource = 'mapbox-dem';
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Landmark,
|
Landmark,
|
||||||
|
Icon,
|
||||||
Shell,
|
Shell,
|
||||||
Bike,
|
Bike,
|
||||||
Building,
|
Building,
|
||||||
@@ -28,7 +29,6 @@ import {
|
|||||||
TriangleAlert,
|
TriangleAlert,
|
||||||
Anchor,
|
Anchor,
|
||||||
Toilet,
|
Toilet,
|
||||||
X,
|
|
||||||
type IconProps,
|
type IconProps,
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import {
|
import {
|
||||||
@@ -61,7 +61,6 @@ import {
|
|||||||
TriangleAlert as TriangleAlertSvg,
|
TriangleAlert as TriangleAlertSvg,
|
||||||
Anchor as AnchorSvg,
|
Anchor as AnchorSvg,
|
||||||
Toilet as ToiletSvg,
|
Toilet as ToiletSvg,
|
||||||
X as XSvg,
|
|
||||||
} from 'lucide-static';
|
} from 'lucide-static';
|
||||||
import type { Component } from 'svelte';
|
import type { Component } from 'svelte';
|
||||||
|
|
||||||
@@ -88,11 +87,7 @@ export const symbols: { [key: string]: Symbol } = {
|
|||||||
icon: ShoppingBasket,
|
icon: ShoppingBasket,
|
||||||
iconSvg: ShoppingBasketSvg,
|
iconSvg: ShoppingBasketSvg,
|
||||||
},
|
},
|
||||||
crossing: {
|
crossing: { value: 'Crossing' },
|
||||||
value: 'Crossing',
|
|
||||||
icon: X,
|
|
||||||
iconSvg: XSvg,
|
|
||||||
},
|
|
||||||
department_store: {
|
department_store: {
|
||||||
value: 'Department Store',
|
value: 'Department Store',
|
||||||
icon: ShoppingBasket,
|
icon: ShoppingBasket,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
href="https://github.com/gpxstudio/gpx.studio/blob/main/LICENSE"
|
href="https://github.com/gpxstudio/gpx.studio/blob/main/LICENSE"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
MIT © 2026 gpx.studio
|
MIT © 2025 gpx.studio
|
||||||
</Button>
|
</Button>
|
||||||
<LanguageSelect class="w-40 mt-3" />
|
<LanguageSelect class="w-40 mt-3" />
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +34,6 @@
|
|||||||
{i18n._('homepage.home')}
|
{i18n._('homepage.home')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
data-sveltekit-reload
|
|
||||||
variant="link"
|
variant="link"
|
||||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||||
href={getURLForLanguage(i18n.lang, '/app')}
|
href={getURLForLanguage(i18n.lang, '/app')}
|
||||||
@@ -71,6 +70,15 @@
|
|||||||
<Logo company="facebook" class="h-4 fill-muted-foreground" />
|
<Logo company="facebook" class="h-4 fill-muted-foreground" />
|
||||||
{i18n._('homepage.facebook')}
|
{i18n._('homepage.facebook')}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||||
|
href="https://x.com/gpxstudio"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Logo company="x" class="h-4 fill-muted-foreground" />
|
||||||
|
{i18n._('homepage.x')}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from '@lucide/svelte';
|
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from '@lucide/svelte';
|
||||||
|
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
import type { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
import type { GPXStatistics } from 'gpx';
|
||||||
import type { Readable } from 'svelte/store';
|
import type { Readable } from 'svelte/store';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
|
|
||||||
@@ -18,14 +18,14 @@
|
|||||||
orientation,
|
orientation,
|
||||||
panelSize,
|
panelSize,
|
||||||
}: {
|
}: {
|
||||||
gpxStatistics: Readable<GPXStatisticsGroup>;
|
gpxStatistics: Readable<GPXStatistics>;
|
||||||
slicedGPXStatistics: Readable<[GPXGlobalStatistics, number, number] | undefined>;
|
slicedGPXStatistics: Readable<[GPXStatistics, number, number] | undefined>;
|
||||||
orientation: 'horizontal' | 'vertical';
|
orientation: 'horizontal' | 'vertical';
|
||||||
panelSize: number;
|
panelSize: number;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let statistics = $derived(
|
let statistics = $derived(
|
||||||
$slicedGPXStatistics !== undefined ? $slicedGPXStatistics[0] : $gpxStatistics.global
|
$slicedGPXStatistics !== undefined ? $slicedGPXStatistics[0] : $gpxStatistics
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -42,15 +42,15 @@
|
|||||||
<Tooltip label={i18n._('quantities.distance')}>
|
<Tooltip label={i18n._('quantities.distance')}>
|
||||||
<span class="flex flex-row items-center">
|
<span class="flex flex-row items-center">
|
||||||
<Ruler size="16" class="mr-1" />
|
<Ruler size="16" class="mr-1" />
|
||||||
<WithUnits value={statistics.distance.total} type="distance" />
|
<WithUnits value={statistics.global.distance.total} type="distance" />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label={i18n._('quantities.elevation_gain_loss')}>
|
<Tooltip label={i18n._('quantities.elevation_gain_loss')}>
|
||||||
<span class="flex flex-row items-center">
|
<span class="flex flex-row items-center">
|
||||||
<MoveUpRight size="16" class="mr-1" />
|
<MoveUpRight size="16" class="mr-1" />
|
||||||
<WithUnits value={statistics.elevation.gain} type="elevation" />
|
<WithUnits value={statistics.global.elevation.gain} type="elevation" />
|
||||||
<MoveDownRight size="16" class="mx-1" />
|
<MoveDownRight size="16" class="mx-1" />
|
||||||
<WithUnits value={statistics.elevation.loss} type="elevation" />
|
<WithUnits value={statistics.global.elevation.loss} type="elevation" />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{#if panelSize > 120 || orientation === 'horizontal'}
|
{#if panelSize > 120 || orientation === 'horizontal'}
|
||||||
@@ -64,9 +64,13 @@
|
|||||||
>
|
>
|
||||||
<span class="flex flex-row items-center">
|
<span class="flex flex-row items-center">
|
||||||
<Zap size="16" class="mr-1" />
|
<Zap size="16" class="mr-1" />
|
||||||
<WithUnits value={statistics.speed.moving} type="speed" showUnits={false} />
|
<WithUnits
|
||||||
|
value={statistics.global.speed.moving}
|
||||||
|
type="speed"
|
||||||
|
showUnits={false}
|
||||||
|
/>
|
||||||
<span class="mx-1">/</span>
|
<span class="mx-1">/</span>
|
||||||
<WithUnits value={statistics.speed.total} type="speed" />
|
<WithUnits value={statistics.global.speed.total} type="speed" />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -79,9 +83,9 @@
|
|||||||
>
|
>
|
||||||
<span class="flex flex-row items-center">
|
<span class="flex flex-row items-center">
|
||||||
<Timer size="16" class="mr-1" />
|
<Timer size="16" class="mr-1" />
|
||||||
<WithUnits value={statistics.time.moving} type="time" />
|
<WithUnits value={statistics.global.time.moving} type="time" />
|
||||||
<span class="mx-1">/</span>
|
<span class="mx-1">/</span>
|
||||||
<WithUnits value={statistics.time.total} type="time" />
|
<WithUnits value={statistics.global.time.total} type="time" />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
...others
|
...others
|
||||||
}: {
|
}: {
|
||||||
iconOnly?: boolean;
|
iconOnly?: boolean;
|
||||||
company?: 'gpx.studio' | 'mapbox' | 'github' | 'crowdin' | 'facebook' | 'reddit';
|
company?: 'gpx.studio' | 'mapbox' | 'github' | 'crowdin' | 'facebook' | 'x' | 'reddit';
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
@@ -55,6 +55,16 @@
|
|||||||
d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"
|
d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"
|
||||||
/></svg
|
/></svg
|
||||||
>
|
>
|
||||||
|
{:else if company === 'x'}
|
||||||
|
<svg
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="fill-foreground {others.class ?? ''}"
|
||||||
|
><title>X</title><path
|
||||||
|
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
{:else if company === 'reddit'}
|
{:else if company === 'reddit'}
|
||||||
<svg
|
<svg
|
||||||
role="img"
|
role="img"
|
||||||
|
|||||||
@@ -538,7 +538,6 @@
|
|||||||
let targetInput =
|
let targetInput =
|
||||||
e &&
|
e &&
|
||||||
e.target &&
|
e.target &&
|
||||||
e.target instanceof HTMLElement &&
|
|
||||||
(e.target.tagName === 'INPUT' ||
|
(e.target.tagName === 'INPUT' ||
|
||||||
e.target.tagName === 'TEXTAREA' ||
|
e.target.tagName === 'TEXTAREA' ||
|
||||||
e.target.tagName === 'SELECT' ||
|
e.target.tagName === 'SELECT' ||
|
||||||
@@ -645,19 +644,6 @@
|
|||||||
} else if (e.key === 'F5') {
|
} else if (e.key === 'F5') {
|
||||||
$routing = !$routing;
|
$routing = !$routing;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if (
|
|
||||||
e.key === 'ArrowRight' ||
|
|
||||||
e.key === 'ArrowDown' ||
|
|
||||||
e.key === 'ArrowLeft' ||
|
|
||||||
e.key === 'ArrowUp'
|
|
||||||
) {
|
|
||||||
if (!targetInput) {
|
|
||||||
selection.updateFromKey(
|
|
||||||
e.key === 'ArrowRight' || e.key === 'ArrowDown',
|
|
||||||
e.shiftKey
|
|
||||||
);
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
on:dragover={(e) => e.preventDefault()}
|
on:dragover={(e) => e.preventDefault()}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
{i18n._('homepage.home')}
|
{i18n._('homepage.home')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
data-sveltekit-reload
|
|
||||||
variant="link"
|
variant="link"
|
||||||
class="text-base px-0 has-[>svg]:px-0"
|
class="text-base px-0 has-[>svg]:px-0"
|
||||||
href={getURLForLanguage(i18n.lang, '/app')}
|
href={getURLForLanguage(i18n.lang, '/app')}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
Construction,
|
Construction,
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import type { Readable, Writable } from 'svelte/store';
|
import type { Readable, Writable } from 'svelte/store';
|
||||||
import type { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
import type { GPXStatistics } from 'gpx';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
import { ElevationProfile } from '$lib/components/elevation-profile/elevation-profile';
|
import { ElevationProfile } from '$lib/components/elevation-profile/elevation-profile';
|
||||||
@@ -32,8 +32,8 @@
|
|||||||
elevationFill,
|
elevationFill,
|
||||||
showControls = true,
|
showControls = true,
|
||||||
}: {
|
}: {
|
||||||
gpxStatistics: Readable<GPXStatisticsGroup>;
|
gpxStatistics: Readable<GPXStatistics>;
|
||||||
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
|
slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
|
||||||
additionalDatasets: Writable<string[]>;
|
additionalDatasets: Writable<string[]>;
|
||||||
elevationFill: Writable<'slope' | 'surface' | 'highway' | undefined>;
|
elevationFill: Writable<'slope' | 'surface' | 'highway' | undefined>;
|
||||||
showControls?: boolean;
|
showControls?: boolean;
|
||||||
|
|||||||
@@ -14,16 +14,11 @@ import {
|
|||||||
getTemperatureWithUnits,
|
getTemperatureWithUnits,
|
||||||
getVelocityWithUnits,
|
getVelocityWithUnits,
|
||||||
} from '$lib/units';
|
} from '$lib/units';
|
||||||
import Chart, {
|
import Chart from 'chart.js/auto';
|
||||||
type ChartEvent,
|
|
||||||
type ChartOptions,
|
|
||||||
type ScriptableLineSegmentContext,
|
|
||||||
type TooltipItem,
|
|
||||||
} from 'chart.js/auto';
|
|
||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { get, type Readable, type Writable } from 'svelte/store';
|
import { get, type Readable, type Writable } from 'svelte/store';
|
||||||
import { map } from '$lib/components/map/map';
|
import { map } from '$lib/components/map/map';
|
||||||
import type { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
import type { GPXStatistics } from 'gpx';
|
||||||
import { mode } from 'mode-watcher';
|
import { mode } from 'mode-watcher';
|
||||||
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors';
|
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors';
|
||||||
|
|
||||||
@@ -32,20 +27,6 @@ const { distanceUnits, velocityUnits, temperatureUnits } = settings;
|
|||||||
Chart.defaults.font.family =
|
Chart.defaults.font.family =
|
||||||
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; // Tailwind CSS font
|
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; // Tailwind CSS font
|
||||||
|
|
||||||
interface ElevationProfilePoint {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
time?: Date;
|
|
||||||
slope: {
|
|
||||||
at: number;
|
|
||||||
segment: number;
|
|
||||||
length: number;
|
|
||||||
};
|
|
||||||
extensions: Record<string, any>;
|
|
||||||
coordinates: [number, number];
|
|
||||||
index: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ElevationProfile {
|
export class ElevationProfile {
|
||||||
private _chart: Chart | null = null;
|
private _chart: Chart | null = null;
|
||||||
private _canvas: HTMLCanvasElement;
|
private _canvas: HTMLCanvasElement;
|
||||||
@@ -54,14 +35,14 @@ export class ElevationProfile {
|
|||||||
private _dragging = false;
|
private _dragging = false;
|
||||||
private _panning = false;
|
private _panning = false;
|
||||||
|
|
||||||
private _gpxStatistics: Readable<GPXStatisticsGroup>;
|
private _gpxStatistics: Readable<GPXStatistics>;
|
||||||
private _slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>;
|
private _slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
|
||||||
private _additionalDatasets: Readable<string[]>;
|
private _additionalDatasets: Readable<string[]>;
|
||||||
private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>;
|
private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
gpxStatistics: Readable<GPXStatisticsGroup>,
|
gpxStatistics: Readable<GPXStatistics>,
|
||||||
slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined>,
|
slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>,
|
||||||
additionalDatasets: Readable<string[]>,
|
additionalDatasets: Readable<string[]>,
|
||||||
elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>,
|
elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>,
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
@@ -109,7 +90,7 @@ export class ElevationProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
let options: ChartOptions<'line'> = {
|
let options = {
|
||||||
animation: false,
|
animation: false,
|
||||||
parsing: false,
|
parsing: false,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
@@ -117,8 +98,8 @@ export class ElevationProfile {
|
|||||||
x: {
|
x: {
|
||||||
type: 'linear',
|
type: 'linear',
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: function (value: number | string) {
|
callback: function (value: number) {
|
||||||
return `${(value as number).toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
|
return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
|
||||||
},
|
},
|
||||||
align: 'inner',
|
align: 'inner',
|
||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
@@ -127,8 +108,8 @@ export class ElevationProfile {
|
|||||||
y: {
|
y: {
|
||||||
type: 'linear',
|
type: 'linear',
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: function (value: number | string) {
|
callback: function (value: number) {
|
||||||
return getElevationWithUnits(value as number, false);
|
return getElevationWithUnits(value, false);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -159,8 +140,8 @@ export class ElevationProfile {
|
|||||||
title: () => {
|
title: () => {
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
label: (context: TooltipItem<'line'>) => {
|
label: (context: Chart.TooltipContext) => {
|
||||||
let point = context.raw as ElevationProfilePoint;
|
let point = context.raw;
|
||||||
if (context.datasetIndex === 0) {
|
if (context.datasetIndex === 0) {
|
||||||
const map_ = get(map);
|
const map_ = get(map);
|
||||||
if (map_ && this._marker) {
|
if (map_ && this._marker) {
|
||||||
@@ -184,10 +165,10 @@ export class ElevationProfile {
|
|||||||
return `${i18n._('quantities.power')}: ${getPowerWithUnits(point.y)}`;
|
return `${i18n._('quantities.power')}: ${getPowerWithUnits(point.y)}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
afterBody: (contexts: TooltipItem<'line'>[]) => {
|
afterBody: (contexts: Chart.TooltipContext[]) => {
|
||||||
let context = contexts.filter((context) => context.datasetIndex === 0);
|
let context = contexts.filter((context) => context.datasetIndex === 0);
|
||||||
if (context.length === 0) return;
|
if (context.length === 0) return;
|
||||||
let point = context[0].raw as ElevationProfilePoint;
|
let point = context[0].raw;
|
||||||
let slope = {
|
let slope = {
|
||||||
at: point.slope.at.toFixed(1),
|
at: point.slope.at.toFixed(1),
|
||||||
segment: point.slope.segment.toFixed(1),
|
segment: point.slope.segment.toFixed(1),
|
||||||
@@ -246,7 +227,6 @@ export class ElevationProfile {
|
|||||||
onPanStart: () => {
|
onPanStart: () => {
|
||||||
this._panning = true;
|
this._panning = true;
|
||||||
this._slicedGPXStatistics.set(undefined);
|
this._slicedGPXStatistics.set(undefined);
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
onPanComplete: () => {
|
onPanComplete: () => {
|
||||||
this._panning = false;
|
this._panning = false;
|
||||||
@@ -258,13 +238,13 @@ export class ElevationProfile {
|
|||||||
},
|
},
|
||||||
mode: 'x',
|
mode: 'x',
|
||||||
onZoomStart: ({ chart, event }: { chart: Chart; event: any }) => {
|
onZoomStart: ({ chart, event }: { chart: Chart; event: any }) => {
|
||||||
if (!this._chart) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const maxZoom = this._chart.getInitialScaleBounds()?.x?.max ?? 0;
|
|
||||||
if (
|
if (
|
||||||
event.deltaY < 0 &&
|
event.deltaY < 0 &&
|
||||||
Math.abs(maxZoom / this._chart.getZoomLevel()) < 0.01
|
Math.abs(
|
||||||
|
this._chart.getInitialScaleBounds().x.max /
|
||||||
|
this._chart.options.plugins.zoom.limits.x.minRange -
|
||||||
|
this._chart.getZoomLevel()
|
||||||
|
) < 0.01
|
||||||
) {
|
) {
|
||||||
// Disable wheel pan if zoomed in to the max, and zooming in
|
// Disable wheel pan if zoomed in to the max, and zooming in
|
||||||
return false;
|
return false;
|
||||||
@@ -282,6 +262,7 @@ export class ElevationProfile {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
stacked: false,
|
||||||
onResize: () => {
|
onResize: () => {
|
||||||
this.updateOverlay();
|
this.updateOverlay();
|
||||||
},
|
},
|
||||||
@@ -289,7 +270,7 @@ export class ElevationProfile {
|
|||||||
|
|
||||||
let datasets: string[] = ['speed', 'hr', 'cad', 'atemp', 'power'];
|
let datasets: string[] = ['speed', 'hr', 'cad', 'atemp', 'power'];
|
||||||
datasets.forEach((id) => {
|
datasets.forEach((id) => {
|
||||||
options.scales![`y${id}`] = {
|
options.scales[`y${id}`] = {
|
||||||
type: 'linear',
|
type: 'linear',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
grid: {
|
grid: {
|
||||||
@@ -310,7 +291,7 @@ export class ElevationProfile {
|
|||||||
{
|
{
|
||||||
id: 'toggleMarker',
|
id: 'toggleMarker',
|
||||||
events: ['mouseout'],
|
events: ['mouseout'],
|
||||||
afterEvent: (chart: Chart, args: { event: ChartEvent }) => {
|
afterEvent: (chart: Chart, args: { event: Chart.ChartEvent }) => {
|
||||||
if (args.event.type === 'mouseout') {
|
if (args.event.type === 'mouseout') {
|
||||||
const map_ = get(map);
|
const map_ = get(map);
|
||||||
if (map_ && this._marker) {
|
if (map_ && this._marker) {
|
||||||
@@ -324,7 +305,7 @@ export class ElevationProfile {
|
|||||||
|
|
||||||
let startIndex = 0;
|
let startIndex = 0;
|
||||||
let endIndex = 0;
|
let endIndex = 0;
|
||||||
const getIndex = (evt: PointerEvent) => {
|
const getIndex = (evt) => {
|
||||||
if (!this._chart) {
|
if (!this._chart) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -342,22 +323,22 @@ export class ElevationProfile {
|
|||||||
if (evt.x - rect.left <= this._chart.chartArea.left) {
|
if (evt.x - rect.left <= this._chart.chartArea.left) {
|
||||||
return 0;
|
return 0;
|
||||||
} else if (evt.x - rect.left >= this._chart.chartArea.right) {
|
} else if (evt.x - rect.left >= this._chart.chartArea.right) {
|
||||||
return this._chart.data.datasets[0].data.length - 1;
|
return get(this._gpxStatistics).local.points.length - 1;
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const point = points.find((point) => (point.element as any).raw);
|
let point = points.find((point) => point.element.raw);
|
||||||
if (point) {
|
if (point) {
|
||||||
return (point.element as any).raw.index;
|
return point.element.raw.index;
|
||||||
} else {
|
} else {
|
||||||
return points[0].index;
|
return points[0].index;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let dragStarted = false;
|
let dragStarted = false;
|
||||||
const onMouseDown = (evt: PointerEvent) => {
|
const onMouseDown = (evt) => {
|
||||||
if (evt.shiftKey) {
|
if (evt.shiftKey) {
|
||||||
// Panning interaction
|
// Panning interaction
|
||||||
return;
|
return;
|
||||||
@@ -366,7 +347,7 @@ export class ElevationProfile {
|
|||||||
this._canvas.style.cursor = 'col-resize';
|
this._canvas.style.cursor = 'col-resize';
|
||||||
startIndex = getIndex(evt);
|
startIndex = getIndex(evt);
|
||||||
};
|
};
|
||||||
const onMouseMove = (evt: PointerEvent) => {
|
const onMouseMove = (evt) => {
|
||||||
if (dragStarted) {
|
if (dragStarted) {
|
||||||
this._dragging = true;
|
this._dragging = true;
|
||||||
endIndex = getIndex(evt);
|
endIndex = getIndex(evt);
|
||||||
@@ -375,7 +356,7 @@ export class ElevationProfile {
|
|||||||
startIndex = endIndex;
|
startIndex = endIndex;
|
||||||
} else if (startIndex !== endIndex) {
|
} else if (startIndex !== endIndex) {
|
||||||
this._slicedGPXStatistics.set([
|
this._slicedGPXStatistics.set([
|
||||||
get(this._gpxStatistics).sliced(
|
get(this._gpxStatistics).slice(
|
||||||
Math.min(startIndex, endIndex),
|
Math.min(startIndex, endIndex),
|
||||||
Math.max(startIndex, endIndex)
|
Math.max(startIndex, endIndex)
|
||||||
),
|
),
|
||||||
@@ -386,7 +367,7 @@ export class ElevationProfile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onMouseUp = (evt: PointerEvent) => {
|
const onMouseUp = (evt) => {
|
||||||
dragStarted = false;
|
dragStarted = false;
|
||||||
this._dragging = false;
|
this._dragging = false;
|
||||||
this._canvas.style.cursor = '';
|
this._canvas.style.cursor = '';
|
||||||
@@ -405,99 +386,85 @@ export class ElevationProfile {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = get(this._gpxStatistics);
|
const data = get(this._gpxStatistics);
|
||||||
const units = {
|
|
||||||
distance: get(distanceUnits),
|
|
||||||
velocity: get(velocityUnits),
|
|
||||||
temperature: get(temperatureUnits),
|
|
||||||
};
|
|
||||||
|
|
||||||
const datasets: Array<Array<any>> = [[], [], [], [], [], []];
|
|
||||||
data.forEachTrackPoint((trkpt, distance, speed, slope, index) => {
|
|
||||||
datasets[0].push({
|
|
||||||
x: getConvertedDistance(distance, units.distance),
|
|
||||||
y: trkpt.ele ? getConvertedElevation(trkpt.ele, units.distance) : 0,
|
|
||||||
time: trkpt.time,
|
|
||||||
slope: slope,
|
|
||||||
extensions: trkpt.getExtensions(),
|
|
||||||
coordinates: trkpt.getCoordinates(),
|
|
||||||
index: index,
|
|
||||||
});
|
|
||||||
if (data.global.time.total > 0) {
|
|
||||||
datasets[1].push({
|
|
||||||
x: getConvertedDistance(distance, units.distance),
|
|
||||||
y: getConvertedVelocity(speed, units.velocity, units.distance),
|
|
||||||
index: index,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (data.global.hr.count > 0) {
|
|
||||||
datasets[2].push({
|
|
||||||
x: getConvertedDistance(distance, units.distance),
|
|
||||||
y: trkpt.getHeartRate(),
|
|
||||||
index: index,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (data.global.cad.count > 0) {
|
|
||||||
datasets[3].push({
|
|
||||||
x: getConvertedDistance(distance, units.distance),
|
|
||||||
y: trkpt.getCadence(),
|
|
||||||
index: index,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (data.global.atemp.count > 0) {
|
|
||||||
datasets[4].push({
|
|
||||||
x: getConvertedDistance(distance, units.distance),
|
|
||||||
y: getConvertedTemperature(trkpt.getTemperature(), units.temperature),
|
|
||||||
index: index,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (data.global.power.count > 0) {
|
|
||||||
datasets[5].push({
|
|
||||||
x: getConvertedDistance(distance, units.distance),
|
|
||||||
y: trkpt.getPower(),
|
|
||||||
index: index,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this._chart.data.datasets[0] = {
|
this._chart.data.datasets[0] = {
|
||||||
label: i18n._('quantities.elevation'),
|
label: i18n._('quantities.elevation'),
|
||||||
data: datasets[0],
|
data: data.local.points.map((point, index) => {
|
||||||
|
return {
|
||||||
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
|
y: point.ele ? getConvertedElevation(point.ele) : 0,
|
||||||
|
time: point.time,
|
||||||
|
slope: {
|
||||||
|
at: data.local.slope.at[index],
|
||||||
|
segment: data.local.slope.segment[index],
|
||||||
|
length: data.local.slope.length[index],
|
||||||
|
},
|
||||||
|
extensions: point.getExtensions(),
|
||||||
|
coordinates: point.getCoordinates(),
|
||||||
|
index: index,
|
||||||
|
};
|
||||||
|
}),
|
||||||
normalized: true,
|
normalized: true,
|
||||||
fill: 'start',
|
fill: 'start',
|
||||||
order: 1,
|
order: 1,
|
||||||
segment: {},
|
segment: {},
|
||||||
};
|
};
|
||||||
this._chart.data.datasets[1] = {
|
this._chart.data.datasets[1] = {
|
||||||
data: datasets[1],
|
data: data.local.points.map((point, index) => {
|
||||||
|
return {
|
||||||
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
|
y: getConvertedVelocity(data.local.speed[index]),
|
||||||
|
index: index,
|
||||||
|
};
|
||||||
|
}),
|
||||||
normalized: true,
|
normalized: true,
|
||||||
yAxisID: 'yspeed',
|
yAxisID: 'yspeed',
|
||||||
};
|
};
|
||||||
this._chart.data.datasets[2] = {
|
this._chart.data.datasets[2] = {
|
||||||
data: datasets[2],
|
data: data.local.points.map((point, index) => {
|
||||||
|
return {
|
||||||
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
|
y: point.getHeartRate(),
|
||||||
|
index: index,
|
||||||
|
};
|
||||||
|
}),
|
||||||
normalized: true,
|
normalized: true,
|
||||||
yAxisID: 'yhr',
|
yAxisID: 'yhr',
|
||||||
};
|
};
|
||||||
this._chart.data.datasets[3] = {
|
this._chart.data.datasets[3] = {
|
||||||
data: datasets[3],
|
data: data.local.points.map((point, index) => {
|
||||||
|
return {
|
||||||
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
|
y: point.getCadence(),
|
||||||
|
index: index,
|
||||||
|
};
|
||||||
|
}),
|
||||||
normalized: true,
|
normalized: true,
|
||||||
yAxisID: 'ycad',
|
yAxisID: 'ycad',
|
||||||
};
|
};
|
||||||
this._chart.data.datasets[4] = {
|
this._chart.data.datasets[4] = {
|
||||||
data: datasets[4],
|
data: data.local.points.map((point, index) => {
|
||||||
|
return {
|
||||||
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
|
y: getConvertedTemperature(point.getTemperature()),
|
||||||
|
index: index,
|
||||||
|
};
|
||||||
|
}),
|
||||||
normalized: true,
|
normalized: true,
|
||||||
yAxisID: 'yatemp',
|
yAxisID: 'yatemp',
|
||||||
};
|
};
|
||||||
this._chart.data.datasets[5] = {
|
this._chart.data.datasets[5] = {
|
||||||
data: datasets[5],
|
data: data.local.points.map((point, index) => {
|
||||||
|
return {
|
||||||
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
|
y: point.getPower(),
|
||||||
|
index: index,
|
||||||
|
};
|
||||||
|
}),
|
||||||
normalized: true,
|
normalized: true,
|
||||||
yAxisID: 'ypower',
|
yAxisID: 'ypower',
|
||||||
};
|
};
|
||||||
|
this._chart.options.scales.x['min'] = 0;
|
||||||
this._chart.options.scales!.x!['min'] = 0;
|
this._chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total);
|
||||||
this._chart.options.scales!.x!['max'] = getConvertedDistance(
|
|
||||||
data.global.distance.total,
|
|
||||||
units.distance
|
|
||||||
);
|
|
||||||
|
|
||||||
this.setVisibility();
|
this.setVisibility();
|
||||||
this.setFill();
|
this.setFill();
|
||||||
@@ -546,24 +513,21 @@ export class ElevationProfile {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const elevationFill = get(this._elevationFill);
|
const elevationFill = get(this._elevationFill);
|
||||||
const dataset = this._chart.data.datasets[0];
|
|
||||||
let segment: any = {};
|
|
||||||
if (elevationFill === 'slope') {
|
if (elevationFill === 'slope') {
|
||||||
segment = {
|
this._chart.data.datasets[0]['segment'] = {
|
||||||
backgroundColor: this.slopeFillCallback,
|
backgroundColor: this.slopeFillCallback,
|
||||||
};
|
};
|
||||||
} else if (elevationFill === 'surface') {
|
} else if (elevationFill === 'surface') {
|
||||||
segment = {
|
this._chart.data.datasets[0]['segment'] = {
|
||||||
backgroundColor: this.surfaceFillCallback,
|
backgroundColor: this.surfaceFillCallback,
|
||||||
};
|
};
|
||||||
} else if (elevationFill === 'highway') {
|
} else if (elevationFill === 'highway') {
|
||||||
segment = {
|
this._chart.data.datasets[0]['segment'] = {
|
||||||
backgroundColor: this.highwayFillCallback,
|
backgroundColor: this.highwayFillCallback,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
segment = {};
|
this._chart.data.datasets[0]['segment'] = {};
|
||||||
}
|
}
|
||||||
Object.assign(dataset, { segment });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOverlay() {
|
updateOverlay() {
|
||||||
@@ -590,12 +554,10 @@ export class ElevationProfile {
|
|||||||
|
|
||||||
const gpxStatistics = get(this._gpxStatistics);
|
const gpxStatistics = get(this._gpxStatistics);
|
||||||
let startPixel = this._chart.scales.x.getPixelForValue(
|
let startPixel = this._chart.scales.x.getPixelForValue(
|
||||||
getConvertedDistance(
|
getConvertedDistance(gpxStatistics.local.distance.total[startIndex])
|
||||||
gpxStatistics.getTrackPoint(startIndex)?.distance.total ?? 0
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
let endPixel = this._chart.scales.x.getPixelForValue(
|
let endPixel = this._chart.scales.x.getPixelForValue(
|
||||||
getConvertedDistance(gpxStatistics.getTrackPoint(endIndex)?.distance.total ?? 0)
|
getConvertedDistance(gpxStatistics.local.distance.total[endIndex])
|
||||||
);
|
);
|
||||||
|
|
||||||
selectionContext.fillRect(
|
selectionContext.fillRect(
|
||||||
@@ -613,22 +575,19 @@ export class ElevationProfile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slopeFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
slopeFillCallback(context) {
|
||||||
const point = context.p0.raw as ElevationProfilePoint;
|
return getSlopeColor(context.p0.raw.slope.segment);
|
||||||
return getSlopeColor(point.slope.segment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
surfaceFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
surfaceFillCallback(context) {
|
||||||
const point = context.p0.raw as ElevationProfilePoint;
|
return getSurfaceColor(context.p0.raw.extensions.surface);
|
||||||
return getSurfaceColor(point.extensions.surface);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
highwayFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
highwayFillCallback(context) {
|
||||||
const point = context.p0.raw as ElevationProfilePoint;
|
|
||||||
return getHighwayColor(
|
return getHighwayColor(
|
||||||
point.extensions.highway,
|
context.p0.raw.extensions.highway,
|
||||||
point.extensions.sac_scale,
|
context.p0.raw.extensions.sac_scale,
|
||||||
point.extensions.mtb_scale
|
context.p0.raw.extensions.mtb_scale
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
import { loadFile } from '$lib/logic/file-actions';
|
import { loadFile } from '$lib/logic/file-actions';
|
||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
import { isSelected, toggle } from '$lib/components/map/layer-control/utils';
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
useHash = true,
|
useHash = true,
|
||||||
@@ -33,7 +32,6 @@
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
currentBasemap,
|
currentBasemap,
|
||||||
selectedBasemapTree,
|
|
||||||
distanceUnits,
|
distanceUnits,
|
||||||
velocityUnits,
|
velocityUnits,
|
||||||
temperatureUnits,
|
temperatureUnits,
|
||||||
@@ -68,9 +66,6 @@
|
|||||||
if (allowedEmbeddingBasemaps.includes(options.basemap)) {
|
if (allowedEmbeddingBasemaps.includes(options.basemap)) {
|
||||||
$currentBasemap = options.basemap;
|
$currentBasemap = options.basemap;
|
||||||
}
|
}
|
||||||
if (!isSelected($selectedBasemapTree, options.basemap)) {
|
|
||||||
$selectedBasemapTree = toggle($selectedBasemapTree, options.basemap);
|
|
||||||
}
|
|
||||||
$distanceMarkers = options.distanceMarkers;
|
$distanceMarkers = options.distanceMarkers;
|
||||||
$directionMarkers = options.directionMarkers;
|
$directionMarkers = options.directionMarkers;
|
||||||
$distanceUnits = options.distanceUnits;
|
$distanceUnits = options.distanceUnits;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
SquareActivity,
|
SquareActivity,
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
import { GPXGlobalStatistics } from 'gpx';
|
import { GPXStatistics } from 'gpx';
|
||||||
import { ListRootItem } from '$lib/components/file-list/file-list';
|
import { ListRootItem } from '$lib/components/file-list/file-list';
|
||||||
import { fileStateCollection } from '$lib/logic/file-state';
|
import { fileStateCollection } from '$lib/logic/file-state';
|
||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
@@ -48,24 +48,24 @@
|
|||||||
extensions: false,
|
extensions: false,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
let statistics = $gpxStatistics.global;
|
let statistics = $gpxStatistics;
|
||||||
if (exportState.current === ExportState.ALL) {
|
if (exportState.current === ExportState.ALL) {
|
||||||
statistics = Array.from(get(fileStateCollection).values())
|
statistics = Array.from(get(fileStateCollection).values())
|
||||||
.map((file) => file.statistics)
|
.map((file) => file.statistics)
|
||||||
.reduce((acc, cur) => {
|
.reduce((acc, cur) => {
|
||||||
if (cur !== undefined) {
|
if (cur !== undefined) {
|
||||||
acc.mergeWith(cur.getStatisticsFor(new ListRootItem()).global);
|
acc.mergeWith(cur.getStatisticsFor(new ListRootItem()));
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, new GPXGlobalStatistics());
|
}, new GPXStatistics());
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
time: statistics.time.total === 0,
|
time: statistics.global.time.total === 0,
|
||||||
hr: statistics.hr.count === 0,
|
hr: statistics.global.hr.count === 0,
|
||||||
cad: statistics.cad.count === 0,
|
cad: statistics.global.cad.count === 0,
|
||||||
atemp: statistics.atemp.count === 0,
|
atemp: statistics.global.atemp.count === 0,
|
||||||
power: statistics.power.count === 0,
|
power: statistics.global.power.count === 0,
|
||||||
extensions: Object.keys(statistics.extensions).length === 0,
|
extensions: Object.keys(statistics.global.extensions).length === 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -121,16 +121,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vertical :global(button) {
|
.vertical :global(button) {
|
||||||
@apply hover:bg-[var(--selection)];
|
@apply hover:bg-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical :global(.sortable-selected button) {
|
||||||
|
@apply hover:bg-accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical :global(.sortable-selected) {
|
.vertical :global(.sortable-selected) {
|
||||||
@apply bg-[var(--selection)];
|
@apply bg-accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.horizontal :global(button) {
|
.horizontal :global(button) {
|
||||||
@apply bg-[var(--selection)];
|
@apply bg-accent;
|
||||||
@apply hover:bg-background;
|
@apply hover:bg-muted;
|
||||||
}
|
}
|
||||||
|
|
||||||
.horizontal :global(.sortable-selected button) {
|
.horizontal :global(.sortable-selected button) {
|
||||||
|
|||||||
@@ -34,10 +34,11 @@
|
|||||||
import { editStyle } from '$lib/components/file-list/style/utils.svelte';
|
import { editStyle } from '$lib/components/file-list/style/utils.svelte';
|
||||||
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
||||||
import { selection, copied, cut } from '$lib/logic/selection';
|
import { selection, copied, cut } from '$lib/logic/selection';
|
||||||
|
import { map } from '$lib/components/map/map';
|
||||||
import { fileActions, pasteSelection } from '$lib/logic/file-actions';
|
import { fileActions, pasteSelection } from '$lib/logic/file-actions';
|
||||||
import { allHidden } from '$lib/logic/hidden';
|
import { allHidden } from '$lib/logic/hidden';
|
||||||
import { boundsManager } from '$lib/logic/bounds';
|
import { boundsManager } from '$lib/logic/bounds';
|
||||||
import { gpxColors, gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
|
import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
|
||||||
import { fileStateCollection } from '$lib/logic/file-state';
|
import { fileStateCollection } from '$lib/logic/file-state';
|
||||||
import { waypointPopup } from '$lib/components/map/gpx-layer/gpx-layer-popup';
|
import { waypointPopup } from '$lib/components/map/gpx-layer/gpx-layer-popup';
|
||||||
import { allowedPastes } from './sortable-file-list';
|
import { allowedPastes } from './sortable-file-list';
|
||||||
@@ -57,31 +58,41 @@
|
|||||||
|
|
||||||
let singleSelection = $derived($selection.size === 1);
|
let singleSelection = $derived($selection.size === 1);
|
||||||
|
|
||||||
let nodeColors: string[] = $derived.by(() => {
|
let nodeColors: string[] = $state([]);
|
||||||
|
|
||||||
|
$effect.pre(() => {
|
||||||
let colors: string[] = [];
|
let colors: string[] = [];
|
||||||
if (node) {
|
if (node && $map) {
|
||||||
if (node instanceof GPXFile) {
|
if (node instanceof GPXFile) {
|
||||||
let defaultColor = $gpxColors.get(item.getFileId());
|
let defaultColor = undefined;
|
||||||
|
|
||||||
|
let layer = gpxLayers.getLayer(item.getFileId());
|
||||||
|
if (layer) {
|
||||||
|
defaultColor = layer.layerColor;
|
||||||
|
}
|
||||||
|
|
||||||
let style = node.getStyle(defaultColor);
|
let style = node.getStyle(defaultColor);
|
||||||
colors = style.color;
|
style.color.forEach((c) => {
|
||||||
|
if (!colors.includes(c)) {
|
||||||
|
colors.push(c);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else if (node instanceof Track) {
|
} else if (node instanceof Track) {
|
||||||
let style = node.getStyle();
|
let style = node.getStyle();
|
||||||
if (
|
if (style) {
|
||||||
style &&
|
if (style['gpx_style:color'] && !colors.includes(style['gpx_style:color'])) {
|
||||||
style['gpx_style:color'] &&
|
colors.push(style['gpx_style:color']);
|
||||||
!colors.includes(style['gpx_style:color'])
|
}
|
||||||
) {
|
|
||||||
colors.push(style['gpx_style:color']);
|
|
||||||
}
|
}
|
||||||
if (colors.length === 0) {
|
if (colors.length === 0) {
|
||||||
let defaultColor = $gpxColors.get(item.getFileId());
|
let layer = gpxLayers.getLayer(item.getFileId());
|
||||||
if (defaultColor) {
|
if (layer) {
|
||||||
colors.push(defaultColor);
|
colors.push(layer.layerColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return colors;
|
nodeColors = colors;
|
||||||
});
|
});
|
||||||
|
|
||||||
let symbolKey = $derived(node instanceof Waypoint ? getSymbolKey(node.sym) : undefined);
|
let symbolKey = $derived(node instanceof Waypoint ? getSymbolKey(node.sym) : undefined);
|
||||||
@@ -164,7 +175,7 @@
|
|||||||
let file = fileStateCollection.getFile(item.getFileId());
|
let file = fileStateCollection.getFile(item.getFileId());
|
||||||
if (layer && file) {
|
if (layer && file) {
|
||||||
let waypoint = file.wpt[item.getWaypointIndex()];
|
let waypoint = file.wpt[item.getWaypointIndex()];
|
||||||
if (waypoint && !waypoint._data.hidden) {
|
if (waypoint) {
|
||||||
waypointPopup?.setItem({
|
waypointPopup?.setItem({
|
||||||
item: waypoint,
|
item: waypoint,
|
||||||
fileId: item.getFileId(),
|
fileId: item.getFileId(),
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
language = 'en';
|
language = 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
map.init(language, hash, geocoder, geolocate);
|
map.init(PUBLIC_MAPBOX_TOKEN, language, hash, geocoder, geolocate);
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
|
|||||||
@@ -16,8 +16,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
class="p-1 has-[>svg]:px-2 h-8 justify-start {className}"
|
||||||
class="justify-start {className}"
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
|
import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
|
||||||
import { DistanceMarkers } from '$lib/components/map/gpx-layer/distance-markers';
|
import { DistanceMarkers } from '$lib/components/map/gpx-layer/distance-markers';
|
||||||
import { StartEndMarkers } from '$lib/components/map/gpx-layer/start-end-markers';
|
import { StartEndMarkers } from '$lib/components/map/gpx-layer/start-end-markers';
|
||||||
@@ -9,10 +9,13 @@
|
|||||||
let distanceMarkers: DistanceMarkers;
|
let distanceMarkers: DistanceMarkers;
|
||||||
let startEndMarkers: StartEndMarkers;
|
let startEndMarkers: StartEndMarkers;
|
||||||
|
|
||||||
map.onLoad((map_) => {
|
onMount(() => {
|
||||||
gpxLayers.init();
|
gpxLayers.init();
|
||||||
startEndMarkers = new StartEndMarkers();
|
startEndMarkers = new StartEndMarkers();
|
||||||
distanceMarkers = new DistanceMarkers();
|
distanceMarkers = new DistanceMarkers();
|
||||||
|
});
|
||||||
|
|
||||||
|
map.onLoad((map_) => {
|
||||||
createPopups(map_);
|
createPopups(map_);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { TrackPoint } from 'gpx';
|
import type { TrackPoint } from 'gpx';
|
||||||
import { Button } from '$lib/components/ui/button';
|
|
||||||
import CopyCoordinates from '$lib/components/map/gpx-layer/CopyCoordinates.svelte';
|
import CopyCoordinates from '$lib/components/map/gpx-layer/CopyCoordinates.svelte';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import WithUnits from '$lib/components/WithUnits.svelte';
|
import WithUnits from '$lib/components/WithUnits.svelte';
|
||||||
import { Compass, Earth, Mountain, Timer } from '@lucide/svelte';
|
import { Compass, Mountain, Timer } from '@lucide/svelte';
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
import type { PopupItem } from '$lib/components/map/map-popup';
|
import type { PopupItem } from '$lib/components/map/map-popup';
|
||||||
import { map } from '$lib/components/map/map';
|
|
||||||
|
|
||||||
let { trackpoint }: { trackpoint: PopupItem<TrackPoint> } = $props();
|
let { trackpoint }: { trackpoint: PopupItem<TrackPoint> } = $props();
|
||||||
</script>
|
</script>
|
||||||
@@ -37,17 +35,5 @@
|
|||||||
onCopy={() => trackpoint.hide?.()}
|
onCopy={() => trackpoint.hide?.()}
|
||||||
class="mt-0.5"
|
class="mt-0.5"
|
||||||
/>
|
/>
|
||||||
{#if trackpoint.fileId === undefined}
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
class="justify-start"
|
|
||||||
href={`https://www.openstreetmap.org/edit?#map=${(($map?.getZoom() ?? 17) + 1).toFixed(0)}/${trackpoint.item.getLatitude().toFixed(5)}/${trackpoint.item.getLongitude().toFixed(5)}`}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<Earth size="14" />
|
|
||||||
{i18n._('menu.edit_osm')}
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|||||||
@@ -13,8 +13,6 @@
|
|||||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||||
import { fileActions } from '$lib/logic/file-actions';
|
import { fileActions } from '$lib/logic/file-actions';
|
||||||
import type { PopupItem } from '$lib/components/map/map-popup';
|
import type { PopupItem } from '$lib/components/map/map-popup';
|
||||||
import { selection } from '$lib/logic/selection';
|
|
||||||
import { ListFileItem } from '$lib/components/file-list/file-list';
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
waypoint,
|
waypoint,
|
||||||
@@ -22,9 +20,6 @@
|
|||||||
waypoint: PopupItem<Waypoint>;
|
waypoint: PopupItem<Waypoint>;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let selected = $derived(
|
|
||||||
waypoint.fileId ? $selection.hasAnyChildren(new ListFileItem(waypoint.fileId)) : false
|
|
||||||
);
|
|
||||||
let symbolKey = $derived(waypoint ? getSymbolKey(waypoint.item.sym) : undefined);
|
let symbolKey = $derived(waypoint ? getSymbolKey(waypoint.item.sym) : undefined);
|
||||||
|
|
||||||
function sanitize(text: string | undefined): string {
|
function sanitize(text: string | undefined): string {
|
||||||
@@ -86,7 +81,7 @@
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<div class="mt-2 flex flex-col gap-1">
|
<div class="mt-2 flex flex-col gap-1">
|
||||||
<CopyCoordinates coordinates={waypoint.item.attributes} />
|
<CopyCoordinates coordinates={waypoint.item.attributes} />
|
||||||
{#if $currentTool === Tool.WAYPOINT && selected}
|
{#if $currentTool === Tool.WAYPOINT}
|
||||||
<Button
|
<Button
|
||||||
class="p-1 has-[>svg]:px-2 h-8"
|
class="p-1 has-[>svg]:px-2 h-8"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -3,12 +3,19 @@ import { gpxStatistics } from '$lib/logic/statistics';
|
|||||||
import { getConvertedDistanceToKilometers } from '$lib/units';
|
import { getConvertedDistanceToKilometers } from '$lib/units';
|
||||||
import type { GeoJSONSource } from 'mapbox-gl';
|
import type { GeoJSONSource } from 'mapbox-gl';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { ANCHOR_LAYER_KEY, map } from '$lib/components/map/map';
|
import { map } from '$lib/components/map/map';
|
||||||
import { allHidden } from '$lib/logic/hidden';
|
import { allHidden } from '$lib/logic/hidden';
|
||||||
|
|
||||||
const { distanceMarkers, distanceUnits } = settings;
|
const { distanceMarkers, distanceUnits } = settings;
|
||||||
|
|
||||||
const levels = [100, 50, 25, 10, 5, 1];
|
const stops = [
|
||||||
|
[100, 0],
|
||||||
|
[50, 7],
|
||||||
|
[25, 8, 10],
|
||||||
|
[10, 10],
|
||||||
|
[5, 11],
|
||||||
|
[1, 13],
|
||||||
|
];
|
||||||
|
|
||||||
export class DistanceMarkers {
|
export class DistanceMarkers {
|
||||||
updateBinded: () => void = this.update.bind(this);
|
updateBinded: () => void = this.update.bind(this);
|
||||||
@@ -43,33 +50,22 @@ export class DistanceMarkers {
|
|||||||
data: this.getDistanceMarkersGeoJSON(),
|
data: this.getDistanceMarkersGeoJSON(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!map_.getLayer('distance-markers')) {
|
stops.forEach(([d, minzoom, maxzoom]) => {
|
||||||
map_.addLayer(
|
if (!map_.getLayer(`distance-markers-${d}`)) {
|
||||||
{
|
map_.addLayer({
|
||||||
id: 'distance-markers',
|
id: `distance-markers-${d}`,
|
||||||
type: 'symbol',
|
type: 'symbol',
|
||||||
source: 'distance-markers',
|
source: 'distance-markers',
|
||||||
filter: [
|
filter:
|
||||||
'match',
|
d === 5
|
||||||
['get', 'level'],
|
? [
|
||||||
100,
|
'any',
|
||||||
['>=', ['zoom'], 0],
|
['==', ['get', 'level'], 5],
|
||||||
50,
|
['==', ['get', 'level'], 25],
|
||||||
['>=', ['zoom'], 7],
|
]
|
||||||
25,
|
: ['==', ['get', 'level'], d],
|
||||||
[
|
minzoom: minzoom,
|
||||||
'any',
|
maxzoom: maxzoom ?? 24,
|
||||||
['all', ['>=', ['zoom'], 8], ['<=', ['zoom'], 9]],
|
|
||||||
['>=', ['zoom'], 11],
|
|
||||||
],
|
|
||||||
10,
|
|
||||||
['>=', ['zoom'], 10],
|
|
||||||
5,
|
|
||||||
['>=', ['zoom'], 11],
|
|
||||||
1,
|
|
||||||
['>=', ['zoom'], 13],
|
|
||||||
false,
|
|
||||||
],
|
|
||||||
layout: {
|
layout: {
|
||||||
'text-field': ['get', 'distance'],
|
'text-field': ['get', 'distance'],
|
||||||
'text-size': 14,
|
'text-size': 14,
|
||||||
@@ -80,14 +76,17 @@ export class DistanceMarkers {
|
|||||||
'text-halo-width': 2,
|
'text-halo-width': 2,
|
||||||
'text-halo-color': 'white',
|
'text-halo-color': 'white',
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
ANCHOR_LAYER_KEY.distanceMarkers
|
} else {
|
||||||
);
|
map_.moveLayer(`distance-markers-${d}`);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
if (map_.getLayer('distance-markers')) {
|
stops.forEach(([d]) => {
|
||||||
map_.removeLayer('distance-markers');
|
if (map_.getLayer(`distance-markers-${d}`)) {
|
||||||
}
|
map_.removeLayer(`distance-markers-${d}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// No reliable way to check if the map is ready to add sources and layers
|
// No reliable way to check if the map is ready to add sources and layers
|
||||||
@@ -102,26 +101,35 @@ export class DistanceMarkers {
|
|||||||
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
|
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
|
||||||
let statistics = get(gpxStatistics);
|
let statistics = get(gpxStatistics);
|
||||||
|
|
||||||
let features: GeoJSON.Feature[] = [];
|
let features = [];
|
||||||
let currentTargetDistance = 1;
|
let currentTargetDistance = 1;
|
||||||
statistics.forEachTrackPoint((trkpt, dist) => {
|
for (let i = 0; i < statistics.local.distance.total.length; i++) {
|
||||||
if (dist >= getConvertedDistanceToKilometers(currentTargetDistance)) {
|
if (
|
||||||
|
statistics.local.distance.total[i] >=
|
||||||
|
getConvertedDistanceToKilometers(currentTargetDistance)
|
||||||
|
) {
|
||||||
let distance = currentTargetDistance.toFixed(0);
|
let distance = currentTargetDistance.toFixed(0);
|
||||||
let level = levels.find((level) => currentTargetDistance % level === 0) || 1;
|
let [level, minzoom] = stops.find(([d]) => currentTargetDistance % d === 0) ?? [
|
||||||
|
0, 0,
|
||||||
|
];
|
||||||
features.push({
|
features.push({
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
geometry: {
|
geometry: {
|
||||||
type: 'Point',
|
type: 'Point',
|
||||||
coordinates: [trkpt.getLongitude(), trkpt.getLatitude()],
|
coordinates: [
|
||||||
|
statistics.local.points[i].getLongitude(),
|
||||||
|
statistics.local.points[i].getLatitude(),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
distance,
|
distance,
|
||||||
level,
|
level,
|
||||||
|
minzoom,
|
||||||
},
|
},
|
||||||
} as GeoJSON.Feature);
|
} as GeoJSON.Feature);
|
||||||
currentTargetDistance += 1;
|
currentTargetDistance += 1;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { get, type Readable } from 'svelte/store';
|
import { get, type Readable } from 'svelte/store';
|
||||||
import mapboxgl, { type FilterSpecification } from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { ANCHOR_LAYER_KEY, map } from '$lib/components/map/map';
|
import { map } from '$lib/components/map/map';
|
||||||
import { waypointPopup, trackpointPopup } from './gpx-layer-popup';
|
import { waypointPopup, trackpointPopup } from './gpx-layer-popup';
|
||||||
import {
|
import {
|
||||||
ListTrackSegmentItem,
|
ListTrackSegmentItem,
|
||||||
@@ -22,7 +22,6 @@ import { fileActionManager } from '$lib/logic/file-action-manager';
|
|||||||
import { fileActions } from '$lib/logic/file-actions';
|
import { fileActions } from '$lib/logic/file-actions';
|
||||||
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
|
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
|
||||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||||
import { gpxColors } from '$lib/components/map/gpx-layer/gpx-layers';
|
|
||||||
|
|
||||||
const colors = [
|
const colors = [
|
||||||
'#ff0000',
|
'#ff0000',
|
||||||
@@ -44,49 +43,26 @@ for (let color of colors) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the color with the least amount of uses
|
// Get the color with the least amount of uses
|
||||||
function getColor(fileId: string) {
|
function getColor() {
|
||||||
let color = colors.reduce((a, b) => (colorCount[a] <= colorCount[b] ? a : b));
|
let color = colors.reduce((a, b) => (colorCount[a] <= colorCount[b] ? a : b));
|
||||||
colorCount[color]++;
|
colorCount[color]++;
|
||||||
gpxColors.update((colors) => {
|
|
||||||
colors.set(fileId, color);
|
|
||||||
return colors;
|
|
||||||
});
|
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceColor(fileId: string, oldColor: string, newColor: string) {
|
function decrementColor(color: string) {
|
||||||
if (colorCount.hasOwnProperty(oldColor)) {
|
|
||||||
colorCount[oldColor]--;
|
|
||||||
}
|
|
||||||
colorCount[newColor]++;
|
|
||||||
gpxColors.update((colors) => {
|
|
||||||
colors.set(fileId, newColor);
|
|
||||||
return colors;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeColor(fileId: string, color: string) {
|
|
||||||
if (colorCount.hasOwnProperty(color)) {
|
if (colorCount.hasOwnProperty(color)) {
|
||||||
colorCount[color]--;
|
colorCount[color]--;
|
||||||
}
|
}
|
||||||
gpxColors.update((colors) => {
|
|
||||||
colors.delete(fileId);
|
|
||||||
return colors;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSvgForSymbol(symbol?: string | undefined, layerColor?: string | undefined) {
|
function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
|
||||||
let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined;
|
let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined;
|
||||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
${
|
${Square.replace('width="24"', 'width="12"')
|
||||||
layerColor
|
.replace('height="24"', 'height="12"')
|
||||||
? Square.replace('width="24"', 'width="12"')
|
.replace('stroke="currentColor"', 'stroke="SteelBlue"')
|
||||||
.replace('height="24"', 'height="12"')
|
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
|
||||||
.replace('stroke="currentColor"', 'stroke="SteelBlue"')
|
.replace('fill="none"', `fill="${layerColor}"`)}
|
||||||
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
|
|
||||||
.replace('fill="none"', `fill="${layerColor}"`)
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
${MapPin.replace('width="24"', '')
|
${MapPin.replace('width="24"', '')
|
||||||
.replace('height="24"', '')
|
.replace('height="24"', '')
|
||||||
.replace('stroke="currentColor"', '')
|
.replace('stroke="currentColor"', '')
|
||||||
@@ -111,10 +87,9 @@ export class GPXLayer {
|
|||||||
fileId: string;
|
fileId: string;
|
||||||
file: Readable<GPXFileWithStatistics | undefined>;
|
file: Readable<GPXFileWithStatistics | undefined>;
|
||||||
layerColor: string;
|
layerColor: string;
|
||||||
|
markers: mapboxgl.Marker[] = [];
|
||||||
selected: boolean = false;
|
selected: boolean = false;
|
||||||
currentWaypointData: GeoJSON.FeatureCollection | null = null;
|
draggable: boolean;
|
||||||
draggedWaypointIndex: number | null = null;
|
|
||||||
draggingStartingPosition: mapboxgl.Point = new mapboxgl.Point(0, 0);
|
|
||||||
unsubscribe: Function[] = [];
|
unsubscribe: Function[] = [];
|
||||||
|
|
||||||
updateBinded: () => void = this.update.bind(this);
|
updateBinded: () => void = this.update.bind(this);
|
||||||
@@ -123,25 +98,11 @@ export class GPXLayer {
|
|||||||
layerOnMouseMoveBinded: (e: any) => void = this.layerOnMouseMove.bind(this);
|
layerOnMouseMoveBinded: (e: any) => void = this.layerOnMouseMove.bind(this);
|
||||||
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
|
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
|
||||||
layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this);
|
layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this);
|
||||||
waypointLayerOnMouseEnterBinded: (e: mapboxgl.MapMouseEvent) => void =
|
|
||||||
this.waypointLayerOnMouseEnter.bind(this);
|
|
||||||
waypointLayerOnMouseLeaveBinded: (e: mapboxgl.MapMouseEvent) => void =
|
|
||||||
this.waypointLayerOnMouseLeave.bind(this);
|
|
||||||
waypointLayerOnClickBinded: (e: mapboxgl.MapMouseEvent) => void =
|
|
||||||
this.waypointLayerOnClick.bind(this);
|
|
||||||
waypointLayerOnMouseDownBinded: (e: mapboxgl.MapMouseEvent) => void =
|
|
||||||
this.waypointLayerOnMouseDown.bind(this);
|
|
||||||
waypointLayerOnTouchStartBinded: (e: mapboxgl.MapTouchEvent) => void =
|
|
||||||
this.waypointLayerOnTouchStart.bind(this);
|
|
||||||
waypointLayerOnMouseMoveBinded: (e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) => void =
|
|
||||||
this.waypointLayerOnMouseMove.bind(this);
|
|
||||||
waypointLayerOnMouseUpBinded: (e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) => void =
|
|
||||||
this.waypointLayerOnMouseUp.bind(this);
|
|
||||||
|
|
||||||
constructor(fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
|
constructor(fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
|
||||||
this.fileId = fileId;
|
this.fileId = fileId;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.layerColor = getColor(fileId);
|
this.layerColor = getColor();
|
||||||
this.unsubscribe.push(
|
this.unsubscribe.push(
|
||||||
map.subscribe(($map) => {
|
map.subscribe(($map) => {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
@@ -164,6 +125,18 @@ export class GPXLayer {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded));
|
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded));
|
||||||
|
this.unsubscribe.push(
|
||||||
|
currentTool.subscribe((tool) => {
|
||||||
|
if (tool === Tool.WAYPOINT && !this.draggable) {
|
||||||
|
this.draggable = true;
|
||||||
|
this.markers.forEach((marker) => marker.setDraggable(true));
|
||||||
|
} else if (tool !== Tool.WAYPOINT && this.draggable) {
|
||||||
|
this.draggable = false;
|
||||||
|
this.markers.forEach((marker) => marker.setDraggable(false));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.draggable = get(currentTool) === Tool.WAYPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
@@ -178,12 +151,10 @@ export class GPXLayer {
|
|||||||
file._data.style.color &&
|
file._data.style.color &&
|
||||||
this.layerColor !== `#${file._data.style.color}`
|
this.layerColor !== `#${file._data.style.color}`
|
||||||
) {
|
) {
|
||||||
replaceColor(this.fileId, this.layerColor, `#${file._data.style.color}`);
|
decrementColor(this.layerColor);
|
||||||
this.layerColor = `#${file._data.style.color}`;
|
this.layerColor = `#${file._data.style.color}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadIcons();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let source = _map.getSource(this.fileId) as mapboxgl.GeoJSONSource | undefined;
|
let source = _map.getSource(this.fileId) as mapboxgl.GeoJSONSource | undefined;
|
||||||
if (source) {
|
if (source) {
|
||||||
@@ -196,23 +167,20 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!_map.getLayer(this.fileId)) {
|
if (!_map.getLayer(this.fileId)) {
|
||||||
_map.addLayer(
|
_map.addLayer({
|
||||||
{
|
id: this.fileId,
|
||||||
id: this.fileId,
|
type: 'line',
|
||||||
type: 'line',
|
source: this.fileId,
|
||||||
source: this.fileId,
|
layout: {
|
||||||
layout: {
|
'line-join': 'round',
|
||||||
'line-join': 'round',
|
'line-cap': 'round',
|
||||||
'line-cap': 'round',
|
|
||||||
},
|
|
||||||
paint: {
|
|
||||||
'line-color': ['get', 'color'],
|
|
||||||
'line-width': ['get', 'width'],
|
|
||||||
'line-opacity': ['get', 'opacity'],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
ANCHOR_LAYER_KEY.tracks
|
paint: {
|
||||||
);
|
'line-color': ['get', 'color'],
|
||||||
|
'line-width': ['get', 'width'],
|
||||||
|
'line-opacity': ['get', 'opacity'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
_map.on('click', this.fileId, this.layerOnClickBinded);
|
_map.on('click', this.fileId, this.layerOnClickBinded);
|
||||||
_map.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
_map.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
||||||
@@ -221,59 +189,6 @@ export class GPXLayer {
|
|||||||
_map.on('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
_map.on('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
let waypointSource = _map.getSource(this.fileId + '-waypoints') as
|
|
||||||
| mapboxgl.GeoJSONSource
|
|
||||||
| undefined;
|
|
||||||
this.currentWaypointData = this.getWaypointsGeoJSON();
|
|
||||||
if (waypointSource) {
|
|
||||||
waypointSource.setData(this.currentWaypointData);
|
|
||||||
} else {
|
|
||||||
_map.addSource(this.fileId + '-waypoints', {
|
|
||||||
type: 'geojson',
|
|
||||||
data: this.currentWaypointData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_map.getLayer(this.fileId + '-waypoints')) {
|
|
||||||
_map.addLayer(
|
|
||||||
{
|
|
||||||
id: this.fileId + '-waypoints',
|
|
||||||
type: 'symbol',
|
|
||||||
source: this.fileId + '-waypoints',
|
|
||||||
layout: {
|
|
||||||
'icon-image': ['get', 'icon'],
|
|
||||||
'icon-size': 0.3,
|
|
||||||
'icon-anchor': 'bottom',
|
|
||||||
'icon-padding': 0,
|
|
||||||
'icon-allow-overlap': true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ANCHOR_LAYER_KEY.waypoints
|
|
||||||
);
|
|
||||||
|
|
||||||
_map.on(
|
|
||||||
'mouseenter',
|
|
||||||
this.fileId + '-waypoints',
|
|
||||||
this.waypointLayerOnMouseEnterBinded
|
|
||||||
);
|
|
||||||
_map.on(
|
|
||||||
'mouseleave',
|
|
||||||
this.fileId + '-waypoints',
|
|
||||||
this.waypointLayerOnMouseLeaveBinded
|
|
||||||
);
|
|
||||||
_map.on('click', this.fileId + '-waypoints', this.waypointLayerOnClickBinded);
|
|
||||||
_map.on(
|
|
||||||
'mousedown',
|
|
||||||
this.fileId + '-waypoints',
|
|
||||||
this.waypointLayerOnMouseDownBinded
|
|
||||||
);
|
|
||||||
_map.on(
|
|
||||||
'touchstart',
|
|
||||||
this.fileId + '-waypoints',
|
|
||||||
this.waypointLayerOnTouchStartBinded
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (get(directionMarkers)) {
|
if (get(directionMarkers)) {
|
||||||
if (!_map.getLayer(this.fileId + '-direction')) {
|
if (!_map.getLayer(this.fileId + '-direction')) {
|
||||||
_map.addLayer(
|
_map.addLayer(
|
||||||
@@ -298,7 +213,7 @@ export class GPXLayer {
|
|||||||
'text-halo-color': 'white',
|
'text-halo-color': 'white',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ANCHOR_LAYER_KEY.directionMarkers
|
_map.getLayer('distance-markers-100') ? 'distance-markers-100' : undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -307,40 +222,151 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let visibleTrackSegmentIds: string[] = [];
|
let visibleItems: [number, number][] = [];
|
||||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||||
if (!segment._data.hidden) {
|
if (!segment._data.hidden) {
|
||||||
visibleTrackSegmentIds.push(`${trackIndex}-${segmentIndex}`);
|
visibleItems.push([trackIndex, segmentIndex]);
|
||||||
}
|
|
||||||
});
|
|
||||||
const segmentFilter: FilterSpecification = [
|
|
||||||
'in',
|
|
||||||
['get', 'trackSegmentId'],
|
|
||||||
['literal', visibleTrackSegmentIds],
|
|
||||||
];
|
|
||||||
|
|
||||||
_map.setFilter(this.fileId, segmentFilter, { validate: false });
|
|
||||||
|
|
||||||
if (_map.getLayer(this.fileId + '-direction')) {
|
|
||||||
_map.setFilter(this.fileId + '-direction', segmentFilter, { validate: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
let visibleWaypoints: number[] = [];
|
|
||||||
file.wpt.forEach((waypoint, waypointIndex) => {
|
|
||||||
if (!waypoint._data.hidden) {
|
|
||||||
visibleWaypoints.push(waypointIndex);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_map.setFilter(
|
_map.setFilter(
|
||||||
this.fileId + '-waypoints',
|
this.fileId,
|
||||||
['in', ['get', 'waypointIndex'], ['literal', visibleWaypoints]],
|
[
|
||||||
|
'any',
|
||||||
|
...visibleItems.map(([trackIndex, segmentIndex]) => [
|
||||||
|
'all',
|
||||||
|
['==', 'trackIndex', trackIndex],
|
||||||
|
['==', 'segmentIndex', segmentIndex],
|
||||||
|
]),
|
||||||
|
],
|
||||||
{ validate: false }
|
{ validate: false }
|
||||||
);
|
);
|
||||||
|
if (_map.getLayer(this.fileId + '-direction')) {
|
||||||
|
_map.setFilter(
|
||||||
|
this.fileId + '-direction',
|
||||||
|
[
|
||||||
|
'any',
|
||||||
|
...visibleItems.map(([trackIndex, segmentIndex]) => [
|
||||||
|
'all',
|
||||||
|
['==', 'trackIndex', trackIndex],
|
||||||
|
['==', 'segmentIndex', segmentIndex],
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
{ validate: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// No reliable way to check if the map is ready to add sources and layers
|
// No reliable way to check if the map is ready to add sources and layers
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let markerIndex = 0;
|
||||||
|
|
||||||
|
if (get(selection).hasAnyChildren(new ListFileItem(this.fileId))) {
|
||||||
|
file.wpt.forEach((waypoint) => {
|
||||||
|
// Update markers
|
||||||
|
let symbolKey = getSymbolKey(waypoint.sym);
|
||||||
|
if (markerIndex < this.markers.length) {
|
||||||
|
this.markers[markerIndex].getElement().innerHTML = getMarkerForSymbol(
|
||||||
|
symbolKey,
|
||||||
|
this.layerColor
|
||||||
|
);
|
||||||
|
this.markers[markerIndex].setLngLat(waypoint.getCoordinates());
|
||||||
|
Object.defineProperty(this.markers[markerIndex], '_waypoint', {
|
||||||
|
value: waypoint,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let element = document.createElement('div');
|
||||||
|
element.classList.add('w-8', 'h-8', 'drop-shadow-xl');
|
||||||
|
element.innerHTML = getMarkerForSymbol(symbolKey, this.layerColor);
|
||||||
|
let marker = new mapboxgl.Marker({
|
||||||
|
draggable: this.draggable,
|
||||||
|
element,
|
||||||
|
anchor: 'bottom',
|
||||||
|
}).setLngLat(waypoint.getCoordinates());
|
||||||
|
Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true });
|
||||||
|
let dragEndTimestamp = 0;
|
||||||
|
marker.getElement().addEventListener('mousemove', (e) => {
|
||||||
|
if (marker._isDragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
waypointPopup?.setItem({ item: marker._waypoint, fileId: this.fileId });
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
marker.getElement().addEventListener('click', (e) => {
|
||||||
|
if (dragEndTimestamp && Date.now() - dragEndTimestamp < 1000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get(currentTool) === Tool.WAYPOINT && e.shiftKey) {
|
||||||
|
fileActions.deleteWaypoint(this.fileId, marker._waypoint._data.index);
|
||||||
|
e.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get(treeFileView)) {
|
||||||
|
if (
|
||||||
|
(e.ctrlKey || e.metaKey) &&
|
||||||
|
get(selection).hasAnyChildren(
|
||||||
|
new ListWaypointsItem(this.fileId),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
selection.addSelectItem(
|
||||||
|
new ListWaypointItem(this.fileId, marker._waypoint._data.index)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
selection.selectItem(
|
||||||
|
new ListWaypointItem(this.fileId, marker._waypoint._data.index)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (get(currentTool) === Tool.WAYPOINT) {
|
||||||
|
selectedWaypoint.set([marker._waypoint, this.fileId]);
|
||||||
|
} else {
|
||||||
|
waypointPopup?.setItem({ item: marker._waypoint, fileId: this.fileId });
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
marker.on('dragstart', () => {
|
||||||
|
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, true);
|
||||||
|
marker.getElement().style.cursor = 'grabbing';
|
||||||
|
waypointPopup?.hide();
|
||||||
|
});
|
||||||
|
marker.on('dragend', (e) => {
|
||||||
|
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
|
||||||
|
marker.getElement().style.cursor = '';
|
||||||
|
getElevation([marker._waypoint]).then((ele) => {
|
||||||
|
fileActionManager.applyToFile(this.fileId, (file) => {
|
||||||
|
let latLng = marker.getLngLat();
|
||||||
|
let wpt = file.wpt[marker._waypoint._data.index];
|
||||||
|
wpt.setCoordinates({
|
||||||
|
lat: latLng.lat,
|
||||||
|
lon: latLng.lng,
|
||||||
|
});
|
||||||
|
wpt.ele = ele[0];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
dragEndTimestamp = Date.now();
|
||||||
|
});
|
||||||
|
this.markers.push(marker);
|
||||||
|
}
|
||||||
|
markerIndex++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
while (markerIndex < this.markers.length) {
|
||||||
|
// Remove extra markers
|
||||||
|
this.markers.pop()?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.markers.forEach((marker) => {
|
||||||
|
if (!marker._waypoint._data.hidden) {
|
||||||
|
marker.addTo(_map);
|
||||||
|
} else {
|
||||||
|
marker.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
@@ -353,24 +379,6 @@ export class GPXLayer {
|
|||||||
_map.off('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
_map.off('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
||||||
_map.off('style.import.load', this.updateBinded);
|
_map.off('style.import.load', this.updateBinded);
|
||||||
|
|
||||||
_map.off(
|
|
||||||
'mouseenter',
|
|
||||||
this.fileId + '-waypoints',
|
|
||||||
this.waypointLayerOnMouseEnterBinded
|
|
||||||
);
|
|
||||||
_map.off(
|
|
||||||
'mouseleave',
|
|
||||||
this.fileId + '-waypoints',
|
|
||||||
this.waypointLayerOnMouseLeaveBinded
|
|
||||||
);
|
|
||||||
_map.off('click', this.fileId + '-waypoints', this.waypointLayerOnClickBinded);
|
|
||||||
_map.off('mousedown', this.fileId + '-waypoints', this.waypointLayerOnMouseDownBinded);
|
|
||||||
_map.off(
|
|
||||||
'touchstart',
|
|
||||||
this.fileId + '-waypoints',
|
|
||||||
this.waypointLayerOnTouchStartBinded
|
|
||||||
);
|
|
||||||
|
|
||||||
if (_map.getLayer(this.fileId + '-direction')) {
|
if (_map.getLayer(this.fileId + '-direction')) {
|
||||||
_map.removeLayer(this.fileId + '-direction');
|
_map.removeLayer(this.fileId + '-direction');
|
||||||
}
|
}
|
||||||
@@ -380,17 +388,15 @@ export class GPXLayer {
|
|||||||
if (_map.getSource(this.fileId)) {
|
if (_map.getSource(this.fileId)) {
|
||||||
_map.removeSource(this.fileId);
|
_map.removeSource(this.fileId);
|
||||||
}
|
}
|
||||||
if (_map.getLayer(this.fileId + '-waypoints')) {
|
|
||||||
_map.removeLayer(this.fileId + '-waypoints');
|
|
||||||
}
|
|
||||||
if (_map.getSource(this.fileId + '-waypoints')) {
|
|
||||||
_map.removeSource(this.fileId + '-waypoints');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.markers.forEach((marker) => {
|
||||||
|
marker.remove();
|
||||||
|
});
|
||||||
|
|
||||||
this.unsubscribe.forEach((unsubscribe) => unsubscribe());
|
this.unsubscribe.forEach((unsubscribe) => unsubscribe());
|
||||||
|
|
||||||
removeColor(this.fileId, this.layerColor);
|
decrementColor(this.layerColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
moveToFront() {
|
moveToFront() {
|
||||||
@@ -399,13 +405,10 @@ export class GPXLayer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_map.getLayer(this.fileId)) {
|
if (_map.getLayer(this.fileId)) {
|
||||||
_map.moveLayer(this.fileId, ANCHOR_LAYER_KEY.tracks);
|
_map.moveLayer(this.fileId);
|
||||||
}
|
|
||||||
if (_map.getLayer(this.fileId + '-waypoints')) {
|
|
||||||
_map.moveLayer(this.fileId + '-waypoints', ANCHOR_LAYER_KEY.waypoints);
|
|
||||||
}
|
}
|
||||||
if (_map.getLayer(this.fileId + '-direction')) {
|
if (_map.getLayer(this.fileId + '-direction')) {
|
||||||
_map.moveLayer(this.fileId + '-direction', ANCHOR_LAYER_KEY.directionMarkers);
|
_map.moveLayer(this.fileId + '-direction');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,7 +449,7 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
layerOnClick(e: mapboxgl.MapMouseEvent) {
|
layerOnClick(e: any) {
|
||||||
if (
|
if (
|
||||||
get(currentTool) === Tool.ROUTING &&
|
get(currentTool) === Tool.ROUTING &&
|
||||||
get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])
|
get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])
|
||||||
@@ -454,8 +457,8 @@ export class GPXLayer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let trackIndex = e.features![0].properties!.trackIndex;
|
let trackIndex = e.features[0].properties.trackIndex;
|
||||||
let segmentIndex = e.features![0].properties!.segmentIndex;
|
let segmentIndex = e.features[0].properties.segmentIndex;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
get(currentTool) === Tool.SCISSORS &&
|
get(currentTool) === Tool.SCISSORS &&
|
||||||
@@ -463,11 +466,6 @@ export class GPXLayer {
|
|||||||
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
|
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (get(map)?.queryRenderedFeatures(e.point, { layers: ['split-controls'] }).length) {
|
|
||||||
// Clicked on split control, ignoring
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fileActions.split(get(splitAs), this.fileId, trackIndex, segmentIndex, {
|
fileActions.split(get(splitAs), this.fileId, trackIndex, segmentIndex, {
|
||||||
lat: e.lngLat.lat,
|
lat: e.lngLat.lat,
|
||||||
lon: e.lngLat.lng,
|
lon: e.lngLat.lng,
|
||||||
@@ -504,160 +502,6 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
waypointLayerOnMouseEnter(e: mapboxgl.MapMouseEvent) {
|
|
||||||
if (this.draggedWaypointIndex !== null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let file = get(this.file)?.file;
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let waypointIndex = e.features![0].properties!.waypointIndex;
|
|
||||||
let waypoint = file.wpt[waypointIndex];
|
|
||||||
waypointPopup?.setItem({ item: waypoint, fileId: this.fileId });
|
|
||||||
|
|
||||||
mapCursor.notify(MapCursorState.WAYPOINT_HOVER, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
waypointLayerOnMouseLeave() {
|
|
||||||
mapCursor.notify(MapCursorState.WAYPOINT_HOVER, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
waypointLayerOnClick(e: mapboxgl.MapMouseEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
let waypointIndex = e.features![0].properties!.waypointIndex;
|
|
||||||
let file = get(this.file)?.file;
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let waypoint = file.wpt[waypointIndex];
|
|
||||||
if (get(currentTool) === Tool.WAYPOINT) {
|
|
||||||
if (this.selected) {
|
|
||||||
if (e.originalEvent.shiftKey) {
|
|
||||||
fileActions.deleteWaypoint(this.fileId, waypointIndex);
|
|
||||||
} else {
|
|
||||||
selection.selectItem(new ListWaypointItem(this.fileId, waypointIndex));
|
|
||||||
selectedWaypoint.set([waypoint, this.fileId]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (get(treeFileView)) {
|
|
||||||
selection.selectItem(new ListWaypointItem(this.fileId, waypointIndex));
|
|
||||||
} else {
|
|
||||||
selection.selectItem(new ListFileItem(this.fileId));
|
|
||||||
}
|
|
||||||
selectedWaypoint.set([waypoint, this.fileId]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (get(treeFileView)) {
|
|
||||||
if ((e.originalEvent.ctrlKey || e.originalEvent.metaKey) && this.selected) {
|
|
||||||
selection.addSelectItem(new ListWaypointItem(this.fileId, waypointIndex));
|
|
||||||
} else {
|
|
||||||
selection.selectItem(new ListWaypointItem(this.fileId, waypointIndex));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!this.selected) {
|
|
||||||
selection.selectItem(new ListFileItem(this.fileId));
|
|
||||||
}
|
|
||||||
waypointPopup?.setItem({ item: waypoint, fileId: this.fileId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
waypointLayerOnMouseDown(e: mapboxgl.MapMouseEvent) {
|
|
||||||
if (get(currentTool) !== Tool.WAYPOINT || !this.selected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const _map = get(map);
|
|
||||||
if (!_map) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.draggedWaypointIndex = e.features![0].properties!.waypointIndex;
|
|
||||||
this.draggingStartingPosition = e.point;
|
|
||||||
waypointPopup?.hide();
|
|
||||||
|
|
||||||
_map.on('mousemove', this.waypointLayerOnMouseMoveBinded);
|
|
||||||
_map.once('mouseup', this.waypointLayerOnMouseUpBinded);
|
|
||||||
}
|
|
||||||
|
|
||||||
waypointLayerOnTouchStart(e: mapboxgl.MapTouchEvent) {
|
|
||||||
if (e.points.length !== 1 || get(currentTool) !== Tool.WAYPOINT || !this.selected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const _map = get(map);
|
|
||||||
if (!_map) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.draggedWaypointIndex = e.features![0].properties!.waypointIndex;
|
|
||||||
this.draggingStartingPosition = e.point;
|
|
||||||
waypointPopup?.hide();
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
_map.on('touchmove', this.waypointLayerOnMouseMoveBinded);
|
|
||||||
_map.once('touchend', this.waypointLayerOnMouseUpBinded);
|
|
||||||
}
|
|
||||||
|
|
||||||
waypointLayerOnMouseMove(e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) {
|
|
||||||
if (this.draggedWaypointIndex === null || e.point.equals(this.draggingStartingPosition)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, true);
|
|
||||||
|
|
||||||
(
|
|
||||||
this.currentWaypointData!.features[this.draggedWaypointIndex].geometry as GeoJSON.Point
|
|
||||||
).coordinates = [e.lngLat.lng, e.lngLat.lat];
|
|
||||||
|
|
||||||
let waypointSource = get(map)?.getSource(this.fileId + '-waypoints') as
|
|
||||||
| mapboxgl.GeoJSONSource
|
|
||||||
| undefined;
|
|
||||||
if (waypointSource) {
|
|
||||||
waypointSource.setData(this.currentWaypointData!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
waypointLayerOnMouseUp(e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) {
|
|
||||||
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
|
|
||||||
|
|
||||||
get(map)?.off('mousemove', this.waypointLayerOnMouseMoveBinded);
|
|
||||||
get(map)?.off('touchmove', this.waypointLayerOnMouseMoveBinded);
|
|
||||||
|
|
||||||
if (this.draggedWaypointIndex === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.point.equals(this.draggingStartingPosition)) {
|
|
||||||
this.draggedWaypointIndex = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
getElevation([
|
|
||||||
{
|
|
||||||
lat: e.lngLat.lat,
|
|
||||||
lon: e.lngLat.lng,
|
|
||||||
},
|
|
||||||
]).then((ele) => {
|
|
||||||
if (this.draggedWaypointIndex === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fileActionManager.applyToFile(this.fileId, (file) => {
|
|
||||||
let wpt = file.wpt[this.draggedWaypointIndex!];
|
|
||||||
wpt.setCoordinates({
|
|
||||||
lat: e.lngLat.lat,
|
|
||||||
lon: e.lngLat.lng,
|
|
||||||
});
|
|
||||||
wpt.ele = ele[0];
|
|
||||||
});
|
|
||||||
this.draggedWaypointIndex = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getGeoJSON(): GeoJSON.FeatureCollection {
|
getGeoJSON(): GeoJSON.FeatureCollection {
|
||||||
let file = get(this.file)?.file;
|
let file = get(this.file)?.file;
|
||||||
if (!file) {
|
if (!file) {
|
||||||
@@ -695,7 +539,6 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
feature.properties.trackIndex = trackIndex;
|
feature.properties.trackIndex = trackIndex;
|
||||||
feature.properties.segmentIndex = segmentIndex;
|
feature.properties.segmentIndex = segmentIndex;
|
||||||
feature.properties.trackSegmentId = `${trackIndex}-${segmentIndex}`;
|
|
||||||
|
|
||||||
segmentIndex++;
|
segmentIndex++;
|
||||||
if (segmentIndex >= file.trk[trackIndex].trkseg.length) {
|
if (segmentIndex >= file.trk[trackIndex].trkseg.length) {
|
||||||
@@ -705,65 +548,4 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
getWaypointsGeoJSON(): GeoJSON.FeatureCollection {
|
|
||||||
let file = get(this.file)?.file;
|
|
||||||
|
|
||||||
let data: GeoJSON.FeatureCollection = {
|
|
||||||
type: 'FeatureCollection',
|
|
||||||
features: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
file.wpt.forEach((waypoint, index) => {
|
|
||||||
data.features.push({
|
|
||||||
type: 'Feature',
|
|
||||||
geometry: {
|
|
||||||
type: 'Point',
|
|
||||||
coordinates: [waypoint.getLongitude(), waypoint.getLatitude()],
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
fileId: this.fileId,
|
|
||||||
waypointIndex: index,
|
|
||||||
icon: `waypoint-${getSymbolKey(waypoint.sym) ?? 'default'}-${this.layerColor}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadIcons() {
|
|
||||||
const _map = get(map);
|
|
||||||
let file = get(this.file)?.file;
|
|
||||||
if (!_map || !file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let symbols = new Set<string | undefined>();
|
|
||||||
file.wpt.forEach((waypoint) => {
|
|
||||||
symbols.add(getSymbolKey(waypoint.sym));
|
|
||||||
});
|
|
||||||
|
|
||||||
symbols.forEach((symbol) => {
|
|
||||||
const iconId = `waypoint-${symbol ?? 'default'}-${this.layerColor}`;
|
|
||||||
if (!_map.hasImage(iconId)) {
|
|
||||||
let icon = new Image(100, 100);
|
|
||||||
icon.onload = () => {
|
|
||||||
if (!_map.hasImage(iconId)) {
|
|
||||||
_map.addImage(iconId, icon);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Lucide icons are SVG files with a 24x24 viewBox
|
|
||||||
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
|
|
||||||
icon.src =
|
|
||||||
'data:image/svg+xml,' +
|
|
||||||
encodeURIComponent(getSvgForSymbol(symbol, this.layerColor));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { GPXFileStateCollectionObserver } from '$lib/logic/file-state';
|
import { GPXFileStateCollectionObserver } from '$lib/logic/file-state';
|
||||||
import { writable } from 'svelte/store';
|
|
||||||
import { GPXLayer } from './gpx-layer';
|
import { GPXLayer } from './gpx-layer';
|
||||||
|
|
||||||
export class GPXLayerCollection {
|
export class GPXLayerCollection {
|
||||||
@@ -43,4 +42,3 @@ export class GPXLayerCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const gpxLayers = new GPXLayerCollection();
|
export const gpxLayers = new GPXLayerCollection();
|
||||||
export const gpxColors = writable(new Map<string, string>());
|
|
||||||
|
|||||||
@@ -34,20 +34,13 @@ export class StartEndMarkers {
|
|||||||
if (!map_) return;
|
if (!map_) return;
|
||||||
|
|
||||||
const tool = get(currentTool);
|
const tool = get(currentTool);
|
||||||
const statistics = get(gpxStatistics);
|
const statistics = get(slicedGPXStatistics)?.[0] ?? get(gpxStatistics);
|
||||||
const slicedStatistics = get(slicedGPXStatistics);
|
|
||||||
const hidden = get(allHidden);
|
const hidden = get(allHidden);
|
||||||
if (statistics.global.length > 0 && tool !== Tool.ROUTING && !hidden) {
|
if (statistics.local.points.length > 0 && tool !== Tool.ROUTING && !hidden) {
|
||||||
this.start
|
this.start.setLngLat(statistics.local.points[0].getCoordinates()).addTo(map_);
|
||||||
.setLngLat(
|
|
||||||
statistics.getTrackPoint(slicedStatistics?.[1] ?? 0)!.trkpt.getCoordinates()
|
|
||||||
)
|
|
||||||
.addTo(map_);
|
|
||||||
this.end
|
this.end
|
||||||
.setLngLat(
|
.setLngLat(
|
||||||
statistics
|
statistics.local.points[statistics.local.points.length - 1].getCoordinates()
|
||||||
.getTrackPoint(slicedStatistics?.[2] ?? statistics.global.length - 1)!
|
|
||||||
.trkpt.getCoordinates()
|
|
||||||
)
|
)
|
||||||
.addTo(map_);
|
.addTo(map_);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -101,7 +101,9 @@
|
|||||||
acc: Record<string, ImportSpecification>,
|
acc: Record<string, ImportSpecification>,
|
||||||
imprt: ImportSpecification
|
imprt: ImportSpecification
|
||||||
) => {
|
) => {
|
||||||
if (!['basemap', 'overlays'].includes(imprt.id)) {
|
if (
|
||||||
|
!['basemap', 'overlays', 'glyphs-and-sprite'].includes(imprt.id)
|
||||||
|
) {
|
||||||
acc[imprt.id] = imprt;
|
acc[imprt.id] = imprt;
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
overlays,
|
overlays,
|
||||||
overlayTree,
|
overlayTree,
|
||||||
overpassTree,
|
overpassTree,
|
||||||
terrainSources,
|
|
||||||
} from '$lib/assets/layers';
|
} from '$lib/assets/layers';
|
||||||
import { getLayers, isSelected, toggle } from '$lib/components/map/layer-control/utils';
|
import { getLayers, isSelected, toggle } from '$lib/components/map/layer-control/utils';
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
@@ -32,7 +31,6 @@
|
|||||||
currentOverpassQueries,
|
currentOverpassQueries,
|
||||||
customLayers,
|
customLayers,
|
||||||
opacities,
|
opacities,
|
||||||
terrainSource,
|
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
const { isLayerFromExtension, getLayerName } = extensionAPI;
|
const { isLayerFromExtension, getLayerName } = extensionAPI;
|
||||||
@@ -56,7 +54,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (open && $selectedBasemapTree && $currentBasemap) {
|
if ($selectedBasemapTree && $currentBasemap) {
|
||||||
if (!isSelected($selectedBasemapTree, $currentBasemap)) {
|
if (!isSelected($selectedBasemapTree, $currentBasemap)) {
|
||||||
if (!isSelected($selectedBasemapTree, defaultBasemap)) {
|
if (!isSelected($selectedBasemapTree, defaultBasemap)) {
|
||||||
$selectedBasemapTree = toggle($selectedBasemapTree, defaultBasemap);
|
$selectedBasemapTree = toggle($selectedBasemapTree, defaultBasemap);
|
||||||
@@ -67,7 +65,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (open && $selectedOverlayTree) {
|
if ($selectedOverlayTree) {
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
if ($currentOverlays) {
|
if ($currentOverlays) {
|
||||||
let overlayLayers = getLayers($currentOverlays);
|
let overlayLayers = getLayers($currentOverlays);
|
||||||
@@ -88,7 +86,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (open && $selectedOverpassTree) {
|
if ($selectedOverpassTree) {
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
if ($currentOverpassQueries) {
|
if ($currentOverpassQueries) {
|
||||||
let overlayLayers = getLayers($currentOverpassQueries);
|
let overlayLayers = getLayers($currentOverpassQueries);
|
||||||
@@ -162,7 +160,7 @@
|
|||||||
type="single"
|
type="single"
|
||||||
onValueChange={setOpacityFromSelection}
|
onValueChange={setOpacityFromSelection}
|
||||||
>
|
>
|
||||||
<Select.Trigger class="mr-1 w-full" size="sm">
|
<Select.Trigger class="h-8 mr-1 w-full">
|
||||||
{#if selectedOverlay}
|
{#if selectedOverlay}
|
||||||
{#if isSelected($selectedOverlayTree, selectedOverlay)}
|
{#if isSelected($selectedOverlayTree, selectedOverlay)}
|
||||||
{#if $isLayerFromExtension(selectedOverlay)}
|
{#if $isLayerFromExtension(selectedOverlay)}
|
||||||
@@ -235,23 +233,6 @@
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Accordion.Content>
|
</Accordion.Content>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
<Accordion.Item value="terrain-source">
|
|
||||||
<Accordion.Trigger>{i18n._('layers.terrain')}</Accordion.Trigger>
|
|
||||||
<Accordion.Content class="flex flex-col gap-3 overflow-visible">
|
|
||||||
<Select.Root bind:value={$terrainSource} type="single">
|
|
||||||
<Select.Trigger class="mr-1 w-full" size="sm">
|
|
||||||
{i18n._(`layers.label.${$terrainSource}`)}
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content class="h-fit max-h-[40dvh] overflow-y-auto">
|
|
||||||
{#each Object.keys(terrainSources) as id}
|
|
||||||
<Select.Item value={id}>
|
|
||||||
{i18n._(`layers.label.${id}`)}
|
|
||||||
</Select.Item>
|
|
||||||
{/each}
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Root>
|
|
||||||
</Accordion.Content>
|
|
||||||
</Accordion.Item>
|
|
||||||
</Accordion.Root>
|
</Accordion.Root>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Sheet.Header>
|
</Sheet.Header>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
{:else if anySelectedLayer(node[id])}
|
{:else if anySelectedLayer(node[id])}
|
||||||
<CollapsibleTreeNode {id}>
|
<CollapsibleTreeNode {id}>
|
||||||
{#snippet trigger()}
|
{#snippet trigger()}
|
||||||
<span>{i18n._(`layers.label.${id}`, id)}</span>
|
<span>{i18n._(`layers.label.${id}`)}</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet content()}
|
{#snippet content()}
|
||||||
<div class="ml-2">
|
<div class="ml-2">
|
||||||
|
|||||||
@@ -54,27 +54,28 @@
|
|||||||
|
|
||||||
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw] gap-0">
|
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw] gap-0">
|
||||||
<Card.Header class="p-0 gap-0">
|
<Card.Header class="p-0 gap-0">
|
||||||
<Card.Title class="text-md flex flex-row">
|
<Card.Title class="text-md">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-row gap-3">
|
||||||
<p>{name}</p>
|
<div class="flex flex-col">
|
||||||
<div class="text-muted-foreground text-xs font-normal">
|
{name}
|
||||||
{poi.item.lat.toFixed(6)}° {poi.item.lon.toFixed(6)}°
|
<div class="text-muted-foreground text-xs font-normal">
|
||||||
|
{poi.item.lat.toFixed(6)}° {poi.item.lon.toFixed(6)}°
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
class="ml-auto"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
href="https://www.openstreetmap.org/edit?editor=id&{poi.item.type ??
|
||||||
|
'node'}={poi.item.id}"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<PencilLine size="16" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
|
||||||
class="ml-auto"
|
|
||||||
variant="outline"
|
|
||||||
size="icon-sm"
|
|
||||||
href="https://www.openstreetmap.org/edit?editor=id&{poi.item.type ?? 'node'}={poi
|
|
||||||
.item.id}"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<PencilLine size="16" />
|
|
||||||
</Button>
|
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content class="flex flex-col gap-1 p-0 text-sm whitespace-normal break-all">
|
<Card.Content class="flex flex-col p-0 text-sm mt-1 whitespace-normal break-all">
|
||||||
<ScrollArea class="flex flex-col max-h-[30dvh]">
|
<ScrollArea class="flex flex-col max-h-[30dvh]">
|
||||||
{#if tags.image || tags['image:0']}
|
{#if tags.image || tags['image:0']}
|
||||||
<div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto">
|
<div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto">
|
||||||
@@ -99,14 +100,8 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<Button
|
<Button class="mt-2" variant="outline" disabled={$selection.size === 0} onclick={addToFile}>
|
||||||
size="sm"
|
<MapPin size="16" />
|
||||||
class="mt-1 justify-start"
|
|
||||||
variant="outline"
|
|
||||||
disabled={$selection.size === 0}
|
|
||||||
onclick={addToFile}
|
|
||||||
>
|
|
||||||
<MapPin size="14" />
|
|
||||||
{i18n._('toolbar.waypoint.add')}
|
{i18n._('toolbar.waypoint.add')}
|
||||||
</Button>
|
</Button>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { map } from '$lib/components/map/map';
|
|||||||
const { currentOverlays, previousOverlays, selectedOverlayTree } = settings;
|
const { currentOverlays, previousOverlays, selectedOverlayTree } = settings;
|
||||||
|
|
||||||
export type CustomOverlay = {
|
export type CustomOverlay = {
|
||||||
extensionName: string;
|
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
tileUrls: string[];
|
tileUrls: string[];
|
||||||
@@ -47,16 +46,8 @@ export class ExtensionAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addOrUpdateOverlay(overlay: CustomOverlay) {
|
addOrUpdateOverlay(overlay: CustomOverlay) {
|
||||||
if (
|
if (!overlay.id || !overlay.name || !overlay.tileUrls || overlay.tileUrls.length === 0) {
|
||||||
!overlay.extensionName ||
|
throw new Error('Overlay must have an id, name, and at least one tile URL.');
|
||||||
!overlay.id ||
|
|
||||||
!overlay.name ||
|
|
||||||
!overlay.tileUrls ||
|
|
||||||
overlay.tileUrls.length === 0
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
'Overlay must have an extensionName, id, name, and at least one tile URL.'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
overlay.id = this.getOverlayId(overlay.id);
|
overlay.id = this.getOverlayId(overlay.id);
|
||||||
|
|
||||||
@@ -84,17 +75,10 @@ export class ExtensionAPI {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!overlayTree.overlays.hasOwnProperty(overlay.extensionName)) {
|
overlayTree.overlays.world[overlay.id] = true;
|
||||||
overlayTree.overlays[overlay.extensionName] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
overlayTree.overlays[overlay.extensionName][overlay.id] = true;
|
|
||||||
|
|
||||||
selectedOverlayTree.update((selected) => {
|
selectedOverlayTree.update((selected) => {
|
||||||
if (!selected.overlays.hasOwnProperty(overlay.extensionName)) {
|
selected.overlays.world[overlay.id] = true;
|
||||||
selected.overlays[overlay.extensionName] = {};
|
|
||||||
}
|
|
||||||
selected.overlays[overlay.extensionName][overlay.id] = true;
|
|
||||||
return selected;
|
return selected;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -110,10 +94,7 @@ export class ExtensionAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentOverlays.update((current) => {
|
currentOverlays.update((current) => {
|
||||||
if (!current.overlays.hasOwnProperty(overlay.extensionName)) {
|
current.overlays.world[overlay.id] = show;
|
||||||
current.overlays[overlay.extensionName] = {};
|
|
||||||
}
|
|
||||||
current.overlays[overlay.extensionName][overlay.id] = show;
|
|
||||||
return current;
|
return current;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -152,29 +133,6 @@ export class ExtensionAPI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOverlaysOrder(ids: string[]) {
|
|
||||||
ids = ids.map((id) => this.getOverlayId(id));
|
|
||||||
selectedOverlayTree.update((selected) => {
|
|
||||||
let isSelected: Record<string, boolean> = {};
|
|
||||||
ids.forEach((id) => {
|
|
||||||
const overlay = get(this._overlays).get(id);
|
|
||||||
if (
|
|
||||||
overlay &&
|
|
||||||
selected.overlays.hasOwnProperty(overlay.extensionName) &&
|
|
||||||
selected.overlays[overlay.extensionName].hasOwnProperty(id)
|
|
||||||
) {
|
|
||||||
isSelected[id] = selected.overlays[overlay.extensionName][id];
|
|
||||||
delete selected.overlays[overlay.extensionName][id];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Object.entries(isSelected).forEach(([id, value]) => {
|
|
||||||
const overlay = get(this._overlays).get(id)!;
|
|
||||||
selected.overlays[overlay.extensionName][id] = value;
|
|
||||||
});
|
|
||||||
return selected;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isLayerFromExtension = derived(this._overlays, ($overlays) => {
|
isLayerFromExtension = derived(this._overlays, ($overlays) => {
|
||||||
return (id: string) => $overlays.has(id);
|
return (id: string) => $overlays.has(id);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { overpassQueryData } from '$lib/assets/layers';
|
|||||||
import { MapPopup } from '$lib/components/map/map-popup';
|
import { MapPopup } from '$lib/components/map/map-popup';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { db } from '$lib/db';
|
import { db } from '$lib/db';
|
||||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/map';
|
|
||||||
|
|
||||||
const { currentOverpassQueries } = settings;
|
const { currentOverpassQueries } = settings;
|
||||||
|
|
||||||
@@ -86,28 +85,23 @@ export class OverpassLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.map.getLayer('overpass')) {
|
if (!this.map.getLayer('overpass')) {
|
||||||
this.map.addLayer(
|
this.map.addLayer({
|
||||||
{
|
id: 'overpass',
|
||||||
id: 'overpass',
|
type: 'symbol',
|
||||||
type: 'symbol',
|
source: 'overpass',
|
||||||
source: 'overpass',
|
layout: {
|
||||||
layout: {
|
'icon-image': ['get', 'icon'],
|
||||||
'icon-image': ['get', 'icon'],
|
'icon-size': 0.25,
|
||||||
'icon-size': 0.25,
|
'icon-padding': 0,
|
||||||
'icon-padding': 0,
|
'icon-allow-overlap': ['step', ['zoom'], false, 14, true],
|
||||||
'icon-allow-overlap': ['step', ['zoom'], false, 14, true],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
ANCHOR_LAYER_KEY.overpass
|
});
|
||||||
);
|
|
||||||
|
|
||||||
this.map.on('mouseenter', 'overpass', this.onHoverBinded);
|
this.map.on('mouseenter', 'overpass', this.onHoverBinded);
|
||||||
this.map.on('click', 'overpass', this.onHoverBinded);
|
this.map.on('click', 'overpass', this.onHoverBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.map.setFilter('overpass', ['in', 'query', ...getCurrentQueries()], {
|
this.map.setFilter('overpass', ['in', 'query', ...getCurrentQueries()]);
|
||||||
validate: false,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// No reliable way to check if the map is ready to add sources and layers
|
// No reliable way to check if the map is ready to add sources and layers
|
||||||
}
|
}
|
||||||
@@ -289,12 +283,10 @@ function getQuery(query: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQueryItem(tags: Record<string, string | string[]>) {
|
function getQueryItem(tags: Record<string, string | boolean | string[]>) {
|
||||||
let arrayEntry = Object.entries(tags).find((entry): entry is [string, string[]] =>
|
let arrayEntry = Object.values(tags).find((value) => Array.isArray(value));
|
||||||
Array.isArray(entry[1])
|
|
||||||
);
|
|
||||||
if (arrayEntry !== undefined) {
|
if (arrayEntry !== undefined) {
|
||||||
return arrayEntry[1]
|
return arrayEntry
|
||||||
.map(
|
.map(
|
||||||
(val) =>
|
(val) =>
|
||||||
`nwr${Object.entries(tags)
|
`nwr${Object.entries(tags)
|
||||||
@@ -317,7 +309,7 @@ function belongsToQuery(element: any, query: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function belongsToQueryItem(element: any, tags: Record<string, string | string[]>) {
|
function belongsToQueryItem(element: any, tags: Record<string, string | boolean | string[]>) {
|
||||||
return Object.entries(tags).every(([tag, value]) =>
|
return Object.entries(tags).every(([tag, value]) =>
|
||||||
Array.isArray(value) ? value.includes(element.tags[tag]) : element.tags[tag] === value
|
Array.isArray(value) ? value.includes(element.tags[tag]) : element.tags[tag] === value
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,16 +3,8 @@ import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
|
|||||||
import { get, writable, type Writable } from 'svelte/store';
|
import { get, writable, type Writable } from 'svelte/store';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import { terrainSources } from '$lib/assets/layers';
|
|
||||||
|
|
||||||
const {
|
const { treeFileView, elevationProfile, bottomPanelSize, rightPanelSize, distanceUnits } = settings;
|
||||||
treeFileView,
|
|
||||||
elevationProfile,
|
|
||||||
bottomPanelSize,
|
|
||||||
rightPanelSize,
|
|
||||||
distanceUnits,
|
|
||||||
terrainSource,
|
|
||||||
} = settings;
|
|
||||||
|
|
||||||
let fitBoundsOptions: mapboxgl.MapOptions['fitBoundsOptions'] = {
|
let fitBoundsOptions: mapboxgl.MapOptions['fitBoundsOptions'] = {
|
||||||
maxZoom: 15,
|
maxZoom: 15,
|
||||||
@@ -20,28 +12,6 @@ let fitBoundsOptions: mapboxgl.MapOptions['fitBoundsOptions'] = {
|
|||||||
easing: () => 1,
|
easing: () => 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const emptySource: mapboxgl.GeoJSONSourceSpecification = {
|
|
||||||
type: 'geojson',
|
|
||||||
data: {
|
|
||||||
type: 'FeatureCollection',
|
|
||||||
features: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export const ANCHOR_LAYER_KEY = {
|
|
||||||
mapillary: 'mapillary-end',
|
|
||||||
tracks: 'tracks-end',
|
|
||||||
directionMarkers: 'direction-markers-end',
|
|
||||||
distanceMarkers: 'distance-markers-end',
|
|
||||||
interactions: 'interactions-end',
|
|
||||||
overpass: 'overpass-end',
|
|
||||||
waypoints: 'waypoints-end',
|
|
||||||
};
|
|
||||||
const anchorLayers: mapboxgl.LayerSpecification[] = Object.values(ANCHOR_LAYER_KEY).map((id) => ({
|
|
||||||
id: id,
|
|
||||||
type: 'symbol',
|
|
||||||
source: 'empty-source',
|
|
||||||
}));
|
|
||||||
|
|
||||||
export class MapboxGLMap {
|
export class MapboxGLMap {
|
||||||
private _map: Writable<mapboxgl.Map | null> = writable(null);
|
private _map: Writable<mapboxgl.Map | null> = writable(null);
|
||||||
private _onLoadCallbacks: ((map: mapboxgl.Map) => void)[] = [];
|
private _onLoadCallbacks: ((map: mapboxgl.Map) => void)[] = [];
|
||||||
@@ -51,16 +21,31 @@ export class MapboxGLMap {
|
|||||||
return this._map.subscribe(run, invalidate);
|
return this._map.subscribe(run, invalidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
init(language: string, hash: boolean, geocoder: boolean, geolocate: boolean) {
|
init(
|
||||||
|
accessToken: string,
|
||||||
|
language: string,
|
||||||
|
hash: boolean,
|
||||||
|
geocoder: boolean,
|
||||||
|
geolocate: boolean
|
||||||
|
) {
|
||||||
const map = new mapboxgl.Map({
|
const map = new mapboxgl.Map({
|
||||||
container: 'map',
|
container: 'map',
|
||||||
style: {
|
style: {
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {
|
sources: {},
|
||||||
'empty-source': emptySource,
|
layers: [],
|
||||||
},
|
|
||||||
layers: anchorLayers,
|
|
||||||
imports: [
|
imports: [
|
||||||
|
{
|
||||||
|
id: 'glyphs-and-sprite', // make Mapbox glyphs and sprite available to other styles
|
||||||
|
url: '',
|
||||||
|
data: {
|
||||||
|
version: 8,
|
||||||
|
sources: {},
|
||||||
|
layers: [],
|
||||||
|
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
|
||||||
|
sprite: `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'basemap',
|
id: 'basemap',
|
||||||
url: '',
|
url: '',
|
||||||
@@ -68,6 +53,11 @@ export class MapboxGLMap {
|
|||||||
{
|
{
|
||||||
id: 'overlays',
|
id: 'overlays',
|
||||||
url: '',
|
url: '',
|
||||||
|
data: {
|
||||||
|
version: 8,
|
||||||
|
sources: {},
|
||||||
|
layers: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -144,26 +134,39 @@ export class MapboxGLMap {
|
|||||||
});
|
});
|
||||||
map.addControl(scaleControl);
|
map.addControl(scaleControl);
|
||||||
map.on('style.load', () => {
|
map.on('style.load', () => {
|
||||||
|
map.addSource('mapbox-dem', {
|
||||||
|
type: 'raster-dem',
|
||||||
|
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
|
||||||
|
tileSize: 512,
|
||||||
|
maxzoom: 14,
|
||||||
|
});
|
||||||
|
if (map.getPitch() > 0) {
|
||||||
|
map.setTerrain({
|
||||||
|
source: 'mapbox-dem',
|
||||||
|
exaggeration: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
map.setFog({
|
map.setFog({
|
||||||
color: 'rgb(186, 210, 235)',
|
color: 'rgb(186, 210, 235)',
|
||||||
'high-color': 'rgb(36, 92, 223)',
|
'high-color': 'rgb(36, 92, 223)',
|
||||||
'horizon-blend': 0.1,
|
'horizon-blend': 0.1,
|
||||||
'space-color': 'rgb(156, 240, 255)',
|
'space-color': 'rgb(156, 240, 255)',
|
||||||
});
|
});
|
||||||
map.on('pitch', this.setTerrain.bind(this));
|
map.on('pitch', () => {
|
||||||
this.setTerrain();
|
if (map.getPitch() > 0) {
|
||||||
});
|
map.setTerrain({
|
||||||
map.on('style.import.load', () => {
|
source: 'mapbox-dem',
|
||||||
const basemap = map.getStyle().imports?.find((imprt) => imprt.id === 'basemap');
|
exaggeration: 1,
|
||||||
if (basemap && basemap.data && basemap.data.glyphs) {
|
});
|
||||||
map.setGlyphsUrl(basemap.data.glyphs);
|
} else {
|
||||||
}
|
map.setTerrain(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
map.on('load', () => {
|
map.on('load', () => {
|
||||||
this._map.set(map); // only set the store after the map has loaded
|
this._map.set(map); // only set the store after the map has loaded
|
||||||
window._map = map; // entry point for extensions
|
window._map = map; // entry point for extensions
|
||||||
this.resize();
|
this.resize();
|
||||||
this.setTerrain();
|
|
||||||
scaleControl.setUnit(get(distanceUnits));
|
scaleControl.setUnit(get(distanceUnits));
|
||||||
|
|
||||||
this._onLoadCallbacks.forEach((callback) => callback(map));
|
this._onLoadCallbacks.forEach((callback) => callback(map));
|
||||||
@@ -179,7 +182,6 @@ export class MapboxGLMap {
|
|||||||
scaleControl.setUnit(units);
|
scaleControl.setUnit(units);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
this._unsubscribes.push(terrainSource.subscribe(() => this.setTerrain()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoad(callback: (map: mapboxgl.Map) => void) {
|
onLoad(callback: (map: mapboxgl.Map) => void) {
|
||||||
@@ -220,29 +222,6 @@ export class MapboxGLMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTerrain() {
|
|
||||||
const map = get(this._map);
|
|
||||||
if (map) {
|
|
||||||
const source = get(terrainSource);
|
|
||||||
try {
|
|
||||||
if (!map.getSource(source)) {
|
|
||||||
map.addSource(source, terrainSources[source]);
|
|
||||||
}
|
|
||||||
if (map.getPitch() > 0) {
|
|
||||||
map.setTerrain({
|
|
||||||
source: source,
|
|
||||||
exaggeration: 1,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
map.setTerrain(null);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// No reliable way to check if the map is ready to add sources and layers
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const map = new MapboxGLMap();
|
export const map = new MapboxGLMap();
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import mapboxgl, { type LayerSpecification, type VectorSourceSpecification } fro
|
|||||||
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
|
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
|
||||||
import 'mapillary-js/dist/mapillary.css';
|
import 'mapillary-js/dist/mapillary.css';
|
||||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/map';
|
|
||||||
|
|
||||||
const mapillarySource: VectorSourceSpecification = {
|
const mapillarySource: VectorSourceSpecification = {
|
||||||
type: 'vector',
|
type: 'vector',
|
||||||
@@ -100,10 +99,10 @@ export class MapillaryLayer {
|
|||||||
this.map.addSource('mapillary', mapillarySource);
|
this.map.addSource('mapillary', mapillarySource);
|
||||||
}
|
}
|
||||||
if (!this.map.getLayer('mapillary-sequence')) {
|
if (!this.map.getLayer('mapillary-sequence')) {
|
||||||
this.map.addLayer(mapillarySequenceLayer, ANCHOR_LAYER_KEY.mapillary);
|
this.map.addLayer(mapillarySequenceLayer);
|
||||||
}
|
}
|
||||||
if (!this.map.getLayer('mapillary-image')) {
|
if (!this.map.getLayer('mapillary-image')) {
|
||||||
this.map.addLayer(mapillaryImageLayer, ANCHOR_LAYER_KEY.mapillary);
|
this.map.addLayer(mapillaryImageLayer);
|
||||||
}
|
}
|
||||||
this.map.on('style.load', this.addBinded);
|
this.map.on('style.load', this.addBinded);
|
||||||
this.map.on('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
this.map.on('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
import { Trash2 } from '@lucide/svelte';
|
import { Trash2 } from '@lucide/svelte';
|
||||||
import { ANCHOR_LAYER_KEY, map } from '$lib/components/map/map';
|
import { map } from '$lib/components/map/map';
|
||||||
import type { GeoJSONSource } from 'mapbox-gl';
|
import type { GeoJSONSource } from 'mapbox-gl';
|
||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import { fileActions } from '$lib/logic/file-actions';
|
import { fileActions } from '$lib/logic/file-actions';
|
||||||
@@ -63,18 +63,15 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!$map.getLayer('rectangle')) {
|
if (!$map.getLayer('rectangle')) {
|
||||||
$map.addLayer(
|
$map.addLayer({
|
||||||
{
|
id: 'rectangle',
|
||||||
id: 'rectangle',
|
type: 'fill',
|
||||||
type: 'fill',
|
source: 'rectangle',
|
||||||
source: 'rectangle',
|
paint: {
|
||||||
paint: {
|
'fill-color': 'SteelBlue',
|
||||||
'fill-color': 'SteelBlue',
|
'fill-opacity': 0.5,
|
||||||
'fill-opacity': 0.5,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
ANCHOR_LAYER_KEY.interactions
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
import { MountainSnow } from '@lucide/svelte';
|
import { MountainSnow } from '@lucide/svelte';
|
||||||
|
import { map } from '$lib/components/map/map';
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
@@ -19,7 +20,11 @@
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
class="whitespace-normal h-fit"
|
class="whitespace-normal h-fit"
|
||||||
disabled={!validSelection}
|
disabled={!validSelection}
|
||||||
onclick={() => fileActions.addElevationToSelection()}
|
onclick={() => {
|
||||||
|
if ($map) {
|
||||||
|
fileActions.addElevationToSelection($map);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<MountainSnow size="16" class="shrink-0" />
|
<MountainSnow size="16" class="shrink-0" />
|
||||||
{i18n._('toolbar.elevation.button')}
|
{i18n._('toolbar.elevation.button')}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
let endTime: string | undefined = $state(undefined);
|
let endTime: string | undefined = $state(undefined);
|
||||||
let movingTime: number | undefined = $state(undefined);
|
let movingTime: number | undefined = $state(undefined);
|
||||||
let speed: number | undefined = $state(undefined);
|
let speed: number | undefined = $state(undefined);
|
||||||
let artificial = $state(true);
|
let artificial = $state(false);
|
||||||
|
|
||||||
function toCalendarDate(date: Date): CalendarDate {
|
function toCalendarDate(date: Date): CalendarDate {
|
||||||
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
||||||
@@ -346,7 +346,7 @@
|
|||||||
let fileId = item.getFileId();
|
let fileId = item.getFileId();
|
||||||
fileActionManager.applyToFile(fileId, (file) => {
|
fileActionManager.applyToFile(fileId, (file) => {
|
||||||
if (item instanceof ListFileItem) {
|
if (item instanceof ListFileItem) {
|
||||||
if (artificial && !$gpxStatistics.global.time.moving) {
|
if (artificial || !$gpxStatistics.global.time.moving) {
|
||||||
file.createArtificialTimestamps(
|
file.createArtificialTimestamps(
|
||||||
getDate(startDate!, startTime!),
|
getDate(startDate!, startTime!),
|
||||||
movingTime!
|
movingTime!
|
||||||
@@ -359,7 +359,7 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (item instanceof ListTrackItem) {
|
} else if (item instanceof ListTrackItem) {
|
||||||
if (artificial && !$gpxStatistics.global.time.moving) {
|
if (artificial || !$gpxStatistics.global.time.moving) {
|
||||||
file.createArtificialTimestamps(
|
file.createArtificialTimestamps(
|
||||||
getDate(startDate!, startTime!),
|
getDate(startDate!, startTime!),
|
||||||
movingTime!,
|
movingTime!,
|
||||||
@@ -374,7 +374,7 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (item instanceof ListTrackSegmentItem) {
|
} else if (item instanceof ListTrackSegmentItem) {
|
||||||
if (artificial && !$gpxStatistics.global.time.moving) {
|
if (artificial || !$gpxStatistics.global.time.moving) {
|
||||||
file.createArtificialTimestamps(
|
file.createArtificialTimestamps(
|
||||||
getDate(startDate!, startTime!),
|
getDate(startDate!, startTime!),
|
||||||
movingTime!,
|
movingTime!,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import { minTolerance, ReducedGPXLayerCollection, tolerance } from './utils.svelte';
|
import { minTolerance, ReducedGPXLayerCollection, tolerance } from './reduce.svelte';
|
||||||
|
|
||||||
let props: { class?: string } = $props();
|
let props: { class?: string } = $props();
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ListItem, ListTrackSegmentItem } from '$lib/components/file-list/file-list';
|
import { ListItem, ListTrackSegmentItem } from '$lib/components/file-list/file-list';
|
||||||
import { ANCHOR_LAYER_KEY, map } from '$lib/components/map/map';
|
import { map } from '$lib/components/map/map';
|
||||||
import { fileActions } from '$lib/logic/file-actions';
|
import { fileActions } from '$lib/logic/file-actions';
|
||||||
import { GPXFileStateCollectionObserver, type GPXFileState } from '$lib/logic/file-state';
|
import { GPXFileStateCollectionObserver, type GPXFileState } from '$lib/logic/file-state';
|
||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
|
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
|
||||||
import type { GeoJSONSource } from 'mapbox-gl';
|
import type { GeoJSONSource } from 'mapbox-gl';
|
||||||
import { get, writable } from 'svelte/store';
|
import { get, writable, type Writable } from 'svelte/store';
|
||||||
|
|
||||||
export const minTolerance = 0.1;
|
export const minTolerance = 0.1;
|
||||||
|
|
||||||
@@ -28,15 +28,17 @@ export class ReducedGPXLayer {
|
|||||||
|
|
||||||
update() {
|
update() {
|
||||||
const file = this._fileState.file;
|
const file = this._fileState.file;
|
||||||
if (!file) {
|
const stats = this._fileState.statistics;
|
||||||
|
if (!file || !stats) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||||
let segmentItem = new ListTrackSegmentItem(file._data.id, trackIndex, segmentIndex);
|
let segmentItem = new ListTrackSegmentItem(file._data.id, trackIndex, segmentIndex);
|
||||||
|
let statistics = stats.getStatisticsFor(segmentItem);
|
||||||
this._updateSimplified(segmentItem.getFullId(), [
|
this._updateSimplified(segmentItem.getFullId(), [
|
||||||
segmentItem,
|
segmentItem,
|
||||||
segment.trkpt.length,
|
statistics.local.points.length,
|
||||||
ramerDouglasPeucker(segment.trkpt, minTolerance),
|
ramerDouglasPeucker(statistics.local.points, minTolerance),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -144,18 +146,17 @@ export class ReducedGPXLayerCollection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!map_.getLayer('simplified')) {
|
if (!map_.getLayer('simplified')) {
|
||||||
map_.addLayer(
|
map_.addLayer({
|
||||||
{
|
id: 'simplified',
|
||||||
id: 'simplified',
|
type: 'line',
|
||||||
type: 'line',
|
source: 'simplified',
|
||||||
source: 'simplified',
|
paint: {
|
||||||
paint: {
|
'line-color': 'white',
|
||||||
'line-color': 'white',
|
'line-width': 3,
|
||||||
'line-width': 3,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
ANCHOR_LAYER_KEY.interactions
|
});
|
||||||
);
|
} else {
|
||||||
|
map_.moveLayer('simplified');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@
|
|||||||
{i18n._('toolbar.routing.activity')}
|
{i18n._('toolbar.routing.activity')}
|
||||||
</span>
|
</span>
|
||||||
<Select.Root type="single" bind:value={$routingProfile}>
|
<Select.Root type="single" bind:value={$routingProfile}>
|
||||||
<Select.Trigger class="grow" size="sm">
|
<Select.Trigger class="h-8 grow">
|
||||||
{i18n._(`toolbar.routing.activities.${$routingProfile}`)}
|
{i18n._(`toolbar.routing.activities.${$routingProfile}`)}
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
disabled={!validSelection}
|
disabled={!validSelection}
|
||||||
onclick={fileActions.reverseSelection}
|
onclick={fileActions.reverseSelection}
|
||||||
>
|
>
|
||||||
<ArrowRightLeft class="size-3" />{i18n._('toolbar.routing.reverse.button')}
|
<ArrowRightLeft size="12" />{i18n._('toolbar.routing.reverse.button')}
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
label={i18n._('toolbar.routing.route_back_to_start.tooltip')}
|
label={i18n._('toolbar.routing.route_back_to_start.tooltip')}
|
||||||
@@ -231,7 +231,7 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<House class="size-3" />{i18n._('toolbar.routing.route_back_to_start.button')}
|
<House size="12" />{i18n._('toolbar.routing.route_back_to_start.button')}
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
label={i18n._('toolbar.routing.round_trip.tooltip')}
|
label={i18n._('toolbar.routing.round_trip.tooltip')}
|
||||||
@@ -240,7 +240,7 @@
|
|||||||
disabled={!validSelection}
|
disabled={!validSelection}
|
||||||
onclick={fileActions.createRoundTripForSelection}
|
onclick={fileActions.createRoundTripForSelection}
|
||||||
>
|
>
|
||||||
<Repeat class="size-3" />{i18n._('toolbar.routing.round_trip.button')}
|
<Repeat size="12" />{i18n._('toolbar.routing.round_trip.button')}
|
||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex flex-row gap-2 items-end justify-between">
|
<div class="w-full flex flex-row gap-2 items-end justify-between">
|
||||||
|
|||||||
@@ -793,25 +793,24 @@ export class RoutingControls {
|
|||||||
replacingDistance +=
|
replacingDistance +=
|
||||||
distance(response[i - 1].getCoordinates(), response[i].getCoordinates()) / 1000;
|
distance(response[i - 1].getCoordinates(), response[i].getCoordinates()) / 1000;
|
||||||
}
|
}
|
||||||
let startAnchorStats = stats.getTrackPoint(anchors[0].point._data.index)!;
|
|
||||||
let endAnchorStats = stats.getTrackPoint(
|
|
||||||
anchors[anchors.length - 1].point._data.index
|
|
||||||
)!;
|
|
||||||
|
|
||||||
let replacedDistance =
|
let replacedDistance =
|
||||||
endAnchorStats.distance.moving - startAnchorStats.distance.moving;
|
stats.local.distance.moving[anchors[anchors.length - 1].point._data.index] -
|
||||||
|
stats.local.distance.moving[anchors[0].point._data.index];
|
||||||
|
|
||||||
let newDistance = stats.global.distance.moving + replacingDistance - replacedDistance;
|
let newDistance = stats.global.distance.moving + replacingDistance - replacedDistance;
|
||||||
let newTime = (newDistance / stats.global.speed.moving) * 3600;
|
let newTime = (newDistance / stats.global.speed.moving) * 3600;
|
||||||
|
|
||||||
let remainingTime =
|
let remainingTime =
|
||||||
stats.global.time.moving -
|
stats.global.time.moving -
|
||||||
(endAnchorStats.time.moving - startAnchorStats.time.moving);
|
(stats.local.time.moving[anchors[anchors.length - 1].point._data.index] -
|
||||||
|
stats.local.time.moving[anchors[0].point._data.index]);
|
||||||
let replacingTime = newTime - remainingTime;
|
let replacingTime = newTime - remainingTime;
|
||||||
|
|
||||||
if (replacingTime <= 0) {
|
if (replacingTime <= 0) {
|
||||||
// Fallback to simple time difference
|
// Fallback to simple time difference
|
||||||
replacingTime = endAnchorStats.time.total - startAnchorStats.time.total;
|
replacingTime =
|
||||||
|
stats.local.time.total[anchors[anchors.length - 1].point._data.index] -
|
||||||
|
stats.local.time.total[anchors[0].point._data.index];
|
||||||
}
|
}
|
||||||
|
|
||||||
speed = (replacingDistance / replacingTime) * 3600;
|
speed = (replacingDistance / replacingTime) * 3600;
|
||||||
@@ -821,7 +820,9 @@ export class RoutingControls {
|
|||||||
let endIndex = anchors[anchors.length - 1].point._data.index;
|
let endIndex = anchors[anchors.length - 1].point._data.index;
|
||||||
startTime = new Date(
|
startTime = new Date(
|
||||||
(segment.trkpt[endIndex].time?.getTime() ?? 0) -
|
(segment.trkpt[endIndex].time?.getTime() ?? 0) -
|
||||||
(replacingTime + endAnchorStats.time.total - endAnchorStats.time.moving) *
|
(replacingTime +
|
||||||
|
stats.local.time.total[endIndex] -
|
||||||
|
stats.local.time.moving[endIndex]) *
|
||||||
1000
|
1000
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,12 @@
|
|||||||
|
|
||||||
let validSelection = $derived(
|
let validSelection = $derived(
|
||||||
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
|
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
|
||||||
$gpxStatistics.global.length > 0
|
$gpxStatistics.local.points.length > 0
|
||||||
);
|
);
|
||||||
let maxSliderValue = $derived(
|
let maxSliderValue = $derived(
|
||||||
validSelection && $gpxStatistics.global.length > 0 ? $gpxStatistics.global.length - 1 : 1
|
validSelection && $gpxStatistics.local.points.length > 0
|
||||||
|
? $gpxStatistics.local.points.length - 1
|
||||||
|
: 1
|
||||||
);
|
);
|
||||||
let sliderValues = $derived([0, maxSliderValue]);
|
let sliderValues = $derived([0, maxSliderValue]);
|
||||||
let canCrop = $derived(sliderValues[0] != 0 || sliderValues[1] != maxSliderValue);
|
let canCrop = $derived(sliderValues[0] != 0 || sliderValues[1] != maxSliderValue);
|
||||||
@@ -43,7 +45,7 @@
|
|||||||
function updateSlicedGPXStatistics() {
|
function updateSlicedGPXStatistics() {
|
||||||
if (validSelection && canCrop) {
|
if (validSelection && canCrop) {
|
||||||
$slicedGPXStatistics = [
|
$slicedGPXStatistics = [
|
||||||
get(gpxStatistics).sliced(sliderValues[0], sliderValues[1]),
|
get(gpxStatistics).slice(sliderValues[0], sliderValues[1]),
|
||||||
sliderValues[0],
|
sliderValues[0],
|
||||||
sliderValues[1],
|
sliderValues[1],
|
||||||
];
|
];
|
||||||
@@ -105,7 +107,7 @@
|
|||||||
{i18n._('toolbar.scissors.split_as')}
|
{i18n._('toolbar.scissors.split_as')}
|
||||||
</span>
|
</span>
|
||||||
<Select.Root bind:value={$splitAs} type="single">
|
<Select.Root bind:value={$splitAs} type="single">
|
||||||
<Select.Trigger class="w-fit grow" size="sm">
|
<Select.Trigger class="h-8 w-fit grow">
|
||||||
{i18n._('gpx.' + $splitAs)}
|
{i18n._('gpx.' + $splitAs)}
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { TrackPoint, TrackSegment } from 'gpx';
|
||||||
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
|
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
|
||||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
||||||
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
|
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
|
||||||
@@ -7,42 +9,20 @@ import { gpxStatistics } from '$lib/logic/statistics';
|
|||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { fileStateCollection } from '$lib/logic/file-state';
|
import { fileStateCollection } from '$lib/logic/file-state';
|
||||||
import { fileActions } from '$lib/logic/file-actions';
|
import { fileActions } from '$lib/logic/file-actions';
|
||||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
|
||||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/map';
|
|
||||||
|
|
||||||
export class SplitControls {
|
export class SplitControls {
|
||||||
|
active: boolean = false;
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
|
controls: ControlWithMarker[] = [];
|
||||||
|
shownControls: ControlWithMarker[] = [];
|
||||||
unsubscribes: Function[] = [];
|
unsubscribes: Function[] = [];
|
||||||
|
|
||||||
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
|
toggleControlsForZoomLevelAndBoundsBinded: () => void =
|
||||||
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
|
this.toggleControlsForZoomLevelAndBounds.bind(this);
|
||||||
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
|
|
||||||
|
|
||||||
constructor(map: mapboxgl.Map) {
|
constructor(map: mapboxgl.Map) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
|
|
||||||
if (!this.map.hasImage('split-control')) {
|
|
||||||
let icon = new Image(100, 100);
|
|
||||||
icon.onload = () => {
|
|
||||||
if (!this.map.hasImage('split-control')) {
|
|
||||||
this.map.addImage('split-control', icon);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Lucide icons are SVG files with a 24x24 viewBox
|
|
||||||
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
|
|
||||||
icon.src =
|
|
||||||
'data:image/svg+xml,' +
|
|
||||||
encodeURIComponent(`
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
|
||||||
<circle cx="20" cy="20" r="20" fill="white" />
|
|
||||||
<g transform="translate(8 8)">
|
|
||||||
${Scissors.replace('stroke="currentColor"', 'stroke="black"')}
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
|
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
|
||||||
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
|
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
|
||||||
this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this)));
|
this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this)));
|
||||||
@@ -51,18 +31,29 @@ export class SplitControls {
|
|||||||
addIfNeeded() {
|
addIfNeeded() {
|
||||||
let scissors = get(currentTool) === Tool.SCISSORS;
|
let scissors = get(currentTool) === Tool.SCISSORS;
|
||||||
if (!scissors) {
|
if (!scissors) {
|
||||||
this.remove();
|
if (this.active) {
|
||||||
|
this.remove();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateControls();
|
if (this.active) {
|
||||||
|
this.updateControls();
|
||||||
|
} else {
|
||||||
|
this.add();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add() {
|
||||||
|
this.active = true;
|
||||||
|
|
||||||
|
this.map.on('zoom', this.toggleControlsForZoomLevelAndBoundsBinded);
|
||||||
|
this.map.on('move', this.toggleControlsForZoomLevelAndBoundsBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateControls() {
|
updateControls() {
|
||||||
let data: GeoJSON.FeatureCollection = {
|
// Update the markers when the files change
|
||||||
type: 'FeatureCollection',
|
let controlIndex = 0;
|
||||||
features: [],
|
|
||||||
};
|
|
||||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||||
let file = fileStateCollection.getFile(fileId);
|
let file = fileStateCollection.getFile(fileId);
|
||||||
|
|
||||||
@@ -73,23 +64,30 @@ export class SplitControls {
|
|||||||
new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
|
new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
for (let i = 1; i < segment.trkpt.length - 1; i++) {
|
for (let point of segment.trkpt.slice(1, -1)) {
|
||||||
let point = segment.trkpt[i];
|
// Update the existing controls (could be improved by matching the existing controls with the new ones?)
|
||||||
if (point._data.anchor) {
|
if (point._data.anchor) {
|
||||||
data.features.push({
|
if (controlIndex < this.controls.length) {
|
||||||
type: 'Feature',
|
this.controls[controlIndex].fileId = fileId;
|
||||||
geometry: {
|
this.controls[controlIndex].point = point;
|
||||||
type: 'Point',
|
this.controls[controlIndex].segment = segment;
|
||||||
coordinates: [point.getLongitude(), point.getLatitude()],
|
this.controls[controlIndex].trackIndex = trackIndex;
|
||||||
},
|
this.controls[controlIndex].segmentIndex = segmentIndex;
|
||||||
properties: {
|
this.controls[controlIndex].marker.setLngLat(
|
||||||
fileId: fileId,
|
point.getCoordinates()
|
||||||
trackIndex: trackIndex,
|
);
|
||||||
segmentIndex: segmentIndex,
|
} else {
|
||||||
pointIndex: i,
|
this.controls.push(
|
||||||
minZoom: point._data.zoom,
|
this.createControl(
|
||||||
},
|
point,
|
||||||
});
|
segment,
|
||||||
|
fileId,
|
||||||
|
trackIndex,
|
||||||
|
segmentIndex
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
controlIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,78 +95,86 @@ export class SplitControls {
|
|||||||
}
|
}
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
try {
|
while (controlIndex < this.controls.length) {
|
||||||
let source = this.map.getSource('split-controls') as mapboxgl.GeoJSONSource | undefined;
|
// Remove the extra controls
|
||||||
if (source) {
|
this.controls.pop()?.marker.remove();
|
||||||
source.setData(data);
|
|
||||||
} else {
|
|
||||||
this.map.addSource('split-controls', {
|
|
||||||
type: 'geojson',
|
|
||||||
data: data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.map.getLayer('split-controls')) {
|
|
||||||
this.map.addLayer(
|
|
||||||
{
|
|
||||||
id: 'split-controls',
|
|
||||||
type: 'symbol',
|
|
||||||
source: 'split-controls',
|
|
||||||
layout: {
|
|
||||||
'icon-image': 'split-control',
|
|
||||||
'icon-size': 0.25,
|
|
||||||
'icon-padding': 0,
|
|
||||||
},
|
|
||||||
filter: ['<=', ['get', 'minZoom'], ['zoom']],
|
|
||||||
},
|
|
||||||
ANCHOR_LAYER_KEY.interactions
|
|
||||||
);
|
|
||||||
|
|
||||||
this.map.on('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
|
|
||||||
this.map.on('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
|
|
||||||
this.map.on('click', 'split-controls', this.layerOnClickBinded);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// No reliable way to check if the map is ready to add sources and layers
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.toggleControlsForZoomLevelAndBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.map.off('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
|
this.active = false;
|
||||||
this.map.off('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
|
|
||||||
this.map.off('click', 'split-controls', this.layerOnClickBinded);
|
|
||||||
|
|
||||||
try {
|
for (let control of this.controls) {
|
||||||
if (this.map.getLayer('split-controls')) {
|
control.marker.remove();
|
||||||
this.map.removeLayer('split-controls');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.map.getSource('split-controls')) {
|
|
||||||
this.map.removeSource('split-controls');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// No reliable way to check if the map is ready to remove sources and layers
|
|
||||||
}
|
}
|
||||||
|
this.map.off('zoom', this.toggleControlsForZoomLevelAndBoundsBinded);
|
||||||
|
this.map.off('move', this.toggleControlsForZoomLevelAndBoundsBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
layerOnMouseEnter(e: any) {
|
toggleControlsForZoomLevelAndBounds() {
|
||||||
mapCursor.notify(MapCursorState.SPLIT_CONTROL, true);
|
// Show markers only if they are in the current zoom level and bounds
|
||||||
|
this.shownControls.splice(0, this.shownControls.length);
|
||||||
|
|
||||||
|
let southWest = this.map.unproject([0, this.map.getCanvas().height]);
|
||||||
|
let northEast = this.map.unproject([this.map.getCanvas().width, 0]);
|
||||||
|
let bounds = new mapboxgl.LngLatBounds(southWest, northEast);
|
||||||
|
|
||||||
|
let zoom = this.map.getZoom();
|
||||||
|
this.controls.forEach((control) => {
|
||||||
|
control.inZoom = control.point._data.zoom <= zoom;
|
||||||
|
if (control.inZoom && bounds.contains(control.marker.getLngLat())) {
|
||||||
|
control.marker.addTo(this.map);
|
||||||
|
this.shownControls.push(control);
|
||||||
|
} else {
|
||||||
|
control.marker.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
layerOnMouseLeave() {
|
createControl(
|
||||||
mapCursor.notify(MapCursorState.SPLIT_CONTROL, false);
|
point: TrackPoint,
|
||||||
}
|
segment: TrackSegment,
|
||||||
|
fileId: string,
|
||||||
|
trackIndex: number,
|
||||||
|
segmentIndex: number
|
||||||
|
): ControlWithMarker {
|
||||||
|
let element = document.createElement('div');
|
||||||
|
element.className = `h-6 w-6 p-0.5 rounded-full bg-white border-2 border-black cursor-pointer`;
|
||||||
|
element.innerHTML = Scissors.replace('width="24"', '')
|
||||||
|
.replace('height="24"', '')
|
||||||
|
.replace('stroke="currentColor"', 'stroke="black"');
|
||||||
|
|
||||||
layerOnClick(e: mapboxgl.MapMouseEvent) {
|
let marker = new mapboxgl.Marker({
|
||||||
let coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates;
|
draggable: true,
|
||||||
fileActions.split(
|
className: 'z-10',
|
||||||
get(splitAs),
|
element,
|
||||||
e.features![0].properties!.fileId,
|
}).setLngLat(point.getCoordinates());
|
||||||
e.features![0].properties!.trackIndex,
|
|
||||||
e.features![0].properties!.segmentIndex,
|
let control = {
|
||||||
{ lon: coordinates[0], lat: coordinates[1] },
|
point,
|
||||||
e.features![0].properties!.pointIndex
|
segment,
|
||||||
);
|
fileId,
|
||||||
|
trackIndex,
|
||||||
|
segmentIndex,
|
||||||
|
marker,
|
||||||
|
inZoom: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
marker.getElement().addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
fileActions.split(
|
||||||
|
get(splitAs),
|
||||||
|
control.fileId,
|
||||||
|
control.trackIndex,
|
||||||
|
control.segmentIndex,
|
||||||
|
control.point.getCoordinates(),
|
||||||
|
control.point._data.index
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return control;
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
@@ -176,3 +182,16 @@ export class SplitControls {
|
|||||||
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Control = {
|
||||||
|
segment: TrackSegment;
|
||||||
|
fileId: string;
|
||||||
|
trackIndex: number;
|
||||||
|
segmentIndex: number;
|
||||||
|
point: TrackPoint;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ControlWithMarker = Control & {
|
||||||
|
marker: mapboxgl.Marker;
|
||||||
|
inZoom: boolean;
|
||||||
|
};
|
||||||
|
|||||||
@@ -16,8 +16,6 @@
|
|||||||
import { fileActions } from '$lib/logic/file-actions';
|
import { fileActions } from '$lib/logic/file-actions';
|
||||||
import { map } from '$lib/components/map/map';
|
import { map } from '$lib/components/map/map';
|
||||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||||
import mapboxgl from 'mapbox-gl';
|
|
||||||
import { getSvgForSymbol } from '$lib/components/map/gpx-layer/gpx-layer';
|
|
||||||
|
|
||||||
let props: {
|
let props: {
|
||||||
class?: string;
|
class?: string;
|
||||||
@@ -41,21 +39,6 @@
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
let marker: mapboxgl.Marker | null = null;
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
if ($selectedWaypoint) {
|
|
||||||
selectedWaypoint.reset();
|
|
||||||
} else {
|
|
||||||
name = '';
|
|
||||||
description = '';
|
|
||||||
link = '';
|
|
||||||
sym = '';
|
|
||||||
longitude = 0;
|
|
||||||
latitude = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($selectedWaypoint) {
|
if ($selectedWaypoint) {
|
||||||
const wpt = $selectedWaypoint[0];
|
const wpt = $selectedWaypoint[0];
|
||||||
@@ -71,7 +54,14 @@
|
|||||||
latitude = parseFloat(wpt.getLatitude().toFixed(6));
|
latitude = parseFloat(wpt.getLatitude().toFixed(6));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
untrack(reset);
|
untrack(() => {
|
||||||
|
name = '';
|
||||||
|
description = '';
|
||||||
|
link = '';
|
||||||
|
sym = '';
|
||||||
|
longitude = 0;
|
||||||
|
latitude = 0;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,14 +85,14 @@
|
|||||||
desc: description.length > 0 ? description : undefined,
|
desc: description.length > 0 ? description : undefined,
|
||||||
cmt: description.length > 0 ? description : undefined,
|
cmt: description.length > 0 ? description : undefined,
|
||||||
link: link.length > 0 ? { attributes: { href: link } } : undefined,
|
link: link.length > 0 ? { attributes: { href: link } } : undefined,
|
||||||
sym: sym.length > 0 ? sym : undefined,
|
sym: sym,
|
||||||
},
|
},
|
||||||
selectedWaypoint.wpt && selectedWaypoint.fileId
|
selectedWaypoint.wpt && selectedWaypoint.fileId
|
||||||
? new ListWaypointItem(selectedWaypoint.fileId, selectedWaypoint.wpt._data.index)
|
? new ListWaypointItem(selectedWaypoint.fileId, selectedWaypoint.wpt._data.index)
|
||||||
: undefined
|
: undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
reset();
|
selectedWaypoint.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCoordinates(e: any) {
|
function setCoordinates(e: any) {
|
||||||
@@ -110,37 +100,6 @@
|
|||||||
longitude = e.lngLat.lng.toFixed(6);
|
longitude = e.lngLat.lng.toFixed(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if ($selectedWaypoint) {
|
|
||||||
if (marker) {
|
|
||||||
marker.remove();
|
|
||||||
marker = null;
|
|
||||||
}
|
|
||||||
} else if (latitude != 0 || longitude != 0) {
|
|
||||||
if ($map) {
|
|
||||||
if (marker) {
|
|
||||||
marker.setLngLat([longitude, latitude]).getElement().innerHTML =
|
|
||||||
getSvgForSymbol(symbolKey);
|
|
||||||
} else {
|
|
||||||
let element = document.createElement('div');
|
|
||||||
element.classList.add('w-8', 'h-8');
|
|
||||||
element.innerHTML = getSvgForSymbol(symbolKey);
|
|
||||||
marker = new mapboxgl.Marker({
|
|
||||||
element,
|
|
||||||
anchor: 'bottom',
|
|
||||||
})
|
|
||||||
.setLngLat([longitude, latitude])
|
|
||||||
.addTo($map);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (marker) {
|
|
||||||
marker.remove();
|
|
||||||
marker = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if ($map) {
|
if ($map) {
|
||||||
$map.on('click', setCoordinates);
|
$map.on('click', setCoordinates);
|
||||||
@@ -153,10 +112,6 @@
|
|||||||
$map.off('click', setCoordinates);
|
$map.off('click', setCoordinates);
|
||||||
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false);
|
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false);
|
||||||
}
|
}
|
||||||
if (marker) {
|
|
||||||
marker.remove();
|
|
||||||
marker = null;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -174,27 +129,19 @@
|
|||||||
bind:value={description}
|
bind:value={description}
|
||||||
id="description"
|
id="description"
|
||||||
disabled={!canCreate && !$selectedWaypoint}
|
disabled={!canCreate && !$selectedWaypoint}
|
||||||
class="min-h-8 h-8 py-1 px-3 text-sm"
|
|
||||||
/>
|
/>
|
||||||
<Label for="symbol">{i18n._('toolbar.waypoint.icon')}</Label>
|
<Label for="symbol">{i18n._('toolbar.waypoint.icon')}</Label>
|
||||||
<Select.Root bind:value={sym} type="single">
|
<Select.Root bind:value={sym} type="single">
|
||||||
<Select.Trigger
|
<Select.Trigger
|
||||||
id="symbol"
|
id="symbol"
|
||||||
size="sm"
|
class="w-full h-8"
|
||||||
class="w-full"
|
|
||||||
disabled={!canCreate && !$selectedWaypoint}
|
disabled={!canCreate && !$selectedWaypoint}
|
||||||
>
|
>
|
||||||
<span class="flex flex-row gap-1.5 items-center">
|
{#if symbolKey}
|
||||||
{#if symbolKey}
|
{i18n._(`gpx.symbol.${symbolKey}`)}
|
||||||
{#if symbols[symbolKey].icon}
|
{:else}
|
||||||
{@const Component = symbols[symbolKey].icon}
|
{sym}
|
||||||
<Component size="14" />
|
{/if}
|
||||||
{/if}
|
|
||||||
{i18n._(`gpx.symbol.${symbolKey}`)}
|
|
||||||
{:else}
|
|
||||||
{sym}
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content class="max-h-60 overflow-y-scroll">
|
<Select.Content class="max-h-60 overflow-y-scroll">
|
||||||
{#each sortedSymbols as [key, symbol]}
|
{#each sortedSymbols as [key, symbol]}
|
||||||
@@ -202,7 +149,7 @@
|
|||||||
<span>
|
<span>
|
||||||
{#if symbol.icon}
|
{#if symbol.icon}
|
||||||
{@const Component = symbol.icon}
|
{@const Component = symbol.icon}
|
||||||
<Component size="14" class="inline-block align-sub" />
|
<Component size="14" class="inline-block align-sub mr-0.5" />
|
||||||
{:else}
|
{:else}
|
||||||
<span class="w-4 inline-block"></span>
|
<span class="w-4 inline-block"></span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -263,7 +210,7 @@
|
|||||||
{i18n._('toolbar.waypoint.create')}
|
{i18n._('toolbar.waypoint.create')}
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="icon" onclick={reset}>
|
<Button variant="outline" size="icon" onclick={() => selectedWaypoint.reset()}>
|
||||||
<CircleX size="16" />
|
<CircleX size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ Pots arrossegar y deixar arxius directament des del seu sistema d'arxius cap a l
|
|||||||
|
|
||||||
Crear una còpia dels arxius seleccionats.
|
Crear una còpia dels arxius seleccionats.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Esborra
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||||
|
|
||||||
Esborra l'arxiu seleccinat.
|
Delete the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Esborra-ho tot
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||||
|
|
||||||
Esborra tots els fitxers.
|
Delete all files.
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportar...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportar...
|
||||||
|
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ Soubory můžete také přetáhnout přímo ze souborového systému do okna.
|
|||||||
|
|
||||||
Vytvořit kopii aktuálně vybraných souborů.
|
Vytvořit kopii aktuálně vybraných souborů.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Smazat
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||||
|
|
||||||
Smazat aktuálně vybrané soubory.
|
Smazat aktuálně vybrané soubory.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Smazat vše
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||||
|
|
||||||
Smazat všechny soubory.
|
Smazat všechny soubory.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Mapbox stellt einige der auf dieser Website verwendeten Karten bereit.
|
Mapbox ist das Unternehmen, das einige der schönen Karten auf dieser Website zur Verfügung stellt.
|
||||||
Sie entwickeln auch die <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">Karten-Engine</a>, die **gpx.studio** unterstützt.
|
Sie entwickeln auch die <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">Karten-Engine</a> welche **gpx.studio** unterstützt.
|
||||||
|
|
||||||
Wir sind froh und dankbar, Teil ihres <a href="https://mapbox.com/community" target="_blank">Community</a> Programms zu sein, das gemeinnützige Organisationen, Bildungseinrichtungen und Organisationen unterstützt.
|
Wir sind äusserst glücklich und dankbar, Teil ihres <a href="https://mapbox.com/community" target="_blank">Community</a> Programms zu sein, das gemeinnützige Organisationen, Bildungseinrichtungen und Organisationen mit positivem Einfluss unterstützt.
|
||||||
Diese Partnerschaft ermöglicht es **gpx.studio**, von den Mapbox-Tools zu ermäßigten Preisen zu profitieren, was erheblich zur finanziellen Tragfähigkeit des Projekts beiträgt und es uns ermöglicht, die bestmögliche Benutzererfahrung zu bieten.
|
Diese Partnerschaft ermöglicht es **gpx.studio**, von den Mapbox-Tools zu ermäßigten Preisen zu profitieren, was erheblich zur finanziellen Tragfähigkeit des Projekts beiträgt und es uns ermöglicht, die bestmögliche Benutzererfahrung zu bieten.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: Opciones de vista
|
title: View options
|
||||||
---
|
---
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ Beste era batez, fitxategiak zuzenean arrastatu eta jaregin ditzakezu zure fitxa
|
|||||||
|
|
||||||
Sortu hautatutako fitxategien kopia bat.
|
Sortu hautatutako fitxategien kopia bat.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Ezabatu
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||||
|
|
||||||
Ezabatu hautatutako fitxategiak.
|
Delete the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Ezabatu guztiak
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||||
|
|
||||||
Ezabatu fitxategi guztiak.
|
Delete all files.
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Esportatu...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Esportatu...
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Supprimer les fichiers sélectionnés.
|
|||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Supprimer tout
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Supprimer tout
|
||||||
|
|
||||||
Supprimer tous les fichiers.
|
Supprimer toutes les fichiers.
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exporter...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exporter...
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ Facendo clic destro su una scheda file, è possibile accedere alle stesse azioni
|
|||||||
|
|
||||||
Come accennato nella [sezione opzioni di visualizzazione](./menu/view), è possibile passare a un layout ad albero per l'elenco dei file.
|
Come accennato nella [sezione opzioni di visualizzazione](./menu/view), è possibile passare a un layout ad albero per l'elenco dei file.
|
||||||
Questo layout è ideale per gestire un gran numero di file aperti, organizzandoli in una lista verticale sul lato destro della mappa.
|
Questo layout è ideale per gestire un gran numero di file aperti, organizzandoli in una lista verticale sul lato destro della mappa.
|
||||||
Inoltre, la vista ad albero dei file consente d'ispezionare [tracce, segmenti e punti di interesse](./gpx) all'interno dei file attraverso sezioni espandibili.
|
Inoltre, la vista ad albero dei file consente di ispezionare [tracce, segmenti e punti di interesse](./gpx) all'interno dei file attraverso sezioni espandibili.
|
||||||
|
|
||||||
Puoi anche applicare [modifiche](./menu/edit) e [strumenti](./toolbar) agli elementi interni del file.
|
Puoi anche applicare [modifiche](./menu/edit) e [strumenti](./toolbar) agli elementi interni del file.
|
||||||
Inoltre, è possibile trascinare e rilasciare gli elementi per riordinarli, o spostarli nella gerarchia o anche in un altro file.
|
Inoltre, è possibile trascinare e rilasciare gli elementi per riordinarli, o spostarli nella gerarchia o anche in un altro file.
|
||||||
@@ -78,7 +78,7 @@ Quando si passa sopra il profilo di elevazione, un suggerimento mostrerà le sta
|
|||||||
Per ottenere le statistiche per una sezione specifica del profilo di elevazione, è possibile trascinare un rettangolo di selezione sul profilo.
|
Per ottenere le statistiche per una sezione specifica del profilo di elevazione, è possibile trascinare un rettangolo di selezione sul profilo.
|
||||||
Fare clic sul profilo per resettare la selezione.
|
Fare clic sul profilo per resettare la selezione.
|
||||||
|
|
||||||
È inoltre possibile utilizzare la rotellina del mouse per ingrandire e rimpicciolire sul profilo di elevazione, e spostarsi a sinistra e a destra trascinando il profilo tenendo premuto il tasto <kbd>Maiuscolo</kbd>.
|
È inoltre possibile utilizzare la rotellina del mouse per ingrandire e rimpicciolire sul profilo di elevazione, e spostarsi a sinistra e a destra trascinando il profilo tenendo premuto il tasto <kbd>Maiusc</kbd>.
|
||||||
|
|
||||||
<div class="h-48 w-full">
|
<div class="h-48 w-full">
|
||||||
<ElevationProfile
|
<ElevationProfile
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Queste sono organizzate in una struttura gerarchica, con le tracce stesse al liv
|
|||||||
- Una **traccia** è composta da una sequenza di segmenti scollegati.
|
- Una **traccia** è composta da una sequenza di segmenti scollegati.
|
||||||
Inoltre, può contenere metadati come un **nome**, una **descrizione**, e **proprietà di visualizzazione**.
|
Inoltre, può contenere metadati come un **nome**, una **descrizione**, e **proprietà di visualizzazione**.
|
||||||
- Un **segmento** è una sequenza di punti GPS che formano un percorso continuo.
|
- Un **segmento** è una sequenza di punti GPS che formano un percorso continuo.
|
||||||
- Un **punto GPS** è una posizione con una latitudine, una longitudine, ed eventualmente una marcatura temporale e un'altitudine.
|
- Un **punto GPS** è una posizione con una latitudine, una longitudine, ed eventualmente un timestamp e un'altitudine.
|
||||||
Alcuni dispositivi memorizzano anche informazioni aggiuntive come frequenza cardiaca, cadenza, temperatura e potenza.
|
Alcuni dispositivi memorizzano anche informazioni aggiuntive come frequenza cardiaca, cadenza, temperatura e potenza.
|
||||||
|
|
||||||
Nella maggior parte dei casi, i file GPX contengono una singola traccia con un singolo segmento.
|
Nella maggior parte dei casi, i file GPX contengono una singola traccia con un singolo segmento.
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Aiuta a mantenere il sito gratuito (e senza pubblicità)
|
## <HeartHandshake size="18" class="inline-block align-baseline" /> Aiuta a mantenere il sito gratuito (e senza pubblicità)
|
||||||
|
|
||||||
Ogni volta che aggiungi o sposti i punti GPS, i nostri server calcolano il percorso migliore sulla rete stradale.
|
Ogni volta che aggiungi o sposti i punti GPS, i nostri server calcolano il percorso migliore sulla rete stradale.
|
||||||
Utilizziamo anche le API di <a href="https://mapbox.com" target="_blank">Mapbox</a> per visualizzare mappe stupende, recuperare i dati altimetrici e consentire la ricerca di luoghi.
|
Utilizziamo anche le API di <a href="https://mapbox.com" target="_blank">Mapbox</a> per visualizzare mappe gradevoli, recuperare i dati altimetrici e consentire la ricerca di luoghi.
|
||||||
|
|
||||||
Sfortunatamente, fare tutto ciò è costoso.
|
Sfortunatamente, questo è costoso.
|
||||||
Se ti piace utilizzare questo strumento e lo trovi utile, per favore considera di fare una piccola donazione per aiutare a mantenere il sito web gratuito e senza pubblicità.
|
Se ti piace utilizzare questo strumento e lo trovi utile, per favore considera di fare una piccola donazione per aiutare a mantenere il sito web gratuito e senza pubblicità.
|
||||||
|
|
||||||
Grazie mille per il vostro supporto! ❤️
|
Grazie mille per il vostro supporto! ❤️
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ cÈ inoltre possibile trascinare i file direttamente dal file system del tuo Pc
|
|||||||
|
|
||||||
Crea una copia dei file attualmente selezionati.
|
Crea una copia dei file attualmente selezionati.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" />Elimina
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||||
|
|
||||||
Elimina i file attualmente selezionati.
|
Delete the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" />Cancella tutto
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||||
|
|
||||||
Elimina tutti i file.
|
Delete all files.
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Esporta...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Esporta...
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Deze handleiding zal je door alle componenten en gereedschappen van de interface
|
|||||||
<DocsImage src="getting-started/interface" alt="De gpx.studio interface." />
|
<DocsImage src="getting-started/interface" alt="De gpx.studio interface." />
|
||||||
|
|
||||||
Zoals weergegeven in bovenstaande scherm, is de interface verdeeld in vier hoofddelen rond de kaart.
|
Zoals weergegeven in bovenstaande scherm, is de interface verdeeld in vier hoofddelen rond de kaart.
|
||||||
Voordat we in de details van elke sectie duiken, eerst een snel overzicht van de interface.
|
Voordat we in de details van elke sectie duiken, hebben we een snel overzicht van de interface.
|
||||||
|
|
||||||
## Menu
|
## Menu
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ Deze actie is alleen beschikbaar wanneer de verticale indeling van de bestandsli
|
|||||||
|
|
||||||
### <ClipboardPaste size="16" class="inline-block" style="margin-bottom: 2px" /> Plakken
|
### <ClipboardPaste size="16" class="inline-block" style="margin-bottom: 2px" /> Plakken
|
||||||
|
|
||||||
Plak de bestandsitems van het klembord naar het huidige hiërarchieniveau indien compatibel.
|
Plak de bestandsitems van het klembord naar het huidige hiërarchie niveau indien compatibel.
|
||||||
|
|
||||||
<DocsNote>
|
<DocsNote>
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ title: FAQ
|
|||||||
|
|
||||||
# { title }
|
# { title }
|
||||||
|
|
||||||
### Czy muszę przekazać darowiznę za korzystanie ze strony internetowej?
|
### Do I need to donate to use the website?
|
||||||
|
|
||||||
Nie.
|
Nie.
|
||||||
Strona internetowa jest darmowa i zawsze będzie (o ile będzie się zgadzał rachunek).
|
The website is free to use and always will be (as long as it is financially sustainable).
|
||||||
Darowizny są jednak doceniane i pomagają utrzymać funkcjonowanie strony.
|
However, donations are appreciated and help keep the website running.
|
||||||
|
|
||||||
### Why is this route chosen over that one? _Or_ how can I add something to the map?
|
### Why is this route chosen over that one? _Or_ how can I add something to the map?
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ title: Wprowadzenie
|
|||||||
|
|
||||||
# { title }
|
# { title }
|
||||||
|
|
||||||
Witamy w oficjalnym przewodniku dla **gpx.studio**!
|
Welcome to the official guide for **gpx.studio**!
|
||||||
Ten przewodnik przeprowadzi Cię przez wszystkie komponenty i narzędzia interfejsu, co pomoże stać się wydajnym użytkownikiem aplikacji.
|
This guide will walk you through all the components and tools of the interface, helping you become a proficient user of the application.
|
||||||
|
|
||||||
<DocsImage src="getting-started/interface" alt="The gpx.studio interface." />
|
<DocsImage src="getting-started/interface" alt="The gpx.studio interface." />
|
||||||
|
|
||||||
Jak pokazano na zrzucie ekranu powyżej, interfejs jest podzielony na cztery główne sekcje zorganizowane wokół mapy.
|
As shown in the screenshot above, the interface is divided into four main sections organized around the map.
|
||||||
Before we dive into the details of each section, let's have a quick overview of the interface.
|
Before we dive into the details of each section, let's have a quick overview of the interface.
|
||||||
|
|
||||||
## Menu główne
|
## Menu główne
|
||||||
@@ -31,7 +31,7 @@ In the [dedicated section](./files-and-stats), we will explain how to select mul
|
|||||||
|
|
||||||
On the left side of the interface, you will find the [toolbar](./toolbar), which contains all the tools you can use to edit your files.
|
On the left side of the interface, you will find the [toolbar](./toolbar), which contains all the tools you can use to edit your files.
|
||||||
|
|
||||||
## Sterowanie mapą
|
## Map controls
|
||||||
|
|
||||||
Na koniec, po prawej stronie interfejsu znajdziesz [sterowanie mapą] (./map-controls).
|
Finally, on the right side of the interface, you will find the [map controls](./map-controls).
|
||||||
Za pomocą tych elementów sterujących można poruszać się po mapie, powiększać i pomniejszać widok oraz przełączać się między różnymi stylami mapy.
|
These controls allow you to navigate the map, zoom in and out, and switch between different map styles.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { HeartHandshake } from '@lucide/svelte';
|
import { HeartHandshake } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Pomóż nam utrzymać tę stronę jako bezpłatną (i wolną od reklam)
|
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||||
|
|
||||||
Za każdym razem, gdy dodasz lub przenosisz punkty GPS, nasze serwery obliczają najlepszą trasę w sieci drogowej.
|
Za każdym razem, gdy dodasz lub przenosisz punkty GPS, nasze serwery obliczają najlepszą trasę w sieci drogowej.
|
||||||
Używamy również API z <a href="https://mapbox.com" target="_blank">Mapbox</a> do wyświetlania pięknych map, pobierania danych wysokości i wyszukiwania miejsc.
|
Używamy również API z <a href="https://mapbox.com" target="_blank">Mapbox</a> do wyświetlania pięknych map, pobierania danych wysokości i wyszukiwania miejsc.
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
import { Languages } from '@lucide/svelte';
|
import { Languages } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <Languages size="18" class="inline-block align-baseline" /> Tłumaczenie
|
## <Languages size="18" class="inline-block align-baseline" /> Translation
|
||||||
|
|
||||||
Strona internetowa jest tłumaczona przez wolontariuszy na platformie do współpracy w tłumaczeniu.
|
Strona internetowa jest tłumaczona przez wolontariuszy na platformie do współpracy w tłumaczeniu.
|
||||||
Możesz przyczynić się do rozwoju naszego projektu, dodając lub ulepszając tłumaczenia w ramach <a href="https://crowdin.com/project/gpxstudio" target="_blank">projektu Crowdin</a>.
|
Możesz pomóc dodając i sprawdzając istniejące tłumaczenie w <a href="https://crowdin.com/project/gpxstudio" target="_blank">projekcie Crowdin</a>.
|
||||||
|
|
||||||
Jeśli chciałbyś dodać język, którego nie ma na liście <a href="#contact">skontaktuj się z nami</a>.
|
Jeśli chciałbyś dodać język, którego nie ma na liście <a href="#contact">skontaktuj się</a>.
|
||||||
|
|
||||||
Każda pomoc jest bardzo mile widziana!
|
Każda pomoc jest bardzo mile widziana!
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: Sterowanie mapą
|
title: Map controls
|
||||||
---
|
---
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -10,10 +10,10 @@ title: Sterowanie mapą
|
|||||||
|
|
||||||
# { title }
|
# { title }
|
||||||
|
|
||||||
Elementy sterujące mapą znajdują się po prawej stronie interfejsu.
|
The map controls are located on the right side of the interface.
|
||||||
Za pomocą tych elementów sterujących można poruszać się po mapie, powiększać i pomniejszać widok oraz przełączać się między różnymi stylami mapy.
|
These controls allow you to navigate the map, zoom in and out, and switch between different map styles.
|
||||||
|
|
||||||
### <Diff size="16" class="inline-block" style="margin-bottom: 2px" /> Nawigacja mapą
|
### <Diff size="16" class="inline-block" style="margin-bottom: 2px" /> Map navigation
|
||||||
|
|
||||||
The controls at the top allow you to zoom in <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> and out <Minus size="16" class="inline-block" style="margin-bottom: 2px" />, and to change the orientation and tilt of the map <Compass size="16" class="inline-block" style="margin-bottom: 2px" />.
|
The controls at the top allow you to zoom in <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> and out <Minus size="16" class="inline-block" style="margin-bottom: 2px" />, and to change the orientation and tilt of the map <Compass size="16" class="inline-block" style="margin-bottom: 2px" />.
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ This only works if you have allowed your browser and <b>gpx.studio</b> to access
|
|||||||
|
|
||||||
### <PersonStanding size="16" class="inline-block" style="margin-bottom: 2px" /> Street view
|
### <PersonStanding size="16" class="inline-block" style="margin-bottom: 2px" /> Street view
|
||||||
|
|
||||||
Ten przycisk może być użyty do włączenia trybu street view na mapie.
|
This button can be used to enable street view mode on the map.
|
||||||
Depending on the street view source chosen in the [settings](./menu/settings), street view imagery can be accessed differently.
|
Depending on the street view source chosen in the [settings](./menu/settings), street view imagery can be accessed differently.
|
||||||
|
|
||||||
- <a href="https://www.mapillary.com/" target="_blank">Mapillary</a>: the street view coverage will appear as green lines on the map. When zoomed in enough, green dots will show the exact locations where street view imagery is available. Hovering over a green dot will show the street view image at that location.
|
- <a href="https://www.mapillary.com/" target="_blank">Mapillary</a>: the street view coverage will appear as green lines on the map. When zoomed in enough, green dots will show the exact locations where street view imagery is available. Hovering over a green dot will show the street view image at that location.
|
||||||
|
|||||||
@@ -9,44 +9,44 @@ title: Akcje menu Plik
|
|||||||
|
|
||||||
# { title }
|
# { title }
|
||||||
|
|
||||||
Menu operacji na plikach zawiera zestaw funkcji, których przeznaczenie jest dość oczywiste.
|
The file actions menu contains a set of pretty self-explanatory file operations.
|
||||||
|
|
||||||
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> Nowy
|
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New
|
||||||
|
|
||||||
Tworzy nowy pusty plik.
|
Tworzy nowy pusty plik.
|
||||||
|
|
||||||
### <FolderOpen size="16" class="inline-block" style="margin-bottom: 2px" /> Otwórz...
|
### <FolderOpen size="16" class="inline-block" style="margin-bottom: 2px" /> Open...
|
||||||
|
|
||||||
Otwórz pliki z komputera.
|
Open files from your computer.
|
||||||
|
|
||||||
<DocsNote>
|
<DocsNote>
|
||||||
|
|
||||||
Można również przeciągać i upuszczać pliki bezpośrednio z systemu plików do okna.
|
You can also drag and drop files directly from your file system into the window.
|
||||||
|
|
||||||
</DocsNote>
|
</DocsNote>
|
||||||
|
|
||||||
### <Copy size="16" class="inline-block" style="margin-bottom: 2px" /> Duplikuj
|
### <Copy size="16" class="inline-block" style="margin-bottom: 2px" /> Duplicate
|
||||||
|
|
||||||
Utwórz kopię aktualnie zaznaczonych plików.
|
Create a copy of the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Usuń
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||||
|
|
||||||
Usuń aktualnie zaznaczone pliki.
|
Delete the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Usuń wszystko
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||||
|
|
||||||
Usuń wszystkie pliki.
|
Delete all files.
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Eksport...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
||||||
|
|
||||||
Otwórz okno dialogowe eksportu, aby zapisać aktualnie wybrane pliki na komputerze.
|
Open the export dialog to save the currently selected files to your computer.
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Eksportuj wszystko...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export all...
|
||||||
|
|
||||||
Otwórz okno dialogowe eksportu, aby zapisać wszystkie pliki na komputerze.
|
Open the export dialog to save all files to your computer.
|
||||||
|
|
||||||
<DocsNote type="warning">
|
<DocsNote type="warning">
|
||||||
|
|
||||||
Jeśli po kliknięciu przycisku pobierania plik nie zacznie się pobierać, sprawdź ustawienia przeglądarki i upewnij się, że zezwalają one na pobieranie plików z witryny <b>gpx.studio</b>.
|
If your download does not start after clicking the download button, please check your browser settings to allow downloads from <b>gpx.studio</b>.
|
||||||
|
|
||||||
</DocsNote>
|
</DocsNote>
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ title: Settings
|
|||||||
|
|
||||||
### <Ruler size="16" class="inline-block" style="margin-bottom: 2px" /> Distance units
|
### <Ruler size="16" class="inline-block" style="margin-bottom: 2px" /> Distance units
|
||||||
|
|
||||||
Zmień jednostki stosowane do wyświetlania odległości w interfejsie.
|
Change the units used to display distances in the interface.
|
||||||
|
|
||||||
### <Zap size="16" class="inline-block" style="margin-bottom: 2px" /> Velocity units
|
### <Zap size="16" class="inline-block" style="margin-bottom: 2px" /> Velocity units
|
||||||
|
|
||||||
Zmień jednostki używane do wyświetlania prędkości w interfejsie.
|
Change the units used to display velocities in the interface.
|
||||||
You can choose between distance per hour or minutes per distance, which can be more suitable for running activities.
|
You can choose between distance per hour or minutes per distance, which can be more suitable for running activities.
|
||||||
|
|
||||||
### <Thermometer size="16" class="inline-block" style="margin-bottom: 2px" /> Temperature units
|
### <Thermometer size="16" class="inline-block" style="margin-bottom: 2px" /> Temperature units
|
||||||
@@ -28,8 +28,8 @@ Change the language used in the interface.
|
|||||||
|
|
||||||
<DocsNote>
|
<DocsNote>
|
||||||
|
|
||||||
Możesz przyczynić się do rozwoju naszego projektu, dodając lub ulepszając tłumaczenia w ramach <a href="https://crowdin.com/project/gpxstudio" target="_blank">projektu Crowdin</a>.
|
Możesz pomóc dodając i sprawdzając istniejące tłumaczenie w <a href="https://crowdin.com/project/gpxstudio" target="_blank">projekcie Crowdin</a>.
|
||||||
Jeśli chciałbyś dodać język, którego nie ma na liście <a href="#contact">skontaktuj się z nami</a>.
|
Jeśli chciałbyś dodać język, którego nie ma na liście <a href="#contact">skontaktuj się</a>.
|
||||||
Każda pomoc jest bardzo mile widziana!
|
Każda pomoc jest bardzo mile widziana!
|
||||||
|
|
||||||
</DocsNote>
|
</DocsNote>
|
||||||
|
|||||||
@@ -9,21 +9,21 @@ title: Akcje menu Widok
|
|||||||
|
|
||||||
# { title }
|
# { title }
|
||||||
|
|
||||||
To menu zawiera opcje zmiany kolejności interfejsu i widoku mapy.
|
This menu provides options to rearrange the interface and the map view.
|
||||||
|
|
||||||
### <ChartArea size="16" class="inline-block" style="margin-bottom: 2px" /> Profil wysokościowy
|
### <ChartArea size="16" class="inline-block" style="margin-bottom: 2px" /> Elevation profile
|
||||||
|
|
||||||
Ukryj profil ukształtowania terenu, aby zrobić miejsce na mapie lub pokaż go, aby sprawdzić bieżący wybór.
|
Hide the elevation profile to make room for the map, or show it to inspect the current selection.
|
||||||
|
|
||||||
### <ListTree size="16" class="inline-block" style="margin-bottom: 2px" /> Drzewo plików
|
### <ListTree size="16" class="inline-block" style="margin-bottom: 2px" /> File tree
|
||||||
|
|
||||||
Przełącz układ drzewa na [listy plików](../files-and-stats).
|
Toggle the tree layout for the [file list](../files-and-stats).
|
||||||
This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map.
|
This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map.
|
||||||
Dodatkowo, widok drzewa plików umożliwia sprawdzenie [tras, segmentów, oraz punktów zainteresowania](../gpx) zawarte w plikach poprzez zwijalne sekcje.
|
In addition, the file tree view enables you to inspect the [tracks, segments, and points of interest](../gpx) contained inside the files through collapsible sections.
|
||||||
|
|
||||||
### <Map size="16" class="inline-block" style="margin-bottom: 2px" /> Przełącz na poprzednią mapę
|
### <Map size="16" class="inline-block" style="margin-bottom: 2px" /> Switch to previous basemap
|
||||||
|
|
||||||
Zmień mapę na mapę wybraną poprzednio przez [sterowanie warstwą map](../map-controls).
|
Change the basemap to the one previously selected through the [map layer control](../map-controls).
|
||||||
|
|
||||||
### <Layers2 size="16" class="inline-block" style="margin-bottom: 2px" /> Toggle overlays
|
### <Layers2 size="16" class="inline-block" style="margin-bottom: 2px" /> Toggle overlays
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: Planowanie i edycja trasy
|
title: Route planning and editing
|
||||||
---
|
---
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -11,11 +11,11 @@ title: Planowanie i edycja trasy
|
|||||||
|
|
||||||
# <Pencil size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
# <Pencil size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
||||||
|
|
||||||
Narzędzie do planowania i edycji trasy pozwala na tworzenie i edytowanie tras poprzez umieszczanie lub przesuwanie punktów kotwiczenia na mapie.
|
The route planning and editing tool allows you to create and edit routes by placing or moving anchor points on the map.
|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
Jak pokazano poniżej, okno dialogowe narzędzia zawiera kilka ustawień do kontrolowania zachowania rutingu.
|
As shown below, the tool dialog contains a few settings to control the routing behavior.
|
||||||
You can minimize the dialog to save space by clicking on <button><SquareArrowUpLeft size="16" class="inline-block" style="margin-bottom: 2px" /></button>.
|
You can minimize the dialog to save space by clicking on <button><SquareArrowUpLeft size="16" class="inline-block" style="margin-bottom: 2px" /></button>.
|
||||||
|
|
||||||
<div class="flex flex-row justify-center">
|
<div class="flex flex-row justify-center">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Validate the selection when you are satisfied with the result.
|
|||||||
## Podziel
|
## Podziel
|
||||||
|
|
||||||
To split the selected trace into two parts, click on one of the split markers displayed along the trace.
|
To split the selected trace into two parts, click on one of the split markers displayed along the trace.
|
||||||
Aby podzielić w wybranym przez Ciebie punkcie, zaznacz punkt na śladzie trasy.
|
To split at a specific point of your choice, hover over the trace on the map.
|
||||||
Scissors will appear at the cursor position, showing that you can split the trace at that point.
|
Scissors will appear at the cursor position, showing that you can split the trace at that point.
|
||||||
|
|
||||||
You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks or segments](../gpx).
|
You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks or segments](../gpx).
|
||||||
|
|||||||
@@ -10,18 +10,18 @@ title: Czas
|
|||||||
|
|
||||||
# <CalendarClock size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
# <CalendarClock size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
||||||
|
|
||||||
To narzędzie pozwala na zmianę lub dodanie znaczników czasu do śladu.
|
This tool allows you to change or add timestamps to a trace.
|
||||||
Musisz po prostu użyć poniższego formularza i potwierdzić go po zakończeniu.
|
You simply need to use the form shown below and validate it when you are done.
|
||||||
|
|
||||||
<div class="flex flex-row justify-center">
|
<div class="flex flex-row justify-center">
|
||||||
<Time class="text-foreground p-3 border rounded-md shadow-lg" />
|
<Time class="text-foreground p-3 border rounded-md shadow-lg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
Podczas edycji prędkości, czas poruszania się jest odpowiednio dostosowywany w formularzu i odwrotnie.
|
When you edit the speed, the moving time is adapted accordingly in the form, and vice versa.
|
||||||
Podobnie, kiedy edytujesz czas rozpoczęcia, czas zakończenia jest aktualizowany, aby zachować ten sam czas trwania i odwrotnie.
|
Similarly, when you edit the start time, the end time is updated to keep the same total duration, and vice versa.
|
||||||
|
|
||||||
<DocsNote>
|
<DocsNote>
|
||||||
|
|
||||||
Gdy używasz to narzędzie z istniejącymi znacznikami czasu, zmiana czasu lub prędkości po prostu się zmieni, rozciągnij lub kompresuj je.
|
When using this tool with existing timestamps, changing the time or speed will simply shift, stretch, or compress them accordingly.
|
||||||
|
|
||||||
</DocsNote>
|
</DocsNote>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ Clicando com o botão direito em uma aba arquivo, você pode acessar as mesmas a
|
|||||||
|
|
||||||
As mentioned in the [view options section](./menu/view), you can switch to a tree layout for the files list.
|
As mentioned in the [view options section](./menu/view), you can switch to a tree layout for the files list.
|
||||||
This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map.
|
This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map.
|
||||||
Além disso, a exibição de árvore de arquivos permite que você inspecione as [faixas, segmentos, e pontos de interesse](./gpx) contidos dentro dos arquivos através de seções recolhidas.
|
In addition, the file tree view enables you to inspect the [tracks, segments, and points of interest](./gpx) contained inside the files through collapsible sections.
|
||||||
|
|
||||||
Você também pode aplicar as [ações de edição](./menu/edit) e [ferramentas](./toolbar) para itens de arquivos internos.
|
Você também pode aplicar as [ações de edição](./menu/edit) e [ferramentas](./toolbar) para itens de arquivos internos.
|
||||||
Além disso, você pode arrastar e soltar os itens internos para reordená-los, ou movê-los na hierarquia ou até mesmo para outro arquivo.
|
Além disso, você pode arrastar e soltar os itens internos para reordená-los, ou movê-los na hierarquia ou até mesmo para outro arquivo.
|
||||||
@@ -105,6 +105,6 @@ Using the <kbd><ChartNoAxesColumn size="16" class="inline-block" style="margin-b
|
|||||||
|
|
||||||
- **slope** information computed from the elevation data, or
|
- **slope** information computed from the elevation data, or
|
||||||
- **surface** or **category** data coming from <a href="https://www.openstreetmap.org/" target="_blank">OpenStreetMap</a>'s <a href="https://wiki.openstreetmap.org/wiki/Key:surface" target="_blank">surface</a> and <a href="https://wiki.openstreetmap.org/wiki/Key:highway" target="_blank">highway</a> tags.
|
- **surface** or **category** data coming from <a href="https://www.openstreetmap.org/" target="_blank">OpenStreetMap</a>'s <a href="https://wiki.openstreetmap.org/wiki/Key:surface" target="_blank">surface</a> and <a href="https://wiki.openstreetmap.org/wiki/Key:highway" target="_blank">highway</a> tags.
|
||||||
Isso só está disponível para arquivos criados com **gpx.studio**.
|
This is only available for files created with **gpx.studio**.
|
||||||
|
|
||||||
If your selection includes it, you can also visualize: **speed**, **heart rate**, **cadence**, **temperature** and **power** data on the elevation profile.
|
If your selection includes it, you can also visualize: **speed**, **heart rate**, **cadence**, **temperature** and **power** data on the elevation profile.
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Delete the currently selected files.
|
|||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||||
|
|
||||||
Apagar todos os arquivos.
|
Delete all files.
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportar...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportar...
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
import { HeartHandshake } from '@lucide/svelte';
|
import { HeartHandshake } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <HeartHandshake size="18" class="inline-block align-baseline" />
|
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||||
Допоможіть нам залишати цей сайт безкоштовним (та без реклами)
|
|
||||||
|
|
||||||
Кожного разу, коли ви додаєте або переміщуєте GPS точки, наші сервери обчислюють найкращий маршрут на мережі доріг.
|
Кожного разу, коли ви додаєте або переміщуєте GPS точки, наші сервери обчислюють найкращий маршрут на мережі доріг.
|
||||||
Ми також використовуємо API від <a href="https://mapbox.com" target="_blank">Mapbox</a> для зображення красивих карт, отримання даних висот та можливості пошуку місць.
|
Ми також використовуємо API від <a href="https://mapbox.com" target="_blank">Mapbox</a> для зображення красивих карт, отримання даних висот та можливості пошуку місць.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Languages } from '@lucide/svelte';
|
import { Languages } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <Languages size="18" class="inline-block align-baseline" /> Переклад
|
## <Languages size="18" class="inline-block align-baseline" /> Translation
|
||||||
|
|
||||||
Сайт перекладається волонтерами з використанням платформи для спільного перекладу.
|
Сайт перекладається волонтерами з використанням платформи для спільного перекладу.
|
||||||
Ви можете зробити свій внесок, додаючи або покращуючи переклади в нашому <a href="https://crowdin.com/project/gpxstudio" target="_blank">проєкті Crowdin</a>.
|
Ви можете зробити свій внесок, додаючи або покращуючи переклади в нашому <a href="https://crowdin.com/project/gpxstudio" target="_blank">проєкті Crowdin</a>.
|
||||||
|
|||||||
@@ -47,6 +47,6 @@ Open the export dialog to save all files to your computer.
|
|||||||
|
|
||||||
<DocsNote type="warning">
|
<DocsNote type="warning">
|
||||||
|
|
||||||
Якщо завантаження не починається після натискання кнопки завантаження, будь ласка, перевірте налаштування браузера, щоб дозволити завантаження з <b>gpx.studio</b>.
|
If your download does not start after clicking the download button, please check your browser settings to allow downloads from <b>gpx.studio</b>.
|
||||||
|
|
||||||
</DocsNote>
|
</DocsNote>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { HeartHandshake } from '@lucide/svelte';
|
import { HeartHandshake } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Hãy giúp duy trì trang web miễn phí (và không có quảng cáo)
|
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||||
|
|
||||||
Khi bạn thêm hoặc di chuyển các điểm định vị, máy chủ của chúng tôi sẽ tính toán đoạn đường tốt nhất trên mạng lưới giao thông.
|
Khi bạn thêm hoặc di chuyển các điểm định vị, máy chủ của chúng tôi sẽ tính toán đoạn đường tốt nhất trên mạng lưới giao thông.
|
||||||
Chúng tôi cũng sử dụng các API từ <a href="https://mapbox.com" target="_blank">Mapbox</a> để hiển thị đa dạng các bản đồ, lưu trữ các dữ liệu độ cao cũng như giúp bạn có thể tìm kiếm các địa điểm khác nhau.
|
Chúng tôi cũng sử dụng các API từ <a href="https://mapbox.com" target="_blank">Mapbox</a> để hiển thị đa dạng các bản đồ, lưu trữ các dữ liệu độ cao cũng như giúp bạn có thể tìm kiếm các địa điểm khác nhau.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Mapbox là công ty cung cấp một số bản đồ đẹp trên trang web này.
|
Mapbox is the company that provides some of the beautiful maps on this website.
|
||||||
Họ cũng phát triển <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">công cụ bản đồ</a> cung cấp sức mạnh cho **gpx.studio**.
|
They also develop the <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">map engine</a> which powers **gpx.studio**.
|
||||||
|
|
||||||
Chúng tôi vô cùng may mắn và biết ơn khi được tham gia chương trình <a href="https://mapbox.com/community" target="_blank">Cộng đồng</a> của họ, chương trình hỗ trợ các tổ chức phi lợi nhuận, các tổ chức giáo dục và các tổ chức tạo ra tác động tích cực.
|
We are incredibly fortunate and grateful to be part of their <a href="https://mapbox.com/community" target="_blank">Community</a> program, which supports nonprofits, educational institutions, and positive impact organizations.
|
||||||
Sự hợp tác này cho phép **gpx.studio** được hưởng lợi từ các công cụ của Mapbox với giá ưu đãi, góp phần đáng kể vào tính khả thi về tài chính của dự án và giúp chúng tôi mang đến trải nghiệm người dùng tốt nhất có thể.
|
This partnership allows **gpx.studio** to benefit from Mapbox tools at discounted prices, greatly contributing to the financial viability of the project and enabling us to offer the best possible user experience.
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ title: Edit actions
|
|||||||
|
|
||||||
# { title }
|
# { title }
|
||||||
|
|
||||||
Không giống như các thao tác trên tệp, các thao tác chỉnh sửa có thể thay đổi nội dung của các tệp hiện đang được chọn.
|
Unlike the file actions, the edit actions can potentially modify the content of the currently selected files.
|
||||||
Hơn nữa, khi bố cục dạng cây của danh sách tệp được bật (xem [Tệp và thống kê](../files-and-stats)), chúng cũng có thể được áp dụng cho [đường đi, đoạn đường và điểm quan tâm](../gpx).
|
Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, and points of interest](../gpx).
|
||||||
Therefore, we will refer to the elements that can be modified by these actions as _file items_.
|
Therefore, we will refer to the elements that can be modified by these actions as _file items_.
|
||||||
Note that except for the undo and redo actions, the edit actions are also accessible through the context menu (right-click) of the file items.
|
Note that except for the undo and redo actions, the edit actions are also accessible through the context menu (right-click) of the file items.
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Create a copy of the currently selected files.
|
|||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||||
|
|
||||||
.
|
Delete the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||||
|
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ title: 文件
|
|||||||
|
|
||||||
创建当前选中文件的副本。
|
创建当前选中文件的副本。
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> 删除
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||||
|
|
||||||
删除当前选中的文件。
|
Delete the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> 删除全部
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||||
|
|
||||||
删除全部文件。
|
Delete all files.
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> 导出...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> 导出...
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class Locale {
|
|||||||
private _isLoadingInitial = $state(true);
|
private _isLoadingInitial = $state(true);
|
||||||
private _isLoading = $state(true);
|
private _isLoading = $state(true);
|
||||||
private dictionary: Dictionary = $state({});
|
private dictionary: Dictionary = $state({});
|
||||||
private _t = $derived((key: string, fallback?: string) => {
|
private _t = $derived((key: string) => {
|
||||||
const keys = key.split('.');
|
const keys = key.split('.');
|
||||||
let value: string | Dictionary = this.dictionary;
|
let value: string | Dictionary = this.dictionary;
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ class Locale {
|
|||||||
if (value && typeof value === 'object' && k in value) {
|
if (value && typeof value === 'object' && k in value) {
|
||||||
value = value[k];
|
value = value[k];
|
||||||
} else {
|
} else {
|
||||||
return fallback || key;
|
return key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,8 +66,10 @@ export class BoundsManager {
|
|||||||
|
|
||||||
finalizeFitBounds() {
|
finalizeFitBounds() {
|
||||||
if (
|
if (
|
||||||
this._bounds.getSouth() >= this._bounds.getNorth() &&
|
this._bounds.getSouth() === 90 &&
|
||||||
this._bounds.getWest() >= this._bounds.getEast()
|
this._bounds.getWest() === 180 &&
|
||||||
|
this._bounds.getNorth() === -90 &&
|
||||||
|
this._bounds.getEast() === -180
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
import { freeze, type WritableDraft } from 'immer';
|
import { freeze, type WritableDraft } from 'immer';
|
||||||
import {
|
import {
|
||||||
|
distance,
|
||||||
GPXFile,
|
GPXFile,
|
||||||
parseGPX,
|
parseGPX,
|
||||||
Track,
|
Track,
|
||||||
@@ -29,7 +30,7 @@ import {
|
|||||||
} from 'gpx';
|
} from 'gpx';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { getClosestLinePoint, getClosestTrackSegments, getElevation } from '$lib/utils';
|
import { getClosestLinePoint, getElevation } from '$lib/utils';
|
||||||
import { gpxStatistics } from '$lib/logic/statistics';
|
import { gpxStatistics } from '$lib/logic/statistics';
|
||||||
import { boundsManager } from './bounds';
|
import { boundsManager } from './bounds';
|
||||||
|
|
||||||
@@ -215,7 +216,7 @@ export const fileActions = {
|
|||||||
reverseSelection: () => {
|
reverseSelection: () => {
|
||||||
if (
|
if (
|
||||||
!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) ||
|
!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) ||
|
||||||
get(gpxStatistics).global.length <= 1
|
get(gpxStatistics).local.points?.length <= 1
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -345,20 +346,19 @@ export const fileActions = {
|
|||||||
let startTime: Date | undefined = undefined;
|
let startTime: Date | undefined = undefined;
|
||||||
if (speed !== undefined) {
|
if (speed !== undefined) {
|
||||||
if (
|
if (
|
||||||
statistics.global.length > 0 &&
|
statistics.local.points.length > 0 &&
|
||||||
statistics.getTrackPoint(0)!.trkpt.time !== undefined
|
statistics.local.points[0].time !== undefined
|
||||||
) {
|
) {
|
||||||
startTime = statistics.getTrackPoint(0)!.trkpt.time;
|
startTime = statistics.local.points[0].time;
|
||||||
} else {
|
} else {
|
||||||
for (let i = 0; i < statistics.global.length; i++) {
|
let index = statistics.local.points.findIndex(
|
||||||
const point = statistics.getTrackPoint(i)!;
|
(point) => point.time !== undefined
|
||||||
if (point.trkpt.time !== undefined) {
|
);
|
||||||
startTime = new Date(
|
if (index !== -1 && statistics.local.points[index].time) {
|
||||||
point.trkpt.time.getTime() -
|
startTime = new Date(
|
||||||
(1000 * 3600 * point.distance.total) / speed
|
statistics.local.points[index].time.getTime() -
|
||||||
);
|
(1000 * 3600 * statistics.local.distance.total[index]) / speed
|
||||||
break;
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -453,13 +453,34 @@ export const fileActions = {
|
|||||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||||
if (level === ListLevel.FILE) {
|
if (level === ListLevel.FILE) {
|
||||||
let file = fileStateCollection.getFile(fileId);
|
let file = fileStateCollection.getFile(fileId);
|
||||||
let statistics = fileStateCollection.getStatistics(fileId);
|
if (file) {
|
||||||
if (file && statistics) {
|
|
||||||
if (file.trk.length > 1) {
|
if (file.trk.length > 1) {
|
||||||
let fileIds = getFileIds(file.trk.length);
|
let fileIds = getFileIds(file.trk.length);
|
||||||
let closest = file.wpt.map((wpt) =>
|
let closest = file.wpt.map((wpt, wptIndex) => {
|
||||||
getClosestTrackSegments(file, statistics, wpt.getCoordinates())
|
return {
|
||||||
);
|
wptIndex: wptIndex,
|
||||||
|
index: [0],
|
||||||
|
distance: Number.MAX_VALUE,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
file.trk.forEach((track, index) => {
|
||||||
|
track.getSegments().forEach((segment) => {
|
||||||
|
segment.trkpt.forEach((point) => {
|
||||||
|
file.wpt.forEach((wpt, wptIndex) => {
|
||||||
|
let dist = distance(
|
||||||
|
point.getCoordinates(),
|
||||||
|
wpt.getCoordinates()
|
||||||
|
);
|
||||||
|
if (dist < closest[wptIndex].distance) {
|
||||||
|
closest[wptIndex].distance = dist;
|
||||||
|
closest[wptIndex].index = [index];
|
||||||
|
} else if (dist === closest[wptIndex].distance) {
|
||||||
|
closest[wptIndex].index.push(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
file.trk.forEach((track, index) => {
|
file.trk.forEach((track, index) => {
|
||||||
let newFile = file.clone();
|
let newFile = file.clone();
|
||||||
let tracks = track.trkseg.map((segment, segmentIndex) => {
|
let tracks = track.trkseg.map((segment, segmentIndex) => {
|
||||||
@@ -474,11 +495,9 @@ export const fileActions = {
|
|||||||
newFile.replaceWaypoints(
|
newFile.replaceWaypoints(
|
||||||
0,
|
0,
|
||||||
file.wpt.length - 1,
|
file.wpt.length - 1,
|
||||||
file.wpt.filter((wpt, wptIndex) =>
|
closest
|
||||||
closest[wptIndex].some(
|
.filter((c) => c.index.includes(index))
|
||||||
([trackIndex, segmentIndex]) => trackIndex === index
|
.map((c) => file.wpt[c.wptIndex])
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
newFile._data.id = fileIds[index];
|
newFile._data.id = fileIds[index];
|
||||||
newFile.metadata.name =
|
newFile.metadata.name =
|
||||||
@@ -487,9 +506,29 @@ export const fileActions = {
|
|||||||
});
|
});
|
||||||
} else if (file.trk.length === 1) {
|
} else if (file.trk.length === 1) {
|
||||||
let fileIds = getFileIds(file.trk[0].trkseg.length);
|
let fileIds = getFileIds(file.trk[0].trkseg.length);
|
||||||
let closest = file.wpt.map((wpt) =>
|
let closest = file.wpt.map((wpt, wptIndex) => {
|
||||||
getClosestTrackSegments(file, statistics, wpt.getCoordinates())
|
return {
|
||||||
);
|
wptIndex: wptIndex,
|
||||||
|
index: [0],
|
||||||
|
distance: Number.MAX_VALUE,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
file.trk[0].trkseg.forEach((segment, index) => {
|
||||||
|
segment.trkpt.forEach((point) => {
|
||||||
|
file.wpt.forEach((wpt, wptIndex) => {
|
||||||
|
let dist = distance(
|
||||||
|
point.getCoordinates(),
|
||||||
|
wpt.getCoordinates()
|
||||||
|
);
|
||||||
|
if (dist < closest[wptIndex].distance) {
|
||||||
|
closest[wptIndex].distance = dist;
|
||||||
|
closest[wptIndex].index = [index];
|
||||||
|
} else if (dist === closest[wptIndex].distance) {
|
||||||
|
closest[wptIndex].index.push(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
file.trk[0].trkseg.forEach((segment, index) => {
|
file.trk[0].trkseg.forEach((segment, index) => {
|
||||||
let newFile = file.clone();
|
let newFile = file.clone();
|
||||||
newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [
|
newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [
|
||||||
@@ -498,11 +537,9 @@ export const fileActions = {
|
|||||||
newFile.replaceWaypoints(
|
newFile.replaceWaypoints(
|
||||||
0,
|
0,
|
||||||
file.wpt.length - 1,
|
file.wpt.length - 1,
|
||||||
file.wpt.filter((wpt, wptIndex) =>
|
closest
|
||||||
closest[wptIndex].some(
|
.filter((c) => c.index.includes(index))
|
||||||
([trackIndex, segmentIndex]) => segmentIndex === index
|
.map((c) => file.wpt[c.wptIndex])
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
newFile._data.id = fileIds[index];
|
newFile._data.id = fileIds[index];
|
||||||
newFile.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`;
|
newFile.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`;
|
||||||
@@ -807,7 +844,7 @@ export const fileActions = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
addElevationToSelection: async () => {
|
addElevationToSelection: async (map: mapboxgl.Map) => {
|
||||||
if (get(selection).size === 0) {
|
if (get(selection).size === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ import { get, writable, type Writable } from 'svelte/store';
|
|||||||
export enum MapCursorState {
|
export enum MapCursorState {
|
||||||
DEFAULT,
|
DEFAULT,
|
||||||
LAYER_HOVER,
|
LAYER_HOVER,
|
||||||
TOOL_WITH_CROSSHAIR,
|
|
||||||
WAYPOINT_HOVER,
|
|
||||||
WAYPOINT_DRAGGING,
|
WAYPOINT_DRAGGING,
|
||||||
TRACKPOINT_DRAGGING,
|
TRACKPOINT_DRAGGING,
|
||||||
|
TOOL_WITH_CROSSHAIR,
|
||||||
SCISSORS,
|
SCISSORS,
|
||||||
SPLIT_CONTROL,
|
|
||||||
MAPILLARY_HOVER,
|
MAPILLARY_HOVER,
|
||||||
STREET_VIEW_CROSSHAIR,
|
STREET_VIEW_CROSSHAIR,
|
||||||
}
|
}
|
||||||
@@ -18,12 +16,10 @@ const scissorsCursor = `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/20
|
|||||||
const cursorStyles = {
|
const cursorStyles = {
|
||||||
[MapCursorState.DEFAULT]: 'default',
|
[MapCursorState.DEFAULT]: 'default',
|
||||||
[MapCursorState.LAYER_HOVER]: 'pointer',
|
[MapCursorState.LAYER_HOVER]: 'pointer',
|
||||||
[MapCursorState.WAYPOINT_HOVER]: 'pointer',
|
|
||||||
[MapCursorState.WAYPOINT_DRAGGING]: 'grabbing',
|
[MapCursorState.WAYPOINT_DRAGGING]: 'grabbing',
|
||||||
[MapCursorState.TRACKPOINT_DRAGGING]: 'grabbing',
|
[MapCursorState.TRACKPOINT_DRAGGING]: 'grabbing',
|
||||||
[MapCursorState.TOOL_WITH_CROSSHAIR]: 'crosshair',
|
[MapCursorState.TOOL_WITH_CROSSHAIR]: 'crosshair',
|
||||||
[MapCursorState.SCISSORS]: scissorsCursor,
|
[MapCursorState.SCISSORS]: scissorsCursor,
|
||||||
[MapCursorState.SPLIT_CONTROL]: 'pointer',
|
|
||||||
[MapCursorState.MAPILLARY_HOVER]: 'pointer',
|
[MapCursorState.MAPILLARY_HOVER]: 'pointer',
|
||||||
[MapCursorState.STREET_VIEW_CROSSHAIR]: 'crosshair',
|
[MapCursorState.STREET_VIEW_CROSSHAIR]: 'crosshair',
|
||||||
};
|
};
|
||||||
@@ -34,8 +30,8 @@ export class MapCursor {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this._states = writable(new Set());
|
this._states = writable(new Set());
|
||||||
this._states.subscribe((states) => {
|
this._states.subscribe((states) => {
|
||||||
let state = Array.from(states.values()).reduce((max, value) => {
|
let state = states.entries().reduce((max, entry) => {
|
||||||
return value > max ? value : max;
|
return entry[0] > max ? entry[0] : max;
|
||||||
}, MapCursorState.DEFAULT);
|
}, MapCursorState.DEFAULT);
|
||||||
let canvas = get(map)?.getCanvas();
|
let canvas = get(map)?.getCanvas();
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
|
|||||||
@@ -179,112 +179,6 @@ export class Selection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFromKey(down: boolean, shift: boolean) {
|
|
||||||
let selected = get(this._selection).getSelected();
|
|
||||||
if (selected.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let next: ListItem | undefined = undefined;
|
|
||||||
if (selected[0] instanceof ListFileItem) {
|
|
||||||
let order = get(settings.fileOrder);
|
|
||||||
let limitIndex: number | undefined = undefined;
|
|
||||||
selected.forEach((item) => {
|
|
||||||
let index = order.indexOf(item.getFileId());
|
|
||||||
if (
|
|
||||||
limitIndex === undefined ||
|
|
||||||
(down && index > limitIndex) ||
|
|
||||||
(!down && index < limitIndex)
|
|
||||||
) {
|
|
||||||
limitIndex = index;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (limitIndex !== undefined) {
|
|
||||||
let nextIndex = down ? limitIndex + 1 : limitIndex - 1;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
if (nextIndex < 0) {
|
|
||||||
nextIndex = order.length - 1;
|
|
||||||
} else if (nextIndex >= order.length) {
|
|
||||||
nextIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextIndex === limitIndex) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
next = new ListFileItem(order[nextIndex]);
|
|
||||||
if (!get(selection).has(next)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextIndex += down ? 1 : -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
selected[0] instanceof ListTrackItem &&
|
|
||||||
selected[selected.length - 1] instanceof ListTrackItem
|
|
||||||
) {
|
|
||||||
let fileId = selected[0].getFileId();
|
|
||||||
let file = fileStateCollection.getFile(fileId);
|
|
||||||
if (file) {
|
|
||||||
let numberOfTracks = file.trk.length;
|
|
||||||
let trackIndex = down
|
|
||||||
? selected[selected.length - 1].getTrackIndex()
|
|
||||||
: selected[0].getTrackIndex();
|
|
||||||
if (down && trackIndex < numberOfTracks - 1) {
|
|
||||||
next = new ListTrackItem(fileId, trackIndex + 1);
|
|
||||||
} else if (!down && trackIndex > 0) {
|
|
||||||
next = new ListTrackItem(fileId, trackIndex - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
selected[0] instanceof ListTrackSegmentItem &&
|
|
||||||
selected[selected.length - 1] instanceof ListTrackSegmentItem
|
|
||||||
) {
|
|
||||||
let fileId = selected[0].getFileId();
|
|
||||||
let file = fileStateCollection.getFile(fileId);
|
|
||||||
if (file) {
|
|
||||||
let trackIndex = selected[0].getTrackIndex();
|
|
||||||
let numberOfSegments = file.trk[trackIndex].trkseg.length;
|
|
||||||
let segmentIndex = down
|
|
||||||
? selected[selected.length - 1].getSegmentIndex()
|
|
||||||
: selected[0].getSegmentIndex();
|
|
||||||
if (down && segmentIndex < numberOfSegments - 1) {
|
|
||||||
next = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex + 1);
|
|
||||||
} else if (!down && segmentIndex > 0) {
|
|
||||||
next = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
selected[0] instanceof ListWaypointItem &&
|
|
||||||
selected[selected.length - 1] instanceof ListWaypointItem
|
|
||||||
) {
|
|
||||||
let fileId = selected[0].getFileId();
|
|
||||||
let file = fileStateCollection.getFile(fileId);
|
|
||||||
if (file) {
|
|
||||||
let numberOfWaypoints = file.wpt.length;
|
|
||||||
let waypointIndex = down
|
|
||||||
? selected[selected.length - 1].getWaypointIndex()
|
|
||||||
: selected[0].getWaypointIndex();
|
|
||||||
if (down && waypointIndex < numberOfWaypoints - 1) {
|
|
||||||
next = new ListWaypointItem(fileId, waypointIndex + 1);
|
|
||||||
} else if (!down && waypointIndex > 0) {
|
|
||||||
next = new ListWaypointItem(fileId, waypointIndex - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next && (!get(this._selection).has(next) || !shift)) {
|
|
||||||
if (shift) {
|
|
||||||
this.addSelectItem(next);
|
|
||||||
} else {
|
|
||||||
this.selectItem(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getOrderedSelection(reverse: boolean = false): ListItem[] {
|
getOrderedSelection(reverse: boolean = false): ListItem[] {
|
||||||
let selected: ListItem[] = [];
|
let selected: ListItem[] = [];
|
||||||
this.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
this.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
defaultOverlayTree,
|
defaultOverlayTree,
|
||||||
defaultOverpassQueries,
|
defaultOverpassQueries,
|
||||||
defaultOverpassTree,
|
defaultOverpassTree,
|
||||||
defaultTerrainSource,
|
|
||||||
type CustomLayer,
|
type CustomLayer,
|
||||||
} from '$lib/assets/layers';
|
} from '$lib/assets/layers';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
@@ -155,7 +154,6 @@ export const settings = {
|
|||||||
customLayers: new Setting<Record<string, CustomLayer>>('customLayers', {}),
|
customLayers: new Setting<Record<string, CustomLayer>>('customLayers', {}),
|
||||||
customBasemapOrder: new Setting<string[]>('customBasemapOrder', []),
|
customBasemapOrder: new Setting<string[]>('customBasemapOrder', []),
|
||||||
customOverlayOrder: new Setting<string[]>('customOverlayOrder', []),
|
customOverlayOrder: new Setting<string[]>('customOverlayOrder', []),
|
||||||
terrainSource: new Setting('terrainSource', defaultTerrainSource),
|
|
||||||
directionMarkers: new Setting('directionMarkers', false),
|
directionMarkers: new Setting('directionMarkers', false),
|
||||||
distanceMarkers: new Setting('distanceMarkers', false),
|
distanceMarkers: new Setting('distanceMarkers', false),
|
||||||
streetViewSource: new Setting('streetViewSource', 'mapillary'),
|
streetViewSource: new Setting('streetViewSource', 'mapillary'),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ListItem, ListLevel } from '$lib/components/file-list/file-list';
|
import { ListItem, ListLevel } from '$lib/components/file-list/file-list';
|
||||||
import { GPXFile, GPXStatistics, GPXStatisticsGroup, type Track } from 'gpx';
|
import { GPXFile, GPXStatistics, type Track } from 'gpx';
|
||||||
|
|
||||||
export class GPXStatisticsTree {
|
export class GPXStatisticsTree {
|
||||||
level: ListLevel;
|
level: ListLevel;
|
||||||
@@ -21,23 +21,23 @@ export class GPXStatisticsTree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatisticsFor(item: ListItem): GPXStatisticsGroup {
|
getStatisticsFor(item: ListItem): GPXStatistics {
|
||||||
let statistics = new GPXStatisticsGroup();
|
let statistics = new GPXStatistics();
|
||||||
let id = item.getIdAtLevel(this.level);
|
let id = item.getIdAtLevel(this.level);
|
||||||
if (id === undefined || id === 'waypoints') {
|
if (id === undefined || id === 'waypoints') {
|
||||||
Object.keys(this.statistics).forEach((key) => {
|
Object.keys(this.statistics).forEach((key) => {
|
||||||
if (this.statistics[key] instanceof GPXStatistics) {
|
if (this.statistics[key] instanceof GPXStatistics) {
|
||||||
statistics.add(this.statistics[key]);
|
statistics.mergeWith(this.statistics[key]);
|
||||||
} else {
|
} else {
|
||||||
statistics.add(this.statistics[key].getStatisticsFor(item));
|
statistics.mergeWith(this.statistics[key].getStatisticsFor(item));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let child = this.statistics[id];
|
let child = this.statistics[id];
|
||||||
if (child instanceof GPXStatistics) {
|
if (child instanceof GPXStatistics) {
|
||||||
statistics.add(child);
|
statistics.mergeWith(child);
|
||||||
} else if (child !== undefined) {
|
} else if (child !== undefined) {
|
||||||
statistics.add(child.getStatisticsFor(item));
|
statistics.mergeWith(child.getStatisticsFor(item));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return statistics;
|
return statistics;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { selection } from '$lib/logic/selection';
|
import { selection } from '$lib/logic/selection';
|
||||||
import { GPXGlobalStatistics, GPXStatisticsGroup } from 'gpx';
|
import { GPXStatistics } from 'gpx';
|
||||||
import { fileStateCollection, GPXFileState } from '$lib/logic/file-state';
|
import { fileStateCollection, GPXFileState } from '$lib/logic/file-state';
|
||||||
import {
|
import {
|
||||||
ListFileItem,
|
ListFileItem,
|
||||||
@@ -12,7 +12,7 @@ import { settings } from '$lib/logic/settings';
|
|||||||
const { fileOrder } = settings;
|
const { fileOrder } = settings;
|
||||||
|
|
||||||
export class SelectedGPXStatistics {
|
export class SelectedGPXStatistics {
|
||||||
private _statistics: Writable<GPXStatisticsGroup>;
|
private _statistics: Writable<GPXStatistics>;
|
||||||
private _files: Map<
|
private _files: Map<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
@@ -22,21 +22,18 @@ export class SelectedGPXStatistics {
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._statistics = writable(new GPXStatisticsGroup());
|
this._statistics = writable(new GPXStatistics());
|
||||||
this._files = new Map();
|
this._files = new Map();
|
||||||
selection.subscribe(() => this.update());
|
selection.subscribe(() => this.update());
|
||||||
fileOrder.subscribe(() => this.update());
|
fileOrder.subscribe(() => this.update());
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(
|
subscribe(run: (value: GPXStatistics) => void, invalidate?: (value?: GPXStatistics) => void) {
|
||||||
run: (value: GPXStatisticsGroup) => void,
|
|
||||||
invalidate?: (value?: GPXStatisticsGroup) => void
|
|
||||||
) {
|
|
||||||
return this._statistics.subscribe(run, invalidate);
|
return this._statistics.subscribe(run, invalidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
let statistics = new GPXStatisticsGroup();
|
let statistics = new GPXStatistics();
|
||||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||||
let stats = fileStateCollection.getStatistics(fileId);
|
let stats = fileStateCollection.getStatistics(fileId);
|
||||||
if (stats) {
|
if (stats) {
|
||||||
@@ -46,7 +43,7 @@ export class SelectedGPXStatistics {
|
|||||||
!(item instanceof ListWaypointItem || item instanceof ListWaypointsItem) ||
|
!(item instanceof ListWaypointItem || item instanceof ListWaypointsItem) ||
|
||||||
first
|
first
|
||||||
) {
|
) {
|
||||||
statistics.add(stats.getStatisticsFor(item));
|
statistics.mergeWith(stats.getStatisticsFor(item));
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -79,7 +76,7 @@ export class SelectedGPXStatistics {
|
|||||||
|
|
||||||
export const gpxStatistics = new SelectedGPXStatistics();
|
export const gpxStatistics = new SelectedGPXStatistics();
|
||||||
|
|
||||||
export const slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined> =
|
export const slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined> =
|
||||||
writable(undefined);
|
writable(undefined);
|
||||||
|
|
||||||
gpxStatistics.subscribe(() => {
|
gpxStatistics.subscribe(() => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { languages } from '../languages';
|
import { languages } from '$lib/languages';
|
||||||
|
|
||||||
function localizeManifest(manifestTemplateData: any, language: string) {
|
function localizeManifest(manifestTemplateData: any, language: string) {
|
||||||
const localizedManifestFile = `static/${language}.manifest.webmanifest`;
|
const localizedManifestFile = `static/${language}.manifest.webmanifest`;
|
||||||
|
|||||||
@@ -229,9 +229,6 @@ export function getConvertedVelocity(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConvertedTemperature(
|
export function getConvertedTemperature(value: number) {
|
||||||
value: number,
|
return get(temperatureUnits) === 'celsius' ? value : celsiusToFahrenheit(value);
|
||||||
targetTemperatureUnits = get(temperatureUnits)
|
|
||||||
) {
|
|
||||||
return targetTemperatureUnits === 'celsius' ? value : celsiusToFahrenheit(value);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ import { type ClassValue, clsx } from 'clsx';
|
|||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
import { base } from '$app/paths';
|
import { base } from '$app/paths';
|
||||||
import { languages } from '$lib/languages';
|
import { languages } from '$lib/languages';
|
||||||
import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance, GPXFile } from 'gpx';
|
import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance } from 'gpx';
|
||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt';
|
import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt';
|
||||||
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||||
import PNGReader from 'png.js';
|
import PNGReader from 'png.js';
|
||||||
import type { GPXStatisticsTree } from '$lib/logic/statistics-tree';
|
|
||||||
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
|
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
@@ -49,59 +47,6 @@ export function getClosestLinePoint(
|
|||||||
return closest;
|
return closest;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getClosestTrackSegments(
|
|
||||||
file: GPXFile,
|
|
||||||
statistics: GPXStatisticsTree,
|
|
||||||
point: Coordinates
|
|
||||||
): [number, number][] {
|
|
||||||
let segmentBoundsDistances: [number, number, number][] = [];
|
|
||||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
|
||||||
let segmentStatistics = statistics.getStatisticsFor(
|
|
||||||
new ListTrackSegmentItem(file._data.id, trackIndex, segmentIndex)
|
|
||||||
);
|
|
||||||
let segmentBounds = segmentStatistics.global.bounds;
|
|
||||||
let northEast = segmentBounds.northEast;
|
|
||||||
let southWest = segmentBounds.southWest;
|
|
||||||
let bounds = new mapboxgl.LngLatBounds(southWest, northEast);
|
|
||||||
if (bounds.contains(point)) {
|
|
||||||
segmentBoundsDistances.push([0, trackIndex, segmentIndex]);
|
|
||||||
} else {
|
|
||||||
let northWest: Coordinates = { lat: northEast.lat, lon: southWest.lon };
|
|
||||||
let southEast: Coordinates = { lat: southWest.lat, lon: northEast.lon };
|
|
||||||
let distanceToBounds = Math.min(
|
|
||||||
crossarcDistance(northWest, northEast, point),
|
|
||||||
crossarcDistance(northEast, southEast, point),
|
|
||||||
crossarcDistance(southEast, southWest, point),
|
|
||||||
crossarcDistance(southWest, northWest, point)
|
|
||||||
);
|
|
||||||
segmentBoundsDistances.push([distanceToBounds, trackIndex, segmentIndex]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
segmentBoundsDistances.sort((a, b) => a[0] - b[0]);
|
|
||||||
|
|
||||||
let closest: { distance: number; indices: [number, number][] } = {
|
|
||||||
distance: Number.MAX_VALUE,
|
|
||||||
indices: [],
|
|
||||||
};
|
|
||||||
for (let s = 0; s < segmentBoundsDistances.length; s++) {
|
|
||||||
if (segmentBoundsDistances[s][0] > closest.distance) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const segment = file.getSegment(segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]);
|
|
||||||
segment.trkpt.forEach((pt) => {
|
|
||||||
let dist = distance(pt.getCoordinates(), point);
|
|
||||||
if (dist < closest.distance) {
|
|
||||||
closest.distance = dist;
|
|
||||||
closest.indices = [[segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]]];
|
|
||||||
} else if (dist === closest.distance) {
|
|
||||||
closest.indices.push([segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return closest.indices;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getElevation(
|
export function getElevation(
|
||||||
points: (TrackPoint | Waypoint | Coordinates)[],
|
points: (TrackPoint | Waypoint | Coordinates)[],
|
||||||
ELEVATION_ZOOM: number = 13,
|
ELEVATION_ZOOM: number = 13,
|
||||||
|
|||||||
@@ -79,8 +79,7 @@
|
|||||||
"unhide": "Паказаць",
|
"unhide": "Паказаць",
|
||||||
"center": "Center",
|
"center": "Center",
|
||||||
"open_in": "Адчыніць у",
|
"open_in": "Адчыніць у",
|
||||||
"copy_coordinates": "Copy coordinates",
|
"copy_coordinates": "Copy coordinates"
|
||||||
"edit_osm": "Edit in OpenStreetMap"
|
|
||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"routing": {
|
"routing": {
|
||||||
@@ -190,8 +189,6 @@
|
|||||||
"from": "The start point is too far from the nearest road",
|
"from": "The start point is too far from the nearest road",
|
||||||
"via": "The via point is too far from the nearest road",
|
"via": "The via point is too far from the nearest road",
|
||||||
"to": "The end point is too far from the nearest road",
|
"to": "The end point is too far from the nearest road",
|
||||||
"distance": "The end point is too far from the start point",
|
|
||||||
"connection": "No connection found between the points",
|
|
||||||
"timeout": "Route calculation took too long, try adding points closer together"
|
"timeout": "Route calculation took too long, try adding points closer together"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -284,7 +281,6 @@
|
|||||||
"update": "Update layer"
|
"update": "Update layer"
|
||||||
},
|
},
|
||||||
"opacity": "Overlay opacity",
|
"opacity": "Overlay opacity",
|
||||||
"terrain": "Terrain source",
|
|
||||||
"label": {
|
"label": {
|
||||||
"basemaps": "Basemaps",
|
"basemaps": "Basemaps",
|
||||||
"overlays": "Overlays",
|
"overlays": "Overlays",
|
||||||
@@ -328,8 +324,6 @@
|
|||||||
"usgs": "USGS",
|
"usgs": "USGS",
|
||||||
"bikerouterGravel": "bikerouter.de Gravel",
|
"bikerouterGravel": "bikerouter.de Gravel",
|
||||||
"cyclOSMlite": "CyclOSM Lite",
|
"cyclOSMlite": "CyclOSM Lite",
|
||||||
"mapterhornHillshade": "Mapterhorn Hillshade",
|
|
||||||
"openRailwayMap": "OpenRailwayMap",
|
|
||||||
"swisstopoSlope": "swisstopo Slope",
|
"swisstopoSlope": "swisstopo Slope",
|
||||||
"swisstopoHiking": "swisstopo Hiking",
|
"swisstopoHiking": "swisstopo Hiking",
|
||||||
"swisstopoHikingClosures": "swisstopo Hiking Closures",
|
"swisstopoHikingClosures": "swisstopo Hiking Closures",
|
||||||
@@ -358,7 +352,6 @@
|
|||||||
"water": "Water",
|
"water": "Water",
|
||||||
"shower": "Shower",
|
"shower": "Shower",
|
||||||
"shelter": "Shelter",
|
"shelter": "Shelter",
|
||||||
"cemetery": "Cemetery",
|
|
||||||
"motorized": "Cars and Motorcycles",
|
"motorized": "Cars and Motorcycles",
|
||||||
"fuel-station": "Fuel Station",
|
"fuel-station": "Fuel Station",
|
||||||
"parking": "Parking",
|
"parking": "Parking",
|
||||||
@@ -382,9 +375,7 @@
|
|||||||
"railway-station": "Railway Station",
|
"railway-station": "Railway Station",
|
||||||
"tram-stop": "Tram Stop",
|
"tram-stop": "Tram Stop",
|
||||||
"bus-stop": "Bus Stop",
|
"bus-stop": "Bus Stop",
|
||||||
"ferry": "Ferry",
|
"ferry": "Ferry"
|
||||||
"mapbox-dem": "Mapbox DEM",
|
|
||||||
"mapterhorn": "Mapterhorn"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chart": {
|
"chart": {
|
||||||
@@ -484,6 +475,7 @@
|
|||||||
"app": "App",
|
"app": "App",
|
||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"reddit": "Reddit",
|
"reddit": "Reddit",
|
||||||
|
"x": "X",
|
||||||
"facebook": "Facebook",
|
"facebook": "Facebook",
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
"crowdin": "Crowdin",
|
"crowdin": "Crowdin",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user