279 Commits

Author SHA1 Message Date
vcoppe
8501ddc87f New translations faq.mdx (Polish) 2025-10-07 19:48:49 +02:00
vcoppe
7d9b94525e New translations time.mdx (Polish) 2025-10-07 19:48:48 +02:00
vcoppe
eb02f0eadf New translations scissors.mdx (Polish) 2025-10-07 19:48:47 +02:00
vcoppe
69a8ba5aec New translations routing.mdx (Polish) 2025-10-07 19:48:46 +02:00
vcoppe
fe49b8e618 New translations merge.mdx (Polish) 2025-10-07 19:48:45 +02:00
vcoppe
26bf4dde5f New translations extract.mdx (Polish) 2025-10-07 19:48:44 +02:00
vcoppe
e9b73050ba New translations toolbar.mdx (Polish) 2025-10-07 19:48:43 +02:00
vcoppe
bacd0ab43f New translations view.mdx (Polish) 2025-10-07 19:48:42 +02:00
vcoppe
e438051371 New translations file.mdx (Polish) 2025-10-07 19:48:40 +02:00
vcoppe
314155593d New translations edit.mdx (Polish) 2025-10-07 19:48:39 +02:00
vcoppe
787f819ce0 New translations menu.mdx (Polish) 2025-10-07 19:48:38 +02:00
vcoppe
3632a62ea3 New translations integration.mdx (Polish) 2025-10-07 19:48:37 +02:00
vcoppe
c7294df007 New translations gpx.mdx (Polish) 2025-10-07 19:48:36 +02:00
vcoppe
e3ad7fe3c0 New translations getting-started.mdx (Polish) 2025-10-07 19:48:34 +02:00
vcoppe
6213683ddf New translations files-and-stats.mdx (Polish) 2025-10-07 19:48:33 +02:00
vcoppe
a4ddfc9970 New translations en.json (Polish) 2025-10-07 19:48:32 +02:00
vcoppe
7ff271f9b9 New translations view.mdx (Spanish) 2025-10-07 02:54:04 +02:00
vcoppe
d75cdd63a9 New translations file.mdx (Spanish) 2025-10-07 02:54:02 +02:00
vcoppe
0a7575d1e4 New translations integration.mdx (Spanish) 2025-10-07 02:54:01 +02:00
vcoppe
ec3022d8ad New translations en.json (Spanish) 2025-10-07 01:50:49 +02:00
vcoppe
d42103b91b New translations en.json (Ukrainian) 2025-10-03 23:57:07 +02:00
vcoppe
00f7d08b04 New translations en.json (Ukrainian) 2025-10-03 22:36:21 +02:00
vcoppe
408cc383cb New translations en.json (Portuguese) 2025-10-03 17:56:00 +02:00
vcoppe
5c926d0ac6 New translations en.json (Ukrainian) 2025-09-23 18:44:01 +02:00
vcoppe
5cb88782fc New translations en.json (Ukrainian) 2025-09-23 15:59:34 +02:00
vcoppe
5eef4e9ece New translations en.json (Russian) 2025-09-20 17:13:17 +02:00
vcoppe
04a2124141 New translations en.json (Italian) 2025-09-20 17:13:15 +02:00
vcoppe
1b6229b2a1 New translations elevation.mdx (Italian) 2025-09-20 16:10:28 +02:00
vcoppe
bca6db50a7 New translations en.json (Italian) 2025-09-20 16:10:27 +02:00
vcoppe
f3aae26996 New translations settings.mdx (Chinese Simplified) 2025-09-10 10:34:12 +02:00
vcoppe
f3c17a8e0f New translations en.json (Indonesian) 2025-09-05 04:13:37 +02:00
vcoppe
d6b24f8753 New translations en.json (Indonesian) 2025-09-05 03:11:41 +02:00
vcoppe
253db0a303 New translations en.json (Norwegian) 2025-09-04 18:13:32 +02:00
vcoppe
8499e52461 New translations en.json (Dutch) 2025-09-01 08:57:32 +02:00
vcoppe
d0153179a9 New translations en.json (Indonesian) 2025-08-29 18:50:34 +02:00
vcoppe
264d03727e New translations edit.mdx (Chinese Traditional, Hong Kong) 2025-08-13 18:27:34 +02:00
vcoppe
544405d9b9 New translations en.json (Chinese Traditional, Hong Kong) 2025-08-13 16:32:09 +02:00
vcoppe
24488a3b67 New translations en.json (Indonesian) 2025-08-08 14:15:35 +02:00
vcoppe
ae78185b29 New translations en.json (Indonesian) 2025-08-08 12:50:22 +02:00
vcoppe
7f682b24ef New translations en.json (Indonesian) 2025-08-04 04:32:52 +02:00
vcoppe
d42a52d8cf New translations en.json (Norwegian) 2025-08-03 16:13:45 +02:00
vcoppe
b85df15890 New translations en.json (Norwegian) 2025-08-03 15:00:56 +02:00
vcoppe
393499f34f New translations en.json (Indonesian) 2025-08-02 13:58:57 +02:00
vcoppe
c656d0f9b5 New translations en.json (Indonesian) 2025-08-02 12:40:02 +02:00
vcoppe
32017a8859 New translations en.json (Indonesian) 2025-08-02 11:35:56 +02:00
vcoppe
d87c5b1140 New translations en.json (Norwegian) 2025-08-01 22:28:15 +02:00
vcoppe
f59f783d3f New translations en.json (Norwegian) 2025-08-01 21:18:36 +02:00
vcoppe
ec298eac61 New translations elevation.mdx (Indonesian) 2025-08-01 16:14:37 +02:00
vcoppe
81a25bb4ee New translations faq.mdx (Indonesian) 2025-08-01 16:14:36 +02:00
vcoppe
e99f044e45 New translations time.mdx (Indonesian) 2025-08-01 16:14:35 +02:00
vcoppe
5ae25a5fd9 New translations scissors.mdx (Indonesian) 2025-08-01 16:14:34 +02:00
vcoppe
e9d1cb4907 New translations routing.mdx (Indonesian) 2025-08-01 16:14:33 +02:00
vcoppe
99f8ca2dca New translations poi.mdx (Indonesian) 2025-08-01 16:14:31 +02:00
vcoppe
ddea5d38b5 New translations minify.mdx (Indonesian) 2025-08-01 16:14:30 +02:00
vcoppe
31d2b83550 New translations merge.mdx (Indonesian) 2025-08-01 16:14:29 +02:00
vcoppe
5535e56ed2 New translations extract.mdx (Indonesian) 2025-08-01 16:14:28 +02:00
vcoppe
d740b95dbc New translations clean.mdx (Indonesian) 2025-08-01 16:14:26 +02:00
vcoppe
ae92e9a945 New translations toolbar.mdx (Indonesian) 2025-08-01 16:14:25 +02:00
vcoppe
29730c3896 New translations view.mdx (Indonesian) 2025-08-01 16:14:24 +02:00
vcoppe
a5ae8270f0 New translations settings.mdx (Indonesian) 2025-08-01 16:14:23 +02:00
vcoppe
54f5fa6432 New translations file.mdx (Indonesian) 2025-08-01 16:14:22 +02:00
vcoppe
0260644063 New translations edit.mdx (Indonesian) 2025-08-01 16:14:20 +02:00
vcoppe
267fc03a82 New translations menu.mdx (Indonesian) 2025-08-01 16:14:19 +02:00
vcoppe
bf1537584c New translations map-controls.mdx (Indonesian) 2025-08-01 16:14:18 +02:00
vcoppe
9ee7825022 New translations integration.mdx (Indonesian) 2025-08-01 16:14:17 +02:00
vcoppe
2be0c42dd1 New translations translation.mdx (Indonesian) 2025-08-01 16:14:16 +02:00
vcoppe
3423c053a2 New translations mapbox.mdx (Indonesian) 2025-08-01 16:14:15 +02:00
vcoppe
26923cca00 New translations funding.mdx (Indonesian) 2025-08-01 16:14:14 +02:00
vcoppe
36e027659c New translations gpx.mdx (Indonesian) 2025-08-01 16:14:13 +02:00
vcoppe
f447dccdb4 New translations getting-started.mdx (Indonesian) 2025-08-01 16:14:11 +02:00
vcoppe
69eae32851 New translations files-and-stats.mdx (Indonesian) 2025-08-01 16:14:10 +02:00
vcoppe
aa2fcfb8cb New translations en.json (Indonesian) 2025-08-01 16:14:09 +02:00
vcoppe
fae5ef2a41 New translations en.json (Norwegian) 2025-07-31 23:43:31 +02:00
vcoppe
7251ca7d2d New translations toolbar.mdx (Norwegian) 2025-07-31 22:34:29 +02:00
vcoppe
7cdbd919bf New translations en.json (Norwegian) 2025-07-31 22:34:27 +02:00
vcoppe
d450f95602 New translations en.json (Dutch) 2025-07-31 14:26:59 +02:00
vcoppe
5a65201971 New translations en.json (Thai) 2025-07-30 18:35:07 +02:00
vcoppe
d303b8db3e New translations gpx.mdx (Portuguese) 2025-07-20 19:33:06 +02:00
vcoppe
06baa33827 New translations gpx.mdx (Portuguese) 2025-07-20 18:31:13 +02:00
vcoppe
42743e637e New translations en.json (French) 2025-07-18 16:38:10 +02:00
vcoppe
9969fd7dec New translations edit.mdx (Swedish) 2025-07-17 23:06:28 +02:00
vcoppe
fc6d5c2a1d New translations en.json (Basque) 2025-07-16 07:51:58 +02:00
vcoppe
f8abb1ca24 New translations elevation.mdx (Thai) 2025-07-15 14:10:56 +02:00
vcoppe
a5af38ae3d New translations faq.mdx (Thai) 2025-07-15 14:10:55 +02:00
vcoppe
aab70951dc New translations time.mdx (Thai) 2025-07-15 14:10:54 +02:00
vcoppe
334cacf93c New translations scissors.mdx (Thai) 2025-07-15 14:10:52 +02:00
vcoppe
53024012fc New translations routing.mdx (Thai) 2025-07-15 14:10:51 +02:00
vcoppe
86a72f77c1 New translations poi.mdx (Thai) 2025-07-15 14:10:50 +02:00
vcoppe
bc11a5ad0a New translations minify.mdx (Thai) 2025-07-15 14:10:49 +02:00
vcoppe
8f2d217fd4 New translations merge.mdx (Thai) 2025-07-15 14:10:47 +02:00
vcoppe
183727cd50 New translations extract.mdx (Thai) 2025-07-15 14:10:46 +02:00
vcoppe
676e87591a New translations clean.mdx (Thai) 2025-07-15 14:10:44 +02:00
vcoppe
8c05fc4da0 New translations toolbar.mdx (Thai) 2025-07-15 14:10:43 +02:00
vcoppe
2bab06561e New translations view.mdx (Thai) 2025-07-15 14:10:42 +02:00
vcoppe
dfa7e2f5bb New translations settings.mdx (Thai) 2025-07-15 14:10:41 +02:00
vcoppe
78bece5616 New translations file.mdx (Thai) 2025-07-15 14:10:39 +02:00
vcoppe
eeea15e373 New translations edit.mdx (Thai) 2025-07-15 14:10:38 +02:00
vcoppe
80cd513ab7 New translations menu.mdx (Thai) 2025-07-15 14:10:37 +02:00
vcoppe
942ef1615e New translations map-controls.mdx (Thai) 2025-07-15 14:10:35 +02:00
vcoppe
a354698022 New translations integration.mdx (Thai) 2025-07-15 14:10:34 +02:00
vcoppe
0cdea488c9 New translations translation.mdx (Thai) 2025-07-15 14:10:33 +02:00
vcoppe
4f4291ac47 New translations mapbox.mdx (Thai) 2025-07-15 14:10:32 +02:00
vcoppe
bf0cf03091 New translations funding.mdx (Thai) 2025-07-15 14:10:30 +02:00
vcoppe
f7da09f20f New translations gpx.mdx (Thai) 2025-07-15 14:10:28 +02:00
vcoppe
be1529331c New translations getting-started.mdx (Thai) 2025-07-15 14:10:27 +02:00
vcoppe
301d658a29 New translations files-and-stats.mdx (Thai) 2025-07-15 14:10:26 +02:00
vcoppe
1cc54e5b2c New translations en.json (Thai) 2025-07-15 14:10:25 +02:00
vcoppe
65a7fd21e7 New translations en.json (Italian) 2025-07-14 12:56:13 +02:00
vcoppe
856537c0cd New translations en.json (Ukrainian) 2025-07-10 01:33:15 +02:00
vcoppe
b2a88e0063 New translations en.json (Ukrainian) 2025-07-10 00:32:33 +02:00
vcoppe
85a7068785 New translations en.json (Ukrainian) 2025-07-09 12:14:44 +02:00
vcoppe
cbb733d99a New translations settings.mdx (Ukrainian) 2025-07-07 18:53:29 +02:00
vcoppe
ce88c94a19 New translations edit.mdx (Ukrainian) 2025-07-07 18:53:28 +02:00
vcoppe
16516915d8 New translations translation.mdx (Ukrainian) 2025-07-07 18:53:27 +02:00
vcoppe
6addb8da23 New translations mapbox.mdx (Ukrainian) 2025-07-07 18:53:26 +02:00
vcoppe
bc7f664fd8 New translations funding.mdx (Ukrainian) 2025-07-07 17:28:09 +02:00
vcoppe
aac17aa33c New translations en.json (Ukrainian) 2025-07-07 17:28:08 +02:00
vcoppe
825500e207 New translations en.json (Ukrainian) 2025-07-07 15:46:23 +02:00
vcoppe
4d42016c72 New translations en.json (Italian) 2025-06-28 14:49:34 +02:00
vcoppe
9d665df602 New translations poi.mdx (Polish) 2025-06-23 23:44:31 +02:00
vcoppe
9087f69fb0 New translations minify.mdx (Polish) 2025-06-23 23:44:30 +02:00
vcoppe
2a06f6a214 New translations clean.mdx (Polish) 2025-06-23 23:44:29 +02:00
vcoppe
78a8428bd0 New translations toolbar.mdx (Polish) 2025-06-23 23:44:28 +02:00
vcoppe
0d235768fa New translations menu.mdx (Polish) 2025-06-23 23:44:26 +02:00
vcoppe
af092bbdec New translations edit.mdx (Polish) 2025-06-23 22:23:55 +02:00
vcoppe
4961630d62 New translations en.json (Chinese Traditional, Hong Kong) 2025-06-20 05:53:38 +02:00
vcoppe
81920b9ab9 New translations en.json (Chinese Traditional, Hong Kong) 2025-06-20 04:39:50 +02:00
vcoppe
9e031d3b5b New translations en.json (Chinese Traditional, Hong Kong) 2025-06-20 03:33:14 +02:00
vcoppe
7ae3ed6d2a New translations elevation.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:44 +02:00
vcoppe
05d79f2b51 New translations faq.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:43 +02:00
vcoppe
274e591354 New translations time.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:42 +02:00
vcoppe
95fd152b3d New translations scissors.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:41 +02:00
vcoppe
ffc91ed6d8 New translations routing.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:40 +02:00
vcoppe
de0b759875 New translations poi.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:38 +02:00
vcoppe
f041dcf944 New translations minify.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:37 +02:00
vcoppe
946b9bd9d1 New translations merge.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:36 +02:00
vcoppe
db77a69838 New translations extract.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:35 +02:00
vcoppe
d10f4d26e2 New translations clean.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:34 +02:00
vcoppe
6b62d686ba New translations toolbar.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:33 +02:00
vcoppe
065826e64d New translations view.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:32 +02:00
vcoppe
a3b096343f New translations settings.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:31 +02:00
vcoppe
b33be91b06 New translations file.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:29 +02:00
vcoppe
a94a1816c5 New translations edit.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:28 +02:00
vcoppe
9a9e7fea07 New translations menu.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:27 +02:00
vcoppe
9a03042077 New translations map-controls.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:26 +02:00
vcoppe
704d3b2d6b New translations integration.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:24 +02:00
vcoppe
e5c2be238d New translations translation.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:23 +02:00
vcoppe
9feea07527 New translations mapbox.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:22 +02:00
vcoppe
b0967d03b8 New translations funding.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:21 +02:00
vcoppe
d33fd71f93 New translations gpx.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:20 +02:00
vcoppe
226b5b2682 New translations getting-started.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:18 +02:00
vcoppe
f8879b0223 New translations files-and-stats.mdx (Chinese Traditional, Hong Kong) 2025-06-19 18:26:17 +02:00
vcoppe
ada09d96c4 New translations en.json (Chinese Traditional, Hong Kong) 2025-06-19 18:26:16 +02:00
vcoppe
a9ea0e223d add turkish language 2025-06-07 11:30:06 +02:00
vcoppe
37e8237f78 New Crowdin updates (#225)
* New translations settings.mdx (Turkish)

* New translations edit.mdx (Turkish)

* New translations file.mdx (Turkish)

* New translations view.mdx (Turkish)

* New translations files-and-stats.mdx (Turkish)

* New translations gpx.mdx (Turkish)

* New translations map-controls.mdx (Turkish)

* New translations routing.mdx (Turkish)

* New translations files-and-stats.mdx (French)

* New translations files-and-stats.mdx (Spanish)

* New translations files-and-stats.mdx (Catalan)

* New translations files-and-stats.mdx (Czech)

* New translations files-and-stats.mdx (German)

* New translations files-and-stats.mdx (Basque)

* New translations files-and-stats.mdx (Italian)

* New translations files-and-stats.mdx (Dutch)

* New translations files-and-stats.mdx (Chinese Simplified)
2025-06-07 11:29:19 +02:00
vcoppe
40deb19837 New Crowdin updates (#224)
* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations funding.mdx (Turkish)

* New translations mapbox.mdx (Turkish)

* New translations translation.mdx (Turkish)

* New translations settings.mdx (Turkish)

* New translations settings.mdx (Turkish)

* New translations edit.mdx (Turkish)

* New translations file.mdx (Turkish)

* New translations view.mdx (Turkish)

* New translations routing.mdx (Turkish)

* New translations view.mdx (Turkish)

* New translations routing.mdx (Turkish)

* New translations clean.mdx (Turkish)

* New translations extract.mdx (Turkish)

* New translations merge.mdx (Turkish)

* New translations minify.mdx (Turkish)

* New translations poi.mdx (Turkish)

* New translations scissors.mdx (Turkish)

* New translations elevation.mdx (Turkish)

* New translations scissors.mdx (Turkish)

* New translations files-and-stats.mdx (Turkish)

* New translations getting-started.mdx (Turkish)

* New translations menu.mdx (Turkish)

* New translations toolbar.mdx (Turkish)

* New translations time.mdx (Turkish)

* New translations faq.mdx (Turkish)

* New translations edit.mdx (Turkish)

* New translations view.mdx (Turkish)

* New translations files-and-stats.mdx (Turkish)

* New translations getting-started.mdx (Turkish)

* New translations menu.mdx (Turkish)

* New translations toolbar.mdx (Turkish)

* New translations gpx.mdx (Turkish)

* New translations integration.mdx (Turkish)

* New translations map-controls.mdx (Turkish)

* New translations view.mdx (Turkish)

* New translations integration.mdx (Turkish)

* New translations map-controls.mdx (Turkish)
2025-06-07 10:36:02 +02:00
vcoppe
a9deb681e0 add Overpass POI website as wpt link 2025-06-04 18:55:14 +02:00
vcoppe
018e638ae3 New Crowdin updates (#220)
* New translations en.json (Chinese Simplified)

* New translations poi.mdx (Basque)

* New translations en.json (Swedish)

* New translations en.json (Polish)

* New translations en.json (Swedish)

* New translations files-and-stats.mdx (Swedish)

* New translations integration.mdx (Swedish)

* New translations getting-started.mdx (Swedish)

* New translations gpx.mdx (Swedish)

* New translations files-and-stats.mdx (Swedish)

* New translations gpx.mdx (Swedish)

* New translations routing.mdx (Swedish)

* New translations elevation.mdx (Swedish)

* New translations minify.mdx (Swedish)

* New translations routing.mdx (Swedish)

* New translations scissors.mdx (Swedish)

* New translations faq.mdx (Swedish)

* New translations edit.mdx (Swedish)

* New translations settings.mdx (Swedish)

* New translations view.mdx (Swedish)

* New translations map-controls.mdx (Swedish)

* New translations menu.mdx (Swedish)

* New translations edit.mdx (Swedish)

* New translations files-and-stats.mdx (Swedish)

* New translations getting-started.mdx (Swedish)

* New translations gpx.mdx (Swedish)

* New translations integration.mdx (Swedish)

* New translations map-controls.mdx (Swedish)

* New translations menu.mdx (Swedish)

* New translations edit.mdx (Swedish)

* New translations routing.mdx (Swedish)

* New translations scissors.mdx (Swedish)

* New translations time.mdx (Swedish)

* New translations extract.mdx (Swedish)

* New translations merge.mdx (Swedish)

* New translations minify.mdx (Swedish)

* New translations poi.mdx (Swedish)

* New translations routing.mdx (Swedish)

* New translations elevation.mdx (Swedish)

* New translations en.json (Swedish)

* New translations edit.mdx (Swedish)

* New translations file.mdx (Swedish)

* New translations view.mdx (Swedish)

* New translations toolbar.mdx (Swedish)

* New translations clean.mdx (Swedish)

* New translations files-and-stats.mdx (Swedish)

* New translations map-controls.mdx (Swedish)

* New translations minify.mdx (Swedish)

* New translations poi.mdx (Swedish)

* New translations routing.mdx (Swedish)

* New translations scissors.mdx (Swedish)

* New translations faq.mdx (Swedish)
2025-06-04 18:41:56 +02:00
vcoppe
967d271667 simplify shift key detection 2025-06-04 18:41:28 +02:00
vcoppe
4e92a16d8d fix zip creation when multiple files have the same name 2025-05-23 09:12:02 +02:00
vcoppe
b4bcda12c2 New Crowdin updates (#219)
* New translations en.json (Basque)

* New translations files-and-stats.mdx (Czech)

* New translations files-and-stats.mdx (Romanian)

* New translations files-and-stats.mdx (French)

* New translations files-and-stats.mdx (Spanish)

* New translations files-and-stats.mdx (Belarusian)

* New translations files-and-stats.mdx (Catalan)

* New translations files-and-stats.mdx (Danish)

* New translations files-and-stats.mdx (German)

* New translations files-and-stats.mdx (Greek)

* New translations files-and-stats.mdx (Basque)

* New translations files-and-stats.mdx (Finnish)

* New translations files-and-stats.mdx (Hebrew)

* New translations files-and-stats.mdx (Hungarian)

* New translations files-and-stats.mdx (Italian)

* New translations files-and-stats.mdx (Korean)

* New translations files-and-stats.mdx (Lithuanian)

* New translations files-and-stats.mdx (Dutch)

* New translations files-and-stats.mdx (Norwegian)

* New translations files-and-stats.mdx (Polish)

* New translations files-and-stats.mdx (Portuguese)

* New translations files-and-stats.mdx (Russian)

* New translations files-and-stats.mdx (Swedish)

* New translations files-and-stats.mdx (Turkish)

* New translations files-and-stats.mdx (Ukrainian)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Vietnamese)

* New translations files-and-stats.mdx (Portuguese, Brazilian)

* New translations files-and-stats.mdx (Latvian)

* New translations files-and-stats.mdx (Serbian (Latin))

* Update source file files-and-stats.mdx

* New translations files-and-stats.mdx (Czech)

* New translations files-and-stats.mdx (French)

* New translations files-and-stats.mdx (Spanish)

* New translations files-and-stats.mdx (Catalan)

* New translations files-and-stats.mdx (German)

* New translations files-and-stats.mdx (Basque)

* New translations files-and-stats.mdx (Italian)

* New translations files-and-stats.mdx (Dutch)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Portuguese, Brazilian)
2025-05-20 00:11:43 +02:00
Vianney Coppé
e5bb902cd4 Merge branch 'dev' of https://github.com/gpxstudio/gpx.studio into dev 2025-05-19 23:36:54 +02:00
Vianney Coppé
a3817fd5cd add elevation profile component to documentation, closes #217 2025-05-19 23:35:56 +02:00
vcoppe
3a6338e9fb add basque language 2025-05-14 21:54:01 +02:00
vcoppe
34d2342bdb New Crowdin updates (#218)
* New translations files-and-stats.mdx (Basque)

* New translations routing.mdx (Basque)

* New translations files-and-stats.mdx (Basque)

* New translations getting-started.mdx (Basque)

* New translations gpx.mdx (Basque)

* New translations integration.mdx (Basque)

* New translations map-controls.mdx (Basque)

* New translations file.mdx (Basque)

* New translations extract.mdx (Basque)

* New translations merge.mdx (Basque)

* New translations scissors.mdx (Basque)

* New translations faq.mdx (Basque)
2025-05-14 21:46:32 +02:00
vcoppe
5623a6b662 New Crowdin updates (#216)
* New translations en.json (Basque)

* New translations en.json (Basque)

* New translations en.json (Basque)

* New translations en.json (Basque)

* New translations funding.mdx (Basque)

* New translations mapbox.mdx (Basque)

* New translations translation.mdx (Basque)

* New translations settings.mdx (Basque)

* New translations translation.mdx (Basque)

* New translations edit.mdx (Basque)

* New translations settings.mdx (Basque)

* New translations en.json (Basque)

* New translations edit.mdx (Basque)

* New translations edit.mdx (Basque)

* New translations file.mdx (Basque)

* New translations funding.mdx (Polish)

* New translations file.mdx (Basque)

* New translations file.mdx (Basque)

* New translations settings.mdx (Basque)

* New translations routing.mdx (Basque)

* New translations settings.mdx (Basque)

* New translations view.mdx (Basque)

* New translations clean.mdx (Basque)

* New translations extract.mdx (Basque)

* New translations elevation.mdx (Basque)

* New translations elevation.mdx (Basque)

* New translations en.json (Portuguese, Brazilian)

* New translations routing.mdx (Basque)

* New translations merge.mdx (Basque)

* New translations merge.mdx (Basque)

* New translations minify.mdx (Basque)

* New translations poi.mdx (Basque)

* New translations routing.mdx (Basque)

* New translations scissors.mdx (Basque)

* New translations time.mdx (Basque)

* New translations faq.mdx (Basque)

* New translations files-and-stats.mdx (Basque)

* New translations getting-started.mdx (Basque)

* New translations gpx.mdx (Basque)

* New translations map-controls.mdx (Basque)

* New translations menu.mdx (Basque)

* New translations edit.mdx (Basque)

* New translations view.mdx (Basque)

* New translations toolbar.mdx (Basque)

* New translations faq.mdx (Basque)

* New translations gpx.mdx (Basque)

* New translations integration.mdx (Basque)

* New translations map-controls.mdx (Basque)

* New translations view.mdx (Basque)

* New translations en.json (Basque)

* New translations map-controls.mdx (Basque)

* New translations menu.mdx (Basque)

* New translations view.mdx (Basque)

* New translations toolbar.mdx (Basque)

* New translations poi.mdx (Basque)

* New translations routing.mdx (Basque)

* New translations elevation.mdx (Basque)

* New translations scissors.mdx (Basque)

* New translations getting-started.mdx (Basque)
2025-05-14 20:51:49 +02:00
vcoppe
de0cc63d53 move ts-node to devDependencies 2025-05-09 19:45:21 +02:00
vcoppe
cb4892ace3 rename file 2025-05-09 19:39:25 +02:00
vcoppe
3cd17d7409 New translations mapbox.mdx (Czech) (#215) 2025-05-08 19:49:19 +02:00
vcoppe
2848402e97 add apple touch icon 2025-05-08 19:48:13 +02:00
vcoppe
7e8f1acf67 remove apple touch icon 2025-05-08 18:54:46 +02:00
vcoppe
8aa8a77260 change import 2025-05-08 18:44:20 +02:00
vcoppe
ea4e078c92 New translations en.json (Dutch) (#213) 2025-05-08 18:39:23 +02:00
vcoppe
21261f732f improve icons 2025-05-08 18:39:12 +02:00
Jon Herman
4e9d65089c Add basic PWA support (#38) (#210)
* Add basic PWA support

See: https://github.com/gpxstudio/gpx.studio/issues/38

This will add a basic manifest.json file to the response that creates an
installable PWA.

It still needs to be internationalized.

* Refactor PWA integration and update dependencies

- Removed @vite-pwa/sveltekit dependency and related configurations from package.json and vite.config.ts.
- Added prebuild script to generate PWA manifest before build.
- Moved the manifest link to hooks.server.js and included Apple touch icon.
-Added images for various platforms. Images were generated with https://www.pwabuilder.com/imageGenerator

* use svg icon, fix urls, and remove generated manifest files

---------

Co-authored-by: jonherman <jonherman@gmail.com>
Co-authored-by: vcoppe <vianney.coppe@gmail.com>
2025-05-08 18:23:07 +02:00
vcoppe
e4f6dfbf78 fix logo 2025-05-08 15:16:47 +02:00
vcoppe
9083784ffd Merge branch 'dev' of https://github.com/gpxstudio/gpx.studio into dev 2025-05-08 14:55:35 +02:00
vcoppe
6c4c4dbac9 add czech 2025-05-08 14:54:48 +02:00
vcoppe
ea0abf15ce New translations view.mdx (Czech) (#212) 2025-05-08 14:54:14 +02:00
vcoppe
71cccc5bdc fix config, closes #211 2025-05-08 14:16:07 +02:00
vcoppe
4964928ac0 New Crowdin updates (#202)
* New translations en.json (German)

* New translations en.json (Italian)

* New translations settings.mdx (Italian)

* New translations en.json (Italian)

* New translations settings.mdx (Italian)

* New translations en.json (Portuguese)

* New translations en.json (Ukrainian)

* New translations en.json (Portuguese)

* New translations en.json (Portuguese)

* New translations en.json (Portuguese)

* New translations en.json (Portuguese)

* New translations en.json (Portuguese)

* New translations menu.mdx (Portuguese)

* New translations toolbar.mdx (Portuguese)

* New translations en.json (German)

* New translations translation.mdx (Portuguese)

* New translations en.json (Chinese Simplified)

* New translations toolbar.mdx (Polish)

* New translations menu.mdx (Portuguese)

* New translations getting-started.mdx (German)

* New translations edit.mdx (German)

* New translations view.mdx (German)

* New translations funding.mdx (Catalan)

* New translations edit.mdx (Czech)

* New translations file.mdx (Czech)

* New translations settings.mdx (Czech)

* New translations settings.mdx (Czech)

* New translations settings.mdx (Czech)

* New translations edit.mdx (Czech)

* New translations view.mdx (Czech)

* New translations extract.mdx (Czech)

* New translations merge.mdx (Czech)

* New translations scissors.mdx (Czech)

* New translations time.mdx (Czech)

* New translations elevation.mdx (Czech)

* New translations extract.mdx (Czech)

* New translations clean.mdx (Czech)

* New translations en.json (Danish)

* New translations faq.mdx (Czech)

* New translations faq.mdx (Czech)

* New translations en.json (Czech)

* New translations files-and-stats.mdx (Czech)

* New translations getting-started.mdx (Czech)

* New translations map-controls.mdx (Czech)

* New translations menu.mdx (Czech)

* New translations toolbar.mdx (Czech)

* New translations files-and-stats.mdx (Czech)

* New translations edit.mdx (Czech)

* New translations view.mdx (Czech)

* New translations files-and-stats.mdx (Czech)

* New translations files-and-stats.mdx (Czech)

* New translations gpx.mdx (Czech)

* New translations gpx.mdx (Czech)

* New translations integration.mdx (Czech)

* New translations view.mdx (Czech)

* New translations map-controls.mdx (Czech)

* New translations integration.mdx (Czech)

* New translations map-controls.mdx (Czech)

* New translations toolbar.mdx (Czech)

* New translations en.json (Czech)

* New translations settings.mdx (Czech)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations en.json (Basque)

* New translations files-and-stats.mdx (Basque)

* New translations getting-started.mdx (Basque)

* New translations gpx.mdx (Basque)

* New translations funding.mdx (Basque)

* New translations mapbox.mdx (Basque)

* New translations translation.mdx (Basque)

* New translations integration.mdx (Basque)

* New translations map-controls.mdx (Basque)

* New translations menu.mdx (Basque)

* New translations edit.mdx (Basque)

* New translations file.mdx (Basque)

* New translations settings.mdx (Basque)

* New translations view.mdx (Basque)

* New translations toolbar.mdx (Basque)

* New translations clean.mdx (Basque)

* New translations extract.mdx (Basque)

* New translations merge.mdx (Basque)

* New translations minify.mdx (Basque)

* New translations poi.mdx (Basque)

* New translations routing.mdx (Basque)

* New translations scissors.mdx (Basque)

* New translations time.mdx (Basque)

* New translations faq.mdx (Basque)

* New translations elevation.mdx (Basque)

* New translations settings.mdx (Portuguese)
2025-05-08 13:03:53 +02:00
vcoppe
7fde60a267 upgrade mapbox gl js 2025-04-24 19:42:46 +02:00
vcoppe
306ed2ae0e use correct OSM type for edit link 2025-04-11 18:40:16 +02:00
vcoppe
a7cfe36b2e safer float parsing 2025-04-04 08:56:53 +02:00
vcoppe
f4879a9e8a update routing server url 2025-03-30 20:30:22 +02:00
vcoppe
c8e09fcd90 better handle time updates with weird original data 2025-03-22 15:54:13 +01:00
vcoppe
c5f20d323c copy coordinates button for POIs, closes #195 2025-03-22 14:46:16 +01:00
vcoppe
e3dcdf2f41 New Crowdin updates, closes #198 (#193)
* New translations en.json (Turkish)

* New translations en.json (Polish)

* New translations en.json (Polish)

* New translations gpx.mdx (Hungarian)

* New translations poi.mdx (Hungarian)

* New translations clean.mdx (Polish)

* New translations translation.mdx (Danish)
2025-03-22 14:18:45 +01:00
vcoppe
82d8b5d61e revert 2025-03-22 13:38:07 +01:00
vcoppe
47692656e4 use new routing server 2025-03-22 13:32:45 +01:00
vcoppe
b5bf06b37a format file 2025-02-15 13:09:41 +01:00
vcoppe
bc3b1e5f7c New Crowdin updates (#184)
* New translations en.json (Spanish)

* New translations funding.mdx (Danish)

* New translations mapbox.mdx (Danish)

* New translations translation.mdx (Danish)

* New translations settings.mdx (Danish)
2025-02-15 12:56:16 +01:00
vcoppe
63eae15191 New translations map-controls.mdx (Italian) (#182) 2025-02-07 18:38:06 +01:00
vcoppe
848b6dcef3 catch invalid dates on export, closes #180 2025-02-07 18:13:46 +01:00
vcoppe
dfcdd71057 New Crowdin updates (#179)
* New translations en.json (Catalan)

* New translations en.json (Polish)

* New translations en.json (German)

* New translations files-and-stats.mdx (Italian)

* New translations getting-started.mdx (Italian)

* New translations map-controls.mdx (Italian)

* New translations faq.mdx (Italian)

* New translations en.json (Italian)

* New translations getting-started.mdx (Italian)

* New translations edit.mdx (Italian)

* New translations file.mdx (Italian)

* New translations settings.mdx (Italian)

* New translations view.mdx (Italian)

* New translations routing.mdx (Italian)
2025-02-07 18:06:49 +01:00
vcoppe
7368945bf3 New Crowdin updates (#178)
* New translations files-and-stats.mdx (Catalan)

* New translations map-controls.mdx (Catalan)
2025-02-02 20:58:49 +01:00
vcoppe
de52203e89 add catalan 2025-02-02 20:36:38 +01:00
vcoppe
62d1a3e01f New Crowdin updates (#177)
* New translations files-and-stats.mdx (Catalan)

* New translations files-and-stats.mdx (Catalan)

* New translations gpx.mdx (Catalan)

* New translations en.json (Serbian (Latin))

* New translations en.json (Serbian (Latin))

* New translations files-and-stats.mdx (Serbian (Latin))

* New translations getting-started.mdx (Serbian (Latin))

* New translations gpx.mdx (Serbian (Latin))

* New translations map-controls.mdx (Serbian (Latin))

* New translations menu.mdx (Serbian (Latin))

* New translations edit.mdx (Serbian (Latin))

* New translations view.mdx (Serbian (Latin))

* New translations toolbar.mdx (Serbian (Latin))

* New translations integration.mdx (Catalan)

* New translations integration.mdx (Catalan)

* New translations map-controls.mdx (Catalan)

* New translations view.mdx (Catalan)
2025-02-02 20:36:13 +01:00
vcoppe
68b4ecadf5 New Crowdin updates (#174)
* New translations en.json (Hungarian)

* New translations en.json (Catalan)

* New translations mapbox.mdx (Catalan)

* New translations edit.mdx (Catalan)

* New translations en.json (Catalan)

* New translations edit.mdx (Catalan)

* New translations edit.mdx (Catalan)

* New translations file.mdx (Catalan)

* New translations settings.mdx (Catalan)

* New translations view.mdx (Catalan)

* New translations extract.mdx (Catalan)

* New translations routing.mdx (Catalan)

* New translations elevation.mdx (Catalan)

* New translations menu.mdx (Catalan)

* New translations file.mdx (Catalan)

* New translations merge.mdx (Catalan)

* New translations minify.mdx (Catalan)

* New translations poi.mdx (Catalan)

* New translations routing.mdx (Catalan)

* New translations routing.mdx (Catalan)

* New translations routing.mdx (Catalan)

* New translations routing.mdx (Catalan)

* New translations routing.mdx (Catalan)

* New translations edit.mdx (Catalan)

* New translations poi.mdx (Catalan)

* New translations routing.mdx (Catalan)

* New translations scissors.mdx (Catalan)

* New translations time.mdx (Catalan)

* New translations en.json (Catalan)

* New translations toolbar.mdx (Catalan)

* New translations faq.mdx (Catalan)

* New translations files-and-stats.mdx (Catalan)

* New translations getting-started.mdx (Catalan)

* New translations map-controls.mdx (Catalan)

* New translations menu.mdx (Catalan)

* New translations edit.mdx (Catalan)

* New translations toolbar.mdx (Catalan)

* New translations files-and-stats.mdx (Catalan)

* New translations view.mdx (Catalan)

* New translations en.json (Dutch)

* New translations en.json (Russian)

* New translations en.json (Turkish)

* New translations en.json (German)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Belarusian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Korean)

* New translations en.json (Lithuanian)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Latvian)

* New translations en.json (Serbian (Latin))
2025-02-02 11:53:12 +01:00
vcoppe
7831774703 handle files with uniform timestamps 2025-02-02 11:43:56 +01:00
vcoppe
52984d4b70 handle files with missing timestamps 2025-02-02 11:27:06 +01:00
vcoppe
0b457f9a1e prettier config + format all, closes #175 2025-02-02 11:17:22 +01:00
vcoppe
01cfd448f0 avoid embedding anything else than /embed 2025-01-30 19:25:58 +01:00
vcoppe
c189ebd8ca fix attribution link for OSM layer, part of #176 2025-01-29 18:31:11 +01:00
vcoppe
b35d11c9ed add missing ign fr attribution to custom styles 2025-01-29 18:24:03 +01:00
vcoppe
5fa5908072 use waypoint symbol in file tree 2025-01-26 12:48:23 +01:00
vcoppe
a89f2754d3 New Crowdin updates (#173)
* New translations en.json (Spanish)

* New translations en.json (Czech)

* New translations en.json (Italian)

* New translations en.json (Dutch)
2025-01-25 20:20:13 +01:00
vcoppe
453ae55db0 remember split type, part of #145 2025-01-25 13:35:21 +01:00
vcoppe
c1a5bdd7ae New Crowdin updates (#172)
* New translations en.json (Dutch)

* New translations en.json (Russian)

* New translations en.json (Turkish)

* New translations en.json (German)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Belarusian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Korean)

* New translations en.json (Lithuanian)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Latvian)

* New translations en.json (Serbian (Latin))

* New translations view.mdx (Chinese Simplified)

* Update source file en.json

* New translations en.json (French)
2025-01-25 13:03:57 +01:00
vcoppe
d19e702084 map context menu with coordinates, closes #149 2025-01-25 12:31:12 +01:00
vcoppe
e02a22eaea fix export for gpx files with no attributes 2025-01-25 10:47:47 +01:00
vcoppe
63f3d63518 use filesaver 2025-01-24 20:42:45 +01:00
Anthony
0d03ebfe96 Export multiple files as zip (#153) 2025-01-24 19:57:08 +01:00
vcoppe
074da855c1 New Crowdin updates (#167)
* New translations en.json (Russian)

* New translations en.json (Dutch)

* New translations en.json (Dutch)

* New translations files-and-stats.mdx (Dutch)

* New translations getting-started.mdx (Dutch)

* New translations gpx.mdx (Dutch)

* New translations funding.mdx (Dutch)

* New translations translation.mdx (Dutch)

* New translations integration.mdx (Dutch)

* New translations map-controls.mdx (Dutch)

* New translations edit.mdx (Dutch)

* New translations file.mdx (Dutch)

* New translations settings.mdx (Dutch)

* New translations view.mdx (Dutch)

* New translations extract.mdx (Dutch)

* New translations merge.mdx (Dutch)

* New translations minify.mdx (Dutch)

* New translations scissors.mdx (Dutch)

* New translations time.mdx (Dutch)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations integration.mdx (Italian)

* New translations toolbar.mdx (Italian)

* New translations integration.mdx (Italian)

* New translations en.json (German)

* New translations files-and-stats.mdx (German)

* New translations view.mdx (German)

* New translations files-and-stats.mdx (German)
2025-01-24 17:42:58 +01:00
vcoppe
9a028b9d5d support all xml namespaces 2025-01-09 20:45:11 +01:00
vcoppe
a502980a39 New Crowdin updates (#165)
* New translations en.json (Polish)

* New translations en.json (Chinese Simplified)

* New translations getting-started.mdx (Chinese Simplified)

* New translations menu.mdx (Chinese Simplified)

* New translations view.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations edit.mdx (Chinese Simplified)

* New translations file.mdx (Chinese Simplified)

* New translations settings.mdx (Chinese Simplified)

* New translations routing.mdx (Chinese Simplified)

* New translations toolbar.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations poi.mdx (Chinese Simplified)

* New translations time.mdx (Chinese Simplified)

* New translations routing.mdx (Chinese Simplified)

* New translations scissors.mdx (Chinese Simplified)

* New translations merge.mdx (Chinese Simplified)

* New translations extract.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations minify.mdx (Chinese Simplified)

* New translations extract.mdx (Chinese Simplified)

* New translations map-controls.mdx (Chinese Simplified)

* New translations clean.mdx (Chinese Simplified)

* New translations elevation.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations gpx.mdx (Chinese Simplified)

* New translations integration.mdx (Chinese Simplified)

* New translations faq.mdx (Chinese Simplified)

* New translations map-controls.mdx (Chinese Simplified)

* New translations elevation.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)
2025-01-09 09:03:18 +01:00
vcoppe
1a1f6e5131 New Crowdin updates (#164)
* New translations files-and-stats.mdx (Italian)

* New translations elevation.mdx (Italian)
2025-01-05 19:36:38 +01:00
vcoppe
a67501e6de add italian 2025-01-05 19:13:07 +01:00
vcoppe
1f4164aca3 New Crowdin updates (#163)
* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Czech)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)
2025-01-05 19:12:23 +01:00
vcoppe
25e05f9855 fix home link for english 2025-01-04 12:48:58 +01:00
vcoppe
1555799533 New Crowdin updates (#162)
* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations funding.mdx (Chinese Simplified)

* New translations mapbox.mdx (Chinese Simplified)

* New translations translation.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations translation.mdx (Chinese Simplified)

* New translations settings.mdx (Chinese Simplified)
2025-01-04 12:38:08 +01:00
vcoppe
93d5211d27 fix home link for languages other than english 2025-01-03 18:23:48 +01:00
vcoppe
de38fea917 New Crowdin updates (#161)
* New translations clean.mdx (Chinese Simplified)

* New translations en.json (German)
2025-01-03 17:35:31 +01:00
vcoppe
3a3bc1c0db add language 2025-01-03 14:28:34 +01:00
vcoppe
b5871d974e New Crowdin updates (#160)
* New translations view.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations file.mdx (Chinese Simplified)

* New translations routing.mdx (Chinese Simplified)

* New translations scissors.mdx (Chinese Simplified)

* New translations view.mdx (Chinese Simplified)

* New translations elevation.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations elevation.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations settings.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Belarusian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Ukrainian)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Korean)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Latvian)

* New translations en.json (Serbian (Latin))

* Update source file en.json

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)
2025-01-03 14:19:47 +01:00
vcoppe
2985c3201b fix localized home link 2025-01-03 14:18:55 +01:00
vcoppe
cf2fcd8221 remove unused color strings 2025-01-03 09:27:50 +01:00
vcoppe
2cb21a43c5 fix tab color when all tracks have a different one 2025-01-02 22:04:14 +01:00
vcoppe
bae0a3f93b handle gpx style attributes without the namespace 2025-01-01 20:01:46 +01:00
vcoppe
a853c45ec7 New Crowdin updates (#158)
* New translations files-and-stats.mdx (Italian)

* New translations map-controls.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations getting-started.mdx (Chinese Simplified)

* New translations edit.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations view.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations edit.mdx (Chinese Simplified)

* New translations funding.mdx (Chinese Simplified)

* New translations edit.mdx (Chinese Simplified)

* New translations en.json (German)

* New translations en.json (Spanish)

* New translations en.json (Dutch)

* New translations files-and-stats.mdx (Spanish)

* New translations files-and-stats.mdx (Dutch)

* New translations getting-started.mdx (Spanish)

* New translations getting-started.mdx (Dutch)

* New translations edit.mdx (Spanish)

* New translations edit.mdx (Dutch)

* New translations view.mdx (Spanish)

* New translations view.mdx (Dutch)

* New translations funding.mdx (Chinese Simplified)

* New translations mapbox.mdx (Chinese Simplified)

* New translations translation.mdx (Chinese Simplified)

* New translations file.mdx (Chinese Simplified)

* New translations settings.mdx (Chinese Simplified)

* New translations settings.mdx (Chinese Simplified)
2025-01-01 14:41:42 +01:00
vcoppe
6cb6c88cd1 fix line weight attribute with correct one: line width 2025-01-01 14:40:28 +01:00
vcoppe
077f2b4435 New Crowdin updates (#157)
* Update source file files-and-stats.mdx

* New translations en.json (Italian)

* New translations files-and-stats.mdx (Romanian)

* New translations files-and-stats.mdx (French)

* New translations files-and-stats.mdx (Spanish)

* New translations files-and-stats.mdx (Belarusian)

* New translations files-and-stats.mdx (Catalan)

* New translations files-and-stats.mdx (Czech)

* New translations files-and-stats.mdx (Danish)

* New translations files-and-stats.mdx (German)

* New translations files-and-stats.mdx (Greek)

* New translations files-and-stats.mdx (Finnish)

* New translations files-and-stats.mdx (Hebrew)

* New translations files-and-stats.mdx (Hungarian)

* New translations files-and-stats.mdx (Italian)

* New translations files-and-stats.mdx (Korean)

* New translations files-and-stats.mdx (Lithuanian)

* New translations files-and-stats.mdx (Dutch)

* New translations files-and-stats.mdx (Norwegian)

* New translations files-and-stats.mdx (Polish)

* New translations files-and-stats.mdx (Portuguese)

* New translations files-and-stats.mdx (Russian)

* New translations files-and-stats.mdx (Swedish)

* New translations files-and-stats.mdx (Turkish)

* New translations files-and-stats.mdx (Ukrainian)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Vietnamese)

* New translations files-and-stats.mdx (Portuguese, Brazilian)

* New translations files-and-stats.mdx (Latvian)

* New translations files-and-stats.mdx (Serbian (Latin))

* Update source file files-and-stats.mdx

* New translations files-and-stats.mdx (French)
2024-12-28 19:44:27 +01:00
vcoppe
8c3c4860f8 fix link 2024-12-28 19:30:58 +01:00
vcoppe
143592f724 New Crowdin updates (#156)
* New translations map-controls.mdx (Chinese Simplified)

* New translations map-controls.mdx (Chinese Simplified)

* New translations gpx.mdx (Chinese Simplified)

* New translations integration.mdx (Chinese Simplified)

* New translations faq.mdx (Chinese Simplified)

* New translations menu.mdx (Chinese Simplified)

* New translations toolbar.mdx (Chinese Simplified)

* New translations getting-started.mdx (Chinese Simplified)

* New translations faq.mdx (Chinese Simplified)

* New translations en.json (Ukrainian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Belarusian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Korean)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Latvian)

* New translations en.json (Serbian (Latin))

* New translations files-and-stats.mdx (Romanian)

* New translations files-and-stats.mdx (French)

* New translations files-and-stats.mdx (Spanish)

* New translations files-and-stats.mdx (Belarusian)

* New translations files-and-stats.mdx (Catalan)

* New translations files-and-stats.mdx (Czech)

* New translations files-and-stats.mdx (Danish)

* New translations files-and-stats.mdx (German)

* New translations view.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Greek)

* New translations files-and-stats.mdx (Finnish)

* New translations files-and-stats.mdx (Hebrew)

* New translations files-and-stats.mdx (Hungarian)

* New translations files-and-stats.mdx (Italian)

* New translations files-and-stats.mdx (Korean)

* New translations files-and-stats.mdx (Lithuanian)

* New translations files-and-stats.mdx (Dutch)

* New translations files-and-stats.mdx (Norwegian)

* New translations files-and-stats.mdx (Polish)

* New translations files-and-stats.mdx (Portuguese)

* New translations files-and-stats.mdx (Russian)

* New translations files-and-stats.mdx (Swedish)

* New translations files-and-stats.mdx (Turkish)

* New translations files-and-stats.mdx (Ukrainian)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Vietnamese)

* New translations files-and-stats.mdx (Portuguese, Brazilian)

* New translations files-and-stats.mdx (Latvian)

* New translations files-and-stats.mdx (Serbian (Latin))

* New translations getting-started.mdx (Romanian)

* New translations getting-started.mdx (French)

* New translations getting-started.mdx (Spanish)

* New translations getting-started.mdx (Belarusian)

* New translations getting-started.mdx (Catalan)

* New translations getting-started.mdx (Czech)

* New translations getting-started.mdx (Danish)

* New translations getting-started.mdx (German)

* New translations getting-started.mdx (Greek)

* New translations getting-started.mdx (Finnish)

* New translations getting-started.mdx (Hebrew)

* New translations getting-started.mdx (Hungarian)

* New translations getting-started.mdx (Italian)

* New translations getting-started.mdx (Korean)

* New translations getting-started.mdx (Lithuanian)

* New translations getting-started.mdx (Dutch)

* New translations getting-started.mdx (Norwegian)

* New translations getting-started.mdx (Polish)

* New translations getting-started.mdx (Portuguese)

* New translations getting-started.mdx (Russian)

* New translations getting-started.mdx (Swedish)

* New translations getting-started.mdx (Turkish)

* New translations getting-started.mdx (Ukrainian)

* New translations getting-started.mdx (Chinese Simplified)

* New translations getting-started.mdx (Vietnamese)

* New translations getting-started.mdx (Portuguese, Brazilian)

* New translations getting-started.mdx (Latvian)

* New translations getting-started.mdx (Serbian (Latin))

* New translations edit.mdx (Romanian)

* New translations edit.mdx (French)

* New translations edit.mdx (Spanish)

* New translations edit.mdx (Belarusian)

* New translations edit.mdx (Catalan)

* New translations edit.mdx (Czech)

* New translations edit.mdx (Danish)

* New translations edit.mdx (German)

* New translations edit.mdx (Greek)

* New translations edit.mdx (Finnish)

* New translations edit.mdx (Hebrew)

* New translations edit.mdx (Hungarian)

* New translations edit.mdx (Italian)

* New translations edit.mdx (Korean)

* New translations edit.mdx (Lithuanian)

* New translations edit.mdx (Dutch)

* New translations edit.mdx (Norwegian)

* New translations edit.mdx (Polish)

* New translations edit.mdx (Portuguese)

* New translations edit.mdx (Russian)

* New translations edit.mdx (Swedish)

* New translations edit.mdx (Turkish)

* New translations edit.mdx (Ukrainian)

* New translations edit.mdx (Chinese Simplified)

* New translations edit.mdx (Vietnamese)

* New translations edit.mdx (Portuguese, Brazilian)

* New translations edit.mdx (Latvian)

* New translations edit.mdx (Serbian (Latin))

* New translations view.mdx (Romanian)

* New translations view.mdx (French)

* New translations view.mdx (Spanish)

* New translations view.mdx (Belarusian)

* New translations view.mdx (Catalan)

* New translations view.mdx (Czech)

* New translations view.mdx (Danish)

* New translations view.mdx (German)

* New translations view.mdx (Greek)

* New translations view.mdx (Finnish)

* New translations view.mdx (Hebrew)

* New translations view.mdx (Hungarian)

* New translations view.mdx (Italian)

* New translations view.mdx (Korean)

* New translations view.mdx (Lithuanian)

* New translations view.mdx (Dutch)

* New translations view.mdx (Norwegian)

* New translations view.mdx (Polish)

* New translations view.mdx (Portuguese)

* New translations view.mdx (Russian)

* New translations view.mdx (Swedish)

* New translations view.mdx (Turkish)

* New translations view.mdx (Ukrainian)

* New translations view.mdx (Vietnamese)

* New translations view.mdx (Portuguese, Brazilian)

* New translations view.mdx (Latvian)

* New translations view.mdx (Serbian (Latin))

* Update source file en.json

* Update source file files-and-stats.mdx

* Update source file getting-started.mdx

* Update source file edit.mdx

* Update source file view.mdx

* New translations en.json (French)

* New translations files-and-stats.mdx (French)

* New translations getting-started.mdx (French)

* New translations edit.mdx (French)

* New translations view.mdx (French)

* New translations view.mdx (Italian)
2024-12-28 19:22:36 +01:00
vcoppe
dc404706c5 detect and ignore duplicate POIs when merging 2024-12-28 16:14:36 +01:00
vcoppe
7a80e9e104 rename "vertical file list" to "file tree" 2024-12-28 15:52:29 +01:00
vcoppe
d7aae81c41 fix style extension handling 2024-12-28 15:16:32 +01:00
vcoppe
745c7e8470 New translations map-controls.mdx (Chinese Simplified) (#155) 2024-12-24 17:07:22 +01:00
vcoppe
52623350bd New Crowdin updates (#140)
* New translations en.json (German)

* New translations en.json (German)

* New translations minify.mdx (German)

* New translations en.json (Belarusian)

* New translations en.json (Belarusian)

* New translations en.json (Belarusian)

* New translations en.json (Spanish)

* New translations en.json (Spanish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Serbian (Latin))

* New translations en.json (Serbian (Latin))

* New translations en.json (Hebrew)

* New translations en.json (Hebrew)

* New translations en.json (Serbian (Latin))

* New translations translation.mdx (Chinese Simplified)

* New translations settings.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations translation.mdx (Chinese Simplified)

* New translations funding.mdx (Chinese Simplified)

* New translations map-controls.mdx (Chinese Simplified)

* New translations menu.mdx (Chinese Simplified)

* New translations toolbar.mdx (Chinese Simplified)

* New translations translation.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations funding.mdx (Chinese Simplified)

* New translations elevation.mdx (Chinese Simplified)

* New translations translation.mdx (Chinese Simplified)

* New translations mapbox.mdx (Chinese Simplified)

* New translations funding.mdx (Chinese Simplified)

* New translations gpx.mdx (Chinese Simplified)

* New translations edit.mdx (Chinese Simplified)

* New translations file.mdx (Chinese Simplified)

* New translations settings.mdx (Chinese Simplified)

* New translations elevation.mdx (Chinese Simplified)

* New translations view.mdx (Chinese Simplified)

* New translations clean.mdx (Chinese Simplified)

* New translations extract.mdx (Chinese Simplified)

* New translations merge.mdx (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations funding.mdx (Chinese Simplified)

* New translations menu.mdx (Chinese Simplified)

* New translations toolbar.mdx (Chinese Simplified)

* New translations merge.mdx (Chinese Simplified)

* New translations minify.mdx (Chinese Simplified)

* New translations poi.mdx (Chinese Simplified)

* New translations faq.mdx (Chinese Simplified)

* New translations getting-started.mdx (Chinese Simplified)

* New translations en.json (Catalan)

* New translations en.json (Belarusian)

* New translations en.json (Norwegian)

* New translations en.json (Norwegian)

* New translations mapbox.mdx (Catalan)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Hungarian)

* New translations en.json (Hungarian)

* New translations en.json (Hungarian)

* New translations files-and-stats.mdx (Hungarian)

* New translations getting-started.mdx (Hungarian)

* New translations integration.mdx (Hungarian)

* New translations map-controls.mdx (Hungarian)

* New translations menu.mdx (Hungarian)

* New translations edit.mdx (Hungarian)

* New translations settings.mdx (Hungarian)

* New translations view.mdx (Hungarian)

* New translations toolbar.mdx (Hungarian)

* New translations routing.mdx (Hungarian)

* New translations scissors.mdx (Hungarian)

* New translations time.mdx (Hungarian)

* New translations en.json (Vietnamese)

* New translations translation.mdx (Vietnamese)

* New translations settings.mdx (Vietnamese)

* New translations funding.mdx (Vietnamese)

* New translations map-controls.mdx (Belarusian)

* New translations view.mdx (Belarusian)

* New translations map-controls.mdx (Belarusian)

* New translations integration.mdx (Belarusian)

* New translations integration.mdx (Belarusian)

* New translations gpx.mdx (Belarusian)

* New translations en.json (Ukrainian)

* New translations en.json (Ukrainian)

* New translations poi.mdx (Chinese Simplified)

* New translations time.mdx (Chinese Simplified)

* New translations poi.mdx (Chinese Simplified)

* New translations time.mdx (Chinese Simplified)

* New translations routing.mdx (Chinese Simplified)

* New translations scissors.mdx (Chinese Simplified)

* New translations routing.mdx (Chinese Simplified)

* New translations poi.mdx (Chinese Simplified)

* New translations time.mdx (Chinese Simplified)

* New translations routing.mdx (Chinese Simplified)

* New translations scissors.mdx (Chinese Simplified)

* New translations merge.mdx (Chinese Simplified)

* New translations minify.mdx (Chinese Simplified)

* New translations extract.mdx (Chinese Simplified)

* New translations extract.mdx (Chinese Simplified)

* New translations map-controls.mdx (Chinese Simplified)

* New translations menu.mdx (Chinese Simplified)

* New translations view.mdx (Chinese Simplified)

* New translations toolbar.mdx (Chinese Simplified)

* New translations clean.mdx (Chinese Simplified)

* New translations elevation.mdx (Chinese Simplified)

* New translations faq.mdx (Italian)
2024-12-24 16:51:29 +01:00
vcoppe
7d2c030ebd update Mapbox GL JS, closes #129 2024-12-24 16:50:52 +01:00
vcoppe
b841326e19 finer tolerances for minify tool, closes #150 2024-12-24 16:40:48 +01:00
vcoppe
23c41f18de use new toilet icon, closes #100 2024-11-18 21:08:30 +01:00
vcoppe
44e11e1a51 back to globe projection 2024-10-23 13:50:28 +02:00
vcoppe
798d8e7a14 add german 2024-10-21 23:27:19 +02:00
vcoppe
29748cf114 New translations integration.mdx (German) (#139) 2024-10-21 23:26:57 +02:00
vcoppe
f5794b1355 New Crowdin updates (#132)
* New translations en.json (Italian)

* New translations funding.mdx (Czech)

* New translations mapbox.mdx (Czech)

* New translations translation.mdx (Czech)

* New translations edit.mdx (Czech)

* New translations settings.mdx (Czech)

* New translations edit.mdx (Polish)

* New translations funding.mdx (Italian)

* New translations edit.mdx (Italian)

* New translations elevation.mdx (Italian)

* New translations en.json (Catalan)

* New translations en.json (Italian)

* New translations files-and-stats.mdx (Italian)

* New translations edit.mdx (Italian)

* New translations file.mdx (Catalan)

* New translations file.mdx (Italian)

* New translations extract.mdx (Catalan)

* New translations routing.mdx (Italian)

* New translations en.json (Catalan)

* New translations files-and-stats.mdx (Catalan)

* New translations getting-started.mdx (Catalan)

* New translations mapbox.mdx (Catalan)

* New translations settings.mdx (Catalan)

* New translations view.mdx (Catalan)

* New translations faq.mdx (Catalan)

* New translations en.json (Catalan)

* New translations files-and-stats.mdx (Italian)

* New translations getting-started.mdx (Italian)

* New translations funding.mdx (Italian)

* New translations view.mdx (Catalan)

* New translations view.mdx (Italian)

* New translations en.json (Catalan)

* New translations en.json (Catalan)

* New translations files-and-stats.mdx (Catalan)

* New translations en.json (Hebrew)

* New translations files-and-stats.mdx (Catalan)

* New translations en.json (Hebrew)

* New translations gpx.mdx (German)

* New translations en.json (German)

* New translations en.json (German)

* New translations funding.mdx (Hebrew)

* New translations en.json (Dutch)

* New translations edit.mdx (German)

* New translations extract.mdx (German)

* New translations elevation.mdx (German)

* New translations en.json (German)

* New translations extract.mdx (German)

* New translations en.json (Italian)

* New translations en.json (Italian)

* New translations files-and-stats.mdx (Italian)

* New translations getting-started.mdx (Italian)

* New translations minify.mdx (Italian)

* New translations poi.mdx (Italian)

* New translations routing.mdx (Italian)

* New translations en.json (Czech)

* New translations en.json (French)

* New translations en.json (German)

* New translations en.json (German)

* New translations en.json (Czech)

* New translations file.mdx (Czech)

* New translations settings.mdx (Czech)

* New translations routing.mdx (Czech)

* New translations en.json (German)

* New translations mapbox.mdx (German)

* New translations en.json (German)

* New translations en.json (Turkish)

* New translations files-and-stats.mdx (Turkish)

* New translations getting-started.mdx (Turkish)

* New translations gpx.mdx (Turkish)

* New translations funding.mdx (Turkish)

* New translations mapbox.mdx (Turkish)

* New translations translation.mdx (Turkish)

* New translations integration.mdx (Turkish)

* New translations map-controls.mdx (Turkish)

* New translations menu.mdx (Turkish)

* New translations edit.mdx (Turkish)

* New translations file.mdx (Turkish)

* New translations settings.mdx (Turkish)

* New translations view.mdx (Turkish)

* New translations toolbar.mdx (Turkish)

* New translations clean.mdx (Turkish)

* New translations extract.mdx (Turkish)

* New translations merge.mdx (Turkish)

* New translations minify.mdx (Turkish)

* New translations poi.mdx (Turkish)

* New translations routing.mdx (Turkish)

* New translations scissors.mdx (Turkish)

* New translations time.mdx (Turkish)

* New translations faq.mdx (Turkish)

* New translations elevation.mdx (Turkish)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations en.json (Turkish)

* New translations en.json (Russian)

* New translations en.json (Ukrainian)

* New translations files-and-stats.mdx (Ukrainian)

* New translations getting-started.mdx (Ukrainian)

* New translations gpx.mdx (Ukrainian)

* New translations funding.mdx (Ukrainian)

* New translations mapbox.mdx (Ukrainian)

* New translations translation.mdx (Ukrainian)

* New translations integration.mdx (Ukrainian)

* New translations map-controls.mdx (Ukrainian)

* New translations menu.mdx (Ukrainian)

* New translations edit.mdx (Ukrainian)

* New translations file.mdx (Ukrainian)

* New translations settings.mdx (Ukrainian)

* New translations view.mdx (Ukrainian)

* New translations toolbar.mdx (Ukrainian)

* New translations clean.mdx (Ukrainian)

* New translations extract.mdx (Ukrainian)

* New translations merge.mdx (Ukrainian)

* New translations minify.mdx (Ukrainian)

* New translations poi.mdx (Ukrainian)

* New translations routing.mdx (Ukrainian)

* New translations scissors.mdx (Ukrainian)

* New translations time.mdx (Ukrainian)

* New translations faq.mdx (Ukrainian)

* New translations elevation.mdx (Ukrainian)

* New translations en.json (Ukrainian)

* New translations merge.mdx (German)

* New translations en.json (German)

* New translations gpx.mdx (German)

* New translations edit.mdx (German)

* New translations extract.mdx (German)

* New translations en.json (Ukrainian)

* New translations merge.mdx (German)

* New translations files-and-stats.mdx (German)

* New translations getting-started.mdx (German)

* New translations funding.mdx (German)

* New translations integration.mdx (German)

* New translations map-controls.mdx (German)

* New translations view.mdx (German)

* New translations toolbar.mdx (German)

* New translations minify.mdx (German)

* New translations poi.mdx (German)

* New translations routing.mdx (German)

* New translations scissors.mdx (German)

* New translations faq.mdx (German)

* New translations en.json (German)

* New translations funding.mdx (German)
2024-10-21 23:08:45 +02:00
vcoppe
4ada271ad3 try to detect 512px custom tiles 2024-10-17 11:55:13 +02:00
vcoppe
1bda957778 scrollable poi content, closes #124 2024-10-14 19:00:05 +02:00
vcoppe
95328db1ee fix poi empty name 2024-10-14 18:42:14 +02:00
vcoppe
65cbf5e751 fix updating overlay opacity 2024-10-14 16:42:05 +02:00
vcoppe
60f24f8757 add h1's 2024-10-10 11:39:14 +02:00
vcoppe
d1e4588813 distance markers hierarchy 2024-10-08 16:58:04 +02:00
vcoppe
711825f5a3 refactor map popups and add inspect trackpoint feature 2024-10-08 15:49:14 +02:00
vcoppe
d823f44558 more precise polyfill 2024-10-08 12:39:00 +02:00
vcoppe
193c77e51a fix xml output for osm attributes 2024-10-08 12:04:07 +02:00
vcoppe
45f6c405c0 fix elevation profile drag on retina screens 2024-10-06 20:03:35 +02:00
vcoppe
78e1ea1e59 New Crowdin updates (#131)
* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Korean)

* New translations en.json (Hebrew)

* New translations en.json (Finnish)

* New translations en.json (Serbian (Latin))

* New translations en.json (Belarusian)

* New translations en.json (Danish)

* New translations en.json (Latvian)

* Update source file en.json

* New translations en.json (Spanish)

* New translations en.json (Dutch)

* New translations en.json (Portuguese, Brazilian)
2024-10-05 17:43:44 +02:00
vcoppe
ca51e1d788 fix derive file style in constructor 2024-10-05 17:40:25 +02:00
vcoppe
72b0b5a706 add spanish satellite layer 2024-10-05 17:06:17 +02:00
vcoppe
4d1de97ba5 New Crowdin updates (#130)
* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Korean)

* New translations en.json (Hebrew)

* New translations en.json (Finnish)

* New translations en.json (Belarusian)

* New translations en.json (Serbian (Latin))

* New translations en.json (Danish)

* New translations en.json (Latvian)

* Update source file en.json

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Dutch)

* New translations en.json (Italian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations files-and-stats.mdx (Romanian)

* New translations files-and-stats.mdx (French)

* New translations files-and-stats.mdx (Spanish)

* New translations files-and-stats.mdx (Catalan)

* New translations files-and-stats.mdx (Czech)

* New translations files-and-stats.mdx (German)

* New translations files-and-stats.mdx (Greek)

* New translations files-and-stats.mdx (Hungarian)

* New translations files-and-stats.mdx (Italian)

* New translations files-and-stats.mdx (Lithuanian)

* New translations files-and-stats.mdx (Dutch)

* New translations files-and-stats.mdx (Norwegian)

* New translations files-and-stats.mdx (Polish)

* New translations files-and-stats.mdx (Portuguese)

* New translations files-and-stats.mdx (Russian)

* New translations files-and-stats.mdx (Swedish)

* New translations files-and-stats.mdx (Chinese Simplified)

* New translations files-and-stats.mdx (Vietnamese)

* New translations files-and-stats.mdx (Portuguese, Brazilian)

* New translations en.json (Korean)

* New translations files-and-stats.mdx (Korean)

* New translations en.json (Hebrew)

* New translations files-and-stats.mdx (Hebrew)

* New translations en.json (Finnish)

* New translations files-and-stats.mdx (Finnish)

* New translations en.json (Serbian (Latin))

* New translations files-and-stats.mdx (Serbian (Latin))

* New translations en.json (Belarusian)

* New translations files-and-stats.mdx (Belarusian)

* New translations en.json (Danish)

* New translations files-and-stats.mdx (Danish)

* New translations en.json (Latvian)

* New translations files-and-stats.mdx (Latvian)

* Update source file en.json

* Update source file files-and-stats.mdx

* New translations en.json (Spanish)

* New translations en.json (Spanish)

* New translations en.json (Spanish)

* New translations en.json (Spanish)

* New translations files-and-stats.mdx (Spanish)

* New translations en.json (French)

* New translations en.json (Dutch)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Korean)

* New translations en.json (Hebrew)

* New translations en.json (Finnish)

* New translations en.json (Serbian (Latin))

* New translations en.json (Belarusian)

* New translations en.json (Danish)

* New translations en.json (Latvian)

* Update source file en.json

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Korean)

* New translations en.json (Hebrew)

* New translations en.json (Finnish)

* New translations en.json (Belarusian)

* New translations en.json (Serbian (Latin))

* New translations en.json (Danish)

* New translations en.json (Latvian)

* New translations en.json (French)

* New translations en.json (Dutch)

* New translations files-and-stats.mdx (French)

* New translations files-and-stats.mdx (Dutch)
2024-10-04 19:53:31 +02:00
vcoppe
04769002d0 update screenshot 2024-10-04 19:53:03 +02:00
vcoppe
8190284148 correct highway naming 2024-10-04 18:10:16 +02:00
vcoppe
a668b4be62 improve highway names 2024-10-04 18:04:20 +02:00
vcoppe
87534468d2 highway, sac_scale and mtb:scale coloring 2024-10-04 17:32:05 +02:00
vcoppe
d7a02f714a add highway info to elevation profile, closes #65 2024-10-03 18:14:01 +02:00
vcoppe
0c16ddd534 fix z-indices 2024-10-03 14:19:51 +02:00
vcoppe
a96f989199 small ui improvements around elevation profile 2024-10-03 14:16:41 +02:00
vcoppe
35c7c9d965 compact elevation profile dataset control 2024-10-03 13:24:23 +02:00
vcoppe
efa05b93ce remove additional scales 2024-10-03 11:41:08 +02:00
vcoppe
0e298cd0e4 same surface tags as brouter 2024-10-03 10:12:10 +02:00
vcoppe
3ef98b2110 improve mapillary integration, closes #127 2024-10-02 18:52:02 +02:00
vcoppe
fbf93ed6f9 increase wpt popup max width 2024-10-02 17:03:59 +02:00
vcoppe
1bd56b6505 New Crowdin updates (#121)
* New translations en.json (Hungarian)

* New translations files-and-stats.mdx (Hungarian)

* New translations en.json (Hungarian)

* New translations funding.mdx (Hungarian)

* New translations integration.mdx (Hungarian)

* New translations faq.mdx (Hungarian)

* New translations en.json (Hungarian)

* New translations file.mdx (Italian)

* New translations file.mdx (Italian)

* New translations en.json (Italian)

* New translations file.mdx (Italian)

* New translations en.json (German)

* New translations en.json (German)

* New translations getting-started.mdx (German)

* New translations map-controls.mdx (German)

* New translations menu.mdx (German)

* New translations toolbar.mdx (German)

* New translations extract.mdx (German)

* New translations en.json (German)

* Update source file en.json

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Korean)

* New translations en.json (Hebrew)

* New translations en.json (Finnish)

* New translations en.json (Belarusian)

* New translations en.json (Serbian (Latin))

* New translations en.json (Danish)

* New translations en.json (Latvian)

* New translations en.json (Hungarian)

* New translations menu.mdx (Hungarian)

* New translations toolbar.mdx (Hungarian)

* New translations en.json (Italian)
2024-10-02 12:53:50 +02:00
vcoppe
acf0750ccb temporary fix for #129 2024-10-02 12:49:08 +02:00
vcoppe
48eaa344e4 put ui elements below popups 2024-10-01 16:59:20 +02:00
vcoppe
3262dec7d3 show popup after content has been rendered, fixes popup placement, see #124 2024-10-01 16:56:13 +02:00
vcoppe
572d206c2c reduce hiding distance for popups 2024-10-01 14:18:23 +02:00
vcoppe
11934e5825 option to remove time gaps when merging 2024-10-01 13:17:39 +02:00
vcoppe
5cca106d18 fix button event propagation 2024-09-30 22:08:54 +02:00
587 changed files with 33479 additions and 34065 deletions

16
.prettierrc Normal file
View File

@@ -0,0 +1,16 @@
{
"useTabs": false,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"overrides": [
{
"files": "**/*.svelte",
"options": {
"plugins": ["prettier-plugin-svelte"],
"parser": "svelte"
}
}
]
}

7
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"svelte.svelte-vscode"
]
}

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

@@ -0,0 +1,13 @@
{
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode"
}
}

1
gpx/.prettierignore Normal file
View File

@@ -0,0 +1 @@
package-lock.json

1617
gpx/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,16 +12,20 @@
"private": true,
"dependencies": {
"fast-xml-parser": "^4.5.0",
"immer": "^10.1.1",
"ts-node": "^10.9.2"
"immer": "^10.1.1"
},
"devDependencies": {
"@types/geojson": "^7946.0.14",
"@types/node": "^20.16.10",
"@typescript-eslint/parser": "^8.22.0",
"prettier": "^3.4.2",
"ts-node": "^10.9.2",
"typescript": "^5.6.2"
},
"scripts": {
"build": "tsc",
"postinstall": "npm run build"
"postinstall": "npm run build",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,4 +2,3 @@ export * from './gpx';
export { Coordinates, LineStyleExtension, WaypointType } from './types';
export { parseGPX, buildGPX } from './io';
export * from './simplify';

View File

@@ -1,25 +1,68 @@
import { XMLParser, XMLBuilder } from "fast-xml-parser";
import { GPXFileType } from "./types";
import { GPXFile } from "./gpx";
import { XMLParser, XMLBuilder } from 'fast-xml-parser';
import { GPXFileType } from './types';
import { GPXFile } from './gpx';
const attributesWithNamespace = {
RoutePointExtension: 'gpxx:RoutePointExtension',
rpt: 'gpxx:rpt',
TrackPointExtension: 'gpxtpx:TrackPointExtension',
PowerExtension: 'gpxpx:PowerExtension',
atemp: 'gpxtpx:atemp',
hr: 'gpxtpx:hr',
cad: 'gpxtpx:cad',
Extensions: 'gpxtpx:Extensions',
PowerInWatts: 'gpxpx:PowerInWatts',
power: 'gpxpx:PowerExtension',
line: 'gpx_style:line',
color: 'gpx_style:color',
opacity: 'gpx_style:opacity',
width: 'gpx_style:width',
};
const floatPatterns = [
/[-+]?\d*\.\d+$/, // decimal
/[-+]?\d+$/, // integer
];
function safeParseFloat(value: string): number {
const parsed = parseFloat(value);
if (!isNaN(parsed)) {
return parsed;
}
for (const pattern of floatPatterns) {
const match = value.match(pattern);
if (match) {
return parseFloat(match[0]);
}
}
return 0.0;
}
export function parseGPX(gpxData: string): GPXFile {
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: "",
attributeNamePrefix: '',
attributesGroupName: 'attributes',
removeNSPrefix: true,
isArray(name: string) {
return name === 'trk' || name === 'trkseg' || name === 'trkpt' || name === 'wpt' || name === 'rte' || name === 'rtept' || name === 'gpxx:rpt';
return (
name === 'trk' ||
name === 'trkseg' ||
name === 'trkpt' ||
name === 'wpt' ||
name === 'rte' ||
name === 'rtept' ||
name === 'gpxx:rpt'
);
},
attributeValueProcessor(attrName, attrValue, jPath) {
if (attrName === 'lat' || attrName === 'lon') {
return parseFloat(attrValue);
return safeParseFloat(attrValue);
}
return attrValue;
},
transformTagName(tagName: string) {
if (tagName === 'power') {
// Transform the simple <power> tag to the more complex <gpxpx:PowerExtension> tag, the nested <gpxpx:PowerInWatts> tag is then handled by the tagValueProcessor
return 'gpxpx:PowerExtension';
if (attributesWithNamespace[tagName]) {
return attributesWithNamespace[tagName];
}
return tagName;
},
@@ -27,22 +70,29 @@ export function parseGPX(gpxData: string): GPXFile {
tagValueProcessor(tagName, tagValue, jPath, hasAttributes, isLeafNode) {
if (isLeafNode) {
if (tagName === 'ele') {
return parseFloat(tagValue);
return safeParseFloat(tagValue);
}
if (tagName === 'time') {
return new Date(tagValue);
}
if (tagName === 'gpxtpx:atemp' || tagName === 'gpxtpx:hr' || tagName === 'gpxtpx:cad' || tagName === 'gpxpx:PowerInWatts' || tagName === 'opacity' || tagName === 'weight') {
return parseFloat(tagValue);
if (
tagName === 'gpxtpx:atemp' ||
tagName === 'gpxtpx:hr' ||
tagName === 'gpxtpx:cad' ||
tagName === 'gpxpx:PowerInWatts' ||
tagName === 'gpx_style:opacity' ||
tagName === 'gpx_style:width'
) {
return safeParseFloat(tagValue);
}
if (tagName === 'gpxpx:PowerExtension') {
// Finish the transformation of the simple <power> tag to the more complex <gpxpx:PowerExtension> tag
// Note that this only targets the transformed <power> tag, since it must be a leaf node
return {
'gpxpx:PowerInWatts': parseFloat(tagValue)
'gpxpx:PowerInWatts': safeParseFloat(tagValue),
};
}
}
@@ -54,7 +104,7 @@ export function parseGPX(gpxData: string): GPXFile {
const parsed: GPXFileType = parser.parse(gpxData).gpx;
// @ts-ignore
if (parsed.metadata === "") {
if (parsed.metadata === '') {
parsed.metadata = {};
}
@@ -64,25 +114,32 @@ export function parseGPX(gpxData: string): GPXFile {
export function buildGPX(file: GPXFile, exclude: string[]): string {
const gpx = file.toGPXFileType(exclude);
let lastDate = undefined;
const builder = new XMLBuilder({
format: true,
ignoreAttributes: false,
attributeNamePrefix: "",
attributeNamePrefix: '',
attributesGroupName: 'attributes',
suppressEmptyNode: true,
tagValueProcessor: (tagName: string, tagValue: unknown): string => {
tagValueProcessor: (tagName: string, tagValue: unknown): string | undefined => {
if (tagValue instanceof Date) {
if (isNaN(tagValue.getTime())) {
return lastDate?.toISOString();
}
lastDate = tagValue;
return tagValue.toISOString();
}
return tagValue.toString();
},
});
gpx.attributes.creator = gpx.attributes.creator ?? 'https://gpx.studio';
if (!gpx.attributes) gpx.attributes = {};
gpx.attributes['creator'] = gpx.attributes['creator'] ?? 'https://gpx.studio';
gpx.attributes['version'] = '1.1';
gpx.attributes['xmlns'] = 'http://www.topografix.com/GPX/1/1';
gpx.attributes['xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance';
gpx.attributes['xsi:schemaLocation'] = 'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 http://www.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd';
gpx.attributes['xsi:schemaLocation'] =
'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 http://www.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd';
gpx.attributes['xmlns:gpxtpx'] = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1';
gpx.attributes['xmlns:gpxx'] = 'http://www.garmin.com/xmlschemas/GpxExtensions/v3';
gpx.attributes['xmlns:gpxpx'] = 'http://www.garmin.com/xmlschemas/PowerExtension/v1';
@@ -93,19 +150,24 @@ export function buildGPX(file: GPXFile, exclude: string[]): string {
}
return builder.build({
"?xml": {
'?xml': {
attributes: {
version: "1.0",
encoding: "UTF-8",
}
version: '1.0',
encoding: 'UTF-8',
},
gpx: removeEmptyElements(gpx)
},
gpx: removeEmptyElements(gpx),
});
}
function removeEmptyElements(obj: GPXFileType): GPXFileType {
for (const key in obj) {
if (obj[key] === null || obj[key] === undefined || obj[key] === '' || (Array.isArray(obj[key]) && obj[key].length === 0)) {
if (
obj[key] === null ||
obj[key] === undefined ||
obj[key] === '' ||
(Array.isArray(obj[key]) && obj[key].length === 0)
) {
delete obj[key];
} else if (typeof obj[key] === 'object' && !(obj[key] instanceof Date)) {
removeEmptyElements(obj[key]);

View File

@@ -1,33 +1,48 @@
import { TrackPoint } from "./gpx";
import { Coordinates } from "./types";
import { TrackPoint } from './gpx';
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(points: TrackPoint[], epsilon: number = 50, measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number = crossarcDistance): SimplifiedTrackPoint[] {
export function ramerDouglasPeucker(
points: TrackPoint[],
epsilon: number = 50,
measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number = crossarcDistance
): SimplifiedTrackPoint[] {
if (points.length == 0) {
return [];
} else if (points.length == 1) {
return [{
point: points[0]
}];
return [
{
point: points[0],
},
];
}
let simplified = [{
point: points[0]
}];
let simplified = [
{
point: points[0],
},
];
ramerDouglasPeuckerRecursive(points, epsilon, measure, 0, points.length - 1, simplified);
simplified.push({
point: points[points.length - 1]
point: points[points.length - 1],
});
return simplified;
}
function ramerDouglasPeuckerRecursive(points: TrackPoint[], epsilon: number, measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number, start: number, end: number, simplified: SimplifiedTrackPoint[]) {
function ramerDouglasPeuckerRecursive(
points: TrackPoint[],
epsilon: number,
measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number,
start: number,
end: number,
simplified: SimplifiedTrackPoint[]
) {
let largest = {
index: 0,
distance: 0
distance: 0,
};
for (let i = start + 1; i < end; i++) {
@@ -45,8 +60,16 @@ function ramerDouglasPeuckerRecursive(points: TrackPoint[], epsilon: number, mea
}
}
export function crossarcDistance(point1: TrackPoint, point2: TrackPoint, point3: TrackPoint | Coordinates): number {
return crossarc(point1.getCoordinates(), point2.getCoordinates(), point3 instanceof TrackPoint ? point3.getCoordinates() : point3);
export function crossarcDistance(
point1: TrackPoint,
point2: TrackPoint,
point3: TrackPoint | Coordinates
): number {
return crossarc(
point1.getCoordinates(),
point2.getCoordinates(),
point3 instanceof TrackPoint ? point3.getCoordinates() : point3
);
}
function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number {
@@ -74,7 +97,7 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
}
// Is relative bearing obtuse?
if (diff > (Math.PI / 2)) {
if (diff > Math.PI / 2) {
return dis13;
}
@@ -83,7 +106,8 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
// Is p4 beyond the arc?
let dis12 = distance(lat1, lon1, lat2, lon2);
let dis14 = Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
let dis14 =
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
if (dis14 > dis12) {
return distance(lat2, lon2, lat3, lon3);
} else {
@@ -93,18 +117,32 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
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;
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));
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(point1: TrackPoint, point2: TrackPoint, point3: TrackPoint | Coordinates): Coordinates {
return projected(point1.getCoordinates(), point2.getCoordinates(), point3 instanceof TrackPoint ? point3.getCoordinates() : point3);
export function projectedPoint(
point1: TrackPoint,
point2: TrackPoint,
point3: TrackPoint | Coordinates
): Coordinates {
return projected(
point1.getCoordinates(),
point2.getCoordinates(),
point3 instanceof TrackPoint ? point3.getCoordinates() : point3
);
}
function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates {
@@ -132,7 +170,7 @@ function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates
}
// Is relative bearing obtuse?
if (diff > (Math.PI / 2)) {
if (diff > Math.PI / 2) {
return coord1;
}
@@ -141,14 +179,22 @@ function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates
// Is p4 beyond the arc?
let dis12 = distance(lat1, lon1, lat2, lon2);
let dis14 = Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
let dis14 =
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
if (dis14 > dis12) {
return coord2;
} else {
// Determine the closest point (p4) on the great circle
const f = dis14 / earthRadius;
const lat4 = Math.asin(Math.sin(lat1) * Math.cos(f) + Math.cos(lat1) * Math.sin(f) * Math.cos(bear12));
const lon4 = lon1 + Math.atan2(Math.sin(bear12) * Math.sin(f) * Math.cos(lat1), Math.cos(f) - Math.sin(lat1) * Math.sin(lat4));
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 };
}

View File

@@ -67,9 +67,9 @@ export type TrackExtensions = {
};
export type LineStyleExtension = {
color?: string;
opacity?: number;
weight?: number;
'gpx_style:color'?: string;
'gpx_style:opacity'?: number;
'gpx_style:width'?: number;
};
export type TrackSegmentType = {
@@ -92,14 +92,12 @@ export type TrackPointExtension = {
'gpxtpx:atemp'?: number;
'gpxtpx:hr'?: number;
'gpxtpx:cad'?: number;
'gpxtpx:Extensions'?: {
surface?: string;
'gpxtpx:Extensions'?: Record<string, string>;
};
}
export type PowerExtension = {
'gpxpx:PowerInWatts'?: number;
}
};
export type Author = {
name?: string;
@@ -116,12 +114,12 @@ export type RouteType = {
type?: string;
extensions?: TrackExtensions;
rtept: WaypointType[];
}
};
export type RoutePointExtension = {
'gpxx:rpt'?: GPXXRoutePoint[];
}
};
export type GPXXRoutePoint = {
attributes: Coordinates;
}
};

View File

@@ -16,9 +16,9 @@
<type>Cycling</type>
<extensions>
<gpx_style:line>
<color>#2d3ee9</color>
<opacity>0.5</opacity>
<weight>6</weight>
<gpx_style:color>2d3ee9</gpx_style:color>
<gpx_style:opacity>0.5</gpx_style:opacity>
<gpx_style:width>6</gpx_style:width>
</gpx_style:line>
</extensions>
<trkseg>

View File

@@ -4,9 +4,7 @@
"target": "ES2015",
"declaration": true,
"outDir": "./dist",
"moduleResolution": "node",
"moduleResolution": "node"
},
"include": [
"src"
],
"include": ["src"]
}

View File

@@ -5,27 +5,27 @@ module.exports = {
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
'prettier',
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
extraFileExtensions: ['.svelte'],
},
env: {
browser: true,
es2017: true,
node: true
node: true,
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
parser: '@typescript-eslint/parser',
},
},
],
};

View File

@@ -2,3 +2,5 @@
pnpm-lock.yaml
package-lock.json
yarn.lock
src/lib/components/ui
*.mdx

View File

@@ -1,8 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

1337
website/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@
"scripts": {
"dev": "vite dev",
"build": "vite build",
"prebuild": "npx tsx src/lib/pwa-manifest.ts",
"postbuild": "npx tsx src/lib/sitemap.ts",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
@@ -20,7 +21,7 @@
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@types/eslint": "^8.56.12",
"@types/events": "^3.0.3",
"@types/mapbox__mapbox-gl-geocoder": "^5.0.0",
"@types/file-saver": "^2.0.7",
"@types/mapbox__tilebelt": "^1.0.4",
"@types/mapbox-gl": "^3.4.0",
"@types/node": "^20.16.10",
@@ -35,7 +36,7 @@
"eslint-plugin-svelte": "^2.44.1",
"events": "^3.3.0",
"glob": "^10.4.5",
"mdsvex": "^0.11.2",
"mdsvex": "^0.12.6",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.7",
@@ -61,11 +62,13 @@
"chartjs-plugin-zoom": "^2.0.1",
"clsx": "^2.1.1",
"dexie": "^4.0.8",
"file-saver": "^2.0.5",
"gpx": "file:../gpx",
"immer": "^10.1.1",
"lucide-static": "^0.427.0",
"lucide-svelte": "^0.427.0",
"mapbox-gl": "^3.7.0",
"jszip": "^3.10.1",
"lucide-static": "^0.460.0",
"lucide-svelte": "^0.460.1",
"mapbox-gl": "^3.11.1",
"mapillary-js": "^4.1.2",
"mode-watcher": "^0.3.1",
"png.js": "^0.2.1",

View File

@@ -3,4 +3,4 @@ export default {
tailwindcss: {},
autoprefixer: {},
},
}
};

View File

@@ -1,15 +1,14 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="apple-touch-icon" href="%sveltekit.assets%/apple-touch-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -38,31 +38,18 @@ export async function handle({ event, resolve }) {
<meta name="twitter:url" content="https://gpx.studio/" />
<meta name="twitter:site" content="@gpxstudio" />
<meta name="twitter:creator" content="@gpxstudio" />
<link rel="alternate" hreflang="x-default" href="https://gpx.studio${getURLForLanguage('en', path)}" />`;
<link rel="alternate" hreflang="x-default" href="https://gpx.studio${getURLForLanguage('en', path)}" />
<link rel="manifest" href="/${language}.manifest.webmanifest" />`;
for (let lang of Object.keys(languages)) {
headTag += ` <link rel="alternate" hreflang="${lang}" href="https://gpx.studio${getURLForLanguage(lang, path)}" />
`;
}
const stringsHTML = page === 'app' ? stringsToHTML(strings) : '';
const response = await resolve(event, {
transformPageChunk: ({ html }) => html.replace('<html>', htmlTag).replace('<head>', headTag).replace('</body>', `<div class="fixed -z-10 text-transparent">${stringsHTML}</div></body>`)
transformPageChunk: ({ html }) =>
html.replace('<html>', htmlTag).replace('<head>', headTag),
});
return response;
}
function stringsToHTML(dictionary, strings = new Set(), root = true) {
Object.values(dictionary).forEach((value) => {
if (typeof value === 'object') {
stringsToHTML(value, strings, false);
} else {
strings.add(value);
}
});
if (root) {
return Array.from(strings).map((string) => `<p>${string}</p>`).join('');
}
}

View File

@@ -0,0 +1,171 @@
export const surfaceColors: { [key: string]: string } = {
missing: '#d1d1d1',
paved: '#8c8c8c',
unpaved: '#6b443a',
asphalt: '#8c8c8c',
concrete: '#8c8c8c',
cobblestone: '#ffd991',
paving_stones: '#8c8c8c',
sett: '#ffd991',
metal: '#8c8c8c',
wood: '#6b443a',
compacted: '#ffffa8',
fine_gravel: '#ffffa8',
gravel: '#ffffa8',
pebblestone: '#ffffa8',
rock: '#ffd991',
dirt: '#ffffa8',
ground: '#6b443a',
earth: '#6b443a',
mud: '#6b443a',
sand: '#ffffc4',
grass: '#61b55c',
grass_paver: '#61b55c',
clay: '#6b443a',
stone: '#ffd991',
};
export function getSurfaceColor(surface: string): string {
return surfaceColors[surface] ? surfaceColors[surface] : surfaceColors.missing;
}
export const highwayColors: { [key: string]: string } = {
missing: '#d1d1d1',
motorway: '#ff4d33',
motorway_link: '#ff4d33',
trunk: '#ff5e4d',
trunk_link: '#ff947f',
primary: '#ff6e5c',
primary_link: '#ff6e5c',
secondary: '#ff8d7b',
secondary_link: '#ff8d7b',
tertiary: '#ffd75f',
tertiary_link: '#ffd75f',
unclassified: '#f1f2a5',
road: '#f1f2a5',
residential: '#73b2ff',
living_street: '#73b2ff',
service: '#9c9cd9',
track: '#a8e381',
footway: '#a8e381',
path: '#a8e381',
pedestrian: '#a8e381',
cycleway: '#9de2ff',
construction: '#e09a4a',
bridleway: '#946f43',
raceway: '#ff0000',
rest_area: '#9c9cd9',
services: '#9c9cd9',
corridor: '#474747',
elevator: '#474747',
steps: '#474747',
bus_stop: '#8545a3',
busway: '#8545a3',
via_ferrata: '#474747',
};
export const sacScaleColors: { [key: string]: string } = {
hiking: '#007700',
mountain_hiking: '#1843ad',
demanding_mountain_hiking: '#ffff00',
alpine_hiking: '#ff9233',
demanding_alpine_hiking: '#ff0000',
difficult_alpine_hiking: '#000000',
};
export const mtbScaleColors: { [key: string]: string } = {
'0-': '#007700',
'0': '#007700',
'0+': '#007700',
'1-': '#1843ad',
'1': '#1843ad',
'1+': '#1843ad',
'2-': '#ffff00',
'2': '#ffff00',
'2+': '#ffff00',
'3': '#ff0000',
'4': '#00ff00',
'5': '#000000',
'6': '#b105eb',
};
function createPattern(
backgroundColor: string,
sacScaleColor: string | undefined,
mtbScaleColor: string | undefined,
size: number = 16,
lineWidth: number = 4
) {
let canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
let ctx = canvas.getContext('2d');
if (ctx) {
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, size, size);
ctx.lineWidth = lineWidth;
const halfSize = size / 2;
const halfLineWidth = lineWidth / 2;
if (sacScaleColor) {
ctx.strokeStyle = sacScaleColor;
ctx.beginPath();
ctx.moveTo(halfSize - halfLineWidth, -halfLineWidth);
ctx.lineTo(size + halfLineWidth, halfSize + halfLineWidth);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-halfLineWidth, halfSize - halfLineWidth);
ctx.lineTo(halfSize + halfLineWidth, size + halfLineWidth);
ctx.stroke();
}
if (mtbScaleColor) {
ctx.strokeStyle = mtbScaleColor;
ctx.beginPath();
ctx.moveTo(halfSize - halfLineWidth, size + halfLineWidth);
ctx.lineTo(size + halfLineWidth, halfSize - halfLineWidth);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-halfLineWidth, halfSize + halfLineWidth);
ctx.lineTo(halfSize + halfLineWidth, -halfLineWidth);
ctx.stroke();
}
}
return ctx?.createPattern(canvas, 'repeat') || backgroundColor;
}
const patterns: Record<string, string | CanvasPattern> = {};
export function getHighwayColor(
highway: string,
sacScale: string | undefined,
mtbScale: string | undefined
) {
let backgroundColor = highwayColors[highway] ? highwayColors[highway] : highwayColors.missing;
let sacScaleColor = sacScale ? sacScaleColors[sacScale] : undefined;
let mtbScaleColor = mtbScale ? mtbScaleColors[mtbScale] : undefined;
if (sacScale || mtbScale) {
let patternId = `${backgroundColor}-${[sacScale, mtbScale].filter((x) => x).join('-')}`;
if (!patterns[patternId]) {
patterns[patternId] = createPattern(backgroundColor, sacScaleColor, mtbScaleColor);
}
return patterns[patternId];
}
return backgroundColor;
}
const maxSlope = 20;
export function getSlopeColor(slope: number): string {
if (slope > maxSlope) {
slope = maxSlope;
} else if (slope < -maxSlope) {
slope = -maxSlope;
}
let v = slope / maxSlope;
v = 1 / (1 + Math.exp(-6 * v));
v = v - 0.5;
let hue = ((0.5 - v) * 120).toString(10);
let lightness = 90 - Math.abs(v) * 70;
return `hsl(${hue},70%,${lightness}%)`;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +0,0 @@
export const surfaceColors: { [key: string]: string } = {
'missing': '#d1d1d1',
'paved': '#8c8c8c',
'unpaved': '#6b443a',
'asphalt': '#8c8c8c',
'concrete': '#8c8c8c',
'chipseal': '#8c8c8c',
'cobblestone': '#ffd991',
'unhewn_cobblestone': '#ffd991',
'paving_stones': '#8c8c8c',
'stepping_stones': '#c7b2db',
'sett': '#ffd991',
'metal': '#8c8c8c',
'wood': '#6b443a',
'compacted': '#ffffa8',
'fine_gravel': '#ffffa8',
'gravel': '#ffffa8',
'pebblestone': '#ffffa8',
'rock': '#ffd991',
'dirt': '#ffffa8',
'ground': '#6b443a',
'earth': '#6b443a',
'snow': '#bdfffc',
'ice': '#bdfffc',
'salt': '#b6c0f2',
'mud': '#6b443a',
'sand': '#ffffc4',
'woodchips': '#6b443a',
'grass': '#61b55c',
'grass_paver': '#61b55c'
}

View File

@@ -1,6 +1,67 @@
import { Landmark, Icon, Shell, Bike, Building, Tent, Car, Wrench, ShoppingBasket, Droplet, DoorOpen, Trees, Fuel, Home, Info, TreeDeciduous, CircleParking, Cross, Utensils, Construction, BrickWall, ShowerHead, Mountain, Phone, TrainFront, Bed, Binoculars, TriangleAlert, Anchor } from "lucide-svelte";
import { Landmark as LandmarkSvg, Shell as ShellSvg, Bike as BikeSvg, Building as BuildingSvg, Tent as TentSvg, Car as CarSvg, Wrench as WrenchSvg, ShoppingBasket as ShoppingBasketSvg, Droplet as DropletSvg, DoorOpen as DoorOpenSvg, Trees as TreesSvg, Fuel as FuelSvg, Home as HomeSvg, Info as InfoSvg, TreeDeciduous as TreeDeciduousSvg, CircleParking as CircleParkingSvg, Cross as CrossSvg, Utensils as UtensilsSvg, Construction as ConstructionSvg, BrickWall as BrickWallSvg, ShowerHead as ShowerHeadSvg, Mountain as MountainSvg, Phone as PhoneSvg, TrainFront as TrainFrontSvg, Bed as BedSvg, Binoculars as BinocularsSvg, TriangleAlert as TriangleAlertSvg, Anchor as AnchorSvg } from "lucide-static";
import type { ComponentType } from "svelte";
import {
Landmark,
Icon,
Shell,
Bike,
Building,
Tent,
Car,
Wrench,
ShoppingBasket,
Droplet,
DoorOpen,
Trees,
Fuel,
Home,
Info,
TreeDeciduous,
CircleParking,
Cross,
Utensils,
Construction,
BrickWall,
ShowerHead,
Mountain,
Phone,
TrainFront,
Bed,
Binoculars,
TriangleAlert,
Anchor,
Toilet,
} from 'lucide-svelte';
import {
Landmark as LandmarkSvg,
Shell as ShellSvg,
Bike as BikeSvg,
Building as BuildingSvg,
Tent as TentSvg,
Car as CarSvg,
Wrench as WrenchSvg,
ShoppingBasket as ShoppingBasketSvg,
Droplet as DropletSvg,
DoorOpen as DoorOpenSvg,
Trees as TreesSvg,
Fuel as FuelSvg,
Home as HomeSvg,
Info as InfoSvg,
TreeDeciduous as TreeDeciduousSvg,
CircleParking as CircleParkingSvg,
Cross as CrossSvg,
Utensils as UtensilsSvg,
Construction as ConstructionSvg,
BrickWall as BrickWallSvg,
ShowerHead as ShowerHeadSvg,
Mountain as MountainSvg,
Phone as PhoneSvg,
TrainFront as TrainFrontSvg,
Bed as BedSvg,
Binoculars as BinocularsSvg,
TriangleAlert as TriangleAlertSvg,
Anchor as AnchorSvg,
Toilet as ToiletSvg,
} from 'lucide-static';
import type { ComponentType } from 'svelte';
export type Symbol = {
value: string;
@@ -20,16 +81,28 @@ export const symbols: { [key: string]: Symbol } = {
campground: { value: 'Campground', icon: Tent, iconSvg: TentSvg },
car: { value: 'Car', icon: Car, iconSvg: CarSvg },
car_repair: { value: 'Car Repair', icon: Wrench, iconSvg: WrenchSvg },
convenience_store: { value: 'Convenience Store', icon: ShoppingBasket, iconSvg: ShoppingBasketSvg },
convenience_store: {
value: 'Convenience Store',
icon: ShoppingBasket,
iconSvg: ShoppingBasketSvg,
},
crossing: { value: 'Crossing' },
department_store: { value: 'Department Store', icon: ShoppingBasket, iconSvg: ShoppingBasketSvg },
department_store: {
value: 'Department Store',
icon: ShoppingBasket,
iconSvg: ShoppingBasketSvg,
},
drinking_water: { value: 'Drinking Water', icon: Droplet, iconSvg: DropletSvg },
exit: { value: 'Exit', icon: DoorOpen, iconSvg: DoorOpenSvg },
lodge: { value: 'Lodge', icon: Home, iconSvg: HomeSvg },
lodging: { value: 'Lodging', icon: Bed, iconSvg: BedSvg },
forest: { value: 'Forest', icon: Trees, iconSvg: TreesSvg },
gas_station: { value: 'Gas Station', icon: Fuel, iconSvg: FuelSvg },
ground_transportation: { value: 'Ground Transportation', icon: TrainFront, iconSvg: TrainFrontSvg },
ground_transportation: {
value: 'Ground Transportation',
icon: TrainFront,
iconSvg: TrainFrontSvg,
},
hotel: { value: 'Hotel', icon: Bed, iconSvg: BedSvg },
house: { value: 'House', icon: Home, iconSvg: HomeSvg },
information: { value: 'Information', icon: Info, iconSvg: InfoSvg },
@@ -39,7 +112,7 @@ export const symbols: { [key: string]: Symbol } = {
picnic_area: { value: 'Picnic Area', icon: Utensils, iconSvg: UtensilsSvg },
restaurant: { value: 'Restaurant', icon: Utensils, iconSvg: UtensilsSvg },
restricted_area: { value: 'Restricted Area', icon: Construction, iconSvg: ConstructionSvg },
restroom: { value: 'Restroom' },
restroom: { value: 'Restroom', icon: Toilet, iconSvg: ToiletSvg },
road: { value: 'Road', icon: BrickWall, iconSvg: BrickWallSvg },
scenic_area: { value: 'Scenic Area', icon: Binoculars, iconSvg: BinocularsSvg },
shelter: { value: 'Shelter', icon: Tent, iconSvg: TentSvg },
@@ -55,6 +128,6 @@ export function getSymbolKey(value: string | undefined): string | undefined {
if (value === undefined) {
return undefined;
} else {
return Object.keys(symbols).find(key => symbols[key].value === value);
return Object.keys(symbols).find((key) => symbols[key].value === value);
}
}

View File

@@ -13,14 +13,14 @@
indexName: 'gpx',
container: '#docsearch',
searchParameters: {
facetFilters: ['lang:' + ($locale ?? 'en')]
facetFilters: ['lang:' + ($locale ?? 'en')],
},
placeholder: $_('docs.search.search'),
disableUserPersonalization: true,
translations: {
button: {
buttonText: $_('docs.search.search'),
buttonAriaLabel: $_('docs.search.search')
buttonAriaLabel: $_('docs.search.search'),
},
modal: {
searchBox: {
@@ -28,19 +28,19 @@
resetButtonAriaLabel: $_('docs.search.clear'),
cancelButtonText: $_('docs.search.cancel'),
cancelButtonAriaLabel: $_('docs.search.cancel'),
searchInputLabel: $_('docs.search.search')
searchInputLabel: $_('docs.search.search'),
},
footer: {
selectText: $_('docs.search.to_select'),
navigateText: $_('docs.search.to_navigate'),
closeText: $_('docs.search.to_close')
closeText: $_('docs.search.to_close'),
},
noResultsScreen: {
noResultsText: $_('docs.search.no_results'),
suggestedQueryText: $_('docs.search.no_results_suggestion')
}
}
}
suggestedQueryText: $_('docs.search.no_results_suggestion'),
},
},
},
});
}

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
import type { Builder } from 'bits-ui';
export let variant:
| 'default'
@@ -12,11 +13,12 @@
| undefined = 'default';
export let label: string;
export let side: 'top' | 'right' | 'bottom' | 'left' = 'top';
export let builders: Builder[] = [];
</script>
<Tooltip.Root>
<Tooltip.Trigger asChild let:builder>
<Button builders={[builder]} {variant} {...$$restProps}>
<Button builders={[...builders, builder]} {variant} {...$$restProps} on:click>
<slot />
</Button>
</Tooltip.Trigger>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import { map } from '$lib/stores';
import { trackpointPopup } from '$lib/components/gpx-layer/GPXLayerPopup';
import { TrackPoint } from 'gpx';
$: if ($map) {
$map.on('contextmenu', (e) => {
trackpointPopup?.setItem({
item: new TrackPoint({
attributes: {
lat: e.lngLat.lat,
lon: e.lngLat.lng,
},
}),
});
});
}
</script>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import ButtonWithTooltip from '$lib/components/ButtonWithTooltip.svelte';
import * as Popover from '$lib/components/ui/popover';
import * as ToggleGroup from '$lib/components/ui/toggle-group';
import Tooltip from '$lib/components/Tooltip.svelte';
import Chart from 'chart.js/auto';
import mapboxgl from 'mapbox-gl';
import { map } from '$lib/stores';
@@ -12,12 +13,15 @@
Orbit,
SquareActivity,
Thermometer,
Zap
Zap,
Circle,
Check,
ChartNoAxesColumn,
Construction,
} from 'lucide-svelte';
import { surfaceColors } from '$lib/assets/surfaces';
import { _, locale } from 'svelte-i18n';
import { getSlopeColor, getSurfaceColor, getHighwayColor } from '$lib/assets/colors';
import { _ } from 'svelte-i18n';
import {
getCadenceUnits,
getCadenceWithUnits,
getConvertedDistance,
getConvertedElevation,
@@ -26,45 +30,26 @@
getDistanceUnits,
getDistanceWithUnits,
getElevationWithUnits,
getHeartRateUnits,
getHeartRateWithUnits,
getPowerUnits,
getPowerWithUnits,
getTemperatureUnits,
getTemperatureWithUnits,
getVelocityUnits,
getVelocityWithUnits,
secondsToHHMMSS
} from '$lib/units';
import type { Writable } from 'svelte/store';
import { DateFormatter } from '@internationalized/date';
import type { GPXStatistics } from 'gpx';
import { settings } from '$lib/db';
import { mode } from 'mode-watcher';
import { df } from '$lib/utils';
export let gpxStatistics: Writable<GPXStatistics>;
export let slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
export let panelSize: number;
export let additionalDatasets: string[];
export let elevationFill: 'slope' | 'surface' | undefined;
export let elevationFill: 'slope' | 'surface' | 'highway' | undefined;
export let showControls: boolean = true;
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
let df: DateFormatter;
$: if ($locale) {
df = new DateFormatter($locale, {
dateStyle: 'medium',
timeStyle: 'medium'
});
}
let canvas: HTMLCanvasElement;
let showAdditionalScales = true;
let updateShowAdditionalScales = () => {
showAdditionalScales = canvas.width / window.devicePixelRatio >= 600;
};
let overlay: HTMLCanvasElement;
let chart: Chart;
@@ -83,42 +68,41 @@
x: {
type: 'linear',
ticks: {
callback: function (value: number, index: number, ticks: { value: number }[]) {
if (index === ticks.length - 1) {
return `${value.toFixed(1).replace(/\.0+$/, '')}`;
}
callback: function (value: number) {
return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
}
}
},
align: 'inner',
maxRotation: 0,
},
},
y: {
type: 'linear',
ticks: {
callback: function (value: number) {
return getElevationWithUnits(value, false);
}
}
}
},
},
},
},
datasets: {
line: {
pointRadius: 0,
tension: 0.4,
borderWidth: 2,
cubicInterpolationMode: 'monotone'
}
cubicInterpolationMode: 'monotone',
},
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
intersect: false,
},
plugins: {
legend: {
display: false
display: false,
},
decimation: {
enabled: true
enabled: true,
},
tooltip: {
enabled: () => !dragging && !panning,
@@ -157,13 +141,20 @@
let slope = {
at: point.slope.at.toFixed(1),
segment: point.slope.segment.toFixed(1),
length: getDistanceWithUnits(point.slope.length)
length: getDistanceWithUnits(point.slope.length),
};
let surface = point.surface ? point.surface : 'unknown';
let surface = point.extensions.surface
? point.extensions.surface
: 'unknown';
let highway = point.extensions.highway
? point.extensions.highway
: 'unknown';
let sacScale = point.extensions.sac_scale;
let mtbScale = point.extensions.mtb_scale;
let labels = [
` ${$_('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`,
` ${$_('quantities.slope')}: ${slope.at} %${elevationFill === 'slope' ? ` (${slope.length} @${slope.segment} %)` : ''}`
` ${$_('quantities.slope')}: ${slope.at} %${elevationFill === 'slope' ? ` (${slope.length} @${slope.segment} %)` : ''}`,
];
if (elevationFill === 'surface') {
@@ -172,13 +163,26 @@
);
}
if (elevationFill === 'highway') {
labels.push(
` ${$_('quantities.highway')}: ${$_(`toolbar.routing.highway.${highway}`)}${
sacScale
? ` (${$_(`toolbar.routing.sac_scale.${sacScale}`)})`
: ''
}`
);
if (mtbScale) {
labels.push(` ${$_('toolbar.routing.mtb_scale')}: ${mtbScale}`);
}
}
if (point.time) {
labels.push(` ${$_('quantities.time')}: ${df.format(point.time)}`);
}
return labels;
}
}
},
},
},
zoom: {
pan: {
@@ -192,18 +196,19 @@
},
onPanComplete: function () {
panning = false;
}
},
},
zoom: {
wheel: {
enabled: true
enabled: true,
},
mode: 'x',
onZoomStart: function ({ chart, event }: { chart: Chart; event: any }) {
if (
event.deltaY < 0 &&
Math.abs(
chart.getInitialScaleBounds().x.max / chart.options.plugins.zoom.limits.x.minRange -
chart.getInitialScaleBounds().x.max /
chart.options.plugins.zoom.limits.x.minRange -
chart.getZoomLevel()
) < 0.01
) {
@@ -212,86 +217,35 @@
}
$slicedGPXStatistics = undefined;
}
},
},
limits: {
x: {
min: 'original',
max: 'original',
minRange: 1
}
}
}
minRange: 1,
},
},
},
},
stacked: false,
onResize: function () {
updateOverlay();
updateShowAdditionalScales();
}
},
};
let datasets: {
[key: string]: {
id: string;
getLabel: () => string;
getUnits: () => string;
};
} = {
speed: {
id: 'speed',
getLabel: () => ($velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')),
getUnits: () => getVelocityUnits()
},
hr: {
id: 'hr',
getLabel: () => $_('quantities.heartrate'),
getUnits: () => getHeartRateUnits()
},
cad: {
id: 'cad',
getLabel: () => $_('quantities.cadence'),
getUnits: () => getCadenceUnits()
},
atemp: {
id: 'atemp',
getLabel: () => $_('quantities.temperature'),
getUnits: () => getTemperatureUnits()
},
power: {
id: 'power',
getLabel: () => $_('quantities.power'),
getUnits: () => getPowerUnits()
}
};
for (let [id, dataset] of Object.entries(datasets)) {
let datasets: string[] = ['speed', 'hr', 'cad', 'atemp', 'power'];
datasets.forEach((id) => {
options.scales[`y${id}`] = {
type: 'linear',
position: 'right',
title: {
display: true,
text: dataset.getLabel() + ' (' + dataset.getUnits() + ')',
padding: {
top: 6,
bottom: 0
}
},
grid: {
display: false
display: false,
},
reverse: () => id === 'speed' && $velocityUnits === 'pace',
display: false
};
}
options.scales.yspeed['ticks'] = {
callback: function (value: number) {
if ($velocityUnits === 'speed') {
return value;
} else {
return secondsToHHMMSS(value);
}
}
display: false,
};
});
onMount(async () => {
Chart.register((await import('chartjs-plugin-zoom')).default); // dynamic import to avoid SSR and 'window is not defined' error
@@ -299,7 +253,7 @@
chart = new Chart(canvas, {
type: 'line',
data: {
datasets: []
datasets: [],
},
options,
plugins: [
@@ -312,20 +266,18 @@
marker.remove();
}
}
}
}
]
},
},
],
});
// Map marker to show on hover
let element = document.createElement('div');
element.className = 'h-4 w-4 rounded-full bg-cyan-500 border-2 border-white';
marker = new mapboxgl.Marker({
element
element,
});
updateShowAdditionalScales();
let startIndex = 0;
let endIndex = 0;
function getIndex(evt) {
@@ -333,7 +285,7 @@
evt,
'x',
{
intersect: false
intersect: false,
},
true
);
@@ -376,9 +328,12 @@
startIndex = endIndex;
} else if (startIndex !== endIndex) {
$slicedGPXStatistics = [
$gpxStatistics.slice(Math.min(startIndex, endIndex), Math.max(startIndex, endIndex)),
$gpxStatistics.slice(
Math.min(startIndex, endIndex),
Math.max(startIndex, endIndex)
),
Math.min(startIndex, endIndex),
Math.max(startIndex, endIndex),
];
}
}
@@ -412,126 +367,111 @@
slope: {
at: data.local.slope.at[index],
segment: data.local.slope.segment[index],
length: data.local.slope.length[index]
length: data.local.slope.length[index],
},
surface: point.getSurface(),
extensions: point.getExtensions(),
coordinates: point.getCoordinates(),
index: index
index: index,
};
}),
normalized: true,
fill: 'start',
order: 1
order: 1,
};
chart.data.datasets[1] = {
label: datasets.speed.getLabel(),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: getConvertedVelocity(data.local.speed[index]),
index: index
index: index,
};
}),
normalized: true,
yAxisID: `y${datasets.speed.id}`,
hidden: true
yAxisID: 'yspeed',
hidden: true,
};
chart.data.datasets[2] = {
label: datasets.hr.getLabel(),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.getHeartRate(),
index: index
index: index,
};
}),
normalized: true,
yAxisID: `y${datasets.hr.id}`,
hidden: true
yAxisID: 'yhr',
hidden: true,
};
chart.data.datasets[3] = {
label: datasets.cad.getLabel(),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.getCadence(),
index: index
index: index,
};
}),
normalized: true,
yAxisID: `y${datasets.cad.id}`,
hidden: true
yAxisID: 'ycad',
hidden: true,
};
chart.data.datasets[4] = {
label: datasets.atemp.getLabel(),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: getConvertedTemperature(point.getTemperature()),
index: index
index: index,
};
}),
normalized: true,
yAxisID: `y${datasets.atemp.id}`,
hidden: true
yAxisID: 'yatemp',
hidden: true,
};
chart.data.datasets[5] = {
label: datasets.power.getLabel(),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.getPower(),
index: index
index: index,
};
}),
normalized: true,
yAxisID: `y${datasets.power.id}`,
hidden: true
yAxisID: 'ypower',
hidden: true,
};
chart.options.scales.x['min'] = 0;
chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total);
// update units
for (let [id, dataset] of Object.entries(datasets)) {
chart.options.scales[`y${id}`].title.text =
dataset.getLabel() + ' (' + dataset.getUnits() + ')';
}
chart.update();
}
let maxSlope = 20;
function slopeFillCallback(context) {
let slope = context.p0.raw.slope.segment;
if (slope > maxSlope) {
slope = maxSlope;
} else if (slope < -maxSlope) {
slope = -maxSlope;
}
let v = slope / maxSlope;
v = 1 / (1 + Math.exp(-6 * v));
v = v - 0.5;
let hue = ((0.5 - v) * 120).toString(10);
let lightness = 90 - Math.abs(v) * 70;
return ['hsl(', hue, ',70%,', lightness, '%)'].join('');
return getSlopeColor(context.p0.raw.slope.segment);
}
function surfaceFillCallback(context) {
let surface = context.p0.raw.surface;
return surfaceColors[surface] ? surfaceColors[surface] : surfaceColors.missing;
return getSurfaceColor(context.p0.raw.extensions.surface);
}
function highwayFillCallback(context) {
return getHighwayColor(
context.p0.raw.extensions.highway,
context.p0.raw.extensions.sac_scale,
context.p0.raw.extensions.mtb_scale
);
}
$: if (chart) {
if (elevationFill === 'slope') {
chart.data.datasets[0]['segment'] = {
backgroundColor: slopeFillCallback
backgroundColor: slopeFillCallback,
};
} else if (elevationFill === 'surface') {
chart.data.datasets[0]['segment'] = {
backgroundColor: surfaceFillCallback
backgroundColor: surfaceFillCallback,
};
} else if (elevationFill === 'highway') {
chart.data.datasets[0]['segment'] = {
backgroundColor: highwayFillCallback,
};
} else {
chart.data.datasets[0]['segment'] = {};
@@ -552,12 +492,6 @@
chart.data.datasets[4].hidden = !includeTemperature;
chart.data.datasets[5].hidden = !includePower;
}
chart.options.scales[`y${datasets.speed.id}`].display = includeSpeed && showAdditionalScales;
chart.options.scales[`y${datasets.hr.id}`].display = includeHeartRate && showAdditionalScales;
chart.options.scales[`y${datasets.cad.id}`].display = includeCadence && showAdditionalScales;
chart.options.scales[`y${datasets.atemp.id}`].display =
includeTemperature && showAdditionalScales;
chart.options.scales[`y${datasets.power.id}`].display = includePower && showAdditionalScales;
chart.update();
}
@@ -568,6 +502,8 @@
overlay.width = canvas.width / window.devicePixelRatio;
overlay.height = canvas.height / window.devicePixelRatio;
overlay.style.width = `${overlay.width}px`;
overlay.style.height = `${overlay.height}px`;
if ($slicedGPXStatistics) {
let startIndex = $slicedGPXStatistics[1];
@@ -591,7 +527,7 @@
startPixel,
chart.chartArea.top,
endPixel - startPixel,
chart.chartArea.bottom - chart.chartArea.top
chart.chartArea.height
);
}
} else if (overlay) {
@@ -611,75 +547,141 @@
});
</script>
<div class="h-full grow min-w-0 flex flex-row gap-4 items-center {$$props.class ?? ''}">
<div class="grow h-full min-w-0 relative">
<div class="h-full grow min-w-0 relative py-2">
<canvas bind:this={overlay} class="w-full h-full absolute pointer-events-none"></canvas>
<canvas bind:this={canvas} class="w-full h-full"></canvas>
</div>
<canvas bind:this={canvas} class="w-full h-full absolute"></canvas>
{#if showControls}
<div class="h-full flex flex-col justify-center" style="width: {panelSize > 158 ? 22 : 42}px">
<div class="absolute bottom-10 right-1.5">
<Popover.Root>
<Popover.Trigger asChild let:builder>
<ButtonWithTooltip
label={$_('chart.settings')}
builders={[builder]}
variant="outline"
class="w-7 h-7 p-0 flex justify-center opacity-70 hover:opacity-100 transition-opacity duration-300 hover:bg-background"
>
<ChartNoAxesColumn size="18" />
</ButtonWithTooltip>
</Popover.Trigger>
<Popover.Content
class="w-fit p-0 flex flex-col divide-y"
side="top"
sideOffset={-32}
>
<ToggleGroup.Root
class="{panelSize > 158
? 'flex-col'
: 'flex-row'} flex-wrap gap-0 min-h-0 content-center border rounded-t-md"
class="flex flex-col items-start gap-0 p-1"
type="single"
bind:value={elevationFill}
>
<ToggleGroup.Item class="p-0 w-5 h-5" value="slope" aria-label={$_('chart.show_slope')}>
<Tooltip side="left" label={$_('chart.show_slope')}>
<TriangleRight size="15" />
</Tooltip>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="slope"
>
<div class="w-6 flex justify-center items-center">
{#if elevationFill === 'slope'}
<Circle class="h-1.5 w-1.5 fill-current text-current" />
{/if}
</div>
<TriangleRight size="15" class="mr-1" />
{$_('quantities.slope')}
</ToggleGroup.Item>
<ToggleGroup.Item class="p-0 w-5 h-5" value="surface" aria-label={$_('chart.show_surface')}>
<Tooltip side="left" label={$_('chart.show_surface')}>
<BrickWall size="15" />
</Tooltip>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="surface"
variant="outline"
>
<div class="w-6 flex justify-center items-center">
{#if elevationFill === 'surface'}
<Circle class="h-1.5 w-1.5 fill-current text-current" />
{/if}
</div>
<BrickWall size="15" class="mr-1" />
{$_('quantities.surface')}
</ToggleGroup.Item>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="highway"
variant="outline"
>
<div class="w-6 flex justify-center items-center">
{#if elevationFill === 'highway'}
<Circle class="h-1.5 w-1.5 fill-current text-current" />
{/if}
</div>
<Construction size="15" class="mr-1" />
{$_('quantities.highway')}
</ToggleGroup.Item>
</ToggleGroup.Root>
<ToggleGroup.Root
class="{panelSize > 158
? 'flex-col'
: 'flex-row'} flex-wrap gap-0 min-h-0 content-center border rounded-b-md -mt-[1px]"
class="flex flex-col items-start gap-0 p-1"
type="multiple"
bind:value={additionalDatasets}
>
<ToggleGroup.Item
class="p-0 w-5 h-5"
class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="speed"
aria-label={$velocityUnits === 'speed' ? $_('chart.show_speed') : $_('chart.show_pace')}
>
<Tooltip
side="left"
label={$velocityUnits === 'speed' ? $_('chart.show_speed') : $_('chart.show_pace')}
>
<Zap size="15" />
</Tooltip>
</ToggleGroup.Item>
<ToggleGroup.Item class="p-0 w-5 h-5" value="hr" aria-label={$_('chart.show_heartrate')}>
<Tooltip side="left" label={$_('chart.show_heartrate')}>
<HeartPulse size="15" />
</Tooltip>
</ToggleGroup.Item>
<ToggleGroup.Item class="p-0 w-5 h-5" value="cad" aria-label={$_('chart.show_cadence')}>
<Tooltip side="left" label={$_('chart.show_cadence')}>
<Orbit size="15" />
</Tooltip>
<div class="w-6 flex justify-center items-center">
{#if additionalDatasets.includes('speed')}
<Check size="14" />
{/if}
</div>
<Zap size="15" class="mr-1" />
{$velocityUnits === 'speed'
? $_('quantities.speed')
: $_('quantities.pace')}
</ToggleGroup.Item>
<ToggleGroup.Item
class="p-0 w-5 h-5"
value="atemp"
aria-label={$_('chart.show_temperature')}
class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="hr"
>
<Tooltip side="left" label={$_('chart.show_temperature')}>
<Thermometer size="15" />
</Tooltip>
<div class="w-6 flex justify-center items-center">
{#if additionalDatasets.includes('hr')}
<Check size="14" />
{/if}
</div>
<HeartPulse size="15" class="mr-1" />
{$_('quantities.heartrate')}
</ToggleGroup.Item>
<ToggleGroup.Item class="p-0 w-5 h-5" value="power" aria-label={$_('chart.show_power')}>
<Tooltip side="left" label={$_('chart.show_power')}>
<SquareActivity size="15" />
</Tooltip>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="cad"
>
<div class="w-6 flex justify-center items-center">
{#if additionalDatasets.includes('cad')}
<Check size="14" />
{/if}
</div>
<Orbit size="15" class="mr-1" />
{$_('quantities.cadence')}
</ToggleGroup.Item>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="atemp"
>
<div class="w-6 flex justify-center items-center">
{#if additionalDatasets.includes('atemp')}
<Check size="14" />
{/if}
</div>
<Thermometer size="15" class="mr-1" />
{$_('quantities.temperature')}
</ToggleGroup.Item>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="power"
>
<div class="w-6 flex justify-center items-center">
{#if additionalDatasets.includes('power')}
<Check size="14" />
{/if}
</div>
<SquareActivity size="15" class="mr-1" />
{$_('quantities.power')}
</ToggleGroup.Item>
</ToggleGroup.Root>
</Popover.Content>
</Popover.Root>
</div>
{/if}
</div>

View File

@@ -10,17 +10,17 @@
exportSelectedFiles,
ExportState,
exportState,
gpxStatistics
gpxStatistics,
} from '$lib/stores';
import { fileObservers } from '$lib/db';
import {
Download,
Zap,
BrickWall,
Earth,
HeartPulse,
Orbit,
Thermometer,
SquareActivity
SquareActivity,
} from 'lucide-svelte';
import { _ } from 'svelte-i18n';
import { selection } from './file-list/Selection';
@@ -31,19 +31,19 @@
let open = false;
let exportOptions: Record<string, boolean> = {
time: true,
surface: true,
hr: true,
cad: true,
atemp: true,
power: true
power: true,
extensions: true,
};
let hide: Record<string, boolean> = {
time: false,
surface: false,
hr: false,
cad: false,
atemp: false,
power: false
power: false,
extensions: false,
};
$: if ($exportState !== ExportState.NONE) {
@@ -63,11 +63,11 @@
}
hide.time = statistics.global.time.total === 0;
hide.surface = !Object.keys(statistics.global.surface).some((key) => key !== 'unknown');
hide.hr = statistics.global.hr.count === 0;
hide.cad = statistics.global.cad.count === 0;
hide.atemp = statistics.global.atemp.count === 0;
hide.power = statistics.global.power.count === 0;
hide.extensions = Object.keys(statistics.global.extensions).length === 0;
}
$: exclude = Object.keys(exportOptions).filter((key) => !exportOptions[key]);
@@ -121,7 +121,9 @@
</Button>
</div>
<div
class="w-full max-w-xl flex flex-col items-center gap-2 {Object.values(hide).some((v) => !v)
class="w-full max-w-xl flex flex-col items-center gap-2 {Object.values(hide).some(
(v) => !v
)
? ''
: 'hidden'}"
>
@@ -144,11 +146,13 @@
{$_('quantities.time')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.surface ? 'hidden' : ''}">
<Checkbox id="export-surface" bind:checked={exportOptions.surface} />
<Label for="export-surface" class="flex flex-row items-center gap-1">
<BrickWall size="16" />
{$_('quantities.surface')}
<div
class="flex flex-row items-center gap-1.5 {hide.extensions ? 'hidden' : ''}"
>
<Checkbox id="export-extensions" bind:checked={exportOptions.extensions} />
<Label for="export-extensions" class="flex flex-row items-center gap-1">
<Earth size="16" />
{$_('quantities.osm_extensions')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.hr ? 'hidden' : ''}">

View File

@@ -28,7 +28,7 @@
<Card.Root
class="h-full {orientation === 'vertical'
? 'min-w-44 sm:min-w-52 text-sm sm:text-base'
? 'min-w-40 sm:min-w-44 text-sm sm:text-base'
: 'w-full'} border-none shadow-none"
>
<Card.Content
@@ -38,28 +38,32 @@
>
<Tooltip label={$_('quantities.distance')}>
<span class="flex flex-row items-center">
<Ruler size="18" class="mr-1" />
<Ruler size="16" class="mr-1" />
<WithUnits value={statistics.global.distance.total} type="distance" />
</span>
</Tooltip>
<Tooltip label={$_('quantities.elevation_gain_loss')}>
<span class="flex flex-row items-center">
<MoveUpRight size="18" class="mr-1" />
<MoveUpRight size="16" class="mr-1" />
<WithUnits value={statistics.global.elevation.gain} type="elevation" />
<MoveDownRight size="18" class="mx-1" />
<MoveDownRight size="16" class="mx-1" />
<WithUnits value={statistics.global.elevation.loss} type="elevation" />
</span>
</Tooltip>
{#if panelSize > 120 || orientation === 'horizontal'}
<Tooltip
class={orientation === 'horizontal' ? 'hidden xs:block' : ''}
label="{$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_(
'quantities.moving'
)} / {$_('quantities.total')})"
label="{$velocityUnits === 'speed'
? $_('quantities.speed')
: $_('quantities.pace')} ({$_('quantities.moving')} / {$_('quantities.total')})"
>
<span class="flex flex-row items-center">
<Zap size="18" class="mr-1" />
<WithUnits value={statistics.global.speed.moving} type="speed" showUnits={false} />
<Zap size="16" class="mr-1" />
<WithUnits
value={statistics.global.speed.moving}
type="speed"
showUnits={false}
/>
<span class="mx-1">/</span>
<WithUnits value={statistics.global.speed.total} type="speed" />
</span>
@@ -68,10 +72,12 @@
{#if panelSize > 160 || orientation === 'horizontal'}
<Tooltip
class={orientation === 'horizontal' ? 'hidden md:block' : ''}
label="{$_('quantities.time')} ({$_('quantities.moving')} / {$_('quantities.total')})"
label="{$_('quantities.time')} ({$_('quantities.moving')} / {$_(
'quantities.total'
)})"
>
<span class="flex flex-row items-center">
<Timer size="18" class="mr-1" />
<Timer size="16" class="mr-1" />
<WithUnits value={statistics.global.time.moving} type="time" />
<span class="mx-1">/</span>
<WithUnits value={statistics.global.time.total} type="time" />

View File

@@ -8,13 +8,13 @@
let selected = {
value: '',
label: ''
label: '',
};
$: if ($locale) {
selected = {
value: $locale,
label: languages[$locale]
label: languages[$locale],
};
}
</script>

View File

@@ -22,16 +22,17 @@
mapboxgl.accessToken = accessToken;
let webgl2Supported = true;
let embeddedApp = false;
let fitBoundsOptions: mapboxgl.FitBoundsOptions = {
maxZoom: 15,
linear: true,
easing: () => 1
easing: () => 1,
};
const { distanceUnits, elevationProfile, verticalFileView, bottomPanelSize, rightPanelSize } =
const { distanceUnits, elevationProfile, treeFileView, bottomPanelSize, rightPanelSize } =
settings;
let scaleControl = new mapboxgl.ScaleControl({
unit: $distanceUnits
unit: $distanceUnits,
});
onMount(() => {
@@ -40,6 +41,10 @@
webgl2Supported = false;
return;
}
if (window.top !== window.self && !$page.route.id?.includes('embed')) {
embeddedApp = true;
return;
}
let language = $page.params.language;
if (language === 'zh') {
@@ -65,12 +70,12 @@
sources: {},
layers: [],
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
sprite: `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${PUBLIC_MAPBOX_TOKEN}`
}
sprite: `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${PUBLIC_MAPBOX_TOKEN}`,
},
},
{
id: 'basemap',
url: ''
url: '',
},
{
id: 'overlays',
@@ -78,17 +83,18 @@
data: {
version: 8,
sources: {},
layers: []
}
}
]
layers: [],
},
},
],
},
projection: 'globe',
zoom: 0,
hash: hash,
language,
attributionControl: false,
logoPosition: 'bottom-right',
boxZoom: false
boxZoom: false,
});
newMap.on('load', () => {
$map = newMap; // only set the store after the map has loaded
@@ -98,13 +104,13 @@
newMap.addControl(
new mapboxgl.AttributionControl({
compact: true
compact: true,
})
);
newMap.addControl(
new mapboxgl.NavigationControl({
visualizePitch: true
visualizePitch: true,
})
);
@@ -128,12 +134,12 @@
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [result.lon, result.lat]
coordinates: [result.lon, result.lat],
},
place_name: result.display_name
place_name: result.display_name,
};
});
})
}),
});
let onKeyDown = geocoder._onKeyDown;
geocoder._onKeyDown = (e: KeyboardEvent) => {
@@ -151,11 +157,11 @@
newMap.addControl(
new mapboxgl.GeolocateControl({
positionOptions: {
enableHighAccuracy: true
enableHighAccuracy: true,
},
fitBoundsOptions,
trackUserLocation: true,
showUserHeading: true
showUserHeading: true,
})
);
}
@@ -167,25 +173,25 @@
type: 'raster-dem',
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
tileSize: 512,
maxzoom: 14
maxzoom: 14,
});
if (newMap.getPitch() > 0) {
newMap.setTerrain({
source: 'mapbox-dem',
exaggeration: 1
exaggeration: 1,
});
}
newMap.setFog({
color: 'rgb(186, 210, 235)',
'high-color': 'rgb(36, 92, 223)',
'horizon-blend': 0.1,
'space-color': 'rgb(156, 240, 255)'
'space-color': 'rgb(156, 240, 255)',
});
newMap.on('pitch', () => {
if (newMap.getPitch() > 0) {
newMap.setTerrain({
source: 'mapbox-dem',
exaggeration: 1
exaggeration: 1,
});
} else {
newMap.setTerrain(null);
@@ -201,23 +207,30 @@
}
});
$: if (
$map &&
(!$verticalFileView || !$elevationProfile || $bottomPanelSize || $rightPanelSize)
) {
$: if ($map && (!$treeFileView || !$elevationProfile || $bottomPanelSize || $rightPanelSize)) {
$map.resize();
}
</script>
<div {...$$restProps}>
<div id="map" class="h-full {webgl2Supported ? '' : 'hidden'}"></div>
<div id="map" class="h-full {webgl2Supported && !embeddedApp ? '' : 'hidden'}"></div>
<div
class="flex flex-col items-center justify-center gap-3 h-full {webgl2Supported ? 'hidden' : ''}"
class="flex flex-col items-center justify-center gap-3 h-full {webgl2Supported &&
!embeddedApp
? 'hidden'
: ''} {embeddedApp ? 'z-30' : ''}"
>
{#if !webgl2Supported}
<p>{$_('webgl2_required')}</p>
<Button href="https://get.webgl.org/webgl2/" target="_blank">
{$_('enable_webgl2')}
</Button>
{:else if embeddedApp}
<p>The app cannot be embedded in an iframe.</p>
<Button href="https://gpx.studio/help/integration" target="_blank">
Learn how to create a map for your website
</Button>
{/if}
</div>
</div>
@@ -334,7 +347,7 @@
div :global(.mapboxgl-popup) {
@apply w-fit;
@apply z-20;
@apply z-50;
}
div :global(.mapboxgl-popup-content) {

View File

@@ -0,0 +1,25 @@
<svelte:options accessors />
<script lang="ts">
import { TrackPoint, Waypoint } from 'gpx';
import type { Writable } from 'svelte/store';
import WaypointPopup from '$lib/components/gpx-layer/WaypointPopup.svelte';
import TrackpointPopup from '$lib/components/gpx-layer/TrackpointPopup.svelte';
import OverpassPopup from '$lib/components/layer-control/OverpassPopup.svelte';
import type { PopupItem } from './MapPopup';
export let item: Writable<PopupItem | null>;
export let container: HTMLDivElement | null = null;
</script>
<div bind:this={container}>
{#if $item}
{#if $item.item instanceof Waypoint}
<WaypointPopup waypoint={$item} />
{:else if $item.item instanceof TrackPoint}
<TrackpointPopup trackpoint={$item} />
{:else}
<OverpassPopup poi={$item} />
{/if}
{/if}
</div>

View File

@@ -0,0 +1,82 @@
import { TrackPoint, Waypoint } from 'gpx';
import mapboxgl from 'mapbox-gl';
import { tick } from 'svelte';
import { get, writable, type Writable } from 'svelte/store';
import MapPopupComponent from './MapPopup.svelte';
export type PopupItem<T = Waypoint | TrackPoint | any> = {
item: T;
fileId?: string;
hide?: () => void;
};
export class MapPopup {
map: mapboxgl.Map;
popup: mapboxgl.Popup;
item: Writable<PopupItem | null> = writable(null);
maybeHideBinded = this.maybeHide.bind(this);
constructor(map: mapboxgl.Map, options?: mapboxgl.PopupOptions) {
this.map = map;
this.popup = new mapboxgl.Popup(options);
let component = new MapPopupComponent({
target: document.body,
props: {
item: this.item,
},
});
tick().then(() => this.popup.setDOMContent(component.container));
}
setItem(item: PopupItem | null) {
if (item) item.hide = () => this.hide();
this.item.set(item);
if (item === null) {
this.hide();
} else {
tick().then(() => this.show());
}
}
show() {
const i = get(this.item);
if (i === null) {
this.hide();
return;
}
this.popup.setLngLat(this.getCoordinates()).addTo(this.map);
this.map.on('mousemove', this.maybeHideBinded);
}
maybeHide(e: mapboxgl.MapMouseEvent) {
const i = get(this.item);
if (i === null) {
this.hide();
return;
}
if (this.map.project(this.getCoordinates()).dist(this.map.project(e.lngLat)) > 60) {
this.hide();
}
}
hide() {
this.popup.remove();
this.map.off('mousemove', this.maybeHideBinded);
}
remove() {
this.popup.remove();
}
getCoordinates() {
const i = get(this.item);
if (i === null) {
return new mapboxgl.LngLat(0, 0);
}
return i.item instanceof Waypoint || i.item instanceof TrackPoint
? i.item.getCoordinates()
: new mapboxgl.LngLat(i.item.lon, i.item.lat);
}
}

View File

@@ -22,7 +22,7 @@
Sun,
Moon,
Layers,
GalleryVertical,
ListTree,
Languages,
Settings,
Info,
@@ -42,7 +42,7 @@
FileX,
BookOpenText,
ChartArea,
Maximize
Maximize,
} from 'lucide-svelte';
import {
@@ -56,7 +56,7 @@
editStyle,
exportState,
ExportState,
centerMapOnSelection
centerMapOnSelection,
} from '$lib/stores';
import {
copied,
@@ -64,7 +64,7 @@
cutSelection,
pasteSelection,
selectAll,
selection
selection,
} from '$lib/components/file-list/Selection';
import { derived } from 'svelte/store';
import { canUndo, canRedo, dbUtils, fileObservers, settings } from '$lib/db';
@@ -83,7 +83,7 @@
velocityUnits,
temperatureUnits,
elevationProfile,
verticalFileView,
treeFileView,
currentBasemap,
previousBasemap,
currentOverlays,
@@ -91,7 +91,7 @@
distanceMarkers,
directionMarkers,
streetViewSource,
routing
routing,
} = settings;
let undoDisabled = derived(canUndo, ($canUndo) => !$canUndo);
@@ -128,7 +128,7 @@
<div
class="w-fit flex flex-row items-center justify-center p-1 bg-background rounded-b-md md:rounded-md pointer-events-auto shadow-md"
>
<a href="./" target="_blank" class="shrink-0">
<a href={getURLForLanguage($locale, '/')} target="_blank" class="shrink-0">
<Logo class="h-5 mt-0.5 mx-2 md:hidden" iconOnly={true} width="16" />
<Logo class="h-5 mt-0.5 mx-2 hidden md:block" width="96" />
</a>
@@ -151,18 +151,27 @@
<Shortcut key="O" ctrl={true} />
</Menubar.Item>
<Menubar.Separator />
<Menubar.Item on:click={dbUtils.duplicateSelection} disabled={$selection.size == 0}>
<Menubar.Item
on:click={dbUtils.duplicateSelection}
disabled={$selection.size == 0}
>
<Copy size="16" class="mr-1" />
{$_('menu.duplicate')}
<Shortcut key="D" ctrl={true} />
</Menubar.Item>
<Menubar.Separator />
<Menubar.Item on:click={dbUtils.deleteSelectedFiles} disabled={$selection.size == 0}>
<Menubar.Item
on:click={dbUtils.deleteSelectedFiles}
disabled={$selection.size == 0}
>
<FileX size="16" class="mr-1" />
{$_('menu.close')}
<Shortcut key="⌫" ctrl={true} />
</Menubar.Item>
<Menubar.Item on:click={dbUtils.deleteAllFiles} disabled={$fileObservers.size == 0}>
<Menubar.Item
on:click={dbUtils.deleteAllFiles}
disabled={$fileObservers.size == 0}
>
<FileX size="16" class="mr-1" />
{$_('menu.close_all')}
<Shortcut key="⌫" ctrl={true} shift={true} />
@@ -207,7 +216,11 @@
disabled={$selection.size !== 1 ||
!$selection
.getSelected()
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)}
.every(
(item) =>
item instanceof ListFileItem ||
item instanceof ListTrackItem
)}
on:click={() => ($editMetadata = true)}
>
<Info size="16" class="mr-1" />
@@ -218,7 +231,11 @@
disabled={$selection.size === 0 ||
!$selection
.getSelected()
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)}
.every(
(item) =>
item instanceof ListFileItem ||
item instanceof ListTrackItem
)}
on:click={() => ($editStyle = true)}
>
<PaintBucket size="16" class="mr-1" />
@@ -243,17 +260,20 @@
{/if}
<Shortcut key="H" ctrl={true} />
</Menubar.Item>
{#if $verticalFileView}
{#if $treeFileView}
{#if $selection.getSelected().some((item) => item instanceof ListFileItem)}
<Menubar.Separator />
<Menubar.Item
on:click={() => dbUtils.addNewTrack($selection.getSelected()[0].getFileId())}
on:click={() =>
dbUtils.addNewTrack($selection.getSelected()[0].getFileId())}
disabled={$selection.size !== 1}
>
<Plus size="16" class="mr-1" />
{$_('menu.new_track')}
</Menubar.Item>
{:else if $selection.getSelected().some((item) => item instanceof ListTrackItem)}
{:else if $selection
.getSelected()
.some((item) => item instanceof ListTrackItem)}
<Menubar.Separator />
<Menubar.Item
on:click={() => {
@@ -284,7 +304,7 @@
{$_('menu.center')}
<Shortcut key="⏎" ctrl={true} />
</Menubar.Item>
{#if $verticalFileView}
{#if $treeFileView}
<Menubar.Separator />
<Menubar.Item on:click={copySelection} disabled={$selection.size === 0}>
<ClipboardCopy size="16" class="mr-1" />
@@ -300,7 +320,9 @@
disabled={$copied === undefined ||
$copied.length === 0 ||
($selection.size > 0 &&
!allowedPastes[$copied[0].level].includes($selection.getSelected().pop()?.level))}
!allowedPastes[$copied[0].level].includes(
$selection.getSelected().pop()?.level
))}
on:click={pasteSelection}
>
<ClipboardPaste size="16" class="mr-1" />
@@ -309,7 +331,10 @@
</Menubar.Item>
{/if}
<Menubar.Separator />
<Menubar.Item on:click={dbUtils.deleteSelection} disabled={$selection.size == 0}>
<Menubar.Item
on:click={dbUtils.deleteSelection}
disabled={$selection.size == 0}
>
<Trash2 size="16" class="mr-1" />
{$_('menu.delete')}
<Shortcut key="⌫" ctrl={true} />
@@ -327,24 +352,32 @@
{$_('menu.elevation_profile')}
<Shortcut key="P" ctrl={true} />
</Menubar.CheckboxItem>
<Menubar.CheckboxItem bind:checked={$verticalFileView}>
<GalleryVertical size="16" class="mr-1" />
{$_('menu.vertical_file_view')}
<Menubar.CheckboxItem bind:checked={$treeFileView}>
<ListTree size="16" class="mr-1" />
{$_('menu.tree_file_view')}
<Shortcut key="L" ctrl={true} />
</Menubar.CheckboxItem>
<Menubar.Separator />
<Menubar.Item inset on:click={switchBasemaps}>
<Map size="16" class="mr-1" />{$_('menu.switch_basemap')}<Shortcut key="F1" />
<Map size="16" class="mr-1" />{$_('menu.switch_basemap')}<Shortcut
key="F1"
/>
</Menubar.Item>
<Menubar.Item inset on:click={toggleOverlays}>
<Layers2 size="16" class="mr-1" />{$_('menu.toggle_overlays')}<Shortcut key="F2" />
<Layers2 size="16" class="mr-1" />{$_('menu.toggle_overlays')}<Shortcut
key="F2"
/>
</Menubar.Item>
<Menubar.Separator />
<Menubar.CheckboxItem bind:checked={$distanceMarkers}>
<Coins size="16" class="mr-1" />{$_('menu.distance_markers')}<Shortcut key="F3" />
<Coins size="16" class="mr-1" />{$_('menu.distance_markers')}<Shortcut
key="F3"
/>
</Menubar.CheckboxItem>
<Menubar.CheckboxItem bind:checked={$directionMarkers}>
<Milestone size="16" class="mr-1" />{$_('menu.direction_markers')}<Shortcut key="F4" />
<Milestone size="16" class="mr-1" />{$_('menu.direction_markers')}<Shortcut
key="F4"
/>
</Menubar.CheckboxItem>
<Menubar.Separator />
<Menubar.Item inset on:click={toggle3D}>
@@ -368,9 +401,15 @@
</Menubar.SubTrigger>
<Menubar.SubContent>
<Menubar.RadioGroup bind:value={$distanceUnits}>
<Menubar.RadioItem value="metric">{$_('menu.metric')}</Menubar.RadioItem>
<Menubar.RadioItem value="imperial">{$_('menu.imperial')}</Menubar.RadioItem>
<Menubar.RadioItem value="nautical">{$_('menu.nautical')}</Menubar.RadioItem>
<Menubar.RadioItem value="metric"
>{$_('menu.metric')}</Menubar.RadioItem
>
<Menubar.RadioItem value="imperial"
>{$_('menu.imperial')}</Menubar.RadioItem
>
<Menubar.RadioItem value="nautical"
>{$_('menu.nautical')}</Menubar.RadioItem
>
</Menubar.RadioGroup>
</Menubar.SubContent>
</Menubar.Sub>
@@ -380,8 +419,12 @@
</Menubar.SubTrigger>
<Menubar.SubContent>
<Menubar.RadioGroup bind:value={$velocityUnits}>
<Menubar.RadioItem value="speed">{$_('quantities.speed')}</Menubar.RadioItem>
<Menubar.RadioItem value="pace">{$_('quantities.pace')}</Menubar.RadioItem>
<Menubar.RadioItem value="speed"
>{$_('quantities.speed')}</Menubar.RadioItem
>
<Menubar.RadioItem value="pace"
>{$_('quantities.pace')}</Menubar.RadioItem
>
</Menubar.RadioGroup>
</Menubar.SubContent>
</Menubar.Sub>
@@ -391,8 +434,12 @@
</Menubar.SubTrigger>
<Menubar.SubContent>
<Menubar.RadioGroup bind:value={$temperatureUnits}>
<Menubar.RadioItem value="celsius">{$_('menu.celsius')}</Menubar.RadioItem>
<Menubar.RadioItem value="fahrenheit">{$_('menu.fahrenheit')}</Menubar.RadioItem>
<Menubar.RadioItem value="celsius"
>{$_('menu.celsius')}</Menubar.RadioItem
>
<Menubar.RadioItem value="fahrenheit"
>{$_('menu.fahrenheit')}</Menubar.RadioItem
>
</Menubar.RadioGroup>
</Menubar.SubContent>
</Menubar.Sub>
@@ -428,8 +475,11 @@
setMode(value);
}}
>
<Menubar.RadioItem value="light">{$_('menu.light')}</Menubar.RadioItem>
<Menubar.RadioItem value="dark">{$_('menu.dark')}</Menubar.RadioItem>
<Menubar.RadioItem value="light"
>{$_('menu.light')}</Menubar.RadioItem
>
<Menubar.RadioItem value="dark">{$_('menu.dark')}</Menubar.RadioItem
>
</Menubar.RadioGroup>
</Menubar.SubContent>
</Menubar.Sub>
@@ -441,8 +491,12 @@
</Menubar.SubTrigger>
<Menubar.SubContent>
<Menubar.RadioGroup bind:value={$streetViewSource}>
<Menubar.RadioItem value="mapillary">{$_('menu.mapillary')}</Menubar.RadioItem>
<Menubar.RadioItem value="google">{$_('menu.google')}</Menubar.RadioItem>
<Menubar.RadioItem value="mapillary"
>{$_('menu.mapillary')}</Menubar.RadioItem
>
<Menubar.RadioItem value="google"
>{$_('menu.google')}</Menubar.RadioItem
>
</Menubar.RadioGroup>
</Menubar.SubContent>
</Menubar.Sub>
@@ -567,7 +621,7 @@
$elevationProfile = !$elevationProfile;
e.preventDefault();
} else if (e.key === 'l' && (e.metaKey || e.ctrlKey)) {
$verticalFileView = !$verticalFileView;
$treeFileView = !$treeFileView;
e.preventDefault();
} else if (e.key === 'h' && (e.metaKey || e.ctrlKey)) {
if ($allHidden) {

View File

@@ -12,7 +12,8 @@
const handleMouseMove = (event: PointerEvent) => {
const newAfter =
startAfter + (orientation === 'col' ? startX - event.clientX : startY - event.clientY);
startAfter +
(orientation === 'col' ? startX - event.clientX : startY - event.clientY);
if (newAfter >= minAfter && newAfter <= maxAfter) {
after = newAfter;
} else if (newAfter < minAfter && after !== minAfter) {

View File

@@ -8,7 +8,7 @@
getDistanceUnits,
getElevationUnits,
getVelocityUnits,
secondsToHHMMSS
secondsToHHMMSS,
} from '$lib/units';
import { _ } from 'svelte-i18n';

View File

@@ -43,6 +43,7 @@
:global(.markdown > a) {
@apply text-link;
@apply hover:underline;
@apply contents;
}
:global(.markdown p > a) {

View File

@@ -18,7 +18,11 @@
class="w-full max-w-3xl"
/>
{:else if src === 'tools/split'}
<enhanced:img src="/src/lib/assets/img/docs/tools/split.png" {alt} class="w-full max-w-3xl" />
<enhanced:img
src="/src/lib/assets/img/docs/tools/split.png"
{alt}
class="w-full max-w-3xl"
/>
{/if}
</div>
<p class="text-center text-sm text-muted-foreground mt-2">{alt}</p>

View File

@@ -1,39 +1,64 @@
import { File, FilePen, View, type Icon, Settings, Pencil, MapPin, Scissors, CalendarClock, Group, Ungroup, Filter, SquareDashedMousePointer, MountainSnow } from "lucide-svelte";
import type { ComponentType } from "svelte";
import {
File,
FilePen,
View,
type Icon,
Settings,
Pencil,
MapPin,
Scissors,
CalendarClock,
Group,
Ungroup,
Filter,
SquareDashedMousePointer,
MountainSnow,
} from 'lucide-svelte';
import type { ComponentType } from 'svelte';
export const guides: Record<string, string[]> = {
'getting-started': [],
menu: ['file', 'edit', 'view', 'settings'],
'files-and-stats': [],
toolbar: ['routing', 'poi', 'scissors', 'time', 'merge', 'extract', 'elevation', 'minify', 'clean'],
toolbar: [
'routing',
'poi',
'scissors',
'time',
'merge',
'extract',
'elevation',
'minify',
'clean',
],
'map-controls': [],
'gpx': [],
'integration': [],
'faq': [],
gpx: [],
integration: [],
faq: [],
};
export const guideIcons: Record<string, string | ComponentType<Icon>> = {
"getting-started": "🚀",
"menu": "📂 ⚙️",
"file": File,
"edit": FilePen,
"view": View,
"settings": Settings,
"files-and-stats": "🗂 📈",
"toolbar": "🧰",
"routing": Pencil,
"poi": MapPin,
"scissors": Scissors,
"time": CalendarClock,
"merge": Group,
"extract": Ungroup,
"elevation": MountainSnow,
"minify": Filter,
"clean": SquareDashedMousePointer,
"map-controls": "🗺",
"gpx": "💾",
"integration": "{ 👩‍💻 }",
"faq": "🔮",
'getting-started': '🚀',
menu: '📂 ⚙️',
file: File,
edit: FilePen,
view: View,
settings: Settings,
'files-and-stats': '🗂 📈',
toolbar: '🧰',
routing: Pencil,
poi: MapPin,
scissors: Scissors,
time: CalendarClock,
merge: Group,
extract: Ungroup,
elevation: MountainSnow,
minify: Filter,
clean: SquareDashedMousePointer,
'map-controls': '🗺',
gpx: '💾',
integration: '{ 👩‍💻 }',
faq: '🔮',
};
export function getPreviousGuide(currentGuide: string): string | undefined {

View File

@@ -12,7 +12,7 @@
embedding,
loadFile,
map,
updateGPXData
updateGPXData,
} from '$lib/stores';
import { onDestroy, onMount } from 'svelte';
import { fileObservers, settings, GPXStatisticsTree } from '$lib/db';
@@ -23,7 +23,7 @@
import {
allowedEmbeddingBasemaps,
getFilesFromEmbeddingOptions,
type EmbeddingOptions
type EmbeddingOptions,
} from './Embedding';
import { mode, setMode } from 'mode-watcher';
import { browser } from '$app/environment';
@@ -37,7 +37,7 @@
temperatureUnits,
fileOrder,
distanceMarkers,
directionMarkers
directionMarkers,
} = settings;
export let useHash = true;
@@ -50,7 +50,7 @@
distanceUnits: 'metric',
velocityUnits: 'speed',
temperatureUnits: 'celsius',
theme: 'system'
theme: 'system',
};
function applyOptions() {
@@ -74,12 +74,12 @@
let bounds = {
southWest: {
lat: 90,
lon: 180
lon: 180,
},
northEast: {
lat: -90,
lon: -180
}
lon: -180,
},
};
fileObservers.update(($fileObservers) => {
@@ -96,12 +96,13 @@
id,
readable({
file,
statistics
statistics,
})
);
ids.push(id);
let fileBounds = statistics.getStatisticsFor(new ListFileItem(id)).global.bounds;
let fileBounds = statistics.getStatisticsFor(new ListFileItem(id)).global
.bounds;
bounds.southWest.lat = Math.min(bounds.southWest.lat, fileBounds.southWest.lat);
bounds.southWest.lon = Math.min(bounds.southWest.lon, fileBounds.southWest.lon);
@@ -130,12 +131,12 @@
bounds.southWest.lon,
bounds.southWest.lat,
bounds.northEast.lon,
bounds.northEast.lat
bounds.northEast.lat,
],
{
padding: 80,
linear: true,
easing: () => 1
easing: () => 1,
}
);
}
@@ -143,7 +144,10 @@
}
});
if (options.basemap !== $currentBasemap && allowedEmbeddingBasemaps.includes(options.basemap)) {
if (
options.basemap !== $currentBasemap &&
allowedEmbeddingBasemaps.includes(options.basemap)
) {
$currentBasemap = options.basemap;
}
@@ -257,12 +261,10 @@
options.elevation.hr ? 'hr' : null,
options.elevation.cad ? 'cad' : null,
options.elevation.temp ? 'temp' : null,
options.elevation.power ? 'power' : null
options.elevation.power ? 'power' : null,
].filter((dataset) => dataset !== null)}
elevationFill={options.elevation.fill}
panelSize={options.elevation.height}
showControls={options.elevation.controls}
class="py-2"
/>
{/if}
</div>

View File

@@ -10,7 +10,7 @@ export type EmbeddingOptions = {
show: boolean;
height: number;
controls: boolean;
fill: 'slope' | 'surface' | undefined;
fill: 'slope' | 'surface' | 'highway' | undefined;
speed: boolean;
hr: boolean;
cad: boolean;
@@ -39,14 +39,14 @@ export const defaultEmbeddingOptions = {
hr: false,
cad: false,
temp: false,
power: false
power: false,
},
distanceMarkers: false,
directionMarkers: false,
distanceUnits: 'metric',
velocityUnits: 'speed',
temperatureUnits: 'celsius',
theme: 'system'
theme: 'system',
};
export function getDefaultEmbeddingOptions(): EmbeddingOptions {
@@ -59,7 +59,11 @@ export function getMergedEmbeddingOptions(
): EmbeddingOptions {
const mergedOptions = JSON.parse(JSON.stringify(defaultOptions));
for (const key in options) {
if (typeof options[key] === 'object' && options[key] !== null && !Array.isArray(options[key])) {
if (
typeof options[key] === 'object' &&
options[key] !== null &&
!Array.isArray(options[key])
) {
mergedOptions[key] = getMergedEmbeddingOptions(options[key], defaultOptions[key]);
} else {
mergedOptions[key] = options[key];
@@ -79,7 +83,10 @@ export function getCleanedEmbeddingOptions(
cleanedOptions[key] !== null &&
!Array.isArray(cleanedOptions[key])
) {
cleanedOptions[key] = getCleanedEmbeddingOptions(cleanedOptions[key], defaultOptions[key]);
cleanedOptions[key] = getCleanedEmbeddingOptions(
cleanedOptions[key],
defaultOptions[key]
);
if (Object.keys(cleanedOptions[key]).length === 0) {
delete cleanedOptions[key];
}
@@ -141,7 +148,7 @@ export function convertOldEmbeddingOptions(options: URLSearchParams): any {
}
if (options.has('slope')) {
newOptions.elevation = {
fill: 'slope'
fill: 'slope',
};
}
return newOptions;

View File

@@ -13,13 +13,13 @@
SquareActivity,
Coins,
Milestone,
Video
Video,
} from 'lucide-svelte';
import { _ } from 'svelte-i18n';
import {
allowedEmbeddingBasemaps,
getCleanedEmbeddingOptions,
getDefaultEmbeddingOptions
getDefaultEmbeddingOptions,
} from './Embedding';
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import Embedding from './Embedding.svelte';
@@ -30,7 +30,7 @@
let options = getDefaultEmbeddingOptions();
options.token = 'YOUR_MAPBOX_TOKEN';
options.files = [
'https://raw.githubusercontent.com/gpxstudio/gpx.studio/main/gpx/test-data/simple.gpx'
'https://raw.githubusercontent.com/gpxstudio/gpx.studio/main/gpx/test-data/simple.gpx',
];
let files = options.files[0];
@@ -130,7 +130,11 @@
<div class="grid grid-cols-2 gap-x-6 gap-y-3 rounded-md border p-3 mt-1">
<Label class="flex flex-row items-center gap-2">
{$_('embedding.height')}
<Input type="number" bind:value={options.elevation.height} class="h-8 w-20" />
<Input
type="number"
bind:value={options.elevation.height}
class="h-8 w-20"
/>
</Label>
<div class="flex flex-row items-center gap-2">
<span class="shrink-0">
@@ -142,7 +146,11 @@
let value = selected?.value;
if (value === 'none') {
options.elevation.fill = undefined;
} else if (value === 'slope' || value === 'surface') {
} else if (
value === 'slope' ||
value === 'surface' ||
value === 'highway'
) {
options.elevation.fill = value;
}
}}
@@ -152,7 +160,10 @@
</Select.Trigger>
<Select.Content>
<Select.Item value="slope">{$_('quantities.slope')}</Select.Item>
<Select.Item value="surface">{$_('quantities.surface')}</Select.Item>
<Select.Item value="surface">{$_('quantities.surface')}</Select.Item
>
<Select.Item value="highway">{$_('quantities.highway')}</Select.Item
>
<Select.Item value="none">{$_('embedding.none')}</Select.Item>
</Select.Content>
</Select.Root>
@@ -165,35 +176,35 @@
<Checkbox id="show-speed" bind:checked={options.elevation.speed} />
<Label for="show-speed" class="flex flex-row items-center gap-1">
<Zap size="16" />
{$_('chart.show_speed')}
{$_('quantities.speed')}
</Label>
</div>
<div class="flex flex-row items-center gap-2">
<Checkbox id="show-hr" bind:checked={options.elevation.hr} />
<Label for="show-hr" class="flex flex-row items-center gap-1">
<HeartPulse size="16" />
{$_('chart.show_heartrate')}
{$_('quantities.heartrate')}
</Label>
</div>
<div class="flex flex-row items-center gap-2">
<Checkbox id="show-cad" bind:checked={options.elevation.cad} />
<Label for="show-cad" class="flex flex-row items-center gap-1">
<Orbit size="16" />
{$_('chart.show_cadence')}
{$_('quantities.cadence')}
</Label>
</div>
<div class="flex flex-row items-center gap-2">
<Checkbox id="show-temp" bind:checked={options.elevation.temp} />
<Label for="show-temp" class="flex flex-row items-center gap-1">
<Thermometer size="16" />
{$_('chart.show_temperature')}
{$_('quantities.temperature')}
</Label>
</div>
<div class="flex flex-row items-center gap-2">
<Checkbox id="show-power" bind:checked={options.elevation.power} />
<Label for="show-power" class="flex flex-row items-center gap-1">
<SquareActivity size="16" />
{$_('chart.show_power')}
{$_('quantities.power')}
</Label>
</div>
</div>
@@ -317,7 +328,8 @@
<Label>
{$_('embedding.code')}
</Label>
<pre class="bg-primary text-primary-foreground p-3 rounded-md whitespace-normal break-all">
<pre
class="bg-primary text-primary-foreground p-3 rounded-md whitespace-normal break-all">
<code class="language-html">
{`<iframe src="https://gpx.studio${base}/embed?options=${encodeURIComponent(JSON.stringify(getCleanedEmbeddingOptions(options)))}${hash}" width="100%" height="600px" frameborder="0" style="outline: none;"/>`}
</code>

View File

@@ -17,9 +17,9 @@
setContext('orientation', orientation);
setContext('recursive', recursive);
const { verticalFileView } = settings;
const { treeFileView } = settings;
verticalFileView.subscribe(($vertical) => {
treeFileView.subscribe(($vertical) => {
if ($vertical) {
selection.update(($selection) => {
$selection.forEach((item) => {

View File

@@ -1,8 +1,8 @@
import { dbUtils, getFile } from "$lib/db";
import { freeze } from "immer";
import { GPXFile, Track, TrackSegment, Waypoint } from "gpx";
import { selection } from "./Selection";
import { newGPXFile } from "$lib/stores";
import { dbUtils, getFile } from '$lib/db';
import { freeze } from 'immer';
import { GPXFile, Track, TrackSegment, Waypoint } from 'gpx';
import { selection } from './Selection';
import { newGPXFile } from '$lib/stores';
export enum ListLevel {
ROOT,
@@ -10,7 +10,7 @@ export enum ListLevel {
TRACK,
SEGMENT,
WAYPOINTS,
WAYPOINT
WAYPOINT,
}
export const allowedMoves: Record<ListLevel, ListLevel[]> = {
@@ -19,7 +19,7 @@ export const allowedMoves: Record<ListLevel, ListLevel[]> = {
[ListLevel.TRACK]: [ListLevel.FILE, ListLevel.TRACK],
[ListLevel.SEGMENT]: [ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
[ListLevel.WAYPOINTS]: [ListLevel.WAYPOINTS],
[ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT]
[ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
};
export const allowedPastes: Record<ListLevel, ListLevel[]> = {
@@ -28,7 +28,7 @@ export const allowedPastes: Record<ListLevel, ListLevel[]> = {
[ListLevel.TRACK]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK],
[ListLevel.SEGMENT]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
[ListLevel.WAYPOINTS]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
[ListLevel.WAYPOINT]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT]
[ListLevel.WAYPOINT]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
};
export abstract class ListItem {
@@ -322,7 +322,13 @@ export function sortItems(items: ListItem[], reverse: boolean = false) {
}
}
export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: ListItem[], toItems: ListItem[], remove: boolean = true) {
export function moveItems(
fromParent: ListItem,
toParent: ListItem,
fromItems: ListItem[],
toItems: ListItem[],
remove: boolean = true
) {
if (fromItems.length === 0) {
return;
}
@@ -338,11 +344,18 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
context.push(file.clone());
} else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) {
context.push(file.trk[item.getTrackIndex()].clone());
} else if (item instanceof ListTrackSegmentItem && item.getTrackIndex() < file.trk.length && item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length) {
} else if (
item instanceof ListTrackSegmentItem &&
item.getTrackIndex() < file.trk.length &&
item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length
) {
context.push(file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()].clone());
} else if (item instanceof ListWaypointsItem) {
context.push(file.wpt.map((wpt) => wpt.clone()));
} else if (item instanceof ListWaypointItem && item.getWaypointIndex() < file.wpt.length) {
} else if (
item instanceof ListWaypointItem &&
item.getWaypointIndex() < file.wpt.length
) {
context.push(file.wpt[item.getWaypointIndex()].clone());
}
}
@@ -359,7 +372,12 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
if (item instanceof ListTrackItem) {
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []);
} else if (item instanceof ListTrackSegmentItem) {
file.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex(), []);
file.replaceTrackSegments(
item.getTrackIndex(),
item.getSegmentIndex(),
item.getSegmentIndex(),
[]
);
} else if (item instanceof ListWaypointsItem) {
file.replaceWaypoints(0, file.wpt.length - 1, []);
} else if (item instanceof ListWaypointItem) {
@@ -371,25 +389,43 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
toItems.forEach((item, i) => {
if (item instanceof ListTrackItem) {
if (context[i] instanceof Track) {
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [context[i]]);
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [
context[i],
]);
} else if (context[i] instanceof TrackSegment) {
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [new Track({
trkseg: [context[i]]
})]);
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [
new Track({
trkseg: [context[i]],
}),
]);
}
} else if (item instanceof ListTrackSegmentItem && context[i] instanceof TrackSegment) {
file.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex() - 1, [context[i]]);
} else if (
item instanceof ListTrackSegmentItem &&
context[i] instanceof TrackSegment
) {
file.replaceTrackSegments(
item.getTrackIndex(),
item.getSegmentIndex(),
item.getSegmentIndex() - 1,
[context[i]]
);
} else if (item instanceof ListWaypointsItem) {
if (Array.isArray(context[i]) && context[i].length > 0 && context[i][0] instanceof Waypoint) {
if (
Array.isArray(context[i]) &&
context[i].length > 0 &&
context[i][0] instanceof Waypoint
) {
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, context[i]);
} else if (context[i] instanceof Waypoint) {
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, [context[i]]);
}
} else if (item instanceof ListWaypointItem && context[i] instanceof Waypoint) {
file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex() - 1, [context[i]]);
file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex() - 1, [
context[i],
]);
}
});
}
},
];
if (fromParent instanceof ListRootItem) {
@@ -400,7 +436,10 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
callbacks.splice(0, 1);
}
dbUtils.applyEachToFilesAndGlobal(files, callbacks, (files, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
dbUtils.applyEachToFilesAndGlobal(
files,
callbacks,
(files, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
toItems.forEach((item, i) => {
if (item instanceof ListFileItem) {
if (context[i] instanceof GPXFile) {
@@ -421,14 +460,18 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
} else if (context[i] instanceof TrackSegment) {
let newFile = newGPXFile();
newFile._data.id = item.getFileId();
newFile.replaceTracks(0, 0, [new Track({
trkseg: [context[i]]
})]);
newFile.replaceTracks(0, 0, [
new Track({
trkseg: [context[i]],
}),
]);
files.set(item.getFileId(), freeze(newFile));
}
}
});
}, context);
},
context
);
selection.update(($selection) => {
$selection.clear();

View File

@@ -5,7 +5,7 @@
TrackSegment,
Waypoint,
type AnyGPXTreeElement,
type GPXTreeElement
type GPXTreeElement,
} from 'gpx';
import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
import { settings, type GPXFileWithStatistics } from '$lib/db';
@@ -19,7 +19,7 @@
ListWaypointItem,
ListWaypointsItem,
type ListItem,
type ListTrackItem
type ListTrackItem,
} from './FileList';
import { _ } from 'svelte-i18n';
import { selection } from './Selection';
@@ -39,19 +39,20 @@
node instanceof GPXFile && item instanceof ListFileItem
? node.metadata.name
: node instanceof Track
? node.name ?? `${$_('gpx.track')} ${(item as ListTrackItem).trackIndex + 1}`
? (node.name ?? `${$_('gpx.track')} ${(item as ListTrackItem).trackIndex + 1}`)
: node instanceof TrackSegment
? `${$_('gpx.segment')} ${(item as ListTrackSegmentItem).segmentIndex + 1}`
: node instanceof Waypoint
? node.name ?? `${$_('gpx.waypoint')} ${(item as ListWaypointItem).waypointIndex + 1}`
? (node.name ??
`${$_('gpx.waypoint')} ${(item as ListWaypointItem).waypointIndex + 1}`)
: node instanceof GPXFile && item instanceof ListWaypointsItem
? $_('gpx.waypoints')
: '';
const { verticalFileView } = settings;
const { treeFileView } = settings;
function openIfSelectedChild() {
if (collapsible && get(verticalFileView) && $selection.hasAnyChildren(item, false)) {
if (collapsible && get(treeFileView) && $selection.hasAnyChildren(item, false)) {
collapsible.openNode();
}
}

View File

@@ -19,7 +19,7 @@
ListWaypointsItem,
allowedMoves,
moveItems,
type ListItem
type ListItem,
} from './FileList';
import { selection } from './Selection';
import { isMac } from '$lib/utils';
@@ -113,7 +113,7 @@
Sortable.utils.select(element);
element.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
block: 'nearest',
});
} else {
Sortable.utils.deselect(element);
@@ -155,7 +155,7 @@
group: {
name: sortableLevel,
pull: allowedMoves[sortableLevel],
put: true
put: true,
},
direction: orientation,
forceAutoScrollFallback: true,
@@ -233,16 +233,16 @@
moveItems(fromItem, toItem, fromItems, toItems);
}
}
},
});
Object.defineProperty(sortable, '_item', {
value: item,
writable: true
writable: true,
});
Object.defineProperty(sortable, '_waypointRoot', {
value: waypointRoot,
writable: true
writable: true,
});
}

View File

@@ -18,7 +18,7 @@
Maximize,
Scissors,
FileStack,
FileX
FileX,
} from 'lucide-svelte';
import {
ListFileItem,
@@ -26,7 +26,7 @@
ListTrackItem,
ListWaypointItem,
allowedPastes,
type ListItem
type ListItem,
} from './FileList';
import {
copied,
@@ -36,7 +36,7 @@
pasteSelection,
selectAll,
selectItem,
selection
selection,
} from './Selection';
import { getContext } from 'svelte';
import { get } from 'svelte/store';
@@ -47,19 +47,14 @@
embedding,
centerMapOnSelection,
gpxLayers,
map
map,
} from '$lib/stores';
import {
GPXTreeElement,
Track,
TrackSegment,
type AnyGPXTreeElement,
Waypoint,
GPXFile
} from 'gpx';
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
import { _ } from 'svelte-i18n';
import MetadataDialog from './MetadataDialog.svelte';
import StyleDialog from './StyleDialog.svelte';
import { waypointPopup } from '$lib/components/gpx-layer/GPXLayerPopup';
import { getSymbolKey, symbols } from '$lib/assets/symbols';
export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
export let item: ListItem;
@@ -75,13 +70,14 @@
nodeColors = [];
if (node instanceof GPXFile) {
let style = node.getStyle();
let defaultColor = undefined;
let layer = gpxLayers.get(item.getFileId());
if (layer) {
style.color.push(layer.layerColor);
defaultColor = layer.layerColor;
}
let style = node.getStyle(defaultColor);
style.color.forEach((c) => {
if (!nodeColors.includes(c)) {
nodeColors.push(c);
@@ -90,8 +86,8 @@
} else if (node instanceof Track) {
let style = node.getStyle();
if (style) {
if (style.color && !nodeColors.includes(style.color)) {
nodeColors.push(style.color);
if (style['gpx_style:color'] && !nodeColors.includes(style['gpx_style:color'])) {
nodeColors.push(style['gpx_style:color']);
}
}
if (nodeColors.length === 0) {
@@ -103,6 +99,8 @@
}
}
$: symbolKey = node instanceof Waypoint ? getSymbolKey(node.sym) : undefined;
let openEditMetadata: boolean = false;
let openEditStyle: boolean = false;
@@ -179,7 +177,10 @@
if (layer && file) {
let waypoint = file.wpt[item.getWaypointIndex()];
if (waypoint) {
layer.showWaypointPopup(waypoint);
waypointPopup?.setItem({
item: waypoint,
fileId: item.getFileId(),
});
}
}
}
@@ -188,7 +189,7 @@
if (item instanceof ListWaypointItem) {
let layer = gpxLayers.get(item.getFileId());
if (layer) {
layer.hideWaypointPopup();
waypointPopup?.setItem(null);
}
}
}}
@@ -196,16 +197,30 @@
{#if item.level === ListLevel.SEGMENT}
<Waypoints size="16" class="mr-1 shrink-0" />
{:else if item.level === ListLevel.WAYPOINT}
{#if symbolKey && symbols[symbolKey].icon}
<svelte:component
this={symbols[symbolKey].icon}
size="16"
class="mr-1 shrink-0"
/>
{:else}
<MapPin size="16" class="mr-1 shrink-0" />
{/if}
<span class="grow select-none truncate {orientation === 'vertical' ? 'last:mr-2' : ''}">
{/if}
<span
class="grow select-none truncate {orientation === 'vertical'
? 'last:mr-2'
: ''}"
>
{label}
</span>
{#if hidden}
<EyeOff
size="12"
class="shrink-0 mt-1 ml-1 {orientation === 'vertical' ? 'mr-2' : ''} {item.level ===
ListLevel.SEGMENT || item.level === ListLevel.WAYPOINT
class="shrink-0 mt-1 ml-1 {orientation === 'vertical'
? 'mr-2'
: ''} {item.level === ListLevel.SEGMENT ||
item.level === ListLevel.WAYPOINT
? 'mr-3'
: ''}"
/>

View File

@@ -17,15 +17,15 @@
let name: string =
node instanceof GPXFile
? node.metadata.name ?? ''
? (node.metadata.name ?? '')
: node instanceof Track
? node.name ?? ''
? (node.name ?? '')
: '';
let description: string =
node instanceof GPXFile
? node.metadata.desc ?? ''
? (node.metadata.desc ?? '')
: node instanceof Track
? node.desc ?? ''
? (node.desc ?? '')
: '';
$: if (!open) {

View File

@@ -1,12 +1,23 @@
import { get, writable } from "svelte/store";
import { ListFileItem, ListItem, ListRootItem, ListTrackItem, ListTrackSegmentItem, ListWaypointItem, ListLevel, sortItems, ListWaypointsItem, moveItems } from "./FileList";
import { fileObservers, getFile, getFileIds, settings } from "$lib/db";
import { get, writable } from 'svelte/store';
import {
ListFileItem,
ListItem,
ListRootItem,
ListTrackItem,
ListTrackSegmentItem,
ListWaypointItem,
ListLevel,
sortItems,
ListWaypointsItem,
moveItems,
} from './FileList';
import { fileObservers, getFile, getFileIds, settings } from '$lib/db';
export class SelectionTreeType {
item: ListItem;
selected: boolean;
children: {
[key: string | number]: SelectionTreeType
[key: string | number]: SelectionTreeType;
};
size: number = 0;
@@ -67,7 +78,11 @@ export class SelectionTreeType {
}
hasAnyParent(item: ListItem, self: boolean = true): boolean {
if (this.selected && this.item.level <= item.level && (self || this.item.level < item.level)) {
if (
this.selected &&
this.item.level <= item.level &&
(self || this.item.level < item.level)
) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
@@ -80,7 +95,11 @@ export class SelectionTreeType {
}
hasAnyChildren(item: ListItem, self: boolean = true, ignoreIds?: (string | number)[]): boolean {
if (this.selected && this.item.level >= item.level && (self || this.item.level > item.level)) {
if (
this.selected &&
this.item.level >= item.level &&
(self || this.item.level > item.level)
) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
@@ -131,7 +150,7 @@ export class SelectionTreeType {
delete this.children[id];
}
}
};
}
export const selection = writable<SelectionTreeType>(new SelectionTreeType(new ListRootItem()));
@@ -181,7 +200,10 @@ export function selectAll() {
let file = getFile(item.getFileId());
if (file) {
file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => {
$selection.set(new ListTrackSegmentItem(item.getFileId(), item.getTrackIndex(), segmentId), true);
$selection.set(
new ListTrackSegmentItem(item.getFileId(), item.getTrackIndex(), segmentId),
true
);
});
}
} else if (item instanceof ListWaypointItem) {
@@ -205,14 +227,24 @@ export function getOrderedSelection(reverse: boolean = false): ListItem[] {
return selected;
}
export function applyToOrderedItemsFromFile(selectedItems: ListItem[], callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void, reverse: boolean = true) {
export function applyToOrderedItemsFromFile(
selectedItems: ListItem[],
callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void,
reverse: boolean = true
) {
get(settings.fileOrder).forEach((fileId) => {
let level: ListLevel | undefined = undefined;
let items: ListItem[] = [];
selectedItems.forEach((item) => {
if (item.getFileId() === fileId) {
level = item.level;
if (item instanceof ListFileItem || item instanceof ListTrackItem || item instanceof ListTrackSegmentItem || item instanceof ListWaypointsItem || item instanceof ListWaypointItem) {
if (
item instanceof ListFileItem ||
item instanceof ListTrackItem ||
item instanceof ListTrackSegmentItem ||
item instanceof ListWaypointsItem ||
item instanceof ListWaypointItem
) {
items.push(item);
}
}
@@ -225,7 +257,10 @@ export function applyToOrderedItemsFromFile(selectedItems: ListItem[], callback:
});
}
export function applyToOrderedSelectedItemsFromFile(callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void, reverse: boolean = true) {
export function applyToOrderedSelectedItemsFromFile(
callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void,
reverse: boolean = true
) {
applyToOrderedItemsFromFile(get(selection).getSelected(), callback, reverse);
}
@@ -270,7 +305,11 @@ export function pasteSelection() {
let startIndex: number | undefined = undefined;
if (fromItems[0].level === toParent.level) {
if (toParent instanceof ListTrackItem || toParent instanceof ListTrackSegmentItem || toParent instanceof ListWaypointItem) {
if (
toParent instanceof ListTrackItem ||
toParent instanceof ListTrackSegmentItem ||
toParent instanceof ListWaypointItem
) {
startIndex = toParent.getId() + 1;
}
toParent = toParent.getParent();
@@ -288,20 +327,41 @@ export function pasteSelection() {
fromItems.forEach((item, index) => {
if (toParent instanceof ListFileItem) {
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
toItems.push(new ListTrackItem(toParent.getFileId(), (startIndex ?? toFile.trk.length) + index));
toItems.push(
new ListTrackItem(
toParent.getFileId(),
(startIndex ?? toFile.trk.length) + index
)
);
} else if (item instanceof ListWaypointsItem) {
toItems.push(new ListWaypointsItem(toParent.getFileId()));
} else if (item instanceof ListWaypointItem) {
toItems.push(new ListWaypointItem(toParent.getFileId(), (startIndex ?? toFile.wpt.length) + index));
toItems.push(
new ListWaypointItem(
toParent.getFileId(),
(startIndex ?? toFile.wpt.length) + index
)
);
}
} else if (toParent instanceof ListTrackItem) {
if (item instanceof ListTrackSegmentItem) {
let toTrackIndex = toParent.getTrackIndex();
toItems.push(new ListTrackSegmentItem(toParent.getFileId(), toTrackIndex, (startIndex ?? toFile.trk[toTrackIndex].trkseg.length) + index));
toItems.push(
new ListTrackSegmentItem(
toParent.getFileId(),
toTrackIndex,
(startIndex ?? toFile.trk[toTrackIndex].trkseg.length) + index
)
);
}
} else if (toParent instanceof ListWaypointsItem) {
if (item instanceof ListWaypointItem) {
toItems.push(new ListWaypointItem(toParent.getFileId(), (startIndex ?? toFile.wpt.length) + index));
toItems.push(
new ListWaypointItem(
toParent.getFileId(),
(startIndex ?? toFile.wpt.length) + index
)
);
}
}
});

View File

@@ -14,20 +14,20 @@
export let item: ListItem;
export let open = false;
const { defaultOpacity, defaultWeight } = settings;
const { defaultOpacity, defaultWidth } = settings;
let colors: string[] = [];
let color: string | undefined = undefined;
let opacity: number[] = [];
let weight: number[] = [];
let width: number[] = [];
let colorChanged = false;
let opacityChanged = false;
let weightChanged = false;
let widthChanged = false;
function setStyleInputs() {
colors = [];
opacity = [];
weight = [];
width = [];
$selection.forEach((item) => {
if (item instanceof ListFileItem) {
@@ -47,9 +47,9 @@
opacity.push(o);
}
});
style.weight.forEach((w) => {
if (!weight.includes(w)) {
weight.push(w);
style.width.forEach((w) => {
if (!width.includes(w)) {
width.push(w);
}
});
}
@@ -60,14 +60,20 @@
let track = file.trk[item.getTrackIndex()];
let style = track.getStyle();
if (style) {
if (style.color && !colors.includes(style.color)) {
colors.push(style.color);
if (
style['gpx_style:color'] &&
!colors.includes(style['gpx_style:color'])
) {
colors.push(style['gpx_style:color']);
}
if (style.opacity && !opacity.includes(style.opacity)) {
opacity.push(style.opacity);
if (
style['gpx_style:opacity'] &&
!opacity.includes(style['gpx_style:opacity'])
) {
opacity.push(style['gpx_style:opacity']);
}
if (style.weight && !weight.includes(style.weight)) {
weight.push(style.weight);
if (style['gpx_style:width'] && !width.includes(style['gpx_style:width'])) {
width.push(style['gpx_style:width']);
}
}
if (!colors.includes(layer.layerColor)) {
@@ -79,11 +85,11 @@
color = colors[0];
opacity = [opacity[0] ?? $defaultOpacity];
weight = [weight[0] ?? $defaultWeight];
width = [width[0] ?? $defaultWidth];
colorChanged = false;
opacityChanged = false;
weightChanged = false;
widthChanged = false;
}
$: if ($selection && open) {
@@ -123,37 +129,37 @@
{$_('menu.style.width')}
<div class="w-40 p-2">
<Slider
bind:value={weight}
id="weight"
bind:value={width}
id="width"
min={1}
max={10}
step={1}
onValueChange={() => (weightChanged = true)}
onValueChange={() => (widthChanged = true)}
/>
</div>
</Label>
<Button
variant="outline"
disabled={!colorChanged && !opacityChanged && !weightChanged}
disabled={!colorChanged && !opacityChanged && !widthChanged}
on:click={() => {
let style = {};
if (colorChanged) {
style.color = color;
style['gpx_style:color'] = color;
}
if (opacityChanged) {
style.opacity = opacity[0];
style['gpx_style:opacity'] = opacity[0];
}
if (weightChanged) {
style.weight = weight[0];
if (widthChanged) {
style['gpx_style:width'] = width[0];
}
dbUtils.setStyleToSelection(style);
if (item instanceof ListFileItem && $selection.size === gpxLayers.size) {
if (style.opacity) {
$defaultOpacity = style.opacity;
if (style['gpx_style:opacity']) {
$defaultOpacity = style['gpx_style:opacity'];
}
if (style.weight) {
$defaultWeight = style.weight;
if (style['gpx_style:width']) {
$defaultWidth = style['gpx_style:width'];
}
}

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { ClipboardCopy } from 'lucide-svelte';
import { _ } from 'svelte-i18n';
import type { Coordinates } from 'gpx';
export let coordinates: Coordinates;
export let onCopy: () => void = () => {};
</script>
<Button
class="w-full px-2 py-1 h-8 justify-start {$$props.class}"
variant="outline"
on:click={() => {
navigator.clipboard.writeText(
`${coordinates.lat.toFixed(6)}, ${coordinates.lon.toFixed(6)}`
);
onCopy();
}}
>
<ClipboardCopy size="16" class="mr-1" />
{$_('menu.copy_coordinates')}
</Button>

View File

@@ -1,10 +1,18 @@
import { settings } from "$lib/db";
import { gpxStatistics } from "$lib/stores";
import { get } from "svelte/store";
import { settings } from '$lib/db';
import { gpxStatistics } from '$lib/stores';
import { get } from 'svelte/store';
const { distanceMarkers, distanceUnits } = settings;
const stops = [
[100, 0],
[50, 7],
[25, 8, 10],
[10, 10],
[5, 11],
[1, 13],
];
export class DistanceMarkers {
map: mapboxgl.Map;
updateBinded: () => void = this.update.bind(this);
@@ -28,41 +36,55 @@ export class DistanceMarkers {
} else {
this.map.addSource('distance-markers', {
type: 'geojson',
data: this.getDistanceMarkersGeoJSON()
data: this.getDistanceMarkersGeoJSON(),
});
}
if (!this.map.getLayer('distance-markers')) {
stops.forEach(([d, minzoom, maxzoom]) => {
if (!this.map.getLayer(`distance-markers-${d}`)) {
this.map.addLayer({
id: 'distance-markers',
id: `distance-markers-${d}`,
type: 'symbol',
source: 'distance-markers',
filter:
d === 5
? [
'any',
['==', ['get', 'level'], 5],
['==', ['get', 'level'], 25],
]
: ['==', ['get', 'level'], d],
minzoom: minzoom,
maxzoom: maxzoom ?? 24,
layout: {
'text-field': ['get', 'distance'],
'text-size': 14,
'text-font': ['Open Sans Bold'],
'text-padding': 20,
},
paint: {
'text-color': 'black',
'text-halo-width': 2,
'text-halo-color': 'white',
},
});
} else {
this.map.moveLayer(`distance-markers-${d}`);
}
});
} else {
this.map.moveLayer('distance-markers');
stops.forEach(([d]) => {
if (this.map.getLayer(`distance-markers-${d}`)) {
this.map.removeLayer(`distance-markers-${d}`);
}
} else {
if (this.map.getLayer('distance-markers')) {
this.map.removeLayer('distance-markers');
});
}
}
} catch (e) { // No reliable way to check if the map is ready to add sources and layers
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
return;
}
}
remove() {
this.unsubscribes.forEach(unsubscribe => unsubscribe());
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
}
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
@@ -71,17 +93,28 @@ export class DistanceMarkers {
let features = [];
let currentTargetDistance = 1;
for (let i = 0; i < statistics.local.distance.total.length; i++) {
if (statistics.local.distance.total[i] >= currentTargetDistance * (get(distanceUnits) === 'metric' ? 1 : 1.60934)) {
if (
statistics.local.distance.total[i] >=
currentTargetDistance * (get(distanceUnits) === 'metric' ? 1 : 1.60934)
) {
let distance = currentTargetDistance.toFixed(0);
let [level, minzoom] = stops.find(([d]) => currentTargetDistance % d === 0) ?? [
0, 0,
];
features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [statistics.local.points[i].getLongitude(), statistics.local.points[i].getLatitude()]
coordinates: [
statistics.local.points[i].getLongitude(),
statistics.local.points[i].getLatitude(),
],
},
properties: {
distance,
}
level,
minzoom,
},
} as GeoJSON.Feature);
currentTargetDistance += 1;
}
@@ -89,7 +122,7 @@ export class DistanceMarkers {
return {
type: 'FeatureCollection',
features
features,
};
}
}

View File

@@ -1,15 +1,28 @@
import { currentTool, map, Tool } from "$lib/stores";
import { settings, type GPXFileWithStatistics, dbUtils } from "$lib/db";
import { get, type Readable } from "svelte/store";
import mapboxgl from "mapbox-gl";
import { currentPopupWaypoint, deleteWaypoint, waypointPopup } from "./WaypointPopup";
import { addSelectItem, selectItem, selection } from "$lib/components/file-list/Selection";
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList";
import type { Waypoint } from "gpx";
import { getElevation, resetCursor, setGrabbingCursor, setPointerCursor, setScissorsCursor } from "$lib/utils";
import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte";
import { MapPin, Square } from "lucide-static";
import { getSymbolKey, symbols } from "$lib/assets/symbols";
import { currentTool, map, Tool } from '$lib/stores';
import { settings, type GPXFileWithStatistics, dbUtils } from '$lib/db';
import { get, type Readable } from 'svelte/store';
import mapboxgl from 'mapbox-gl';
import { waypointPopup, deleteWaypoint, trackpointPopup } from './GPXLayerPopup';
import { addSelectItem, selectItem, selection } from '$lib/components/file-list/Selection';
import {
ListTrackSegmentItem,
ListWaypointItem,
ListWaypointsItem,
ListTrackItem,
ListFileItem,
ListRootItem,
} from '$lib/components/file-list/FileList';
import {
getClosestLinePoint,
getElevation,
resetCursor,
setGrabbingCursor,
setPointerCursor,
setScissorsCursor,
} from '$lib/utils';
import { selectedWaypoint } from '$lib/components/toolbar/tools/Waypoint.svelte';
import { MapPin, Square } from 'lucide-static';
import { getSymbolKey, symbols } from '$lib/assets/symbols';
const colors = [
'#ff0000',
@@ -22,7 +35,7 @@ const colors = [
'#288228',
'#9933ff',
'#50f0be',
'#8c645a'
'#8c645a',
];
const colorCount: { [key: string]: number } = {};
@@ -46,26 +59,30 @@ function decrementColor(color: string) {
function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined;
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
${Square
.replace('width="24"', 'width="12"')
${Square.replace('width="24"', 'width="12"')
.replace('height="24"', 'height="12"')
.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('stroke="currentColor"', '')
.replace('path', `path fill="#3fb1ce" stroke="SteelBlue" stroke-width="1"`)
.replace('circle', `circle fill="${symbolSvg ? 'none' : 'white'}" stroke="${symbolSvg ? 'none' : 'white'}" stroke-width="2"`)}
${symbolSvg?.replace('width="24"', 'width="10"')
.replace(
'circle',
`circle fill="${symbolSvg ? 'none' : 'white'}" stroke="${symbolSvg ? 'none' : 'white'}" stroke-width="2"`
)}
${
symbolSvg
?.replace('width="24"', 'width="10"')
.replace('height="24"', 'height="10"')
.replace('stroke="currentColor"', 'stroke="white"')
.replace('stroke-width="2"', 'stroke-width="2.5" x="7" y="5"') ?? ''}
.replace('stroke-width="2"', 'stroke-width="2.5" x="7" y="5"') ?? ''
}
</svg>`;
}
const { directionMarkers, verticalFileView, defaultOpacity, defaultWeight } = settings;
const { directionMarkers, treeFileView, defaultOpacity, defaultWidth } = settings;
export class GPXLayer {
map: mapboxgl.Map;
@@ -80,17 +97,22 @@ export class GPXLayer {
updateBinded: () => void = this.update.bind(this);
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
layerOnMouseMoveBinded: (e: any) => void = this.layerOnMouseMove.bind(this);
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this);
maybeHideWaypointPopupBinded: (e: any) => void = this.maybeHideWaypointPopup.bind(this);
constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
constructor(
map: mapboxgl.Map,
fileId: string,
file: Readable<GPXFileWithStatistics | undefined>
) {
this.map = map;
this.fileId = fileId;
this.file = file;
this.layerColor = getColor();
this.unsubscribe.push(file.subscribe(this.updateBinded));
this.unsubscribe.push(selection.subscribe($selection => {
this.unsubscribe.push(
selection.subscribe(($selection) => {
let newSelected = $selection.hasAnyChildren(new ListFileItem(this.fileId));
if (this.selected || newSelected) {
this.selected = newSelected;
@@ -99,17 +121,20 @@ export class GPXLayer {
if (newSelected) {
this.moveToFront();
}
}));
})
);
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded));
this.unsubscribe.push(currentTool.subscribe(tool => {
this.unsubscribe.push(
currentTool.subscribe((tool) => {
if (tool === Tool.WAYPOINT && !this.draggable) {
this.draggable = true;
this.markers.forEach(marker => marker.setDraggable(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.markers.forEach((marker) => marker.setDraggable(false));
}
}));
})
);
this.draggable = get(currentTool) === Tool.WAYPOINT;
this.map.on('style.import.load', this.updateBinded);
@@ -121,7 +146,11 @@ export class GPXLayer {
return;
}
if (file._data.style && file._data.style.color && this.layerColor !== `#${file._data.style.color}`) {
if (
file._data.style &&
file._data.style.color &&
this.layerColor !== `#${file._data.style.color}`
) {
decrementColor(this.layerColor);
this.layerColor = `#${file._data.style.color}`;
}
@@ -133,7 +162,7 @@ export class GPXLayer {
} else {
this.map.addSource(this.fileId, {
type: 'geojson',
data: this.getGeoJSON()
data: this.getGeoJSON(),
});
}
@@ -144,24 +173,26 @@ export class GPXLayer {
source: this.fileId,
layout: {
'line-join': 'round',
'line-cap': 'round'
'line-cap': 'round',
},
paint: {
'line-color': ['get', 'color'],
'line-width': ['get', 'weight'],
'line-opacity': ['get', 'opacity']
}
'line-width': ['get', 'width'],
'line-opacity': ['get', 'opacity'],
},
});
this.map.on('click', this.fileId, this.layerOnClickBinded);
this.map.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
this.map.on('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
this.map.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
this.map.on('mousemove', this.fileId, this.layerOnMouseMoveBinded);
}
if (get(directionMarkers)) {
if (!this.map.getLayer(this.fileId + '-direction')) {
this.map.addLayer({
this.map.addLayer(
{
id: this.fileId + '-direction',
type: 'symbol',
source: this.fileId,
@@ -179,9 +210,11 @@ export class GPXLayer {
'text-color': 'white',
'text-opacity': 0.7,
'text-halo-width': 0.2,
'text-halo-color': 'white'
}
}, this.map.getLayer('distance-markers') ? 'distance-markers' : undefined);
'text-halo-color': 'white',
},
},
this.map.getLayer('distance-markers') ? 'distance-markers' : undefined
);
}
} else {
if (this.map.getLayer(this.fileId + '-direction')) {
@@ -196,23 +229,53 @@ export class GPXLayer {
}
});
this.map.setFilter(this.fileId, ['any', ...visibleItems.map(([trackIndex, segmentIndex]) => ['all', ['==', 'trackIndex', trackIndex], ['==', 'segmentIndex', segmentIndex]])], { validate: false });
this.map.setFilter(
this.fileId,
[
'any',
...visibleItems.map(([trackIndex, segmentIndex]) => [
'all',
['==', 'trackIndex', trackIndex],
['==', 'segmentIndex', segmentIndex],
]),
],
{ validate: false }
);
if (this.map.getLayer(this.fileId + '-direction')) {
this.map.setFilter(this.fileId + '-direction', ['any', ...visibleItems.map(([trackIndex, segmentIndex]) => ['all', ['==', 'trackIndex', trackIndex], ['==', 'segmentIndex', segmentIndex]])], { validate: false });
this.map.setFilter(
this.fileId + '-direction',
[
'any',
...visibleItems.map(([trackIndex, segmentIndex]) => [
'all',
['==', 'trackIndex', trackIndex],
['==', 'segmentIndex', segmentIndex],
]),
],
{ validate: false }
);
}
} catch (e) { // No reliable way to check if the map is ready to add sources and layers
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
return;
}
let markerIndex = 0;
if (get(selection).hasAnyChildren(new ListFileItem(this.fileId))) {
file.wpt.forEach((waypoint) => { // Update markers
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].getElement().innerHTML = getMarkerForSymbol(
symbolKey,
this.layerColor
);
this.markers[markerIndex].setLngLat(waypoint.getCoordinates());
Object.defineProperty(this.markers[markerIndex], '_waypoint', { value: waypoint, writable: true });
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');
@@ -220,15 +283,15 @@ export class GPXLayer {
let marker = new mapboxgl.Marker({
draggable: this.draggable,
element,
anchor: 'bottom'
anchor: 'bottom',
}).setLngLat(waypoint.getCoordinates());
Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true });
let dragEndTimestamp = 0;
marker.getElement().addEventListener('mouseover', (e) => {
marker.getElement().addEventListener('mousemove', (e) => {
if (marker._isDragging) {
return;
}
this.showWaypointPopup(marker._waypoint);
waypointPopup?.setItem({ item: marker._waypoint, fileId: this.fileId });
e.stopPropagation();
});
marker.getElement().addEventListener('click', (e) => {
@@ -242,23 +305,33 @@ export class GPXLayer {
return;
}
if (get(verticalFileView)) {
if ((e.ctrlKey || e.metaKey) && get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), false)) {
addSelectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index));
if (get(treeFileView)) {
if (
(e.ctrlKey || e.metaKey) &&
get(selection).hasAnyChildren(
new ListWaypointsItem(this.fileId),
false
)
) {
addSelectItem(
new ListWaypointItem(this.fileId, marker._waypoint._data.index)
);
} else {
selectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index));
selectItem(
new ListWaypointItem(this.fileId, marker._waypoint._data.index)
);
}
} else if (get(currentTool) === Tool.WAYPOINT) {
selectedWaypoint.set([marker._waypoint, this.fileId]);
} else {
this.showWaypointPopup(marker._waypoint);
waypointPopup?.setItem({ item: marker._waypoint, fileId: this.fileId });
}
e.stopPropagation();
});
marker.on('dragstart', () => {
setGrabbingCursor();
marker.getElement().style.cursor = 'grabbing';
this.hideWaypointPopup();
waypointPopup?.hide();
});
marker.on('dragend', (e) => {
resetCursor();
@@ -269,12 +342,12 @@ export class GPXLayer {
let wpt = file.wpt[marker._waypoint._data.index];
wpt.setCoordinates({
lat: latLng.lat,
lon: latLng.lng
lon: latLng.lng,
});
wpt.ele = ele[0];
});
});
dragEndTimestamp = Date.now()
dragEndTimestamp = Date.now();
});
this.markers.push(marker);
}
@@ -282,7 +355,8 @@ export class GPXLayer {
});
}
while (markerIndex < this.markers.length) { // Remove extra markers
while (markerIndex < this.markers.length) {
// Remove extra markers
this.markers.pop()?.remove();
}
@@ -307,6 +381,7 @@ export class GPXLayer {
this.map.off('contextmenu', this.fileId, this.layerOnContextMenuBinded);
this.map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
this.map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
this.map.off('mousemove', this.fileId, this.layerOnMouseMoveBinded);
this.map.off('style.import.load', this.updateBinded);
if (this.map.getLayer(this.fileId + '-direction')) {
@@ -334,7 +409,10 @@ export class GPXLayer {
this.map.moveLayer(this.fileId);
}
if (this.map.getLayer(this.fileId + '-direction')) {
this.map.moveLayer(this.fileId + '-direction', this.map.getLayer('distance-markers') ? 'distance-markers' : undefined);
this.map.moveLayer(
this.fileId + '-direction',
this.map.getLayer('distance-markers') ? 'distance-markers' : undefined
);
}
}
@@ -342,7 +420,12 @@ export class GPXLayer {
let trackIndex = e.features[0].properties.trackIndex;
let segmentIndex = e.features[0].properties.segmentIndex;
if (get(currentTool) === Tool.SCISSORS && get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
if (
get(currentTool) === Tool.SCISSORS &&
get(selection).hasAnyParent(
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
)
) {
setScissorsCursor();
} else {
setPointerCursor();
@@ -353,16 +436,43 @@ export class GPXLayer {
resetCursor();
}
layerOnMouseMove(e: any) {
if (e.originalEvent.shiftKey) {
let trackIndex = e.features[0].properties.trackIndex;
let segmentIndex = e.features[0].properties.segmentIndex;
const file = get(this.file)?.file;
if (file) {
const closest = getClosestLinePoint(
file.trk[trackIndex].trkseg[segmentIndex].trkpt,
{ lat: e.lngLat.lat, lon: e.lngLat.lng }
);
trackpointPopup?.setItem({ item: closest, fileId: this.fileId });
}
}
}
layerOnClick(e: any) {
if (get(currentTool) === Tool.ROUTING && get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])) {
if (
get(currentTool) === Tool.ROUTING &&
get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])
) {
return;
}
let trackIndex = e.features[0].properties.trackIndex;
let segmentIndex = e.features[0].properties.segmentIndex;
if (get(currentTool) === Tool.SCISSORS && get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
dbUtils.split(this.fileId, trackIndex, segmentIndex, { lat: e.lngLat.lat, lon: e.lngLat.lng });
if (
get(currentTool) === Tool.SCISSORS &&
get(selection).hasAnyParent(
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
)
) {
dbUtils.split(this.fileId, trackIndex, segmentIndex, {
lat: e.lngLat.lat,
lon: e.lngLat.lng,
});
return;
}
@@ -372,8 +482,12 @@ export class GPXLayer {
}
let item = undefined;
if (get(verticalFileView) && file.getSegments().length > 1) { // Select inner item
item = file.children[trackIndex].children.length > 1 ? new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) : new ListTrackItem(this.fileId, trackIndex);
if (get(treeFileView) && file.getSegments().length > 1) {
// Select inner item
item =
file.children[trackIndex].children.length > 1
? new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
: new ListTrackItem(this.fileId, trackIndex);
} else {
item = new ListFileItem(this.fileId);
}
@@ -391,55 +505,19 @@ export class GPXLayer {
}
}
showWaypointPopup(waypoint: Waypoint) {
if (get(currentPopupWaypoint) !== null) {
this.hideWaypointPopup();
}
let marker = this.markers[waypoint._data.index];
if (marker) {
currentPopupWaypoint.set([waypoint, this.fileId]);
marker.setPopup(waypointPopup);
marker.togglePopup();
this.map.on('mousemove', this.maybeHideWaypointPopupBinded);
}
}
maybeHideWaypointPopup(e: any) {
let waypoint = get(currentPopupWaypoint)?.[0];
if (waypoint) {
let marker = this.markers[waypoint._data.index];
if (marker) {
if (this.map.project(marker.getLngLat()).dist(this.map.project(e.lngLat)) > 100) {
this.hideWaypointPopup();
}
} else {
this.hideWaypointPopup();
}
}
}
hideWaypointPopup() {
let waypoint = get(currentPopupWaypoint)?.[0];
if (waypoint) {
let marker = this.markers[waypoint._data.index];
marker?.getPopup()?.remove();
currentPopupWaypoint.set(null);
this.map.off('mousemove', this.maybeHideWaypointPopupBinded);
}
}
getGeoJSON(): GeoJSON.FeatureCollection {
let file = get(this.file)?.file;
if (!file) {
return {
type: 'FeatureCollection',
features: []
features: [],
};
}
let data = file.toGeoJSON();
let trackIndex = 0, segmentIndex = 0;
let trackIndex = 0,
segmentIndex = 0;
for (let feature of data.features) {
if (!feature.properties) {
feature.properties = {};
@@ -447,14 +525,19 @@ export class GPXLayer {
if (!feature.properties.color) {
feature.properties.color = this.layerColor;
}
if (!feature.properties.weight) {
feature.properties.weight = get(defaultWeight);
}
if (!feature.properties.opacity) {
feature.properties.opacity = get(defaultOpacity);
}
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)) || get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), true)) {
feature.properties.weight = feature.properties.weight + 2;
if (!feature.properties.width) {
feature.properties.width = get(defaultWidth);
}
if (
get(selection).hasAnyParent(
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
) ||
get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), true)
) {
feature.properties.width = feature.properties.width + 2;
feature.properties.opacity = Math.min(1, feature.properties.opacity + 0.1);
}
feature.properties.trackIndex = trackIndex;

View File

@@ -0,0 +1,44 @@
import { dbUtils } from '$lib/db';
import { MapPopup } from '$lib/components/MapPopup';
export let waypointPopup: MapPopup | null = null;
export let trackpointPopup: MapPopup | null = null;
export function createPopups(map: mapboxgl.Map) {
removePopups();
waypointPopup = new MapPopup(map, {
closeButton: false,
focusAfterOpen: false,
maxWidth: undefined,
offset: {
top: [0, 0],
'top-left': [0, 0],
'top-right': [0, 0],
bottom: [0, -30],
'bottom-left': [0, -30],
'bottom-right': [0, -30],
left: [10, -15],
right: [-10, -15],
},
});
trackpointPopup = new MapPopup(map, {
closeButton: false,
focusAfterOpen: false,
maxWidth: undefined,
});
}
export function removePopups() {
if (waypointPopup !== null) {
waypointPopup.remove();
waypointPopup = null;
}
if (trackpointPopup !== null) {
trackpointPopup.remove();
trackpointPopup = null;
}
}
export function deleteWaypoint(fileId: string, waypointIndex: number) {
dbUtils.applyToFile(fileId, (file) => file.replaceWaypoints(waypointIndex, waypointIndex, []));
}

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { map, gpxLayers } from '$lib/stores';
import { GPXLayer } from './GPXLayer';
import WaypointPopup from './WaypointPopup.svelte';
import { fileObservers } from '$lib/db';
import { DistanceMarkers } from './DistanceMarkers';
import { StartEndMarkers } from './StartEndMarkers';
import { onDestroy } from 'svelte';
import { createPopups, removePopups } from './GPXLayerPopup';
let distanceMarkers: DistanceMarkers | undefined = undefined;
let startEndMarkers: StartEndMarkers | undefined = undefined;
@@ -35,6 +35,7 @@
if (startEndMarkers) {
startEndMarkers.remove();
}
createPopups($map);
distanceMarkers = new DistanceMarkers($map);
startEndMarkers = new StartEndMarkers($map);
}
@@ -42,17 +43,14 @@
onDestroy(() => {
gpxLayers.forEach((layer) => layer.remove());
gpxLayers.clear();
removePopups();
if (distanceMarkers) {
distanceMarkers.remove();
distanceMarkers = undefined;
}
if (startEndMarkers) {
startEndMarkers.remove();
startEndMarkers = undefined;
}
});
</script>
<WaypointPopup />

View File

@@ -1,6 +1,6 @@
import { gpxStatistics, slicedGPXStatistics, currentTool, Tool } from "$lib/stores";
import mapboxgl from "mapbox-gl";
import { get } from "svelte/store";
import { gpxStatistics, slicedGPXStatistics, currentTool, Tool } from '$lib/stores';
import mapboxgl from 'mapbox-gl';
import { get } from 'svelte/store';
export class StartEndMarkers {
map: mapboxgl.Map;
@@ -16,7 +16,8 @@ export class StartEndMarkers {
let endElement = document.createElement('div');
startElement.className = `h-4 w-4 rounded-full bg-green-500 border-2 border-white`;
endElement.className = `h-4 w-4 rounded-full border-2 border-white`;
endElement.style.background = 'repeating-conic-gradient(#fff 0 90deg, #000 0 180deg) 0 0/8px 8px round';
endElement.style.background =
'repeating-conic-gradient(#fff 0 90deg, #000 0 180deg) 0 0/8px 8px round';
this.start = new mapboxgl.Marker({ element: startElement });
this.end = new mapboxgl.Marker({ element: endElement });
@@ -31,7 +32,11 @@ export class StartEndMarkers {
let statistics = get(slicedGPXStatistics)?.[0] ?? get(gpxStatistics);
if (statistics.local.points.length > 0 && tool !== Tool.ROUTING) {
this.start.setLngLat(statistics.local.points[0].getCoordinates()).addTo(this.map);
this.end.setLngLat(statistics.local.points[statistics.local.points.length - 1].getCoordinates()).addTo(this.map);
this.end
.setLngLat(
statistics.local.points[statistics.local.points.length - 1].getCoordinates()
)
.addTo(this.map);
} else {
this.start.remove();
this.end.remove();
@@ -39,7 +44,7 @@ export class StartEndMarkers {
}
remove() {
this.unsubscribes.forEach(unsubscribe => unsubscribe());
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
this.start.remove();
this.end.remove();

View File

@@ -0,0 +1,43 @@
<script lang="ts">
import type { TrackPoint } from 'gpx';
import type { PopupItem } from '$lib/components/MapPopup';
import CopyCoordinates from '$lib/components/gpx-layer/CopyCoordinates.svelte';
import * as Card from '$lib/components/ui/card';
import WithUnits from '$lib/components/WithUnits.svelte';
import { Compass, Mountain, Timer } from 'lucide-svelte';
import { df } from '$lib/utils';
import { _ } from 'svelte-i18n';
export let trackpoint: PopupItem<TrackPoint>;
</script>
<Card.Root class="border-none shadow-md text-base p-2">
<Card.Header class="p-0">
<Card.Title class="text-md"></Card.Title>
</Card.Header>
<Card.Content class="flex flex-col p-0 text-xs gap-1">
<div class="flex flex-row items-center gap-1">
<Compass size="14" />
{trackpoint.item.getLatitude().toFixed(6)}&deg; {trackpoint.item
.getLongitude()
.toFixed(6)}&deg;
</div>
{#if trackpoint.item.ele !== undefined}
<div class="flex flex-row items-center gap-1">
<Mountain size="14" />
<WithUnits value={trackpoint.item.ele} type="elevation" />
</div>
{/if}
{#if trackpoint.item.time}
<div class="flex flex-row items-center gap-1">
<Timer size="14" />
{df.format(trackpoint.item.time)}
</div>
{/if}
<CopyCoordinates
coordinates={trackpoint.item.attributes}
onCopy={() => trackpoint.hide?.()}
class="mt-0.5"
/>
</Card.Content>
</Card.Root>

View File

@@ -2,23 +2,21 @@
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import Shortcut from '$lib/components/Shortcut.svelte';
import { waypointPopup, currentPopupWaypoint, deleteWaypoint } from './WaypointPopup';
import CopyCoordinates from '$lib/components/gpx-layer/CopyCoordinates.svelte';
import { deleteWaypoint } from './GPXLayerPopup';
import WithUnits from '$lib/components/WithUnits.svelte';
import { Dot, ExternalLink, Trash2 } from 'lucide-svelte';
import { onMount } from 'svelte';
import { Tool, currentTool } from '$lib/stores';
import { getSymbolKey, symbols } from '$lib/assets/symbols';
import { _ } from 'svelte-i18n';
import sanitizeHtml from 'sanitize-html';
import type { Waypoint } from 'gpx';
import type { PopupItem } from '$lib/components/MapPopup';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
let popupElement: HTMLDivElement;
export let waypoint: PopupItem<Waypoint>;
onMount(() => {
waypointPopup.setDOMContent(popupElement);
popupElement.classList.remove('hidden');
});
$: symbolKey = $currentPopupWaypoint ? getSymbolKey($currentPopupWaypoint[0].sym) : undefined;
$: symbolKey = waypoint ? getSymbolKey(waypoint.item.sym) : undefined;
function sanitize(text: string | undefined): string {
if (text === undefined) {
@@ -28,28 +26,26 @@
allowedTags: ['a', 'br', 'img'],
allowedAttributes: {
a: ['href', 'target'],
img: ['src']
}
img: ['src'],
},
}).trim();
}
</script>
<div bind:this={popupElement} class="hidden">
{#if $currentPopupWaypoint}
<Card.Root class="border-none shadow-md text-base max-w-80 p-2">
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw]">
<Card.Header class="p-0">
<Card.Title class="text-md">
{#if $currentPopupWaypoint[0].link && $currentPopupWaypoint[0].link.attributes && $currentPopupWaypoint[0].link.attributes.href}
<a href={$currentPopupWaypoint[0].link.attributes.href} target="_blank">
{$currentPopupWaypoint[0].name ?? $currentPopupWaypoint[0].link.attributes.href}
{#if waypoint.item.link && waypoint.item.link.attributes && waypoint.item.link.attributes.href}
<a href={waypoint.item.link.attributes.href} target="_blank">
{waypoint.item.name ?? waypoint.item.link.attributes.href}
<ExternalLink size="12" class="inline-block mb-1.5" />
</a>
{:else}
{$currentPopupWaypoint[0].name ?? $_('gpx.waypoint')}
{waypoint.item.name ?? $_('gpx.waypoint')}
{/if}
</Card.Title>
</Card.Header>
<Card.Content class="flex flex-col p-0 text-sm">
<Card.Content class="flex flex-col text-sm p-0">
<div class="flex flex-row items-center text-muted-foreground text-xs whitespace-nowrap">
{#if symbolKey}
<span>
@@ -66,36 +62,38 @@
</span>
<Dot size="16" />
{/if}
{$currentPopupWaypoint[0].getLatitude().toFixed(6)}&deg; {$currentPopupWaypoint[0]
{waypoint.item.getLatitude().toFixed(6)}&deg; {waypoint.item
.getLongitude()
.toFixed(6)}&deg;
{#if $currentPopupWaypoint[0].ele !== undefined}
{#if waypoint.item.ele !== undefined}
<Dot size="16" />
<WithUnits value={$currentPopupWaypoint[0].ele} type="elevation" />
<WithUnits value={waypoint.item.ele} type="elevation" />
{/if}
</div>
{#if $currentPopupWaypoint[0].desc}
<span class="whitespace-pre-wrap">{@html sanitize($currentPopupWaypoint[0].desc)}</span>
<ScrollArea class="flex flex-col" viewportClasses="max-h-[30dvh]">
{#if waypoint.item.desc}
<span class="whitespace-pre-wrap">{@html sanitize(waypoint.item.desc)}</span>
{/if}
{#if $currentPopupWaypoint[0].cmt && $currentPopupWaypoint[0].cmt !== $currentPopupWaypoint[0].desc}
<span class="whitespace-pre-wrap">{@html sanitize($currentPopupWaypoint[0].cmt)}</span>
{#if waypoint.item.cmt && waypoint.item.cmt !== waypoint.item.desc}
<span class="whitespace-pre-wrap">{@html sanitize(waypoint.item.cmt)}</span>
{/if}
</ScrollArea>
<div class="mt-2 flex flex-col gap-1">
<CopyCoordinates coordinates={waypoint.item.attributes} />
{#if $currentTool === Tool.WAYPOINT}
<Button
class="mt-2 w-full px-2 py-1 h-8 justify-start"
class="w-full px-2 py-1 h-8 justify-start"
variant="outline"
on:click={() =>
deleteWaypoint($currentPopupWaypoint[1], $currentPopupWaypoint[0]._data.index)}
on:click={() => deleteWaypoint(waypoint.fileId, waypoint.item._data.index)}
>
<Trash2 size="16" class="mr-1" />
{$_('menu.delete')}
<Shortcut shift={true} click={true} />
</Button>
{/if}
</div>
</Card.Content>
</Card.Root>
{/if}
</div>
<style lang="postcss">
div :global(a) {

View File

@@ -1,25 +0,0 @@
import { dbUtils } from "$lib/db";
import type { Waypoint } from "gpx";
import mapboxgl from "mapbox-gl";
import { writable } from "svelte/store";
export const currentPopupWaypoint = writable<[Waypoint, string] | null>(null);
export const waypointPopup = new mapboxgl.Popup({
closeButton: false,
maxWidth: undefined,
offset: {
'top': [0, 0],
'top-left': [0, 0],
'top-right': [0, 0],
'bottom': [0, -30],
'bottom-left': [0, -30],
'bottom-right': [0, -30],
'left': [0, 0],
'right': [0, 0]
},
});
export function deleteWaypoint(fileId: string, waypointIndex: number) {
dbUtils.applyToFile(fileId, (file) => file.replaceWaypoints(waypointIndex, waypointIndex, []));
}

View File

@@ -15,7 +15,7 @@
Trash2,
Move,
Map,
Layers2
Layers2,
} from 'lucide-svelte';
import { _ } from 'svelte-i18n';
import { settings } from '$lib/db';
@@ -34,7 +34,7 @@
currentOverlays,
previousOverlays,
customBasemapOrder,
customOverlayOrder
customOverlayOrder,
} = settings;
let name: string = '';
@@ -68,7 +68,7 @@
acc[id] = true;
return acc;
}, {});
}
},
});
overlaySortable = Sortable.create(overlayContainer, {
onSort: (e) => {
@@ -77,7 +77,7 @@
acc[id] = true;
return acc;
}, {});
}
},
});
basemapSortable.sort($customBasemapOrder);
@@ -108,6 +108,7 @@
if (typeof maxZoom === 'string') {
maxZoom = parseInt(maxZoom);
}
let is512 = tileUrls.some((url) => url.includes('512'));
let layerId = selectedLayerId ?? getLayerId();
let layer: CustomLayer = {
@@ -117,7 +118,7 @@
maxZoom: maxZoom,
layerType: layerType,
resourceType: resourceType,
value: ''
value: '',
};
if (resourceType === 'vector') {
@@ -129,17 +130,17 @@
[layerId]: {
type: 'raster',
tiles: layer.tileUrls,
tileSize: 256,
maxzoom: maxZoom
}
tileSize: is512 ? 512 : 256,
maxzoom: maxZoom,
},
},
layers: [
{
id: layerId,
type: 'raster',
source: layerId
}
]
source: layerId,
},
],
};
}
$customLayers[layerId] = layer;

View File

@@ -13,7 +13,6 @@
import { get, writable } from 'svelte/store';
import { customBasemapUpdate, getLayers } from './utils';
import { OverpassLayer } from './OverpassLayer';
import OverpassPopup from './OverpassPopup.svelte';
let container: HTMLDivElement;
let overpassLayer: OverpassLayer;
@@ -27,14 +26,14 @@
selectedOverlayTree,
selectedOverpassTree,
customLayers,
opacities
opacities,
} = settings;
function setStyle() {
if ($map) {
let basemap = basemaps.hasOwnProperty($currentBasemap)
? basemaps[$currentBasemap]
: $customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap];
: ($customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap]);
$map.removeImport('basemap');
if (typeof basemap === 'string') {
$map.addImport({ id: 'basemap', url: basemap }, 'overlays');
@@ -42,7 +41,7 @@
$map.addImport(
{
id: 'basemap',
data: basemap
data: basemap,
},
'overlays'
);
@@ -71,12 +70,12 @@
layer.paint['raster-opacity'] = $opacities[id];
}
return layer;
})
}),
};
}
$map.addImport({
id,
data: overlay
data: overlay,
});
}
} catch (e) {
@@ -85,18 +84,21 @@
}
function updateOverlays() {
if ($map && $currentOverlays) {
if ($map && $currentOverlays && $opacities) {
let overlayLayers = getLayers($currentOverlays);
try {
let activeOverlays = $map
.getStyle()
.imports.filter((i) => i.id !== 'basemap' && i.id !== 'overlays');
let toRemove = activeOverlays.filter((i) => !overlayLayers[i.id]);
toRemove.forEach((i) => {
$map.removeImport(i.id);
let activeOverlays = $map.getStyle().imports.reduce((acc, i) => {
if (!['basemap', 'overlays', 'glyphs-and-sprite'].includes(i.id)) {
acc[i.id] = i;
}
return acc;
}, {});
let toRemove = Object.keys(activeOverlays).filter((id) => !overlayLayers[id]);
toRemove.forEach((id) => {
$map.removeImport(id);
});
let toAdd = Object.entries(overlayLayers)
.filter(([id, selected]) => selected && !activeOverlays.some((j) => j.id === id))
.filter(([id, selected]) => selected && !activeOverlays.hasOwnProperty(id))
.map(([id]) => id);
toAdd.forEach((id) => {
addOverlay(id);
@@ -107,7 +109,7 @@
}
}
$: if ($map && $currentOverlays) {
$: if ($map && $currentOverlays && $opacities) {
updateOverlays();
}
@@ -130,7 +132,9 @@
});
currentBasemap.subscribe((value) => {
// Updates coming from the database, or from the user swapping basemaps
if (value !== get(selectedBasemap)) {
selectedBasemap.set(value);
}
});
let open = false;
@@ -209,8 +213,6 @@
</div>
</CustomControl>
<OverpassPopup />
<svelte:window
on:click={(e) => {
if (open && !cancelEvents && !container.contains(e.target)) {

View File

@@ -14,7 +14,7 @@
defaultBasemap,
overlays,
overlayTree,
overpassTree
overpassTree,
} from '$lib/assets/layers';
import { getLayers, isSelected, toggle } from '$lib/components/layer-control/utils';
import { settings } from '$lib/db';
@@ -31,7 +31,7 @@
currentBasemap,
currentOverlays,
customLayers,
opacities
opacities,
} = settings;
export let open: boolean;
@@ -137,7 +137,9 @@
<Select.Content class="h-fit max-h-[40dvh] overflow-y-auto">
{#each Object.keys(overlays) as id}
{#if isSelected($selectedOverlayTree, id)}
<Select.Item value={id}>{$_(`layers.label.${id}`)}</Select.Item>
<Select.Item value={id}
>{$_(`layers.label.${id}`)}</Select.Item
>
{/if}
{/each}
{#each Object.entries($customLayers) as [id, layer]}
@@ -157,15 +159,22 @@
max={1}
step={0.1}
disabled={$selectedOverlay === undefined}
onValueChange={() => {
onValueChange={(value) => {
if ($selectedOverlay) {
$opacities[$selectedOverlay.value] = $overlayOpacity[0];
if ($map) {
if ($map.getLayer($selectedOverlay.value)) {
$map.removeLayer($selectedOverlay.value);
$currentOverlays = $currentOverlays;
if (
$map &&
isSelected(
$currentOverlays,
$selectedOverlay.value
)
) {
try {
$map.removeImport($selectedOverlay.value);
} catch (e) {
// No reliable way to check if the map is ready to remove sources and layers
}
}
$opacities[$selectedOverlay.value] = value[0];
}
}}
/>

View File

@@ -49,7 +49,13 @@
aria-label={$_(`layers.label.${id}`)}
/>
{:else}
<input id="{name}-{id}" type="radio" {name} value={id} bind:group={selected} />
<input
id="{name}-{id}"
type="radio"
{name}
value={id}
bind:group={selected}
/>
{/if}
<Label for="{name}-{id}" class="flex flex-row items-center gap-1">
{#if $customLayers.hasOwnProperty(id)}
@@ -64,7 +70,13 @@
<CollapsibleTreeNode {id}>
<span slot="trigger">{$_(`layers.label.${id}`)}</span>
<div slot="content">
<svelte:self node={node[id]} {name} bind:selected {multiple} bind:checked={checked[id]} />
<svelte:self
node={node[id]}
{name}
bind:selected
{multiple}
bind:checked={checked[id]}
/>
</div>
</CollapsibleTreeNode>
{/if}

View File

@@ -1,27 +1,17 @@
import SphericalMercator from "@mapbox/sphericalmercator";
import { getLayers } from "./utils";
import mapboxgl from "mapbox-gl";
import { get, writable } from "svelte/store";
import { liveQuery } from "dexie";
import { db, settings } from "$lib/db";
import { overpassQueryData } from "$lib/assets/layers";
import SphericalMercator from '@mapbox/sphericalmercator';
import { getLayers } from './utils';
import { get, writable } from 'svelte/store';
import { liveQuery } from 'dexie';
import { db, settings } from '$lib/db';
import { overpassQueryData } from '$lib/assets/layers';
import { MapPopup } from '$lib/components/MapPopup';
const {
currentOverpassQueries
} = settings;
const { currentOverpassQueries } = settings;
const mercator = new SphericalMercator({
size: 256,
});
export const overpassPopupPOI = writable<Record<string, any> | null>(null);
export const overpassPopup = new mapboxgl.Popup({
closeButton: false,
maxWidth: undefined,
offset: 15,
});
let data = writable<GeoJSON.FeatureCollection>({ type: 'FeatureCollection', features: [] });
liveQuery(() => db.overpassdata.toArray()).subscribe((pois) => {
@@ -34,28 +24,36 @@ export class OverpassLayer {
queryZoom = 12;
expirationTime = 7 * 24 * 3600 * 1000;
map: mapboxgl.Map;
popup: MapPopup;
currentQueries: Set<string> = new Set();
nextQueries: Map<string, { x: number, y: number, queries: string[] }> = new Map();
nextQueries: Map<string, { x: number; y: number; queries: string[] }> = new Map();
unsubscribes: (() => void)[] = [];
queryIfNeededBinded = this.queryIfNeeded.bind(this);
updateBinded = this.update.bind(this);
onHoverBinded = this.onHover.bind(this);
maybeHidePopupBinded = this.maybeHidePopup.bind(this);
constructor(map: mapboxgl.Map) {
this.map = map;
this.popup = new MapPopup(map, {
closeButton: false,
focusAfterOpen: false,
maxWidth: undefined,
offset: 15,
});
}
add() {
this.map.on('moveend', this.queryIfNeededBinded);
this.map.on('style.import.load', this.updateBinded);
this.unsubscribes.push(data.subscribe(this.updateBinded));
this.unsubscribes.push(currentOverpassQueries.subscribe(() => {
this.unsubscribes.push(
currentOverpassQueries.subscribe(() => {
this.updateBinded();
this.queryIfNeededBinded();
}));
})
);
this.update();
}
@@ -125,27 +123,12 @@ export class OverpassLayer {
}
onHover(e: any) {
overpassPopupPOI.set({
this.popup.setItem({
item: {
...e.features[0].properties,
sym: overpassQueryData[e.features[0].properties.query].symbol ?? ''
sym: overpassQueryData[e.features[0].properties.query].symbol ?? '',
},
});
overpassPopup.setLngLat(e.features[0].geometry.coordinates);
overpassPopup.addTo(this.map);
this.map.on('mousemove', this.maybeHidePopupBinded);
}
maybeHidePopup(e: any) {
let poi = get(overpassPopupPOI);
if (poi && this.map.project([poi.lon, poi.lat]).dist(this.map.project(e.lngLat)) > 100) {
this.hideWaypointPopup();
}
}
hideWaypointPopup() {
overpassPopupPOI.set(null);
overpassPopup.remove();
this.map.off('mousemove', this.maybeHidePopupBinded);
}
query(bbox: [number, number, number, number]) {
@@ -163,8 +146,19 @@ export class OverpassLayer {
continue;
}
db.overpasstiles.where('[x+y]').equals([x, y]).toArray().then((querytiles) => {
let missingQueries = queries.filter((query) => !querytiles.some((querytile) => querytile.query === query && time - querytile.time < this.expirationTime));
db.overpasstiles
.where('[x+y]')
.equals([x, y])
.toArray()
.then((querytiles) => {
let missingQueries = queries.filter(
(query) =>
!querytiles.some(
(querytile) =>
querytile.query === query &&
time - querytile.time < this.expirationTime
)
);
if (missingQueries.length > 0) {
this.queryTile(x, y, missingQueries);
}
@@ -182,13 +176,16 @@ export class OverpassLayer {
const bounds = mercator.bbox(x, y, this.queryZoom);
fetch(`${this.overpassUrl}?data=${getQueryForBounds(bounds, queries)}`)
.then((response) => {
.then(
(response) => {
if (response.ok) {
return response.json();
}
this.currentQueries.delete(`${x},${y}`);
return Promise.reject();
}, () => (this.currentQueries.delete(`${x},${y}`)))
},
() => this.currentQueries.delete(`${x},${y}`)
)
.then((data) => this.storeOverpassData(x, y, queries, data))
.catch(() => this.currentQueries.delete(`${x},${y}`));
}
@@ -196,7 +193,7 @@ export class OverpassLayer {
storeOverpassData(x: number, y: number, queries: string[], data: any) {
let time = Date.now();
let queryTiles = queries.map((query) => ({ x, y, query, time }));
let pois: { query: string, id: number, poi: GeoJSON.Feature }[] = [];
let pois: { query: string; id: number; poi: GeoJSON.Feature }[] = [];
if (data.elements === undefined) {
return;
@@ -212,7 +209,9 @@ export class OverpassLayer {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: element.center ? [element.center.lon, element.center.lat] : [element.lon, element.lat],
coordinates: element.center
? [element.center.lon, element.center.lat]
: [element.lon, element.lat],
},
properties: {
id: element.id,
@@ -220,9 +219,10 @@ export class OverpassLayer {
lon: element.center ? element.center.lon : element.lon,
query: query,
icon: `overpass-${query}`,
tags: element.tags
tags: element.tags,
type: element.type,
},
},
}
});
}
}
@@ -245,11 +245,13 @@ export class OverpassLayer {
if (!this.map.hasImage(`overpass-${query}`)) {
this.map.addImage(`overpass-${query}`, 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(`
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="${overpassQueryData[query].icon.color}" />
<g transform="translate(8 8)">
@@ -281,9 +283,14 @@ function getQuery(query: string) {
function getQueryItem(tags: Record<string, string | boolean | string[]>) {
let arrayEntry = Object.entries(tags).find(([_, value]) => Array.isArray(value));
if (arrayEntry !== undefined) {
return arrayEntry[1].map((val) => `nwr${Object.entries(tags)
return arrayEntry[1]
.map(
(val) =>
`nwr${Object.entries(tags)
.map(([tag, value]) => `[${tag}=${tag === arrayEntry[0] ? val : value}]`)
.join('')};`).join('');
.join('')};`
)
.join('');
} else {
return `nwr${Object.entries(tags)
.map(([tag, value]) => `[${tag}=${value}]`)
@@ -300,8 +307,9 @@ function belongsToQuery(element: any, query: string) {
}
function belongsToQueryItem(element: any, tags: Record<string, string | boolean | string[]>) {
return Object.entries(tags)
.every(([tag, value]) => Array.isArray(value) ? value.includes(element.tags[tag]) : element.tags[tag] === value);
return Object.entries(tags).every(([tag, value]) =>
Array.isArray(value) ? value.includes(element.tags[tag]) : element.tags[tag] === value
);
}
function getCurrentQueries() {
@@ -310,5 +318,7 @@ function getCurrentQueries() {
return [];
}
return Object.entries(getLayers(currentQueries)).filter(([_, selected]) => selected).map(([query, _]) => query);
return Object.entries(getLayers(currentQueries))
.filter(([_, selected]) => selected)
.map(([query, _]) => query);
}

View File

@@ -1,34 +1,52 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { overpassPopup, overpassPopupPOI } from './OverpassLayer';
import { selection } from '$lib/components/file-list/Selection';
import { PencilLine, MapPin } from 'lucide-svelte';
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { dbUtils } from '$lib/db';
import type { PopupItem } from '$lib/components/MapPopup';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import type { WaypointType } from 'gpx';
let popupElement: HTMLDivElement;
export let poi: PopupItem<any>;
onMount(() => {
overpassPopup.setDOMContent(popupElement);
popupElement.classList.remove('hidden');
});
let tags = {};
let tags: { [key: string]: string } = {};
let name = '';
$: if ($overpassPopupPOI) {
tags = JSON.parse($overpassPopupPOI.tags);
$: if (poi) {
tags = JSON.parse(poi.item.tags);
if (tags.name !== undefined && tags.name !== '') {
name = tags.name;
} else {
name = $_(`layers.label.${$overpassPopupPOI.query}`);
name = $_(`layers.label.${poi.item.query}`);
}
}
function addToFile() {
const desc = Object.entries(tags)
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
let wpt: WaypointType = {
attributes: {
lat: poi.item.lat,
lon: poi.item.lon,
},
name: name,
desc: desc,
cmt: desc,
sym: poi.item.sym,
};
if (tags.website) {
wpt.link = {
attributes: {
href: tags.website,
},
};
}
dbUtils.addOrUpdateWaypoint(wpt);
}
</script>
<div bind:this={popupElement} class="hidden">
{#if $overpassPopupPOI}
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw]">
<Card.Header class="p-0">
<Card.Title class="text-md">
@@ -36,13 +54,14 @@
<div class="flex flex-col">
{name}
<div class="text-muted-foreground text-sm font-normal">
{$overpassPopupPOI.lat.toFixed(6)}&deg; {$overpassPopupPOI.lon.toFixed(6)}&deg;
{poi.item.lat.toFixed(6)}&deg; {poi.item.lon.toFixed(6)}&deg;
</div>
</div>
<Button
class="ml-auto p-1.5 h-8"
variant="outline"
href="https://www.openstreetmap.org/edit?editor=id&node={$overpassPopupPOI.id}"
href="https://www.openstreetmap.org/edit?editor=id&{poi.item.type ??
'node'}={poi.item.id}"
target="_blank"
>
<PencilLine size="16" />
@@ -50,18 +69,19 @@
</div>
</Card.Title>
</Card.Header>
<Card.Content class="flex flex-col p-0 text-sm mt-1 whitespace-normal break-all">
<ScrollArea class="flex flex-col" viewportClasses="max-h-[30dvh]">
{#if tags.image || tags['image:0']}
<div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto">
<!-- svelte-ignore a11y-missing-attribute -->
<img src={tags.image ?? tags['image:0']} />
</div>
{/if}
<Card.Content class="flex flex-col p-0 text-sm mt-1 whitespace-normal break-all">
<div class="grid grid-cols-[auto_auto] gap-x-3">
{#each Object.entries(tags) as [key, value]}
{#if key !== 'name' && !key.includes('image')}
<span class="font-mono">{key}</span>
{#if key === 'website' || key === 'contact:website' || key === 'contact:facebook' || key === 'contact:instagram' || key === 'contact:twitter'}
{#if key === 'website' || key.startsWith('website:') || key === 'contact:website' || key === 'contact:facebook' || key === 'contact:instagram' || key === 'contact:twitter'}
<a href={value} target="_blank" class="text-link underline">{value}</a>
{:else if key === 'phone' || key === 'contact:phone'}
<a href={'tel:' + value} class="text-link underline">{value}</a>
@@ -73,30 +93,15 @@
{/if}
{/each}
</div>
</ScrollArea>
<Button
class="mt-2"
variant="outline"
disabled={$selection.size === 0}
on:click={() => {
let desc = Object.entries(tags)
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
dbUtils.addOrUpdateWaypoint({
attributes: {
lat: $overpassPopupPOI.lat,
lon: $overpassPopupPOI.lon
},
name: name,
desc: desc,
cmt: desc,
sym: $overpassPopupPOI.sym
});
}}
on:click={addToFile}
>
<MapPin size="16" class="mr-1" />
{$_('toolbar.waypoint.add')}
</Button>
</Card.Content>
</Card.Root>
{/if}
</div>

View File

@@ -1,9 +1,10 @@
import type { LayerTreeType } from "$lib/assets/layers";
import { writable } from "svelte/store";
import type { LayerTreeType } from '$lib/assets/layers';
import { writable } from 'svelte/store';
export function anySelectedLayer(node: LayerTreeType) {
return Object.keys(node).find((id) => {
if (typeof node[id] == "boolean") {
return (
Object.keys(node).find((id) => {
if (typeof node[id] == 'boolean') {
if (node[id]) {
return true;
}
@@ -13,12 +14,16 @@ export function anySelectedLayer(node: LayerTreeType) {
}
}
return false;
}) !== undefined;
}) !== undefined
);
}
export function getLayers(node: LayerTreeType, layers: { [key: string]: boolean } = {}): { [key: string]: boolean } {
export function getLayers(
node: LayerTreeType,
layers: { [key: string]: boolean } = {}
): { [key: string]: boolean } {
Object.keys(node).forEach((id) => {
if (typeof node[id] == "boolean") {
if (typeof node[id] == 'boolean') {
layers[id] = node[id];
} else {
getLayers(node[id], layers);
@@ -32,7 +37,7 @@ export function isSelected(node: LayerTreeType, id: string) {
if (key === id) {
return node[key];
}
if (typeof node[key] !== "boolean" && isSelected(node[key], id)) {
if (typeof node[key] !== 'boolean' && isSelected(node[key], id)) {
return true;
}
return false;
@@ -43,7 +48,7 @@ export function toggle(node: LayerTreeType, id: string) {
Object.keys(node).forEach((key) => {
if (key === id) {
node[key] = !node[key];
} else if (typeof node[key] !== "boolean") {
} else if (typeof node[key] !== 'boolean') {
toggle(node[key], id);
}
});

View File

@@ -1,5 +1,5 @@
import { resetCursor, setCrosshairCursor } from "$lib/utils";
import type mapboxgl from "mapbox-gl";
import { resetCursor, setCrosshairCursor } from '$lib/utils';
import type mapboxgl from 'mapbox-gl';
export class GoogleRedirect {
map: mapboxgl.Map;

View File

@@ -1,16 +1,19 @@
import mapboxgl from "mapbox-gl";
import { Viewer } from 'mapillary-js/dist/mapillary.module';
import mapboxgl, { type LayerSpecification, type VectorSourceSpecification } from 'mapbox-gl';
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
import 'mapillary-js/dist/mapillary.css';
import { resetCursor, setPointerCursor } from "$lib/utils";
import { resetCursor, setPointerCursor } from '$lib/utils';
import type { Writable } from 'svelte/store';
const mapillarySource = {
const mapillarySource: VectorSourceSpecification = {
type: 'vector',
tiles: ['https://tiles.mapillary.com/maps/vtp/mly1_computed_public/2/{z}/{x}/{y}?access_token=MLY|4381405525255083|3204871ec181638c3c31320490f03011'],
tiles: [
'https://tiles.mapillary.com/maps/vtp/mly1_computed_public/2/{z}/{x}/{y}?access_token=MLY|4381405525255083|3204871ec181638c3c31320490f03011',
],
minzoom: 6,
maxzoom: 14,
};
const mapillarySequenceLayer = {
const mapillarySequenceLayer: LayerSpecification = {
id: 'mapillary-sequence',
type: 'line',
source: 'mapillary',
@@ -26,7 +29,7 @@ const mapillarySequenceLayer = {
},
};
const mapillaryImageLayer = {
const mapillaryImageLayer: LayerSpecification = {
id: 'mapillary-image',
type: 'circle',
source: 'mapillary',
@@ -40,35 +43,56 @@ const mapillaryImageLayer = {
export class MapillaryLayer {
map: mapboxgl.Map;
popup: mapboxgl.Popup;
marker: mapboxgl.Marker;
viewer: Viewer;
active = false;
popupOpen: Writable<boolean>;
addBinded = this.add.bind(this);
onMouseEnterBinded = this.onMouseEnter.bind(this);
onMouseLeaveBinded = this.onMouseLeave.bind(this);
constructor(map: mapboxgl.Map, container: HTMLElement) {
constructor(map: mapboxgl.Map, container: HTMLElement, popupOpen: Writable<boolean>) {
this.map = map;
this.viewer = new Viewer({
accessToken: 'MLY|4381405525255083|3204871ec181638c3c31320490f03011',
container,
});
container.classList.remove('hidden');
this.popup = new mapboxgl.Popup({
closeButton: false,
maxWidth: container.style.width,
}).setDOMContent(container);
const element = document.createElement('div');
element.className = 'mapboxgl-user-location mapboxgl-user-location-show-heading';
const dot = document.createElement('div');
dot.className = 'mapboxgl-user-location-dot';
const heading = document.createElement('div');
heading.className = 'mapboxgl-user-location-heading';
element.appendChild(dot);
element.appendChild(heading);
this.marker = new mapboxgl.Marker({
rotationAlignment: 'map',
element,
});
this.viewer.on('position', async () => {
if (this.popup.isOpen()) {
if (this.active) {
popupOpen.set(true);
let latLng = await this.viewer.getPosition();
this.popup.setLngLat(latLng);
if (!this.map.getBounds().contains(latLng)) {
this.marker.setLngLat(latLng).addTo(this.map);
if (!this.map.getBounds()?.contains(latLng)) {
this.map.panTo(latLng);
}
}
});
this.viewer.on('bearing', (e: ViewerBearingEvent) => {
if (this.active) {
this.marker.setRotation(e.bearing);
}
});
this.popupOpen = popupOpen;
}
add() {
@@ -101,15 +125,19 @@ export class MapillaryLayer {
this.map.removeSource('mapillary');
}
this.popup.remove();
this.marker.remove();
this.popupOpen.set(false);
}
closePopup() {
this.popup.remove();
this.active = false;
this.marker.remove();
this.popupOpen.set(false);
}
onMouseEnter(e: mapboxgl.MapLayerMouseEvent) {
this.popup.addTo(this.map).setLngLat(e.lngLat);
onMouseEnter(e: mapboxgl.MapMouseEvent) {
this.active = true;
this.viewer.resize();
this.viewer.moveTo(e.features[0].properties.id);

View File

@@ -8,16 +8,18 @@
import { map, streetViewEnabled } from '$lib/stores';
import { settings } from '$lib/db';
import { _ } from 'svelte-i18n';
import { writable } from 'svelte/store';
const { streetViewSource } = settings;
let googleRedirect: GoogleRedirect;
let mapillaryLayer: MapillaryLayer;
let mapillaryOpen = writable(false);
let container: HTMLElement;
$: if ($map) {
googleRedirect = new GoogleRedirect($map);
mapillaryLayer = new MapillaryLayer($map, container);
mapillaryLayer = new MapillaryLayer($map, container, mapillaryOpen);
}
$: if (mapillaryLayer) {
@@ -53,7 +55,9 @@
<div
bind:this={container}
class="hidden relative w-[50vw] h-[40vh] rounded-md border-background border-2"
class="{$mapillaryOpen
? ''
: 'hidden'} !absolute bottom-[44px] right-2.5 z-10 w-[40%] h-[40%] bg-background rounded-md overflow-hidden border-background border-2"
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->

View File

@@ -10,7 +10,7 @@
MapPin,
Filter,
Scissors,
MountainSnow
MountainSnow,
} from 'lucide-svelte';
import { _ } from 'svelte-i18n';

View File

@@ -24,7 +24,7 @@
onMount(() => {
popup = new mapboxgl.Popup({
closeButton: false,
maxWidth: undefined
maxWidth: undefined,
});
popup.setDOMContent(popupElement);
popupElement.classList.remove('hidden');

View File

@@ -1,7 +1,7 @@
<script lang="ts" context="module">
enum CleanType {
INSIDE = 'inside',
OUTSIDE = 'outside'
OUTSIDE = 'outside',
}
</script>
@@ -41,10 +41,10 @@
[rectangleCoordinates[1].lng, rectangleCoordinates[0].lat],
[rectangleCoordinates[1].lng, rectangleCoordinates[1].lat],
[rectangleCoordinates[0].lng, rectangleCoordinates[1].lat],
[rectangleCoordinates[0].lng, rectangleCoordinates[0].lat]
]
]
}
[rectangleCoordinates[0].lng, rectangleCoordinates[0].lat],
],
],
},
};
let source = $map.getSource('rectangle');
if (source) {
@@ -52,7 +52,7 @@
} else {
$map.addSource('rectangle', {
type: 'geojson',
data: data
data: data,
});
}
if (!$map.getLayer('rectangle')) {
@@ -62,8 +62,8 @@
source: 'rectangle',
paint: {
'fill-color': 'SteelBlue',
'fill-opacity': 0.5
}
'fill-opacity': 0.5,
},
});
}
}
@@ -161,12 +161,12 @@
[
{
lat: Math.min(rectangleCoordinates[0].lat, rectangleCoordinates[1].lat),
lon: Math.min(rectangleCoordinates[0].lng, rectangleCoordinates[1].lng)
lon: Math.min(rectangleCoordinates[0].lng, rectangleCoordinates[1].lng),
},
{
lat: Math.max(rectangleCoordinates[0].lat, rectangleCoordinates[1].lat),
lon: Math.max(rectangleCoordinates[0].lng, rectangleCoordinates[1].lng)
}
lon: Math.max(rectangleCoordinates[0].lng, rectangleCoordinates[1].lng),
},
],
cleanType === CleanType.INSIDE,
deleteTrackpoints,

View File

@@ -7,7 +7,7 @@
ListTrackItem,
ListTrackSegmentItem,
ListWaypointItem,
ListWaypointsItem
ListWaypointsItem,
} from '$lib/components/file-list/FileList';
import Help from '$lib/components/Help.svelte';
import { dbUtils, getFile } from '$lib/db';

View File

@@ -1,7 +1,7 @@
<script lang="ts" context="module">
enum MergeType {
TRACES = 'traces',
CONTENTS = 'contents'
CONTENTS = 'contents',
}
</script>
@@ -11,15 +11,18 @@
import { selection } from '$lib/components/file-list/Selection';
import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label/index.js';
import { Checkbox } from '$lib/components/ui/checkbox';
import * as RadioGroup from '$lib/components/ui/radio-group';
import { _, locale } from 'svelte-i18n';
import { dbUtils, getFile } from '$lib/db';
import { Group } from 'lucide-svelte';
import { getURLForLanguage } from '$lib/utils';
import Shortcut from '$lib/components/Shortcut.svelte';
import { gpxStatistics } from '$lib/stores';
let canMergeTraces = false;
let canMergeContents = false;
let removeGaps = false;
$: if ($selection.size > 1) {
canMergeTraces = true;
@@ -56,22 +59,31 @@
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
<RadioGroup.Root bind:value={mergeType}>
<Label class="flex flex-row items-center gap-2 leading-5">
<Label class="flex flex-row items-center gap-1.5 leading-5">
<RadioGroup.Item value={MergeType.TRACES} />
{$_('toolbar.merge.merge_traces')}
</Label>
<Label class="flex flex-row items-center gap-2 leading-5">
<Label class="flex flex-row items-center gap-1.5 leading-5">
<RadioGroup.Item value={MergeType.CONTENTS} />
{$_('toolbar.merge.merge_contents')}
</Label>
</RadioGroup.Root>
{#if mergeType === MergeType.TRACES && $gpxStatistics.global.time.total > 0}
<div class="flex flex-row items-center gap-1.5">
<Checkbox id="remove-gaps" bind:checked={removeGaps} />
<Label for="remove-gaps">{$_('toolbar.merge.remove_gaps')}</Label>
</div>
{/if}
<Button
variant="outline"
class="whitespace-normal h-fit"
disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
(mergeType === MergeType.CONTENTS && !canMergeContents)}
on:click={() => {
dbUtils.mergeSelection(mergeType === MergeType.TRACES);
dbUtils.mergeSelection(
mergeType === MergeType.TRACES,
mergeType === MergeType.TRACES && $gpxStatistics.global.time.total > 0 && removeGaps
);
}}
>
<Group size="16" class="mr-1 shrink-0" />

View File

@@ -3,7 +3,11 @@
import { Button } from '$lib/components/ui/button';
import { Slider } from '$lib/components/ui/slider';
import { selection } from '$lib/components/file-list/Selection';
import { ListItem, ListRootItem, ListTrackSegmentItem } from '$lib/components/file-list/FileList';
import {
ListItem,
ListRootItem,
ListTrackSegmentItem,
} from '$lib/components/file-list/FileList';
import Help from '$lib/components/Help.svelte';
import { Filter } from 'lucide-svelte';
import { _, locale } from 'svelte-i18n';
@@ -18,10 +22,13 @@
let sliderValue = [50];
let maxPoints = 0;
let currentPoints = 0;
const minTolerance = 0.1;
const maxTolerance = 10000;
$: validSelection = $selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']);
$: tolerance = 2 ** (sliderValue[0] / (100 / Math.log2(10000)));
$: tolerance =
minTolerance * 2 ** (sliderValue[0] / (100 / Math.log2(maxTolerance / minTolerance)));
let simplified = new Map<string, [ListItem, number, SimplifiedTrackPoint[]]>();
let unsubscribes = new Map<string, () => void>();
@@ -32,7 +39,7 @@
let data: GeoJSON.FeatureCollection = {
type: 'FeatureCollection',
features: []
features: [],
};
simplified.forEach(([item, maxPts, points], itemFullId) => {
@@ -49,10 +56,10 @@
type: 'LineString',
coordinates: current.map((point) => [
point.point.getLongitude(),
point.point.getLatitude()
])
point.point.getLatitude(),
]),
},
properties: {}
properties: {},
});
});
@@ -63,7 +70,7 @@
} else {
$map.addSource('simplified', {
type: 'geojson',
data: data
data: data,
});
}
if (!$map.getLayer('simplified')) {
@@ -73,8 +80,8 @@
source: 'simplified',
paint: {
'line-color': 'white',
'line-width': 3
}
'line-width': 3,
},
});
} else {
$map.moveLayer('simplified');
@@ -91,17 +98,23 @@
});
$fileObservers.forEach((fileStore, fileId) => {
if (!unsubscribes.has(fileId)) {
let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [fs, sel]).subscribe(
([fs, sel]) => {
let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [
fs,
sel,
]).subscribe(([fs, sel]) => {
if (fs) {
fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
let segmentItem = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex);
let segmentItem = new ListTrackSegmentItem(
fileId,
trackIndex,
segmentIndex
);
if (sel.hasAnyParent(segmentItem)) {
let statistics = fs.statistics.getStatisticsFor(segmentItem);
simplified.set(segmentItem.getFullId(), [
segmentItem,
statistics.local.points.length,
ramerDouglasPeucker(statistics.local.points, 1)
ramerDouglasPeucker(statistics.local.points, minTolerance),
]);
update();
} else if (simplified.has(segmentItem.getFullId())) {
@@ -110,8 +123,7 @@
}
});
}
}
);
});
unsubscribes.set(fileId, unsubscribe);
}
});
@@ -154,7 +166,7 @@
</div>
<Label class="flex flex-row justify-between">
<span>{$_('toolbar.reduce.tolerance')}</span>
<WithUnits value={tolerance / 1000} type="distance" decimals={3} class="font-normal" />
<WithUnits value={tolerance / 1000} type="distance" decimals={4} class="font-normal" />
</Label>
<Label class="flex flex-row justify-between">
<span>{$_('toolbar.reduce.number_of_points')}</span>

View File

@@ -11,7 +11,7 @@
distancePerHourToSecondsPerDistance,
getConvertedVelocity,
milesToKilometers,
nauticalMilesToKilometers
nauticalMilesToKilometers,
} from '$lib/units';
import { CalendarDate, type DateValue } from '@internationalized/date';
import { CalendarClock, CirclePlay, CircleStop, CircleX, Timer, Zap } from 'lucide-svelte';
@@ -23,7 +23,7 @@
ListFileItem,
ListRootItem,
ListTrackItem,
ListTrackSegmentItem
ListTrackSegmentItem,
} from '$lib/components/file-list/FileList';
import Help from '$lib/components/Help.svelte';
import { getURLForLanguage } from '$lib/utils';
@@ -69,14 +69,14 @@
endDate = undefined;
endTime = undefined;
}
if ($gpxStatistics.global.time.moving) {
if ($gpxStatistics.global.time.moving && $gpxStatistics.global.speed.moving) {
movingTime = $gpxStatistics.global.time.moving;
setSpeed($gpxStatistics.global.speed.moving);
} else if ($gpxStatistics.global.time.total && $gpxStatistics.global.speed.total) {
movingTime = $gpxStatistics.global.time.total;
setSpeed($gpxStatistics.global.speed.total);
} else {
movingTime = undefined;
}
if ($gpxStatistics.global.speed.moving) {
setSpeed($gpxStatistics.global.speed.moving);
} else {
speed = undefined;
}
}
@@ -305,7 +305,11 @@
class="grow whitespace-normal h-fit"
on:click={() => {
let effectiveSpeed = getSpeed();
if (startDate === undefined || startTime === undefined || effectiveSpeed === undefined) {
if (
startDate === undefined ||
startTime === undefined ||
effectiveSpeed === undefined
) {
return;
}
@@ -325,13 +329,20 @@
let fileId = item.getFileId();
dbUtils.applyToFile(fileId, (file) => {
if (item instanceof ListFileItem) {
if (artificial) {
file.createArtificialTimestamps(getDate(startDate, startTime), movingTime);
if (artificial || !$gpxStatistics.global.time.moving) {
file.createArtificialTimestamps(
getDate(startDate, startTime),
movingTime
);
} else {
file.changeTimestamps(getDate(startDate, startTime), effectiveSpeed, ratio);
file.changeTimestamps(
getDate(startDate, startTime),
effectiveSpeed,
ratio
);
}
} else if (item instanceof ListTrackItem) {
if (artificial) {
if (artificial || !$gpxStatistics.global.time.moving) {
file.createArtificialTimestamps(
getDate(startDate, startTime),
movingTime,
@@ -346,7 +357,7 @@
);
}
} else if (item instanceof ListTrackSegmentItem) {
if (artificial) {
if (artificial || !$gpxStatistics.global.time.moving) {
file.createArtificialTimestamps(
getDate(startDate, startTime),
movingTime,

View File

@@ -31,14 +31,14 @@
let selectedSymbol = {
value: '',
label: ''
label: '',
};
const { verticalFileView } = settings;
const { treeFileView } = settings;
$: canCreate = $selection.size > 0;
$: if ($verticalFileView && $selection) {
$: if ($treeFileView && $selection) {
selectedWaypoint.update(() => {
if ($selection.size === 1) {
let item = $selection.getSelected()[0];
@@ -74,12 +74,12 @@
if (symbolKey) {
selectedSymbol = {
value: symbol,
label: $_(`gpx.symbol.${symbolKey}`)
label: $_(`gpx.symbol.${symbolKey}`),
};
} else {
selectedSymbol = {
value: symbol,
label: ''
label: '',
};
}
longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6));
@@ -99,7 +99,7 @@
link = '';
selectedSymbol = {
value: '',
label: ''
label: '',
};
longitude = 0;
latitude = 0;
@@ -134,13 +134,13 @@
{
attributes: {
lat: latitude,
lon: longitude
lon: longitude,
},
name: name.length > 0 ? name : undefined,
desc: description.length > 0 ? description : undefined,
cmt: description.length > 0 ? description : undefined,
link: link.length > 0 ? { attributes: { href: link } } : undefined,
sym: selectedSymbol.value.length > 0 ? selectedSymbol.value : undefined
sym: selectedSymbol.value.length > 0 ? selectedSymbol.value : undefined,
},
$selectedWaypoint
? new ListWaypointItem($selectedWaypoint[1], $selectedWaypoint[0]._data.index)
@@ -195,7 +195,11 @@
/>
<Label for="symbol">{$_('toolbar.waypoint.icon')}</Label>
<Select.Root bind:selected={selectedSymbol}>
<Select.Trigger id="symbol" class="w-full h-8" disabled={!canCreate && !$selectedWaypoint}>
<Select.Trigger
id="symbol"
class="w-full h-8"
disabled={!canCreate && !$selectedWaypoint}
>
<Select.Value />
</Select.Trigger>
<Select.Content class="max-h-60 overflow-y-scroll">
@@ -218,7 +222,12 @@
</Select.Content>
</Select.Root>
<Label for="link">{$_('toolbar.waypoint.link')}</Label>
<Input bind:value={link} id="link" class="h-8" disabled={!canCreate && !$selectedWaypoint} />
<Input
bind:value={link}
id="link"
class="h-8"
disabled={!canCreate && !$selectedWaypoint}
/>
<div class="flex flex-row gap-2">
<div class="grow">
<Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label>

View File

@@ -19,7 +19,7 @@
RouteOff,
Repeat,
SquareArrowUpLeft,
SquareArrowOutDownRight
SquareArrowOutDownRight,
} from 'lucide-svelte';
import { map, newGPXFile, routingControls, selectFileWhenLoaded } from '$lib/stores';
@@ -37,7 +37,7 @@
ListRootItem,
ListTrackItem,
ListTrackSegmentItem,
type ListItem
type ListItem,
} from '$lib/components/file-list/FileList';
import { flyAndScale, getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
import { onDestroy, onMount } from 'svelte';
@@ -68,7 +68,10 @@
// add controls for new files
$fileObservers.forEach((file, fileId) => {
if (!routingControls.has(fileId)) {
routingControls.set(fileId, new RoutingControls($map, fileId, file, popup, popupElement));
routingControls.set(
fileId,
new RoutingControls($map, fileId, file, popup, popupElement)
);
}
});
}
@@ -82,9 +85,9 @@
new TrackPoint({
attributes: {
lat: e.lngLat.lat,
lon: e.lngLat.lng
}
})
lon: e.lngLat.lng,
},
}),
]);
file._data.id = getFileIds(1)[0];
dbUtils.add(file);
@@ -195,7 +198,8 @@
if (selected[0] instanceof ListFileItem) {
return firstFile.trk[0]?.trkseg[0]?.trkpt[0];
} else if (selected[0] instanceof ListTrackItem) {
return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[0]?.trkpt[0];
return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[0]
?.trkpt[0];
} else if (selected[0] instanceof ListTrackSegmentItem) {
return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[
selected[0].getSegmentIndex()

View File

@@ -1,10 +1,9 @@
import type { Coordinates } from "gpx";
import { TrackPoint, distance } from "gpx";
import { derived, get, writable } from "svelte/store";
import { settings } from "$lib/db";
import { _, isLoading, locale } from "svelte-i18n";
import { map } from "$lib/stores";
import { getElevation } from "$lib/utils";
import type { Coordinates } from 'gpx';
import { TrackPoint, distance } from 'gpx';
import { derived, get, writable } from 'svelte/store';
import { settings } from '$lib/db';
import { _, isLoading, locale } from 'svelte-i18n';
import { getElevation } from '$lib/utils';
const { routing, routingProfile, privateRoads } = settings;
@@ -16,22 +15,31 @@ export const brouterProfiles: { [key: string]: string } = {
foot: 'Hiking-Alpine-SAC6',
motorcycle: 'Car-FastEco',
water: 'river',
railway: 'rail'
railway: 'rail',
};
export const routingProfileSelectItem = writable({
value: '',
label: ''
label: '',
});
derived([routingProfile, locale, isLoading], ([profile, l, i]) => [profile, l, i]).subscribe(([profile, l, i]) => {
if (!i && profile !== '' && (profile !== get(routingProfileSelectItem).value || get(_)(`toolbar.routing.activities.${profile}`) !== get(routingProfileSelectItem).label) && l !== null) {
derived([routingProfile, locale, isLoading], ([profile, l, i]) => [profile, l, i]).subscribe(
([profile, l, i]) => {
if (
!i &&
profile !== '' &&
(profile !== get(routingProfileSelectItem).value ||
get(_)(`toolbar.routing.activities.${profile}`) !==
get(routingProfileSelectItem).label) &&
l !== null
) {
routingProfileSelectItem.update((item) => {
item.value = profile;
item.label = get(_)(`toolbar.routing.activities.${profile}`);
return item;
});
}
});
}
);
routingProfileSelectItem.subscribe((item) => {
if (item.value !== '' && item.value !== get(routingProfile)) {
routingProfile.set(item.value);
@@ -46,8 +54,12 @@ export function route(points: Coordinates[]): Promise<TrackPoint[]> {
}
}
async function getRoute(points: Coordinates[], brouterProfile: string, privateRoads: boolean): Promise<TrackPoint[]> {
let url = `https://routing.gpx.studio?lonlats=${points.map(point => `${point.lon.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile + (privateRoads ? '-private' : '')}&format=geojson&alternativeidx=0`;
async function getRoute(
points: Coordinates[],
brouterProfile: string,
privateRoads: boolean
): 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(url);
@@ -62,71 +74,81 @@ async function getRoute(points: Coordinates[], brouterProfile: string, privateRo
let coordinates = geojson.features[0].geometry.coordinates;
let messages = geojson.features[0].properties.messages;
const lngIdx = messages[0].indexOf("Longitude");
const latIdx = messages[0].indexOf("Latitude");
const tagIdx = messages[0].indexOf("WayTags");
const lngIdx = messages[0].indexOf('Longitude');
const latIdx = messages[0].indexOf('Latitude');
const tagIdx = messages[0].indexOf('WayTags');
let messageIdx = 1;
let surface = messageIdx < messages.length ? getSurface(messages[messageIdx][tagIdx]) : undefined;
let tags = messageIdx < messages.length ? getTags(messages[messageIdx][tagIdx]) : {};
for (let i = 0; i < coordinates.length; i++) {
let coord = coordinates[i];
route.push(new TrackPoint({
route.push(
new TrackPoint({
attributes: {
lat: coord[1],
lon: coord[0]
lon: coord[0],
},
ele: coord[2] ?? (i > 0 ? route[i - 1].ele : 0)
}));
ele: coord[2] ?? (i > 0 ? route[i - 1].ele : 0),
})
);
if (messageIdx < messages.length &&
if (
messageIdx < messages.length &&
coordinates[i][0] == Number(messages[messageIdx][lngIdx]) / 1000000 &&
coordinates[i][1] == Number(messages[messageIdx][latIdx]) / 1000000) {
coordinates[i][1] == Number(messages[messageIdx][latIdx]) / 1000000
) {
messageIdx++;
if (messageIdx == messages.length) surface = undefined;
else surface = getSurface(messages[messageIdx][tagIdx]);
if (messageIdx == messages.length) tags = {};
else tags = getTags(messages[messageIdx][tagIdx]);
}
if (surface) {
route[route.length - 1].setSurface(surface);
}
route[route.length - 1].setExtensions(tags);
}
return route;
}
function getSurface(message: string): string | undefined {
const fields = message.split(" ");
for (let i = 0; i < fields.length; i++) if (fields[i].startsWith("surface=")) {
return fields[i].substring(8);
function getTags(message: string): { [key: string]: string } {
const fields = message.split(' ');
let tags: { [key: string]: string } = {};
for (let i = 0; i < fields.length; i++) {
let [key, value] = fields[i].split('=');
key = key.replace(/:/g, '_');
tags[key] = value;
}
return tags;
}
return undefined;
};
function getIntermediatePoints(points: Coordinates[]): Promise<TrackPoint[]> {
let route: TrackPoint[] = [];
let step = 0.05;
for (let i = 0; i < points.length - 1; i++) { // Add intermediate points between each pair of points
for (let i = 0; i < points.length - 1; i++) {
// Add intermediate points between each pair of points
let dist = distance(points[i], points[i + 1]) / 1000;
for (let d = 0; d < dist; d += step) {
let lat = points[i].lat + d / dist * (points[i + 1].lat - points[i].lat);
let lon = points[i].lon + d / dist * (points[i + 1].lon - points[i].lon);
route.push(new TrackPoint({
let lat = points[i].lat + (d / dist) * (points[i + 1].lat - points[i].lat);
let lon = points[i].lon + (d / dist) * (points[i + 1].lon - points[i].lon);
route.push(
new TrackPoint({
attributes: {
lat: lat,
lon: lon
}
}));
lon: lon,
},
})
);
}
}
route.push(new TrackPoint({
route.push(
new TrackPoint({
attributes: {
lat: points[points.length - 1].lat,
lon: points[points.length - 1].lon
}
}));
lon: points[points.length - 1].lon,
},
})
);
return getElevation(route).then((elevations) => {
route.forEach((point, i) => {

View File

@@ -1,17 +1,20 @@
import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from "gpx";
import { get, writable, type Readable } from "svelte/store";
import mapboxgl from "mapbox-gl";
import { route } from "./Routing";
import { toast } from "svelte-sonner";
import { _ } from "svelte-i18n";
import { dbUtils, type GPXFileWithStatistics } from "$lib/db";
import { getOrderedSelection, selection } from "$lib/components/file-list/Selection";
import { ListFileItem, ListTrackItem, ListTrackSegmentItem } from "$lib/components/file-list/FileList";
import { currentTool, streetViewEnabled, Tool } from "$lib/stores";
import { getClosestLinePoint, resetCursor, setGrabbingCursor } from "$lib/utils";
import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from 'gpx';
import { get, writable, type Readable } from 'svelte/store';
import mapboxgl from 'mapbox-gl';
import { route } from './Routing';
import { toast } from 'svelte-sonner';
import { _ } from 'svelte-i18n';
import { dbUtils, settings, type GPXFileWithStatistics } from '$lib/db';
import { getOrderedSelection, selection } from '$lib/components/file-list/Selection';
import {
ListFileItem,
ListTrackItem,
ListTrackSegmentItem,
} from '$lib/components/file-list/FileList';
import { currentTool, streetViewEnabled, Tool } from '$lib/stores';
import { getClosestLinePoint, resetCursor, setGrabbingCursor } from '$lib/utils';
const { streetViewSource } = settings;
export const canChangeStart = writable(false);
function stopPropagation(e: any) {
@@ -32,12 +35,19 @@ export class RoutingControls {
fileUnsubscribe: () => void = () => {};
unsubscribes: Function[] = [];
toggleAnchorsForZoomLevelAndBoundsBinded: () => void = this.toggleAnchorsForZoomLevelAndBounds.bind(this);
toggleAnchorsForZoomLevelAndBoundsBinded: () => void =
this.toggleAnchorsForZoomLevelAndBounds.bind(this);
showTemporaryAnchorBinded: (e: any) => void = this.showTemporaryAnchor.bind(this);
updateTemporaryAnchorBinded: (e: any) => void = this.updateTemporaryAnchor.bind(this);
appendAnchorBinded: (e: mapboxgl.MapMouseEvent) => void = this.appendAnchor.bind(this);
constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFileWithStatistics | undefined>, popup: mapboxgl.Popup, popupElement: HTMLElement) {
constructor(
map: mapboxgl.Map,
fileId: string,
file: Readable<GPXFileWithStatistics | undefined>,
popup: mapboxgl.Popup,
popupElement: HTMLElement
) {
this.map = map;
this.fileId = fileId;
this.file = file;
@@ -47,8 +57,8 @@ export class RoutingControls {
let point = new TrackPoint({
attributes: {
lat: 0,
lon: 0
}
lon: 0,
},
});
this.temporaryAnchor = this.createAnchor(point, new TrackSegment(), 0, 0);
this.temporaryAnchor.marker.getElement().classList.remove('z-10'); // Show below the other markers
@@ -66,7 +76,9 @@ export class RoutingControls {
return;
}
let selected = get(selection).hasAnyChildren(new ListFileItem(this.fileId), true, ['waypoints']);
let selected = get(selection).hasAnyChildren(new ListFileItem(this.fileId), true, [
'waypoints',
]);
if (selected) {
if (this.active) {
this.updateControls();
@@ -89,7 +101,8 @@ export class RoutingControls {
this.fileUnsubscribe = this.file.subscribe(this.updateControls.bind(this));
}
updateControls() { // Update the markers when the file changes
updateControls() {
// Update the markers when the file changes
let file = get(this.file)?.file;
if (!file) {
return;
@@ -97,8 +110,13 @@ export class RoutingControls {
let anchorIndex = 0;
file.forEachSegment((segment, trackIndex, segmentIndex) => {
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
for (let point of segment.trkpt) { // Update the existing anchors (could be improved by matching the existing anchors with the new ones?)
if (
get(selection).hasAnyParent(
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
)
) {
for (let point of segment.trkpt) {
// Update the existing anchors (could be improved by matching the existing anchors with the new ones?)
if (point._data.anchor) {
if (anchorIndex < this.anchors.length) {
this.anchors[anchorIndex].point = point;
@@ -107,7 +125,9 @@ export class RoutingControls {
this.anchors[anchorIndex].segmentIndex = segmentIndex;
this.anchors[anchorIndex].marker.setLngLat(point.getCoordinates());
} else {
this.anchors.push(this.createAnchor(point, segment, trackIndex, segmentIndex));
this.anchors.push(
this.createAnchor(point, segment, trackIndex, segmentIndex)
);
}
anchorIndex++;
}
@@ -115,7 +135,8 @@ export class RoutingControls {
}
});
while (anchorIndex < this.anchors.length) { // Remove the extra anchors
while (anchorIndex < this.anchors.length) {
// Remove the extra anchors
this.anchors.pop()?.marker.remove();
}
@@ -142,14 +163,19 @@ export class RoutingControls {
this.map = map;
}
createAnchor(point: TrackPoint, segment: TrackSegment, trackIndex: number, segmentIndex: number): AnchorWithMarker {
createAnchor(
point: TrackPoint,
segment: TrackSegment,
trackIndex: number,
segmentIndex: number
): AnchorWithMarker {
let element = document.createElement('div');
element.className = `h-5 w-5 xs:h-4 xs:w-4 md:h-3 md:w-3 rounded-full bg-white border-2 border-black cursor-pointer`;
let marker = new mapboxgl.Marker({
draggable: true,
className: 'z-10',
element
element,
}).setLngLat(point.getCoordinates());
let anchor = {
@@ -158,7 +184,7 @@ export class RoutingControls {
trackIndex,
segmentIndex,
marker,
inZoom: false
inZoom: false,
};
marker.on('dragstart', (e) => {
@@ -186,7 +212,8 @@ export class RoutingControls {
e.preventDefault();
e.stopPropagation();
if (Date.now() - this.lastDragEvent < 100) { // Prevent click event during drag
if (Date.now() - this.lastDragEvent < 100) {
// Prevent click event during drag
return;
}
@@ -205,7 +232,12 @@ export class RoutingControls {
return false;
}
let segment = anchor.segment;
if (distance(segment.trkpt[0].getCoordinates(), segment.trkpt[segment.trkpt.length - 1].getCoordinates()) > 1000) {
if (
distance(
segment.trkpt[0].getCoordinates(),
segment.trkpt[segment.trkpt.length - 1].getCoordinates()
) > 1000
) {
return false;
}
return true;
@@ -225,7 +257,8 @@ export class RoutingControls {
};
}
toggleAnchorsForZoomLevelAndBounds() { // Show markers only if they are in the current zoom level and bounds
toggleAnchorsForZoomLevelAndBounds() {
// Show markers only if they are in the current zoom level and bounds
this.shownAnchors.splice(0, this.shownAnchors.length);
let center = this.map.getCenter();
@@ -246,7 +279,8 @@ export class RoutingControls {
}
showTemporaryAnchor(e: any) {
if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) { // Do not not change the source point if it is already being dragged
if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) {
// Do not not change the source point if it is already being dragged
return;
}
@@ -254,7 +288,15 @@ export class RoutingControls {
return;
}
if (!get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, e.features[0].properties.trackIndex, e.features[0].properties.segmentIndex))) {
if (
!get(selection).hasAnyParent(
new ListTrackSegmentItem(
this.fileId,
e.features[0].properties.trackIndex,
e.features[0].properties.segmentIndex
)
)
) {
return;
}
@@ -264,7 +306,7 @@ export class RoutingControls {
this.temporaryAnchor.point.setCoordinates({
lat: e.lngLat.lat,
lon: e.lngLat.lng
lon: e.lngLat.lng,
});
this.temporaryAnchor.marker.setLngLat(e.lngLat).addTo(this.map);
@@ -272,12 +314,17 @@ export class RoutingControls {
}
updateTemporaryAnchor(e: any) {
if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) { // Do not hide if it is being dragged, and stop listening for mousemove
if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) {
// Do not hide if it is being dragged, and stop listening for mousemove
this.map.off('mousemove', this.updateTemporaryAnchorBinded);
return;
}
if (e.point.dist(this.map.project(this.temporaryAnchor.point.getCoordinates())) > 20 || this.temporaryAnchorCloseToOtherAnchor(e)) { // Hide if too far from the layer
if (
e.point.dist(this.map.project(this.temporaryAnchor.point.getCoordinates())) > 20 ||
this.temporaryAnchorCloseToOtherAnchor(e)
) {
// Hide if too far from the layer
this.temporaryAnchor.marker.remove();
this.map.off('mousemove', this.updateTemporaryAnchorBinded);
return;
@@ -295,14 +342,16 @@ export class RoutingControls {
return false;
}
async moveAnchor(anchorWithMarker: AnchorWithMarker) { // Move the anchor and update the route from and to the neighbouring anchors
async moveAnchor(anchorWithMarker: AnchorWithMarker) {
// Move the anchor and update the route from and to the neighbouring anchors
let coordinates = {
lat: anchorWithMarker.marker.getLngLat().lat,
lon: anchorWithMarker.marker.getLngLat().lng
lon: anchorWithMarker.marker.getLngLat().lng,
};
let anchor = anchorWithMarker as Anchor;
if (anchorWithMarker === this.temporaryAnchor) { // Temporary anchor, need to find the closest point of the segment and create an anchor for it
if (anchorWithMarker === this.temporaryAnchor) {
// Temporary anchor, need to find the closest point of the segment and create an anchor for it
this.temporaryAnchor.marker.remove();
anchor = this.getPermanentAnchor();
}
@@ -327,7 +376,8 @@ export class RoutingControls {
let success = await this.routeBetweenAnchors(anchors, targetCoordinates);
if (!success) { // Route failed, revert the anchor to the previous position
if (!success) {
// Route failed, revert the anchor to the previous position
anchorWithMarker.marker.setLngLat(anchorWithMarker.point.getCoordinates());
}
}
@@ -339,16 +389,24 @@ export class RoutingControls {
let minDetails: any = { distance: Number.MAX_VALUE };
let minAnchor = this.temporaryAnchor as Anchor;
file?.forEachSegment((segment, trackIndex, segmentIndex) => {
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
if (
get(selection).hasAnyParent(
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
)
) {
let details: any = {};
let closest = getClosestLinePoint(segment.trkpt, this.temporaryAnchor.point, details);
let closest = getClosestLinePoint(
segment.trkpt,
this.temporaryAnchor.point,
details
);
if (details.distance < minDetails.distance) {
minDetails = details;
minAnchor = {
point: closest,
segment,
trackIndex,
segmentIndex
segmentIndex,
};
}
}
@@ -375,41 +433,67 @@ export class RoutingControls {
point: this.temporaryAnchor.point,
trackIndex: -1,
segmentIndex: -1,
trkptIndex: -1
trkptIndex: -1,
};
file?.forEachSegment((segment, trackIndex, segmentIndex) => {
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
if (
get(selection).hasAnyParent(
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
)
) {
let details: any = {};
getClosestLinePoint(segment.trkpt, this.temporaryAnchor.point, details);
if (details.distance < minDetails.distance) {
minDetails = details;
let before = details.before ? details.index : details.index - 1;
let projectedPt = projectedPoint(segment.trkpt[before], segment.trkpt[before + 1], this.temporaryAnchor.point);
let ratio = distance(segment.trkpt[before], projectedPt) / distance(segment.trkpt[before], segment.trkpt[before + 1]);
let projectedPt = projectedPoint(
segment.trkpt[before],
segment.trkpt[before + 1],
this.temporaryAnchor.point
);
let ratio =
distance(segment.trkpt[before], projectedPt) /
distance(segment.trkpt[before], segment.trkpt[before + 1]);
let point = segment.trkpt[before].clone();
point.setCoordinates(projectedPt);
point.ele = (1 - ratio) * (segment.trkpt[before].ele ?? 0) + ratio * (segment.trkpt[before + 1].ele ?? 0);
point.time = (segment.trkpt[before].time && segment.trkpt[before + 1].time) ? new Date((1 - ratio) * segment.trkpt[before].time.getTime() + ratio * segment.trkpt[before + 1].time.getTime()) : undefined;
point.ele =
(1 - ratio) * (segment.trkpt[before].ele ?? 0) +
ratio * (segment.trkpt[before + 1].ele ?? 0);
point.time =
segment.trkpt[before].time && segment.trkpt[before + 1].time
? new Date(
(1 - ratio) * segment.trkpt[before].time.getTime() +
ratio * segment.trkpt[before + 1].time.getTime()
)
: undefined;
point._data = {
anchor: true,
zoom: 0
zoom: 0,
};
minInfo = {
point,
trackIndex,
segmentIndex,
trkptIndex: before + 1
trkptIndex: before + 1,
};
}
}
});
if (minInfo.trackIndex !== -1) {
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(minInfo.trackIndex, minInfo.segmentIndex, minInfo.trkptIndex, minInfo.trkptIndex - 1, [minInfo.point]));
dbUtils.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints(
minInfo.trackIndex,
minInfo.segmentIndex,
minInfo.trkptIndex,
minInfo.trkptIndex - 1,
[minInfo.point]
)
);
}
}
@@ -417,22 +501,46 @@ export class RoutingControls {
return () => this.deleteAnchor(anchor);
}
async deleteAnchor(anchor: Anchor) { // Remove the anchor and route between the neighbouring anchors if they exist
async deleteAnchor(anchor: Anchor) {
// Remove the anchor and route between the neighbouring anchors if they exist
this.popup.remove();
let [previousAnchor, nextAnchor] = this.getNeighbouringAnchors(anchor);
if (previousAnchor === null && nextAnchor === null) { // Only one point, remove it
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, 0, []));
} else if (previousAnchor === null) { // First point, remove trackpoints until nextAnchor
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, nextAnchor.point._data.index - 1, []));
} else if (nextAnchor === null) { // Last point, remove trackpoints from previousAnchor
if (previousAnchor === null && nextAnchor === null) {
// Only one point, remove it
dbUtils.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, 0, [])
);
} else if (previousAnchor === null) {
// First point, remove trackpoints until nextAnchor
dbUtils.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints(
anchor.trackIndex,
anchor.segmentIndex,
0,
nextAnchor.point._data.index - 1,
[]
)
);
} else if (nextAnchor === null) {
// Last point, remove trackpoints from previousAnchor
dbUtils.applyToFile(this.fileId, (file) => {
let segment = file.getSegment(anchor.trackIndex, anchor.segmentIndex);
file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, previousAnchor.point._data.index + 1, segment.trkpt.length - 1, []);
file.replaceTrackPoints(
anchor.trackIndex,
anchor.segmentIndex,
previousAnchor.point._data.index + 1,
segment.trkpt.length - 1,
[]
);
});
} else { // Route between previousAnchor and nextAnchor
this.routeBetweenAnchors([previousAnchor, nextAnchor], [previousAnchor.point.getCoordinates(), nextAnchor.point.getCoordinates()]);
} else {
// Route between previousAnchor and nextAnchor
this.routeBetweenAnchors(
[previousAnchor, nextAnchor],
[previousAnchor.point.getCoordinates(), nextAnchor.point.getCoordinates()]
);
}
}
@@ -448,27 +556,43 @@ export class RoutingControls {
return;
}
let speed = fileWithStats.statistics.getStatisticsFor(new ListTrackSegmentItem(this.fileId, anchor.trackIndex, anchor.segmentIndex)).global.speed.moving;
let speed = fileWithStats.statistics.getStatisticsFor(
new ListTrackSegmentItem(this.fileId, anchor.trackIndex, anchor.segmentIndex)
).global.speed.moving;
let segment = anchor.segment;
dbUtils.applyToFile(this.fileId, (file) => {
file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, segment.trkpt.length, segment.trkpt.length - 1, segment.trkpt.slice(0, anchor.point._data.index), speed > 0 ? speed : undefined);
file.crop(anchor.point._data.index, anchor.point._data.index + segment.trkpt.length - 1, [anchor.trackIndex], [anchor.segmentIndex]);
file.replaceTrackPoints(
anchor.trackIndex,
anchor.segmentIndex,
segment.trkpt.length,
segment.trkpt.length - 1,
segment.trkpt.slice(0, anchor.point._data.index),
speed > 0 ? speed : undefined
);
file.crop(
anchor.point._data.index,
anchor.point._data.index + segment.trkpt.length - 1,
[anchor.trackIndex],
[anchor.segmentIndex]
);
});
}
async appendAnchor(e: mapboxgl.MapMouseEvent) { // Add a new anchor to the end of the last segment
if (get(streetViewEnabled)) {
async appendAnchor(e: mapboxgl.MapMouseEvent) {
// Add a new anchor to the end of the last segment
if (get(streetViewEnabled) && get(streetViewSource) === 'google') {
return;
}
this.appendAnchorWithCoordinates({
lat: e.lngLat.lat,
lon: e.lngLat.lng
lon: e.lngLat.lng,
});
}
async appendAnchorWithCoordinates(coordinates: Coordinates) { // Add a new anchor to the end of the last segment
async appendAnchorWithCoordinates(coordinates: Coordinates) {
// Add a new anchor to the end of the last segment
let selected = getOrderedSelection();
if (selected.length === 0 || selected[selected.length - 1].getFileId() !== this.fileId) {
return;
@@ -478,7 +602,7 @@ export class RoutingControls {
let lastAnchor = this.anchors[this.anchors.length - 1];
let newPoint = new TrackPoint({
attributes: coordinates
attributes: coordinates,
});
newPoint._data.anchor = true;
newPoint._data.zoom = 0;
@@ -489,7 +613,10 @@ export class RoutingControls {
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
trackIndex = item.getTrackIndex();
}
let segmentIndex = (file.trk.length > 0 && file.trk[trackIndex].trkseg.length > 0) ? file.trk[trackIndex].trkseg.length - 1 : 0;
let segmentIndex =
file.trk.length > 0 && file.trk[trackIndex].trkseg.length > 0
? file.trk[trackIndex].trkseg.length - 1
: 0;
if (item instanceof ListTrackSegmentItem) {
segmentIndex = item.getSegmentIndex();
}
@@ -513,10 +640,13 @@ export class RoutingControls {
point: newPoint,
segment: lastAnchor.segment,
trackIndex: lastAnchor.trackIndex,
segmentIndex: lastAnchor.segmentIndex
segmentIndex: lastAnchor.segmentIndex,
};
await this.routeBetweenAnchors([lastAnchor, newAnchor], [lastAnchor.point.getCoordinates(), newAnchor.point.getCoordinates()]);
await this.routeBetweenAnchors(
[lastAnchor, newAnchor],
[lastAnchor.point.getCoordinates(), newAnchor.point.getCoordinates()]
);
}
getNeighbouringAnchors(anchor: Anchor): [Anchor | null, Anchor | null] {
@@ -526,11 +656,17 @@ export class RoutingControls {
for (let i = 0; i < this.anchors.length; i++) {
if (this.anchors[i].segment === anchor.segment && this.anchors[i].inZoom) {
if (this.anchors[i].point._data.index < anchor.point._data.index) {
if (!previousAnchor || this.anchors[i].point._data.index > previousAnchor.point._data.index) {
if (
!previousAnchor ||
this.anchors[i].point._data.index > previousAnchor.point._data.index
) {
previousAnchor = this.anchors[i];
}
} else if (this.anchors[i].point._data.index > anchor.point._data.index) {
if (!nextAnchor || this.anchors[i].point._data.index < nextAnchor.point._data.index) {
if (
!nextAnchor ||
this.anchors[i].point._data.index < nextAnchor.point._data.index
) {
nextAnchor = this.anchors[i];
}
}
@@ -540,7 +676,10 @@ export class RoutingControls {
return [previousAnchor, nextAnchor];
}
async routeBetweenAnchors(anchors: Anchor[], targetCoordinates: Coordinates[]): Promise<boolean> {
async routeBetweenAnchors(
anchors: Anchor[],
targetCoordinates: Coordinates[]
): Promise<boolean> {
let segment = anchors[0].segment;
let fileWithStats = get(this.file);
@@ -548,10 +687,15 @@ export class RoutingControls {
return false;
}
if (anchors.length === 1) { // Only one anchor, update the point in the segment
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(anchors[0].trackIndex, anchors[0].segmentIndex, 0, 0, [new TrackPoint({
if (anchors.length === 1) {
// Only one anchor, update the point in the segment
dbUtils.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints(anchors[0].trackIndex, anchors[0].segmentIndex, 0, 0, [
new TrackPoint({
attributes: targetCoordinates[0],
})]));
}),
])
);
return true;
}
@@ -560,23 +704,28 @@ export class RoutingControls {
response = await route(targetCoordinates);
} catch (e: any) {
if (e.message.includes('from-position not mapped in existing datafile')) {
toast.error(get(_)("toolbar.routing.error.from"));
toast.error(get(_)('toolbar.routing.error.from'));
} else if (e.message.includes('via1-position not mapped in existing datafile')) {
toast.error(get(_)("toolbar.routing.error.via"));
toast.error(get(_)('toolbar.routing.error.via'));
} else if (e.message.includes('to-position not mapped in existing datafile')) {
toast.error(get(_)("toolbar.routing.error.to"));
toast.error(get(_)('toolbar.routing.error.to'));
} else if (e.message.includes('Time-out')) {
toast.error(get(_)("toolbar.routing.error.timeout"));
toast.error(get(_)('toolbar.routing.error.timeout'));
} else {
toast.error(e.message);
}
return false;
}
if (anchors[0].point._data.index === 0) { // First anchor is the first point of the segment
if (anchors[0].point._data.index === 0) {
// First anchor is the first point of the segment
anchors[0].point = response[0]; // replace the first anchor
anchors[0].point._data.index = 0;
} else if (anchors[0].point._data.index === segment.trkpt.length - 1 && distance(anchors[0].point.getCoordinates(), response[0].getCoordinates()) < 1) { // First anchor is the last point of the segment, and the new point is close enough
} else if (
anchors[0].point._data.index === segment.trkpt.length - 1 &&
distance(anchors[0].point.getCoordinates(), response[0].getCoordinates()) < 1
) {
// First anchor is the last point of the segment, and the new point is close enough
anchors[0].point = response[0]; // replace the first anchor
anchors[0].point._data.index = segment.trkpt.length - 1;
} else {
@@ -584,7 +733,8 @@ export class RoutingControls {
response.splice(0, 0, anchors[0].point); // Insert it in the response to keep it
}
if (anchors[anchors.length - 1].point._data.index === segment.trkpt.length - 1) { // Last anchor is the last point of the segment
if (anchors[anchors.length - 1].point._data.index === segment.trkpt.length - 1) {
// Last anchor is the last point of the segment
anchors[anchors.length - 1].point = response[response.length - 1]; // replace the last anchor
anchors[anchors.length - 1].point._data.index = segment.trkpt.length - 1;
} else {
@@ -603,36 +753,64 @@ export class RoutingControls {
anchor.point._data.zoom = 0; // Make these anchors permanent
});
let stats = fileWithStats.statistics.getStatisticsFor(new ListTrackSegmentItem(this.fileId, anchors[0].trackIndex, anchors[0].segmentIndex));
let stats = fileWithStats.statistics.getStatisticsFor(
new ListTrackSegmentItem(this.fileId, anchors[0].trackIndex, anchors[0].segmentIndex)
);
let speed: number | undefined = undefined;
let startTime = anchors[0].point.time;
if (stats.global.speed.moving > 0) {
let replacingDistance = 0;
for (let i = 1; i < response.length; i++) {
replacingDistance += distance(response[i - 1].getCoordinates(), response[i].getCoordinates()) / 1000;
replacingDistance +=
distance(response[i - 1].getCoordinates(), response[i].getCoordinates()) / 1000;
}
let replacedDistance = stats.local.distance.moving[anchors[anchors.length - 1].point._data.index] - stats.local.distance.moving[anchors[0].point._data.index];
let replacedDistance =
stats.local.distance.moving[anchors[anchors.length - 1].point._data.index] -
stats.local.distance.moving[anchors[0].point._data.index];
let newDistance = stats.global.distance.moving + replacingDistance - replacedDistance;
let newTime = newDistance / stats.global.speed.moving * 3600;
let newTime = (newDistance / stats.global.speed.moving) * 3600;
let remainingTime = stats.global.time.moving - (stats.local.time.moving[anchors[anchors.length - 1].point._data.index] - stats.local.time.moving[anchors[0].point._data.index]);
let remainingTime =
stats.global.time.moving -
(stats.local.time.moving[anchors[anchors.length - 1].point._data.index] -
stats.local.time.moving[anchors[0].point._data.index]);
let replacingTime = newTime - remainingTime;
if (replacingTime <= 0) { // Fallback to simple time difference
replacingTime = stats.local.time.total[anchors[anchors.length - 1].point._data.index] - stats.local.time.total[anchors[0].point._data.index];
if (replacingTime <= 0) {
// Fallback to simple time difference
replacingTime =
stats.local.time.total[anchors[anchors.length - 1].point._data.index] -
stats.local.time.total[anchors[0].point._data.index];
}
speed = replacingDistance / replacingTime * 3600;
speed = (replacingDistance / replacingTime) * 3600;
if (startTime === undefined) { // Replacing the first point
if (startTime === undefined) {
// Replacing the first point
let endIndex = anchors[anchors.length - 1].point._data.index;
startTime = new Date((segment.trkpt[endIndex].time?.getTime() ?? 0) - (replacingTime + stats.local.time.total[endIndex] - stats.local.time.moving[endIndex]) * 1000);
startTime = new Date(
(segment.trkpt[endIndex].time?.getTime() ?? 0) -
(replacingTime +
stats.local.time.total[endIndex] -
stats.local.time.moving[endIndex]) *
1000
);
}
}
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(anchors[0].trackIndex, anchors[0].segmentIndex, anchors[0].point._data.index, anchors[anchors.length - 1].point._data.index, response, speed, startTime));
dbUtils.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints(
anchors[0].trackIndex,
anchors[0].segmentIndex,
anchors[0].point._data.index,
anchors[anchors.length - 1].point._data.index,
response,
speed,
startTime
)
);
return true;
}

View File

@@ -1,4 +1,4 @@
import { ramerDouglasPeucker, type GPXFile, type TrackSegment } from "gpx";
import { ramerDouglasPeucker, type GPXFile, type TrackSegment } from 'gpx';
const earthRadius = 6371008.8;
@@ -17,7 +17,8 @@ export function updateAnchorPoints(file: GPXFile) {
let segments = file.getSegments();
for (let segment of segments) {
if (!segment._data.anchors) { // New segment, compute anchor points for it
if (!segment._data.anchors) {
// New segment, compute anchor points for it
computeAnchorPoints(segment);
continue;
}
@@ -42,4 +43,3 @@ function computeAnchorPoints(segment: TrackSegment) {
});
segment._data.anchors = true;
}

View File

@@ -2,7 +2,7 @@
export enum SplitType {
FILES = 'files',
TRACKS = 'tracks',
SEGMENTS = 'segments'
SEGMENTS = 'segments',
}
</script>
@@ -50,7 +50,7 @@
$slicedGPXStatistics = [
get(gpxStatistics).slice(sliderValues[0], sliderValues[1]),
sliderValues[0],
sliderValues[1]
sliderValues[1],
];
} else {
$slicedGPXStatistics = undefined;
@@ -93,10 +93,10 @@
const splitTypes = [
{ value: SplitType.FILES, label: $_('gpx.files') },
{ value: SplitType.TRACKS, label: $_('gpx.tracks') },
{ value: SplitType.SEGMENTS, label: $_('gpx.segments') }
{ value: SplitType.SEGMENTS, label: $_('gpx.segments') },
];
let splitType = splitTypes[0];
let splitType = splitTypes.find((type) => type.value === $splitAs) ?? splitTypes[0];
$: splitAs.set(splitType.value);
@@ -111,7 +111,12 @@
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
<div class="p-2">
<Slider bind:value={sliderValues} max={maxSliderValue} step={1} disabled={!validSelection} />
<Slider
bind:value={sliderValues}
max={maxSliderValue}
step={1}
disabled={!validSelection}
/>
</div>
<Button
variant="outline"

View File

@@ -1,12 +1,15 @@
import { TrackPoint, TrackSegment } from "gpx";
import { get } from "svelte/store";
import mapboxgl from "mapbox-gl";
import { dbUtils, getFile } from "$lib/db";
import { applyToOrderedSelectedItemsFromFile, selection } from "$lib/components/file-list/Selection";
import { ListTrackSegmentItem } from "$lib/components/file-list/FileList";
import { currentTool, gpxStatistics, Tool } from "$lib/stores";
import { _ } from "svelte-i18n";
import { Scissors } from "lucide-static";
import { TrackPoint, TrackSegment } from 'gpx';
import { get } from 'svelte/store';
import mapboxgl from 'mapbox-gl';
import { dbUtils, getFile } from '$lib/db';
import {
applyToOrderedSelectedItemsFromFile,
selection,
} from '$lib/components/file-list/Selection';
import { ListTrackSegmentItem } from '$lib/components/file-list/FileList';
import { currentTool, gpxStatistics, Tool } from '$lib/stores';
import { _ } from 'svelte-i18n';
import { Scissors } from 'lucide-static';
export class SplitControls {
active: boolean = false;
@@ -15,7 +18,8 @@ export class SplitControls {
shownControls: ControlWithMarker[] = [];
unsubscribes: Function[] = [];
toggleControlsForZoomLevelAndBoundsBinded: () => void = this.toggleControlsForZoomLevelAndBounds.bind(this);
toggleControlsForZoomLevelAndBoundsBinded: () => void =
this.toggleControlsForZoomLevelAndBounds.bind(this);
constructor(map: mapboxgl.Map) {
this.map = map;
@@ -48,15 +52,21 @@ export class SplitControls {
this.map.on('move', this.toggleControlsForZoomLevelAndBoundsBinded);
}
updateControls() { // Update the markers when the files change
updateControls() {
// Update the markers when the files change
let controlIndex = 0;
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
let file = getFile(fileId);
if (file) {
file.forEachSegment((segment, trackIndex, segmentIndex) => {
if (get(selection).hasAnyParent(new ListTrackSegmentItem(fileId, trackIndex, segmentIndex))) {
for (let point of segment.trkpt.slice(1, -1)) { // Update the existing controls (could be improved by matching the existing controls with the new ones?)
if (
get(selection).hasAnyParent(
new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
)
) {
for (let point of segment.trkpt.slice(1, -1)) {
// Update the existing controls (could be improved by matching the existing controls with the new ones?)
if (point._data.anchor) {
if (controlIndex < this.controls.length) {
this.controls[controlIndex].fileId = fileId;
@@ -64,20 +74,30 @@ export class SplitControls {
this.controls[controlIndex].segment = segment;
this.controls[controlIndex].trackIndex = trackIndex;
this.controls[controlIndex].segmentIndex = segmentIndex;
this.controls[controlIndex].marker.setLngLat(point.getCoordinates());
this.controls[controlIndex].marker.setLngLat(
point.getCoordinates()
);
} else {
this.controls.push(this.createControl(point, segment, fileId, trackIndex, segmentIndex));
this.controls.push(
this.createControl(
point,
segment,
fileId,
trackIndex,
segmentIndex
)
);
}
controlIndex++;
}
}
}
});
}
}, false);
while (controlIndex < this.controls.length) { // Remove the extra controls
while (controlIndex < this.controls.length) {
// Remove the extra controls
this.controls.pop()?.marker.remove();
}
@@ -94,7 +114,8 @@ export class SplitControls {
this.map.off('move', this.toggleControlsForZoomLevelAndBoundsBinded);
}
toggleControlsForZoomLevelAndBounds() { // Show markers only if they are in the current zoom level and bounds
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]);
@@ -113,15 +134,23 @@ export class SplitControls {
});
}
createControl(point: TrackPoint, segment: TrackSegment, fileId: string, trackIndex: number, segmentIndex: number): ControlWithMarker {
createControl(
point: TrackPoint,
segment: TrackSegment,
fileId: string,
trackIndex: number,
segmentIndex: number
): ControlWithMarker {
let element = document.createElement('div');
element.className = `h-6 w-6 p-0.5 rounded-full bg-white border-2 border-black cursor-pointer`;
element.innerHTML = Scissors.replace('width="24"', "").replace('height="24"', "").replace('stroke="currentColor"', 'stroke="black"');
element.innerHTML = Scissors.replace('width="24"', '')
.replace('height="24"', '')
.replace('stroke="currentColor"', 'stroke="black"');
let marker = new mapboxgl.Marker({
draggable: true,
className: 'z-10',
element
element,
}).setLngLat(point.getCoordinates());
let control = {
@@ -131,12 +160,18 @@ export class SplitControls {
trackIndex,
segmentIndex,
marker,
inZoom: false
inZoom: false,
};
marker.getElement().addEventListener('click', (e) => {
e.stopPropagation();
dbUtils.split(control.fileId, control.trackIndex, control.segmentIndex, control.point.getCoordinates(), control.point._data.index);
dbUtils.split(
control.fileId,
control.trackIndex,
control.segmentIndex,
control.point.getCoordinates(),
control.point._data.index
);
});
return control;

View File

@@ -1,31 +1,33 @@
<script lang="ts">
import { ScrollArea as ScrollAreaPrimitive } from "bits-ui";
import { Scrollbar } from "./index.js";
import { cn } from "$lib/utils.js";
import { ScrollArea as ScrollAreaPrimitive } from 'bits-ui';
import { Scrollbar } from './index.js';
import { cn } from '$lib/utils.js';
type $$Props = ScrollAreaPrimitive.Props & {
orientation?: "vertical" | "horizontal" | "both";
orientation?: 'vertical' | 'horizontal' | 'both';
scrollbarXClasses?: string;
scrollbarYClasses?: string;
viewportClasses?: string;
};
let className: $$Props["class"] = undefined;
let className: $$Props['class'] = undefined;
export { className as class };
export let orientation = "vertical";
export let scrollbarXClasses: string = "";
export let scrollbarYClasses: string = "";
export let orientation = 'vertical';
export let scrollbarXClasses: string = '';
export let scrollbarYClasses: string = '';
export let viewportClasses: string = '';
</script>
<ScrollAreaPrimitive.Root {...$$restProps} class={cn("relative overflow-hidden", className)}>
<ScrollAreaPrimitive.Viewport class="h-full w-full rounded-[inherit]">
<ScrollAreaPrimitive.Root {...$$restProps} class={cn('relative overflow-hidden', className)}>
<ScrollAreaPrimitive.Viewport class={cn('h-full w-full rounded-[inherit]', viewportClasses)}>
<ScrollAreaPrimitive.Content>
<slot />
</ScrollAreaPrimitive.Content>
</ScrollAreaPrimitive.Viewport>
{#if orientation === "vertical" || orientation === "both"}
{#if orientation === 'vertical' || orientation === 'both'}
<Scrollbar orientation="vertical" class={scrollbarYClasses} />
{/if}
{#if orientation === "horizontal" || orientation === "both"}
{#if orientation === 'horizontal' || orientation === 'both'}
<Scrollbar orientation="horizontal" class={scrollbarXClasses} />
{/if}
<ScrollAreaPrimitive.Corner />

View File

@@ -1,32 +1,83 @@
import Dexie, { liveQuery } from 'dexie';
import { GPXFile, GPXStatistics, Track, TrackSegment, Waypoint, TrackPoint, type Coordinates, distance, type LineStyleExtension, type WaypointType } from 'gpx';
import { enableMapSet, enablePatches, applyPatches, type Patch, type WritableDraft, freeze, produceWithPatches } from 'immer';
import {
GPXFile,
GPXStatistics,
Track,
TrackSegment,
Waypoint,
TrackPoint,
type Coordinates,
distance,
type LineStyleExtension,
type WaypointType,
} from 'gpx';
import {
enableMapSet,
enablePatches,
applyPatches,
type Patch,
type WritableDraft,
freeze,
produceWithPatches,
} from 'immer';
import { writable, get, derived, type Readable, type Writable } from 'svelte/store';
import { gpxStatistics, initTargetMapBounds, map, splitAs, updateAllHidden, updateTargetMapBounds } from './stores';
import { defaultBasemap, defaultBasemapTree, defaultOverlayTree, defaultOverlays, type CustomLayer, defaultOpacities, defaultOverpassQueries, defaultOverpassTree } from './assets/layers';
import { applyToOrderedItemsFromFile, applyToOrderedSelectedItemsFromFile, selection } from '$lib/components/file-list/Selection';
import { ListFileItem, ListItem, ListTrackItem, ListLevel, ListTrackSegmentItem, ListWaypointItem, ListRootItem } from '$lib/components/file-list/FileList';
import {
gpxStatistics,
initTargetMapBounds,
map,
splitAs,
updateAllHidden,
updateTargetMapBounds,
} from './stores';
import {
defaultBasemap,
defaultBasemapTree,
defaultOverlayTree,
defaultOverlays,
type CustomLayer,
defaultOpacities,
defaultOverpassQueries,
defaultOverpassTree,
} from './assets/layers';
import {
applyToOrderedItemsFromFile,
applyToOrderedSelectedItemsFromFile,
selection,
} from '$lib/components/file-list/Selection';
import {
ListFileItem,
ListItem,
ListTrackItem,
ListLevel,
ListTrackSegmentItem,
ListWaypointItem,
ListRootItem,
} from '$lib/components/file-list/FileList';
import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/Simplify';
import { SplitType } from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
import { getClosestLinePoint, getElevation } from '$lib/utils';
import { browser } from '$app/environment';
import type mapboxgl from 'mapbox-gl';
enableMapSet();
enablePatches();
class Database extends Dexie {
fileids!: Dexie.Table<string, string>;
files!: Dexie.Table<GPXFile, string>;
patches!: Dexie.Table<{ patch: Patch[], inversePatch: Patch[], index: number }, number>;
patches!: Dexie.Table<{ patch: Patch[]; inversePatch: Patch[]; index: number }, number>;
settings!: Dexie.Table<any, string>;
overpasstiles!: Dexie.Table<{ query: string, x: number, y: number, time: number }, [string, number, number]>;
overpassdata!: Dexie.Table<{ query: string, id: number, poi: GeoJSON.Feature }, [string, number]>;
overpasstiles!: Dexie.Table<
{ query: string; x: number; y: number; time: number },
[string, number, number]
>;
overpassdata!: Dexie.Table<
{ query: string; id: number; poi: GeoJSON.Feature },
[string, number]
>;
constructor() {
super("Database", {
cache: 'immutable'
super('Database', {
cache: 'immutable',
});
this.version(1).stores({
fileids: ',&fileid',
@@ -42,10 +93,15 @@ class Database extends Dexie {
export const db = new Database();
// Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber, and updates to the store are pushed to the DB
export function bidirectionalDexieStore<K, V>(table: Dexie.Table<V, K>, key: K, initial: V, initialize: boolean = true): Writable<V | undefined> {
export function bidirectionalDexieStore<K, V>(
table: Dexie.Table<V, K>,
key: K,
initial: V,
initialize: boolean = true
): Writable<V | undefined> {
let first = true;
let store = writable<V | undefined>(initialize ? initial : undefined);
liveQuery(() => table.get(key)).subscribe(value => {
liveQuery(() => table.get(key)).subscribe((value) => {
if (value === undefined) {
if (first) {
if (!initialize) {
@@ -71,11 +127,15 @@ export function bidirectionalDexieStore<K, V>(table: Dexie.Table<V, K>, key: K,
if (typeof newValue === 'object' || newValue !== get(store)) {
table.put(newValue, key);
}
}
},
};
}
export function dexieSettingStore<T>(key: string, initial: T, initialize: boolean = true): Writable<T> {
export function dexieSettingStore<T>(
key: string,
initial: T,
initialize: boolean = true
): Writable<T> {
return bidirectionalDexieStore(db.settings, key, initial, initialize);
}
@@ -86,7 +146,7 @@ export const settings = {
elevationProfile: dexieSettingStore('elevationProfile', true),
additionalDatasets: dexieSettingStore<string[]>('additionalDatasets', []),
elevationFill: dexieSettingStore<'slope' | 'surface' | undefined>('elevationFill', undefined),
verticalFileView: dexieSettingStore<boolean>('fileView', false),
treeFileView: dexieSettingStore<boolean>('fileView', false),
minimizeRoutingMenu: dexieSettingStore('minimizeRoutingMenu', false),
routing: dexieSettingStore('routing', true),
routingProfile: dexieSettingStore('routingProfile', 'bike'),
@@ -97,7 +157,11 @@ export const settings = {
currentOverlays: dexieSettingStore('currentOverlays', defaultOverlays, false),
previousOverlays: dexieSettingStore('previousOverlays', defaultOverlays),
selectedOverlayTree: dexieSettingStore('selectedOverlayTree', defaultOverlayTree),
currentOverpassQueries: dexieSettingStore('currentOverpassQueries', defaultOverpassQueries, false),
currentOverpassQueries: dexieSettingStore(
'currentOverpassQueries',
defaultOverpassQueries,
false
),
selectedOverpassTree: dexieSettingStore('selectedOverpassTree', defaultOverpassTree),
opacities: dexieSettingStore('opacities', defaultOpacities),
customLayers: dexieSettingStore<Record<string, CustomLayer>>('customLayers', {}),
@@ -108,7 +172,7 @@ export const settings = {
streetViewSource: dexieSettingStore('streetViewSource', 'mapillary'),
fileOrder: dexieSettingStore<string[]>('fileOrder', []),
defaultOpacity: dexieSettingStore('defaultOpacity', 0.7),
defaultWeight: dexieSettingStore('defaultWeight', (browser && window.innerWidth < 600) ? 8 : 5),
defaultWidth: dexieSettingStore('defaultWidth', browser && window.innerWidth < 600 ? 8 : 5),
bottomPanelSize: dexieSettingStore('bottomPanelSize', 170),
rightPanelSize: dexieSettingStore('rightPanelSize', 240),
};
@@ -116,7 +180,7 @@ export const settings = {
// Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber
function dexieStore<T>(querier: () => T | Promise<T>, initial?: T): Readable<T> {
let store = writable<T>(initial);
liveQuery(querier).subscribe(value => {
liveQuery(querier).subscribe((value) => {
if (value !== undefined) {
store.set(value);
}
@@ -150,7 +214,7 @@ export class GPXStatisticsTree {
let statistics = new GPXStatistics();
let id = item.getIdAtLevel(this.level);
if (id === undefined || id === 'waypoints') {
Object.keys(this.statistics).forEach(key => {
Object.keys(this.statistics).forEach((key) => {
if (this.statistics[key] instanceof GPXStatistics) {
statistics.mergeWith(this.statistics[key]);
} else {
@@ -167,26 +231,30 @@ export class GPXStatisticsTree {
}
return statistics;
}
};
export type GPXFileWithStatistics = { file: GPXFile, statistics: GPXStatisticsTree };
}
export type GPXFileWithStatistics = { file: GPXFile; statistics: GPXStatisticsTree };
// Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber, also takes care of the conversion to a GPXFile object
function dexieGPXFileStore(id: string): Readable<GPXFileWithStatistics> & { destroy: () => void } {
let store = writable<GPXFileWithStatistics>(undefined);
let query = liveQuery(() => db.files.get(id)).subscribe(value => {
let query = liveQuery(() => db.files.get(id)).subscribe((value) => {
if (value !== undefined) {
let gpx = new GPXFile(value);
updateAnchorPoints(gpx);
let statistics = new GPXStatisticsTree(gpx);
if (!fileState.has(id)) { // Update the map bounds for new files
updateTargetMapBounds(id, statistics.getStatisticsFor(new ListFileItem(id)).global.bounds);
if (!fileState.has(id)) {
// Update the map bounds for new files
updateTargetMapBounds(
id,
statistics.getStatisticsFor(new ListFileItem(id)).global.bounds
);
}
fileState.set(id, gpx);
store.set({
file: gpx,
statistics
statistics,
});
if (get(selection).hasAnyChildren(new ListFileItem(id))) {
@@ -199,7 +267,7 @@ function dexieGPXFileStore(id: string): Readable<GPXFileWithStatistics> & { dest
destroy: () => {
fileState.delete(id);
query.unsubscribe();
}
},
};
}
@@ -211,22 +279,30 @@ function updateSelection(updatedFiles: GPXFile[], deletedFileIds: string[]) {
if (file) {
items.forEach((item) => {
if (item instanceof ListTrackItem) {
let newTrackIndex = file.trk.findIndex((track) => track._data.trackIndex === item.getTrackIndex());
let newTrackIndex = file.trk.findIndex(
(track) => track._data.trackIndex === item.getTrackIndex()
);
if (newTrackIndex === -1) {
removedItems.push(item);
}
} else if (item instanceof ListTrackSegmentItem) {
let newTrackIndex = file.trk.findIndex((track) => track._data.trackIndex === item.getTrackIndex());
let newTrackIndex = file.trk.findIndex(
(track) => track._data.trackIndex === item.getTrackIndex()
);
if (newTrackIndex === -1) {
removedItems.push(item);
} else {
let newSegmentIndex = file.trk[newTrackIndex].trkseg.findIndex((segment) => segment._data.segmentIndex === item.getSegmentIndex());
let newSegmentIndex = file.trk[newTrackIndex].trkseg.findIndex(
(segment) => segment._data.segmentIndex === item.getSegmentIndex()
);
if (newSegmentIndex === -1) {
removedItems.push(item);
}
}
} else if (item instanceof ListWaypointItem) {
let newWaypointIndex = file.wpt.findIndex((wpt) => wpt._data.index === item.getWaypointIndex());
let newWaypointIndex = file.wpt.findIndex(
(wpt) => wpt._data.index === item.getWaypointIndex()
);
if (newWaypointIndex === -1) {
removedItems.push(item);
}
@@ -256,9 +332,10 @@ function updateSelection(updatedFiles: GPXFile[], deletedFileIds: string[]) {
// Commit the changes to the file state to the database
function commitFileStateChange(newFileState: ReadonlyMap<string, GPXFile>, patch: Patch[]) {
let changedFileIds = getChangedFileIds(patch);
let updatedFileIds: string[] = [], deletedFileIds: string[] = [];
let updatedFileIds: string[] = [],
deletedFileIds: string[] = [];
changedFileIds.forEach(id => {
changedFileIds.forEach((id) => {
if (newFileState.has(id)) {
updatedFileIds.push(id);
} else {
@@ -266,8 +343,10 @@ function commitFileStateChange(newFileState: ReadonlyMap<string, GPXFile>, patch
}
});
let updatedFiles = updatedFileIds.map(id => newFileState.get(id)).filter(file => file !== undefined) as GPXFile[];
updatedFileIds = updatedFiles.map(file => file._data.id);
let updatedFiles = updatedFileIds
.map((id) => newFileState.get(id))
.filter((file) => file !== undefined) as GPXFile[];
updatedFileIds = updatedFiles.map((file) => file._data.id);
updateSelection(updatedFiles, deletedFileIds);
@@ -283,13 +362,15 @@ function commitFileStateChange(newFileState: ReadonlyMap<string, GPXFile>, patch
});
}
export const fileObservers: Writable<Map<string, Readable<GPXFileWithStatistics | undefined> & { destroy?: () => void }>> = writable(new Map());
export const fileObservers: Writable<
Map<string, Readable<GPXFileWithStatistics | undefined> & { destroy?: () => void }>
> = writable(new Map());
const fileState: Map<string, GPXFile> = new Map(); // Used to generate patches
// Observe the file ids in the database, and maintain a map of file observers for the corresponding files
export function observeFilesFromDatabase(fitBounds: boolean) {
let initialize = true;
liveQuery(() => db.fileids.toArray()).subscribe(dbFileIds => {
liveQuery(() => db.fileids.toArray()).subscribe((dbFileIds) => {
if (initialize) {
if (fitBounds && dbFileIds.length > 0) {
initTargetMapBounds(dbFileIds);
@@ -297,17 +378,21 @@ export function observeFilesFromDatabase(fitBounds: boolean) {
initialize = false;
}
// Find new files to observe
let newFiles = dbFileIds.filter(id => !get(fileObservers).has(id)).sort((a, b) => parseInt(a.split('-')[1]) - parseInt(b.split('-')[1]));
let newFiles = dbFileIds
.filter((id) => !get(fileObservers).has(id))
.sort((a, b) => parseInt(a.split('-')[1]) - parseInt(b.split('-')[1]));
// Find deleted files to stop observing
let deletedFiles = Array.from(get(fileObservers).keys()).filter(id => !dbFileIds.find(fileId => fileId === id));
let deletedFiles = Array.from(get(fileObservers).keys()).filter(
(id) => !dbFileIds.find((fileId) => fileId === id)
);
// Update the store
if (newFiles.length > 0 || deletedFiles.length > 0) {
fileObservers.update($files => {
newFiles.forEach(id => {
fileObservers.update(($files) => {
newFiles.forEach((id) => {
$files.set(id, dexieGPXFileStore(id));
});
deletedFiles.forEach(id => {
deletedFiles.forEach((id) => {
$files.get(id)?.destroy?.();
$files.delete(id);
});
@@ -342,15 +427,28 @@ export function getStatistics(fileId: string): GPXStatisticsTree | undefined {
}
const patchIndex: Readable<number> = dexieStore(() => db.settings.get('patchIndex'), -1);
const patchMinMaxIndex: Readable<{ min: number, max: number }> = dexieStore(() => db.patches.orderBy(':id').keys().then(keys => {
const patchMinMaxIndex: Readable<{ min: number; max: number }> = dexieStore(
() =>
db.patches
.orderBy(':id')
.keys()
.then((keys) => {
if (keys.length === 0) {
return { min: 0, max: 0 };
} else {
return { min: keys[0], max: keys[keys.length - 1] + 1 };
}
}), { min: 0, max: 0 });
export const canUndo: Readable<boolean> = derived([patchIndex, patchMinMaxIndex], ([$patchIndex, $patchMinMaxIndex]) => $patchIndex >= $patchMinMaxIndex.min);
export const canRedo: Readable<boolean> = derived([patchIndex, patchMinMaxIndex], ([$patchIndex, $patchMinMaxIndex]) => $patchIndex < $patchMinMaxIndex.max - 1);
}),
{ min: 0, max: 0 }
);
export const canUndo: Readable<boolean> = derived(
[patchIndex, patchMinMaxIndex],
([$patchIndex, $patchMinMaxIndex]) => $patchIndex >= $patchMinMaxIndex.min
);
export const canRedo: Readable<boolean> = derived(
[patchIndex, patchMinMaxIndex],
([$patchIndex, $patchMinMaxIndex]) => $patchIndex < $patchMinMaxIndex.max - 1
);
// Helper function to apply a callback to the global file state
function applyGlobal(callback: (files: Map<string, GPXFile>) => void) {
@@ -378,7 +476,12 @@ function applyToFiles(fileIds: string[], callback: (file: WritableDraft<GPXFile>
}
// Helper function to apply different callbacks to multiple files
function applyEachToFilesAndGlobal(fileIds: string[], callbacks: ((file: WritableDraft<GPXFile>, context?: any) => void)[], globalCallback: (files: Map<string, GPXFile>, context?: any) => void, context?: any) {
function applyEachToFilesAndGlobal(
fileIds: string[],
callbacks: ((file: WritableDraft<GPXFile>, context?: any) => void)[],
globalCallback: (files: Map<string, GPXFile>, context?: any) => void,
context?: any
) {
const [newFileState, patch, inversePatch] = produceWithPatches(fileState, (draft) => {
fileIds.forEach((fileId, index) => {
let file = draft.get(fileId);
@@ -401,16 +504,22 @@ async function storePatches(patch: Patch[], inversePatch: Patch[]) {
db.patches.where(':id').above(get(patchIndex)).delete(); // Delete all patches after the current patch to avoid redoing them
let minmax = get(patchMinMaxIndex);
if (minmax.max - minmax.min + 1 > MAX_PATCHES) {
db.patches.where(':id').belowOrEqual(get(patchMinMaxIndex).max - MAX_PATCHES).delete();
db.patches
.where(':id')
.belowOrEqual(get(patchMinMaxIndex).max - MAX_PATCHES)
.delete();
}
}
db.transaction('rw', db.patches, db.settings, async () => {
let index = get(patchIndex) + 1;
await db.patches.put({
await db.patches.put(
{
patch,
inversePatch,
index,
},
index
}, index);
);
await db.settings.put(index, 'patchIndex');
});
}
@@ -468,7 +577,12 @@ export const dbUtils = {
applyToFiles: (ids: string[], callback: (file: WritableDraft<GPXFile>) => void) => {
applyToFiles(ids, callback);
},
applyEachToFilesAndGlobal: (ids: string[], callbacks: ((file: WritableDraft<GPXFile>, context?: any) => void)[], globalCallback: (files: Map<string, GPXFile>, context?: any) => void, context?: any) => {
applyEachToFilesAndGlobal: (
ids: string[],
callbacks: ((file: WritableDraft<GPXFile>, context?: any) => void)[],
globalCallback: (files: Map<string, GPXFile>, context?: any) => void,
context?: any
) => {
applyEachToFilesAndGlobal(ids, callbacks, globalCallback, context);
},
duplicateSelection: () => {
@@ -492,20 +606,33 @@ export const dbUtils = {
if (level === ListLevel.TRACK) {
for (let item of items) {
let trackIndex = (item as ListTrackItem).getTrackIndex();
file.replaceTracks(trackIndex + 1, trackIndex, [file.trk[trackIndex].clone()]);
file.replaceTracks(trackIndex + 1, trackIndex, [
file.trk[trackIndex].clone(),
]);
}
} else if (level === ListLevel.SEGMENT) {
for (let item of items) {
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
file.replaceTrackSegments(trackIndex, segmentIndex + 1, segmentIndex, [file.trk[trackIndex].trkseg[segmentIndex].clone()]);
file.replaceTrackSegments(
trackIndex,
segmentIndex + 1,
segmentIndex,
[file.trk[trackIndex].trkseg[segmentIndex].clone()]
);
}
} else if (level === ListLevel.WAYPOINTS) {
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, file.wpt.map((wpt) => wpt.clone()));
file.replaceWaypoints(
file.wpt.length,
file.wpt.length - 1,
file.wpt.map((wpt) => wpt.clone())
);
} else if (level === ListLevel.WAYPOINT) {
for (let item of items) {
let waypointIndex = (item as ListWaypointItem).getWaypointIndex();
file.replaceWaypoints(waypointIndex + 1, waypointIndex, [file.wpt[waypointIndex].clone()]);
file.replaceWaypoints(waypointIndex + 1, waypointIndex, [
file.wpt[waypointIndex].clone(),
]);
}
}
}
@@ -514,16 +641,23 @@ export const dbUtils = {
});
},
addNewTrack: (fileId: string) => {
dbUtils.applyToFile(fileId, (file) => file.replaceTracks(file.trk.length, file.trk.length, [new Track()]));
dbUtils.applyToFile(fileId, (file) =>
file.replaceTracks(file.trk.length, file.trk.length, [new Track()])
);
},
addNewSegment: (fileId: string, trackIndex: number) => {
dbUtils.applyToFile(fileId, (file) => {
let track = file.trk[trackIndex];
track.replaceTrackSegments(track.trkseg.length, track.trkseg.length, [new TrackSegment()]);
track.replaceTrackSegments(track.trkseg.length, track.trkseg.length, [
new TrackSegment(),
]);
});
},
reverseSelection: () => {
if (!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) || get(gpxStatistics).local.points?.length <= 1) {
if (
!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) ||
get(gpxStatistics).local.points?.length <= 1
) {
return;
}
applyGlobal((draft) => {
@@ -574,19 +708,19 @@ export const dbUtils = {
});
});
},
mergeSelection: (mergeTraces: boolean) => {
mergeSelection: (mergeTraces: boolean, removeGaps: boolean) => {
applyGlobal((draft) => {
let first = true;
let target: ListItem = new ListRootItem();
let targetFile: GPXFile | undefined = undefined;
let toMerge: {
trk: Track[],
trkseg: TrackSegment[],
wpt: Waypoint[]
trk: Track[];
trkseg: TrackSegment[];
wpt: Waypoint[];
} = {
trk: [],
trkseg: [],
wpt: []
wpt: [],
};
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
let file = draft.get(fileId);
@@ -594,7 +728,11 @@ export const dbUtils = {
if (file && originalFile) {
if (level === ListLevel.FILE) {
toMerge.trk.push(...originalFile.trk.map((track) => track.clone()));
toMerge.wpt.push(...originalFile.wpt.map((wpt) => wpt.clone()));
for (const wpt of originalFile.wpt) {
if (!toMerge.wpt.some((w) => w.equals(wpt))) {
toMerge.wpt.push(wpt.clone());
}
}
if (first) {
target = items[0];
targetFile = file;
@@ -605,8 +743,15 @@ export const dbUtils = {
if (level === ListLevel.TRACK) {
items.forEach((item, index) => {
let trackIndex = (item as ListTrackItem).getTrackIndex();
toMerge.trkseg.splice(0, 0, ...originalFile.trk[trackIndex].trkseg.map((segment) => segment.clone()));
if (index === items.length - 1) { // Order is reversed, so the last track is the first one and the one to keep
toMerge.trkseg.splice(
0,
0,
...originalFile.trk[trackIndex].trkseg.map((segment) =>
segment.clone()
)
);
if (index === items.length - 1) {
// Order is reversed, so the last track is the first one and the one to keep
target = item;
file.trk[trackIndex].trkseg = [];
} else {
@@ -617,10 +762,15 @@ export const dbUtils = {
items.forEach((item, index) => {
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
if (index === items.length - 1) { // Order is reversed, so the last segment is the first one and the one to keep
if (index === items.length - 1) {
// Order is reversed, so the last segment is the first one and the one to keep
target = item;
}
toMerge.trkseg.splice(0, 0, originalFile.trk[trackIndex].trkseg[segmentIndex].clone());
toMerge.trkseg.splice(
0,
0,
originalFile.trk[trackIndex].trkseg[segmentIndex].clone()
);
file.trk[trackIndex].trkseg.splice(segmentIndex, 1);
});
}
@@ -632,15 +782,24 @@ export const dbUtils = {
if (mergeTraces) {
let statistics = get(gpxStatistics);
let speed = statistics.global.speed.moving > 0 ? statistics.global.speed.moving : undefined;
let speed =
statistics.global.speed.moving > 0 ? statistics.global.speed.moving : undefined;
let startTime: Date | undefined = undefined;
if (speed !== undefined) {
if (statistics.local.points.length > 0 && statistics.local.points[0].time !== undefined) {
if (
statistics.local.points.length > 0 &&
statistics.local.points[0].time !== undefined
) {
startTime = statistics.local.points[0].time;
} else {
let index = statistics.local.points.findIndex((point) => point.time !== undefined);
let index = statistics.local.points.findIndex(
(point) => point.time !== undefined
);
if (index !== -1) {
startTime = new Date(statistics.local.points[index].time.getTime() - 1000 * 3600 * statistics.local.distance.total[index] / speed);
startTime = new Date(
statistics.local.points[index].time.getTime() -
(1000 * 3600 * statistics.local.distance.total[index]) / speed
);
}
}
}
@@ -649,7 +808,14 @@ export const dbUtils = {
let s = new TrackSegment();
toMerge.trk.map((track) => {
track.trkseg.forEach((segment) => {
s.replaceTrackPoints(s.trkpt.length, s.trkpt.length, segment.trkpt.slice(), speed, startTime);
s.replaceTrackPoints(
s.trkpt.length,
s.trkpt.length,
segment.trkpt.slice(),
speed,
startTime,
removeGaps
);
});
});
toMerge.trk = [toMerge.trk[0]];
@@ -658,7 +824,14 @@ export const dbUtils = {
if (toMerge.trkseg.length > 0) {
let s = new TrackSegment();
toMerge.trkseg.forEach((segment) => {
s.replaceTrackPoints(s.trkpt.length, s.trkpt.length, segment.trkpt.slice(), speed, startTime);
s.replaceTrackPoints(
s.trkpt.length,
s.trkpt.length,
segment.trkpt.slice(),
speed,
startTime,
removeGaps
);
});
toMerge.trkseg = [s];
}
@@ -674,7 +847,12 @@ export const dbUtils = {
} else if (target instanceof ListTrackSegmentItem) {
let trackIndex = target.getTrackIndex();
let segmentIndex = target.getSegmentIndex();
targetFile.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex - 1, toMerge.trkseg);
targetFile.replaceTrackSegments(
trackIndex,
segmentIndex,
segmentIndex - 1,
toMerge.trkseg
);
}
}
});
@@ -697,11 +875,15 @@ export const dbUtils = {
start -= length;
end -= length;
} else if (level === ListLevel.TRACK) {
let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex());
let trackIndices = items.map((item) =>
(item as ListTrackItem).getTrackIndex()
);
file.crop(start, end, trackIndices);
} else if (level === ListLevel.SEGMENT) {
let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()];
let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex());
let segmentIndices = items.map((item) =>
(item as ListTrackSegmentItem).getSegmentIndex()
);
file.crop(start, end, trackIndices, segmentIndices);
}
}
@@ -721,14 +903,17 @@ export const dbUtils = {
return {
wptIndex: wptIndex,
index: [0],
distance: Number.MAX_VALUE
distance: Number.MAX_VALUE,
};
})
});
file.trk.forEach((track, index) => {
track.getSegments().forEach((segment) => {
segment.trkpt.forEach((point) => {
file.wpt.forEach((wpt, wptIndex) => {
let dist = distance(point.getCoordinates(), wpt.getCoordinates());
let dist = distance(
point.getCoordinates(),
wpt.getCoordinates()
);
if (dist < closest[wptIndex].distance) {
closest[wptIndex].distance = dist;
closest[wptIndex].index = [index];
@@ -736,7 +921,7 @@ export const dbUtils = {
closest[wptIndex].index.push(index);
}
});
})
});
});
});
@@ -751,9 +936,16 @@ export const dbUtils = {
return t;
});
newFile.replaceTracks(0, file.trk.length - 1, tracks);
newFile.replaceWaypoints(0, file.wpt.length - 1, closest.filter((c) => c.index.includes(index)).map((c) => file.wpt[c.wptIndex]));
newFile.replaceWaypoints(
0,
file.wpt.length - 1,
closest
.filter((c) => c.index.includes(index))
.map((c) => file.wpt[c.wptIndex])
);
newFile._data.id = fileIds[index];
newFile.metadata.name = track.name ?? `${file.metadata.name} (${index + 1})`;
newFile.metadata.name =
track.name ?? `${file.metadata.name} (${index + 1})`;
draft.set(newFile._data.id, freeze(newFile));
});
} else if (file.trk.length === 1) {
@@ -763,13 +955,16 @@ export const dbUtils = {
return {
wptIndex: wptIndex,
index: [0],
distance: Number.MAX_VALUE
distance: Number.MAX_VALUE,
};
})
});
file.trk[0].trkseg.forEach((segment, index) => {
segment.trkpt.forEach((point) => {
file.wpt.forEach((wpt, wptIndex) => {
let dist = distance(point.getCoordinates(), wpt.getCoordinates());
let dist = distance(
point.getCoordinates(),
wpt.getCoordinates()
);
if (dist < closest[wptIndex].distance) {
closest[wptIndex].distance = dist;
closest[wptIndex].index = [index];
@@ -782,8 +977,16 @@ export const dbUtils = {
file.trk[0].trkseg.forEach((segment, index) => {
let newFile = file.clone();
newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [segment]);
newFile.replaceWaypoints(0, file.wpt.length - 1, closest.filter((c) => c.index.includes(index)).map((c) => file.wpt[c.wptIndex]));
newFile.replaceTrackSegments(0, 0, file.trk[0].trkseg.length - 1, [
segment,
]);
newFile.replaceWaypoints(
0,
file.wpt.length - 1,
closest
.filter((c) => c.index.includes(index))
.map((c) => file.wpt[c.wptIndex])
);
newFile._data.id = fileIds[index];
newFile.metadata.name = `${file.trk[0].name ?? file.metadata.name} (${index + 1})`;
draft.set(newFile._data.id, freeze(newFile));
@@ -812,7 +1015,13 @@ export const dbUtils = {
});
});
},
split(fileId: string, trackIndex: number, segmentIndex: number, coordinates: Coordinates, trkptIndex?: number) {
split(
fileId: string,
trackIndex: number,
segmentIndex: number,
coordinates: Coordinates,
trkptIndex?: number
) {
let splitType = get(splitAs);
return applyGlobal((draft) => {
let file = getFile(fileId);
@@ -830,7 +1039,10 @@ export const dbUtils = {
let absoluteIndex = minIndex;
file.forEachSegment((seg, trkIndex, segIndex) => {
if ((trkIndex < trackIndex && splitType === SplitType.FILES) || (trkIndex === trackIndex && segIndex < segmentIndex)) {
if (
(trkIndex < trackIndex && splitType === SplitType.FILES) ||
(trkIndex === trackIndex && segIndex < segmentIndex)
) {
absoluteIndex += seg.trkpt.length;
}
});
@@ -860,13 +1072,21 @@ export const dbUtils = {
start.crop(0, minIndex);
let end = segment.clone();
end.crop(minIndex, segment.trkpt.length - 1);
newFile.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex, [start, end]);
newFile.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex, [
start,
end,
]);
}
}
}
});
},
cleanSelection: (bounds: [Coordinates, Coordinates], inside: boolean, deleteTrackPoints: boolean, deleteWaypoints: boolean) => {
cleanSelection: (
bounds: [Coordinates, Coordinates],
inside: boolean,
deleteTrackPoints: boolean,
deleteWaypoints: boolean
) => {
if (get(selection).size === 0) {
return;
}
@@ -877,16 +1097,35 @@ export const dbUtils = {
if (level === ListLevel.FILE) {
file.clean(bounds, inside, deleteTrackPoints, deleteWaypoints);
} else if (level === ListLevel.TRACK) {
let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex());
file.clean(bounds, inside, deleteTrackPoints, deleteWaypoints, trackIndices);
let trackIndices = items.map((item) =>
(item as ListTrackItem).getTrackIndex()
);
file.clean(
bounds,
inside,
deleteTrackPoints,
deleteWaypoints,
trackIndices
);
} else if (level === ListLevel.SEGMENT) {
let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()];
let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex());
file.clean(bounds, inside, deleteTrackPoints, deleteWaypoints, trackIndices, segmentIndices);
let segmentIndices = items.map((item) =>
(item as ListTrackSegmentItem).getSegmentIndex()
);
file.clean(
bounds,
inside,
deleteTrackPoints,
deleteWaypoints,
trackIndices,
segmentIndices
);
} else if (level === ListLevel.WAYPOINTS) {
file.clean(bounds, inside, false, deleteWaypoints);
} else if (level === ListLevel.WAYPOINT) {
let waypointIndices = items.map((item) => (item as ListWaypointItem).getWaypointIndex());
let waypointIndices = items.map((item) =>
(item as ListWaypointItem).getWaypointIndex()
);
file.clean(bounds, inside, false, deleteWaypoints, [], [], waypointIndices);
}
}
@@ -908,7 +1147,15 @@ export const dbUtils = {
let segmentIndex = item.getSegmentIndex();
let points = itemsAndPoints.get(item);
if (points) {
file.replaceTrackPoints(trackIndex, segmentIndex, 0, file.trk[trackIndex].trkseg[segmentIndex].getNumberOfTrackPoints() - 1, points);
file.replaceTrackPoints(
trackIndex,
segmentIndex,
0,
file.trk[trackIndex].trkseg[
segmentIndex
].getNumberOfTrackPoints() - 1,
points
);
}
}
}
@@ -935,7 +1182,9 @@ export const dbUtils = {
});
} else {
let fileIds = new Set<string>();
get(selection).getSelected().forEach((item) => {
get(selection)
.getSelected()
.forEach((item) => {
fileIds.add(item.getFileId());
});
let wpt = new Waypoint(waypoint);
@@ -981,16 +1230,22 @@ export const dbUtils = {
if (level === ListLevel.FILE) {
file.setHidden(hidden);
} else if (level === ListLevel.TRACK) {
let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex());
let trackIndices = items.map((item) =>
(item as ListTrackItem).getTrackIndex()
);
file.setHidden(hidden, trackIndices);
} else if (level === ListLevel.SEGMENT) {
let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()];
let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex());
let segmentIndices = items.map((item) =>
(item as ListTrackSegmentItem).getSegmentIndex()
);
file.setHidden(hidden, trackIndices, segmentIndices);
} else if (level === ListLevel.WAYPOINTS) {
file.setHiddenWaypoints(hidden);
} else if (level === ListLevel.WAYPOINT) {
let waypointIndices = items.map((item) => (item as ListWaypointItem).getWaypointIndex());
let waypointIndices = items.map((item) =>
(item as ListWaypointItem).getWaypointIndex()
);
file.setHiddenWaypoints(hidden, waypointIndices);
}
}
@@ -1017,7 +1272,12 @@ export const dbUtils = {
for (let item of items) {
let trackIndex = (item as ListTrackSegmentItem).getTrackIndex();
let segmentIndex = (item as ListTrackSegmentItem).getSegmentIndex();
file.replaceTrackSegments(trackIndex, segmentIndex, segmentIndex, []);
file.replaceTrackSegments(
trackIndex,
segmentIndex,
segmentIndex,
[]
);
}
} else if (level === ListLevel.WAYPOINTS) {
file.replaceWaypoints(0, file.wpt.length - 1, []);
@@ -1050,14 +1310,18 @@ export const dbUtils = {
});
} else if (level === ListLevel.SEGMENT) {
let trackIndex = (items[0] as ListTrackSegmentItem).getTrackIndex();
let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex());
let segmentIndices = items.map((item) =>
(item as ListTrackSegmentItem).getSegmentIndex()
);
segmentIndices.forEach((segmentIndex) => {
points.push(...file.trk[trackIndex].trkseg[segmentIndex].getTrackPoints());
});
} else if (level === ListLevel.WAYPOINTS) {
points.push(...file.wpt);
} else if (level === ListLevel.WAYPOINT) {
let waypointIndices = items.map((item) => (item as ListWaypointItem).getWaypointIndex());
let waypointIndices = items.map((item) =>
(item as ListWaypointItem).getWaypointIndex()
);
points.push(...waypointIndices.map((waypointIndex) => file.wpt[waypointIndex]));
}
}
@@ -1075,16 +1339,22 @@ export const dbUtils = {
if (level === ListLevel.FILE) {
file.addElevation(elevations);
} else if (level === ListLevel.TRACK) {
let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex());
let trackIndices = items.map((item) =>
(item as ListTrackItem).getTrackIndex()
);
file.addElevation(elevations, trackIndices, undefined, []);
} else if (level === ListLevel.SEGMENT) {
let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()];
let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex());
let segmentIndices = items.map((item) =>
(item as ListTrackSegmentItem).getSegmentIndex()
);
file.addElevation(elevations, trackIndices, segmentIndices, []);
} else if (level === ListLevel.WAYPOINTS) {
file.addElevation(elevations, [], [], undefined);
} else if (level === ListLevel.WAYPOINT) {
let waypointIndices = items.map((item) => (item as ListWaypointItem).getWaypointIndex());
let waypointIndices = items.map((item) =>
(item as ListWaypointItem).getWaypointIndex()
);
file.addElevation(elevations, [], [], waypointIndices);
}
}
@@ -1111,7 +1381,7 @@ export const dbUtils = {
undo: () => {
if (get(canUndo)) {
let index = get(patchIndex);
db.patches.get(index).then(patch => {
db.patches.get(index).then((patch) => {
if (patch) {
applyPatch(patch.inversePatch);
db.settings.put(index - 1, 'patchIndex');
@@ -1122,12 +1392,12 @@ export const dbUtils = {
redo: () => {
if (get(canRedo)) {
let index = get(patchIndex) + 1;
db.patches.get(index).then(patch => {
db.patches.get(index).then((patch) => {
if (patch) {
applyPatch(patch.patch);
db.settings.put(index, 'patchIndex');
}
});
}
}
}
},
};

View File

@@ -2,9 +2,18 @@
title: Files and statistics
---
<script>
import { TriangleRight, BrickWall, Zap, HeartPulse, Orbit, Thermometer, SquareActivity } from 'lucide-svelte';
<script lang="ts">
import { ChartNoAxesColumn } from 'lucide-svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
import ElevationProfile from '$lib/components/ElevationProfile.svelte';
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
import { exampleGPXFile } from '$lib/assets/example';
import { writable } from 'svelte/store';
let gpxStatistics = writable(exampleGPXFile.getStatistics());
let slicedGPXStatistics = writable(undefined);
let additionalDatasets = writable(['speed', 'atemp']);
let elevationFill = writable<'slope' | 'surface' | undefined>(undefined);
</script>
# { title }
@@ -37,11 +46,11 @@ You can also navigate through the files using the arrow keys on your keyboard, a
By right-clicking on a file tab, you can access the same actions as in the [edit menu](./menu/edit).
### Vertical layout
### Tree layout
As mentioned in the [view options section](./menu/view), you can switch between a horizontal and a vertical layout for the file list.
The vertical file list is useful when you have many files open, or files with multiple [tracks, segments, or points of interest](./gpx).
Indeed, this layout allows you to inspect the content of the files through collapsible sections.
As mentioned in the [view options section](./menu/view), you can switch to a tree layout for the files list.
This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map.
In addition, the file tree view enables you to inspect the [tracks, segments, and points of interest](./gpx) contained inside the files through collapsible sections.
You can also apply [edit actions](./menu/edit) and [tools](./toolbar) to internal file items.
Furthermore, you can drag and drop the inner items to reorder them, or move them in the hierarchy or even to another file.
@@ -71,12 +80,31 @@ Click on the profile to reset the selection.
You can also use the mouse wheel to zoom in and out on the elevation profile, and move left and right by dragging the profile while holding the <kbd>Shift</kbd> key.
<div class="h-48 w-full">
<ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
additionalDatasets={$additionalDatasets}
elevationFill={$elevationFill}
/>
</div>
<div class="flex flex-col items-center -mt-6">
<div class="h-10 w-fit">
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
panelSize={120}
orientation={'horizontal'}
/>
</div>
</div>
### Additional data
Using the buttons on the right of the elevation profile, you can optionally color the elevation profile by:
Using the <kbd><ChartNoAxesColumn size="16" class="inline-block" style="margin-bottom: 2px"/></kbd> button at the bottom-right of the elevation profile, you can optionally color the elevation profile by:
- **slope** <TriangleRight size="16" class="inline-block" style="margin-bottom: 2px" /> information computed from the elevation data, or
- **surface** <BrickWall size="16" class="inline-block" style="margin-bottom: 2px" /> data coming from <a href="https://www.openstreetmap.org/" target="_blank">OpenStreetMap</a>'s <a href="https://wiki.openstreetmap.org/wiki/Key:surface" target="_blank">surface</a> tags.
- **slope** information computed from the elevation data, or
- **surface** or **category** data coming from <a href="https://www.openstreetmap.org/" target="_blank">OpenStreetMap</a>'s <a href="https://wiki.openstreetmap.org/wiki/Key:surface" target="_blank">surface</a> and <a href="https://wiki.openstreetmap.org/wiki/Key:highway" target="_blank">highway</a> tags.
This is only available for files created with **gpx.studio**.
If your selection includes it, you can also visualize: **speed** <Zap size="16" class="inline-block" style="margin-bottom: 2px" />, **heart rate** <HeartPulse size="16" class="inline-block" style="margin-bottom: 2px" />, **cadence** <Orbit size="16" class="inline-block" style="margin-bottom: 2px" />, **temperature** <Thermometer size="16" class="inline-block" style="margin-bottom: 2px" />, and **power** <SquareActivity size="16" class="inline-block" style="margin-bottom: 2px" /> data on the elevation profile.
If your selection includes it, you can also visualize: **speed**, **heart rate**, **cadence**, **temperature** and **power** data on the elevation profile.

View File

@@ -25,7 +25,7 @@ This is where you can access common actions such as opening, closing, and export
At the bottom of the interface, you will find the list of files currently open in the application.
You can click on a file to select it and display its statistics below the list.
In the [dedicated section](./files-and-stats), we will explain how to select multiple files and switch to a vertical layout for advanced file management.
In the [dedicated section](./files-and-stats), we will explain how to select multiple files and switch to a tree layout for advanced file management.
## Toolbar

View File

@@ -1,5 +1,5 @@
---
title: GPX file format
title: Фармат файла GPX
---
<script>
@@ -8,27 +8,27 @@ title: GPX file format
# { title }
The <a href="https://www.topografix.com/gpx.asp" target="_blank">GPX file format</a> is an open standard for exchanging GPS data between applications and GPS devices.
It essentially consists of a series of GPS points encoding one or multiple GPS traces, and, optionally, some points of interest.
<a href="https://www.topografix.com/gpx.asp" target="_blank">Фармат файла GPX</a> - гэта адкрыты стандарт для абмену дадзенымі GPS паміж праграмамі і прыладамі GPS.
Па сутнасці, ён складаецца з серыі кропак GPS, якія кадзіруюць адну або некалькі слядоў GPS, і, па жаданні, некаторыя кропкі цікавасці.
GPX files may also contain metadata, of which the **name** and **description** fields are the most useful for users.
Файлы GPX могуць таксама ўтрымліваць метададзеныя, з якіх палі **імя** і **апісанне** найбольш карысныя для карыстальнікаў.
### <Waypoints size="16" class="inline-block" style="margin-bottom: 2px" /> Tracks, segments, and GPS points
### <Waypoints size="16" class="inline-block" style="margin-bottom: 2px" /> Трэкі, сегменты і кропкі GPS
As mentioned above, a GPX file can contain multiple GPS traces.
These are organized in a hierarchical structure, with tracks at the top level.
Як згадвалася вышэй, файл GPX можа ўтрымліваць некалькі слядоў GPS.
Яны арганізаваны ў іерархічнай структуры з трэкамі на верхнім узроўні.
- A **track** is made of a sequence of disconnected segments.
Furthermore, it can contain metadata such as a **name**, a **description**, and **appearance properties**.
- A **segment** is a sequence of GPS points that form a continuous path.
- A **GPS point** is a location with a latitude, a longitude, and optionally a timestamp and an altitude.
Some devices also store additional information such as heart rate, cadence, temperature, and power.
- **Трэк** складаецца з паслядоўнасці раз'яднаных сегментаў.
Акрамя таго, ён можа ўтрымліваць метададзеныя, такія як **імя**, **апісанне** і **знешнія ўласцівасці**.
- **Сегмент** - гэта паслядоўнасць GPS кропак, якія ўтвараюць бесперапынны шлях.
- **Кропка GPS** - гэта месцазнаходжанне з шыратой, даўгатой і, магчыма, пазнакай часу і вышыні.
Некаторыя прылады таксама захоўваюць дадатковую інфармацыю, такую ​​як пульс, кадэнцыя, тэмпература і магутнасць.
In most cases, GPX files contain a single track with a single segment.
However, the hierarchy described above allows for more advanced use cases, such as planning multi-day trips with several variants for each day.
У большасці выпадкаў файлы GPX утрымліваюць адзін трэк з адным сегментам.
Аднак іерархія, апісаная вышэй, дазваляе выкарыстоўваць больш складаныя выпадкі, напрыклад, планаваць шматдзённыя паездкі з некалькімі варыянтамі на кожны дзень.
### <MapPin size="16" class="inline-block" style="margin-bottom: 2px" /> Points of interest
### <MapPin size="16" class="inline-block" style="margin-bottom: 2px" /> Кропкі цікавасці
**Points of interest** (technically called _waypoints_) represent locations of interest to show either on a GPS device or on a digital map.
**Кропкі цікавасці** (тэхнічна званыя _маршрутнымі кропкамі_) уяўляюць цікавыя месцы, якія можна паказаць альбо на прыладзе GPS, альбо на лічбавай карце.
In addition to its coordinates, a point of interest can have a **name** and a **description**.
У дадатак да каардынатаў кропка цікавасці можа мець **імя** і **апісанне**.

View File

@@ -1,5 +1,5 @@
---
title: Integration
title: Інтэграцыя
---
<script>
@@ -9,18 +9,18 @@ title: Integration
# { title }
You can use **gpx.studio** to create maps showing your GPX files and embed them in your website.
Вы можаце выкарыстоўваць **gpx.studio** для стварэння карт, якія паказваюць вашыя файлы GPX, і ўбудаваць іх у свой сайт.
All you need is:
Усё, што вам трэба, гэта:
1. A <a href="https://account.mapbox.com/auth/signup" target="_blank">Mapbox access token</a> to load the map, and
2. GPX files hosted on your server or on Google Drive, or accessible via a public URL.
1. <a href="https://account.mapbox.com/auth/signup" target="_blank">Ключ доступу Mapbox</a> для загрузкі карты і
2. Файлы GPX, размешчаныя на вашым серверы або на Google Drive, або даступныя праз публічны URL.
You can then play with the configurator below to customize your map and generate the corresponding HTML code.
Затым вы можаце пагуляць з канфігуратарам ніжэй, каб наладзіць сваю карту і стварыць адпаведны HTML-код.
<DocsNote type="warning">
You will need to set up <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" target="_blank">Cross-Origin Resource Sharing (CORS)</a> headers on your server to allow <b>gpx.studio</b> to load your GPX files.
Вам трэба будзе наладзіць загалоўкі <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" target="_blank">Cross-Origin Resource Sharing (CORS)</a> на вашым серверы, каб дазволіць <b>gpx.studio</b> загружаць вашы файлы GPX.
</DocsNote>

View File

@@ -10,61 +10,61 @@ title: Map controls
# { title }
The map controls are located on the right side of the interface.
Элементы кіравання картай знаходзяцца ў правай частцы інтэрфэйсу.
These controls allow you to navigate the map, zoom in and out, and switch between different map styles.
### <Diff size="16" class="inline-block" style="margin-bottom: 2px" /> Map navigation
### <Diff size="16" class="inline-block" style="margin-bottom: 2px" /> Навігацыя па карце
The controls at the top allow you to zoom in <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> and out <Minus size="16" class="inline-block" style="margin-bottom: 2px" />, and to change the orientation and tilt of the map <Compass size="16" class="inline-block" style="margin-bottom: 2px" />.
Элементы кіравання ўверсе дазваляюць павялічваць <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> і памяншаць <Minus size="16" class="inline-block" style="margin-bottom: 2px" />, а таксама змяняць арыентацыю і нахіл карты <Compass size="16" class="inline-block" style="margin-bottom: 2px" />.
<DocsNote>
To control the orientation and tilt of the map, you can also drag the map while holding <kbd>Ctrl</kbd>.
Каб кіраваць арыентацыяй і нахілам карты, вы таксама можаце перацягнуць карту, утрымліваючы <kbd>Ctrl</kbd>.
</DocsNote>
### <Search size="16" class="inline-block" style="margin-bottom: 2px" /> Search bar
### <Search size="16" class="inline-block" style="margin-bottom: 2px" /> Пошукавы радок
You can use the search bar to look for an address and navigate to it on the map.
Вы можаце выкарыстоўваць пошукавы радок, каб знайсці адрас і перайсці да яго на карце.
### <LocateFixed size="16" class="inline-block" style="margin-bottom: 2px" /> Locate button
### <LocateFixed size="16" class="inline-block" style="margin-bottom: 2px" /> Кнопка месцазнаходжання
The locate button centers the map on your current location.
Кнопка цэнтруе карту на вашым бягучым месцазнаходжанні.
<DocsNote>
This only works if you have allowed your browser and <b>gpx.studio</b> to access your location.
Гэта працуе, толькі калі вы дазволілі вашаму браўзеру і <b>gpx.studio</b> доступ да вашага месцазнаходжання.
</DocsNote>
### <PersonStanding size="16" class="inline-block" style="margin-bottom: 2px" /> Street view
### <PersonStanding size="16" class="inline-block" style="margin-bottom: 2px" /> Прагляд вуліц
This button can be used to enable street view mode on the map.
Depending on the street view source chosen in the [settings](./menu/settings), street view imagery can be accessed differently.
Гэтую кнопку можна выкарыстоўваць для ўключэння рэжыму прагляду вуліц на карце.
У залежнасці ад крыніцы прагляду вуліц, абранай у [наладах](./menu/settings), да прагляду вуліц можна атрымаць доступ па-рознаму.
- <a href="https://www.mapillary.com/" target="_blank">Mapillary</a>: the street view coverage will appear as green lines on the map. When zoomed in enough, green dots will show the exact locations where street view imagery is available. Hovering over a green dot will show the street view image at that location.
- <a href="https://www.google.com/streetview/" target="_blank">Google Street View</a>: click on the map to open a new tab with the street view imagery at that location.
- <a href="https://www.mapillary.com/" target="_blank">Mapillary</a>: прагляд вуліц будзе адлюстроўвацца на карце ў выглядзе зялёных ліній. Пры дастатковым павелічэнні зялёныя кропкі будуць паказваць дакладныя месцы, дзе даступныя здымкі вуліц. Пры навядзенні курсора на зялёную кропку будзе паказаны здымак вуліцы ў гэтым месцы.
- <a href="https://www.google.com/streetview/" target="_blank">Google Street View</a>: націсніце на карту, каб адкрыць новую ўкладку са здымкамі вуліц у гэтым месцы.
### <Layers size="16" class="inline-block" style="margin-bottom: 2px" /> Map layers
### <Layers size="16" class="inline-block" style="margin-bottom: 2px" /> Слаі карты
The map layers button allows you to switch between different basemaps, and toggle map overlays and categories of points of interest.
Кнопка слаёў карты дазваляе вам пераключацца паміж рознымі базавымі картамі, а таксама пераключаць слаі карты і катэгорыі кропак цікавасці.
- **Basemaps** are background maps that present the main geographic features of the world.
Depending on their purpose, basemaps have different styles and levels of detail.
Only one basemap can be displayed at a time.
- **Overlays** are additional layers that can be displayed on top of the basemap to provide complementary information.
- **Points of interest** can be added to the map to show different categories of places, such as shops, restaurants, or accommodations.
- **Базавыя карты** - гэта фонавыя карты, якія прадстаўляюць асноўныя геаграфічныя аб'екты свету.
У залежнасці ад прызначэння базавыя карты маюць розныя стылі і ўзроўні дэталізацыі.
Адначасова можа быць адлюстравана толькі адна базавая карта.
- **Накладкі** - гэта дадатковыя слаі, якія могуць адлюстроўвацца паверх базавай карты для атрымання дадатковай інфармацыі.
- **Кропкі цікавасці** можна дадаць на карту, каб паказаць розныя катэгорыі месцаў, такіх як крамы, рэстараны або жыллё.
<div class="flex flex-col items-center">
<DocsLayers />
<span class="text-sm text-center mt-2">
Hover over the map to show the <a href="https://hiking.waymarkedtrails.org" target="_blank">Waymarked Trails hiking</a> overlay on top of the <a href="https://www.mapbox.com/maps/outdoors" target="_blank">Mapbox Outdoors</a> basemap.
Навядзіце курсор мышы на карту, каб паказаць накладанне <a href="https://hiking.waymarkedtrails.org" target="_blank">Пешаходных Сцежак</a> на базавай карце <a href="https://www.mapbox.com/maps/outdoors" target="_blank">Mapbox Outdoors</a>.
</span>
</div>
A large collection of global and local basemaps and overlays is available in **gpx.studio**, as well as a selection of point-of-interest categories.
They can be enabled in the [map layer settings dialog](./menu/settings).
Вялікая калекцыя глабальных і лакальных базавых карт і накладанняў даступная ў **gpx.studio**, а таксама выбар катэгорый кропак цікавасці.
Іх можна ўключыць у [дыялогавым акне налад слаёў карты](./menu/settings).
In these settings, you can also manage the opacity of the overlays.
У гэтых наладах вы таксама можаце кіраваць непразрыстасцю накладанняў.
For advanced users, it is possible to add custom basemaps and overlays by providing <a href="https://en.wikipedia.org/wiki/Web_Map_Tile_Service" target="_blank">WMTS</a>, <a href="https://en.wikipedia.org/wiki/Web_Map_Service" target="_blank">WMS</a>, or <a href="https://docs.mapbox.com/help/glossary/style/" target="_blank">Mapbox style JSON</a> URLs.
Для прасунутых карыстальнікаў можна дадаваць карыстальніцкія базавыя карты і накладкі, дадаўшы URL-адрасы <a href="https://en.wikipedia.org/wiki/Web_Map_Tile_Service" target="_blank">WMTS</a>, <a href="https://en.wikipedia.org/wiki/Web_Map_Service" target="_blank">WMS</a> або <a href="https://docs.mapbox.com/help/glossary/style/" target="_blank">JSON у стылі Mapbox</a>.

View File

@@ -10,7 +10,7 @@ title: Edit actions
# { title }
Unlike the file actions, the edit actions can potentially modify the content of the currently selected files.
Moreover, when the vertical layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, and points of interest](../gpx).
Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, and points of interest](../gpx).
Therefore, we will refer to the elements that can be modified by these actions as _file items_.
Note that except for the undo and redo actions, the edit actions are also accessible through the context menu (right-click) of the file items.
@@ -37,7 +37,7 @@ Create a new track in the selected file.
<DocsNote>
This action is only available when the vertical layout of the files list is enabled.
This action is only available when the tree layout of the files list is enabled.
Additionally, the selection must be a single file.
</DocsNote>
@@ -48,7 +48,7 @@ Create a new segment in the selected track.
<DocsNote>
This action is only available when the vertical layout of the files list is enabled.
This action is only available when the tree layout of the files list is enabled.
Additionally, the selection must be a single track.
</DocsNote>
@@ -67,7 +67,7 @@ Copy the selected file items to the clipboard.
<DocsNote>
This action is only available when the vertical layout of the files list is enabled.
This action is only available when the tree layout of the files list is enabled.
</DocsNote>
@@ -77,7 +77,7 @@ Cut the selected file items to the clipboard.
<DocsNote>
This action is only available when the vertical layout of the files list is enabled.
This action is only available when the tree layout of the files list is enabled.
</DocsNote>
@@ -87,7 +87,7 @@ Paste the file items from the clipboard to the current hierarchy level if they a
<DocsNote>
This action is only available when the vertical layout of the files list is enabled.
This action is only available when the tree layout of the files list is enabled.
</DocsNote>

View File

@@ -3,7 +3,7 @@ title: View options
---
<script lang="ts">
import { ChartArea, GalleryVertical, Map, Layers2, Coins, Milestone, Box } from 'lucide-svelte';
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, Box } from 'lucide-svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
@@ -15,10 +15,11 @@ This menu provides options to rearrange the interface and the map view.
Hide the elevation profile to make room for the map, or show it to inspect the current selection.
### <GalleryVertical size="16" class="inline-block" style="margin-bottom: 2px" /> Vertical file list
### <ListTree size="16" class="inline-block" style="margin-bottom: 2px" /> File tree
Switch between a vertical and a horizontal layout for the file list.
The [vertical file list](../files-and-stats) is useful when you have many files open, or files with multiple [tracks, segments, or points of interest](../gpx).
Toggle the tree layout for the [file list](../files-and-stats).
This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map.
In addition, the file tree view enables you to inspect the [tracks, segments, and points of interest](../gpx) contained inside the files through collapsible sections.
### <Map size="16" class="inline-block" style="margin-bottom: 2px" /> Switch to previous basemap
@@ -43,6 +44,6 @@ Enter or exit the 3D map view.
<DocsNote>
To control the orientation and tilt of the map, you can also drag the map while holding <kbd>Ctrl</kbd>.
Каб кіраваць арыентацыяй і нахілам карты, вы таксама можаце перацягнуць карту, утрымліваючы <kbd>Ctrl</kbd>.
</DocsNote>

Some files were not shown because too many files have changed in this diff Show More