mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-03-14 00:32:59 +00:00
Compare commits
597 Commits
0f7f64fb2f
...
graphhoppe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01a7ec916e | ||
|
|
dd94a7d613 | ||
|
|
089b88c62d | ||
|
|
a01ca79a82 | ||
|
|
c91baf7c83 | ||
|
|
5062de8ddf | ||
|
|
f0f1ecb2df | ||
|
|
2eb6ef6f03 | ||
|
|
f7c0805161 | ||
|
|
4e18e3c8a0 | ||
|
|
59f31caf26 | ||
|
|
f24956c58d | ||
|
|
9019317e5c | ||
|
|
2a0227c1de | ||
|
|
f70db42b91 | ||
|
|
9cd87742f0 | ||
|
|
5dcb93ca5d | ||
|
|
256d62b29b | ||
|
|
595ea8e2d3 | ||
|
|
9ca46b9d35 | ||
|
|
d3e733aa3e | ||
|
|
a011768d2d | ||
|
|
4b45b5d716 | ||
|
|
ebe9681c12 | ||
|
|
51c85e4cd5 | ||
|
|
2e171dfbee | ||
|
|
a6a3917986 | ||
|
|
21f2448213 | ||
|
|
e7a1d0488b | ||
|
|
22b8e0edb4 | ||
|
|
d062a38e8f | ||
|
|
affa59130f | ||
|
|
3c816567bc | ||
|
|
7c2e24bbc4 | ||
|
|
e92e48ffde | ||
|
|
4ce7777b86 | ||
|
|
bc130ad867 | ||
|
|
867b6a6ac7 | ||
|
|
e585fd084c | ||
|
|
b47bb4a771 | ||
|
|
9cff71fed3 | ||
|
|
e76040e416 | ||
|
|
1facf50621 | ||
|
|
57f3cc8bc0 | ||
|
|
1ab3fe1c4a | ||
|
|
10cff632fd | ||
|
|
4105687c0a | ||
|
|
8fe6565527 | ||
|
|
69b018022d | ||
|
|
467cb2e589 | ||
|
|
f13d8c1e22 | ||
|
|
e230d55b82 | ||
|
|
46fcdb4bb2 | ||
|
|
429212ef23 | ||
|
|
4ea0ea6a7a | ||
|
|
2e3ce83605 | ||
|
|
fda908dd0d | ||
|
|
cad77e2b10 | ||
|
|
3542a7c24d | ||
|
|
0d6d161e23 | ||
|
|
89a2e0086b | ||
|
|
cd443faf61 | ||
|
|
bfc56b02a8 | ||
|
|
25bafc6bf1 | ||
|
|
6387580626 | ||
|
|
09b8aa65fc | ||
|
|
6c15193f32 | ||
|
|
4442e29b66 | ||
|
|
b6f96d9f4d | ||
|
|
36b66100f9 | ||
|
|
49d8143cc6 | ||
|
|
fc279fecaf | ||
|
|
bd307daa57 | ||
|
|
7a72f44722 | ||
|
|
8e63fc6946 | ||
|
|
3a65f8dc16 | ||
|
|
d624352a0b | ||
|
|
3fb597a774 | ||
|
|
85e46bc524 | ||
|
|
d3790878b3 | ||
|
|
e648e50f1b | ||
|
|
cb6cede0b6 | ||
|
|
a0157ddc2a | ||
|
|
8f595fbc7b | ||
|
|
2e3b22c5fa | ||
|
|
42b968372b | ||
|
|
415e7b492f | ||
|
|
2ea8e46723 | ||
|
|
977c6c6dde | ||
|
|
1e5db9dc6c | ||
|
|
252dc10e61 | ||
|
|
f9e2315ba1 | ||
|
|
0eca588280 | ||
|
|
33523bbfb9 | ||
|
|
110f23bdf1 | ||
|
|
50a5cb23f5 | ||
|
|
30e72db5ea | ||
|
|
c4c64c8fe8 | ||
|
|
df39350d7d | ||
|
|
5daacd3ed4 | ||
|
|
f3270e19df | ||
|
|
1b9ad41c87 | ||
|
|
c6586f0eed | ||
|
|
f40bdc8ed9 | ||
|
|
e5ad8bbb70 | ||
|
|
7f6acbfdbc | ||
|
|
2e070529e0 | ||
|
|
f4b31e5f0a | ||
|
|
f7f093a464 | ||
|
|
95cc340de5 | ||
|
|
51a003c816 | ||
|
|
977152139f | ||
|
|
78833df95e | ||
|
|
099d941d2e | ||
|
|
ea58f378a9 | ||
|
|
4060884909 | ||
|
|
d9277c11d2 | ||
|
|
bcbc90820a | ||
|
|
e9caa95673 | ||
|
|
9cd6703b05 | ||
|
|
4233bd7771 | ||
|
|
0e2db441f2 | ||
|
|
571b101ea4 | ||
|
|
0b9dca61ab | ||
|
|
d8fa76d076 | ||
|
|
6116eef513 | ||
|
|
fabe987f2c | ||
|
|
af20880f37 | ||
|
|
44eeab0d4b | ||
|
|
b331900158 | ||
|
|
d74380404c | ||
|
|
3833a9cd6b | ||
|
|
74fdd943c9 | ||
|
|
ad5b772502 | ||
|
|
9bc941aa31 | ||
|
|
705df43047 | ||
|
|
b31e3bb710 | ||
|
|
4082d0a368 | ||
|
|
ce85286cdf | ||
|
|
415cf1a777 | ||
|
|
f249919ec8 | ||
|
|
054c9787d3 | ||
|
|
c1e88e2b5a | ||
|
|
f5efeb16c4 | ||
|
|
59afae7bca | ||
|
|
92c6339064 | ||
|
|
582ae233f2 | ||
|
|
3c3016a211 | ||
|
|
e24e1d9d3c | ||
|
|
85fb564be7 | ||
|
|
18f7db9eee | ||
|
|
ea0770fd11 | ||
|
|
9c28fab3f9 | ||
|
|
bac332a8c4 | ||
|
|
86d2542bd9 | ||
|
|
5f63ec884f | ||
|
|
0f87b33354 | ||
|
|
c7dc99a12f | ||
|
|
05c3c3f8f3 | ||
|
|
2437a43471 | ||
|
|
16cd812ba0 | ||
|
|
c036128720 | ||
|
|
645db15848 | ||
|
|
21e142ffc3 | ||
|
|
23a6b3db72 | ||
|
|
b87d109625 | ||
|
|
651bd295b9 | ||
|
|
01607e92b9 | ||
|
|
f40d54adbb | ||
|
|
60fb387495 | ||
|
|
a7df18723c | ||
|
|
4e39cc937a | ||
|
|
fe513c17ab | ||
|
|
f7fb88ed3d | ||
|
|
b3247c7cbe | ||
|
|
97e79c23f6 | ||
|
|
7803a29875 | ||
|
|
08093beed1 | ||
|
|
ae46ae89bc | ||
|
|
f7977dca39 | ||
|
|
0b344f3e08 | ||
|
|
6058154eca | ||
|
|
941016f04b | ||
|
|
7a15898087 | ||
|
|
6b4effa90f | ||
|
|
77d56e4a4c | ||
|
|
c4dd622e7b | ||
|
|
9f24535142 | ||
|
|
a45161fcb8 | ||
|
|
4379763a73 | ||
|
|
a25b376dfd | ||
|
|
fb429f6777 | ||
|
|
ae33227c3a | ||
|
|
2fedc61d1e | ||
|
|
6988a36dd1 | ||
|
|
60e1124970 | ||
|
|
a007684006 | ||
|
|
090a709522 | ||
|
|
9e06655214 | ||
|
|
3651825e79 | ||
|
|
dcf3160b58 | ||
|
|
fd014f42cd | ||
|
|
a760f2f7fc | ||
|
|
262c114b7d | ||
|
|
c90bdd83bb | ||
|
|
fdb2d37b12 | ||
|
|
7951837ecf | ||
|
|
d2e112a672 | ||
|
|
90e86077ba | ||
|
|
f69dfcdefe | ||
|
|
5cff7c4e72 | ||
|
|
0eaa833cdf | ||
|
|
db015d925e | ||
|
|
69fd47601e | ||
|
|
17521574f0 | ||
|
|
56650a76ae | ||
|
|
984a98e792 | ||
|
|
49eb0cf202 | ||
|
|
8e24a6b036 | ||
|
|
d07bf6d699 | ||
|
|
877d12c676 | ||
|
|
ca629f625a | ||
|
|
087dd9a4b6 | ||
|
|
d9a967c072 | ||
|
|
6d522c82c3 | ||
|
|
867af98083 | ||
|
|
1be058d831 | ||
|
|
71bc044ae5 | ||
|
|
d660c50ade | ||
|
|
3fd733d903 | ||
|
|
7703b2361d | ||
|
|
68fdb9ebc6 | ||
|
|
b6513343be | ||
|
|
cc95ff1c55 | ||
|
|
c954ee0fde | ||
|
|
1ada5e5d18 | ||
|
|
f5244c3d93 | ||
|
|
1f17776cd4 | ||
|
|
ebd4f36f94 | ||
|
|
dbb9b5f254 | ||
|
|
4301472cb2 | ||
|
|
15e7954321 | ||
|
|
32d1de08e9 | ||
|
|
ea88663dce | ||
|
|
97aecaf890 | ||
|
|
b16688e1b7 | ||
|
|
5ac856251b | ||
|
|
62a9aacd85 | ||
|
|
dff0b55a8c | ||
|
|
3b21c75b13 | ||
|
|
2c7cc4b8e5 | ||
|
|
bcf8c0e35c | ||
|
|
11d2936fca | ||
|
|
5e84429e24 | ||
|
|
48c88b2d7e | ||
|
|
e5185c0b77 | ||
|
|
15773d3aba | ||
|
|
b577837446 | ||
|
|
324e234b2a | ||
|
|
d40fefb0ea | ||
|
|
7e05568549 | ||
|
|
0249a52d1c | ||
|
|
5df1c5b09b | ||
|
|
953ec8fe31 | ||
|
|
6054afebdc | ||
|
|
04f356e119 | ||
|
|
a6ebefbb30 | ||
|
|
9a1edbe1fa | ||
|
|
c46d74be54 | ||
|
|
72e949586a | ||
|
|
68dacad741 | ||
|
|
7e6505ca73 | ||
|
|
362d83f504 | ||
|
|
e4879736b7 | ||
|
|
68fe6628c7 | ||
|
|
f39ef569be | ||
|
|
42c199376a | ||
|
|
cd6b774a8c | ||
|
|
62598f94f8 | ||
|
|
aa3b46141f | ||
|
|
dcaa2aaeab | ||
|
|
e36f5e47da | ||
|
|
578d7b41b4 | ||
|
|
c4f81ce279 | ||
|
|
02a7dbea85 | ||
|
|
0305d3fe36 | ||
|
|
84fd034197 | ||
|
|
61b48e2048 | ||
|
|
c91f85389c | ||
|
|
f3683355a9 | ||
|
|
a86da886f4 | ||
|
|
7bf8d42eb8 | ||
|
|
88533e29b9 | ||
|
|
8299dcc881 | ||
|
|
0d0c250fea | ||
|
|
eb8d86616b | ||
|
|
1edc90810f | ||
|
|
c011d84a35 | ||
|
|
b97f62ac12 | ||
|
|
200a38bdc0 | ||
|
|
e12c53a90e | ||
|
|
7892916f56 | ||
|
|
5d681beab3 | ||
|
|
a54a2affb3 | ||
|
|
120062bb85 | ||
|
|
a0754d2bd7 | ||
|
|
f4b13a84d4 | ||
|
|
1c98e27e4d | ||
|
|
6067e25df8 | ||
|
|
304c50a247 | ||
|
|
274ad8eac2 | ||
|
|
9946a3fc0e | ||
|
|
622d6db968 | ||
|
|
d756ce0656 | ||
|
|
316e776355 | ||
|
|
f861b7ad99 | ||
|
|
01382e98f3 | ||
|
|
3371442bbc | ||
|
|
a81a804364 | ||
|
|
b24cf5c946 | ||
|
|
3d60213644 | ||
|
|
a23822c9df | ||
|
|
9fa69758f1 | ||
|
|
c892d3f134 | ||
|
|
73271501dc | ||
|
|
a62ea520e7 | ||
|
|
a2a0b3c71e | ||
|
|
51bedbe003 | ||
|
|
7062a3e657 | ||
|
|
209d95d5da | ||
|
|
95ee74ab2b | ||
|
|
9f6b268dce | ||
|
|
96e8ebcc10 | ||
|
|
ca261c7037 | ||
|
|
a95fe13b31 | ||
|
|
c3fe76adf9 | ||
|
|
e9b9d51a6e | ||
|
|
aff66205e9 | ||
|
|
aa0c9d65ae | ||
|
|
423bd2122f | ||
|
|
7533910114 | ||
|
|
0d4d377c22 | ||
|
|
3a576b3ea5 | ||
|
|
27f37744a0 | ||
|
|
012ecad0bb | ||
|
|
49b5d5e961 | ||
|
|
01ca48fe96 | ||
|
|
5abe5045b2 | ||
|
|
8c79a577b9 | ||
|
|
5b18f1016f | ||
|
|
bda58312bb | ||
|
|
eff352a003 | ||
|
|
3109d4ff82 | ||
|
|
c44eda3af6 | ||
|
|
33e8c41c6f | ||
|
|
e0b515ba2b | ||
|
|
c6b6ad41fb | ||
|
|
99576238cd | ||
|
|
db712650ef | ||
|
|
d77e7b07eb | ||
|
|
47b35fad3f | ||
|
|
720105ca92 | ||
|
|
ce656068e3 | ||
|
|
e69ba76e7d | ||
|
|
d1bab213a3 | ||
|
|
016a9341ad | ||
|
|
81f407ad5f | ||
|
|
37ff2f6bdb | ||
|
|
13cf1a9551 | ||
|
|
799aaac449 | ||
|
|
fbe430e4cf | ||
|
|
511746620e | ||
|
|
fed5b648f8 | ||
|
|
1f601743f6 | ||
|
|
db3ccf5da7 | ||
|
|
038898d580 | ||
|
|
dafbc6bc51 | ||
|
|
3d8271e7a1 | ||
|
|
e2ade95219 | ||
|
|
604971a576 | ||
|
|
76173103af | ||
|
|
3fcbc60232 | ||
|
|
835bdb39d0 | ||
|
|
200f09c5d2 | ||
|
|
99a008f122 | ||
|
|
9b71e89ba0 | ||
|
|
9955369cfa | ||
|
|
6ef6ffab66 | ||
|
|
0dd46f0f20 | ||
|
|
cce0d85dd4 | ||
|
|
1ed3eae038 | ||
|
|
2854c24197 | ||
|
|
4b90ddda2a | ||
|
|
552c39e583 | ||
|
|
5176e459b5 | ||
|
|
39515d0600 | ||
|
|
5314949394 | ||
|
|
59e5be749c | ||
|
|
da252a0070 | ||
|
|
b6199e430c | ||
|
|
e260a68c26 | ||
|
|
570cb2deaf | ||
|
|
7a36f03fb5 | ||
|
|
6c3058ba97 | ||
|
|
48a1034c12 | ||
|
|
aaddb50ab9 | ||
|
|
a947586cfe | ||
|
|
f3cfa14a59 | ||
|
|
a2c0a77c53 | ||
|
|
c078f9d5cb | ||
|
|
08eab8a157 | ||
|
|
9c36f234bc | ||
|
|
36b81d0e2a | ||
|
|
4432c14377 | ||
|
|
99e6dd5ca3 | ||
|
|
6327a25aec | ||
|
|
40989de7f5 | ||
|
|
58af44e795 | ||
|
|
4910cc05f8 | ||
|
|
164ee24d16 | ||
|
|
0ffda4ab7c | ||
|
|
4694a6271d | ||
|
|
2f50bc747a | ||
|
|
a13f621a81 | ||
|
|
0cc520cc67 | ||
|
|
c9451c3f2d | ||
|
|
8da53ffda2 | ||
|
|
4319761687 | ||
|
|
a1f3227cd9 | ||
|
|
b07f87c920 | ||
|
|
9c8f23eb64 | ||
|
|
2d232b3c4b | ||
|
|
712dc9bb34 | ||
|
|
5c338d53ae | ||
|
|
8d26842aab | ||
|
|
76e654304b | ||
|
|
32ba679719 | ||
|
|
cac0fefcdb | ||
|
|
498c76dd96 | ||
|
|
7526182304 | ||
|
|
d46bbd9cbf | ||
|
|
e98b537499 | ||
|
|
fc9d8509e5 | ||
|
|
7c6bbb61b5 | ||
|
|
8501ddc87f | ||
|
|
7d9b94525e | ||
|
|
eb02f0eadf | ||
|
|
69a8ba5aec | ||
|
|
fe49b8e618 | ||
|
|
26bf4dde5f | ||
|
|
e9b73050ba | ||
|
|
bacd0ab43f | ||
|
|
e438051371 | ||
|
|
314155593d | ||
|
|
787f819ce0 | ||
|
|
3632a62ea3 | ||
|
|
c7294df007 | ||
|
|
e3ad7fe3c0 | ||
|
|
6213683ddf | ||
|
|
a4ddfc9970 | ||
|
|
7ff271f9b9 | ||
|
|
d75cdd63a9 | ||
|
|
0a7575d1e4 | ||
|
|
ec3022d8ad | ||
|
|
d42103b91b | ||
|
|
00f7d08b04 | ||
|
|
408cc383cb | ||
|
|
5c926d0ac6 | ||
|
|
5cb88782fc | ||
|
|
5eef4e9ece | ||
|
|
04a2124141 | ||
|
|
1b6229b2a1 | ||
|
|
bca6db50a7 | ||
|
|
f3aae26996 | ||
|
|
f3c17a8e0f | ||
|
|
d6b24f8753 | ||
|
|
253db0a303 | ||
|
|
8499e52461 | ||
|
|
d0153179a9 | ||
|
|
264d03727e | ||
|
|
544405d9b9 | ||
|
|
24488a3b67 | ||
|
|
ae78185b29 | ||
|
|
7f682b24ef | ||
|
|
d42a52d8cf | ||
|
|
b85df15890 | ||
|
|
393499f34f | ||
|
|
c656d0f9b5 | ||
|
|
32017a8859 | ||
|
|
d87c5b1140 | ||
|
|
f59f783d3f | ||
|
|
ec298eac61 | ||
|
|
81a25bb4ee | ||
|
|
e99f044e45 | ||
|
|
5ae25a5fd9 | ||
|
|
e9d1cb4907 | ||
|
|
99f8ca2dca | ||
|
|
ddea5d38b5 | ||
|
|
31d2b83550 | ||
|
|
5535e56ed2 | ||
|
|
d740b95dbc | ||
|
|
ae92e9a945 | ||
|
|
29730c3896 | ||
|
|
a5ae8270f0 | ||
|
|
54f5fa6432 | ||
|
|
0260644063 | ||
|
|
267fc03a82 | ||
|
|
bf1537584c | ||
|
|
9ee7825022 | ||
|
|
2be0c42dd1 | ||
|
|
3423c053a2 | ||
|
|
26923cca00 | ||
|
|
36e027659c | ||
|
|
f447dccdb4 | ||
|
|
69eae32851 | ||
|
|
aa2fcfb8cb | ||
|
|
fae5ef2a41 | ||
|
|
7251ca7d2d | ||
|
|
7cdbd919bf | ||
|
|
d450f95602 | ||
|
|
5a65201971 | ||
|
|
d303b8db3e | ||
|
|
06baa33827 | ||
|
|
42743e637e | ||
|
|
9969fd7dec | ||
|
|
fc6d5c2a1d | ||
|
|
f8abb1ca24 | ||
|
|
a5af38ae3d | ||
|
|
aab70951dc | ||
|
|
334cacf93c | ||
|
|
53024012fc | ||
|
|
86a72f77c1 | ||
|
|
bc11a5ad0a | ||
|
|
8f2d217fd4 | ||
|
|
183727cd50 | ||
|
|
676e87591a | ||
|
|
8c05fc4da0 | ||
|
|
2bab06561e | ||
|
|
dfa7e2f5bb | ||
|
|
78bece5616 | ||
|
|
eeea15e373 | ||
|
|
80cd513ab7 | ||
|
|
942ef1615e | ||
|
|
a354698022 | ||
|
|
0cdea488c9 | ||
|
|
4f4291ac47 | ||
|
|
bf0cf03091 | ||
|
|
f7da09f20f | ||
|
|
be1529331c | ||
|
|
301d658a29 | ||
|
|
1cc54e5b2c | ||
|
|
65a7fd21e7 | ||
|
|
856537c0cd | ||
|
|
b2a88e0063 | ||
|
|
85a7068785 | ||
|
|
cbb733d99a | ||
|
|
ce88c94a19 | ||
|
|
16516915d8 | ||
|
|
6addb8da23 | ||
|
|
bc7f664fd8 | ||
|
|
aac17aa33c | ||
|
|
825500e207 | ||
|
|
4d42016c72 | ||
|
|
9d665df602 | ||
|
|
9087f69fb0 | ||
|
|
2a06f6a214 | ||
|
|
78a8428bd0 | ||
|
|
0d235768fa | ||
|
|
af092bbdec | ||
|
|
4961630d62 | ||
|
|
81920b9ab9 | ||
|
|
9e031d3b5b | ||
|
|
7ae3ed6d2a | ||
|
|
05d79f2b51 | ||
|
|
274e591354 | ||
|
|
95fd152b3d | ||
|
|
ffc91ed6d8 | ||
|
|
de0b759875 | ||
|
|
f041dcf944 | ||
|
|
946b9bd9d1 | ||
|
|
db77a69838 | ||
|
|
d10f4d26e2 | ||
|
|
6b62d686ba | ||
|
|
065826e64d | ||
|
|
a3b096343f | ||
|
|
b33be91b06 | ||
|
|
a94a1816c5 | ||
|
|
9a9e7fea07 | ||
|
|
9a03042077 | ||
|
|
704d3b2d6b | ||
|
|
e5c2be238d | ||
|
|
9feea07527 | ||
|
|
b0967d03b8 | ||
|
|
d33fd71f93 | ||
|
|
226b5b2682 | ||
|
|
f8879b0223 | ||
|
|
ada09d96c4 |
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@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 24
|
||||||
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: Build gpx
|
- name: Install dependencies for gpx
|
||||||
run: npm run build --prefix gpx
|
run: npm install --prefix gpx
|
||||||
|
|
||||||
- name: Install dependencies for website
|
- name: Build gpx
|
||||||
run: npm install --prefix website
|
run: npm run build --prefix gpx
|
||||||
|
|
||||||
- name: Create env file
|
- name: Install dependencies for website
|
||||||
run: |
|
run: npm install --prefix website
|
||||||
touch website/.env
|
|
||||||
echo PUBLIC_MAPBOX_TOKEN=${{ secrets.PUBLIC_MAPBOX_TOKEN }} >> website/.env
|
|
||||||
cat website/.env
|
|
||||||
|
|
||||||
- name: Build website
|
- name: Create env file
|
||||||
env:
|
run: |
|
||||||
BASE_PATH: ''
|
touch website/.env
|
||||||
run: |
|
echo PUBLIC_MAPBOX_TOKEN=${{ secrets.PUBLIC_MAPBOX_TOKEN }} >> website/.env
|
||||||
npm run build --prefix website
|
cat website/.env
|
||||||
|
|
||||||
- name: Upload Artifacts
|
- name: Build website
|
||||||
uses: actions/upload-pages-artifact@v3
|
env:
|
||||||
with:
|
BASE_PATH: ''
|
||||||
path: 'website/build/'
|
run: |
|
||||||
|
npm run build --prefix website
|
||||||
|
|
||||||
deploy:
|
- name: Upload Artifacts
|
||||||
needs: build_site
|
uses: actions/upload-pages-artifact@v4
|
||||||
runs-on: ubuntu-latest
|
with:
|
||||||
|
path: 'website/build/'
|
||||||
|
|
||||||
permissions:
|
deploy:
|
||||||
pages: write
|
needs: build_site
|
||||||
id-token: write
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
environment:
|
permissions:
|
||||||
name: github-pages
|
pages: write
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
id-token: write
|
||||||
|
|
||||||
steps:
|
environment:
|
||||||
- name: Deploy
|
name: github-pages
|
||||||
id: deployment
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
uses: actions/deploy-pages@v4
|
|
||||||
|
steps:
|
||||||
|
- name: Deploy
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
# Ignore files for PNPM, NPM and YARN
|
website/src/lib/components/ui
|
||||||
pnpm-lock.yaml
|
website/src/lib/docs/**/*.mdx
|
||||||
package-lock.json
|
**/*.webmanifest
|
||||||
yarn.lock
|
|
||||||
src/lib/components/ui
|
|
||||||
*.mdx
|
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2024 gpx.studio
|
Copyright (c) 2026 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 . && eslint .",
|
"lint": "prettier --check . --config ../.prettierrc && eslint .",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write . --config ../.prettierrc"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
688
gpx/src/gpx.ts
688
gpx/src/gpx.ts
@@ -1,4 +1,5 @@
|
|||||||
import { ramerDouglasPeucker } from './simplify';
|
import { ramerDouglasPeucker } from './simplify';
|
||||||
|
import { GPXStatistics, GPXStatisticsGroup, TrackPointLocalStatistics } from './statistics';
|
||||||
import {
|
import {
|
||||||
Coordinates,
|
Coordinates,
|
||||||
GPXFileAttributes,
|
GPXFileAttributes,
|
||||||
@@ -17,6 +18,9 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -33,7 +37,6 @@ 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[];
|
||||||
|
|
||||||
@@ -73,14 +76,6 @@ 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());
|
||||||
}
|
}
|
||||||
@@ -145,7 +140,9 @@ export class GPXFile extends GPXTreeNode<Track> {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.wpt = gpx.wpt ? gpx.wpt.map((waypoint) => new Waypoint(waypoint)) : [];
|
this.wpt = gpx.wpt
|
||||||
|
? 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)));
|
||||||
@@ -183,9 +180,6 @@ 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> {
|
||||||
@@ -206,8 +200,16 @@ 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 {
|
||||||
return this.trk
|
const style = this.trk
|
||||||
.map((track) => track.getStyle())
|
.map((track) => track.getStyle())
|
||||||
.reduce(
|
.reduce(
|
||||||
(acc, style) => {
|
(acc, style) => {
|
||||||
@@ -217,8 +219,6 @@ 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,6 +242,10 @@ export class GPXFile extends GPXTreeNode<Track> {
|
|||||||
width: [],
|
width: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
if (style.color.length === 0 && defaultColor) {
|
||||||
|
style.color.push(defaultColor);
|
||||||
|
}
|
||||||
|
return style;
|
||||||
}
|
}
|
||||||
|
|
||||||
clone(): GPXFile {
|
clone(): GPXFile {
|
||||||
@@ -804,7 +808,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) => new TrackPoint(point));
|
this.trkpt = segment.trkpt.map((point, index) => new TrackPoint(point, index));
|
||||||
if (segment.hasOwnProperty('_data')) {
|
if (segment.hasOwnProperty('_data')) {
|
||||||
this._data = segment._data;
|
this._data = segment._data;
|
||||||
}
|
}
|
||||||
@@ -816,15 +820,12 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
_computeStatistics(): GPXStatistics {
|
_computeStatistics(): GPXStatistics {
|
||||||
let statistics = new GPXStatistics();
|
let statistics = new GPXStatistics();
|
||||||
|
|
||||||
statistics.local.points = this.trkpt.map((point) => point);
|
statistics.global.length = this.trkpt.length;
|
||||||
|
statistics.local.points = this.trkpt.slice(0);
|
||||||
statistics.local.elevation.smoothed = this._computeSmoothedElevation();
|
statistics.local.data = this.trkpt.map(() => new TrackPointLocalStatistics());
|
||||||
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) {
|
||||||
@@ -833,34 +834,18 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
statistics.global.distance.total += dist;
|
statistics.global.distance.total += dist;
|
||||||
}
|
}
|
||||||
|
|
||||||
statistics.local.distance.total.push(statistics.global.distance.total);
|
statistics.local.data[i].distance.total = 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.time.total.push(0);
|
statistics.local.data[i].time.total = 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.time.total.push(
|
statistics.local.data[i].time.total =
|
||||||
(points[i].time.getTime() - statistics.global.time.start.getTime()) / 1000
|
(points[i].time.getTime() - statistics.global.time.start.getTime()) / 1000;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// speed
|
// speed
|
||||||
@@ -875,8 +860,8 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statistics.local.distance.moving.push(statistics.global.distance.moving);
|
statistics.local.data[i].distance.moving = statistics.global.distance.moving;
|
||||||
statistics.local.time.moving.push(statistics.global.time.moving);
|
statistics.local.data[i].time.moving = statistics.global.time.moving;
|
||||||
|
|
||||||
// bounds
|
// bounds
|
||||||
statistics.global.bounds.southWest.lat = Math.min(
|
statistics.global.bounds.southWest.lat = Math.min(
|
||||||
@@ -960,8 +945,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[statistics.local.slope.segment, statistics.local.slope.length] =
|
this._elevationComputation(statistics);
|
||||||
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
|
||||||
@@ -977,73 +961,115 @@ 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;
|
||||||
|
|
||||||
statistics.local.speed = distanceWindowSmoothingWithDistanceAccumulator(
|
timeWindowSmoothing(
|
||||||
points,
|
points,
|
||||||
200,
|
10000,
|
||||||
(accumulated, start, end) =>
|
(start, end) =>
|
||||||
points[start].time && points[end].time
|
points[start].time && points[end].time
|
||||||
? (3600 * accumulated) /
|
? (3600 *
|
||||||
(points[end].time.getTime() - points[start].time.getTime())
|
(statistics.local.data[end].distance.total -
|
||||||
: undefined
|
statistics.local.data[start].distance.total)) /
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
_computeSmoothedElevation(): number[] {
|
_elevationComputation(statistics: GPXStatistics) {
|
||||||
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)
|
||||||
);
|
);
|
||||||
|
|
||||||
let slope = [];
|
for (let i = 0; i < simplified.length - 1; i++) {
|
||||||
let length = [];
|
let start = simplified[i].point._data.index;
|
||||||
|
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.distance.total[end] - statistics.local.distance.total[start];
|
statistics.local.data[end].distance.total -
|
||||||
|
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++) {
|
||||||
slope.push((0.1 * ele) / dist);
|
statistics.local.data[j].slope.segment = (0.1 * ele) / dist;
|
||||||
length.push(dist);
|
statistics.local.data[j].slope.length = dist;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [slope, length];
|
distanceWindowSmoothing(
|
||||||
|
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 {
|
||||||
@@ -1290,8 +1316,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 slope = og._computeSlope();
|
let statistics = og._computeStatistics();
|
||||||
let trkpt = withArtificialTimestamps(og.trkpt, totalTime, lastPoint, startTime, slope);
|
let trkpt = withArtificialTimestamps(og.trkpt, totalTime, lastPoint, startTime, statistics);
|
||||||
this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1300,6 +1326,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emptyExtensions: Record<string, string> = {};
|
||||||
export class TrackPoint {
|
export class TrackPoint {
|
||||||
[immerable] = true;
|
[immerable] = true;
|
||||||
|
|
||||||
@@ -1310,7 +1337,7 @@ export class TrackPoint {
|
|||||||
|
|
||||||
_data: { [key: string]: any } = {};
|
_data: { [key: string]: any } = {};
|
||||||
|
|
||||||
constructor(point: (TrackPointType & { _data?: any }) | TrackPoint) {
|
constructor(point: (TrackPointType & { _data?: any }) | TrackPoint, index?: number) {
|
||||||
this.attributes = point.attributes;
|
this.attributes = point.attributes;
|
||||||
this.ele = point.ele;
|
this.ele = point.ele;
|
||||||
this.time = point.time;
|
this.time = point.time;
|
||||||
@@ -1318,6 +1345,9 @@ 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 {
|
||||||
@@ -1368,10 +1398,7 @@ export class TrackPoint {
|
|||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
setExtensions(extensions: Record<string, string>) {
|
setExtension(key: string, value: string) {
|
||||||
if (Object.keys(extensions).length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.extensions) {
|
if (!this.extensions) {
|
||||||
this.extensions = {};
|
this.extensions = {};
|
||||||
}
|
}
|
||||||
@@ -1381,8 +1408,12 @@ export class TrackPoint {
|
|||||||
if (!this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions']) {
|
if (!this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions']) {
|
||||||
this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions'] = {};
|
this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions'] = {};
|
||||||
}
|
}
|
||||||
|
this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions'][key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
setExtensions(extensions: Record<string, string>) {
|
||||||
Object.entries(extensions).forEach(([key, value]) => {
|
Object.entries(extensions).forEach(([key, value]) => {
|
||||||
this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions'][key] = value;
|
this.setExtension(key, value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1391,7 +1422,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 {
|
||||||
@@ -1461,11 +1492,18 @@ export class TrackPoint {
|
|||||||
|
|
||||||
clone(): TrackPoint {
|
clone(): TrackPoint {
|
||||||
return new TrackPoint({
|
return new TrackPoint({
|
||||||
attributes: cloneJSON(this.attributes),
|
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: cloneJSON(this.extensions),
|
extensions: this.extensions ? cloneJSON(this.extensions) : undefined,
|
||||||
_data: cloneJSON(this._data),
|
_data: {
|
||||||
|
index: this._data?.index,
|
||||||
|
anchor: this._data?.anchor,
|
||||||
|
zoom: this._data?.zoom,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1484,19 +1522,28 @@ export class Waypoint {
|
|||||||
type?: string;
|
type?: string;
|
||||||
_data: { [key: string]: any } = {};
|
_data: { [key: string]: any } = {};
|
||||||
|
|
||||||
constructor(waypoint: (WaypointType & { _data?: any }) | Waypoint) {
|
constructor(waypoint: (WaypointType & { _data?: any }) | Waypoint, index?: number) {
|
||||||
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;
|
this.name = waypoint.name === '' ? undefined : waypoint.name;
|
||||||
this.cmt = waypoint.cmt;
|
this.cmt = waypoint.cmt === '' ? undefined : waypoint.cmt;
|
||||||
this.desc = waypoint.desc;
|
this.desc = waypoint.desc === '' ? undefined : waypoint.desc;
|
||||||
this.link = waypoint.link;
|
this.link =
|
||||||
this.sym = waypoint.sym;
|
!waypoint.link ||
|
||||||
this.type = waypoint.type;
|
!waypoint.link.attributes ||
|
||||||
|
!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 {
|
||||||
@@ -1544,7 +1591,10 @@ export class Waypoint {
|
|||||||
|
|
||||||
clone(): Waypoint {
|
clone(): Waypoint {
|
||||||
return new Waypoint({
|
return new Waypoint({
|
||||||
attributes: cloneJSON(this.attributes),
|
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,
|
||||||
@@ -1593,310 +1643,6 @@ 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,
|
||||||
@@ -1911,11 +1657,15 @@ 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(lat1) * Math.sin(lat2) +
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||||
Math.cos(lat1) * Math.cos(lat2) * Math.cos((coord2.lon - coord1.lon) * rad);
|
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||||
const maxMeters = earthRadius * Math.acos(Math.min(a, 1));
|
const c = 2 * Math.asin(Math.sqrt(Math.min(a, 1)));
|
||||||
return maxMeters;
|
return earthRadius * c;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
||||||
@@ -1926,9 +1676,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.distance.total[point1._data.index] * 1000;
|
let x1 = statistics.local.data[point1._data.index].distance.total * 1000;
|
||||||
let x2 = statistics.local.distance.total[point2._data.index] * 1000;
|
let x2 = statistics.local.data[point2._data.index].distance.total * 1000;
|
||||||
let x3 = statistics.local.distance.total[point3._data.index] * 1000;
|
let x3 = statistics.local.data[point3._data.index].distance.total * 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;
|
||||||
@@ -1942,57 +1692,61 @@ export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function distanceWindowSmoothing(
|
function windowSmoothing(
|
||||||
points: TrackPoint[],
|
left: number,
|
||||||
distanceWindow: number,
|
right: number,
|
||||||
accumulate: (index: number) => number,
|
distance: (index1: number, index2: number) => number,
|
||||||
compute: (accumulated: number, start: number, end: number) => number,
|
window: number,
|
||||||
remove?: (index: number) => number
|
compute: (start: number, end: number) => number,
|
||||||
): number[] {
|
callback: (value: number, index: number) => void
|
||||||
let result = [];
|
): void {
|
||||||
|
let start = left;
|
||||||
let start = 0,
|
for (var i = left; i < right; i++) {
|
||||||
end = 0,
|
while (start + 1 < i && distance(start, i) > window) {
|
||||||
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++;
|
||||||
}
|
}
|
||||||
while (
|
let end = Math.min(i + 2, right);
|
||||||
end < points.length &&
|
while (end < right && distance(i, end) <= window) {
|
||||||
distance(points[i].getCoordinates(), points[end].getCoordinates()) <= distanceWindow
|
|
||||||
) {
|
|
||||||
accumulated += accumulate(end);
|
|
||||||
end++;
|
end++;
|
||||||
}
|
}
|
||||||
result[i] = compute(accumulated, start, end - 1);
|
callback(compute(start, end - 1), i);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function distanceWindowSmoothingWithDistanceAccumulator(
|
function distanceWindowSmoothing(
|
||||||
points: TrackPoint[],
|
left: number,
|
||||||
distanceWindow: number,
|
right: number,
|
||||||
compute: (accumulated: number, start: number, end: number) => number
|
statistics: GPXStatistics,
|
||||||
): number[] {
|
window: number,
|
||||||
return distanceWindowSmoothing(
|
compute: (start: number, end: number) => number,
|
||||||
points,
|
callback: (value: number, index: number) => void
|
||||||
distanceWindow,
|
): void {
|
||||||
(index) =>
|
windowSmoothing(
|
||||||
index > 0
|
left,
|
||||||
? distance(points[index - 1].getCoordinates(), points[index].getCoordinates())
|
right,
|
||||||
: 0,
|
(index1, index2) =>
|
||||||
|
statistics.local.data[index2].distance.total -
|
||||||
|
statistics.local.data[index1].distance.total,
|
||||||
|
window,
|
||||||
compute,
|
compute,
|
||||||
(index) => distance(points[index].getCoordinates(), points[index + 1].getCoordinates())
|
callback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeWindowSmoothing(
|
||||||
|
points: TrackPoint[],
|
||||||
|
window: number,
|
||||||
|
compute: (start: number, end: number) => number,
|
||||||
|
callback: (value: number, index: number) => void
|
||||||
|
): void {
|
||||||
|
windowSmoothing(
|
||||||
|
0,
|
||||||
|
points.length,
|
||||||
|
(index1, index2) =>
|
||||||
|
points[index2].time?.getTime() - points[index1].time?.getTime() || 2 * window,
|
||||||
|
window,
|
||||||
|
compute,
|
||||||
|
callback
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2044,14 +1798,14 @@ function withArtificialTimestamps(
|
|||||||
totalTime: number,
|
totalTime: number,
|
||||||
lastPoint: TrackPoint | undefined,
|
lastPoint: TrackPoint | undefined,
|
||||||
startTime: Date,
|
startTime: Date,
|
||||||
slope: number[]
|
statistics: GPXStatistics
|
||||||
): 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 * slope[i])));
|
let w = dist * (0.5 + 1 / (1 + Math.exp(-0.2 * statistics.local.data[i].slope.at)));
|
||||||
weight.push(w);
|
weight.push(w);
|
||||||
totalWeight += w;
|
totalWeight += w;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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,8 +3,6 @@ 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,
|
||||||
@@ -61,76 +59,56 @@ function ramerDouglasPeuckerRecursive(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function crossarcDistance(
|
export function crossarcDistance(
|
||||||
point1: TrackPoint,
|
point1: TrackPoint | Coordinates,
|
||||||
point2: TrackPoint,
|
point2: TrackPoint | Coordinates,
|
||||||
point3: TrackPoint | Coordinates
|
point3: TrackPoint | Coordinates
|
||||||
): number {
|
): number {
|
||||||
return crossarc(
|
return crossarc(
|
||||||
point1.getCoordinates(),
|
point1 instanceof TrackPoint ? point1.getCoordinates() : point1,
|
||||||
point2.getCoordinates(),
|
point2 instanceof TrackPoint ? point2.getCoordinates() : point2,
|
||||||
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 shortest distance in meters
|
// Calculates the perpendicular distance in meters
|
||||||
// between an arc (defined by p1 and p2) and a third point, p3.
|
// between a line segment (defined by p1 and p2) and a third point, p3.
|
||||||
// Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees.
|
// Uses simple planar geometry (ignores earth curvature).
|
||||||
|
|
||||||
const rad = Math.PI / 180;
|
// Convert to meters using approximate scaling
|
||||||
const lat1 = coord1.lat * rad;
|
const metersPerLongitudeDegree = getMetersPerLongitudeDegree(coord1.lat);
|
||||||
const lat2 = coord2.lat * rad;
|
|
||||||
const lat3 = coord3.lat * rad;
|
|
||||||
|
|
||||||
const lon1 = coord1.lon * rad;
|
const x1 = coord1.lon * metersPerLongitudeDegree;
|
||||||
const lon2 = coord2.lon * rad;
|
const y1 = coord1.lat * metersPerLatitudeDegree;
|
||||||
const lon3 = coord3.lon * rad;
|
const x2 = coord2.lon * metersPerLongitudeDegree;
|
||||||
|
const y2 = coord2.lat * metersPerLatitudeDegree;
|
||||||
|
const x3 = coord3.lon * metersPerLongitudeDegree;
|
||||||
|
const y3 = coord3.lat * metersPerLatitudeDegree;
|
||||||
|
|
||||||
// Prerequisites for the formulas
|
const dx = x2 - x1;
|
||||||
const bear12 = bearing(lat1, lon1, lat2, lon2);
|
const dy = y2 - y1;
|
||||||
const bear13 = bearing(lat1, lon1, lat3, lon3);
|
const segmentLengthSquared = dx * dx + dy * dy;
|
||||||
let dis13 = distance(lat1, lon1, lat3, lon3);
|
|
||||||
|
|
||||||
let diff = Math.abs(bear13 - bear12);
|
if (segmentLengthSquared === 0) {
|
||||||
if (diff > Math.PI) {
|
// p1 and p2 are the same point
|
||||||
diff = 2 * Math.PI - diff;
|
return Math.sqrt((x3 - x1) * (x3 - x1) + (y3 - y1) * (y3 - y1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is relative bearing obtuse?
|
// Project p3 onto the line defined by p1-p2
|
||||||
if (diff > Math.PI / 2) {
|
const t = Math.max(0, Math.min(1, ((x3 - x1) * dx + (y3 - y1) * dy) / segmentLengthSquared));
|
||||||
return dis13;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the cross-track distance.
|
// Find the closest point on the segment
|
||||||
let dxt = Math.asin(Math.sin(dis13 / earthRadius) * Math.sin(bear13 - bear12)) * earthRadius;
|
const projX = x1 + t * dx;
|
||||||
|
const projY = y1 + t * dy;
|
||||||
|
|
||||||
// Is p4 beyond the arc?
|
// Return distance from p3 to the projected point
|
||||||
let dis12 = distance(lat1, lon1, lat2, lon2);
|
return Math.sqrt((x3 - projX) * (x3 - projX) + (y3 - projY) * (y3 - projY));
|
||||||
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(
|
||||||
@@ -146,56 +124,39 @@ 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 defined by p1 and p2
|
// Calculates the point on the line segment defined by p1 and p2
|
||||||
// that is closest to the third point, p3.
|
// that is closest to the third point, p3.
|
||||||
// Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees.
|
// Uses simple planar geometry (ignores earth curvature).
|
||||||
|
|
||||||
const rad = Math.PI / 180;
|
// Convert to meters using approximate scaling
|
||||||
const lat1 = coord1.lat * rad;
|
const metersPerLongitudeDegree = getMetersPerLongitudeDegree(coord1.lat);
|
||||||
const lat2 = coord2.lat * rad;
|
|
||||||
const lat3 = coord3.lat * rad;
|
|
||||||
|
|
||||||
const lon1 = coord1.lon * rad;
|
const x1 = coord1.lon * metersPerLongitudeDegree;
|
||||||
const lon2 = coord2.lon * rad;
|
const y1 = coord1.lat * metersPerLatitudeDegree;
|
||||||
const lon3 = coord3.lon * rad;
|
const x2 = coord2.lon * metersPerLongitudeDegree;
|
||||||
|
const y2 = coord2.lat * metersPerLatitudeDegree;
|
||||||
|
const x3 = coord3.lon * metersPerLongitudeDegree;
|
||||||
|
const y3 = coord3.lat * metersPerLatitudeDegree;
|
||||||
|
|
||||||
// Prerequisites for the formulas
|
const dx = x2 - x1;
|
||||||
const bear12 = bearing(lat1, lon1, lat2, lon2);
|
const dy = y2 - y1;
|
||||||
const bear13 = bearing(lat1, lon1, lat3, lon3);
|
const segmentLengthSquared = dx * dx + dy * dy;
|
||||||
let dis13 = distance(lat1, lon1, lat3, lon3);
|
|
||||||
|
|
||||||
let diff = Math.abs(bear13 - bear12);
|
if (segmentLengthSquared === 0) {
|
||||||
if (diff > Math.PI) {
|
// p1 and p2 are the same point
|
||||||
diff = 2 * Math.PI - diff;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is relative bearing obtuse?
|
|
||||||
if (diff > Math.PI / 2) {
|
|
||||||
return coord1;
|
return coord1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the cross-track distance.
|
// Project p3 onto the line defined by p1-p2
|
||||||
let dxt = Math.asin(Math.sin(dis13 / earthRadius) * Math.sin(bear13 - bear12)) * earthRadius;
|
const t = Math.max(0, Math.min(1, ((x3 - x1) * dx + (y3 - y1) * dy) / segmentLengthSquared));
|
||||||
|
|
||||||
// Is p4 beyond the arc?
|
// Find the closest point on the segment
|
||||||
let dis12 = distance(lat1, lon1, lat2, lon2);
|
const projX = x1 + t * dx;
|
||||||
let dis14 =
|
const projY = y1 + t * dy;
|
||||||
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)
|
|
||||||
);
|
|
||||||
|
|
||||||
return { lat: lat4 / rad, lon: lon4 / rad };
|
// Convert back to degrees
|
||||||
}
|
return {
|
||||||
|
lat: projY / metersPerLatitudeDegree,
|
||||||
|
lon: projX / metersPerLongitudeDegree,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
391
gpx/src/statistics.ts
Normal file
391
gpx/src/statistics.ts
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
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
6
package-lock.json
generated
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"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.4.9",
|
"chart.js": "^4.5.1",
|
||||||
"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.12.0",
|
"mapbox-gl": "^3.17.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.12.0",
|
"bits-ui": "^2.14.4",
|
||||||
"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,9 +1701,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mapbox/point-geometry": {
|
"node_modules/@mapbox/point-geometry": {
|
||||||
"version": "0.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz",
|
||||||
"integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ=="
|
"integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==",
|
||||||
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/@mapbox/polyline": {
|
"node_modules/@mapbox/polyline": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
@@ -1738,11 +1739,26 @@
|
|||||||
"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": "1.3.1",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz",
|
||||||
"integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
|
"integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/point-geometry": "~0.1.0"
|
"@mapbox/point-geometry": "~1.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": {
|
||||||
@@ -2644,7 +2660,8 @@
|
|||||||
"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",
|
||||||
@@ -2660,16 +2677,6 @@
|
|||||||
"@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",
|
||||||
@@ -3234,9 +3241,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/bits-ui": {
|
"node_modules/bits-ui": {
|
||||||
"version": "2.12.0",
|
"version": "2.14.4",
|
||||||
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.14.4.tgz",
|
||||||
"integrity": "sha512-8NF4ILNyAJlIxDXpl/akGXGBV5QmZAe+8gTfPttM5P6/+LrijumcSfFXY5cr4QkXwTmLA7H5stYpbgJf2XFJvg==",
|
"integrity": "sha512-W6kenhnbd/YVvur+DKkaVJ6GldE53eLewur5AhUCqslYQ0vjZr8eWlOfwZnMiPB+PF5HMVqf61vXBvmyrAmPWg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3657,9 +3664,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chart.js": {
|
"node_modules/chart.js": {
|
||||||
"version": "4.4.9",
|
"version": "4.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz",
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||||
"integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==",
|
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kurkle/color": "^0.3.0"
|
"@kurkle/color": "^0.3.0"
|
||||||
@@ -4947,9 +4954,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/gl-matrix": {
|
"node_modules/gl-matrix": {
|
||||||
"version": "3.4.3",
|
"version": "3.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
|
||||||
"integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="
|
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "11.0.2",
|
"version": "11.0.2",
|
||||||
@@ -6061,44 +6069,55 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mapbox-gl": {
|
"node_modules/mapbox-gl": {
|
||||||
"version": "3.12.0",
|
"version": "3.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.17.0.tgz",
|
||||||
"integrity": "sha512-DV6TRr+xoPrLSKuGiUcbyLVkoLdNaNNpn6O7+ZC27yQH7BOOIF7l6JKbTCMhfMJuZBVJfL8YRJjlMJ6MZCTggA==",
|
"integrity": "sha512-nCrDKRlr5di6xUksUDslNWwxroJ5yv1hT8pyVFtcpWJOOKsYQxF/wOFTMie8oxMnXeFkrz1Tl1TwA1XN1yX0KA==",
|
||||||
"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": "^0.1.0",
|
"@mapbox/point-geometry": "^1.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": "^1.3.1",
|
"@mapbox/vector-tile": "^2.0.4",
|
||||||
"@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.3",
|
"gl-matrix": "^3.4.4",
|
||||||
"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": "^3.2.1",
|
"pbf": "^4.0.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": {
|
||||||
@@ -7616,14 +7635,6 @@
|
|||||||
"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",
|
||||||
@@ -9021,16 +9032,6 @@
|
|||||||
"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 . && eslint .",
|
"lint": "prettier --check . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore && eslint .",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore"
|
||||||
},
|
},
|
||||||
"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.12.0",
|
"bits-ui": "^2.14.4",
|
||||||
"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.4.9",
|
"chart.js": "^4.5.1",
|
||||||
"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.12.0",
|
"mapbox-gl": "^3.17.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,124 +1,126 @@
|
|||||||
@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);
|
|
||||||
|
|
||||||
--breakpoint-xs: 540px;
|
/* 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);
|
||||||
|
|
||||||
|
--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,6 +24,14 @@ 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}" />
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
Binoculars,
|
Binoculars,
|
||||||
Toilet,
|
Toilet,
|
||||||
} from 'lucide-static';
|
} from 'lucide-static';
|
||||||
import { type StyleSpecification } from 'mapbox-gl';
|
import { type RasterDEMSourceSpecification, 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/tiles/topographic/EPSG:3857/style/topographic.json?api=d01fbtg0ar23gctac5m0jgyy2ds',
|
linz: 'https://basemaps.linz.govt.nz/v1/styles/topographic-v2.json?api=d01fbtg0ar23gctac5m0jgyy2ds',
|
||||||
linzTopo: {
|
linzTopo: {
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {
|
sources: {
|
||||||
@@ -368,6 +368,42 @@ 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: {
|
||||||
@@ -799,8 +835,10 @@ export const overlayTree: LayerTreeType = {
|
|||||||
waymarkedTrailsHorseRiding: true,
|
waymarkedTrailsHorseRiding: true,
|
||||||
waymarkedTrailsWinter: true,
|
waymarkedTrailsWinter: true,
|
||||||
},
|
},
|
||||||
cyclOSMlite: true,
|
|
||||||
bikerouterGravel: true,
|
bikerouterGravel: true,
|
||||||
|
cyclOSMlite: true,
|
||||||
|
mapterhornHillshade: true,
|
||||||
|
openRailwayMap: true,
|
||||||
},
|
},
|
||||||
countries: {
|
countries: {
|
||||||
france: {
|
france: {
|
||||||
@@ -836,6 +874,7 @@ export const overpassTree: LayerTreeType = {
|
|||||||
shower: true,
|
shower: true,
|
||||||
shelter: true,
|
shelter: true,
|
||||||
barrier: true,
|
barrier: true,
|
||||||
|
cemetery: true,
|
||||||
},
|
},
|
||||||
tourism: {
|
tourism: {
|
||||||
attraction: true,
|
attraction: true,
|
||||||
@@ -882,8 +921,10 @@ export const defaultOverlays: LayerTreeType = {
|
|||||||
waymarkedTrailsHorseRiding: false,
|
waymarkedTrailsHorseRiding: false,
|
||||||
waymarkedTrailsWinter: false,
|
waymarkedTrailsWinter: false,
|
||||||
},
|
},
|
||||||
cyclOSMlite: false,
|
|
||||||
bikerouterGravel: false,
|
bikerouterGravel: false,
|
||||||
|
cyclOSMlite: false,
|
||||||
|
mapterhornHillshade: false,
|
||||||
|
openRailwayMap: false,
|
||||||
},
|
},
|
||||||
countries: {
|
countries: {
|
||||||
france: {
|
france: {
|
||||||
@@ -919,6 +960,7 @@ export const defaultOverpassQueries: LayerTreeType = {
|
|||||||
shower: false,
|
shower: false,
|
||||||
shelter: false,
|
shelter: false,
|
||||||
barrier: false,
|
barrier: false,
|
||||||
|
cemetery: false,
|
||||||
},
|
},
|
||||||
tourism: {
|
tourism: {
|
||||||
attraction: false,
|
attraction: false,
|
||||||
@@ -1016,8 +1058,10 @@ export const defaultOverlayTree: LayerTreeType = {
|
|||||||
waymarkedTrailsHorseRiding: false,
|
waymarkedTrailsHorseRiding: false,
|
||||||
waymarkedTrailsWinter: false,
|
waymarkedTrailsWinter: false,
|
||||||
},
|
},
|
||||||
cyclOSMlite: false,
|
|
||||||
bikerouterGravel: false,
|
bikerouterGravel: false,
|
||||||
|
cyclOSMlite: false,
|
||||||
|
mapterhornHillshade: false,
|
||||||
|
openRailwayMap: false,
|
||||||
},
|
},
|
||||||
countries: {
|
countries: {
|
||||||
france: {
|
france: {
|
||||||
@@ -1053,6 +1097,7 @@ export const defaultOverpassTree: LayerTreeType = {
|
|||||||
shower: false,
|
shower: false,
|
||||||
shelter: false,
|
shelter: false,
|
||||||
barrier: false,
|
barrier: false,
|
||||||
|
cemetery: false,
|
||||||
},
|
},
|
||||||
tourism: {
|
tourism: {
|
||||||
attraction: false,
|
attraction: false,
|
||||||
@@ -1099,9 +1144,7 @@ type OverpassQueryData = {
|
|||||||
svg: string;
|
svg: string;
|
||||||
color: string;
|
color: string;
|
||||||
};
|
};
|
||||||
tags:
|
tags: Record<string, string | string[]> | Record<string, string | string[]>[];
|
||||||
| Record<string, string | boolean | string[]>
|
|
||||||
| Record<string, string | boolean | string[]>[];
|
|
||||||
symbol?: string;
|
symbol?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1182,6 +1225,20 @@ 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,
|
||||||
@@ -1218,7 +1275,25 @@ export const overpassQueryData: Record<string, OverpassQueryData> = {
|
|||||||
color: '#000000',
|
color: '#000000',
|
||||||
},
|
},
|
||||||
tags: {
|
tags: {
|
||||||
barrier: true,
|
barrier: [
|
||||||
|
'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: {
|
||||||
@@ -1378,3 +1453,18 @@ 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,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Landmark,
|
Landmark,
|
||||||
Icon,
|
|
||||||
Shell,
|
Shell,
|
||||||
Bike,
|
Bike,
|
||||||
Building,
|
Building,
|
||||||
@@ -29,6 +28,7 @@ import {
|
|||||||
TriangleAlert,
|
TriangleAlert,
|
||||||
Anchor,
|
Anchor,
|
||||||
Toilet,
|
Toilet,
|
||||||
|
X,
|
||||||
type IconProps,
|
type IconProps,
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import {
|
import {
|
||||||
@@ -61,6 +61,7 @@ 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';
|
||||||
|
|
||||||
@@ -87,7 +88,11 @@ export const symbols: { [key: string]: Symbol } = {
|
|||||||
icon: ShoppingBasket,
|
icon: ShoppingBasket,
|
||||||
iconSvg: ShoppingBasketSvg,
|
iconSvg: ShoppingBasketSvg,
|
||||||
},
|
},
|
||||||
crossing: { value: 'Crossing' },
|
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 © 2025 gpx.studio
|
MIT © 2026 gpx.studio
|
||||||
</Button>
|
</Button>
|
||||||
<LanguageSelect class="w-40 mt-3" />
|
<LanguageSelect class="w-40 mt-3" />
|
||||||
</div>
|
</div>
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
{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')}
|
||||||
@@ -70,15 +71,6 @@
|
|||||||
<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 { GPXStatistics } from 'gpx';
|
import type { GPXGlobalStatistics, GPXStatisticsGroup } 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<GPXStatistics>;
|
gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||||
slicedGPXStatistics: Readable<[GPXStatistics, number, number] | undefined>;
|
slicedGPXStatistics: Readable<[GPXGlobalStatistics, 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
|
$slicedGPXStatistics !== undefined ? $slicedGPXStatistics[0] : $gpxStatistics.global
|
||||||
);
|
);
|
||||||
</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.global.distance.total} type="distance" />
|
<WithUnits value={statistics.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.global.elevation.gain} type="elevation" />
|
<WithUnits value={statistics.elevation.gain} type="elevation" />
|
||||||
<MoveDownRight size="16" class="mx-1" />
|
<MoveDownRight size="16" class="mx-1" />
|
||||||
<WithUnits value={statistics.global.elevation.loss} type="elevation" />
|
<WithUnits value={statistics.elevation.loss} type="elevation" />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{#if panelSize > 120 || orientation === 'horizontal'}
|
{#if panelSize > 120 || orientation === 'horizontal'}
|
||||||
@@ -64,13 +64,9 @@
|
|||||||
>
|
>
|
||||||
<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
|
<WithUnits value={statistics.speed.moving} type="speed" showUnits={false} />
|
||||||
value={statistics.global.speed.moving}
|
|
||||||
type="speed"
|
|
||||||
showUnits={false}
|
|
||||||
/>
|
|
||||||
<span class="mx-1">/</span>
|
<span class="mx-1">/</span>
|
||||||
<WithUnits value={statistics.global.speed.total} type="speed" />
|
<WithUnits value={statistics.speed.total} type="speed" />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -83,9 +79,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.global.time.moving} type="time" />
|
<WithUnits value={statistics.time.moving} type="time" />
|
||||||
<span class="mx-1">/</span>
|
<span class="mx-1">/</span>
|
||||||
<WithUnits value={statistics.global.time.total} type="time" />
|
<WithUnits value={statistics.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' | 'x' | 'reddit';
|
company?: 'gpx.studio' | 'mapbox' | 'github' | 'crowdin' | 'facebook' | 'reddit';
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
@@ -55,16 +55,6 @@
|
|||||||
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,6 +538,7 @@
|
|||||||
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' ||
|
||||||
@@ -644,6 +645,19 @@
|
|||||||
} 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,6 +23,7 @@
|
|||||||
{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 { GPXStatistics } from 'gpx';
|
import type { GPXGlobalStatistics, GPXStatisticsGroup } 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<GPXStatistics>;
|
gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||||
slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
|
slicedGPXStatistics: Writable<[GPXGlobalStatistics, 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,11 +14,16 @@ import {
|
|||||||
getTemperatureWithUnits,
|
getTemperatureWithUnits,
|
||||||
getVelocityWithUnits,
|
getVelocityWithUnits,
|
||||||
} from '$lib/units';
|
} from '$lib/units';
|
||||||
import Chart from 'chart.js/auto';
|
import Chart, {
|
||||||
|
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 { GPXStatistics } from 'gpx';
|
import type { GPXGlobalStatistics, GPXStatisticsGroup } 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';
|
||||||
|
|
||||||
@@ -27,6 +32,20 @@ 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;
|
||||||
@@ -35,14 +54,14 @@ export class ElevationProfile {
|
|||||||
private _dragging = false;
|
private _dragging = false;
|
||||||
private _panning = false;
|
private _panning = false;
|
||||||
|
|
||||||
private _gpxStatistics: Readable<GPXStatistics>;
|
private _gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||||
private _slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
|
private _slicedGPXStatistics: Writable<[GPXGlobalStatistics, 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<GPXStatistics>,
|
gpxStatistics: Readable<GPXStatisticsGroup>,
|
||||||
slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>,
|
slicedGPXStatistics: Writable<[GPXGlobalStatistics, 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,
|
||||||
@@ -90,7 +109,7 @@ export class ElevationProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
let options = {
|
let options: ChartOptions<'line'> = {
|
||||||
animation: false,
|
animation: false,
|
||||||
parsing: false,
|
parsing: false,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
@@ -98,8 +117,8 @@ export class ElevationProfile {
|
|||||||
x: {
|
x: {
|
||||||
type: 'linear',
|
type: 'linear',
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: function (value: number) {
|
callback: function (value: number | string) {
|
||||||
return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
|
return `${(value as number).toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
|
||||||
},
|
},
|
||||||
align: 'inner',
|
align: 'inner',
|
||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
@@ -108,8 +127,8 @@ export class ElevationProfile {
|
|||||||
y: {
|
y: {
|
||||||
type: 'linear',
|
type: 'linear',
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: function (value: number) {
|
callback: function (value: number | string) {
|
||||||
return getElevationWithUnits(value, false);
|
return getElevationWithUnits(value as number, false);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -140,8 +159,8 @@ export class ElevationProfile {
|
|||||||
title: () => {
|
title: () => {
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
label: (context: Chart.TooltipContext) => {
|
label: (context: TooltipItem<'line'>) => {
|
||||||
let point = context.raw;
|
let point = context.raw as ElevationProfilePoint;
|
||||||
if (context.datasetIndex === 0) {
|
if (context.datasetIndex === 0) {
|
||||||
const map_ = get(map);
|
const map_ = get(map);
|
||||||
if (map_ && this._marker) {
|
if (map_ && this._marker) {
|
||||||
@@ -165,10 +184,10 @@ export class ElevationProfile {
|
|||||||
return `${i18n._('quantities.power')}: ${getPowerWithUnits(point.y)}`;
|
return `${i18n._('quantities.power')}: ${getPowerWithUnits(point.y)}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
afterBody: (contexts: Chart.TooltipContext[]) => {
|
afterBody: (contexts: TooltipItem<'line'>[]) => {
|
||||||
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;
|
let point = context[0].raw as ElevationProfilePoint;
|
||||||
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),
|
||||||
@@ -227,6 +246,7 @@ 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;
|
||||||
@@ -238,13 +258,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(
|
Math.abs(maxZoom / this._chart.getZoomLevel()) < 0.01
|
||||||
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;
|
||||||
@@ -262,7 +282,6 @@ export class ElevationProfile {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
stacked: false,
|
|
||||||
onResize: () => {
|
onResize: () => {
|
||||||
this.updateOverlay();
|
this.updateOverlay();
|
||||||
},
|
},
|
||||||
@@ -270,7 +289,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: {
|
||||||
@@ -291,7 +310,7 @@ export class ElevationProfile {
|
|||||||
{
|
{
|
||||||
id: 'toggleMarker',
|
id: 'toggleMarker',
|
||||||
events: ['mouseout'],
|
events: ['mouseout'],
|
||||||
afterEvent: (chart: Chart, args: { event: Chart.ChartEvent }) => {
|
afterEvent: (chart: Chart, args: { event: 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) {
|
||||||
@@ -305,7 +324,7 @@ export class ElevationProfile {
|
|||||||
|
|
||||||
let startIndex = 0;
|
let startIndex = 0;
|
||||||
let endIndex = 0;
|
let endIndex = 0;
|
||||||
const getIndex = (evt) => {
|
const getIndex = (evt: PointerEvent) => {
|
||||||
if (!this._chart) {
|
if (!this._chart) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -323,22 +342,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 get(this._gpxStatistics).local.points.length - 1;
|
return this._chart.data.datasets[0].data.length - 1;
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let point = points.find((point) => point.element.raw);
|
const point = points.find((point) => (point.element as any).raw);
|
||||||
if (point) {
|
if (point) {
|
||||||
return point.element.raw.index;
|
return (point.element as any).raw.index;
|
||||||
} else {
|
} else {
|
||||||
return points[0].index;
|
return points[0].index;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let dragStarted = false;
|
let dragStarted = false;
|
||||||
const onMouseDown = (evt) => {
|
const onMouseDown = (evt: PointerEvent) => {
|
||||||
if (evt.shiftKey) {
|
if (evt.shiftKey) {
|
||||||
// Panning interaction
|
// Panning interaction
|
||||||
return;
|
return;
|
||||||
@@ -347,7 +366,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) => {
|
const onMouseMove = (evt: PointerEvent) => {
|
||||||
if (dragStarted) {
|
if (dragStarted) {
|
||||||
this._dragging = true;
|
this._dragging = true;
|
||||||
endIndex = getIndex(evt);
|
endIndex = getIndex(evt);
|
||||||
@@ -356,7 +375,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).slice(
|
get(this._gpxStatistics).sliced(
|
||||||
Math.min(startIndex, endIndex),
|
Math.min(startIndex, endIndex),
|
||||||
Math.max(startIndex, endIndex)
|
Math.max(startIndex, endIndex)
|
||||||
),
|
),
|
||||||
@@ -367,7 +386,7 @@ export class ElevationProfile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onMouseUp = (evt) => {
|
const onMouseUp = (evt: PointerEvent) => {
|
||||||
dragStarted = false;
|
dragStarted = false;
|
||||||
this._dragging = false;
|
this._dragging = false;
|
||||||
this._canvas.style.cursor = '';
|
this._canvas.style.cursor = '';
|
||||||
@@ -386,85 +405,99 @@ 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: data.local.points.map((point, index) => {
|
data: datasets[0],
|
||||||
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: data.local.points.map((point, index) => {
|
data: datasets[1],
|
||||||
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: data.local.points.map((point, index) => {
|
data: datasets[2],
|
||||||
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: data.local.points.map((point, index) => {
|
data: datasets[3],
|
||||||
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: data.local.points.map((point, index) => {
|
data: datasets[4],
|
||||||
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: data.local.points.map((point, index) => {
|
data: datasets[5],
|
||||||
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['max'] = getConvertedDistance(data.global.distance.total);
|
this._chart.options.scales!.x!['min'] = 0;
|
||||||
|
this._chart.options.scales!.x!['max'] = getConvertedDistance(
|
||||||
|
data.global.distance.total,
|
||||||
|
units.distance
|
||||||
|
);
|
||||||
|
|
||||||
this.setVisibility();
|
this.setVisibility();
|
||||||
this.setFill();
|
this.setFill();
|
||||||
@@ -513,21 +546,24 @@ 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') {
|
||||||
this._chart.data.datasets[0]['segment'] = {
|
segment = {
|
||||||
backgroundColor: this.slopeFillCallback,
|
backgroundColor: this.slopeFillCallback,
|
||||||
};
|
};
|
||||||
} else if (elevationFill === 'surface') {
|
} else if (elevationFill === 'surface') {
|
||||||
this._chart.data.datasets[0]['segment'] = {
|
segment = {
|
||||||
backgroundColor: this.surfaceFillCallback,
|
backgroundColor: this.surfaceFillCallback,
|
||||||
};
|
};
|
||||||
} else if (elevationFill === 'highway') {
|
} else if (elevationFill === 'highway') {
|
||||||
this._chart.data.datasets[0]['segment'] = {
|
segment = {
|
||||||
backgroundColor: this.highwayFillCallback,
|
backgroundColor: this.highwayFillCallback,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
this._chart.data.datasets[0]['segment'] = {};
|
segment = {};
|
||||||
}
|
}
|
||||||
|
Object.assign(dataset, { segment });
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOverlay() {
|
updateOverlay() {
|
||||||
@@ -554,10 +590,12 @@ 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(gpxStatistics.local.distance.total[startIndex])
|
getConvertedDistance(
|
||||||
|
gpxStatistics.getTrackPoint(startIndex)?.distance.total ?? 0
|
||||||
|
)
|
||||||
);
|
);
|
||||||
let endPixel = this._chart.scales.x.getPixelForValue(
|
let endPixel = this._chart.scales.x.getPixelForValue(
|
||||||
getConvertedDistance(gpxStatistics.local.distance.total[endIndex])
|
getConvertedDistance(gpxStatistics.getTrackPoint(endIndex)?.distance.total ?? 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
selectionContext.fillRect(
|
selectionContext.fillRect(
|
||||||
@@ -575,19 +613,22 @@ export class ElevationProfile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slopeFillCallback(context) {
|
slopeFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
||||||
return getSlopeColor(context.p0.raw.slope.segment);
|
const point = context.p0.raw as ElevationProfilePoint;
|
||||||
|
return getSlopeColor(point.slope.segment);
|
||||||
}
|
}
|
||||||
|
|
||||||
surfaceFillCallback(context) {
|
surfaceFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
||||||
return getSurfaceColor(context.p0.raw.extensions.surface);
|
const point = context.p0.raw as ElevationProfilePoint;
|
||||||
|
return getSurfaceColor(point.extensions.surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
highwayFillCallback(context) {
|
highwayFillCallback(context: ScriptableLineSegmentContext & { p0: { raw: any } }) {
|
||||||
|
const point = context.p0.raw as ElevationProfilePoint;
|
||||||
return getHighwayColor(
|
return getHighwayColor(
|
||||||
context.p0.raw.extensions.highway,
|
point.extensions.highway,
|
||||||
context.p0.raw.extensions.sac_scale,
|
point.extensions.sac_scale,
|
||||||
context.p0.raw.extensions.mtb_scale
|
point.extensions.mtb_scale
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
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,
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
currentBasemap,
|
currentBasemap,
|
||||||
|
selectedBasemapTree,
|
||||||
distanceUnits,
|
distanceUnits,
|
||||||
velocityUnits,
|
velocityUnits,
|
||||||
temperatureUnits,
|
temperatureUnits,
|
||||||
@@ -66,6 +68,9 @@
|
|||||||
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 { GPXStatistics } from 'gpx';
|
import { GPXGlobalStatistics } 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;
|
let statistics = $gpxStatistics.global;
|
||||||
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()));
|
acc.mergeWith(cur.getStatisticsFor(new ListRootItem()).global);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, new GPXStatistics());
|
}, new GPXGlobalStatistics());
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
time: statistics.global.time.total === 0,
|
time: statistics.time.total === 0,
|
||||||
hr: statistics.global.hr.count === 0,
|
hr: statistics.hr.count === 0,
|
||||||
cad: statistics.global.cad.count === 0,
|
cad: statistics.cad.count === 0,
|
||||||
atemp: statistics.global.atemp.count === 0,
|
atemp: statistics.atemp.count === 0,
|
||||||
power: statistics.global.power.count === 0,
|
power: statistics.power.count === 0,
|
||||||
extensions: Object.keys(statistics.global.extensions).length === 0,
|
extensions: Object.keys(statistics.extensions).length === 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<ScrollArea
|
<ScrollArea
|
||||||
class="shrink-0 {orientation === 'vertical' ? 'p-0 pr-3' : 'h-10 px-1'}"
|
class="shrink-0 {orientation === 'vertical' ? 'p-0 pr-3' : 'h-10 px-1'}"
|
||||||
{orientation}
|
{orientation}
|
||||||
scrollbarXClasses={orientation === 'vertical' ? '' : 'mt-1 h-2'}
|
scrollbarXClasses={orientation === 'vertical' ? '' : 'hidden'}
|
||||||
scrollbarYClasses={orientation === 'vertical' ? '' : ''}
|
scrollbarYClasses={orientation === 'vertical' ? '' : ''}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -121,20 +121,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vertical :global(button) {
|
.vertical :global(button) {
|
||||||
@apply hover:bg-muted;
|
@apply hover:bg-[var(--selection)];
|
||||||
}
|
|
||||||
|
|
||||||
.vertical :global(.sortable-selected button) {
|
|
||||||
@apply hover:bg-accent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical :global(.sortable-selected) {
|
.vertical :global(.sortable-selected) {
|
||||||
@apply bg-accent;
|
@apply bg-[var(--selection)];
|
||||||
}
|
}
|
||||||
|
|
||||||
.horizontal :global(button) {
|
.horizontal :global(button) {
|
||||||
@apply bg-accent;
|
@apply bg-[var(--selection)];
|
||||||
@apply hover:bg-muted;
|
@apply hover:bg-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
.horizontal :global(.sortable-selected button) {
|
.horizontal :global(.sortable-selected button) {
|
||||||
|
|||||||
@@ -72,17 +72,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let style = node.getStyle(defaultColor);
|
let style = node.getStyle(defaultColor);
|
||||||
style.color.forEach((c) => {
|
colors = style.color;
|
||||||
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 (style) {
|
if (
|
||||||
if (style['gpx_style:color'] && !colors.includes(style['gpx_style:color'])) {
|
style &&
|
||||||
colors.push(style['gpx_style:color']);
|
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 layer = gpxLayers.getLayer(item.getFileId());
|
let layer = gpxLayers.getLayer(item.getFileId());
|
||||||
@@ -175,7 +173,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) {
|
if (waypoint && !waypoint._data.hidden) {
|
||||||
waypointPopup?.setItem({
|
waypointPopup?.setItem({
|
||||||
item: waypoint,
|
item: waypoint,
|
||||||
fileId: item.getFileId(),
|
fileId: item.getFileId(),
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
class="p-1 has-[>svg]:px-2 h-8 justify-start {className}"
|
size="sm"
|
||||||
|
class="justify-start {className}"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<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, Mountain, Timer } from '@lucide/svelte';
|
import { Compass, Earth, 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>
|
||||||
@@ -35,5 +37,16 @@
|
|||||||
onCopy={() => trackpoint.hide?.()}
|
onCopy={() => trackpoint.hide?.()}
|
||||||
class="mt-0.5"
|
class="mt-0.5"
|
||||||
/>
|
/>
|
||||||
|
{#if trackpoint.fileId === undefined}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
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,6 +13,8 @@
|
|||||||
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,
|
||||||
@@ -20,6 +22,9 @@
|
|||||||
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 {
|
||||||
@@ -81,7 +86,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}
|
{#if $currentTool === Tool.WAYPOINT && selected}
|
||||||
<Button
|
<Button
|
||||||
class="p-1 has-[>svg]:px-2 h-8"
|
class="p-1 has-[>svg]:px-2 h-8"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -8,14 +8,7 @@ import { allHidden } from '$lib/logic/hidden';
|
|||||||
|
|
||||||
const { distanceMarkers, distanceUnits } = settings;
|
const { distanceMarkers, distanceUnits } = settings;
|
||||||
|
|
||||||
const stops = [
|
const levels = [100, 50, 25, 10, 5, 1];
|
||||||
[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);
|
||||||
@@ -50,43 +43,50 @@ export class DistanceMarkers {
|
|||||||
data: this.getDistanceMarkersGeoJSON(),
|
data: this.getDistanceMarkersGeoJSON(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
stops.forEach(([d, minzoom, maxzoom]) => {
|
if (!map_.getLayer('distance-markers')) {
|
||||||
if (!map_.getLayer(`distance-markers-${d}`)) {
|
map_.addLayer({
|
||||||
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]],
|
||||||
layout: {
|
['>=', ['zoom'], 11],
|
||||||
'text-field': ['get', 'distance'],
|
],
|
||||||
'text-size': 14,
|
10,
|
||||||
'text-font': ['Open Sans Bold'],
|
['>=', ['zoom'], 10],
|
||||||
},
|
5,
|
||||||
paint: {
|
['>=', ['zoom'], 11],
|
||||||
'text-color': 'black',
|
1,
|
||||||
'text-halo-width': 2,
|
['>=', ['zoom'], 13],
|
||||||
'text-halo-color': 'white',
|
false,
|
||||||
},
|
],
|
||||||
});
|
layout: {
|
||||||
} else {
|
'text-field': ['get', 'distance'],
|
||||||
map_.moveLayer(`distance-markers-${d}`);
|
'text-size': 14,
|
||||||
}
|
'text-font': ['Open Sans Bold'],
|
||||||
});
|
},
|
||||||
|
paint: {
|
||||||
|
'text-color': 'black',
|
||||||
|
'text-halo-width': 2,
|
||||||
|
'text-halo-color': 'white',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
map_.moveLayer('distance-markers');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
stops.forEach(([d]) => {
|
if (map_.getLayer('distance-markers')) {
|
||||||
if (map_.getLayer(`distance-markers-${d}`)) {
|
map_.removeLayer('distance-markers');
|
||||||
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
|
||||||
@@ -101,35 +101,26 @@ export class DistanceMarkers {
|
|||||||
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
|
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
|
||||||
let statistics = get(gpxStatistics);
|
let statistics = get(gpxStatistics);
|
||||||
|
|
||||||
let features = [];
|
let features: GeoJSON.Feature[] = [];
|
||||||
let currentTargetDistance = 1;
|
let currentTargetDistance = 1;
|
||||||
for (let i = 0; i < statistics.local.distance.total.length; i++) {
|
statistics.forEachTrackPoint((trkpt, dist) => {
|
||||||
if (
|
if (dist >= getConvertedDistanceToKilometers(currentTargetDistance)) {
|
||||||
statistics.local.distance.total[i] >=
|
|
||||||
getConvertedDistanceToKilometers(currentTargetDistance)
|
|
||||||
) {
|
|
||||||
let distance = currentTargetDistance.toFixed(0);
|
let distance = currentTargetDistance.toFixed(0);
|
||||||
let [level, minzoom] = stops.find(([d]) => currentTargetDistance % d === 0) ?? [
|
let level = levels.find((level) => currentTargetDistance % level === 0) || 1;
|
||||||
0, 0,
|
|
||||||
];
|
|
||||||
features.push({
|
features.push({
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
geometry: {
|
geometry: {
|
||||||
type: 'Point',
|
type: 'Point',
|
||||||
coordinates: [
|
coordinates: [trkpt.getLongitude(), trkpt.getLatitude()],
|
||||||
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,5 +1,5 @@
|
|||||||
import { get, type Readable } from 'svelte/store';
|
import { get, type Readable } from 'svelte/store';
|
||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl, { type FilterSpecification } from 'mapbox-gl';
|
||||||
import { 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 {
|
||||||
@@ -55,14 +55,18 @@ function decrementColor(color: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
|
export function getSvgForSymbol(symbol?: string | undefined, layerColor?: string | undefined) {
|
||||||
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"')
|
${
|
||||||
.replace('height="24"', 'height="12"')
|
layerColor
|
||||||
.replace('stroke="currentColor"', 'stroke="SteelBlue"')
|
? Square.replace('width="24"', 'width="12"')
|
||||||
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
|
.replace('height="24"', 'height="12"')
|
||||||
.replace('fill="none"', `fill="${layerColor}"`)}
|
.replace('stroke="currentColor"', 'stroke="SteelBlue"')
|
||||||
|
.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"', '')
|
||||||
@@ -87,9 +91,10 @@ 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;
|
||||||
draggable: boolean;
|
currentWaypointData: GeoJSON.FeatureCollection | null = null;
|
||||||
|
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);
|
||||||
@@ -98,6 +103,20 @@ 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;
|
||||||
@@ -125,18 +144,6 @@ 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() {
|
||||||
@@ -155,6 +162,8 @@ export class GPXLayer {
|
|||||||
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) {
|
||||||
@@ -189,6 +198,56 @@ 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
_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(
|
||||||
@@ -213,7 +272,7 @@ export class GPXLayer {
|
|||||||
'text-halo-color': 'white',
|
'text-halo-color': 'white',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
_map.getLayer('distance-markers-100') ? 'distance-markers-100' : undefined
|
_map.getLayer('distance-markers') ? 'distance-markers' : undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -222,151 +281,40 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let visibleItems: [number, number][] = [];
|
let visibleTrackSegmentIds: string[] = [];
|
||||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||||
if (!segment._data.hidden) {
|
if (!segment._data.hidden) {
|
||||||
visibleItems.push([trackIndex, segmentIndex]);
|
visibleTrackSegmentIds.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,
|
this.fileId + '-waypoints',
|
||||||
[
|
['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() {
|
||||||
@@ -379,6 +327,24 @@ 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');
|
||||||
}
|
}
|
||||||
@@ -388,12 +354,14 @@ 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());
|
||||||
|
|
||||||
decrementColor(this.layerColor);
|
decrementColor(this.layerColor);
|
||||||
@@ -407,6 +375,9 @@ export class GPXLayer {
|
|||||||
if (_map.getLayer(this.fileId)) {
|
if (_map.getLayer(this.fileId)) {
|
||||||
_map.moveLayer(this.fileId);
|
_map.moveLayer(this.fileId);
|
||||||
}
|
}
|
||||||
|
if (_map.getLayer(this.fileId + '-waypoints')) {
|
||||||
|
_map.moveLayer(this.fileId + '-waypoints');
|
||||||
|
}
|
||||||
if (_map.getLayer(this.fileId + '-direction')) {
|
if (_map.getLayer(this.fileId + '-direction')) {
|
||||||
_map.moveLayer(this.fileId + '-direction');
|
_map.moveLayer(this.fileId + '-direction');
|
||||||
}
|
}
|
||||||
@@ -449,7 +420,7 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
layerOnClick(e: any) {
|
layerOnClick(e: mapboxgl.MapMouseEvent) {
|
||||||
if (
|
if (
|
||||||
get(currentTool) === Tool.ROUTING &&
|
get(currentTool) === Tool.ROUTING &&
|
||||||
get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])
|
get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])
|
||||||
@@ -457,8 +428,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 &&
|
||||||
@@ -466,6 +437,11 @@ 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,
|
||||||
@@ -502,6 +478,160 @@ 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) {
|
||||||
@@ -539,6 +669,7 @@ 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) {
|
||||||
@@ -548,4 +679,65 @@ 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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,13 +34,20 @@ export class StartEndMarkers {
|
|||||||
if (!map_) return;
|
if (!map_) return;
|
||||||
|
|
||||||
const tool = get(currentTool);
|
const tool = get(currentTool);
|
||||||
const statistics = get(slicedGPXStatistics)?.[0] ?? get(gpxStatistics);
|
const statistics = get(gpxStatistics);
|
||||||
|
const slicedStatistics = get(slicedGPXStatistics);
|
||||||
const hidden = get(allHidden);
|
const hidden = get(allHidden);
|
||||||
if (statistics.local.points.length > 0 && tool !== Tool.ROUTING && !hidden) {
|
if (statistics.global.length > 0 && tool !== Tool.ROUTING && !hidden) {
|
||||||
this.start.setLngLat(statistics.local.points[0].getCoordinates()).addTo(map_);
|
this.start
|
||||||
|
.setLngLat(
|
||||||
|
statistics.getTrackPoint(slicedStatistics?.[1] ?? 0)!.trkpt.getCoordinates()
|
||||||
|
)
|
||||||
|
.addTo(map_);
|
||||||
this.end
|
this.end
|
||||||
.setLngLat(
|
.setLngLat(
|
||||||
statistics.local.points[statistics.local.points.length - 1].getCoordinates()
|
statistics
|
||||||
|
.getTrackPoint(slicedStatistics?.[2] ?? statistics.global.length - 1)!
|
||||||
|
.trkpt.getCoordinates()
|
||||||
)
|
)
|
||||||
.addTo(map_);
|
.addTo(map_);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -101,9 +101,7 @@
|
|||||||
acc: Record<string, ImportSpecification>,
|
acc: Record<string, ImportSpecification>,
|
||||||
imprt: ImportSpecification
|
imprt: ImportSpecification
|
||||||
) => {
|
) => {
|
||||||
if (
|
if (!['basemap', 'overlays'].includes(imprt.id)) {
|
||||||
!['basemap', 'overlays', 'glyphs-and-sprite'].includes(imprt.id)
|
|
||||||
) {
|
|
||||||
acc[imprt.id] = imprt;
|
acc[imprt.id] = imprt;
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
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';
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
import CustomLayers from './CustomLayers.svelte';
|
import CustomLayers from './CustomLayers.svelte';
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
import { extensionAPI } from './extension-api';
|
import { extensionAPI } from '$lib/components/map/layer-control/extension-api';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedBasemapTree,
|
selectedBasemapTree,
|
||||||
@@ -31,8 +32,11 @@
|
|||||||
currentOverpassQueries,
|
currentOverpassQueries,
|
||||||
customLayers,
|
customLayers,
|
||||||
opacities,
|
opacities,
|
||||||
|
terrainSource,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
|
const { isLayerFromExtension, getLayerName } = extensionAPI;
|
||||||
|
|
||||||
let { open = $bindable() }: { open: boolean } = $props();
|
let { open = $bindable() }: { open: boolean } = $props();
|
||||||
|
|
||||||
let accordionValue: string | undefined = $state(undefined);
|
let accordionValue: string | undefined = $state(undefined);
|
||||||
@@ -52,7 +56,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($selectedBasemapTree && $currentBasemap) {
|
if (open && $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);
|
||||||
@@ -63,7 +67,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($selectedOverlayTree) {
|
if (open && $selectedOverlayTree) {
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
if ($currentOverlays) {
|
if ($currentOverlays) {
|
||||||
let overlayLayers = getLayers($currentOverlays);
|
let overlayLayers = getLayers($currentOverlays);
|
||||||
@@ -84,7 +88,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($selectedOverpassTree) {
|
if (open && $selectedOverpassTree) {
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
if ($currentOverpassQueries) {
|
if ($currentOverpassQueries) {
|
||||||
let overlayLayers = getLayers($currentOverpassQueries);
|
let overlayLayers = getLayers($currentOverpassQueries);
|
||||||
@@ -158,11 +162,11 @@
|
|||||||
type="single"
|
type="single"
|
||||||
onValueChange={setOpacityFromSelection}
|
onValueChange={setOpacityFromSelection}
|
||||||
>
|
>
|
||||||
<Select.Trigger class="h-8 mr-1 w-full">
|
<Select.Trigger class="mr-1 w-full" size="sm">
|
||||||
{#if selectedOverlay}
|
{#if selectedOverlay}
|
||||||
{#if isSelected($selectedOverlayTree, selectedOverlay)}
|
{#if isSelected($selectedOverlayTree, selectedOverlay)}
|
||||||
{#if extensionAPI.isLayerFromExtension(selectedOverlay)}
|
{#if $isLayerFromExtension(selectedOverlay)}
|
||||||
{extensionAPI.getLayerName(selectedOverlay)}
|
{$getLayerName(selectedOverlay)}
|
||||||
{:else}
|
{:else}
|
||||||
{i18n._(`layers.label.${selectedOverlay}`)}
|
{i18n._(`layers.label.${selectedOverlay}`)}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -175,8 +179,8 @@
|
|||||||
{#each Object.keys(overlays) as id}
|
{#each Object.keys(overlays) as id}
|
||||||
{#if isSelected($selectedOverlayTree, id)}
|
{#if isSelected($selectedOverlayTree, id)}
|
||||||
<Select.Item value={id}>
|
<Select.Item value={id}>
|
||||||
{#if extensionAPI.isLayerFromExtension(id)}
|
{#if $isLayerFromExtension(id)}
|
||||||
{extensionAPI.getLayerName(id)}
|
{$getLayerName(id)}
|
||||||
{:else}
|
{:else}
|
||||||
{i18n._(`layers.label.${id}`)}
|
{i18n._(`layers.label.${id}`)}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -231,6 +235,23 @@
|
|||||||
</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>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const { customLayers } = settings;
|
const { customLayers } = settings;
|
||||||
|
const { isLayerFromExtension, getLayerName } = extensionAPI;
|
||||||
|
|
||||||
$effect.pre(() => {
|
$effect.pre(() => {
|
||||||
if (checked !== undefined) {
|
if (checked !== undefined) {
|
||||||
@@ -73,8 +74,8 @@
|
|||||||
<Label for="{name}-{id}" class="flex flex-row items-center gap-1">
|
<Label for="{name}-{id}" class="flex flex-row items-center gap-1">
|
||||||
{#if $customLayers.hasOwnProperty(id)}
|
{#if $customLayers.hasOwnProperty(id)}
|
||||||
{$customLayers[id].name}
|
{$customLayers[id].name}
|
||||||
{:else if extensionAPI.isLayerFromExtension(id)}
|
{:else if $isLayerFromExtension(id)}
|
||||||
{extensionAPI.getLayerName(id)}
|
{$getLayerName(id)}
|
||||||
{:else}
|
{:else}
|
||||||
{i18n._(`layers.label.${id}`)}
|
{i18n._(`layers.label.${id}`)}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -84,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}`)}</span>
|
<span>{i18n._(`layers.label.${id}`, id)}</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet content()}
|
{#snippet content()}
|
||||||
<div class="ml-2">
|
<div class="ml-2">
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { map, type MapboxGLMap } from '$lib/components/map/map';
|
|
||||||
import { settings } from '$lib/logic/settings';
|
import { settings } from '$lib/logic/settings';
|
||||||
import { get } from 'svelte/store';
|
import { derived, get, writable, type Writable } from 'svelte/store';
|
||||||
import { isSelected, remove, removeByPrefix, toggle } from './utils';
|
import { isSelected, remove, removeAll } from './utils';
|
||||||
import { overlays, overlayTree } from '$lib/assets/layers';
|
import { overlays, overlayTree } from '$lib/assets/layers';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
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[];
|
||||||
@@ -15,11 +16,9 @@ export type CustomOverlay = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class ExtensionAPI {
|
export class ExtensionAPI {
|
||||||
private _map: MapboxGLMap;
|
private _overlays: Writable<Map<string, CustomOverlay>> = writable(new Map());
|
||||||
private _overlays: Map<string, CustomOverlay> = new Map();
|
|
||||||
|
|
||||||
constructor(map: MapboxGLMap) {
|
init() {
|
||||||
this._map = map;
|
|
||||||
if (browser && !window.hasOwnProperty('gpxstudio')) {
|
if (browser && !window.hasOwnProperty('gpxstudio')) {
|
||||||
Object.defineProperty(window, 'gpxstudio', {
|
Object.defineProperty(window, 'gpxstudio', {
|
||||||
value: this,
|
value: this,
|
||||||
@@ -30,21 +29,41 @@ export class ExtensionAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureLoaded(): Promise<void> {
|
ensureLoaded(): Promise<void> {
|
||||||
return new Promise((resolve) => {
|
let unsubscribe: () => void;
|
||||||
this._map.onLoad(() => {
|
const promise = new Promise<void>((resolve) => {
|
||||||
resolve();
|
map.onLoad(() => {
|
||||||
|
unsubscribe = currentOverlays.subscribe((current) => {
|
||||||
|
if (current) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
promise.finally(() => {
|
||||||
|
unsubscribe?.();
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
addOrUpdateOverlay(overlay: CustomOverlay) {
|
addOrUpdateOverlay(overlay: CustomOverlay) {
|
||||||
if (!overlay.id || !overlay.tileUrls || overlay.tileUrls.length === 0) {
|
if (
|
||||||
throw new Error('Overlay must have an id and at least one tile URL.');
|
!overlay.extensionName ||
|
||||||
|
!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);
|
||||||
|
|
||||||
this._overlays.set(overlay.id, overlay);
|
this._overlays.update(($overlays) => {
|
||||||
|
$overlays.set(overlay.id, overlay);
|
||||||
|
return $overlays;
|
||||||
|
});
|
||||||
|
|
||||||
overlays[overlay.id] = {
|
overlays[overlay.id] = {
|
||||||
version: 8,
|
version: 8,
|
||||||
@@ -65,103 +84,130 @@ export class ExtensionAPI {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
overlayTree.overlays.world[overlay.id] = true;
|
if (!overlayTree.overlays.hasOwnProperty(overlay.extensionName)) {
|
||||||
|
overlayTree.overlays[overlay.extensionName] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
overlayTree.overlays[overlay.extensionName][overlay.id] = true;
|
||||||
|
|
||||||
selectedOverlayTree.update((selected) => {
|
selectedOverlayTree.update((selected) => {
|
||||||
selected.overlays.world[overlay.id] = true;
|
if (!selected.overlays.hasOwnProperty(overlay.extensionName)) {
|
||||||
|
selected.overlays[overlay.extensionName] = {};
|
||||||
|
}
|
||||||
|
selected.overlays[overlay.extensionName][overlay.id] = true;
|
||||||
return selected;
|
return selected;
|
||||||
});
|
});
|
||||||
|
|
||||||
const current = get(currentOverlays);
|
const current = get(currentOverlays);
|
||||||
|
let show = false;
|
||||||
if (current && isSelected(current, overlay.id)) {
|
if (current && isSelected(current, overlay.id)) {
|
||||||
|
show = true;
|
||||||
try {
|
try {
|
||||||
get(this._map)?.removeImport(overlay.id);
|
get(map)?.removeImport(overlay.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// No reliable way to check if the map is ready to remove sources and layers
|
// No reliable way to check if the map is ready to remove sources and layers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentOverlays.update((current) => {
|
currentOverlays.update((current) => {
|
||||||
current.overlays.world[overlay.id] = true;
|
if (!current.overlays.hasOwnProperty(overlay.extensionName)) {
|
||||||
|
current.overlays[overlay.extensionName] = {};
|
||||||
|
}
|
||||||
|
current.overlays[overlay.extensionName][overlay.id] = show;
|
||||||
return current;
|
return current;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
removeOverlaysWithPrefix(prefix: string) {
|
filterOverlays(ids: string[]) {
|
||||||
prefix = this.getOverlayId(prefix);
|
ids = ids.map((id) => this.getOverlayId(id));
|
||||||
|
const idsToRemove = Array.from(get(this._overlays).keys()).filter(
|
||||||
|
(id) => !ids.includes(id)
|
||||||
|
);
|
||||||
|
|
||||||
currentOverlays.update((overlays) => {
|
currentOverlays.update((current) => {
|
||||||
removeByPrefix(overlays, prefix);
|
removeAll(current, idsToRemove);
|
||||||
return overlays;
|
return current;
|
||||||
});
|
});
|
||||||
previousOverlays.update((overlays) => {
|
previousOverlays.update((previous) => {
|
||||||
removeByPrefix(overlays, prefix);
|
removeAll(previous, idsToRemove);
|
||||||
return overlays;
|
return previous;
|
||||||
});
|
});
|
||||||
selectedOverlayTree.update((overlays) => {
|
selectedOverlayTree.update((selected) => {
|
||||||
removeByPrefix(overlays, prefix);
|
removeAll(selected, idsToRemove);
|
||||||
return overlays;
|
return selected;
|
||||||
});
|
});
|
||||||
Object.keys(overlays).forEach((id) => {
|
Object.keys(overlays).forEach((id) => {
|
||||||
if (id.startsWith(prefix)) {
|
if (idsToRemove.includes(id)) {
|
||||||
delete overlays[id];
|
delete overlays[id];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Object.keys(overlayTree.overlays.world).forEach((id) => {
|
removeAll(overlayTree, idsToRemove);
|
||||||
if (id.startsWith(prefix)) {
|
this._overlays.update(($overlays) => {
|
||||||
delete overlayTree.overlays.world[id];
|
$overlays.forEach((_, id) => {
|
||||||
}
|
if (idsToRemove.includes(id)) {
|
||||||
});
|
$overlays.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleOverlay(id: string) {
|
|
||||||
id = this.getOverlayId(id);
|
|
||||||
|
|
||||||
currentOverlays.update((overlays) => {
|
|
||||||
toggle(overlays, id);
|
|
||||||
return overlays;
|
|
||||||
});
|
|
||||||
if (!isSelected(get(selectedOverlayTree), id)) {
|
|
||||||
selectedOverlayTree.update((overlays) => {
|
|
||||||
toggle(overlays, id);
|
|
||||||
return overlays;
|
|
||||||
});
|
});
|
||||||
}
|
return $overlays;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
isLayerFromExtension(id: string): boolean {
|
updateOverlaysOrder(ids: string[]) {
|
||||||
return this._overlays.has(id);
|
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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getLayerName(id: string): string {
|
isLayerFromExtension = derived(this._overlays, ($overlays) => {
|
||||||
const overlay = this._overlays.get(id);
|
return (id: string) => $overlays.has(id);
|
||||||
return overlay ? overlay.name : '';
|
});
|
||||||
}
|
|
||||||
|
getLayerName = derived(this._overlays, ($overlays) => {
|
||||||
|
return (id: string) => $overlays.get(id)?.name || '';
|
||||||
|
});
|
||||||
|
|
||||||
private getOverlayId(id: string): string {
|
private getOverlayId(id: string): string {
|
||||||
return `extension-${id}`;
|
return `extension-${id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private destroy() {
|
private destroy() {
|
||||||
currentOverlays.update((overlays) => {
|
const ids = Array.from(get(this._overlays).keys());
|
||||||
this._overlays.forEach((_, id) => {
|
currentOverlays.update((current) => {
|
||||||
remove(overlays, id);
|
ids.forEach((id) => {
|
||||||
|
remove(current, id);
|
||||||
});
|
});
|
||||||
return overlays;
|
return current;
|
||||||
});
|
});
|
||||||
previousOverlays.update((overlays) => {
|
previousOverlays.update((previous) => {
|
||||||
this._overlays.forEach((_, id) => {
|
ids.forEach((id) => {
|
||||||
remove(overlays, id);
|
remove(previous, id);
|
||||||
});
|
});
|
||||||
return overlays;
|
return previous;
|
||||||
});
|
});
|
||||||
selectedOverlayTree.update((overlays) => {
|
selectedOverlayTree.update((selected) => {
|
||||||
this._overlays.forEach((_, id) => {
|
ids.forEach((id) => {
|
||||||
remove(overlays, id);
|
remove(selected, id);
|
||||||
});
|
});
|
||||||
return overlays;
|
return selected;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const extensionAPI = new ExtensionAPI(map);
|
export const extensionAPI = new ExtensionAPI();
|
||||||
|
|||||||
@@ -101,7 +101,9 @@ export class OverpassLayer {
|
|||||||
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
|
||||||
}
|
}
|
||||||
@@ -283,10 +285,12 @@ function getQuery(query: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQueryItem(tags: Record<string, string | boolean | string[]>) {
|
function getQueryItem(tags: Record<string, string | string[]>) {
|
||||||
let arrayEntry = Object.values(tags).find((value) => Array.isArray(value));
|
let arrayEntry = Object.entries(tags).find((entry): entry is [string, string[]] =>
|
||||||
|
Array.isArray(entry[1])
|
||||||
|
);
|
||||||
if (arrayEntry !== undefined) {
|
if (arrayEntry !== undefined) {
|
||||||
return arrayEntry
|
return arrayEntry[1]
|
||||||
.map(
|
.map(
|
||||||
(val) =>
|
(val) =>
|
||||||
`nwr${Object.entries(tags)
|
`nwr${Object.entries(tags)
|
||||||
@@ -309,7 +313,7 @@ function belongsToQuery(element: any, query: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function belongsToQueryItem(element: any, tags: Record<string, string | boolean | string[]>) {
|
function belongsToQueryItem(element: any, tags: Record<string, string | 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
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -66,12 +66,12 @@ export function remove(node: LayerTreeType, id: string) {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeByPrefix(node: LayerTreeType, prefix: string) {
|
export function removeAll(node: LayerTreeType, ids: string[]) {
|
||||||
Object.keys(node).forEach((key) => {
|
Object.keys(node).forEach((key) => {
|
||||||
if (key.startsWith(prefix)) {
|
if (ids.includes(key)) {
|
||||||
delete node[key];
|
delete node[key];
|
||||||
} else if (typeof node[key] !== 'boolean') {
|
} else if (typeof node[key] !== 'boolean') {
|
||||||
remove(node[key], prefix);
|
removeAll(node[key], ids);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return node;
|
return node;
|
||||||
|
|||||||
@@ -3,8 +3,16 @@ 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 { treeFileView, elevationProfile, bottomPanelSize, rightPanelSize, distanceUnits } = settings;
|
const {
|
||||||
|
treeFileView,
|
||||||
|
elevationProfile,
|
||||||
|
bottomPanelSize,
|
||||||
|
rightPanelSize,
|
||||||
|
distanceUnits,
|
||||||
|
terrainSource,
|
||||||
|
} = settings;
|
||||||
|
|
||||||
let fitBoundsOptions: mapboxgl.MapOptions['fitBoundsOptions'] = {
|
let fitBoundsOptions: mapboxgl.MapOptions['fitBoundsOptions'] = {
|
||||||
maxZoom: 15,
|
maxZoom: 15,
|
||||||
@@ -35,17 +43,6 @@ export class MapboxGLMap {
|
|||||||
sources: {},
|
sources: {},
|
||||||
layers: [],
|
layers: [],
|
||||||
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: '',
|
||||||
@@ -134,39 +131,26 @@ 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', () => {
|
map.on('pitch', this.setTerrain.bind(this));
|
||||||
if (map.getPitch() > 0) {
|
this.setTerrain();
|
||||||
map.setTerrain({
|
});
|
||||||
source: 'mapbox-dem',
|
map.on('style.import.load', () => {
|
||||||
exaggeration: 1,
|
const basemap = map.getStyle().imports?.find((imprt) => imprt.id === 'basemap');
|
||||||
});
|
if (basemap && basemap.data && basemap.data.glyphs) {
|
||||||
} else {
|
map.setGlyphsUrl(basemap.data.glyphs);
|
||||||
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));
|
||||||
@@ -182,6 +166,7 @@ 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) {
|
||||||
@@ -222,6 +207,24 @@ export class MapboxGLMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTerrain() {
|
||||||
|
const map = get(this._map);
|
||||||
|
if (map) {
|
||||||
|
const source = get(terrainSource);
|
||||||
|
if (!map.getSource(source)) {
|
||||||
|
map.addSource(source, terrainSources[source]);
|
||||||
|
}
|
||||||
|
if (map.getPitch() > 0) {
|
||||||
|
map.setTerrain({
|
||||||
|
source: source,
|
||||||
|
exaggeration: 1,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
map.setTerrain(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const map = new MapboxGLMap();
|
export const map = new MapboxGLMap();
|
||||||
|
|||||||
@@ -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(false);
|
let artificial = $state(true);
|
||||||
|
|
||||||
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 './reduce.svelte';
|
import { minTolerance, ReducedGPXLayerCollection, tolerance } from './utils.svelte';
|
||||||
|
|
||||||
let props: { class?: string } = $props();
|
let props: { class?: string } = $props();
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { GPXFileStateCollectionObserver, type GPXFileState } from '$lib/logic/fi
|
|||||||
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, type Writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
|
|
||||||
export const minTolerance = 0.1;
|
export const minTolerance = 0.1;
|
||||||
|
|
||||||
@@ -28,17 +28,15 @@ export class ReducedGPXLayer {
|
|||||||
|
|
||||||
update() {
|
update() {
|
||||||
const file = this._fileState.file;
|
const file = this._fileState.file;
|
||||||
const stats = this._fileState.statistics;
|
if (!file) {
|
||||||
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,
|
||||||
statistics.local.points.length,
|
segment.trkpt.length,
|
||||||
ramerDouglasPeucker(statistics.local.points, minTolerance),
|
ramerDouglasPeucker(segment.trkpt, minTolerance),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
SquareArrowUpLeft,
|
SquareArrowUpLeft,
|
||||||
SquareArrowOutDownRight,
|
SquareArrowOutDownRight,
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import { brouterProfiles } from '$lib/components/toolbar/tools/routing/routing';
|
import { routingProfiles } from '$lib/components/toolbar/tools/routing/routing';
|
||||||
import { i18n } from '$lib/i18n.svelte';
|
import { i18n } from '$lib/i18n.svelte';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import {
|
import {
|
||||||
@@ -163,11 +163,11 @@
|
|||||||
{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="h-8 grow">
|
<Select.Trigger class="grow" size="sm">
|
||||||
{i18n._(`toolbar.routing.activities.${$routingProfile}`)}
|
{i18n._(`toolbar.routing.activities.${$routingProfile}`)}
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
{#each Object.keys(brouterProfiles) as profile}
|
{#each Object.keys(routingProfiles) as profile}
|
||||||
<Select.Item value={profile}
|
<Select.Item value={profile}
|
||||||
>{i18n._(
|
>{i18n._(
|
||||||
`toolbar.routing.activities.${profile}`
|
`toolbar.routing.activities.${profile}`
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
disabled={!validSelection}
|
disabled={!validSelection}
|
||||||
onclick={fileActions.reverseSelection}
|
onclick={fileActions.reverseSelection}
|
||||||
>
|
>
|
||||||
<ArrowRightLeft size="12" />{i18n._('toolbar.routing.reverse.button')}
|
<ArrowRightLeft class="size-3" />{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 size="12" />{i18n._('toolbar.routing.route_back_to_start.button')}
|
<House class="size-3" />{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 size="12" />{i18n._('toolbar.routing.round_trip.button')}
|
<Repeat class="size-3" />{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">
|
||||||
|
|||||||
@@ -731,17 +731,7 @@ export class RoutingControls {
|
|||||||
try {
|
try {
|
||||||
response = await route(targetCoordinates);
|
response = await route(targetCoordinates);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.message.includes('from-position not mapped in existing datafile')) {
|
toast.error(i18n._(e.message, e.message));
|
||||||
toast.error(i18n._('toolbar.routing.error.from'));
|
|
||||||
} else if (e.message.includes('via1-position not mapped in existing datafile')) {
|
|
||||||
toast.error(i18n._('toolbar.routing.error.via'));
|
|
||||||
} else if (e.message.includes('to-position not mapped in existing datafile')) {
|
|
||||||
toast.error(i18n._('toolbar.routing.error.to'));
|
|
||||||
} else if (e.message.includes('Time-out')) {
|
|
||||||
toast.error(i18n._('toolbar.routing.error.timeout'));
|
|
||||||
} else {
|
|
||||||
toast.error(e.message);
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,24 +783,25 @@ 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 =
|
||||||
stats.local.distance.moving[anchors[anchors.length - 1].point._data.index] -
|
endAnchorStats.distance.moving - startAnchorStats.distance.moving;
|
||||||
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 -
|
||||||
(stats.local.time.moving[anchors[anchors.length - 1].point._data.index] -
|
(endAnchorStats.time.moving - startAnchorStats.time.moving);
|
||||||
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 =
|
replacingTime = endAnchorStats.time.total - startAnchorStats.time.total;
|
||||||
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;
|
||||||
@@ -820,9 +811,7 @@ 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 +
|
(replacingTime + endAnchorStats.time.total - endAnchorStats.time.moving) *
|
||||||
stats.local.time.total[endIndex] -
|
|
||||||
stats.local.time.moving[endIndex]) *
|
|
||||||
1000
|
1000
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,37 +6,213 @@ import { get } from 'svelte/store';
|
|||||||
|
|
||||||
const { routing, routingProfile, privateRoads } = settings;
|
const { routing, routingProfile, privateRoads } = settings;
|
||||||
|
|
||||||
export const brouterProfiles: { [key: string]: string } = {
|
export type RoutingProfile = {
|
||||||
bike: 'Trekking-dry',
|
engine: 'graphhopper' | 'brouter';
|
||||||
racing_bike: 'fastbike',
|
profile: string;
|
||||||
gravel_bike: 'gravel',
|
};
|
||||||
mountain_bike: 'MTB',
|
|
||||||
foot: 'Hiking-Alpine-SAC6',
|
export const routingProfiles: { [key: string]: RoutingProfile } = {
|
||||||
motorcycle: 'Car-FastEco',
|
bike: { engine: 'graphhopper', profile: 'bike' },
|
||||||
water: 'river',
|
racing_bike: { engine: 'graphhopper', profile: 'racingbike' },
|
||||||
railway: 'rail',
|
gravel_bike: { engine: 'graphhopper', profile: 'gravelbike' },
|
||||||
|
mountain_bike: { engine: 'graphhopper', profile: 'mtb' },
|
||||||
|
foot: { engine: 'graphhopper', profile: 'foot' },
|
||||||
|
motorcycle: { engine: 'graphhopper', profile: 'motorcycle' },
|
||||||
|
water: { engine: 'brouter', profile: 'river' },
|
||||||
|
railway: { engine: 'brouter', profile: 'rail' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export function route(points: Coordinates[]): Promise<TrackPoint[]> {
|
export function route(points: Coordinates[]): Promise<TrackPoint[]> {
|
||||||
if (get(routing)) {
|
if (get(routing)) {
|
||||||
return getRoute(points, brouterProfiles[get(routingProfile)], get(privateRoads));
|
const profile = routingProfiles[get(routingProfile)];
|
||||||
|
if (profile.engine === 'graphhopper') {
|
||||||
|
return getGraphHopperRoute(points, profile.profile, get(privateRoads));
|
||||||
|
} else {
|
||||||
|
return getBRouterRoute(points, profile.profile);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return getIntermediatePoints(points);
|
return getIntermediatePoints(points);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRoute(
|
const graphhopperDetails = ['road_class', 'surface', 'hike_rating', 'mtb_rating'];
|
||||||
|
const hikeRatingToSACScale: { [key: string]: string } = {
|
||||||
|
'1': 'hiking',
|
||||||
|
'2': 'mountain_hiking',
|
||||||
|
'3': 'demanding_mountain_hiking',
|
||||||
|
'4': 'alpine_hiking',
|
||||||
|
'5': 'demanding_alpine_hiking',
|
||||||
|
'6': 'difficult_alpine_hiking',
|
||||||
|
};
|
||||||
|
const mtbRatingToScale: { [key: string]: string } = {
|
||||||
|
'1': '0',
|
||||||
|
'2': '1',
|
||||||
|
'3': '2',
|
||||||
|
'4': '3',
|
||||||
|
'5': '4',
|
||||||
|
'6': '5',
|
||||||
|
'7': '6',
|
||||||
|
};
|
||||||
|
|
||||||
|
const graphhopperBlockPrivateCustomModels: { [key: string]: any } = {
|
||||||
|
bike: {
|
||||||
|
priority: [
|
||||||
|
{
|
||||||
|
if: 'bike_road_access == PRIVATE',
|
||||||
|
multiply_by: '0.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
racingbike: {
|
||||||
|
priority: [
|
||||||
|
{
|
||||||
|
if: 'bike_road_access == PRIVATE',
|
||||||
|
multiply_by: '0.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
gravelbike: {
|
||||||
|
priority: [
|
||||||
|
{
|
||||||
|
if: 'bike_road_access == PRIVATE',
|
||||||
|
multiply_by: '0.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
mtb: {
|
||||||
|
priority: [
|
||||||
|
{
|
||||||
|
if: 'bike_road_access == PRIVATE',
|
||||||
|
multiply_by: '0.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
foot: {
|
||||||
|
priority: [
|
||||||
|
{
|
||||||
|
if: 'foot_road_access == PRIVATE',
|
||||||
|
multiply_by: '0.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
motorcycle: {
|
||||||
|
priority: [
|
||||||
|
{
|
||||||
|
if: 'road_access == PRIVATE',
|
||||||
|
multiply_by: '0.0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
async function getGraphHopperRoute(
|
||||||
points: Coordinates[],
|
points: Coordinates[],
|
||||||
brouterProfile: string,
|
graphHopperProfile: string,
|
||||||
privateRoads: boolean
|
privateRoads: boolean
|
||||||
): Promise<TrackPoint[]> {
|
): Promise<TrackPoint[]> {
|
||||||
let url = `https://brouter.gpx.studio?lonlats=${points.map((point) => `${point.lon.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile + (privateRoads ? '-private' : '')}&format=geojson&alternativeidx=0`;
|
let response = await fetch('https://graphhopper.gpx.studio/route', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
points: points.map((point) => [point.lon, point.lat]),
|
||||||
|
profile: graphHopperProfile,
|
||||||
|
elevation: true,
|
||||||
|
points_encoded: false,
|
||||||
|
details: graphhopperDetails,
|
||||||
|
custom_model: privateRoads
|
||||||
|
? {}
|
||||||
|
: graphhopperBlockPrivateCustomModels[graphHopperProfile] || {},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
if (error.message.includes('Cannot find point 0')) {
|
||||||
|
throw new Error('toolbar.routing.error.from');
|
||||||
|
} else if (error.message.includes('Cannot find point 1')) {
|
||||||
|
if (points.length == 3) {
|
||||||
|
throw new Error('toolbar.routing.error.via');
|
||||||
|
} else {
|
||||||
|
throw new Error('toolbar.routing.error.to');
|
||||||
|
}
|
||||||
|
} else if (error.hints[0].details.includes('PointDistanceExceededException')) {
|
||||||
|
throw new Error('toolbar.routing.error.distance');
|
||||||
|
} else if (error.hints[0].details.includes('ConnectionNotFoundException')) {
|
||||||
|
throw new Error('toolbar.routing.error.connection');
|
||||||
|
} else {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let json = await response.json();
|
||||||
|
|
||||||
|
let route: TrackPoint[] = [];
|
||||||
|
let coordinates = json.paths[0].points.coordinates;
|
||||||
|
let details = json.paths[0].details;
|
||||||
|
|
||||||
|
for (let i = 0; i < coordinates.length; i++) {
|
||||||
|
route.push(
|
||||||
|
new TrackPoint({
|
||||||
|
attributes: {
|
||||||
|
lat: coordinates[i][1],
|
||||||
|
lon: coordinates[i][0],
|
||||||
|
},
|
||||||
|
ele: coordinates[i][2] ?? (i > 0 ? route[i - 1].ele : 0),
|
||||||
|
extensions: {},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let key of graphhopperDetails) {
|
||||||
|
let detail = details[key];
|
||||||
|
for (let i = 0; i < detail.length; i++) {
|
||||||
|
for (let j = detail[i][0]; j < detail[i][1] + (i == detail.length - 1); j++) {
|
||||||
|
if (detail[i][2] !== undefined && detail[i][2] !== 'missing') {
|
||||||
|
if (key === 'road_class') {
|
||||||
|
route[j].setExtension('highway', detail[i][2]);
|
||||||
|
} else if (key === 'hike_rating') {
|
||||||
|
const sacScale = hikeRatingToSACScale[detail[i][2]];
|
||||||
|
if (sacScale) {
|
||||||
|
route[j].setExtension('sac_scale', sacScale);
|
||||||
|
}
|
||||||
|
} else if (key === 'mtb_rating') {
|
||||||
|
const mtbScale = mtbRatingToScale[detail[i][2]];
|
||||||
|
if (mtbScale) {
|
||||||
|
route[j].setExtension('mtb_scale', mtbScale);
|
||||||
|
}
|
||||||
|
} else if (key === 'surface' && detail[i][2] !== 'other') {
|
||||||
|
route[j].setExtension('surface', detail[i][2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBRouterRoute(
|
||||||
|
points: Coordinates[],
|
||||||
|
brouterProfile: string
|
||||||
|
): Promise<TrackPoint[]> {
|
||||||
|
let url = `https://brouter.de/brouter?lonlats=${points.map((point) => `${point.lon.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile}&format=geojson&alternativeidx=0`;
|
||||||
|
|
||||||
let response = await fetch(url);
|
let response = await fetch(url);
|
||||||
|
|
||||||
// Check if the response is ok
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`${await response.text()}`);
|
const error = await response.text();
|
||||||
|
if (error.includes('from-position not mapped in existing datafile')) {
|
||||||
|
throw new Error('toolbar.routing.error.from');
|
||||||
|
} else if (error.includes('via1-position not mapped in existing datafile')) {
|
||||||
|
throw new Error('toolbar.routing.error.via');
|
||||||
|
} else if (error.includes('to-position not mapped in existing datafile')) {
|
||||||
|
throw new Error('toolbar.routing.error.to');
|
||||||
|
} else if (error.includes('Time-out')) {
|
||||||
|
throw new Error('toolbar.routing.error.timeout');
|
||||||
|
} else {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let geojson = await response.json();
|
let geojson = await response.json();
|
||||||
@@ -52,14 +228,13 @@ async function getRoute(
|
|||||||
let tags = messageIdx < messages.length ? getTags(messages[messageIdx][tagIdx]) : {};
|
let tags = messageIdx < messages.length ? getTags(messages[messageIdx][tagIdx]) : {};
|
||||||
|
|
||||||
for (let i = 0; i < coordinates.length; i++) {
|
for (let i = 0; i < coordinates.length; i++) {
|
||||||
let coord = coordinates[i];
|
|
||||||
route.push(
|
route.push(
|
||||||
new TrackPoint({
|
new TrackPoint({
|
||||||
attributes: {
|
attributes: {
|
||||||
lat: coord[1],
|
lat: coordinates[i][1],
|
||||||
lon: coord[0],
|
lon: coordinates[i][0],
|
||||||
},
|
},
|
||||||
ele: coord[2] ?? (i > 0 ? route[i - 1].ele : 0),
|
ele: coordinates[i][2] ?? (i > 0 ? route[i - 1].ele : 0),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -26,12 +26,10 @@
|
|||||||
|
|
||||||
let validSelection = $derived(
|
let validSelection = $derived(
|
||||||
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
|
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
|
||||||
$gpxStatistics.local.points.length > 0
|
$gpxStatistics.global.length > 0
|
||||||
);
|
);
|
||||||
let maxSliderValue = $derived(
|
let maxSliderValue = $derived(
|
||||||
validSelection && $gpxStatistics.local.points.length > 0
|
validSelection && $gpxStatistics.global.length > 0 ? $gpxStatistics.global.length - 1 : 1
|
||||||
? $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);
|
||||||
@@ -45,7 +43,7 @@
|
|||||||
function updateSlicedGPXStatistics() {
|
function updateSlicedGPXStatistics() {
|
||||||
if (validSelection && canCrop) {
|
if (validSelection && canCrop) {
|
||||||
$slicedGPXStatistics = [
|
$slicedGPXStatistics = [
|
||||||
get(gpxStatistics).slice(sliderValues[0], sliderValues[1]),
|
get(gpxStatistics).sliced(sliderValues[0], sliderValues[1]),
|
||||||
sliderValues[0],
|
sliderValues[0],
|
||||||
sliderValues[1],
|
sliderValues[1],
|
||||||
];
|
];
|
||||||
@@ -107,7 +105,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="h-8 w-fit grow">
|
<Select.Trigger class="w-fit grow" size="sm">
|
||||||
{i18n._('gpx.' + $splitAs)}
|
{i18n._('gpx.' + $splitAs)}
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
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';
|
||||||
@@ -9,20 +7,41 @@ 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';
|
||||||
|
|
||||||
export class SplitControls {
|
export class SplitControls {
|
||||||
active: boolean = false;
|
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
controls: ControlWithMarker[] = [];
|
|
||||||
shownControls: ControlWithMarker[] = [];
|
|
||||||
unsubscribes: Function[] = [];
|
unsubscribes: Function[] = [];
|
||||||
|
|
||||||
toggleControlsForZoomLevelAndBoundsBinded: () => void =
|
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
|
||||||
this.toggleControlsForZoomLevelAndBounds.bind(this);
|
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.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)));
|
||||||
@@ -31,29 +50,18 @@ export class SplitControls {
|
|||||||
addIfNeeded() {
|
addIfNeeded() {
|
||||||
let scissors = get(currentTool) === Tool.SCISSORS;
|
let scissors = get(currentTool) === Tool.SCISSORS;
|
||||||
if (!scissors) {
|
if (!scissors) {
|
||||||
if (this.active) {
|
this.remove();
|
||||||
this.remove();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.active) {
|
this.updateControls();
|
||||||
this.updateControls();
|
|
||||||
} else {
|
|
||||||
this.add();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
add() {
|
|
||||||
this.active = true;
|
|
||||||
|
|
||||||
this.map.on('zoom', this.toggleControlsForZoomLevelAndBoundsBinded);
|
|
||||||
this.map.on('move', this.toggleControlsForZoomLevelAndBoundsBinded);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateControls() {
|
updateControls() {
|
||||||
// Update the markers when the files change
|
let data: GeoJSON.FeatureCollection = {
|
||||||
let controlIndex = 0;
|
type: 'FeatureCollection',
|
||||||
|
features: [],
|
||||||
|
};
|
||||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||||
let file = fileStateCollection.getFile(fileId);
|
let file = fileStateCollection.getFile(fileId);
|
||||||
|
|
||||||
@@ -64,30 +72,23 @@ export class SplitControls {
|
|||||||
new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
|
new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
for (let point of segment.trkpt.slice(1, -1)) {
|
for (let i = 1; i < segment.trkpt.length - 1; i++) {
|
||||||
// Update the existing controls (could be improved by matching the existing controls with the new ones?)
|
let point = segment.trkpt[i];
|
||||||
if (point._data.anchor) {
|
if (point._data.anchor) {
|
||||||
if (controlIndex < this.controls.length) {
|
data.features.push({
|
||||||
this.controls[controlIndex].fileId = fileId;
|
type: 'Feature',
|
||||||
this.controls[controlIndex].point = point;
|
geometry: {
|
||||||
this.controls[controlIndex].segment = segment;
|
type: 'Point',
|
||||||
this.controls[controlIndex].trackIndex = trackIndex;
|
coordinates: [point.getLongitude(), point.getLatitude()],
|
||||||
this.controls[controlIndex].segmentIndex = segmentIndex;
|
},
|
||||||
this.controls[controlIndex].marker.setLngLat(
|
properties: {
|
||||||
point.getCoordinates()
|
fileId: fileId,
|
||||||
);
|
trackIndex: trackIndex,
|
||||||
} else {
|
segmentIndex: segmentIndex,
|
||||||
this.controls.push(
|
pointIndex: i,
|
||||||
this.createControl(
|
minZoom: point._data.zoom,
|
||||||
point,
|
},
|
||||||
segment,
|
});
|
||||||
fileId,
|
|
||||||
trackIndex,
|
|
||||||
segmentIndex
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
controlIndex++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,86 +96,77 @@ export class SplitControls {
|
|||||||
}
|
}
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
while (controlIndex < this.controls.length) {
|
try {
|
||||||
// Remove the extra controls
|
let source = this.map.getSource('split-controls') as mapboxgl.GeoJSONSource | undefined;
|
||||||
this.controls.pop()?.marker.remove();
|
if (source) {
|
||||||
}
|
source.setData(data);
|
||||||
|
} else {
|
||||||
|
this.map.addSource('split-controls', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.toggleControlsForZoomLevelAndBounds();
|
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']],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.map.on('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
|
||||||
|
this.map.on('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
|
||||||
|
this.map.on('click', 'split-controls', this.layerOnClickBinded);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.map.moveLayer('split-controls');
|
||||||
|
} catch (e) {
|
||||||
|
// No reliable way to check if the map is ready to add sources and layers
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.active = false;
|
this.map.off('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
|
||||||
|
this.map.off('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
|
||||||
|
this.map.off('click', 'split-controls', this.layerOnClickBinded);
|
||||||
|
|
||||||
for (let control of this.controls) {
|
try {
|
||||||
control.marker.remove();
|
if (this.map.getLayer('split-controls')) {
|
||||||
}
|
this.map.removeLayer('split-controls');
|
||||||
this.map.off('zoom', this.toggleControlsForZoomLevelAndBoundsBinded);
|
|
||||||
this.map.off('move', this.toggleControlsForZoomLevelAndBoundsBinded);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleControlsForZoomLevelAndBounds() {
|
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createControl(
|
layerOnMouseEnter(e: any) {
|
||||||
point: TrackPoint,
|
mapCursor.notify(MapCursorState.SPLIT_CONTROL, true);
|
||||||
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"');
|
|
||||||
|
|
||||||
let marker = new mapboxgl.Marker({
|
layerOnMouseLeave() {
|
||||||
draggable: true,
|
mapCursor.notify(MapCursorState.SPLIT_CONTROL, false);
|
||||||
className: 'z-10',
|
}
|
||||||
element,
|
|
||||||
}).setLngLat(point.getCoordinates());
|
|
||||||
|
|
||||||
let control = {
|
layerOnClick(e: mapboxgl.MapMouseEvent) {
|
||||||
point,
|
let coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates;
|
||||||
segment,
|
fileActions.split(
|
||||||
fileId,
|
get(splitAs),
|
||||||
trackIndex,
|
e.features![0].properties!.fileId,
|
||||||
segmentIndex,
|
e.features![0].properties!.trackIndex,
|
||||||
marker,
|
e.features![0].properties!.segmentIndex,
|
||||||
inZoom: false,
|
{ lon: coordinates[0], lat: coordinates[1] },
|
||||||
};
|
e.features![0].properties!.pointIndex
|
||||||
|
);
|
||||||
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() {
|
||||||
@@ -182,16 +174,3 @@ 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,6 +16,8 @@
|
|||||||
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;
|
||||||
@@ -39,6 +41,21 @@
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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];
|
||||||
@@ -54,14 +71,7 @@
|
|||||||
latitude = parseFloat(wpt.getLatitude().toFixed(6));
|
latitude = parseFloat(wpt.getLatitude().toFixed(6));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
untrack(() => {
|
untrack(reset);
|
||||||
name = '';
|
|
||||||
description = '';
|
|
||||||
link = '';
|
|
||||||
sym = '';
|
|
||||||
longitude = 0;
|
|
||||||
latitude = 0;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,14 +95,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,
|
sym: sym.length > 0 ? sym : undefined,
|
||||||
},
|
},
|
||||||
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
|
||||||
);
|
);
|
||||||
|
|
||||||
selectedWaypoint.reset();
|
reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCoordinates(e: any) {
|
function setCoordinates(e: any) {
|
||||||
@@ -100,6 +110,37 @@
|
|||||||
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);
|
||||||
@@ -112,6 +153,10 @@
|
|||||||
$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>
|
||||||
|
|
||||||
@@ -129,19 +174,27 @@
|
|||||||
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"
|
||||||
class="w-full h-8"
|
size="sm"
|
||||||
|
class="w-full"
|
||||||
disabled={!canCreate && !$selectedWaypoint}
|
disabled={!canCreate && !$selectedWaypoint}
|
||||||
>
|
>
|
||||||
{#if symbolKey}
|
<span class="flex flex-row gap-1.5 items-center">
|
||||||
{i18n._(`gpx.symbol.${symbolKey}`)}
|
{#if symbolKey}
|
||||||
{:else}
|
{#if symbols[symbolKey].icon}
|
||||||
{sym}
|
{@const Component = symbols[symbolKey].icon}
|
||||||
{/if}
|
<Component size="14" />
|
||||||
|
{/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]}
|
||||||
@@ -149,7 +202,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 mr-0.5" />
|
<Component size="14" class="inline-block align-sub" />
|
||||||
{:else}
|
{:else}
|
||||||
<span class="w-4 inline-block"></span>
|
<span class="w-4 inline-block"></span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -210,7 +263,7 @@
|
|||||||
{i18n._('toolbar.waypoint.create')}
|
{i18n._('toolbar.waypoint.create')}
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="icon" onclick={() => selectedWaypoint.reset()}>
|
<Button variant="outline" size="icon" onclick={reset}>
|
||||||
<CircleX size="16" />
|
<CircleX size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { HeartHandshake } from '@lucide/svelte';
|
import { HeartHandshake } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Help keep the website free (and ad-free)
|
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||||
|
|
||||||
Each time you add or move GPS points, our servers calculate the best route on the road network.
|
Each time you add or move GPS points, our servers calculate the best route on the road network.
|
||||||
We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Languages } from '@lucide/svelte';
|
import { Languages } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Translation
|
## <Languages size="18" class="inline-block align-baseline" /> Translation
|
||||||
|
|
||||||
The website is translated by volunteers using a collaborative translation platform.
|
The website is translated by volunteers using a collaborative translation platform.
|
||||||
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
|
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ You can also drag and drop files directly from your file system into the window.
|
|||||||
|
|
||||||
Create a copy of the currently selected files.
|
Create a copy of the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||||
|
|
||||||
Close the currently selected files.
|
Delete the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close all
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||||
|
|
||||||
Close all files.
|
Delete all files.
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Route planning and editing
|
|||||||
---
|
---
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||||
@@ -71,7 +71,7 @@ The following tools automate some common route modification operations.
|
|||||||
|
|
||||||
Reverse the direction of the route.
|
Reverse the direction of the route.
|
||||||
|
|
||||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
||||||
|
|
||||||
Connect the last point of the route with the starting point, using the chosen routing settings.
|
Connect the last point of the route with the starting point, using the chosen routing settings.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { HeartHandshake } from '@lucide/svelte';
|
import { HeartHandshake } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Ajuda a mantenir aquesta pàgina web gratuïta (i sense anuncis)
|
## <HeartHandshake size="18" class="inline-block align-baseline" /> Ajuda a mantenir aquesta pàgina web gratuïta (i sense anuncis)
|
||||||
|
|
||||||
Cada cop que afegeixes o mous un punt GPS, els nostres servidors calculen la millor ruta possible.
|
Cada cop que afegeixes o mous un punt GPS, els nostres servidors calculen la millor ruta possible.
|
||||||
També utilitzen l'API de <a href="https://mapbox.com" target="_blank">Mapbox</a> per ensenyar mapes bonics, donar informació sobre l'altitud i permetre la cerca de llocs d'interès.
|
També utilitzen l'API de <a href="https://mapbox.com" target="_blank">Mapbox</a> per ensenyar mapes bonics, donar informació sobre l'altitud i permetre la cerca de llocs d'interès.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Languages } from '@lucide/svelte';
|
import { Languages } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <Languages size="18" class="mr-1 inline-block align-baseline" />Traducció
|
## <Languages size="18" class="inline-block align-baseline" /> Traducció
|
||||||
|
|
||||||
Aquesta pàgina web ha estat traduïda per voluntaris utilitzant una plataforma de traducció col·laborativa.
|
Aquesta pàgina web ha estat traduïda per voluntaris utilitzant una plataforma de traducció col·laborativa.
|
||||||
Tu també pots contribuir-hi afegint o millorant les traduccions al nostre <a href="https://crowdin.com/project/gpxstudio" target="_blank">projecte de Crowdin</a>.
|
Tu també pots contribuir-hi afegint o millorant les traduccions al nostre <a href="https://crowdin.com/project/gpxstudio" target="_blank">projecte de Crowdin</a>.
|
||||||
|
|||||||
@@ -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" /> Tanca
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||||
|
|
||||||
Tanca els arxius seleccionats.
|
Delete the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Tanca tot
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||||
|
|
||||||
Tanca tots els arxius.
|
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...
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Planificació i edició de rutes
|
|||||||
---
|
---
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||||
@@ -71,7 +71,7 @@ Les eines següents automatitzen algunes operacions comunes de modificació de r
|
|||||||
|
|
||||||
Inverteix el sentit de la ruta.
|
Inverteix el sentit de la ruta.
|
||||||
|
|
||||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Tornar a l'inici
|
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Tornar a l'inici
|
||||||
|
|
||||||
Connecta l'últim punt de la ruta amb el punt d'inici, utilitzant la configuració de ruta escollida.
|
Connecta l'últim punt de la ruta amb el punt d'inici, utilitzant la configuració de ruta escollida.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { HeartHandshake } from '@lucide/svelte';
|
import { HeartHandshake } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Pomozte udržet web zdarma (a bez reklam)
|
## <HeartHandshake size="18" class="inline-block align-baseline" /> Pomozte udržet web zdarma (a bez reklam)
|
||||||
|
|
||||||
Vždy, když přidáte nebo přesunete GPS body, naše servery vypočítají nejlepší cestu po silniční síti.
|
Vždy, když přidáte nebo přesunete GPS body, naše servery vypočítají nejlepší cestu po silniční síti.
|
||||||
Používáme také API z <a href="https://mapbox.com" target="_blank">Mapboxu</a> pro zobrazení krásných map, získání dat o nadmořské výšce a vyhledávání míst.
|
Používáme také API z <a href="https://mapbox.com" target="_blank">Mapboxu</a> pro zobrazení krásných map, získání dat o nadmořské výšce a vyhledávání míst.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Languages } from '@lucide/svelte';
|
import { Languages } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Překlad
|
## <Languages size="18" class="inline-block align-baseline" /> Překlad
|
||||||
|
|
||||||
Tento web je překládán dobrovolníky prostřednictvím kolaborativní překladatelské platformy.
|
Tento web je překládán dobrovolníky prostřednictvím kolaborativní překladatelské platformy.
|
||||||
Ke zlepšení překladů můžete přispět na našem <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin projektu</a>.
|
Ke zlepšení překladů můžete přispět na našem <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin projektu</a>.
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ 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" /> Zavřít
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Smazat
|
||||||
|
|
||||||
Zavřít aktuálně vybrané soubory.
|
Smazat aktuálně vybrané soubory.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Zavřít vše
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Smazat vše
|
||||||
|
|
||||||
Zavřít všechny soubory.
|
Smazat všechny soubory.
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Plánování a úpravy tras
|
|||||||
---
|
---
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||||
@@ -24,7 +24,7 @@ Dialog lze minimalizovat kliknutím na <button><SquareArrowUpLeft size="16" clas
|
|||||||
|
|
||||||
### <Route size="16" class="inline-block" style="margin-bottom: 2px" /> Plánování tras
|
### <Route size="16" class="inline-block" style="margin-bottom: 2px" /> Plánování tras
|
||||||
|
|
||||||
Když je plánování trasy aktivní, kotevní body umístěné nebo přesunuté po mapě budou projenou trasou vypočítanou v silniční síti <a href="https://www.openstreetmap.org" target="_blank">OpenStreetMap</a>.
|
Když je plánování trasy aktivní, kotevní body umístěné nebo přesunuté po mapě budou propojeny trasou vypočítanou v silniční síti <a href="https://www.openstreetmap.org" target="_blank">OpenStreetMap</a>.
|
||||||
Zakázat plánování trasy pro propojení kotevních bodů přímými čarami.
|
Zakázat plánování trasy pro propojení kotevních bodů přímými čarami.
|
||||||
Toto nastavení lze také přepnout stisknutím <kbd>F5</kbd>.
|
Toto nastavení lze také přepnout stisknutím <kbd>F5</kbd>.
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ Následující nástroje automatizují některé běžné operace úpravy trasy.
|
|||||||
|
|
||||||
Obrátit směr trasy.
|
Obrátit směr trasy.
|
||||||
|
|
||||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Zpět na začátek
|
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Zpět na začátek
|
||||||
|
|
||||||
Propojit poslední bod trasy s výchozím bodem pomocí zvoleného nastavení směrování.
|
Propojit poslední bod trasy s výchozím bodem pomocí zvoleného nastavení směrování.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { HeartHandshake } from '@lucide/svelte';
|
import { HeartHandshake } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Help keep the website free (and ad-free)
|
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||||
|
|
||||||
Each time you add or move GPS points, our servers calculate the best route on the road network.
|
Each time you add or move GPS points, our servers calculate the best route on the road network.
|
||||||
We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Languages } from '@lucide/svelte';
|
import { Languages } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Oversættelse
|
## <Languages size="18" class="inline-block align-baseline" /> Translation
|
||||||
|
|
||||||
Hjemmesiden er oversat af frivillige ved hjælp af en kollaborativ oversættelsesplatform.
|
Hjemmesiden er oversat af frivillige ved hjælp af en kollaborativ oversættelsesplatform.
|
||||||
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
|
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ You can also drag and drop files directly from your file system into the window.
|
|||||||
|
|
||||||
Create a copy of the currently selected files.
|
Create a copy of the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||||
|
|
||||||
Close the currently selected files.
|
Delete the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close all
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||||
|
|
||||||
Close all files.
|
Delete all files.
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" />Eksporter...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" />Eksporter...
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Route planning and editing
|
|||||||
---
|
---
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||||
@@ -71,7 +71,7 @@ The following tools automate some common route modification operations.
|
|||||||
|
|
||||||
Reverse the direction of the route.
|
Reverse the direction of the route.
|
||||||
|
|
||||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
||||||
|
|
||||||
Connect the last point of the route with the starting point, using the chosen routing settings.
|
Connect the last point of the route with the starting point, using the chosen routing settings.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { HeartHandshake } from '@lucide/svelte';
|
import { HeartHandshake } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Helfen Sie, die Website kostenlos (und werbefrei) zu erhalten
|
## <HeartHandshake size="18" class="inline-block align-baseline" /> Helfen Sie, die Website kostenlos (und werbefrei) zu erhalten
|
||||||
|
|
||||||
Jedes Mal, wenn Sie GPS-Punkte hinzufügen oder verschieben, berechnen unsere Server die beste Route im Straßennetz.
|
Jedes Mal, wenn Sie GPS-Punkte hinzufügen oder verschieben, berechnen unsere Server die beste Route im Straßennetz.
|
||||||
Wir verwenden auch APIs von <a href="https://mapbox.com" target="_blank">Mapbox</a>, um schöne Karten anzuzeigen, Höhendaten abzurufen und Ihnen die Suche nach Orten zu ermöglichen.
|
Wir verwenden auch APIs von <a href="https://mapbox.com" target="_blank">Mapbox</a>, um schöne Karten anzuzeigen, Höhendaten abzurufen und Ihnen die Suche nach Orten zu ermöglichen.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Mapbox ist das Unternehmen, das einige der schönen Karten auf dieser Website zur Verfügung stellt.
|
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> welche **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 ä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.
|
Wir sind äußerst 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.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Languages } from '@lucide/svelte';
|
import { Languages } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Übersetzung
|
## <Languages size="18" class="inline-block align-baseline" /> Übersetzung
|
||||||
|
|
||||||
Die Webseite wird von Freiwilligen mit einer gemeinsamen Übersetzungsplattform übersetzt.
|
Die Webseite wird von Freiwilligen mit einer gemeinsamen Übersetzungsplattform übersetzt.
|
||||||
Sie können dazu beitragen, indem Sie Übersetzungen in unserem <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin Projekt</a> hinzufügen oder verbessern.
|
Sie können dazu beitragen, indem Sie Übersetzungen in unserem <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin Projekt</a> hinzufügen oder verbessern.
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ Sie können auch Dateien per Drag-and-Drop aus Ihrem Dateisystem in das Fenster
|
|||||||
|
|
||||||
Erstelle eine Kopie der aktuell ausgewählten Dateien.
|
Erstelle eine Kopie der aktuell ausgewählten Dateien.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Schließen
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||||
|
|
||||||
Die aktuell ausgewählten Dateien schließen.
|
Delete the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Alle schließen
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||||
|
|
||||||
Schließe alle Dateien.
|
Delete all files.
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportieren...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportieren...
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Routenplanung und Bearbeitung
|
|||||||
---
|
---
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||||
@@ -71,7 +71,7 @@ Die folgenden Tools automatisieren einige gemeinsame Routenmodifikationsoperatio
|
|||||||
|
|
||||||
Die Richtung der Route umkehren.
|
Die Richtung der Route umkehren.
|
||||||
|
|
||||||
### Zurück zum Start
|
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Zurück zum Start
|
||||||
|
|
||||||
Verbinden Sie den letzten Punkt der Route mit dem Startpunkt mit den gewählten Routing-Einstellungen.
|
Verbinden Sie den letzten Punkt der Route mit dem Startpunkt mit den gewählten Routing-Einstellungen.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { HeartHandshake } from '@lucide/svelte';
|
import { HeartHandshake } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Help keep the website free (and ad-free)
|
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||||
|
|
||||||
Each time you add or move GPS points, our servers calculate the best route on the road network.
|
Each time you add or move GPS points, our servers calculate the best route on the road network.
|
||||||
We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Languages } from '@lucide/svelte';
|
import { Languages } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Translation
|
## <Languages size="18" class="inline-block align-baseline" /> Translation
|
||||||
|
|
||||||
The website is translated by volunteers using a collaborative translation platform.
|
The website is translated by volunteers using a collaborative translation platform.
|
||||||
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
|
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ You can also drag and drop files directly from your file system into the window.
|
|||||||
|
|
||||||
Create a copy of the currently selected files.
|
Create a copy of the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||||
|
|
||||||
Close the currently selected files.
|
Delete the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close all
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||||
|
|
||||||
Close all files.
|
Delete all files.
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Route planning and editing
|
|||||||
---
|
---
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||||
@@ -71,7 +71,7 @@ The following tools automate some common route modification operations.
|
|||||||
|
|
||||||
Reverse the direction of the route.
|
Reverse the direction of the route.
|
||||||
|
|
||||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
||||||
|
|
||||||
Connect the last point of the route with the starting point, using the chosen routing settings.
|
Connect the last point of the route with the starting point, using the chosen routing settings.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { HeartHandshake } from '@lucide/svelte';
|
import { HeartHandshake } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Ayude a mantener este sitio gratis (y libre de anuncios)
|
## <HeartHandshake size="18" class="inline-block align-baseline" /> Ayude a mantener este sitio gratis (y libre de anuncios)
|
||||||
|
|
||||||
Cada vez que añade o mueve puntos GPS, nuestros servidores calculan la mejor ruta en la red de carreteras.
|
Cada vez que añade o mueve puntos GPS, nuestros servidores calculan la mejor ruta en la red de carreteras.
|
||||||
También usamos APIs de <a href="https://mapbox.com" target="_blank">Mapbox</a> para mostrar hermosos mapas, obtener datos de elevación y permitirle buscar lugares.
|
También usamos APIs de <a href="https://mapbox.com" target="_blank">Mapbox</a> para mostrar hermosos mapas, obtener datos de elevación y permitirle buscar lugares.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Languages } from '@lucide/svelte';
|
import { Languages } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Traducción
|
## <Languages size="18" class="inline-block align-baseline" /> Traducción
|
||||||
|
|
||||||
Este sitio está traducido por voluntarios usando una plataforma de traducción colaborativa.
|
Este sitio está traducido por voluntarios usando una plataforma de traducción colaborativa.
|
||||||
Puede contribuir añadiendo o mejorando las traducciones en nuestro <a href="https://crowdin.com/project/gpxstudio" target="_blank">proyecto Crowdin</a>.
|
Puede contribuir añadiendo o mejorando las traducciones en nuestro <a href="https://crowdin.com/project/gpxstudio" target="_blank">proyecto Crowdin</a>.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: Integración
|
title: Integraciones
|
||||||
---
|
---
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ Puede arrastrar y soltar archivos directamente desde su sistema de archivos a la
|
|||||||
|
|
||||||
Crear una copia de los archivos seleccionados actualmente.
|
Crear una copia de los archivos seleccionados actualmente.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Cerrar
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Borrar
|
||||||
|
|
||||||
Cerrar los archivos seleccionados actualmente.
|
Borrar los archivos seleccionados.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Cerrar todo
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Borrar todos
|
||||||
|
|
||||||
Cerrar todos los archivos.
|
Borrar todos los archivos.
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportar...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportar...
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Planificación y edición de rutas
|
|||||||
---
|
---
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||||
@@ -71,7 +71,7 @@ Las siguientes herramientas automatizan algunas operaciones comunes de modificac
|
|||||||
|
|
||||||
Invierte el sentido de la ruta.
|
Invierte el sentido de la ruta.
|
||||||
|
|
||||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Volver al inicio
|
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Volver al inicio
|
||||||
|
|
||||||
Conecta el último punto de la ruta con el punto de inicio, usando los ajustes de ruta elegidos.
|
Conecta el último punto de la ruta con el punto de inicio, usando los ajustes de ruta elegidos.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { HeartHandshake } from '@lucide/svelte';
|
import { HeartHandshake } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Lagundu mantentzen webgunea doan (eta propagandarik gabe)
|
## <HeartHandshake size="18" class="inline-block align-baseline" /> Lagundu mantentzen webgunea doan (eta propagandarik gabe)
|
||||||
|
|
||||||
GPS puntuak gehitzen edo mugitzen dituzun bakoitzean, gure zerbitzariek bide sareko ibilbide onena kalkulatzen dute.<a href="https://mapbox.com" target="_blank">Mapbox</a>en APIak erabiltzen ditugu erakusteko mapa argiak, kota-datuak eskaintzeko eta ahalbidetzeko lekuen bilaketa.
|
GPS puntuak gehitzen edo mugitzen dituzun bakoitzean, gure zerbitzariek bide sareko ibilbide onena kalkulatzen dute.<a href="https://mapbox.com" target="_blank">Mapbox</a>en APIak erabiltzen ditugu erakusteko mapa argiak, kota-datuak eskaintzeko eta ahalbidetzeko lekuen bilaketa.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Languages } from '@lucide/svelte';
|
import { Languages } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Itzulpena
|
## <Languages size="18" class="inline-block align-baseline" /> Itzulpena
|
||||||
|
|
||||||
Webgune hau itzulpenerako elkarlaneko plataforma bat erabiltzen duten boluntarioen lanari esker itzultzen da.
|
Webgune hau itzulpenerako elkarlaneko plataforma bat erabiltzen duten boluntarioen lanari esker itzultzen da.
|
||||||
Itzulpenean lagundu dezakezu gure <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin proiektu</a>an.
|
Itzulpenean lagundu dezakezu gure <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin proiektu</a>an.
|
||||||
|
|||||||
@@ -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" /> Itxi
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Ezabatu
|
||||||
|
|
||||||
Itxi hautatutako fitxategiak.
|
Ezabatu hautatutako fitxategiak.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Itxi guztiak
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Ezabatu guztiak
|
||||||
|
|
||||||
Itxi fitxategi guztiak.
|
Ezabatu fitxategi guztiak.
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Esportatu...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Esportatu...
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Ibilbideak planifikatzea eta editatzea
|
|||||||
---
|
---
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||||
@@ -70,7 +70,7 @@ Hurrengo tresnek ibilbidearen aldaketa arrunt batzuk automatizatzen dituzte.
|
|||||||
|
|
||||||
Ibilbidearen norabidea alderantzikatu.
|
Ibilbidearen norabidea alderantzikatu.
|
||||||
|
|
||||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" />Bueltatu abiapuntura
|
### <House size="16" class="inline-block" style="margin-bottom: 2px" />Bueltatu abiapuntura
|
||||||
|
|
||||||
Konektatu ibilbidearen azken puntua abiapuntuarekin, aukeratutako bideratze ezarpenak erabiliz.
|
Konektatu ibilbidearen azken puntua abiapuntuarekin, aukeratutako bideratze ezarpenak erabiliz.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { HeartHandshake } from '@lucide/svelte';
|
import { HeartHandshake } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Help keep the website free (and ad-free)
|
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||||
|
|
||||||
Each time you add or move GPS points, our servers calculate the best route on the road network.
|
Each time you add or move GPS points, our servers calculate the best route on the road network.
|
||||||
We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Languages } from '@lucide/svelte';
|
import { Languages } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Translation
|
## <Languages size="18" class="inline-block align-baseline" /> Translation
|
||||||
|
|
||||||
The website is translated by volunteers using a collaborative translation platform.
|
The website is translated by volunteers using a collaborative translation platform.
|
||||||
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
|
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ You can also drag and drop files directly from your file system into the window.
|
|||||||
|
|
||||||
Create a copy of the currently selected files.
|
Create a copy of the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||||
|
|
||||||
Close the currently selected files.
|
Delete the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close all
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||||
|
|
||||||
Close all files.
|
Delete all files.
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Route planning and editing
|
|||||||
---
|
---
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||||
@@ -71,7 +71,7 @@ The following tools automate some common route modification operations.
|
|||||||
|
|
||||||
Reverse the direction of the route.
|
Reverse the direction of the route.
|
||||||
|
|
||||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
||||||
|
|
||||||
Connect the last point of the route with the starting point, using the chosen routing settings.
|
Connect the last point of the route with the starting point, using the chosen routing settings.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { HeartHandshake } from '@lucide/svelte';
|
import { HeartHandshake } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Aidez à garder le site gratuit (et sans pub)
|
## <HeartHandshake size="18" class="inline-block align-baseline" /> Aidez à garder le site gratuit (et sans pub)
|
||||||
|
|
||||||
Chaque fois que vous ajoutez ou déplacez des points GPS, nos serveurs calculent le meilleur itinéraire sur le réseau routier.
|
Chaque fois que vous ajoutez ou déplacez des points GPS, nos serveurs calculent le meilleur itinéraire sur le réseau routier.
|
||||||
Nous utilisons également des services de <a href="https://mapbox.com" target="_blank">Mapbox</a> pour afficher de magnifiques cartes, récupérer des données d'altitude et vous permettre de rechercher des adresses.
|
Nous utilisons également des services de <a href="https://mapbox.com" target="_blank">Mapbox</a> pour afficher de magnifiques cartes, récupérer des données d'altitude et vous permettre de rechercher des adresses.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Languages } from '@lucide/svelte';
|
import { Languages } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Traduction
|
## <Languages size="18" class="inline-block align-baseline" /> Traduction
|
||||||
|
|
||||||
Le site est traduit par des bénévoles sur une plate-forme de traduction collaborative.
|
Le site est traduit par des bénévoles sur une plate-forme de traduction collaborative.
|
||||||
Vous pouvez contribuer en ajoutant ou en améliorant des traductions sur notre projet <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin</a>.
|
Vous pouvez contribuer en ajoutant ou en améliorant des traductions sur notre projet <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin</a>.
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ Vous pouvez également glisser-déposer des fichiers directement depuis votre sy
|
|||||||
|
|
||||||
Créer une copie des fichiers sélectionnés.
|
Créer une copie des fichiers sélectionnés.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Fermer
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Supprimer
|
||||||
|
|
||||||
Fermer les fichiers sélectionnés.
|
Supprimer les fichiers sélectionnés.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Fermer tout
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Supprimer tout
|
||||||
|
|
||||||
Fermer 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...
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Planification et édition d'itinéraires
|
|||||||
---
|
---
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||||
@@ -71,7 +71,7 @@ Les outils suivants automatisent certaines opérations communes de modification
|
|||||||
|
|
||||||
Inverser le sens de l'itinéraire.
|
Inverser le sens de l'itinéraire.
|
||||||
|
|
||||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Retour au départ
|
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Retour au départ
|
||||||
|
|
||||||
Connecter le dernier point de l'itinéraire avec le point de départ, en utilisant les paramètres de routage choisis.
|
Connecter le dernier point de l'itinéraire avec le point de départ, en utilisant les paramètres de routage choisis.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { HeartHandshake } from '@lucide/svelte';
|
import { HeartHandshake } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## עזור לשמור על אתר זה נקי מפרסומות
|
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||||
|
|
||||||
בכל פעם שאתה מוסיף או מעביר נקודות GPS, השרתים שלנו מחשבים את המסלול הטוב ביותר ברשת הדרכים.
|
בכל פעם שאתה מוסיף או מעביר נקודות GPS, השרתים שלנו מחשבים את המסלול הטוב ביותר ברשת הדרכים.
|
||||||
We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Languages } from '@lucide/svelte';
|
import { Languages } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Translation
|
## <Languages size="18" class="inline-block align-baseline" /> Translation
|
||||||
|
|
||||||
The website is translated by volunteers using a collaborative translation platform.
|
The website is translated by volunteers using a collaborative translation platform.
|
||||||
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
|
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ You can also drag and drop files directly from your file system into the window.
|
|||||||
|
|
||||||
Create a copy of the currently selected files.
|
Create a copy of the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||||
|
|
||||||
Close the currently selected files.
|
Delete the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close all
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||||
|
|
||||||
Close all files.
|
Delete all files.
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Route planning and editing
|
|||||||
---
|
---
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||||
@@ -71,7 +71,7 @@ The following tools automate some common route modification operations.
|
|||||||
|
|
||||||
Reverse the direction of the route.
|
Reverse the direction of the route.
|
||||||
|
|
||||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
||||||
|
|
||||||
Connect the last point of the route with the starting point, using the chosen routing settings.
|
Connect the last point of the route with the starting point, using the chosen routing settings.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { HeartHandshake } from '@lucide/svelte';
|
import { HeartHandshake } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## Segítsen megőrizni a webhely ingyenességét (és reklám mentességét)
|
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||||
|
|
||||||
Minden alkalommal, amikor GPS-pontokat ad hozzá vagy mozgat, szervereink kiszámítják a legjobb útvonalat az úthálózaton.<a href="https://mapbox.com" target="_blank">Mapbox</a> API-jait használjuk a gyönyörű térképek megjelenítésére, a magassági adatok lekérésére és a helyek keresésére.
|
Minden alkalommal, amikor GPS-pontokat ad hozzá vagy mozgat, szervereink kiszámítják a legjobb útvonalat az úthálózaton.<a href="https://mapbox.com" target="_blank">Mapbox</a> API-jait használjuk a gyönyörű térképek megjelenítésére, a magassági adatok lekérésére és a helyek keresésére.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Languages } from '@lucide/svelte';
|
import { Languages } from '@lucide/svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
## Fordítás
|
## <Languages size="18" class="inline-block align-baseline" /> Translation
|
||||||
|
|
||||||
A weboldalt önkéntesek fordítják egy közös fordítói platformon.
|
A weboldalt önkéntesek fordítják egy közös fordítói platformon.
|
||||||
Hozzájárulhat fordítások hozzáadásával vagy javításával <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin projektünkben</a>.
|
Hozzájárulhat fordítások hozzáadásával vagy javításával <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin projektünkben</a>.
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ A fájlokat közvetlenül a fájlrendszerből is áthúzhatja az ablakba.
|
|||||||
|
|
||||||
Készítsen másolatot az aktuálisan kiválasztott fájlokról.
|
Készítsen másolatot az aktuálisan kiválasztott fájlokról.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Bezárás
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||||
|
|
||||||
Zárja be az aktuálisan kiválasztott fájlokat.
|
Delete the currently selected files.
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Az összes bezárása
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||||
|
|
||||||
Zárja be az összes fájlt.
|
Delete all files.
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportálás...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportálás...
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Útvonal tervezés és szerkesztés
|
|||||||
---
|
---
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||||
@@ -71,7 +71,7 @@ A következő eszközök automatizálnak néhány gyakori útvonal-módosítási
|
|||||||
|
|
||||||
Fordítsa meg az útvonal irányát.
|
Fordítsa meg az útvonal irányát.
|
||||||
|
|
||||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Vissza a kezdéshez
|
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
||||||
|
|
||||||
Csatlakoztassa az útvonal utolsó pontját a kiindulási ponthoz a kiválasztott útvonalbeállítások segítségével.
|
Csatlakoztassa az útvonal utolsó pontját a kiindulási ponthoz a kiválasztott útvonalbeállítások segítségével.
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user