881 Commits

Author SHA1 Message Date
vcoppe
3a65f8dc16 New Crowdin updates (#270)
* New translations en.json (Romanian)

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

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

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Thai)

* New translations en.json (Latvian)

* New translations en.json (Chinese Traditional, Hong Kong)

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

* New translations en.json (French)
2025-11-14 18:56:43 +01:00
vcoppe
d624352a0b improve contrast for selection 2025-11-14 18:45:49 +01:00
vcoppe
3fb597a774 fix page title flickering 2025-11-14 18:45:38 +01:00
vcoppe
85e46bc524 add basemap in basemap tree when missing in embedding mode 2025-11-14 18:45:20 +01:00
vcoppe
d3790878b3 remove x 2025-11-14 18:44:32 +01:00
vcoppe
e648e50f1b remove unused file 2025-11-14 18:44:16 +01:00
vcoppe
cb6cede0b6 update license 2025-11-14 18:44:06 +01:00
vcoppe
a0157ddc2a New translations file.mdx (Czech) (#268) 2025-11-13 09:06:27 +01:00
vcoppe
8f595fbc7b fix safari incompatibility 2025-11-13 09:06:00 +01:00
vcoppe
2e3b22c5fa update deploy action 2025-11-12 18:26:35 +01:00
vcoppe
42b968372b fix import 2025-11-12 18:14:49 +01:00
vcoppe
415e7b492f New Crowdin updates (#267)
* New translations extract.mdx (Czech)

* New translations extract.mdx (Danish)

* New translations extract.mdx (German)

* New translations extract.mdx (Greek)

* New translations extract.mdx (Basque)

* New translations extract.mdx (Finnish)

* New translations extract.mdx (Hebrew)

* New translations extract.mdx (Hungarian)

* New translations extract.mdx (Italian)

* New translations extract.mdx (Korean)

* New translations extract.mdx (Lithuanian)

* New translations extract.mdx (Dutch)

* New translations extract.mdx (Norwegian)

* New translations extract.mdx (Polish)

* New translations extract.mdx (Portuguese)

* New translations extract.mdx (Russian)

* New translations extract.mdx (Swedish)

* New translations extract.mdx (Turkish)

* New translations extract.mdx (Ukrainian)

* New translations extract.mdx (Chinese Simplified)

* New translations extract.mdx (Vietnamese)

* New translations extract.mdx (Portuguese, Brazilian)

* New translations extract.mdx (Indonesian)

* New translations extract.mdx (Thai)

* New translations extract.mdx (Latvian)

* New translations extract.mdx (Chinese Traditional, Hong Kong)

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

* New translations merge.mdx (Romanian)

* New translations merge.mdx (French)

* New translations merge.mdx (Spanish)

* New translations merge.mdx (Belarusian)

* New translations merge.mdx (Catalan)

* New translations merge.mdx (Czech)

* New translations merge.mdx (Danish)

* New translations merge.mdx (German)

* New translations merge.mdx (Greek)

* New translations merge.mdx (Basque)

* New translations merge.mdx (Finnish)

* New translations merge.mdx (Hebrew)

* New translations merge.mdx (Hungarian)

* New translations merge.mdx (Italian)

* New translations merge.mdx (Korean)

* New translations merge.mdx (Lithuanian)

* New translations merge.mdx (Dutch)

* New translations merge.mdx (Norwegian)

* New translations merge.mdx (Polish)

* New translations merge.mdx (Portuguese)

* New translations merge.mdx (Russian)

* New translations merge.mdx (Swedish)

* New translations merge.mdx (Turkish)

* New translations merge.mdx (Ukrainian)

* New translations merge.mdx (Chinese Simplified)

* New translations merge.mdx (Vietnamese)

* New translations merge.mdx (Portuguese, Brazilian)

* New translations merge.mdx (Indonesian)

* New translations merge.mdx (Thai)

* New translations merge.mdx (Latvian)

* New translations merge.mdx (Chinese Traditional, Hong Kong)

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

* New translations minify.mdx (Romanian)

* New translations minify.mdx (French)

* New translations minify.mdx (Spanish)

* New translations minify.mdx (Belarusian)

* New translations minify.mdx (Catalan)

* New translations minify.mdx (Czech)

* New translations minify.mdx (Danish)

* New translations minify.mdx (German)

* New translations minify.mdx (Greek)

* New translations minify.mdx (Basque)

* New translations minify.mdx (Finnish)

* New translations minify.mdx (Hebrew)

* New translations minify.mdx (Hungarian)

* New translations minify.mdx (Italian)

* New translations minify.mdx (Korean)

* New translations minify.mdx (Lithuanian)

* New translations minify.mdx (Dutch)

* New translations minify.mdx (Norwegian)

* New translations minify.mdx (Polish)

* New translations minify.mdx (Portuguese)

* New translations minify.mdx (Russian)

* New translations minify.mdx (Swedish)

* New translations minify.mdx (Turkish)

* New translations minify.mdx (Ukrainian)

* New translations minify.mdx (Chinese Simplified)

* New translations minify.mdx (Vietnamese)

* New translations minify.mdx (Portuguese, Brazilian)

* New translations minify.mdx (Indonesian)

* New translations minify.mdx (Thai)

* New translations minify.mdx (Latvian)

* New translations minify.mdx (Chinese Traditional, Hong Kong)

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

* New translations poi.mdx (Romanian)

* New translations poi.mdx (French)

* New translations poi.mdx (Spanish)

* New translations poi.mdx (Belarusian)

* New translations poi.mdx (Catalan)

* New translations poi.mdx (Czech)

* New translations poi.mdx (Danish)

* New translations poi.mdx (German)

* New translations poi.mdx (Greek)

* New translations poi.mdx (Basque)

* New translations poi.mdx (Finnish)

* New translations poi.mdx (Hebrew)

* New translations poi.mdx (Hungarian)

* New translations poi.mdx (Italian)

* New translations poi.mdx (Korean)

* New translations poi.mdx (Lithuanian)

* New translations poi.mdx (Dutch)

* New translations poi.mdx (Norwegian)

* New translations poi.mdx (Polish)

* New translations poi.mdx (Portuguese)

* New translations poi.mdx (Russian)

* New translations poi.mdx (Swedish)

* New translations poi.mdx (Turkish)

* New translations poi.mdx (Ukrainian)

* New translations poi.mdx (Chinese Simplified)

* New translations poi.mdx (Vietnamese)

* New translations poi.mdx (Portuguese, Brazilian)

* New translations poi.mdx (Indonesian)

* New translations poi.mdx (Thai)

* New translations poi.mdx (Latvian)

* New translations poi.mdx (Chinese Traditional, Hong Kong)

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

* New translations routing.mdx (Romanian)

* New translations routing.mdx (French)

* New translations routing.mdx (Spanish)

* New translations routing.mdx (Belarusian)

* New translations routing.mdx (Catalan)

* New translations routing.mdx (Danish)

* New translations routing.mdx (German)

* New translations routing.mdx (Greek)

* New translations routing.mdx (Basque)

* New translations routing.mdx (Finnish)

* New translations routing.mdx (Hebrew)

* New translations routing.mdx (Hungarian)

* New translations routing.mdx (Italian)

* New translations routing.mdx (Korean)

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

* New translations routing.mdx (Ukrainian)

* New translations routing.mdx (Chinese Simplified)

* New translations routing.mdx (Vietnamese)

* New translations routing.mdx (Portuguese, Brazilian)

* New translations routing.mdx (Indonesian)

* New translations routing.mdx (Thai)

* New translations routing.mdx (Latvian)

* New translations routing.mdx (Chinese Traditional, Hong Kong)

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

* New translations scissors.mdx (Romanian)

* New translations scissors.mdx (French)

* New translations scissors.mdx (Spanish)

* New translations scissors.mdx (Belarusian)

* New translations scissors.mdx (Catalan)

* New translations scissors.mdx (Czech)

* New translations scissors.mdx (Danish)

* New translations scissors.mdx (German)

* New translations scissors.mdx (Greek)

* New translations scissors.mdx (Basque)

* New translations scissors.mdx (Finnish)

* New translations scissors.mdx (Hebrew)

* New translations scissors.mdx (Hungarian)

* New translations scissors.mdx (Italian)

* New translations scissors.mdx (Korean)

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

* New translations scissors.mdx (Ukrainian)

* New translations scissors.mdx (Chinese Simplified)

* New translations scissors.mdx (Vietnamese)

* New translations scissors.mdx (Portuguese, Brazilian)

* New translations scissors.mdx (Indonesian)

* New translations scissors.mdx (Thai)

* New translations scissors.mdx (Latvian)

* New translations scissors.mdx (Chinese Traditional, Hong Kong)

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

* New translations time.mdx (Romanian)

* New translations time.mdx (French)

* New translations time.mdx (Spanish)

* New translations time.mdx (Belarusian)

* New translations time.mdx (Catalan)

* New translations time.mdx (Czech)

* New translations time.mdx (Danish)

* New translations time.mdx (German)

* New translations time.mdx (Greek)

* New translations time.mdx (Basque)

* New translations time.mdx (Finnish)

* New translations time.mdx (Hebrew)

* New translations time.mdx (Hungarian)

* New translations time.mdx (Italian)

* New translations time.mdx (Korean)

* New translations time.mdx (Lithuanian)

* New translations time.mdx (Dutch)

* New translations time.mdx (Norwegian)

* New translations time.mdx (Polish)

* New translations time.mdx (Portuguese)

* New translations time.mdx (Russian)

* New translations time.mdx (Swedish)

* New translations time.mdx (Turkish)

* New translations time.mdx (Ukrainian)

* New translations time.mdx (Chinese Simplified)

* New translations time.mdx (Vietnamese)

* New translations time.mdx (Portuguese, Brazilian)

* New translations time.mdx (Indonesian)

* New translations time.mdx (Thai)

* New translations time.mdx (Latvian)

* New translations time.mdx (Chinese Traditional, Hong Kong)

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

* New translations elevation.mdx (Romanian)

* New translations elevation.mdx (French)

* New translations elevation.mdx (Spanish)

* New translations elevation.mdx (Belarusian)

* New translations elevation.mdx (Catalan)

* New translations elevation.mdx (Czech)

* New translations elevation.mdx (Danish)

* New translations elevation.mdx (German)

* New translations elevation.mdx (Greek)

* New translations elevation.mdx (Basque)

* New translations elevation.mdx (Finnish)

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

* New translations elevation.mdx (Ukrainian)

* New translations elevation.mdx (Chinese Simplified)

* New translations elevation.mdx (Vietnamese)

* New translations elevation.mdx (Portuguese, Brazilian)

* New translations elevation.mdx (Indonesian)

* New translations elevation.mdx (Thai)

* New translations elevation.mdx (Latvian)

* New translations elevation.mdx (Chinese Traditional, Hong Kong)

* New translations elevation.mdx (Serbian (Latin))
2025-11-12 18:03:06 +01:00
vcoppe
2ea8e46723 putting correct import back 2025-11-12 17:45:55 +01:00
vcoppe
977c6c6dde trying to force update the import on crowdin 2025-11-12 17:28:46 +01:00
vcoppe
1e5db9dc6c fix import 2025-11-12 16:17:25 +01:00
vcoppe
252dc10e61 Merge remote-tracking branch 'origin/l10n' into dev 2025-11-12 16:08:18 +01:00
vcoppe
f9e2315ba1 only show layer if it has been activated before 2025-11-12 15:50:05 +01:00
vcoppe
0eca588280 update extension api 2025-11-12 14:48:17 +01:00
vcoppe
33523bbfb9 New translations files-and-stats.mdx (Lithuanian) 2025-11-12 12:56:32 +01:00
vcoppe
110f23bdf1 update extension api 2025-11-12 12:47:26 +01:00
vcoppe
50a5cb23f5 remove unused imports 2025-11-12 11:39:51 +01:00
vcoppe
30e72db5ea hide horizontal scroll bar 2025-11-12 09:05:20 +01:00
vcoppe
c4c64c8fe8 load files from urls/ids once local ones are loaded 2025-11-12 09:02:09 +01:00
vcoppe
df39350d7d New translations file.mdx (Czech) 2025-11-11 18:33:32 +01:00
vcoppe
5daacd3ed4 New translations en.json (Czech) 2025-11-11 18:33:27 +01:00
vcoppe
0f7f64fb2f migrate component 2025-11-11 17:30:06 +01:00
vcoppe
b09a1fdcb7 migrate component 2025-11-11 17:23:24 +01:00
vcoppe
e5d45dee3a fix hidden computation for new files 2025-11-11 14:03:07 +01:00
vcoppe
f3270e19df New translations file.mdx (Dutch) 2025-11-11 13:57:07 +01:00
vcoppe
1b9ad41c87 New translations en.json (Dutch) 2025-11-11 13:57:05 +01:00
vcoppe
8c3365ef24 update nz basemap 2025-11-11 13:00:34 +01:00
vcoppe
db5cbffb70 api for adding overlays from extensions 2025-11-11 12:11:38 +01:00
vcoppe
c6586f0eed New translations file.mdx (Spanish) 2025-11-11 12:11:02 +01:00
vcoppe
f40bdc8ed9 New translations en.json (Spanish) 2025-11-11 12:11:00 +01:00
vcoppe
683ac4e118 clean custom layer logic 2025-11-11 10:37:06 +01:00
vcoppe
88c9abb78e open collapsible if an item item is selected 2025-11-11 09:31:08 +01:00
vcoppe
1729a2f734 remove dead code 2025-11-11 09:29:07 +01:00
vcoppe
e5ad8bbb70 New translations file.mdx (French) 2025-11-10 20:34:51 +01:00
vcoppe
7f6acbfdbc Update source file file.mdx 2025-11-10 20:33:50 +01:00
vcoppe
2e070529e0 New translations routing.mdx (Portuguese, Brazilian) 2025-11-10 19:08:15 +01:00
vcoppe
f4b31e5f0a New translations routing.mdx (Chinese Simplified) 2025-11-10 19:08:14 +01:00
vcoppe
f7f093a464 New translations routing.mdx (Italian) 2025-11-10 19:08:09 +01:00
vcoppe
95cc340de5 New translations routing.mdx (Basque) 2025-11-10 19:08:06 +01:00
vcoppe
51a003c816 New translations routing.mdx (German) 2025-11-10 19:08:04 +01:00
vcoppe
977152139f New translations routing.mdx (Catalan) 2025-11-10 19:08:03 +01:00
vcoppe
c6798dbcd5 delete empty file 2025-11-10 19:06:56 +01:00
vcoppe
78833df95e New translations file.mdx (Serbian (Latin)) 2025-11-10 19:06:14 +01:00
vcoppe
099d941d2e New translations file.mdx (Chinese Traditional, Hong Kong) 2025-11-10 19:06:13 +01:00
vcoppe
ea58f378a9 New translations file.mdx (Latvian) 2025-11-10 19:06:11 +01:00
vcoppe
4060884909 New translations file.mdx (Thai) 2025-11-10 19:06:10 +01:00
vcoppe
d9277c11d2 New translations file.mdx (Indonesian) 2025-11-10 19:06:09 +01:00
vcoppe
bcbc90820a New translations file.mdx (Portuguese, Brazilian) 2025-11-10 19:06:07 +01:00
vcoppe
e9caa95673 New translations file.mdx (Vietnamese) 2025-11-10 19:06:06 +01:00
vcoppe
9cd6703b05 New translations file.mdx (Chinese Simplified) 2025-11-10 19:06:05 +01:00
vcoppe
4233bd7771 New translations file.mdx (Ukrainian) 2025-11-10 19:06:03 +01:00
vcoppe
0e2db441f2 New translations file.mdx (Turkish) 2025-11-10 19:06:02 +01:00
vcoppe
571b101ea4 New translations file.mdx (Swedish) 2025-11-10 19:06:01 +01:00
vcoppe
0b9dca61ab New translations file.mdx (Russian) 2025-11-10 19:06:00 +01:00
vcoppe
d8fa76d076 New translations file.mdx (Portuguese) 2025-11-10 19:05:58 +01:00
vcoppe
6116eef513 New translations file.mdx (Polish) 2025-11-10 19:05:57 +01:00
vcoppe
fabe987f2c New translations file.mdx (Norwegian) 2025-11-10 19:05:56 +01:00
vcoppe
af20880f37 New translations file.mdx (Dutch) 2025-11-10 19:05:55 +01:00
vcoppe
44eeab0d4b New translations file.mdx (Lithuanian) 2025-11-10 19:05:53 +01:00
vcoppe
b331900158 New translations file.mdx (Korean) 2025-11-10 19:05:52 +01:00
vcoppe
d74380404c New translations file.mdx (Italian) 2025-11-10 19:05:51 +01:00
vcoppe
3833a9cd6b New translations file.mdx (Hungarian) 2025-11-10 19:05:50 +01:00
vcoppe
74fdd943c9 New translations file.mdx (Hebrew) 2025-11-10 19:05:49 +01:00
vcoppe
ad5b772502 New translations file.mdx (Finnish) 2025-11-10 19:05:47 +01:00
vcoppe
9bc941aa31 New translations file.mdx (Basque) 2025-11-10 19:05:46 +01:00
vcoppe
705df43047 New translations file.mdx (Greek) 2025-11-10 19:05:45 +01:00
vcoppe
b31e3bb710 New translations file.mdx (German) 2025-11-10 19:05:44 +01:00
vcoppe
4082d0a368 New translations file.mdx (Danish) 2025-11-10 19:05:42 +01:00
vcoppe
ce85286cdf New translations file.mdx (Czech) 2025-11-10 19:05:41 +01:00
vcoppe
415cf1a777 New translations file.mdx (Catalan) 2025-11-10 19:05:40 +01:00
vcoppe
f249919ec8 New translations file.mdx (Belarusian) 2025-11-10 19:05:39 +01:00
vcoppe
054c9787d3 New translations file.mdx (Spanish) 2025-11-10 19:05:37 +01:00
vcoppe
c1e88e2b5a New translations file.mdx (French) 2025-11-10 19:05:36 +01:00
vcoppe
f5efeb16c4 New translations file.mdx (Romanian) 2025-11-10 19:05:35 +01:00
vcoppe
59afae7bca New translations translation.mdx (Portuguese, Brazilian) 2025-11-10 19:04:37 +01:00
vcoppe
92c6339064 New translations translation.mdx (Chinese Simplified) 2025-11-10 19:04:35 +01:00
vcoppe
582ae233f2 New translations translation.mdx (Turkish) 2025-11-10 19:04:34 +01:00
vcoppe
3c3016a211 New translations translation.mdx (Dutch) 2025-11-10 19:04:31 +01:00
vcoppe
e24e1d9d3c New translations translation.mdx (Italian) 2025-11-10 19:04:28 +01:00
vcoppe
85fb564be7 New translations translation.mdx (Basque) 2025-11-10 19:04:26 +01:00
vcoppe
18f7db9eee New translations translation.mdx (German) 2025-11-10 19:04:24 +01:00
vcoppe
ea0770fd11 New translations translation.mdx (Czech) 2025-11-10 19:04:23 +01:00
vcoppe
9c28fab3f9 New translations translation.mdx (Catalan) 2025-11-10 19:04:22 +01:00
vcoppe
bac332a8c4 New translations translation.mdx (Spanish) 2025-11-10 19:04:20 +01:00
vcoppe
86d2542bd9 New translations funding.mdx (Portuguese, Brazilian) 2025-11-10 19:04:02 +01:00
vcoppe
5f63ec884f New translations funding.mdx (Chinese Simplified) 2025-11-10 19:04:00 +01:00
vcoppe
0f87b33354 New translations funding.mdx (Turkish) 2025-11-10 19:03:58 +01:00
vcoppe
c7dc99a12f New translations funding.mdx (Dutch) 2025-11-10 19:03:55 +01:00
vcoppe
05c3c3f8f3 New translations funding.mdx (Italian) 2025-11-10 19:03:53 +01:00
vcoppe
2437a43471 New translations funding.mdx (Basque) 2025-11-10 19:03:50 +01:00
vcoppe
16cd812ba0 New translations funding.mdx (German) 2025-11-10 19:03:49 +01:00
vcoppe
c036128720 New translations funding.mdx (Czech) 2025-11-10 19:03:47 +01:00
vcoppe
645db15848 New translations funding.mdx (Catalan) 2025-11-10 19:03:45 +01:00
vcoppe
21e142ffc3 New translations funding.mdx (Spanish) 2025-11-10 19:03:44 +01:00
vcoppe
23a6b3db72 New translations funding.mdx (French) 2025-11-10 19:03:43 +01:00
vcoppe
b87d109625 New translations routing.mdx (Czech) 2025-11-10 19:03:04 +01:00
vcoppe
651bd295b9 New translations en.json (French) 2025-11-10 19:02:37 +01:00
vcoppe
01607e92b9 Update source file routing.mdx 2025-11-10 19:02:30 +01:00
vcoppe
f40d54adbb Update source file poi.mdx 2025-11-10 19:02:29 +01:00
vcoppe
60fb387495 Update source file minify.mdx 2025-11-10 19:02:28 +01:00
vcoppe
a7df18723c Update source file toolbar.mdx 2025-11-10 19:02:26 +01:00
vcoppe
4e39cc937a Update source file translation.mdx 2025-11-10 19:02:24 +01:00
vcoppe
fe513c17ab Update source file funding.mdx 2025-11-10 19:02:23 +01:00
vcoppe
f7fb88ed3d Update source file files-and-stats.mdx 2025-11-10 19:02:22 +01:00
vcoppe
b3247c7cbe Update source file en.json 2025-11-10 19:02:21 +01:00
vcoppe
d490dc0a8b match updated wording 2025-11-10 19:02:03 +01:00
vcoppe
97e79c23f6 New translations routing.mdx (Serbian (Latin)) 2025-11-10 18:51:41 +01:00
vcoppe
7803a29875 New translations routing.mdx (Chinese Traditional, Hong Kong) 2025-11-10 18:51:40 +01:00
vcoppe
08093beed1 New translations routing.mdx (Latvian) 2025-11-10 18:51:38 +01:00
vcoppe
ae46ae89bc New translations routing.mdx (Thai) 2025-11-10 18:51:37 +01:00
vcoppe
f7977dca39 New translations routing.mdx (Indonesian) 2025-11-10 18:51:36 +01:00
vcoppe
0b344f3e08 New translations routing.mdx (Portuguese, Brazilian) 2025-11-10 18:51:35 +01:00
vcoppe
6058154eca New translations routing.mdx (Vietnamese) 2025-11-10 18:51:34 +01:00
vcoppe
941016f04b New translations routing.mdx (Chinese Simplified) 2025-11-10 18:51:33 +01:00
vcoppe
7a15898087 New translations routing.mdx (Ukrainian) 2025-11-10 18:51:31 +01:00
vcoppe
6b4effa90f New translations routing.mdx (Turkish) 2025-11-10 18:51:30 +01:00
vcoppe
77d56e4a4c New translations routing.mdx (Swedish) 2025-11-10 18:51:29 +01:00
vcoppe
c4dd622e7b New translations routing.mdx (Russian) 2025-11-10 18:51:28 +01:00
vcoppe
9f24535142 New translations routing.mdx (Portuguese) 2025-11-10 18:51:26 +01:00
vcoppe
a45161fcb8 New translations routing.mdx (Polish) 2025-11-10 18:51:25 +01:00
vcoppe
4379763a73 New translations routing.mdx (Norwegian) 2025-11-10 18:51:23 +01:00
vcoppe
a25b376dfd New translations routing.mdx (Dutch) 2025-11-10 18:51:21 +01:00
vcoppe
fb429f6777 New translations routing.mdx (Lithuanian) 2025-11-10 18:51:20 +01:00
vcoppe
ae33227c3a New translations routing.mdx (Korean) 2025-11-10 18:51:19 +01:00
vcoppe
2fedc61d1e New translations routing.mdx (Italian) 2025-11-10 18:51:18 +01:00
vcoppe
6988a36dd1 New translations routing.mdx (Hungarian) 2025-11-10 18:51:17 +01:00
vcoppe
60e1124970 New translations routing.mdx (Hebrew) 2025-11-10 18:51:15 +01:00
vcoppe
a007684006 New translations routing.mdx (Finnish) 2025-11-10 18:51:13 +01:00
vcoppe
090a709522 New translations routing.mdx (Basque) 2025-11-10 18:51:12 +01:00
vcoppe
9e06655214 New translations routing.mdx (Greek) 2025-11-10 18:51:11 +01:00
vcoppe
3651825e79 New translations routing.mdx (German) 2025-11-10 18:51:09 +01:00
vcoppe
dcf3160b58 New translations routing.mdx (Danish) 2025-11-10 18:51:08 +01:00
vcoppe
fd014f42cd New translations routing.mdx (Catalan) 2025-11-10 18:51:07 +01:00
vcoppe
a760f2f7fc New translations routing.mdx (Belarusian) 2025-11-10 18:51:06 +01:00
vcoppe
262c114b7d New translations routing.mdx (Spanish) 2025-11-10 18:51:05 +01:00
vcoppe
c90bdd83bb New translations routing.mdx (French) 2025-11-10 18:51:03 +01:00
vcoppe
fdb2d37b12 New translations routing.mdx (Romanian) 2025-11-10 18:51:02 +01:00
vcoppe
7951837ecf New translations poi.mdx (Serbian (Latin)) 2025-11-10 18:51:00 +01:00
vcoppe
d2e112a672 New translations poi.mdx (Chinese Traditional, Hong Kong) 2025-11-10 18:50:59 +01:00
vcoppe
90e86077ba New translations poi.mdx (Latvian) 2025-11-10 18:50:58 +01:00
vcoppe
f69dfcdefe New translations poi.mdx (Thai) 2025-11-10 18:50:57 +01:00
vcoppe
5cff7c4e72 New translations poi.mdx (Indonesian) 2025-11-10 18:50:56 +01:00
vcoppe
0eaa833cdf New translations poi.mdx (Portuguese, Brazilian) 2025-11-10 18:50:55 +01:00
vcoppe
db015d925e New translations poi.mdx (Vietnamese) 2025-11-10 18:50:54 +01:00
vcoppe
69fd47601e New translations poi.mdx (Chinese Simplified) 2025-11-10 18:50:52 +01:00
vcoppe
17521574f0 New translations poi.mdx (Ukrainian) 2025-11-10 18:50:51 +01:00
vcoppe
56650a76ae New translations poi.mdx (Turkish) 2025-11-10 18:50:49 +01:00
vcoppe
984a98e792 New translations poi.mdx (Swedish) 2025-11-10 18:50:48 +01:00
vcoppe
49eb0cf202 New translations poi.mdx (Russian) 2025-11-10 18:50:47 +01:00
vcoppe
8e24a6b036 New translations poi.mdx (Portuguese) 2025-11-10 18:50:46 +01:00
vcoppe
d07bf6d699 New translations poi.mdx (Polish) 2025-11-10 18:50:45 +01:00
vcoppe
877d12c676 New translations poi.mdx (Norwegian) 2025-11-10 18:50:44 +01:00
vcoppe
ca629f625a New translations poi.mdx (Dutch) 2025-11-10 18:50:43 +01:00
vcoppe
087dd9a4b6 New translations poi.mdx (Lithuanian) 2025-11-10 18:50:41 +01:00
vcoppe
d9a967c072 New translations poi.mdx (Korean) 2025-11-10 18:50:40 +01:00
vcoppe
6d522c82c3 New translations poi.mdx (Italian) 2025-11-10 18:50:39 +01:00
vcoppe
867af98083 New translations poi.mdx (Hungarian) 2025-11-10 18:50:38 +01:00
vcoppe
1be058d831 New translations poi.mdx (Hebrew) 2025-11-10 18:50:37 +01:00
vcoppe
71bc044ae5 New translations poi.mdx (Finnish) 2025-11-10 18:50:36 +01:00
vcoppe
d660c50ade New translations poi.mdx (Basque) 2025-11-10 18:50:35 +01:00
vcoppe
3fd733d903 New translations poi.mdx (Greek) 2025-11-10 18:50:33 +01:00
vcoppe
7703b2361d New translations poi.mdx (German) 2025-11-10 18:50:32 +01:00
vcoppe
68fdb9ebc6 New translations poi.mdx (Danish) 2025-11-10 18:50:31 +01:00
vcoppe
b6513343be New translations poi.mdx (Czech) 2025-11-10 18:50:30 +01:00
vcoppe
cc95ff1c55 New translations poi.mdx (Catalan) 2025-11-10 18:50:29 +01:00
vcoppe
c954ee0fde New translations poi.mdx (Belarusian) 2025-11-10 18:50:28 +01:00
vcoppe
1ada5e5d18 New translations poi.mdx (Spanish) 2025-11-10 18:50:27 +01:00
vcoppe
f5244c3d93 New translations poi.mdx (French) 2025-11-10 18:50:26 +01:00
vcoppe
1f17776cd4 New translations poi.mdx (Romanian) 2025-11-10 18:50:24 +01:00
vcoppe
ebd4f36f94 New translations minify.mdx (Serbian (Latin)) 2025-11-10 18:50:23 +01:00
vcoppe
dbb9b5f254 New translations minify.mdx (Chinese Traditional, Hong Kong) 2025-11-10 18:50:22 +01:00
vcoppe
4301472cb2 New translations minify.mdx (Latvian) 2025-11-10 18:50:20 +01:00
vcoppe
15e7954321 New translations minify.mdx (Thai) 2025-11-10 18:50:19 +01:00
vcoppe
32d1de08e9 New translations minify.mdx (Indonesian) 2025-11-10 18:50:18 +01:00
vcoppe
ea88663dce New translations minify.mdx (Portuguese, Brazilian) 2025-11-10 18:50:16 +01:00
vcoppe
97aecaf890 New translations minify.mdx (Vietnamese) 2025-11-10 18:50:15 +01:00
vcoppe
b16688e1b7 New translations minify.mdx (Chinese Simplified) 2025-11-10 18:50:13 +01:00
vcoppe
5ac856251b New translations minify.mdx (Ukrainian) 2025-11-10 18:50:12 +01:00
vcoppe
62a9aacd85 New translations minify.mdx (Turkish) 2025-11-10 18:50:10 +01:00
vcoppe
dff0b55a8c New translations minify.mdx (Swedish) 2025-11-10 18:50:09 +01:00
vcoppe
3b21c75b13 New translations minify.mdx (Russian) 2025-11-10 18:50:08 +01:00
vcoppe
2c7cc4b8e5 New translations minify.mdx (Portuguese) 2025-11-10 18:50:07 +01:00
vcoppe
bcf8c0e35c New translations minify.mdx (Polish) 2025-11-10 18:50:05 +01:00
vcoppe
11d2936fca New translations minify.mdx (Norwegian) 2025-11-10 18:50:04 +01:00
vcoppe
5e84429e24 New translations minify.mdx (Dutch) 2025-11-10 18:50:02 +01:00
vcoppe
48c88b2d7e New translations minify.mdx (Lithuanian) 2025-11-10 18:50:00 +01:00
vcoppe
e5185c0b77 New translations minify.mdx (Korean) 2025-11-10 18:49:59 +01:00
vcoppe
15773d3aba New translations minify.mdx (Italian) 2025-11-10 18:49:58 +01:00
vcoppe
b577837446 New translations minify.mdx (Hungarian) 2025-11-10 18:49:56 +01:00
vcoppe
324e234b2a New translations minify.mdx (Hebrew) 2025-11-10 18:49:55 +01:00
vcoppe
d40fefb0ea New translations minify.mdx (Finnish) 2025-11-10 18:49:54 +01:00
vcoppe
7e05568549 New translations minify.mdx (Basque) 2025-11-10 18:49:52 +01:00
vcoppe
0249a52d1c New translations minify.mdx (Greek) 2025-11-10 18:49:50 +01:00
vcoppe
5df1c5b09b New translations minify.mdx (German) 2025-11-10 18:49:49 +01:00
vcoppe
953ec8fe31 New translations minify.mdx (Danish) 2025-11-10 18:49:48 +01:00
vcoppe
6054afebdc New translations minify.mdx (Czech) 2025-11-10 18:49:46 +01:00
vcoppe
04f356e119 New translations minify.mdx (Catalan) 2025-11-10 18:49:45 +01:00
vcoppe
a6ebefbb30 New translations minify.mdx (Belarusian) 2025-11-10 18:49:44 +01:00
vcoppe
9a1edbe1fa New translations minify.mdx (Spanish) 2025-11-10 18:49:43 +01:00
vcoppe
c46d74be54 New translations minify.mdx (French) 2025-11-10 18:49:42 +01:00
vcoppe
72e949586a New translations minify.mdx (Romanian) 2025-11-10 18:49:41 +01:00
vcoppe
68dacad741 New translations toolbar.mdx (Serbian (Latin)) 2025-11-10 18:48:33 +01:00
vcoppe
7e6505ca73 New translations toolbar.mdx (Chinese Traditional, Hong Kong) 2025-11-10 18:48:32 +01:00
vcoppe
362d83f504 New translations toolbar.mdx (Latvian) 2025-11-10 18:48:31 +01:00
vcoppe
e4879736b7 New translations toolbar.mdx (Thai) 2025-11-10 18:48:29 +01:00
vcoppe
68fe6628c7 New translations toolbar.mdx (Indonesian) 2025-11-10 18:48:28 +01:00
vcoppe
f39ef569be New translations toolbar.mdx (Portuguese, Brazilian) 2025-11-10 18:48:27 +01:00
vcoppe
42c199376a New translations toolbar.mdx (Vietnamese) 2025-11-10 18:48:26 +01:00
vcoppe
cd6b774a8c New translations toolbar.mdx (Chinese Simplified) 2025-11-10 18:48:25 +01:00
vcoppe
62598f94f8 New translations toolbar.mdx (Ukrainian) 2025-11-10 18:48:24 +01:00
vcoppe
aa3b46141f New translations toolbar.mdx (Turkish) 2025-11-10 18:48:21 +01:00
vcoppe
dcaa2aaeab New translations toolbar.mdx (Swedish) 2025-11-10 18:48:20 +01:00
vcoppe
e36f5e47da New translations toolbar.mdx (Russian) 2025-11-10 18:48:19 +01:00
vcoppe
578d7b41b4 New translations toolbar.mdx (Portuguese) 2025-11-10 18:48:18 +01:00
vcoppe
c4f81ce279 New translations toolbar.mdx (Polish) 2025-11-10 18:48:16 +01:00
vcoppe
02a7dbea85 New translations toolbar.mdx (Norwegian) 2025-11-10 18:48:15 +01:00
vcoppe
0305d3fe36 New translations toolbar.mdx (Dutch) 2025-11-10 18:48:13 +01:00
vcoppe
84fd034197 New translations toolbar.mdx (Lithuanian) 2025-11-10 18:48:11 +01:00
vcoppe
61b48e2048 New translations toolbar.mdx (Korean) 2025-11-10 18:48:10 +01:00
vcoppe
c91f85389c New translations toolbar.mdx (Italian) 2025-11-10 18:48:09 +01:00
vcoppe
f3683355a9 New translations toolbar.mdx (Hungarian) 2025-11-10 18:48:08 +01:00
vcoppe
a86da886f4 New translations toolbar.mdx (Hebrew) 2025-11-10 18:48:06 +01:00
vcoppe
7bf8d42eb8 New translations toolbar.mdx (Finnish) 2025-11-10 18:48:05 +01:00
vcoppe
88533e29b9 New translations toolbar.mdx (Basque) 2025-11-10 18:48:03 +01:00
vcoppe
8299dcc881 New translations toolbar.mdx (Greek) 2025-11-10 18:48:01 +01:00
vcoppe
0d0c250fea New translations toolbar.mdx (German) 2025-11-10 18:48:00 +01:00
vcoppe
eb8d86616b New translations toolbar.mdx (Danish) 2025-11-10 18:47:59 +01:00
vcoppe
1edc90810f New translations toolbar.mdx (Czech) 2025-11-10 18:47:58 +01:00
vcoppe
c011d84a35 New translations toolbar.mdx (Catalan) 2025-11-10 18:47:56 +01:00
vcoppe
b97f62ac12 New translations toolbar.mdx (Belarusian) 2025-11-10 18:47:55 +01:00
vcoppe
200a38bdc0 New translations toolbar.mdx (Spanish) 2025-11-10 18:47:54 +01:00
vcoppe
e12c53a90e New translations toolbar.mdx (French) 2025-11-10 18:47:53 +01:00
vcoppe
7892916f56 New translations toolbar.mdx (Romanian) 2025-11-10 18:47:51 +01:00
vcoppe
5d681beab3 New translations translation.mdx (Serbian (Latin)) 2025-11-10 18:45:10 +01:00
vcoppe
a54a2affb3 New translations translation.mdx (Chinese Traditional, Hong Kong) 2025-11-10 18:45:09 +01:00
vcoppe
120062bb85 New translations translation.mdx (Latvian) 2025-11-10 18:45:07 +01:00
vcoppe
a0754d2bd7 New translations translation.mdx (Thai) 2025-11-10 18:45:06 +01:00
vcoppe
f4b13a84d4 New translations translation.mdx (Indonesian) 2025-11-10 18:45:05 +01:00
vcoppe
1c98e27e4d New translations translation.mdx (Portuguese, Brazilian) 2025-11-10 18:45:04 +01:00
vcoppe
6067e25df8 New translations translation.mdx (Vietnamese) 2025-11-10 18:45:02 +01:00
vcoppe
304c50a247 New translations translation.mdx (Chinese Simplified) 2025-11-10 18:45:00 +01:00
vcoppe
274ad8eac2 New translations translation.mdx (Ukrainian) 2025-11-10 18:44:59 +01:00
vcoppe
9946a3fc0e New translations translation.mdx (Turkish) 2025-11-10 18:44:58 +01:00
vcoppe
622d6db968 New translations translation.mdx (Swedish) 2025-11-10 18:44:56 +01:00
vcoppe
d756ce0656 New translations translation.mdx (Russian) 2025-11-10 18:44:55 +01:00
vcoppe
316e776355 New translations translation.mdx (Portuguese) 2025-11-10 18:44:54 +01:00
vcoppe
f861b7ad99 New translations translation.mdx (Polish) 2025-11-10 18:44:53 +01:00
vcoppe
01382e98f3 New translations translation.mdx (Norwegian) 2025-11-10 18:44:52 +01:00
vcoppe
3371442bbc New translations translation.mdx (Dutch) 2025-11-10 18:44:51 +01:00
vcoppe
a81a804364 New translations translation.mdx (Lithuanian) 2025-11-10 18:44:50 +01:00
vcoppe
b24cf5c946 New translations translation.mdx (Korean) 2025-11-10 18:44:48 +01:00
vcoppe
3d60213644 New translations translation.mdx (Italian) 2025-11-10 18:44:47 +01:00
vcoppe
a23822c9df New translations translation.mdx (Hungarian) 2025-11-10 18:44:46 +01:00
vcoppe
9fa69758f1 New translations translation.mdx (Hebrew) 2025-11-10 18:44:45 +01:00
vcoppe
c892d3f134 New translations translation.mdx (Finnish) 2025-11-10 18:44:44 +01:00
vcoppe
73271501dc New translations translation.mdx (Basque) 2025-11-10 18:44:43 +01:00
vcoppe
a62ea520e7 New translations translation.mdx (Greek) 2025-11-10 18:44:42 +01:00
vcoppe
a2a0b3c71e New translations translation.mdx (German) 2025-11-10 18:44:40 +01:00
vcoppe
51bedbe003 New translations translation.mdx (Danish) 2025-11-10 18:44:39 +01:00
vcoppe
7062a3e657 New translations translation.mdx (Czech) 2025-11-10 18:44:38 +01:00
vcoppe
209d95d5da New translations translation.mdx (Catalan) 2025-11-10 18:44:37 +01:00
vcoppe
95ee74ab2b New translations translation.mdx (Belarusian) 2025-11-10 18:44:36 +01:00
vcoppe
9f6b268dce New translations translation.mdx (Spanish) 2025-11-10 18:44:35 +01:00
vcoppe
96e8ebcc10 New translations translation.mdx (French) 2025-11-10 18:44:33 +01:00
vcoppe
ca261c7037 New translations translation.mdx (Romanian) 2025-11-10 18:44:32 +01:00
vcoppe
a95fe13b31 New translations funding.mdx (Serbian (Latin)) 2025-11-10 18:44:09 +01:00
vcoppe
c3fe76adf9 New translations funding.mdx (Chinese Traditional, Hong Kong) 2025-11-10 18:44:07 +01:00
vcoppe
e9b9d51a6e New translations funding.mdx (Latvian) 2025-11-10 18:44:06 +01:00
vcoppe
aff66205e9 New translations funding.mdx (Thai) 2025-11-10 18:44:05 +01:00
vcoppe
aa0c9d65ae New translations funding.mdx (Indonesian) 2025-11-10 18:44:04 +01:00
vcoppe
423bd2122f New translations funding.mdx (Portuguese, Brazilian) 2025-11-10 18:44:02 +01:00
vcoppe
7533910114 New translations funding.mdx (Vietnamese) 2025-11-10 18:44:01 +01:00
vcoppe
0d4d377c22 New translations funding.mdx (Chinese Simplified) 2025-11-10 18:43:59 +01:00
vcoppe
3a576b3ea5 New translations funding.mdx (Ukrainian) 2025-11-10 18:43:58 +01:00
vcoppe
27f37744a0 New translations funding.mdx (Turkish) 2025-11-10 18:43:57 +01:00
vcoppe
012ecad0bb New translations funding.mdx (Swedish) 2025-11-10 18:43:55 +01:00
vcoppe
49b5d5e961 New translations funding.mdx (Russian) 2025-11-10 18:43:54 +01:00
vcoppe
01ca48fe96 New translations funding.mdx (Portuguese) 2025-11-10 18:43:52 +01:00
vcoppe
5abe5045b2 New translations funding.mdx (Polish) 2025-11-10 18:43:51 +01:00
vcoppe
8c79a577b9 New translations funding.mdx (Norwegian) 2025-11-10 18:43:50 +01:00
vcoppe
5b18f1016f New translations funding.mdx (Dutch) 2025-11-10 18:43:49 +01:00
vcoppe
bda58312bb New translations funding.mdx (Lithuanian) 2025-11-10 18:43:47 +01:00
vcoppe
eff352a003 New translations funding.mdx (Korean) 2025-11-10 18:43:46 +01:00
vcoppe
3109d4ff82 New translations funding.mdx (Italian) 2025-11-10 18:43:45 +01:00
vcoppe
c44eda3af6 New translations funding.mdx (Hungarian) 2025-11-10 18:43:44 +01:00
vcoppe
33e8c41c6f New translations funding.mdx (Hebrew) 2025-11-10 18:43:43 +01:00
vcoppe
e0b515ba2b New translations funding.mdx (Finnish) 2025-11-10 18:43:42 +01:00
vcoppe
c6b6ad41fb New translations funding.mdx (Basque) 2025-11-10 18:43:40 +01:00
vcoppe
99576238cd New translations funding.mdx (Greek) 2025-11-10 18:43:39 +01:00
vcoppe
db712650ef New translations funding.mdx (German) 2025-11-10 18:43:38 +01:00
vcoppe
d77e7b07eb New translations funding.mdx (Danish) 2025-11-10 18:43:37 +01:00
vcoppe
47b35fad3f New translations funding.mdx (Czech) 2025-11-10 18:43:36 +01:00
vcoppe
720105ca92 New translations funding.mdx (Catalan) 2025-11-10 18:43:35 +01:00
vcoppe
ce656068e3 New translations funding.mdx (Belarusian) 2025-11-10 18:43:34 +01:00
vcoppe
e69ba76e7d New translations funding.mdx (Spanish) 2025-11-10 18:43:32 +01:00
vcoppe
d1bab213a3 New translations funding.mdx (French) 2025-11-10 18:43:31 +01:00
vcoppe
016a9341ad New translations funding.mdx (Romanian) 2025-11-10 18:43:30 +01:00
vcoppe
81f407ad5f New translations files-and-stats.mdx (Serbian (Latin)) 2025-11-10 18:42:44 +01:00
vcoppe
37ff2f6bdb New translations files-and-stats.mdx (Chinese Traditional, Hong Kong) 2025-11-10 18:42:43 +01:00
vcoppe
13cf1a9551 New translations files-and-stats.mdx (Latvian) 2025-11-10 18:42:41 +01:00
vcoppe
799aaac449 New translations files-and-stats.mdx (Thai) 2025-11-10 18:42:40 +01:00
vcoppe
fbe430e4cf New translations files-and-stats.mdx (Indonesian) 2025-11-10 18:42:39 +01:00
vcoppe
511746620e New translations files-and-stats.mdx (Portuguese, Brazilian) 2025-11-10 18:42:38 +01:00
vcoppe
fed5b648f8 New translations files-and-stats.mdx (Vietnamese) 2025-11-10 18:42:37 +01:00
vcoppe
1f601743f6 New translations files-and-stats.mdx (Chinese Simplified) 2025-11-10 18:42:36 +01:00
vcoppe
db3ccf5da7 New translations files-and-stats.mdx (Ukrainian) 2025-11-10 18:42:34 +01:00
vcoppe
038898d580 New translations files-and-stats.mdx (Turkish) 2025-11-10 18:42:33 +01:00
vcoppe
dafbc6bc51 New translations files-and-stats.mdx (Swedish) 2025-11-10 18:42:32 +01:00
vcoppe
3d8271e7a1 New translations files-and-stats.mdx (Russian) 2025-11-10 18:42:31 +01:00
vcoppe
e2ade95219 New translations files-and-stats.mdx (Portuguese) 2025-11-10 18:42:30 +01:00
vcoppe
604971a576 New translations files-and-stats.mdx (Polish) 2025-11-10 18:42:28 +01:00
vcoppe
76173103af New translations files-and-stats.mdx (Norwegian) 2025-11-10 18:42:27 +01:00
vcoppe
3fcbc60232 New translations routing.mdx (Czech) 2025-11-10 18:42:26 +01:00
vcoppe
835bdb39d0 New translations files-and-stats.mdx (Dutch) 2025-11-10 18:42:25 +01:00
vcoppe
200f09c5d2 New translations files-and-stats.mdx (Lithuanian) 2025-11-10 18:42:23 +01:00
vcoppe
99a008f122 New translations files-and-stats.mdx (Korean) 2025-11-10 18:42:22 +01:00
vcoppe
9b71e89ba0 New translations files-and-stats.mdx (Italian) 2025-11-10 18:42:20 +01:00
vcoppe
9955369cfa New translations files-and-stats.mdx (Hungarian) 2025-11-10 18:42:18 +01:00
vcoppe
6ef6ffab66 New translations files-and-stats.mdx (Hebrew) 2025-11-10 18:42:17 +01:00
vcoppe
0dd46f0f20 New translations files-and-stats.mdx (Finnish) 2025-11-10 18:42:15 +01:00
vcoppe
cce0d85dd4 New translations files-and-stats.mdx (Basque) 2025-11-10 18:42:14 +01:00
vcoppe
1ed3eae038 New translations files-and-stats.mdx (Greek) 2025-11-10 18:42:12 +01:00
vcoppe
2854c24197 New translations files-and-stats.mdx (German) 2025-11-10 18:42:10 +01:00
vcoppe
4b90ddda2a New translations files-and-stats.mdx (Danish) 2025-11-10 18:42:09 +01:00
vcoppe
552c39e583 New translations files-and-stats.mdx (Czech) 2025-11-10 18:42:08 +01:00
vcoppe
5176e459b5 New translations files-and-stats.mdx (Catalan) 2025-11-10 18:42:06 +01:00
vcoppe
39515d0600 New translations files-and-stats.mdx (Belarusian) 2025-11-10 18:42:05 +01:00
vcoppe
5314949394 New translations files-and-stats.mdx (Spanish) 2025-11-10 18:42:02 +01:00
vcoppe
59e5be749c New translations files-and-stats.mdx (French) 2025-11-10 18:42:01 +01:00
vcoppe
da252a0070 New translations files-and-stats.mdx (Romanian) 2025-11-10 18:42:00 +01:00
vcoppe
b6199e430c New translations en.json (Serbian (Latin)) 2025-11-10 18:41:58 +01:00
vcoppe
e260a68c26 New translations en.json (Chinese Traditional, Hong Kong) 2025-11-10 18:41:57 +01:00
vcoppe
570cb2deaf New translations en.json (Latvian) 2025-11-10 18:41:55 +01:00
vcoppe
7a36f03fb5 New translations en.json (Thai) 2025-11-10 18:41:54 +01:00
vcoppe
6c3058ba97 New translations en.json (Indonesian) 2025-11-10 18:41:53 +01:00
vcoppe
48a1034c12 New translations en.json (Portuguese, Brazilian) 2025-11-10 18:41:52 +01:00
vcoppe
aaddb50ab9 New translations en.json (Vietnamese) 2025-11-10 18:41:51 +01:00
vcoppe
a947586cfe New translations en.json (Chinese Simplified) 2025-11-10 18:41:49 +01:00
vcoppe
f3cfa14a59 New translations en.json (Ukrainian) 2025-11-10 18:41:48 +01:00
vcoppe
a2c0a77c53 New translations en.json (Turkish) 2025-11-10 18:41:47 +01:00
vcoppe
c078f9d5cb New translations en.json (Swedish) 2025-11-10 18:41:46 +01:00
vcoppe
08eab8a157 New translations en.json (Russian) 2025-11-10 18:41:45 +01:00
vcoppe
9c36f234bc New translations en.json (Portuguese) 2025-11-10 18:41:44 +01:00
vcoppe
36b81d0e2a New translations en.json (Polish) 2025-11-10 18:41:42 +01:00
vcoppe
4432c14377 New translations en.json (Norwegian) 2025-11-10 18:41:41 +01:00
vcoppe
99e6dd5ca3 New translations en.json (Dutch) 2025-11-10 18:41:40 +01:00
vcoppe
6327a25aec New translations en.json (Lithuanian) 2025-11-10 18:41:39 +01:00
vcoppe
40989de7f5 New translations en.json (Korean) 2025-11-10 18:41:38 +01:00
vcoppe
58af44e795 New translations en.json (Italian) 2025-11-10 18:41:36 +01:00
vcoppe
4910cc05f8 New translations en.json (Hungarian) 2025-11-10 18:41:35 +01:00
vcoppe
164ee24d16 New translations en.json (Hebrew) 2025-11-10 18:41:34 +01:00
vcoppe
0ffda4ab7c New translations en.json (Finnish) 2025-11-10 18:41:33 +01:00
vcoppe
4694a6271d New translations en.json (Basque) 2025-11-10 18:41:32 +01:00
vcoppe
2f50bc747a New translations en.json (Greek) 2025-11-10 18:41:30 +01:00
vcoppe
a13f621a81 New translations en.json (German) 2025-11-10 18:41:29 +01:00
vcoppe
0cc520cc67 New translations en.json (Czech) 2025-11-10 18:41:28 +01:00
vcoppe
c9451c3f2d New translations en.json (Catalan) 2025-11-10 18:41:26 +01:00
vcoppe
8da53ffda2 New translations en.json (Belarusian) 2025-11-10 18:41:25 +01:00
vcoppe
4319761687 New translations en.json (Spanish) 2025-11-10 18:41:24 +01:00
vcoppe
a1f3227cd9 New translations en.json (Romanian) 2025-11-10 18:41:22 +01:00
vcoppe
b07f87c920 New translations en.json (Danish) 2025-11-10 18:41:21 +01:00
vcoppe
9c8f23eb64 New translations en.json (French) 2025-11-10 18:41:19 +01:00
vcoppe
36c6c623de fix crawling 2025-11-10 18:37:31 +01:00
vcoppe
e334419e24 fix import 2025-11-10 16:54:09 +01:00
vcoppe
01240c4f2a fix spacing 2025-11-10 16:47:16 +01:00
vcoppe
431a9ce827 migrate component 2025-11-10 16:45:50 +01:00
vcoppe
20ab41c3b4 update lucid icon name 2025-11-10 16:23:35 +01:00
vcoppe
3f4ea27be2 update gitignore 2025-11-10 16:07:06 +01:00
vcoppe
5bb5b2f8c8 fix destroy 2025-11-10 16:03:03 +01:00
vcoppe
e9bb9e27bb fix spacing 2025-11-10 16:02:54 +01:00
vcoppe
ee1dd1fae7 migrate component 2025-11-10 15:47:43 +01:00
vcoppe
738530a960 remove active layers when removed from selection 2025-11-10 15:26:12 +01:00
vcoppe
16023b0688 fix some typescript errors 2025-11-10 13:11:44 +01:00
vcoppe
bce7b5984f fix footer spacing 2025-11-10 11:56:28 +01:00
vcoppe
4e5d7d391a small style fixes 2025-11-10 11:51:16 +01:00
vcoppe
0554a85e01 fix toolbar animation 2025-11-10 11:11:37 +01:00
vcoppe
2d232b3c4b New translations routing.mdx (Czech) 2025-11-09 23:00:54 +01:00
vcoppe
b2a5462372 improve website link 2025-11-09 20:14:52 +01:00
vcoppe
9d61f51270 fix spacing 2025-11-09 20:11:35 +01:00
vcoppe
a0eb7d61cc remove swedish map 2025-11-09 20:05:00 +01:00
vcoppe
9861b319f4 fix popup max height 2025-11-09 19:52:02 +01:00
vcoppe
b04e0f10b2 resize map on load 2025-11-09 19:20:10 +01:00
vcoppe
e6d089b34b close -> delete 2025-11-09 19:09:16 +01:00
vcoppe
9df014e986 fix sortable 2025-11-09 19:00:33 +01:00
vcoppe
39b8d2e70d initialize missing settings 2025-11-09 18:45:20 +01:00
vcoppe
59710d2e1a fix embedding + playground 2025-11-09 18:03:27 +01:00
vcoppe
712dc9bb34 New translations en.json (Danish) 2025-11-04 06:53:16 +01:00
vcoppe
5c338d53ae New translations en.json (Danish) 2025-11-04 05:49:17 +01:00
vcoppe
ec3eb387e5 sortable file list, to be fixed 2025-11-02 16:01:17 +01:00
vcoppe
722cf58486 progress 2025-10-26 12:12:23 +01:00
vcoppe
17e5347d55 update date 2025-10-26 11:04:23 +01:00
vcoppe
2e828dfde3 migrate custom layers sortable list from sortablejs to svelte-dnd-action 2025-10-25 18:34:24 +02:00
vcoppe
1b035bcde3 fix metadata and style dialogs 2025-10-25 17:44:41 +02:00
vcoppe
30981130c9 fix menu not closing 2025-10-25 15:05:11 +02:00
vcoppe
6db8696a36 small fixes for tools 2025-10-24 20:07:15 +02:00
vcoppe
9c83dcafa7 fix gpx markers 2025-10-24 20:06:54 +02:00
vcoppe
1db9ecafef fix coordinates popup 2025-10-23 19:07:32 +02:00
vcoppe
aa624e2c60 renaming 2025-10-23 18:58:33 +02:00
vcoppe
bde7e3e8aa rename files 2025-10-23 18:56:04 +02:00
vcoppe
b2b3e1b153 clean scissors logic 2025-10-23 18:54:01 +02:00
vcoppe
76b3d09320 fix layer control 2025-10-23 18:42:10 +02:00
vcoppe
899dcddd2e fix custom layer creation 2025-10-22 21:54:22 +02:00
vcoppe
9edae7e1b8 fix elevation profile 2025-10-22 19:05:20 +02:00
vcoppe
8d26842aab New translations en.json (Russian) 2025-10-21 12:25:32 +02:00
vcoppe
76e654304b New translations en.json (Russian) 2025-10-21 10:35:36 +02:00
vcoppe
d73b684999 move file 2025-10-20 20:17:47 +02:00
vcoppe
a78bd8d7ca minor fixes 2025-10-20 19:53:42 +02:00
vcoppe
2ca53c1004 fix 2025-10-19 16:51:30 +02:00
vcoppe
d621516d59 fix separator 2025-10-19 16:47:23 +02:00
vcoppe
ef310cc3cc fix elevation profile toggle 2025-10-19 16:45:12 +02:00
vcoppe
776c867c0b fix xs selector 2025-10-19 16:44:15 +02:00
vcoppe
8abe0ec333 fix fill 2025-10-19 16:19:22 +02:00
vcoppe
e57ced0ce7 bounds management 2025-10-19 16:14:05 +02:00
vcoppe
117c46341b add utagawaVTT layer 2025-10-19 14:19:44 +02:00
vcoppe
ba1ac69151 start/end & distance markers 2025-10-19 14:15:52 +02:00
vcoppe
a8d3af35de fix gap 2025-10-19 13:55:01 +02:00
vcoppe
307eed86e3 fix export 2025-10-19 13:51:56 +02:00
vcoppe
3f103323c7 fix hiding 2025-10-19 13:45:05 +02:00
vcoppe
05df3ca064 start fixing elevation profile 2025-10-18 20:12:19 +02:00
vcoppe
356884cf58 starting to fix time tool 2025-10-18 19:21:10 +02:00
vcoppe
e68da7354e update shadcn components 2025-10-18 18:51:11 +02:00
vcoppe
c59cd66141 fix tools 2025-10-18 16:10:08 +02:00
vcoppe
9fa8fe5767 fix copied & cut stores 2025-10-18 09:36:55 +02:00
vcoppe
4ae0bc25c2 update selection on file removal 2025-10-18 00:46:59 +02:00
vcoppe
de81a8940e statistics 2025-10-18 00:31:14 +02:00
vcoppe
a73da0d81d progress 2025-10-17 23:54:45 +02:00
vcoppe
32ba679719 New translations en.json (Korean) 2025-10-17 09:13:10 +02:00
vcoppe
cac0fefcdb New translations en.json (Chinese Traditional, Hong Kong) 2025-10-14 02:30:52 +02:00
vcoppe
498c76dd96 New translations en.json (Chinese Simplified) 2025-10-14 02:30:51 +02:00
vcoppe
7526182304 New translations en.json (Romanian) 2025-10-12 09:39:34 +02:00
vcoppe
d46bbd9cbf New translations en.json (Romanian) 2025-10-12 08:41:16 +02:00
vcoppe
e98b537499 New translations en.json (Ukrainian) 2025-10-09 12:12:30 +02:00
vcoppe
fc9d8509e5 New translations en.json (Ukrainian) 2025-10-09 10:21:48 +02:00
vcoppe
7c6bbb61b5 New translations en.json (Ukrainian) 2025-10-08 21:12:54 +02:00
vcoppe
8501ddc87f New translations faq.mdx (Polish) 2025-10-07 19:48:49 +02:00
vcoppe
7d9b94525e New translations time.mdx (Polish) 2025-10-07 19:48:48 +02:00
vcoppe
eb02f0eadf New translations scissors.mdx (Polish) 2025-10-07 19:48:47 +02:00
vcoppe
69a8ba5aec New translations routing.mdx (Polish) 2025-10-07 19:48:46 +02:00
vcoppe
fe49b8e618 New translations merge.mdx (Polish) 2025-10-07 19:48:45 +02:00
vcoppe
26bf4dde5f New translations extract.mdx (Polish) 2025-10-07 19:48:44 +02:00
vcoppe
e9b73050ba New translations toolbar.mdx (Polish) 2025-10-07 19:48:43 +02:00
vcoppe
bacd0ab43f New translations view.mdx (Polish) 2025-10-07 19:48:42 +02:00
vcoppe
e438051371 New translations file.mdx (Polish) 2025-10-07 19:48:40 +02:00
vcoppe
314155593d New translations edit.mdx (Polish) 2025-10-07 19:48:39 +02:00
vcoppe
787f819ce0 New translations menu.mdx (Polish) 2025-10-07 19:48:38 +02:00
vcoppe
3632a62ea3 New translations integration.mdx (Polish) 2025-10-07 19:48:37 +02:00
vcoppe
c7294df007 New translations gpx.mdx (Polish) 2025-10-07 19:48:36 +02:00
vcoppe
e3ad7fe3c0 New translations getting-started.mdx (Polish) 2025-10-07 19:48:34 +02:00
vcoppe
6213683ddf New translations files-and-stats.mdx (Polish) 2025-10-07 19:48:33 +02:00
vcoppe
a4ddfc9970 New translations en.json (Polish) 2025-10-07 19:48:32 +02:00
vcoppe
7ff271f9b9 New translations view.mdx (Spanish) 2025-10-07 02:54:04 +02:00
vcoppe
d75cdd63a9 New translations file.mdx (Spanish) 2025-10-07 02:54:02 +02:00
vcoppe
0a7575d1e4 New translations integration.mdx (Spanish) 2025-10-07 02:54:01 +02:00
vcoppe
ec3022d8ad New translations en.json (Spanish) 2025-10-07 01:50:49 +02:00
vcoppe
0733562c0d progress 2025-10-05 19:34:05 +02:00
vcoppe
d42103b91b New translations en.json (Ukrainian) 2025-10-03 23:57:07 +02:00
vcoppe
00f7d08b04 New translations en.json (Ukrainian) 2025-10-03 22:36:21 +02:00
vcoppe
408cc383cb New translations en.json (Portuguese) 2025-10-03 17:56:00 +02:00
vcoppe
5c926d0ac6 New translations en.json (Ukrainian) 2025-09-23 18:44:01 +02:00
vcoppe
5cb88782fc New translations en.json (Ukrainian) 2025-09-23 15:59:34 +02:00
vcoppe
5eef4e9ece New translations en.json (Russian) 2025-09-20 17:13:17 +02:00
vcoppe
04a2124141 New translations en.json (Italian) 2025-09-20 17:13:15 +02:00
vcoppe
1b6229b2a1 New translations elevation.mdx (Italian) 2025-09-20 16:10:28 +02:00
vcoppe
bca6db50a7 New translations en.json (Italian) 2025-09-20 16:10:27 +02:00
vcoppe
f3aae26996 New translations settings.mdx (Chinese Simplified) 2025-09-10 10:34:12 +02:00
vcoppe
f3c17a8e0f New translations en.json (Indonesian) 2025-09-05 04:13:37 +02:00
vcoppe
d6b24f8753 New translations en.json (Indonesian) 2025-09-05 03:11:41 +02:00
vcoppe
253db0a303 New translations en.json (Norwegian) 2025-09-04 18:13:32 +02:00
vcoppe
8499e52461 New translations en.json (Dutch) 2025-09-01 08:57:32 +02:00
vcoppe
d0153179a9 New translations en.json (Indonesian) 2025-08-29 18:50:34 +02:00
vcoppe
264d03727e New translations edit.mdx (Chinese Traditional, Hong Kong) 2025-08-13 18:27:34 +02:00
vcoppe
544405d9b9 New translations en.json (Chinese Traditional, Hong Kong) 2025-08-13 16:32:09 +02:00
vcoppe
24488a3b67 New translations en.json (Indonesian) 2025-08-08 14:15:35 +02:00
vcoppe
ae78185b29 New translations en.json (Indonesian) 2025-08-08 12:50:22 +02:00
vcoppe
7f682b24ef New translations en.json (Indonesian) 2025-08-04 04:32:52 +02:00
vcoppe
d42a52d8cf New translations en.json (Norwegian) 2025-08-03 16:13:45 +02:00
vcoppe
b85df15890 New translations en.json (Norwegian) 2025-08-03 15:00:56 +02:00
vcoppe
393499f34f New translations en.json (Indonesian) 2025-08-02 13:58:57 +02:00
vcoppe
c656d0f9b5 New translations en.json (Indonesian) 2025-08-02 12:40:02 +02:00
vcoppe
32017a8859 New translations en.json (Indonesian) 2025-08-02 11:35:56 +02:00
vcoppe
d87c5b1140 New translations en.json (Norwegian) 2025-08-01 22:28:15 +02:00
vcoppe
f59f783d3f New translations en.json (Norwegian) 2025-08-01 21:18:36 +02:00
vcoppe
ec298eac61 New translations elevation.mdx (Indonesian) 2025-08-01 16:14:37 +02:00
vcoppe
81a25bb4ee New translations faq.mdx (Indonesian) 2025-08-01 16:14:36 +02:00
vcoppe
e99f044e45 New translations time.mdx (Indonesian) 2025-08-01 16:14:35 +02:00
vcoppe
5ae25a5fd9 New translations scissors.mdx (Indonesian) 2025-08-01 16:14:34 +02:00
vcoppe
e9d1cb4907 New translations routing.mdx (Indonesian) 2025-08-01 16:14:33 +02:00
vcoppe
99f8ca2dca New translations poi.mdx (Indonesian) 2025-08-01 16:14:31 +02:00
vcoppe
ddea5d38b5 New translations minify.mdx (Indonesian) 2025-08-01 16:14:30 +02:00
vcoppe
31d2b83550 New translations merge.mdx (Indonesian) 2025-08-01 16:14:29 +02:00
vcoppe
5535e56ed2 New translations extract.mdx (Indonesian) 2025-08-01 16:14:28 +02:00
vcoppe
d740b95dbc New translations clean.mdx (Indonesian) 2025-08-01 16:14:26 +02:00
vcoppe
ae92e9a945 New translations toolbar.mdx (Indonesian) 2025-08-01 16:14:25 +02:00
vcoppe
29730c3896 New translations view.mdx (Indonesian) 2025-08-01 16:14:24 +02:00
vcoppe
a5ae8270f0 New translations settings.mdx (Indonesian) 2025-08-01 16:14:23 +02:00
vcoppe
54f5fa6432 New translations file.mdx (Indonesian) 2025-08-01 16:14:22 +02:00
vcoppe
0260644063 New translations edit.mdx (Indonesian) 2025-08-01 16:14:20 +02:00
vcoppe
267fc03a82 New translations menu.mdx (Indonesian) 2025-08-01 16:14:19 +02:00
vcoppe
bf1537584c New translations map-controls.mdx (Indonesian) 2025-08-01 16:14:18 +02:00
vcoppe
9ee7825022 New translations integration.mdx (Indonesian) 2025-08-01 16:14:17 +02:00
vcoppe
2be0c42dd1 New translations translation.mdx (Indonesian) 2025-08-01 16:14:16 +02:00
vcoppe
3423c053a2 New translations mapbox.mdx (Indonesian) 2025-08-01 16:14:15 +02:00
vcoppe
26923cca00 New translations funding.mdx (Indonesian) 2025-08-01 16:14:14 +02:00
vcoppe
36e027659c New translations gpx.mdx (Indonesian) 2025-08-01 16:14:13 +02:00
vcoppe
f447dccdb4 New translations getting-started.mdx (Indonesian) 2025-08-01 16:14:11 +02:00
vcoppe
69eae32851 New translations files-and-stats.mdx (Indonesian) 2025-08-01 16:14:10 +02:00
vcoppe
aa2fcfb8cb New translations en.json (Indonesian) 2025-08-01 16:14:09 +02:00
vcoppe
fae5ef2a41 New translations en.json (Norwegian) 2025-07-31 23:43:31 +02:00
vcoppe
7251ca7d2d New translations toolbar.mdx (Norwegian) 2025-07-31 22:34:29 +02:00
vcoppe
7cdbd919bf New translations en.json (Norwegian) 2025-07-31 22:34:27 +02:00
vcoppe
d450f95602 New translations en.json (Dutch) 2025-07-31 14:26:59 +02:00
vcoppe
5a65201971 New translations en.json (Thai) 2025-07-30 18:35:07 +02:00
vcoppe
d303b8db3e New translations gpx.mdx (Portuguese) 2025-07-20 19:33:06 +02:00
vcoppe
06baa33827 New translations gpx.mdx (Portuguese) 2025-07-20 18:31:13 +02:00
vcoppe
42743e637e New translations en.json (French) 2025-07-18 16:38:10 +02:00
vcoppe
9969fd7dec New translations edit.mdx (Swedish) 2025-07-17 23:06:28 +02:00
vcoppe
fc6d5c2a1d New translations en.json (Basque) 2025-07-16 07:51:58 +02:00
vcoppe
f8abb1ca24 New translations elevation.mdx (Thai) 2025-07-15 14:10:56 +02:00
vcoppe
a5af38ae3d New translations faq.mdx (Thai) 2025-07-15 14:10:55 +02:00
vcoppe
aab70951dc New translations time.mdx (Thai) 2025-07-15 14:10:54 +02:00
vcoppe
334cacf93c New translations scissors.mdx (Thai) 2025-07-15 14:10:52 +02:00
vcoppe
53024012fc New translations routing.mdx (Thai) 2025-07-15 14:10:51 +02:00
vcoppe
86a72f77c1 New translations poi.mdx (Thai) 2025-07-15 14:10:50 +02:00
vcoppe
bc11a5ad0a New translations minify.mdx (Thai) 2025-07-15 14:10:49 +02:00
vcoppe
8f2d217fd4 New translations merge.mdx (Thai) 2025-07-15 14:10:47 +02:00
vcoppe
183727cd50 New translations extract.mdx (Thai) 2025-07-15 14:10:46 +02:00
vcoppe
676e87591a New translations clean.mdx (Thai) 2025-07-15 14:10:44 +02:00
vcoppe
8c05fc4da0 New translations toolbar.mdx (Thai) 2025-07-15 14:10:43 +02:00
vcoppe
2bab06561e New translations view.mdx (Thai) 2025-07-15 14:10:42 +02:00
vcoppe
dfa7e2f5bb New translations settings.mdx (Thai) 2025-07-15 14:10:41 +02:00
vcoppe
78bece5616 New translations file.mdx (Thai) 2025-07-15 14:10:39 +02:00
vcoppe
eeea15e373 New translations edit.mdx (Thai) 2025-07-15 14:10:38 +02:00
vcoppe
80cd513ab7 New translations menu.mdx (Thai) 2025-07-15 14:10:37 +02:00
vcoppe
942ef1615e New translations map-controls.mdx (Thai) 2025-07-15 14:10:35 +02:00
vcoppe
a354698022 New translations integration.mdx (Thai) 2025-07-15 14:10:34 +02:00
vcoppe
0cdea488c9 New translations translation.mdx (Thai) 2025-07-15 14:10:33 +02:00
vcoppe
4f4291ac47 New translations mapbox.mdx (Thai) 2025-07-15 14:10:32 +02:00
vcoppe
bf0cf03091 New translations funding.mdx (Thai) 2025-07-15 14:10:30 +02:00
vcoppe
f7da09f20f New translations gpx.mdx (Thai) 2025-07-15 14:10:28 +02:00
vcoppe
be1529331c New translations getting-started.mdx (Thai) 2025-07-15 14:10:27 +02:00
vcoppe
301d658a29 New translations files-and-stats.mdx (Thai) 2025-07-15 14:10:26 +02:00
vcoppe
1cc54e5b2c New translations en.json (Thai) 2025-07-15 14:10:25 +02:00
vcoppe
65a7fd21e7 New translations en.json (Italian) 2025-07-14 12:56:13 +02:00
vcoppe
856537c0cd New translations en.json (Ukrainian) 2025-07-10 01:33:15 +02:00
vcoppe
b2a88e0063 New translations en.json (Ukrainian) 2025-07-10 00:32:33 +02:00
vcoppe
85a7068785 New translations en.json (Ukrainian) 2025-07-09 12:14:44 +02:00
vcoppe
cbb733d99a New translations settings.mdx (Ukrainian) 2025-07-07 18:53:29 +02:00
vcoppe
ce88c94a19 New translations edit.mdx (Ukrainian) 2025-07-07 18:53:28 +02:00
vcoppe
16516915d8 New translations translation.mdx (Ukrainian) 2025-07-07 18:53:27 +02:00
vcoppe
6addb8da23 New translations mapbox.mdx (Ukrainian) 2025-07-07 18:53:26 +02:00
vcoppe
bc7f664fd8 New translations funding.mdx (Ukrainian) 2025-07-07 17:28:09 +02:00
vcoppe
aac17aa33c New translations en.json (Ukrainian) 2025-07-07 17:28:08 +02:00
vcoppe
825500e207 New translations en.json (Ukrainian) 2025-07-07 15:46:23 +02:00
vcoppe
4d42016c72 New translations en.json (Italian) 2025-06-28 14:49:34 +02:00
vcoppe
9d665df602 New translations poi.mdx (Polish) 2025-06-23 23:44:31 +02:00
vcoppe
9087f69fb0 New translations minify.mdx (Polish) 2025-06-23 23:44:30 +02:00
vcoppe
2a06f6a214 New translations clean.mdx (Polish) 2025-06-23 23:44:29 +02:00
vcoppe
78a8428bd0 New translations toolbar.mdx (Polish) 2025-06-23 23:44:28 +02:00
vcoppe
0d235768fa New translations menu.mdx (Polish) 2025-06-23 23:44:26 +02:00
vcoppe
af092bbdec New translations edit.mdx (Polish) 2025-06-23 22:23:55 +02:00
vcoppe
1cc07901f6 progress 2025-06-21 21:07:36 +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
f0230d4634 commit before upgrading to tailwind 4 2025-06-08 16:32:41 +02:00
vcoppe
228ad1044e remove svelte-i18n dependency, replace with minimalistic implementation 2025-06-08 13:49:39 +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
1171 changed files with 60752 additions and 42513 deletions

View File

@@ -1,63 +1,63 @@
name: Deploy to GitHub Pages
on:
push:
branches: 'main'
push:
branches: 'main'
jobs:
build_site:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
build_site:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: |
gpx/package-lock.json
website/package-lock.json
- name: Install dependencies for gpx
run: npm install --prefix gpx
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: npm
cache-dependency-path: |
gpx/package-lock.json
website/package-lock.json
- name: Build gpx
run: npm run build --prefix gpx
- name: Install dependencies for gpx
run: npm install --prefix gpx
- name: Install dependencies for website
run: npm install --prefix website
- name: Build gpx
run: npm run build --prefix gpx
- name: Create env file
run: |
touch website/.env
echo PUBLIC_MAPBOX_TOKEN=${{ secrets.PUBLIC_MAPBOX_TOKEN }} >> website/.env
cat website/.env
- name: Install dependencies for website
run: npm install --prefix website
- name: Build website
env:
BASE_PATH: '/${{ github.event.repository.name }}'
run: |
npm run build --prefix website
- name: Create env file
run: |
touch website/.env
echo PUBLIC_MAPBOX_TOKEN=${{ secrets.PUBLIC_MAPBOX_TOKEN }} >> website/.env
cat website/.env
- name: Upload Artifacts
uses: actions/upload-pages-artifact@v3
with:
path: 'website/build/'
- name: Build website
env:
BASE_PATH: ''
run: |
npm run build --prefix website
deploy:
needs: build_site
runs-on: ubuntu-latest
- name: Upload Artifacts
uses: actions/upload-pages-artifact@v4
with:
path: 'website/build/'
permissions:
pages: write
id-token: write
deploy:
needs: build_site
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
permissions:
pages: write
id-token: write
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4

View File

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

16
.prettierrc Normal file
View File

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

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

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

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

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

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 gpx.studio
Copyright (c) 2025 gpx.studio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -3,11 +3,11 @@
<img alt="Logo of gpx.studio." src="website/static/logo.svg">
</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)
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
@@ -26,8 +26,9 @@ Any help is greatly appreciated!
## Development
The code is split into two parts:
- `gpx`: a Typescript library for parsing and manipulating GPX files,
- `website`: the website itself, which is a [SvelteKit](https://kit.svelte.dev/) application.
- `gpx`: a Typescript library for parsing and manipulating GPX files,
- `website`: the website itself, which is a [SvelteKit](https://kit.svelte.dev/) application.
You will need [Node.js](https://nodejs.org/) to build and run these two parts.
@@ -54,24 +55,25 @@ npm run dev
This project has been made possible thanks to the following open source projects:
- Development:
- [Svelte](https://github.com/sveltejs/svelte) and [SvelteKit](https://github.com/sveltejs/kit) — seamless development experience
- [MDsveX](https://github.com/pngwn/MDsveX) — allowing a Markdown-based documentation
- [svelte-i18n](https://github.com/kaisermann/svelte-i18n) — easy localization
- Design:
- [shadcn-svelte](https://github.com/huntabyte/shadcn-svelte) — beautiful components
- [lucide-svelte](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-svelte) — beautiful icons
- [tailwindcss](https://github.com/tailwindlabs/tailwindcss) — easy styling
- [Chart.js](https://github.com/chartjs/Chart.js) — beautiful and fast charts
- Logic:
- [immer](https://github.com/immerjs/immer) — complex state management
- [Dexie.js](https://github.com/dexie/Dexie.js) — IndexedDB wrapper
- [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) — fast GPX file parsing
- [SortableJS](https://github.com/SortableJS/Sortable) — creating a sortable file tree
- Mapping:
- [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js) — beautiful and fast interactive maps
- [brouter](https://github.com/abrensch/brouter) — routing engine
- [OpenStreetMap](https://www.openstreetmap.org) — map data used by Mapbox and brouter
- Development:
- [Svelte](https://github.com/sveltejs/svelte) and [SvelteKit](https://github.com/sveltejs/kit) — seamless development experience
- [MDsveX](https://github.com/pngwn/MDsveX) — allowing a Markdown-based documentation
- Design:
- [shadcn-svelte](https://github.com/huntabyte/shadcn-svelte) — beautiful components
- [@lucide/svelte](https://github.com/lucide-icons/lucide/tree/main/packages/svelte) — beautiful icons
- [tailwindcss](https://github.com/tailwindlabs/tailwindcss) — easy styling
- [Chart.js](https://github.com/chartjs/Chart.js) — beautiful and fast charts
- Logic:
- [immer](https://github.com/immerjs/immer) — complex state management
- [Dexie.js](https://github.com/dexie/Dexie.js) — IndexedDB wrapper
- [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) — fast GPX file parsing
- [SortableJS](https://github.com/SortableJS/Sortable) — creating a sortable file tree
- Mapping:
- [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js) — beautiful and fast interactive maps
- [brouter](https://github.com/abrensch/brouter) — routing engine
- [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

1673
gpx/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,33 +1,48 @@
import { TrackPoint } from "./gpx";
import { Coordinates } from "./types";
import { TrackPoint } from './gpx';
import { Coordinates } from './types';
export type SimplifiedTrackPoint = { point: TrackPoint, distance?: number };
export type SimplifiedTrackPoint = { point: TrackPoint; distance?: number };
const earthRadius = 6371008.8;
export function ramerDouglasPeucker(points: TrackPoint[], epsilon: number = 50, measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number = crossarcDistance): SimplifiedTrackPoint[] {
export function ramerDouglasPeucker(
points: TrackPoint[],
epsilon: number = 50,
measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number = crossarcDistance
): SimplifiedTrackPoint[] {
if (points.length == 0) {
return [];
} else if (points.length == 1) {
return [{
point: points[0]
}];
return [
{
point: points[0],
},
];
}
let simplified = [{
point: points[0]
}];
let simplified = [
{
point: points[0],
},
];
ramerDouglasPeuckerRecursive(points, epsilon, measure, 0, points.length - 1, simplified);
simplified.push({
point: points[points.length - 1]
point: points[points.length - 1],
});
return simplified;
}
function ramerDouglasPeuckerRecursive(points: TrackPoint[], epsilon: number, measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number, start: number, end: number, simplified: SimplifiedTrackPoint[]) {
function ramerDouglasPeuckerRecursive(
points: TrackPoint[],
epsilon: number,
measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number,
start: number,
end: number,
simplified: SimplifiedTrackPoint[]
) {
let largest = {
index: 0,
distance: 0
distance: 0,
};
for (let i = start + 1; i < end; i++) {
@@ -45,12 +60,20 @@ function ramerDouglasPeuckerRecursive(points: TrackPoint[], epsilon: number, mea
}
}
export function crossarcDistance(point1: TrackPoint, point2: TrackPoint, point3: TrackPoint | Coordinates): number {
return crossarc(point1.getCoordinates(), point2.getCoordinates(), point3 instanceof TrackPoint ? point3.getCoordinates() : point3);
export function crossarcDistance(
point1: TrackPoint,
point2: TrackPoint,
point3: TrackPoint | Coordinates
): number {
return crossarc(
point1.getCoordinates(),
point2.getCoordinates(),
point3 instanceof TrackPoint ? point3.getCoordinates() : point3
);
}
function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number {
// Calculates the shortest distance in meters
// Calculates the shortest distance in meters
// between an arc (defined by p1 and p2) and a third point, p3.
// Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees.
@@ -74,7 +97,7 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
}
// Is relative bearing obtuse?
if (diff > (Math.PI / 2)) {
if (diff > Math.PI / 2) {
return dis13;
}
@@ -83,7 +106,8 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
// Is p4 beyond the arc?
let dis12 = distance(lat1, lon1, lat2, lon2);
let dis14 = Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
let dis14 =
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
if (dis14 > dis12) {
return distance(lat2, lon2, lat3, lon3);
} else {
@@ -93,12 +117,85 @@ function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates)
function distance(latA: number, lonA: number, latB: number, lonB: number): number {
// Finds the distance between two lat / lon points.
return Math.acos(Math.sin(latA) * Math.sin(latB) + Math.cos(latA) * Math.cos(latB) * Math.cos(lonB - lonA)) * earthRadius;
return (
Math.acos(
Math.sin(latA) * Math.sin(latB) +
Math.cos(latA) * Math.cos(latB) * Math.cos(lonB - lonA)
) * earthRadius
);
}
function bearing(latA: number, lonA: number, latB: number, lonB: number): number {
// Finds the bearing from one lat / lon point to another.
return Math.atan2(Math.sin(lonB - lonA) * Math.cos(latB),
Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA));
}
return Math.atan2(
Math.sin(lonB - lonA) * Math.cos(latB),
Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA)
);
}
export function projectedPoint(
point1: TrackPoint,
point2: TrackPoint,
point3: TrackPoint | Coordinates
): Coordinates {
return projected(
point1.getCoordinates(),
point2.getCoordinates(),
point3 instanceof TrackPoint ? point3.getCoordinates() : point3
);
}
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 };
}
}

View File

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

View File

@@ -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>

View File

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

View File

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

View File

@@ -1,31 +1,31 @@
/** @type { import("eslint").Linter.Config } */
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier',
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte'],
},
env: {
browser: true,
es2017: true,
node: true,
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
},
},
],
};

2
website/.gitignore vendored
View File

@@ -8,3 +8,5 @@ node_modules
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
static/*.webmanifest
!static/en.manifest.webmanifest

View File

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

View File

@@ -2,13 +2,16 @@
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.js",
"css": "src/app.css",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils"
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks",
"lib": "$lib"
},
"typescript": true
}
"typescript": true,
"registry": "https://shadcn-svelte.com/registry"
}

7585
website/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,8 @@
"scripts": {
"dev": "vite dev",
"build": "vite build",
"postbuild": "npx tsx src/lib/sitemap.ts",
"prebuild": "npx tsx src/lib/scripts/pwa-manifest.ts",
"postbuild": "npx tsx src/lib/scripts/sitemap.ts",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
@@ -13,61 +14,71 @@
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.2.2",
"@sveltejs/adapter-static": "^3.0.2",
"@sveltejs/enhanced-img": "^0.3.0",
"@sveltejs/kit": "^2.5.17",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@types/eslint": "^8.56.10",
"@lucide/svelte": "^0.544.0",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/enhanced-img": "^0.6.0",
"@sveltejs/kit": "^2.21.2",
"@sveltejs/vite-plugin-svelte": "^5.1.0",
"@tailwindcss/vite": "^4.1.8",
"@types/eslint": "^9.6.1",
"@types/events": "^3.0.3",
"@types/mapbox__mapbox-gl-geocoder": "^5.0.0",
"@types/mapbox-gl": "^3.1.0",
"@types/node": "^20.14.6",
"@types/sanitize-html": "^2.11.0",
"@types/file-saver": "^2.0.7",
"@types/mapbox__tilebelt": "^1.0.4",
"@types/mapbox-gl": "^3.4.1",
"@types/node": "^22.15.30",
"@types/png.js": "^0.2.3",
"@types/sanitize-html": "^2.16.0",
"@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^7.13.1",
"@typescript-eslint/parser": "^7.13.1",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.40.0",
"@typescript-eslint/eslint-plugin": "^8.33.1",
"@typescript-eslint/parser": "^8.33.1",
"bits-ui": "^2.12.0",
"eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "^3.9.1",
"events": "^3.3.0",
"glob": "^10.4.3",
"mdsvex": "^0.11.2",
"postcss": "^8.4.38",
"prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.2.4",
"svelte": "^4.2.18",
"svelte-check": "^3.8.1",
"tailwindcss": "^3.4.4",
"tslib": "^2.6.3",
"tsx": "^4.15.7",
"typescript": "^5.4.5",
"vite": "^5.3.1"
"glob": "^11.0.2",
"lucide-static": "^0.513.0",
"mdsvex": "^0.12.6",
"mode-watcher": "^1.1.0",
"paneforge": "^1.0.0-next.5",
"postcss": "^8.4.47",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.33.18",
"svelte-check": "^4.0.0",
"svelte-dnd-action": "^0.9.65",
"svelte-sonner": "^1.0.5",
"tailwind-variants": "^3.1.1",
"tailwindcss": "^4.1.8",
"tslib": "^2.8.1",
"tsx": "^4.19.1",
"tw-animate-css": "^1.3.4",
"typescript": "^5.8.3",
"vaul-svelte": "^1.0.0-next.7",
"vite": "^6.3.5",
"vite-plugin-node-polyfills": "^0.23.0"
},
"type": "module",
"dependencies": {
"@internationalized/date": "^3.5.4",
"@mapbox/mapbox-gl-geocoder": "^5.0.2",
"@mapbox/sphericalmercator": "^1.2.0",
"@docsearch/js": "^3.9.0",
"@internationalized/date": "^3.8.2",
"@mapbox/mapbox-gl-geocoder": "^5.0.3",
"@mapbox/sphericalmercator": "^2.0.1",
"@mapbox/tilebelt": "^2.0.2",
"@types/mapbox__sphericalmercator": "^1.2.3",
"bits-ui": "^0.21.12",
"chart.js": "^4.4.3",
"chartjs-plugin-zoom": "^2.0.1",
"chart.js": "^4.4.9",
"chartjs-plugin-zoom": "^2.2.0",
"clsx": "^2.1.1",
"dexie": "^4.0.7",
"dexie": "^4.0.11",
"file-saver": "^2.0.5",
"gpx": "file:../gpx",
"immer": "^10.1.1",
"lucide-static": "^0.427.0",
"lucide-svelte": "^0.427.0",
"mapbox-gl": "^3.4.0",
"jszip": "^3.10.1",
"mapbox-gl": "^3.12.0",
"mapillary-js": "^4.1.2",
"mode-watcher": "^0.3.1",
"sanitize-html": "^2.13.0",
"sortablejs": "^1.15.2",
"svelte-i18n": "^4.0.0",
"svelte-sonner": "^0.3.24",
"tailwind-merge": "^2.3.0",
"tailwind-variants": "^0.2.1"
"png.js": "^0.2.1",
"sanitize-html": "^2.17.0",
"sortablejs": "^1.15.6",
"tailwind-merge": "^3.3.0"
}
}

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

126
website/src/app.css Normal file
View File

@@ -0,0 +1,126 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
:root {
--background: hsl(0 0% 100%) /* <- Wrap in HSL */;
--foreground: hsl(240 10% 3.9%);
--muted: hsl(240 4.8% 95.9%);
--muted-foreground: hsl(240 3.8% 46.1%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(240 10% 3.9%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(240 10% 3.9%);
--border: hsl(240 5.9% 90%);
--input: hsl(240 5.9% 90%);
--primary: hsl(240 5.9% 10%);
--primary-foreground: hsl(0 0% 98%);
--secondary: hsl(240 4.8% 95.9%);
--secondary-foreground: hsl(240 5.9% 10%);
--accent: hsl(240 4.8% 95.9%);
--accent-foreground: hsl(240 5.9% 10%);
--destructive: hsl(0 72.2% 50.6%);
--destructive-foreground: hsl(0 0% 98%);
--ring: hsl(240 10% 3.9%);
--sidebar: hsl(0 0% 98%);
--sidebar-foreground: hsl(240 5.3% 26.1%);
--sidebar-primary: hsl(240 5.9% 10%);
--sidebar-primary-foreground: hsl(0 0% 98%);
--sidebar-accent: hsl(240 4.8% 95.9%);
--sidebar-accent-foreground: hsl(240 5.9% 10%);
--sidebar-border: hsl(220 13% 91%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
--support: rgb(220 15 130);
--link: rgb(0 110 180);
--selection: hsl(240 4.8% 93%);
--radius: 0.5rem;
}
.dark {
--background: hsl(240 10% 3.9%);
--foreground: hsl(0 0% 98%);
--muted: hsl(240 3.7% 15.9%);
--muted-foreground: hsl(240 5% 64.9%);
--popover: hsl(240 10% 3.9%);
--popover-foreground: hsl(0 0% 98%);
--card: hsl(240 10% 3.9%);
--card-foreground: hsl(0 0% 98%);
--border: hsl(240 3.7% 15.9%);
--input: hsl(240 3.7% 15.9%);
--primary: hsl(0 0% 98%);
--primary-foreground: hsl(240 5.9% 10%);
--secondary: hsl(240 3.7% 15.9%);
--secondary-foreground: hsl(0 0% 98%);
--accent: hsl(240 3.7% 15.9%);
--accent-foreground: hsl(0 0% 98%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(0 0% 98%);
--ring: hsl(240 4.9% 83.9%);
--sidebar: hsl(240 5.9% 10%);
--sidebar-foreground: hsl(240 4.8% 95.9%);
--sidebar-primary: hsl(224.3 76.3% 48%);
--sidebar-primary-foreground: hsl(0 0% 100%);
--sidebar-accent: hsl(240 3.7% 15.9%);
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
--sidebar-border: hsl(240 3.7% 15.9%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
--support: rgb(255 110 190);
--link: rgb(80 190 255);
--selection: hsl(240 3.7% 22%);
}
@theme inline {
/* Radius (for rounded-*) */
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
/* Colors */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-ring: var(--ring);
--color-radius: var(--radius);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--color-support: var(--support);
--color-link: var(--link);
--breakpoint-xs: 540px;
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

14
website/src/app.d.ts vendored
View File

@@ -1,13 +1,13 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View File

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

View File

@@ -1,82 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 92%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 210 40% 98%;
--support: 220 15 130;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 30%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--support: 255 110 190;
--ring: hsl(212.7,26.8%,83.9);
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,57 @@
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" />`;
if (page !== '404') {
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;
}

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}%)`;
}

View File

@@ -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

View File

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

View File

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

View File

@@ -0,0 +1,66 @@
<script lang="ts">
import docsearch from '@docsearch/js';
import '@docsearch/css';
import { onMount } from 'svelte';
import { i18n } from '$lib/i18n.svelte';
let props: {
class?: string;
} = $props();
let mounted = false;
function initDocsearch() {
docsearch({
appId: '21XLD94PE3',
apiKey: 'd2c1ed6cb0ed12adb2bd84eb2a38494d',
indexName: 'gpx',
container: '#docsearch',
searchParameters: {
facetFilters: ['lang:' + i18n.lang],
},
placeholder: i18n._('docs.search.search'),
disableUserPersonalization: true,
translations: {
button: {
buttonText: i18n._('docs.search.search'),
buttonAriaLabel: i18n._('docs.search.search'),
},
modal: {
searchBox: {
resetButtonTitle: i18n._('docs.search.clear'),
resetButtonAriaLabel: i18n._('docs.search.clear'),
cancelButtonText: i18n._('docs.search.cancel'),
cancelButtonAriaLabel: i18n._('docs.search.cancel'),
searchInputLabel: i18n._('docs.search.search'),
},
footer: {
selectText: i18n._('docs.search.to_select'),
navigateText: i18n._('docs.search.to_navigate'),
closeText: i18n._('docs.search.to_close'),
},
noResultsScreen: {
noResultsText: i18n._('docs.search.no_results'),
suggestedQueryText: i18n._('docs.search.no_results_suggestion'),
},
},
},
});
}
onMount(() => {
mounted = true;
});
$effect(() => {
if (mounted && i18n.lang && !i18n.isLoading) {
initDocsearch();
}
});
</script>
<svelte:head>
<link rel="preconnect" href="https://21XLD94PE3-dsn.algolia.net" crossorigin />
</svelte:head>
<div id="docsearch" class={props.class ?? ''}></div>

View File

@@ -0,0 +1,38 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
import type { Snippet } from 'svelte';
const {
variant = 'default',
label,
side = 'top',
disabled = false,
class: className = '',
children,
onclick,
}: {
variant?: 'default' | 'secondary' | 'link' | 'destructive' | 'outline' | 'ghost';
label: string;
side?: 'top' | 'right' | 'bottom' | 'left';
disabled?: boolean;
class?: string;
children: Snippet;
onclick?: (event: MouseEvent) => void;
} = $props();
</script>
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger>
{#snippet child({ props })}
<Button {...props} {variant} class={className} {onclick}>
{@render children()}
</Button>
{/snippet}
</Tooltip.Trigger>
<Tooltip.Content {side}>
<span>{label}</span>
</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>

View File

@@ -1,682 +0,0 @@
<script lang="ts">
import * as ToggleGroup from '$lib/components/ui/toggle-group';
import Tooltip from '$lib/components/Tooltip.svelte';
import Chart from 'chart.js/auto';
import mapboxgl from 'mapbox-gl';
import { map } from '$lib/stores';
import { onDestroy, onMount } from 'svelte';
import {
BrickWall,
TriangleRight,
HeartPulse,
Orbit,
SquareActivity,
Thermometer,
Zap
} from 'lucide-svelte';
import { surfaceColors } from '$lib/assets/surfaces';
import { _, locale } from 'svelte-i18n';
import {
getCadenceUnits,
getCadenceWithUnits,
getConvertedDistance,
getConvertedElevation,
getConvertedTemperature,
getConvertedVelocity,
getDistanceUnits,
getDistanceWithUnits,
getElevationWithUnits,
getHeartRateUnits,
getHeartRateWithUnits,
getPowerUnits,
getPowerWithUnits,
getTemperatureUnits,
getTemperatureWithUnits,
getVelocityUnits,
getVelocityWithUnits,
secondsToHHMMSS
} from '$lib/units';
import type { Writable } from 'svelte/store';
import { DateFormatter } from '@internationalized/date';
import type { GPXStatistics } from 'gpx';
import { settings } from '$lib/db';
import { mode } from 'mode-watcher';
export let gpxStatistics: Writable<GPXStatistics>;
export let slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
export let panelSize: number;
export let additionalDatasets: string[];
export let elevationFill: 'slope' | 'surface' | undefined;
export let showControls: boolean = true;
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
let df: DateFormatter;
$: if ($locale) {
df = new DateFormatter($locale, {
dateStyle: 'medium',
timeStyle: 'medium'
});
}
let canvas: HTMLCanvasElement;
let showAdditionalScales = true;
let updateShowAdditionalScales = () => {
showAdditionalScales = canvas.width / window.devicePixelRatio >= 600;
};
let overlay: HTMLCanvasElement;
let chart: Chart;
Chart.defaults.font.family =
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; // Tailwind CSS font
let marker: mapboxgl.Marker | null = null;
let dragging = false;
let panning = false;
let options = {
animation: false,
parsing: false,
maintainAspectRatio: false,
scales: {
x: {
type: 'linear',
ticks: {
callback: function (value: number, index: number, ticks: { value: number }[]) {
if (index === ticks.length - 1) {
return `${value.toFixed(1).replace(/\.0+$/, '')}`;
}
return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
}
}
},
y: {
type: 'linear',
ticks: {
callback: function (value: number) {
return getElevationWithUnits(value, false);
}
}
}
},
datasets: {
line: {
pointRadius: 0,
tension: 0.4,
borderWidth: 2
}
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
},
plugins: {
legend: {
display: false
},
decimation: {
enabled: true
},
tooltip: {
enabled: () => !dragging && !panning,
callbacks: {
title: function () {
return '';
},
label: function (context: Chart.TooltipContext) {
let point = context.raw;
if (context.datasetIndex === 0) {
if ($map && marker) {
if (dragging) {
marker.remove();
} else {
marker.setLngLat(point.coordinates);
marker.addTo($map);
}
}
return `${$_('quantities.elevation')}: ${getElevationWithUnits(point.y, false)}`;
} else if (context.datasetIndex === 1) {
return `${$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')}: ${getVelocityWithUnits(point.y, false)}`;
} else if (context.datasetIndex === 2) {
return `${$_('quantities.heartrate')}: ${getHeartRateWithUnits(point.y)}`;
} else if (context.datasetIndex === 3) {
return `${$_('quantities.cadence')}: ${getCadenceWithUnits(point.y)}`;
} else if (context.datasetIndex === 4) {
return `${$_('quantities.temperature')}: ${getTemperatureWithUnits(point.y, false)}`;
} else if (context.datasetIndex === 5) {
return `${$_('quantities.power')}: ${getPowerWithUnits(point.y)}`;
}
},
afterBody: function (contexts: Chart.TooltipContext[]) {
let context = contexts.filter((context) => context.datasetIndex === 0);
if (context.length === 0) return;
let point = context[0].raw;
let slope = {
at: point.slope.at.toFixed(1),
segment: point.slope.segment.toFixed(1),
length: getDistanceWithUnits(point.slope.length)
};
let surface = point.surface ? point.surface : 'unknown';
let labels = [
` ${$_('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`,
` ${$_('quantities.slope')}: ${slope.at} %${elevationFill === 'slope' ? ` (${slope.length} @${slope.segment} %)` : ''}`
];
if (elevationFill === 'surface') {
labels.push(
` ${$_('quantities.surface')}: ${$_(`toolbar.routing.surface.${surface}`)}`
);
}
if (point.time) {
labels.push(` ${$_('quantities.time')}: ${df.format(point.time)}`);
}
return labels;
}
}
},
zoom: {
pan: {
enabled: true,
mode: 'x',
modifierKey: 'shift',
onPanStart: function () {
// hide tooltip
panning = true;
$slicedGPXStatistics = undefined;
},
onPanComplete: function () {
panning = false;
}
},
zoom: {
wheel: {
enabled: true
},
mode: 'x',
onZoomStart: function ({ chart, event }: { chart: Chart; event: any }) {
if (
event.deltaY < 0 &&
Math.abs(
chart.getInitialScaleBounds().x.max / chart.options.plugins.zoom.limits.x.minRange -
chart.getZoomLevel()
) < 0.01
) {
// Disable wheel pan if zoomed in to the max, and zooming in
return false;
}
$slicedGPXStatistics = undefined;
}
},
limits: {
x: {
min: 'original',
max: 'original',
minRange: 1
}
}
}
},
stacked: false,
onResize: function () {
updateOverlay();
updateShowAdditionalScales();
}
};
let datasets: {
[key: string]: {
id: string;
getLabel: () => string;
getUnits: () => string;
};
} = {
speed: {
id: 'speed',
getLabel: () => ($velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')),
getUnits: () => getVelocityUnits()
},
hr: {
id: 'hr',
getLabel: () => $_('quantities.heartrate'),
getUnits: () => getHeartRateUnits()
},
cad: {
id: 'cad',
getLabel: () => $_('quantities.cadence'),
getUnits: () => getCadenceUnits()
},
atemp: {
id: 'atemp',
getLabel: () => $_('quantities.temperature'),
getUnits: () => getTemperatureUnits()
},
power: {
id: 'power',
getLabel: () => $_('quantities.power'),
getUnits: () => getPowerUnits()
}
};
for (let [id, dataset] of Object.entries(datasets)) {
options.scales[`y${id}`] = {
type: 'linear',
position: 'right',
title: {
display: true,
text: dataset.getLabel() + ' (' + dataset.getUnits() + ')',
padding: {
top: 6,
bottom: 0
}
},
grid: {
display: false
},
reverse: () => id === 'speed' && $velocityUnits === 'pace',
display: false
};
}
options.scales.yspeed['ticks'] = {
callback: function (value: number) {
if ($velocityUnits === 'speed') {
return value;
} else {
return secondsToHHMMSS(value);
}
}
};
onMount(async () => {
Chart.register((await import('chartjs-plugin-zoom')).default); // dynamic import to avoid SSR and 'window is not defined' error
chart = new Chart(canvas, {
type: 'line',
data: {
datasets: []
},
options,
plugins: [
{
id: 'toggleMarker',
events: ['mouseout'],
afterEvent: function (chart: Chart, args: { event: Chart.ChartEvent }) {
if (args.event.type === 'mouseout') {
if ($map && marker) {
marker.remove();
}
}
}
}
]
});
// Map marker to show on hover
let element = document.createElement('div');
element.className = 'h-4 w-4 rounded-full bg-cyan-500 border-2 border-white';
marker = new mapboxgl.Marker({
element
});
updateShowAdditionalScales();
let startIndex = 0;
let endIndex = 0;
function getIndex(evt) {
const points = chart.getElementsAtEventForMode(
evt,
'x',
{
intersect: false
},
true
);
if (points.length === 0) {
const rect = canvas.getBoundingClientRect();
if (evt.x - rect.left <= chart.chartArea.left) {
return 0;
} else if (evt.x - rect.left >= chart.chartArea.right) {
return $gpxStatistics.local.points.length - 1;
} else {
return undefined;
}
}
let point = points.find((point) => point.element.raw);
if (point) {
return point.element.raw.index;
} else {
return points[0].index;
}
}
let dragStarted = false;
function onMouseDown(evt) {
if (evt.shiftKey) {
// Panning interaction
return;
}
dragStarted = true;
canvas.style.cursor = 'col-resize';
startIndex = getIndex(evt);
}
function onMouseMove(evt) {
if (dragStarted) {
dragging = true;
endIndex = getIndex(evt);
if (endIndex !== undefined) {
if (startIndex === undefined) {
startIndex = endIndex;
} else if (startIndex !== endIndex) {
$slicedGPXStatistics = [
$gpxStatistics.slice(Math.min(startIndex, endIndex), Math.max(startIndex, endIndex)),
Math.min(startIndex, endIndex),
Math.max(startIndex, endIndex)
];
}
}
}
}
function onMouseUp(evt) {
dragStarted = false;
dragging = false;
canvas.style.cursor = '';
endIndex = getIndex(evt);
if (startIndex === endIndex) {
$slicedGPXStatistics = undefined;
}
}
canvas.addEventListener('pointerdown', onMouseDown);
canvas.addEventListener('pointermove', onMouseMove);
canvas.addEventListener('pointerup', onMouseUp);
});
$: if (chart && $distanceUnits && $velocityUnits && $temperatureUnits) {
let data = $gpxStatistics;
// update data
chart.data.datasets[0] = {
label: $_('quantities.elevation'),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.ele ? getConvertedElevation(point.ele) : 0,
time: point.time,
slope: {
at: data.local.slope.at[index],
segment: data.local.slope.segment[index],
length: data.local.slope.length[index]
},
surface: point.getSurface(),
coordinates: point.getCoordinates(),
index: index
};
}),
normalized: true,
fill: 'start',
order: 1
};
chart.data.datasets[1] = {
label: datasets.speed.getLabel(),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: getConvertedVelocity(data.local.speed[index]),
index: index
};
}),
normalized: true,
yAxisID: `y${datasets.speed.id}`,
hidden: true
};
chart.data.datasets[2] = {
label: datasets.hr.getLabel(),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.getHeartRate(),
index: index
};
}),
normalized: true,
yAxisID: `y${datasets.hr.id}`,
hidden: true
};
chart.data.datasets[3] = {
label: datasets.cad.getLabel(),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.getCadence(),
index: index
};
}),
normalized: true,
yAxisID: `y${datasets.cad.id}`,
hidden: true
};
chart.data.datasets[4] = {
label: datasets.atemp.getLabel(),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: getConvertedTemperature(point.getTemperature()),
index: index
};
}),
normalized: true,
yAxisID: `y${datasets.atemp.id}`,
hidden: true
};
chart.data.datasets[5] = {
label: datasets.power.getLabel(),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.getPower(),
index: index
};
}),
normalized: true,
yAxisID: `y${datasets.power.id}`,
hidden: true
};
chart.options.scales.x['min'] = 0;
chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total);
// update units
for (let [id, dataset] of Object.entries(datasets)) {
chart.options.scales[`y${id}`].title.text =
dataset.getLabel() + ' (' + dataset.getUnits() + ')';
}
chart.update();
}
let maxSlope = 20;
function slopeFillCallback(context) {
let slope = context.p0.raw.slope.segment;
if (slope > maxSlope) {
slope = maxSlope;
} else if (slope < -maxSlope) {
slope = -maxSlope;
}
let v = slope / maxSlope;
v = 1 / (1 + Math.exp(-6 * v));
v = v - 0.5;
let hue = ((0.5 - v) * 120).toString(10);
let lightness = 90 - Math.abs(v) * 70;
return ['hsl(', hue, ',70%,', lightness, '%)'].join('');
}
function surfaceFillCallback(context) {
let surface = context.p0.raw.surface;
return surfaceColors[surface] ? surfaceColors[surface] : surfaceColors.missing;
}
$: if (chart) {
if (elevationFill === 'slope') {
chart.data.datasets[0]['segment'] = {
backgroundColor: slopeFillCallback
};
} else if (elevationFill === 'surface') {
chart.data.datasets[0]['segment'] = {
backgroundColor: surfaceFillCallback
};
} else {
chart.data.datasets[0]['segment'] = {};
}
chart.update();
}
$: if (additionalDatasets && chart) {
let includeSpeed = additionalDatasets.includes('speed');
let includeHeartRate = additionalDatasets.includes('hr');
let includeCadence = additionalDatasets.includes('cad');
let includeTemperature = additionalDatasets.includes('atemp');
let includePower = additionalDatasets.includes('power');
if (chart.data.datasets.length > 0) {
chart.data.datasets[1].hidden = !includeSpeed;
chart.data.datasets[2].hidden = !includeHeartRate;
chart.data.datasets[3].hidden = !includeCadence;
chart.data.datasets[4].hidden = !includeTemperature;
chart.data.datasets[5].hidden = !includePower;
}
chart.options.scales[`y${datasets.speed.id}`].display = includeSpeed && showAdditionalScales;
chart.options.scales[`y${datasets.hr.id}`].display = includeHeartRate && showAdditionalScales;
chart.options.scales[`y${datasets.cad.id}`].display = includeCadence && showAdditionalScales;
chart.options.scales[`y${datasets.atemp.id}`].display =
includeTemperature && showAdditionalScales;
chart.options.scales[`y${datasets.power.id}`].display = includePower && showAdditionalScales;
chart.update();
}
function updateOverlay() {
if (!canvas) {
return;
}
overlay.width = canvas.width / window.devicePixelRatio;
overlay.height = canvas.height / window.devicePixelRatio;
if ($slicedGPXStatistics) {
let startIndex = $slicedGPXStatistics[1];
let endIndex = $slicedGPXStatistics[2];
// Draw selection rectangle
let selectionContext = overlay.getContext('2d');
if (selectionContext) {
selectionContext.fillStyle = $mode === 'dark' ? 'white' : 'black';
selectionContext.globalAlpha = $mode === 'dark' ? 0.2 : 0.1;
selectionContext.clearRect(0, 0, overlay.width, overlay.height);
let startPixel = chart.scales.x.getPixelForValue(
getConvertedDistance($gpxStatistics.local.distance.total[startIndex])
);
let endPixel = chart.scales.x.getPixelForValue(
getConvertedDistance($gpxStatistics.local.distance.total[endIndex])
);
selectionContext.fillRect(
startPixel,
chart.chartArea.top,
endPixel - startPixel,
chart.chartArea.bottom - chart.chartArea.top
);
}
} else if (overlay) {
let selectionContext = overlay.getContext('2d');
if (selectionContext) {
selectionContext.clearRect(0, 0, overlay.width, overlay.height);
}
}
}
$: $slicedGPXStatistics, $mode, updateOverlay();
onDestroy(() => {
if (chart) {
chart.destroy();
}
});
</script>
<div class="h-full grow min-w-0 flex flex-row gap-4 items-center {$$props.class ?? ''}">
<div class="grow h-full min-w-0 relative">
<canvas bind:this={overlay} class=" w-full h-full absolute pointer-events-none"></canvas>
<canvas bind:this={canvas} class="w-full h-full"></canvas>
</div>
{#if showControls}
<div class="h-full flex flex-col justify-center" style="width: {panelSize > 158 ? 22 : 42}px">
<ToggleGroup.Root
class="{panelSize > 158
? 'flex-col'
: 'flex-row'} flex-wrap gap-0 min-h-0 content-center border rounded-t-md"
type="single"
bind:value={elevationFill}
>
<ToggleGroup.Item class="p-0 w-5 h-5" value="slope">
<Tooltip side="left">
<TriangleRight slot="data" size="15" />
<span slot="tooltip">{$_('chart.show_slope')}</span>
</Tooltip>
</ToggleGroup.Item>
<ToggleGroup.Item class="p-0 w-5 h-5" value="surface">
<Tooltip side="left">
<BrickWall slot="data" size="15" />
<span slot="tooltip">{$_('chart.show_surface')}</span>
</Tooltip>
</ToggleGroup.Item>
</ToggleGroup.Root>
<ToggleGroup.Root
class="{panelSize > 158
? 'flex-col'
: 'flex-row'} flex-wrap gap-0 min-h-0 content-center border rounded-b-md -mt-[1px]"
type="multiple"
bind:value={additionalDatasets}
>
<ToggleGroup.Item class="p-0 w-5 h-5" value="speed">
<Tooltip side="left">
<Zap slot="data" size="15" />
<span slot="tooltip"
>{$velocityUnits === 'speed' ? $_('chart.show_speed') : $_('chart.show_pace')}</span
>
</Tooltip>
</ToggleGroup.Item>
<ToggleGroup.Item class="p-0 w-5 h-5" value="hr">
<Tooltip side="left">
<HeartPulse slot="data" size="15" />
<span slot="tooltip">{$_('chart.show_heartrate')}</span>
</Tooltip>
</ToggleGroup.Item>
<ToggleGroup.Item class="p-0 w-5 h-5" value="cad">
<Tooltip side="left">
<Orbit slot="data" size="15" />
<span slot="tooltip">{$_('chart.show_cadence')}</span>
</Tooltip>
</ToggleGroup.Item>
<ToggleGroup.Item class="p-0 w-5 h-5" value="atemp">
<Tooltip side="left">
<Thermometer slot="data" size="15" />
<span slot="tooltip">{$_('chart.show_temperature')}</span>
</Tooltip>
</ToggleGroup.Item>
<ToggleGroup.Item class="p-0 w-5 h-5" value="power">
<Tooltip side="left">
<SquareActivity slot="data" size="15" />
<span slot="tooltip">{$_('chart.show_power')}</span>
</Tooltip>
</ToggleGroup.Item>
</ToggleGroup.Root>
</div>
{/if}
</div>

View File

@@ -1,181 +0,0 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label';
import { Checkbox } from '$lib/components/ui/checkbox';
import { Separator } from '$lib/components/ui/separator';
import { Dialog } from 'bits-ui';
import {
currentTool,
exportAllFiles,
exportSelectedFiles,
ExportState,
exportState,
gpxStatistics
} from '$lib/stores';
import { fileObservers } from '$lib/db';
import {
Download,
Zap,
BrickWall,
HeartPulse,
Orbit,
Thermometer,
SquareActivity
} from 'lucide-svelte';
import { _ } from 'svelte-i18n';
import { selection } from './file-list/Selection';
import { get } from 'svelte/store';
import { GPXStatistics } from 'gpx';
import { ListRootItem } from './file-list/FileList';
let open = false;
let exportOptions: Record<string, boolean> = {
time: true,
surface: true,
hr: true,
cad: true,
atemp: true,
power: true
};
let hide: Record<string, boolean> = {
time: false,
surface: false,
hr: false,
cad: false,
atemp: false,
power: false
};
$: if ($exportState !== ExportState.NONE) {
open = true;
$currentTool = null;
let statistics = $gpxStatistics;
if ($exportState === ExportState.ALL) {
statistics = Array.from($fileObservers.values())
.map((file) => get(file)?.statistics)
.reduce((acc, cur) => {
if (cur !== undefined) {
acc.mergeWith(cur.getStatisticsFor(new ListRootItem()));
}
return acc;
}, new GPXStatistics());
}
hide.time = statistics.global.time.total === 0;
hide.hr = statistics.global.hr.count === 0;
hide.cad = statistics.global.cad.count === 0;
hide.atemp = statistics.global.atemp.count === 0;
hide.power = statistics.global.power.count === 0;
}
$: exclude = Object.keys(exportOptions).filter((key) => !exportOptions[key]);
</script>
<Dialog.Root
bind:open
onOpenChange={(isOpen) => {
if (!isOpen) {
$exportState = ExportState.NONE;
}
}}
>
<Dialog.Trigger class="hidden" />
<Dialog.Portal>
<Dialog.Content
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
class="w-full flex flex-row items-center justify-center gap-4 border rounded-md p-2 bg-accent"
>
<span>⚠️</span>
<span class="max-w-96 text-sm">
{$_('menu.support_message')}
</span>
</div>
<div class="w-full flex flex-row flex-wrap gap-2">
<Button class="bg-support grow" href="https://ko-fi.com/gpxstudio" target="_blank">
{$_('menu.support_button')}
<span class="ml-2">🙏</span>
</Button>
<Button
variant="outline"
class="grow"
on:click={() => {
if ($exportState === ExportState.SELECTION) {
exportSelectedFiles(exclude);
} else if ($exportState === ExportState.ALL) {
exportAllFiles(exclude);
}
open = false;
$exportState = ExportState.NONE;
}}
>
<Download size="16" class="mr-1" />
{#if $fileObservers.size === 1 || ($exportState === ExportState.SELECTION && $selection.size === 1)}
{$_('menu.download_file')}
{:else}
{$_('menu.download_files')}
{/if}
</Button>
</div>
<div class="w-full max-w-xl flex flex-col items-center gap-2">
<div class="w-full flex flex-row items-center gap-3">
<div class="grow">
<Separator />
</div>
<Label class="shrink-0">
{$_('menu.export_options')}
</Label>
<div class="grow">
<Separator />
</div>
</div>
<div class="flex flex-row flex-wrap justify-center gap-x-6 gap-y-2">
<div class="flex flex-row items-center gap-1.5 {hide.time ? 'hidden' : ''}">
<Checkbox id="export-time" bind:checked={exportOptions.time} />
<Label for="export-time" class="flex flex-row items-center gap-1">
<Zap size="16" />
{$_('quantities.time')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5">
<Checkbox id="export-surface" bind:checked={exportOptions.surface} />
<Label for="export-surface" class="flex flex-row items-center gap-1">
<BrickWall size="16" />
{$_('quantities.surface')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.hr ? 'hidden' : ''}">
<Checkbox id="export-heartrate" bind:checked={exportOptions.hr} />
<Label for="export-heartrate" class="flex flex-row items-center gap-1">
<HeartPulse size="16" />
{$_('quantities.heartrate')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.cad ? 'hidden' : ''}">
<Checkbox id="export-cadence" bind:checked={exportOptions.cad} />
<Label for="export-cadence" class="flex flex-row items-center gap-1">
<Orbit size="16" />
{$_('quantities.cadence')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.atemp ? 'hidden' : ''}">
<Checkbox id="export-temperature" bind:checked={exportOptions.atemp} />
<Label for="export-temperature" class="flex flex-row items-center gap-1">
<Thermometer size="16" />
{$_('quantities.temperature')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.power ? 'hidden' : ''}">
<Checkbox id="export-power" bind:checked={exportOptions.power} />
<Label for="export-power" class="flex flex-row items-center gap-1">
<SquareActivity size="16" />
{$_('quantities.power')}
</Label>
</div>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>

View File

@@ -1,116 +1,116 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import LanguageSelect from '$lib/components/LanguageSelect.svelte';
import Logo from '$lib/components/Logo.svelte';
import { AtSign, BookOpenText, Heart, Home, Map } from 'lucide-svelte';
import { _, locale } from 'svelte-i18n';
import { getURLForLanguage } from '$lib/utils';
import { Button } from '$lib/components/ui/button';
import LanguageSelect from '$lib/components/LanguageSelect.svelte';
import Logo from '$lib/components/Logo.svelte';
import { AtSign, BookOpenText, Heart, House, Map } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte';
import { getURLForLanguage } from '$lib/utils';
</script>
<footer class="w-full">
<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="grow flex flex-col items-start">
<Logo class="h-8" />
<Button
variant="link"
class="h-6 px-0 text-muted-foreground"
href="https://github.com/gpxstudio/gpx.studio/blob/main/LICENSE"
target="_blank"
>
MIT © 2024 gpx.studio
</Button>
<LanguageSelect class="w-40 mt-3" />
</div>
<div class="grow max-w-2xl flex flex-row flex-wrap justify-between gap-x-10 gap-y-6">
<div class="flex flex-col items-start gap-1">
<span class="font-semibold">{$_('homepage.website')}</span>
<Button
variant="link"
class="h-6 px-0 text-muted-foreground"
href={getURLForLanguage($locale, '/')}
>
<Home size="16" class="mr-1" />
{$_('homepage.home')}
</Button>
<Button
variant="link"
class="h-6 px-0 text-muted-foreground"
href={getURLForLanguage($locale, '/app')}
>
<Map size="16" class="mr-1" />
{$_('homepage.app')}
</Button>
<Button
variant="link"
class="h-6 px-0 text-muted-foreground"
href={getURLForLanguage($locale, '/help')}
>
<BookOpenText size="16" class="mr-1" />
{$_('menu.help')}
</Button>
</div>
<div class="flex flex-col items-start gap-1" id="contact">
<span class="font-semibold">{$_('homepage.contact')}</span>
<Button
variant="link"
class="h-6 px-0 text-muted-foreground"
href="https://facebook.com/gpx.studio"
target="_blank"
>
<Logo company="facebook" class="h-4 mr-1 fill-muted-foreground" />
{$_('homepage.facebook')}
</Button>
<Button
variant="link"
class="h-6 px-0 text-muted-foreground"
href="https://x.com/gpxstudio"
target="_blank"
>
<Logo company="x" class="h-4 mr-1 fill-muted-foreground" />
{$_('homepage.x')}
</Button>
<Button
variant="link"
class="h-6 px-0 text-muted-foreground"
href="mailto:hello@gpx.studio"
target="_blank"
>
<AtSign size="16" class="mr-1" />
{$_('homepage.email')}
</Button>
</div>
<div class="flex flex-col items-start gap-1">
<span class="font-semibold">{$_('homepage.contribute')}</span>
<Button
variant="link"
class="h-6 px-0 text-muted-foreground"
href="https://ko-fi.com/gpxstudio"
target="_blank"
>
<Heart size="16" class="mr-1" />
{$_('menu.donate')}
</Button>
<Button
variant="link"
class="h-6 px-0 text-muted-foreground"
href="https://crowdin.com/project/gpxstudio"
target="_blank"
>
<Logo company="crowdin" class="h-4 mr-1 fill-muted-foreground" />
{$_('homepage.crowdin')}
</Button>
<Button
variant="link"
class="h-6 px-0 text-muted-foreground"
href="https://github.com/gpxstudio/gpx.studio"
target="_blank"
>
<Logo company="github" class="h-4 mr-1 fill-muted-foreground" />
{$_('homepage.github')}
</Button>
</div>
</div>
</div>
</div>
<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="grow flex flex-col items-start">
<Logo class="h-8" width="153" />
<Button
variant="link"
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://github.com/gpxstudio/gpx.studio/blob/main/LICENSE"
target="_blank"
>
MIT © 2025 gpx.studio
</Button>
<LanguageSelect class="w-40 mt-3" />
</div>
<div class="grow max-w-2xl flex flex-row flex-wrap justify-between gap-x-10 gap-y-6">
<div class="flex flex-col items-start gap-1">
<span class="font-semibold">{i18n._('homepage.website')}</span>
<Button
variant="link"
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href={getURLForLanguage(i18n.lang, '/')}
>
<House size="16" />
{i18n._('homepage.home')}
</Button>
<Button
variant="link"
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href={getURLForLanguage(i18n.lang, '/app')}
>
<Map size="16" />
{i18n._('homepage.app')}
</Button>
<Button
variant="link"
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href={getURLForLanguage(i18n.lang, '/help')}
>
<BookOpenText size="16" />
{i18n._('menu.help')}
</Button>
</div>
<div class="flex flex-col items-start gap-1" id="contact">
<span class="font-semibold">{i18n._('homepage.contact')}</span>
<Button
variant="link"
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://www.reddit.com/r/gpxstudio/"
target="_blank"
>
<Logo company="reddit" class="h-4 fill-muted-foreground" />
{i18n._('homepage.reddit')}
</Button>
<Button
variant="link"
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://facebook.com/gpx.studio"
target="_blank"
>
<Logo company="facebook" class="h-4 fill-muted-foreground" />
{i18n._('homepage.facebook')}
</Button>
<Button
variant="link"
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="mailto:hello@gpx.studio"
target="_blank"
>
<AtSign size="16" />
{i18n._('homepage.email')}
</Button>
</div>
<div class="flex flex-col items-start gap-1">
<span class="font-semibold">{i18n._('homepage.contribute')}</span>
<Button
variant="link"
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://ko-fi.com/gpxstudio"
target="_blank"
>
<Heart size="16" />
{i18n._('menu.donate')}
</Button>
<Button
variant="link"
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://crowdin.com/project/gpxstudio"
target="_blank"
>
<Logo company="crowdin" class="h-4 fill-muted-foreground" />
{i18n._('homepage.crowdin')}
</Button>
<Button
variant="link"
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
href="https://github.com/gpxstudio/gpx.studio"
target="_blank"
>
<Logo company="github" class="h-4 fill-muted-foreground" />
{i18n._('homepage.github')}
</Button>
</div>
</div>
</div>
</div>
</footer>

View File

@@ -1,84 +1,93 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import Tooltip from '$lib/components/Tooltip.svelte';
import WithUnits from '$lib/components/WithUnits.svelte';
import * as Card from '$lib/components/ui/card';
import Tooltip from '$lib/components/Tooltip.svelte';
import WithUnits from '$lib/components/WithUnits.svelte';
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from 'lucide-svelte';
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from '@lucide/svelte';
import { _ } from 'svelte-i18n';
import type { GPXStatistics } from 'gpx';
import type { Writable } from 'svelte/store';
import { settings } from '$lib/db';
import { i18n } from '$lib/i18n.svelte';
import type { GPXStatistics } from 'gpx';
import type { Readable } from 'svelte/store';
import { settings } from '$lib/logic/settings';
export let gpxStatistics: Writable<GPXStatistics>;
export let slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
export let orientation: 'horizontal' | 'vertical';
export let panelSize: number;
const { velocityUnits } = settings;
const { velocityUnits } = settings;
let {
gpxStatistics,
slicedGPXStatistics,
orientation,
panelSize,
}: {
gpxStatistics: Readable<GPXStatistics>;
slicedGPXStatistics: Readable<[GPXStatistics, number, number] | undefined>;
orientation: 'horizontal' | 'vertical';
panelSize: number;
} = $props();
let statistics: GPXStatistics;
$: if ($slicedGPXStatistics !== undefined) {
statistics = $slicedGPXStatistics[0];
} else {
statistics = $gpxStatistics;
}
let statistics = $derived(
$slicedGPXStatistics !== undefined ? $slicedGPXStatistics[0] : $gpxStatistics
);
</script>
<Card.Root
class="h-full {orientation === 'vertical'
? 'min-w-44 sm:min-w-52 text-sm sm:text-base'
: 'w-full'} border-none shadow-none"
class="h-full {orientation === 'vertical'
? 'min-w-40 sm:min-w-44 text-sm sm:text-base'
: 'w-full'} border-none shadow-none p-0"
>
<Card.Content
class="h-full flex {orientation === 'vertical'
? 'flex-col justify-center'
: 'flex-row w-full justify-between'} gap-4 p-0"
>
<Tooltip>
<span slot="data" class="flex flex-row items-center">
<Ruler size="18" class="mr-1" />
<WithUnits value={statistics.global.distance.total} type="distance" />
</span>
<span slot="tooltip">{$_('quantities.distance')}</span>
</Tooltip>
<Tooltip>
<span slot="data" class="flex flex-row items-center">
<MoveUpRight size="18" class="mr-1" />
<WithUnits value={statistics.global.elevation.gain} type="elevation" />
<MoveDownRight size="18" class="mx-1" />
<WithUnits value={statistics.global.elevation.loss} type="elevation" />
</span>
<span slot="tooltip">{$_('quantities.elevation')}</span>
</Tooltip>
{#if panelSize > 120 || orientation === 'horizontal'}
<Tooltip class={orientation === 'horizontal' ? 'hidden xs:block' : ''}>
<span slot="data" class="flex flex-row items-center">
<Zap size="18" class="mr-1" />
<WithUnits value={statistics.global.speed.moving} type="speed" showUnits={false} />
<span class="mx-1">/</span>
<WithUnits value={statistics.global.speed.total} type="speed" />
</span>
<span slot="tooltip"
>{$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_(
'quantities.moving'
)} / {$_('quantities.total')})</span
>
</Tooltip>
{/if}
{#if panelSize > 160 || orientation === 'horizontal'}
<Tooltip class={orientation === 'horizontal' ? 'hidden md:block' : ''}>
<span slot="data" class="flex flex-row items-center">
<Timer size="18" class="mr-1" />
<WithUnits value={statistics.global.time.moving} type="time" />
<span class="mx-1">/</span>
<WithUnits value={statistics.global.time.total} type="time" />
</span>
<span slot="tooltip"
>{$_('quantities.time')} ({$_('quantities.moving')} / {$_('quantities.total')})</span
>
</Tooltip>
{/if}
</Card.Content>
<Card.Content
class="h-full flex {orientation === 'vertical'
? 'flex-col justify-center'
: 'flex-row w-full justify-between'} gap-4 p-0"
>
<Tooltip label={i18n._('quantities.distance')}>
<span class="flex flex-row items-center">
<Ruler size="16" class="mr-1" />
<WithUnits value={statistics.global.distance.total} type="distance" />
</span>
</Tooltip>
<Tooltip label={i18n._('quantities.elevation_gain_loss')}>
<span class="flex flex-row items-center">
<MoveUpRight size="16" class="mr-1" />
<WithUnits value={statistics.global.elevation.gain} type="elevation" />
<MoveDownRight size="16" class="mx-1" />
<WithUnits value={statistics.global.elevation.loss} type="elevation" />
</span>
</Tooltip>
{#if panelSize > 120 || orientation === 'horizontal'}
<Tooltip
class={orientation === 'horizontal' ? 'hidden xs:block' : ''}
label="{$velocityUnits === 'speed'
? i18n._('quantities.speed')
: i18n._('quantities.pace')} ({i18n._('quantities.moving')} / {i18n._(
'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>
<WithUnits value={statistics.global.speed.total} type="speed" />
</span>
</Tooltip>
{/if}
{#if panelSize > 160 || orientation === 'horizontal'}
<Tooltip
class={orientation === 'horizontal' ? 'hidden md:block' : ''}
label="{i18n._('quantities.time')} ({i18n._('quantities.moving')} / {i18n._(
'quantities.total'
)})"
>
<span class="flex flex-row items-center">
<Timer size="16" class="mr-1" />
<WithUnits value={statistics.global.time.moving} type="time" />
<span class="mx-1">/</span>
<WithUnits value={statistics.global.time.total} type="time" />
</span>
</Tooltip>
{/if}
</Card.Content>
</Card.Root>

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>

View File

@@ -1,22 +1,27 @@
<script lang="ts">
import { CircleHelp } from 'lucide-svelte';
import { _ } from 'svelte-i18n';
import { CircleQuestionMark } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte';
import type { Snippet } from 'svelte';
export let link: string | undefined = undefined;
let {
link,
class: className = '',
children,
}: {
link: string;
class?: string;
children: Snippet;
} = $props();
</script>
<div class="text-sm bg-muted rounded border flex flex-row items-center p-2 {$$props.class || ''}">
<CircleHelp size="16" class="w-4 mr-2 shrink-0 grow-0" />
<div>
<slot />
{#if link}
<a
href={link}
target="_blank"
class="text-sm text-blue-500 dark:text-blue-300 hover:underline"
>
{$_('menu.more')}
</a>
{/if}
</div>
<div class="text-sm bg-secondary rounded border flex flex-row items-center p-2 {className}">
<CircleQuestionMark size="16" class="w-4 mr-2 shrink-0 grow-0" />
<div>
{@render children()}
{#if link}
<a href={link} target="_blank" class="text-sm text-link hover:underline">
{i18n._('menu.more')}
</a>
{/if}
</div>
</div>

View File

@@ -1,42 +1,36 @@
<script lang="ts">
import * as Select from '$lib/components/ui/select';
import { languages } from '$lib/languages';
import { getURLForLanguage } from '$lib/utils';
import { Languages } from 'lucide-svelte';
import { _, locale } from 'svelte-i18n';
import { page } from '$app/state';
import * as Select from '$lib/components/ui/select';
import { languages } from '$lib/languages';
import { getURLForLanguage } from '$lib/utils';
import { Languages } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte';
let selected = {
value: '',
label: ''
};
$: if ($locale) {
selected = {
value: $locale,
label: languages[$locale]
};
}
let {
class: className = '',
}: {
class?: string;
} = $props();
</script>
<Select.Root bind:selected>
<Select.Trigger class="w-[180px] {$$props.class ?? ''}">
<Languages size="16" />
<Select.Value class="ml-2 mr-auto" />
</Select.Trigger>
<Select.Content>
{#each Object.entries(languages) as [lang, label]}
<a href={getURLForLanguage(lang)}>
<Select.Item value={lang}>{label}</Select.Item>
</a>
{/each}
</Select.Content>
<Select.Root type="single" value={i18n.lang}>
<Select.Trigger class="min-w-[180px] {className}" aria-label={i18n._('menu.language')}>
<Languages size="16" />
<span class="mr-auto">
{languages[i18n.lang]}
</span>
</Select.Trigger>
<Select.Content>
{#each Object.entries(languages) as [lang, label]}
{#if page.url.pathname.includes('404')}
<a href={getURLForLanguage(lang, '/')}>
<Select.Item value={lang}>{label}</Select.Item>
</a>
{:else}
<a href={getURLForLanguage(lang, page.url.pathname)}>
<Select.Item value={lang}>{label}</Select.Item>
</a>
{/if}
{/each}
</Select.Content>
</Select.Root>
<!-- hidden links for svelte crawling -->
<div class="hidden">
{#each Object.entries(languages) as [lang, label]}
<a href={getURLForLanguage(lang)}>
{label}
</a>
{/each}
</div>

View File

@@ -1,63 +1,68 @@
<script lang="ts">
import { base } from '$app/paths';
import { mode, systemPrefersMode } from 'mode-watcher';
import { mode } from 'mode-watcher';
import { base } from '$app/paths';
export let iconOnly = false;
export let company = 'gpx.studio';
$: effectiveMode = $mode ?? $systemPrefersMode ?? 'light';
let {
iconOnly = false,
company = 'gpx.studio',
...others
}: {
iconOnly?: boolean;
company?: 'gpx.studio' | 'mapbox' | 'github' | 'crowdin' | 'facebook' | 'reddit';
[key: string]: any;
} = $props();
</script>
{#if company === 'gpx.studio'}
<img
src="{base}/{iconOnly ? 'icon' : 'logo'}{effectiveMode === 'dark' ? '-dark' : ''}.svg"
alt="Logo of gpx.studio."
{...$$restProps}
/>
<img
src="{base}/{iconOnly ? 'icon' : 'logo'}{mode.current === 'dark' ? '-dark' : ''}.svg"
alt="Logo of gpx.studio."
{...others}
/>
{:else if company === 'mapbox'}
<img
src="{base}/mapbox-logo-{effectiveMode === 'dark' ? 'white' : 'black'}.svg"
alt="Logo of Mapbox."
{...$$restProps}
/>
<img
src="{base}/mapbox-logo-{mode.current === 'dark' ? 'white' : 'black'}.svg"
alt="Logo of Mapbox."
{...others}
/>
{:else if company === 'github'}
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}"
><title>GitHub</title><path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
/></svg
>
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {others.class ?? ''}"
><title>GitHub</title><path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
/></svg
>
{:else if company === 'crowdin'}
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}"
><title>Crowdin</title><path
d="M16.119 17.793a2.619 2.619 0 0 1-1.667-.562c-.546-.436-1.004-1.09-1.018-1.858-.008-.388.414-.388.414-.388l1.018-.008c.332.008.43.47.445.586.128 1.04.717 1.495 1.168 1.702.273.123.204.513-.362.528zm-5.695-5.287L8.5 12.252c-.867-.214-.844-.982-.807-1.247a5.119 5.119 0 0 1 .814-2.125c.545-.804 1.303-1.508 2.29-2.073 1.856-1.074 4.45-1.673 7.31-1.673 2.09 0 4.256.27 4.29.27.197.025.328.213.333.437a.377.377 0 0 1-.355.393l-.92-.01c-2.902 0-4.968.394-6.506 1.248-1.527.837-2.57 2.117-3.287 4.012-.076.163-.335 1.12-1.24 1.022zm2.533 7.823c-1.44 0-2.797-.622-3.825-1.746-.87-.96-1.397-1.931-1.493-3.164-.06-.813.3-1.094.788-1.044l1.988.218c.45.092.75.34.825.854.397 2.736 2.122 3.814 3.15 4.046.18.042.292.157.283.365a.412.412 0 0 1-.322.398c-.458.074-.936.073-1.394.073zm-4.101 2.418a14.216 14.216 0 0 1-2.307-.214c-1.202-.214-2.208-.582-3.072-1.13C1.41 20.095.163 17.786.014 15.048c-.037-.65-.11-1.89 1.427-1.797.638.033 1.653.343 2.368.548.887.247 1.314.933 1.314 1.608 0 3.858 3.494 6.408 5.02 6.408.654 0 .414.701.127.779-.502.136-1.15.153-1.413.153zM3.525 11.419c-.605-.109-1.194-.358-1.768-.5C-.018 10.479.284 8.688.45 8.196c1.617-4.757 6.746-6.35 10.887-6.773 3.898-.4 7.978-.092 11.778.967.31.083 1.269.327.718.891-.35.358-1.7-.016-2.073-.041-2.23-.167-4.434-.192-6.656.15-2.349.357-4.768 1.099-6.71 2.665-.938.758-1.76 1.723-2.313 2.866-.144.3-.256.6-.354.9-.11.327-.47 1.91-2.215 1.6zm9.94.917c.332-1.488 1.81-3.848 6.385-3.686 1.05.033.57.749.052.731-2.586-.09-3.815 1.578-4.457 3.27-.219.546-.68.626-1.271.53-.415-.074-.866-.123-.71-.846Z"
/></svg
>
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {others.class ?? ''}"
><title>Crowdin</title><path
d="M16.119 17.793a2.619 2.619 0 0 1-1.667-.562c-.546-.436-1.004-1.09-1.018-1.858-.008-.388.414-.388.414-.388l1.018-.008c.332.008.43.47.445.586.128 1.04.717 1.495 1.168 1.702.273.123.204.513-.362.528zm-5.695-5.287L8.5 12.252c-.867-.214-.844-.982-.807-1.247a5.119 5.119 0 0 1 .814-2.125c.545-.804 1.303-1.508 2.29-2.073 1.856-1.074 4.45-1.673 7.31-1.673 2.09 0 4.256.27 4.29.27.197.025.328.213.333.437a.377.377 0 0 1-.355.393l-.92-.01c-2.902 0-4.968.394-6.506 1.248-1.527.837-2.57 2.117-3.287 4.012-.076.163-.335 1.12-1.24 1.022zm2.533 7.823c-1.44 0-2.797-.622-3.825-1.746-.87-.96-1.397-1.931-1.493-3.164-.06-.813.3-1.094.788-1.044l1.988.218c.45.092.75.34.825.854.397 2.736 2.122 3.814 3.15 4.046.18.042.292.157.283.365a.412.412 0 0 1-.322.398c-.458.074-.936.073-1.394.073zm-4.101 2.418a14.216 14.216 0 0 1-2.307-.214c-1.202-.214-2.208-.582-3.072-1.13C1.41 20.095.163 17.786.014 15.048c-.037-.65-.11-1.89 1.427-1.797.638.033 1.653.343 2.368.548.887.247 1.314.933 1.314 1.608 0 3.858 3.494 6.408 5.02 6.408.654 0 .414.701.127.779-.502.136-1.15.153-1.413.153zM3.525 11.419c-.605-.109-1.194-.358-1.768-.5C-.018 10.479.284 8.688.45 8.196c1.617-4.757 6.746-6.35 10.887-6.773 3.898-.4 7.978-.092 11.778.967.31.083 1.269.327.718.891-.35.358-1.7-.016-2.073-.041-2.23-.167-4.434-.192-6.656.15-2.349.357-4.768 1.099-6.71 2.665-.938.758-1.76 1.723-2.313 2.866-.144.3-.256.6-.354.9-.11.327-.47 1.91-2.215 1.6zm9.94.917c.332-1.488 1.81-3.848 6.385-3.686 1.05.033.57.749.052.731-2.586-.09-3.815 1.578-4.457 3.27-.219.546-.68.626-1.271.53-.415-.074-.866-.123-.71-.846Z"
/></svg
>
{:else if company === 'facebook'}
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}"
><title>Facebook</title><path
d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"
/></svg
>
{:else if company === 'x'}
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {$$restProps.class ?? ''}"
><title>X</title><path
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/></svg
>
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {others.class ?? ''}"
><title>Facebook</title><path
d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"
/></svg
>
{:else if company === 'reddit'}
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="fill-foreground {others.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}

View File

@@ -1,331 +0,0 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
import { Button } from '$lib/components/ui/button';
import { map } from '$lib/stores';
import { settings } from '$lib/db';
import { _ } from 'svelte-i18n';
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import { page } from '$app/stores';
export let accessToken = PUBLIC_MAPBOX_TOKEN;
export let geolocate = true;
export let geocoder = true;
export let hash = true;
mapboxgl.accessToken = accessToken;
let webgl2Supported = true;
let fitBoundsOptions: mapboxgl.FitBoundsOptions = {
maxZoom: 15,
linear: true,
easing: () => 1
};
const { distanceUnits, elevationProfile, verticalFileView, bottomPanelSize, rightPanelSize } =
settings;
let scaleControl = new mapboxgl.ScaleControl({
unit: $distanceUnits
});
onMount(() => {
let gl = document.createElement('canvas').getContext('webgl2');
if (!gl) {
webgl2Supported = false;
return;
}
let language = $page.params.language;
if (language === 'zh') {
language = 'zh-Hans';
} else if (language?.includes('-')) {
language = language.split('-')[0];
} else if (language === '' || language === undefined) {
language = 'en';
}
let newMap = new mapboxgl.Map({
container: 'map',
style: { version: 8, sources: {}, layers: [] },
zoom: 0,
hash: hash,
language,
attributionControl: false,
logoPosition: 'bottom-right',
boxZoom: false
});
newMap.on('load', () => {
$map = newMap; // only set the store after the map has loaded
scaleControl.setUnit($distanceUnits);
});
newMap.addControl(
new mapboxgl.AttributionControl({
compact: true
})
);
newMap.addControl(
new mapboxgl.NavigationControl({
visualizePitch: true
})
);
if (geocoder) {
newMap.addControl(
new MapboxGeocoder({
accessToken: mapboxgl.accessToken,
mapboxgl: mapboxgl,
collapsed: true,
flyTo: fitBoundsOptions,
language
})
);
}
if (geolocate) {
newMap.addControl(
new mapboxgl.GeolocateControl({
positionOptions: {
enableHighAccuracy: true
},
fitBoundsOptions,
trackUserLocation: true,
showUserHeading: true
})
);
}
newMap.addControl(scaleControl);
newMap.on('style.load', () => {
newMap.addSource('mapbox-dem', {
type: 'raster-dem',
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
tileSize: 512,
maxzoom: 14
});
newMap.setTerrain({
source: 'mapbox-dem',
exaggeration: newMap.getPitch() > 0 ? 1 : 0
});
newMap.setFog({
color: 'rgb(186, 210, 235)',
'high-color': 'rgb(36, 92, 223)',
'horizon-blend': 0.1,
'space-color': 'rgb(156, 240, 255)'
});
newMap.on('pitch', () => {
if (newMap.getPitch() > 0) {
newMap.setTerrain({
source: 'mapbox-dem',
exaggeration: 1
});
} else {
newMap.setTerrain({
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)'
}
});
});
});
onDestroy(() => {
if ($map) {
$map.remove();
$map = null;
}
});
$: if (
$map &&
(!$verticalFileView || !$elevationProfile || $bottomPanelSize || $rightPanelSize)
) {
$map.resize();
}
</script>
<div {...$$restProps}>
<div id="map" class="h-full {webgl2Supported ? '' : 'hidden'}"></div>
<div
class="flex flex-col items-center justify-center gap-3 h-full {webgl2Supported ? 'hidden' : ''}"
>
<p>{$_('webgl2_required')}</p>
<Button href="https://get.webgl.org/webgl2/" target="_blank">
{$_('enable_webgl2')}
</Button>
</div>
</div>
<style lang="postcss">
div :global(.mapboxgl-map) {
@apply font-sans;
}
div :global(.mapboxgl-ctrl-top-right > .mapboxgl-ctrl) {
@apply shadow-md;
@apply bg-background;
@apply text-foreground;
}
div :global(.mapboxgl-ctrl-icon) {
@apply dark:brightness-[4.7];
}
div :global(.mapboxgl-ctrl-geocoder) {
@apply flex;
@apply flex-row;
@apply w-fit;
@apply min-w-fit;
@apply items-center;
@apply shadow-md;
}
div :global(.suggestions) {
@apply shadow-md;
@apply bg-background;
@apply text-foreground;
}
div :global(.mapboxgl-ctrl-geocoder .suggestions > li > a) {
@apply text-foreground;
@apply hover:text-accent-foreground;
@apply hover:bg-accent;
}
div :global(.mapboxgl-ctrl-geocoder .suggestions > .active > a) {
@apply bg-background;
}
div :global(.mapboxgl-ctrl-geocoder--button) {
@apply bg-transparent;
@apply hover:bg-transparent;
}
div :global(.mapboxgl-ctrl-geocoder--icon) {
@apply fill-foreground;
@apply hover:fill-accent-foreground;
}
div :global(.mapboxgl-ctrl-geocoder--icon-search) {
@apply relative;
@apply top-0;
@apply left-0;
@apply my-2;
@apply w-[29px];
}
div :global(.mapboxgl-ctrl-geocoder--input) {
@apply relative;
@apply w-64;
@apply py-0;
@apply pl-2;
@apply focus:outline-none;
@apply transition-[width];
@apply duration-200;
@apply text-foreground;
}
div :global(.mapboxgl-ctrl-geocoder--collapsed .mapboxgl-ctrl-geocoder--input) {
@apply w-0;
@apply p-0;
}
div :global(.mapboxgl-ctrl-top-right) {
@apply z-40;
@apply flex;
@apply flex-col;
@apply items-end;
@apply h-full;
@apply overflow-hidden;
}
.horizontal :global(.mapboxgl-ctrl-bottom-left) {
@apply bottom-[42px];
}
.horizontal :global(.mapboxgl-ctrl-bottom-right) {
@apply bottom-[42px];
}
div :global(.mapboxgl-ctrl-attrib) {
@apply dark:bg-transparent;
}
div :global(.mapboxgl-compact-show.mapboxgl-ctrl-attrib) {
@apply dark:bg-background;
}
div :global(.mapboxgl-ctrl-attrib-button) {
@apply dark:bg-foreground;
}
div :global(.mapboxgl-compact-show .mapboxgl-ctrl-attrib-button) {
@apply dark:bg-foreground;
}
div :global(.mapboxgl-ctrl-attrib a) {
@apply text-foreground;
}
div :global(.mapboxgl-popup) {
@apply w-fit;
@apply z-20;
}
div :global(.mapboxgl-popup-content) {
@apply p-0;
@apply bg-transparent;
@apply shadow-none;
}
div :global(.mapboxgl-popup-anchor-top .mapboxgl-popup-tip) {
@apply border-b-background;
}
div :global(.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip) {
@apply border-b-background;
}
div :global(.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip) {
@apply border-b-background;
}
div :global(.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip) {
@apply border-t-background;
@apply drop-shadow-md;
}
div :global(.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip) {
@apply border-t-background;
@apply drop-shadow-md;
}
div :global(.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip) {
@apply border-t-background;
@apply drop-shadow-md;
}
div :global(.mapboxgl-popup-anchor-left .mapboxgl-popup-tip) {
@apply border-r-background;
}
div :global(.mapboxgl-popup-anchor-right .mapboxgl-popup-tip) {
@apply border-l-background;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,28 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Moon, Sun } from 'lucide-svelte';
import { mode, setMode, systemPrefersMode } from 'mode-watcher';
import { _ } from 'svelte-i18n';
import { Button } from '$lib/components/ui/button';
import { Moon, Sun } from '@lucide/svelte';
import { mode, setMode } from 'mode-watcher';
import { i18n } from '$lib/i18n.svelte';
export let size = '20';
$: selectedMode = $mode ?? $systemPrefersMode ?? 'light';
let {
class: className = '',
}: {
class?: string;
} = $props();
</script>
<Button
variant="ghost"
class="h-8 px-1.5 {$$props.class ?? ''}"
on:click={() => {
setMode(selectedMode === 'light' ? 'dark' : 'light');
}}
variant="ghost"
size="icon"
class={className}
onclick={() => {
setMode(mode.current === 'light' ? 'dark' : 'light');
}}
aria-label={i18n._('menu.mode')}
>
{#if selectedMode === 'light'}
<Sun {size} />
{:else}
<Moon {size} />
{/if}
{#if mode.current === 'light'}
<Sun />
{:else}
<Moon />
{/if}
</Button>

View File

@@ -1,30 +1,44 @@
<script lang="ts">
import Logo from '$lib/components/Logo.svelte';
import { Button } from '$lib/components/ui/button';
import ModeSwitch from '$lib/components/ModeSwitch.svelte';
import { BookOpenText, Home, Map } from 'lucide-svelte';
import { _, locale } from 'svelte-i18n';
import { getURLForLanguage } from '$lib/utils';
import Logo from '$lib/components/Logo.svelte';
import { Button } from '$lib/components/ui/button';
import AlgoliaDocSearch from '$lib/components/AlgoliaDocSearch.svelte';
import ModeSwitch from '$lib/components/ModeSwitch.svelte';
import { BookOpenText, House, Map } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte';
import { getURLForLanguage } from '$lib/utils';
</script>
<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">
<a href={getURLForLanguage($locale, '/')} class="shrink-0 translate-y-0.5">
<Logo class="h-8 sm:hidden" iconOnly={true} />
<Logo class="h-8 hidden sm:block" />
</a>
<Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/')}>
<Home size="18" class="mr-1.5" />
{$_('homepage.home')}
</Button>
<Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/app')}>
<Map size="18" class="mr-1.5" />
{$_('homepage.app')}
</Button>
<Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/help')}>
<BookOpenText size="18" class="mr-1.5" />
{$_('menu.help')}
</Button>
<ModeSwitch class="ml-auto" />
</div>
<div class="mx-6 py-2 flex flex-row items-center border-b gap-4 sm:gap-8">
<a href={getURLForLanguage(i18n.lang, '/')} class="shrink-0 translate-y-0.5">
<Logo class="h-8 sm:hidden" iconOnly={true} width="26" />
<Logo class="h-8 hidden sm:block" width="153" />
</a>
<Button
variant="link"
class="text-base px-0 has-[>svg]:px-0"
href={getURLForLanguage(i18n.lang, '/')}
>
<House size="18" />
{i18n._('homepage.home')}
</Button>
<Button
variant="link"
class="text-base px-0 has-[>svg]:px-0"
href={getURLForLanguage(i18n.lang, '/app')}
>
<Map size="18" />
{i18n._('homepage.app')}
</Button>
<Button
variant="link"
class="text-base px-0 has-[>svg]:px-0"
href={getURLForLanguage(i18n.lang, '/help')}
>
<BookOpenText size="18" />
{i18n._('menu.help')}
</Button>
<AlgoliaDocSearch class="ml-auto" />
<ModeSwitch class="hidden xs:inline-flex" />
</div>
</nav>

View File

@@ -1,41 +1,48 @@
<script lang="ts">
export let orientation: 'col' | 'row' = 'col';
let {
orientation = 'col',
after = $bindable(),
minAfter = 0,
maxAfter = Number.MAX_SAFE_INTEGER,
}: {
orientation?: 'col' | 'row';
after: number;
minAfter?: number;
maxAfter?: number;
} = $props();
export let after: number;
export let minAfter: number = 0;
export let maxAfter: number = Number.MAX_SAFE_INTEGER;
function handleMouseDown(event: PointerEvent) {
const startX = event.clientX;
const startY = event.clientY;
const startAfter = after;
function handleMouseDown(event: PointerEvent) {
const startX = event.clientX;
const startY = event.clientY;
const startAfter = after;
const handleMouseMove = (event: PointerEvent) => {
const newAfter =
startAfter +
(orientation === 'col' ? startX - event.clientX : startY - event.clientY);
if (newAfter >= minAfter && newAfter <= maxAfter) {
after = newAfter;
} else if (newAfter < minAfter && after !== minAfter) {
after = minAfter;
} else if (newAfter > maxAfter && after !== maxAfter) {
after = maxAfter;
}
};
const handleMouseMove = (event: PointerEvent) => {
const newAfter =
startAfter + (orientation === 'col' ? startX - event.clientX : startY - event.clientY);
if (newAfter >= minAfter && newAfter <= maxAfter) {
after = newAfter;
} else if (newAfter < minAfter && after !== minAfter) {
after = minAfter;
} else if (newAfter > maxAfter && after !== maxAfter) {
after = maxAfter;
}
};
const handleMouseUp = () => {
window.removeEventListener('pointermove', handleMouseMove);
window.removeEventListener('pointerup', handleMouseUp);
};
const handleMouseUp = () => {
window.removeEventListener('pointermove', handleMouseMove);
window.removeEventListener('pointerup', handleMouseUp);
};
window.addEventListener('pointermove', handleMouseMove);
window.addEventListener('pointerup', handleMouseUp);
}
window.addEventListener('pointermove', handleMouseMove);
window.addEventListener('pointerup', handleMouseUp);
}
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="{orientation === 'col'
? 'w-1 h-full cursor-col-resize border-l'
: 'w-full h-1 cursor-row-resize border-t'} {orientation}"
on:pointerdown={handleMouseDown}
/>
class="{orientation === 'col'
? 'w-1 h-full cursor-col-resize border-l'
: 'w-full h-1 cursor-row-resize border-t'} {orientation}"
onpointerdown={handleMouseDown}
></div>

View File

@@ -1,26 +1,43 @@
<script lang="ts">
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { isMac, isSafari } from '$lib/utils';
import { onMount } from 'svelte';
import { i18n } from '$lib/i18n.svelte';
import * as Kbd from '$lib/components/ui/kbd/index.js';
export let key: string;
export let shift: boolean = false;
export let ctrl: boolean = false;
export let click: boolean = false;
let {
key = undefined,
shift = false,
ctrl = false,
click = false,
class: className = '',
}: {
key?: string;
shift?: boolean;
ctrl?: boolean;
click?: boolean;
class?: string;
} = $props();
let isMac = false;
let isSafari = false;
let mac = $state(false);
let safari = $state(false);
onMount(() => {
isMac = navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
});
onMount(() => {
mac = isMac();
safari = isSafari();
});
</script>
<div
class="ml-auto pl-2 text-xs tracking-widest text-muted-foreground flex flex-row gap-0 items-baseline"
>
<span>{shift ? '⇧' : ''}</span>
<span>{ctrl ? (isMac && !isSafari ? '⌘' : $_('menu.ctrl') + '+') : ''}</span>
<span class={key === '+' ? 'font-medium text-sm/4' : ''}>{key}</span>
<span>{click ? $_('menu.click') : ''}</span>
</div>
<Kbd.Root class="ml-auto {className}">
{#if shift}
{/if}
{#if ctrl}
{mac && !safari ? '⌘' : i18n._('menu.ctrl')}
{/if}
{#if key}
{key}
{/if}
{#if click}
{i18n._('menu.click')}
{/if}
</Kbd.Root>

View File

@@ -1,14 +1,32 @@
<script lang="ts">
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
import type { Snippet } from 'svelte';
export let side: 'top' | 'right' | 'bottom' | 'left' = 'top';
let {
label,
side = 'top',
children,
extra,
class: className = '',
}: {
label: string;
side?: 'top' | 'right' | 'bottom' | 'left';
children: Snippet;
extra?: Snippet;
class?: string;
} = $props();
</script>
<Tooltip.Root>
<Tooltip.Trigger {...$$restProps}>
<slot name="data" />
</Tooltip.Trigger>
<Tooltip.Content {side}>
<slot name="tooltip" />
</Tooltip.Content>
</Tooltip.Root>
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger class={className} aria-label={label}>
{@render children()}
</Tooltip.Trigger>
<Tooltip.Content {side}>
<div class="flex flex-row items-center gap-2">
<span>{label}</span>
{@render extra?.()}
</div>
</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>

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>

View File

@@ -1,58 +1,56 @@
<script lang="ts">
import { settings } from '$lib/db';
import {
celsiusToFahrenheit,
distancePerHourToSecondsPerDistance,
kilometersToMiles,
metersToFeet,
secondsToHHMMSS
} from '$lib/units';
import {
celsiusToFahrenheit,
getConvertedDistance,
getConvertedElevation,
getConvertedVelocity,
getDistanceUnits,
getElevationUnits,
getVelocityUnits,
secondsToHHMMSS,
} from '$lib/units';
import { i18n } from '$lib/i18n.svelte';
import { settings } from '$lib/logic/settings';
import { _ } from 'svelte-i18n';
let {
value,
type,
showUnits = true,
decimals = undefined,
class: className = '',
}: {
value: number;
type: 'distance' | 'elevation' | 'speed' | 'temperature' | 'time';
showUnits?: boolean;
decimals?: number;
class?: string;
} = $props();
export let value: number;
export let type: 'distance' | 'elevation' | 'speed' | 'temperature' | 'time';
export let showUnits: boolean = true;
export let decimals: number | undefined = undefined;
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
</script>
<span class={$$props.class}>
{#if type === 'distance'}
{#if $distanceUnits === 'metric'}
{value.toFixed(decimals ?? 2)} {showUnits ? $_('units.kilometers') : ''}
{:else}
{kilometersToMiles(value).toFixed(decimals ?? 2)} {showUnits ? $_('units.miles') : ''}
{/if}
{:else if type === 'elevation'}
{#if $distanceUnits === 'metric'}
{value.toFixed(decimals ?? 0)} {showUnits ? $_('units.meters') : ''}
{:else}
{metersToFeet(value).toFixed(decimals ?? 0)} {showUnits ? $_('units.feet') : ''}
{/if}
{:else if type === 'speed'}
{#if $distanceUnits === 'metric'}
{#if $velocityUnits === 'speed'}
{value.toFixed(decimals ?? 2)} {showUnits ? $_('units.kilometers_per_hour') : ''}
{:else}
{secondsToHHMMSS(distancePerHourToSecondsPerDistance(value))}
{showUnits ? $_('units.minutes_per_kilometer') : ''}
{/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}
{:else if type === 'temperature'}
{#if $temperatureUnits === 'celsius'}
{value} {showUnits ? $_('units.celsius') : ''}
{:else}
{celsiusToFahrenheit(value)} {showUnits ? $_('units.fahrenheit') : ''}
{/if}
{:else if type === 'time'}
{secondsToHHMMSS(value)}
{/if}
<span class={className}>
{#if type === 'distance'}
{getConvertedDistance(value, $distanceUnits).toFixed(decimals ?? 2)}
{showUnits ? getDistanceUnits($distanceUnits) : ''}
{:else if type === 'elevation'}
{getConvertedElevation(value, $distanceUnits).toFixed(decimals ?? 0)}
{showUnits ? getElevationUnits($distanceUnits) : ''}
{:else if type === 'speed'}
{#if $velocityUnits === 'speed'}
{getConvertedVelocity(value, $velocityUnits, $distanceUnits).toFixed(decimals ?? 2)}
{showUnits ? getVelocityUnits($velocityUnits, $distanceUnits) : ''}
{:else}
{secondsToHHMMSS(getConvertedVelocity(value, $velocityUnits, $distanceUnits))}
{showUnits ? getVelocityUnits($velocityUnits, $distanceUnits) : ''}
{/if}
{:else if type === 'temperature'}
{#if $temperatureUnits === 'celsius'}
{value} {showUnits ? i18n._('units.celsius') : ''}
{:else}
{celsiusToFahrenheit(value)} {showUnits ? i18n._('units.fahrenheit') : ''}
{/if}
{:else if type === 'time'}
{secondsToHHMMSS(value)}
{/if}
</span>

View File

@@ -1,20 +1,28 @@
<script lang="ts">
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
import { setContext, type Snippet } from 'svelte';
import { CollapsibleTreeState } from './utils.svelte';
export let defaultState: 'open' | 'closed' = 'open';
export let side: 'left' | 'right' = 'right';
export let nohover: boolean = false;
export let slotInsideTrigger: boolean = true;
const {
defaultState = 'open',
side = 'right',
nohover = false,
slotInsideTrigger = true,
children,
}: {
defaultState?: 'open' | 'closed';
side?: 'left' | 'right';
nohover?: boolean;
slotInsideTrigger?: boolean;
children: Snippet;
} = $props();
let open = writable<Record<string, boolean>>({});
let open = $state(new CollapsibleTreeState(defaultState));
setContext('collapsible-tree-default-state', defaultState);
setContext('collapsible-tree-state', open);
setContext('collapsible-tree-side', side);
setContext('collapsible-tree-nohover', nohover);
setContext('collapsible-tree-parent-id', 'root');
setContext('collapsible-tree-slot-inside-trigger', slotInsideTrigger);
setContext('collapsible-tree-state', open);
setContext('collapsible-tree-side', side);
setContext('collapsible-tree-nohover', nohover);
setContext('collapsible-tree-parent-id', 'root');
setContext('collapsible-tree-slot-inside-trigger', slotInsideTrigger);
</script>
<slot />
{@render children()}

View File

@@ -1,97 +1,93 @@
<script lang="ts">
import * as Collapsible from '$lib/components/ui/collapsible';
import { Button } from '$lib/components/ui/button';
import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-svelte';
import { getContext, onMount, setContext } from 'svelte';
import { get, type Writable } from 'svelte/store';
import * as Collapsible from '$lib/components/ui/collapsible';
import { Button } from '$lib/components/ui/button';
import { ChevronDown, ChevronLeft, ChevronRight } from '@lucide/svelte';
import { getContext, setContext, type Snippet } from 'svelte';
import type { ClassValue } from 'svelte/elements';
import type { CollapsibleTreeState } from './utils.svelte';
export let id: string | number;
const props: {
id: string | number;
class?: ClassValue;
trigger: Snippet;
content: Snippet;
} = $props();
let defaultState = getContext<'open' | 'closed'>('collapsible-tree-default-state');
let open = getContext<Writable<Record<string, boolean>>>('collapsible-tree-state');
let side = getContext<'left' | 'right'>('collapsible-tree-side');
let nohover = getContext<boolean>('collapsible-tree-nohover');
let slotInsideTrigger = getContext<boolean>('collapsible-tree-slot-inside-trigger');
let parentId = getContext<string>('collapsible-tree-parent-id');
let state = getContext<CollapsibleTreeState>('collapsible-tree-state');
let side = getContext<'left' | 'right'>('collapsible-tree-side');
let nohover = getContext<boolean>('collapsible-tree-nohover');
let slotInsideTrigger = getContext<boolean>('collapsible-tree-slot-inside-trigger');
let parentId = getContext<string>('collapsible-tree-parent-id');
let fullId = `${parentId}.${id}`;
setContext('collapsible-tree-parent-id', fullId);
let fullId = `${parentId}.${props.id}`;
setContext('collapsible-tree-parent-id', fullId);
onMount(() => {
if (!get(open).hasOwnProperty(fullId)) {
open.update((value) => {
value[fullId] = defaultState === 'open';
return value;
});
}
});
let open = state.get(fullId);
export function openNode() {
open.update((value) => {
value[fullId] = true;
return value;
});
}
export function openNode() {
open.current = true;
}
</script>
<Collapsible.Root bind:open={$open[fullId]} class={$$props.class ?? ''}>
{#if slotInsideTrigger}
<Collapsible.Trigger class="w-full">
<Button
variant="ghost"
class="w-full flex flex-row {side === 'right'
? 'justify-between'
: 'justify-start'} py-0 px-1 h-fit {nohover
? 'hover:bg-background'
: ''} pointer-events-none"
>
{#if side === 'left'}
{#if $open[fullId]}
<ChevronDown size="16" class="shrink-0" />
{:else}
<ChevronRight size="16" class="shrink-0" />
{/if}
{/if}
<slot name="trigger" />
{#if side === 'right'}
{#if $open[fullId]}
<ChevronDown size="16" class="shrink-0" />
{:else}
<ChevronLeft size="16" class="shrink-0" />
{/if}
{/if}
</Button>
</Collapsible.Trigger>
{:else}
<Button
variant="ghost"
class="w-full flex flex-row {side === 'right'
? 'justify-between'
: 'justify-start'} py-0 px-1 h-fit {nohover ? 'hover:bg-background' : ''}"
>
{#if side === 'left'}
<Collapsible.Trigger>
{#if $open[fullId]}
<ChevronDown size="16" class="shrink-0" />
{:else}
<ChevronRight size="16" class="shrink-0" />
{/if}
</Collapsible.Trigger>
{/if}
<slot name="trigger" />
{#if side === 'right'}
<Collapsible.Trigger>
{#if $open[fullId]}
<ChevronDown size="16" class="shrink-0" />
{:else}
<ChevronLeft size="16" class="shrink-0" />
{/if}
</Collapsible.Trigger>
{/if}
</Button>
{/if}
<Collapsible.Content class="ml-2">
<slot name="content" />
</Collapsible.Content>
<Collapsible.Root bind:open={open.current} class={props.class}>
{#if slotInsideTrigger}
<Collapsible.Trigger class="w-full">
<Button
variant="ghost"
size="icon"
class="w-full flex flex-row gap-1 {side === 'right'
? 'justify-between'
: 'justify-start pl-1'} h-fit {nohover
? 'hover:bg-background'
: ''} pointer-events-none"
>
{#if side === 'left'}
{#if open.current}
<ChevronDown size="16" class="shrink-0" />
{:else}
<ChevronRight size="16" class="shrink-0" />
{/if}
{/if}
{@render props.trigger()}
{#if side === 'right'}
{#if open.current}
<ChevronDown size="16" class="shrink-0" />
{:else}
<ChevronLeft size="16" class="shrink-0" />
{/if}
{/if}
</Button>
</Collapsible.Trigger>
{:else}
<Button
variant="ghost"
size="icon"
class="w-full flex flex-row gap-1 {side === 'right'
? 'justify-between'
: 'justify-start pl-1'} h-fit {nohover ? 'hover:bg-background' : ''}"
>
{#if side === 'left'}
<Collapsible.Trigger>
{#if open.current}
<ChevronDown size="16" class="shrink-0" />
{:else}
<ChevronRight size="16" class="shrink-0" />
{/if}
</Collapsible.Trigger>
{/if}
{@render props.trigger()}
{#if side === 'right'}
<Collapsible.Trigger>
{#if open.current}
<ChevronDown size="16" class="shrink-0" />
{:else}
<ChevronLeft size="16" class="shrink-0" />
{/if}
</Collapsible.Trigger>
{/if}
</Button>
{/if}
<Collapsible.Content>
{@render props.content()}
</Collapsible.Content>
</Collapsible.Root>

View File

@@ -1,2 +1,2 @@
export { default as CollapsibleTree } from './CollapsibleTree.svelte';
export { default as CollapsibleTreeNode } from './CollapsibleTreeNode.svelte';
export { default as CollapsibleTreeNode } from './CollapsibleTreeNode.svelte';

View File

@@ -0,0 +1,31 @@
export class CollapsibleNodeState {
private _open: boolean;
constructor(defaultState: 'open' | 'closed') {
this._open = $state(defaultState === 'open');
}
get current(): boolean {
return this._open;
}
set current(value: boolean) {
this._open = value;
}
}
export class CollapsibleTreeState {
private _open: Record<string, CollapsibleNodeState> = {};
private _defaultState: 'open' | 'closed';
constructor(defaultState: 'open' | 'closed') {
this._defaultState = defaultState;
}
get(id: string): CollapsibleNodeState {
if (this._open[id] === undefined) {
this._open[id] = new CollapsibleNodeState(this._defaultState);
}
return this._open[id];
}
}

View File

@@ -1,27 +0,0 @@
<script lang="ts">
import CustomControl from './CustomControl';
import { map } from '$lib/stores';
export let position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' = 'top-right';
let container: HTMLDivElement;
let control: CustomControl | undefined = undefined;
$: if ($map && container) {
if (position.includes('right')) container.classList.add('float-right');
else container.classList.add('float-left');
container.classList.remove('hidden');
if (control === undefined) {
control = new CustomControl(container);
}
$map.addControl(control, position);
}
</script>
<div
bind:this={container}
class="{$$props.class ||
''} clear-both translate-0 m-[10px] mb-0 last:mb-[10px] pointer-events-auto bg-background rounded shadow-md hidden"
>
<slot />
</div>

View File

@@ -0,0 +1,85 @@
<script lang="ts">
import type { Component } from 'svelte';
let { module: Module }: { module: Component } = $props();
</script>
<div class="markdown flex flex-col gap-3">
<Module />
</div>
<style lang="postcss">
@reference "../../../app.css";
: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>

View File

@@ -1,11 +1,34 @@
<script lang="ts">
export let src;
export let alt: string;
let {
src,
alt,
}: {
src: 'getting-started/interface' | 'tools/routing' | 'tools/split';
alt: string;
} = $props();
</script>
<div class="flex flex-col items-center py-6 w-full">
<div class="rounded-md overflow-clip shadow-xl mx-auto">
<enhanced:img {src} {alt} class="w-full max-w-3xl" />
</div>
<p class="text-center text-sm text-muted-foreground mt-2">{alt}</p>
<div class="rounded-md overflow-hidden overflow-clip shadow-xl mx-auto">
{#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>
<p class="text-center text-sm text-muted-foreground mt-2">{alt}</p>
</div>

View File

@@ -1,13 +1,13 @@
<script lang="ts">
import mapboxOutdoorsMap from '$lib/assets/img/home/mapbox-outdoors.png?enhanced';
import waymarkedMap from '$lib/assets/img/home/waymarked.png?enhanced';
import mapboxOutdoorsMap from '$lib/assets/img/home/mapbox-outdoors.png?enhanced';
import waymarkedMap from '$lib/assets/img/home/waymarked.png?enhanced';
</script>
<div class="relative h-80 aspect-square rounded-2xl shadow-xl overflow-clip">
<enhanced:img src={mapboxOutdoorsMap} alt="Mapbox Outdoors map screenshot." class="absolute" />
<enhanced:img
src={waymarkedMap}
alt="Waymarked Trails map screenshot."
class="absolute opacity-0 hover:opacity-100 transition-opacity duration-200"
/>
<enhanced:img src={mapboxOutdoorsMap} alt="Mapbox Outdoors map screenshot." class="absolute" />
<enhanced:img
src={waymarkedMap}
alt="Waymarked Trails map screenshot."
class="absolute opacity-0 hover:opacity-100 transition-opacity duration-200"
/>
</div>

View File

@@ -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>

View File

@@ -1,18 +1,22 @@
<script lang="ts">
export let type: 'note' | 'warning' = 'note';
import type { Snippet } from 'svelte';
let { type = 'note', children }: { type?: 'note' | 'warning'; children: Snippet } = $props();
</script>
<div
class="bg-accent border-l-8 {type === 'note'
? 'border-blue-500'
: 'border-destructive'} p-2 text-sm rounded-md"
class="bg-secondary border-l-8 {type === 'note'
? 'border-link'
: 'border-destructive'} p-2 text-sm rounded-md"
>
<slot />
{@render children()}
</div>
<style lang="postcss">
div :global(a) {
@apply text-blue-500;
@apply hover:underline;
}
@reference "../../../app.css";
div :global(a) {
@apply text-link;
@apply hover:underline;
}
</style>

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

View File

@@ -0,0 +1,203 @@
<script lang="ts">
import ButtonWithTooltip from '$lib/components/ButtonWithTooltip.svelte';
import * as Popover from '$lib/components/ui/popover/index.js';
import * as ToggleGroup from '$lib/components/ui/toggle-group/index.js';
import Separator from '$lib/components/ui/separator/separator.svelte';
import { onDestroy, onMount } from 'svelte';
import {
BrickWall,
TriangleRight,
HeartPulse,
Orbit,
SquareActivity,
Thermometer,
Zap,
Circle,
Check,
ChartNoAxesColumn,
Construction,
} from '@lucide/svelte';
import type { Readable, Writable } from 'svelte/store';
import type { GPXStatistics } from 'gpx';
import { settings } from '$lib/logic/settings';
import { i18n } from '$lib/i18n.svelte';
import { ElevationProfile } from '$lib/components/elevation-profile/elevation-profile';
const { velocityUnits } = settings;
let {
gpxStatistics,
slicedGPXStatistics,
additionalDatasets,
elevationFill,
showControls = true,
}: {
gpxStatistics: Readable<GPXStatistics>;
slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
additionalDatasets: Writable<string[]>;
elevationFill: Writable<'slope' | 'surface' | 'highway' | undefined>;
showControls?: boolean;
} = $props();
let canvas: HTMLCanvasElement;
let overlay: HTMLCanvasElement;
let elevationProfile: ElevationProfile | null = null;
onMount(() => {
elevationProfile = new ElevationProfile(
gpxStatistics,
slicedGPXStatistics,
additionalDatasets,
elevationFill,
canvas,
overlay
);
});
onDestroy(() => {
if (elevationProfile) {
elevationProfile.destroy();
}
});
</script>
<div class="h-full grow min-w-0 relative py-2">
<canvas bind:this={overlay} class="w-full h-full absolute pointer-events-none"></canvas>
<canvas bind:this={canvas} class="w-full h-full absolute"></canvas>
{#if showControls}
<div class="absolute bottom-10 right-1.5">
<Popover.Root>
<Popover.Trigger>
<ButtonWithTooltip
label={i18n._('chart.settings')}
variant="outline"
side="left"
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"
side="top"
align="end"
sideOffset={-32}
>
<ToggleGroup.Root
class="flex flex-col items-start gap-0 p-1 w-full border-none"
type="single"
bind:value={$elevationFill}
>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="slope"
>
<div class="w-6 flex justify-center items-center">
{#if $elevationFill === 'slope'}
<Circle class="size-1.5 fill-current text-current" />
{/if}
</div>
<TriangleRight size="15" />
{i18n._('quantities.slope')}
</ToggleGroup.Item>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="surface"
variant="outline"
>
<div class="w-6 flex justify-center items-center">
{#if $elevationFill === 'surface'}
<Circle class="size-1.5 fill-current text-current" />
{/if}
</div>
<BrickWall size="15" />
{i18n._('quantities.surface')}
</ToggleGroup.Item>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 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="size-1.5 fill-current text-current" />
{/if}
</div>
<Construction size="15" />
{i18n._('quantities.highway')}
</ToggleGroup.Item>
</ToggleGroup.Root>
<Separator />
<ToggleGroup.Root
class="flex flex-col items-start gap-0 p-1"
type="multiple"
bind:value={$additionalDatasets}
>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="speed"
>
<div class="w-6 flex justify-center items-center">
{#if $additionalDatasets.includes('speed')}
<Check size="14" />
{/if}
</div>
<Zap size="15" />
{$velocityUnits === 'speed'
? i18n._('quantities.speed')
: i18n._('quantities.pace')}
</ToggleGroup.Item>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="hr"
>
<div class="w-6 flex justify-center items-center">
{#if $additionalDatasets.includes('hr')}
<Check size="14" />
{/if}
</div>
<HeartPulse size="15" />
{i18n._('quantities.heartrate')}
</ToggleGroup.Item>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="cad"
>
<div class="w-6 flex justify-center items-center">
{#if $additionalDatasets.includes('cad')}
<Check size="14" />
{/if}
</div>
<Orbit size="15" />
{i18n._('quantities.cadence')}
</ToggleGroup.Item>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="atemp"
>
<div class="w-6 flex justify-center items-center">
{#if $additionalDatasets.includes('atemp')}
<Check size="14" />
{/if}
</div>
<Thermometer size="15" />
{i18n._('quantities.temperature')}
</ToggleGroup.Item>
<ToggleGroup.Item
class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
value="power"
>
<div class="w-6 flex justify-center items-center">
{#if $additionalDatasets.includes('power')}
<Check size="14" />
{/if}
</div>
<SquareActivity size="15" />
{i18n._('quantities.power')}
</ToggleGroup.Item>
</ToggleGroup.Root>
</Popover.Content>
</Popover.Root>
</div>
{/if}
</div>

View File

@@ -0,0 +1,603 @@
import { i18n } from '$lib/i18n.svelte';
import { settings } from '$lib/logic/settings';
import {
getCadenceWithUnits,
getConvertedDistance,
getConvertedElevation,
getConvertedTemperature,
getConvertedVelocity,
getDistanceUnits,
getDistanceWithUnits,
getElevationWithUnits,
getHeartRateWithUnits,
getPowerWithUnits,
getTemperatureWithUnits,
getVelocityWithUnits,
} from '$lib/units';
import Chart from 'chart.js/auto';
import mapboxgl from 'mapbox-gl';
import { get, type Readable, type Writable } from 'svelte/store';
import { map } from '$lib/components/map/map';
import type { GPXStatistics } from 'gpx';
import { mode } from 'mode-watcher';
import { getHighwayColor, getSlopeColor, getSurfaceColor } from '$lib/assets/colors';
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
Chart.defaults.font.family =
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; // Tailwind CSS font
export class ElevationProfile {
private _chart: Chart | null = null;
private _canvas: HTMLCanvasElement;
private _overlay: HTMLCanvasElement;
private _marker: mapboxgl.Marker | null = null;
private _dragging = false;
private _panning = false;
private _gpxStatistics: Readable<GPXStatistics>;
private _slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
private _additionalDatasets: Readable<string[]>;
private _elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>;
constructor(
gpxStatistics: Readable<GPXStatistics>,
slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>,
additionalDatasets: Readable<string[]>,
elevationFill: Readable<'slope' | 'surface' | 'highway' | undefined>,
canvas: HTMLCanvasElement,
overlay: HTMLCanvasElement
) {
this._gpxStatistics = gpxStatistics;
this._slicedGPXStatistics = slicedGPXStatistics;
this._additionalDatasets = additionalDatasets;
this._elevationFill = elevationFill;
this._canvas = canvas;
this._overlay = overlay;
let element = document.createElement('div');
element.className = 'h-4 w-4 rounded-full bg-cyan-500 border-2 border-white';
this._marker = new mapboxgl.Marker({
element,
});
import('chartjs-plugin-zoom').then((module) => {
Chart.register(module.default);
this.initialize();
this._gpxStatistics.subscribe(() => {
this.updateData();
});
this._slicedGPXStatistics.subscribe(() => {
this.updateOverlay();
});
distanceUnits.subscribe(() => {
this.updateData();
});
velocityUnits.subscribe(() => {
this.updateData();
});
temperatureUnits.subscribe(() => {
this.updateData();
});
this._additionalDatasets.subscribe(() => {
this.updateDataVisibility();
});
this._elevationFill.subscribe(() => {
this.updateFill();
});
});
}
initialize() {
let options = {
animation: false,
parsing: false,
maintainAspectRatio: false,
scales: {
x: {
type: 'linear',
ticks: {
callback: function (value: number) {
return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
},
align: 'inner',
maxRotation: 0,
},
},
y: {
type: 'linear',
ticks: {
callback: function (value: number) {
return getElevationWithUnits(value, false);
},
},
},
},
datasets: {
line: {
pointRadius: 0,
tension: 0.4,
borderWidth: 2,
cubicInterpolationMode: 'monotone',
},
},
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false,
},
plugins: {
legend: {
display: false,
},
decimation: {
enabled: true,
},
tooltip: {
enabled: () => !this._dragging && !this._panning,
callbacks: {
title: () => {
return '';
},
label: (context: Chart.TooltipContext) => {
let point = context.raw;
if (context.datasetIndex === 0) {
const map_ = get(map);
if (map_ && this._marker) {
if (this._dragging) {
this._marker.remove();
} else {
this._marker.setLngLat(point.coordinates);
this._marker.addTo(map_);
}
}
return `${i18n._('quantities.elevation')}: ${getElevationWithUnits(point.y, false)}`;
} else if (context.datasetIndex === 1) {
return `${get(velocityUnits) === 'speed' ? i18n._('quantities.speed') : i18n._('quantities.pace')}: ${getVelocityWithUnits(point.y, false)}`;
} else if (context.datasetIndex === 2) {
return `${i18n._('quantities.heartrate')}: ${getHeartRateWithUnits(point.y)}`;
} else if (context.datasetIndex === 3) {
return `${i18n._('quantities.cadence')}: ${getCadenceWithUnits(point.y)}`;
} else if (context.datasetIndex === 4) {
return `${i18n._('quantities.temperature')}: ${getTemperatureWithUnits(point.y, false)}`;
} else if (context.datasetIndex === 5) {
return `${i18n._('quantities.power')}: ${getPowerWithUnits(point.y)}`;
}
},
afterBody: (contexts: Chart.TooltipContext[]) => {
let context = contexts.filter((context) => context.datasetIndex === 0);
if (context.length === 0) return;
let point = context[0].raw;
let slope = {
at: point.slope.at.toFixed(1),
segment: point.slope.segment.toFixed(1),
length: getDistanceWithUnits(point.slope.length),
};
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 = [
` ${i18n._('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`,
` ${i18n._('quantities.slope')}: ${slope.at} %${get(this._elevationFill) === 'slope' ? ` (${slope.length} @${slope.segment} %)` : ''}`,
];
if (get(this._elevationFill) === 'surface') {
labels.push(
` ${i18n._('quantities.surface')}: ${i18n._(`toolbar.routing.surface.${surface}`)}`
);
}
if (get(this._elevationFill) === 'highway') {
labels.push(
` ${i18n._('quantities.highway')}: ${i18n._(`toolbar.routing.highway.${highway}`)}${
sacScale
? ` (${i18n._(`toolbar.routing.sac_scale.${sacScale}`)})`
: ''
}`
);
if (mtbScale) {
labels.push(
` ${i18n._('toolbar.routing.mtb_scale')}: ${mtbScale}`
);
}
}
if (point.time) {
labels.push(
` ${i18n._('quantities.time')}: ${i18n.df.format(point.time)}`
);
}
return labels;
},
},
},
zoom: {
pan: {
enabled: true,
mode: 'x',
modifierKey: 'shift',
onPanStart: () => {
this._panning = true;
this._slicedGPXStatistics.set(undefined);
},
onPanComplete: () => {
this._panning = false;
},
},
zoom: {
wheel: {
enabled: true,
},
mode: 'x',
onZoomStart: ({ chart, event }: { chart: Chart; event: any }) => {
if (
event.deltaY < 0 &&
Math.abs(
this._chart.getInitialScaleBounds().x.max /
this._chart.options.plugins.zoom.limits.x.minRange -
this._chart.getZoomLevel()
) < 0.01
) {
// Disable wheel pan if zoomed in to the max, and zooming in
return false;
}
this._slicedGPXStatistics.set(undefined);
},
},
limits: {
x: {
min: 'original',
max: 'original',
minRange: 1,
},
},
},
},
stacked: false,
onResize: () => {
this.updateOverlay();
},
};
let datasets: string[] = ['speed', 'hr', 'cad', 'atemp', 'power'];
datasets.forEach((id) => {
options.scales[`y${id}`] = {
type: 'linear',
position: 'right',
grid: {
display: false,
},
reverse: () => id === 'speed' && get(velocityUnits) === 'pace',
display: false,
};
});
this._chart = new Chart(this._canvas, {
type: 'line',
data: {
datasets: [],
},
options,
plugins: [
{
id: 'toggleMarker',
events: ['mouseout'],
afterEvent: (chart: Chart, args: { event: Chart.ChartEvent }) => {
if (args.event.type === 'mouseout') {
const map_ = get(map);
if (map_ && this._marker) {
this._marker.remove();
}
}
},
},
],
});
let startIndex = 0;
let endIndex = 0;
const getIndex = (evt) => {
if (!this._chart) {
return undefined;
}
const points = this._chart.getElementsAtEventForMode(
evt,
'x',
{
intersect: false,
},
true
);
if (points.length === 0) {
const rect = this._canvas.getBoundingClientRect();
if (evt.x - rect.left <= this._chart.chartArea.left) {
return 0;
} else if (evt.x - rect.left >= this._chart.chartArea.right) {
return get(this._gpxStatistics).local.points.length - 1;
} else {
return undefined;
}
}
let point = points.find((point) => point.element.raw);
if (point) {
return point.element.raw.index;
} else {
return points[0].index;
}
};
let dragStarted = false;
const onMouseDown = (evt) => {
if (evt.shiftKey) {
// Panning interaction
return;
}
dragStarted = true;
this._canvas.style.cursor = 'col-resize';
startIndex = getIndex(evt);
};
const onMouseMove = (evt) => {
if (dragStarted) {
this._dragging = true;
endIndex = getIndex(evt);
if (endIndex !== undefined) {
if (startIndex === undefined) {
startIndex = endIndex;
} else if (startIndex !== endIndex) {
this._slicedGPXStatistics.set([
get(this._gpxStatistics).slice(
Math.min(startIndex, endIndex),
Math.max(startIndex, endIndex)
),
Math.min(startIndex, endIndex),
Math.max(startIndex, endIndex),
]);
}
}
}
};
const onMouseUp = (evt) => {
dragStarted = false;
this._dragging = false;
this._canvas.style.cursor = '';
endIndex = getIndex(evt);
if (startIndex === endIndex) {
this._slicedGPXStatistics.set(undefined);
}
};
this._canvas.addEventListener('pointerdown', onMouseDown);
this._canvas.addEventListener('pointermove', onMouseMove);
this._canvas.addEventListener('pointerup', onMouseUp);
}
updateData() {
if (!this._chart) {
return;
}
const data = get(this._gpxStatistics);
this._chart.data.datasets[0] = {
label: i18n._('quantities.elevation'),
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.ele ? getConvertedElevation(point.ele) : 0,
time: point.time,
slope: {
at: data.local.slope.at[index],
segment: data.local.slope.segment[index],
length: data.local.slope.length[index],
},
extensions: point.getExtensions(),
coordinates: point.getCoordinates(),
index: index,
};
}),
normalized: true,
fill: 'start',
order: 1,
segment: {},
};
this._chart.data.datasets[1] = {
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: getConvertedVelocity(data.local.speed[index]),
index: index,
};
}),
normalized: true,
yAxisID: 'yspeed',
};
this._chart.data.datasets[2] = {
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.getHeartRate(),
index: index,
};
}),
normalized: true,
yAxisID: 'yhr',
};
this._chart.data.datasets[3] = {
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.getCadence(),
index: index,
};
}),
normalized: true,
yAxisID: 'ycad',
};
this._chart.data.datasets[4] = {
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: getConvertedTemperature(point.getTemperature()),
index: index,
};
}),
normalized: true,
yAxisID: 'yatemp',
};
this._chart.data.datasets[5] = {
data: data.local.points.map((point, index) => {
return {
x: getConvertedDistance(data.local.distance.total[index]),
y: point.getPower(),
index: index,
};
}),
normalized: true,
yAxisID: 'ypower',
};
this._chart.options.scales.x['min'] = 0;
this._chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total);
this.setVisibility();
this.setFill();
this._chart.update();
}
updateDataVisibility() {
if (!this._chart) {
return;
}
this.setVisibility();
this._chart.update();
}
setVisibility() {
if (!this._chart) {
return;
}
const additionalDatasets = get(this._additionalDatasets);
let includeSpeed = additionalDatasets.includes('speed');
let includeHeartRate = additionalDatasets.includes('hr');
let includeCadence = additionalDatasets.includes('cad');
let includeTemperature = additionalDatasets.includes('atemp');
let includePower = additionalDatasets.includes('power');
if (this._chart.data.datasets.length == 6) {
this._chart.data.datasets[1].hidden = !includeSpeed;
this._chart.data.datasets[2].hidden = !includeHeartRate;
this._chart.data.datasets[3].hidden = !includeCadence;
this._chart.data.datasets[4].hidden = !includeTemperature;
this._chart.data.datasets[5].hidden = !includePower;
}
}
updateFill() {
if (!this._chart) {
return;
}
this.setFill();
this._chart.update();
}
setFill() {
if (!this._chart) {
return;
}
const elevationFill = get(this._elevationFill);
if (elevationFill === 'slope') {
this._chart.data.datasets[0]['segment'] = {
backgroundColor: this.slopeFillCallback,
};
} else if (elevationFill === 'surface') {
this._chart.data.datasets[0]['segment'] = {
backgroundColor: this.surfaceFillCallback,
};
} else if (elevationFill === 'highway') {
this._chart.data.datasets[0]['segment'] = {
backgroundColor: this.highwayFillCallback,
};
} else {
this._chart.data.datasets[0]['segment'] = {};
}
}
updateOverlay() {
if (!this._chart) {
return;
}
this._overlay.width = this._canvas.width / window.devicePixelRatio;
this._overlay.height = this._canvas.height / window.devicePixelRatio;
this._overlay.style.width = `${this._overlay.width}px`;
this._overlay.style.height = `${this._overlay.height}px`;
const slicedGPXStatistics = get(this._slicedGPXStatistics);
if (slicedGPXStatistics) {
let startIndex = slicedGPXStatistics[1];
let endIndex = slicedGPXStatistics[2];
// Draw selection rectangle
let selectionContext = this._overlay.getContext('2d');
if (selectionContext) {
selectionContext.fillStyle = mode.current === 'dark' ? 'white' : 'black';
selectionContext.globalAlpha = mode.current === 'dark' ? 0.2 : 0.1;
selectionContext.clearRect(0, 0, this._overlay.width, this._overlay.height);
const gpxStatistics = get(this._gpxStatistics);
let startPixel = this._chart.scales.x.getPixelForValue(
getConvertedDistance(gpxStatistics.local.distance.total[startIndex])
);
let endPixel = this._chart.scales.x.getPixelForValue(
getConvertedDistance(gpxStatistics.local.distance.total[endIndex])
);
selectionContext.fillRect(
startPixel,
this._chart.chartArea.top,
endPixel - startPixel,
this._chart.chartArea.height
);
}
} else if (this._overlay) {
let selectionContext = this._overlay.getContext('2d');
if (selectionContext) {
selectionContext.clearRect(0, 0, this._overlay.width, this._overlay.height);
}
}
}
slopeFillCallback(context) {
return getSlopeColor(context.p0.raw.slope.segment);
}
surfaceFillCallback(context) {
return getSurfaceColor(context.p0.raw.extensions.surface);
}
highwayFillCallback(context) {
return getHighwayColor(
context.p0.raw.extensions.highway,
context.p0.raw.extensions.sac_scale,
context.p0.raw.extensions.mtb_scale
);
}
destroy() {
if (this._chart) {
this._chart.destroy();
this._chart = null;
}
if (this._marker) {
this._marker.remove();
}
}
}

View File

@@ -1,264 +1,139 @@
<script lang="ts">
import GPXLayers from '$lib/components/gpx-layer/GPXLayers.svelte';
import ElevationProfile from '$lib/components/ElevationProfile.svelte';
import FileList from '$lib/components/file-list/FileList.svelte';
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
import Map from '$lib/components/Map.svelte';
import LayerControl from '$lib/components/layer-control/LayerControl.svelte';
import OpenIn from '$lib/components/embedding/OpenIn.svelte';
import {
gpxStatistics,
slicedGPXStatistics,
embedding,
loadFile,
map,
updateGPXData
} from '$lib/stores';
import { onDestroy, onMount } from 'svelte';
import { fileObservers, settings, GPXStatisticsTree } from '$lib/db';
import { readable } from 'svelte/store';
import type { GPXFile } from 'gpx';
import { selection } from '$lib/components/file-list/Selection';
import { ListFileItem } from '$lib/components/file-list/FileList';
import { allowedEmbeddingBasemaps, type EmbeddingOptions } from './Embedding';
import { mode, setMode } from 'mode-watcher';
import GPXLayers from '$lib/components/map/gpx-layer/GPXLayers.svelte';
import ElevationProfile from '$lib/components/elevation-profile/ElevationProfile.svelte';
import FileList from '$lib/components/file-list/FileList.svelte';
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
import Map from '$lib/components/map/Map.svelte';
import LayerControl from '$lib/components/map/layer-control/LayerControl.svelte';
import OpenIn from '$lib/components/embedding/OpenIn.svelte';
import { writable } from 'svelte/store';
import type { GPXFile } from 'gpx';
import {
allowedEmbeddingBasemaps,
getFilesFromEmbeddingOptions,
type EmbeddingOptions,
} from './embedding';
import { setMode } from 'mode-watcher';
import { settings } from '$lib/logic/settings';
import { fileStateCollection } from '$lib/logic/file-state';
import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
import { loadFile } from '$lib/logic/file-actions';
import { selection } from '$lib/logic/selection';
import { untrack } from 'svelte';
import { isSelected, toggle } from '$lib/components/map/layer-control/utils';
$embedding = true;
let {
useHash = true,
options = $bindable(),
hash = $bindable(),
}: { useHash?: boolean; options: EmbeddingOptions; hash: string } = $props();
const {
currentBasemap,
distanceUnits,
velocityUnits,
temperatureUnits,
fileOrder,
distanceMarkers,
directionMarkers
} = settings;
let additionalDatasets = writable<string[]>([]);
let elevationFill = writable<'slope' | 'surface' | 'highway' | undefined>(undefined);
export let useHash = true;
export let options: EmbeddingOptions;
export let hash: string;
const {
currentBasemap,
selectedBasemapTree,
distanceUnits,
velocityUnits,
temperatureUnits,
fileOrder,
distanceMarkers,
directionMarkers,
} = settings;
let prevSettings = {
distanceMarkers: false,
directionMarkers: false,
distanceUnits: 'metric',
velocityUnits: 'speed',
temperatureUnits: 'celsius',
theme: 'system'
};
settings.initialize();
function applyOptions() {
fileObservers.update(($fileObservers) => {
$fileObservers.clear();
return $fileObservers;
});
function applyOptions() {
let downloads: Promise<GPXFile | null>[] = getFilesFromEmbeddingOptions(options).map(
(url) => {
return fetch(url)
.then((response) => response.blob())
.then((blob) => new File([blob], url.split('/').pop() ?? url))
.then(loadFile);
}
);
Promise.all(downloads).then((answers) => {
const files = answers.filter((file) => file !== null) as GPXFile[];
let ids: string[] = [];
files.forEach((file, index) => {
let id = `gpx-${index}-embed`;
file._data.id = id;
ids.push(id);
});
fileStateCollection.setEmbeddedFiles(files);
$fileOrder = ids;
selection.selectAll();
});
if (allowedEmbeddingBasemaps.includes(options.basemap)) {
$currentBasemap = options.basemap;
}
if (!isSelected($selectedBasemapTree, options.basemap)) {
$selectedBasemapTree = toggle($selectedBasemapTree, options.basemap);
}
$distanceMarkers = options.distanceMarkers;
$directionMarkers = options.directionMarkers;
$distanceUnits = options.distanceUnits;
$velocityUnits = options.velocityUnits;
$temperatureUnits = options.temperatureUnits;
if (options.theme != 'system') {
setMode(options.theme);
}
let downloads: Promise<GPXFile | null>[] = [];
options.files.forEach((url) => {
downloads.push(
fetch(url)
.then((response) => response.blob())
.then((blob) => new File([blob], url.split('/').pop() ?? url))
.then(loadFile)
);
});
additionalDatasets.set(
[
options.elevation.speed ? 'speed' : null,
options.elevation.hr ? 'hr' : null,
options.elevation.cad ? 'cad' : null,
options.elevation.temp ? 'temp' : null,
options.elevation.power ? 'power' : null,
].filter((dataset) => dataset !== null)
);
elevationFill.set(options.elevation.fill == 'none' ? undefined : options.elevation.fill);
}
Promise.all(downloads).then((files) => {
let ids: string[] = [];
let bounds = {
southWest: {
lat: 90,
lon: 180
},
northEast: {
lat: -90,
lon: -180
}
};
fileObservers.update(($fileObservers) => {
files.forEach((file, index) => {
if (file === null) {
return;
}
let id = `gpx-${index}-embed`;
file._data.id = id;
let statistics = new GPXStatisticsTree(file);
$fileObservers.set(
id,
readable({
file,
statistics
})
);
ids.push(id);
let fileBounds = statistics.getStatisticsFor(new ListFileItem(id)).global.bounds;
bounds.southWest.lat = Math.min(bounds.southWest.lat, fileBounds.southWest.lat);
bounds.southWest.lon = Math.min(bounds.southWest.lon, fileBounds.southWest.lon);
bounds.northEast.lat = Math.max(bounds.northEast.lat, fileBounds.northEast.lat);
bounds.northEast.lon = Math.max(bounds.northEast.lon, fileBounds.northEast.lon);
});
return $fileObservers;
});
$fileOrder = [...$fileOrder.filter((id) => !id.includes('embed')), ...ids];
selection.update(($selection) => {
$selection.clear();
ids.forEach((id) => {
$selection.toggle(new ListFileItem(id));
});
return $selection;
});
if (hash.length === 0) {
map.subscribe(($map) => {
if ($map) {
$map.fitBounds(
[
bounds.southWest.lon,
bounds.southWest.lat,
bounds.northEast.lon,
bounds.northEast.lat
],
{
padding: 80,
linear: true,
easing: () => 1
}
);
}
});
}
});
if (options.basemap !== $currentBasemap && allowedEmbeddingBasemaps.includes(options.basemap)) {
$currentBasemap = options.basemap;
}
if (options.distanceMarkers !== $distanceMarkers) {
$distanceMarkers = options.distanceMarkers;
}
if (options.directionMarkers !== $directionMarkers) {
$directionMarkers = options.directionMarkers;
}
if (options.distanceUnits !== $distanceUnits) {
$distanceUnits = options.distanceUnits;
}
if (options.velocityUnits !== $velocityUnits) {
$velocityUnits = options.velocityUnits;
}
if (options.temperatureUnits !== $temperatureUnits) {
$temperatureUnits = options.temperatureUnits;
}
if (options.theme !== $mode) {
setMode(options.theme);
}
}
onMount(() => {
prevSettings.distanceMarkers = $distanceMarkers;
prevSettings.directionMarkers = $directionMarkers;
prevSettings.distanceUnits = $distanceUnits;
prevSettings.velocityUnits = $velocityUnits;
prevSettings.temperatureUnits = $temperatureUnits;
prevSettings.theme = $mode ?? 'system';
});
$: if (options) {
applyOptions();
}
$: if ($fileOrder) {
updateGPXData();
}
onDestroy(() => {
if ($distanceMarkers !== prevSettings.distanceMarkers) {
$distanceMarkers = prevSettings.distanceMarkers;
}
if ($directionMarkers !== prevSettings.directionMarkers) {
$directionMarkers = prevSettings.directionMarkers;
}
if ($distanceUnits !== prevSettings.distanceUnits) {
$distanceUnits = prevSettings.distanceUnits;
}
if ($velocityUnits !== prevSettings.velocityUnits) {
$velocityUnits = prevSettings.velocityUnits;
}
if ($temperatureUnits !== prevSettings.temperatureUnits) {
$temperatureUnits = prevSettings.temperatureUnits;
}
if ($mode !== prevSettings.theme) {
setMode(prevSettings.theme);
}
$selection.clear();
$fileObservers.clear();
$fileOrder = $fileOrder.filter((id) => !id.includes('embed'));
});
$effect(() => {
options;
untrack(applyOptions);
});
</script>
<div class="absolute flex flex-col h-full w-full border rounded-xl overflow-clip">
<div class="grow relative">
<Map
class="h-full {$fileObservers.size > 1 ? 'horizontal' : ''}"
accessToken={options.token}
geocoder={false}
geolocate={false}
hash={useHash}
/>
<OpenIn bind:files={options.files} />
<LayerControl />
<GPXLayers />
{#if $fileObservers.size > 1}
<div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30">
<FileList orientation="horizontal" />
</div>
{/if}
</div>
<div
class="{options.elevation.show ? '' : 'h-10'} flex flex-row gap-2 px-2 sm:px-4"
style={options.elevation.show ? `height: ${options.elevation.height}px` : ''}
>
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
panelSize={options.elevation.height}
orientation={options.elevation.show ? 'vertical' : 'horizontal'}
/>
{#if options.elevation.show}
<ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
additionalDatasets={[
options.elevation.speed ? 'speed' : null,
options.elevation.hr ? 'hr' : null,
options.elevation.cad ? 'cad' : null,
options.elevation.temp ? 'temp' : null,
options.elevation.power ? 'power' : null
].filter((dataset) => dataset !== null)}
elevationFill={options.elevation.fill}
panelSize={options.elevation.height}
showControls={options.elevation.controls}
class="py-2"
/>
{/if}
</div>
<div class="grow relative">
<Map
class="h-full {$fileStateCollection.size > 1 ? 'horizontal' : ''}"
accessToken={options.token}
geocoder={false}
geolocate={true}
hash={useHash}
/>
<OpenIn files={options.files} ids={options.ids} />
<LayerControl />
<GPXLayers />
{#if $fileStateCollection.size > 1}
<div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30">
<FileList orientation="horizontal" />
</div>
{/if}
</div>
<div
class="{options.elevation.show ? '' : 'h-10'} flex flex-row gap-2 px-2 sm:px-4"
style={options.elevation.show ? `height: ${options.elevation.height}px` : ''}
>
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
panelSize={options.elevation.height}
orientation={options.elevation.show ? 'vertical' : 'horizontal'}
/>
{#if options.elevation.show}
<ElevationProfile
{gpxStatistics}
{slicedGPXStatistics}
{additionalDatasets}
{elevationFill}
showControls={options.elevation.controls}
/>
{/if}
</div>
</div>

View File

@@ -1,80 +0,0 @@
import { basemaps } from "$lib/assets/layers";
export type EmbeddingOptions = {
token: string;
files: string[];
basemap: string;
elevation: {
show: boolean;
height: number,
controls: boolean,
fill: 'slope' | 'surface' | undefined,
speed: boolean,
hr: boolean,
cad: boolean,
temp: boolean,
power: boolean,
},
distanceMarkers: boolean,
directionMarkers: boolean,
distanceUnits: 'metric' | 'imperial',
velocityUnits: 'speed' | 'pace',
temperatureUnits: 'celsius' | 'fahrenheit',
theme: 'system' | 'light' | 'dark',
};
export const defaultEmbeddingOptions = {
token: '',
files: [],
basemap: 'mapboxOutdoors',
elevation: {
show: true,
height: 170,
controls: true,
fill: undefined,
speed: false,
hr: false,
cad: false,
temp: false,
power: false,
},
distanceMarkers: false,
directionMarkers: false,
distanceUnits: 'metric',
velocityUnits: 'speed',
temperatureUnits: 'celsius',
theme: 'system',
};
export function getDefaultEmbeddingOptions(): EmbeddingOptions {
return JSON.parse(JSON.stringify(defaultEmbeddingOptions));
}
export function getMergedEmbeddingOptions(options: any, defaultOptions: any = defaultEmbeddingOptions): EmbeddingOptions {
const mergedOptions = JSON.parse(JSON.stringify(defaultOptions));
for (const key in options) {
if (typeof options[key] === 'object' && options[key] !== null && !Array.isArray(options[key])) {
mergedOptions[key] = getMergedEmbeddingOptions(options[key], defaultOptions[key]);
} else {
mergedOptions[key] = options[key];
}
}
return mergedOptions;
}
export function getCleanedEmbeddingOptions(options: any, defaultOptions: any = defaultEmbeddingOptions): any {
const cleanedOptions = JSON.parse(JSON.stringify(options));
for (const key in cleanedOptions) {
if (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) {
delete cleanedOptions[key];
}
} else if (JSON.stringify(cleanedOptions[key]) === JSON.stringify(defaultOptions[key])) {
delete cleanedOptions[key];
}
}
return cleanedOptions;
}
export const allowedEmbeddingBasemaps = Object.keys(basemaps).filter(basemap => !['ordnanceSurvey'].includes(basemap));

View File

@@ -1,313 +1,329 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { Label } from '$lib/components/ui/label';
import { Input } from '$lib/components/ui/input';
import * as Select from '$lib/components/ui/select';
import { Checkbox } from '$lib/components/ui/checkbox';
import * as RadioGroup from '$lib/components/ui/radio-group';
import {
Zap,
HeartPulse,
Orbit,
Thermometer,
SquareActivity,
Coins,
Milestone,
Video
} from 'lucide-svelte';
import { _ } from 'svelte-i18n';
import {
allowedEmbeddingBasemaps,
getCleanedEmbeddingOptions,
getDefaultEmbeddingOptions
} from './Embedding';
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import Embedding from './Embedding.svelte';
import { map } from '$lib/stores';
import { tick } from 'svelte';
import { base } from '$app/paths';
import * as Card from '$lib/components/ui/card';
import { Label } from '$lib/components/ui/label';
import { Input } from '$lib/components/ui/input';
import * as Select from '$lib/components/ui/select';
import { Checkbox } from '$lib/components/ui/checkbox';
import * as RadioGroup from '$lib/components/ui/radio-group';
import {
Zap,
HeartPulse,
Orbit,
Thermometer,
SquareActivity,
Coins,
Milestone,
Video,
} from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte';
import {
allowedEmbeddingBasemaps,
defaultEmbeddingOptions,
getCleanedEmbeddingOptions,
getMergedEmbeddingOptions,
} from './embedding';
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import Embedding from './Embedding.svelte';
import { onDestroy } from 'svelte';
import { base } from '$app/paths';
import { map } from '$lib/components/map/map';
import { mode } from 'mode-watcher';
let options = getDefaultEmbeddingOptions();
options.token = 'YOUR_MAPBOX_TOKEN';
options.files = [
'https://raw.githubusercontent.com/gpxstudio/gpx.studio/main/gpx/test-data/simple.gpx'
];
let options = $state(
getMergedEmbeddingOptions(
{
token: 'YOUR_MAPBOX_TOKEN',
theme: mode.current,
},
defaultEmbeddingOptions
)
);
let files = $state(
'https://raw.githubusercontent.com/gpxstudio/gpx.studio/main/gpx/test-data/simple.gpx'
);
let driveIds = $state('');
let files = options.files[0];
$: if (files) {
let urls = files.split(',');
urls = urls.filter((url) => url.length > 0);
if (JSON.stringify(urls) !== JSON.stringify(options.files)) {
options.files = urls;
}
}
let iframeOptions = $derived(
getMergedEmbeddingOptions(
{
token:
options.token.length === 0 || options.token === 'YOUR_MAPBOX_TOKEN'
? PUBLIC_MAPBOX_TOKEN
: options.token,
files: files.split(',').filter((url) => url.length > 0),
ids: driveIds.split(',').filter((id) => id.length > 0),
elevation: {
fill: options.elevation.fill === 'none' ? undefined : options.elevation.fill,
},
},
options
)
);
let manualCamera = false;
let manualCamera = $state(false);
let zoom = $state('0');
let lat = $state('0');
let lon = $state('0');
let bearing = $state('0');
let pitch = $state('0');
let hash = $derived(manualCamera ? `#${zoom}/${lat}/${lon}/${bearing}/${pitch}` : '');
let zoom = '0';
let lat = '0';
let lon = '0';
let bearing = '0';
let pitch = '0';
$effect(() => {
if (options.elevation.show || options.elevation.height) {
map.resize();
}
});
$: hash = manualCamera ? `#${zoom}/${lat}/${lon}/${bearing}/${pitch}` : '';
function updateCamera() {
if ($map) {
let center = $map.getCenter();
lat = center.lat.toFixed(4);
lon = center.lng.toFixed(4);
zoom = $map.getZoom().toFixed(2);
bearing = $map.getBearing().toFixed(1);
pitch = $map.getPitch().toFixed(0);
}
}
$: iframeOptions =
options.token.length === 0 || options.token === 'YOUR_MAPBOX_TOKEN'
? Object.assign({}, options, { token: PUBLIC_MAPBOX_TOKEN })
: options;
map.onLoad((map_) => {
map_.on('moveend', updateCamera);
});
async function resizeMap() {
if ($map) {
await tick();
$map.resize();
}
}
$: if (options.elevation.height || options.elevation.show) {
resizeMap();
}
function updateCamera() {
if ($map) {
let center = $map.getCenter();
lat = center.lat.toFixed(4);
lon = center.lng.toFixed(4);
zoom = $map.getZoom().toFixed(2);
bearing = $map.getBearing().toFixed(1);
pitch = $map.getPitch().toFixed(0);
}
}
$: if ($map) {
$map.on('moveend', updateCamera);
}
onDestroy(() => {
if ($map) {
$map.off('moveend', updateCamera);
}
});
</script>
<Card.Root>
<Card.Header>
<Card.Title>{$_('embedding.title')}</Card.Title>
</Card.Header>
<Card.Content>
<fieldset class="flex flex-col gap-3">
<Label for="token">{$_('embedding.mapbox_token')}</Label>
<Input id="token" type="text" class="h-8" bind:value={options.token} />
<Label for="file_urls">{$_('embedding.file_urls')}</Label>
<Input id="file_urls" type="text" class="h-8" bind:value={files} />
<Label for="basemap">{$_('embedding.basemap')}</Label>
<Select.Root
selected={{ value: options.basemap, label: $_(`layers.label.${options.basemap}`) }}
onSelectedChange={(selected) => {
if (selected?.value) {
options.basemap = selected?.value;
}
}}
>
<Select.Trigger id="basemap" class="w-full h-8">
<Select.Value />
</Select.Trigger>
<Select.Content class="max-h-60 overflow-y-scroll">
{#each allowedEmbeddingBasemaps as basemap}
<Select.Item value={basemap}>{$_(`layers.label.${basemap}`)}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
<div class="flex flex-row items-center gap-2">
<Label for="profile">{$_('menu.elevation_profile')}</Label>
<Checkbox id="profile" bind:checked={options.elevation.show} />
</div>
{#if options.elevation.show}
<div class="grid grid-cols-2 gap-x-6 gap-y-3 rounded-md border p-3 mt-1">
<Label class="flex flex-row items-center gap-2">
{$_('embedding.height')}
<Input type="number" bind:value={options.elevation.height} class="h-8 w-20" />
</Label>
<div class="flex flex-row items-center gap-2">
<span class="shrink-0">
{$_('embedding.fill_by')}
</span>
<Select.Root
selected={{ value: 'none', label: $_('embedding.none') }}
onSelectedChange={(selected) => {
let value = selected?.value;
if (value === 'none') {
options.elevation.fill = undefined;
} else if (value === 'slope' || value === 'surface') {
options.elevation.fill = value;
}
}}
>
<Select.Trigger class="grow h-8">
<Select.Value />
</Select.Trigger>
<Select.Content>
<Select.Item value="slope">{$_('quantities.slope')}</Select.Item>
<Select.Item value="surface">{$_('quantities.surface')}</Select.Item>
<Select.Item value="none">{$_('embedding.none')}</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div class="flex flex-row items-center gap-2">
<Checkbox id="controls" bind:checked={options.elevation.controls} />
<Label for="controls">{$_('embedding.show_controls')}</Label>
</div>
<div class="flex flex-row items-center gap-2">
<Checkbox id="show-speed" bind:checked={options.elevation.speed} />
<Label for="show-speed" class="flex flex-row items-center gap-1">
<Zap size="16" />
{$_('chart.show_speed')}
</Label>
</div>
<div class="flex flex-row items-center gap-2">
<Checkbox id="show-hr" bind:checked={options.elevation.hr} />
<Label for="show-hr" class="flex flex-row items-center gap-1">
<HeartPulse size="16" />
{$_('chart.show_heartrate')}
</Label>
</div>
<div class="flex flex-row items-center gap-2">
<Checkbox id="show-cad" bind:checked={options.elevation.cad} />
<Label for="show-cad" class="flex flex-row items-center gap-1">
<Orbit size="16" />
{$_('chart.show_cadence')}
</Label>
</div>
<div class="flex flex-row items-center gap-2">
<Checkbox id="show-temp" bind:checked={options.elevation.temp} />
<Label for="show-temp" class="flex flex-row items-center gap-1">
<Thermometer size="16" />
{$_('chart.show_temperature')}
</Label>
</div>
<div class="flex flex-row items-center gap-2">
<Checkbox id="show-power" bind:checked={options.elevation.power} />
<Label for="show-power" class="flex flex-row items-center gap-1">
<SquareActivity size="16" />
{$_('chart.show_power')}
</Label>
</div>
</div>
{/if}
<div class="flex flex-row items-center gap-2">
<Checkbox id="distance-markers" bind:checked={options.distanceMarkers} />
<Label for="distance-markers" class="flex flex-row items-center gap-1">
<Coins size="16" />
{$_('menu.distance_markers')}
</Label>
</div>
<div class="flex flex-row items-center gap-2">
<Checkbox id="direction-markers" bind:checked={options.directionMarkers} />
<Label for="direction-markers" class="flex flex-row items-center gap-1">
<Milestone size="16" />
{$_('menu.direction_markers')}
</Label>
</div>
<div class="flex flex-row flex-wrap justify-between gap-3">
<Label class="flex flex-col items-start gap-2">
{$_('menu.distance_units')}
<RadioGroup.Root bind:value={options.distanceUnits}>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="metric" id="metric" />
<Label for="metric">{$_('menu.metric')}</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="imperial" id="imperial" />
<Label for="imperial">{$_('menu.imperial')}</Label>
</div>
</RadioGroup.Root>
</Label>
<Label class="flex flex-col items-start gap-2">
{$_('menu.velocity_units')}
<RadioGroup.Root bind:value={options.velocityUnits}>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="speed" id="speed" />
<Label for="speed">{$_('quantities.speed')}</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="pace" id="pace" />
<Label for="pace">{$_('quantities.pace')}</Label>
</div>
</RadioGroup.Root>
</Label>
<Label class="flex flex-col items-start gap-2">
{$_('menu.temperature_units')}
<RadioGroup.Root bind:value={options.temperatureUnits}>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="celsius" id="celsius" />
<Label for="celsius">{$_('menu.celsius')}</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="fahrenheit" id="fahrenheit" />
<Label for="fahrenheit">{$_('menu.fahrenheit')}</Label>
</div>
</RadioGroup.Root>
</Label>
</div>
<Label class="flex flex-col items-start gap-2">
{$_('menu.mode')}
<RadioGroup.Root bind:value={options.theme} class="flex flex-row">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="system" id="system" />
<Label for="system">{$_('menu.system')}</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="light" id="light" />
<Label for="light">{$_('menu.light')}</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="dark" id="dark" />
<Label for="dark">{$_('menu.dark')}</Label>
</div>
</RadioGroup.Root>
</Label>
<div class="flex flex-col gap-3 p-3 border rounded-md">
<div class="flex flex-row items-center gap-2">
<Checkbox id="manual-camera" bind:checked={manualCamera} />
<Label for="manual-camera" class="flex flex-row items-center gap-1">
<Video size="16" />
{$_('embedding.manual_camera')}
</Label>
</div>
<p class="text-sm text-muted-foreground">
{$_('embedding.manual_camera_description')}
</p>
<div class="flex flex-row flex-wrap items-center gap-6">
<Label class="flex flex-col gap-1">
<span>{$_('embedding.latitude')}</span>
<span>{lat}</span>
</Label>
<Label class="flex flex-col gap-1">
<span>{$_('embedding.longitude')}</span>
<span>{lon}</span>
</Label>
<Label class="flex flex-col gap-1">
<span>{$_('embedding.zoom')}</span>
<span>{zoom}</span>
</Label>
<Label class="flex flex-col gap-1">
<span>{$_('embedding.bearing')}</span>
<span>{bearing}</span>
</Label>
<Label class="flex flex-col gap-1">
<span>{$_('embedding.pitch')}</span>
<span>{pitch}</span>
</Label>
</div>
</div>
<Label>
{$_('embedding.preview')}
</Label>
<div class="relative h-[600px]">
<Embedding bind:options={iframeOptions} bind:hash useHash={false} />
</div>
<Label>
{$_('embedding.code')}
</Label>
<pre class="bg-primary text-primary-foreground p-3 rounded-md whitespace-normal break-all">
<Card.Root id="embedding-playground">
<Card.Header>
<Card.Title>{i18n._('embedding.title')}</Card.Title>
</Card.Header>
<Card.Content>
<fieldset class="flex flex-col gap-3">
<Label for="token">{i18n._('embedding.mapbox_token')}</Label>
<Input id="token" type="text" class="h-8" bind:value={options.token} />
<Label for="file_urls">{i18n._('embedding.file_urls')}</Label>
<Input id="file_urls" type="text" class="h-8" bind:value={files} />
<Label for="drive_ids">{i18n._('embedding.drive_ids')}</Label>
<Input id="drive_ids" type="text" class="h-8" bind:value={driveIds} />
<Label for="basemap">{i18n._('embedding.basemap')}</Label>
<Select.Root type="single" bind:value={options.basemap}>
<Select.Trigger id="basemap" class="w-full h-8">
{i18n._(`layers.label.${options.basemap}`)}
</Select.Trigger>
<Select.Content class="max-h-60 overflow-y-scroll">
{#each allowedEmbeddingBasemaps as basemap}
<Select.Item value={basemap}
>{i18n._(`layers.label.${basemap}`)}</Select.Item
>
{/each}
</Select.Content>
</Select.Root>
<div class="flex flex-row items-center gap-2">
<Label for="profile">{i18n._('menu.elevation_profile')}</Label>
<Checkbox id="profile" bind:checked={options.elevation.show} />
</div>
{#if options.elevation.show}
<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">
{i18n._('embedding.height')}
<Input
type="number"
bind:value={options.elevation.height}
class="h-8 w-20"
/>
</Label>
<div class="flex flex-row items-center gap-2">
<span class="shrink-0">
{i18n._('embedding.fill_by')}
</span>
<Select.Root type="single" bind:value={options.elevation.fill}>
<Select.Trigger class="grow h-8">
{options.elevation.fill !== 'none'
? i18n._(`quantities.${options.elevation.fill}`)
: i18n._('embedding.none')}
</Select.Trigger>
<Select.Content>
<Select.Item value="slope">{i18n._('quantities.slope')}</Select.Item
>
<Select.Item value="surface"
>{i18n._('quantities.surface')}</Select.Item
>
<Select.Item value="highway"
>{i18n._('quantities.highway')}</Select.Item
>
<Select.Item value="none">{i18n._('embedding.none')}</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div class="flex flex-row items-center gap-2">
<Checkbox id="controls" bind:checked={options.elevation.controls} />
<Label for="controls">{i18n._('embedding.show_controls')}</Label>
</div>
<div class="flex flex-row items-center gap-2">
<Checkbox id="show-speed" bind:checked={options.elevation.speed} />
<Label for="show-speed" class="flex flex-row items-center gap-1">
<Zap size="16" />
{i18n._('quantities.speed')}
</Label>
</div>
<div class="flex flex-row items-center gap-2">
<Checkbox id="show-hr" bind:checked={options.elevation.hr} />
<Label for="show-hr" class="flex flex-row items-center gap-1">
<HeartPulse size="16" />
{i18n._('quantities.heartrate')}
</Label>
</div>
<div class="flex flex-row items-center gap-2">
<Checkbox id="show-cad" bind:checked={options.elevation.cad} />
<Label for="show-cad" class="flex flex-row items-center gap-1">
<Orbit size="16" />
{i18n._('quantities.cadence')}
</Label>
</div>
<div class="flex flex-row items-center gap-2">
<Checkbox id="show-temp" bind:checked={options.elevation.temp} />
<Label for="show-temp" class="flex flex-row items-center gap-1">
<Thermometer size="16" />
{i18n._('quantities.temperature')}
</Label>
</div>
<div class="flex flex-row items-center gap-2">
<Checkbox id="show-power" bind:checked={options.elevation.power} />
<Label for="show-power" class="flex flex-row items-center gap-1">
<SquareActivity size="16" />
{i18n._('quantities.power')}
</Label>
</div>
</div>
{/if}
<div class="flex flex-row items-center gap-2">
<Checkbox id="distance-markers" bind:checked={options.distanceMarkers} />
<Label for="distance-markers" class="flex flex-row items-center gap-1">
<Coins size="16" />
{i18n._('menu.distance_markers')}
</Label>
</div>
<div class="flex flex-row items-center gap-2">
<Checkbox id="direction-markers" bind:checked={options.directionMarkers} />
<Label for="direction-markers" class="flex flex-row items-center gap-1">
<Milestone size="16" />
{i18n._('menu.direction_markers')}
</Label>
</div>
<div class="flex flex-row flex-wrap justify-between gap-3">
<Label class="flex flex-col items-start gap-2">
{i18n._('menu.distance_units')}
<RadioGroup.Root bind:value={options.distanceUnits}>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="metric" id="metric" />
<Label for="metric">{i18n._('menu.metric')}</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="imperial" id="imperial" />
<Label for="imperial">{i18n._('menu.imperial')}</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="nautical" id="nautical" />
<Label for="nautical">{i18n._('menu.nautical')}</Label>
</div>
</RadioGroup.Root>
</Label>
<Label class="flex flex-col items-start gap-2">
{i18n._('menu.velocity_units')}
<RadioGroup.Root bind:value={options.velocityUnits}>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="speed" id="speed" />
<Label for="speed">{i18n._('quantities.speed')}</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="pace" id="pace" />
<Label for="pace">{i18n._('quantities.pace')}</Label>
</div>
</RadioGroup.Root>
</Label>
<Label class="flex flex-col items-start gap-2">
{i18n._('menu.temperature_units')}
<RadioGroup.Root bind:value={options.temperatureUnits}>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="celsius" id="celsius" />
<Label for="celsius">{i18n._('menu.celsius')}</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="fahrenheit" id="fahrenheit" />
<Label for="fahrenheit">{i18n._('menu.fahrenheit')}</Label>
</div>
</RadioGroup.Root>
</Label>
</div>
<Label class="flex flex-col items-start gap-2">
{i18n._('menu.mode')}
<RadioGroup.Root bind:value={options.theme} class="flex flex-row">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="system" id="system" />
<Label for="system">{i18n._('menu.system')}</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="light" id="light" />
<Label for="light">{i18n._('menu.light')}</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="dark" id="dark" />
<Label for="dark">{i18n._('menu.dark')}</Label>
</div>
</RadioGroup.Root>
</Label>
<div class="flex flex-col gap-3 p-3 border rounded-md">
<div class="flex flex-row items-center gap-2">
<Checkbox id="manual-camera" bind:checked={manualCamera} />
<Label for="manual-camera" class="flex flex-row items-center gap-1">
<Video size="16" />
{i18n._('embedding.manual_camera')}
</Label>
</div>
<p class="text-sm text-muted-foreground">
{i18n._('embedding.manual_camera_description')}
</p>
<div class="flex flex-row flex-wrap items-center gap-6">
<Label class="flex flex-col gap-1">
<span>{i18n._('embedding.latitude')}</span>
<span>{lat}</span>
</Label>
<Label class="flex flex-col gap-1">
<span>{i18n._('embedding.longitude')}</span>
<span>{lon}</span>
</Label>
<Label class="flex flex-col gap-1">
<span>{i18n._('embedding.zoom')}</span>
<span>{zoom}</span>
</Label>
<Label class="flex flex-col gap-1">
<span>{i18n._('embedding.bearing')}</span>
<span>{bearing}</span>
</Label>
<Label class="flex flex-col gap-1">
<span>{i18n._('embedding.pitch')}</span>
<span>{pitch}</span>
</Label>
</div>
</div>
<Label>
{i18n._('embedding.preview')}
</Label>
<div class="relative h-[600px]">
<Embedding options={iframeOptions} bind:hash useHash={false} />
</div>
<Label>
{i18n._('embedding.code')}
</Label>
<pre
class="bg-primary text-primary-foreground p-3 rounded-md whitespace-normal break-all">
<code class="language-html">
{`<iframe src="https://gpx.studio${base}/embed?options=${encodeURIComponent(JSON.stringify(getCleanedEmbeddingOptions(options)))}${hash}" width="100%" height="600px" frameborder="0" style="outline: none;"/>`}
{`<iframe src="https://gpx.studio${base}/embed?options=${encodeURIComponent(JSON.stringify(getCleanedEmbeddingOptions(iframeOptions)))}${hash}" width="100%" height="600px" frameborder="0" style="outline: none;"/>`}
</code>
</pre>
</fieldset>
</Card.Content>
</fieldset>
</Card.Content>
</Card.Root>

View File

@@ -1,18 +1,28 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import Logo from '$lib/components/Logo.svelte';
import { getURLForLanguage } from '$lib/utils';
import { _, locale } from 'svelte-i18n';
import { Button } from '$lib/components/ui/button';
import Logo from '$lib/components/Logo.svelte';
import { getURLForLanguage } from '$lib/utils';
import { i18n } from '$lib/i18n.svelte';
export let files: string[];
let {
files,
ids,
}: {
files: string[];
ids: string[];
} = $props();
</script>
<Button
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"
href="{getURLForLanguage($locale, '/app')}?files={encodeURIComponent(JSON.stringify(files))}"
target="_blank"
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"
href="{getURLForLanguage(i18n.lang, '/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"
>
{$_('menu.open_in')}
<Logo class="h-[18px] xs:h-5 translate-y-[1px]" />
{i18n._('menu.open_in')}
<Logo class="h-[18px] xs:h-5 translate-y-[1px]" />
</Button>

View File

@@ -0,0 +1,151 @@
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
import { basemaps } from '$lib/assets/layers';
export type EmbeddingOptions = {
token: string;
files: string[];
ids: string[];
basemap: string;
elevation: {
show: boolean;
height: number;
controls: boolean;
fill: 'slope' | 'surface' | 'highway' | 'none';
speed: boolean;
hr: boolean;
cad: boolean;
temp: boolean;
power: boolean;
};
distanceMarkers: boolean;
directionMarkers: boolean;
distanceUnits: 'metric' | 'imperial' | 'nautical';
velocityUnits: 'speed' | 'pace';
temperatureUnits: 'celsius' | 'fahrenheit';
theme: 'system' | 'light' | 'dark';
};
export const defaultEmbeddingOptions = {
token: '',
files: [],
ids: [],
basemap: 'mapboxOutdoors',
elevation: {
show: true,
height: 170,
controls: true,
fill: 'none',
speed: false,
hr: false,
cad: false,
temp: false,
power: false,
},
distanceMarkers: false,
directionMarkers: false,
distanceUnits: 'metric',
velocityUnits: 'speed',
temperatureUnits: 'celsius',
theme: 'system',
};
export function getMergedEmbeddingOptions(
options: any,
defaultOptions: any = defaultEmbeddingOptions
): EmbeddingOptions {
const mergedOptions = JSON.parse(JSON.stringify(defaultOptions));
for (const key in options) {
if (
typeof options[key] === 'object' &&
options[key] !== null &&
!Array.isArray(options[key])
) {
mergedOptions[key] = getMergedEmbeddingOptions(options[key], defaultOptions[key]);
} else {
mergedOptions[key] = options[key];
}
}
return mergedOptions;
}
export function getCleanedEmbeddingOptions(
options: any,
defaultOptions: any = defaultEmbeddingOptions
): any {
const cleanedOptions = JSON.parse(JSON.stringify(options));
for (const key in cleanedOptions) {
if (
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) {
delete cleanedOptions[key];
}
} else if (JSON.stringify(cleanedOptions[key]) === JSON.stringify(defaultOptions[key])) {
delete cleanedOptions[key];
}
}
return cleanedOptions;
}
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;
}

View File

@@ -0,0 +1,195 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Label } from '$lib/components/ui/label';
import { Checkbox } from '$lib/components/ui/checkbox';
import { Separator } from '$lib/components/ui/separator';
import { Dialog } from 'bits-ui';
import {
exportAllFiles,
exportSelectedFiles,
ExportState,
exportState,
} from '$lib/components/export/utils.svelte';
import { currentTool } from '$lib/components/toolbar/tools';
import {
Download,
Zap,
Earth,
HeartPulse,
Orbit,
Thermometer,
SquareActivity,
} from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte';
import { GPXStatistics } from 'gpx';
import { ListRootItem } from '$lib/components/file-list/file-list';
import { fileStateCollection } from '$lib/logic/file-state';
import { selection } from '$lib/logic/selection';
import { gpxStatistics } from '$lib/logic/statistics';
import { get } from 'svelte/store';
let open = $derived(exportState.current !== ExportState.NONE);
let exportOptions: Record<string, boolean> = $state({
time: true,
hr: true,
cad: true,
atemp: true,
power: true,
extensions: false,
});
let hide: Record<string, boolean> = $derived.by(() => {
if (exportState.current === ExportState.NONE) {
return {
time: false,
hr: false,
cad: false,
atemp: false,
power: false,
extensions: false,
};
} else {
let statistics = $gpxStatistics;
if (exportState.current === ExportState.ALL) {
statistics = Array.from(get(fileStateCollection).values())
.map((file) => file.statistics)
.reduce((acc, cur) => {
if (cur !== undefined) {
acc.mergeWith(cur.getStatisticsFor(new ListRootItem()));
}
return acc;
}, new GPXStatistics());
}
return {
time: statistics.global.time.total === 0,
hr: statistics.global.hr.count === 0,
cad: statistics.global.cad.count === 0,
atemp: statistics.global.atemp.count === 0,
power: statistics.global.power.count === 0,
extensions: Object.keys(statistics.global.extensions).length === 0,
};
}
});
let exclude = $derived(Object.keys(exportOptions).filter((key) => !exportOptions[key]));
$effect(() => {
if (open) {
currentTool.set(null);
}
});
</script>
<Dialog.Root
bind:open
onOpenChange={(isOpen) => {
if (!isOpen) {
exportState.current = ExportState.NONE;
}
}}
>
<Dialog.Trigger class="hidden" />
<Dialog.Portal>
<Dialog.Content
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
class="w-full flex flex-col sm:flex-row items-center justify-center gap-1 sm:gap-2 border rounded-md p-2 bg-secondary"
>
<span class="w-12 shrink-0 text-center text-xl">⚠️</span>
<span class="text-sm">
{i18n._('menu.support_message')}
</span>
</div>
<div class="w-full flex flex-row flex-wrap gap-2">
<Button class="bg-support grow" href="https://ko-fi.com/gpxstudio" target="_blank">
{i18n._('menu.support_button')}
<span>🙏</span>
</Button>
<Button
variant="outline"
class="grow"
onclick={() => {
if (exportState.current === ExportState.SELECTION) {
exportSelectedFiles(exclude);
} else if (exportState.current === ExportState.ALL) {
exportAllFiles(exclude);
}
open = false;
exportState.current = ExportState.NONE;
}}
>
<Download size="16" />
{#if $fileStateCollection.size === 1 || (exportState.current === ExportState.SELECTION && $selection.size === 1)}
{i18n._('menu.download_file')}
{:else}
{i18n._('menu.download_files')}
{/if}
</Button>
</div>
<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="grow">
<Separator />
</div>
<Label class="shrink-0">
{i18n._('menu.export_options')}
</Label>
<div class="grow">
<Separator />
</div>
</div>
<div class="flex flex-row flex-wrap justify-center gap-x-6 gap-y-2">
<div class="flex flex-row items-center gap-1.5 {hide.time ? 'hidden' : ''}">
<Checkbox id="export-time" bind:checked={exportOptions.time} />
<Label for="export-time" class="flex flex-row items-center gap-1">
<Zap size="16" />
{i18n._('quantities.time')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.hr ? 'hidden' : ''}">
<Checkbox id="export-heartrate" bind:checked={exportOptions.hr} />
<Label for="export-heartrate" class="flex flex-row items-center gap-1">
<HeartPulse size="16" />
{i18n._('quantities.heartrate')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.cad ? 'hidden' : ''}">
<Checkbox id="export-cadence" bind:checked={exportOptions.cad} />
<Label for="export-cadence" class="flex flex-row items-center gap-1">
<Orbit size="16" />
{i18n._('quantities.cadence')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.atemp ? 'hidden' : ''}">
<Checkbox id="export-temperature" bind:checked={exportOptions.atemp} />
<Label for="export-temperature" class="flex flex-row items-center gap-1">
<Thermometer size="16" />
{i18n._('quantities.temperature')}
</Label>
</div>
<div class="flex flex-row items-center gap-1.5 {hide.power ? 'hidden' : ''}">
<Checkbox id="export-power" bind:checked={exportOptions.power} />
<Label for="export-power" class="flex flex-row items-center gap-1">
<SquareActivity size="16" />
{i18n._('quantities.power')}
</Label>
</div>
<div
class="flex flex-row items-center gap-1.5 {hide.extensions ? 'hidden' : ''}"
>
<Checkbox id="export-extensions" bind:checked={exportOptions.extensions} />
<Label for="export-extensions" class="flex flex-row items-center gap-1">
<Earth size="16" />
{i18n._('quantities.osm_extensions')}
</Label>
</div>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>

View File

@@ -0,0 +1,66 @@
import { selection } from '$lib/logic/selection';
import { fileStateCollection } from '$lib/logic/file-state';
import { settings } from '$lib/logic/settings';
import { buildGPX, type GPXFile } from 'gpx';
import FileSaver from 'file-saver';
import JSZip from 'jszip';
import { get } from 'svelte/store';
export enum ExportState {
NONE,
SELECTION,
ALL,
}
export const exportState = $state({
current: ExportState.NONE,
});
async function exportFiles(fileIds: string[], exclude: string[]) {
if (fileIds.length > 1) {
await exportFilesAsZip(fileIds, exclude);
} else {
const firstFileId = fileIds.at(0);
if (firstFileId != null) {
const file = fileStateCollection.getFile(firstFileId);
if (file) {
exportFile(file, exclude);
}
}
}
}
export async function exportSelectedFiles(exclude: string[]) {
const fileIds: string[] = [];
selection.applyToOrderedSelectedItemsFromFile(async (fileId, level, items) => {
fileIds.push(fileId);
});
await exportFiles(fileIds, exclude);
}
export async function exportAllFiles(exclude: string[]) {
await exportFiles(get(settings.fileOrder), exclude);
}
function exportFile(file: GPXFile, exclude: string[]) {
const blob = new Blob([buildGPX(file, exclude)], { type: 'application/gpx+xml' });
FileSaver.saveAs(blob, `${file.metadata.name}.gpx`);
}
async function exportFilesAsZip(fileIds: string[], exclude: string[]) {
const zip = new JSZip();
for (const fileId of fileIds) {
const file = fileStateCollection.getFile(fileId);
if (file) {
const gpx = buildGPX(file, exclude);
let filename = file.metadata.name;
for (let i = 1; zip.files[filename + '.gpx']; i++) {
filename = file.metadata.name + `-${i}`;
}
zip.file(filename + '.gpx', gpx);
}
}
if (Object.keys(zip.files).length > 0) {
const blob = await zip.generateAsync({ type: 'blob' });
FileSaver.saveAs(blob, 'gpx-files.zip');
}
}

View File

@@ -1,89 +1,91 @@
<script lang="ts">
import { ScrollArea } from '$lib/components/ui/scroll-area/index';
import * as ContextMenu from '$lib/components/ui/context-menu';
import FileListNode from './FileListNode.svelte';
import { fileObservers, settings } from '$lib/db';
import { setContext } from 'svelte';
import { ListFileItem, ListLevel, ListRootItem, allowedPastes } from './FileList';
import { copied, pasteSelection, selectAll, selection } from './Selection';
import { ClipboardPaste, FileStack, Plus } from 'lucide-svelte';
import Shortcut from '$lib/components/Shortcut.svelte';
import { _ } from 'svelte-i18n';
import { createFile } from '$lib/stores';
import { ScrollArea } from '$lib/components/ui/scroll-area/index';
import * as ContextMenu from '$lib/components/ui/context-menu';
import FileListNode from './FileListNode.svelte';
import { onMount, setContext } from 'svelte';
import { ListFileItem, ListLevel, ListRootItem } from './file-list';
import { ClipboardPaste, FileStack, Plus } from '@lucide/svelte';
import Shortcut from '$lib/components/Shortcut.svelte';
import { i18n } from '$lib/i18n.svelte';
import { fileStateCollection } from '$lib/logic/file-state';
import { createFile, pasteSelection } from '$lib/logic/file-actions';
import { selection, copied } from '$lib/logic/selection';
import { allowedPastes } from './sortable-file-list';
export let orientation: 'vertical' | 'horizontal';
export let recursive = false;
let {
orientation,
recursive = false,
class: className = '',
style = '',
}: {
orientation: 'vertical' | 'horizontal';
recursive?: boolean;
class?: string;
style?: string;
} = $props();
setContext('orientation', orientation);
setContext('recursive', recursive);
setContext('orientation', orientation);
setContext('recursive', recursive);
const { verticalFileView } = settings;
verticalFileView.subscribe(($vertical) => {
if ($vertical) {
selection.update(($selection) => {
$selection.forEach((item) => {
if ($selection.hasAnyChildren(item, false)) {
$selection.toggle(item);
}
});
return $selection;
});
} else {
selection.update(($selection) => {
$selection.forEach((item) => {
if (!(item instanceof ListFileItem)) {
$selection.toggle(item);
$selection.set(new ListFileItem(item.getFileId()), true);
}
});
return $selection;
});
}
});
onMount(() => {
if (orientation === 'horizontal') {
selection.update(($selection) => {
$selection.forEach((item) => {
if (!(item instanceof ListFileItem)) {
$selection.toggle(item);
$selection.set(new ListFileItem(item.getFileId()), true);
}
});
return $selection;
});
}
});
</script>
<ScrollArea
class="shrink-0 {orientation === 'vertical' ? 'p-0 pr-3' : 'h-10 px-1'}"
{orientation}
scrollbarXClasses={orientation === 'vertical' ? '' : 'mt-1 h-2'}
scrollbarYClasses={orientation === 'vertical' ? '' : ''}
class="shrink-0 {orientation === 'vertical' ? 'p-0 pr-3' : 'h-10 px-1'}"
{orientation}
scrollbarXClasses={orientation === 'vertical' ? '' : 'hidden'}
scrollbarYClasses={orientation === 'vertical' ? '' : ''}
>
<div
class="flex {orientation === 'vertical'
? 'flex-col py-1 pl-1 min-h-screen'
: 'flex-row'} {$$props.class ?? ''}"
{...$$restProps}
>
<FileListNode bind:node={$fileObservers} item={new ListRootItem()} />
{#if orientation === 'vertical'}
<ContextMenu.Root>
<ContextMenu.Trigger class="grow" />
<ContextMenu.Content>
<ContextMenu.Item on:click={createFile}>
<Plus size="16" class="mr-1" />
{$_('menu.new_file')}
<Shortcut key="+" ctrl={true} />
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item on:click={selectAll} disabled={$fileObservers.size === 0}>
<FileStack size="16" class="mr-1" />
{$_('menu.select_all')}
<Shortcut key="A" ctrl={true} />
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item
disabled={$copied === undefined ||
$copied.length === 0 ||
!allowedPastes[$copied[0].level].includes(ListLevel.ROOT)}
on:click={pasteSelection}
>
<ClipboardPaste size="16" class="mr-1" />
{$_('menu.paste')}
<Shortcut key="V" ctrl={true} />
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
{/if}
</div>
<div
class="flex {orientation === 'vertical'
? 'flex-col py-1 pl-1 min-h-screen'
: 'flex-row'} {className ?? ''}"
{style}
>
<FileListNode node={$fileStateCollection} item={new ListRootItem()} />
{#if orientation === 'vertical'}
<ContextMenu.Root>
<ContextMenu.Trigger class="grow" />
<ContextMenu.Content>
<ContextMenu.Item onclick={createFile}>
<Plus size="16" />
{i18n._('menu.new_file')}
<Shortcut key="+" ctrl={true} />
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item
onclick={() => selection.selectAll()}
disabled={$fileStateCollection.size === 0}
>
<FileStack size="16" />
{i18n._('menu.select_all')}
<Shortcut key="A" ctrl={true} />
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item
disabled={$copied === undefined ||
$copied.length === 0 ||
!allowedPastes[$copied[0].level].includes(ListLevel.ROOT)}
onclick={pasteSelection}
>
<ClipboardPaste size="16" />
{i18n._('menu.paste')}
<Shortcut key="V" ctrl={true} />
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
{/if}
</div>
</ScrollArea>

View File

@@ -1,83 +1,89 @@
<script lang="ts">
import {
GPXFile,
Track,
TrackSegment,
Waypoint,
type AnyGPXTreeElement,
type GPXTreeElement
} from 'gpx';
import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
import { settings, type GPXFileWithStatistics } from '$lib/db';
import { get, type Readable } from 'svelte/store';
import FileListNodeContent from './FileListNodeContent.svelte';
import FileListNodeLabel from './FileListNodeLabel.svelte';
import { afterUpdate, getContext } from 'svelte';
import {
ListFileItem,
ListTrackSegmentItem,
ListWaypointItem,
ListWaypointsItem,
type ListItem,
type ListTrackItem
} from './FileList';
import { _ } from 'svelte-i18n';
import { selection } from './Selection';
import {
GPXFile,
Track,
TrackSegment,
Waypoint,
type AnyGPXTreeElement,
type GPXTreeElement,
} from 'gpx';
import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
import { type Readable } from 'svelte/store';
import FileListNodeContent from './FileListNodeContent.svelte';
import FileListNodeLabel from './FileListNodeLabel.svelte';
import { getContext } from 'svelte';
import {
ListFileItem,
ListTrackSegmentItem,
ListWaypointItem,
ListWaypointsItem,
type ListItem,
type ListTrackItem,
} from './file-list';
import { i18n } from '$lib/i18n.svelte';
import { settings } from '$lib/logic/settings';
import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree';
import { selection } from '$lib/logic/selection';
export let node:
| Map<string, Readable<GPXFileWithStatistics | undefined>>
| GPXTreeElement<AnyGPXTreeElement>
| Waypoint[]
| Waypoint;
export let item: ListItem;
let {
node,
item,
}: {
node:
| Map<string, Readable<GPXFileWithStatistics | undefined>>
| GPXTreeElement<AnyGPXTreeElement>
| Waypoint[]
| Waypoint;
item: ListItem;
} = $props();
let recursive = getContext<boolean>('recursive');
let recursive = getContext<boolean>('recursive');
let collapsible: CollapsibleTreeNode;
let collapsible: CollapsibleTreeNode | undefined = $state();
$: label =
node instanceof GPXFile && item instanceof ListFileItem
? node.metadata.name
: node instanceof Track
? node.name ?? `${$_('gpx.track')} ${(item as ListTrackItem).trackIndex + 1}`
: node instanceof TrackSegment
? `${$_('gpx.segment')} ${(item as ListTrackSegmentItem).segmentIndex + 1}`
: node instanceof Waypoint
? node.name ?? `${$_('gpx.waypoint')} ${(item as ListWaypointItem).waypointIndex + 1}`
: node instanceof GPXFile && item instanceof ListWaypointsItem
? $_('gpx.waypoints')
: '';
let label = $derived(
node instanceof GPXFile && item instanceof ListFileItem
? node.metadata.name
: node instanceof Track
? (node.name ?? `${i18n._('gpx.track')} ${(item as ListTrackItem).trackIndex + 1}`)
: node instanceof TrackSegment
? `${i18n._('gpx.segment')} ${(item as ListTrackSegmentItem).segmentIndex + 1}`
: node instanceof Waypoint
? (node.name ??
`${i18n._('gpx.waypoint')} ${(item as ListWaypointItem).waypointIndex + 1}`)
: node instanceof GPXFile && item instanceof ListWaypointsItem
? i18n._('gpx.waypoints')
: ''
);
const { verticalFileView } = settings;
const { treeFileView } = settings;
function openIfSelectedChild() {
if (collapsible && get(verticalFileView) && $selection.hasAnyChildren(item, false)) {
collapsible.openNode();
}
}
if ($selection) {
openIfSelectedChild();
}
afterUpdate(openIfSelectedChild);
$effect(() => {
if (collapsible && $treeFileView && $selection.hasAnyChildren(item, false)) {
collapsible.openNode();
}
});
</script>
{#if node instanceof Map}
<FileListNodeContent {node} {item} />
<FileListNodeContent {node} {item} />
{:else if node instanceof TrackSegment}
<FileListNodeLabel {node} {item} {label} />
<FileListNodeLabel {node} {item} {label} />
{:else if node instanceof Waypoint}
<FileListNodeLabel {node} {item} {label} />
<FileListNodeLabel {node} {item} {label} />
{:else if recursive}
<CollapsibleTreeNode id={item.getId()} bind:this={collapsible}>
<FileListNodeLabel {node} {item} {label} slot="trigger" />
<div slot="content" class="ml-2">
{#key node}
<FileListNodeContent {node} {item} />
{/key}
</div>
</CollapsibleTreeNode>
<CollapsibleTreeNode id={item.getId()} bind:this={collapsible}>
{#snippet trigger()}
<FileListNodeLabel {node} {item} {label} />
{/snippet}
{#snippet content()}
<div class="ml-4">
{#key node}
<FileListNodeContent {node} {item} />
{/key}
</div>
{/snippet}
</CollapsibleTreeNode>
{:else}
<FileListNodeLabel {node} {item} {label} />
<FileListNodeLabel {node} {item} {label} />
{/if}

View File

@@ -1,364 +1,139 @@
<script lang="ts" context="module">
let dragging: Writable<ListLevel | null> = writable(null);
let updating = false;
</script>
<script lang="ts">
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
import Sortable from 'sortablejs/Sortable';
import { getFileIds, settings, type GPXFileWithStatistics } from '$lib/db';
import { get, writable, type Readable, type Writable } from 'svelte/store';
import FileListNodeStore from './FileListNodeStore.svelte';
import FileListNode from './FileListNode.svelte';
import {
ListFileItem,
ListLevel,
ListRootItem,
ListWaypointsItem,
allowedMoves,
moveItems,
type ListItem
} from './FileList';
import { selection } from './Selection';
import { _ } from 'svelte-i18n';
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
import { getContext, onDestroy, onMount } from 'svelte';
import { type Readable } from 'svelte/store';
import FileListNodeStore from './FileListNodeStore.svelte';
import FileListNode from './FileListNode.svelte';
import FileListNodeContent from './FileListNodeContent.svelte';
import { ListFileItem, ListLevel, ListWaypointsItem, type ListItem } from './file-list';
import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree';
import { allowedMoves, dragging, SortableFileList } from './sortable-file-list';
export let node:
| Map<string, Readable<GPXFileWithStatistics | undefined>>
| GPXTreeElement<AnyGPXTreeElement>
| Waypoint;
export let item: ListItem;
export let waypointRoot: boolean = false;
let {
node,
item,
waypointRoot = false,
}: {
node:
| Map<string, Readable<GPXFileWithStatistics | undefined>>
| GPXTreeElement<AnyGPXTreeElement>
| Waypoint[]
| Waypoint;
item: ListItem;
waypointRoot?: boolean;
} = $props();
let container: HTMLElement;
let elements: { [id: string]: HTMLElement } = {};
let sortableLevel: ListLevel =
node instanceof Map
? ListLevel.FILE
: node instanceof GPXFile
? waypointRoot
? ListLevel.WAYPOINTS
: item instanceof ListWaypointsItem
? ListLevel.WAYPOINT
: ListLevel.TRACK
: node instanceof Track
? ListLevel.SEGMENT
: ListLevel.WAYPOINT;
let sortable: Sortable;
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
let container: HTMLElement;
let sortableLevel: ListLevel =
node instanceof Map
? ListLevel.FILE
: node instanceof GPXFile
? waypointRoot
? ListLevel.WAYPOINTS
: item instanceof ListWaypointsItem
? ListLevel.WAYPOINT
: ListLevel.TRACK
: node instanceof Track
? ListLevel.SEGMENT
: ListLevel.WAYPOINT;
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
let destroyed = false;
let lastUpdateStart = 0;
function updateToSelection(e) {
if (destroyed) {
return;
}
let canDrop = $derived($dragging !== null && allowedMoves[$dragging].includes(sortableLevel));
lastUpdateStart = Date.now();
setTimeout(() => {
if (Date.now() - lastUpdateStart >= 40) {
if (updating) {
return;
}
let sortable: SortableFileList;
updating = true;
// Sortable updates selection
let changed = getChangedIds();
if (changed.length > 0) {
selection.update(($selection) => {
$selection.clear();
Object.entries(elements).forEach(([id, element]) => {
$selection.set(
item.extend(getRealId(id)),
element.classList.contains('sortable-selected')
);
});
onMount(() => {
sortable = new SortableFileList(
container,
node,
item,
waypointRoot,
sortableLevel,
orientation
);
});
if (
e.originalEvent &&
!(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
$selection.clear();
$selection.set(item.extend(getRealId(changed[0])), true);
}
$effect(() => {
if (sortable && node) {
sortable.updateElements();
}
});
return $selection;
});
}
updating = false;
}
}, 50);
}
function updateFromSelection() {
if (destroyed || updating) {
return;
}
updating = true;
// Selection updates sortable
let changed = getChangedIds();
for (let id of changed) {
let element = elements[id];
if (element) {
if ($selection.has(item.extend(id))) {
Sortable.utils.select(element);
element.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
} else {
Sortable.utils.deselect(element);
}
}
}
updating = false;
}
$: if ($selection) {
updateFromSelection();
}
const { fileOrder } = settings;
function syncFileOrder() {
if (!sortable || sortableLevel !== ListLevel.FILE) {
return;
}
const currentOrder = sortable.toArray();
if (currentOrder.length !== $fileOrder.length) {
sortable.sort($fileOrder);
} else {
for (let i = 0; i < currentOrder.length; i++) {
if (currentOrder[i] !== $fileOrder[i]) {
sortable.sort($fileOrder);
break;
}
}
}
}
$: if ($fileOrder) {
syncFileOrder();
}
function createSortable() {
sortable = Sortable.create(container, {
group: {
name: sortableLevel,
pull: allowedMoves[sortableLevel],
put: true
},
direction: orientation,
forceAutoScrollFallback: true,
multiDrag: true,
multiDragKey: 'Meta',
avoidImplicitDeselect: true,
onSelect: updateToSelection,
onDeselect: updateToSelection,
onStart: () => {
dragging.set(sortableLevel);
},
onEnd: () => {
dragging.set(null);
},
onSort: (e) => {
if (sortableLevel === ListLevel.FILE) {
let newFileOrder = sortable.toArray();
if (newFileOrder.length !== get(fileOrder).length) {
fileOrder.set(newFileOrder);
} else {
for (let i = 0; i < newFileOrder.length; i++) {
if (newFileOrder[i] !== get(fileOrder)[i]) {
fileOrder.set(newFileOrder);
break;
}
}
}
}
let fromItem = Sortable.get(e.from)._item;
let toItem = Sortable.get(e.to)._item;
if (item === toItem && !(fromItem instanceof ListRootItem)) {
// Event is triggered on source and destination list, only handle it once
let fromItems = [];
let toItems = [];
if (Sortable.get(e.from)._waypointRoot) {
fromItems = [fromItem.extend('waypoints')];
} else {
let oldIndices: number[] =
e.oldIndicies.length > 0 ? e.oldIndicies.map((i) => i.index) : [e.oldIndex];
oldIndices = oldIndices.filter((i) => i >= 0);
oldIndices.sort((a, b) => a - b);
fromItems = oldIndices.map((i) => fromItem.extend(i));
}
if (Sortable.get(e.from)._waypointRoot && Sortable.get(e.to)._waypointRoot) {
toItems = [toItem.extend('waypoints')];
} else {
if (Sortable.get(e.to)._waypointRoot) {
toItem = toItem.extend('waypoints');
}
let newIndices: number[] =
e.newIndicies.length > 0 ? e.newIndicies.map((i) => i.index) : [e.newIndex];
newIndices = newIndices.filter((i) => i >= 0);
newIndices.sort((a, b) => a - b);
if (toItem instanceof ListRootItem) {
let newFileIds = getFileIds(newIndices.length);
toItems = newIndices.map((i, index) => {
$fileOrder.splice(i, 0, newFileIds[index]);
return item.extend(newFileIds[index]);
});
} else {
toItems = newIndices.map((i) => toItem.extend(i));
}
}
moveItems(fromItem, toItem, fromItems, toItems);
}
}
});
Object.defineProperty(sortable, '_item', {
value: item,
writable: true
});
Object.defineProperty(sortable, '_waypointRoot', {
value: waypointRoot,
writable: true
});
}
onMount(() => {
createSortable();
destroyed = false;
});
afterUpdate(() => {
elements = {};
container.childNodes.forEach((element) => {
if (element instanceof HTMLElement) {
let attr = element.getAttribute('data-id');
if (attr) {
if (node instanceof Map && !node.has(attr)) {
element.remove();
} else {
elements[attr] = element;
}
}
}
});
syncFileOrder();
updateFromSelection();
});
onDestroy(() => {
destroyed = true;
});
function getChangedIds() {
let changed: (string | number)[] = [];
Object.entries(elements).forEach(([id, element]) => {
let realId = getRealId(id);
let realItem = item.extend(realId);
let inSelection = get(selection).has(realItem);
let isSelected = element.classList.contains('sortable-selected');
if (inSelection !== isSelected) {
changed.push(realId);
}
});
return changed;
}
function getRealId(id: string | number) {
return sortableLevel === ListLevel.FILE || sortableLevel === ListLevel.WAYPOINTS
? id
: parseInt(id);
}
$: canDrop = $dragging !== null && allowedMoves[$dragging].includes(sortableLevel);
onDestroy(() => {
sortable.destroy();
});
</script>
<div
bind:this={container}
class="sortable {orientation} flex {orientation === 'vertical'
? 'flex-col'
: 'flex-row gap-1'} {canDrop ? 'min-h-5' : ''}"
bind:this={container}
class="sortable {orientation} flex {orientation === 'vertical'
? 'flex-col'
: 'flex-row gap-1'} {canDrop ? 'min-h-5' : ''}"
>
{#if node instanceof Map}
{#each node as [fileId, file] (fileId)}
<div data-id={fileId}>
<FileListNodeStore {file} />
</div>
{/each}
{:else if node instanceof GPXFile}
{#if item instanceof ListWaypointsItem}
{#each node.wpt as wpt, i (wpt)}
<div data-id={i} class="ml-1">
<FileListNode node={wpt} item={item.extend(i)} />
</div>
{/each}
{:else if waypointRoot}
{#if node.wpt.length > 0}
<div data-id="waypoints">
<FileListNode {node} item={item.extend('waypoints')} />
</div>
{/if}
{:else}
{#each node.children as child, i (child)}
<div data-id={i}>
<FileListNode node={child} item={item.extend(i)} />
</div>
{/each}
{/if}
{:else if node instanceof Track}
{#each node.children as child, i (child)}
<div data-id={i} class="ml-1">
<FileListNode node={child} item={item.extend(i)} />
</div>
{/each}
{/if}
{#if node instanceof Map}
{#each node as [fileId, file] (fileId)}
<div data-id={fileId}>
<FileListNodeStore {file} />
</div>
{/each}
{:else if node instanceof GPXFile}
{#if item instanceof ListWaypointsItem}
{#each node.wpt as wpt, i (wpt)}
<div data-id={i} class="ml-1">
<FileListNode node={wpt} item={item.extend(i)} />
</div>
{/each}
{:else if waypointRoot}
{#if node.wpt.length > 0}
<div data-id="waypoints">
<FileListNode {node} item={item.extend('waypoints')} />
</div>
{/if}
{:else}
{#each node.children as child, i (child)}
<div data-id={i}>
<FileListNode node={child} item={item.extend(i)} />
</div>
{/each}
{/if}
{:else if node instanceof Track}
{#each node.children as child, i (child)}
<div data-id={i} class="ml-1">
<FileListNode node={child} item={item.extend(i)} />
</div>
{/each}
{/if}
</div>
{#if node instanceof GPXFile && item instanceof ListFileItem}
{#if !waypointRoot}
<svelte:self {node} {item} waypointRoot={true} />
{/if}
{#if !waypointRoot}
<FileListNodeContent {node} {item} waypointRoot={true} />
{/if}
{/if}
<style lang="postcss">
.sortable > div {
@apply rounded-md;
@apply h-fit;
@apply leading-none;
}
@reference "../../../app.css";
.vertical :global(button) {
@apply hover:bg-muted;
}
.sortable > div {
@apply rounded-md;
@apply h-fit;
@apply leading-none;
}
.vertical :global(.sortable-selected button) {
@apply hover:bg-accent;
}
.vertical :global(button) {
@apply hover:bg-[var(--selection)];
}
.vertical :global(.sortable-selected) {
@apply bg-accent;
}
.vertical :global(.sortable-selected) {
@apply bg-[var(--selection)];
}
.horizontal :global(button) {
@apply bg-accent;
@apply hover:bg-muted;
}
.horizontal :global(button) {
@apply bg-[var(--selection)];
@apply hover:bg-background;
}
.horizontal :global(.sortable-selected button) {
@apply bg-background;
}
.horizontal :global(.sortable-selected button) {
@apply bg-background;
}
</style>

View File

@@ -1,321 +1,327 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as ContextMenu from '$lib/components/ui/context-menu';
import Shortcut from '$lib/components/Shortcut.svelte';
import { dbUtils, getFile } from '$lib/db';
import {
Copy,
Info,
MapPin,
PaintBucket,
Plus,
Trash2,
Waypoints,
Eye,
EyeOff,
ClipboardCopy,
ClipboardPaste,
Scissors,
FileStack,
FileX
} from 'lucide-svelte';
import {
ListFileItem,
ListLevel,
ListTrackItem,
ListWaypointItem,
allowedPastes,
type ListItem
} from './FileList';
import {
copied,
copySelection,
cut,
cutSelection,
pasteSelection,
selectAll,
selectItem,
selection
} from './Selection';
import { getContext } from 'svelte';
import { get } from 'svelte/store';
import { allHidden, editMetadata, editStyle, embedding, gpxLayers, map } from '$lib/stores';
import {
GPXTreeElement,
Track,
TrackSegment,
type AnyGPXTreeElement,
Waypoint,
GPXFile
} from 'gpx';
import { _ } from 'svelte-i18n';
import MetadataDialog from './MetadataDialog.svelte';
import StyleDialog from './StyleDialog.svelte';
import { Button } from '$lib/components/ui/button';
import * as ContextMenu from '$lib/components/ui/context-menu';
import Shortcut from '$lib/components/Shortcut.svelte';
import {
Copy,
Info,
MapPin,
PaintBucket,
Plus,
Trash2,
Waypoints,
Eye,
EyeOff,
ClipboardCopy,
ClipboardPaste,
Maximize,
Scissors,
FileStack,
} from '@lucide/svelte';
import {
ListFileItem,
ListLevel,
ListTrackItem,
ListWaypointItem,
type ListItem,
} from './file-list';
import { getContext } from 'svelte';
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
import { i18n } from '$lib/i18n.svelte';
import MetadataDialog from '$lib/components/file-list/metadata/MetadataDialog.svelte';
import { editMetadata } from '$lib/components/file-list/metadata/utils.svelte';
import StyleDialog from '$lib/components/file-list/style/StyleDialog.svelte';
import { editStyle } from '$lib/components/file-list/style/utils.svelte';
import { getSymbolKey, symbols } from '$lib/assets/symbols';
import { selection, copied, cut } from '$lib/logic/selection';
import { map } from '$lib/components/map/map';
import { fileActions, pasteSelection } from '$lib/logic/file-actions';
import { allHidden } from '$lib/logic/hidden';
import { boundsManager } from '$lib/logic/bounds';
import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
import { fileStateCollection } from '$lib/logic/file-state';
import { waypointPopup } from '$lib/components/map/gpx-layer/gpx-layer-popup';
import { allowedPastes } from './sortable-file-list';
export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
export let item: ListItem;
export let label: string | undefined;
let {
node,
item,
label,
}: {
node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
item: ListItem;
label: string | undefined;
} = $props();
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
let embedding = getContext<boolean>('embedding');
$: singleSelection = $selection.size === 1;
let singleSelection = $derived($selection.size === 1);
let nodeColors: string[] = [];
let nodeColors: string[] = $state([]);
$: if (node && $map) {
nodeColors = [];
$effect.pre(() => {
let colors: string[] = [];
if (node && $map) {
if (node instanceof GPXFile) {
let defaultColor = undefined;
if (node instanceof GPXFile) {
let style = node.getStyle();
let layer = gpxLayers.getLayer(item.getFileId());
if (layer) {
defaultColor = layer.layerColor;
}
let layer = gpxLayers.get(item.getFileId());
if (layer) {
style.color.push(layer.layerColor);
}
let style = node.getStyle(defaultColor);
style.color.forEach((c) => {
if (!colors.includes(c)) {
colors.push(c);
}
});
} else if (node instanceof Track) {
let style = node.getStyle();
if (style) {
if (style['gpx_style:color'] && !colors.includes(style['gpx_style:color'])) {
colors.push(style['gpx_style:color']);
}
}
if (colors.length === 0) {
let layer = gpxLayers.getLayer(item.getFileId());
if (layer) {
colors.push(layer.layerColor);
}
}
}
}
nodeColors = colors;
});
style.color.forEach((c) => {
if (!nodeColors.includes(c)) {
nodeColors.push(c);
}
});
} else if (node instanceof Track) {
let style = node.getStyle();
if (style) {
if (style.color && !nodeColors.includes(style.color)) {
nodeColors.push(style.color);
}
}
if (nodeColors.length === 0) {
let layer = gpxLayers.get(item.getFileId());
if (layer) {
nodeColors.push(layer.layerColor);
}
}
}
}
let symbolKey = $derived(node instanceof Waypoint ? getSymbolKey(node.sym) : undefined);
let openEditMetadata: boolean = false;
let openEditStyle: boolean = false;
let openEditMetadata: boolean = $derived(
editMetadata.current && singleSelection && $selection.has(item)
);
let openEditStyle: boolean = $derived(
editStyle.current &&
$selection.has(item) &&
$selection.getSelected().findIndex((i) => i.getFullId() === item.getFullId()) === 0
);
$: openEditMetadata = $editMetadata && singleSelection && $selection.has(item);
$: openEditStyle =
$editStyle &&
$selection.has(item) &&
$selection.getSelected().findIndex((i) => i.getFullId() === item.getFullId()) === 0;
$: hidden = item.level === ListLevel.WAYPOINTS ? node._data.hiddenWpt : node._data.hidden;
let hidden = $derived(
item.level === ListLevel.WAYPOINTS ? node._data.hiddenWpt : node._data.hidden
);
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<ContextMenu.Root
onOpenChange={(open) => {
if (open) {
if (!get(selection).has(item)) {
selectItem(item);
}
}
}}
onOpenChange={(open) => {
if (open) {
if (!$selection.has(item)) {
selection.selectItem(item);
}
}
}}
>
<ContextMenu.Trigger class="grow truncate">
<Button
variant="ghost"
class="relative w-full p-0 px-1 border-none overflow-hidden focus-visible:ring-0 focus-visible:ring-offset-0 {orientation ===
'vertical'
? 'h-fit'
: 'h-9 px-1.5 shadow-md'} pointer-events-auto"
>
{#if item instanceof ListFileItem || item instanceof ListTrackItem}
<MetadataDialog bind:open={openEditMetadata} {node} {item} />
<StyleDialog bind:open={openEditStyle} {item} />
{/if}
{#if item.level === ListLevel.FILE || item.level === ListLevel.TRACK}
<div
class="absolute {orientation === 'vertical'
? 'top-0 bottom-0 right-1 w-1'
: 'top-0 h-1 left-0 right-0'}"
style="background:linear-gradient(to {orientation === 'vertical'
? 'bottom'
: 'right'},{nodeColors
.map(
(c, i) =>
`${c} ${Math.floor((100 * i) / nodeColors.length)}% ${Math.floor((100 * (i + 1)) / nodeColors.length)}%`
)
.join(',')})"
/>
{/if}
<span
class="w-full text-left truncate py-1 flex flex-row items-center {hidden
? 'text-muted-foreground'
: ''} {$cut && $copied?.some((i) => i.getFullId() === item.getFullId())
? 'text-muted-foreground'
: ''}"
on:contextmenu={(e) => {
if ($embedding) {
e.preventDefault();
e.stopPropagation();
return;
}
if (e.ctrlKey) {
// Add to selection instead of opening context menu
e.preventDefault();
e.stopPropagation();
$selection.toggle(item);
$selection = $selection;
}
}}
on:mouseenter={() => {
if (item instanceof ListWaypointItem) {
let layer = gpxLayers.get(item.getFileId());
let file = getFile(item.getFileId());
if (layer && file) {
let waypoint = file.wpt[item.getWaypointIndex()];
if (waypoint) {
layer.showWaypointPopup(waypoint);
}
}
}
}}
on:mouseleave={() => {
if (item instanceof ListWaypointItem) {
let layer = gpxLayers.get(item.getFileId());
if (layer) {
layer.hideWaypointPopup();
}
}
}}
>
{#if item.level === ListLevel.SEGMENT}
<Waypoints size="16" class="mr-1 shrink-0" />
{:else if item.level === ListLevel.WAYPOINT}
<MapPin size="16" class="mr-1 shrink-0" />
{/if}
<span class="grow select-none truncate {orientation === 'vertical' ? 'last:mr-2' : ''}">
{label}
</span>
{#if hidden}
<EyeOff
size="12"
class="shrink-0 mt-1 ml-1 {orientation === 'vertical' ? 'mr-2' : ''} {item.level ===
ListLevel.SEGMENT || item.level === ListLevel.WAYPOINT
? 'mr-3'
: ''}"
/>
{/if}
</span>
</Button>
</ContextMenu.Trigger>
<ContextMenu.Content>
{#if item instanceof ListFileItem || item instanceof ListTrackItem}
<ContextMenu.Item disabled={!singleSelection} on:click={() => ($editMetadata = true)}>
<Info size="16" class="mr-1" />
{$_('menu.metadata.button')}
<Shortcut key="I" ctrl={true} />
</ContextMenu.Item>
<ContextMenu.Item on:click={() => ($editStyle = true)}>
<PaintBucket size="16" class="mr-1" />
{$_('menu.style.button')}
</ContextMenu.Item>
{/if}
<ContextMenu.Item
on:click={() => {
if ($allHidden) {
dbUtils.setHiddenToSelection(false);
} else {
dbUtils.setHiddenToSelection(true);
}
}}
>
{#if $allHidden}
<Eye size="16" class="mr-1" />
{$_('menu.unhide')}
{:else}
<EyeOff size="16" class="mr-1" />
{$_('menu.hide')}
{/if}
<Shortcut key="H" ctrl={true} />
</ContextMenu.Item>
<ContextMenu.Separator />
{#if orientation === 'vertical'}
{#if item instanceof ListFileItem}
<ContextMenu.Item
disabled={!singleSelection}
on:click={() =>
dbUtils.applyToFile(item.getFileId(), (file) =>
file.replaceTracks(file.trk.length, file.trk.length, [new Track()])
)}
>
<Plus size="16" class="mr-1" />
{$_('menu.new_track')}
</ContextMenu.Item>
<ContextMenu.Separator />
{:else if item instanceof ListTrackItem}
<ContextMenu.Item
disabled={!singleSelection}
on:click={() => {
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" />
{$_('menu.new_segment')}
</ContextMenu.Item>
<ContextMenu.Separator />
{/if}
{/if}
{#if item.level !== ListLevel.WAYPOINTS}
<ContextMenu.Item on:click={selectAll}>
<FileStack size="16" class="mr-1" />
{$_('menu.select_all')}
<Shortcut key="A" ctrl={true} />
</ContextMenu.Item>
<ContextMenu.Separator />
{/if}
{#if orientation === 'vertical'}
<ContextMenu.Item on:click={dbUtils.duplicateSelection}>
<Copy size="16" class="mr-1" />
{$_('menu.duplicate')}
<Shortcut key="D" ctrl={true} /></ContextMenu.Item
>
{#if orientation === 'vertical'}
<ContextMenu.Item on:click={copySelection}>
<ClipboardCopy size="16" class="mr-1" />
{$_('menu.copy')}
<Shortcut key="C" ctrl={true} />
</ContextMenu.Item>
<ContextMenu.Item on:click={cutSelection}>
<Scissors size="16" class="mr-1" />
{$_('menu.cut')}
<Shortcut key="X" ctrl={true} />
</ContextMenu.Item>
<ContextMenu.Item
disabled={$copied === undefined ||
$copied.length === 0 ||
!allowedPastes[$copied[0].level].includes(item.level)}
on:click={pasteSelection}
>
<ClipboardPaste size="16" class="mr-1" />
{$_('menu.paste')}
<Shortcut key="V" ctrl={true} />
</ContextMenu.Item>
{/if}
<ContextMenu.Separator />
{/if}
<ContextMenu.Item on:click={dbUtils.deleteSelection}>
{#if item instanceof ListFileItem}
<FileX size="16" class="mr-1" />
{$_('menu.close')}
{:else}
<Trash2 size="16" class="mr-1" />
{$_('menu.delete')}
{/if}
<Shortcut key="⌫" ctrl={true} />
</ContextMenu.Item>
</ContextMenu.Content>
<ContextMenu.Trigger class="grow truncate">
<Button
variant="ghost"
class="relative w-full p-0 overflow-hidden focus-visible:ring-0 focus-visible:ring-offset-0 {orientation ===
'vertical'
? 'h-fit'
: 'h-9 px-1.5 shadow-md'} pointer-events-auto"
>
{#if item instanceof ListFileItem || item instanceof ListTrackItem}
<MetadataDialog bind:open={openEditMetadata} {node} {item} />
<StyleDialog bind:open={openEditStyle} {item} />
{/if}
{#if item.level === ListLevel.FILE || item.level === ListLevel.TRACK}
<div
class="absolute {orientation === 'vertical'
? 'top-0 bottom-0 right-1 w-1'
: 'top-0 h-1 left-0 right-0'}"
style="background:linear-gradient(to {orientation === 'vertical'
? 'bottom'
: 'right'},{nodeColors
.map(
(c, i) =>
`${c} ${Math.floor((100 * i) / nodeColors.length)}% ${Math.floor((100 * (i + 1)) / nodeColors.length)}%`
)
.join(',')})"
></div>
{/if}
<span
class="w-full text-left truncate py-1 flex flex-row items-center {hidden
? 'text-muted-foreground'
: ''} {$cut && $copied?.some((i) => i.getFullId() === item.getFullId())
? 'text-muted-foreground'
: ''}"
oncontextmenu={(e) => {
if (embedding) {
e.preventDefault();
e.stopPropagation();
return;
}
if (e.ctrlKey) {
// Add to selection instead of opening context menu
e.preventDefault();
e.stopPropagation();
$selection.toggle(item);
$selection = $selection;
}
}}
onmouseenter={() => {
if (item instanceof ListWaypointItem) {
let layer = gpxLayers.getLayer(item.getFileId());
let file = fileStateCollection.getFile(item.getFileId());
if (layer && file) {
let waypoint = file.wpt[item.getWaypointIndex()];
if (waypoint) {
waypointPopup?.setItem({
item: waypoint,
fileId: item.getFileId(),
});
}
}
}
}}
onmouseleave={() => {
if (item instanceof ListWaypointItem) {
let layer = gpxLayers.getLayer(item.getFileId());
if (layer) {
waypointPopup?.setItem(null);
}
}
}}
>
{#if item.level === ListLevel.SEGMENT}
<Waypoints size="16" class="mx-1 shrink-0" />
{:else if item.level === ListLevel.WAYPOINT}
{#if symbolKey && symbols[symbolKey].icon}
{@const SymbolIcon = symbols[symbolKey].icon}
<SymbolIcon size="16" class="mx-1 shrink-0" />
{:else}
<MapPin size="16" class="mx-1 shrink-0" />
{/if}
{/if}
<span
class="grow select-none truncate {orientation === 'vertical'
? 'last:mr-2'
: ''}"
>
{label}
</span>
{#if hidden}
<EyeOff
size="10"
class="shrink-0 size-3.5 ml-1 {orientation === 'vertical'
? 'mr-3'
: 'mt-0.5'}"
/>
{/if}
</span>
</Button>
</ContextMenu.Trigger>
<ContextMenu.Content>
{#if item instanceof ListFileItem || item instanceof ListTrackItem}
<ContextMenu.Item
disabled={!singleSelection}
onclick={() => (editMetadata.current = true)}
>
<Info size="16" />
{i18n._('menu.metadata.button')}
<Shortcut key="I" ctrl={true} />
</ContextMenu.Item>
<ContextMenu.Item onclick={() => (editStyle.current = true)}>
<PaintBucket size="16" />
{i18n._('menu.style.button')}
</ContextMenu.Item>
{/if}
<ContextMenu.Item
onclick={() => {
if ($allHidden) {
fileActions.setHiddenToSelection(false);
} else {
fileActions.setHiddenToSelection(true);
}
}}
>
{#if $allHidden}
<Eye size="16" />
{i18n._('menu.unhide')}
{:else}
<EyeOff size="16" />
{i18n._('menu.hide')}
{/if}
<Shortcut key="H" ctrl={true} />
</ContextMenu.Item>
<ContextMenu.Separator />
{#if orientation === 'vertical'}
{#if item instanceof ListFileItem}
<ContextMenu.Item
disabled={!singleSelection}
onclick={() => fileActions.addNewTrack(item.getFileId())}
>
<Plus size="16" />
{i18n._('menu.new_track')}
</ContextMenu.Item>
<ContextMenu.Separator />
{:else if item instanceof ListTrackItem}
<ContextMenu.Item
disabled={!singleSelection}
onclick={() =>
fileActions.addNewSegment(item.getFileId(), item.getTrackIndex())}
>
<Plus size="16" />
{i18n._('menu.new_segment')}
</ContextMenu.Item>
<ContextMenu.Separator />
{/if}
{/if}
{#if item.level !== ListLevel.WAYPOINTS}
<ContextMenu.Item onclick={() => selection.selectAll()}>
<FileStack size="16" />
{i18n._('menu.select_all')}
<Shortcut key="A" ctrl={true} />
</ContextMenu.Item>
{/if}
<ContextMenu.Item onclick={() => boundsManager.centerMapOnSelection()}>
<Maximize size="16" />
{i18n._('menu.center')}
<Shortcut key="" ctrl={true} />
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item onclick={fileActions.duplicateSelection}>
<Copy size="16" />
{i18n._('menu.duplicate')}
<Shortcut key="D" ctrl={true} />
</ContextMenu.Item>
{#if orientation === 'vertical'}
<ContextMenu.Item onclick={() => selection.copySelection()}>
<ClipboardCopy size="16" />
{i18n._('menu.copy')}
<Shortcut key="C" ctrl={true} />
</ContextMenu.Item>
<ContextMenu.Item onclick={() => selection.cutSelection()}>
<Scissors size="16" />
{i18n._('menu.cut')}
<Shortcut key="X" ctrl={true} />
</ContextMenu.Item>
<ContextMenu.Item
disabled={$copied === undefined ||
$copied.length === 0 ||
!allowedPastes[$copied[0].level].includes(item.level)}
onclick={pasteSelection}
>
<ClipboardPaste size="16" />
{i18n._('menu.paste')}
<Shortcut key="V" ctrl={true} />
</ContextMenu.Item>
{/if}
<ContextMenu.Separator />
<ContextMenu.Item onclick={fileActions.deleteSelection}>
<Trash2 size="16" />
{i18n._('menu.delete')}
<Shortcut key="⌫" ctrl={true} />
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>

View File

@@ -1,23 +1,27 @@
<script lang="ts">
import CollapsibleTree from '$lib/components/collapsible-tree/CollapsibleTree.svelte';
import FileListNode from '$lib/components/file-list/FileListNode.svelte';
import CollapsibleTree from '$lib/components/collapsible-tree/CollapsibleTree.svelte';
import FileListNode from '$lib/components/file-list/FileListNode.svelte';
import type { GPXFileWithStatistics } from '$lib/db';
import { getContext } from 'svelte';
import type { Readable } from 'svelte/store';
import { ListFileItem } from './FileList';
import { getContext } from 'svelte';
import type { Readable } from 'svelte/store';
import { ListFileItem } from './file-list';
import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree';
export let file: Readable<GPXFileWithStatistics | undefined>;
let {
file,
}: {
file: Readable<GPXFileWithStatistics | undefined>;
} = $props();
let recursive = getContext<boolean>('recursive');
let recursive = getContext<boolean>('recursive');
</script>
{#if $file}
{#if recursive}
<CollapsibleTree side="left" defaultState="closed" slotInsideTrigger={false}>
<FileListNode node={$file.file} item={new ListFileItem($file.file._data.id)} />
</CollapsibleTree>
{:else}
<FileListNode node={$file.file} item={new ListFileItem($file.file._data.id)} />
{/if}
{#if recursive}
<CollapsibleTree side="left" defaultState="closed" slotInsideTrigger={false}>
<FileListNode node={$file.file} item={new ListFileItem($file.file._data.id)} />
</CollapsibleTree>
{:else}
<FileListNode node={$file.file} item={new ListFileItem($file.file._data.id)} />
{/if}
{/if}

View File

@@ -1,62 +0,0 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Textarea } from '$lib/components/ui/textarea';
import { Label } from '$lib/components/ui/label/index.js';
import * as Popover from '$lib/components/ui/popover';
import { dbUtils } from '$lib/db';
import { Save } from 'lucide-svelte';
import { ListFileItem, ListTrackItem, type ListItem } from './FileList';
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
import { _ } from 'svelte-i18n';
import { editMetadata } from '$lib/stores';
export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
export let item: ListItem;
export let open = false;
let name: string =
node instanceof GPXFile
? node.metadata.name ?? ''
: node instanceof Track
? node.name ?? ''
: '';
let description: string =
node instanceof GPXFile
? node.metadata.desc ?? ''
: node instanceof Track
? node.desc ?? ''
: '';
$: if (!open) {
$editMetadata = false;
}
</script>
<Popover.Root bind:open>
<Popover.Trigger />
<Popover.Content side="top" sideOffset={22} alignOffset={30} class="flex flex-col gap-3">
<Label for="name">{$_('menu.metadata.name')}</Label>
<Input bind:value={name} id="name" class="font-semibold h-8" />
<Label for="description">{$_('menu.metadata.description')}</Label>
<Textarea bind:value={description} id="description" />
<Button
variant="outline"
on:click={() => {
dbUtils.applyToFile(item.getFileId(), (file) => {
if (item instanceof ListFileItem && node instanceof GPXFile) {
file.metadata.name = name;
file.metadata.desc = description;
} else if (item instanceof ListTrackItem && node instanceof Track) {
file.trk[item.getTrackIndex()].name = name;
file.trk[item.getTrackIndex()].desc = description;
}
});
open = false;
}}
>
<Save size="16" class="mr-1" />
{$_('menu.metadata.save')}
</Button>
</Popover.Content>
</Popover.Root>

View File

@@ -1,315 +0,0 @@
import { get, writable } from "svelte/store";
import { ListFileItem, ListItem, ListRootItem, ListTrackItem, ListTrackSegmentItem, ListWaypointItem, ListLevel, sortItems, ListWaypointsItem, moveItems } from "./FileList";
import { fileObservers, getFile, getFileIds, settings } from "$lib/db";
export class SelectionTreeType {
item: ListItem;
selected: boolean;
children: {
[key: string | number]: SelectionTreeType
};
size: number = 0;
constructor(item: ListItem) {
this.item = item;
this.selected = false;
this.children = {};
}
clear() {
this.selected = false;
for (let key in this.children) {
this.children[key].clear();
}
this.size = 0;
}
_setOrToggle(item: ListItem, value?: boolean) {
if (item.level === this.item.level) {
let newSelected = value === undefined ? !this.selected : value;
if (this.selected !== newSelected) {
this.selected = newSelected;
this.size += this.selected ? 1 : -1;
}
} else {
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (!this.children.hasOwnProperty(id)) {
this.children[id] = new SelectionTreeType(this.item.extend(id));
}
this.size -= this.children[id].size;
this.children[id]._setOrToggle(item, value);
this.size += this.children[id].size;
}
}
}
set(item: ListItem, value: boolean) {
this._setOrToggle(item, value);
}
toggle(item: ListItem) {
this._setOrToggle(item);
}
has(item: ListItem): boolean {
if (item.level === this.item.level) {
return this.selected;
} else {
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].has(item);
}
}
}
return false;
}
hasAnyParent(item: ListItem, self: boolean = true): boolean {
if (this.selected && this.item.level <= item.level && (self || this.item.level < item.level)) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].hasAnyParent(item, self);
}
}
return false;
}
hasAnyChildren(item: ListItem, self: boolean = true, ignoreIds?: (string | number)[]): boolean {
if (this.selected && this.item.level >= item.level && (self || this.item.level > item.level)) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (ignoreIds === undefined || ignoreIds.indexOf(id) === -1) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].hasAnyChildren(item, self, ignoreIds);
}
}
} else {
for (let key in this.children) {
if (ignoreIds === undefined || ignoreIds.indexOf(key) === -1) {
if (this.children[key].hasAnyChildren(item, self, ignoreIds)) {
return true;
}
}
}
}
return false;
}
getSelected(selection: ListItem[] = []): ListItem[] {
if (this.selected) {
selection.push(this.item);
}
for (let key in this.children) {
this.children[key].getSelected(selection);
}
return selection;
}
forEach(callback: (item: ListItem) => void) {
if (this.selected) {
callback(this.item);
}
for (let key in this.children) {
this.children[key].forEach(callback);
}
}
getChild(id: string | number): SelectionTreeType | undefined {
return this.children[id];
}
deleteChild(id: string | number) {
if (this.children.hasOwnProperty(id)) {
this.size -= this.children[id].size;
delete this.children[id];
}
}
};
export const selection = writable<SelectionTreeType>(new SelectionTreeType(new ListRootItem()));
export function selectItem(item: ListItem) {
selection.update(($selection) => {
$selection.clear();
$selection.set(item, true);
return $selection;
});
}
export function selectFile(fileId: string) {
selectItem(new ListFileItem(fileId));
}
export function addSelectItem(item: ListItem) {
selection.update(($selection) => {
$selection.toggle(item);
return $selection;
});
}
export function addSelectFile(fileId: string) {
addSelectItem(new ListFileItem(fileId));
}
export function selectAll() {
selection.update(($selection) => {
let item: ListItem = new ListRootItem();
$selection.forEach((i) => {
item = i;
});
if (item instanceof ListRootItem || item instanceof ListFileItem) {
$selection.clear();
get(fileObservers).forEach((_file, fileId) => {
$selection.set(new ListFileItem(fileId), true);
});
} else if (item instanceof ListTrackItem) {
let file = getFile(item.getFileId());
if (file) {
file.trk.forEach((_track, trackId) => {
$selection.set(new ListTrackItem(item.getFileId(), trackId), true);
});
}
} else if (item instanceof ListTrackSegmentItem) {
let file = getFile(item.getFileId());
if (file) {
file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => {
$selection.set(new ListTrackSegmentItem(item.getFileId(), item.getTrackIndex(), segmentId), true);
});
}
} else if (item instanceof ListWaypointItem) {
let file = getFile(item.getFileId());
if (file) {
file.wpt.forEach((_waypoint, waypointId) => {
$selection.set(new ListWaypointItem(item.getFileId(), waypointId), true);
});
}
}
return $selection;
});
}
export function getOrderedSelection(reverse: boolean = false): ListItem[] {
let selected: ListItem[] = [];
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
selected.push(...items);
}, reverse);
return selected;
}
export function applyToOrderedItemsFromFile(selectedItems: ListItem[], callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void, reverse: boolean = true) {
get(settings.fileOrder).forEach((fileId) => {
let level: ListLevel | undefined = undefined;
let items: ListItem[] = [];
selectedItems.forEach((item) => {
if (item.getFileId() === fileId) {
level = item.level;
if (item instanceof ListFileItem || item instanceof ListTrackItem || item instanceof ListTrackSegmentItem || item instanceof ListWaypointsItem || item instanceof ListWaypointItem) {
items.push(item);
}
}
});
if (items.length > 0) {
sortItems(items, reverse);
callback(fileId, level, items);
}
});
}
export function applyToOrderedSelectedItemsFromFile(callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void, reverse: boolean = true) {
applyToOrderedItemsFromFile(get(selection).getSelected(), callback, reverse);
}
export const copied = writable<ListItem[] | undefined>(undefined);
export const cut = writable(false);
export function copySelection(): boolean {
let selected = get(selection).getSelected();
if (selected.length > 0) {
copied.set(selected);
cut.set(false);
return true;
}
return false;
}
export function cutSelection() {
if (copySelection()) {
cut.set(true);
}
}
function resetCopied() {
copied.set(undefined);
cut.set(false);
}
export function pasteSelection() {
let fromItems = get(copied);
if (fromItems === undefined || fromItems.length === 0) {
return;
}
let selected = get(selection).getSelected();
if (selected.length === 0) {
selected = [new ListRootItem()];
}
let fromParent = fromItems[0].getParent();
let toParent = selected[selected.length - 1];
let startIndex: number | undefined = undefined;
if (fromItems[0].level === toParent.level) {
if (toParent instanceof ListTrackItem || toParent instanceof ListTrackSegmentItem || toParent instanceof ListWaypointItem) {
startIndex = toParent.getId() + 1;
}
toParent = toParent.getParent();
}
let toItems: ListItem[] = [];
if (toParent.level === ListLevel.ROOT) {
let fileIds = getFileIds(fromItems.length);
fileIds.forEach((fileId) => {
toItems.push(new ListFileItem(fileId));
});
} else {
let toFile = getFile(toParent.getFileId());
if (toFile) {
fromItems.forEach((item, index) => {
if (toParent instanceof ListFileItem) {
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
toItems.push(new ListTrackItem(toParent.getFileId(), (startIndex ?? toFile.trk.length) + index));
} else if (item instanceof ListWaypointsItem) {
toItems.push(new ListWaypointsItem(toParent.getFileId()));
} else if (item instanceof ListWaypointItem) {
toItems.push(new ListWaypointItem(toParent.getFileId(), (startIndex ?? toFile.wpt.length) + index));
}
} else if (toParent instanceof ListTrackItem) {
if (item instanceof ListTrackSegmentItem) {
let toTrackIndex = toParent.getTrackIndex();
toItems.push(new ListTrackSegmentItem(toParent.getFileId(), toTrackIndex, (startIndex ?? toFile.trk[toTrackIndex].trkseg.length) + index));
}
} else if (toParent instanceof ListWaypointsItem) {
if (item instanceof ListWaypointItem) {
toItems.push(new ListWaypointItem(toParent.getFileId(), (startIndex ?? toFile.wpt.length) + index));
}
}
});
}
}
if (fromItems.length === toItems.length) {
moveItems(fromParent, toParent, fromItems, toItems, get(cut));
resetCopied();
}
}

View File

@@ -1,167 +0,0 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label/index.js';
import { Slider } from '$lib/components/ui/slider';
import * as Popover from '$lib/components/ui/popover';
import { dbUtils, getFile, settings } from '$lib/db';
import { Save } from 'lucide-svelte';
import { ListFileItem, ListTrackItem, type ListItem } from './FileList';
import { selection } from './Selection';
import { editStyle, gpxLayers } from '$lib/stores';
import { _ } from 'svelte-i18n';
export let item: ListItem;
export let open = false;
const { defaultOpacity, defaultWeight } = settings;
let colors: string[] = [];
let color: string | undefined = undefined;
let opacity: number[] = [];
let weight: number[] = [];
let colorChanged = false;
let opacityChanged = false;
let weightChanged = false;
function setStyleInputs() {
colors = [];
opacity = [];
weight = [];
$selection.forEach((item) => {
if (item instanceof ListFileItem) {
let file = getFile(item.getFileId());
let layer = gpxLayers.get(item.getFileId());
if (file && layer) {
let style = file.getStyle();
style.color.push(layer.layerColor);
style.color.forEach((c) => {
if (!colors.includes(c)) {
colors.push(c);
}
});
style.opacity.forEach((o) => {
if (!opacity.includes(o)) {
opacity.push(o);
}
});
style.weight.forEach((w) => {
if (!weight.includes(w)) {
weight.push(w);
}
});
}
} else if (item instanceof ListTrackItem) {
let file = getFile(item.getFileId());
let layer = gpxLayers.get(item.getFileId());
if (file && layer) {
let track = file.trk[item.getTrackIndex()];
let style = track.getStyle();
if (style) {
if (style.color && !colors.includes(style.color)) {
colors.push(style.color);
}
if (style.opacity && !opacity.includes(style.opacity)) {
opacity.push(style.opacity);
}
if (style.weight && !weight.includes(style.weight)) {
weight.push(style.weight);
}
}
if (!colors.includes(layer.layerColor)) {
colors.push(layer.layerColor);
}
}
}
});
color = colors[0];
opacity = [opacity[0] ?? $defaultOpacity];
weight = [weight[0] ?? $defaultWeight];
colorChanged = false;
opacityChanged = false;
weightChanged = false;
}
$: if ($selection && open) {
setStyleInputs();
}
$: if (!open) {
$editStyle = false;
}
</script>
<Popover.Root bind:open>
<Popover.Trigger />
<Popover.Content side="top" sideOffset={22} alignOffset={30} class="flex flex-col gap-3">
<Label class="flex flex-row gap-2 items-center justify-between">
{$_('menu.style.color')}
<Input
bind:value={color}
type="color"
class="p-0 h-6 w-40"
on:change={() => (colorChanged = true)}
/>
</Label>
<Label class="flex flex-row gap-2 items-center justify-between">
{$_('menu.style.opacity')}
<div class="w-40 p-2">
<Slider
bind:value={opacity}
min={0.3}
max={1}
step={0.1}
onValueChange={() => (opacityChanged = true)}
/>
</div>
</Label>
<Label class="flex flex-row gap-2 items-center justify-between">
{$_('menu.style.width')}
<div class="w-40 p-2">
<Slider
bind:value={weight}
id="weight"
min={1}
max={10}
step={1}
onValueChange={() => (weightChanged = true)}
/>
</div>
</Label>
<Button
variant="outline"
disabled={!colorChanged && !opacityChanged && !weightChanged}
on:click={() => {
let style = {};
if (colorChanged) {
style.color = color;
}
if (opacityChanged) {
style.opacity = opacity[0];
}
if (weightChanged) {
style.weight = weight[0];
}
dbUtils.setStyleToSelection(style);
if (item instanceof ListFileItem && $selection.size === gpxLayers.size) {
if (style.opacity) {
$defaultOpacity = style.opacity;
}
if (style.weight) {
$defaultWeight = style.weight;
}
}
open = false;
}}
>
<Save size="16" class="mr-1" />
{$_('menu.metadata.save')}
</Button>
</Popover.Content>
</Popover.Root>

View File

@@ -1,37 +1,14 @@
import { dbUtils, getFile } from "$lib/db";
import { freeze } from "immer";
import { GPXFile, Track, TrackSegment, Waypoint } from "gpx";
import { selection } from "./Selection";
import { newGPXFile } from "$lib/stores";
export enum ListLevel {
ROOT,
FILE,
TRACK,
SEGMENT,
WAYPOINTS,
WAYPOINT
WAYPOINT,
}
export const allowedMoves: Record<ListLevel, ListLevel[]> = {
[ListLevel.ROOT]: [],
[ListLevel.FILE]: [ListLevel.FILE],
[ListLevel.TRACK]: [ListLevel.FILE, ListLevel.TRACK],
[ListLevel.SEGMENT]: [ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
[ListLevel.WAYPOINTS]: [ListLevel.WAYPOINTS],
[ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT]
};
export const allowedPastes: Record<ListLevel, ListLevel[]> = {
[ListLevel.ROOT]: [],
[ListLevel.FILE]: [ListLevel.ROOT, ListLevel.FILE],
[ListLevel.TRACK]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK],
[ListLevel.SEGMENT]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
[ListLevel.WAYPOINTS]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
[ListLevel.WAYPOINT]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT]
};
export abstract class ListItem {
[x: string]: any;
level: ListLevel;
constructor(level: ListLevel) {
@@ -321,120 +298,3 @@ export function sortItems(items: ListItem[], reverse: boolean = false) {
items.reverse();
}
}
export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: ListItem[], toItems: ListItem[], remove: boolean = true) {
if (fromItems.length === 0) {
return;
}
sortItems(fromItems, false);
sortItems(toItems, false);
let context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[] = [];
fromItems.forEach((item) => {
let file = getFile(item.getFileId());
if (file) {
if (item instanceof ListFileItem) {
context.push(file.clone());
} else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) {
context.push(file.trk[item.getTrackIndex()].clone());
} else if (item instanceof ListTrackSegmentItem && item.getTrackIndex() < file.trk.length && item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length) {
context.push(file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()].clone());
} else if (item instanceof ListWaypointsItem) {
context.push(file.wpt.map((wpt) => wpt.clone()));
} else if (item instanceof ListWaypointItem && item.getWaypointIndex() < file.wpt.length) {
context.push(file.wpt[item.getWaypointIndex()].clone());
}
}
});
if (remove && !(fromParent instanceof ListRootItem)) {
sortItems(fromItems, true);
}
let files = [fromParent.getFileId(), toParent.getFileId()];
let callbacks = [
(file, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
fromItems.forEach((item) => {
if (item instanceof ListTrackItem) {
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []);
} else if (item instanceof ListTrackSegmentItem) {
file.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex(), []);
} else if (item instanceof ListWaypointsItem) {
file.replaceWaypoints(0, file.wpt.length - 1, []);
} else if (item instanceof ListWaypointItem) {
file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex(), []);
}
});
},
(file, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
toItems.forEach((item, i) => {
if (item instanceof ListTrackItem) {
if (context[i] instanceof Track) {
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [context[i]]);
} else if (context[i] instanceof TrackSegment) {
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [new Track({
trkseg: [context[i]]
})]);
}
} else if (item instanceof ListTrackSegmentItem && context[i] instanceof TrackSegment) {
file.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex() - 1, [context[i]]);
} else if (item instanceof ListWaypointsItem) {
if (Array.isArray(context[i]) && context[i].length > 0 && context[i][0] instanceof Waypoint) {
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, context[i]);
} else if (context[i] instanceof Waypoint) {
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, [context[i]]);
}
} else if (item instanceof ListWaypointItem && context[i] instanceof Waypoint) {
file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex() - 1, [context[i]]);
}
});
}
];
if (fromParent instanceof ListRootItem) {
files = [];
callbacks = [];
} else if (!remove) {
files.splice(0, 1);
callbacks.splice(0, 1);
}
dbUtils.applyEachToFilesAndGlobal(files, callbacks, (files, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
toItems.forEach((item, i) => {
if (item instanceof ListFileItem) {
if (context[i] instanceof GPXFile) {
let newFile = context[i];
if (remove) {
files.delete(newFile._data.id);
}
newFile._data.id = item.getFileId();
files.set(item.getFileId(), freeze(newFile));
} else if (context[i] instanceof Track) {
let newFile = newGPXFile();
newFile._data.id = item.getFileId();
if (context[i].name) {
newFile.metadata.name = context[i].name;
}
newFile.replaceTracks(0, 0, [context[i]]);
files.set(item.getFileId(), freeze(newFile));
} else if (context[i] instanceof TrackSegment) {
let newFile = newGPXFile();
newFile._data.id = item.getFileId();
newFile.replaceTracks(0, 0, [new Track({
trkseg: [context[i]]
})]);
files.set(item.getFileId(), freeze(newFile));
}
}
});
}, context);
selection.update(($selection) => {
$selection.clear();
toItems.forEach((item) => {
$selection.set(item, true);
});
return $selection;
});
}

View File

@@ -0,0 +1,75 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Textarea } from '$lib/components/ui/textarea';
import { Label } from '$lib/components/ui/label/index.js';
import * as Popover from '$lib/components/ui/popover';
import { Save } from '@lucide/svelte';
import { ListFileItem, ListTrackItem, type ListItem } from '../file-list';
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
import { i18n } from '$lib/i18n.svelte';
import { editMetadata } from '$lib/components/file-list/metadata/utils.svelte';
import { fileActionManager } from '$lib/logic/file-action-manager';
let {
node,
item,
open = $bindable(),
}: {
node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
item: ListItem;
open: boolean;
} = $props();
let name: string = $derived(
node instanceof GPXFile
? (node.metadata.name ?? '')
: node instanceof Track
? (node.name ?? '')
: ''
);
let description: string = $derived(
node instanceof GPXFile
? (node.metadata.desc ?? '')
: node instanceof Track
? (node.desc ?? '')
: ''
);
$effect(() => {
if (!open) {
editMetadata.current = false;
}
});
</script>
<Popover.Root bind:open>
<Popover.Trigger class="-mx-1" />
<Popover.Content side="top" sideOffset={22} alignOffset={30} class="flex flex-col gap-3">
<Label for="name">{i18n._('menu.metadata.name')}</Label>
<Input bind:value={name} id="name" class="font-semibold h-8" />
<Label for="description">{i18n._('menu.metadata.description')}</Label>
<Textarea bind:value={description} id="description" />
<Button
variant="outline"
onclick={() => {
fileActionManager.applyToFile(item.getFileId(), (file) => {
if (item instanceof ListFileItem && node instanceof GPXFile) {
file.metadata.name = name;
file.metadata.desc = description;
if (file.trk.length === 1) {
file.trk[0].name = name;
}
} else if (item instanceof ListTrackItem && node instanceof Track) {
file.trk[item.getTrackIndex()].name = name;
file.trk[item.getTrackIndex()].desc = description;
}
});
open = false;
}}
>
<Save size="16" />
{i18n._('menu.metadata.save')}
</Button>
</Popover.Content>
</Popover.Root>

View File

@@ -0,0 +1,3 @@
export const editMetadata = $state({
current: false,
});

View File

@@ -0,0 +1,284 @@
import { isMac } from '$lib/utils';
import Sortable, { type Direction } from 'sortablejs/Sortable';
import { ListItem, ListLevel, ListRootItem } from './file-list';
import { selection } from '$lib/logic/selection';
import { getFileIds, moveItems } from '$lib/logic/file-actions';
import { get, writable, type Readable } from 'svelte/store';
import { settings } from '$lib/logic/settings';
import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree';
import type { AnyGPXTreeElement, GPXTreeElement, Waypoint } from 'gpx';
import { tick } from 'svelte';
const { fileOrder } = settings;
export const allowedMoves: Record<ListLevel, ListLevel[]> = {
[ListLevel.ROOT]: [],
[ListLevel.FILE]: [ListLevel.FILE],
[ListLevel.TRACK]: [ListLevel.FILE, ListLevel.TRACK],
[ListLevel.SEGMENT]: [ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
[ListLevel.WAYPOINTS]: [ListLevel.WAYPOINTS],
[ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
};
export const allowedPastes: Record<ListLevel, ListLevel[]> = {
[ListLevel.ROOT]: [],
[ListLevel.FILE]: [ListLevel.ROOT, ListLevel.FILE],
[ListLevel.TRACK]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK],
[ListLevel.SEGMENT]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
[ListLevel.WAYPOINTS]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
[ListLevel.WAYPOINT]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
};
export const dragging = writable<ListLevel | null>(null);
export class SortableFileList {
private _node:
| Map<string, Readable<GPXFileWithStatistics | undefined>>
| GPXTreeElement<AnyGPXTreeElement>
| Waypoint[]
| Waypoint;
private _item: ListItem;
private _sortableLevel: ListLevel;
private _container: HTMLElement;
private _sortable: Sortable | null = null;
private _elements: { [id: string]: HTMLElement } = {};
private _updatingSelection: boolean = false;
private _unsubscribes: (() => void)[] = [];
constructor(
container: HTMLElement,
node:
| Map<string, Readable<GPXFileWithStatistics | undefined>>
| GPXTreeElement<AnyGPXTreeElement>
| Waypoint[]
| Waypoint,
item: ListItem,
waypointRoot: boolean,
sortableLevel: ListLevel,
orientation: Direction
) {
this._node = node;
this._item = item;
this._sortableLevel = sortableLevel;
this._container = container;
this._sortable = Sortable.create(container, {
group: {
name: sortableLevel,
pull: allowedMoves[sortableLevel],
put: true,
},
direction: orientation,
forceAutoScrollFallback: true,
multiDrag: true,
multiDragKey: isMac() ? 'Meta' : 'Ctrl',
avoidImplicitDeselect: true,
onSelect: (e: Sortable.SortableEvent) =>
setTimeout(() => this.updateToSelection(e), 50),
onDeselect: (e: Sortable.SortableEvent) =>
setTimeout(() => this.updateToSelection(e), 50),
onStart: () => dragging.set(sortableLevel),
onEnd: () => dragging.set(null),
onSort: (e: Sortable.SortableEvent) => this.onSort(e),
});
Object.defineProperty(this._sortable, '_item', {
value: item,
writable: true,
});
Object.defineProperty(this._sortable, '_waypointRoot', {
value: waypointRoot,
writable: true,
});
this._unsubscribes.push(
selection.subscribe(() => tick().then(() => this.updateFromSelection()))
);
this._unsubscribes.push(fileOrder.subscribe(() => this.updateFromFileOrder()));
}
onSort(e: Sortable.SortableEvent) {
this.updateToFileOrder();
const from = Sortable.get(e.from);
const to = Sortable.get(e.to);
if (!from || !to) {
return;
}
let fromItem = from._item;
let toItem = to._item;
if (this._item === toItem && !(fromItem instanceof ListRootItem)) {
// Event is triggered on source and destination list, only handle it once
let fromItems = [];
let toItems = [];
if (from._waypointRoot) {
fromItems = [fromItem.extend('waypoints')];
} else {
let oldIndices: number[] =
e.oldIndicies.length > 0 ? e.oldIndicies.map((i) => i.index) : [e.oldIndex];
oldIndices = oldIndices.filter((i) => i >= 0);
oldIndices.sort((a, b) => a - b);
fromItems = oldIndices.map((i) => fromItem.extend(i));
}
if (from._waypointRoot && to._waypointRoot) {
toItems = [toItem.extend('waypoints')];
} else {
if (to._waypointRoot) {
toItem = toItem.extend('waypoints');
}
let newIndices: number[] =
e.newIndicies.length > 0 ? e.newIndicies.map((i) => i.index) : [e.newIndex];
newIndices = newIndices.filter((i) => i >= 0);
newIndices.sort((a, b) => a - b);
if (toItem instanceof ListRootItem) {
let newFileIds = getFileIds(newIndices.length);
toItems = newIndices.map((i, index) => {
get(fileOrder).splice(i, 0, newFileIds[index]);
return this._item.extend(newFileIds[index]);
});
} else {
toItems = newIndices.map((i) => toItem.extend(i));
}
}
moveItems(fromItem, toItem, fromItems, toItems);
}
}
updateFromSelection() {
const changed = this.getChangedIds();
if (changed.length === 0) {
return;
}
const selection_ = get(selection);
for (let id of changed) {
let element = this._elements[id];
if (element) {
if (selection_.has(this._item.extend(id))) {
Sortable.utils.select(element);
element.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
});
} else {
Sortable.utils.deselect(element);
}
}
}
}
updateToSelection(e: Sortable.SortableEvent) {
if (!this._sortable) return;
if (this._updatingSelection) return;
this._updatingSelection = true;
const changed = this.getChangedIds();
if (changed.length == 0) {
this._updatingSelection = false;
return;
}
selection.update(($selection) => {
$selection.clear();
Object.entries(this._elements).forEach(([id, element]) => {
$selection.set(
this._item.extend(this.getRealId(id)),
element.classList.contains('sortable-selected')
);
});
if (
e.originalEvent &&
!(e.originalEvent.ctrlKey || e.originalEvent.metaKey || e.originalEvent.shiftKey) &&
($selection.size > 1 ||
!$selection.has(this._item.extend(this.getRealId(changed[0]))))
) {
// Fix bug that sometimes causes a single select to be treated as a multi-select
$selection.clear();
$selection.set(this._item.extend(this.getRealId(changed[0])), true);
}
return $selection;
});
this._updatingSelection = false;
}
updateFromFileOrder() {
if (!this._sortable || this._sortableLevel !== ListLevel.FILE) {
return;
}
const fileOrder_ = get(fileOrder);
const sortableOrder = this._sortable.toArray();
if (
fileOrder_.length !== sortableOrder.length ||
fileOrder_.some((value, index) => value !== sortableOrder[index])
) {
this._sortable.sort(fileOrder_);
}
}
updateToFileOrder() {
if (!this._sortable || this._sortableLevel !== ListLevel.FILE) {
return;
}
const fileOrder_ = get(fileOrder);
const sortableOrder = this._sortable.toArray();
if (
fileOrder_.length !== sortableOrder.length ||
fileOrder_.some((value, index) => value !== sortableOrder[index])
) {
fileOrder.set(sortableOrder);
}
}
updateElements() {
this._elements = {};
this._container.childNodes.forEach((element) => {
if (element instanceof HTMLElement) {
let attr = element.getAttribute('data-id');
if (attr) {
if (this._node instanceof Map && !this._node.has(attr)) {
element.remove();
} else {
this._elements[attr] = element;
}
}
}
});
}
destroy() {
this._sortable = null;
this._unsubscribes.forEach((unsubscribe) => unsubscribe());
this._unsubscribes = [];
}
getChangedIds() {
let changed: (string | number)[] = [];
const selection_ = get(selection);
Object.entries(this._elements).forEach(([id, element]) => {
let realId = this.getRealId(id);
let realItem = this._item.extend(realId);
let inSelection = selection_.has(realItem);
let isSelected = element.classList.contains('sortable-selected');
if (inSelection !== isSelected) {
changed.push(realId);
}
});
return changed;
}
getRealId(id: string | number) {
return this._sortableLevel === ListLevel.FILE || this._sortableLevel === ListLevel.WAYPOINTS
? id
: parseInt(id as string);
}
}

View File

@@ -0,0 +1,171 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label/index.js';
import { Slider } from '$lib/components/ui/slider';
import * as Popover from '$lib/components/ui/popover';
import { Save } from '@lucide/svelte';
import {
ListFileItem,
ListTrackItem,
type ListItem,
} from '$lib/components/file-list/file-list';
import { editStyle } from '$lib/components/file-list/style/utils.svelte';
import { i18n } from '$lib/i18n.svelte';
import type { LineStyleExtension } from 'gpx';
import { settings } from '$lib/logic/settings';
import { selection } from '$lib/logic/selection';
import { fileStateCollection } from '$lib/logic/file-state';
import { gpxLayers } from '$lib/components/map/gpx-layer/gpx-layers';
import { untrack } from 'svelte';
import { fileActions } from '$lib/logic/file-actions';
let {
item,
open = $bindable(),
}: {
item: ListItem;
open: boolean;
} = $props();
const { defaultOpacity, defaultWidth } = settings;
let color: string = $state('');
let opacity: number = $state(0);
let width: number = $state(0);
let colorChanged = $state(false);
let opacityChanged = $state(false);
let widthChanged = $state(false);
function setStyleInputs() {
opacity = $defaultOpacity;
width = $defaultWidth;
$selection.forEach((item) => {
if (item instanceof ListFileItem) {
let file = fileStateCollection.getFile(item.getFileId());
let layer = gpxLayers.getLayer(item.getFileId());
if (file && layer) {
let style = file.getStyle();
color = layer.layerColor;
if (style.opacity.length > 0) {
opacity = style.opacity[0];
}
if (style.width.length > 0) {
width = style.width[0];
}
}
} else if (item instanceof ListTrackItem) {
let file = fileStateCollection.getFile(item.getFileId());
let layer = gpxLayers.getLayer(item.getFileId());
if (file && layer) {
color = layer.layerColor;
let track = file.trk[item.getTrackIndex()];
let style = track.getStyle();
if (style) {
if (style['gpx_style:color']) {
color = style['gpx_style:color'];
}
if (style['gpx_style:opacity']) {
opacity = style['gpx_style:opacity'];
}
if (style['gpx_style:width']) {
width = style['gpx_style:width'];
}
}
}
}
});
colorChanged = false;
opacityChanged = false;
widthChanged = false;
}
$effect(() => {
if ($selection && open) {
untrack(() => setStyleInputs());
}
});
$effect(() => {
if (!open) {
editStyle.current = false;
}
});
function applyStyle() {
let style: LineStyleExtension = {};
if (colorChanged) {
style['gpx_style:color'] = color;
}
if (opacityChanged) {
style['gpx_style:opacity'] = opacity;
}
if (widthChanged) {
style['gpx_style:width'] = width;
}
fileActions.setStyleToSelection(style);
if (item instanceof ListFileItem && $selection.size === fileStateCollection.size) {
if (style['gpx_style:opacity']) {
$defaultOpacity = style['gpx_style:opacity'];
}
if (style['gpx_style:width']) {
$defaultWidth = style['gpx_style:width'];
}
}
open = false;
}
</script>
<Popover.Root bind:open>
<Popover.Trigger class="-mx-1" />
<Popover.Content side="top" sideOffset={22} alignOffset={30} class="flex flex-col gap-3">
<Label class="flex flex-row gap-2 items-center justify-between">
{i18n._('menu.style.color')}
<Input
bind:value={color}
type="color"
class="p-0 h-6 w-40"
onchange={() => (colorChanged = true)}
/>
</Label>
<Label class="flex flex-row gap-2 items-center justify-between">
{i18n._('menu.style.opacity')}
<div class="w-40 p-2">
<Slider
bind:value={opacity}
min={0.3}
max={1}
step={0.1}
onValueChange={() => (opacityChanged = true)}
type="single"
/>
</div>
</Label>
<Label class="flex flex-row gap-2 items-center justify-between">
{i18n._('menu.style.width')}
<div class="w-40 p-2">
<Slider
bind:value={width}
id="width"
min={1}
max={10}
step={1}
onValueChange={() => (widthChanged = true)}
type="single"
/>
</div>
</Label>
<Button
variant="outline"
disabled={!colorChanged && !opacityChanged && !widthChanged}
onclick={applyStyle}
>
<Save size="16" />
{i18n._('menu.metadata.save')}
</Button>
</Popover.Content>
</Popover.Root>

View File

@@ -0,0 +1,3 @@
export const editStyle = $state({
current: false,
});

View File

@@ -1,96 +0,0 @@
import { font } from "$lib/assets/layers";
import { settings } from "$lib/db";
import { gpxStatistics } from "$lib/stores";
import { get } from "svelte/store";
const { distanceMarkers, distanceUnits, currentBasemap } = settings;
export class DistanceMarkers {
map: mapboxgl.Map;
updateBinded: () => void = this.update.bind(this);
unsubscribes: (() => void)[] = [];
constructor(map: mapboxgl.Map) {
this.map = map;
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
this.unsubscribes.push(distanceMarkers.subscribe(this.updateBinded));
this.unsubscribes.push(distanceUnits.subscribe(this.updateBinded));
this.map.on('style.load', this.updateBinded);
}
update() {
try {
if (get(distanceMarkers)) {
let distanceSource = this.map.getSource('distance-markers');
if (distanceSource) {
distanceSource.setData(this.getDistanceMarkersGeoJSON());
} else {
this.map.addSource('distance-markers', {
type: 'geojson',
data: this.getDistanceMarkersGeoJSON()
});
}
if (!this.map.getLayer('distance-markers')) {
this.map.addLayer({
id: 'distance-markers',
type: 'symbol',
source: 'distance-markers',
layout: {
'text-field': ['get', 'distance'],
'text-size': 14,
'text-font': [font[get(currentBasemap)] ?? 'Open Sans Bold'],
'text-padding': 20,
},
paint: {
'text-color': 'black',
'text-halo-width': 2,
'text-halo-color': 'white',
}
});
} else {
this.map.moveLayer('distance-markers');
}
} else {
if (this.map.getLayer('distance-markers')) {
this.map.removeLayer('distance-markers');
}
}
} catch (e) { // No reliable way to check if the map is ready to add sources and layers
return;
}
}
remove() {
this.unsubscribes.forEach(unsubscribe => unsubscribe());
}
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
let statistics = get(gpxStatistics);
let features = [];
let currentTargetDistance = 1;
for (let i = 0; i < statistics.local.distance.total.length; i++) {
if (statistics.local.distance.total[i] >= currentTargetDistance * (get(distanceUnits) === 'metric' ? 1 : 1.60934)) {
let distance = currentTargetDistance.toFixed(0);
features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [statistics.local.points[i].getLongitude(), statistics.local.points[i].getLatitude()]
},
properties: {
distance,
}
} as GeoJSON.Feature);
currentTargetDistance += 1;
}
}
return {
type: 'FeatureCollection',
features
};
}
}

View File

@@ -1,461 +0,0 @@
import { currentTool, map, Tool } from "$lib/stores";
import { settings, type GPXFileWithStatistics, dbUtils } from "$lib/db";
import { get, type Readable } from "svelte/store";
import mapboxgl from "mapbox-gl";
import { currentPopupWaypoint, deleteWaypoint, waypointPopup } from "./WaypointPopup";
import { addSelectItem, selectItem, selection } from "$lib/components/file-list/Selection";
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList";
import type { Waypoint } from "gpx";
import { getElevation, resetCursor, setGrabbingCursor, setPointerCursor, setScissorsCursor } from "$lib/utils";
import { font } from "$lib/assets/layers";
import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte";
import { MapPin, Square } from "lucide-static";
import { getSymbolKey, symbols } from "$lib/assets/symbols";
const colors = [
'#ff0000',
'#0000ff',
'#46e646',
'#00ccff',
'#ff9900',
'#ff00ff',
'#ffff32',
'#288228',
'#9933ff',
'#50f0be',
'#8c645a'
];
const colorCount: { [key: string]: number } = {};
for (let color of colors) {
colorCount[color] = 0;
}
// Get the color with the least amount of uses
function getColor() {
let color = colors.reduce((a, b) => (colorCount[a] <= colorCount[b] ? a : b));
colorCount[color]++;
return color;
}
function decrementColor(color: string) {
if (colorCount.hasOwnProperty(color)) {
colorCount[color]--;
}
}
function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined;
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
${Square
.replace('width="24"', 'width="12"')
.replace('height="24"', 'height="12"')
.replace('stroke="currentColor"', 'stroke="SteelBlue"')
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
.replace('fill="none"', `fill="${layerColor}"`)}
${MapPin
.replace('width="24"', '')
.replace('height="24"', '')
.replace('stroke="currentColor"', '')
.replace('path', `path fill="#3fb1ce" stroke="SteelBlue" stroke-width="1"`)
.replace('circle', `circle fill="${symbolSvg ? 'none' : 'white'}" stroke="${symbolSvg ? 'none' : 'white'}" stroke-width="2"`)}
${symbolSvg?.replace('width="24"', 'width="10"')
.replace('height="24"', 'height="10"')
.replace('stroke="currentColor"', 'stroke="white"')
.replace('stroke-width="2"', 'stroke-width="2.5" x="7" y="5"') ?? ''}
</svg>`;
}
const { directionMarkers, verticalFileView, currentBasemap, defaultOpacity, defaultWeight } = settings;
export class GPXLayer {
map: mapboxgl.Map;
fileId: string;
file: Readable<GPXFileWithStatistics | undefined>;
layerColor: string;
markers: mapboxgl.Marker[] = [];
selected: boolean = false;
draggable: boolean;
unsubscribe: Function[] = [];
updateBinded: () => void = this.update.bind(this);
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
maybeHideWaypointPopupBinded: (e: any) => void = this.maybeHideWaypointPopup.bind(this);
constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
this.map = map;
this.fileId = fileId;
this.file = file;
this.layerColor = getColor();
this.unsubscribe.push(file.subscribe(this.updateBinded));
this.unsubscribe.push(selection.subscribe($selection => {
let newSelected = $selection.hasAnyChildren(new ListFileItem(this.fileId));
if (this.selected || newSelected) {
this.selected = newSelected;
this.update();
}
if (newSelected) {
this.moveToFront();
}
}));
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded));
this.unsubscribe.push(currentTool.subscribe(tool => {
if (tool === Tool.WAYPOINT && !this.draggable) {
this.draggable = true;
this.markers.forEach(marker => marker.setDraggable(true));
} else if (tool !== Tool.WAYPOINT && this.draggable) {
this.draggable = false;
this.markers.forEach(marker => marker.setDraggable(false));
}
}));
this.draggable = get(currentTool) === Tool.WAYPOINT;
this.map.on('style.load', this.updateBinded);
}
update() {
let file = get(this.file)?.file;
if (!file) {
return;
}
if (file._data.style && file._data.style.color && this.layerColor !== `#${file._data.style.color}`) {
decrementColor(this.layerColor);
this.layerColor = `#${file._data.style.color}`;
}
try {
let source = this.map.getSource(this.fileId);
if (source) {
source.setData(this.getGeoJSON());
} else {
this.map.addSource(this.fileId, {
type: 'geojson',
data: this.getGeoJSON()
});
}
if (!this.map.getLayer(this.fileId)) {
this.map.addLayer({
id: this.fileId,
type: 'line',
source: this.fileId,
layout: {
'line-join': 'round',
'line-cap': 'round'
},
paint: {
'line-color': ['get', 'color'],
'line-width': ['get', 'weight'],
'line-opacity': ['get', 'opacity']
}
});
this.map.on('click', this.fileId, this.layerOnClickBinded);
this.map.on('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
this.map.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
}
if (get(directionMarkers)) {
if (!this.map.getLayer(this.fileId + '-direction')) {
this.map.addLayer({
id: this.fileId + '-direction',
type: 'symbol',
source: this.fileId,
layout: {
'text-field': '»',
'text-offset': [0, -0.1],
'text-keep-upright': false,
'text-max-angle': 361,
'text-allow-overlap': true,
'text-font': [font[get(currentBasemap)] ?? 'Open Sans Bold'],
'symbol-placement': 'line',
'symbol-spacing': 20,
},
paint: {
'text-color': 'white',
'text-opacity': 0.7,
'text-halo-width': 0.2,
'text-halo-color': 'white'
}
}, this.map.getLayer('distance-markers') ? 'distance-markers' : undefined);
}
} else {
if (this.map.getLayer(this.fileId + '-direction')) {
this.map.removeLayer(this.fileId + '-direction');
}
}
let visibleItems: [number, number][] = [];
file.forEachSegment((segment, trackIndex, segmentIndex) => {
if (!segment._data.hidden) {
visibleItems.push([trackIndex, segmentIndex]);
}
});
this.map.setFilter(this.fileId, ['any', ...visibleItems.map(([trackIndex, segmentIndex]) => ['all', ['==', 'trackIndex', trackIndex], ['==', 'segmentIndex', segmentIndex]])], { validate: false });
if (this.map.getLayer(this.fileId + '-direction')) {
this.map.setFilter(this.fileId + '-direction', ['any', ...visibleItems.map(([trackIndex, segmentIndex]) => ['all', ['==', 'trackIndex', trackIndex], ['==', 'segmentIndex', segmentIndex]])], { validate: false });
}
} catch (e) { // No reliable way to check if the map is ready to add sources and layers
return;
}
let markerIndex = 0;
if (get(selection).hasAnyChildren(new ListFileItem(this.fileId))) {
file.wpt.forEach((waypoint) => { // Update markers
let symbolKey = getSymbolKey(waypoint.sym);
if (markerIndex < this.markers.length) {
this.markers[markerIndex].getElement().innerHTML = getMarkerForSymbol(symbolKey, this.layerColor);
this.markers[markerIndex].setLngLat(waypoint.getCoordinates());
Object.defineProperty(this.markers[markerIndex], '_waypoint', { value: waypoint, writable: true });
} else {
let element = document.createElement('div');
element.classList.add('w-8', 'h-8', 'drop-shadow-xl');
element.innerHTML = getMarkerForSymbol(symbolKey, this.layerColor);
let marker = new mapboxgl.Marker({
draggable: this.draggable,
element,
anchor: 'bottom'
}).setLngLat(waypoint.getCoordinates());
Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true });
let dragEndTimestamp = 0;
marker.getElement().addEventListener('mouseover', (e) => {
if (marker._isDragging) {
return;
}
this.showWaypointPopup(marker._waypoint);
e.stopPropagation();
});
marker.getElement().addEventListener('click', (e) => {
if (dragEndTimestamp && Date.now() - dragEndTimestamp < 1000) {
return;
}
if (get(currentTool) === Tool.WAYPOINT && e.shiftKey) {
deleteWaypoint(this.fileId, marker._waypoint._data.index);
e.stopPropagation();
return;
}
if (get(verticalFileView)) {
if ((e.ctrlKey || e.metaKey) && get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), false)) {
addSelectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index));
} else {
selectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index));
}
} else if (get(currentTool) === Tool.WAYPOINT) {
selectedWaypoint.set([marker._waypoint, this.fileId]);
} else {
this.showWaypointPopup(marker._waypoint);
}
e.stopPropagation();
});
marker.on('dragstart', () => {
setGrabbingCursor();
marker.getElement().style.cursor = 'grabbing';
this.hideWaypointPopup();
});
marker.on('dragend', (e) => {
resetCursor();
marker.getElement().style.cursor = '';
dbUtils.applyToFile(this.fileId, (file) => {
let latLng = marker.getLngLat();
let wpt = file.wpt[marker._waypoint._data.index];
wpt.setCoordinates({
lat: latLng.lat,
lon: latLng.lng
});
wpt.ele = getElevation(this.map, wpt.getCoordinates());
});
dragEndTimestamp = Date.now()
});
this.markers.push(marker);
}
markerIndex++;
});
}
while (markerIndex < this.markers.length) { // Remove extra markers
this.markers.pop()?.remove();
}
this.markers.forEach((marker) => {
if (!marker._waypoint._data.hidden) {
marker.addTo(this.map);
} else {
marker.remove();
}
});
}
updateMap(map: mapboxgl.Map) {
this.map = map;
this.map.on('style.load', this.updateBinded);
this.update();
}
remove() {
if (get(map)) {
this.map.off('click', this.fileId, this.layerOnClickBinded);
this.map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
this.map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
this.map.off('style.load', this.updateBinded);
if (this.map.getLayer(this.fileId + '-direction')) {
this.map.removeLayer(this.fileId + '-direction');
}
if (this.map.getLayer(this.fileId)) {
this.map.removeLayer(this.fileId);
}
if (this.map.getSource(this.fileId)) {
this.map.removeSource(this.fileId);
}
}
this.markers.forEach((marker) => {
marker.remove();
});
this.unsubscribe.forEach((unsubscribe) => unsubscribe());
decrementColor(this.layerColor);
}
moveToFront() {
if (this.map.getLayer(this.fileId)) {
this.map.moveLayer(this.fileId);
}
if (this.map.getLayer(this.fileId + '-direction')) {
this.map.moveLayer(this.fileId + '-direction', this.map.getLayer('distance-markers') ? 'distance-markers' : undefined);
}
}
layerOnMouseEnter(e: any) {
let trackIndex = e.features[0].properties.trackIndex;
let segmentIndex = e.features[0].properties.segmentIndex;
if (get(currentTool) === Tool.SCISSORS && get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
setScissorsCursor();
} else {
setPointerCursor();
}
}
layerOnMouseLeave() {
resetCursor();
}
layerOnClick(e: any) {
if (get(currentTool) === Tool.ROUTING && get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])) {
return;
}
let trackIndex = e.features[0].properties.trackIndex;
let segmentIndex = e.features[0].properties.segmentIndex;
if (get(currentTool) === Tool.SCISSORS && get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
dbUtils.split(this.fileId, trackIndex, segmentIndex, { lat: e.lngLat.lat, lon: e.lngLat.lng });
return;
}
let file = get(this.file)?.file;
if (!file) {
return;
}
let item = undefined;
if (get(verticalFileView) && file.getSegments().length > 1) { // Select inner item
item = file.children[trackIndex].children.length > 1 ? new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) : new ListTrackItem(this.fileId, trackIndex);
} else {
item = new ListFileItem(this.fileId);
}
if (e.originalEvent.ctrlKey || e.originalEvent.metaKey) {
addSelectItem(item);
} else {
selectItem(item);
}
}
showWaypointPopup(waypoint: Waypoint) {
if (get(currentPopupWaypoint) !== null) {
this.hideWaypointPopup();
}
let marker = this.markers[waypoint._data.index];
if (marker) {
currentPopupWaypoint.set([waypoint, this.fileId]);
marker.setPopup(waypointPopup);
marker.togglePopup();
this.map.on('mousemove', this.maybeHideWaypointPopupBinded);
}
}
maybeHideWaypointPopup(e: any) {
let waypoint = get(currentPopupWaypoint)?.[0];
if (waypoint) {
let marker = this.markers[waypoint._data.index];
if (marker) {
if (this.map.project(marker.getLngLat()).dist(this.map.project(e.lngLat)) > 100) {
this.hideWaypointPopup();
}
} else {
this.hideWaypointPopup();
}
}
}
hideWaypointPopup() {
let waypoint = get(currentPopupWaypoint)?.[0];
if (waypoint) {
let marker = this.markers[waypoint._data.index];
marker?.getPopup()?.remove();
currentPopupWaypoint.set(null);
this.map.off('mousemove', this.maybeHideWaypointPopupBinded);
}
}
getGeoJSON(): GeoJSON.FeatureCollection {
let file = get(this.file)?.file;
if (!file) {
return {
type: 'FeatureCollection',
features: []
};
}
let data = file.toGeoJSON();
let trackIndex = 0, segmentIndex = 0;
for (let feature of data.features) {
if (!feature.properties) {
feature.properties = {};
}
if (!feature.properties.color) {
feature.properties.color = this.layerColor;
}
if (!feature.properties.weight) {
feature.properties.weight = get(defaultWeight);
}
if (!feature.properties.opacity) {
feature.properties.opacity = get(defaultOpacity);
}
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)) || get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), true)) {
feature.properties.weight = feature.properties.weight + 2;
feature.properties.opacity = Math.min(1, feature.properties.opacity + 0.1);
}
feature.properties.trackIndex = trackIndex;
feature.properties.segmentIndex = segmentIndex;
segmentIndex++;
if (segmentIndex >= file.trk[trackIndex].trkseg.length) {
segmentIndex = 0;
trackIndex++;
}
}
return data;
}
}

View File

@@ -1,58 +0,0 @@
<script lang="ts">
import { map, gpxLayers } from '$lib/stores';
import { GPXLayer } from './GPXLayer';
import WaypointPopup from './WaypointPopup.svelte';
import { fileObservers } from '$lib/db';
import { DistanceMarkers } from './DistanceMarkers';
import { StartEndMarkers } from './StartEndMarkers';
import { onDestroy } from 'svelte';
let distanceMarkers: DistanceMarkers | undefined = undefined;
let startEndMarkers: StartEndMarkers | undefined = undefined;
$: if ($map && $fileObservers) {
// remove layers for deleted files
gpxLayers.forEach((layer, fileId) => {
if (!$fileObservers.has(fileId)) {
layer.remove();
gpxLayers.delete(fileId);
} else if ($map !== layer.map) {
layer.updateMap($map);
}
});
// add layers for new files
$fileObservers.forEach((file, fileId) => {
if (!gpxLayers.has(fileId)) {
gpxLayers.set(fileId, new GPXLayer($map, fileId, file));
}
});
}
$: if ($map) {
if (distanceMarkers) {
distanceMarkers.remove();
}
if (startEndMarkers) {
startEndMarkers.remove();
}
distanceMarkers = new DistanceMarkers($map);
startEndMarkers = new StartEndMarkers($map);
}
onDestroy(() => {
gpxLayers.forEach((layer) => layer.remove());
gpxLayers.clear();
if (distanceMarkers) {
distanceMarkers.remove();
distanceMarkers = undefined;
}
if (startEndMarkers) {
startEndMarkers.remove();
startEndMarkers = undefined;
}
});
</script>
<WaypointPopup />

View File

@@ -1,105 +0,0 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import Shortcut from '$lib/components/Shortcut.svelte';
import { waypointPopup, currentPopupWaypoint, deleteWaypoint } from './WaypointPopup';
import WithUnits from '$lib/components/WithUnits.svelte';
import { Dot, ExternalLink, Trash2 } from 'lucide-svelte';
import { onMount } from 'svelte';
import { Tool, currentTool } from '$lib/stores';
import { getSymbolKey, symbols } from '$lib/assets/symbols';
import { _ } from 'svelte-i18n';
import sanitizeHtml from 'sanitize-html';
let popupElement: HTMLDivElement;
onMount(() => {
waypointPopup.setDOMContent(popupElement);
popupElement.classList.remove('hidden');
});
$: symbolKey = $currentPopupWaypoint ? getSymbolKey($currentPopupWaypoint[0].sym) : undefined;
function sanitize(text: string | undefined): string {
if (text === undefined) {
return '';
}
let sanitized = sanitizeHtml(text, {
allowedTags: ['a', 'br'],
allowedAttributes: {
a: ['href', 'target']
}
}).trim();
return sanitized;
}
</script>
<div bind:this={popupElement} class="hidden">
{#if $currentPopupWaypoint}
<Card.Root class="border-none shadow-md text-base max-w-80 p-2">
<Card.Header class="p-0">
<Card.Title class="text-md">
{#if $currentPopupWaypoint[0].link && $currentPopupWaypoint[0].link.attributes && $currentPopupWaypoint[0].link.attributes.href}
<a href={$currentPopupWaypoint[0].link.attributes.href} target="_blank">
{$currentPopupWaypoint[0].name ?? $currentPopupWaypoint[0].link.attributes.href}
<ExternalLink size="12" class="inline-block mb-1.5" />
</a>
{:else}
{$currentPopupWaypoint[0].name ?? $_('gpx.waypoint')}
{/if}
</Card.Title>
</Card.Header>
<Card.Content class="flex flex-col p-0 text-sm">
<div class="flex flex-row items-center text-muted-foreground text-xs whitespace-nowrap">
{#if symbolKey}
<span>
{#if symbols[symbolKey].icon}
<svelte:component
this={symbols[symbolKey].icon}
size="12"
class="inline-block mb-0.5"
/>
{:else}
<span class="w-4 inline-block" />
{/if}
{$_(`gpx.symbol.${symbolKey}`)}
</span>
<Dot size="16" />
{/if}
{$currentPopupWaypoint[0].getLatitude().toFixed(6)}&deg; {$currentPopupWaypoint[0]
.getLongitude()
.toFixed(6)}&deg;
{#if $currentPopupWaypoint[0].ele !== undefined}
<Dot size="16" />
<WithUnits value={$currentPopupWaypoint[0].ele} type="elevation" />
{/if}
</div>
{#if $currentPopupWaypoint[0].desc}
<span class="whitespace-pre-wrap">{@html sanitize($currentPopupWaypoint[0].desc)}</span>
{/if}
{#if $currentPopupWaypoint[0].cmt && $currentPopupWaypoint[0].cmt !== $currentPopupWaypoint[0].desc}
<span class="whitespace-pre-wrap">{@html sanitize($currentPopupWaypoint[0].cmt)}</span>
{/if}
{#if $currentTool === Tool.WAYPOINT}
<Button
class="mt-2 w-full px-2 py-1 h-8 justify-start"
variant="outline"
on:click={() =>
deleteWaypoint($currentPopupWaypoint[1], $currentPopupWaypoint[0]._data.index)}
>
<Trash2 size="16" class="mr-1" />
{$_('menu.delete')}
<Shortcut key="" shift={true} click={true} />
</Button>
{/if}
</Card.Content>
</Card.Root>
{/if}
</div>
<style lang="postcss">
div :global(a) {
@apply text-blue-500 dark:text-blue-300;
@apply hover:underline;
}
</style>

View File

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

View File

@@ -1,417 +0,0 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Button } from '$lib/components/ui/button';
import { Separator } from '$lib/components/ui/separator';
import * as RadioGroup from '$lib/components/ui/radio-group';
import {
CirclePlus,
CircleX,
Minus,
Pencil,
Plus,
Save,
Trash2,
Move,
Map,
Layers2
} from 'lucide-svelte';
import { _ } from 'svelte-i18n';
import { settings } from '$lib/db';
import { defaultBasemap, extendBasemap, type CustomLayer } from '$lib/assets/layers';
import { map } from '$lib/stores';
import { onDestroy, onMount } from 'svelte';
import Sortable from 'sortablejs/Sortable';
const {
customLayers,
selectedBasemapTree,
selectedOverlayTree,
currentBasemap,
previousBasemap,
currentOverlays,
previousOverlays,
customBasemapOrder,
customOverlayOrder
} = settings;
let name: string = '';
let tileUrls: string[] = [''];
let maxZoom: number = 20;
let layerType: 'basemap' | 'overlay' = 'basemap';
let resourceType: 'raster' | 'vector' = 'raster';
let basemapContainer: HTMLElement;
let overlayContainer: HTMLElement;
let basemapSortable: Sortable;
let overlaySortable: Sortable;
onMount(() => {
if ($customBasemapOrder.length === 0) {
$customBasemapOrder = Object.keys($customLayers).filter(
(id) => $customLayers[id].layerType === 'basemap'
);
}
if ($customOverlayOrder.length === 0) {
$customOverlayOrder = Object.keys($customLayers).filter(
(id) => $customLayers[id].layerType === 'overlay'
);
}
basemapSortable = Sortable.create(basemapContainer, {
onSort: (e) => {
$customBasemapOrder = basemapSortable.toArray();
$selectedBasemapTree.basemaps['custom'] = $customBasemapOrder.reduce((acc, id) => {
acc[id] = true;
return acc;
}, {});
}
});
overlaySortable = Sortable.create(overlayContainer, {
onSort: (e) => {
$customOverlayOrder = overlaySortable.toArray();
$selectedOverlayTree.overlays['custom'] = $customOverlayOrder.reduce((acc, id) => {
acc[id] = true;
return acc;
}, {});
}
});
basemapSortable.sort($customBasemapOrder);
overlaySortable.sort($customOverlayOrder);
});
onDestroy(() => {
basemapSortable.destroy();
overlaySortable.destroy();
});
$: if (tileUrls[0].length > 0) {
if (
tileUrls[0].includes('.json') ||
(tileUrls[0].includes('api.mapbox.com/styles') && !tileUrls[0].includes('tiles'))
) {
resourceType = 'vector';
layerType = 'basemap';
} else {
resourceType = 'raster';
}
}
function createLayer() {
if (selectedLayerId && $customLayers[selectedLayerId].layerType !== layerType) {
deleteLayer(selectedLayerId);
}
if (typeof maxZoom === 'string') {
maxZoom = parseInt(maxZoom);
}
let layerId = selectedLayerId ?? getLayerId();
let layer: CustomLayer = {
id: layerId,
name: name,
tileUrls: tileUrls,
maxZoom: maxZoom,
layerType: layerType,
resourceType: resourceType,
value: ''
};
if (resourceType === 'vector') {
layer.value = tileUrls[0];
} else {
if (layerType === 'basemap') {
layer.value = extendBasemap({
version: 8,
sources: {
[layerId]: {
type: 'raster',
tiles: tileUrls,
maxzoom: maxZoom
}
},
layers: [
{
id: layerId,
type: 'raster',
source: layerId
}
]
});
} else {
layer.value = {
type: 'raster',
tiles: tileUrls,
maxzoom: maxZoom
};
}
}
$customLayers[layerId] = layer;
addLayer(layerId);
selectedLayerId = undefined;
setDataFromSelectedLayer();
}
function getLayerId() {
for (let id = 0; ; id++) {
if (!$customLayers.hasOwnProperty(`custom-${id}`)) {
return `custom-${id}`;
}
}
}
function addLayer(layerId: string) {
if (layerType === 'basemap') {
selectedBasemapTree.update(($tree) => {
if (!$tree.basemaps.hasOwnProperty('custom')) {
$tree.basemaps['custom'] = {};
}
$tree.basemaps['custom'][layerId] = true;
return $tree;
});
$currentBasemap = layerId;
if (!$customBasemapOrder.includes(layerId)) {
$customBasemapOrder = [...$customBasemapOrder, layerId];
}
} else {
selectedOverlayTree.update(($tree) => {
if (!$tree.overlays.hasOwnProperty('custom')) {
$tree.overlays['custom'] = {};
}
$tree.overlays['custom'][layerId] = true;
return $tree;
});
if ($map && $map.getSource(layerId)) {
// Reset source when updating an existing layer
if ($map.getLayer(layerId)) {
$map.removeLayer(layerId);
}
$map.removeSource(layerId);
}
if (!$currentOverlays.overlays.hasOwnProperty('custom')) {
$currentOverlays.overlays['custom'] = {};
}
$currentOverlays.overlays['custom'][layerId] = true;
if (!$customOverlayOrder.includes(layerId)) {
$customOverlayOrder = [...$customOverlayOrder, layerId];
}
}
}
function tryDeleteLayer(node: any, id: string): any {
if (node.hasOwnProperty(id)) {
delete node[id];
}
return node;
}
function deleteLayer(layerId: string) {
let layer = $customLayers[layerId];
if (layer.layerType === 'basemap') {
if (layerId === $currentBasemap) {
$currentBasemap = defaultBasemap;
}
if (layerId === $previousBasemap) {
$previousBasemap = defaultBasemap;
}
$selectedBasemapTree.basemaps['custom'] = tryDeleteLayer(
$selectedBasemapTree.basemaps['custom'],
layerId
);
if (Object.keys($selectedBasemapTree.basemaps['custom']).length === 0) {
$selectedBasemapTree.basemaps = tryDeleteLayer($selectedBasemapTree.basemaps, 'custom');
}
$customBasemapOrder = $customBasemapOrder.filter((id) => id !== layerId);
} else {
$currentOverlays.overlays['custom'][layerId] = false;
if ($previousOverlays.overlays['custom']) {
$previousOverlays.overlays['custom'] = tryDeleteLayer(
$previousOverlays.overlays['custom'],
layerId
);
}
$selectedOverlayTree.overlays['custom'] = tryDeleteLayer(
$selectedOverlayTree.overlays['custom'],
layerId
);
if (Object.keys($selectedOverlayTree.overlays['custom']).length === 0) {
$selectedOverlayTree.overlays = tryDeleteLayer($selectedOverlayTree.overlays, 'custom');
}
$customOverlayOrder = $customOverlayOrder.filter((id) => id !== layerId);
if ($map) {
if ($map.getLayer(layerId)) {
$map.removeLayer(layerId);
}
if ($map.getSource(layerId)) {
$map.removeSource(layerId);
}
}
}
$customLayers = tryDeleteLayer($customLayers, layerId);
}
let selectedLayerId: string | undefined = undefined;
function setDataFromSelectedLayer() {
if (selectedLayerId) {
const layer = $customLayers[selectedLayerId];
name = layer.name;
tileUrls = layer.tileUrls;
maxZoom = layer.maxZoom;
layerType = layer.layerType;
resourceType = layer.resourceType;
} else {
name = '';
tileUrls = [''];
maxZoom = 20;
layerType = 'basemap';
resourceType = 'raster';
}
}
$: selectedLayerId, setDataFromSelectedLayer();
</script>
<div class="flex flex-col">
{#if $customBasemapOrder.length > 0}
<div class="flex flex-row items-center gap-1 font-semibold mb-2">
<Map size="16" />
{$_('layers.label.basemaps')}
<div class="grow">
<Separator />
</div>
</div>
{/if}
<div
bind:this={basemapContainer}
class="ml-1.5 flex flex-col gap-1 {$customBasemapOrder.length > 0 ? 'mb-2' : ''}"
>
{#each $customBasemapOrder as id (id)}
<div class="flex flex-row items-center gap-2" data-id={id}>
<Move size="12" />
<span class="grow">{$customLayers[id].name}</span>
<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
<Pencil size="16" />
</Button>
<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
<Trash2 size="16" />
</Button>
</div>
{/each}
</div>
{#if $customOverlayOrder.length > 0}
<div class="flex flex-row items-center gap-1 font-semibold mb-2">
<Layers2 size="16" />
{$_('layers.label.overlays')}
<div class="grow">
<Separator />
</div>
</div>
{/if}
<div
bind:this={overlayContainer}
class="ml-1.5 flex flex-col gap-1 {$customOverlayOrder.length > 0 ? 'mb-2' : ''}"
>
{#each $customOverlayOrder as id (id)}
<div class="flex flex-row items-center gap-2" data-id={id}>
<Move size="12" />
<span class="grow">{$customLayers[id].name}</span>
<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
<Pencil size="16" />
</Button>
<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
<Trash2 size="16" />
</Button>
</div>
{/each}
</div>
<Card.Root>
<Card.Header class="p-3">
<Card.Title class="text-base">
{#if selectedLayerId}
{$_('layers.custom_layers.edit')}
{:else}
{$_('layers.custom_layers.new')}
{/if}
</Card.Title>
</Card.Header>
<Card.Content class="p-3 pt-0">
<fieldset class="flex flex-col gap-2">
<Label for="name">{$_('menu.metadata.name')}</Label>
<Input bind:value={name} id="name" class="h-8" />
<Label for="url">{$_('layers.custom_layers.urls')}</Label>
{#each tileUrls as url, i}
<div class="flex flex-row gap-2">
<Input
bind:value={tileUrls[i]}
id="url"
class="h-8"
placeholder={$_('layers.custom_layers.url_placeholder')}
/>
{#if tileUrls.length > 1}
<Button
on:click={() => (tileUrls = tileUrls.filter((_, index) => index !== i))}
variant="outline"
class="p-1 h-8"
>
<Minus size="16" />
</Button>
{/if}
{#if i === tileUrls.length - 1}
<Button
on:click={() => (tileUrls = [...tileUrls, ''])}
variant="outline"
class="p-1 h-8"
>
<Plus size="16" />
</Button>
{/if}
</div>
{/each}
{#if resourceType === 'raster'}
<Label for="maxZoom">{$_('layers.custom_layers.max_zoom')}</Label>
<Input type="number" bind:value={maxZoom} id="maxZoom" min={0} max={22} class="h-8" />
{/if}
<Label>{$_('layers.custom_layers.layer_type')}</Label>
<RadioGroup.Root bind:value={layerType} class="flex flex-row">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="basemap" id="basemap" />
<Label for="basemap">{$_('layers.custom_layers.basemap')}</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="overlay" id="overlay" disabled={resourceType === 'vector'} />
<Label for="overlay">{$_('layers.custom_layers.overlay')}</Label>
</div>
</RadioGroup.Root>
{#if selectedLayerId}
<div class="mt-2 flex flex-row gap-2">
<Button variant="outline" on:click={createLayer} class="grow">
<Save size="16" class="mr-1" />
{$_('layers.custom_layers.update')}
</Button>
<Button variant="outline" on:click={() => (selectedLayerId = undefined)}>
<CircleX size="16" />
</Button>
</div>
{:else}
<Button variant="outline" class="mt-2" on:click={createLayer}>
<CirclePlus size="16" class="mr-1" />
{$_('layers.custom_layers.create')}
</Button>
{/if}
</fieldset>
</Card.Content>
</Card.Root>
</div>

View File

@@ -1,203 +0,0 @@
<script lang="ts">
import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
import LayerTree from './LayerTree.svelte';
import { Separator } from '$lib/components/ui/separator';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import { Layers } from 'lucide-svelte';
import { basemaps, defaultBasemap, overlays } from '$lib/assets/layers';
import { settings } from '$lib/db';
import { map } from '$lib/stores';
import { get, writable } from 'svelte/store';
import { getLayers } from './utils';
import { OverpassLayer } from './OverpassLayer';
import OverpassPopup from './OverpassPopup.svelte';
let container: HTMLDivElement;
let overpassLayer: OverpassLayer;
const {
currentBasemap,
previousBasemap,
currentOverlays,
currentOverpassQueries,
selectedBasemapTree,
selectedOverlayTree,
selectedOverpassTree,
customLayers,
opacities
} = settings;
function setStyle() {
if ($map) {
let basemap = basemaps.hasOwnProperty($currentBasemap)
? basemaps[$currentBasemap]
: $customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap];
$map.setStyle(basemap, {
diff: false
});
}
}
$: if ($map && $currentBasemap) {
setStyle();
}
$: if ($map && $currentOverlays) {
// Add or remove overlay layers depending on the current overlays
let overlayLayers = getLayers($currentOverlays);
Object.keys(overlayLayers).forEach((id) => {
if (overlayLayers[id]) {
if (!addOverlayLayer.hasOwnProperty(id)) {
addOverlayLayer[id] = addOverlayLayerForId(id);
}
if (!$map.getLayer(id)) {
addOverlayLayer[id]();
$map.on('style.load', addOverlayLayer[id]);
}
} else if ($map.getLayer(id)) {
$map.removeLayer(id);
$map.off('style.load', addOverlayLayer[id]);
}
});
}
$: if ($map) {
if (overpassLayer) {
overpassLayer.remove();
}
overpassLayer = new OverpassLayer($map);
overpassLayer.add();
}
let selectedBasemap = writable(get(currentBasemap));
selectedBasemap.subscribe((value) => {
// Updates coming from radio buttons
if (value !== get(currentBasemap)) {
previousBasemap.set(get(currentBasemap));
currentBasemap.set(value);
}
});
currentBasemap.subscribe((value) => {
// Updates coming from the database, or from the user swapping basemaps
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;
function openLayerControl() {
open = true;
}
function closeLayerControl() {
open = false;
}
let cancelEvents = false;
</script>
<CustomControl class="group min-w-[29px] min-h-[29px] overflow-hidden">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
bind:this={container}
class="h-full w-full"
on:mouseenter={openLayerControl}
on:mouseleave={closeLayerControl}
on:pointerenter={() => {
if (!open) {
cancelEvents = true;
openLayerControl();
setTimeout(() => {
cancelEvents = false;
}, 500);
}
}}
>
<div
class="flex flex-row justify-center items-center delay-100 transition-[opacity] duration-0 {open
? 'opacity-0 w-0 h-0 delay-0'
: 'w-[29px] h-[29px]'}"
>
<Layers size="20" />
</div>
<div
class="transition-[grid-template-rows grid-template-cols] grid grid-rows-[0fr] grid-cols-[0fr] duration-150 h-full {open
? 'grid-rows-[1fr] grid-cols-[1fr]'
: ''} {cancelEvents ? 'pointer-events-none' : ''}"
>
<ScrollArea>
<div class="h-fit">
<div class="p-2">
<LayerTree
layerTree={$selectedBasemapTree}
name="basemaps"
bind:selected={$selectedBasemap}
/>
</div>
<Separator class="w-full" />
<div class="p-2">
{#if $currentOverlays}
<LayerTree
layerTree={$selectedOverlayTree}
name="overlays"
multiple={true}
bind:checked={$currentOverlays}
/>
{/if}
</div>
<Separator class="w-full" />
<div class="p-2">
{#if $currentOverpassQueries}
<LayerTree
layerTree={$selectedOverpassTree}
name="overpass"
multiple={true}
bind:checked={$currentOverpassQueries}
/>
{/if}
</div>
</div>
</ScrollArea>
</div>
</div>
</CustomControl>
<OverpassPopup />
<svelte:window
on:click={(e) => {
if (open && !cancelEvents && !container.contains(e.target)) {
closeLayerControl();
}
}}
/>

View File

@@ -1,157 +0,0 @@
<script lang="ts">
import LayerTree from './LayerTree.svelte';
import { Separator } from '$lib/components/ui/separator';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import * as Sheet from '$lib/components/ui/sheet';
import * as Accordion from '$lib/components/ui/accordion';
import { Label } from '$lib/components/ui/label';
import * as Select from '$lib/components/ui/select';
import { Slider } from '$lib/components/ui/slider';
import { basemapTree, overlays, overlayTree, overpassTree } from '$lib/assets/layers';
import { isSelected } from '$lib/components/layer-control/utils';
import { settings } from '$lib/db';
import { _ } from 'svelte-i18n';
import { writable } from 'svelte/store';
import { map } from '$lib/stores';
import CustomLayers from './CustomLayers.svelte';
const {
selectedBasemapTree,
selectedOverlayTree,
selectedOverpassTree,
currentOverlays,
customLayers,
opacities
} = settings;
export let open: boolean;
let accordionValue: string | string[] | undefined = undefined;
let selectedOverlay = writable(undefined);
let overlayOpacity = writable([1]);
function setOpacityFromSelection() {
if ($selectedOverlay) {
let overlayId = $selectedOverlay.value;
if ($opacities.hasOwnProperty(overlayId)) {
$overlayOpacity = [$opacities[overlayId]];
} else {
$overlayOpacity = [1];
}
} else {
$overlayOpacity = [1];
}
}
$: if ($selectedOverlay) {
setOpacityFromSelection();
}
</script>
<Sheet.Root bind:open>
<Sheet.Trigger class="hidden" />
<Sheet.Content>
<Sheet.Header class="h-full">
<Sheet.Title>{$_('layers.settings')}</Sheet.Title>
<ScrollArea class="w-[105%] min-h-full pr-4">
<Sheet.Description>
{$_('layers.settings_help')}
</Sheet.Description>
<Accordion.Root class="flex flex-col" bind:value={accordionValue}>
<Accordion.Item value="layer-selection" class="flex flex-col">
<Accordion.Trigger>{$_('layers.selection')}</Accordion.Trigger>
<Accordion.Content class="grow flex flex-col border rounded">
<div class="py-2 pl-1 pr-2">
<LayerTree
layerTree={basemapTree}
name="basemapSettings"
multiple={true}
bind:checked={$selectedBasemapTree}
/>
</div>
<Separator />
<div class="py-2 pl-1 pr-2">
<LayerTree
layerTree={overlayTree}
name="overlaySettings"
multiple={true}
bind:checked={$selectedOverlayTree}
/>
</div>
<Separator />
<div class="py-2 pl-1 pr-2">
<LayerTree
layerTree={overpassTree}
name="overpassSettings"
multiple={true}
bind:checked={$selectedOverpassTree}
/>
</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="overlay-opacity">
<Accordion.Trigger>{$_('layers.opacity')}</Accordion.Trigger>
<Accordion.Content class="flex flex-col gap-3 overflow-visible">
<div class="flex flex-row gap-6 items-center">
<Label>
{$_('layers.custom_layers.overlay')}
</Label>
<Select.Root bind:selected={$selectedOverlay}>
<Select.Trigger class="h-8 mr-1">
<Select.Value />
</Select.Trigger>
<Select.Content class="h-fit max-h-[40dvh] overflow-y-auto">
{#each Object.keys(overlays) as id}
{#if isSelected($selectedOverlayTree, id)}
<Select.Item value={id}>{$_(`layers.label.${id}`)}</Select.Item>
{/if}
{/each}
{#each Object.entries($customLayers) as [id, layer]}
{#if layer.layerType === 'overlay'}
<Select.Item value={id}>{layer.name}</Select.Item>
{/if}
{/each}
</Select.Content>
</Select.Root>
</div>
<Label class="flex flex-row gap-6 items-center">
{$_('menu.style.opacity')}
<div class="p-2 pr-3 grow">
<Slider
bind:value={$overlayOpacity}
min={0.1}
max={1}
step={0.1}
disabled={$selectedOverlay === undefined}
onValueChange={() => {
if ($selectedOverlay) {
$opacities[$selectedOverlay.value] = $overlayOpacity[0];
if ($map) {
if ($map.getLayer($selectedOverlay.value)) {
$map.removeLayer($selectedOverlay.value);
$currentOverlays = $currentOverlays;
}
}
}
}}
/>
</div>
</Label>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="custom-layers">
<Accordion.Trigger>{$_('layers.custom_layers.title')}</Accordion.Trigger>
<Accordion.Content>
<ScrollArea>
<CustomLayers />
</ScrollArea>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
</ScrollArea>
</Sheet.Header>
</Sheet.Content>
</Sheet.Root>

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