419 Commits

Author SHA1 Message Date
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
vcoppe d5022c3ce2 update dependencies 2024-09-30 14:14:43 +02:00
vcoppe db881cbaf1 disable poi form inputs if no selection 2024-09-30 13:10:15 +02:00
vcoppe 4cacafa381 fix layout shift 2024-09-30 12:56:58 +02:00
vcoppe c681029288 fix sitemap 2024-09-30 11:04:20 +02:00
vcoppe f7d0bc1250 New Crowdin updates (#120)
* New translations file.mdx (Chinese Simplified)

* New translations settings.mdx (Chinese Simplified)

* New translations view.mdx (Chinese Simplified)

* New translations minify.mdx (Chinese Simplified)

* New translations poi.mdx (Chinese Simplified)

* New translations routing.mdx (Chinese Simplified)

* New translations scissors.mdx (Chinese Simplified)

* New translations time.mdx (Chinese Simplified)

* New translations faq.mdx (Chinese Simplified)

* 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 (French)

* New translations en.json (Dutch)

* New translations en.json (Spanish)

* New translations en.json (Korean)
2024-09-26 17:25:08 +02:00
vcoppe ce974d7791 add multiple selection tip 2024-09-26 13:31:19 +02:00
vcoppe 01bf1274d9 New Crowdin updates (#119)
* New translations en.json (Czech)

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

* New translations getting-started.mdx (Czech)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations files-and-stats.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 (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 (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 (Portuguese, Brazilian)

* New translations en.json (Korean)

* New translations en.json (Serbian (Latin))
2024-09-26 11:49:59 +02:00
vcoppe 01a29226e5 change page titles 2024-09-26 11:25:57 +02:00
vcoppe c0ac148a97 New Crowdin updates (#118)
* New translations map-controls.mdx (Spanish)

* New translations map-controls.mdx (Dutch)
2024-09-24 17:43:07 +02:00
vcoppe c6e4796cdb only set terrain when pitch > 0 2024-09-24 17:42:05 +02:00
vcoppe eba6989606 change layers max zoom 2024-09-24 17:41:49 +02:00
vcoppe eed13abeb0 remove embed pages and 404 from search results 2024-09-24 15:52:34 +02:00
vcoppe c36636652b improve contrast 2024-09-24 15:16:33 +02:00
vcoppe 294ff5bedf revert to 2 step build 2024-09-24 14:50:44 +02:00
vcoppe 58415af7da New Crowdin updates (#117)
* New translations en.json (Italian)

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

* New translations getting-started.mdx (Romanian)

* New translations getting-started.mdx (French)

* New translations getting-started.mdx (Spanish)

* New translations getting-started.mdx (Catalan)

* New translations getting-started.mdx (Czech)

* New translations getting-started.mdx (German)

* New translations getting-started.mdx (Greek)

* New translations getting-started.mdx (Hungarian)

* New translations getting-started.mdx (Italian)

* 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 (Chinese Simplified)

* New translations getting-started.mdx (Vietnamese)

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

* New translations map-controls.mdx (Romanian)

* New translations map-controls.mdx (French)

* New translations map-controls.mdx (Spanish)

* New translations map-controls.mdx (Catalan)

* New translations map-controls.mdx (Czech)

* New translations map-controls.mdx (German)

* New translations map-controls.mdx (Greek)

* New translations map-controls.mdx (Hungarian)

* New translations map-controls.mdx (Italian)

* New translations map-controls.mdx (Lithuanian)

* New translations map-controls.mdx (Dutch)

* New translations map-controls.mdx (Norwegian)

* New translations map-controls.mdx (Polish)

* New translations map-controls.mdx (Portuguese)

* New translations map-controls.mdx (Russian)

* New translations map-controls.mdx (Swedish)

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

* New translations map-controls.mdx (Vietnamese)

* New translations map-controls.mdx (Portuguese, Brazilian)

* New translations routing.mdx (Romanian)

* New translations routing.mdx (French)

* New translations routing.mdx (Spanish)

* New translations routing.mdx (Catalan)

* New translations routing.mdx (Czech)

* New translations routing.mdx (German)

* New translations routing.mdx (Greek)

* New translations routing.mdx (Hungarian)

* New translations routing.mdx (Italian)

* New translations routing.mdx (Lithuanian)

* New translations routing.mdx (Dutch)

* New translations routing.mdx (Norwegian)

* New translations routing.mdx (Polish)

* New translations routing.mdx (Portuguese)

* New translations routing.mdx (Russian)

* New translations routing.mdx (Swedish)

* New translations routing.mdx (Chinese Simplified)

* New translations routing.mdx (Vietnamese)

* New translations routing.mdx (Portuguese, Brazilian)

* New translations scissors.mdx (Romanian)

* New translations getting-started.mdx (Korean)

* New translations scissors.mdx (French)

* New translations scissors.mdx (Spanish)

* New translations scissors.mdx (Catalan)

* New translations scissors.mdx (Czech)

* New translations scissors.mdx (German)

* New translations scissors.mdx (Greek)

* New translations scissors.mdx (Hungarian)

* New translations scissors.mdx (Italian)

* New translations scissors.mdx (Lithuanian)

* New translations scissors.mdx (Dutch)

* New translations scissors.mdx (Norwegian)

* New translations scissors.mdx (Polish)

* New translations scissors.mdx (Portuguese)

* New translations scissors.mdx (Russian)

* New translations scissors.mdx (Swedish)

* New translations scissors.mdx (Chinese Simplified)

* New translations scissors.mdx (Vietnamese)

* New translations scissors.mdx (Portuguese, Brazilian)

* New translations map-controls.mdx (Korean)

* New translations getting-started.mdx (Hebrew)

* New translations routing.mdx (Korean)

* New translations scissors.mdx (Korean)

* New translations map-controls.mdx (Hebrew)

* New translations routing.mdx (Hebrew)

* New translations scissors.mdx (Hebrew)

* New translations getting-started.mdx (Finnish)

* New translations map-controls.mdx (Finnish)

* New translations routing.mdx (Finnish)

* New translations scissors.mdx (Finnish)

* New translations elevation.mdx (Italian)

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

* New translations getting-started.mdx (Belarusian)

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

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

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

* New translations map-controls.mdx (Belarusian)

* New translations routing.mdx (Belarusian)

* New translations scissors.mdx (Belarusian)

* New translations getting-started.mdx (Danish)

* New translations getting-started.mdx (Latvian)

* New translations map-controls.mdx (Danish)

* New translations routing.mdx (Danish)

* New translations scissors.mdx (Danish)

* New translations map-controls.mdx (Latvian)

* New translations routing.mdx (Latvian)

* New translations scissors.mdx (Latvian)

* Update source file getting-started.mdx

* Update source file map-controls.mdx

* Update source file routing.mdx

* Update source file scissors.mdx

* New translations getting-started.mdx (Romanian)

* New translations getting-started.mdx (French)

* New translations getting-started.mdx (Spanish)

* New translations getting-started.mdx (Catalan)

* New translations getting-started.mdx (Czech)

* New translations getting-started.mdx (German)

* New translations getting-started.mdx (Greek)

* New translations getting-started.mdx (Hungarian)

* New translations getting-started.mdx (Italian)

* 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 (Chinese Simplified)

* New translations getting-started.mdx (Vietnamese)

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

* New translations routing.mdx (Romanian)

* New translations routing.mdx (French)

* New translations routing.mdx (Spanish)

* New translations routing.mdx (Catalan)

* New translations routing.mdx (Czech)

* New translations routing.mdx (German)

* New translations routing.mdx (Greek)

* New translations routing.mdx (Hungarian)

* New translations routing.mdx (Italian)

* New translations routing.mdx (Lithuanian)

* New translations routing.mdx (Dutch)

* New translations routing.mdx (Norwegian)

* New translations routing.mdx (Polish)

* New translations routing.mdx (Portuguese)

* New translations routing.mdx (Russian)

* New translations routing.mdx (Swedish)

* New translations routing.mdx (Chinese Simplified)

* New translations routing.mdx (Vietnamese)

* New translations routing.mdx (Portuguese, Brazilian)

* New translations scissors.mdx (Romanian)

* New translations getting-started.mdx (Korean)

* New translations scissors.mdx (French)

* New translations scissors.mdx (Spanish)

* New translations scissors.mdx (Catalan)

* New translations scissors.mdx (Czech)

* New translations scissors.mdx (German)

* New translations scissors.mdx (Greek)

* New translations scissors.mdx (Hungarian)

* New translations scissors.mdx (Italian)

* New translations scissors.mdx (Lithuanian)

* New translations scissors.mdx (Dutch)

* New translations scissors.mdx (Norwegian)

* New translations scissors.mdx (Polish)

* New translations scissors.mdx (Portuguese)

* New translations scissors.mdx (Russian)

* New translations scissors.mdx (Swedish)

* New translations scissors.mdx (Chinese Simplified)

* New translations scissors.mdx (Vietnamese)

* New translations scissors.mdx (Portuguese, Brazilian)

* New translations getting-started.mdx (Hebrew)

* New translations routing.mdx (Korean)

* New translations scissors.mdx (Korean)

* New translations routing.mdx (Hebrew)

* New translations scissors.mdx (Hebrew)

* New translations getting-started.mdx (Finnish)

* New translations routing.mdx (Finnish)

* New translations scissors.mdx (Finnish)

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

* New translations getting-started.mdx (Belarusian)

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

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

* New translations routing.mdx (Belarusian)

* New translations scissors.mdx (Belarusian)

* New translations getting-started.mdx (Danish)

* New translations getting-started.mdx (Latvian)

* New translations routing.mdx (Danish)

* New translations scissors.mdx (Danish)

* New translations routing.mdx (Latvian)

* New translations scissors.mdx (Latvian)

* Update source file getting-started.mdx

* Update source file routing.mdx

* Update source file scissors.mdx

* New translations getting-started.mdx (French)

* New translations getting-started.mdx (Spanish)

* New translations getting-started.mdx (German)

* New translations getting-started.mdx (Dutch)

* New translations getting-started.mdx (Russian)

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

* New translations map-controls.mdx (French)

* New translations routing.mdx (French)

* New translations routing.mdx (Spanish)

* New translations routing.mdx (Czech)

* New translations routing.mdx (Hungarian)

* New translations routing.mdx (Dutch)

* New translations routing.mdx (Russian)

* New translations routing.mdx (Portuguese, Brazilian)

* New translations scissors.mdx (Spanish)

* New translations scissors.mdx (Italian)

* New translations scissors.mdx (Portuguese, Brazilian)
2024-09-24 14:44:41 +02:00
vcoppe 369c2a5fb6 remove layer from map when removed from layer selection, maybe related to #110 2024-09-24 14:40:08 +02:00
vcoppe 9eb716e36c change name of img parameter 2024-09-24 13:50:44 +02:00
vcoppe 4c56468970 rely on gpx postinstall script to build the library 2024-09-24 13:45:30 +02:00
vcoppe 9a2541b6f3 try to improve core web vitals 2024-09-24 13:45:03 +02:00
vcoppe 2be5c837cb New Crowdin updates (#115)
* New translations files-and-stats.mdx (Dutch)

* New translations elevation.mdx (Italian)

* New translations en.json (Italian)

* New translations files-and-stats.mdx (Italian)
2024-09-24 11:05:39 +02:00
vcoppe 43803717f4 reduce example size 2024-09-24 11:03:14 +02:00
vcoppe 0d4376ee6f avoid more dynamic imports 2024-09-24 10:42:15 +02:00
vcoppe 7a72e3d44e New Crowdin updates (#114)
* Update source file files-and-stats.mdx

* 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 files-and-stats.mdx (Korean)

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

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

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

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

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

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

* Update source file files-and-stats.mdx

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

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

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

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

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

* New translations files-and-stats.mdx (Portuguese, Brazilian)
2024-09-23 19:16:21 +02:00
vcoppe b8b74cc7de fix link 2024-09-23 19:03:50 +02:00
vcoppe ea3d10fcc3 fix overlay opacity 2024-09-23 18:26:01 +02:00
vcoppe 45bfac4f88 New Crowdin updates (#113)
* 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 map-controls.mdx (Dutch)

* 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 (French)
2024-09-23 18:01:46 +02:00
vcoppe 74d37f1d45 more aria labels 2024-09-23 17:46:10 +02:00
vcoppe 195671acb6 add aria labels 2024-09-23 16:41:12 +02:00
vcoppe 2e1ead31ea add width to logo 2024-09-23 16:20:47 +02:00
vcoppe 3af91213fe decrease gap between guide sections 2024-09-23 16:11:31 +02:00
vcoppe 6585f05ce3 New Crowdin updates (#111)
* New translations map-controls.mdx (Italian)

* New translations settings.mdx (Italian)

* New translations en.json (French)

* New translations en.json (Latvian)

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

* New translations getting-started.mdx (Latvian)

* New translations gpx.mdx (Latvian)

* New translations funding.mdx (Latvian)

* New translations mapbox.mdx (Latvian)

* New translations translation.mdx (Latvian)

* New translations integration.mdx (Latvian)

* New translations map-controls.mdx (Latvian)

* New translations menu.mdx (Latvian)

* New translations edit.mdx (Latvian)

* New translations file.mdx (Latvian)

* New translations settings.mdx (Latvian)

* New translations view.mdx (Latvian)

* New translations toolbar.mdx (Latvian)

* New translations clean.mdx (Latvian)

* New translations extract.mdx (Latvian)

* New translations merge.mdx (Latvian)

* New translations minify.mdx (Latvian)

* New translations poi.mdx (Latvian)

* New translations routing.mdx (Latvian)

* New translations scissors.mdx (Latvian)

* New translations time.mdx (Latvian)

* New translations faq.mdx (Latvian)

* New translations elevation.mdx (Latvian)

* New translations en.json (Latvian)

* New translations funding.mdx (Latvian)

* New translations translation.mdx (Latvian)

* New translations time.mdx (Latvian)

* New translations en.json (French)
2024-09-23 15:21:28 +02:00
vcoppe bcc29480c7 preload all guide titles 2024-09-23 15:21:01 +02:00
vcoppe c02d96e90f fix rounded docs img on safari 2024-09-23 14:55:38 +02:00
vcoppe 56e4522da7 fix surface 2024-09-22 13:05:28 +02:00
vcoppe 5592cf47e0 New Crowdin updates (#109)
* New translations edit.mdx (Italian)

* New translations edit.mdx (Italian)

* New translations en.json (Portuguese, Brazilian)

* New translations edit.mdx (Portuguese, Brazilian)

* New translations faq.mdx (Portuguese, Brazilian)

* New translations elevation.mdx (Portuguese, Brazilian)

* New translations map-controls.mdx (Romanian)

* New translations map-controls.mdx (French)

* New translations map-controls.mdx (Spanish)

* New translations map-controls.mdx (Catalan)

* New translations map-controls.mdx (Czech)

* New translations map-controls.mdx (German)

* New translations map-controls.mdx (Greek)

* New translations map-controls.mdx (Hungarian)

* New translations map-controls.mdx (Italian)

* New translations map-controls.mdx (Lithuanian)

* New translations map-controls.mdx (Dutch)

* New translations map-controls.mdx (Norwegian)

* New translations map-controls.mdx (Polish)

* New translations map-controls.mdx (Portuguese)

* New translations map-controls.mdx (Russian)

* New translations map-controls.mdx (Swedish)

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

* New translations map-controls.mdx (Vietnamese)

* New translations map-controls.mdx (Portuguese, Brazilian)

* New translations settings.mdx (Romanian)

* New translations settings.mdx (French)

* New translations settings.mdx (Spanish)

* New translations settings.mdx (Catalan)

* New translations settings.mdx (Czech)

* New translations settings.mdx (German)

* New translations settings.mdx (Greek)

* New translations settings.mdx (Hungarian)

* New translations settings.mdx (Italian)

* New translations settings.mdx (Lithuanian)

* New translations settings.mdx (Dutch)

* New translations settings.mdx (Norwegian)

* New translations settings.mdx (Polish)

* New translations settings.mdx (Portuguese)

* New translations settings.mdx (Russian)

* New translations settings.mdx (Swedish)

* New translations settings.mdx (Chinese Simplified)

* New translations settings.mdx (Vietnamese)

* New translations settings.mdx (Portuguese, Brazilian)

* New translations map-controls.mdx (Korean)

* New translations settings.mdx (Korean)

* New translations map-controls.mdx (Hebrew)

* New translations settings.mdx (Hebrew)

* New translations map-controls.mdx (Finnish)

* New translations settings.mdx (Finnish)

* New translations map-controls.mdx (Belarusian)

* New translations settings.mdx (Belarusian)

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

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

* New translations map-controls.mdx (Danish)

* New translations settings.mdx (Danish)

* Update source file map-controls.mdx

* Update source file settings.mdx

* New translations map-controls.mdx (French)

* New translations map-controls.mdx (Spanish)

* New translations map-controls.mdx (German)

* New translations map-controls.mdx (Dutch)

* New translations map-controls.mdx (Russian)

* New translations map-controls.mdx (Portuguese, Brazilian)

* New translations settings.mdx (French)

* New translations settings.mdx (Spanish)

* New translations settings.mdx (German)

* New translations settings.mdx (Dutch)

* New translations settings.mdx (Russian)

* New translations settings.mdx (Portuguese, Brazilian)
2024-09-22 11:43:15 +02:00
vcoppe 764c5030b9 consistent map layers icon 2024-09-22 11:28:13 +02:00
vcoppe d4460f95dd improve layout shift and accessibility 2024-09-21 19:15:19 +02:00
vcoppe 1483460ec6 add missing buttons to default view 2024-09-21 16:42:13 +02:00
vcoppe 1a10ecc44b unique title for each docs page 2024-09-21 10:18:01 +02:00
vcoppe f77793b7fe non-blocking docs load 2024-09-20 14:22:55 +02:00
vcoppe a48da3fcf0 remove debugging info 2024-09-20 13:22:18 +02:00
vcoppe 9d13e9bcdc prerender mdx components 2024-09-20 13:22:05 +02:00
vcoppe 484aeedbb1 New translations clean.mdx (Dutch) (#107) 2024-09-20 12:42:48 +02:00
vcoppe 534b1ca8db New Crowdin updates (#106)
* New translations map-controls.mdx (Dutch)

* New translations toolbar.mdx (Dutch)

* New translations poi.mdx (Dutch)
2024-09-20 11:31:59 +02:00
vcoppe 4d16efe62f New Crowdin updates (#105)
* New translations files-and-stats.mdx (Lithuanian)

* New translations files-and-stats.mdx (Portuguese, Brazilian)
2024-09-20 11:15:10 +02:00
vcoppe d3c11f6153 New translations files-and-stats.mdx (Dutch) (#104) 2024-09-20 11:00:19 +02:00
vcoppe 2c62abd3eb New translations files-and-stats.mdx (French) (#103) 2024-09-20 10:48:58 +02:00
vcoppe a735852898 New Crowdin updates (#102)
* New translations en.json (Belarusian)

* New translations menu.mdx (Belarusian)

* 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 files-and-stats.mdx (Korean)

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

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

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

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

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

* Update source file files-and-stats.mdx

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

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

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

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

* New translations files-and-stats.mdx (Portuguese, Brazilian)
2024-09-20 10:29:35 +02:00
vcoppe f94edf3e3a fix urls 2024-09-20 10:15:28 +02:00
vcoppe 930b4b84ed remove debugging info 2024-09-20 08:57:04 +02:00
vcoppe 553bc2e0a3 New translations edit.mdx (Spanish) (#101) 2024-09-19 10:36:02 +02:00
vcoppe aa50f1f2b0 rework map bounds logic when opening files from url 2024-09-19 10:35:02 +02:00
vcoppe a27de23fa4 New Crowdin updates (#98)
* New translations en.json (Spanish)

* New translations en.json (Finnish)

* New translations en.json (Finnish)

* New translations en.json (Danish)

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

* New translations getting-started.mdx (Danish)

* New translations gpx.mdx (Danish)

* New translations funding.mdx (Danish)

* New translations mapbox.mdx (Danish)

* New translations translation.mdx (Danish)

* New translations integration.mdx (Danish)

* New translations map-controls.mdx (Danish)

* New translations menu.mdx (Danish)

* New translations edit.mdx (Danish)

* New translations file.mdx (Danish)

* New translations settings.mdx (Danish)

* New translations view.mdx (Danish)

* New translations toolbar.mdx (Danish)

* New translations clean.mdx (Danish)

* New translations extract.mdx (Danish)

* New translations merge.mdx (Danish)

* New translations minify.mdx (Danish)

* New translations poi.mdx (Danish)

* New translations routing.mdx (Danish)

* New translations scissors.mdx (Danish)

* New translations time.mdx (Danish)

* New translations faq.mdx (Danish)

* New translations elevation.mdx (Danish)

* New translations edit.mdx (Danish)

* New translations file.mdx (Danish)

* New translations faq.mdx (Danish)

* New translations toolbar.mdx (Belarusian)

* New translations toolbar.mdx (Belarusian)

* New translations toolbar.mdx (Belarusian)

* New translations edit.mdx (Romanian)

* New translations edit.mdx (French)

* New translations edit.mdx (Spanish)

* New translations edit.mdx (Catalan)

* New translations edit.mdx (Czech)

* New translations edit.mdx (German)

* New translations edit.mdx (Greek)

* New translations edit.mdx (Hungarian)

* New translations edit.mdx (Italian)

* 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 (Chinese Simplified)

* New translations edit.mdx (Vietnamese)

* New translations edit.mdx (Portuguese, Brazilian)

* New translations edit.mdx (Korean)

* New translations edit.mdx (Hebrew)

* New translations edit.mdx (Finnish)

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

* New translations edit.mdx (Belarusian)

* New translations edit.mdx (Danish)

* Update source file edit.mdx

* New translations edit.mdx (French)

* New translations edit.mdx (Dutch)
2024-09-18 17:28:48 +02:00
vcoppe deedd8c6c2 reset geocoder suggestions when input changes, closes #99 2024-09-18 17:12:08 +02:00
vcoppe 5c4181498d add new track/segment to edit menu, and to docs 2024-09-18 16:09:50 +02:00
vcoppe 29a78e8af3 more refactoring 2024-09-16 14:03:06 +02:00
vcoppe 4ebf2b6fa9 parameterize slope segment function 2024-09-16 13:52:41 +02:00
vcoppe 1b741c3b2f revert geojson import 2024-09-16 11:38:47 +02:00
vcoppe 81484789b5 import geojson types explicitly 2024-09-16 11:27:38 +02:00
vcoppe c63e5cfb6b remove dist folder 2024-09-16 11:12:31 +02:00
vcoppe 8e37a308c3 New Crowdin updates (#96)
* New translations en.json (Catalan)

* New translations en.json (Catalan)

* New translations map-controls.mdx (Spanish)

* New translations map-controls.mdx (German)

* New translations map-controls.mdx (Italian)

* New translations map-controls.mdx (Polish)

* New translations map-controls.mdx (Portuguese)

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

* New translations map-controls.mdx (Portuguese, Brazilian)

* New translations settings.mdx (Chinese Simplified)

* New translations en.json (Italian)

* New translations en.json (Finnish)

* New translations en.json (Finnish)

* New translations en.json (Finnish)

* New translations en.json (Finnish)

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

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

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Finnish)

* New translations en.json (Finnish)

* New translations en.json (Belarusian)

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

* New translations getting-started.mdx (Belarusian)

* New translations gpx.mdx (Belarusian)

* New translations funding.mdx (Belarusian)

* New translations mapbox.mdx (Belarusian)

* New translations translation.mdx (Belarusian)

* New translations integration.mdx (Belarusian)

* New translations map-controls.mdx (Belarusian)

* New translations menu.mdx (Belarusian)

* New translations edit.mdx (Belarusian)

* New translations file.mdx (Belarusian)

* New translations settings.mdx (Belarusian)

* New translations view.mdx (Belarusian)

* New translations toolbar.mdx (Belarusian)

* New translations clean.mdx (Belarusian)

* New translations extract.mdx (Belarusian)

* New translations merge.mdx (Belarusian)

* New translations minify.mdx (Belarusian)

* New translations poi.mdx (Belarusian)

* New translations routing.mdx (Belarusian)

* New translations scissors.mdx (Belarusian)

* New translations time.mdx (Belarusian)

* New translations faq.mdx (Belarusian)

* New translations elevation.mdx (Belarusian)

* New translations en.json (Italian)

* New translations en.json (Finnish)

* New translations en.json (Finnish)

* New translations en.json (Finnish)

* New translations en.json (Finnish)
2024-09-16 10:57:49 +02:00
vcoppe 0baa956160 commit dist folder 2024-09-16 10:57:10 +02:00
vcoppe b638863df3 New Crowdin updates (#94)
* 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 translation.mdx (Hungarian)

* New translations edit.mdx (Hungarian)

* New translations clean.mdx (Hungarian)

* New translations merge.mdx (Hungarian)

* New translations minify.mdx (Hungarian)

* New translations en.json (Korean)

* New translations en.json (Hebrew)

* New translations en.json (Finnish)

* New translations elevation.mdx (Hungarian)

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

* New translations en.json (Catalan)

* New translations edit.mdx (Hungarian)

* New translations file.mdx (Hungarian)

* New translations poi.mdx (Hungarian)

* New translations routing.mdx (Hungarian)
2024-09-13 18:49:24 +02:00
Pierre Donias ec7629aea7 Import routes elevation and time information (#95)
* Import routes elevation and time information

* map routepoint to trackpoint explicitly

---------

Co-authored-by: vcoppe <vianney.coppe@gmail.com>
2024-09-13 18:48:22 +02:00
vcoppe 3c7f78ae38 fix search box language 2024-09-13 15:54:56 +02:00
vcoppe 4ca749d1cc New Crowdin updates (#93)
* 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 (Serbian (Latin))
2024-09-13 15:17:34 +02:00
vcoppe 0883bfed03 remove personalized search 2024-09-13 15:10:51 +02:00
vcoppe 130c12bb73 New Crowdin updates (#89)
* New translations en.json (Polish)

* New translations en.json (Italian)

* New translations en.json (Finnish)

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

* New translations getting-started.mdx (Finnish)

* New translations gpx.mdx (Finnish)

* New translations funding.mdx (Finnish)

* New translations mapbox.mdx (Finnish)

* New translations translation.mdx (Finnish)

* New translations integration.mdx (Finnish)

* New translations map-controls.mdx (Finnish)

* New translations menu.mdx (Finnish)

* New translations edit.mdx (Finnish)

* New translations file.mdx (Finnish)

* New translations settings.mdx (Finnish)

* New translations view.mdx (Finnish)

* New translations toolbar.mdx (Finnish)

* New translations clean.mdx (Finnish)

* New translations extract.mdx (Finnish)

* New translations merge.mdx (Finnish)

* New translations minify.mdx (Finnish)

* New translations poi.mdx (Finnish)

* New translations routing.mdx (Finnish)

* New translations scissors.mdx (Finnish)

* New translations time.mdx (Finnish)

* New translations faq.mdx (Finnish)

* New translations elevation.mdx (Finnish)

* 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 funding.mdx (Serbian (Latin))

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* New translations en.json (Italian)

* New translations clean.mdx (Catalan)

* New translations elevation.mdx (Catalan)

* New translations settings.mdx (Catalan)

* 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))

* 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 (Serbian (Latin))

* Update source file en.json

* New translations en.json (French)
2024-09-13 14:25:31 +02:00
vcoppe f513aa28ab small language changes 2024-09-13 14:10:21 +02:00
vcoppe 5e1244cc82 small language changes 2024-09-13 14:07:48 +02:00
vcoppe 4c17c3ddfe Merge branch 'main' into dev 2024-09-13 14:02:15 +02:00
Pierre Donias 5236fc5191 Tab name: also fallback to filename when metadata "name" tag exists but is empty (#92) 2024-09-13 13:59:25 +02:00
vcoppe 1d443f0626 algolia docsearch 2024-09-13 13:58:48 +02:00
vcoppe c83b32e6ae add reddit links 2024-09-13 13:56:28 +02:00
vcoppe 7adf660b76 use dvh only if supported 2024-09-12 17:10:28 +02:00
vcoppe 9a0d54c684 do not use locale time string, gives different formats and causes errors 2024-09-12 16:05:54 +02:00
vcoppe 3e57bdc7c8 only put all dictionary strings for app page 2024-09-12 12:15:26 +02:00
vcoppe 3cc4d569f1 fix ctrl+click on tab, relates to #91 2024-09-12 11:13:55 +02:00
vcoppe dc76c71ae2 fix ctrl+click on gpx layer, relates to #91 2024-09-12 10:13:03 +02:00
vcoppe 1190a471fb set correct lang attribute, and prevent google translate 2024-09-12 09:59:23 +02:00
vcoppe 84b1a42e30 fix time export 2024-09-11 15:08:44 +02:00
vcoppe 08ad9b4186 prevent default on undo-redo 2024-09-11 14:41:02 +02:00
vcoppe b8128aaf86 fix export bug 2024-09-10 20:17:33 +02:00
vcoppe fb00220cc8 fix logo shrinking 2024-09-10 20:15:36 +02:00
vcoppe 723a0138b6 add text content in static html for seo 2024-09-10 18:51:08 +02:00
vcoppe 10e328f2a3 fix some empty actions 2024-09-10 17:42:36 +02:00
vcoppe a1383a7e97 rename track when renaming file if it only contains one track, closes #75 2024-09-10 16:52:21 +02:00
vcoppe 56e229f000 remove empty elements from output 2024-09-10 16:45:37 +02:00
vcoppe 8511a18de1 New Crowdin updates (#86)
* New translations translation.mdx (Chinese Simplified)

* New translations settings.mdx (Chinese Simplified)

* New translations settings.mdx (Chinese Simplified)

* New translations en.json (Czech)

* New translations en.json (Czech)

* New translations minify.mdx (Czech)

* New translations poi.mdx (Czech)

* New translations routing.mdx (Czech)

* New translations map-controls.mdx (French)

* New translations edit.mdx (Dutch)

* New translations settings.mdx (Dutch)

* New translations routing.mdx (Dutch)
2024-09-09 19:44:07 +02:00
vcoppe 4b0e49d171 register map object in window for extensions 2024-09-09 19:43:22 +02:00
vcoppe 14e9d3319d New Crowdin updates (#85)
* New translations en.json (Spanish)

* New translations elevation.mdx (Spanish)
2024-09-06 15:50:33 +02:00
vcoppe 39e6532c26 use nominatim for geocoding 2024-09-06 15:49:12 +02:00
vcoppe b343123ca6 remove console.log 2024-09-06 14:04:15 +02:00
vcoppe 3246742437 New Crowdin updates (#84)
* New translations en.json (Spanish)

* New translations en.json (Dutch)

* New translations en.json (Russian)

* New translations menu.mdx (Russian)

* New translations toolbar.mdx (Russian)

* New translations elevation.mdx (Dutch)

* New translations en.json (Russian)

* New translations en.json (French)

* New translations map-controls.mdx (Russian)

* New translations menu.mdx (Russian)

* New translations en.json (Russian)

* New translations mapbox.mdx (Russian)

* New translations integration.mdx (Russian)

* New translations map-controls.mdx (Russian)

* New translations view.mdx (Russian)

* New translations poi.mdx (Russian)

* New translations routing.mdx (Russian)

* New translations time.mdx (Russian)

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

* New translations getting-started.mdx (Russian)

* New translations gpx.mdx (Russian)

* New translations funding.mdx (Russian)

* New translations translation.mdx (Russian)

* New translations map-controls.mdx (Russian)

* New translations menu.mdx (Russian)

* New translations edit.mdx (Russian)

* New translations settings.mdx (Russian)

* New translations toolbar.mdx (Russian)

* New translations faq.mdx (Russian)

* New translations elevation.mdx (Russian)
2024-09-06 13:53:46 +02:00
vcoppe 3ce5391658 simplify specifying drive ids 2024-09-06 13:53:14 +02:00
vcoppe 71c88b15c6 small ui improvements 2024-09-06 13:36:36 +02:00
vcoppe 25a3df5756 fix time input for iOS 2024-09-05 10:23:34 +02:00
vcoppe 9d5391805d fix consecutive splits 2024-09-05 09:51:43 +02:00
vcoppe 7d801be682 handle elevation tile request failure 2024-09-05 09:41:14 +02:00
vcoppe f846b03e74 fix adding elevation to multiple files 2024-09-04 22:15:46 +02:00
vcoppe e48789bb75 New Crowdin updates (#83)
* New translations en.json (Catalan)

* New translations faq.mdx (Spanish)

* 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 faq.mdx (Romanian)

* New translations faq.mdx (French)

* New translations faq.mdx (Spanish)

* New translations faq.mdx (Catalan)

* New translations faq.mdx (Czech)

* New translations faq.mdx (German)

* New translations faq.mdx (Greek)

* New translations faq.mdx (Hebrew)

* New translations faq.mdx (Hungarian)

* New translations faq.mdx (Italian)

* New translations faq.mdx (Korean)

* New translations faq.mdx (Lithuanian)

* New translations faq.mdx (Dutch)

* New translations faq.mdx (Norwegian)

* New translations faq.mdx (Polish)

* New translations faq.mdx (Portuguese)

* New translations faq.mdx (Russian)

* New translations faq.mdx (Swedish)

* New translations faq.mdx (Chinese Simplified)

* New translations faq.mdx (Vietnamese)

* New translations faq.mdx (Portuguese, Brazilian)

* New translations elevation.mdx (Romanian)

* New translations elevation.mdx (French)

* New translations elevation.mdx (Spanish)

* New translations elevation.mdx (Catalan)

* New translations elevation.mdx (Czech)

* New translations elevation.mdx (German)

* New translations elevation.mdx (Greek)

* New translations elevation.mdx (Hebrew)

* New translations elevation.mdx (Hungarian)

* New translations elevation.mdx (Italian)

* New translations elevation.mdx (Korean)

* New translations elevation.mdx (Lithuanian)

* New translations elevation.mdx (Dutch)

* New translations elevation.mdx (Norwegian)

* New translations elevation.mdx (Polish)

* New translations elevation.mdx (Portuguese)

* New translations elevation.mdx (Russian)

* New translations elevation.mdx (Swedish)

* New translations elevation.mdx (Chinese Simplified)

* New translations elevation.mdx (Vietnamese)

* New translations elevation.mdx (Portuguese, Brazilian)

* Update source file en.json

* Update source file faq.mdx

* Update source file elevation.mdx

* 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 elevation.mdx (French)

* Update source file en.json

* New translations en.json (French)

* New translations en.json (Spanish)
2024-09-04 21:53:06 +02:00
vcoppe 30cc709627 add link to docs 2024-09-04 19:39:22 +02:00
vcoppe 9c85a014da clarify elevation vs elevation gain/loss 2024-09-04 19:34:28 +02:00
vcoppe f55a3c0224 put origin info in note 2024-09-04 19:15:36 +02:00
vcoppe 8985623639 add elevation tool 2024-09-04 19:11:56 +02:00
vcoppe 9ba07ce1ed Merge branch 'dev' into elevation-tool 2024-09-04 15:18:44 +02:00
vcoppe 2e50dea71d fix loading files without metadata 2024-09-04 14:49:10 +02:00
vcoppe 0757efcb79 get all previous point additional data when inserting new anchor 2024-09-04 14:46:08 +02:00
vcoppe 5be02d1c36 add new anchors by clicking on the temporary one 2024-09-04 14:42:26 +02:00
vcoppe 2612eb2e91 update selected routing profile label when locale changes 2024-09-04 13:36:19 +02:00
vcoppe 2996f047d3 fix embedding playground update 2024-09-04 13:26:00 +02:00
vcoppe 96836228db remove custom layer import only if present 2024-09-04 13:15:36 +02:00
vcoppe a3dc17d780 decode custom layer urls 2024-09-04 12:45:40 +02:00
vcoppe e733c96c5a New Crowdin updates (#81)
* Update source file faq.mdx

* New translations en.json (Italian)

* New translations faq.mdx (Romanian)

* New translations faq.mdx (French)

* New translations faq.mdx (Spanish)

* New translations faq.mdx (Catalan)

* New translations faq.mdx (Czech)

* New translations faq.mdx (German)

* New translations faq.mdx (Greek)

* New translations faq.mdx (Hebrew)

* New translations faq.mdx (Hungarian)

* New translations faq.mdx (Italian)

* New translations faq.mdx (Korean)

* New translations faq.mdx (Lithuanian)

* New translations faq.mdx (Dutch)

* New translations faq.mdx (Norwegian)

* New translations faq.mdx (Polish)

* New translations faq.mdx (Portuguese)

* New translations faq.mdx (Russian)

* New translations faq.mdx (Swedish)

* New translations faq.mdx (Chinese Simplified)

* New translations faq.mdx (Vietnamese)

* New translations faq.mdx (Portuguese, Brazilian)

* New translations faq.mdx (Romanian)

* New translations faq.mdx (French)

* New translations faq.mdx (Spanish)

* New translations faq.mdx (Catalan)

* New translations faq.mdx (Czech)

* New translations faq.mdx (German)

* New translations faq.mdx (Greek)

* New translations faq.mdx (Hebrew)

* New translations faq.mdx (Hungarian)

* New translations faq.mdx (Italian)

* New translations faq.mdx (Korean)

* New translations faq.mdx (Lithuanian)

* New translations faq.mdx (Dutch)

* New translations faq.mdx (Norwegian)

* New translations faq.mdx (Polish)

* New translations faq.mdx (Portuguese)

* New translations faq.mdx (Russian)

* New translations faq.mdx (Swedish)

* New translations faq.mdx (Chinese Simplified)

* New translations faq.mdx (Vietnamese)

* New translations faq.mdx (Portuguese, Brazilian)

* Update source file faq.mdx

* New translations faq.mdx (Romanian)

* New translations faq.mdx (French)

* New translations faq.mdx (Spanish)

* New translations faq.mdx (Catalan)

* New translations faq.mdx (Czech)

* New translations faq.mdx (German)

* New translations faq.mdx (Greek)

* New translations faq.mdx (Hebrew)

* New translations faq.mdx (Hungarian)

* New translations faq.mdx (Italian)

* New translations faq.mdx (Korean)

* New translations faq.mdx (Lithuanian)

* New translations faq.mdx (Dutch)

* New translations faq.mdx (Norwegian)

* New translations faq.mdx (Polish)

* New translations faq.mdx (Portuguese)

* New translations faq.mdx (Russian)

* New translations faq.mdx (Swedish)

* New translations faq.mdx (Chinese Simplified)

* New translations faq.mdx (Vietnamese)

* New translations faq.mdx (Portuguese, Brazilian)

* Update source file faq.mdx

* New translations faq.mdx (Romanian)

* New translations faq.mdx (French)

* New translations faq.mdx (Spanish)

* New translations faq.mdx (Catalan)

* New translations faq.mdx (Czech)

* New translations faq.mdx (German)

* New translations faq.mdx (Greek)

* New translations faq.mdx (Hebrew)

* New translations faq.mdx (Hungarian)

* New translations faq.mdx (Italian)

* New translations faq.mdx (Korean)

* New translations faq.mdx (Lithuanian)

* New translations faq.mdx (Dutch)

* New translations faq.mdx (Norwegian)

* New translations faq.mdx (Polish)

* New translations faq.mdx (Portuguese)

* New translations faq.mdx (Russian)

* New translations faq.mdx (Swedish)

* New translations faq.mdx (Chinese Simplified)

* New translations faq.mdx (Vietnamese)

* New translations faq.mdx (Portuguese, Brazilian)

* Update source file faq.mdx

* New translations en.json (Hungarian)

* New translations en.json (Dutch)

* New translations faq.mdx (French)

* New translations faq.mdx (Dutch)

* New translations en.json (Hungarian)

* New translations faq.mdx (Italian)

* New translations en.json (Hungarian)

* New translations en.json (Spanish)

* New translations faq.mdx (Spanish)

* 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)
2024-09-04 11:55:05 +02:00
vcoppe 1de5e9443e fix compatibility issues with basecamp 2024-09-04 11:53:45 +02:00
vcoppe 481bc3b8a1 more rephrasing in faq 2024-09-03 18:47:58 +02:00
vcoppe ce5b0d87a9 more rephrasing in faq 2024-09-03 18:46:43 +02:00
vcoppe efa40edc80 more rephrasing in faq 2024-09-03 18:45:41 +02:00
vcoppe 8497044473 rephrase faq section 2024-09-03 18:36:17 +02:00
vcoppe 41ed9c06c1 add faq section 2024-09-03 15:51:15 +02:00
vcoppe 75dc8512e7 New Crowdin updates (#80)
* New translations en.json (Chinese Simplified)

* New translations toolbar.mdx (German)

* New translations funding.mdx (Spanish)

* New translations merge.mdx (German)

* 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)
2024-09-03 14:51:06 +02:00
vcoppe 979cdd1bac fix error when adding custom overlay 2024-09-03 14:43:20 +02:00
vcoppe 056e7a3980 improve swedish maps 2024-09-03 12:45:35 +02:00
vcoppe 47df6d8f80 update readme 2024-09-02 13:50:27 +02:00
vcoppe 3cbfaba5e7 change base path 2024-09-02 12:32:33 +02:00
vcoppe 24181b932a New Crowdin updates (#77)
* New translations en.json (Dutch)

* New translations en.json (Spanish)
2024-09-02 12:27:12 +02:00
vcoppe 666693f374 New Crowdin updates (#76)
* 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)
2024-08-31 16:05:08 +02:00
vcoppe 0cb781176e use style imports instead of layers to allow stacking mapbox styles, closes #32 2024-08-31 15:57:58 +02:00
vcoppe 33f3b6cc32 New Crowdin updates (#74)
* New translations en.json (Italian)

* New translations edit.mdx (Italian)

* New translations en.json (Spanish)

* New translations en.json (Italian)

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

* New translations getting-started.mdx (Italian)

* New translations integration.mdx (Italian)

* New translations map-controls.mdx (Italian)

* New translations edit.mdx (Spanish)

* New translations file.mdx (Italian)

* New translations settings.mdx (Italian)

* New translations view.mdx (Italian)

* New translations toolbar.mdx (Italian)

* New translations minify.mdx (Italian)

* New translations poi.mdx (Italian)

* New translations routing.mdx (Italian)

* New translations scissors.mdx (Italian)

* New translations edit.mdx (Italian)

* New translations en.json (Romanian)

* New translations en.json (Dutch)

* New translations edit.mdx (Dutch)
2024-08-29 19:03:15 +02:00
vcoppe a1b5fe6352 realistic timestamps option 2024-08-29 18:37:06 +02:00
vcoppe 920e7901f4 remove disclaimer 2024-08-29 14:10:40 +02:00
vcoppe a23fea3d98 fix split control icon color for dark theme 2024-08-26 17:57:22 +02:00
vcoppe a751ada6c5 New Crowdin updates (#63)
* New translations integration.mdx (Dutch)

* New translations funding.mdx (Vietnamese)

* New translations en.json (Italian)

* New translations en.json (Italian)

* New translations getting-started.mdx (Italian)

* New translations gpx.mdx (Italian)

* New translations poi.mdx (Italian)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Portuguese, Brazilian)

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

* New translations integration.mdx (Portuguese, Brazilian)

* New translations map-controls.mdx (Portuguese, Brazilian)

* New translations routing.mdx (Portuguese, Brazilian)

* New translations file.mdx (Italian)

* New translations integration.mdx (Italian)

* New translations file.mdx (Italian)

* New translations settings.mdx (Italian)

* New translations view.mdx (Italian)

* New translations toolbar.mdx (Italian)

* New translations routing.mdx (Italian)

* New translations scissors.mdx (Italian)

* New translations time.mdx (Italian)

* New translations view.mdx (Italian)

* New translations getting-started.mdx (Italian)

* New translations map-controls.mdx (Italian)

* New translations toolbar.mdx (Italian)

* New translations file.mdx (Italian)

* New translations settings.mdx (Italian)

* New translations en.json (Spanish)

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

* New translations integration.mdx (Spanish)

* New translations en.json (Korean)

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

* New translations getting-started.mdx (Korean)

* New translations gpx.mdx (Korean)

* New translations funding.mdx (Korean)

* New translations mapbox.mdx (Korean)

* New translations translation.mdx (Korean)

* New translations integration.mdx (Korean)

* New translations map-controls.mdx (Korean)

* New translations menu.mdx (Korean)

* New translations edit.mdx (Korean)

* New translations file.mdx (Korean)

* New translations settings.mdx (Korean)

* New translations view.mdx (Korean)

* New translations toolbar.mdx (Korean)

* New translations clean.mdx (Korean)

* New translations extract.mdx (Korean)

* New translations merge.mdx (Korean)

* New translations minify.mdx (Korean)

* New translations poi.mdx (Korean)

* New translations routing.mdx (Korean)

* New translations scissors.mdx (Korean)

* New translations time.mdx (Korean)

* New translations en.json (Korean)

* New translations en.json (Korean)

* New translations translation.mdx (Korean)

* New translations settings.mdx (Korean)

* New translations en.json (Korean)

* New translations en.json (Polish)

* New translations poi.mdx (Italian)

* New translations poi.mdx (Italian)

* New translations scissors.mdx (Italian)

* New translations en.json (Polish)

* New translations menu.mdx (Polish)

* New translations en.json (Polish)

* New translations funding.mdx (Polish)

* New translations mapbox.mdx (Polish)

* New translations translation.mdx (Polish)

* New translations settings.mdx (Polish)

* New translations en.json (Polish)

* New translations edit.mdx (Polish)

* New translations map-controls.mdx (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 en.json (Korean)

* New translations en.json (Hebrew)

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

* New translations getting-started.mdx (Hebrew)

* New translations gpx.mdx (Hebrew)

* New translations funding.mdx (Hebrew)

* New translations mapbox.mdx (Hebrew)

* New translations translation.mdx (Hebrew)

* New translations integration.mdx (Hebrew)

* New translations map-controls.mdx (Hebrew)

* New translations menu.mdx (Hebrew)

* New translations edit.mdx (Hebrew)

* New translations file.mdx (Hebrew)

* New translations settings.mdx (Hebrew)

* New translations view.mdx (Hebrew)

* New translations toolbar.mdx (Hebrew)

* New translations clean.mdx (Hebrew)

* New translations extract.mdx (Hebrew)

* New translations merge.mdx (Hebrew)

* New translations minify.mdx (Hebrew)

* New translations poi.mdx (Hebrew)

* New translations routing.mdx (Hebrew)

* New translations scissors.mdx (Hebrew)

* New translations time.mdx (Hebrew)

* 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)

* Update source file en.json

* New translations en.json (French)

* New translations edit.mdx (Romanian)

* New translations edit.mdx (French)

* New translations edit.mdx (Spanish)

* New translations edit.mdx (Catalan)

* New translations edit.mdx (Czech)

* New translations edit.mdx (German)

* New translations edit.mdx (Greek)

* New translations edit.mdx (Hungarian)

* New translations edit.mdx (Italian)

* 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 (Chinese Simplified)

* New translations edit.mdx (Vietnamese)

* New translations edit.mdx (Portuguese, Brazilian)

* New translations edit.mdx (Korean)

* New translations edit.mdx (Hebrew)

* Update source file edit.mdx

* New translations edit.mdx (French)
2024-08-26 17:29:47 +02:00
vcoppe 1cf1ce762e Merge branch 'dev' of https://github.com/gpxstudio/gpx.studio into dev 2024-08-26 14:27:24 +02:00
vcoppe 833f3e58da add center action to docs 2024-08-26 14:27:22 +02:00
mbof b9ca55c798 Implement fit-to-track (#43) (#69)
* Implement fit-to-track (#43)

* * Change icon to Maximize
* Use default animation
* Rename feature to "Fly to"
* Move logic to stores so that it can be reused in the main menu (and eventually a keyboard shortcut)

* improve zoom on selected POIs, rework zoom buttons and add shortcut

---------

Co-authored-by: vcoppe <vianney.coppe@gmail.com>
2024-08-26 14:00:53 +02:00
mbof 766ebe0275 Add support for nautical units (#61) (#66)
* Add support for nautical units

* Make panel reactive to unit changes even after pushing down conversions into utility functions.

* Fix issue: km->mph conversion was not done right.

* Add support for nautical units to the Time dialog.

* Add support for nautical units to the embedding view and embedding playground.

* add missing parameter and rename

* "npx prettier" pass on the files changed in this PR

Does not include changes to `website/src/lib/db.ts`, because there would otherwise be lots of unrelated formatting changes

* Change elevation unit to meters for 'nautical' distances.

* hide elevation decimals

---------

Co-authored-by: bdbkun <1308709+mbof@users.noreply.github.com>
Co-authored-by: vcoppe <vianney.coppe@gmail.com>
2024-08-26 12:51:05 +02:00
mbof d939ef2f60 Set interpolation mode to "monotone" to avoid backwards-going lines (fix #70) (#71) 2024-08-26 12:24:45 +02:00
vcoppe c1faea787a block delete shortcut when focusing an input #68 2024-08-23 10:31:02 +02:00
vcoppe e42dd6e144 fix stats slice bug, closes #67 2024-08-22 14:56:39 +02:00
vcoppe e10e2412c4 fix timestamp edition for files with missing ones 2024-08-22 11:33:11 +02:00
vcoppe b5fd8ea09b refine routing controls interactions 2024-08-22 10:41:04 +02:00
vcoppe 1a4ae96782 fix routing anchor visibility for any bearing 2024-08-19 12:55:17 +02:00
vcoppe 14ed58aaab show duplicate button in horizontal list, closes #64 2024-08-19 12:03:08 +02:00
vcoppe 779c700d13 ordnance survey vector version 2024-08-19 11:46:47 +02:00
vcoppe 31a2aa2fee fix embedding redirect 2024-08-16 13:08:29 +02:00
vcoppe 9d403c861f fix embedding redirect 2024-08-16 12:58:24 +02:00
vcoppe 49582dcddd fix some URLs 2024-08-16 12:51:55 +02:00
vcoppe fa30739fd0 inject static meta tags for each language 2024-08-16 12:25:24 +02:00
vcoppe 3bc9ac4639 New Crowdin updates (#60)
* New translations gpx.mdx (Italian)

* New translations funding.mdx (Italian)

* New translations clean.mdx (Italian)

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

* New translations funding.mdx (Hungarian)

* New translations funding.mdx (Hungarian)

* New translations mapbox.mdx (Hungarian)

* New translations translation.mdx (Hungarian)

* New translations settings.mdx (Hungarian)

* New translations clean.mdx (Italian)

* New translations extract.mdx (Italian)

* New translations toolbar.mdx (German)

* New translations file.mdx (German)

* New translations funding.mdx (German)

* New translations mapbox.mdx (German)

* New translations translation.mdx (German)

* New translations edit.mdx (German)

* New translations file.mdx (German)

* New translations settings.mdx (German)

* New translations routing.mdx (German)

* New translations time.mdx (German)

* New translations edit.mdx (German)

* New translations settings.mdx (German)

* New translations view.mdx (German)

* New translations clean.mdx (German)

* New translations extract.mdx (German)

* New translations extract.mdx (Italian)

* New translations merge.mdx (Italian)

* New translations minify.mdx (Italian)

* New translations routing.mdx (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 integration.mdx (Romanian)

* New translations integration.mdx (French)

* New translations integration.mdx (Spanish)

* New translations integration.mdx (Catalan)

* New translations integration.mdx (Czech)

* New translations integration.mdx (German)

* New translations integration.mdx (Greek)

* New translations integration.mdx (Hungarian)

* New translations integration.mdx (Italian)

* New translations integration.mdx (Lithuanian)

* New translations integration.mdx (Dutch)

* New translations integration.mdx (Norwegian)

* New translations integration.mdx (Polish)

* New translations integration.mdx (Portuguese)

* New translations integration.mdx (Russian)

* New translations integration.mdx (Swedish)

* New translations integration.mdx (Chinese Simplified)

* New translations integration.mdx (Vietnamese)

* New translations integration.mdx (Portuguese, Brazilian)

* Update source file en.json

* Update source file integration.mdx

* 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 (Dutch)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Chinese Simplified)

* New translations integration.mdx (French)
2024-08-14 19:02:46 +02:00
vcoppe 84b3d29e2e embedding: add support files hosted on google drive 2024-08-14 18:27:47 +02:00
vcoppe 9327870d54 support html img in wpt desc 2024-08-14 18:19:28 +02:00
vcoppe f36194b336 add robots.txt 2024-08-14 16:58:26 +02:00
vcoppe f34b23253e use base in embed redirect 2024-08-14 16:35:35 +02:00
vcoppe cfa40238e4 fix 404 imports 2024-08-14 12:46:36 +02:00
vcoppe 66b57e0013 fix 404 2024-08-14 12:14:29 +02:00
vcoppe 879b65953f backward compatibility with old embedding URLs 2024-08-14 11:29:23 +02:00
vcoppe e800b2ebef optional parameter for language, instead of rest parameter 2024-08-14 09:27:53 +02:00
vcoppe 22e9c76a5b fix custom basemap tile URL update 2024-08-14 09:21:55 +02:00
vcoppe d81d189cdf elevation tool test 2024-07-19 13:18:38 +02:00
840 changed files with 52534 additions and 35296 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
- name: Build website - name: Build website
env: env:
BASE_PATH: '/${{ github.event.repository.name }}' BASE_PATH: ''
run: | run: |
npm run build --prefix website npm run build --prefix website
+16
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
View File
@@ -0,0 +1,7 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"svelte.svelte-vscode"
]
}
+13
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"
}
}
+4 -2
View File
@@ -3,11 +3,11 @@
<img alt="Logo of gpx.studio." src="website/static/logo.svg"> <img alt="Logo of gpx.studio." src="website/static/logo.svg">
</picture> </picture>
**gpx.studio** is an online tool for creating and editing GPX files. [**gpx.studio**](https://gpx.studio) is an online tool for creating and editing GPX files.
![gpx.studio screenshot](website/src/lib/assets/img/docs/getting-started/interface.png) ![gpx.studio screenshot](website/src/lib/assets/img/docs/getting-started/interface.png)
This repository contains the source code of the new website, currently available [here](https://gpx.studio/gpx.studio). This repository contains the source code of the website.
## Contributing ## Contributing
@@ -72,6 +72,8 @@ This project has been made possible thanks to the following open source projects
- [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js) — beautiful and fast interactive maps - [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js) — beautiful and fast interactive maps
- [brouter](https://github.com/abrensch/brouter) — routing engine - [brouter](https://github.com/abrensch/brouter) — routing engine
- [OpenStreetMap](https://www.openstreetmap.org) — map data used by Mapbox and brouter - [OpenStreetMap](https://www.openstreetmap.org) — map data used by Mapbox and brouter
- Search:
- [DocSearch](https://github.com/algolia/docsearch) — search engine for the documentation
## License ## License
+1
View File
@@ -0,0 +1 @@
package-lock.json
+1632 -41
View File
File diff suppressed because it is too large Load Diff
+13 -8
View File
@@ -11,16 +11,21 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"fast-xml-parser": "^4.4.0", "fast-xml-parser": "^4.5.0",
"immer": "^10.1.1", "immer": "^10.1.1"
"ts-node": "^10.9.2"
},
"scripts": {
"build": "tsc"
}, },
"devDependencies": { "devDependencies": {
"@types/geojson": "^7946.0.14", "@types/geojson": "^7946.0.14",
"@types/node": "^20.14.6", "@types/node": "^20.16.10",
"typescript": "^5.4.5" "@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",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
} }
} }
+954 -287
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -2,4 +2,3 @@ export * from './gpx';
export { Coordinates, LineStyleExtension, WaypointType } from './types'; export { Coordinates, LineStyleExtension, WaypointType } from './types';
export { parseGPX, buildGPX } from './io'; export { parseGPX, buildGPX } from './io';
export * from './simplify'; export * from './simplify';
+99 -31
View File
@@ -1,25 +1,68 @@
import { XMLParser, XMLBuilder } from "fast-xml-parser"; import { XMLParser, XMLBuilder } from 'fast-xml-parser';
import { GPXFileType } from "./types"; import { GPXFileType } from './types';
import { GPXFile } from "./gpx"; 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 { export function parseGPX(gpxData: string): GPXFile {
const parser = new XMLParser({ const parser = new XMLParser({
ignoreAttributes: false, ignoreAttributes: false,
attributeNamePrefix: "", attributeNamePrefix: '',
attributesGroupName: 'attributes', attributesGroupName: 'attributes',
removeNSPrefix: true,
isArray(name: string) { 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) { attributeValueProcessor(attrName, attrValue, jPath) {
if (attrName === 'lat' || attrName === 'lon') { if (attrName === 'lat' || attrName === 'lon') {
return parseFloat(attrValue); return safeParseFloat(attrValue);
} }
return attrValue; return attrValue;
}, },
transformTagName(tagName: string) { transformTagName(tagName: string) {
if (tagName === 'power') { if (attributesWithNamespace[tagName]) {
// Transform the simple <power> tag to the more complex <gpxpx:PowerExtension> tag, the nested <gpxpx:PowerInWatts> tag is then handled by the tagValueProcessor return attributesWithNamespace[tagName];
return 'gpxpx:PowerExtension';
} }
return tagName; return tagName;
}, },
@@ -27,22 +70,29 @@ export function parseGPX(gpxData: string): GPXFile {
tagValueProcessor(tagName, tagValue, jPath, hasAttributes, isLeafNode) { tagValueProcessor(tagName, tagValue, jPath, hasAttributes, isLeafNode) {
if (isLeafNode) { if (isLeafNode) {
if (tagName === 'ele') { if (tagName === 'ele') {
return parseFloat(tagValue); return safeParseFloat(tagValue);
} }
if (tagName === 'time') { if (tagName === 'time') {
return new Date(tagValue); return new Date(tagValue);
} }
if (tagName === 'gpxtpx:hr' || tagName === 'gpxtpx:cad' || tagName === 'gpxtpx:atemp' || tagName === 'gpxpx:PowerInWatts' || tagName === 'opacity' || tagName === 'weight') { if (
return parseFloat(tagValue); 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') { if (tagName === 'gpxpx:PowerExtension') {
// Finish the transformation of the simple <power> tag to the more complex <gpxpx:PowerExtension> tag // 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 // Note that this only targets the transformed <power> tag, since it must be a leaf node
return { 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; const parsed: GPXFileType = parser.parse(gpxData).gpx;
// @ts-ignore // @ts-ignore
if (parsed.metadata === "") { if (parsed.metadata === '') {
parsed.metadata = {}; parsed.metadata = {};
} }
@@ -64,49 +114,67 @@ export function parseGPX(gpxData: string): GPXFile {
export function buildGPX(file: GPXFile, exclude: string[]): string { export function buildGPX(file: GPXFile, exclude: string[]): string {
const gpx = file.toGPXFileType(exclude); const gpx = file.toGPXFileType(exclude);
let lastDate = undefined;
const builder = new XMLBuilder({ const builder = new XMLBuilder({
format: true, format: true,
ignoreAttributes: false, ignoreAttributes: false,
attributeNamePrefix: "", attributeNamePrefix: '',
attributesGroupName: 'attributes', attributesGroupName: 'attributes',
suppressEmptyNode: true, suppressEmptyNode: true,
tagValueProcessor: (tagName: string, tagValue: unknown): string => { tagValueProcessor: (tagName: string, tagValue: unknown): string | undefined => {
if (tagValue instanceof Date) { if (tagValue instanceof Date) {
if (isNaN(tagValue.getTime())) {
return lastDate?.toISOString();
}
lastDate = tagValue;
return tagValue.toISOString(); return tagValue.toISOString();
} }
return tagValue.toString(); 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['version'] = '1.1';
gpx.attributes['xmlns'] = 'http://www.topografix.com/GPX/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['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:gpxtpx'] = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1';
gpx.attributes['xmlns:gpxx'] = 'http://www.garmin.com/xmlschemas/GpxExtensions/v3'; gpx.attributes['xmlns:gpxx'] = 'http://www.garmin.com/xmlschemas/GpxExtensions/v3';
gpx.attributes['xmlns:gpxpx'] = 'http://www.garmin.com/xmlschemas/PowerExtension/v1'; gpx.attributes['xmlns:gpxpx'] = 'http://www.garmin.com/xmlschemas/PowerExtension/v1';
gpx.attributes['xmlns:gpx_style'] = 'http://www.topografix.com/GPX/gpx_style/0/2'; gpx.attributes['xmlns:gpx_style'] = 'http://www.topografix.com/GPX/gpx_style/0/2';
gpx.metadata.author = {
name: 'gpx.studio',
link: {
attributes: {
href: 'https://gpx.studio',
}
}
};
if (gpx.trk.length === 1 && (gpx.trk[0].name === undefined || gpx.trk[0].name === '')) { if (gpx.trk.length === 1 && (gpx.trk[0].name === undefined || gpx.trk[0].name === '')) {
gpx.trk[0].name = gpx.metadata.name; gpx.trk[0].name = gpx.metadata.name;
} }
return builder.build({ return builder.build({
"?xml": { '?xml': {
attributes: { attributes: {
version: "1.0", version: '1.0',
encoding: "UTF-8", encoding: 'UTF-8',
}
}, },
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)
) {
delete obj[key];
} else if (typeof obj[key] === 'object' && !(obj[key] instanceof Date)) {
removeEmptyElements(obj[key]);
if (Object.keys(obj[key]).length === 0) {
delete obj[key];
}
}
}
return obj;
}
+118 -21
View File
@@ -1,33 +1,48 @@
import { TrackPoint } from "./gpx"; import { TrackPoint } from './gpx';
import { Coordinates } from "./types"; import { Coordinates } from './types';
export type SimplifiedTrackPoint = { point: TrackPoint, distance?: number }; export type SimplifiedTrackPoint = { point: TrackPoint; distance?: number };
const earthRadius = 6371008.8; 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) { if (points.length == 0) {
return []; return [];
} else if (points.length == 1) { } else if (points.length == 1) {
return [{ return [
point: points[0] {
}]; point: points[0],
},
];
} }
let simplified = [{ let simplified = [
point: points[0] {
}]; point: points[0],
},
];
ramerDouglasPeuckerRecursive(points, epsilon, measure, 0, points.length - 1, simplified); ramerDouglasPeuckerRecursive(points, epsilon, measure, 0, points.length - 1, simplified);
simplified.push({ simplified.push({
point: points[points.length - 1] point: points[points.length - 1],
}); });
return simplified; 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 = { let largest = {
index: 0, index: 0,
distance: 0 distance: 0,
}; };
for (let i = start + 1; i < end; i++) { 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 { export function crossarcDistance(
return crossarc(point1.getCoordinates(), point2.getCoordinates(), point3 instanceof TrackPoint ? point3.getCoordinates() : point3); 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 { 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? // Is relative bearing obtuse?
if (diff > (Math.PI / 2)) { if (diff > Math.PI / 2) {
return dis13; return dis13;
} }
@@ -83,7 +106,8 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
// Is p4 beyond the arc? // Is p4 beyond the arc?
let dis12 = distance(lat1, lon1, lat2, lon2); 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) { if (dis14 > dis12) {
return distance(lat2, lon2, lat3, lon3); return distance(lat2, lon2, lat3, lon3);
} else { } else {
@@ -93,12 +117,85 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
function distance(latA: number, lonA: number, latB: number, lonB: number): number { function distance(latA: number, lonA: number, latB: number, lonB: number): number {
// Finds the distance between two lat / lon points. // 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 { function bearing(latA: number, lonA: number, latB: number, lonB: number): number {
// Finds the bearing from one lat / lon point to another. // Finds the bearing from one lat / lon point to another.
return Math.atan2(Math.sin(lonB - lonA) * Math.cos(latB), return Math.atan2(
Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA)); 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
);
}
function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates {
// Calculates the point on the line defined by p1 and p2
// that is closest to the third point, p3.
// Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees.
const rad = Math.PI / 180;
const lat1 = coord1.lat * rad;
const lat2 = coord2.lat * rad;
const lat3 = coord3.lat * rad;
const lon1 = coord1.lon * rad;
const lon2 = coord2.lon * rad;
const lon3 = coord3.lon * rad;
// Prerequisites for the formulas
const bear12 = bearing(lat1, lon1, lat2, lon2);
const bear13 = bearing(lat1, lon1, lat3, lon3);
let dis13 = distance(lat1, lon1, lat3, lon3);
let diff = Math.abs(bear13 - bear12);
if (diff > Math.PI) {
diff = 2 * Math.PI - diff;
}
// Is relative bearing obtuse?
if (diff > Math.PI / 2) {
return coord1;
}
// Find the cross-track distance.
let dxt = Math.asin(Math.sin(dis13 / earthRadius) * Math.sin(bear13 - bear12)) * earthRadius;
// 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;
if (dis14 > dis12) {
return coord2;
} else {
// Determine the closest point (p4) on the great circle
const f = dis14 / earthRadius;
const lat4 = Math.asin(
Math.sin(lat1) * Math.cos(f) + Math.cos(lat1) * Math.sin(f) * Math.cos(bear12)
);
const lon4 =
lon1 +
Math.atan2(
Math.sin(bear12) * Math.sin(f) * Math.cos(lat1),
Math.cos(f) - Math.sin(lat1) * Math.sin(lat4)
);
return { lat: lat4 / rad, lon: lon4 / rad };
}
} }
+11 -13
View File
@@ -58,8 +58,8 @@ export type TrackType = {
src?: string; src?: string;
link?: Link; link?: Link;
type?: string; type?: string;
trkseg: TrackSegmentType[];
extensions?: TrackExtensions; extensions?: TrackExtensions;
trkseg: TrackSegmentType[];
}; };
export type TrackExtensions = { export type TrackExtensions = {
@@ -67,9 +67,9 @@ export type TrackExtensions = {
}; };
export type LineStyleExtension = { export type LineStyleExtension = {
color?: string; 'gpx_style:color'?: string;
opacity?: number; 'gpx_style:opacity'?: number;
weight?: number; 'gpx_style:width'?: number;
}; };
export type TrackSegmentType = { export type TrackSegmentType = {
@@ -89,17 +89,15 @@ export type TrackPointExtensions = {
}; };
export type TrackPointExtension = { export type TrackPointExtension = {
'gpxtpx:atemp'?: number;
'gpxtpx:hr'?: number; 'gpxtpx:hr'?: number;
'gpxtpx:cad'?: number; 'gpxtpx:cad'?: number;
'gpxtpx:atemp'?: number; 'gpxtpx:Extensions'?: Record<string, string>;
'gpxtpx:Extensions'?: { };
surface?: string;
};
}
export type PowerExtension = { export type PowerExtension = {
'gpxpx:PowerInWatts'?: number; 'gpxpx:PowerInWatts'?: number;
} };
export type Author = { export type Author = {
name?: string; name?: string;
@@ -116,12 +114,12 @@ export type RouteType = {
type?: string; type?: string;
extensions?: TrackExtensions; extensions?: TrackExtensions;
rtept: WaypointType[]; rtept: WaypointType[];
} };
export type RoutePointExtension = { export type RoutePointExtension = {
'gpxx:rpt'?: GPXXRoutePoint[]; 'gpxx:rpt'?: GPXXRoutePoint[];
} };
export type GPXXRoutePoint = { export type GPXXRoutePoint = {
attributes: Coordinates; attributes: Coordinates;
} };
+253
View File
@@ -0,0 +1,253 @@
<?xml version="1.0" encoding="UTF-8"?>
<gpx xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.topografix.com/GPX/1/1" 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.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd"
xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1"
xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3"
xmlns:gpx_style="http://www.topografix.com/GPX/gpx_style/0/2" version="1.1" creator="https://gpx.studio">
<metadata>
<name>with_routes</name>
<author>
<name>gpx.studio</name>
<link href="https://gpx.studio"></link>
</author>
</metadata>
<rte>
<name>route 1</name>
<type>Cycling</type>
<rtept lat="50.790867" lon="4.404968">
<ele>109.0</ele>
</rtept>
<rtept lat="50.790714" lon="4.405036">
<ele>110.8</ele>
</rtept>
<rtept lat="50.790336" lon="4.405259">
<ele>110.3</ele>
</rtept>
<rtept lat="50.790165" lon="4.405331">
<ele>110.0</ele>
</rtept>
<rtept lat="50.790008" lon="4.405359">
<ele>110.3</ele>
</rtept>
<rtept lat="50.789818" lon="4.405359">
<ele>109.3</ele>
</rtept>
<rtept lat="50.789409" lon="4.40534">
<ele>107.0</ele>
</rtept>
<rtept lat="50.789105" lon="4.405411">
<ele>106.0</ele>
</rtept>
<rtept lat="50.788799" lon="4.405527">
<ele>108.5</ele>
</rtept>
<rtept lat="50.788645" lon="4.405606">
<ele>109.8</ele>
</rtept>
<rtept lat="50.7885" lon="4.405711">
<ele>110.8</ele>
</rtept>
<rtept lat="50.78822" lon="4.405959">
<ele>112.0</ele>
</rtept>
<rtept lat="50.787956" lon="4.406092">
<ele>112.8</ele>
</rtept>
<rtept lat="50.787814" lon="4.406143">
<ele>113.5</ele>
</rtept>
<rtept lat="50.787674" lon="4.406177">
<ele>114.3</ele>
</rtept>
<rtept lat="50.787451" lon="4.406199">
<ele>115.3</ele>
</rtept>
<rtept lat="50.787297" lon="4.406177">
<ele>114.8</ele>
</rtept>
<rtept lat="50.78716" lon="4.406098">
<ele>114.3</ele>
</rtept>
<rtept lat="50.787045" lon="4.405984">
<ele>114.3</ele>
</rtept>
<rtept lat="50.786683" lon="4.405653">
<ele>114.5</ele>
</rtept>
<rtept lat="50.786538" lon="4.405543">
<ele>115.0</ele>
</rtept>
<rtept lat="50.78635" lon="4.405441">
<ele>115.8</ele>
</rtept>
<rtept lat="50.786275" lon="4.40542">
<ele>115.8</ele>
</rtept>
<rtept lat="50.786182" lon="4.405435">
<ele>116.0</ele>
</rtept>
<rtept lat="50.786121" lon="4.405475">
<ele>115.8</ele>
</rtept>
<rtept lat="50.786042" lon="4.405558">
<ele>115.5</ele>
</rtept>
<rtept lat="50.785821" lon="4.405925">
<ele>114.5</ele>
</rtept>
<rtept lat="50.785672" lon="4.406119">
<ele>112.5</ele>
</rtept>
<rtept lat="50.785516" lon="4.406256">
<ele>110.8</ele>
</rtept>
<rtept lat="50.785384" lon="4.406364">
<ele>109.0</ele>
</rtept>
<rtept lat="50.785126" lon="4.406475">
<ele>106.3</ele>
</rtept>
<rtept lat="50.784697" lon="4.406537">
<ele>104.3</ele>
</rtept>
<rtept lat="50.784591" lon="4.40657">
<ele>104.0</ele>
</rtept>
<rtept lat="50.784507" lon="4.406612">
<ele>103.8</ele>
</rtept>
<rtept lat="50.784435" lon="4.40669">
<ele>103.3</ele>
</rtept>
<rtept lat="50.784209" lon="4.407148">
<ele>103.5</ele>
</rtept>
<rtept lat="50.784162" lon="4.407257">
<ele>103.8</ele>
</rtept>
<rtept lat="50.784077" lon="4.407372">
<ele>104.8</ele>
</rtept>
<rtept lat="50.784006" lon="4.407435">
<ele>105.8</ele>
</rtept>
<rtept lat="50.783924" lon="4.407471">
<ele>106.8</ele>
</rtept>
<rtept lat="50.783837" lon="4.407486">
<ele>107.8</ele>
</rtept>
<rtept lat="50.783771" lon="4.407472">
<ele>108.5</ele>
</rtept>
<rtept lat="50.783697" lon="4.407428">
<ele>109.3</ele>
</rtept>
<rtept lat="50.783626" lon="4.407363">
<ele>110.0</ele>
</rtept>
<rtept lat="50.783548" lon="4.407274">
<ele>110.5</ele>
</rtept>
<rtept lat="50.783458" lon="4.407134">
<ele>110.8</ele>
</rtept>
<rtept lat="50.783123" lon="4.406435">
<ele>111.8</ele>
</rtept>
<rtept lat="50.782982" lon="4.406168">
<ele>112.8</ele>
</rtept>
<rtept lat="50.782871" lon="4.406044">
<ele>113.3</ele>
</rtept>
</rte>
<rte>
<name>route 2</name>
<type>Cycling</type>
<rtept lat="50.782212" lon="4.406377">
<ele>115.5</ele>
</rtept>
<rtept lat="50.782175" lon="4.406413">
<ele>115.8</ele>
</rtept>
<rtept lat="50.781749" lon="4.407018">
<ele>118.5</ele>
</rtept>
<rtept lat="50.781654" lon="4.407316">
<ele>119.5</ele>
</rtept>
<rtept lat="50.781563" lon="4.407764">
<ele>121.3</ele>
</rtept>
<rtept lat="50.781487" lon="4.407984">
<ele>122.0</ele>
</rtept>
<rtept lat="50.781422" lon="4.408216">
<ele>122.8</ele>
</rtept>
<rtept lat="50.781395" lon="4.408508">
<ele>123.5</ele>
</rtept>
<rtept lat="50.781399" lon="4.409114">
<ele>126.3</ele>
</rtept>
<rtept lat="50.781367" lon="4.409428">
<ele>128.0</ele>
</rtept>
<rtept lat="50.781286" lon="4.409607">
<ele>129.0</ele>
</rtept>
<rtept lat="50.78116" lon="4.409789">
<ele>130.0</ele>
</rtept>
<rtept lat="50.780804" lon="4.409993">
<ele>130.8</ele>
</rtept>
<rtept lat="50.780389" lon="4.410334">
<ele>131.8</ele>
</rtept>
<rtept lat="50.780232" lon="4.410563">
<ele>132.3</ele>
</rtept>
<rtept lat="50.780094" lon="4.410827">
<ele>132.8</ele>
</rtept>
<rtept lat="50.779723" lon="4.411582">
<ele>135.8</ele>
</rtept>
<rtept lat="50.779591" lon="4.411791">
<ele>135.5</ele>
</rtept>
<rtept lat="50.779125" lon="4.412435">
<ele>132.5</ele>
</rtept>
<rtept lat="50.778676" lon="4.412979">
<ele>134.0</ele>
</rtept>
<rtept lat="50.778194" lon="4.413466">
<ele>136.8</ele>
</rtept>
<rtept lat="50.777427" lon="4.414302">
<ele>137.5</ele>
</rtept>
<rtept lat="50.777165" lon="4.414736">
<ele>137.3</ele>
</rtept>
<rtept lat="50.776927" lon="4.415201">
<ele>137.5</ele>
</rtept>
<rtept lat="50.776778" lon="4.415613">
<ele>137.3</ele>
</rtept>
<rtept lat="50.776553" lon="4.416425">
<ele>134.8</ele>
</rtept>
<rtept lat="50.776326" lon="4.417304">
<ele>132.3</ele>
</rtept>
<rtept lat="50.776129" lon="4.418383">
<ele>129.5</ele>
</rtept>
</rte>
</gpx>
+3 -3
View File
@@ -16,9 +16,9 @@
<type>Cycling</type> <type>Cycling</type>
<extensions> <extensions>
<gpx_style:line> <gpx_style:line>
<color>#2d3ee9</color> <gpx_style:color>2d3ee9</gpx_style:color>
<opacity>0.5</opacity> <gpx_style:opacity>0.5</gpx_style:opacity>
<weight>6</weight> <gpx_style:width>6</gpx_style:width>
</gpx_style:line> </gpx_style:line>
</extensions> </extensions>
<trkseg> <trkseg>
+2 -4
View File
@@ -4,9 +4,7 @@
"target": "ES2015", "target": "ES2015",
"declaration": true, "declaration": true,
"outDir": "./dist", "outDir": "./dist",
"moduleResolution": "node", "moduleResolution": "node"
}, },
"include": [ "include": ["src"]
"src"
],
} }
+7 -7
View File
@@ -5,27 +5,27 @@ module.exports = {
'eslint:recommended', 'eslint:recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended', 'plugin:svelte/recommended',
'prettier' 'prettier',
], ],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'], plugins: ['@typescript-eslint'],
parserOptions: { parserOptions: {
sourceType: 'module', sourceType: 'module',
ecmaVersion: 2020, ecmaVersion: 2020,
extraFileExtensions: ['.svelte'] extraFileExtensions: ['.svelte'],
}, },
env: { env: {
browser: true, browser: true,
es2017: true, es2017: true,
node: true node: true,
}, },
overrides: [ overrides: [
{ {
files: ['*.svelte'], files: ['*.svelte'],
parser: 'svelte-eslint-parser', parser: 'svelte-eslint-parser',
parserOptions: { parserOptions: {
parser: '@typescript-eslint/parser' parser: '@typescript-eslint/parser',
} },
} },
] ],
}; };
+2
View File
@@ -2,3 +2,5 @@
pnpm-lock.yaml pnpm-lock.yaml
package-lock.json package-lock.json
yarn.lock yarn.lock
src/lib/components/ui
*.mdx
-8
View File
@@ -1,8 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}
+4132 -1492
View File
File diff suppressed because it is too large Load Diff
+47 -38
View File
@@ -5,6 +5,7 @@
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "vite build", "build": "vite build",
"prebuild": "npx tsx src/lib/pwa-manifest.ts",
"postbuild": "npx tsx src/lib/sitemap.ts", "postbuild": "npx tsx src/lib/sitemap.ts",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
@@ -13,61 +14,69 @@
"format": "prettier --write ." "format": "prettier --write ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.2.2", "@sveltejs/adapter-auto": "^3.2.5",
"@sveltejs/adapter-static": "^3.0.2", "@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/enhanced-img": "^0.3.0", "@sveltejs/enhanced-img": "^0.3.8",
"@sveltejs/kit": "^2.5.17", "@sveltejs/kit": "^2.6.1",
"@sveltejs/vite-plugin-svelte": "^3.1.1", "@sveltejs/vite-plugin-svelte": "^3.1.2",
"@types/eslint": "^8.56.10", "@types/eslint": "^8.56.12",
"@types/events": "^3.0.3", "@types/events": "^3.0.3",
"@types/mapbox__mapbox-gl-geocoder": "^5.0.0", "@types/file-saver": "^2.0.7",
"@types/mapbox-gl": "^3.1.0", "@types/mapbox__tilebelt": "^1.0.4",
"@types/node": "^20.14.6", "@types/mapbox-gl": "^3.4.0",
"@types/sanitize-html": "^2.11.0", "@types/node": "^20.16.10",
"@types/png.js": "^0.2.3",
"@types/sanitize-html": "^2.13.0",
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^7.13.1", "@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.13.1", "@typescript-eslint/parser": "^7.18.0",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.20",
"eslint": "^8.57.0", "eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.40.0", "eslint-plugin-svelte": "^2.44.1",
"events": "^3.3.0", "events": "^3.3.0",
"glob": "^10.4.3", "glob": "^10.4.5",
"mdsvex": "^0.11.2", "mdsvex": "^0.12.6",
"postcss": "^8.4.38", "postcss": "^8.4.47",
"prettier": "^3.3.2", "prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.4", "prettier-plugin-svelte": "^3.2.7",
"svelte": "^4.2.18", "svelte": "^4.2.19",
"svelte-check": "^3.8.1", "svelte-check": "^3.8.6",
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.13",
"tslib": "^2.6.3", "tslib": "^2.7.0",
"tsx": "^4.15.7", "tsx": "^4.19.1",
"typescript": "^5.4.5", "typescript": "^5.6.2",
"vite": "^5.3.1" "vite": "^5.4.8",
"vite-plugin-node-polyfills": "^0.22.0"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@internationalized/date": "^3.5.4", "@docsearch/js": "^3.6.2",
"@mapbox/mapbox-gl-geocoder": "^5.0.2", "@internationalized/date": "^3.5.5",
"@mapbox/mapbox-gl-geocoder": "^5.0.3",
"@mapbox/sphericalmercator": "^1.2.0", "@mapbox/sphericalmercator": "^1.2.0",
"@mapbox/tilebelt": "^1.0.2",
"@types/mapbox__sphericalmercator": "^1.2.3", "@types/mapbox__sphericalmercator": "^1.2.3",
"bits-ui": "^0.21.12", "bits-ui": "^0.21.15",
"chart.js": "^4.4.3", "chart.js": "^4.4.4",
"chartjs-plugin-zoom": "^2.0.1", "chartjs-plugin-zoom": "^2.0.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dexie": "^4.0.7", "dexie": "^4.0.8",
"file-saver": "^2.0.5",
"gpx": "file:../gpx", "gpx": "file:../gpx",
"immer": "^10.1.1", "immer": "^10.1.1",
"lucide-static": "^0.427.0", "jszip": "^3.10.1",
"lucide-svelte": "^0.427.0", "lucide-static": "^0.460.0",
"mapbox-gl": "^3.4.0", "lucide-svelte": "^0.460.1",
"mapbox-gl": "^3.11.1",
"mapillary-js": "^4.1.2", "mapillary-js": "^4.1.2",
"mode-watcher": "^0.3.1", "mode-watcher": "^0.3.1",
"png.js": "^0.2.1",
"sanitize-html": "^2.13.0", "sanitize-html": "^2.13.0",
"sortablejs": "^1.15.2", "sortablejs": "^1.15.3",
"svelte-i18n": "^4.0.0", "svelte-i18n": "^4.0.0",
"svelte-sonner": "^0.3.24", "svelte-sonner": "^0.3.28",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.5.2",
"tailwind-variants": "^0.2.1" "tailwind-variants": "^0.2.1"
} }
} }
+1 -1
View File
@@ -3,4 +3,4 @@ export default {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };
+7 -8
View File
@@ -1,15 +1,14 @@
<!doctype html> <!doctype html>
<html lang="en"> <html>
<head>
<head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=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% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>
+6 -2
View File
@@ -8,7 +8,7 @@
--foreground: 222.2 84% 4.9%; --foreground: 222.2 84% 4.9%;
--muted: 210 40% 96.1%; --muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%; --muted-foreground: 215.4 16.3% 45%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%; --popover-foreground: 222.2 84% 4.9%;
@@ -33,6 +33,8 @@
--support: 220 15 130; --support: 220 15 130;
--link: 0 110 180;
--ring: 222.2 84% 4.9%; --ring: 222.2 84% 4.9%;
--radius: 0.5rem; --radius: 0.5rem;
@@ -68,7 +70,9 @@
--support: 255 110 190; --support: 255 110 190;
--ring: hsl(212.7,26.8%,83.9); --link: 80 190 255;
--ring: hsl(212.7, 26.8%, 83.9);
} }
} }
+55
View File
@@ -0,0 +1,55 @@
import { base } from '$app/paths';
import { languages } from '$lib/languages';
import { getURLForLanguage } from '$lib/utils';
export async function handle({ event, resolve }) {
const language = event.params.language ?? 'en';
const strings = await import(`./locales/${language}.json`);
const path = event.url.pathname;
const page = event.route.id?.replace('/[[language]]', '').split('/')[1] ?? 'home';
let title = strings.metadata[`${page}_title`];
const description = strings.metadata[`description`];
if (page === 'help' && event.params.guide) {
const [guide, subguide] = event.params.guide.split('/');
const guideModule = subguide
? await import(`./lib/docs/${language}/${guide}/${subguide}.mdx`)
: await import(`./lib/docs/${language}/${guide}.mdx`);
title = `${title} | ${guideModule.metadata.title}`;
}
const htmlTag = `<html lang="${language}" translate="no">`;
let headTag = `<head>
<title>gpx.studio — ${title}</title>
<meta name="description" content="${description}" />
<meta property="og:title" content="gpx.studio — ${title}" />
<meta property="og:description" content="${description}" />
<meta name="twitter:title" content="gpx.studio — ${title}" />
<meta name="twitter:description" content="${description}" />
<meta property="og:image" content="https://gpx.studio${base}/og_logo.png" />
<meta property="og:url" content="https://gpx.studio/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="gpx.studio" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://gpx.studio${base}/og_logo.png" />
<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="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 response = await resolve(event, {
transformPageChunk: ({ html }) =>
html.replace('<html>', htmlTag).replace('<head>', headTag),
});
return response;
}
+171
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}%)`;
}
@@ -0,0 +1,864 @@
{
"_info": "Taken from https://github.com/mjaschen/gravel-overlay, with prior authorization from the author (https://github.com/gpxstudio/gpx.studio/issues/32#issuecomment-2320219804).",
"version": 8,
"name": "Gravel Overlay",
"metadata": {
"mapbox:autocomposite": false,
"mapbox:type": "template",
"maputnik:renderer": "mbgljs",
"openmaptiles:version": "3.x",
"openmaptiles:mapbox:owner": "openmaptiles",
"openmaptiles:mapbox:source:url": "mapbox://openmaptiles.4qljc88t"
},
"sources": {
"openmaptiles": {
"type": "vector",
"url": "https://tiles.bikerouter.de/services/gravel/"
}
},
"sprite": "https://demotiles.maplibre.org/styles/osm-bright-gl-style/sprite",
"glyphs": "https://api.maptiler.com/fonts/{fontstack}/{range}.pbf?key={key}",
"layers": [
{
"id": "background",
"type": "background",
"layout": {
"visibility": "none"
},
"paint": {
"background-color": "rgba(145, 211, 164, 1)"
}
},
{
"id": "debug_rail",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"filter": ["all", ["==", "$type", "LineString"], ["in", "class", "rail"]],
"layout": {
"visibility": "none"
},
"paint": {
"line-color": "rgba(144, 144, 144, 1)"
}
},
{
"id": "debug_road",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"filter": [
"all",
["==", "$type", "LineString"],
[
"in",
"class",
"motorway",
"trunk",
"primary",
"secondary",
"tertiary",
"minor",
"residential",
"track",
"path"
]
],
"layout": {
"visibility": "none"
},
"paint": {
"line-color": "rgba(204, 204, 204, 1)",
"line-width": {
"stops": [
[10, 0.5],
[12, 1]
]
}
}
},
{
"id": "tr_X_g45-bg",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"minzoom": 10,
"filter": [
"all",
["==", ["geometry-type"], "LineString"],
["match", ["get", "class"], ["track"], true, false],
[
"any",
["match", ["get", "tracktype"], ["grade5"], true, false],
[
"all",
["match", ["get", "tracktype"], ["grade4"], true, false],
["match", ["get", "surface"], ["dirt", "grass", "mud", "sand"], true, false]
]
]
],
"layout": {
"line-cap": "square",
"line-join": "bevel",
"visibility": "visible"
},
"paint": {
"line-color": "rgba(255, 0, 0, 0.7)",
"line-width": {
"base": 1.55,
"stops": [
[10, 0.4],
[12, 1.3],
[14, 1.7]
]
},
"line-dasharray": [1],
"line-offset": {
"stops": [
[12, 0],
[13, 1.8],
[15, 3],
[16, 4]
],
"base": 1.55
}
}
},
{
"id": "tr_X_g45",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"minzoom": 10,
"filter": [
"all",
["==", ["geometry-type"], "LineString"],
["match", ["get", "class"], ["track"], true, false],
[
"any",
["match", ["get", "tracktype"], ["grade5"], true, false],
[
"all",
["match", ["get", "tracktype"], ["grade4"], true, false],
["match", ["get", "surface"], ["dirt", "grass", "mud", "sand"], true, false]
]
]
],
"layout": {
"line-cap": "square",
"line-join": "bevel",
"visibility": "visible"
},
"paint": {
"line-color": "rgba(255, 255, 0, 0.7)",
"line-width": {
"base": 1.55,
"stops": [
[10, 0.4],
[12, 1.3],
[14, 1.7]
]
},
"line-dasharray": [2, 2],
"line-offset": {
"stops": [
[12, 0],
[13, 1.8],
[15, 3],
[16, 4]
],
"base": 1.55
}
}
},
{
"id": "tr_B_g3",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"minzoom": 10,
"filter": [
"all",
["==", ["geometry-type"], "LineString"],
["==", ["get", "class"], "track"],
["match", ["get", "tracktype"], ["grade3"], true, false],
[
"any",
["match", ["get", "smoothness"], ["bad", "good", "intermediate"], true, false],
[
"match",
["get", "surface"],
["compacted", "fine_gravel", "gravel"],
true,
false
]
]
],
"layout": {
"line-cap": "butt",
"line-join": "miter",
"visibility": "visible"
},
"paint": {
"line-color": "rgba(235, 6, 158, 1)",
"line-width": {
"base": 1.55,
"stops": [
[10, 0.4],
[12, 1.3],
[14, 1.7]
]
},
"line-offset": {
"stops": [
[12, 0],
[13, 2],
[15, 4],
[16, 5]
],
"base": 1.55
},
"line-dasharray": [3, 1.5]
}
},
{
"id": "tr_A_g2-plain-case",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"minzoom": 10,
"filter": [
"all",
["==", ["geometry-type"], "LineString"],
["==", ["get", "class"], "track"],
["match", ["get", "tracktype"], ["grade2"], true, false],
["!", ["has", "surface"]],
["!", ["has", "smoothness"]]
],
"layout": {
"line-cap": "butt",
"line-join": "miter",
"visibility": "visible"
},
"paint": {
"line-color": "rgba(255, 255, 255, 0.6)",
"line-width": {
"base": 1.55,
"stops": [
[10, 0.8],
[12, 3],
[14, 4]
]
},
"line-offset": {
"stops": [
[12, 0],
[13, 1.5],
[15, 3],
[16, 4]
],
"base": 1.55
}
}
},
{
"id": "tr_A_g2-plain",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"minzoom": 10,
"filter": [
"all",
["==", ["geometry-type"], "LineString"],
["==", ["get", "class"], "track"],
["match", ["get", "tracktype"], ["grade2"], true, false],
["!", ["has", "surface"]],
["!", ["has", "smoothness"]]
],
"layout": {
"line-cap": "butt",
"line-join": "miter",
"visibility": "visible"
},
"paint": {
"line-color": "rgba(235, 6, 158, 0.6)",
"line-width": {
"base": 1.55,
"stops": [
[10, 0.5],
[12, 1.5],
[14, 2]
]
},
"line-offset": {
"stops": [
[12, 0],
[13, 1.5],
[15, 3],
[16, 4]
],
"base": 1.55
},
"line-dasharray": [5, 1]
}
},
{
"id": "tr_A_g2-case",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"minzoom": 10,
"filter": [
"all",
["==", ["geometry-type"], "LineString"],
["==", ["get", "class"], "track"],
["match", ["get", "tracktype"], ["grade2"], true, false],
[
"any",
[
"match",
["get", "surface"],
["compacted", "fine_gravel", "gravel"],
true,
false
],
["match", ["get", "smoothness"], ["bad", "good", "intermediate"], true, false]
]
],
"layout": {
"line-cap": "butt",
"line-join": "miter",
"visibility": "visible"
},
"paint": {
"line-color": "rgba(255, 255, 255, 1)",
"line-width": {
"base": 1.55,
"stops": [
[10, 0.8],
[12, 3],
[14, 4]
]
},
"line-offset": {
"stops": [
[12, 0],
[13, 2],
[15, 4],
[16, 5]
],
"base": 1.55
}
}
},
{
"id": "tr_A_g2",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"minzoom": 10,
"filter": [
"all",
["==", ["geometry-type"], "LineString"],
["==", ["get", "class"], "track"],
["match", ["get", "tracktype"], ["grade2"], true, false],
[
"any",
[
"match",
["get", "surface"],
["compacted", "fine_gravel", "gravel"],
true,
false
],
["match", ["get", "smoothness"], ["bad", "good", "intermediate"], true, false]
]
],
"layout": {
"line-cap": "butt",
"line-join": "miter",
"visibility": "visible"
},
"paint": {
"line-color": "rgba(235, 6, 158, 1)",
"line-width": {
"base": 1.55,
"stops": [
[10, 0.5],
[12, 1.5],
[14, 2]
]
},
"line-offset": {
"stops": [
[12, 0],
[13, 2],
[15, 4],
[16, 5]
],
"base": 1.55
}
}
},
{
"id": "p_X-bg",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"minzoom": 10,
"filter": [
"all",
["==", "$type", "LineString"],
["in", "class", "path"],
["in", "smoothness", "very_bad", "horrible", "very_horrible", "impassable"],
["!in", "tracktype", "grade5", "grade4"]
],
"layout": {
"line-cap": "square",
"line-join": "bevel",
"visibility": "visible"
},
"paint": {
"line-color": "rgba(255, 0, 0, 0.7)",
"line-width": {
"base": 1.55,
"stops": [
[10, 0.4],
[12, 1.1],
[14, 1.5]
]
},
"line-offset": {
"stops": [
[12, 0],
[13, 1.8],
[15, 3],
[16, 4]
],
"base": 1.55
}
}
},
{
"id": "p_X",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"minzoom": 10,
"filter": [
"all",
["==", "$type", "LineString"],
["in", "class", "path"],
["in", "smoothness", "very_bad", "horrible", "very_horrible", "impassable"],
["!in", "tracktype", "grade5", "grade4"]
],
"layout": {
"line-cap": "square",
"line-join": "bevel",
"visibility": "visible"
},
"paint": {
"line-color": "rgba(255, 255, 0, 0.7)",
"line-width": {
"base": 1.55,
"stops": [
[10, 0.4],
[12, 1.1],
[14, 1.5]
]
},
"line-dasharray": [2, 2],
"line-offset": {
"stops": [
[12, 0],
[13, 1.8],
[15, 3],
[16, 4]
],
"base": 1.55
}
}
},
{
"id": "p_B",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"minzoom": 10,
"filter": [
"all",
["==", "$type", "LineString"],
["==", "class", "path"],
["in", "smoothness", "good", "intermediate", "bad"],
[
"!in",
"surface",
"gravel",
"fine_gravel",
"compacted",
"cobblestone",
"sett",
"unhewn_cobblestone",
"paving_stones"
],
["!in", "bicycle", "no"],
["!in", "access", "no"]
],
"layout": {
"line-cap": "butt",
"line-join": "miter",
"visibility": "visible"
},
"paint": {
"line-color": "rgba(235, 6, 158, 1)",
"line-width": {
"base": 1.55,
"stops": [
[10, 0.4],
[12, 1.1],
[14, 1.5]
]
},
"line-offset": {
"stops": [
[12, 0],
[13, 2],
[15, 4],
[16, 5]
],
"base": 1.55
},
"line-dasharray": [1.5, 1]
}
},
{
"id": "p_A-case",
"type": "line",
"metadata": {
"maputnik:comment": "Gravel surface with ok-ish smoothness"
},
"source": "openmaptiles",
"source-layer": "transportation",
"minzoom": 10,
"filter": [
"all",
["==", ["geometry-type"], "LineString"],
["==", ["get", "class"], "path"],
[
"any",
["match", ["get", "surface"], ["compacted", "fine_gravel"], true, false],
[
"all",
["match", ["get", "surface"], ["gravel"], true, false],
[
"match",
["get", "smoothness"],
["bad", "good", "intermediate"],
true,
false
]
]
],
["match", ["get", "bicycle"], ["no"], false, true],
["match", ["get", "access"], ["no"], false, true]
],
"layout": {
"line-cap": "butt",
"line-join": "miter",
"visibility": "visible"
},
"paint": {
"line-color": "rgba(255, 255, 255, 1)",
"line-width": {
"base": 1.55,
"stops": [
[10, 0.7],
[12, 2.5],
[14, 3.2]
]
},
"line-offset": {
"stops": [
[12, 0],
[13, 2],
[15, 4],
[16, 5]
],
"base": 1.55
}
}
},
{
"id": "p_A",
"type": "line",
"metadata": {
"maputnik:comment": "Gravel surface with ok-ish smoothness"
},
"source": "openmaptiles",
"source-layer": "transportation",
"minzoom": 10,
"filter": [
"all",
["==", ["geometry-type"], "LineString"],
["==", ["get", "class"], "path"],
[
"any",
["match", ["get", "surface"], ["compacted", "fine_gravel"], true, false],
[
"all",
["match", ["get", "surface"], ["gravel"], true, false],
[
"match",
["get", "smoothness"],
["bad", "good", "intermediate"],
true,
false
]
]
],
["match", ["get", "bicycle"], ["no"], false, true],
["match", ["get", "access"], ["no"], false, true]
],
"layout": {
"line-cap": "butt",
"line-join": "miter",
"visibility": "visible"
},
"paint": {
"line-color": "rgba(235, 6, 158, 1)",
"line-width": {
"base": 1.55,
"stops": [
[10, 0.5],
[12, 1.5],
[14, 2]
]
},
"line-offset": {
"stops": [
[12, 0],
[13, 2],
[15, 4],
[16, 5]
],
"base": 1.55
}
}
},
{
"id": "r_X_cobbles-case",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"minzoom": 10,
"filter": [
"all",
["==", "$type", "LineString"],
["in", "class", "minor", "service", "track", "path", "residential"],
["in", "surface", "sett", "cobblestone", "unhewn_cobblestone"],
["!in", "service", "driveway"]
],
"layout": {
"line-cap": "butt",
"line-join": "miter",
"visibility": "visible"
},
"paint": {
"line-color": "rgba(0, 0, 0, 1)",
"line-width": {
"base": 1.55,
"stops": [
[10, 0.5],
[12, 1.5],
[14, 2]
]
}
}
},
{
"id": "r_X_cobbles",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"minzoom": 10,
"filter": [
"all",
["==", "$type", "LineString"],
["in", "class", "minor", "service", "track", "path", "residential"],
["in", "surface", "sett", "cobblestone", "unhewn_cobblestone"],
["!in", "service", "driveway"]
],
"layout": {
"line-cap": "butt",
"line-join": "miter",
"visibility": "visible"
},
"paint": {
"line-color": "rgba(245, 255, 0, 1)",
"line-width": {
"base": 1.55,
"stops": [
[10, 0.5],
[12, 1.5],
[14, 2]
]
},
"line-dasharray": [1.5, 1]
}
},
{
"id": "r_X-bg",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"minzoom": 10,
"filter": [
"all",
["==", "$type", "LineString"],
["in", "class", "minor"],
["in", "smoothness", "very_bad", "horrible", "very_horrible", "impassable"],
["!in", "surface", "sett", "cobblestone", "unhewn_cobblestone"]
],
"layout": {
"line-cap": "square",
"line-join": "bevel",
"visibility": "visible"
},
"paint": {
"line-color": "rgba(255, 0, 0, 0.7)",
"line-width": {
"base": 1.55,
"stops": [
[10, 0.4],
[12, 1.1],
[14, 1.5]
]
}
}
},
{
"id": "r_X",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"minzoom": 10,
"filter": [
"all",
["==", "$type", "LineString"],
["in", "class", "minor"],
["in", "smoothness", "very_bad", "horrible", "very_horrible", "impassable"],
["!in", "surface", "sett", "cobblestone", "unhewn_cobblestone"]
],
"layout": {
"line-cap": "square",
"line-join": "bevel",
"visibility": "visible"
},
"paint": {
"line-color": "rgba(255, 255, 0, 0.7)",
"line-width": {
"base": 1.55,
"stops": [
[10, 0.4],
[12, 1.1],
[14, 1.5]
]
},
"line-dasharray": [2, 2]
}
},
{
"id": "r_A_case",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"minzoom": 10,
"filter": [
"all",
["==", "$type", "LineString"],
["in", "class", "minor", "residential", "service"],
["in", "surface", "gravel", "compacted", "fine_gravel"],
["!in", "service", "driveway", "parking_aisle", "drive-through", "emergency_access"]
],
"layout": {
"line-cap": "square",
"line-join": "bevel",
"visibility": "visible"
},
"paint": {
"line-color": "rgba(255, 255, 255, 1)",
"line-width": {
"base": 1.55,
"stops": [
[10, 0.8],
[12, 3],
[14, 4]
]
}
}
},
{
"id": "r_A",
"type": "line",
"source": "openmaptiles",
"source-layer": "transportation",
"minzoom": 10,
"filter": [
"all",
["==", "$type", "LineString"],
["in", "class", "minor", "residential", "service"],
["in", "surface", "gravel", "compacted", "fine_gravel"],
["!in", "service", "driveway", "parking_aisle", "drive-through", "emergency_access"]
],
"layout": {
"line-cap": "square",
"line-join": "bevel",
"visibility": "visible"
},
"paint": {
"line-color": "rgba(235, 6, 158, 1)",
"line-width": {
"base": 1.55,
"stops": [
[10, 0.5],
[12, 1.5],
[14, 2]
]
}
}
},
{
"id": "cemetery",
"type": "symbol",
"source": "openmaptiles",
"source-layer": "landuse",
"filter": ["all", ["==", "class", "cemetery"]],
"layout": {
"icon-image": "cemetery_11",
"icon-rotation-alignment": "map",
"icon-size": 1.5
}
},
{
"id": "drinking_water",
"type": "symbol",
"source": "openmaptiles",
"source-layer": "poi",
"minzoom": 9,
"maxzoom": 20,
"filter": [
"any",
["==", "class", "drinking_water"],
["==", "subclass", "drinking_water"]
],
"layout": {
"icon-image": "drinking_water_11",
"visibility": "visible",
"icon-rotation-alignment": "map",
"icon-size": 1.4
}
}
],
"id": "basic",
"owner": "Marcus Jaschen"
}
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: 1.4 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

File diff suppressed because it is too large Load Diff
-31
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'
}
+81 -8
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 {
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"; Landmark,
import type { ComponentType } from "svelte"; 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 = { export type Symbol = {
value: string; value: string;
@@ -20,16 +81,28 @@ export const symbols: { [key: string]: Symbol } = {
campground: { value: 'Campground', icon: Tent, iconSvg: TentSvg }, campground: { value: 'Campground', icon: Tent, iconSvg: TentSvg },
car: { value: 'Car', icon: Car, iconSvg: CarSvg }, car: { value: 'Car', icon: Car, iconSvg: CarSvg },
car_repair: { value: 'Car Repair', icon: Wrench, iconSvg: WrenchSvg }, 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' }, 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 }, drinking_water: { value: 'Drinking Water', icon: Droplet, iconSvg: DropletSvg },
exit: { value: 'Exit', icon: DoorOpen, iconSvg: DoorOpenSvg }, exit: { value: 'Exit', icon: DoorOpen, iconSvg: DoorOpenSvg },
lodge: { value: 'Lodge', icon: Home, iconSvg: HomeSvg }, lodge: { value: 'Lodge', icon: Home, iconSvg: HomeSvg },
lodging: { value: 'Lodging', icon: Bed, iconSvg: BedSvg }, lodging: { value: 'Lodging', icon: Bed, iconSvg: BedSvg },
forest: { value: 'Forest', icon: Trees, iconSvg: TreesSvg }, forest: { value: 'Forest', icon: Trees, iconSvg: TreesSvg },
gas_station: { value: 'Gas Station', icon: Fuel, iconSvg: FuelSvg }, 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 }, hotel: { value: 'Hotel', icon: Bed, iconSvg: BedSvg },
house: { value: 'House', icon: Home, iconSvg: HomeSvg }, house: { value: 'House', icon: Home, iconSvg: HomeSvg },
information: { value: 'Information', icon: Info, iconSvg: InfoSvg }, 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 }, picnic_area: { value: 'Picnic Area', icon: Utensils, iconSvg: UtensilsSvg },
restaurant: { value: 'Restaurant', icon: Utensils, iconSvg: UtensilsSvg }, restaurant: { value: 'Restaurant', icon: Utensils, iconSvg: UtensilsSvg },
restricted_area: { value: 'Restricted Area', icon: Construction, iconSvg: ConstructionSvg }, 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 }, road: { value: 'Road', icon: BrickWall, iconSvg: BrickWallSvg },
scenic_area: { value: 'Scenic Area', icon: Binoculars, iconSvg: BinocularsSvg }, scenic_area: { value: 'Scenic Area', icon: Binoculars, iconSvg: BinocularsSvg },
shelter: { value: 'Shelter', icon: Tent, iconSvg: TentSvg }, shelter: { value: 'Shelter', icon: Tent, iconSvg: TentSvg },
@@ -55,6 +128,6 @@ export function getSymbolKey(value: string | undefined): string | undefined {
if (value === undefined) { if (value === undefined) {
return undefined; return undefined;
} else { } else {
return Object.keys(symbols).find(key => symbols[key].value === value); return Object.keys(symbols).find((key) => symbols[key].value === value);
} }
} }
@@ -0,0 +1,60 @@
<script lang="ts">
import docsearch from '@docsearch/js';
import '@docsearch/css';
import { onMount } from 'svelte';
import { _, locale, waitLocale } from 'svelte-i18n';
let mounted = false;
function initDocsearch() {
docsearch({
appId: '21XLD94PE3',
apiKey: 'd2c1ed6cb0ed12adb2bd84eb2a38494d',
indexName: 'gpx',
container: '#docsearch',
searchParameters: {
facetFilters: ['lang:' + ($locale ?? 'en')],
},
placeholder: $_('docs.search.search'),
disableUserPersonalization: true,
translations: {
button: {
buttonText: $_('docs.search.search'),
buttonAriaLabel: $_('docs.search.search'),
},
modal: {
searchBox: {
resetButtonTitle: $_('docs.search.clear'),
resetButtonAriaLabel: $_('docs.search.clear'),
cancelButtonText: $_('docs.search.cancel'),
cancelButtonAriaLabel: $_('docs.search.cancel'),
searchInputLabel: $_('docs.search.search'),
},
footer: {
selectText: $_('docs.search.to_select'),
navigateText: $_('docs.search.to_navigate'),
closeText: $_('docs.search.to_close'),
},
noResultsScreen: {
noResultsText: $_('docs.search.no_results'),
suggestedQueryText: $_('docs.search.no_results_suggestion'),
},
},
},
});
}
onMount(() => {
mounted = true;
});
$: if (mounted && $locale) {
waitLocale().then(initDocsearch);
}
</script>
<svelte:head>
<link rel="preconnect" href="https://21XLD94PE3-dsn.algolia.net" crossorigin />
</svelte:head>
<div id="docsearch" {...$$restProps}></div>
@@ -0,0 +1,28 @@
<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'
| 'secondary'
| 'link'
| 'destructive'
| 'outline'
| 'ghost'
| 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={[...builders, builder]} {variant} {...$$restProps} on:click>
<slot />
</Button>
</Tooltip.Trigger>
<Tooltip.Content {side}>
<span>{label}</span>
</Tooltip.Content>
</Tooltip.Root>
@@ -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>
+229 -224
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <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 * as ToggleGroup from '$lib/components/ui/toggle-group';
import Tooltip from '$lib/components/Tooltip.svelte';
import Chart from 'chart.js/auto'; import Chart from 'chart.js/auto';
import mapboxgl from 'mapbox-gl'; import mapboxgl from 'mapbox-gl';
import { map } from '$lib/stores'; import { map } from '$lib/stores';
@@ -12,12 +13,15 @@
Orbit, Orbit,
SquareActivity, SquareActivity,
Thermometer, Thermometer,
Zap Zap,
Circle,
Check,
ChartNoAxesColumn,
Construction,
} from 'lucide-svelte'; } from 'lucide-svelte';
import { surfaceColors } from '$lib/assets/surfaces'; import { getSlopeColor, getSurfaceColor, getHighwayColor } from '$lib/assets/colors';
import { _, locale } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import { import {
getCadenceUnits,
getCadenceWithUnits, getCadenceWithUnits,
getConvertedDistance, getConvertedDistance,
getConvertedElevation, getConvertedElevation,
@@ -26,45 +30,26 @@
getDistanceUnits, getDistanceUnits,
getDistanceWithUnits, getDistanceWithUnits,
getElevationWithUnits, getElevationWithUnits,
getHeartRateUnits,
getHeartRateWithUnits, getHeartRateWithUnits,
getPowerUnits,
getPowerWithUnits, getPowerWithUnits,
getTemperatureUnits,
getTemperatureWithUnits, getTemperatureWithUnits,
getVelocityUnits,
getVelocityWithUnits, getVelocityWithUnits,
secondsToHHMMSS
} from '$lib/units'; } from '$lib/units';
import type { Writable } from 'svelte/store'; import type { Writable } from 'svelte/store';
import { DateFormatter } from '@internationalized/date';
import type { GPXStatistics } from 'gpx'; import type { GPXStatistics } from 'gpx';
import { settings } from '$lib/db'; import { settings } from '$lib/db';
import { mode } from 'mode-watcher'; import { mode } from 'mode-watcher';
import { df } from '$lib/utils';
export let gpxStatistics: Writable<GPXStatistics>; export let gpxStatistics: Writable<GPXStatistics>;
export let slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>; export let slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
export let panelSize: number;
export let additionalDatasets: string[]; export let additionalDatasets: string[];
export let elevationFill: 'slope' | 'surface' | undefined; export let elevationFill: 'slope' | 'surface' | 'highway' | undefined;
export let showControls: boolean = true; export let showControls: boolean = true;
const { distanceUnits, velocityUnits, temperatureUnits } = settings; const { distanceUnits, velocityUnits, temperatureUnits } = settings;
let df: DateFormatter;
$: if ($locale) {
df = new DateFormatter($locale, {
dateStyle: 'medium',
timeStyle: 'medium'
});
}
let canvas: HTMLCanvasElement; let canvas: HTMLCanvasElement;
let showAdditionalScales = true;
let updateShowAdditionalScales = () => {
showAdditionalScales = canvas.width / window.devicePixelRatio >= 600;
};
let overlay: HTMLCanvasElement; let overlay: HTMLCanvasElement;
let chart: Chart; let chart: Chart;
@@ -83,41 +68,41 @@
x: { x: {
type: 'linear', type: 'linear',
ticks: { ticks: {
callback: function (value: number, index: number, ticks: { value: number }[]) { callback: function (value: number) {
if (index === ticks.length - 1) {
return `${value.toFixed(1).replace(/\.0+$/, '')}`;
}
return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`; return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
} },
} align: 'inner',
maxRotation: 0,
},
}, },
y: { y: {
type: 'linear', type: 'linear',
ticks: { ticks: {
callback: function (value: number) { callback: function (value: number) {
return getElevationWithUnits(value, false); return getElevationWithUnits(value, false);
} },
} },
} },
}, },
datasets: { datasets: {
line: { line: {
pointRadius: 0, pointRadius: 0,
tension: 0.4, tension: 0.4,
borderWidth: 2 borderWidth: 2,
} cubicInterpolationMode: 'monotone',
},
}, },
interaction: { interaction: {
mode: 'nearest', mode: 'nearest',
axis: 'x', axis: 'x',
intersect: false intersect: false,
}, },
plugins: { plugins: {
legend: { legend: {
display: false display: false,
}, },
decimation: { decimation: {
enabled: true enabled: true,
}, },
tooltip: { tooltip: {
enabled: () => !dragging && !panning, enabled: () => !dragging && !panning,
@@ -156,13 +141,20 @@
let slope = { let slope = {
at: point.slope.at.toFixed(1), at: point.slope.at.toFixed(1),
segment: point.slope.segment.toFixed(1), segment: point.slope.segment.toFixed(1),
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 = [ let labels = [
` ${$_('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`, ` ${$_('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') { if (elevationFill === 'surface') {
@@ -171,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) { if (point.time) {
labels.push(` ${$_('quantities.time')}: ${df.format(point.time)}`); labels.push(` ${$_('quantities.time')}: ${df.format(point.time)}`);
} }
return labels; return labels;
} },
} },
}, },
zoom: { zoom: {
pan: { pan: {
@@ -191,18 +196,19 @@
}, },
onPanComplete: function () { onPanComplete: function () {
panning = false; panning = false;
} },
}, },
zoom: { zoom: {
wheel: { wheel: {
enabled: true enabled: true,
}, },
mode: 'x', mode: 'x',
onZoomStart: function ({ chart, event }: { chart: Chart; event: any }) { onZoomStart: function ({ chart, event }: { chart: Chart; event: any }) {
if ( if (
event.deltaY < 0 && event.deltaY < 0 &&
Math.abs( 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() chart.getZoomLevel()
) < 0.01 ) < 0.01
) { ) {
@@ -211,86 +217,35 @@
} }
$slicedGPXStatistics = undefined; $slicedGPXStatistics = undefined;
} },
}, },
limits: { limits: {
x: { x: {
min: 'original', min: 'original',
max: 'original', max: 'original',
minRange: 1 minRange: 1,
} },
} },
} },
}, },
stacked: false, stacked: false,
onResize: function () { onResize: function () {
updateOverlay(); updateOverlay();
updateShowAdditionalScales(); },
}
}; };
let datasets: { let datasets: string[] = ['speed', 'hr', 'cad', 'atemp', 'power'];
[key: string]: { datasets.forEach((id) => {
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)) {
options.scales[`y${id}`] = { options.scales[`y${id}`] = {
type: 'linear', type: 'linear',
position: 'right', position: 'right',
title: {
display: true,
text: dataset.getLabel() + ' (' + dataset.getUnits() + ')',
padding: {
top: 6,
bottom: 0
}
},
grid: { grid: {
display: false display: false,
}, },
reverse: () => id === 'speed' && $velocityUnits === 'pace', reverse: () => id === 'speed' && $velocityUnits === 'pace',
display: false display: false,
};
}
options.scales.yspeed['ticks'] = {
callback: function (value: number) {
if ($velocityUnits === 'speed') {
return value;
} else {
return secondsToHHMMSS(value);
}
}
}; };
});
onMount(async () => { onMount(async () => {
Chart.register((await import('chartjs-plugin-zoom')).default); // dynamic import to avoid SSR and 'window is not defined' error Chart.register((await import('chartjs-plugin-zoom')).default); // dynamic import to avoid SSR and 'window is not defined' error
@@ -298,7 +253,7 @@
chart = new Chart(canvas, { chart = new Chart(canvas, {
type: 'line', type: 'line',
data: { data: {
datasets: [] datasets: [],
}, },
options, options,
plugins: [ plugins: [
@@ -311,20 +266,18 @@
marker.remove(); marker.remove();
} }
} }
} },
} },
] ],
}); });
// Map marker to show on hover // Map marker to show on hover
let element = document.createElement('div'); let element = document.createElement('div');
element.className = 'h-4 w-4 rounded-full bg-cyan-500 border-2 border-white'; element.className = 'h-4 w-4 rounded-full bg-cyan-500 border-2 border-white';
marker = new mapboxgl.Marker({ marker = new mapboxgl.Marker({
element element,
}); });
updateShowAdditionalScales();
let startIndex = 0; let startIndex = 0;
let endIndex = 0; let endIndex = 0;
function getIndex(evt) { function getIndex(evt) {
@@ -332,7 +285,7 @@
evt, evt,
'x', 'x',
{ {
intersect: false intersect: false,
}, },
true true
); );
@@ -375,9 +328,12 @@
startIndex = endIndex; startIndex = endIndex;
} else if (startIndex !== endIndex) { } else if (startIndex !== endIndex) {
$slicedGPXStatistics = [ $slicedGPXStatistics = [
$gpxStatistics.slice(Math.min(startIndex, endIndex), Math.max(startIndex, endIndex)), $gpxStatistics.slice(
Math.min(startIndex, endIndex), Math.min(startIndex, endIndex),
Math.max(startIndex, endIndex) Math.max(startIndex, endIndex)
),
Math.min(startIndex, endIndex),
Math.max(startIndex, endIndex),
]; ];
} }
} }
@@ -411,126 +367,111 @@
slope: { slope: {
at: data.local.slope.at[index], at: data.local.slope.at[index],
segment: data.local.slope.segment[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(), coordinates: point.getCoordinates(),
index: index index: index,
}; };
}), }),
normalized: true, normalized: true,
fill: 'start', fill: 'start',
order: 1 order: 1,
}; };
chart.data.datasets[1] = { chart.data.datasets[1] = {
label: datasets.speed.getLabel(),
data: data.local.points.map((point, index) => { data: data.local.points.map((point, index) => {
return { return {
x: getConvertedDistance(data.local.distance.total[index]), x: getConvertedDistance(data.local.distance.total[index]),
y: getConvertedVelocity(data.local.speed[index]), y: getConvertedVelocity(data.local.speed[index]),
index: index index: index,
}; };
}), }),
normalized: true, normalized: true,
yAxisID: `y${datasets.speed.id}`, yAxisID: 'yspeed',
hidden: true hidden: true,
}; };
chart.data.datasets[2] = { chart.data.datasets[2] = {
label: datasets.hr.getLabel(),
data: data.local.points.map((point, index) => { data: data.local.points.map((point, index) => {
return { return {
x: getConvertedDistance(data.local.distance.total[index]), x: getConvertedDistance(data.local.distance.total[index]),
y: point.getHeartRate(), y: point.getHeartRate(),
index: index index: index,
}; };
}), }),
normalized: true, normalized: true,
yAxisID: `y${datasets.hr.id}`, yAxisID: 'yhr',
hidden: true hidden: true,
}; };
chart.data.datasets[3] = { chart.data.datasets[3] = {
label: datasets.cad.getLabel(),
data: data.local.points.map((point, index) => { data: data.local.points.map((point, index) => {
return { return {
x: getConvertedDistance(data.local.distance.total[index]), x: getConvertedDistance(data.local.distance.total[index]),
y: point.getCadence(), y: point.getCadence(),
index: index index: index,
}; };
}), }),
normalized: true, normalized: true,
yAxisID: `y${datasets.cad.id}`, yAxisID: 'ycad',
hidden: true hidden: true,
}; };
chart.data.datasets[4] = { chart.data.datasets[4] = {
label: datasets.atemp.getLabel(),
data: data.local.points.map((point, index) => { data: data.local.points.map((point, index) => {
return { return {
x: getConvertedDistance(data.local.distance.total[index]), x: getConvertedDistance(data.local.distance.total[index]),
y: getConvertedTemperature(point.getTemperature()), y: getConvertedTemperature(point.getTemperature()),
index: index index: index,
}; };
}), }),
normalized: true, normalized: true,
yAxisID: `y${datasets.atemp.id}`, yAxisID: 'yatemp',
hidden: true hidden: true,
}; };
chart.data.datasets[5] = { chart.data.datasets[5] = {
label: datasets.power.getLabel(),
data: data.local.points.map((point, index) => { data: data.local.points.map((point, index) => {
return { return {
x: getConvertedDistance(data.local.distance.total[index]), x: getConvertedDistance(data.local.distance.total[index]),
y: point.getPower(), y: point.getPower(),
index: index index: index,
}; };
}), }),
normalized: true, normalized: true,
yAxisID: `y${datasets.power.id}`, yAxisID: 'ypower',
hidden: true hidden: true,
}; };
chart.options.scales.x['min'] = 0; chart.options.scales.x['min'] = 0;
chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total); 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(); chart.update();
} }
let maxSlope = 20;
function slopeFillCallback(context) { function slopeFillCallback(context) {
let slope = context.p0.raw.slope.segment; return getSlopeColor(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('');
} }
function surfaceFillCallback(context) { function surfaceFillCallback(context) {
let surface = context.p0.raw.surface; return getSurfaceColor(context.p0.raw.extensions.surface);
return surfaceColors[surface] ? surfaceColors[surface] : surfaceColors.missing; }
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 (chart) {
if (elevationFill === 'slope') { if (elevationFill === 'slope') {
chart.data.datasets[0]['segment'] = { chart.data.datasets[0]['segment'] = {
backgroundColor: slopeFillCallback backgroundColor: slopeFillCallback,
}; };
} else if (elevationFill === 'surface') { } else if (elevationFill === 'surface') {
chart.data.datasets[0]['segment'] = { chart.data.datasets[0]['segment'] = {
backgroundColor: surfaceFillCallback backgroundColor: surfaceFillCallback,
};
} else if (elevationFill === 'highway') {
chart.data.datasets[0]['segment'] = {
backgroundColor: highwayFillCallback,
}; };
} else { } else {
chart.data.datasets[0]['segment'] = {}; chart.data.datasets[0]['segment'] = {};
@@ -551,12 +492,6 @@
chart.data.datasets[4].hidden = !includeTemperature; chart.data.datasets[4].hidden = !includeTemperature;
chart.data.datasets[5].hidden = !includePower; 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(); chart.update();
} }
@@ -567,6 +502,8 @@
overlay.width = canvas.width / window.devicePixelRatio; overlay.width = canvas.width / window.devicePixelRatio;
overlay.height = canvas.height / window.devicePixelRatio; overlay.height = canvas.height / window.devicePixelRatio;
overlay.style.width = `${overlay.width}px`;
overlay.style.height = `${overlay.height}px`;
if ($slicedGPXStatistics) { if ($slicedGPXStatistics) {
let startIndex = $slicedGPXStatistics[1]; let startIndex = $slicedGPXStatistics[1];
@@ -590,7 +527,7 @@
startPixel, startPixel,
chart.chartArea.top, chart.chartArea.top,
endPixel - startPixel, endPixel - startPixel,
chart.chartArea.bottom - chart.chartArea.top chart.chartArea.height
); );
} }
} else if (overlay) { } else if (overlay) {
@@ -610,73 +547,141 @@
}); });
</script> </script>
<div class="h-full grow min-w-0 flex flex-row gap-4 items-center {$$props.class ?? ''}"> <div class="h-full grow min-w-0 relative py-2">
<div class="grow h-full min-w-0 relative"> <canvas bind:this={overlay} class="w-full h-full absolute pointer-events-none"></canvas>
<canvas bind:this={overlay} class=" w-full h-full absolute pointer-events-none"></canvas> <canvas bind:this={canvas} class="w-full h-full absolute"></canvas>
<canvas bind:this={canvas} class="w-full h-full"></canvas>
</div>
{#if showControls} {#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 <ToggleGroup.Root
class="{panelSize > 158 class="flex flex-col items-start gap-0 p-1"
? 'flex-col'
: 'flex-row'} flex-wrap gap-0 min-h-0 content-center border rounded-t-md"
type="single" type="single"
bind:value={elevationFill} bind:value={elevationFill}
> >
<ToggleGroup.Item class="p-0 w-5 h-5" value="slope"> <ToggleGroup.Item
<Tooltip side="left"> 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"
<TriangleRight slot="data" size="15" /> value="slope"
<span slot="tooltip">{$_('chart.show_slope')}</span> >
</Tooltip> <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>
<ToggleGroup.Item class="p-0 w-5 h-5" value="surface"> <ToggleGroup.Item
<Tooltip side="left"> 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"
<BrickWall slot="data" size="15" /> value="surface"
<span slot="tooltip">{$_('chart.show_surface')}</span> variant="outline"
</Tooltip> >
<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.Item>
</ToggleGroup.Root> </ToggleGroup.Root>
<ToggleGroup.Root <ToggleGroup.Root
class="{panelSize > 158 class="flex flex-col items-start gap-0 p-1"
? 'flex-col'
: 'flex-row'} flex-wrap gap-0 min-h-0 content-center border rounded-b-md -mt-[1px]"
type="multiple" type="multiple"
bind:value={additionalDatasets} bind:value={additionalDatasets}
> >
<ToggleGroup.Item class="p-0 w-5 h-5" value="speed"> <ToggleGroup.Item
<Tooltip side="left"> 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"
<Zap slot="data" size="15" /> value="speed"
<span slot="tooltip"
>{$velocityUnits === 'speed' ? $_('chart.show_speed') : $_('chart.show_pace')}</span
> >
</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>
<ToggleGroup.Item class="p-0 w-5 h-5" value="hr"> <ToggleGroup.Item
<Tooltip side="left"> 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"
<HeartPulse slot="data" size="15" /> value="hr"
<span slot="tooltip">{$_('chart.show_heartrate')}</span> >
</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>
<ToggleGroup.Item class="p-0 w-5 h-5" value="cad"> <ToggleGroup.Item
<Tooltip side="left"> 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"
<Orbit slot="data" size="15" /> value="cad"
<span slot="tooltip">{$_('chart.show_cadence')}</span> >
</Tooltip> <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>
<ToggleGroup.Item class="p-0 w-5 h-5" value="atemp"> <ToggleGroup.Item
<Tooltip side="left"> 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"
<Thermometer slot="data" size="15" /> value="atemp"
<span slot="tooltip">{$_('chart.show_temperature')}</span> >
</Tooltip> <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>
<ToggleGroup.Item class="p-0 w-5 h-5" value="power"> <ToggleGroup.Item
<Tooltip side="left"> 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"
<SquareActivity slot="data" size="15" /> value="power"
<span slot="tooltip">{$_('chart.show_power')}</span> >
</Tooltip> <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.Item>
</ToggleGroup.Root> </ToggleGroup.Root>
</Popover.Content>
</Popover.Root>
</div> </div>
{/if} {/if}
</div> </div>
+24 -15
View File
@@ -10,17 +10,17 @@
exportSelectedFiles, exportSelectedFiles,
ExportState, ExportState,
exportState, exportState,
gpxStatistics gpxStatistics,
} from '$lib/stores'; } from '$lib/stores';
import { fileObservers } from '$lib/db'; import { fileObservers } from '$lib/db';
import { import {
Download, Download,
Zap, Zap,
BrickWall, Earth,
HeartPulse, HeartPulse,
Orbit, Orbit,
Thermometer, Thermometer,
SquareActivity SquareActivity,
} from 'lucide-svelte'; } from 'lucide-svelte';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import { selection } from './file-list/Selection'; import { selection } from './file-list/Selection';
@@ -31,19 +31,19 @@
let open = false; let open = false;
let exportOptions: Record<string, boolean> = { let exportOptions: Record<string, boolean> = {
time: true, time: true,
surface: true,
hr: true, hr: true,
cad: true, cad: true,
atemp: true, atemp: true,
power: true power: true,
extensions: true,
}; };
let hide: Record<string, boolean> = { let hide: Record<string, boolean> = {
time: false, time: false,
surface: false,
hr: false, hr: false,
cad: false, cad: false,
atemp: false, atemp: false,
power: false power: false,
extensions: false,
}; };
$: if ($exportState !== ExportState.NONE) { $: if ($exportState !== ExportState.NONE) {
@@ -67,6 +67,7 @@
hide.cad = statistics.global.cad.count === 0; hide.cad = statistics.global.cad.count === 0;
hide.atemp = statistics.global.atemp.count === 0; hide.atemp = statistics.global.atemp.count === 0;
hide.power = statistics.global.power.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]); $: exclude = Object.keys(exportOptions).filter((key) => !exportOptions[key]);
@@ -86,10 +87,10 @@
class="fixed left-[50%] top-[50%] z-50 w-fit max-w-full translate-x-[-50%] translate-y-[-50%] flex flex-col items-center gap-3 border bg-background p-3 shadow-lg rounded-md" class="fixed left-[50%] top-[50%] z-50 w-fit max-w-full translate-x-[-50%] translate-y-[-50%] flex flex-col items-center gap-3 border bg-background p-3 shadow-lg rounded-md"
> >
<div <div
class="w-full flex flex-row items-center justify-center gap-4 border rounded-md p-2 bg-accent" class="w-full flex flex-row items-center justify-center gap-4 border rounded-md p-2 bg-secondary"
> >
<span>⚠️</span> <span>⚠️</span>
<span class="max-w-96 text-sm"> <span class="max-w-[80%] text-sm">
{$_('menu.support_message')} {$_('menu.support_message')}
</span> </span>
</div> </div>
@@ -119,7 +120,13 @@
{/if} {/if}
</Button> </Button>
</div> </div>
<div class="w-full max-w-xl flex flex-col items-center gap-2"> <div
class="w-full max-w-xl flex flex-col items-center gap-2 {Object.values(hide).some(
(v) => !v
)
? ''
: 'hidden'}"
>
<div class="w-full flex flex-row items-center gap-3"> <div class="w-full flex flex-row items-center gap-3">
<div class="grow"> <div class="grow">
<Separator /> <Separator />
@@ -139,11 +146,13 @@
{$_('quantities.time')} {$_('quantities.time')}
</Label> </Label>
</div> </div>
<div class="flex flex-row items-center gap-1.5"> <div
<Checkbox id="export-surface" bind:checked={exportOptions.surface} /> class="flex flex-row items-center gap-1.5 {hide.extensions ? 'hidden' : ''}"
<Label for="export-surface" class="flex flex-row items-center gap-1"> >
<BrickWall size="16" /> <Checkbox id="export-extensions" bind:checked={exportOptions.extensions} />
{$_('quantities.surface')} <Label for="export-extensions" class="flex flex-row items-center gap-1">
<Earth size="16" />
{$_('quantities.osm_extensions')}
</Label> </Label>
</div> </div>
<div class="flex flex-row items-center gap-1.5 {hide.hr ? 'hidden' : ''}"> <div class="flex flex-row items-center gap-1.5 {hide.hr ? 'hidden' : ''}">
+10 -1
View File
@@ -11,7 +11,7 @@
<div class="mx-6 border-t"> <div class="mx-6 border-t">
<div class="mx-12 py-10 flex flex-row flex-wrap justify-between gap-x-10 gap-y-6"> <div class="mx-12 py-10 flex flex-row flex-wrap justify-between gap-x-10 gap-y-6">
<div class="grow flex flex-col items-start"> <div class="grow flex flex-col items-start">
<Logo class="h-8" /> <Logo class="h-8" width="153" />
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 text-muted-foreground"
@@ -52,6 +52,15 @@
</div> </div>
<div class="flex flex-col items-start gap-1" id="contact"> <div class="flex flex-col items-start gap-1" id="contact">
<span class="font-semibold">{$_('homepage.contact')}</span> <span class="font-semibold">{$_('homepage.contact')}</span>
<Button
variant="link"
class="h-6 px-0 text-muted-foreground"
href="https://www.reddit.com/r/gpxstudio/"
target="_blank"
>
<Logo company="reddit" class="h-4 mr-1 fill-muted-foreground" />
{$_('homepage.reddit')}
</Button>
<Button <Button
variant="link" variant="link"
class="h-6 px-0 text-muted-foreground" class="h-6 px-0 text-muted-foreground"
+29 -25
View File
@@ -28,7 +28,7 @@
<Card.Root <Card.Root
class="h-full {orientation === 'vertical' 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" : 'w-full'} border-none shadow-none"
> >
<Card.Content <Card.Content
@@ -36,48 +36,52 @@
? 'flex-col justify-center' ? 'flex-col justify-center'
: 'flex-row w-full justify-between'} gap-4 p-0" : 'flex-row w-full justify-between'} gap-4 p-0"
> >
<Tooltip> <Tooltip label={$_('quantities.distance')}>
<span slot="data" class="flex flex-row items-center"> <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" /> <WithUnits value={statistics.global.distance.total} type="distance" />
</span> </span>
<span slot="tooltip">{$_('quantities.distance')}</span>
</Tooltip> </Tooltip>
<Tooltip> <Tooltip label={$_('quantities.elevation_gain_loss')}>
<span slot="data" class="flex flex-row items-center"> <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" /> <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" /> <WithUnits value={statistics.global.elevation.loss} type="elevation" />
</span> </span>
<span slot="tooltip">{$_('quantities.elevation')}</span>
</Tooltip> </Tooltip>
{#if panelSize > 120 || orientation === 'horizontal'} {#if panelSize > 120 || orientation === 'horizontal'}
<Tooltip class={orientation === 'horizontal' ? 'hidden xs:block' : ''}> <Tooltip
<span slot="data" class="flex flex-row items-center"> class={orientation === 'horizontal' ? 'hidden xs:block' : ''}
<Zap size="18" class="mr-1" /> label="{$velocityUnits === 'speed'
<WithUnits value={statistics.global.speed.moving} type="speed" showUnits={false} /> ? $_('quantities.speed')
: $_('quantities.pace')} ({$_('quantities.moving')} / {$_('quantities.total')})"
>
<span class="flex flex-row items-center">
<Zap size="16" class="mr-1" />
<WithUnits
value={statistics.global.speed.moving}
type="speed"
showUnits={false}
/>
<span class="mx-1">/</span> <span class="mx-1">/</span>
<WithUnits value={statistics.global.speed.total} type="speed" /> <WithUnits value={statistics.global.speed.total} type="speed" />
</span> </span>
<span slot="tooltip"
>{$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_(
'quantities.moving'
)} / {$_('quantities.total')})</span
>
</Tooltip> </Tooltip>
{/if} {/if}
{#if panelSize > 160 || orientation === 'horizontal'} {#if panelSize > 160 || orientation === 'horizontal'}
<Tooltip class={orientation === 'horizontal' ? 'hidden md:block' : ''}> <Tooltip
<span slot="data" class="flex flex-row items-center"> class={orientation === 'horizontal' ? 'hidden md:block' : ''}
<Timer size="18" class="mr-1" /> label="{$_('quantities.time')} ({$_('quantities.moving')} / {$_(
'quantities.total'
)})"
>
<span class="flex flex-row items-center">
<Timer size="16" class="mr-1" />
<WithUnits value={statistics.global.time.moving} type="time" /> <WithUnits value={statistics.global.time.moving} type="time" />
<span class="mx-1">/</span> <span class="mx-1">/</span>
<WithUnits value={statistics.global.time.total} type="time" /> <WithUnits value={statistics.global.time.total} type="time" />
</span> </span>
<span slot="tooltip"
>{$_('quantities.time')} ({$_('quantities.moving')} / {$_('quantities.total')})</span
>
</Tooltip> </Tooltip>
{/if} {/if}
</Card.Content> </Card.Content>
-70
View File
@@ -1,70 +0,0 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { languages } from '$lib/languages';
import { _, isLoading } from 'svelte-i18n';
let location: string;
let title: string;
$: if ($page.route.id) {
location = $page.route.id;
Object.keys($page.params).forEach((param) => {
if (param !== 'language') {
location = location.replace(`[${param}]`, $page.params[param]);
location = location.replace(`[...${param}]`, $page.params[param]);
}
});
title = location.replace('/[...language]', '').split('/')[1] ?? 'home';
}
</script>
<svelte:head>
{#if $isLoading}
<title>gpx.studio — the online GPX file editor</title>
<meta
name="description"
content="View, edit and create GPX files online with advanced route planning capabilities and file processing tools, beautiful maps and detailed data visualizations."
/>
<meta property="og:title" content="gpx.studio — the online GPX file editor" />
<meta
property="og:description"
content="View, edit and create GPX files online with advanced route planning capabilities and file processing tools, beautiful maps and detailed data visualizations."
/>
<meta name="twitter:title" content="gpx.studio — the online GPX file editor" />
<meta
name="twitter:description"
content="View, edit and create GPX files online with advanced route planning capabilities and file processing tools, beautiful maps and detailed data visualizations."
/>
{:else}
<title>gpx.studio — {$_(`metadata.${title}_title`)}</title>
<meta name="description" content={$_('metadata.description')} />
<meta property="og:title" content="gpx.studio — {$_(`metadata.${title}_title`)}" />
<meta property="og:description" content={$_('metadata.description')} />
<meta name="twitter:title" content="gpx.studio — {$_(`metadata.${title}_title`)}" />
<meta name="twitter:description" content={$_('metadata.description')} />
{/if}
<meta property="og:image" content="https://gpx.studio/og_logo.png" />
<meta property="og:url" content="https://gpx.studio/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="gpx.studio" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://gpx.studio/og_logo.png" />
<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{base}{location.replace('/[...language]', '')}"
/>
{#each Object.keys(languages) as lang}
<link
rel="alternate"
hreflang={lang}
href="https://gpx.studio{base}{location.replace('[...language]', lang)}"
/>
{/each}
</svelte:head>
+4 -6
View File
@@ -5,16 +5,14 @@
export let link: string | undefined = undefined; export let link: string | undefined = undefined;
</script> </script>
<div class="text-sm bg-muted rounded border flex flex-row items-center p-2 {$$props.class || ''}"> <div
class="text-sm bg-secondary rounded border flex flex-row items-center p-2 {$$props.class || ''}"
>
<CircleHelp size="16" class="w-4 mr-2 shrink-0 grow-0" /> <CircleHelp size="16" class="w-4 mr-2 shrink-0 grow-0" />
<div> <div>
<slot /> <slot />
{#if link} {#if link}
<a <a href={link} target="_blank" class="text-sm text-link hover:underline">
href={link}
target="_blank"
class="text-sm text-blue-500 dark:text-blue-300 hover:underline"
>
{$_('menu.more')} {$_('menu.more')}
</a> </a>
{/if} {/if}
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores';
import * as Select from '$lib/components/ui/select'; import * as Select from '$lib/components/ui/select';
import { languages } from '$lib/languages'; import { languages } from '$lib/languages';
import { getURLForLanguage } from '$lib/utils'; import { getURLForLanguage } from '$lib/utils';
@@ -7,36 +8,44 @@
let selected = { let selected = {
value: '', value: '',
label: '' label: '',
}; };
$: if ($locale) { $: if ($locale) {
selected = { selected = {
value: $locale, value: $locale,
label: languages[$locale] label: languages[$locale],
}; };
} }
</script> </script>
<Select.Root bind:selected> <Select.Root bind:selected>
<Select.Trigger class="w-[180px] {$$props.class ?? ''}"> <Select.Trigger class="w-[180px] {$$props.class ?? ''}" aria-label={$_('menu.language')}>
<Languages size="16" /> <Languages size="16" />
<Select.Value class="ml-2 mr-auto" /> <Select.Value class="ml-2 mr-auto" />
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
{#each Object.entries(languages) as [lang, label]} {#each Object.entries(languages) as [lang, label]}
<a href={getURLForLanguage(lang)}> {#if $page.url.pathname.includes('404')}
<a href={getURLForLanguage(lang, '/')}>
<Select.Item value={lang}>{label}</Select.Item> <Select.Item value={lang}>{label}</Select.Item>
</a> </a>
{:else}
<a href={getURLForLanguage(lang, $page.url.pathname)}>
<Select.Item value={lang}>{label}</Select.Item>
</a>
{/if}
{/each} {/each}
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
<!-- hidden links for svelte crawling --> <!-- hidden links for svelte crawling -->
<div class="hidden"> <div class="hidden">
{#if !$page.url.pathname.includes('404')}
{#each Object.entries(languages) as [lang, label]} {#each Object.entries(languages) as [lang, label]}
<a href={getURLForLanguage(lang)}> <a href={getURLForLanguage(lang, $page.url.pathname)}>
{label} {label}
</a> </a>
{/each} {/each}
{/if}
</div> </div>
+10
View File
@@ -60,4 +60,14 @@
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/></svg /></svg
> >
{:else if company === 'reddit'}
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}"
><title>Reddit</title><path
d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z"
/></svg
>
{/if} {/if}
+100 -38
View File
@@ -22,16 +22,17 @@
mapboxgl.accessToken = accessToken; mapboxgl.accessToken = accessToken;
let webgl2Supported = true; let webgl2Supported = true;
let embeddedApp = false;
let fitBoundsOptions: mapboxgl.FitBoundsOptions = { let fitBoundsOptions: mapboxgl.FitBoundsOptions = {
maxZoom: 15, maxZoom: 15,
linear: true, linear: true,
easing: () => 1 easing: () => 1,
}; };
const { distanceUnits, elevationProfile, verticalFileView, bottomPanelSize, rightPanelSize } = const { distanceUnits, elevationProfile, treeFileView, bottomPanelSize, rightPanelSize } =
settings; settings;
let scaleControl = new mapboxgl.ScaleControl({ let scaleControl = new mapboxgl.ScaleControl({
unit: $distanceUnits unit: $distanceUnits,
}); });
onMount(() => { onMount(() => {
@@ -40,6 +41,10 @@
webgl2Supported = false; webgl2Supported = false;
return; return;
} }
if (window.top !== window.self && !$page.route.id?.includes('embed')) {
embeddedApp = true;
return;
}
let language = $page.params.language; let language = $page.params.language;
if (language === 'zh') { if (language === 'zh') {
@@ -52,52 +57,111 @@
let newMap = new mapboxgl.Map({ let newMap = new mapboxgl.Map({
container: 'map', container: 'map',
style: { version: 8, sources: {}, layers: [] }, style: {
version: 8,
sources: {},
layers: [],
imports: [
{
id: 'glyphs-and-sprite', // make Mapbox glyphs and sprite available to other styles
url: '',
data: {
version: 8,
sources: {},
layers: [],
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
sprite: `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${PUBLIC_MAPBOX_TOKEN}`,
},
},
{
id: 'basemap',
url: '',
},
{
id: 'overlays',
url: '',
data: {
version: 8,
sources: {},
layers: [],
},
},
],
},
projection: 'globe',
zoom: 0, zoom: 0,
hash: hash, hash: hash,
language, language,
attributionControl: false, attributionControl: false,
logoPosition: 'bottom-right', logoPosition: 'bottom-right',
boxZoom: false boxZoom: false,
}); });
newMap.on('load', () => { newMap.on('load', () => {
$map = newMap; // only set the store after the map has loaded $map = newMap; // only set the store after the map has loaded
window._map = newMap; // entry point for extensions
scaleControl.setUnit($distanceUnits); scaleControl.setUnit($distanceUnits);
}); });
newMap.addControl( newMap.addControl(
new mapboxgl.AttributionControl({ new mapboxgl.AttributionControl({
compact: true compact: true,
}) })
); );
newMap.addControl( newMap.addControl(
new mapboxgl.NavigationControl({ new mapboxgl.NavigationControl({
visualizePitch: true visualizePitch: true,
}) })
); );
if (geocoder) { if (geocoder) {
newMap.addControl( let geocoder = new MapboxGeocoder({
new MapboxGeocoder({
accessToken: mapboxgl.accessToken,
mapboxgl: mapboxgl, mapboxgl: mapboxgl,
enableEventLogging: false,
collapsed: true, collapsed: true,
flyTo: fitBoundsOptions, flyTo: fitBoundsOptions,
language language,
}) localGeocoder: () => [],
); localGeocoderOnly: true,
externalGeocoder: (query: string) =>
fetch(
`https://nominatim.openstreetmap.org/search?format=json&q=${query}&limit=5&accept-language=${language}`
)
.then((response) => response.json())
.then((data) => {
return data.map((result: any) => {
return {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [result.lon, result.lat],
},
place_name: result.display_name,
};
});
}),
});
let onKeyDown = geocoder._onKeyDown;
geocoder._onKeyDown = (e: KeyboardEvent) => {
// Trigger search on Enter key only
if (e.key === 'Enter') {
onKeyDown.apply(geocoder, [{ target: geocoder._inputEl }]);
} else if (geocoder._typeahead.data.length > 0) {
geocoder._typeahead.clear();
}
};
newMap.addControl(geocoder);
} }
if (geolocate) { if (geolocate) {
newMap.addControl( newMap.addControl(
new mapboxgl.GeolocateControl({ new mapboxgl.GeolocateControl({
positionOptions: { positionOptions: {
enableHighAccuracy: true enableHighAccuracy: true,
}, },
fitBoundsOptions, fitBoundsOptions,
trackUserLocation: true, trackUserLocation: true,
showUserHeading: true showUserHeading: true,
}) })
); );
} }
@@ -109,37 +173,28 @@
type: 'raster-dem', type: 'raster-dem',
url: 'mapbox://mapbox.mapbox-terrain-dem-v1', url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
tileSize: 512, tileSize: 512,
maxzoom: 14 maxzoom: 14,
}); });
if (newMap.getPitch() > 0) {
newMap.setTerrain({ newMap.setTerrain({
source: 'mapbox-dem', source: 'mapbox-dem',
exaggeration: newMap.getPitch() > 0 ? 1 : 0 exaggeration: 1,
}); });
}
newMap.setFog({ newMap.setFog({
color: 'rgb(186, 210, 235)', color: 'rgb(186, 210, 235)',
'high-color': 'rgb(36, 92, 223)', 'high-color': 'rgb(36, 92, 223)',
'horizon-blend': 0.1, 'horizon-blend': 0.1,
'space-color': 'rgb(156, 240, 255)' 'space-color': 'rgb(156, 240, 255)',
}); });
newMap.on('pitch', () => { newMap.on('pitch', () => {
if (newMap.getPitch() > 0) { if (newMap.getPitch() > 0) {
newMap.setTerrain({ newMap.setTerrain({
source: 'mapbox-dem', source: 'mapbox-dem',
exaggeration: 1 exaggeration: 1,
}); });
} else { } else {
newMap.setTerrain({ newMap.setTerrain(null);
source: 'mapbox-dem',
exaggeration: 0
});
}
});
// add dummy layer to place the overlay layers below
newMap.addLayer({
id: 'overlays',
type: 'background',
paint: {
'background-color': 'rgba(0, 0, 0, 0)'
} }
}); });
}); });
@@ -152,23 +207,30 @@
} }
}); });
$: if ( $: if ($map && (!$treeFileView || !$elevationProfile || $bottomPanelSize || $rightPanelSize)) {
$map &&
(!$verticalFileView || !$elevationProfile || $bottomPanelSize || $rightPanelSize)
) {
$map.resize(); $map.resize();
} }
</script> </script>
<div {...$$restProps}> <div {...$$restProps}>
<div id="map" class="h-full {webgl2Supported ? '' : 'hidden'}"></div> <div id="map" class="h-full {webgl2Supported && !embeddedApp ? '' : 'hidden'}"></div>
<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> <p>{$_('webgl2_required')}</p>
<Button href="https://get.webgl.org/webgl2/" target="_blank"> <Button href="https://get.webgl.org/webgl2/" target="_blank">
{$_('enable_webgl2')} {$_('enable_webgl2')}
</Button> </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>
</div> </div>
@@ -285,7 +347,7 @@
div :global(.mapboxgl-popup) { div :global(.mapboxgl-popup) {
@apply w-fit; @apply w-fit;
@apply z-20; @apply z-50;
} }
div :global(.mapboxgl-popup-content) { div :global(.mapboxgl-popup-content) {
@@ -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>
+82
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);
}
}
+148 -47
View File
@@ -21,8 +21,8 @@
Thermometer, Thermometer,
Sun, Sun,
Moon, Moon,
Layers3, Layers,
GalleryVertical, ListTree,
Languages, Languages,
Settings, Settings,
Info, Info,
@@ -41,7 +41,8 @@
FileStack, FileStack,
FileX, FileX,
BookOpenText, BookOpenText,
ChartArea ChartArea,
Maximize,
} from 'lucide-svelte'; } from 'lucide-svelte';
import { import {
@@ -54,7 +55,8 @@
editMetadata, editMetadata,
editStyle, editStyle,
exportState, exportState,
ExportState ExportState,
centerMapOnSelection,
} from '$lib/stores'; } from '$lib/stores';
import { import {
copied, copied,
@@ -62,7 +64,7 @@
cutSelection, cutSelection,
pasteSelection, pasteSelection,
selectAll, selectAll,
selection selection,
} from '$lib/components/file-list/Selection'; } from '$lib/components/file-list/Selection';
import { derived } from 'svelte/store'; import { derived } from 'svelte/store';
import { canUndo, canRedo, dbUtils, fileObservers, settings } from '$lib/db'; import { canUndo, canRedo, dbUtils, fileObservers, settings } from '$lib/db';
@@ -81,7 +83,7 @@
velocityUnits, velocityUnits,
temperatureUnits, temperatureUnits,
elevationProfile, elevationProfile,
verticalFileView, treeFileView,
currentBasemap, currentBasemap,
previousBasemap, previousBasemap,
currentOverlays, currentOverlays,
@@ -89,7 +91,7 @@
distanceMarkers, distanceMarkers,
directionMarkers, directionMarkers,
streetViewSource, streetViewSource,
routing routing,
} = settings; } = settings;
let undoDisabled = derived(canUndo, ($canUndo) => !$canUndo); let undoDisabled = derived(canUndo, ($canUndo) => !$canUndo);
@@ -126,13 +128,13 @@
<div <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" 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"> <a href={getURLForLanguage($locale, '/')} target="_blank" class="shrink-0">
<Logo class="h-5 mt-0.5 mx-2 md:hidden" iconOnly={true} /> <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" /> <Logo class="h-5 mt-0.5 mx-2 hidden md:block" width="96" />
</a> </a>
<Menubar.Root class="border-none h-fit p-0"> <Menubar.Root class="border-none h-fit p-0">
<Menubar.Menu> <Menubar.Menu>
<Menubar.Trigger> <Menubar.Trigger aria-label={$_('gpx.file')}>
<File size="18" class="md:hidden" /> <File size="18" class="md:hidden" />
<span class="hidden md:block">{$_('gpx.file')}</span> <span class="hidden md:block">{$_('gpx.file')}</span>
</Menubar.Trigger> </Menubar.Trigger>
@@ -149,18 +151,27 @@
<Shortcut key="O" ctrl={true} /> <Shortcut key="O" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Separator /> <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" /> <Copy size="16" class="mr-1" />
{$_('menu.duplicate')} {$_('menu.duplicate')}
<Shortcut key="D" ctrl={true} /> <Shortcut key="D" ctrl={true} />
</Menubar.Item> </Menubar.Item>
<Menubar.Separator /> <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" /> <FileX size="16" class="mr-1" />
{$_('menu.close')} {$_('menu.close')}
<Shortcut key="⌫" ctrl={true} /> <Shortcut key="⌫" ctrl={true} />
</Menubar.Item> </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" /> <FileX size="16" class="mr-1" />
{$_('menu.close_all')} {$_('menu.close_all')}
<Shortcut key="⌫" ctrl={true} shift={true} /> <Shortcut key="⌫" ctrl={true} shift={true} />
@@ -185,7 +196,7 @@
</Menubar.Content> </Menubar.Content>
</Menubar.Menu> </Menubar.Menu>
<Menubar.Menu> <Menubar.Menu>
<Menubar.Trigger> <Menubar.Trigger aria-label={$_('menu.edit')}>
<FilePen size="18" class="md:hidden" /> <FilePen size="18" class="md:hidden" />
<span class="hidden md:block">{$_('menu.edit')}</span> <span class="hidden md:block">{$_('menu.edit')}</span>
</Menubar.Trigger> </Menubar.Trigger>
@@ -205,7 +216,11 @@
disabled={$selection.size !== 1 || disabled={$selection.size !== 1 ||
!$selection !$selection
.getSelected() .getSelected()
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)} .every(
(item) =>
item instanceof ListFileItem ||
item instanceof ListTrackItem
)}
on:click={() => ($editMetadata = true)} on:click={() => ($editMetadata = true)}
> >
<Info size="16" class="mr-1" /> <Info size="16" class="mr-1" />
@@ -216,7 +231,11 @@
disabled={$selection.size === 0 || disabled={$selection.size === 0 ||
!$selection !$selection
.getSelected() .getSelected()
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)} .every(
(item) =>
item instanceof ListFileItem ||
item instanceof ListTrackItem
)}
on:click={() => ($editStyle = true)} on:click={() => ($editStyle = true)}
> >
<PaintBucket size="16" class="mr-1" /> <PaintBucket size="16" class="mr-1" />
@@ -241,13 +260,51 @@
{/if} {/if}
<Shortcut key="H" ctrl={true} /> <Shortcut key="H" ctrl={true} />
</Menubar.Item> </Menubar.Item>
{#if $treeFileView}
{#if $selection.getSelected().some((item) => item instanceof ListFileItem)}
<Menubar.Separator />
<Menubar.Item
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)}
<Menubar.Separator />
<Menubar.Item
on:click={() => {
let item = $selection.getSelected()[0];
dbUtils.addNewSegment(item.getFileId(), item.getTrackIndex());
}}
disabled={$selection.size !== 1}
>
<Plus size="16" class="mr-1" />
{$_('menu.new_segment')}
</Menubar.Item>
{/if}
{/if}
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item on:click={selectAll} disabled={$fileObservers.size == 0}> <Menubar.Item on:click={selectAll} disabled={$fileObservers.size == 0}>
<FileStack size="16" class="mr-1" /> <FileStack size="16" class="mr-1" />
{$_('menu.select_all')} {$_('menu.select_all')}
<Shortcut key="A" ctrl={true} /> <Shortcut key="A" ctrl={true} />
</Menubar.Item> </Menubar.Item>
{#if $verticalFileView} <Menubar.Item
on:click={() => {
if ($selection.size > 0) {
centerMapOnSelection();
}
}}
>
<Maximize size="16" class="mr-1" />
{$_('menu.center')}
<Shortcut key="⏎" ctrl={true} />
</Menubar.Item>
{#if $treeFileView}
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item on:click={copySelection} disabled={$selection.size === 0}> <Menubar.Item on:click={copySelection} disabled={$selection.size === 0}>
<ClipboardCopy size="16" class="mr-1" /> <ClipboardCopy size="16" class="mr-1" />
@@ -263,7 +320,9 @@
disabled={$copied === undefined || disabled={$copied === undefined ||
$copied.length === 0 || $copied.length === 0 ||
($selection.size > 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} on:click={pasteSelection}
> >
<ClipboardPaste size="16" class="mr-1" /> <ClipboardPaste size="16" class="mr-1" />
@@ -272,7 +331,10 @@
</Menubar.Item> </Menubar.Item>
{/if} {/if}
<Menubar.Separator /> <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" /> <Trash2 size="16" class="mr-1" />
{$_('menu.delete')} {$_('menu.delete')}
<Shortcut key="⌫" ctrl={true} /> <Shortcut key="⌫" ctrl={true} />
@@ -280,7 +342,7 @@
</Menubar.Content> </Menubar.Content>
</Menubar.Menu> </Menubar.Menu>
<Menubar.Menu> <Menubar.Menu>
<Menubar.Trigger> <Menubar.Trigger aria-label={$_('menu.view')}>
<View size="18" class="md:hidden" /> <View size="18" class="md:hidden" />
<span class="hidden md:block">{$_('menu.view')}</span> <span class="hidden md:block">{$_('menu.view')}</span>
</Menubar.Trigger> </Menubar.Trigger>
@@ -290,24 +352,32 @@
{$_('menu.elevation_profile')} {$_('menu.elevation_profile')}
<Shortcut key="P" ctrl={true} /> <Shortcut key="P" ctrl={true} />
</Menubar.CheckboxItem> </Menubar.CheckboxItem>
<Menubar.CheckboxItem bind:checked={$verticalFileView}> <Menubar.CheckboxItem bind:checked={$treeFileView}>
<GalleryVertical size="16" class="mr-1" /> <ListTree size="16" class="mr-1" />
{$_('menu.vertical_file_view')} {$_('menu.tree_file_view')}
<Shortcut key="L" ctrl={true} /> <Shortcut key="L" ctrl={true} />
</Menubar.CheckboxItem> </Menubar.CheckboxItem>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item inset on:click={switchBasemaps}> <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>
<Menubar.Item inset on:click={toggleOverlays}> <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.Item>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.CheckboxItem bind:checked={$distanceMarkers}> <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>
<Menubar.CheckboxItem bind:checked={$directionMarkers}> <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.CheckboxItem>
<Menubar.Separator /> <Menubar.Separator />
<Menubar.Item inset on:click={toggle3D}> <Menubar.Item inset on:click={toggle3D}>
@@ -318,32 +388,43 @@
</Menubar.Content> </Menubar.Content>
</Menubar.Menu> </Menubar.Menu>
<Menubar.Menu> <Menubar.Menu>
<Menubar.Trigger> <Menubar.Trigger aria-label={$_('menu.settings')}>
<Settings size="18" class="md:hidden" /> <Settings size="18" class="md:hidden" />
<span class="hidden md:block"> <span class="hidden md:block">
{$_('menu.settings')} {$_('menu.settings')}
</span> </span>
</Menubar.Trigger> </Menubar.Trigger>
<Menubar.Content class="border-none" <Menubar.Content class="border-none">
><Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger> <Menubar.SubTrigger>
<Ruler size="16" class="mr-1" />{$_('menu.distance_units')} <Ruler size="16" class="mr-1" />{$_('menu.distance_units')}
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$distanceUnits}> <Menubar.RadioGroup bind:value={$distanceUnits}>
<Menubar.RadioItem value="metric">{$_('menu.metric')}</Menubar.RadioItem> <Menubar.RadioItem value="metric"
<Menubar.RadioItem value="imperial">{$_('menu.imperial')}</Menubar.RadioItem> >{$_('menu.metric')}</Menubar.RadioItem
>
<Menubar.RadioItem value="imperial"
>{$_('menu.imperial')}</Menubar.RadioItem
>
<Menubar.RadioItem value="nautical"
>{$_('menu.nautical')}</Menubar.RadioItem
>
</Menubar.RadioGroup> </Menubar.RadioGroup>
</Menubar.SubContent> </Menubar.SubContent>
</Menubar.Sub> </Menubar.Sub>
<Menubar.Sub> <Menubar.Sub>
<Menubar.SubTrigger <Menubar.SubTrigger>
><Zap size="16" class="mr-1" />{$_('menu.velocity_units')}</Menubar.SubTrigger <Zap size="16" class="mr-1" />{$_('menu.velocity_units')}
> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$velocityUnits}> <Menubar.RadioGroup bind:value={$velocityUnits}>
<Menubar.RadioItem value="speed">{$_('quantities.speed')}</Menubar.RadioItem> <Menubar.RadioItem value="speed"
<Menubar.RadioItem value="pace">{$_('quantities.pace')}</Menubar.RadioItem> >{$_('quantities.speed')}</Menubar.RadioItem
>
<Menubar.RadioItem value="pace"
>{$_('quantities.pace')}</Menubar.RadioItem
>
</Menubar.RadioGroup> </Menubar.RadioGroup>
</Menubar.SubContent> </Menubar.SubContent>
</Menubar.Sub> </Menubar.Sub>
@@ -353,8 +434,12 @@
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$temperatureUnits}> <Menubar.RadioGroup bind:value={$temperatureUnits}>
<Menubar.RadioItem value="celsius">{$_('menu.celsius')}</Menubar.RadioItem> <Menubar.RadioItem value="celsius"
<Menubar.RadioItem value="fahrenheit">{$_('menu.fahrenheit')}</Menubar.RadioItem> >{$_('menu.celsius')}</Menubar.RadioItem
>
<Menubar.RadioItem value="fahrenheit"
>{$_('menu.fahrenheit')}</Menubar.RadioItem
>
</Menubar.RadioGroup> </Menubar.RadioGroup>
</Menubar.SubContent> </Menubar.SubContent>
</Menubar.Sub> </Menubar.Sub>
@@ -367,7 +452,7 @@
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$locale}> <Menubar.RadioGroup bind:value={$locale}>
{#each Object.entries(languages) as [lang, label]} {#each Object.entries(languages) as [lang, label]}
<a href={getURLForLanguage(lang)}> <a href={getURLForLanguage(lang, '/app')}>
<Menubar.RadioItem value={lang}>{label}</Menubar.RadioItem> <Menubar.RadioItem value={lang}>{label}</Menubar.RadioItem>
</a> </a>
{/each} {/each}
@@ -390,8 +475,11 @@
setMode(value); setMode(value);
}} }}
> >
<Menubar.RadioItem value="light">{$_('menu.light')}</Menubar.RadioItem> <Menubar.RadioItem value="light"
<Menubar.RadioItem value="dark">{$_('menu.dark')}</Menubar.RadioItem> >{$_('menu.light')}</Menubar.RadioItem
>
<Menubar.RadioItem value="dark">{$_('menu.dark')}</Menubar.RadioItem
>
</Menubar.RadioGroup> </Menubar.RadioGroup>
</Menubar.SubContent> </Menubar.SubContent>
</Menubar.Sub> </Menubar.Sub>
@@ -403,13 +491,17 @@
</Menubar.SubTrigger> </Menubar.SubTrigger>
<Menubar.SubContent> <Menubar.SubContent>
<Menubar.RadioGroup bind:value={$streetViewSource}> <Menubar.RadioGroup bind:value={$streetViewSource}>
<Menubar.RadioItem value="mapillary">{$_('menu.mapillary')}</Menubar.RadioItem> <Menubar.RadioItem value="mapillary"
<Menubar.RadioItem value="google">{$_('menu.google')}</Menubar.RadioItem> >{$_('menu.mapillary')}</Menubar.RadioItem
>
<Menubar.RadioItem value="google"
>{$_('menu.google')}</Menubar.RadioItem
>
</Menubar.RadioGroup> </Menubar.RadioGroup>
</Menubar.SubContent> </Menubar.SubContent>
</Menubar.Sub> </Menubar.Sub>
<Menubar.Item on:click={() => (layerSettingsOpen = true)}> <Menubar.Item on:click={() => (layerSettingsOpen = true)}>
<Layers3 size="16" class="mr-1" /> <Layers size="16" class="mr-1" />
{$_('menu.layers')} {$_('menu.layers')}
</Menubar.Item> </Menubar.Item>
</Menubar.Content> </Menubar.Content>
@@ -421,6 +513,7 @@
href="./help" href="./help"
target="_blank" target="_blank"
class="cursor-default h-fit rounded-sm px-3 py-0.5" class="cursor-default h-fit rounded-sm px-3 py-0.5"
aria-label={$_('menu.help')}
> >
<BookOpenText size="18" class="md:hidden" /> <BookOpenText size="18" class="md:hidden" />
<span class="hidden md:block"> <span class="hidden md:block">
@@ -432,6 +525,7 @@
href="https://ko-fi.com/gpxstudio" href="https://ko-fi.com/gpxstudio"
target="_blank" target="_blank"
class="cursor-default h-fit rounded-sm font-bold text-support hover:text-support px-3 py-0.5" class="cursor-default h-fit rounded-sm font-bold text-support hover:text-support px-3 py-0.5"
aria-label={$_('menu.donate')}
> >
<HeartHandshake size="18" class="md:hidden" /> <HeartHandshake size="18" class="md:hidden" />
<span class="hidden md:flex flex-row items-center"> <span class="hidden md:flex flex-row items-center">
@@ -498,13 +592,16 @@
} else { } else {
dbUtils.undo(); dbUtils.undo();
} }
e.preventDefault();
} else if ((e.key === 'Backspace' || e.key === 'Delete') && (e.metaKey || e.ctrlKey)) { } else if ((e.key === 'Backspace' || e.key === 'Delete') && (e.metaKey || e.ctrlKey)) {
if (!targetInput) {
if (e.shiftKey) { if (e.shiftKey) {
dbUtils.deleteAllFiles(); dbUtils.deleteAllFiles();
} else { } else {
dbUtils.deleteSelection(); dbUtils.deleteSelection();
} }
e.preventDefault(); e.preventDefault();
}
} else if (e.key === 'a' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'a' && (e.metaKey || e.ctrlKey)) {
if (!targetInput) { if (!targetInput) {
selectAll(); selectAll();
@@ -524,7 +621,7 @@
$elevationProfile = !$elevationProfile; $elevationProfile = !$elevationProfile;
e.preventDefault(); e.preventDefault();
} else if (e.key === 'l' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'l' && (e.metaKey || e.ctrlKey)) {
$verticalFileView = !$verticalFileView; $treeFileView = !$treeFileView;
e.preventDefault(); e.preventDefault();
} else if (e.key === 'h' && (e.metaKey || e.ctrlKey)) { } else if (e.key === 'h' && (e.metaKey || e.ctrlKey)) {
if ($allHidden) { if ($allHidden) {
@@ -533,6 +630,10 @@
dbUtils.setHiddenToSelection(true); dbUtils.setHiddenToSelection(true);
} }
e.preventDefault(); e.preventDefault();
} else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
if ($selection.size > 0) {
centerMapOnSelection();
}
} else if (e.key === 'F1') { } else if (e.key === 'F1') {
switchBasemaps(); switchBasemaps();
e.preventDefault(); e.preventDefault();
@@ -15,6 +15,7 @@
on:click={() => { on:click={() => {
setMode(selectedMode === 'light' ? 'dark' : 'light'); setMode(selectedMode === 'light' ? 'dark' : 'light');
}} }}
aria-label={$_('menu.mode')}
> >
{#if selectedMode === 'light'} {#if selectedMode === 'light'}
<Sun {size} /> <Sun {size} />
+5 -3
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import Logo from '$lib/components/Logo.svelte'; import Logo from '$lib/components/Logo.svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import AlgoliaDocSearch from '$lib/components/AlgoliaDocSearch.svelte';
import ModeSwitch from '$lib/components/ModeSwitch.svelte'; import ModeSwitch from '$lib/components/ModeSwitch.svelte';
import { BookOpenText, Home, Map } from 'lucide-svelte'; import { BookOpenText, Home, Map } from 'lucide-svelte';
import { _, locale } from 'svelte-i18n'; import { _, locale } from 'svelte-i18n';
@@ -10,8 +11,8 @@
<nav class="w-full sticky top-0 bg-background z-50"> <nav class="w-full sticky top-0 bg-background z-50">
<div class="mx-6 py-2 flex flex-row items-center border-b gap-4 sm:gap-8"> <div class="mx-6 py-2 flex flex-row items-center border-b gap-4 sm:gap-8">
<a href={getURLForLanguage($locale, '/')} class="shrink-0 translate-y-0.5"> <a href={getURLForLanguage($locale, '/')} class="shrink-0 translate-y-0.5">
<Logo class="h-8 sm:hidden" iconOnly={true} /> <Logo class="h-8 sm:hidden" iconOnly={true} width="26" />
<Logo class="h-8 hidden sm:block" /> <Logo class="h-8 hidden sm:block" width="153" />
</a> </a>
<Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/')}> <Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/')}>
<Home size="18" class="mr-1.5" /> <Home size="18" class="mr-1.5" />
@@ -25,6 +26,7 @@
<BookOpenText size="18" class="mr-1.5" /> <BookOpenText size="18" class="mr-1.5" />
{$_('menu.help')} {$_('menu.help')}
</Button> </Button>
<ModeSwitch class="ml-auto" /> <AlgoliaDocSearch class="ml-auto" />
<ModeSwitch class="hidden xs:block" />
</div> </div>
</nav> </nav>
+2 -1
View File
@@ -12,7 +12,8 @@
const handleMouseMove = (event: PointerEvent) => { const handleMouseMove = (event: PointerEvent) => {
const newAfter = 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) { if (newAfter >= minAfter && newAfter <= maxAfter) {
after = newAfter; after = newAfter;
} else if (newAfter < minAfter && after !== minAfter) { } else if (newAfter < minAfter && after !== minAfter) {
+18 -8
View File
@@ -1,26 +1,36 @@
<script lang="ts"> <script lang="ts">
import { isMac, isSafari } from '$lib/utils';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
export let key: string; export let key: string | undefined = undefined;
export let shift: boolean = false; export let shift: boolean = false;
export let ctrl: boolean = false; export let ctrl: boolean = false;
export let click: boolean = false; export let click: boolean = false;
let isMac = false; let mac = false;
let isSafari = false; let safari = false;
onMount(() => { onMount(() => {
isMac = navigator.userAgent.toUpperCase().indexOf('MAC') >= 0; mac = isMac();
isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); safari = isSafari();
}); });
</script> </script>
<div <div
class="ml-auto pl-2 text-xs tracking-widest text-muted-foreground flex flex-row gap-0 items-baseline" class="ml-auto pl-2 text-xs tracking-widest text-muted-foreground flex flex-row gap-0 items-baseline"
{...$$props}
> >
<span>{shift ? '⇧' : ''}</span> {#if shift}
<span>{ctrl ? (isMac && !isSafari ? '⌘' : $_('menu.ctrl') + '+') : ''}</span> <span></span>
{/if}
{#if ctrl}
<span>{mac && !safari ? '⌘' : $_('menu.ctrl') + '+'}</span>
{/if}
{#if key}
<span class={key === '+' ? 'font-medium text-sm/4' : ''}>{key}</span> <span class={key === '+' ? 'font-medium text-sm/4' : ''}>{key}</span>
<span>{click ? $_('menu.click') : ''}</span> {/if}
{#if click}
<span>{$_('menu.click')}</span>
{/if}
</div> </div>
+7 -3
View File
@@ -1,14 +1,18 @@
<script lang="ts"> <script lang="ts">
import * as Tooltip from '$lib/components/ui/tooltip/index.js'; import * as Tooltip from '$lib/components/ui/tooltip/index.js';
export let label: string;
export let side: 'top' | 'right' | 'bottom' | 'left' = 'top'; export let side: 'top' | 'right' | 'bottom' | 'left' = 'top';
</script> </script>
<Tooltip.Root> <Tooltip.Root>
<Tooltip.Trigger {...$$restProps}> <Tooltip.Trigger {...$$restProps} aria-label={label}>
<slot name="data" /> <slot />
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Content {side}> <Tooltip.Content {side}>
<slot name="tooltip" /> <div class="flex flex-row items-center">
<span>{label}</span>
<slot name="extra" />
</div>
</Tooltip.Content> </Tooltip.Content>
</Tooltip.Root> </Tooltip.Root>
-29
View File
@@ -1,29 +0,0 @@
<script lang="ts">
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { settings } from '$lib/db';
const { showWelcomeMessage } = settings;
</script>
<AlertDialog.Root
open={$showWelcomeMessage === true}
closeOnEscape={false}
closeOnOutsideClick={false}
onOpenChange={() => ($showWelcomeMessage = false)}
>
<AlertDialog.Trigger class="hidden"></AlertDialog.Trigger>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>
Welcome to the new version of <b>gpx.studio</b>!
</AlertDialog.Title>
<AlertDialog.Description class="space-y-1">
<p>The website is still under development and may contain bugs.</p>
<p>Please report any issues you find by email or on GitHub.</p>
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Action>Let's go!</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
+15 -25
View File
@@ -2,10 +2,13 @@
import { settings } from '$lib/db'; import { settings } from '$lib/db';
import { import {
celsiusToFahrenheit, celsiusToFahrenheit,
distancePerHourToSecondsPerDistance, getConvertedDistance,
kilometersToMiles, getConvertedElevation,
metersToFeet, getConvertedVelocity,
secondsToHHMMSS getDistanceUnits,
getElevationUnits,
getVelocityUnits,
secondsToHHMMSS,
} from '$lib/units'; } from '$lib/units';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
@@ -20,31 +23,18 @@
<span class={$$props.class}> <span class={$$props.class}>
{#if type === 'distance'} {#if type === 'distance'}
{#if $distanceUnits === 'metric'} {getConvertedDistance(value, $distanceUnits).toFixed(decimals ?? 2)}
{value.toFixed(decimals ?? 2)} {showUnits ? $_('units.kilometers') : ''} {showUnits ? getDistanceUnits($distanceUnits) : ''}
{:else}
{kilometersToMiles(value).toFixed(decimals ?? 2)} {showUnits ? $_('units.miles') : ''}
{/if}
{:else if type === 'elevation'} {:else if type === 'elevation'}
{#if $distanceUnits === 'metric'} {getConvertedElevation(value, $distanceUnits).toFixed(decimals ?? 0)}
{value.toFixed(decimals ?? 0)} {showUnits ? $_('units.meters') : ''} {showUnits ? getElevationUnits($distanceUnits) : ''}
{:else}
{metersToFeet(value).toFixed(decimals ?? 0)} {showUnits ? $_('units.feet') : ''}
{/if}
{:else if type === 'speed'} {:else if type === 'speed'}
{#if $distanceUnits === 'metric'}
{#if $velocityUnits === 'speed'} {#if $velocityUnits === 'speed'}
{value.toFixed(decimals ?? 2)} {showUnits ? $_('units.kilometers_per_hour') : ''} {getConvertedVelocity(value, $velocityUnits, $distanceUnits).toFixed(decimals ?? 2)}
{showUnits ? getVelocityUnits($velocityUnits, $distanceUnits) : ''}
{:else} {:else}
{secondsToHHMMSS(distancePerHourToSecondsPerDistance(value))} {secondsToHHMMSS(getConvertedVelocity(value, $velocityUnits, $distanceUnits))}
{showUnits ? $_('units.minutes_per_kilometer') : ''} {showUnits ? getVelocityUnits($velocityUnits, $distanceUnits) : ''}
{/if}
{:else if $velocityUnits === 'speed'}
{kilometersToMiles(value).toFixed(decimals ?? 2)}
{showUnits ? $_('units.miles_per_hour') : ''}
{:else}
{secondsToHHMMSS(distancePerHourToSecondsPerDistance(kilometersToMiles(value)))}
{showUnits ? $_('units.minutes_per_mile') : ''}
{/if} {/if}
{:else if type === 'temperature'} {:else if type === 'temperature'}
{#if $temperatureUnits === 'celsius'} {#if $temperatureUnits === 'celsius'}
@@ -0,0 +1,83 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
export let module;
</script>
<div class="markdown flex flex-col gap-3">
<svelte:component this={module} />
</div>
<style lang="postcss">
:global(.markdown) {
@apply text-muted-foreground;
}
:global(.markdown h1) {
@apply text-foreground;
@apply text-3xl;
@apply font-semibold;
@apply mb-3 pt-6;
}
:global(.markdown h2) {
@apply text-foreground;
@apply text-2xl;
@apply font-semibold;
@apply pt-3;
}
:global(.markdown h3) {
@apply text-foreground;
@apply text-lg;
@apply font-semibold;
@apply pt-1.5;
}
:global(.markdown p > button, .markdown li > button) {
@apply border;
@apply rounded-md;
@apply px-1;
}
:global(.markdown > a) {
@apply text-link;
@apply hover:underline;
@apply contents;
}
:global(.markdown p > a) {
@apply text-link;
@apply hover:underline;
}
:global(.markdown li > a) {
@apply text-link;
@apply hover:underline;
}
:global(.markdown kbd) {
@apply p-1;
@apply rounded-md;
@apply border;
}
:global(.markdown ul) {
@apply list-disc;
@apply pl-4;
}
:global(.markdown ol) {
@apply list-decimal;
@apply pl-4;
}
:global(.markdown li) {
@apply mt-1;
@apply first:mt-0;
}
:global(.markdown hr) {
@apply my-5;
}
</style>
@@ -1,11 +1,29 @@
<script lang="ts"> <script lang="ts">
export let src; export let src: 'getting-started/interface' | 'tools/routing' | 'tools/split';
export let alt: string; export let alt: string;
</script> </script>
<div class="flex flex-col items-center py-6 w-full"> <div class="flex flex-col items-center py-6 w-full">
<div class="rounded-md overflow-clip shadow-xl mx-auto"> <div class="rounded-md overflow-hidden overflow-clip shadow-xl mx-auto">
<enhanced:img {src} {alt} class="w-full max-w-3xl" /> {#if src === 'getting-started/interface'}
<enhanced:img
src="/src/lib/assets/img/docs/getting-started/interface.png"
{alt}
class="w-full max-w-3xl"
/>
{:else if src === 'tools/routing'}
<enhanced:img
src="/src/lib/assets/img/docs/tools/routing.png"
{alt}
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"
/>
{/if}
</div> </div>
<p class="text-center text-sm text-muted-foreground mt-2">{alt}</p> <p class="text-center text-sm text-muted-foreground mt-2">{alt}</p>
</div> </div>
@@ -1,112 +0,0 @@
<script lang="ts">
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { _, locale } from 'svelte-i18n';
export let path: string;
export let titleOnly: boolean = false;
let module = undefined;
let metadata: Record<string, any> = {};
const modules = import.meta.glob('/src/lib/docs/**/*.mdx');
function loadModule(path: string) {
modules[path]?.().then((mod) => {
module = mod.default;
metadata = mod.metadata;
});
}
$: if ($locale) {
if (modules.hasOwnProperty(`/src/lib/docs/${$locale}/${path}`)) {
loadModule(`/src/lib/docs/${$locale}/${path}`);
} else if (browser) {
goto(`${base}/404`);
}
}
</script>
{#if module !== undefined}
{#if titleOnly}
{metadata.title}
{:else}
<div class="markdown flex flex-col gap-3">
<svelte:component this={module} />
</div>
{/if}
{/if}
<style lang="postcss">
:global(.markdown) {
@apply text-muted-foreground;
}
:global(.markdown h1) {
@apply text-foreground;
@apply text-3xl;
@apply font-semibold;
@apply mb-3 pt-6;
}
:global(.markdown h2) {
@apply text-foreground;
@apply text-2xl;
@apply font-semibold;
@apply pt-3;
}
:global(.markdown h3) {
@apply text-foreground;
@apply text-lg;
@apply font-semibold;
@apply pt-1.5;
}
:global(.markdown p > button) {
@apply border;
@apply rounded-md;
@apply px-1;
}
:global(.markdown > a) {
@apply text-blue-500;
@apply hover:underline;
}
:global(.markdown p > a) {
@apply text-blue-500;
@apply hover:underline;
}
:global(.markdown li > a) {
@apply text-blue-500;
@apply hover:underline;
}
:global(.markdown kbd) {
@apply p-1;
@apply rounded-md;
@apply border;
}
:global(.markdown ul) {
@apply list-disc;
@apply pl-4;
}
:global(.markdown ol) {
@apply list-decimal;
@apply pl-4;
}
:global(.markdown li) {
@apply mt-1;
@apply first:mt-0;
}
:global(.markdown hr) {
@apply my-5;
}
</style>
@@ -3,8 +3,8 @@
</script> </script>
<div <div
class="bg-accent border-l-8 {type === 'note' class="bg-secondary border-l-8 {type === 'note'
? 'border-blue-500' ? 'border-link'
: 'border-destructive'} p-2 text-sm rounded-md" : 'border-destructive'} p-2 text-sm rounded-md"
> >
<slot /> <slot />
@@ -12,7 +12,7 @@
<style lang="postcss"> <style lang="postcss">
div :global(a) { div :global(a) {
@apply text-blue-500; @apply text-link;
@apply hover:underline; @apply hover:underline;
} }
</style> </style>
+52 -24
View File
@@ -1,36 +1,64 @@
import { File, FilePen, View, type Icon, Settings, Pencil, MapPin, Scissors, CalendarClock, Group, Ungroup, Filter, SquareDashedMousePointer } from "lucide-svelte"; import {
import type { ComponentType } from "svelte"; 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[]> = { export const guides: Record<string, string[]> = {
'getting-started': [], 'getting-started': [],
menu: ['file', 'edit', 'view', 'settings'], menu: ['file', 'edit', 'view', 'settings'],
'files-and-stats': [], 'files-and-stats': [],
toolbar: ['routing', 'poi', 'scissors', 'time', 'merge', 'extract', 'minify', 'clean'], toolbar: [
'routing',
'poi',
'scissors',
'time',
'merge',
'extract',
'elevation',
'minify',
'clean',
],
'map-controls': [], 'map-controls': [],
'gpx': [], gpx: [],
'integration': [], integration: [],
faq: [],
}; };
export const guideIcons: Record<string, string | ComponentType<Icon>> = { export const guideIcons: Record<string, string | ComponentType<Icon>> = {
"getting-started": "🚀", 'getting-started': '🚀',
"menu": "📂 ⚙️", menu: '📂 ⚙️',
"file": File, file: File,
"edit": FilePen, edit: FilePen,
"view": View, view: View,
"settings": Settings, settings: Settings,
"files-and-stats": "🗂 📈", 'files-and-stats': '🗂 📈',
"toolbar": "🧰", toolbar: '🧰',
"routing": Pencil, routing: Pencil,
"poi": MapPin, poi: MapPin,
"scissors": Scissors, scissors: Scissors,
"time": CalendarClock, time: CalendarClock,
"merge": Group, merge: Group,
"extract": Ungroup, extract: Ungroup,
"minify": Filter, elevation: MountainSnow,
"clean": SquareDashedMousePointer, minify: Filter,
"map-controls": "🗺", clean: SquareDashedMousePointer,
"gpx": "💾", 'map-controls': '🗺',
"integration": "{ 👩‍💻 }", gpx: '💾',
integration: '{ 👩‍💻 }',
faq: '🔮',
}; };
export function getPreviousGuide(currentGuide: string): string | undefined { export function getPreviousGuide(currentGuide: string): string | undefined {
@@ -12,7 +12,7 @@
embedding, embedding,
loadFile, loadFile,
map, map,
updateGPXData updateGPXData,
} from '$lib/stores'; } from '$lib/stores';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { fileObservers, settings, GPXStatisticsTree } from '$lib/db'; import { fileObservers, settings, GPXStatisticsTree } from '$lib/db';
@@ -20,8 +20,13 @@
import type { GPXFile } from 'gpx'; import type { GPXFile } from 'gpx';
import { selection } from '$lib/components/file-list/Selection'; import { selection } from '$lib/components/file-list/Selection';
import { ListFileItem } from '$lib/components/file-list/FileList'; import { ListFileItem } from '$lib/components/file-list/FileList';
import { allowedEmbeddingBasemaps, type EmbeddingOptions } from './Embedding'; import {
allowedEmbeddingBasemaps,
getFilesFromEmbeddingOptions,
type EmbeddingOptions,
} from './Embedding';
import { mode, setMode } from 'mode-watcher'; import { mode, setMode } from 'mode-watcher';
import { browser } from '$app/environment';
$embedding = true; $embedding = true;
@@ -32,7 +37,7 @@
temperatureUnits, temperatureUnits,
fileOrder, fileOrder,
distanceMarkers, distanceMarkers,
directionMarkers directionMarkers,
} = settings; } = settings;
export let useHash = true; export let useHash = true;
@@ -45,7 +50,7 @@
distanceUnits: 'metric', distanceUnits: 'metric',
velocityUnits: 'speed', velocityUnits: 'speed',
temperatureUnits: 'celsius', temperatureUnits: 'celsius',
theme: 'system' theme: 'system',
}; };
function applyOptions() { function applyOptions() {
@@ -55,7 +60,7 @@
}); });
let downloads: Promise<GPXFile | null>[] = []; let downloads: Promise<GPXFile | null>[] = [];
options.files.forEach((url) => { getFilesFromEmbeddingOptions(options).forEach((url) => {
downloads.push( downloads.push(
fetch(url) fetch(url)
.then((response) => response.blob()) .then((response) => response.blob())
@@ -69,12 +74,12 @@
let bounds = { let bounds = {
southWest: { southWest: {
lat: 90, lat: 90,
lon: 180 lon: 180,
}, },
northEast: { northEast: {
lat: -90, lat: -90,
lon: -180 lon: -180,
} },
}; };
fileObservers.update(($fileObservers) => { fileObservers.update(($fileObservers) => {
@@ -91,12 +96,13 @@
id, id,
readable({ readable({
file, file,
statistics statistics,
}) })
); );
ids.push(id); 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.lat = Math.min(bounds.southWest.lat, fileBounds.southWest.lat);
bounds.southWest.lon = Math.min(bounds.southWest.lon, fileBounds.southWest.lon); bounds.southWest.lon = Math.min(bounds.southWest.lon, fileBounds.southWest.lon);
@@ -125,12 +131,12 @@
bounds.southWest.lon, bounds.southWest.lon,
bounds.southWest.lat, bounds.southWest.lat,
bounds.northEast.lon, bounds.northEast.lon,
bounds.northEast.lat bounds.northEast.lat,
], ],
{ {
padding: 80, padding: 80,
linear: true, linear: true,
easing: () => 1 easing: () => 1,
} }
); );
} }
@@ -138,7 +144,10 @@
} }
}); });
if (options.basemap !== $currentBasemap && allowedEmbeddingBasemaps.includes(options.basemap)) { if (
options.basemap !== $currentBasemap &&
allowedEmbeddingBasemaps.includes(options.basemap)
) {
$currentBasemap = options.basemap; $currentBasemap = options.basemap;
} }
@@ -176,7 +185,7 @@
prevSettings.theme = $mode ?? 'system'; prevSettings.theme = $mode ?? 'system';
}); });
$: if (options) { $: if (browser && options) {
applyOptions(); applyOptions();
} }
@@ -224,7 +233,7 @@
geolocate={false} geolocate={false}
hash={useHash} hash={useHash}
/> />
<OpenIn bind:files={options.files} /> <OpenIn bind:files={options.files} bind:ids={options.ids} />
<LayerControl /> <LayerControl />
<GPXLayers /> <GPXLayers />
{#if $fileObservers.size > 1} {#if $fileObservers.size > 1}
@@ -252,12 +261,10 @@
options.elevation.hr ? 'hr' : null, options.elevation.hr ? 'hr' : null,
options.elevation.cad ? 'cad' : null, options.elevation.cad ? 'cad' : null,
options.elevation.temp ? 'temp' : null, options.elevation.temp ? 'temp' : null,
options.elevation.power ? 'power' : null options.elevation.power ? 'power' : null,
].filter((dataset) => dataset !== null)} ].filter((dataset) => dataset !== null)}
elevationFill={options.elevation.fill} elevationFill={options.elevation.fill}
panelSize={options.elevation.height}
showControls={options.elevation.controls} showControls={options.elevation.controls}
class="py-2"
/> />
{/if} {/if}
</div> </div>
@@ -1,31 +1,34 @@
import { basemaps } from "$lib/assets/layers"; import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import { basemaps } from '$lib/assets/layers';
export type EmbeddingOptions = { export type EmbeddingOptions = {
token: string; token: string;
files: string[]; files: string[];
ids: string[];
basemap: string; basemap: string;
elevation: { elevation: {
show: boolean; show: boolean;
height: number, height: number;
controls: boolean, controls: boolean;
fill: 'slope' | 'surface' | undefined, fill: 'slope' | 'surface' | 'highway' | undefined;
speed: boolean, speed: boolean;
hr: boolean, hr: boolean;
cad: boolean, cad: boolean;
temp: boolean, temp: boolean;
power: boolean, power: boolean;
}, };
distanceMarkers: boolean, distanceMarkers: boolean;
directionMarkers: boolean, directionMarkers: boolean;
distanceUnits: 'metric' | 'imperial', distanceUnits: 'metric' | 'imperial' | 'nautical';
velocityUnits: 'speed' | 'pace', velocityUnits: 'speed' | 'pace';
temperatureUnits: 'celsius' | 'fahrenheit', temperatureUnits: 'celsius' | 'fahrenheit';
theme: 'system' | 'light' | 'dark', theme: 'system' | 'light' | 'dark';
}; };
export const defaultEmbeddingOptions = { export const defaultEmbeddingOptions = {
token: '', token: '',
files: [], files: [],
ids: [],
basemap: 'mapboxOutdoors', basemap: 'mapboxOutdoors',
elevation: { elevation: {
show: true, show: true,
@@ -50,10 +53,17 @@ export function getDefaultEmbeddingOptions(): EmbeddingOptions {
return JSON.parse(JSON.stringify(defaultEmbeddingOptions)); return JSON.parse(JSON.stringify(defaultEmbeddingOptions));
} }
export function getMergedEmbeddingOptions(options: any, defaultOptions: any = defaultEmbeddingOptions): EmbeddingOptions { export function getMergedEmbeddingOptions(
options: any,
defaultOptions: any = defaultEmbeddingOptions
): EmbeddingOptions {
const mergedOptions = JSON.parse(JSON.stringify(defaultOptions)); const mergedOptions = JSON.parse(JSON.stringify(defaultOptions));
for (const key in options) { 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]); mergedOptions[key] = getMergedEmbeddingOptions(options[key], defaultOptions[key]);
} else { } else {
mergedOptions[key] = options[key]; mergedOptions[key] = options[key];
@@ -62,11 +72,21 @@ export function getMergedEmbeddingOptions(options: any, defaultOptions: any = de
return mergedOptions; return mergedOptions;
} }
export function getCleanedEmbeddingOptions(options: any, defaultOptions: any = defaultEmbeddingOptions): any { export function getCleanedEmbeddingOptions(
options: any,
defaultOptions: any = defaultEmbeddingOptions
): any {
const cleanedOptions = JSON.parse(JSON.stringify(options)); const cleanedOptions = JSON.parse(JSON.stringify(options));
for (const key in cleanedOptions) { for (const key in cleanedOptions) {
if (typeof cleanedOptions[key] === 'object' && cleanedOptions[key] !== null && !Array.isArray(cleanedOptions[key])) { if (
cleanedOptions[key] = getCleanedEmbeddingOptions(cleanedOptions[key], defaultOptions[key]); typeof cleanedOptions[key] === 'object' &&
cleanedOptions[key] !== null &&
!Array.isArray(cleanedOptions[key])
) {
cleanedOptions[key] = getCleanedEmbeddingOptions(
cleanedOptions[key],
defaultOptions[key]
);
if (Object.keys(cleanedOptions[key]).length === 0) { if (Object.keys(cleanedOptions[key]).length === 0) {
delete cleanedOptions[key]; delete cleanedOptions[key];
} }
@@ -77,4 +97,59 @@ export function getCleanedEmbeddingOptions(options: any, defaultOptions: any = d
return cleanedOptions; return cleanedOptions;
} }
export const allowedEmbeddingBasemaps = Object.keys(basemaps).filter(basemap => !['ordnanceSurvey'].includes(basemap)); export const allowedEmbeddingBasemaps = Object.keys(basemaps).filter(
(basemap) => !['ordnanceSurvey'].includes(basemap)
);
export function getFilesFromEmbeddingOptions(options: EmbeddingOptions): string[] {
return options.files.concat(options.ids.map((id) => getURLForGoogleDriveFile(id)));
}
export function getURLForGoogleDriveFile(fileId: string): string {
return `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media&key=AIzaSyA2ZadQob_hXiT2VaYIkAyafPvz_4ZMssk`;
}
export function convertOldEmbeddingOptions(options: URLSearchParams): any {
let newOptions: any = {
token: PUBLIC_MAPBOX_TOKEN,
files: [],
ids: [],
};
if (options.has('state')) {
let state = JSON.parse(options.get('state')!);
if (state.ids) {
newOptions.ids.push(...state.ids);
}
if (state.urls) {
newOptions.files.push(...state.urls);
}
}
if (options.has('source')) {
let basemap = options.get('source')!;
if (basemap === 'satellite') {
newOptions.basemap = 'mapboxSatellite';
} else if (basemap === 'otm') {
newOptions.basemap = 'openTopoMap';
} else if (basemap === 'ohm') {
newOptions.basemap = 'openHikingMap';
}
}
if (options.has('imperial')) {
newOptions.distanceUnits = 'imperial';
}
if (options.has('running')) {
newOptions.velocityUnits = 'pace';
}
if (options.has('distance')) {
newOptions.distanceMarkers = true;
}
if (options.has('direction')) {
newOptions.directionMarkers = true;
}
if (options.has('slope')) {
newOptions.elevation = {
fill: 'slope',
};
}
return newOptions;
}
@@ -13,13 +13,13 @@
SquareActivity, SquareActivity,
Coins, Coins,
Milestone, Milestone,
Video Video,
} from 'lucide-svelte'; } from 'lucide-svelte';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import { import {
allowedEmbeddingBasemaps, allowedEmbeddingBasemaps,
getCleanedEmbeddingOptions, getCleanedEmbeddingOptions,
getDefaultEmbeddingOptions getDefaultEmbeddingOptions,
} from './Embedding'; } from './Embedding';
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public'; import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import Embedding from './Embedding.svelte'; import Embedding from './Embedding.svelte';
@@ -30,17 +30,25 @@
let options = getDefaultEmbeddingOptions(); let options = getDefaultEmbeddingOptions();
options.token = 'YOUR_MAPBOX_TOKEN'; options.token = 'YOUR_MAPBOX_TOKEN';
options.files = [ 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]; let files = options.files[0];
$: if (files) { $: {
let urls = files.split(','); let urls = files.split(',');
urls = urls.filter((url) => url.length > 0); urls = urls.filter((url) => url.length > 0);
if (JSON.stringify(urls) !== JSON.stringify(options.files)) { if (JSON.stringify(urls) !== JSON.stringify(options.files)) {
options.files = urls; options.files = urls;
} }
} }
let driveIds = '';
$: {
let ids = driveIds.split(',');
ids = ids.filter((id) => id.length > 0);
if (JSON.stringify(ids) !== JSON.stringify(options.ids)) {
options.ids = ids;
}
}
let manualCamera = false; let manualCamera = false;
@@ -84,7 +92,7 @@
} }
</script> </script>
<Card.Root> <Card.Root id="embedding-playground">
<Card.Header> <Card.Header>
<Card.Title>{$_('embedding.title')}</Card.Title> <Card.Title>{$_('embedding.title')}</Card.Title>
</Card.Header> </Card.Header>
@@ -94,6 +102,8 @@
<Input id="token" type="text" class="h-8" bind:value={options.token} /> <Input id="token" type="text" class="h-8" bind:value={options.token} />
<Label for="file_urls">{$_('embedding.file_urls')}</Label> <Label for="file_urls">{$_('embedding.file_urls')}</Label>
<Input id="file_urls" type="text" class="h-8" bind:value={files} /> <Input id="file_urls" type="text" class="h-8" bind:value={files} />
<Label for="drive_ids">{$_('embedding.drive_ids')}</Label>
<Input id="drive_ids" type="text" class="h-8" bind:value={driveIds} />
<Label for="basemap">{$_('embedding.basemap')}</Label> <Label for="basemap">{$_('embedding.basemap')}</Label>
<Select.Root <Select.Root
selected={{ value: options.basemap, label: $_(`layers.label.${options.basemap}`) }} selected={{ value: options.basemap, label: $_(`layers.label.${options.basemap}`) }}
@@ -120,7 +130,11 @@
<div class="grid grid-cols-2 gap-x-6 gap-y-3 rounded-md border p-3 mt-1"> <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"> <Label class="flex flex-row items-center gap-2">
{$_('embedding.height')} {$_('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> </Label>
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<span class="shrink-0"> <span class="shrink-0">
@@ -132,7 +146,11 @@
let value = selected?.value; let value = selected?.value;
if (value === 'none') { if (value === 'none') {
options.elevation.fill = undefined; options.elevation.fill = undefined;
} else if (value === 'slope' || value === 'surface') { } else if (
value === 'slope' ||
value === 'surface' ||
value === 'highway'
) {
options.elevation.fill = value; options.elevation.fill = value;
} }
}} }}
@@ -142,7 +160,10 @@
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
<Select.Item value="slope">{$_('quantities.slope')}</Select.Item> <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.Item value="none">{$_('embedding.none')}</Select.Item>
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
@@ -155,35 +176,35 @@
<Checkbox id="show-speed" bind:checked={options.elevation.speed} /> <Checkbox id="show-speed" bind:checked={options.elevation.speed} />
<Label for="show-speed" class="flex flex-row items-center gap-1"> <Label for="show-speed" class="flex flex-row items-center gap-1">
<Zap size="16" /> <Zap size="16" />
{$_('chart.show_speed')} {$_('quantities.speed')}
</Label> </Label>
</div> </div>
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<Checkbox id="show-hr" bind:checked={options.elevation.hr} /> <Checkbox id="show-hr" bind:checked={options.elevation.hr} />
<Label for="show-hr" class="flex flex-row items-center gap-1"> <Label for="show-hr" class="flex flex-row items-center gap-1">
<HeartPulse size="16" /> <HeartPulse size="16" />
{$_('chart.show_heartrate')} {$_('quantities.heartrate')}
</Label> </Label>
</div> </div>
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<Checkbox id="show-cad" bind:checked={options.elevation.cad} /> <Checkbox id="show-cad" bind:checked={options.elevation.cad} />
<Label for="show-cad" class="flex flex-row items-center gap-1"> <Label for="show-cad" class="flex flex-row items-center gap-1">
<Orbit size="16" /> <Orbit size="16" />
{$_('chart.show_cadence')} {$_('quantities.cadence')}
</Label> </Label>
</div> </div>
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<Checkbox id="show-temp" bind:checked={options.elevation.temp} /> <Checkbox id="show-temp" bind:checked={options.elevation.temp} />
<Label for="show-temp" class="flex flex-row items-center gap-1"> <Label for="show-temp" class="flex flex-row items-center gap-1">
<Thermometer size="16" /> <Thermometer size="16" />
{$_('chart.show_temperature')} {$_('quantities.temperature')}
</Label> </Label>
</div> </div>
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">
<Checkbox id="show-power" bind:checked={options.elevation.power} /> <Checkbox id="show-power" bind:checked={options.elevation.power} />
<Label for="show-power" class="flex flex-row items-center gap-1"> <Label for="show-power" class="flex flex-row items-center gap-1">
<SquareActivity size="16" /> <SquareActivity size="16" />
{$_('chart.show_power')} {$_('quantities.power')}
</Label> </Label>
</div> </div>
</div> </div>
@@ -214,6 +235,10 @@
<RadioGroup.Item value="imperial" id="imperial" /> <RadioGroup.Item value="imperial" id="imperial" />
<Label for="imperial">{$_('menu.imperial')}</Label> <Label for="imperial">{$_('menu.imperial')}</Label>
</div> </div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="nautical" id="nautical" />
<Label for="nautical">{$_('menu.nautical')}</Label>
</div>
</RadioGroup.Root> </RadioGroup.Root>
</Label> </Label>
<Label class="flex flex-col items-start gap-2"> <Label class="flex flex-col items-start gap-2">
@@ -303,7 +328,8 @@
<Label> <Label>
{$_('embedding.code')} {$_('embedding.code')}
</Label> </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"> <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;"/>`} {`<iframe src="https://gpx.studio${base}/embed?options=${encodeURIComponent(JSON.stringify(getCleanedEmbeddingOptions(options)))}${hash}" width="100%" height="600px" frameborder="0" style="outline: none;"/>`}
</code> </code>
@@ -5,12 +5,17 @@
import { _, locale } from 'svelte-i18n'; import { _, locale } from 'svelte-i18n';
export let files: string[]; export let files: string[];
export let ids: string[];
</script> </script>
<Button <Button
variant="ghost" variant="ghost"
class="absolute top-0 flex-wrap h-fit bg-background font-semibold rounded-md py-1 px-2 gap-1.5 xs:text-base mt-2.5 ml-2.5 mr-12" class="absolute top-0 flex-wrap h-fit bg-background font-semibold rounded-md py-1 px-2 gap-1.5 xs:text-base mt-2.5 ml-2.5 mr-12"
href="{getURLForLanguage($locale, '/app')}?files={encodeURIComponent(JSON.stringify(files))}" href="{getURLForLanguage($locale, '/app')}?{files.length > 0
? `files=${encodeURIComponent(JSON.stringify(files))}`
: ''}{files.length > 0 && ids.length > 0 ? '&' : ''}{ids.length > 0
? `ids=${encodeURIComponent(JSON.stringify(ids))}`
: ''}"
target="_blank" target="_blank"
> >
{$_('menu.open_in')} {$_('menu.open_in')}
@@ -17,9 +17,9 @@
setContext('orientation', orientation); setContext('orientation', orientation);
setContext('recursive', recursive); setContext('recursive', recursive);
const { verticalFileView } = settings; const { treeFileView } = settings;
verticalFileView.subscribe(($vertical) => { treeFileView.subscribe(($vertical) => {
if ($vertical) { if ($vertical) {
selection.update(($selection) => { selection.update(($selection) => {
$selection.forEach((item) => { $selection.forEach((item) => {
@@ -1,8 +1,8 @@
import { dbUtils, getFile } from "$lib/db"; import { dbUtils, getFile } from '$lib/db';
import { freeze } from "immer"; import { freeze } from 'immer';
import { GPXFile, Track, TrackSegment, Waypoint } from "gpx"; import { GPXFile, Track, TrackSegment, Waypoint } from 'gpx';
import { selection } from "./Selection"; import { selection } from './Selection';
import { newGPXFile } from "$lib/stores"; import { newGPXFile } from '$lib/stores';
export enum ListLevel { export enum ListLevel {
ROOT, ROOT,
@@ -10,7 +10,7 @@ export enum ListLevel {
TRACK, TRACK,
SEGMENT, SEGMENT,
WAYPOINTS, WAYPOINTS,
WAYPOINT WAYPOINT,
} }
export const allowedMoves: Record<ListLevel, ListLevel[]> = { export const allowedMoves: Record<ListLevel, ListLevel[]> = {
@@ -19,7 +19,7 @@ export const allowedMoves: Record<ListLevel, ListLevel[]> = {
[ListLevel.TRACK]: [ListLevel.FILE, ListLevel.TRACK], [ListLevel.TRACK]: [ListLevel.FILE, ListLevel.TRACK],
[ListLevel.SEGMENT]: [ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT], [ListLevel.SEGMENT]: [ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
[ListLevel.WAYPOINTS]: [ListLevel.WAYPOINTS], [ListLevel.WAYPOINTS]: [ListLevel.WAYPOINTS],
[ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT] [ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
}; };
export const allowedPastes: Record<ListLevel, ListLevel[]> = { 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.TRACK]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK],
[ListLevel.SEGMENT]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT], [ListLevel.SEGMENT]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
[ListLevel.WAYPOINTS]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT], [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 { 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) { if (fromItems.length === 0) {
return; return;
} }
@@ -338,11 +344,18 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
context.push(file.clone()); context.push(file.clone());
} else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) { } else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) {
context.push(file.trk[item.getTrackIndex()].clone()); 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()); context.push(file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()].clone());
} else if (item instanceof ListWaypointsItem) { } else if (item instanceof ListWaypointsItem) {
context.push(file.wpt.map((wpt) => wpt.clone())); 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()); context.push(file.wpt[item.getWaypointIndex()].clone());
} }
} }
@@ -359,7 +372,12 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
if (item instanceof ListTrackItem) { if (item instanceof ListTrackItem) {
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []); file.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []);
} else if (item instanceof ListTrackSegmentItem) { } 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) { } else if (item instanceof ListWaypointsItem) {
file.replaceWaypoints(0, file.wpt.length - 1, []); file.replaceWaypoints(0, file.wpt.length - 1, []);
} else if (item instanceof ListWaypointItem) { } else if (item instanceof ListWaypointItem) {
@@ -371,25 +389,43 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
toItems.forEach((item, i) => { toItems.forEach((item, i) => {
if (item instanceof ListTrackItem) { if (item instanceof ListTrackItem) {
if (context[i] instanceof Track) { 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) { } else if (context[i] instanceof TrackSegment) {
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [new Track({ file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [
trkseg: [context[i]] new Track({
})]); trkseg: [context[i]],
}),
]);
} }
} else if (item instanceof ListTrackSegmentItem && context[i] instanceof TrackSegment) { } else if (
file.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex() - 1, [context[i]]); item instanceof ListTrackSegmentItem &&
context[i] instanceof TrackSegment
) {
file.replaceTrackSegments(
item.getTrackIndex(),
item.getSegmentIndex(),
item.getSegmentIndex() - 1,
[context[i]]
);
} else if (item instanceof ListWaypointsItem) { } 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]); file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, context[i]);
} else if (context[i] instanceof Waypoint) { } else if (context[i] instanceof Waypoint) {
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, [context[i]]); file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, [context[i]]);
} }
} else if (item instanceof ListWaypointItem && context[i] instanceof Waypoint) { } 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) { if (fromParent instanceof ListRootItem) {
@@ -400,7 +436,10 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
callbacks.splice(0, 1); 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) => { toItems.forEach((item, i) => {
if (item instanceof ListFileItem) { if (item instanceof ListFileItem) {
if (context[i] instanceof GPXFile) { if (context[i] instanceof GPXFile) {
@@ -421,14 +460,18 @@ export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: L
} else if (context[i] instanceof TrackSegment) { } else if (context[i] instanceof TrackSegment) {
let newFile = newGPXFile(); let newFile = newGPXFile();
newFile._data.id = item.getFileId(); newFile._data.id = item.getFileId();
newFile.replaceTracks(0, 0, [new Track({ newFile.replaceTracks(0, 0, [
trkseg: [context[i]] new Track({
})]); trkseg: [context[i]],
}),
]);
files.set(item.getFileId(), freeze(newFile)); files.set(item.getFileId(), freeze(newFile));
} }
} }
}); });
}, context); },
context
);
selection.update(($selection) => { selection.update(($selection) => {
$selection.clear(); $selection.clear();
@@ -5,7 +5,7 @@
TrackSegment, TrackSegment,
Waypoint, Waypoint,
type AnyGPXTreeElement, type AnyGPXTreeElement,
type GPXTreeElement type GPXTreeElement,
} from 'gpx'; } from 'gpx';
import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index'; import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
import { settings, type GPXFileWithStatistics } from '$lib/db'; import { settings, type GPXFileWithStatistics } from '$lib/db';
@@ -19,7 +19,7 @@
ListWaypointItem, ListWaypointItem,
ListWaypointsItem, ListWaypointsItem,
type ListItem, type ListItem,
type ListTrackItem type ListTrackItem,
} from './FileList'; } from './FileList';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import { selection } from './Selection'; import { selection } from './Selection';
@@ -39,19 +39,20 @@
node instanceof GPXFile && item instanceof ListFileItem node instanceof GPXFile && item instanceof ListFileItem
? node.metadata.name ? node.metadata.name
: node instanceof Track : node instanceof Track
? node.name ?? `${$_('gpx.track')} ${(item as ListTrackItem).trackIndex + 1}` ? (node.name ?? `${$_('gpx.track')} ${(item as ListTrackItem).trackIndex + 1}`)
: node instanceof TrackSegment : node instanceof TrackSegment
? `${$_('gpx.segment')} ${(item as ListTrackSegmentItem).segmentIndex + 1}` ? `${$_('gpx.segment')} ${(item as ListTrackSegmentItem).segmentIndex + 1}`
: node instanceof Waypoint : 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 : node instanceof GPXFile && item instanceof ListWaypointsItem
? $_('gpx.waypoints') ? $_('gpx.waypoints')
: ''; : '';
const { verticalFileView } = settings; const { treeFileView } = settings;
function openIfSelectedChild() { function openIfSelectedChild() {
if (collapsible && get(verticalFileView) && $selection.hasAnyChildren(item, false)) { if (collapsible && get(treeFileView) && $selection.hasAnyChildren(item, false)) {
collapsible.openNode(); collapsible.openNode();
} }
} }
@@ -19,9 +19,10 @@
ListWaypointsItem, ListWaypointsItem,
allowedMoves, allowedMoves,
moveItems, moveItems,
type ListItem type ListItem,
} from './FileList'; } from './FileList';
import { selection } from './Selection'; import { selection } from './Selection';
import { isMac } from '$lib/utils';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
export let node: export let node:
@@ -77,8 +78,13 @@
if ( if (
e.originalEvent && e.originalEvent &&
!(e.originalEvent.ctrlKey || e.originalEvent.metaKey || e.originalEvent.shiftKey) && !(
($selection.size > 1 || !$selection.has(item.extend(getRealId(changed[0])))) e.originalEvent.ctrlKey ||
e.originalEvent.metaKey ||
e.originalEvent.shiftKey
) &&
($selection.size > 1 ||
!$selection.has(item.extend(getRealId(changed[0]))))
) { ) {
// Fix bug that sometimes causes a single select to be treated as a multi-select // Fix bug that sometimes causes a single select to be treated as a multi-select
$selection.clear(); $selection.clear();
@@ -107,7 +113,7 @@
Sortable.utils.select(element); Sortable.utils.select(element);
element.scrollIntoView({ element.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'nearest' block: 'nearest',
}); });
} else { } else {
Sortable.utils.deselect(element); Sortable.utils.deselect(element);
@@ -149,12 +155,12 @@
group: { group: {
name: sortableLevel, name: sortableLevel,
pull: allowedMoves[sortableLevel], pull: allowedMoves[sortableLevel],
put: true put: true,
}, },
direction: orientation, direction: orientation,
forceAutoScrollFallback: true, forceAutoScrollFallback: true,
multiDrag: true, multiDrag: true,
multiDragKey: 'Meta', multiDragKey: isMac() ? 'Meta' : 'Ctrl',
avoidImplicitDeselect: true, avoidImplicitDeselect: true,
onSelect: updateToSelection, onSelect: updateToSelection,
onDeselect: updateToSelection, onDeselect: updateToSelection,
@@ -191,7 +197,9 @@
fromItems = [fromItem.extend('waypoints')]; fromItems = [fromItem.extend('waypoints')];
} else { } else {
let oldIndices: number[] = let oldIndices: number[] =
e.oldIndicies.length > 0 ? e.oldIndicies.map((i) => i.index) : [e.oldIndex]; e.oldIndicies.length > 0
? e.oldIndicies.map((i) => i.index)
: [e.oldIndex];
oldIndices = oldIndices.filter((i) => i >= 0); oldIndices = oldIndices.filter((i) => i >= 0);
oldIndices.sort((a, b) => a - b); oldIndices.sort((a, b) => a - b);
@@ -206,7 +214,9 @@
} }
let newIndices: number[] = let newIndices: number[] =
e.newIndicies.length > 0 ? e.newIndicies.map((i) => i.index) : [e.newIndex]; e.newIndicies.length > 0
? e.newIndicies.map((i) => i.index)
: [e.newIndex];
newIndices = newIndices.filter((i) => i >= 0); newIndices = newIndices.filter((i) => i >= 0);
newIndices.sort((a, b) => a - b); newIndices.sort((a, b) => a - b);
@@ -223,16 +233,16 @@
moveItems(fromItem, toItem, fromItems, toItems); moveItems(fromItem, toItem, fromItems, toItems);
} }
} },
}); });
Object.defineProperty(sortable, '_item', { Object.defineProperty(sortable, '_item', {
value: item, value: item,
writable: true writable: true,
}); });
Object.defineProperty(sortable, '_waypointRoot', { Object.defineProperty(sortable, '_waypointRoot', {
value: waypointRoot, value: waypointRoot,
writable: true writable: true,
}); });
} }
@@ -15,9 +15,10 @@
EyeOff, EyeOff,
ClipboardCopy, ClipboardCopy,
ClipboardPaste, ClipboardPaste,
Maximize,
Scissors, Scissors,
FileStack, FileStack,
FileX FileX,
} from 'lucide-svelte'; } from 'lucide-svelte';
import { import {
ListFileItem, ListFileItem,
@@ -25,7 +26,7 @@
ListTrackItem, ListTrackItem,
ListWaypointItem, ListWaypointItem,
allowedPastes, allowedPastes,
type ListItem type ListItem,
} from './FileList'; } from './FileList';
import { import {
copied, copied,
@@ -35,22 +36,25 @@
pasteSelection, pasteSelection,
selectAll, selectAll,
selectItem, selectItem,
selection selection,
} from './Selection'; } from './Selection';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { allHidden, editMetadata, editStyle, embedding, gpxLayers, map } from '$lib/stores';
import { import {
GPXTreeElement, allHidden,
Track, editMetadata,
TrackSegment, editStyle,
type AnyGPXTreeElement, embedding,
Waypoint, centerMapOnSelection,
GPXFile gpxLayers,
} from 'gpx'; map,
} from '$lib/stores';
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import MetadataDialog from './MetadataDialog.svelte'; import MetadataDialog from './MetadataDialog.svelte';
import StyleDialog from './StyleDialog.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 node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
export let item: ListItem; export let item: ListItem;
@@ -66,13 +70,14 @@
nodeColors = []; nodeColors = [];
if (node instanceof GPXFile) { if (node instanceof GPXFile) {
let style = node.getStyle(); let defaultColor = undefined;
let layer = gpxLayers.get(item.getFileId()); let layer = gpxLayers.get(item.getFileId());
if (layer) { if (layer) {
style.color.push(layer.layerColor); defaultColor = layer.layerColor;
} }
let style = node.getStyle(defaultColor);
style.color.forEach((c) => { style.color.forEach((c) => {
if (!nodeColors.includes(c)) { if (!nodeColors.includes(c)) {
nodeColors.push(c); nodeColors.push(c);
@@ -81,8 +86,8 @@
} else if (node instanceof Track) { } else if (node instanceof Track) {
let style = node.getStyle(); let style = node.getStyle();
if (style) { if (style) {
if (style.color && !nodeColors.includes(style.color)) { if (style['gpx_style:color'] && !nodeColors.includes(style['gpx_style:color'])) {
nodeColors.push(style.color); nodeColors.push(style['gpx_style:color']);
} }
} }
if (nodeColors.length === 0) { if (nodeColors.length === 0) {
@@ -94,6 +99,8 @@
} }
} }
$: symbolKey = node instanceof Waypoint ? getSymbolKey(node.sym) : undefined;
let openEditMetadata: boolean = false; let openEditMetadata: boolean = false;
let openEditStyle: boolean = false; let openEditStyle: boolean = false;
@@ -170,7 +177,10 @@
if (layer && file) { if (layer && file) {
let waypoint = file.wpt[item.getWaypointIndex()]; let waypoint = file.wpt[item.getWaypointIndex()];
if (waypoint) { if (waypoint) {
layer.showWaypointPopup(waypoint); waypointPopup?.setItem({
item: waypoint,
fileId: item.getFileId(),
});
} }
} }
} }
@@ -179,7 +189,7 @@
if (item instanceof ListWaypointItem) { if (item instanceof ListWaypointItem) {
let layer = gpxLayers.get(item.getFileId()); let layer = gpxLayers.get(item.getFileId());
if (layer) { if (layer) {
layer.hideWaypointPopup(); waypointPopup?.setItem(null);
} }
} }
}} }}
@@ -187,16 +197,30 @@
{#if item.level === ListLevel.SEGMENT} {#if item.level === ListLevel.SEGMENT}
<Waypoints size="16" class="mr-1 shrink-0" /> <Waypoints size="16" class="mr-1 shrink-0" />
{:else if item.level === ListLevel.WAYPOINT} {: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" /> <MapPin size="16" class="mr-1 shrink-0" />
{/if} {/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} {label}
</span> </span>
{#if hidden} {#if hidden}
<EyeOff <EyeOff
size="12" size="12"
class="shrink-0 mt-1 ml-1 {orientation === 'vertical' ? 'mr-2' : ''} {item.level === class="shrink-0 mt-1 ml-1 {orientation === 'vertical'
ListLevel.SEGMENT || item.level === ListLevel.WAYPOINT ? 'mr-2'
: ''} {item.level === ListLevel.SEGMENT ||
item.level === ListLevel.WAYPOINT
? 'mr-3' ? 'mr-3'
: ''}" : ''}"
/> />
@@ -239,10 +263,7 @@
{#if item instanceof ListFileItem} {#if item instanceof ListFileItem}
<ContextMenu.Item <ContextMenu.Item
disabled={!singleSelection} disabled={!singleSelection}
on:click={() => on:click={() => dbUtils.addNewTrack(item.getFileId())}
dbUtils.applyToFile(item.getFileId(), (file) =>
file.replaceTracks(file.trk.length, file.trk.length, [new Track()])
)}
> >
<Plus size="16" class="mr-1" /> <Plus size="16" class="mr-1" />
{$_('menu.new_track')} {$_('menu.new_track')}
@@ -251,17 +272,7 @@
{:else if item instanceof ListTrackItem} {:else if item instanceof ListTrackItem}
<ContextMenu.Item <ContextMenu.Item
disabled={!singleSelection} disabled={!singleSelection}
on:click={() => { on:click={() => dbUtils.addNewSegment(item.getFileId(), item.getTrackIndex())}
let trackIndex = item.getTrackIndex();
dbUtils.applyToFile(item.getFileId(), (file) =>
file.replaceTrackSegments(
trackIndex,
file.trk[trackIndex].trkseg.length,
file.trk[trackIndex].trkseg.length,
[new TrackSegment()]
)
);
}}
> >
<Plus size="16" class="mr-1" /> <Plus size="16" class="mr-1" />
{$_('menu.new_segment')} {$_('menu.new_segment')}
@@ -275,9 +286,13 @@
{$_('menu.select_all')} {$_('menu.select_all')}
<Shortcut key="A" ctrl={true} /> <Shortcut key="A" ctrl={true} />
</ContextMenu.Item> </ContextMenu.Item>
<ContextMenu.Separator />
{/if} {/if}
{#if orientation === 'vertical'} <ContextMenu.Item on:click={centerMapOnSelection}>
<Maximize size="16" class="mr-1" />
{$_('menu.center')}
<Shortcut key="⏎" ctrl={true} />
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item on:click={dbUtils.duplicateSelection}> <ContextMenu.Item on:click={dbUtils.duplicateSelection}>
<Copy size="16" class="mr-1" /> <Copy size="16" class="mr-1" />
{$_('menu.duplicate')} {$_('menu.duplicate')}
@@ -306,7 +321,6 @@
</ContextMenu.Item> </ContextMenu.Item>
{/if} {/if}
<ContextMenu.Separator /> <ContextMenu.Separator />
{/if}
<ContextMenu.Item on:click={dbUtils.deleteSelection}> <ContextMenu.Item on:click={dbUtils.deleteSelection}>
{#if item instanceof ListFileItem} {#if item instanceof ListFileItem}
<FileX size="16" class="mr-1" /> <FileX size="16" class="mr-1" />
@@ -17,15 +17,15 @@
let name: string = let name: string =
node instanceof GPXFile node instanceof GPXFile
? node.metadata.name ?? '' ? (node.metadata.name ?? '')
: node instanceof Track : node instanceof Track
? node.name ?? '' ? (node.name ?? '')
: ''; : '';
let description: string = let description: string =
node instanceof GPXFile node instanceof GPXFile
? node.metadata.desc ?? '' ? (node.metadata.desc ?? '')
: node instanceof Track : node instanceof Track
? node.desc ?? '' ? (node.desc ?? '')
: ''; : '';
$: if (!open) { $: if (!open) {
@@ -47,6 +47,9 @@
if (item instanceof ListFileItem && node instanceof GPXFile) { if (item instanceof ListFileItem && node instanceof GPXFile) {
file.metadata.name = name; file.metadata.name = name;
file.metadata.desc = description; file.metadata.desc = description;
if (file.trk.length === 1) {
file.trk[0].name = name;
}
} else if (item instanceof ListTrackItem && node instanceof Track) { } else if (item instanceof ListTrackItem && node instanceof Track) {
file.trk[item.getTrackIndex()].name = name; file.trk[item.getTrackIndex()].name = name;
file.trk[item.getTrackIndex()].desc = description; file.trk[item.getTrackIndex()].desc = description;
@@ -1,12 +1,23 @@
import { get, writable } from "svelte/store"; import { get, writable } from 'svelte/store';
import { ListFileItem, ListItem, ListRootItem, ListTrackItem, ListTrackSegmentItem, ListWaypointItem, ListLevel, sortItems, ListWaypointsItem, moveItems } from "./FileList"; import {
import { fileObservers, getFile, getFileIds, settings } from "$lib/db"; ListFileItem,
ListItem,
ListRootItem,
ListTrackItem,
ListTrackSegmentItem,
ListWaypointItem,
ListLevel,
sortItems,
ListWaypointsItem,
moveItems,
} from './FileList';
import { fileObservers, getFile, getFileIds, settings } from '$lib/db';
export class SelectionTreeType { export class SelectionTreeType {
item: ListItem; item: ListItem;
selected: boolean; selected: boolean;
children: { children: {
[key: string | number]: SelectionTreeType [key: string | number]: SelectionTreeType;
}; };
size: number = 0; size: number = 0;
@@ -67,7 +78,11 @@ export class SelectionTreeType {
} }
hasAnyParent(item: ListItem, self: boolean = true): boolean { 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; return this.selected;
} }
let id = item.getIdAtLevel(this.item.level); let id = item.getIdAtLevel(this.item.level);
@@ -80,7 +95,11 @@ export class SelectionTreeType {
} }
hasAnyChildren(item: ListItem, self: boolean = true, ignoreIds?: (string | number)[]): boolean { 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; return this.selected;
} }
let id = item.getIdAtLevel(this.item.level); let id = item.getIdAtLevel(this.item.level);
@@ -131,7 +150,7 @@ export class SelectionTreeType {
delete this.children[id]; delete this.children[id];
} }
} }
}; }
export const selection = writable<SelectionTreeType>(new SelectionTreeType(new ListRootItem())); export const selection = writable<SelectionTreeType>(new SelectionTreeType(new ListRootItem()));
@@ -181,7 +200,10 @@ export function selectAll() {
let file = getFile(item.getFileId()); let file = getFile(item.getFileId());
if (file) { if (file) {
file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => { 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) { } else if (item instanceof ListWaypointItem) {
@@ -205,14 +227,24 @@ export function getOrderedSelection(reverse: boolean = false): ListItem[] {
return selected; 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) => { get(settings.fileOrder).forEach((fileId) => {
let level: ListLevel | undefined = undefined; let level: ListLevel | undefined = undefined;
let items: ListItem[] = []; let items: ListItem[] = [];
selectedItems.forEach((item) => { selectedItems.forEach((item) => {
if (item.getFileId() === fileId) { if (item.getFileId() === fileId) {
level = item.level; 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); 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); applyToOrderedItemsFromFile(get(selection).getSelected(), callback, reverse);
} }
@@ -270,7 +305,11 @@ export function pasteSelection() {
let startIndex: number | undefined = undefined; let startIndex: number | undefined = undefined;
if (fromItems[0].level === toParent.level) { 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; startIndex = toParent.getId() + 1;
} }
toParent = toParent.getParent(); toParent = toParent.getParent();
@@ -288,20 +327,41 @@ export function pasteSelection() {
fromItems.forEach((item, index) => { fromItems.forEach((item, index) => {
if (toParent instanceof ListFileItem) { if (toParent instanceof ListFileItem) {
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) { 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) { } else if (item instanceof ListWaypointsItem) {
toItems.push(new ListWaypointsItem(toParent.getFileId())); toItems.push(new ListWaypointsItem(toParent.getFileId()));
} else if (item instanceof ListWaypointItem) { } 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) { } else if (toParent instanceof ListTrackItem) {
if (item instanceof ListTrackSegmentItem) { if (item instanceof ListTrackSegmentItem) {
let toTrackIndex = toParent.getTrackIndex(); 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) { } else if (toParent instanceof ListWaypointsItem) {
if (item instanceof ListWaypointItem) { 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
)
);
} }
} }
}); });
@@ -14,20 +14,20 @@
export let item: ListItem; export let item: ListItem;
export let open = false; export let open = false;
const { defaultOpacity, defaultWeight } = settings; const { defaultOpacity, defaultWidth } = settings;
let colors: string[] = []; let colors: string[] = [];
let color: string | undefined = undefined; let color: string | undefined = undefined;
let opacity: number[] = []; let opacity: number[] = [];
let weight: number[] = []; let width: number[] = [];
let colorChanged = false; let colorChanged = false;
let opacityChanged = false; let opacityChanged = false;
let weightChanged = false; let widthChanged = false;
function setStyleInputs() { function setStyleInputs() {
colors = []; colors = [];
opacity = []; opacity = [];
weight = []; width = [];
$selection.forEach((item) => { $selection.forEach((item) => {
if (item instanceof ListFileItem) { if (item instanceof ListFileItem) {
@@ -47,9 +47,9 @@
opacity.push(o); opacity.push(o);
} }
}); });
style.weight.forEach((w) => { style.width.forEach((w) => {
if (!weight.includes(w)) { if (!width.includes(w)) {
weight.push(w); width.push(w);
} }
}); });
} }
@@ -60,14 +60,20 @@
let track = file.trk[item.getTrackIndex()]; let track = file.trk[item.getTrackIndex()];
let style = track.getStyle(); let style = track.getStyle();
if (style) { if (style) {
if (style.color && !colors.includes(style.color)) { if (
colors.push(style.color); style['gpx_style:color'] &&
!colors.includes(style['gpx_style:color'])
) {
colors.push(style['gpx_style:color']);
} }
if (style.opacity && !opacity.includes(style.opacity)) { if (
opacity.push(style.opacity); style['gpx_style:opacity'] &&
!opacity.includes(style['gpx_style:opacity'])
) {
opacity.push(style['gpx_style:opacity']);
} }
if (style.weight && !weight.includes(style.weight)) { if (style['gpx_style:width'] && !width.includes(style['gpx_style:width'])) {
weight.push(style.weight); width.push(style['gpx_style:width']);
} }
} }
if (!colors.includes(layer.layerColor)) { if (!colors.includes(layer.layerColor)) {
@@ -79,11 +85,11 @@
color = colors[0]; color = colors[0];
opacity = [opacity[0] ?? $defaultOpacity]; opacity = [opacity[0] ?? $defaultOpacity];
weight = [weight[0] ?? $defaultWeight]; width = [width[0] ?? $defaultWidth];
colorChanged = false; colorChanged = false;
opacityChanged = false; opacityChanged = false;
weightChanged = false; widthChanged = false;
} }
$: if ($selection && open) { $: if ($selection && open) {
@@ -123,37 +129,37 @@
{$_('menu.style.width')} {$_('menu.style.width')}
<div class="w-40 p-2"> <div class="w-40 p-2">
<Slider <Slider
bind:value={weight} bind:value={width}
id="weight" id="width"
min={1} min={1}
max={10} max={10}
step={1} step={1}
onValueChange={() => (weightChanged = true)} onValueChange={() => (widthChanged = true)}
/> />
</div> </div>
</Label> </Label>
<Button <Button
variant="outline" variant="outline"
disabled={!colorChanged && !opacityChanged && !weightChanged} disabled={!colorChanged && !opacityChanged && !widthChanged}
on:click={() => { on:click={() => {
let style = {}; let style = {};
if (colorChanged) { if (colorChanged) {
style.color = color; style['gpx_style:color'] = color;
} }
if (opacityChanged) { if (opacityChanged) {
style.opacity = opacity[0]; style['gpx_style:opacity'] = opacity[0];
} }
if (weightChanged) { if (widthChanged) {
style.weight = weight[0]; style['gpx_style:width'] = width[0];
} }
dbUtils.setStyleToSelection(style); dbUtils.setStyleToSelection(style);
if (item instanceof ListFileItem && $selection.size === gpxLayers.size) { if (item instanceof ListFileItem && $selection.size === gpxLayers.size) {
if (style.opacity) { if (style['gpx_style:opacity']) {
$defaultOpacity = style.opacity; $defaultOpacity = style['gpx_style:opacity'];
} }
if (style.weight) { if (style['gpx_style:width']) {
$defaultWeight = style.weight; $defaultWidth = style['gpx_style:width'];
} }
} }
@@ -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>
@@ -1,10 +1,17 @@
import { settings } from '$lib/db';
import { gpxStatistics } from '$lib/stores';
import { get } from 'svelte/store';
import { font } from "$lib/assets/layers"; const { distanceMarkers, distanceUnits } = settings;
import { settings } from "$lib/db";
import { gpxStatistics } from "$lib/stores";
import { get } from "svelte/store";
const { distanceMarkers, distanceUnits, currentBasemap } = settings; const stops = [
[100, 0],
[50, 7],
[25, 8, 10],
[10, 10],
[5, 11],
[1, 13],
];
export class DistanceMarkers { export class DistanceMarkers {
map: mapboxgl.Map; map: mapboxgl.Map;
@@ -17,7 +24,7 @@ export class DistanceMarkers {
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded)); this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
this.unsubscribes.push(distanceMarkers.subscribe(this.updateBinded)); this.unsubscribes.push(distanceMarkers.subscribe(this.updateBinded));
this.unsubscribes.push(distanceUnits.subscribe(this.updateBinded)); this.unsubscribes.push(distanceUnits.subscribe(this.updateBinded));
this.map.on('style.load', this.updateBinded); this.map.on('style.import.load', this.updateBinded);
} }
update() { update() {
@@ -29,41 +36,55 @@ export class DistanceMarkers {
} else { } else {
this.map.addSource('distance-markers', { this.map.addSource('distance-markers', {
type: 'geojson', 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({ this.map.addLayer({
id: 'distance-markers', id: `distance-markers-${d}`,
type: 'symbol', type: 'symbol',
source: 'distance-markers', source: 'distance-markers',
filter:
d === 5
? [
'any',
['==', ['get', 'level'], 5],
['==', ['get', 'level'], 25],
]
: ['==', ['get', 'level'], d],
minzoom: minzoom,
maxzoom: maxzoom ?? 24,
layout: { layout: {
'text-field': ['get', 'distance'], 'text-field': ['get', 'distance'],
'text-size': 14, 'text-size': 14,
'text-font': [font[get(currentBasemap)] ?? 'Open Sans Bold'], 'text-font': ['Open Sans Bold'],
'text-padding': 20,
}, },
paint: { paint: {
'text-color': 'black', 'text-color': 'black',
'text-halo-width': 2, 'text-halo-width': 2,
'text-halo-color': 'white', 'text-halo-color': 'white',
},
});
} else {
this.map.moveLayer(`distance-markers-${d}`);
} }
}); });
} else { } 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) {
} catch (e) { // No reliable way to check if the map is ready to add sources and layers // No reliable way to check if the map is ready to add sources and layers
return; return;
} }
} }
remove() { remove() {
this.unsubscribes.forEach(unsubscribe => unsubscribe()); this.unsubscribes.forEach((unsubscribe) => unsubscribe());
} }
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection { getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
@@ -72,17 +93,28 @@ export class DistanceMarkers {
let features = []; let features = [];
let currentTargetDistance = 1; let currentTargetDistance = 1;
for (let i = 0; i < statistics.local.distance.total.length; i++) { 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 distance = currentTargetDistance.toFixed(0);
let [level, minzoom] = stops.find(([d]) => currentTargetDistance % d === 0) ?? [
0, 0,
];
features.push({ features.push({
type: 'Feature', type: 'Feature',
geometry: { geometry: {
type: 'Point', type: 'Point',
coordinates: [statistics.local.points[i].getLongitude(), statistics.local.points[i].getLatitude()] coordinates: [
statistics.local.points[i].getLongitude(),
statistics.local.points[i].getLatitude(),
],
}, },
properties: { properties: {
distance, distance,
} level,
minzoom,
},
} as GeoJSON.Feature); } as GeoJSON.Feature);
currentTargetDistance += 1; currentTargetDistance += 1;
} }
@@ -90,7 +122,7 @@ export class DistanceMarkers {
return { return {
type: 'FeatureCollection', type: 'FeatureCollection',
features features,
}; };
} }
} }
+204 -111
View File
@@ -1,16 +1,28 @@
import { currentTool, map, Tool } from "$lib/stores"; import { currentTool, map, Tool } from '$lib/stores';
import { settings, type GPXFileWithStatistics, dbUtils } from "$lib/db"; import { settings, type GPXFileWithStatistics, dbUtils } from '$lib/db';
import { get, type Readable } from "svelte/store"; import { get, type Readable } from 'svelte/store';
import mapboxgl from "mapbox-gl"; import mapboxgl from 'mapbox-gl';
import { currentPopupWaypoint, deleteWaypoint, waypointPopup } from "./WaypointPopup"; import { waypointPopup, deleteWaypoint, trackpointPopup } from './GPXLayerPopup';
import { addSelectItem, selectItem, selection } from "$lib/components/file-list/Selection"; import { addSelectItem, selectItem, selection } from '$lib/components/file-list/Selection';
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList"; import {
import type { Waypoint } from "gpx"; ListTrackSegmentItem,
import { getElevation, resetCursor, setGrabbingCursor, setPointerCursor, setScissorsCursor } from "$lib/utils"; ListWaypointItem,
import { font } from "$lib/assets/layers"; ListWaypointsItem,
import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte"; ListTrackItem,
import { MapPin, Square } from "lucide-static"; ListFileItem,
import { getSymbolKey, symbols } from "$lib/assets/symbols"; 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 = [ const colors = [
'#ff0000', '#ff0000',
@@ -23,7 +35,7 @@ const colors = [
'#288228', '#288228',
'#9933ff', '#9933ff',
'#50f0be', '#50f0be',
'#8c645a' '#8c645a',
]; ];
const colorCount: { [key: string]: number } = {}; const colorCount: { [key: string]: number } = {};
@@ -47,26 +59,30 @@ function decrementColor(color: string) {
function getMarkerForSymbol(symbol: string | undefined, layerColor: string) { function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined; let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined;
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
${Square ${Square.replace('width="24"', 'width="12"')
.replace('width="24"', 'width="12"')
.replace('height="24"', 'height="12"') .replace('height="24"', 'height="12"')
.replace('stroke="currentColor"', 'stroke="SteelBlue"') .replace('stroke="currentColor"', 'stroke="SteelBlue"')
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"') .replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
.replace('fill="none"', `fill="${layerColor}"`)} .replace('fill="none"', `fill="${layerColor}"`)}
${MapPin ${MapPin.replace('width="24"', '')
.replace('width="24"', '')
.replace('height="24"', '') .replace('height="24"', '')
.replace('stroke="currentColor"', '') .replace('stroke="currentColor"', '')
.replace('path', `path fill="#3fb1ce" stroke="SteelBlue" stroke-width="1"`) .replace('path', `path fill="#3fb1ce" stroke="SteelBlue" stroke-width="1"`)
.replace('circle', `circle fill="${symbolSvg ? 'none' : 'white'}" stroke="${symbolSvg ? 'none' : 'white'}" stroke-width="2"`)} .replace(
${symbolSvg?.replace('width="24"', 'width="10"') '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('height="24"', 'height="10"')
.replace('stroke="currentColor"', 'stroke="white"') .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>`; </svg>`;
} }
const { directionMarkers, verticalFileView, currentBasemap, defaultOpacity, defaultWeight } = settings; const { directionMarkers, treeFileView, defaultOpacity, defaultWidth } = settings;
export class GPXLayer { export class GPXLayer {
map: mapboxgl.Map; map: mapboxgl.Map;
@@ -81,16 +97,22 @@ export class GPXLayer {
updateBinded: () => void = this.update.bind(this); updateBinded: () => void = this.update.bind(this);
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this); layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this); layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
layerOnMouseMoveBinded: (e: any) => void = this.layerOnMouseMove.bind(this);
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this); layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
maybeHideWaypointPopupBinded: (e: any) => void = this.maybeHideWaypointPopup.bind(this); layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.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.map = map;
this.fileId = fileId; this.fileId = fileId;
this.file = file; this.file = file;
this.layerColor = getColor(); this.layerColor = getColor();
this.unsubscribe.push(file.subscribe(this.updateBinded)); 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)); let newSelected = $selection.hasAnyChildren(new ListFileItem(this.fileId));
if (this.selected || newSelected) { if (this.selected || newSelected) {
this.selected = newSelected; this.selected = newSelected;
@@ -99,20 +121,23 @@ export class GPXLayer {
if (newSelected) { if (newSelected) {
this.moveToFront(); this.moveToFront();
} }
})); })
);
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded)); 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) { if (tool === Tool.WAYPOINT && !this.draggable) {
this.draggable = true; this.draggable = true;
this.markers.forEach(marker => marker.setDraggable(true)); this.markers.forEach((marker) => marker.setDraggable(true));
} else if (tool !== Tool.WAYPOINT && this.draggable) { } else if (tool !== Tool.WAYPOINT && this.draggable) {
this.draggable = false; this.draggable = false;
this.markers.forEach(marker => marker.setDraggable(false)); this.markers.forEach((marker) => marker.setDraggable(false));
} }
})); })
);
this.draggable = get(currentTool) === Tool.WAYPOINT; this.draggable = get(currentTool) === Tool.WAYPOINT;
this.map.on('style.load', this.updateBinded); this.map.on('style.import.load', this.updateBinded);
} }
update() { update() {
@@ -121,7 +146,11 @@ export class GPXLayer {
return; 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); decrementColor(this.layerColor);
this.layerColor = `#${file._data.style.color}`; this.layerColor = `#${file._data.style.color}`;
} }
@@ -133,7 +162,7 @@ export class GPXLayer {
} else { } else {
this.map.addSource(this.fileId, { this.map.addSource(this.fileId, {
type: 'geojson', type: 'geojson',
data: this.getGeoJSON() data: this.getGeoJSON(),
}); });
} }
@@ -144,23 +173,26 @@ export class GPXLayer {
source: this.fileId, source: this.fileId,
layout: { layout: {
'line-join': 'round', 'line-join': 'round',
'line-cap': 'round' 'line-cap': 'round',
}, },
paint: { paint: {
'line-color': ['get', 'color'], 'line-color': ['get', 'color'],
'line-width': ['get', 'weight'], 'line-width': ['get', 'width'],
'line-opacity': ['get', 'opacity'] 'line-opacity': ['get', 'opacity'],
} },
}); });
this.map.on('click', this.fileId, this.layerOnClickBinded); 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('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
this.map.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded); this.map.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
this.map.on('mousemove', this.fileId, this.layerOnMouseMoveBinded);
} }
if (get(directionMarkers)) { if (get(directionMarkers)) {
if (!this.map.getLayer(this.fileId + '-direction')) { if (!this.map.getLayer(this.fileId + '-direction')) {
this.map.addLayer({ this.map.addLayer(
{
id: this.fileId + '-direction', id: this.fileId + '-direction',
type: 'symbol', type: 'symbol',
source: this.fileId, source: this.fileId,
@@ -170,7 +202,7 @@ export class GPXLayer {
'text-keep-upright': false, 'text-keep-upright': false,
'text-max-angle': 361, 'text-max-angle': 361,
'text-allow-overlap': true, 'text-allow-overlap': true,
'text-font': [font[get(currentBasemap)] ?? 'Open Sans Bold'], 'text-font': ['Open Sans Bold'],
'symbol-placement': 'line', 'symbol-placement': 'line',
'symbol-spacing': 20, 'symbol-spacing': 20,
}, },
@@ -178,9 +210,11 @@ export class GPXLayer {
'text-color': 'white', 'text-color': 'white',
'text-opacity': 0.7, 'text-opacity': 0.7,
'text-halo-width': 0.2, 'text-halo-width': 0.2,
'text-halo-color': 'white' 'text-halo-color': 'white',
} },
}, this.map.getLayer('distance-markers') ? 'distance-markers' : undefined); },
this.map.getLayer('distance-markers') ? 'distance-markers' : undefined
);
} }
} else { } else {
if (this.map.getLayer(this.fileId + '-direction')) { if (this.map.getLayer(this.fileId + '-direction')) {
@@ -195,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')) { 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; return;
} }
let markerIndex = 0; let markerIndex = 0;
if (get(selection).hasAnyChildren(new ListFileItem(this.fileId))) { 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); let symbolKey = getSymbolKey(waypoint.sym);
if (markerIndex < this.markers.length) { 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()); 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 { } else {
let element = document.createElement('div'); let element = document.createElement('div');
element.classList.add('w-8', 'h-8', 'drop-shadow-xl'); element.classList.add('w-8', 'h-8', 'drop-shadow-xl');
@@ -219,15 +283,15 @@ export class GPXLayer {
let marker = new mapboxgl.Marker({ let marker = new mapboxgl.Marker({
draggable: this.draggable, draggable: this.draggable,
element, element,
anchor: 'bottom' anchor: 'bottom',
}).setLngLat(waypoint.getCoordinates()); }).setLngLat(waypoint.getCoordinates());
Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true }); Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true });
let dragEndTimestamp = 0; let dragEndTimestamp = 0;
marker.getElement().addEventListener('mouseover', (e) => { marker.getElement().addEventListener('mousemove', (e) => {
if (marker._isDragging) { if (marker._isDragging) {
return; return;
} }
this.showWaypointPopup(marker._waypoint); waypointPopup?.setItem({ item: marker._waypoint, fileId: this.fileId });
e.stopPropagation(); e.stopPropagation();
}); });
marker.getElement().addEventListener('click', (e) => { marker.getElement().addEventListener('click', (e) => {
@@ -241,37 +305,49 @@ export class GPXLayer {
return; return;
} }
if (get(verticalFileView)) { if (get(treeFileView)) {
if ((e.ctrlKey || e.metaKey) && get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), false)) { if (
addSelectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index)); (e.ctrlKey || e.metaKey) &&
get(selection).hasAnyChildren(
new ListWaypointsItem(this.fileId),
false
)
) {
addSelectItem(
new ListWaypointItem(this.fileId, marker._waypoint._data.index)
);
} else { } 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) { } else if (get(currentTool) === Tool.WAYPOINT) {
selectedWaypoint.set([marker._waypoint, this.fileId]); selectedWaypoint.set([marker._waypoint, this.fileId]);
} else { } else {
this.showWaypointPopup(marker._waypoint); waypointPopup?.setItem({ item: marker._waypoint, fileId: this.fileId });
} }
e.stopPropagation(); e.stopPropagation();
}); });
marker.on('dragstart', () => { marker.on('dragstart', () => {
setGrabbingCursor(); setGrabbingCursor();
marker.getElement().style.cursor = 'grabbing'; marker.getElement().style.cursor = 'grabbing';
this.hideWaypointPopup(); waypointPopup?.hide();
}); });
marker.on('dragend', (e) => { marker.on('dragend', (e) => {
resetCursor(); resetCursor();
marker.getElement().style.cursor = ''; marker.getElement().style.cursor = '';
getElevation([marker._waypoint]).then((ele) => {
dbUtils.applyToFile(this.fileId, (file) => { dbUtils.applyToFile(this.fileId, (file) => {
let latLng = marker.getLngLat(); let latLng = marker.getLngLat();
let wpt = file.wpt[marker._waypoint._data.index]; let wpt = file.wpt[marker._waypoint._data.index];
wpt.setCoordinates({ wpt.setCoordinates({
lat: latLng.lat, lat: latLng.lat,
lon: latLng.lng lon: latLng.lng,
}); });
wpt.ele = getElevation(this.map, wpt.getCoordinates()); wpt.ele = ele[0];
}); });
dragEndTimestamp = Date.now() });
dragEndTimestamp = Date.now();
}); });
this.markers.push(marker); this.markers.push(marker);
} }
@@ -279,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(); this.markers.pop()?.remove();
} }
@@ -294,16 +371,18 @@ export class GPXLayer {
updateMap(map: mapboxgl.Map) { updateMap(map: mapboxgl.Map) {
this.map = map; this.map = map;
this.map.on('style.load', this.updateBinded); this.map.on('style.import.load', this.updateBinded);
this.update(); this.update();
} }
remove() { remove() {
if (get(map)) { if (get(map)) {
this.map.off('click', this.fileId, this.layerOnClickBinded); this.map.off('click', this.fileId, this.layerOnClickBinded);
this.map.off('contextmenu', this.fileId, this.layerOnContextMenuBinded);
this.map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded); this.map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
this.map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded); this.map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
this.map.off('style.load', this.updateBinded); this.map.off('mousemove', this.fileId, this.layerOnMouseMoveBinded);
this.map.off('style.import.load', this.updateBinded);
if (this.map.getLayer(this.fileId + '-direction')) { if (this.map.getLayer(this.fileId + '-direction')) {
this.map.removeLayer(this.fileId + '-direction'); this.map.removeLayer(this.fileId + '-direction');
@@ -330,7 +409,10 @@ export class GPXLayer {
this.map.moveLayer(this.fileId); this.map.moveLayer(this.fileId);
} }
if (this.map.getLayer(this.fileId + '-direction')) { 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
);
} }
} }
@@ -338,7 +420,12 @@ export class GPXLayer {
let trackIndex = e.features[0].properties.trackIndex; let trackIndex = e.features[0].properties.trackIndex;
let segmentIndex = e.features[0].properties.segmentIndex; let segmentIndex = e.features[0].properties.segmentIndex;
if (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(); setScissorsCursor();
} else { } else {
setPointerCursor(); setPointerCursor();
@@ -349,16 +436,43 @@ export class GPXLayer {
resetCursor(); 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) { 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; return;
} }
let trackIndex = e.features[0].properties.trackIndex; let trackIndex = e.features[0].properties.trackIndex;
let segmentIndex = e.features[0].properties.segmentIndex; let segmentIndex = e.features[0].properties.segmentIndex;
if (get(currentTool) === Tool.SCISSORS && get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) { if (
dbUtils.split(this.fileId, trackIndex, segmentIndex, { lat: e.lngLat.lat, lon: e.lngLat.lng }); 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; return;
} }
@@ -368,8 +482,12 @@ export class GPXLayer {
} }
let item = undefined; let item = undefined;
if (get(verticalFileView) && file.getSegments().length > 1) { // Select inner item if (get(treeFileView) && file.getSegments().length > 1) {
item = file.children[trackIndex].children.length > 1 ? new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) : new ListTrackItem(this.fileId, trackIndex); // Select inner item
item =
file.children[trackIndex].children.length > 1
? new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
: new ListTrackItem(this.fileId, trackIndex);
} else { } else {
item = new ListFileItem(this.fileId); item = new ListFileItem(this.fileId);
} }
@@ -381,40 +499,9 @@ export class GPXLayer {
} }
} }
showWaypointPopup(waypoint: Waypoint) { layerOnContextMenu(e: any) {
if (get(currentPopupWaypoint) !== null) { if (e.originalEvent.ctrlKey) {
this.hideWaypointPopup(); this.layerOnClick(e);
}
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);
} }
} }
@@ -423,13 +510,14 @@ export class GPXLayer {
if (!file) { if (!file) {
return { return {
type: 'FeatureCollection', type: 'FeatureCollection',
features: [] features: [],
}; };
} }
let data = file.toGeoJSON(); let data = file.toGeoJSON();
let trackIndex = 0, segmentIndex = 0; let trackIndex = 0,
segmentIndex = 0;
for (let feature of data.features) { for (let feature of data.features) {
if (!feature.properties) { if (!feature.properties) {
feature.properties = {}; feature.properties = {};
@@ -437,14 +525,19 @@ export class GPXLayer {
if (!feature.properties.color) { if (!feature.properties.color) {
feature.properties.color = this.layerColor; feature.properties.color = this.layerColor;
} }
if (!feature.properties.weight) {
feature.properties.weight = get(defaultWeight);
}
if (!feature.properties.opacity) { if (!feature.properties.opacity) {
feature.properties.opacity = get(defaultOpacity); feature.properties.opacity = get(defaultOpacity);
} }
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)) || get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), true)) { if (!feature.properties.width) {
feature.properties.weight = feature.properties.weight + 2; 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.opacity = Math.min(1, feature.properties.opacity + 0.1);
} }
feature.properties.trackIndex = trackIndex; feature.properties.trackIndex = trackIndex;
@@ -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, []));
}
@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import { map, gpxLayers } from '$lib/stores'; import { map, gpxLayers } from '$lib/stores';
import { GPXLayer } from './GPXLayer'; import { GPXLayer } from './GPXLayer';
import WaypointPopup from './WaypointPopup.svelte';
import { fileObservers } from '$lib/db'; import { fileObservers } from '$lib/db';
import { DistanceMarkers } from './DistanceMarkers'; import { DistanceMarkers } from './DistanceMarkers';
import { StartEndMarkers } from './StartEndMarkers'; import { StartEndMarkers } from './StartEndMarkers';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { createPopups, removePopups } from './GPXLayerPopup';
let distanceMarkers: DistanceMarkers | undefined = undefined; let distanceMarkers: DistanceMarkers | undefined = undefined;
let startEndMarkers: StartEndMarkers | undefined = undefined; let startEndMarkers: StartEndMarkers | undefined = undefined;
@@ -35,6 +35,7 @@
if (startEndMarkers) { if (startEndMarkers) {
startEndMarkers.remove(); startEndMarkers.remove();
} }
createPopups($map);
distanceMarkers = new DistanceMarkers($map); distanceMarkers = new DistanceMarkers($map);
startEndMarkers = new StartEndMarkers($map); startEndMarkers = new StartEndMarkers($map);
} }
@@ -42,17 +43,14 @@
onDestroy(() => { onDestroy(() => {
gpxLayers.forEach((layer) => layer.remove()); gpxLayers.forEach((layer) => layer.remove());
gpxLayers.clear(); gpxLayers.clear();
removePopups();
if (distanceMarkers) { if (distanceMarkers) {
distanceMarkers.remove(); distanceMarkers.remove();
distanceMarkers = undefined; distanceMarkers = undefined;
} }
if (startEndMarkers) { if (startEndMarkers) {
startEndMarkers.remove(); startEndMarkers.remove();
startEndMarkers = undefined; startEndMarkers = undefined;
} }
}); });
</script> </script>
<WaypointPopup />
@@ -1,6 +1,6 @@
import { gpxStatistics, slicedGPXStatistics, currentTool, Tool } from "$lib/stores"; import { gpxStatistics, slicedGPXStatistics, currentTool, Tool } from '$lib/stores';
import mapboxgl from "mapbox-gl"; import mapboxgl from 'mapbox-gl';
import { get } from "svelte/store"; import { get } from 'svelte/store';
export class StartEndMarkers { export class StartEndMarkers {
map: mapboxgl.Map; map: mapboxgl.Map;
@@ -16,7 +16,8 @@ export class StartEndMarkers {
let endElement = document.createElement('div'); let endElement = document.createElement('div');
startElement.className = `h-4 w-4 rounded-full bg-green-500 border-2 border-white`; 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.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.start = new mapboxgl.Marker({ element: startElement });
this.end = new mapboxgl.Marker({ element: endElement }); this.end = new mapboxgl.Marker({ element: endElement });
@@ -31,7 +32,11 @@ export class StartEndMarkers {
let statistics = get(slicedGPXStatistics)?.[0] ?? get(gpxStatistics); let statistics = get(slicedGPXStatistics)?.[0] ?? get(gpxStatistics);
if (statistics.local.points.length > 0 && tool !== Tool.ROUTING) { if (statistics.local.points.length > 0 && tool !== Tool.ROUTING) {
this.start.setLngLat(statistics.local.points[0].getCoordinates()).addTo(this.map); 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 { } else {
this.start.remove(); this.start.remove();
this.end.remove(); this.end.remove();
@@ -39,7 +44,7 @@ export class StartEndMarkers {
} }
remove() { remove() {
this.unsubscribes.forEach(unsubscribe => unsubscribe()); this.unsubscribes.forEach((unsubscribe) => unsubscribe());
this.start.remove(); this.start.remove();
this.end.remove(); this.end.remove();
@@ -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>
@@ -2,54 +2,50 @@
import * as Card from '$lib/components/ui/card'; import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import Shortcut from '$lib/components/Shortcut.svelte'; 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 WithUnits from '$lib/components/WithUnits.svelte';
import { Dot, ExternalLink, Trash2 } from 'lucide-svelte'; import { Dot, ExternalLink, Trash2 } from 'lucide-svelte';
import { onMount } from 'svelte';
import { Tool, currentTool } from '$lib/stores'; import { Tool, currentTool } from '$lib/stores';
import { getSymbolKey, symbols } from '$lib/assets/symbols'; import { getSymbolKey, symbols } from '$lib/assets/symbols';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import sanitizeHtml from 'sanitize-html'; 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(() => { $: symbolKey = waypoint ? getSymbolKey(waypoint.item.sym) : undefined;
waypointPopup.setDOMContent(popupElement);
popupElement.classList.remove('hidden');
});
$: symbolKey = $currentPopupWaypoint ? getSymbolKey($currentPopupWaypoint[0].sym) : undefined;
function sanitize(text: string | undefined): string { function sanitize(text: string | undefined): string {
if (text === undefined) { if (text === undefined) {
return ''; return '';
} }
let sanitized = sanitizeHtml(text, { return sanitizeHtml(text, {
allowedTags: ['a', 'br'], allowedTags: ['a', 'br', 'img'],
allowedAttributes: { allowedAttributes: {
a: ['href', 'target'] a: ['href', 'target'],
} img: ['src'],
},
}).trim(); }).trim();
return sanitized;
} }
</script> </script>
<div bind:this={popupElement} class="hidden"> <Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw]">
{#if $currentPopupWaypoint}
<Card.Root class="border-none shadow-md text-base max-w-80 p-2">
<Card.Header class="p-0"> <Card.Header class="p-0">
<Card.Title class="text-md"> <Card.Title class="text-md">
{#if $currentPopupWaypoint[0].link && $currentPopupWaypoint[0].link.attributes && $currentPopupWaypoint[0].link.attributes.href} {#if waypoint.item.link && waypoint.item.link.attributes && waypoint.item.link.attributes.href}
<a href={$currentPopupWaypoint[0].link.attributes.href} target="_blank"> <a href={waypoint.item.link.attributes.href} target="_blank">
{$currentPopupWaypoint[0].name ?? $currentPopupWaypoint[0].link.attributes.href} {waypoint.item.name ?? waypoint.item.link.attributes.href}
<ExternalLink size="12" class="inline-block mb-1.5" /> <ExternalLink size="12" class="inline-block mb-1.5" />
</a> </a>
{:else} {:else}
{$currentPopupWaypoint[0].name ?? $_('gpx.waypoint')} {waypoint.item.name ?? $_('gpx.waypoint')}
{/if} {/if}
</Card.Title> </Card.Title>
</Card.Header> </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"> <div class="flex flex-row items-center text-muted-foreground text-xs whitespace-nowrap">
{#if symbolKey} {#if symbolKey}
<span> <span>
@@ -66,40 +62,47 @@
</span> </span>
<Dot size="16" /> <Dot size="16" />
{/if} {/if}
{$currentPopupWaypoint[0].getLatitude().toFixed(6)}&deg; {$currentPopupWaypoint[0] {waypoint.item.getLatitude().toFixed(6)}&deg; {waypoint.item
.getLongitude() .getLongitude()
.toFixed(6)}&deg; .toFixed(6)}&deg;
{#if $currentPopupWaypoint[0].ele !== undefined} {#if waypoint.item.ele !== undefined}
<Dot size="16" /> <Dot size="16" />
<WithUnits value={$currentPopupWaypoint[0].ele} type="elevation" /> <WithUnits value={waypoint.item.ele} type="elevation" />
{/if} {/if}
</div> </div>
{#if $currentPopupWaypoint[0].desc} <ScrollArea class="flex flex-col" viewportClasses="max-h-[30dvh]">
<span class="whitespace-pre-wrap">{@html sanitize($currentPopupWaypoint[0].desc)}</span> {#if waypoint.item.desc}
<span class="whitespace-pre-wrap">{@html sanitize(waypoint.item.desc)}</span>
{/if} {/if}
{#if $currentPopupWaypoint[0].cmt && $currentPopupWaypoint[0].cmt !== $currentPopupWaypoint[0].desc} {#if waypoint.item.cmt && waypoint.item.cmt !== waypoint.item.desc}
<span class="whitespace-pre-wrap">{@html sanitize($currentPopupWaypoint[0].cmt)}</span> <span class="whitespace-pre-wrap">{@html sanitize(waypoint.item.cmt)}</span>
{/if} {/if}
</ScrollArea>
<div class="mt-2 flex flex-col gap-1">
<CopyCoordinates coordinates={waypoint.item.attributes} />
{#if $currentTool === Tool.WAYPOINT} {#if $currentTool === Tool.WAYPOINT}
<Button <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" variant="outline"
on:click={() => on:click={() => deleteWaypoint(waypoint.fileId, waypoint.item._data.index)}
deleteWaypoint($currentPopupWaypoint[1], $currentPopupWaypoint[0]._data.index)}
> >
<Trash2 size="16" class="mr-1" /> <Trash2 size="16" class="mr-1" />
{$_('menu.delete')} {$_('menu.delete')}
<Shortcut key="" shift={true} click={true} /> <Shortcut shift={true} click={true} />
</Button> </Button>
{/if} {/if}
</div>
</Card.Content> </Card.Content>
</Card.Root> </Card.Root>
{/if}
</div>
<style lang="postcss"> <style lang="postcss">
div :global(a) { div :global(a) {
@apply text-blue-500 dark:text-blue-300; @apply text-link;
@apply hover:underline; @apply hover:underline;
} }
div :global(img) {
@apply my-0;
@apply rounded-md;
}
</style> </style>
@@ -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, []));
}
@@ -15,14 +15,15 @@
Trash2, Trash2,
Move, Move,
Map, Map,
Layers2 Layers2,
} from 'lucide-svelte'; } from 'lucide-svelte';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import { settings } from '$lib/db'; import { settings } from '$lib/db';
import { defaultBasemap, extendBasemap, type CustomLayer } from '$lib/assets/layers'; import { defaultBasemap, type CustomLayer } from '$lib/assets/layers';
import { map } from '$lib/stores'; import { map } from '$lib/stores';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import Sortable from 'sortablejs/Sortable'; import Sortable from 'sortablejs/Sortable';
import { customBasemapUpdate } from './utils';
const { const {
customLayers, customLayers,
@@ -33,7 +34,7 @@
currentOverlays, currentOverlays,
previousOverlays, previousOverlays,
customBasemapOrder, customBasemapOrder,
customOverlayOrder customOverlayOrder,
} = settings; } = settings;
let name: string = ''; let name: string = '';
@@ -67,7 +68,7 @@
acc[id] = true; acc[id] = true;
return acc; return acc;
}, {}); }, {});
} },
}); });
overlaySortable = Sortable.create(overlayContainer, { overlaySortable = Sortable.create(overlayContainer, {
onSort: (e) => { onSort: (e) => {
@@ -76,7 +77,7 @@
acc[id] = true; acc[id] = true;
return acc; return acc;
}, {}); }, {});
} },
}); });
basemapSortable.sort($customBasemapOrder); basemapSortable.sort($customBasemapOrder);
@@ -94,7 +95,6 @@
(tileUrls[0].includes('api.mapbox.com/styles') && !tileUrls[0].includes('tiles')) (tileUrls[0].includes('api.mapbox.com/styles') && !tileUrls[0].includes('tiles'))
) { ) {
resourceType = 'vector'; resourceType = 'vector';
layerType = 'basemap';
} else { } else {
resourceType = 'raster'; resourceType = 'raster';
} }
@@ -108,47 +108,41 @@
if (typeof maxZoom === 'string') { if (typeof maxZoom === 'string') {
maxZoom = parseInt(maxZoom); maxZoom = parseInt(maxZoom);
} }
let is512 = tileUrls.some((url) => url.includes('512'));
let layerId = selectedLayerId ?? getLayerId(); let layerId = selectedLayerId ?? getLayerId();
let layer: CustomLayer = { let layer: CustomLayer = {
id: layerId, id: layerId,
name: name, name: name,
tileUrls: tileUrls, tileUrls: tileUrls.map((url) => decodeURI(url.trim())),
maxZoom: maxZoom, maxZoom: maxZoom,
layerType: layerType, layerType: layerType,
resourceType: resourceType, resourceType: resourceType,
value: '' value: '',
}; };
if (resourceType === 'vector') { if (resourceType === 'vector') {
layer.value = tileUrls[0]; layer.value = layer.tileUrls[0];
} else { } else {
if (layerType === 'basemap') { layer.value = {
layer.value = extendBasemap({
version: 8, version: 8,
sources: { sources: {
[layerId]: { [layerId]: {
type: 'raster', type: 'raster',
tiles: tileUrls, tiles: layer.tileUrls,
maxzoom: maxZoom tileSize: is512 ? 512 : 256,
} maxzoom: maxZoom,
},
}, },
layers: [ layers: [
{ {
id: layerId, id: layerId,
type: 'raster', type: 'raster',
source: layerId source: layerId,
} },
] ],
});
} else {
layer.value = {
type: 'raster',
tiles: tileUrls,
maxzoom: maxZoom
}; };
} }
}
$customLayers[layerId] = layer; $customLayers[layerId] = layer;
addLayer(layerId); addLayer(layerId);
selectedLayerId = undefined; selectedLayerId = undefined;
@@ -173,7 +167,11 @@
return $tree; return $tree;
}); });
if ($currentBasemap === layerId) {
$customBasemapUpdate++;
} else {
$currentBasemap = layerId; $currentBasemap = layerId;
}
if (!$customBasemapOrder.includes(layerId)) { if (!$customBasemapOrder.includes(layerId)) {
$customBasemapOrder = [...$customBasemapOrder, layerId]; $customBasemapOrder = [...$customBasemapOrder, layerId];
@@ -187,12 +185,16 @@
return $tree; return $tree;
}); });
if ($map && $map.getSource(layerId)) { if (
// Reset source when updating an existing layer $currentOverlays.overlays['custom'] &&
if ($map.getLayer(layerId)) { $currentOverlays.overlays['custom'][layerId] &&
$map.removeLayer(layerId); $map
) {
try {
$map.removeImport(layerId);
} catch (e) {
// No reliable way to check if the map is ready to remove sources and layers
} }
$map.removeSource(layerId);
} }
if (!$currentOverlays.overlays.hasOwnProperty('custom')) { if (!$currentOverlays.overlays.hasOwnProperty('custom')) {
@@ -228,7 +230,10 @@
layerId layerId
); );
if (Object.keys($selectedBasemapTree.basemaps['custom']).length === 0) { if (Object.keys($selectedBasemapTree.basemaps['custom']).length === 0) {
$selectedBasemapTree.basemaps = tryDeleteLayer($selectedBasemapTree.basemaps, 'custom'); $selectedBasemapTree.basemaps = tryDeleteLayer(
$selectedBasemapTree.basemaps,
'custom'
);
} }
$customBasemapOrder = $customBasemapOrder.filter((id) => id !== layerId); $customBasemapOrder = $customBasemapOrder.filter((id) => id !== layerId);
} else { } else {
@@ -245,16 +250,22 @@
layerId layerId
); );
if (Object.keys($selectedOverlayTree.overlays['custom']).length === 0) { if (Object.keys($selectedOverlayTree.overlays['custom']).length === 0) {
$selectedOverlayTree.overlays = tryDeleteLayer($selectedOverlayTree.overlays, 'custom'); $selectedOverlayTree.overlays = tryDeleteLayer(
$selectedOverlayTree.overlays,
'custom'
);
} }
$customOverlayOrder = $customOverlayOrder.filter((id) => id !== layerId); $customOverlayOrder = $customOverlayOrder.filter((id) => id !== layerId);
if ($map) { if (
if ($map.getLayer(layerId)) { $currentOverlays.overlays['custom'] &&
$map.removeLayer(layerId); $currentOverlays.overlays['custom'][layerId] &&
} $map
if ($map.getSource(layerId)) { ) {
$map.removeSource(layerId); try {
$map.removeImport(layerId);
} catch (e) {
// No reliable way to check if the map is ready to remove sources and layers
} }
} }
} }
@@ -362,7 +373,8 @@
/> />
{#if tileUrls.length > 1} {#if tileUrls.length > 1}
<Button <Button
on:click={() => (tileUrls = tileUrls.filter((_, index) => index !== i))} on:click={() =>
(tileUrls = tileUrls.filter((_, index) => index !== i))}
variant="outline" variant="outline"
class="p-1 h-8" class="p-1 h-8"
> >
@@ -382,7 +394,14 @@
{/each} {/each}
{#if resourceType === 'raster'} {#if resourceType === 'raster'}
<Label for="maxZoom">{$_('layers.custom_layers.max_zoom')}</Label> <Label for="maxZoom">{$_('layers.custom_layers.max_zoom')}</Label>
<Input type="number" bind:value={maxZoom} id="maxZoom" min={0} max={22} class="h-8" /> <Input
type="number"
bind:value={maxZoom}
id="maxZoom"
min={0}
max={22}
class="h-8"
/>
{/if} {/if}
<Label>{$_('layers.custom_layers.layer_type')}</Label> <Label>{$_('layers.custom_layers.layer_type')}</Label>
<RadioGroup.Root bind:value={layerType} class="flex flex-row"> <RadioGroup.Root bind:value={layerType} class="flex flex-row">
@@ -391,7 +410,7 @@
<Label for="basemap">{$_('layers.custom_layers.basemap')}</Label> <Label for="basemap">{$_('layers.custom_layers.basemap')}</Label>
</div> </div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<RadioGroup.Item value="overlay" id="overlay" disabled={resourceType === 'vector'} /> <RadioGroup.Item value="overlay" id="overlay" />
<Label for="overlay">{$_('layers.custom_layers.overlay')}</Label> <Label for="overlay">{$_('layers.custom_layers.overlay')}</Label>
</div> </div>
</RadioGroup.Root> </RadioGroup.Root>
@@ -11,9 +11,8 @@
import { settings } from '$lib/db'; import { settings } from '$lib/db';
import { map } from '$lib/stores'; import { map } from '$lib/stores';
import { get, writable } from 'svelte/store'; import { get, writable } from 'svelte/store';
import { getLayers } from './utils'; import { customBasemapUpdate, getLayers } from './utils';
import { OverpassLayer } from './OverpassLayer'; import { OverpassLayer } from './OverpassLayer';
import OverpassPopup from './OverpassPopup.svelte';
let container: HTMLDivElement; let container: HTMLDivElement;
let overpassLayer: OverpassLayer; let overpassLayer: OverpassLayer;
@@ -27,42 +26,92 @@
selectedOverlayTree, selectedOverlayTree,
selectedOverpassTree, selectedOverpassTree,
customLayers, customLayers,
opacities opacities,
} = settings; } = settings;
function setStyle() { function setStyle() {
if ($map) { if ($map) {
let basemap = basemaps.hasOwnProperty($currentBasemap) let basemap = basemaps.hasOwnProperty($currentBasemap)
? basemaps[$currentBasemap] ? basemaps[$currentBasemap]
: $customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap]; : ($customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap]);
$map.setStyle(basemap, { $map.removeImport('basemap');
diff: false if (typeof basemap === 'string') {
}); $map.addImport({ id: 'basemap', url: basemap }, 'overlays');
} else {
$map.addImport(
{
id: 'basemap',
data: basemap,
},
'overlays'
);
}
} }
} }
$: if ($map && $currentBasemap) { $: if ($map && ($currentBasemap || $customBasemapUpdate)) {
setStyle(); setStyle();
} }
$: if ($map && $currentOverlays) { function addOverlay(id: string) {
// Add or remove overlay layers depending on the current overlays try {
let overlayLayers = getLayers($currentOverlays); let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id];
Object.keys(overlayLayers).forEach((id) => { if (typeof overlay === 'string') {
if (overlayLayers[id]) { $map.addImport({ id, url: overlay });
if (!addOverlayLayer.hasOwnProperty(id)) { } else {
addOverlayLayer[id] = addOverlayLayerForId(id); if ($opacities.hasOwnProperty(id)) {
overlay = {
...overlay,
layers: overlay.layers.map((layer) => {
if (layer.type === 'raster') {
if (!layer.paint) {
layer.paint = {};
} }
if (!$map.getLayer(id)) { layer.paint['raster-opacity'] = $opacities[id];
addOverlayLayer[id]();
$map.on('style.load', addOverlayLayer[id]);
} }
} else if ($map.getLayer(id)) { return layer;
$map.removeLayer(id); }),
$map.off('style.load', addOverlayLayer[id]); };
} }
$map.addImport({
id,
data: overlay,
}); });
} }
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
}
}
function updateOverlays() {
if ($map && $currentOverlays && $opacities) {
let overlayLayers = getLayers($currentOverlays);
try {
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.hasOwnProperty(id))
.map(([id]) => id);
toAdd.forEach((id) => {
addOverlay(id);
});
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
}
}
}
$: if ($map && $currentOverlays && $opacities) {
updateOverlays();
}
$: if ($map) { $: if ($map) {
if (overpassLayer) { if (overpassLayer) {
@@ -70,6 +119,7 @@
} }
overpassLayer = new OverpassLayer($map); overpassLayer = new OverpassLayer($map);
overpassLayer.add(); overpassLayer.add();
$map.on('style.import.load', updateOverlays);
} }
let selectedBasemap = writable(get(currentBasemap)); let selectedBasemap = writable(get(currentBasemap));
@@ -82,40 +132,11 @@
}); });
currentBasemap.subscribe((value) => { currentBasemap.subscribe((value) => {
// Updates coming from the database, or from the user swapping basemaps // Updates coming from the database, or from the user swapping basemaps
if (value !== get(selectedBasemap)) {
selectedBasemap.set(value); selectedBasemap.set(value);
}
}); });
let addOverlayLayer: { [key: string]: () => void } = {};
function addOverlayLayerForId(id: string) {
return () => {
if ($map) {
try {
let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id];
if (!$map.getSource(id)) {
$map.addSource(id, overlay);
}
$map.addLayer(
{
id,
type: overlay.type === 'raster' ? 'raster' : 'line',
source: id,
paint: {
...(id in $opacities
? overlay.type === 'raster'
? { 'raster-opacity': $opacities[id] }
: { 'line-opacity': $opacities[id] }
: {})
}
},
'overlays'
);
} catch (e) {
// No reliable way to check if the map is ready to add sources and layers
}
}
};
}
let open = false; let open = false;
function openLayerControl() { function openLayerControl() {
open = true; open = true;
@@ -192,8 +213,6 @@
</div> </div>
</CustomControl> </CustomControl>
<OverpassPopup />
<svelte:window <svelte:window
on:click={(e) => { on:click={(e) => {
if (open && !cancelEvents && !container.contains(e.target)) { if (open && !cancelEvents && !container.contains(e.target)) {
@@ -9,8 +9,14 @@
import * as Select from '$lib/components/ui/select'; import * as Select from '$lib/components/ui/select';
import { Slider } from '$lib/components/ui/slider'; import { Slider } from '$lib/components/ui/slider';
import { basemapTree, overlays, overlayTree, overpassTree } from '$lib/assets/layers'; import {
import { isSelected } from '$lib/components/layer-control/utils'; basemapTree,
defaultBasemap,
overlays,
overlayTree,
overpassTree,
} from '$lib/assets/layers';
import { getLayers, isSelected, toggle } from '$lib/components/layer-control/utils';
import { settings } from '$lib/db'; import { settings } from '$lib/db';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
@@ -22,9 +28,10 @@
selectedBasemapTree, selectedBasemapTree,
selectedOverlayTree, selectedOverlayTree,
selectedOverpassTree, selectedOverpassTree,
currentBasemap,
currentOverlays, currentOverlays,
customLayers, customLayers,
opacities opacities,
} = settings; } = settings;
export let open: boolean; export let open: boolean;
@@ -46,6 +53,30 @@
} }
} }
$: if ($selectedBasemapTree && $currentBasemap) {
if (!isSelected($selectedBasemapTree, $currentBasemap)) {
if (!isSelected($selectedBasemapTree, defaultBasemap)) {
$selectedBasemapTree = toggle($selectedBasemapTree, defaultBasemap);
}
$currentBasemap = defaultBasemap;
}
}
$: if ($selectedOverlayTree && $currentOverlays) {
let overlayLayers = getLayers($currentOverlays);
let toRemove = Object.entries(overlayLayers).filter(
([id, checked]) => checked && !isSelected($selectedOverlayTree, id)
);
if (toRemove.length > 0) {
currentOverlays.update((tree) => {
toRemove.forEach(([id]) => {
toggle(tree, id);
});
return tree;
});
}
}
$: if ($selectedOverlay) { $: if ($selectedOverlay) {
setOpacityFromSelection(); setOpacityFromSelection();
} }
@@ -106,7 +137,9 @@
<Select.Content class="h-fit max-h-[40dvh] overflow-y-auto"> <Select.Content class="h-fit max-h-[40dvh] overflow-y-auto">
{#each Object.keys(overlays) as id} {#each Object.keys(overlays) as id}
{#if isSelected($selectedOverlayTree, id)} {#if isSelected($selectedOverlayTree, id)}
<Select.Item value={id}>{$_(`layers.label.${id}`)}</Select.Item> <Select.Item value={id}
>{$_(`layers.label.${id}`)}</Select.Item
>
{/if} {/if}
{/each} {/each}
{#each Object.entries($customLayers) as [id, layer]} {#each Object.entries($customLayers) as [id, layer]}
@@ -126,15 +159,22 @@
max={1} max={1}
step={0.1} step={0.1}
disabled={$selectedOverlay === undefined} disabled={$selectedOverlay === undefined}
onValueChange={() => { onValueChange={(value) => {
if ($selectedOverlay) { if ($selectedOverlay) {
$opacities[$selectedOverlay.value] = $overlayOpacity[0]; if (
if ($map) { $map &&
if ($map.getLayer($selectedOverlay.value)) { isSelected(
$map.removeLayer($selectedOverlay.value); $currentOverlays,
$currentOverlays = $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];
} }
}} }}
/> />
@@ -46,9 +46,16 @@
value={id} value={id}
bind:checked={checked[id]} bind:checked={checked[id]}
class="scale-90" class="scale-90"
aria-label={$_(`layers.label.${id}`)}
/> />
{:else} {: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} {/if}
<Label for="{name}-{id}" class="flex flex-row items-center gap-1"> <Label for="{name}-{id}" class="flex flex-row items-center gap-1">
{#if $customLayers.hasOwnProperty(id)} {#if $customLayers.hasOwnProperty(id)}
@@ -63,7 +70,13 @@
<CollapsibleTreeNode {id}> <CollapsibleTreeNode {id}>
<span slot="trigger">{$_(`layers.label.${id}`)}</span> <span slot="trigger">{$_(`layers.label.${id}`)}</span>
<div slot="content"> <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> </div>
</CollapsibleTreeNode> </CollapsibleTreeNode>
{/if} {/if}
@@ -1,27 +1,17 @@
import SphericalMercator from "@mapbox/sphericalmercator"; import SphericalMercator from '@mapbox/sphericalmercator';
import { getLayers } from "./utils"; import { getLayers } from './utils';
import mapboxgl from "mapbox-gl"; import { get, writable } from 'svelte/store';
import { get, writable } from "svelte/store"; import { liveQuery } from 'dexie';
import { liveQuery } from "dexie"; import { db, settings } from '$lib/db';
import { db, settings } from "$lib/db"; import { overpassQueryData } from '$lib/assets/layers';
import { overpassQueryData } from "$lib/assets/layers"; import { MapPopup } from '$lib/components/MapPopup';
const { const { currentOverpassQueries } = settings;
currentOverpassQueries
} = settings;
const mercator = new SphericalMercator({ const mercator = new SphericalMercator({
size: 256, 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: [] }); let data = writable<GeoJSON.FeatureCollection>({ type: 'FeatureCollection', features: [] });
liveQuery(() => db.overpassdata.toArray()).subscribe((pois) => { liveQuery(() => db.overpassdata.toArray()).subscribe((pois) => {
@@ -34,28 +24,36 @@ export class OverpassLayer {
queryZoom = 12; queryZoom = 12;
expirationTime = 7 * 24 * 3600 * 1000; expirationTime = 7 * 24 * 3600 * 1000;
map: mapboxgl.Map; map: mapboxgl.Map;
popup: MapPopup;
currentQueries: Set<string> = new Set(); 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)[] = []; unsubscribes: (() => void)[] = [];
queryIfNeededBinded = this.queryIfNeeded.bind(this); queryIfNeededBinded = this.queryIfNeeded.bind(this);
updateBinded = this.update.bind(this); updateBinded = this.update.bind(this);
onHoverBinded = this.onHover.bind(this); onHoverBinded = this.onHover.bind(this);
maybeHidePopupBinded = this.maybeHidePopup.bind(this);
constructor(map: mapboxgl.Map) { constructor(map: mapboxgl.Map) {
this.map = map; this.map = map;
this.popup = new MapPopup(map, {
closeButton: false,
focusAfterOpen: false,
maxWidth: undefined,
offset: 15,
});
} }
add() { add() {
this.map.on('moveend', this.queryIfNeededBinded); this.map.on('moveend', this.queryIfNeededBinded);
this.map.on('style.load', this.updateBinded); this.map.on('style.import.load', this.updateBinded);
this.unsubscribes.push(data.subscribe(this.updateBinded)); this.unsubscribes.push(data.subscribe(this.updateBinded));
this.unsubscribes.push(currentOverpassQueries.subscribe(() => { this.unsubscribes.push(
currentOverpassQueries.subscribe(() => {
this.updateBinded(); this.updateBinded();
this.queryIfNeededBinded(); this.queryIfNeededBinded();
})); })
);
this.update(); this.update();
} }
@@ -108,9 +106,10 @@ export class OverpassLayer {
remove() { remove() {
this.map.off('moveend', this.queryIfNeededBinded); this.map.off('moveend', this.queryIfNeededBinded);
this.map.off('style.load', this.updateBinded); this.map.off('style.import.load', this.updateBinded);
this.unsubscribes.forEach((unsubscribe) => unsubscribe()); this.unsubscribes.forEach((unsubscribe) => unsubscribe());
try {
if (this.map.getLayer('overpass')) { if (this.map.getLayer('overpass')) {
this.map.removeLayer('overpass'); this.map.removeLayer('overpass');
} }
@@ -118,30 +117,18 @@ export class OverpassLayer {
if (this.map.getSource('overpass')) { if (this.map.getSource('overpass')) {
this.map.removeSource('overpass'); this.map.removeSource('overpass');
} }
} catch (e) {
// No reliable way to check if the map is ready to remove sources and layers
}
} }
onHover(e: any) { onHover(e: any) {
overpassPopupPOI.set({ this.popup.setItem({
item: {
...e.features[0].properties, ...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]) { query(bbox: [number, number, number, number]) {
@@ -159,8 +146,19 @@ export class OverpassLayer {
continue; continue;
} }
db.overpasstiles.where('[x+y]').equals([x, y]).toArray().then((querytiles) => { db.overpasstiles
let missingQueries = queries.filter((query) => !querytiles.some((querytile) => querytile.query === query && time - querytile.time < this.expirationTime)); .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) { if (missingQueries.length > 0) {
this.queryTile(x, y, missingQueries); this.queryTile(x, y, missingQueries);
} }
@@ -178,13 +176,16 @@ export class OverpassLayer {
const bounds = mercator.bbox(x, y, this.queryZoom); const bounds = mercator.bbox(x, y, this.queryZoom);
fetch(`${this.overpassUrl}?data=${getQueryForBounds(bounds, queries)}`) fetch(`${this.overpassUrl}?data=${getQueryForBounds(bounds, queries)}`)
.then((response) => { .then(
(response) => {
if (response.ok) { if (response.ok) {
return response.json(); return response.json();
} }
this.currentQueries.delete(`${x},${y}`); this.currentQueries.delete(`${x},${y}`);
return Promise.reject(); return Promise.reject();
}, () => (this.currentQueries.delete(`${x},${y}`))) },
() => this.currentQueries.delete(`${x},${y}`)
)
.then((data) => this.storeOverpassData(x, y, queries, data)) .then((data) => this.storeOverpassData(x, y, queries, data))
.catch(() => this.currentQueries.delete(`${x},${y}`)); .catch(() => this.currentQueries.delete(`${x},${y}`));
} }
@@ -192,7 +193,7 @@ export class OverpassLayer {
storeOverpassData(x: number, y: number, queries: string[], data: any) { storeOverpassData(x: number, y: number, queries: string[], data: any) {
let time = Date.now(); let time = Date.now();
let queryTiles = queries.map((query) => ({ x, y, query, time })); 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) { if (data.elements === undefined) {
return; return;
@@ -208,7 +209,9 @@ export class OverpassLayer {
type: 'Feature', type: 'Feature',
geometry: { geometry: {
type: 'Point', 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: { properties: {
id: element.id, id: element.id,
@@ -216,9 +219,10 @@ export class OverpassLayer {
lon: element.center ? element.center.lon : element.lon, lon: element.center ? element.center.lon : element.lon,
query: query, query: query,
icon: `overpass-${query}`, icon: `overpass-${query}`,
tags: element.tags tags: element.tags,
type: element.type,
},
}, },
}
}); });
} }
} }
@@ -241,11 +245,13 @@ export class OverpassLayer {
if (!this.map.hasImage(`overpass-${query}`)) { if (!this.map.hasImage(`overpass-${query}`)) {
this.map.addImage(`overpass-${query}`, icon); this.map.addImage(`overpass-${query}`, icon);
} }
} };
// Lucide icons are SVG files with a 24x24 viewBox // Lucide icons are SVG files with a 24x24 viewBox
// Create a new SVG with a 32x32 viewBox and center the icon in a circle // 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"> <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}" /> <circle cx="20" cy="20" r="20" fill="${overpassQueryData[query].icon.color}" />
<g transform="translate(8 8)"> <g transform="translate(8 8)">
@@ -277,9 +283,14 @@ function getQuery(query: string) {
function getQueryItem(tags: Record<string, string | boolean | string[]>) { function getQueryItem(tags: Record<string, string | boolean | string[]>) {
let arrayEntry = Object.entries(tags).find(([_, value]) => Array.isArray(value)); let arrayEntry = Object.entries(tags).find(([_, value]) => Array.isArray(value));
if (arrayEntry !== undefined) { 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}]`) .map(([tag, value]) => `[${tag}=${tag === arrayEntry[0] ? val : value}]`)
.join('')};`).join(''); .join('')};`
)
.join('');
} else { } else {
return `nwr${Object.entries(tags) return `nwr${Object.entries(tags)
.map(([tag, value]) => `[${tag}=${value}]`) .map(([tag, value]) => `[${tag}=${value}]`)
@@ -296,8 +307,9 @@ function belongsToQuery(element: any, query: string) {
} }
function belongsToQueryItem(element: any, tags: Record<string, string | boolean | string[]>) { function belongsToQueryItem(element: any, tags: Record<string, string | boolean | string[]>) {
return Object.entries(tags) return Object.entries(tags).every(([tag, value]) =>
.every(([tag, value]) => Array.isArray(value) ? value.includes(element.tags[tag]) : element.tags[tag] === value); Array.isArray(value) ? value.includes(element.tags[tag]) : element.tags[tag] === value
);
} }
function getCurrentQueries() { function getCurrentQueries() {
@@ -306,5 +318,7 @@ function getCurrentQueries() {
return []; return [];
} }
return Object.entries(getLayers(currentQueries)).filter(([_, selected]) => selected).map(([query, _]) => query); return Object.entries(getLayers(currentQueries))
.filter(([_, selected]) => selected)
.map(([query, _]) => query);
} }
@@ -1,48 +1,67 @@
<script lang="ts"> <script lang="ts">
import * as Card from '$lib/components/ui/card'; import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { overpassPopup, overpassPopupPOI } from './OverpassLayer';
import { selection } from '$lib/components/file-list/Selection'; import { selection } from '$lib/components/file-list/Selection';
import { PencilLine, MapPin } from 'lucide-svelte'; import { PencilLine, MapPin } from 'lucide-svelte';
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
import { dbUtils } from '$lib/db'; 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(() => { let tags: { [key: string]: string } = {};
overpassPopup.setDOMContent(popupElement);
popupElement.classList.remove('hidden');
});
let tags = {};
let name = ''; let name = '';
$: if ($overpassPopupPOI) { $: if (poi) {
tags = JSON.parse($overpassPopupPOI.tags); tags = JSON.parse(poi.item.tags);
if (tags.name !== undefined && tags.name !== '') { if (tags.name !== undefined && tags.name !== '') {
name = tags.name; name = tags.name;
} else { } 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> </script>
<div bind:this={popupElement} class="hidden"> <Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw]">
{#if $overpassPopupPOI}
<Card.Root class="border-none shadow-md text-base p-2 max-w-[50dvw]">
<Card.Header class="p-0"> <Card.Header class="p-0">
<Card.Title class="text-md"> <Card.Title class="text-md">
<div class="flex flex-row gap-3"> <div class="flex flex-row gap-3">
<div class="flex flex-col"> <div class="flex flex-col">
{name} {name}
<div class="text-muted-foreground text-sm font-normal"> <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>
</div> </div>
<Button <Button
class="ml-auto p-1.5 h-8" class="ml-auto p-1.5 h-8"
variant="outline" 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" target="_blank"
> >
<PencilLine size="16" /> <PencilLine size="16" />
@@ -50,53 +69,39 @@
</div> </div>
</Card.Title> </Card.Title>
</Card.Header> </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']} {#if tags.image || tags['image:0']}
<div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto"> <div class="w-full rounded-md overflow-clip my-2 max-w-96 mx-auto">
<!-- svelte-ignore a11y-missing-attribute --> <!-- svelte-ignore a11y-missing-attribute -->
<img src={tags.image ?? tags['image:0']} /> <img src={tags.image ?? tags['image:0']} />
</div> </div>
{/if} {/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"> <div class="grid grid-cols-[auto_auto] gap-x-3">
{#each Object.entries(tags) as [key, value]} {#each Object.entries(tags) as [key, value]}
{#if key !== 'name' && !key.includes('image')} {#if key !== 'name' && !key.includes('image')}
<span class="font-mono">{key}</span> <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-blue-500 underline">{value}</a> <a href={value} target="_blank" class="text-link underline">{value}</a>
{:else if key === 'phone' || key === 'contact:phone'} {:else if key === 'phone' || key === 'contact:phone'}
<a href={'tel:' + value} class="text-blue-500 underline">{value}</a> <a href={'tel:' + value} class="text-link underline">{value}</a>
{:else if key === 'email' || key === 'contact:email'} {:else if key === 'email' || key === 'contact:email'}
<a href={'mailto:' + value} class="text-blue-500 underline">{value}</a> <a href={'mailto:' + value} class="text-link underline">{value}</a>
{:else} {:else}
<span>{value}</span> <span>{value}</span>
{/if} {/if}
{/if} {/if}
{/each} {/each}
</div> </div>
</ScrollArea>
<Button <Button
class="mt-2" class="mt-2"
variant="outline" variant="outline"
disabled={$selection.size === 0} disabled={$selection.size === 0}
on:click={() => { on:click={addToFile}
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
});
}}
> >
<MapPin size="16" class="mr-1" /> <MapPin size="16" class="mr-1" />
{$_('toolbar.waypoint.add')} {$_('toolbar.waypoint.add')}
</Button> </Button>
</Card.Content> </Card.Content>
</Card.Root> </Card.Root>
{/if}
</div>
@@ -1,8 +1,10 @@
import type { LayerTreeType } from "$lib/assets/layers"; import type { LayerTreeType } from '$lib/assets/layers';
import { writable } from 'svelte/store';
export function anySelectedLayer(node: LayerTreeType) { export function anySelectedLayer(node: LayerTreeType) {
return Object.keys(node).find((id) => { return (
if (typeof node[id] == "boolean") { Object.keys(node).find((id) => {
if (typeof node[id] == 'boolean') {
if (node[id]) { if (node[id]) {
return true; return true;
} }
@@ -12,12 +14,16 @@ export function anySelectedLayer(node: LayerTreeType) {
} }
} }
return false; 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) => { Object.keys(node).forEach((id) => {
if (typeof node[id] == "boolean") { if (typeof node[id] == 'boolean') {
layers[id] = node[id]; layers[id] = node[id];
} else { } else {
getLayers(node[id], layers); getLayers(node[id], layers);
@@ -31,9 +37,22 @@ export function isSelected(node: LayerTreeType, id: string) {
if (key === id) { if (key === id) {
return node[key]; return node[key];
} }
if (typeof node[key] !== "boolean" && isSelected(node[key], id)) { if (typeof node[key] !== 'boolean' && isSelected(node[key], id)) {
return true; return true;
} }
return false; return false;
}); });
} }
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') {
toggle(node[key], id);
}
});
return node;
}
export const customBasemapUpdate = writable(0);
@@ -1,5 +1,5 @@
import { resetCursor, setCrosshairCursor } from "$lib/utils"; import { resetCursor, setCrosshairCursor } from '$lib/utils';
import type mapboxgl from "mapbox-gl"; import type mapboxgl from 'mapbox-gl';
export class GoogleRedirect { export class GoogleRedirect {
map: mapboxgl.Map; map: mapboxgl.Map;
@@ -1,16 +1,19 @@
import mapboxgl from "mapbox-gl"; import mapboxgl, { type LayerSpecification, type VectorSourceSpecification } from 'mapbox-gl';
import { Viewer } from 'mapillary-js/dist/mapillary.module'; import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
import 'mapillary-js/dist/mapillary.css'; import 'mapillary-js/dist/mapillary.css';
import { resetCursor, setPointerCursor } from "$lib/utils"; import { resetCursor, setPointerCursor } from '$lib/utils';
import type { Writable } from 'svelte/store';
const mapillarySource = { const mapillarySource: VectorSourceSpecification = {
type: 'vector', 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, minzoom: 6,
maxzoom: 14, maxzoom: 14,
}; };
const mapillarySequenceLayer = { const mapillarySequenceLayer: LayerSpecification = {
id: 'mapillary-sequence', id: 'mapillary-sequence',
type: 'line', type: 'line',
source: 'mapillary', source: 'mapillary',
@@ -26,7 +29,7 @@ const mapillarySequenceLayer = {
}, },
}; };
const mapillaryImageLayer = { const mapillaryImageLayer: LayerSpecification = {
id: 'mapillary-image', id: 'mapillary-image',
type: 'circle', type: 'circle',
source: 'mapillary', source: 'mapillary',
@@ -40,35 +43,56 @@ const mapillaryImageLayer = {
export class MapillaryLayer { export class MapillaryLayer {
map: mapboxgl.Map; map: mapboxgl.Map;
popup: mapboxgl.Popup; marker: mapboxgl.Marker;
viewer: Viewer; viewer: Viewer;
active = false;
popupOpen: Writable<boolean>;
addBinded = this.add.bind(this); addBinded = this.add.bind(this);
onMouseEnterBinded = this.onMouseEnter.bind(this); onMouseEnterBinded = this.onMouseEnter.bind(this);
onMouseLeaveBinded = this.onMouseLeave.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.map = map;
this.viewer = new Viewer({ this.viewer = new Viewer({
accessToken: 'MLY|4381405525255083|3204871ec181638c3c31320490f03011', accessToken: 'MLY|4381405525255083|3204871ec181638c3c31320490f03011',
container, container,
}); });
container.classList.remove('hidden');
this.popup = new mapboxgl.Popup({ const element = document.createElement('div');
closeButton: false, element.className = 'mapboxgl-user-location mapboxgl-user-location-show-heading';
maxWidth: container.style.width, const dot = document.createElement('div');
}).setDOMContent(container); 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 () => { this.viewer.on('position', async () => {
if (this.popup.isOpen()) { if (this.active) {
popupOpen.set(true);
let latLng = await this.viewer.getPosition(); let latLng = await this.viewer.getPosition();
this.popup.setLngLat(latLng); this.marker.setLngLat(latLng).addTo(this.map);
if (!this.map.getBounds().contains(latLng)) { if (!this.map.getBounds()?.contains(latLng)) {
this.map.panTo(latLng); this.map.panTo(latLng);
} }
} }
}); });
this.viewer.on('bearing', (e: ViewerBearingEvent) => {
if (this.active) {
this.marker.setRotation(e.bearing);
}
});
this.popupOpen = popupOpen;
} }
add() { add() {
@@ -101,15 +125,19 @@ export class MapillaryLayer {
this.map.removeSource('mapillary'); this.map.removeSource('mapillary');
} }
this.popup.remove(); this.marker.remove();
this.popupOpen.set(false);
} }
closePopup() { closePopup() {
this.popup.remove(); this.active = false;
this.marker.remove();
this.popupOpen.set(false);
} }
onMouseEnter(e: mapboxgl.MapLayerMouseEvent) { onMouseEnter(e: mapboxgl.MapMouseEvent) {
this.popup.addTo(this.map).setLngLat(e.lngLat); this.active = true;
this.viewer.resize(); this.viewer.resize();
this.viewer.moveTo(e.features[0].properties.id); this.viewer.moveTo(e.features[0].properties.id);
@@ -1,21 +1,25 @@
<script lang="ts"> <script lang="ts">
import CustomControl from '$lib/components/custom-control/CustomControl.svelte'; import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
import { Toggle } from '$lib/components/ui/toggle'; import { Toggle } from '$lib/components/ui/toggle';
import { PersonStanding, X } from 'lucide-svelte'; import { PersonStanding, X } from 'lucide-svelte';
import { MapillaryLayer } from './Mapillary'; import { MapillaryLayer } from './Mapillary';
import { GoogleRedirect } from './Google'; import { GoogleRedirect } from './Google';
import { map, streetViewEnabled } from '$lib/stores'; import { map, streetViewEnabled } from '$lib/stores';
import { settings } from '$lib/db'; import { settings } from '$lib/db';
import { _ } from 'svelte-i18n';
import { writable } from 'svelte/store';
const { streetViewSource } = settings; const { streetViewSource } = settings;
let googleRedirect: GoogleRedirect; let googleRedirect: GoogleRedirect;
let mapillaryLayer: MapillaryLayer; let mapillaryLayer: MapillaryLayer;
let mapillaryOpen = writable(false);
let container: HTMLElement; let container: HTMLElement;
$: if ($map) { $: if ($map) {
googleRedirect = new GoogleRedirect($map); googleRedirect = new GoogleRedirect($map);
mapillaryLayer = new MapillaryLayer($map, container); mapillaryLayer = new MapillaryLayer($map, container, mapillaryOpen);
} }
$: if (mapillaryLayer) { $: if (mapillaryLayer) {
@@ -38,14 +42,22 @@
</script> </script>
<CustomControl class="w-[29px] h-[29px] shrink-0"> <CustomControl class="w-[29px] h-[29px] shrink-0">
<Toggle bind:pressed={$streetViewEnabled} class="w-full h-full rounded p-0"> <Tooltip class="w-full h-full" side="left" label={$_('menu.toggle_street_view')}>
<Toggle
bind:pressed={$streetViewEnabled}
class="w-full h-full rounded p-0"
aria-label={$_('menu.toggle_street_view')}
>
<PersonStanding size="22" /> <PersonStanding size="22" />
</Toggle> </Toggle>
</Tooltip>
</CustomControl> </CustomControl>
<div <div
bind:this={container} 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-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
@@ -9,7 +9,8 @@
Ungroup, Ungroup,
MapPin, MapPin,
Filter, Filter,
Scissors Scissors,
MountainSnow,
} from 'lucide-svelte'; } from 'lucide-svelte';
import { _ } from 'svelte-i18n'; import { _ } from 'svelte-i18n';
@@ -21,37 +22,32 @@
class="h-fit flex flex-col p-1 gap-1.5 bg-background rounded-r-md pointer-events-auto shadow-md {$$props.class ?? class="h-fit flex flex-col p-1 gap-1.5 bg-background rounded-r-md pointer-events-auto shadow-md {$$props.class ??
''}" ''}"
> >
<ToolbarItem tool={Tool.ROUTING}> <ToolbarItem tool={Tool.ROUTING} label={$_('toolbar.routing.tooltip')}>
<Pencil slot="icon" size="18" class="h-" /> <Pencil slot="icon" size="18" />
<span slot="tooltip">{$_('toolbar.routing.tooltip')}</span>
</ToolbarItem> </ToolbarItem>
<ToolbarItem tool={Tool.WAYPOINT}> <ToolbarItem tool={Tool.WAYPOINT} label={$_('toolbar.waypoint.tooltip')}>
<MapPin slot="icon" size="18" /> <MapPin slot="icon" size="18" />
<span slot="tooltip">{$_('toolbar.waypoint.tooltip')}</span>
</ToolbarItem> </ToolbarItem>
<ToolbarItem tool={Tool.SCISSORS}> <ToolbarItem tool={Tool.SCISSORS} label={$_('toolbar.scissors.tooltip')}>
<Scissors slot="icon" size="18" /> <Scissors slot="icon" size="18" />
<span slot="tooltip">{$_('toolbar.scissors.tooltip')}</span>
</ToolbarItem> </ToolbarItem>
<ToolbarItem tool={Tool.TIME}> <ToolbarItem tool={Tool.TIME} label={$_('toolbar.time.tooltip')}>
<CalendarClock slot="icon" size="18" /> <CalendarClock slot="icon" size="18" />
<span slot="tooltip">{$_('toolbar.time.tooltip')}</span>
</ToolbarItem> </ToolbarItem>
<ToolbarItem tool={Tool.MERGE}> <ToolbarItem tool={Tool.MERGE} label={$_('toolbar.merge.tooltip')}>
<Group slot="icon" size="18" /> <Group slot="icon" size="18" />
<span slot="tooltip">{$_('toolbar.merge.tooltip')}</span>
</ToolbarItem> </ToolbarItem>
<ToolbarItem tool={Tool.EXTRACT}> <ToolbarItem tool={Tool.EXTRACT} label={$_('toolbar.extract.tooltip')}>
<Ungroup slot="icon" size="18" /> <Ungroup slot="icon" size="18" />
<span slot="tooltip">{$_('toolbar.extract.tooltip')}</span>
</ToolbarItem> </ToolbarItem>
<ToolbarItem tool={Tool.REDUCE}> <ToolbarItem tool={Tool.ELEVATION} label={$_('toolbar.elevation.button')}>
<MountainSnow slot="icon" size="18" />
</ToolbarItem>
<ToolbarItem tool={Tool.REDUCE} label={$_('toolbar.reduce.tooltip')}>
<Filter slot="icon" size="18" /> <Filter slot="icon" size="18" />
<span slot="tooltip">{$_('toolbar.reduce.tooltip')}</span>
</ToolbarItem> </ToolbarItem>
<ToolbarItem tool={Tool.CLEAN}> <ToolbarItem tool={Tool.CLEAN} label={$_('toolbar.clean.tooltip')}>
<SquareDashedMousePointer slot="icon" size="18" /> <SquareDashedMousePointer slot="icon" size="18" />
<span slot="tooltip">{$_('toolbar.clean.tooltip')}</span>
</ToolbarItem> </ToolbarItem>
</div> </div>
<ToolbarItemMenu class={$$props.class ?? ''} /> <ToolbarItemMenu class={$$props.class ?? ''} />
@@ -4,6 +4,7 @@
import { currentTool, type Tool } from '$lib/stores'; import { currentTool, type Tool } from '$lib/stores';
export let tool: Tool; export let tool: Tool;
export let label: string;
function toggleTool() { function toggleTool() {
currentTool.update((current) => (current === tool ? null : tool)); currentTool.update((current) => (current === tool ? null : tool));
@@ -17,11 +18,12 @@
variant="ghost" variant="ghost"
class="h-[26px] px-1 py-1.5 {$currentTool === tool ? 'bg-accent' : ''}" class="h-[26px] px-1 py-1.5 {$currentTool === tool ? 'bg-accent' : ''}"
on:click={toggleTool} on:click={toggleTool}
aria-label={label}
> >
<slot name="icon" /> <slot name="icon" />
</Button> </Button>
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Content side="right"> <Tooltip.Content side="right">
<slot name="tooltip" /> <span>{label}</span>
</Tooltip.Content> </Tooltip.Content>
</Tooltip.Root> </Tooltip.Root>
@@ -9,6 +9,7 @@
import Time from '$lib/components/toolbar/tools/Time.svelte'; import Time from '$lib/components/toolbar/tools/Time.svelte';
import Merge from '$lib/components/toolbar/tools/Merge.svelte'; import Merge from '$lib/components/toolbar/tools/Merge.svelte';
import Extract from '$lib/components/toolbar/tools/Extract.svelte'; import Extract from '$lib/components/toolbar/tools/Extract.svelte';
import Elevation from '$lib/components/toolbar/tools/Elevation.svelte';
import Clean from '$lib/components/toolbar/tools/Clean.svelte'; import Clean from '$lib/components/toolbar/tools/Clean.svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte'; import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte'; import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte';
@@ -23,7 +24,7 @@
onMount(() => { onMount(() => {
popup = new mapboxgl.Popup({ popup = new mapboxgl.Popup({
closeButton: false, closeButton: false,
maxWidth: undefined maxWidth: undefined,
}); });
popup.setDOMContent(popupElement); popup.setDOMContent(popupElement);
popupElement.classList.remove('hidden'); popupElement.classList.remove('hidden');
@@ -48,6 +49,8 @@
<Time /> <Time />
{:else if $currentTool === Tool.MERGE} {:else if $currentTool === Tool.MERGE}
<Merge /> <Merge />
{:else if $currentTool === Tool.ELEVATION}
<Elevation />
{:else if $currentTool === Tool.EXTRACT} {:else if $currentTool === Tool.EXTRACT}
<Extract /> <Extract />
{:else if $currentTool === Tool.CLEAN} {:else if $currentTool === Tool.CLEAN}
@@ -1,7 +1,7 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
enum CleanType { enum CleanType {
INSIDE = 'inside', INSIDE = 'inside',
OUTSIDE = 'outside' OUTSIDE = 'outside',
} }
</script> </script>
@@ -11,9 +11,9 @@
import * as RadioGroup from '$lib/components/ui/radio-group'; import * as RadioGroup from '$lib/components/ui/radio-group';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { _ } from 'svelte-i18n'; import { _, locale } from 'svelte-i18n';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { resetCursor, setCrosshairCursor } from '$lib/utils'; import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
import { Trash2 } from 'lucide-svelte'; import { Trash2 } from 'lucide-svelte';
import { map } from '$lib/stores'; import { map } from '$lib/stores';
import { selection } from '$lib/components/file-list/Selection'; import { selection } from '$lib/components/file-list/Selection';
@@ -41,10 +41,10 @@
[rectangleCoordinates[1].lng, rectangleCoordinates[0].lat], [rectangleCoordinates[1].lng, rectangleCoordinates[0].lat],
[rectangleCoordinates[1].lng, rectangleCoordinates[1].lat], [rectangleCoordinates[1].lng, rectangleCoordinates[1].lat],
[rectangleCoordinates[0].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'); let source = $map.getSource('rectangle');
if (source) { if (source) {
@@ -52,7 +52,7 @@
} else { } else {
$map.addSource('rectangle', { $map.addSource('rectangle', {
type: 'geojson', type: 'geojson',
data: data data: data,
}); });
} }
if (!$map.getLayer('rectangle')) { if (!$map.getLayer('rectangle')) {
@@ -62,8 +62,8 @@
source: 'rectangle', source: 'rectangle',
paint: { paint: {
'fill-color': 'SteelBlue', 'fill-color': 'SteelBlue',
'fill-opacity': 0.5 'fill-opacity': 0.5,
} },
}); });
} }
} }
@@ -161,12 +161,12 @@
[ [
{ {
lat: Math.min(rectangleCoordinates[0].lat, rectangleCoordinates[1].lat), 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), 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, cleanType === CleanType.INSIDE,
deleteTrackpoints, deleteTrackpoints,
@@ -178,7 +178,7 @@
<Trash2 size="16" class="mr-1" /> <Trash2 size="16" class="mr-1" />
{$_('toolbar.clean.button')} {$_('toolbar.clean.button')}
</Button> </Button>
<Help link="./help/toolbar/clean"> <Help link={getURLForLanguage($locale, '/help/toolbar/clean')}>
{#if validSelection} {#if validSelection}
{$_('toolbar.clean.help')} {$_('toolbar.clean.help')}
{:else} {:else}
@@ -0,0 +1,35 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { selection } from '$lib/components/file-list/Selection';
import Help from '$lib/components/Help.svelte';
import { MountainSnow } from 'lucide-svelte';
import { dbUtils } from '$lib/db';
import { map } from '$lib/stores';
import { _, locale } from 'svelte-i18n';
import { getURLForLanguage } from '$lib/utils';
$: validSelection = $selection.size > 0;
</script>
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
<Button
variant="outline"
class="whitespace-normal h-fit"
disabled={!validSelection}
on:click={async () => {
if ($map) {
dbUtils.addElevationToSelection($map);
}
}}
>
<MountainSnow size="16" class="mr-1 shrink-0" />
{$_('toolbar.elevation.button')}
</Button>
<Help link={getURLForLanguage($locale, '/help/toolbar/elevation')}>
{#if validSelection}
{$_('toolbar.elevation.help')}
{:else}
{$_('toolbar.elevation.help_no_selection')}
{/if}
</Help>
</div>
@@ -7,11 +7,12 @@
ListTrackItem, ListTrackItem,
ListTrackSegmentItem, ListTrackSegmentItem,
ListWaypointItem, ListWaypointItem,
ListWaypointsItem ListWaypointsItem,
} from '$lib/components/file-list/FileList'; } from '$lib/components/file-list/FileList';
import Help from '$lib/components/Help.svelte'; import Help from '$lib/components/Help.svelte';
import { dbUtils, getFile } from '$lib/db'; import { dbUtils, getFile } from '$lib/db';
import { _ } from 'svelte-i18n'; import { _, locale } from 'svelte-i18n';
import { getURLForLanguage } from '$lib/utils';
$: validSelection = $: validSelection =
$selection.size > 0 && $selection.size > 0 &&
@@ -42,7 +43,7 @@
<Ungroup size="16" class="mr-1" /> <Ungroup size="16" class="mr-1" />
{$_('toolbar.extract.button')} {$_('toolbar.extract.button')}
</Button> </Button>
<Help link="./help/toolbar/extract"> <Help link={getURLForLanguage($locale, '/help/toolbar/extract')}>
{#if validSelection} {#if validSelection}
{$_('toolbar.extract.help')} {$_('toolbar.extract.help')}
{:else} {:else}
@@ -1,7 +1,7 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
enum MergeType { enum MergeType {
TRACES = 'traces', TRACES = 'traces',
CONTENTS = 'contents' CONTENTS = 'contents',
} }
</script> </script>
@@ -11,13 +11,18 @@
import { selection } from '$lib/components/file-list/Selection'; import { selection } from '$lib/components/file-list/Selection';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label/index.js'; 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 * as RadioGroup from '$lib/components/ui/radio-group';
import { _ } from 'svelte-i18n'; import { _, locale } from 'svelte-i18n';
import { dbUtils, getFile } from '$lib/db'; import { dbUtils, getFile } from '$lib/db';
import { Group } from 'lucide-svelte'; 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 canMergeTraces = false;
let canMergeContents = false; let canMergeContents = false;
let removeGaps = false;
$: if ($selection.size > 1) { $: if ($selection.size > 1) {
canMergeTraces = true; canMergeTraces = true;
@@ -54,35 +59,59 @@
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}"> <div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
<RadioGroup.Root bind:value={mergeType}> <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} /> <RadioGroup.Item value={MergeType.TRACES} />
{$_('toolbar.merge.merge_traces')} {$_('toolbar.merge.merge_traces')}
</Label> </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} /> <RadioGroup.Item value={MergeType.CONTENTS} />
{$_('toolbar.merge.merge_contents')} {$_('toolbar.merge.merge_contents')}
</Label> </Label>
</RadioGroup.Root> </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 <Button
variant="outline" variant="outline"
class="whitespace-normal h-fit"
disabled={(mergeType === MergeType.TRACES && !canMergeTraces) || disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
(mergeType === MergeType.CONTENTS && !canMergeContents)} (mergeType === MergeType.CONTENTS && !canMergeContents)}
on:click={() => { 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" /> <Group size="16" class="mr-1 shrink-0" />
{$_('toolbar.merge.merge_selection')} {$_('toolbar.merge.merge_selection')}
</Button> </Button>
<Help link="./help/toolbar/merge"> <Help link={getURLForLanguage($locale, '/help/toolbar/merge')}>
{#if mergeType === MergeType.TRACES && canMergeTraces} {#if mergeType === MergeType.TRACES && canMergeTraces}
{$_('toolbar.merge.help_merge_traces')} {$_('toolbar.merge.help_merge_traces')}
{:else if mergeType === MergeType.TRACES && !canMergeTraces} {:else if mergeType === MergeType.TRACES && !canMergeTraces}
{$_('toolbar.merge.help_cannot_merge_traces')} {$_('toolbar.merge.help_cannot_merge_traces')}
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[0]}
<Shortcut
ctrl={true}
click={true}
class="inline-flex text-muted-foreground text-xs border rounded p-0.5 gap-0"
/>
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[1]}
{:else if mergeType === MergeType.CONTENTS && canMergeContents} {:else if mergeType === MergeType.CONTENTS && canMergeContents}
{$_('toolbar.merge.help_merge_contents')} {$_('toolbar.merge.help_merge_contents')}
{:else if mergeType === MergeType.CONTENTS && !canMergeContents} {:else if mergeType === MergeType.CONTENTS && !canMergeContents}
{$_('toolbar.merge.help_cannot_merge_contents')} {$_('toolbar.merge.help_cannot_merge_contents')}
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[0]}
<Shortcut
ctrl={true}
click={true}
class="inline-flex text-muted-foreground text-xs border rounded p-0.5 gap-0"
/>
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[1]}
{/if} {/if}
</Help> </Help>
</div> </div>

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