16 Commits
Author SHA1 Message Date
vcoppe c0e6e3d2eb fix release step 2026-11-01 17:43:28 +01:00
vcoppe 9bd38cd5f8 fix tests 2026-11-01 17:24:49 +01:00
vcoppe a163e1b897 New Crowdin updates (#404)
* New translations en.json (Dutch)

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

* New translations files-and-stats.mdx (Chinese Traditional, Hong Kong)

[ci skip]

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

[ci skip]

* New translations gpx.mdx (Dutch)

[ci skip]

* New translations map-controls.mdx (Basque)

[ci skip]

* New translations edit.mdx (Dutch)

[ci skip]
2026-11-01 17:18:56 +01:00
vcoppe 33abec525a remove unused key in locales 2026-11-01 17:00:12 +01:00
vcoppe 77c542d9aa fix windows build continued 2026-11-01 16:52:36 +01:00
vcoppe 17b5c1d567 fix windows build 2026-11-01 16:07:49 +01:00
vcoppe 2048076fe6 tauri build test 2026-11-01 15:52:34 +01:00
vcoppe d7ff7d645b New Crowdin updates (#403)
* New translations edit.mdx (Lithuanian)

[ci skip]

* New translations edit.mdx (Dutch)

[ci skip]

* New translations edit.mdx (Norwegian)

[ci skip]

* New translations edit.mdx (Polish)

[ci skip]

* New translations edit.mdx (Portuguese)

[ci skip]

* New translations edit.mdx (Russian)

[ci skip]

* New translations edit.mdx (Swedish)

[ci skip]

* New translations edit.mdx (Turkish)

[ci skip]

* New translations edit.mdx (Ukrainian)

[ci skip]

* New translations edit.mdx (Chinese Simplified)

[ci skip]

* New translations edit.mdx (Vietnamese)

[ci skip]

* New translations edit.mdx (Portuguese, Brazilian)

[ci skip]

* New translations edit.mdx (Indonesian)

[ci skip]

* New translations edit.mdx (Thai)

[ci skip]

* New translations edit.mdx (Latvian)

[ci skip]

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

[ci skip]

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

[ci skip]

* New translations file.mdx (Romanian)

[ci skip]

* New translations file.mdx (French)

[ci skip]

* New translations file.mdx (Spanish)

[ci skip]

* New translations file.mdx (Belarusian)

[ci skip]

* New translations file.mdx (Catalan)

[ci skip]

* New translations file.mdx (Czech)

[ci skip]

* New translations file.mdx (Danish)

[ci skip]

* New translations file.mdx (German)

[ci skip]

* New translations file.mdx (Greek)

[ci skip]

* New translations file.mdx (Basque)

[ci skip]

* New translations file.mdx (Finnish)

[ci skip]

* New translations file.mdx (Hebrew)

[ci skip]

* New translations file.mdx (Hungarian)

[ci skip]

* New translations file.mdx (Italian)

[ci skip]

* New translations file.mdx (Korean)

[ci skip]

* New translations file.mdx (Lithuanian)

[ci skip]

* New translations file.mdx (Dutch)

[ci skip]

* New translations file.mdx (Norwegian)

[ci skip]

* New translations file.mdx (Polish)

[ci skip]

* New translations file.mdx (Portuguese)

[ci skip]

* New translations file.mdx (Russian)

[ci skip]

* New translations file.mdx (Swedish)

[ci skip]

* New translations file.mdx (Turkish)

[ci skip]

* New translations file.mdx (Ukrainian)

[ci skip]

* New translations file.mdx (Chinese Simplified)

[ci skip]

* New translations file.mdx (Vietnamese)

[ci skip]

* New translations file.mdx (Portuguese, Brazilian)

[ci skip]

* New translations file.mdx (Indonesian)

[ci skip]

* New translations file.mdx (Thai)

[ci skip]

* New translations file.mdx (Latvian)

[ci skip]

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

[ci skip]

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

[ci skip]

* New translations view.mdx (Romanian)

[ci skip]

* New translations view.mdx (French)

[ci skip]

* New translations view.mdx (Spanish)

[ci skip]

* New translations view.mdx (Belarusian)

[ci skip]

* New translations view.mdx (Catalan)

[ci skip]

* New translations view.mdx (Czech)

[ci skip]

* New translations view.mdx (Danish)

[ci skip]

* New translations view.mdx (German)

[ci skip]

* New translations view.mdx (Greek)

[ci skip]

* New translations view.mdx (Basque)

[ci skip]

* New translations view.mdx (Finnish)

[ci skip]

* New translations view.mdx (Hebrew)

[ci skip]

* New translations view.mdx (Hungarian)

[ci skip]

* New translations view.mdx (Italian)

[ci skip]

* New translations view.mdx (Korean)

[ci skip]

* New translations view.mdx (Lithuanian)

[ci skip]

* New translations view.mdx (Dutch)

[ci skip]

* New translations view.mdx (Norwegian)

[ci skip]

* New translations view.mdx (Polish)

[ci skip]

* New translations view.mdx (Portuguese)

[ci skip]

* New translations view.mdx (Russian)

[ci skip]

* New translations view.mdx (Swedish)

[ci skip]

* New translations view.mdx (Turkish)

[ci skip]

* New translations view.mdx (Ukrainian)

[ci skip]

* New translations view.mdx (Chinese Simplified)

[ci skip]

* New translations view.mdx (Vietnamese)

[ci skip]

* New translations view.mdx (Portuguese, Brazilian)

[ci skip]

* New translations view.mdx (Indonesian)

[ci skip]

* New translations view.mdx (Thai)

[ci skip]

* New translations view.mdx (Latvian)

[ci skip]

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

[ci skip]

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

[ci skip]

* New translations extract.mdx (Romanian)

[ci skip]

* New translations extract.mdx (French)

[ci skip]

* New translations extract.mdx (Spanish)

[ci skip]

* New translations extract.mdx (Belarusian)

[ci skip]

* New translations extract.mdx (Catalan)

[ci skip]

* New translations extract.mdx (Czech)

[ci skip]

* New translations extract.mdx (Danish)

[ci skip]

* New translations extract.mdx (German)

[ci skip]

* New translations extract.mdx (Greek)

[ci skip]

* New translations extract.mdx (Basque)

[ci skip]

* New translations extract.mdx (Finnish)

[ci skip]

* New translations extract.mdx (Hebrew)

[ci skip]

* New translations extract.mdx (Hungarian)

[ci skip]

* New translations extract.mdx (Italian)

[ci skip]

* New translations extract.mdx (Korean)

[ci skip]

* New translations extract.mdx (Lithuanian)

[ci skip]

* New translations extract.mdx (Dutch)

[ci skip]

* New translations extract.mdx (Norwegian)

[ci skip]

* New translations extract.mdx (Polish)

[ci skip]

* New translations extract.mdx (Portuguese)

[ci skip]

* New translations extract.mdx (Russian)

[ci skip]

* New translations extract.mdx (Swedish)

[ci skip]

* New translations extract.mdx (Turkish)

[ci skip]

* New translations extract.mdx (Ukrainian)

[ci skip]

* New translations extract.mdx (Chinese Simplified)

[ci skip]

* New translations extract.mdx (Vietnamese)

[ci skip]

* New translations extract.mdx (Portuguese, Brazilian)

[ci skip]

* New translations extract.mdx (Indonesian)

[ci skip]

* New translations extract.mdx (Thai)

[ci skip]

* New translations extract.mdx (Latvian)

[ci skip]

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

[ci skip]

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

[ci skip]

* New translations merge.mdx (Romanian)

[ci skip]

* New translations merge.mdx (French)

[ci skip]

* New translations merge.mdx (Spanish)

[ci skip]

* New translations merge.mdx (Belarusian)

[ci skip]

* New translations merge.mdx (Catalan)

[ci skip]

* New translations merge.mdx (Czech)

[ci skip]

* New translations merge.mdx (Danish)

[ci skip]

* New translations merge.mdx (German)

[ci skip]

* New translations merge.mdx (Greek)

[ci skip]

* New translations merge.mdx (Basque)

[ci skip]

* New translations merge.mdx (Finnish)

[ci skip]

* New translations merge.mdx (Hebrew)

[ci skip]

* New translations merge.mdx (Hungarian)

[ci skip]

* New translations merge.mdx (Italian)

[ci skip]

* New translations merge.mdx (Korean)

[ci skip]

* New translations merge.mdx (Lithuanian)

[ci skip]

* New translations merge.mdx (Dutch)

[ci skip]

* New translations merge.mdx (Norwegian)

[ci skip]

* New translations merge.mdx (Polish)

[ci skip]

* New translations merge.mdx (Portuguese)

[ci skip]

* New translations merge.mdx (Russian)

[ci skip]

* New translations merge.mdx (Swedish)

[ci skip]

* New translations merge.mdx (Turkish)

[ci skip]

* New translations merge.mdx (Ukrainian)

[ci skip]

* New translations merge.mdx (Chinese Simplified)

[ci skip]

* New translations merge.mdx (Vietnamese)

[ci skip]

* New translations merge.mdx (Portuguese, Brazilian)

[ci skip]

* New translations merge.mdx (Indonesian)

[ci skip]

* New translations merge.mdx (Thai)

[ci skip]

* New translations merge.mdx (Latvian)

[ci skip]

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

[ci skip]

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

[ci skip]

* New translations scissors.mdx (Romanian)

[ci skip]

* New translations scissors.mdx (French)

[ci skip]

* New translations scissors.mdx (Spanish)

[ci skip]

* New translations scissors.mdx (Belarusian)

[ci skip]

* New translations scissors.mdx (Catalan)

[ci skip]

* New translations scissors.mdx (Czech)

[ci skip]

* New translations scissors.mdx (Danish)

[ci skip]

* New translations scissors.mdx (German)

[ci skip]

* New translations scissors.mdx (Greek)

[ci skip]

* New translations scissors.mdx (Basque)

[ci skip]

* New translations scissors.mdx (Finnish)

[ci skip]

* New translations scissors.mdx (Hebrew)

[ci skip]

* New translations scissors.mdx (Hungarian)

[ci skip]

* New translations scissors.mdx (Italian)

[ci skip]

* New translations scissors.mdx (Korean)

[ci skip]

* New translations scissors.mdx (Lithuanian)

[ci skip]

* New translations scissors.mdx (Dutch)

[ci skip]

* New translations scissors.mdx (Norwegian)

[ci skip]

* New translations scissors.mdx (Polish)

[ci skip]

* New translations scissors.mdx (Portuguese)

[ci skip]

* New translations scissors.mdx (Russian)

[ci skip]

* New translations scissors.mdx (Swedish)

[ci skip]

* New translations scissors.mdx (Turkish)

[ci skip]

* New translations scissors.mdx (Ukrainian)

[ci skip]

* New translations scissors.mdx (Chinese Simplified)

[ci skip]

* New translations scissors.mdx (Vietnamese)

[ci skip]

* New translations scissors.mdx (Portuguese, Brazilian)

[ci skip]

* New translations scissors.mdx (Indonesian)

[ci skip]

* New translations scissors.mdx (Thai)

[ci skip]

* New translations scissors.mdx (Latvian)

[ci skip]

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

[ci skip]

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

[ci skip]

* New translations en.json (French)

[ci skip]

* New translations edit.mdx (Romanian)

[ci skip]

* New translations edit.mdx (French)

[ci skip]

* New translations edit.mdx (Spanish)

[ci skip]

* New translations edit.mdx (Belarusian)

[ci skip]

* New translations edit.mdx (Catalan)

[ci skip]

* New translations edit.mdx (Czech)

[ci skip]

* New translations edit.mdx (Danish)

[ci skip]

* New translations edit.mdx (German)

[ci skip]

* New translations edit.mdx (Greek)

[ci skip]

* New translations edit.mdx (Basque)

[ci skip]

* New translations edit.mdx (Finnish)

[ci skip]

* New translations edit.mdx (Hebrew)

[ci skip]

* New translations edit.mdx (Hungarian)

[ci skip]

* New translations edit.mdx (Italian)

[ci skip]

* New translations edit.mdx (Korean)

[ci skip]

* New translations edit.mdx (Lithuanian)

[ci skip]

* New translations edit.mdx (Dutch)

[ci skip]

* New translations edit.mdx (Norwegian)

[ci skip]

* New translations edit.mdx (Polish)

[ci skip]

* New translations edit.mdx (Portuguese)

[ci skip]

* New translations edit.mdx (Russian)

[ci skip]

* New translations edit.mdx (Swedish)

[ci skip]

* New translations edit.mdx (Turkish)

[ci skip]

* New translations edit.mdx (Ukrainian)

[ci skip]

* New translations edit.mdx (Chinese Simplified)

[ci skip]

* New translations edit.mdx (Vietnamese)

[ci skip]

* New translations edit.mdx (Portuguese, Brazilian)

[ci skip]

* New translations edit.mdx (Indonesian)

[ci skip]

* New translations edit.mdx (Thai)

[ci skip]

* New translations edit.mdx (Latvian)

[ci skip]

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

[ci skip]

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

[ci skip]

* New translations file.mdx (French)

[ci skip]

* New translations view.mdx (French)

[ci skip]

* New translations extract.mdx (French)

[ci skip]

* New translations merge.mdx (French)

[ci skip]

* New translations scissors.mdx (French)

[ci skip]

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

[ci skip]

* New translations gpx.mdx (French)

[ci skip]

* New translations edit.mdx (Romanian)

[ci skip]

* New translations edit.mdx (French)

[ci skip]

* New translations edit.mdx (Spanish)

[ci skip]

* New translations edit.mdx (Belarusian)

[ci skip]

* New translations edit.mdx (Catalan)

[ci skip]

* New translations edit.mdx (Czech)

[ci skip]

* New translations edit.mdx (Danish)

[ci skip]

* New translations edit.mdx (German)

[ci skip]

* New translations edit.mdx (Greek)

[ci skip]

* New translations edit.mdx (Basque)

[ci skip]

* New translations edit.mdx (Finnish)

[ci skip]

* New translations edit.mdx (Hebrew)

[ci skip]

* New translations edit.mdx (Hungarian)

[ci skip]

* New translations edit.mdx (Italian)

[ci skip]

* New translations edit.mdx (Korean)

[ci skip]

* New translations edit.mdx (Lithuanian)

[ci skip]

* New translations edit.mdx (Dutch)

[ci skip]

* New translations edit.mdx (Norwegian)

[ci skip]

* New translations edit.mdx (Polish)

[ci skip]

* New translations edit.mdx (Portuguese)

[ci skip]

* New translations edit.mdx (Russian)

[ci skip]

* New translations edit.mdx (Swedish)

[ci skip]

* New translations edit.mdx (Turkish)

[ci skip]

* New translations edit.mdx (Ukrainian)

[ci skip]

* New translations edit.mdx (Chinese Simplified)

[ci skip]

* New translations edit.mdx (Vietnamese)

[ci skip]

* New translations edit.mdx (Portuguese, Brazilian)

[ci skip]

* New translations edit.mdx (Indonesian)

[ci skip]

* New translations edit.mdx (Thai)

[ci skip]

* New translations edit.mdx (Latvian)

[ci skip]

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

[ci skip]

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

[ci skip]

* New translations en.json (French)

[ci skip]
2026-11-01 15:41:07 +01:00
vcoppe 262b590bd4 remove note 2026-11-01 15:04:24 +01:00
vcoppe 14bf9cddd2 Merge branch 'dev' of https://github.com/gpxstudio/gpx.studio into dev 2026-11-01 14:51:44 +01:00
vcoppe b9566160e5 rephrasing 2026-11-01 14:51:42 +01:00
vcoppe 0cc9d6f664 New Crowdin updates (#402)
* New translations en.json (Polish)

[ci skip]

* New translations en.json (Romanian)

[ci skip]

* New translations en.json (French)

[ci skip]

* New translations en.json (Spanish)

[ci skip]

* New translations en.json (Belarusian)

[ci skip]

* New translations en.json (Catalan)

[ci skip]

* New translations en.json (Czech)

[ci skip]

* New translations en.json (Danish)

[ci skip]

* New translations en.json (German)

[ci skip]

* New translations en.json (Greek)

[ci skip]

* New translations en.json (Basque)

[ci skip]

* New translations en.json (Finnish)

[ci skip]

* New translations en.json (Hebrew)

[ci skip]

* New translations en.json (Italian)

[ci skip]

* New translations en.json (Korean)

[ci skip]

* New translations en.json (Lithuanian)

[ci skip]

* New translations en.json (Dutch)

[ci skip]

* New translations en.json (Norwegian)

[ci skip]

* New translations en.json (Portuguese)

[ci skip]

* New translations en.json (Russian)

[ci skip]

* New translations en.json (Swedish)

[ci skip]

* New translations en.json (Turkish)

[ci skip]

* New translations en.json (Ukrainian)

[ci skip]

* New translations en.json (Chinese Simplified)

[ci skip]

* New translations en.json (Vietnamese)

[ci skip]

* New translations en.json (Portuguese, Brazilian)

[ci skip]

* New translations en.json (Indonesian)

[ci skip]

* New translations en.json (Thai)

[ci skip]

* New translations en.json (Latvian)

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

* New translations files-and-stats.mdx (Chinese Traditional, Hong Kong)

[ci skip]

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

[ci skip]
2026-10-31 18:53:31 +01:00
vcoppe 1942e0b9e1 New Crowdin updates (#376)
* New translations en.json (Spanish)

[ci skip]

* New translations elevation.mdx (German)

[ci skip]

* New translations integration.mdx (Basque)

[ci skip]

* New translations map-controls.mdx (Basque)

[ci skip]

* New translations view.mdx (Basque)

[ci skip]

* New translations merge.mdx (Basque)

[ci skip]

* New translations elevation.mdx (Basque)

[ci skip]

* New translations en.json (Dutch)

[ci skip]

* New translations elevation.mdx (Dutch)

[ci skip]

* New translations routing.mdx (Russian)

[ci skip]

* New translations files-and-stats.mdx (Chinese Traditional, Hong Kong)

[ci skip]

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

[ci skip]

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

[ci skip]

* New translations en.json (Portuguese)

[ci skip]

* New translations en.json (Portuguese)

[ci skip]

* New translations edit.mdx (Russian)

[ci skip]

* New translations en.json (Portuguese)

[ci skip]

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

[ci skip]

* New translations getting-started.mdx (Portuguese)

[ci skip]

* New translations integration.mdx (Portuguese)

[ci skip]

* New translations map-controls.mdx (Portuguese)

[ci skip]

* New translations file.mdx (Portuguese)

[ci skip]

* New translations view.mdx (Portuguese)

[ci skip]

* New translations clean.mdx (Portuguese)

[ci skip]

* New translations extract.mdx (Portuguese)

[ci skip]

* New translations merge.mdx (Portuguese)

[ci skip]

* New translations minify.mdx (Portuguese)

[ci skip]

* New translations poi.mdx (Portuguese)

[ci skip]

* New translations routing.mdx (Portuguese)

[ci skip]

* New translations scissors.mdx (Portuguese)

[ci skip]

* New translations time.mdx (Portuguese)

[ci skip]

* New translations faq.mdx (Portuguese)

[ci skip]

* New translations elevation.mdx (Portuguese)

[ci skip]

* New translations en.json (Portuguese)

[ci skip]

* New translations en.json (Portuguese)

[ci skip]

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

[ci skip]

* New translations getting-started.mdx (Portuguese)

[ci skip]

* New translations edit.mdx (Portuguese)

[ci skip]

* New translations file.mdx (Russian)

[ci skip]

* New translations settings.mdx (Russian)

[ci skip]

* New translations view.mdx (Russian)

[ci skip]

* New translations settings.mdx (Russian)

[ci skip]

* New translations routing.mdx (Russian)

[ci skip]

* New translations en.json (Hungarian)

[ci skip]

* New translations en.json (Hungarian)

[ci skip]

* New translations edit.mdx (Hungarian)

[ci skip]

* New translations file.mdx (Hungarian)

[ci skip]

* New translations view.mdx (Hungarian)

[ci skip]

* New translations view.mdx (Hungarian)

[ci skip]

* New translations clean.mdx (Hungarian)

[ci skip]

* New translations merge.mdx (Hungarian)

[ci skip]

* New translations elevation.mdx (Hungarian)

[ci skip]

* New translations en.json (Hungarian)

[ci skip]

* New translations merge.mdx (Hungarian)

[ci skip]

* New translations getting-started.mdx (Hungarian)

[ci skip]

* New translations settings.mdx (Hungarian)

[ci skip]

* New translations minify.mdx (Hungarian)

[ci skip]

* New translations routing.mdx (Hungarian)

[ci skip]

* New translations scissors.mdx (Hungarian)

[ci skip]

* New translations faq.mdx (Hungarian)

[ci skip]

* New translations en.json (Hungarian)

[ci skip]

* New translations view.mdx (Hungarian)

[ci skip]

* New translations faq.mdx (Hungarian)

[ci skip]

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

[ci skip]

* New translations gpx.mdx (Hungarian)

[ci skip]

* New translations integration.mdx (Hungarian)

[ci skip]

* New translations map-controls.mdx (Hungarian)

[ci skip]

* New translations en.json (Polish)

[ci skip]

* New translations settings.mdx (Polish)

[ci skip]

* New translations routing.mdx (Polish)

[ci skip]

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

[ci skip]

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

[ci skip]

* New translations gpx.mdx (Chinese Simplified)

[ci skip]

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

[ci skip]

* New translations edit.mdx (Chinese Simplified)

[ci skip]

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

[ci skip]

* New translations settings.mdx (Chinese Simplified)

[ci skip]

* New translations view.mdx (Chinese Simplified)

[ci skip]

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

[ci skip]

* New translations toolbar.mdx (Chinese Simplified)

[ci skip]

* New translations extract.mdx (Chinese Simplified)

[ci skip]

* New translations merge.mdx (Chinese Simplified)

[ci skip]

* New translations minify.mdx (Chinese Simplified)

[ci skip]

* New translations poi.mdx (Chinese Simplified)

[ci skip]

* New translations routing.mdx (Chinese Simplified)

[ci skip]

* New translations scissors.mdx (Chinese Simplified)

[ci skip]

* New translations time.mdx (Chinese Simplified)

[ci skip]

* New translations faq.mdx (Chinese Simplified)

[ci skip]

* New translations elevation.mdx (Chinese Simplified)

[ci skip]
2026-10-31 18:45:38 +01:00
vcoppe 70a24f53b3 silence warning 2026-10-31 18:33:12 +01:00
vcoppe 001246a49e fix tests 2026-10-31 18:25:48 +01:00
vcoppeandvcoppe 8fe7d8d64d Migration to new Rust-based engine (#401)
* wasm first steps

* parsing start

* parsing continued

* progress

* progress

* progress

* progress

* progress

* progress

* progress

* progress

* fixes

* reorganize

* change type

* fix

* progress

* refactoring

* progress

* progress

* progress

* keep style info

* progress

* progress

* remove (copy) in name

* progress

* merge methods into 1

* splice

* progress

* progress

* progress

* cleaning

* progress

* progress

* progress

* visibility

* progress

* progress

* progress

* progress

* progress

* fixes

* progress

* fix markers

* fix

* progress

* progress

* progress

* progress

* progress

* progress

* small fix

* use engine info

* fix popups

* small fix

* progress

* progress

* progress

* clean tool

* handle self closing elements

* reduce data exchanges across boundary

* copy on wasm side to avoid any data corruption

* speed fix

* routing

* refactoring

* reverse

* reverse

* crop

* split

* merge

* progress

* time tool

* fix bounds for waypoints

* reduce

* fix reactivity

* handle corrupt time data

* export

* progress

* remove gpx js package

* remove todo

* update readme

* cleaning

* progress

* fix link fields

* remove unused fields

* refactoring and tests

* import and export of routes, ref #192

* per-chunk categories storage

* cross-tab sync

* outdated state

* refactoring

* reorder methods

* refactoring

* fix

* improve flow

* renaming

* progress

* progress

* fix

* progress

* per file history

* keep original flag

* use history variant

* fix

* progress

* file explorer

* progress

* fix

* sticky folders

* improved sticky folders

* fix sortable

* fix sortable

* fix

* fix tab size

* no file delete on desktop

* fix routing on other file

* fix sorting

* remember language

* open links in browser

* commit missing changes

* avoid selecting on resize

* clarify filename

* explorer actions

* fix folder styling

* drop anywhere in folder

* cleaning

* fixes

* remove focus-ring

* full screen

* small fix

* consistent highlight color

* improved preview

* performance improvements

* sparser data transfers

* sparser data transfers

* show popup once data is loaded

* fix routing control size

* fix

* sparse desktop build

* remove custom ign styles

* use new endpoint for openhikingmap

* strip native menu

* reorder context menu items

* route full support

* route - track conversion

* create route file action

* route export, filter out anchors at high zoom level

* clarify name handling

* move hide & center to view

* routes continued

* update docs

* fix power format, closes #378

* lint fixes

* fix display of additional data with gaps, closes #361

* fix linting issues

* fix import modules

* fix multidrag

* change base to resolve

* fix type errors

* fix layer typings

* fix last type error

* each keys

* improve typings

* deploy to cloudflare

---------

Co-authored-by: vcoppe <vcoppe@hexaly.com>
2026-10-31 18:08:22 +01:00
343 changed files with 4772 additions and 2041 deletions

No files matched your search

+1 -1
View File
@@ -63,5 +63,5 @@ jobs:
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy website/build --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME || 'gpxstudio' }} --branch=${{ github.ref_name }}
command: pages deploy website/build --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME || 'gpxstudio' }} --branch=${{ github.ref_name }} --commit-dirty=true
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
+124
View File
@@ -0,0 +1,124 @@
name: Desktop beta
on:
push:
branches: 'dev'
workflow_dispatch:
concurrency:
group: desktop-beta-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- platform: macos-latest
rust_targets: aarch64-apple-darwin,x86_64-apple-darwin,wasm32-unknown-unknown
args: --target universal-apple-darwin
bundles: target/universal-apple-darwin/release/bundle/dmg/*.dmg
# the oldest supported Ubuntu, so that the AppImage runs on older systems too
- platform: ubuntu-22.04
rust_targets: wasm32-unknown-unknown
args: ''
bundles: |
target/release/bundle/deb/*.deb
target/release/bundle/rpm/*.rpm
target/release/bundle/appimage/*.AppImage
- platform: windows-latest
rust_targets: wasm32-unknown-unknown
args: ''
bundles: |
target/release/bundle/msi/*.msi
target/release/bundle/nsis/*.exe
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install Linux dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: npm
cache-dependency-path: website/package-lock.json
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.rust_targets }}
- name: Cache Rust build
uses: Swatinem/rust-cache@v2
- name: Install wasm-pack
uses: taiki-e/install-action@v2
with:
tool: wasm-pack
# the desktop app does not use it, but the website depends on its package
- name: Build wasm
run: wasm-pack build --target bundler
working-directory: wasm
- name: Install dependencies for website
run: npm install --prefix website
# the key would be readable in the app: it is left empty, so only the MapTiler layer fails
- name: Create env file
run: echo "PUBLIC_MAPTILER_KEY=" >> website/.env
- name: Build app
run: npx --yes @tauri-apps/cli@2 build ${{ matrix.args }}
working-directory: tauri
# after the build, which gives the app the website it embeds
- name: Test engine and app
run: cargo test -p gpx-engine -p gpx-studio-desktop
- name: Upload bundles
uses: actions/upload-artifact@v4
with:
name: bundles-${{ matrix.platform }}
path: ${{ matrix.bundles }}
if-no-files-found: error
# replaces the beta release only once every platform is built
release:
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
steps:
- name: Download bundles
uses: actions/download-artifact@v5
with:
pattern: bundles-*
path: bundles
merge-multiple: true
- name: Publish beta release
# the bundles of a platform are in a folder per format (deb, msi…)
run: |
mapfile -t files < <(find bundles -type f)
gh release delete desktop-beta --cleanup-tag --yes || true
gh release create desktop-beta "${files[@]}" \
--prerelease \
--target ${{ github.sha }} \
--title "Desktop beta" \
--notes "Beta of the desktop app, built from ${{ github.sha }} on the dev branch. The builds are not signed yet: macOS and Windows will warn before opening them."
+9
View File
@@ -7,6 +7,15 @@ edition = "2024"
[lib]
name = "gpx_studio_desktop_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
# the tests are in `tests/app`: on Windows, a test binary of the library or of the app could not
# start without the manifest, which `build.rs` only gives to the binaries of `tests`
test = false
doctest = false
[[bin]]
name = "gpx-studio-desktop"
path = "src/main.rs"
test = false
[dev-dependencies]
tauri = { version = "2", features = ["test"] }
+17
View File
@@ -1,3 +1,20 @@
fn main() {
// On Windows, the dialogs need Common Controls v6, which a binary only gets with a manifest that
// asks for it: tauri-build gives one to the app, but not to the test binaries, which would stop
// with STATUS_ENTRYPOINT_NOT_FOUND before running any test. This gives them the same manifest
// (`-tests` only reaches the binaries of `tests`, hence the tests are there).
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows")
&& std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc")
{
let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("windows-test.manifest");
println!("cargo:rerun-if-changed={}", manifest.display());
println!("cargo:rustc-link-arg-tests=/MANIFEST:EMBED");
println!(
"cargo:rustc-link-arg-tests=/MANIFESTINPUT:{}",
manifest.display()
);
}
tauri_build::build()
}
+6 -9
View File
@@ -3,20 +3,20 @@
mod bytes;
mod commands;
mod state;
mod store;
mod workspace;
pub mod state;
pub mod store;
pub mod workspace;
use commands::*;
use tauri::{Emitter, Manager};
/// The engine of the app: each file has its own history, so that undoing only concerns the
/// selected files, and opening or closing a file is not something to undo.
pub(crate) type Engine = gpx_engine::Engine<gpx_engine::PerFileHistory>;
pub type Engine = gpx_engine::Engine<gpx_engine::PerFileHistory>;
/// The GPX files among what the system gave to the app (the arguments of the command line, when a
/// file is opened with it).
fn gpx_paths(arguments: impl IntoIterator<Item = String>) -> Vec<std::path::PathBuf> {
pub fn gpx_paths(arguments: impl IntoIterator<Item = String>) -> Vec<std::path::PathBuf> {
arguments
.into_iter()
.map(std::path::PathBuf::from)
@@ -108,7 +108,7 @@ fn menu<R: tauri::Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<tauri::me
}
/// Gives the builder the commands that reach the engine (which it has to be given as `AppState`).
fn register_commands<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::Builder<R> {
pub fn register_commands<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::Builder<R> {
builder.invoke_handler(tauri::generate_handler![
// selection
selection::select,
@@ -234,6 +234,3 @@ fn register_commands<R: tauri::Runtime>(builder: tauri::Builder<R>) -> tauri::Bu
system::reveal_path,
])
}
#[cfg(test)]
mod tests;
-7
View File
@@ -280,13 +280,6 @@ impl AppState {
})
}
/// An engine that keeps its data in a directory of its own, and has nobody to tell.
#[cfg(test)]
pub fn temporary() -> Self {
let directory = std::env::temp_dir().join(format!("gpx-studio-{}", uuid::Uuid::new_v4()));
Self::open(directory, Arc::new(|_, _| {})).expect("the storage can be opened")
}
/// Opens files that the system gave to the app, see `Core::open_external`.
pub fn open_external(&self, paths: Vec<PathBuf>) {
self.core(move |core| core.open_external(&paths));
-66
View File
@@ -217,69 +217,3 @@ impl Storage for FileStorage {
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::Engine;
use gpx_engine::{Action, Command, Load, LoadFiles, Persister};
use super::*;
fn directory() -> PathBuf {
std::env::temp_dir().join(format!("gpx-studio-store-{}", Uuid::new_v4()))
}
fn engine_with_a_file() -> Engine {
let mut engine = Engine::default();
let data = std::fs::read("../gpx-rs/data/simple.gpx").unwrap();
engine
.execute(Action::Edit(Command::LoadFiles(LoadFiles {
files: vec![Load {
data: &data,
name: "simple",
}],
})))
.unwrap();
engine
}
#[test]
fn keeps_the_files_and_the_settings_between_sessions() {
let root = directory();
let engine = engine_with_a_file();
let order = engine.order().to_vec();
let storage = FileStorage::open(&root).unwrap();
pollster::block_on(Persister::default().save(&storage, engine.snapshot())).unwrap();
pollster::block_on(storage.put_setting("units", "\"metric\"")).unwrap();
// another session
let storage = FileStorage::open(&root).unwrap();
let restored = pollster::block_on(Persister::default().restore(&storage)).unwrap();
let mut other = Engine::default();
other.restore(restored);
assert_eq!(other.order(), order);
assert_eq!(
other.file_statistics(&order[0]).unwrap().total_distance,
engine.file_statistics(&order[0]).unwrap().total_distance
);
assert_eq!(storage.settings()["units"], "\"metric\"");
fs::remove_dir_all(root).unwrap();
}
#[test]
fn refuses_a_commit_that_is_not_based_on_the_head() {
let root = directory();
let storage = FileStorage::open(&root).unwrap();
let batch = Batch {
order: Some(vec![]),
..Default::default()
};
assert_eq!(pollster::block_on(storage.commit(&batch)), Ok(Revision(1)));
assert_eq!(
pollster::block_on(storage.commit(&batch)),
Err(CommitError::Stale { head: Revision(1) })
);
fs::remove_dir_all(root).unwrap();
}
}
+2 -812
View File
@@ -148,7 +148,7 @@ pub struct Entry {
pub is_dir: bool,
}
fn is_gpx(path: &Path) -> bool {
pub fn is_gpx(path: &Path) -> bool {
path.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| extension.eq_ignore_ascii_case("gpx"))
@@ -156,7 +156,7 @@ fn is_gpx(path: &Path) -> bool {
/// Compares names the way people read them: the numbers in them by their value ("track 2" before
/// "track 10"), the rest regardless of case.
fn natural_cmp(a: &str, b: &str) -> Ordering {
pub fn natural_cmp(a: &str, b: &str) -> Ordering {
let (mut a, mut b) = (a.chars().peekable(), b.chars().peekable());
loop {
let order = match (a.peek().copied(), b.peek().copied()) {
@@ -1040,813 +1040,3 @@ impl Workspace {
})
}
}
#[cfg(test)]
mod tests {
use gpx_engine::SelectMode;
use super::*;
fn gpx(name: &str) -> String {
format!(
r#"<?xml version="1.0"?><gpx version="1.1" creator="test" xmlns="http://www.topografix.com/GPX/1/1"><metadata><name>{name}</name></metadata><trk><name>{name}</name><trkseg><trkpt lat="50.0" lon="4.0"><ele>10</ele></trkpt><trkpt lat="50.1" lon="4.1"><ele>20</ele></trkpt></trkseg></trk></gpx>"#
)
}
/// The GPX files of a folder and of the folders in it.
pub(super) fn gpx_files(folder: &Path) -> Vec<PathBuf> {
let mut found = vec![];
for entry in fs::read_dir(folder).unwrap().flatten() {
let path = entry.path();
if path.is_dir() {
found.extend(gpx_files(&path));
} else if is_gpx(&path) {
found.push(path);
}
}
found.sort();
found
}
struct Fixture {
folder: PathBuf,
engine: Engine,
workspace: Workspace,
}
impl Fixture {
fn new() -> Self {
let folder =
canonical(&std::env::temp_dir()).join(format!("gpx-ws-{}", Uuid::new_v4()));
fs::create_dir_all(&folder).unwrap();
Self {
folder,
engine: Engine::default(),
workspace: Workspace::default(),
}
}
fn write(&self, name: &str, title: &str) -> PathBuf {
let path = self.folder.join(name);
fs::write(&path, gpx(title)).unwrap();
path
}
/// Opens the folder, and all the files that are in it.
fn open_folder(&mut self) {
let folder = self.folder.clone();
self.workspace.open_folder(&mut self.engine, &folder, false);
let files = gpx_files(&folder);
self.workspace.open_files(&mut self.engine, &files, false);
}
fn reconcile(&mut self, changed: Option<&[PathBuf]>) -> Option<api::Outcome> {
self.workspace.reconcile(&mut self.engine, changed)
}
fn save(&mut self, ids: &[Uuid]) {
self.workspace
.save(&mut self.engine, ids, &BTreeMap::new(), None)
.unwrap();
}
fn ids(&self) -> Vec<Uuid> {
self.engine.order().iter().map(|id| id.0).collect()
}
fn id_named(&self, name: &str) -> Uuid {
self.ids()
.into_iter()
.find(|id| self.name(*id) == name)
.unwrap()
}
fn name(&self, id: Uuid) -> String {
self.engine
.file_structure(&FileId(id))
.unwrap()
.name
.clone()
}
/// What the file holds: the name of its track (its own name is the one of the file).
fn content(&self, id: Uuid) -> String {
self.engine.file_structure(&FileId(id)).unwrap().tracks[0]
.info
.name
.clone()
.unwrap_or_default()
}
fn rename(&mut self, id: Uuid, name: &str) {
api::select(&mut self.engine, &[id], SelectMode::Replace);
assert!(!api::metadata(&mut self.engine, name, "").has_error());
}
fn status(&self, id: Uuid) -> FileStatus {
self.workspace
.state(&self.engine)
.files
.remove(&id.to_string())
.unwrap()
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.folder);
}
}
#[test]
fn an_opened_file_is_clean_until_it_is_edited_and_clean_again_once_saved() {
let mut fx = Fixture::new();
let path = fx.write("a.gpx", "a");
fx.workspace
.open_files(&mut fx.engine, std::slice::from_ref(&path), true);
let id = fx.ids()[0];
assert!(!fx.workspace.is_dirty(&fx.engine, id));
assert_eq!(fx.status(id).path.as_deref(), path.to_str());
fx.rename(id, "renamed");
assert!(fx.workspace.is_dirty(&fx.engine, id));
// nothing is written before it is saved
assert!(fs::read_to_string(&path).unwrap().contains(">a<"));
fx.save(&[id]);
assert!(!fx.workspace.is_dirty(&fx.engine, id));
assert!(fs::read_to_string(&path).unwrap().contains(">renamed<"));
// going back before the save is an edit again
api::select(&mut fx.engine, &[id], SelectMode::Replace);
api::undo(&mut fx.engine);
assert!(fx.workspace.is_dirty(&fx.engine, id));
api::redo(&mut fx.engine);
assert!(!fx.workspace.is_dirty(&fx.engine, id));
}
#[test]
fn a_file_that_is_not_on_the_disk_is_unsaved_until_it_is_given_a_place() {
let mut fx = Fixture::new();
api::new_file(&mut fx.engine, "fresh", None, None, None, false);
let id = fx.ids()[0];
assert!(fx.workspace.is_dirty(&fx.engine, id));
assert_eq!(
fx.workspace.unlinked(&fx.engine, &[id]),
[(id, "fresh".to_string())]
);
// nowhere to put it without being told
fx.save(&[id]);
assert!(fx.workspace.is_dirty(&fx.engine, id));
let path = fx.folder.join("fresh.gpx");
let paths = BTreeMap::from([(id, path.clone())]);
fx.workspace
.save(&mut fx.engine, &[id], &paths, None)
.unwrap();
assert!(path.exists());
assert!(!fx.workspace.is_dirty(&fx.engine, id));
assert!(fx.workspace.unlinked(&fx.engine, &[id]).is_empty());
}
#[test]
fn several_files_without_a_place_go_in_a_folder_with_names_that_are_free() {
let mut fx = Fixture::new();
fx.write("fresh.gpx", "already there");
api::new_file(&mut fx.engine, "fresh", None, None, None, false);
api::new_file(&mut fx.engine, "a/b: c", None, None, None, false);
api::new_file(&mut fx.engine, "fresh", None, None, None, false);
let ids = fx.ids();
let folder = fx.folder.clone();
fx.workspace
.save(&mut fx.engine, &ids, &BTreeMap::new(), Some(&folder))
.unwrap();
for name in ["fresh (2).gpx", "fresh (3).gpx", "a_b_ c.gpx"] {
assert!(fx.folder.join(name).exists(), "{name}");
}
assert!(ids.iter().all(|id| !fx.workspace.is_dirty(&fx.engine, *id)));
}
#[test]
fn what_this_app_writes_is_not_a_change_of_the_disk() {
let mut fx = Fixture::new();
let path = fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
api::select(&mut fx.engine, &[id], SelectMode::Replace);
assert!(!api::style(&mut fx.engine, Some("#ff0000"), None, None).has_error());
fx.save(&[id]);
assert!(fx.reconcile(Some(&[path])).is_none());
assert!(!fx.status(id).replaced && !fx.status(id).dirty);
}
#[test]
fn the_folders_of_the_files_outside_the_open_folder_are_watched_too() {
let mut fx = Fixture::new();
let inside = fx.write("in.gpx", "in");
let elsewhere = canonical(&std::env::temp_dir()).join(format!("gpx-ws-{}", Uuid::new_v4()));
fs::create_dir_all(&elsewhere).unwrap();
let (one, two) = (elsewhere.join("one.gpx"), elsewhere.join("two.gpx"));
fs::write(&one, gpx("one")).unwrap();
fs::write(&two, gpx("two")).unwrap();
let folder = fx.folder.clone();
// no folder: only the folder of the files
fx.workspace.open_files(&mut fx.engine, &[one, two], false);
assert_eq!(fx.workspace.watched(), [(elsewhere.clone(), false)]);
// the open folder as a whole, and the file in it needs nothing more
fx.workspace.open_folder(&mut fx.engine, &folder, false);
fx.workspace.open_files(&mut fx.engine, &[inside], false);
assert_eq!(
fx.workspace.watched(),
[(folder, true), (elsewhere.clone(), false)]
);
// closing the files stops it
let ids = fx.ids();
fx.workspace.close_files(&mut fx.engine, &ids, true);
assert_eq!(fx.workspace.watched(), [(fx.folder.clone(), true)]);
fs::remove_dir_all(elsewhere).unwrap();
}
#[test]
fn opening_a_folder_does_not_open_its_files() {
let mut fx = Fixture::new();
fx.write("a.gpx", "a");
let folder = fx.folder.clone();
let outcome = fx.workspace.open_folder(&mut fx.engine, &folder, false);
assert!(!outcome.changed);
assert!(fx.ids().is_empty());
assert_eq!(fx.workspace.folder.as_deref(), Some(folder.as_path()));
}
#[test]
fn folders_are_made_renamed_and_moved_and_the_open_files_follow() {
let mut fx = Fixture::new();
let a = fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
let root = fx.folder.clone();
let path_of = |fx: &Fixture| PathBuf::from(fx.status(id).path.unwrap());
let first = fx.workspace.create_folder(&root, "New folder").unwrap();
let second = fx.workspace.create_folder(&root, "New folder").unwrap();
assert_eq!(first, root.join("New folder"));
assert_eq!(second, root.join("New folder 2"));
fx.workspace.move_paths(&[a], &first).unwrap();
assert_eq!(path_of(&fx), first.join("a.gpx"));
assert!(first.join("a.gpx").exists() && !fx.status(id).dirty);
let trips = fx.workspace.rename_path(&first, "Trips").unwrap();
assert_eq!(path_of(&fx), root.join("Trips/a.gpx"));
// a folder does not go into itself, nor onto something else
assert!(
fx.workspace
.move_paths(std::slice::from_ref(&trips), &trips)
.is_err()
);
assert_eq!(
fx.workspace.rename_path(&trips, "New folder 2"),
Err(NAME_TAKEN.to_owned())
);
fx.workspace.move_paths(&[trips], &second).unwrap();
assert_eq!(path_of(&fx), second.join("Trips/a.gpx"));
// a file keeps its extension
let closed = fx.write("c.gpx", "c");
assert_eq!(
fx.workspace.rename_path(&closed, "d").unwrap(),
root.join("d.gpx")
);
// the open folder and what is not in it are left alone
assert!(fx.workspace.rename_path(&root, "x").is_err());
let elsewhere = canonical(&std::env::temp_dir());
assert!(fx.workspace.rename_path(&elsewhere, "x").is_err());
assert!(fx.workspace.create_folder(&elsewhere, "x").is_err());
}
#[test]
fn deleted_files_are_closed_unless_they_have_edits() {
let mut fx = Fixture::new();
let a = fx.write("a.gpx", "a");
let b = fx.write("b.gpx", "b");
fx.open_folder();
let (id_a, id_b) = (fx.id_named("a"), fx.id_named("b"));
api::select(&mut fx.engine, &[id_b], SelectMode::Replace);
assert!(!api::style(&mut fx.engine, Some("#ff0000"), None, None).has_error());
let outcome = fx
.workspace
.delete_paths(&mut fx.engine, &[a.clone(), b.clone()], |paths| {
paths.iter().for_each(|path| fs::remove_file(path).unwrap());
Ok(())
})
.unwrap();
assert!(outcome.changed);
assert_eq!(fx.ids(), [id_b]);
assert!(!fx.ids().contains(&id_a));
let status = fx.status(id_b);
assert!(status.dirty && status.missing);
assert!(!a.exists() && !b.exists());
}
#[test]
fn the_tree_of_the_folder_shows_folders_first_and_only_gpx_files() {
let mut fx = Fixture::new();
fx.write("b.gpx", "b");
fx.write("A.GPX", "a");
fx.write("b 10.gpx", "b 10");
fx.write("b 2.gpx", "b 2");
fx.write("notes.txt", "not a track");
fx.write(".hidden.gpx", "hidden");
fs::create_dir(fx.folder.join("zoo")).unwrap();
fs::create_dir(fx.folder.join("Alps")).unwrap();
fs::create_dir(fx.folder.join(".git")).unwrap();
fx.write("zoo/c.gpx", "c");
let folder = fx.folder.clone();
fx.workspace.open_folder(&mut fx.engine, &folder, false);
let names = |entries: Vec<Entry>| -> Vec<(String, bool)> {
entries.into_iter().map(|e| (e.name, e.is_dir)).collect()
};
assert_eq!(
names(fx.workspace.list_folder(&folder).unwrap()),
[
("Alps".to_string(), true),
("zoo".to_string(), true),
("A.GPX".to_string(), false),
("b.gpx".to_string(), false),
("b 2.gpx".to_string(), false),
("b 10.gpx".to_string(), false),
]
);
assert_eq!(
names(fx.workspace.list_folder(&folder.join("zoo")).unwrap()),
[("c.gpx".to_string(), false)]
);
// only what is in the open folder can be listed
let elsewhere = canonical(&std::env::temp_dir());
assert!(fx.workspace.list_folder(&elsewhere).is_err());
}
#[test]
fn names_are_sorted_by_the_numbers_they_hold() {
let mut names = vec![
"track 10",
"track 2",
"Track 1",
"track",
"track 02b",
"track 2a",
"b",
"A",
];
names.sort_by(|a, b| natural_cmp(a, b).then_with(|| a.cmp(b)));
assert_eq!(
names,
[
"A",
"b",
"track",
"Track 1",
"track 2",
"track 2a",
"track 02b",
"track 10",
]
);
}
#[test]
fn a_file_without_edits_is_read_again_when_the_disk_changes() {
let mut fx = Fixture::new();
let a = fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
fx.write("a.gpx", "theirs");
let outcome = fx.reconcile(Some(std::slice::from_ref(&a))).unwrap();
assert!(outcome.changed);
// the same file, now the one on the disk
assert_eq!(fx.ids(), [id]);
assert_eq!(fx.content(id), "theirs");
let status = fx.status(id);
assert!(!status.dirty && !status.replaced);
// looking again changes nothing
assert!(fx.reconcile(Some(std::slice::from_ref(&a))).is_none());
}
#[test]
fn a_file_with_edits_is_flagged_when_the_disk_changes_and_not_read_again() {
let mut fx = Fixture::new();
let a = fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
fx.rename(id, "mine");
fx.write("a.gpx", "theirs");
let outcome = fx.reconcile(Some(std::slice::from_ref(&a))).unwrap();
assert!(!outcome.changed);
assert_eq!(fx.name(id), "mine");
let status = fx.status(id);
assert!(status.dirty && status.replaced && !status.missing);
// looking again changes nothing
assert!(fx.reconcile(Some(std::slice::from_ref(&a))).is_none());
// the disk goes back to what was read: only the edits are left
fx.write("a.gpx", "a");
assert!(fx.reconcile(Some(std::slice::from_ref(&a))).is_some());
let status = fx.status(id);
assert!(status.dirty && !status.replaced);
}
#[test]
fn the_user_can_take_the_disk_instead_of_the_edits() {
let mut fx = Fixture::new();
fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
fx.rename(id, "mine");
fx.write("a.gpx", "theirs");
fx.reconcile(None);
assert!(fx.status(id).replaced);
fx.workspace.reload(&mut fx.engine, &[id]).unwrap();
assert_eq!(fx.content(id), "theirs");
let status = fx.status(id);
assert!(!status.dirty && !status.replaced);
// and it is one undo step
api::select(&mut fx.engine, &[id], SelectMode::Replace);
api::undo(&mut fx.engine);
assert_eq!(fx.content(id), "mine");
}
#[test]
fn saving_overwrites_what_replaced_the_file() {
let mut fx = Fixture::new();
let a = fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
fx.rename(id, "mine");
fx.write("a.gpx", "theirs");
fx.reconcile(None);
assert!(fx.status(id).replaced);
fx.save(&[id]);
assert!(fs::read_to_string(a).unwrap().contains(">mine<"));
assert!(!fx.status(id).replaced && !fx.status(id).dirty);
}
#[test]
fn a_file_that_is_gone_from_the_disk_stays_open_and_dirty_until_it_is_saved_again() {
let mut fx = Fixture::new();
let a = fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
fs::remove_file(&a).unwrap();
assert!(fx.reconcile(Some(std::slice::from_ref(&a))).is_some());
assert_eq!(fx.ids(), [id]);
let status = fx.status(id);
assert!(status.dirty && status.missing && status.path.is_some());
// saving puts it back
fx.save(&[id]);
assert!(a.exists());
let status = fx.status(id);
assert!(!status.dirty && !status.missing);
}
#[test]
fn a_file_that_comes_back_different_is_read_again() {
let mut fx = Fixture::new();
let a = fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
fs::remove_file(&a).unwrap();
fx.reconcile(None);
assert!(fx.status(id).missing);
fx.write("a.gpx", "back");
fx.reconcile(None);
assert_eq!(fx.content(id), "back");
let status = fx.status(id);
assert!(!status.dirty && !status.missing);
}
#[test]
fn what_changed_while_the_app_was_closed_is_found_when_it_starts() {
let mut fx = Fixture::new();
let a = fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
fx.write("a.gpx", "changed");
assert!(fx.reconcile(None).is_some());
assert_eq!(fx.content(id), "changed");
fs::remove_file(a).unwrap();
assert!(fx.reconcile(None).is_some());
assert!(fx.status(id).missing);
}
#[test]
fn new_files_made_from_a_file_of_the_folder_are_saved_next_to_it() {
let mut fx = Fixture::new();
fs::create_dir(fx.folder.join("sub")).unwrap();
fx.write("sub/b.gpx", "b");
fx.write("a.gpx", "a");
fx.open_folder();
let b = fx.id_named("b");
let sub = fx.folder.join("sub");
assert_eq!(fx.workspace.source_dir(&[b]), Some(canonical(&sub)));
let before = fx.ids();
api::select(&mut fx.engine, &[b], SelectMode::Replace);
let outcome = api::duplicate(&mut fx.engine);
let copy = fx
.ids()
.into_iter()
.find(|id| !before.contains(id))
.unwrap();
fx.workspace.inherit(outcome.added(), &canonical(&sub));
assert_eq!(fx.status(copy).folder.as_deref(), canonical(&sub).to_str());
assert!(fx.status(copy).path.is_none() && fx.status(copy).dirty);
// nobody needs to be asked where it goes
assert!(fx.workspace.unlinked(&fx.engine, &[copy]).is_empty());
fx.save(&[copy]);
assert!(sub.join("b (2).gpx").exists());
let status = fx.status(copy);
assert!(!status.dirty && status.folder.is_none());
assert_eq!(
status.path.as_deref(),
canonical(&sub.join("b (2).gpx")).to_str()
);
// the copy of the copy is next to it as well
assert_eq!(fx.workspace.source_dir(&[copy]), Some(canonical(&sub)));
}
#[test]
fn a_file_outside_the_open_folder_hands_nothing_down() {
let mut fx = Fixture::new();
let elsewhere = canonical(&std::env::temp_dir()).join(format!("gpx-ws-{}", Uuid::new_v4()));
fs::create_dir_all(&elsewhere).unwrap();
let outside = elsewhere.join("o.gpx");
fs::write(&outside, gpx("o")).unwrap();
let folder = fx.folder.clone();
fx.workspace.open_folder(&mut fx.engine, &folder, false);
fx.workspace
.open_files(&mut fx.engine, std::slice::from_ref(&outside), true);
let id = fx.ids()[0];
assert_eq!(fx.workspace.source_dir(&[id]), None);
// a new file with no source is not linked to a folder by `source_dir`: the caller falls back
fs::remove_dir_all(elsewhere).unwrap();
}
#[test]
fn closing_files_asks_what_to_do_with_the_unsaved_ones() {
let mut fx = Fixture::new();
fx.write("a.gpx", "a");
fx.write("b.gpx", "b");
fx.open_folder();
let (a, b) = (fx.id_named("a"), fx.id_named("b"));
fx.rename(a, "edited");
assert_eq!(fx.workspace.unsaved(&fx.engine, &[a, b]), [a]);
// without a choice what has edits stays
let outcome = fx.workspace.close_files(&mut fx.engine, &[a, b], false);
assert!(outcome.changed);
assert_eq!(fx.ids(), [a]);
// saved first
fx.save(&[a]);
fx.workspace.close_files(&mut fx.engine, &[a], false);
assert!(fx.ids().is_empty());
assert!(
fs::read_to_string(fx.folder.join("a.gpx"))
.unwrap()
.contains("edited")
);
assert!(fx.workspace.links.is_empty());
}
#[test]
fn closing_files_can_discard_their_edits() {
let mut fx = Fixture::new();
fx.write("a.gpx", "a");
fx.open_folder();
let a = fx.ids()[0];
fx.rename(a, "edited");
api::new_file(&mut fx.engine, "fresh", None, None, None, false);
let fresh = fx.id_named("fresh");
fx.workspace.close_files(&mut fx.engine, &[a, fresh], true);
assert!(fx.ids().is_empty());
assert!(
fs::read_to_string(fx.folder.join("a.gpx"))
.unwrap()
.contains(">a<")
);
}
#[test]
fn closing_the_folder_closes_every_file() {
let mut fx = Fixture::new();
fx.write("a.gpx", "a");
fx.open_folder();
api::new_file(&mut fx.engine, "fresh", None, None, None, false);
assert_eq!(fx.ids().len(), 2);
// the new file has nowhere to go: it has to be discarded
let outcome = fx.workspace.close_folder(&mut fx.engine, false);
assert!(outcome.changed);
assert_eq!(fx.ids().len(), 1);
assert!(fx.workspace.folder.is_none());
let outcome = fx.workspace.close_folder(&mut fx.engine, true);
assert!(outcome.changed);
assert!(fx.ids().is_empty());
}
#[test]
fn opening_another_folder_closes_the_first_with_all_its_files() {
let mut fx = Fixture::new();
fx.write("a.gpx", "a");
fx.open_folder();
let a = fx.ids()[0];
fx.rename(a, "edited");
let other = canonical(&std::env::temp_dir()).join(format!("gpx-ws-{}", Uuid::new_v4()));
fs::create_dir_all(&other).unwrap();
// the edits stay, nothing is lost without a choice
fx.workspace.open_folder(&mut fx.engine, &other, false);
assert_eq!(fx.ids(), [a]);
assert_eq!(fx.workspace.folder.as_deref(), Some(other.as_path()));
let first = fx.folder.clone();
fx.workspace.open_folder(&mut fx.engine, &first, true);
assert!(fx.ids().is_empty());
assert!(
fs::read_to_string(fx.folder.join("a.gpx"))
.unwrap()
.contains(">a<")
);
fs::remove_dir_all(other).unwrap();
}
#[test]
fn opening_the_same_folder_again_changes_nothing() {
let mut fx = Fixture::new();
fx.write("a.gpx", "a");
fx.open_folder();
let folder = fx.folder.clone();
let outcome = fx.workspace.open_folder(&mut fx.engine, &folder, true);
assert!(!outcome.changed);
assert_eq!(fx.ids().len(), 1);
}
#[test]
fn a_failed_save_stops_at_the_file_and_leaves_it_dirty() {
let mut fx = Fixture::new();
fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
fx.rename(id, "edited");
// the place of the file is taken by a folder
fs::remove_file(fx.folder.join("a.gpx")).unwrap();
fs::create_dir(fx.folder.join("a.gpx")).unwrap();
let result = fx
.workspace
.save(&mut fx.engine, &[id], &BTreeMap::new(), None);
assert!(result.is_err());
assert!(fx.workspace.is_dirty(&fx.engine, id));
}
#[test]
fn saving_as_gives_the_file_another_place() {
let mut fx = Fixture::new();
let a = fx.write("a.gpx", "a");
fx.write("b.gpx", "b");
fx.open_folder();
let id = fx.id_named("a");
let other = fx.id_named("b");
fx.rename(id, "edited");
let target = fx.folder.join("copy.gpx");
fx.workspace.save_as(&mut fx.engine, id, &target).unwrap();
assert!(fs::read_to_string(&target).unwrap().contains("edited"));
assert!(fs::read_to_string(a).unwrap().contains(">a<"));
let status = fx.status(id);
assert!(!status.dirty);
assert_eq!(status.path.as_deref(), canonical(&target).to_str());
// the place of another open file is not available
let taken = fx.folder.join("b.gpx");
assert!(fx.workspace.save_as(&mut fx.engine, id, &taken).is_err());
assert!(!fx.workspace.is_dirty(&fx.engine, other));
}
}
#[cfg(test)]
mod restart_tests {
use gpx_engine::Persister;
use super::*;
use crate::store::FileStorage;
/// What the app does when it starts again: the engine and the links are read from what was
/// kept.
fn restart(engine: &Engine, workspace: &Workspace, root: &Path) -> (Engine, Workspace) {
let storage = FileStorage::open(root).unwrap();
pollster::block_on(Persister::default().save(&storage, engine.snapshot())).unwrap();
let mut restarted = Engine::default();
let restored = pollster::block_on(Persister::default().restore(&storage)).unwrap();
restarted.restore(restored);
let links = serde_json::to_string(workspace).unwrap();
(restarted, serde_json::from_str(&links).unwrap())
}
fn setup() -> (PathBuf, Engine, Workspace, Vec<Uuid>) {
let root = std::env::temp_dir().join(format!("gpx-restart-{}", Uuid::new_v4()));
let folder = root.join("tracks");
fs::create_dir_all(&folder).unwrap();
for name in ["with_tracks", "with_time", "with_waypoint"] {
let _ = fs::copy(
format!("../gpx-rs/data/{name}.gpx"),
folder.join(format!("{name}.gpx")),
);
}
let mut engine = Engine::default();
let mut workspace = Workspace::default();
workspace.open_folder(&mut engine, &folder, false);
workspace.open_files(&mut engine, &tests::gpx_files(&folder), false);
let ids = engine.order().iter().map(|id| id.0).collect();
(root, engine, workspace, ids)
}
#[test]
fn files_that_were_not_edited_are_not_unsaved_after_a_restart() {
let (root, engine, workspace, ids) = setup();
assert!(ids.len() >= 2);
let (restarted, mut workspace) = restart(&engine, &workspace, &root.join("store"));
// the files are the same, but the engine knows their contents by new identities
assert!(ids.iter().any(|id| workspace.is_dirty(&restarted, *id)));
workspace.verify(&restarted);
assert!(ids.iter().all(|id| !workspace.is_dirty(&restarted, *id)));
fs::remove_dir_all(root).unwrap();
}
#[test]
fn files_that_were_edited_are_still_unsaved_after_a_restart() {
let (root, mut engine, workspace, ids) = setup();
api::select(&mut engine, &[ids[0]], gpx_engine::SelectMode::Replace);
api::metadata(&mut engine, "edited", "");
assert!(workspace.is_dirty(&engine, ids[0]));
let (restarted, mut workspace) = restart(&engine, &workspace, &root.join("store"));
workspace.verify(&restarted);
assert!(workspace.is_dirty(&restarted, ids[0]));
assert!(
ids[1..]
.iter()
.all(|id| !workspace.is_dirty(&restarted, *id))
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn links_made_before_the_content_was_kept_are_checked_against_the_disk() {
let (root, mut engine, mut workspace, ids) = setup();
api::select(&mut engine, &[ids[0]], gpx_engine::SelectMode::Replace);
api::metadata(&mut engine, "edited", "");
for link in workspace.links.values_mut() {
link.export_hash = 0;
}
let (restarted, mut workspace) = restart(&engine, &workspace, &root.join("store"));
workspace.verify(&restarted);
assert!(workspace.is_dirty(&restarted, ids[0]));
assert!(
ids[1..]
.iter()
.all(|id| !workspace.is_dirty(&restarted, *id))
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn the_folders_of_new_files_and_the_old_flag_name_survive_a_restart() {
let (root, engine, mut workspace, ids) = setup();
let folder = workspace.folder.clone().unwrap();
let fresh = Uuid::new_v4();
workspace.inherit(&[fresh.to_string()], &folder);
workspace.links.get_mut(&ids[0]).unwrap().replaced = true;
let json = serde_json::to_string(&workspace).unwrap();
// links written by the previous version had `conflict`, which is the same thing
let json = json.replace("\"replaced\"", "\"conflict\"");
assert!(json.contains("\"conflict\":true"));
let read: Workspace = serde_json::from_str(&json).unwrap();
assert!(read.links[&ids[0]].replaced);
assert_eq!(read.dirs.get(&fresh), Some(&folder));
let _ = engine;
fs::remove_dir_all(root).unwrap();
}
}
@@ -6,12 +6,21 @@ use tauri::test::{INVOKE_KEY, get_ipc_response, mock_builder, mock_context, noop
use tauri::webview::InvokeRequest;
use tauri::{WebviewWindow, ipc::CallbackFn, ipc::InvokeBody, ipc::InvokeResponseBody};
/// The origin of the pages of the app, the one Tauri allows the commands from: Windows has no
/// custom protocols, the pages come from `http://tauri.localhost` there.
const ORIGIN: &str = if cfg!(windows) {
"http://tauri.localhost"
} else {
"tauri://localhost"
};
struct App(WebviewWindow<tauri::test::MockRuntime>);
impl App {
fn new() -> Self {
let app =
crate::register_commands(mock_builder().manage(crate::state::AppState::temporary()))
let app = gpx_studio_desktop_lib::register_commands(
mock_builder().manage(crate::temporary_state()),
)
.build(mock_context(noop_assets()))
.unwrap();
let window = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
@@ -34,7 +43,7 @@ impl App {
cmd: cmd.into(),
callback: CallbackFn(0),
error: CallbackFn(1),
url: "tauri://localhost".parse().unwrap(),
url: ORIGIN.parse().unwrap(),
body,
headers,
invoke_key: INVOKE_KEY.to_string(),
@@ -56,7 +65,7 @@ impl App {
cmd: cmd.into(),
callback: CallbackFn(0),
error: CallbackFn(1),
url: "tauri://localhost".parse().unwrap(),
url: ORIGIN.parse().unwrap(),
body: InvokeBody::Json(args),
headers: Default::default(),
invoke_key: INVOKE_KEY.to_string(),
@@ -311,8 +320,8 @@ fn keeps_settings_for_the_session() {
#[test]
fn the_files_are_still_there_in_the_next_session() {
use crate::state::AppState;
use gpx_engine::api;
use gpx_studio_desktop_lib::state::AppState;
let directory =
std::env::temp_dir().join(format!("gpx-studio-session-{}", uuid::Uuid::new_v4()));
@@ -347,14 +356,14 @@ fn only_gpx_files_that_exist_are_taken_from_what_the_system_gives() {
text.to_string_lossy().into_owned(),
directory.join("missing.gpx").to_string_lossy().into_owned(),
];
assert_eq!(crate::gpx_paths(arguments), [track]);
assert_eq!(gpx_studio_desktop_lib::gpx_paths(arguments), [track]);
std::fs::remove_dir_all(directory).unwrap();
}
#[test]
fn a_file_opened_with_the_app_is_opened_selected_and_announced() {
use crate::state::AppState;
use gpx_engine::api;
use gpx_studio_desktop_lib::state::AppState;
use std::sync::{Arc, Mutex};
let directory = std::env::temp_dir().join(format!("gpx-studio-open-{}", uuid::Uuid::new_v4()));
@@ -441,7 +450,7 @@ fn a_file_outside_the_open_folder_is_read_again_when_the_disk_changes() {
let (events, received) = mpsc::channel();
let directory = std::env::temp_dir().join(format!("gpx-store-{}", uuid::Uuid::new_v4()));
let state = crate::state::AppState::open(
let state = gpx_studio_desktop_lib::state::AppState::open(
directory.clone(),
std::sync::Arc::new(move |name, _| {
let _ = events.send(name.to_string());
@@ -461,8 +470,8 @@ fn a_file_outside_the_open_folder_is_read_again_when_the_disk_changes() {
});
let id = uuid::Uuid::parse_str(&id).unwrap();
// the name comes from the path, which does not change: the content does
let signature = |state: &crate::state::AppState| {
state.core(move |core| crate::workspace::signature(&core.engine, id))
let signature = |state: &gpx_studio_desktop_lib::state::AppState| {
state.core(move |core| gpx_studio_desktop_lib::workspace::signature(&core.engine, id))
};
let before = signature(&state);
@@ -483,7 +492,7 @@ fn a_file_outside_the_open_folder_is_read_again_when_the_disk_changes() {
#[test]
fn files_made_from_a_file_of_the_open_folder_are_written_next_to_it_at_once() {
let state = crate::state::AppState::temporary();
let state = crate::temporary_state();
let folder = std::env::temp_dir().join(format!("gpx-tests-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(folder.join("sub")).unwrap();
std::fs::copy("../gpx-rs/data/simple.gpx", folder.join("sub/track.gpx")).unwrap();
@@ -516,19 +525,23 @@ fn files_made_from_a_file_of_the_open_folder_are_written_next_to_it_at_once() {
.collect();
// the copy is in the subfolder, named after its name (the one in the file, not of the file)
assert!(
paths.iter().any(|path| path.ends_with("sub/simple.gpx")),
paths
.iter()
.any(|path| std::path::Path::new(path).ends_with("sub/simple.gpx")),
"{paths:?}"
);
// the new file went where the selected file is
assert!(
paths.iter().any(|path| path.ends_with("sub/fresh.gpx")),
paths
.iter()
.any(|path| std::path::Path::new(path).ends_with("sub/fresh.gpx")),
"{paths:?}"
);
}
#[test]
fn the_parts_of_a_split_are_numbered_from_one() {
let state = crate::state::AppState::temporary();
let state = crate::temporary_state();
let folder = std::env::temp_dir().join(format!("gpx-tests-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&folder).unwrap();
std::fs::copy("../gpx-rs/data/simple.gpx", folder.join("track.gpx")).unwrap();
@@ -649,7 +662,7 @@ fn a_file_keeps_its_name_and_its_file_is_renamed_apart() {
// the name of another file is not taken
let error = app.error("rename_file", json!({ "fileId": id, "name": "taken" }));
assert_eq!(error, crate::workspace::NAME_TAKEN);
assert_eq!(error, gpx_studio_desktop_lib::workspace::NAME_TAKEN);
assert!(folder.join("moved.gpx").exists());
// a name is one that a file can have
+15
View File
@@ -0,0 +1,15 @@
//! The tests of the desktop app. They are integration tests, in a single binary, rather than unit
//! tests: on Windows, only test binaries can be given the manifest that the app needs to start
//! (see `build.rs`).
mod commands;
mod store;
mod workspace;
use gpx_studio_desktop_lib::state::AppState;
/// An engine that keeps its data in a directory of its own, and has nobody to tell.
fn temporary_state() -> AppState {
let directory = std::env::temp_dir().join(format!("gpx-studio-{}", uuid::Uuid::new_v4()));
AppState::open(directory, std::sync::Arc::new(|_, _| {})).expect("the storage can be opened")
}
+69
View File
@@ -0,0 +1,69 @@
//! The storage of the engine on the file system (`store`).
use std::fs;
use std::path::PathBuf;
use gpx_engine::{
Action, Batch, Command, CommitError, Load, LoadFiles, Persister, Revision, Storage,
};
use gpx_studio_desktop_lib::Engine;
use gpx_studio_desktop_lib::store::FileStorage;
use uuid::Uuid;
fn directory() -> PathBuf {
std::env::temp_dir().join(format!("gpx-studio-store-{}", Uuid::new_v4()))
}
fn engine_with_a_file() -> Engine {
let mut engine = Engine::default();
let data = std::fs::read("../gpx-rs/data/simple.gpx").unwrap();
engine
.execute(Action::Edit(Command::LoadFiles(LoadFiles {
files: vec![Load {
data: &data,
name: "simple",
}],
})))
.unwrap();
engine
}
#[test]
fn keeps_the_files_and_the_settings_between_sessions() {
let root = directory();
let engine = engine_with_a_file();
let order = engine.order().to_vec();
let storage = FileStorage::open(&root).unwrap();
pollster::block_on(Persister::default().save(&storage, engine.snapshot())).unwrap();
pollster::block_on(storage.put_setting("units", "\"metric\"")).unwrap();
// another session
let storage = FileStorage::open(&root).unwrap();
let restored = pollster::block_on(Persister::default().restore(&storage)).unwrap();
let mut other = Engine::default();
other.restore(restored);
assert_eq!(other.order(), order);
assert_eq!(
other.file_statistics(&order[0]).unwrap().total_distance,
engine.file_statistics(&order[0]).unwrap().total_distance
);
assert_eq!(storage.settings()["units"], "\"metric\"");
fs::remove_dir_all(root).unwrap();
}
#[test]
fn refuses_a_commit_that_is_not_based_on_the_head() {
let root = directory();
let storage = FileStorage::open(&root).unwrap();
let batch = Batch {
order: Some(vec![]),
..Default::default()
};
assert_eq!(pollster::block_on(storage.commit(&batch)), Ok(Revision(1)));
assert_eq!(
pollster::block_on(storage.commit(&batch)),
Err(CommitError::Stale { head: Revision(1) })
);
fs::remove_dir_all(root).unwrap();
}
+811
View File
@@ -0,0 +1,811 @@
//! The places of the files of the engine on the disk (`workspace`).
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use gpx_engine::{FileId, SelectMode, api};
use gpx_studio_desktop_lib::Engine;
use gpx_studio_desktop_lib::workspace::*;
use uuid::Uuid;
fn gpx(name: &str) -> String {
format!(
r#"<?xml version="1.0"?><gpx version="1.1" creator="test" xmlns="http://www.topografix.com/GPX/1/1"><metadata><name>{name}</name></metadata><trk><name>{name}</name><trkseg><trkpt lat="50.0" lon="4.0"><ele>10</ele></trkpt><trkpt lat="50.1" lon="4.1"><ele>20</ele></trkpt></trkseg></trk></gpx>"#
)
}
/// The GPX files of a folder and of the folders in it.
pub fn gpx_files(folder: &Path) -> Vec<PathBuf> {
let mut found = vec![];
for entry in fs::read_dir(folder).unwrap().flatten() {
let path = entry.path();
if path.is_dir() {
found.extend(gpx_files(&path));
} else if is_gpx(&path) {
found.push(path);
}
}
found.sort();
found
}
struct Fixture {
folder: PathBuf,
engine: Engine,
workspace: Workspace,
}
impl Fixture {
fn new() -> Self {
let folder = canonical(&std::env::temp_dir()).join(format!("gpx-ws-{}", Uuid::new_v4()));
fs::create_dir_all(&folder).unwrap();
Self {
folder,
engine: Engine::default(),
workspace: Workspace::default(),
}
}
fn write(&self, name: &str, title: &str) -> PathBuf {
let path = self.folder.join(name);
fs::write(&path, gpx(title)).unwrap();
path
}
/// Opens the folder, and all the files that are in it.
fn open_folder(&mut self) {
let folder = self.folder.clone();
self.workspace.open_folder(&mut self.engine, &folder, false);
let files = gpx_files(&folder);
self.workspace.open_files(&mut self.engine, &files, false);
}
fn reconcile(&mut self, changed: Option<&[PathBuf]>) -> Option<api::Outcome> {
self.workspace.reconcile(&mut self.engine, changed)
}
fn save(&mut self, ids: &[Uuid]) {
self.workspace
.save(&mut self.engine, ids, &BTreeMap::new(), None)
.unwrap();
}
fn ids(&self) -> Vec<Uuid> {
self.engine.order().iter().map(|id| id.0).collect()
}
fn id_named(&self, name: &str) -> Uuid {
self.ids()
.into_iter()
.find(|id| self.name(*id) == name)
.unwrap()
}
fn name(&self, id: Uuid) -> String {
self.engine
.file_structure(&FileId(id))
.unwrap()
.name
.clone()
}
/// What the file holds: the name of its track (its own name is the one of the file).
fn content(&self, id: Uuid) -> String {
self.engine.file_structure(&FileId(id)).unwrap().tracks[0]
.info
.name
.clone()
.unwrap_or_default()
}
fn rename(&mut self, id: Uuid, name: &str) {
api::select(&mut self.engine, &[id], SelectMode::Replace);
assert!(!api::metadata(&mut self.engine, name, "").has_error());
}
fn status(&self, id: Uuid) -> FileStatus {
self.workspace
.state(&self.engine)
.files
.remove(&id.to_string())
.unwrap()
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.folder);
}
}
#[test]
fn an_opened_file_is_clean_until_it_is_edited_and_clean_again_once_saved() {
let mut fx = Fixture::new();
let path = fx.write("a.gpx", "a");
fx.workspace
.open_files(&mut fx.engine, std::slice::from_ref(&path), true);
let id = fx.ids()[0];
assert!(!fx.workspace.is_dirty(&fx.engine, id));
assert_eq!(fx.status(id).path.as_deref(), path.to_str());
fx.rename(id, "renamed");
assert!(fx.workspace.is_dirty(&fx.engine, id));
// nothing is written before it is saved
assert!(fs::read_to_string(&path).unwrap().contains(">a<"));
fx.save(&[id]);
assert!(!fx.workspace.is_dirty(&fx.engine, id));
assert!(fs::read_to_string(&path).unwrap().contains(">renamed<"));
// going back before the save is an edit again
api::select(&mut fx.engine, &[id], SelectMode::Replace);
api::undo(&mut fx.engine);
assert!(fx.workspace.is_dirty(&fx.engine, id));
api::redo(&mut fx.engine);
assert!(!fx.workspace.is_dirty(&fx.engine, id));
}
#[test]
fn a_file_that_is_not_on_the_disk_is_unsaved_until_it_is_given_a_place() {
let mut fx = Fixture::new();
api::new_file(&mut fx.engine, "fresh", None, None, None, false);
let id = fx.ids()[0];
assert!(fx.workspace.is_dirty(&fx.engine, id));
assert_eq!(
fx.workspace.unlinked(&fx.engine, &[id]),
[(id, "fresh".to_string())]
);
// nowhere to put it without being told
fx.save(&[id]);
assert!(fx.workspace.is_dirty(&fx.engine, id));
let path = fx.folder.join("fresh.gpx");
let paths = BTreeMap::from([(id, path.clone())]);
fx.workspace
.save(&mut fx.engine, &[id], &paths, None)
.unwrap();
assert!(path.exists());
assert!(!fx.workspace.is_dirty(&fx.engine, id));
assert!(fx.workspace.unlinked(&fx.engine, &[id]).is_empty());
}
#[test]
fn several_files_without_a_place_go_in_a_folder_with_names_that_are_free() {
let mut fx = Fixture::new();
fx.write("fresh.gpx", "already there");
api::new_file(&mut fx.engine, "fresh", None, None, None, false);
api::new_file(&mut fx.engine, "a/b: c", None, None, None, false);
api::new_file(&mut fx.engine, "fresh", None, None, None, false);
let ids = fx.ids();
let folder = fx.folder.clone();
fx.workspace
.save(&mut fx.engine, &ids, &BTreeMap::new(), Some(&folder))
.unwrap();
for name in ["fresh (2).gpx", "fresh (3).gpx", "a_b_ c.gpx"] {
assert!(fx.folder.join(name).exists(), "{name}");
}
assert!(ids.iter().all(|id| !fx.workspace.is_dirty(&fx.engine, *id)));
}
#[test]
fn what_this_app_writes_is_not_a_change_of_the_disk() {
let mut fx = Fixture::new();
let path = fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
api::select(&mut fx.engine, &[id], SelectMode::Replace);
assert!(!api::style(&mut fx.engine, Some("#ff0000"), None, None).has_error());
fx.save(&[id]);
assert!(fx.reconcile(Some(&[path])).is_none());
assert!(!fx.status(id).replaced && !fx.status(id).dirty);
}
#[test]
fn the_folders_of_the_files_outside_the_open_folder_are_watched_too() {
let mut fx = Fixture::new();
let inside = fx.write("in.gpx", "in");
let elsewhere = canonical(&std::env::temp_dir()).join(format!("gpx-ws-{}", Uuid::new_v4()));
fs::create_dir_all(&elsewhere).unwrap();
let (one, two) = (elsewhere.join("one.gpx"), elsewhere.join("two.gpx"));
fs::write(&one, gpx("one")).unwrap();
fs::write(&two, gpx("two")).unwrap();
let folder = fx.folder.clone();
// no folder: only the folder of the files
fx.workspace.open_files(&mut fx.engine, &[one, two], false);
assert_eq!(fx.workspace.watched(), [(elsewhere.clone(), false)]);
// the open folder as a whole, and the file in it needs nothing more
fx.workspace.open_folder(&mut fx.engine, &folder, false);
fx.workspace.open_files(&mut fx.engine, &[inside], false);
assert_eq!(
fx.workspace.watched(),
[(folder, true), (elsewhere.clone(), false)]
);
// closing the files stops it
let ids = fx.ids();
fx.workspace.close_files(&mut fx.engine, &ids, true);
assert_eq!(fx.workspace.watched(), [(fx.folder.clone(), true)]);
fs::remove_dir_all(elsewhere).unwrap();
}
#[test]
fn opening_a_folder_does_not_open_its_files() {
let mut fx = Fixture::new();
fx.write("a.gpx", "a");
let folder = fx.folder.clone();
let outcome = fx.workspace.open_folder(&mut fx.engine, &folder, false);
assert!(!outcome.changed);
assert!(fx.ids().is_empty());
assert_eq!(fx.workspace.folder.as_deref(), Some(folder.as_path()));
}
#[test]
fn folders_are_made_renamed_and_moved_and_the_open_files_follow() {
let mut fx = Fixture::new();
let a = fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
let root = fx.folder.clone();
let path_of = |fx: &Fixture| PathBuf::from(fx.status(id).path.unwrap());
let first = fx.workspace.create_folder(&root, "New folder").unwrap();
let second = fx.workspace.create_folder(&root, "New folder").unwrap();
assert_eq!(first, root.join("New folder"));
assert_eq!(second, root.join("New folder 2"));
fx.workspace.move_paths(&[a], &first).unwrap();
assert_eq!(path_of(&fx), first.join("a.gpx"));
assert!(first.join("a.gpx").exists() && !fx.status(id).dirty);
let trips = fx.workspace.rename_path(&first, "Trips").unwrap();
assert_eq!(path_of(&fx), root.join("Trips/a.gpx"));
// a folder does not go into itself, nor onto something else
assert!(
fx.workspace
.move_paths(std::slice::from_ref(&trips), &trips)
.is_err()
);
assert_eq!(
fx.workspace.rename_path(&trips, "New folder 2"),
Err(NAME_TAKEN.to_owned())
);
fx.workspace.move_paths(&[trips], &second).unwrap();
assert_eq!(path_of(&fx), second.join("Trips/a.gpx"));
// a file keeps its extension
let closed = fx.write("c.gpx", "c");
assert_eq!(
fx.workspace.rename_path(&closed, "d").unwrap(),
root.join("d.gpx")
);
// the open folder and what is not in it are left alone
assert!(fx.workspace.rename_path(&root, "x").is_err());
let elsewhere = canonical(&std::env::temp_dir());
assert!(fx.workspace.rename_path(&elsewhere, "x").is_err());
assert!(fx.workspace.create_folder(&elsewhere, "x").is_err());
}
#[test]
fn deleted_files_are_closed_unless_they_have_edits() {
let mut fx = Fixture::new();
let a = fx.write("a.gpx", "a");
let b = fx.write("b.gpx", "b");
fx.open_folder();
let (id_a, id_b) = (fx.id_named("a"), fx.id_named("b"));
api::select(&mut fx.engine, &[id_b], SelectMode::Replace);
assert!(!api::style(&mut fx.engine, Some("#ff0000"), None, None).has_error());
let outcome = fx
.workspace
.delete_paths(&mut fx.engine, &[a.clone(), b.clone()], |paths| {
paths.iter().for_each(|path| fs::remove_file(path).unwrap());
Ok(())
})
.unwrap();
assert!(outcome.changed);
assert_eq!(fx.ids(), [id_b]);
assert!(!fx.ids().contains(&id_a));
let status = fx.status(id_b);
assert!(status.dirty && status.missing);
assert!(!a.exists() && !b.exists());
}
#[test]
fn the_tree_of_the_folder_shows_folders_first_and_only_gpx_files() {
let mut fx = Fixture::new();
fx.write("b.gpx", "b");
fx.write("A.GPX", "a");
fx.write("b 10.gpx", "b 10");
fx.write("b 2.gpx", "b 2");
fx.write("notes.txt", "not a track");
fx.write(".hidden.gpx", "hidden");
fs::create_dir(fx.folder.join("zoo")).unwrap();
fs::create_dir(fx.folder.join("Alps")).unwrap();
fs::create_dir(fx.folder.join(".git")).unwrap();
fx.write("zoo/c.gpx", "c");
let folder = fx.folder.clone();
fx.workspace.open_folder(&mut fx.engine, &folder, false);
let names = |entries: Vec<Entry>| -> Vec<(String, bool)> {
entries.into_iter().map(|e| (e.name, e.is_dir)).collect()
};
assert_eq!(
names(fx.workspace.list_folder(&folder).unwrap()),
[
("Alps".to_string(), true),
("zoo".to_string(), true),
("A.GPX".to_string(), false),
("b.gpx".to_string(), false),
("b 2.gpx".to_string(), false),
("b 10.gpx".to_string(), false),
]
);
assert_eq!(
names(fx.workspace.list_folder(&folder.join("zoo")).unwrap()),
[("c.gpx".to_string(), false)]
);
// only what is in the open folder can be listed
let elsewhere = canonical(&std::env::temp_dir());
assert!(fx.workspace.list_folder(&elsewhere).is_err());
}
#[test]
fn names_are_sorted_by_the_numbers_they_hold() {
let mut names = vec![
"track 10",
"track 2",
"Track 1",
"track",
"track 02b",
"track 2a",
"b",
"A",
];
names.sort_by(|a, b| natural_cmp(a, b).then_with(|| a.cmp(b)));
assert_eq!(
names,
[
"A",
"b",
"track",
"Track 1",
"track 2",
"track 2a",
"track 02b",
"track 10",
]
);
}
#[test]
fn a_file_without_edits_is_read_again_when_the_disk_changes() {
let mut fx = Fixture::new();
let a = fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
fx.write("a.gpx", "theirs");
let outcome = fx.reconcile(Some(std::slice::from_ref(&a))).unwrap();
assert!(outcome.changed);
// the same file, now the one on the disk
assert_eq!(fx.ids(), [id]);
assert_eq!(fx.content(id), "theirs");
let status = fx.status(id);
assert!(!status.dirty && !status.replaced);
// looking again changes nothing
assert!(fx.reconcile(Some(std::slice::from_ref(&a))).is_none());
}
#[test]
fn a_file_with_edits_is_flagged_when_the_disk_changes_and_not_read_again() {
let mut fx = Fixture::new();
let a = fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
fx.rename(id, "mine");
fx.write("a.gpx", "theirs");
let outcome = fx.reconcile(Some(std::slice::from_ref(&a))).unwrap();
assert!(!outcome.changed);
assert_eq!(fx.name(id), "mine");
let status = fx.status(id);
assert!(status.dirty && status.replaced && !status.missing);
// looking again changes nothing
assert!(fx.reconcile(Some(std::slice::from_ref(&a))).is_none());
// the disk goes back to what was read: only the edits are left
fx.write("a.gpx", "a");
assert!(fx.reconcile(Some(std::slice::from_ref(&a))).is_some());
let status = fx.status(id);
assert!(status.dirty && !status.replaced);
}
#[test]
fn the_user_can_take_the_disk_instead_of_the_edits() {
let mut fx = Fixture::new();
fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
fx.rename(id, "mine");
fx.write("a.gpx", "theirs");
fx.reconcile(None);
assert!(fx.status(id).replaced);
fx.workspace.reload(&mut fx.engine, &[id]).unwrap();
assert_eq!(fx.content(id), "theirs");
let status = fx.status(id);
assert!(!status.dirty && !status.replaced);
// and it is one undo step
api::select(&mut fx.engine, &[id], SelectMode::Replace);
api::undo(&mut fx.engine);
assert_eq!(fx.content(id), "mine");
}
#[test]
fn saving_overwrites_what_replaced_the_file() {
let mut fx = Fixture::new();
let a = fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
fx.rename(id, "mine");
fx.write("a.gpx", "theirs");
fx.reconcile(None);
assert!(fx.status(id).replaced);
fx.save(&[id]);
assert!(fs::read_to_string(a).unwrap().contains(">mine<"));
assert!(!fx.status(id).replaced && !fx.status(id).dirty);
}
#[test]
fn a_file_that_is_gone_from_the_disk_stays_open_and_dirty_until_it_is_saved_again() {
let mut fx = Fixture::new();
let a = fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
fs::remove_file(&a).unwrap();
assert!(fx.reconcile(Some(std::slice::from_ref(&a))).is_some());
assert_eq!(fx.ids(), [id]);
let status = fx.status(id);
assert!(status.dirty && status.missing && status.path.is_some());
// saving puts it back
fx.save(&[id]);
assert!(a.exists());
let status = fx.status(id);
assert!(!status.dirty && !status.missing);
}
#[test]
fn a_file_that_comes_back_different_is_read_again() {
let mut fx = Fixture::new();
let a = fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
fs::remove_file(&a).unwrap();
fx.reconcile(None);
assert!(fx.status(id).missing);
fx.write("a.gpx", "back");
fx.reconcile(None);
assert_eq!(fx.content(id), "back");
let status = fx.status(id);
assert!(!status.dirty && !status.missing);
}
#[test]
fn what_changed_while_the_app_was_closed_is_found_when_it_starts() {
let mut fx = Fixture::new();
let a = fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
fx.write("a.gpx", "changed");
assert!(fx.reconcile(None).is_some());
assert_eq!(fx.content(id), "changed");
fs::remove_file(a).unwrap();
assert!(fx.reconcile(None).is_some());
assert!(fx.status(id).missing);
}
#[test]
fn new_files_made_from_a_file_of_the_folder_are_saved_next_to_it() {
let mut fx = Fixture::new();
fs::create_dir(fx.folder.join("sub")).unwrap();
fx.write("sub/b.gpx", "b");
fx.write("a.gpx", "a");
fx.open_folder();
let b = fx.id_named("b");
let sub = fx.folder.join("sub");
assert_eq!(fx.workspace.source_dir(&[b]), Some(canonical(&sub)));
let before = fx.ids();
api::select(&mut fx.engine, &[b], SelectMode::Replace);
let outcome = api::duplicate(&mut fx.engine);
let copy = fx
.ids()
.into_iter()
.find(|id| !before.contains(id))
.unwrap();
fx.workspace.inherit(outcome.added(), &canonical(&sub));
assert_eq!(fx.status(copy).folder.as_deref(), canonical(&sub).to_str());
assert!(fx.status(copy).path.is_none() && fx.status(copy).dirty);
// nobody needs to be asked where it goes
assert!(fx.workspace.unlinked(&fx.engine, &[copy]).is_empty());
fx.save(&[copy]);
assert!(sub.join("b (2).gpx").exists());
let status = fx.status(copy);
assert!(!status.dirty && status.folder.is_none());
assert_eq!(
status.path.as_deref(),
canonical(&sub.join("b (2).gpx")).to_str()
);
// the copy of the copy is next to it as well
assert_eq!(fx.workspace.source_dir(&[copy]), Some(canonical(&sub)));
}
#[test]
fn a_file_outside_the_open_folder_hands_nothing_down() {
let mut fx = Fixture::new();
let elsewhere = canonical(&std::env::temp_dir()).join(format!("gpx-ws-{}", Uuid::new_v4()));
fs::create_dir_all(&elsewhere).unwrap();
let outside = elsewhere.join("o.gpx");
fs::write(&outside, gpx("o")).unwrap();
let folder = fx.folder.clone();
fx.workspace.open_folder(&mut fx.engine, &folder, false);
fx.workspace
.open_files(&mut fx.engine, std::slice::from_ref(&outside), true);
let id = fx.ids()[0];
assert_eq!(fx.workspace.source_dir(&[id]), None);
// a new file with no source is not linked to a folder by `source_dir`: the caller falls back
fs::remove_dir_all(elsewhere).unwrap();
}
#[test]
fn closing_files_asks_what_to_do_with_the_unsaved_ones() {
let mut fx = Fixture::new();
fx.write("a.gpx", "a");
fx.write("b.gpx", "b");
fx.open_folder();
let (a, b) = (fx.id_named("a"), fx.id_named("b"));
fx.rename(a, "edited");
assert_eq!(fx.workspace.unsaved(&fx.engine, &[a, b]), [a]);
// without a choice what has edits stays
let outcome = fx.workspace.close_files(&mut fx.engine, &[a, b], false);
assert!(outcome.changed);
assert_eq!(fx.ids(), [a]);
// saved first
fx.save(&[a]);
fx.workspace.close_files(&mut fx.engine, &[a], false);
assert!(fx.ids().is_empty());
assert!(
fs::read_to_string(fx.folder.join("a.gpx"))
.unwrap()
.contains("edited")
);
assert!(fx.workspace.links.is_empty());
}
#[test]
fn closing_files_can_discard_their_edits() {
let mut fx = Fixture::new();
fx.write("a.gpx", "a");
fx.open_folder();
let a = fx.ids()[0];
fx.rename(a, "edited");
api::new_file(&mut fx.engine, "fresh", None, None, None, false);
let fresh = fx.id_named("fresh");
fx.workspace.close_files(&mut fx.engine, &[a, fresh], true);
assert!(fx.ids().is_empty());
assert!(
fs::read_to_string(fx.folder.join("a.gpx"))
.unwrap()
.contains(">a<")
);
}
#[test]
fn closing_the_folder_closes_every_file() {
let mut fx = Fixture::new();
fx.write("a.gpx", "a");
fx.open_folder();
api::new_file(&mut fx.engine, "fresh", None, None, None, false);
assert_eq!(fx.ids().len(), 2);
// the new file has nowhere to go: it has to be discarded
let outcome = fx.workspace.close_folder(&mut fx.engine, false);
assert!(outcome.changed);
assert_eq!(fx.ids().len(), 1);
assert!(fx.workspace.folder.is_none());
let outcome = fx.workspace.close_folder(&mut fx.engine, true);
assert!(outcome.changed);
assert!(fx.ids().is_empty());
}
#[test]
fn opening_another_folder_closes_the_first_with_all_its_files() {
let mut fx = Fixture::new();
fx.write("a.gpx", "a");
fx.open_folder();
let a = fx.ids()[0];
fx.rename(a, "edited");
let other = canonical(&std::env::temp_dir()).join(format!("gpx-ws-{}", Uuid::new_v4()));
fs::create_dir_all(&other).unwrap();
// the edits stay, nothing is lost without a choice
fx.workspace.open_folder(&mut fx.engine, &other, false);
assert_eq!(fx.ids(), [a]);
assert_eq!(fx.workspace.folder.as_deref(), Some(other.as_path()));
let first = fx.folder.clone();
fx.workspace.open_folder(&mut fx.engine, &first, true);
assert!(fx.ids().is_empty());
assert!(
fs::read_to_string(fx.folder.join("a.gpx"))
.unwrap()
.contains(">a<")
);
fs::remove_dir_all(other).unwrap();
}
#[test]
fn opening_the_same_folder_again_changes_nothing() {
let mut fx = Fixture::new();
fx.write("a.gpx", "a");
fx.open_folder();
let folder = fx.folder.clone();
let outcome = fx.workspace.open_folder(&mut fx.engine, &folder, true);
assert!(!outcome.changed);
assert_eq!(fx.ids().len(), 1);
}
#[test]
fn a_failed_save_stops_at_the_file_and_leaves_it_dirty() {
let mut fx = Fixture::new();
fx.write("a.gpx", "a");
fx.open_folder();
let id = fx.ids()[0];
fx.rename(id, "edited");
// the place of the file is taken by a folder
fs::remove_file(fx.folder.join("a.gpx")).unwrap();
fs::create_dir(fx.folder.join("a.gpx")).unwrap();
let result = fx
.workspace
.save(&mut fx.engine, &[id], &BTreeMap::new(), None);
assert!(result.is_err());
assert!(fx.workspace.is_dirty(&fx.engine, id));
}
#[test]
fn saving_as_gives_the_file_another_place() {
let mut fx = Fixture::new();
let a = fx.write("a.gpx", "a");
fx.write("b.gpx", "b");
fx.open_folder();
let id = fx.id_named("a");
let other = fx.id_named("b");
fx.rename(id, "edited");
let target = fx.folder.join("copy.gpx");
fx.workspace.save_as(&mut fx.engine, id, &target).unwrap();
assert!(fs::read_to_string(&target).unwrap().contains("edited"));
assert!(fs::read_to_string(a).unwrap().contains(">a<"));
let status = fx.status(id);
assert!(!status.dirty);
assert_eq!(status.path.as_deref(), canonical(&target).to_str());
// the place of another open file is not available
let taken = fx.folder.join("b.gpx");
assert!(fx.workspace.save_as(&mut fx.engine, id, &taken).is_err());
assert!(!fx.workspace.is_dirty(&fx.engine, other));
}
mod restart {
use gpx_engine::Persister;
use super::*;
use gpx_studio_desktop_lib::store::FileStorage;
/// What the app does when it starts again: the engine and the links are read from what was
/// kept.
fn restart(engine: &Engine, workspace: &Workspace, root: &Path) -> (Engine, Workspace) {
let storage = FileStorage::open(root).unwrap();
pollster::block_on(Persister::default().save(&storage, engine.snapshot())).unwrap();
let mut restarted = Engine::default();
let restored = pollster::block_on(Persister::default().restore(&storage)).unwrap();
restarted.restore(restored);
let links = serde_json::to_string(workspace).unwrap();
(restarted, serde_json::from_str(&links).unwrap())
}
fn setup() -> (PathBuf, Engine, Workspace, Vec<Uuid>) {
let root = std::env::temp_dir().join(format!("gpx-restart-{}", Uuid::new_v4()));
let folder = root.join("tracks");
fs::create_dir_all(&folder).unwrap();
for name in ["with_tracks", "with_time", "with_waypoint"] {
let _ = fs::copy(
format!("../gpx-rs/data/{name}.gpx"),
folder.join(format!("{name}.gpx")),
);
}
let mut engine = Engine::default();
let mut workspace = Workspace::default();
workspace.open_folder(&mut engine, &folder, false);
workspace.open_files(&mut engine, &gpx_files(&folder), false);
let ids = engine.order().iter().map(|id| id.0).collect();
(root, engine, workspace, ids)
}
#[test]
fn files_that_were_not_edited_are_not_unsaved_after_a_restart() {
let (root, engine, workspace, ids) = setup();
assert!(ids.len() >= 2);
let (restarted, mut workspace) = restart(&engine, &workspace, &root.join("store"));
// the files are the same, but the engine knows their contents by new identities
assert!(ids.iter().any(|id| workspace.is_dirty(&restarted, *id)));
workspace.verify(&restarted);
assert!(ids.iter().all(|id| !workspace.is_dirty(&restarted, *id)));
fs::remove_dir_all(root).unwrap();
}
#[test]
fn files_that_were_edited_are_still_unsaved_after_a_restart() {
let (root, mut engine, workspace, ids) = setup();
api::select(&mut engine, &[ids[0]], gpx_engine::SelectMode::Replace);
api::metadata(&mut engine, "edited", "");
assert!(workspace.is_dirty(&engine, ids[0]));
let (restarted, mut workspace) = restart(&engine, &workspace, &root.join("store"));
workspace.verify(&restarted);
assert!(workspace.is_dirty(&restarted, ids[0]));
assert!(
ids[1..]
.iter()
.all(|id| !workspace.is_dirty(&restarted, *id))
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn links_made_before_the_content_was_kept_are_checked_against_the_disk() {
let (root, mut engine, mut workspace, ids) = setup();
api::select(&mut engine, &[ids[0]], gpx_engine::SelectMode::Replace);
api::metadata(&mut engine, "edited", "");
for link in workspace.links.values_mut() {
link.export_hash = 0;
}
let (restarted, mut workspace) = restart(&engine, &workspace, &root.join("store"));
workspace.verify(&restarted);
assert!(workspace.is_dirty(&restarted, ids[0]));
assert!(
ids[1..]
.iter()
.all(|id| !workspace.is_dirty(&restarted, *id))
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn the_folders_of_new_files_and_the_old_flag_name_survive_a_restart() {
let (root, engine, mut workspace, ids) = setup();
let folder = workspace.folder.clone().unwrap();
let fresh = Uuid::new_v4();
workspace.inherit(&[fresh.to_string()], &folder);
workspace.links.get_mut(&ids[0]).unwrap().replaced = true;
let json = serde_json::to_string(&workspace).unwrap();
// links written by the previous version had `conflict`, which is the same thing
let json = json.replace("\"replaced\"", "\"conflict\"");
assert!(json.contains("\"conflict\":true"));
let read: Workspace = serde_json::from_str(&json).unwrap();
assert!(read.links[&ids[0]].replaced);
assert_eq!(read.dirs.get(&fresh), Some(&folder));
let _ = engine;
fs::remove_dir_all(root).unwrap();
}
}
+14
View File
@@ -0,0 +1,14 @@
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
</assembly>
+26
View File
@@ -44,6 +44,7 @@
"@typescript-eslint/parser": "^8.33.1",
"bits-ui": "^2.17.2",
"clsx": "^2.1.1",
"cross-env": "^10.1.0",
"eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "^3.9.1",
@@ -354,6 +355,13 @@
"tslib": "^2.4.0"
}
},
"node_modules/@epic-web/invariant": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
"dev": true,
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
@@ -3275,6 +3283,24 @@
"version": "1.0.3",
"license": "MIT"
},
"node_modules/cross-env": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@epic-web/invariant": "^1.0.0",
"cross-spawn": "^7.0.6"
},
"bin": {
"cross-env": "dist/bin/cross-env.js",
"cross-env-shell": "dist/bin/cross-env-shell.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"dev": true,
+3 -1
View File
@@ -5,11 +5,12 @@
"scripts": {
"dev": "vite dev",
"build": "vite build",
"build:desktop": "BUILD_TARGET=desktop BASE_PATH='' vite build",
"build:desktop": "cross-env BUILD_TARGET=desktop BASE_PATH= vite build",
"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",
"pretest": "svelte-kit sync",
"test": "node --import tsx --import ./src/test/register.mjs --experimental-wasm-modules --disable-warning=ExperimentalWarning --test \"src/**/*.test.ts\"",
"generate:example": "node --import tsx src/lib/scripts/example-statistics.ts",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
@@ -38,6 +39,7 @@
"@typescript-eslint/parser": "^8.33.1",
"bits-ui": "^2.17.2",
"clsx": "^2.1.1",
"cross-env": "^10.1.0",
"eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "^3.9.1",
+1 -1
View File
@@ -51,7 +51,7 @@ By right-clicking on a file tab, you can access the same actions as in the [edit
As mentioned in the [view options section](./menu/view), you can switch to a tree layout for the files list.
This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map.
In addition, the file tree view enables you to inspect the [tracks, segments, and points of interest](./gpx) contained inside the files through collapsible sections.
In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](./gpx) contained inside the files through collapsible sections.
You can also apply [edit actions](./menu/edit) and [tools](./toolbar) to internal file items.
Furthermore, you can drag and drop the inner items to reorder them, or move them in the hierarchy or even to another file.
+17 -1
View File
@@ -3,13 +3,15 @@ title: Фармат файла GPX
---
<script>
import { Waypoints, MapPin } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
import { Waypoints, Route, MapPin } from '@lucide/svelte';
</script>
# { title }
<a href="https://www.topografix.com/gpx.asp" target="_blank">Фармат файла GPX</a> - гэта адкрыты стандарт для абмену дадзенымі GPS паміж праграмамі і прыладамі GPS.
Па сутнасці, ён складаецца з серыі кропак GPS, якія кадзіруюць адну або некалькі слядоў GPS, і, па жаданні, некаторыя кропкі цікавасці.
These traces can be stored either as tracks or as routes.
Файлы GPX могуць таксама ўтрымліваць метададзеныя, з якіх палі **імя** і **апісанне** найбольш карысныя для карыстальнікаў.
@@ -27,6 +29,20 @@ title: Фармат файла GPX
У большасці выпадкаў файлы GPX утрымліваюць адзін трэк з адным сегментам.
Аднак іерархія, апісаная вышэй, дазваляе выкарыстоўваць больш складаныя выпадкі, напрыклад, планаваць шматдзённыя паездкі з некалькімі варыянтамі на кожны дзень.
### <Route size="16" class="inline-block" style="margin-bottom: 2px" /> Routes
A **route** is a single continuous path made of a sequence of **route points**.
These are mandatory checkpoints, which correspond to the anchor points of the [routing tool](./toolbar/routing).
The points in between are only a suggested path connecting the route points.
Like a track, a route can have a **name**, a **description**, and **appearance properties**.
<DocsNote>
Nowadays, most apps and GPS devices work perfectly well with tracks, even for navigation.
However, some of them only work with routes: in that case, you can [convert](./menu/edit) your tracks to routes, or [create](./menu/file) a route file from the start.
</DocsNote>
### <MapPin size="16" class="inline-block" style="margin-bottom: 2px" /> Кропкі цікавасці
**Кропкі цікавасці** (тэхнічна званыя _маршрутнымі кропкамі_) уяўляюць цікавыя месцы, якія можна паказаць альбо на прыладзе GPS, альбо на лічбавай карце.
+20 -10
View File
@@ -3,14 +3,14 @@ title: Edit actions
---
<script lang="ts">
import { Undo2, Redo2, Info, PaintBucket, EyeOff, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Maximize, Plus } from '@lucide/svelte';
import { Undo2, Redo2, Info, PaintBucket, ArrowRightLeft, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Plus } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
# { title }
Unlike the file actions, the edit actions can potentially modify the content of the currently selected files.
Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, and points of interest](../gpx).
Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, routes, and points of interest](../gpx).
Therefore, we will refer to the elements that can be modified by these actions as _file items_.
Note that except for the undo and redo actions, the edit actions are also accessible through the context menu (right-click) of the file items.
@@ -27,10 +27,6 @@ Open the information dialog of the currently selected file item, where you can s
Open the appearance dialog, where you can change the color, opacity, and width of the selected file items on the map.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Hide/unhide
Toggle the visibility of the selected file items on the map.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New track
Create a new track in the selected file.
@@ -42,6 +38,17 @@ Additionally, the selection must be a single file.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New route
Create a new route in the selected file.
<DocsNote>
This action is only available when the tree layout of the files list is enabled.
Additionally, the selection must be a single file.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New segment
Create a new segment in the selected track.
@@ -53,14 +60,17 @@ Additionally, the selection must be a single track.
</DocsNote>
### <ArrowRightLeft size="16" class="inline-block" style="margin-bottom: 2px" /> Convert to route/track
Convert the selected routes to tracks, or the selected tracks or segments to routes.
Since a route has a single segment, a track with multiple segments is converted into one route per segment.
When the selected files contain only routes, or only tracks, you can also convert all of them at once.
### <FileStack size="16" class="inline-block" style="margin-bottom: 2px" /> Select all
Add all file items in the current hierarchy level to the selection.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Center
Center the map on the selected file items.
### <ClipboardCopy size="16" class="inline-block" style="margin-bottom: 2px" /> Copy
Copy the selected file items to the clipboard.
+4
View File
@@ -15,6 +15,10 @@ The file actions menu contains a set of pretty self-explanatory file operations.
Create a new empty file.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New route file
Create a new file containing an empty [route](../gpx).
### <FolderOpen size="16" class="inline-block" style="margin-bottom: 2px" /> Open...
Open files from your computer.
+10 -2
View File
@@ -3,7 +3,7 @@ title: View options
---
<script lang="ts">
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, Box, Maximize2 } from '@lucide/svelte';
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, EyeOff, Maximize, Box, Maximize2 } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
@@ -19,7 +19,7 @@ Hide the elevation profile to make room for the map, or show it to inspect the c
Toggle the tree layout for the [file list](../files-and-stats).
This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map.
In addition, the file tree view enables you to inspect the [tracks, segments, and points of interest](../gpx) contained inside the files through collapsible sections.
In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](../gpx) contained inside the files through collapsible sections.
### <Map size="16" class="inline-block" style="margin-bottom: 2px" /> Switch to previous basemap
@@ -38,6 +38,14 @@ They are displayed for the current selection, like the [elevation profile](../fi
Toggle the visibility of direction arrows on the map.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Hide/unhide
Toggle the visibility of the selected file items on the map.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Center
Center the map on the selected file items.
### <Box size="16" class="inline-block" style="margin-bottom: 2px" /> Toggle 3D
Enter or exit the 3D map view.
+1 -1
View File
@@ -16,7 +16,7 @@ This tool allows you to extract [tracks (or segments)](../gpx) from files (or tr
<Extract class="text-foreground p-3 border rounded-md shadow-lg" />
</div>
Applying the tool to a file containing multiple tracks will create a new file for each of the tracks it contains.
Applying the tool to a file containing multiple [tracks or routes](../gpx) will create a new file for each of the tracks and routes it contains.
Similarly, applying the tool to a track containing multiple segments will create (in the same file) a new track for each of the segments it contains.
<DocsNote>
+3 -3
View File
@@ -10,11 +10,11 @@ title: Merge
# <Group size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
To use this tool, you need to [select](../files-and-stats) multiple files, [tracks, or segments](../gpx).
To use this tool, you need to [select](../files-and-stats) multiple files, [tracks, segments or routes](../gpx).
- If your goal is to create a single continuous trace from your selection, use the **Connect the traces** option and validate.
- The second option can be used to create or manage files with multiple [tracks or segments](../gpx).
Merging files (or tracks) will result in a single file (or track) containing all tracks (or segments) from the selection.
- The second option can be used to create or manage files with multiple [tracks, segments or routes](../gpx).
Merging files (or tracks) will result in a single file (or track) containing all tracks and routes (or segments) from the selection.
<DocsNote>
+1 -1
View File
@@ -27,6 +27,6 @@ To split the selected trace into two parts, click on one of the split markers di
To split at a specific point of your choice, hover over the trace on the map.
Scissors will appear at the cursor position, showing that you can split the trace at that point.
You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks or segments](../gpx).
You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks, segments or routes](../gpx).
<DocsImage src="tools/split" alt="Hovering over the selected trace turns your cursor into scissors." />
+1 -1
View File
@@ -51,7 +51,7 @@ Clicant el botó dret en la pestanya d'un arxiu, pots accedir a les mateixes acc
Tal i com es menciona en la [secció d'opcions de vista](./menu/view), pots canviar a un esquema d'arbre per a la llista d'arxius.
Aquesta disposició és ideal per tal d'administrar un gran nombre d'arxius oberts, ja que els organitza en una llista vertical en la part dreta del mapa.
A més, la vista d'arbre d'arxius permet inspeccionar [tracs, segments i punts d'interès](./gpx) continguts dins dels arxius a través de seccions plegables.
In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](./gpx) contained inside the files through collapsible sections.
També pots aplicar [accions d'edició](./menu/edit) i [eines](./toolbar) a elements interns de l'arxiu.
A més, pots arrossegar i deixar anar els elements interns per reordenar-los, o moure'ls en la jerarquia o fins i tot a un altre arxiu.
+17 -1
View File
@@ -3,13 +3,15 @@ title: Format d'arxiu GPX
---
<script>
import { Waypoints, MapPin } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
import { Waypoints, Route, MapPin } from '@lucide/svelte';
</script>
# { title }
El <a href="https://www.topografix.com/gpx.asp" target="_blank">format d'arxiu GPX</a> és un estàndard obert per intercanviar dades GPS entre aplicacions i aparells GPS.
Bàsicament consisteix en una sèrie de punts GPS que codifiquen una o diverses traces GPS i, opcionalment, alguns punts d'interès.
These traces can be stored either as tracks or as routes.
Els arxius GPX poden contenir també metadades, de les quals els camps **nom** i **descripció** són els més útils pels usuaris.
@@ -27,6 +29,20 @@ Aquests estan organitzats en una estructura jeràrquica, amb els tracs en el niv
En la majoria dels casos, els arxius GPX contenen un sol trac amb un sol segment.
Tanmateix, la jerarquia descrita a dalt permet casos d'ús més avançat, com la planificació de viatges de diversos dies amb variants per cada dia.
### <Route size="16" class="inline-block" style="margin-bottom: 2px" /> Routes
A **route** is a single continuous path made of a sequence of **route points**.
These are mandatory checkpoints, which correspond to the anchor points of the [routing tool](./toolbar/routing).
The points in between are only a suggested path connecting the route points.
Like a track, a route can have a **name**, a **description**, and **appearance properties**.
<DocsNote>
Nowadays, most apps and GPS devices work perfectly well with tracks, even for navigation.
However, some of them only work with routes: in that case, you can [convert](./menu/edit) your tracks to routes, or [create](./menu/file) a route file from the start.
</DocsNote>
### <MapPin size="16" class="inline-block" style="margin-bottom: 2px" /> Punts d'interès
**Punts d'interès** (tècnicament anomenats _waypoints_) representen ubicacions d'interès per mostrar en un dispositiu GPS o en mapa digital.
+20 -10
View File
@@ -3,14 +3,14 @@ title: Accions d'edició
---
<script lang="ts">
import { Undo2, Redo2, Info, PaintBucket, EyeOff, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Maximize, Plus } from '@lucide/svelte';
import { Undo2, Redo2, Info, PaintBucket, ArrowRightLeft, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Plus } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
# { title }
A diferència de les accions de l'arxiu, les accions d'edició poden potencialment modificar el contingut dels arxius seleccionats actualment.
A més a més, quan està habilitada la disposició de l'arbre de la llista d'arxius (veure [Arxius i estadístiques](../files-and-stats)), també es poden aplicar a [tracs, segments i punts d'interès](../gpx).
Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, routes, and points of interest](../gpx).
Per tant, ens referirem als elements que poden ser modificats per aquestes accions com a _elements d'arxiu_.
Tingui en compte que excepte per a les accions de desfer i refer, les accions d'edició també són accessibles a través del menú contextual (clic dret) dels elements de l'arxiu.
@@ -27,10 +27,6 @@ Obre el quadre de diàleg d'informació de l'element de l'arxiu seleccionat, on
Obre el diàleg d'aparença, on pots canviar el color, l'opacitat i l'amplada del mapa dels elements de l'arxiu seleccionat.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Amaga/mostra
Alterna la visibilitat dels elements de l'arxiu seleccionat en el mapa.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> Nou trac
Crear nova ruta en l'arxiu seleccionat.
@@ -42,6 +38,17 @@ A més, la selecció ha de ser un únic arxiu.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New route
Create a new route in the selected file.
<DocsNote>
Aquesta acció només està disponible quan està habilitada la disposició de l'arbre de la llista d'arxius.
A més, la selecció ha de ser un únic arxiu.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> Nou segment
Crear un nou segment en el trac seleccionat.
@@ -53,14 +60,17 @@ A més, la selecció ha de ser un trac únic.
</DocsNote>
### <ArrowRightLeft size="16" class="inline-block" style="margin-bottom: 2px" /> Convert to route/track
Convert the selected routes to tracks, or the selected tracks or segments to routes.
Since a route has a single segment, a track with multiple segments is converted into one route per segment.
When the selected files contain only routes, or only tracks, you can also convert all of them at once.
### <FileStack size="16" class="inline-block" style="margin-bottom: 2px" /> Selecciona tot
Afegeix a la selecció tots els elements de l'arxiu en el nivell de jerarquia actual.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Centrar
Centrar el mapa en els elements de l'arxiu seleccionat.
### <ClipboardCopy size="16" class="inline-block" style="margin-bottom: 2px" /> Copia
Copia els elements de l'arxiu seleccionat en el porta-retalls.
+4
View File
@@ -15,6 +15,10 @@ El menú d'accions d'arxiu conté un conjunt d'operacions força autoexplicative
Crea un arxiu nou buit.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New route file
Create a new file containing an empty [route](../gpx).
### <FolderOpen size="16" class="inline-block" style="margin-bottom: 2px" /> Obrir...
Obrir arxius del teu equip.
+10 -2
View File
@@ -3,7 +3,7 @@ title: Opcions de vista
---
<script lang="ts">
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, Box, Maximize2 } from '@lucide/svelte';
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, EyeOff, Maximize, Box, Maximize2 } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
@@ -19,7 +19,7 @@ Amagar el perfil d'elevació per fer espai al mapa o mostra-lo per inspeccionar
Alterna el disseny de l'arbre per la [llista d'arxius](../files-and-stats).
Aquesta disposició és ideal per tal d'administrar un gran nombre d'arxius oberts, ja que els organitza en una llista vertical en la part dreta del mapa.
A més, la vista de l'arbre d'arxius permet inspeccionar [tracs, segments i punts d'interès](../gpx) continguts en els arxius a través de les seccions plegables.
In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](../gpx) contained inside the files through collapsible sections.
### <Map size="16" class="inline-block" style="margin-bottom: 2px" /> Canvia la mapa base anterior
@@ -38,6 +38,14 @@ Es mostren per a la selecció actual, com el [perfil d'elevació](../files-and-s
Alterna la visibilitat de les fletxes de direcció en el mapa.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Hide/unhide
Toggle the visibility of the selected file items on the map.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Center
Center the map on the selected file items.
### <Box size="16" class="inline-block" style="margin-bottom: 2px" /> Alternar 3D
Entra o surt de la vista de mapa en 3D.
+1 -1
View File
@@ -16,7 +16,7 @@ Aquesta eina permet extreure [tracs (o segments)](../gpx) des d'arxius (o tracs)
<Extract class="text-foreground p-3 border rounded-md shadow-lg" />
</div>
Aplicant l'eina a un arxiu que contingui múltiples tracs, crearà un arxiu nou per cada trac que contingui.
Applying the tool to a file containing multiple [tracks or routes](../gpx) will create a new file for each of the tracks and routes it contains.
Igualment, aplicant l'eina a un trac que contingui múltiples segments, crearà (en el mateix arxiu) un nou trac per cada un dels segments que contingui.
<DocsNote>
+3 -3
View File
@@ -10,11 +10,11 @@ title: Fusionar
# <Group size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
Per utilitzar aquesta eina, s'ha de [seleccionar](../files-and-stats) múltiples arxius, [tracs o segments](../gpx).
To use this tool, you need to [select](../files-and-stats) multiple files, [tracks, segments or routes](../gpx).
- Si el teu objectiu és crear una traça contínua a partir de la selecció, utilitza l'opció **Connectar les traces** i valida-ho.
- La segona opció es pot utilitzar per a crear o gestionar arxius amb múltiples [tracs o segments](../gpx).
Fusionar arxius (o tracs) crearà un sol arxiu (o trac) que contindrà tots els tracs (o segments) seleccionats.
- The second option can be used to create or manage files with multiple [tracks, segments or routes](../gpx).
Merging files (or tracks) will result in a single file (or track) containing all tracks and routes (or segments) from the selection.
<DocsNote>
+1 -1
View File
@@ -27,6 +27,6 @@ Per dividir la traça seleccionada en dues parts, clica sobre un dels marcadors
Per dividir en un punt triat específicament, situat sobre la traça del mapa.
Les tisores apareixeran en la posició del cursor, mostrant que pots dividir el traçat en aquest punt.
Pots triar dividir el traçat en dos arxius GPX o mantenir les parts dividides en el mateix arxiu com a [tracs o segments](../gpx).
You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks, segments or routes](../gpx).
<DocsImage src="tools/split" alt="Situar el cursor sobre el traçat seleccionat el converteix en tisores." />
+1 -1
View File
@@ -51,7 +51,7 @@ Kliknutím pravým tlačítkem myši na kartu souboru můžete provést stejné
Jak je uvedeno v části [možnosti zobrazení](./menu/view), můžete přepnout na stromové rozvržení seznamu souborů.
Toto zobrazení je ideální pro správu velkého počtu otevřených souborů, protože je uspořádá do svislého seznamu na pravé straně mapy.
Stromové rozvržení souborů navíc umožňuje zkontrolovat [trasy, úseky a body zájmu](./gpx) obsažené v souborech prostřednictvím sbalitelných sekcí.
In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](./gpx) contained inside the files through collapsible sections.
Rovněž můžete využít [akce úprav](./menu/edit) a [nástroje](./toolbar) na prvky uvnitř souborů.
Navíc přetažením prvků uvnitř souborů můžete změnit jejich pořadí nebo je přesunout v hierarchii či dokonce do jiného souboru.
+17 -1
View File
@@ -3,13 +3,15 @@ title: Formát souboru GPX
---
<script>
import { Waypoints, MapPin } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
import { Waypoints, Route, MapPin } from '@lucide/svelte';
</script>
# { title }
<a href="https://www.topografix.com/gpx.asp" target="_blank">GPX formát</a> je otevřeným standardem pro výměnu GPS dat mezi aplikacemi a GPS zařízeními.
V podstatě se skládá z řady GPS bodů kódujících jednu nebo více GPS stop a volitelně i nějaké body zájmu.
These traces can be stored either as tracks or as routes.
GPX soubory mohou obsahovat také metadata, z nichž pole **název** a **popis** jsou pro uživatele nejužitečnější.
@@ -27,6 +29,20 @@ Ty jsou uspořádány v hierarchické struktuře, přičemž trasy jsou na nejvy
Ve většině případů obsahují GPX soubory pouze jednu trasu s jedním úsekem.
Nicméně výše popsaná hierarchie umožňuje pokročilejší případy použití, například plánování vícedenních výletů s několika variantami pro každý den.
### <Route size="16" class="inline-block" style="margin-bottom: 2px" /> Routes
A **route** is a single continuous path made of a sequence of **route points**.
These are mandatory checkpoints, which correspond to the anchor points of the [routing tool](./toolbar/routing).
The points in between are only a suggested path connecting the route points.
Like a track, a route can have a **name**, a **description**, and **appearance properties**.
<DocsNote>
Nowadays, most apps and GPS devices work perfectly well with tracks, even for navigation.
However, some of them only work with routes: in that case, you can [convert](./menu/edit) your tracks to routes, or [create](./menu/file) a route file from the start.
</DocsNote>
### <MapPin size="16" class="inline-block" style="margin-bottom: 2px" /> Body zájmu
**Body zájmu** (odborně nazývané _waypoints_) představují zajímavá místa, která se zobrazí buď na zařízení GPS, nebo na digitální mapě.
+20 -10
View File
@@ -3,14 +3,14 @@ title: Akce úprav
---
<script lang="ts">
import { Undo2, Redo2, Info, PaintBucket, EyeOff, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Maximize, Plus } from '@lucide/svelte';
import { Undo2, Redo2, Info, PaintBucket, ArrowRightLeft, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Plus } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
# { title }
Na rozdíl od akcí v menu Soubor, mohou akce úprav potenciálně změnit obsah aktuálně vybraných souborů.
Navíc pokud je povoleno rozvržení stromu seznamu souborů (viz [Soubory a statistiky](../files-and-stats)), mohou být také použity na [trasy, segmenty a body zájmu](../gpx).
Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, routes, and points of interest](../gpx).
Proto budeme všechny prvky, které mohou být těmito akcemi upraveny, označovat jako _prvky souborů_.
Všechny akce kromě zpět a opakovat jsou také dostupné z kontextového menu (klik pravým tlačítkem) prvků souborů.
@@ -27,10 +27,6 @@ Otevře informační dialog zvoleného prvku souborů, kde lze zobrazit a změni
Otevře dialog vzhledu, kde můžete změnit barvu, neprůhlednost a tloušťku zvolených prvků souborů v mapě.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Skrýt/zobrazit
Přepnout viditelnost zvolených prvků souborů v mapě.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> Nová trasa
Vytvořit novou trasu ve zvoleném souboru.
@@ -42,6 +38,17 @@ Kromě toho musí být vybrán právě jeden soubor.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New route
Create a new route in the selected file.
<DocsNote>
Tato akce je dostupná pouze v případě, že je povoleno rozvržení stromu seznamu souborů.
Kromě toho musí být vybrán právě jeden soubor.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> Nový úsek
Vytvořit nový úsek na zvolené trase.
@@ -53,14 +60,17 @@ Kromě toho musí být zvolena právě jedna trasa.
</DocsNote>
### <ArrowRightLeft size="16" class="inline-block" style="margin-bottom: 2px" /> Convert to route/track
Convert the selected routes to tracks, or the selected tracks or segments to routes.
Since a route has a single segment, a track with multiple segments is converted into one route per segment.
When the selected files contain only routes, or only tracks, you can also convert all of them at once.
### <FileStack size="16" class="inline-block" style="margin-bottom: 2px" /> Vybrat vše
Přidejte k výběru všechny prvky souborů v dané úrovni hierarchie.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Vycentrovat
Vystředí mapu na označené prvky souborů.
### <ClipboardCopy size="16" class="inline-block" style="margin-bottom: 2px" /> Kopírovat
Zkopíruje zvolené prvky souborů do schránky.
+4
View File
@@ -15,6 +15,10 @@ Menu akce souboru obsahuje sadu poměrně samovysvětlujících operací.
Vytvořit nový prázdný soubor.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New route file
Create a new file containing an empty [route](../gpx).
### <FolderOpen size="16" class="inline-block" style="margin-bottom: 2px" /> Otevřít...
Otevřít soubor z vašeho počítače.
+10 -2
View File
@@ -3,7 +3,7 @@ title: Možnosti zobrazení
---
<script lang="ts">
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, Box, Maximize2 } from '@lucide/svelte';
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, EyeOff, Maximize, Box, Maximize2 } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
@@ -19,7 +19,7 @@ Skrýt výškový profil, aby se vytvořilo místo pro mapu, nebo ho zobrazit a
Přepnout rozvržení stromu pro [seznam souborů](../files-and-stats).
Toto zobrazení je ideální pro správu velkého počtu otevřených souborů, protože je uspořádá do svislého seznamu na pravé straně mapy.
Kromě jiného umožňuje zobrazení stromu souborů zkontrolovat [trasy, segmenty, a body zájmu](../gpx) obsažené uvnitř souborů skrze sbalitelné sekce.
In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](../gpx) contained inside the files through collapsible sections.
### <Map size="16" class="inline-block" style="margin-bottom: 2px" /> Přepnout na předchozí základní mapu
@@ -38,6 +38,14 @@ Zobrazují se pro aktuální výběr, jako je [výškový profil](../files-and-s
Přepnout viditelnost směrových šipek na mapě.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Hide/unhide
Toggle the visibility of the selected file items on the map.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Center
Center the map on the selected file items.
### <Box size="16" class="inline-block" style="margin-bottom: 2px" /> Přepnout 3D
Zapnout nebo vypnout 3D zobrazení mapy.
+1 -1
View File
@@ -16,7 +16,7 @@ Tento nástroj umožňuje extrahovat [trasy (nebo úseky)](../gpx) ze souborů (
<Extract class="text-foreground p-3 border rounded-md shadow-lg" />
</div>
Použití nástroje na soubor obsahující více tras vytvoří nový soubor pro každou z tras, kterou obsahuje.
Applying the tool to a file containing multiple [tracks or routes](../gpx) will create a new file for each of the tracks and routes it contains.
Obdobně použití nástroje na trasu obsahující více úseků vytvoří (ve stejném souboru) novou trasu pro každý úsek, který obsahuje.
<DocsNote>
+3 -3
View File
@@ -10,11 +10,11 @@ title: Sloučit
# <Group size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
Chcete-li použít tento nástroj, musíte [vybrat](../files-and-stats) více souborů, [tras nebo segmentů](../gpx).
To use this tool, you need to [select](../files-and-stats) multiple files, [tracks, segments or routes](../gpx).
- Pokud je vaším cílem vytvořit z vašeho výběru jednu spojitou stopu, použijte možnost **Připojit stopy** a potvrďte.
- Druhá možnost může být použita k vytvoření nebo správě souborů s více [trasami nebo segmenty](../gpx).
Sloučením souborů (nebo tras) vznikne jeden soubor (nebo trasa) obsahující všechny trasy (nebo úseky) z výběru.
- The second option can be used to create or manage files with multiple [tracks, segments or routes](../gpx).
Merging files (or tracks) will result in a single file (or track) containing all tracks and routes (or segments) from the selection.
<DocsNote>
+1 -1
View File
@@ -27,6 +27,6 @@ Chcete-li rozdělit vybranou stopu na dvě části, klikněte na jednu z rozděl
Chcete-li rozdělit stopu v určitém bodě dle vašeho výběru, najeďte nad stopu na mapě.
Na pozici kurzoru se objeví nůžky, což znázorňuje, že v tomto bodě můžete stopu rozdělit.
Můžete zvolit rozdělení stopy na dva GPX soubory, nebo uchovat rozdělené části ve stejném souboru jako [trasy nebo segmenty](../gpx).
You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks, segments or routes](../gpx).
<DocsImage src="tools/split" alt="Najetí kurzorem nad vybranou stopu změní kurzor v nůžky." />
+1 -1
View File
@@ -51,7 +51,7 @@ By right-clicking on a file tab, you can access the same actions as in the [edit
As mentioned in the [view options section](./menu/view), you can switch to a tree layout for the files list.
This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map.
In addition, the file tree view enables you to inspect the [tracks, segments, and points of interest](./gpx) contained inside the files through collapsible sections.
In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](./gpx) contained inside the files through collapsible sections.
You can also apply [edit actions](./menu/edit) and [tools](./toolbar) to internal file items.
Furthermore, you can drag and drop the inner items to reorder them, or move them in the hierarchy or even to another file.
+17 -1
View File
@@ -3,13 +3,15 @@ title: GPX file format
---
<script>
import { Waypoints, MapPin } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
import { Waypoints, Route, MapPin } from '@lucide/svelte';
</script>
# { title }
The <a href="https://www.topografix.com/gpx.asp" target="_blank">GPX file format</a> is an open standard for exchanging GPS data between applications and GPS devices.
It essentially consists of a series of GPS points encoding one or multiple GPS traces, and, optionally, some points of interest.
These traces can be stored either as tracks or as routes.
GPX files may also contain metadata, of which the **name** and **description** fields are the most useful for users.
@@ -27,6 +29,20 @@ These are organized in a hierarchical structure, with tracks at the top level.
In most cases, GPX files contain a single track with a single segment.
However, the hierarchy described above allows for more advanced use cases, such as planning multi-day trips with several variants for each day.
### <Route size="16" class="inline-block" style="margin-bottom: 2px" /> Routes
A **route** is a single continuous path made of a sequence of **route points**.
These are mandatory checkpoints, which correspond to the anchor points of the [routing tool](./toolbar/routing).
The points in between are only a suggested path connecting the route points.
Like a track, a route can have a **name**, a **description**, and **appearance properties**.
<DocsNote>
Nowadays, most apps and GPS devices work perfectly well with tracks, even for navigation.
However, some of them only work with routes: in that case, you can [convert](./menu/edit) your tracks to routes, or [create](./menu/file) a route file from the start.
</DocsNote>
### <MapPin size="16" class="inline-block" style="margin-bottom: 2px" /> Points of interest
**Points of interest** (technically called _waypoints_) represent locations of interest to show either on a GPS device or on a digital map.
+20 -10
View File
@@ -3,14 +3,14 @@ title: Edit actions
---
<script lang="ts">
import { Undo2, Redo2, Info, PaintBucket, EyeOff, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Maximize, Plus } from '@lucide/svelte';
import { Undo2, Redo2, Info, PaintBucket, ArrowRightLeft, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Plus } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
# { title }
Unlike the file actions, the edit actions can potentially modify the content of the currently selected files.
Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, and points of interest](../gpx).
Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, routes, and points of interest](../gpx).
Therefore, we will refer to the elements that can be modified by these actions as _file items_.
Note that except for the undo and redo actions, the edit actions are also accessible through the context menu (right-click) of the file items.
@@ -27,10 +27,6 @@ Open the information dialog of the currently selected file item, where you can s
Open the appearance dialog, where you can change the color, opacity, and width of the selected file items on the map.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Hide/unhide
Toggle the visibility of the selected file items on the map.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New track
Create a new track in the selected file.
@@ -42,6 +38,17 @@ Additionally, the selection must be a single file.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New route
Create a new route in the selected file.
<DocsNote>
This action is only available when the tree layout of the files list is enabled.
Additionally, the selection must be a single file.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New segment
Create a new segment in the selected track.
@@ -53,14 +60,17 @@ Additionally, the selection must be a single track.
</DocsNote>
### <ArrowRightLeft size="16" class="inline-block" style="margin-bottom: 2px" /> Convert to route/track
Convert the selected routes to tracks, or the selected tracks or segments to routes.
Since a route has a single segment, a track with multiple segments is converted into one route per segment.
When the selected files contain only routes, or only tracks, you can also convert all of them at once.
### <FileStack size="16" class="inline-block" style="margin-bottom: 2px" /> Select all
Add all file items in the current hierarchy level to the selection.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Center
Center the map on the selected file items.
### <ClipboardCopy size="16" class="inline-block" style="margin-bottom: 2px" /> Kopier
Copy the selected file items to the clipboard.
+4
View File
@@ -15,6 +15,10 @@ The file actions menu contains a set of pretty self-explanatory file operations.
Create a new empty file.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New route file
Create a new file containing an empty [route](../gpx).
### <FolderOpen size="16" class="inline-block" style="margin-bottom: 2px" /> Open...
Open files from your computer.
+10 -2
View File
@@ -3,7 +3,7 @@ title: View options
---
<script lang="ts">
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, Box, Maximize2 } from '@lucide/svelte';
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, EyeOff, Maximize, Box, Maximize2 } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
@@ -19,7 +19,7 @@ Hide the elevation profile to make room for the map, or show it to inspect the c
Toggle the tree layout for the [file list](../files-and-stats).
This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map.
In addition, the file tree view enables you to inspect the [tracks, segments, and points of interest](../gpx) contained inside the files through collapsible sections.
In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](../gpx) contained inside the files through collapsible sections.
### <Map size="16" class="inline-block" style="margin-bottom: 2px" /> Switch to previous basemap
@@ -38,6 +38,14 @@ They are displayed for the current selection, like the [elevation profile](../fi
Toggle the visibility of direction arrows on the map.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Hide/unhide
Toggle the visibility of the selected file items on the map.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Center
Center the map on the selected file items.
### <Box size="16" class="inline-block" style="margin-bottom: 2px" /> Toggle 3D
Enter or exit the 3D map view.
+1 -1
View File
@@ -16,7 +16,7 @@ This tool allows you to extract [tracks (or segments)](../gpx) from files (or tr
<Extract class="text-foreground p-3 border rounded-md shadow-lg" />
</div>
Applying the tool to a file containing multiple tracks will create a new file for each of the tracks it contains.
Applying the tool to a file containing multiple [tracks or routes](../gpx) will create a new file for each of the tracks and routes it contains.
Similarly, applying the tool to a track containing multiple segments will create (in the same file) a new track for each of the segments it contains.
<DocsNote>
+3 -3
View File
@@ -10,11 +10,11 @@ title: Merge
# <Group size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
To use this tool, you need to [select](../files-and-stats) multiple files, [tracks, or segments](../gpx).
To use this tool, you need to [select](../files-and-stats) multiple files, [tracks, segments or routes](../gpx).
- If your goal is to create a single continuous trace from your selection, use the **Connect the traces** option and validate.
- The second option can be used to create or manage files with multiple [tracks or segments](../gpx).
Merging files (or tracks) will result in a single file (or track) containing all tracks (or segments) from the selection.
- The second option can be used to create or manage files with multiple [tracks, segments or routes](../gpx).
Merging files (or tracks) will result in a single file (or track) containing all tracks and routes (or segments) from the selection.
<DocsNote>
+1 -1
View File
@@ -27,6 +27,6 @@ To split the selected trace into two parts, click on one of the split markers di
To split at a specific point of your choice, hover over the trace on the map.
Scissors will appear at the cursor position, showing that you can split the trace at that point.
You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks or segments](../gpx).
You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks, segments or routes](../gpx).
<DocsImage src="tools/split" alt="Hovering over the selected trace turns your cursor into scissors." />
+1 -1
View File
@@ -51,7 +51,7 @@ Wenn du mit der rechten Maustaste auf einen Datei-Tab klickst, kannst du auf die
Wie schon in den Anzeigeoptionen (/Menü/Ansicht) erwähnt, kann man das Layout des Dateibaums für die Dateienliste anpassen.
Dieses Layout eignet sich am besten um für eine große Anzahl von Dateien, da es diese in einer vertikalen Liste auf der rechten Seite der Karte anordnet.
Darüber hinaus ermöglicht die Dateibaumansicht die Prüfung der [Tracks, Segmente, und Punkte von Interesse](./gpx), die in den Dateien durch zusammenklappbare Abschnitte enthalten sind.
In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](./gpx) contained inside the files through collapsible sections.
Du kannst auch [Bearbeiten von Aktionen](./menu/edit) und [tools](./toolbar) auf interne Dateielemente anwenden.
Außerdem können Sie die inneren Elemente per Drag & Drop verschieben, um sie neu zu ordnen oder in die Hierarchie oder sogar in eine andere Datei zu verschieben.
+17 -1
View File
@@ -3,13 +3,15 @@ title: GPX Dateiformat
---
<script>
import { Waypoints, MapPin } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
import { Waypoints, Route, MapPin } from '@lucide/svelte';
</script>
# { title }
Das <a href="https://www.topografix.com/gpx.asp" target="_blank">GPX-Dateiformat</a> ist ein offener Standard für den Austausch von GPS-Daten zwischen Anwendungen und GPS-Geräten.
Es besteht im Wesentlichen aus einer Serie von GPS-Punkten, die einen oder mehrere GPS-Spuren kodieren, und optional aus einigen interessanten Punkten.
These traces can be stored either as tracks or as routes.
GPX-Dateien können auch Metadaten enthalten, von denen die Felder **Name** und **Beschreibung** für Benutzer am nützlichsten sind.
@@ -27,6 +29,20 @@ Diese sind in einer hierarchischen Struktur mit Spuren auf der obersten Ebene or
In den meisten Fällen enthalten GPX-Dateien einen einzigen Track mit einem Segment.
Die oben beschriebene Hierarchie erlaubt jedoch weitergehende Anwendungsfälle, wie zum Beispiel die Planung von Mehrtagestouren mit mehreren Varianten pro Tag.
### <Route size="16" class="inline-block" style="margin-bottom: 2px" /> Routes
A **route** is a single continuous path made of a sequence of **route points**.
These are mandatory checkpoints, which correspond to the anchor points of the [routing tool](./toolbar/routing).
The points in between are only a suggested path connecting the route points.
Like a track, a route can have a **name**, a **description**, and **appearance properties**.
<DocsNote>
Nowadays, most apps and GPS devices work perfectly well with tracks, even for navigation.
However, some of them only work with routes: in that case, you can [convert](./menu/edit) your tracks to routes, or [create](./menu/file) a route file from the start.
</DocsNote>
### <MapPin size="16" class="inline-block" style="margin-bottom: 2px" /> Punkte von Interesse
**Points of Interessive** (technisch genannt _Wegpunkte_) stellen Orte dar, die auf einem GPS-Gerät oder auf einer digitalen Karte angezeigt werden.
+20 -10
View File
@@ -3,14 +3,14 @@ title: Bearbeite Aktionen
---
<script lang="ts">
import { Undo2, Redo2, Info, PaintBucket, EyeOff, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Maximize, Plus } from '@lucide/svelte';
import { Undo2, Redo2, Info, PaintBucket, ArrowRightLeft, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Plus } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
# { title }
Im Gegensatz zu den Datei-Aktionen können die Bearbeitungs-Aktionen möglicherweise den Inhalt der aktuell ausgewählten Dateien verändern.
Wenn das vertikale Layout der Dateiliste aktiviert ist (siehe [Dateien und Statistik](.. /files-and-stats)), können dieses auch auf [Tracks, Segmente und Points-of-Interest](../gpx) angewendet werden.
Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, routes, and points of interest](../gpx).
Daher bezeichnen wir die Elemente, die durch diese Aktionen geändert werden können, als _file items_.
Beachte, dass die Bearbeitungsaktionen auch über das Kontextmenü (Rechtsklick) der Dateielemente zugänglich sind, ausgenommen von "rückgängig" und "wiederholen".
@@ -27,10 +27,6 @@ Dies gilt für alle Aktionen der Bedienoberfläche, aber nicht für Optionen, An
Öffnen Sie das Dialogfeld Darstellung, in der Sie die Farbe, Deckkraft und Breite der ausgewählten Dateielemente auf der Karte ändern können.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Einblenden/Ausblenden
Konfigurieren Sie die Sichtbarkeit der ausgewählten Dateielemente auf der Karte.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> Neuer Track
Erstelle ein neues Segment im ausgewählten Track.
@@ -42,6 +38,17 @@ Außerdem muss die Auswahl aus einem einzelnen Track bestehen.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New route
Create a new route in the selected file.
<DocsNote>
Diese Aktion ist nur verfügbar, wenn das vertikale Layout der Dateiliste aktiviert ist.
Außerdem muss die Auswahl aus einem einzelnen Track bestehen.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> Neues Segment
Erstelle ein neues Segment im ausgewählten Track.
@@ -53,14 +60,17 @@ Außerdem muss die Auswahl aus einem einzelnen Track bestehen.
</DocsNote>
### <ArrowRightLeft size="16" class="inline-block" style="margin-bottom: 2px" /> Convert to route/track
Convert the selected routes to tracks, or the selected tracks or segments to routes.
Since a route has a single segment, a track with multiple segments is converted into one route per segment.
When the selected files contain only routes, or only tracks, you can also convert all of them at once.
### <FileStack size="16" class="inline-block" style="margin-bottom: 2px" /> Alle auswählen
Alle Dateielemente in der aktuellen Hierarchieebene zur Auswahl hinzufügen.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Zentrieren
Zentrieren der Karte auf den ausgewählten Dateielementen.
### <ClipboardCopy size="16" class="inline-block" style="margin-bottom: 2px" /> Kopieren
Kopieren Sie die ausgewählten Dateielemente in die Zwischenablage.
+4
View File
@@ -15,6 +15,10 @@ Das Aktionsmenü Datei enthält eine Reihe von selbsterklärenden Dateioperation
Neue leere Datei erstellen.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New route file
Create a new file containing an empty [route](../gpx).
### <FolderOpen size="16" class="inline-block" style="margin-bottom: 2px" /> Öffnen...
Öffne Dateien von Ihrem Computer.
+10 -2
View File
@@ -3,7 +3,7 @@ title: Anzeigeoptionen
---
<script lang="ts">
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, Box, Maximize2 } from '@lucide/svelte';
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, EyeOff, Maximize, Box, Maximize2 } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
@@ -19,7 +19,7 @@ Das Höhenprofil ausblenden, um Platz für die Karte zu schaffen, oder um die ak
Umschalten des vertikalen Layouts für die [Dateiliste](../files-and-stats).
Dieses Layout eignet sich am besten um für eine große Anzahl von Dateien, da es diese in einer vertikalen Liste auf der rechten Seite der Karte anordnet.
Darüber hinaus ermöglicht die Dateibaumansicht die Prüfung der [Tracks, Segmente, und Punkte von Interesse](../gpx), die in den Dateien durch zusammenklappbare Abschnitte enthalten sind.
In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](../gpx) contained inside the files through collapsible sections.
### <Map size="16" class="inline-block" style="margin-bottom: 2px" /> Wechseln zur vorherigen Basiskarte
@@ -38,6 +38,14 @@ Sie werden für die aktuelle Auswahl angezeigt, wie in [elevation profile](../fi
Schaltet die Sichtbarkeit der Richtungspfeile auf der Karte um.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Hide/unhide
Toggle the visibility of the selected file items on the map.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Center
Center the map on the selected file items.
### <Box size="16" class="inline-block" style="margin-bottom: 2px" /> 3D Ein/Aus
Aufrufen oder Beenden der 3D-Kartenansicht.
@@ -19,6 +19,6 @@ Mit diesem Tool kannst du Höhendaten zu Routen und [Points of Interest] (../gpx
<DocsNote>
Elevation data is provided by <a href="https://mapterhorn.com" target="_blank">Mapterhorn</a>.
You can learn more about its origin and accuracy in the <a href="https://mapterhorn.com/attribution/" target="_blank">documentation</a>.
Mehr über die Herkunft und Genauigkeit erfährst du in der <a href="https://mapterhorn.com/attribution/" target="_blank">Dokumentation</a>.
</DocsNote>
+1 -1
View File
@@ -16,7 +16,7 @@ Dieses Werkzeug ermöglicht Ihnen [Tracks (oder Segmente)](../gpx) von Dateien (
<Extract class="text-foreground p-3 border rounded-md shadow-lg" />
</div>
Wenn Sie das Werkzeug auf eine Datei mit mehreren enthaltenen Tracks anwenden, wird für jeden der darin enthaltenen Tracks eine neue Datei erstellt.
Applying the tool to a file containing multiple [tracks or routes](../gpx) will create a new file for each of the tracks and routes it contains.
Gleichermaßen gilt: wenn du das Werkzeug auf einen Track anwendest, der mehrere Segmente enthält, wird (in derselben Datei) ein neuer Track für jedes der darin enthaltenen Segmente erstellt.
<DocsNote>
+3 -3
View File
@@ -10,11 +10,11 @@ title: Zusammenfügen
# <Group size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
Um dieses Tool nutzen zu können, musst du [select](../files-and-stats) mehrere Dateien, [Tracks oder Segmente](../gpx).
To use this tool, you need to [select](../files-and-stats) multiple files, [tracks, segments or routes](../gpx).
- Wenn Ihr Ziel darin besteht, eine einzige kontinuierliche Spur aus Ihrer Auswahl zu erstellen, verwenden Sie die Option **Verbinden Sie die Spuren** und validieren Sie sie.
- Die zweite Option kann verwendet werden, um Dateien mit mehreren [Tracks oder Segments](../gpx) zu erstellen oder zu verwalten.
Das Zusammenführen von Dateien (oder Tracks) führt zu einer einzigen Datei (oder Track), die alle Tracks (oder Segmente) aus der Auswahl enthält.
- The second option can be used to create or manage files with multiple [tracks, segments or routes](../gpx).
Merging files (or tracks) will result in a single file (or track) containing all tracks and routes (or segments) from the selection.
<DocsNote>
+1 -1
View File
@@ -27,6 +27,6 @@ Um die ausgewählte Spur in zwei Teile aufzuteilen, klicken Sie auf einen der ge
Um sich an einem bestimmten Punkt Ihrer Wahl zu teilen, fahren Sie über die Spur auf der Karte.
Schere werden an der Cursorposition angezeigt, so dass Sie die Spur an diesem Punkt teilen können.
Du kannst die Trace in zwei GPX-Dateien aufteilen oder die Teile in der gleichen Datei wie [Tracks oder Segmente](../gpx).
You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks, segments or routes](../gpx).
<DocsImage src="tools/split" alt="Über dem ausgewählten Pfad schweben verwandelt den Cursor in Scheren." />
+1 -1
View File
@@ -51,7 +51,7 @@ By right-clicking on a file tab, you can access the same actions as in the [edit
As mentioned in the [view options section](./menu/view), you can switch to a tree layout for the files list.
This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map.
In addition, the file tree view enables you to inspect the [tracks, segments, and points of interest](./gpx) contained inside the files through collapsible sections.
In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](./gpx) contained inside the files through collapsible sections.
You can also apply [edit actions](./menu/edit) and [tools](./toolbar) to internal file items.
Furthermore, you can drag and drop the inner items to reorder them, or move them in the hierarchy or even to another file.
+17 -1
View File
@@ -3,13 +3,15 @@ title: GPX file format
---
<script>
import { Waypoints, MapPin } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
import { Waypoints, Route, MapPin } from '@lucide/svelte';
</script>
# { title }
The <a href="https://www.topografix.com/gpx.asp" target="_blank">GPX file format</a> is an open standard for exchanging GPS data between applications and GPS devices.
It essentially consists of a series of GPS points encoding one or multiple GPS traces, and, optionally, some points of interest.
These traces can be stored either as tracks or as routes.
GPX files may also contain metadata, of which the **name** and **description** fields are the most useful for users.
@@ -27,6 +29,20 @@ These are organized in a hierarchical structure, with tracks at the top level.
In most cases, GPX files contain a single track with a single segment.
However, the hierarchy described above allows for more advanced use cases, such as planning multi-day trips with several variants for each day.
### <Route size="16" class="inline-block" style="margin-bottom: 2px" /> Routes
A **route** is a single continuous path made of a sequence of **route points**.
These are mandatory checkpoints, which correspond to the anchor points of the [routing tool](./toolbar/routing).
The points in between are only a suggested path connecting the route points.
Like a track, a route can have a **name**, a **description**, and **appearance properties**.
<DocsNote>
Nowadays, most apps and GPS devices work perfectly well with tracks, even for navigation.
However, some of them only work with routes: in that case, you can [convert](./menu/edit) your tracks to routes, or [create](./menu/file) a route file from the start.
</DocsNote>
### <MapPin size="16" class="inline-block" style="margin-bottom: 2px" /> Points of interest
**Points of interest** (technically called _waypoints_) represent locations of interest to show either on a GPS device or on a digital map.
+20 -10
View File
@@ -3,14 +3,14 @@ title: Edit actions
---
<script lang="ts">
import { Undo2, Redo2, Info, PaintBucket, EyeOff, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Maximize, Plus } from '@lucide/svelte';
import { Undo2, Redo2, Info, PaintBucket, ArrowRightLeft, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Plus } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
# { title }
Unlike the file actions, the edit actions can potentially modify the content of the currently selected files.
Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, and points of interest](../gpx).
Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, routes, and points of interest](../gpx).
Therefore, we will refer to the elements that can be modified by these actions as _file items_.
Note that except for the undo and redo actions, the edit actions are also accessible through the context menu (right-click) of the file items.
@@ -27,10 +27,6 @@ Open the information dialog of the currently selected file item, where you can s
Open the appearance dialog, where you can change the color, opacity, and width of the selected file items on the map.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Hide/unhide
Toggle the visibility of the selected file items on the map.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New track
Create a new track in the selected file.
@@ -42,6 +38,17 @@ Additionally, the selection must be a single file.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New route
Create a new route in the selected file.
<DocsNote>
This action is only available when the tree layout of the files list is enabled.
Additionally, the selection must be a single file.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New segment
Create a new segment in the selected track.
@@ -53,14 +60,17 @@ Additionally, the selection must be a single track.
</DocsNote>
### <ArrowRightLeft size="16" class="inline-block" style="margin-bottom: 2px" /> Convert to route/track
Convert the selected routes to tracks, or the selected tracks or segments to routes.
Since a route has a single segment, a track with multiple segments is converted into one route per segment.
When the selected files contain only routes, or only tracks, you can also convert all of them at once.
### <FileStack size="16" class="inline-block" style="margin-bottom: 2px" /> Select all
Add all file items in the current hierarchy level to the selection.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Center
Center the map on the selected file items.
### <ClipboardCopy size="16" class="inline-block" style="margin-bottom: 2px" /> Copy
Copy the selected file items to the clipboard.
+4
View File
@@ -15,6 +15,10 @@ The file actions menu contains a set of pretty self-explanatory file operations.
Create a new empty file.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New route file
Create a new file containing an empty [route](../gpx).
### <FolderOpen size="16" class="inline-block" style="margin-bottom: 2px" /> Open...
Open files from your computer.
+10 -2
View File
@@ -3,7 +3,7 @@ title: View options
---
<script lang="ts">
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, Box, Maximize2 } from '@lucide/svelte';
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, EyeOff, Maximize, Box, Maximize2 } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
@@ -19,7 +19,7 @@ Hide the elevation profile to make room for the map, or show it to inspect the c
Toggle the tree layout for the [file list](../files-and-stats).
This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map.
In addition, the file tree view enables you to inspect the [tracks, segments, and points of interest](../gpx) contained inside the files through collapsible sections.
In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](../gpx) contained inside the files through collapsible sections.
### <Map size="16" class="inline-block" style="margin-bottom: 2px" /> Switch to previous basemap
@@ -38,6 +38,14 @@ They are displayed for the current selection, like the [elevation profile](../fi
Toggle the visibility of direction arrows on the map.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Hide/unhide
Toggle the visibility of the selected file items on the map.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Center
Center the map on the selected file items.
### <Box size="16" class="inline-block" style="margin-bottom: 2px" /> Toggle 3D
Enter or exit the 3D map view.
+1 -1
View File
@@ -16,7 +16,7 @@ This tool allows you to extract [tracks (or segments)](../gpx) from files (or tr
<Extract class="text-foreground p-3 border rounded-md shadow-lg" />
</div>
Applying the tool to a file containing multiple tracks will create a new file for each of the tracks it contains.
Applying the tool to a file containing multiple [tracks or routes](../gpx) will create a new file for each of the tracks and routes it contains.
Similarly, applying the tool to a track containing multiple segments will create (in the same file) a new track for each of the segments it contains.
<DocsNote>
+3 -3
View File
@@ -10,11 +10,11 @@ title: Merge
# <Group size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
To use this tool, you need to [select](../files-and-stats) multiple files, [tracks, or segments](../gpx).
To use this tool, you need to [select](../files-and-stats) multiple files, [tracks, segments or routes](../gpx).
- If your goal is to create a single continuous trace from your selection, use the **Connect the traces** option and validate.
- The second option can be used to create or manage files with multiple [tracks or segments](../gpx).
Merging files (or tracks) will result in a single file (or track) containing all tracks (or segments) from the selection.
- The second option can be used to create or manage files with multiple [tracks, segments or routes](../gpx).
Merging files (or tracks) will result in a single file (or track) containing all tracks and routes (or segments) from the selection.
<DocsNote>
+1 -1
View File
@@ -27,6 +27,6 @@ To split the selected trace into two parts, click on one of the split markers di
To split at a specific point of your choice, hover over the trace on the map.
Scissors will appear at the cursor position, showing that you can split the trace at that point.
You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks or segments](../gpx).
You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks, segments or routes](../gpx).
<DocsImage src="tools/split" alt="Hovering over the selected trace turns your cursor into scissors." />
-6
View File
@@ -67,12 +67,6 @@ Since a route has a single segment, a track with multiple segments is converted
When the selected files contain only routes, or only tracks, you can also convert all of them at once.
<DocsNote>
Except for whole files, this action is only available when the tree layout of the files list is enabled.
</DocsNote>
### <FileStack size="16" class="inline-block" style="margin-bottom: 2px" /> Select all
Add all file items in the current hierarchy level to the selection.
+1 -1
View File
@@ -51,7 +51,7 @@ Al hacer clic con el botón derecho en una pestaña de archivo, puede acceder a
Como se mencionó en la [sección de opciones de vista](./menu/view), puede cambiar a un esquema de árbol para la lista de archivos.
Esta disposición es ideal para administrar un gran número de archivos abiertos ya que los organiza en una lista vertical en la parte derecha del mapa.
Además, la vista de árbol de archivos permite inspeccionar [pistas, segmentos y puntos de interés](./gpx) contenidos dentro de los archivos a través de secciones plegables.
In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](./gpx) contained inside the files through collapsible sections.
También puede aplicar [acciones de edición](./menu/edit) y [herramientas](./toolbar) a los elementos internos del archivo.
Además, puede arrastrar y soltar los elementos internos para reordenarlos, o moverlos dentro de la jerarquía o incluso a otro archivo.
+17 -1
View File
@@ -3,13 +3,15 @@ title: Formato de archivo GPX
---
<script>
import { Waypoints, MapPin } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
import { Waypoints, Route, MapPin } from '@lucide/svelte';
</script>
# { title }
El <a href="https://www.topografix.com/gpx.asp" target="_blank">formato de archivo GPX</a> es un estándar abierto para el intercambio de datos GPS entre aplicaciones y dispositivos GPS.
Consiste esencialmente en una serie de puntos GPS que codifican uno o varios trazados GPS y, opcionalmente, algunos puntos de interés.
These traces can be stored either as tracks or as routes.
Los archivos GPX también pueden contener metadatos, de los cuales los campos **nombre** y **descripción** son los más útiles para los usuarios.
@@ -27,6 +29,20 @@ Como se mencionó anteriormente, un archivo GPX puede contener múltiples trazad
En la mayoría de los casos, los archivos GPX contienen una sola pista con un solo segmento.
Sin embargo, la jerarquía descrita anteriormente permite casos de uso más avanzados, como la planificación de viajes de varios días con varias variantes para cada día.
### <Route size="16" class="inline-block" style="margin-bottom: 2px" /> Routes
A **route** is a single continuous path made of a sequence of **route points**.
These are mandatory checkpoints, which correspond to the anchor points of the [routing tool](./toolbar/routing).
The points in between are only a suggested path connecting the route points.
Like a track, a route can have a **name**, a **description**, and **appearance properties**.
<DocsNote>
Nowadays, most apps and GPS devices work perfectly well with tracks, even for navigation.
However, some of them only work with routes: in that case, you can [convert](./menu/edit) your tracks to routes, or [create](./menu/file) a route file from the start.
</DocsNote>
### <MapPin size="16" class="inline-block" style="margin-bottom: 2px" /> Puntos de interés
**Puntos de interés** (técnicamente llamados _waypoints_) representan ubicaciones de interés para mostrar en un dispositivo GPS o en un mapa digital.
+20 -10
View File
@@ -3,14 +3,14 @@ title: Acciones de edición
---
<script lang="ts">
import { Undo2, Redo2, Info, PaintBucket, EyeOff, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Maximize, Plus } from '@lucide/svelte';
import { Undo2, Redo2, Info, PaintBucket, ArrowRightLeft, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Plus } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
# { title }
A diferencia de las acciones de archivo, las acciones de edición pueden potencialmente modificar el contenido de los archivos seleccionados actualmente.
Además, cuando está habilitada la disposición del árbol de la lista de archivos (ver [Archivos y estadísticas](../files-and-stats)), se pueden aplicar a [pistas, segmentos y puntos de interés](../gpx).
Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, routes, and points of interest](../gpx).
Por lo tanto, nos referiremos a los elementos que pueden ser modificados por estas acciones como _elementos de archivo_.
Tenga en cuenta que, excepto para las acciones de deshacer y rehacer, las acciones de edición también son accesibles a través del menú contextual (clic derecho) de los elementos del archivo.
@@ -27,10 +27,6 @@ Abre el cuadro de diálogo de información del elemento del archivo seleccionado
Abre el diálogo de apariencia, donde puede cambiar el color, la opacidad y el ancho en el mapa de los elementos de archivo seleccionados.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Ocultar/mostrar
Alterna la visibilidad de los elementos del archivo seleccionado en el mapa.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> Nueva pista
Crear una nueva pista en el archivo seleccionado.
@@ -42,6 +38,17 @@ Además, la selección debe ser un solo archivo.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New route
Create a new route in the selected file.
<DocsNote>
Esta acción sólo está disponible cuando está habilitada la disposición del árbol de la lista de archivos.
Además, la selección debe ser un solo archivo.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> Nuevo segmento
Crear un nuevo segmento en la pista seleccionada.
@@ -53,14 +60,17 @@ Además, la selección debe ser una sola pista.
</DocsNote>
### <ArrowRightLeft size="16" class="inline-block" style="margin-bottom: 2px" /> Convert to route/track
Convert the selected routes to tracks, or the selected tracks or segments to routes.
Since a route has a single segment, a track with multiple segments is converted into one route per segment.
When the selected files contain only routes, or only tracks, you can also convert all of them at once.
### <FileStack size="16" class="inline-block" style="margin-bottom: 2px" /> Seleccionar todo
Añade todos los elementos del archivo en el nivel de jerarquía actual a la selección.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Centrar
Centrar el mapa en los elementos de archivo seleccionados.
### <ClipboardCopy size="16" class="inline-block" style="margin-bottom: 2px" /> Copiar
Copia los elementos del archivo seleccionado al portapapeles.
+4
View File
@@ -15,6 +15,10 @@ El menú de acciones de archivo contiene un conjunto de operaciones bastante aut
Crea un nuevo archivo vacío.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New route file
Create a new file containing an empty [route](../gpx).
### <FolderOpen size="16" class="inline-block" style="margin-bottom: 2px" /> Abrir...
Abre archivos desde su equipo.
+10 -2
View File
@@ -3,7 +3,7 @@ title: Opciones de vista
---
<script lang="ts">
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, Box, Maximize2 } from '@lucide/svelte';
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, EyeOff, Maximize, Box, Maximize2 } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
@@ -19,7 +19,7 @@ Oculta el perfil de elevación para dejar espacio para el mapa, o lo muestra par
Alterna el diseño de árbol para la [lista de archivos](../files-and-stats).
Esta disposición es ideal para administrar un gran número de archivos abiertos ya que los organiza en una lista vertical en la parte derecha del mapa.
Además, la vista de árbol de archivos le permite inspeccionar [pistas, segmentos y puntos de interés](../gpx) contenidos dentro de los archivos a través de secciones plegables.
In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](../gpx) contained inside the files through collapsible sections.
### <Map size="16" class="inline-block" style="margin-bottom: 2px" /> Cambiar al mapa base anterior
@@ -38,6 +38,14 @@ Se muestran para la selección actual, como el [perfil de elevación](../files-a
Alterna la visibilidad de las flechas de dirección en el mapa.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Hide/unhide
Toggle the visibility of the selected file items on the map.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Center
Center the map on the selected file items.
### <Box size="16" class="inline-block" style="margin-bottom: 2px" /> Alternar 3D
Entra o sale de la vista 3D del mapa.
+1 -1
View File
@@ -16,7 +16,7 @@ Esta herramienta le permite extraer [pistas (o segmentos)](../gpx) de archivos (
<Extract class="text-foreground p-3 border rounded-md shadow-lg" />
</div>
Aplicar la herramienta a un archivo que contenga múltiples pistas creará un nuevo archivo para cada una de las pistas.
Applying the tool to a file containing multiple [tracks or routes](../gpx) will create a new file for each of the tracks and routes it contains.
Del mismo modo, la aplicación de la herramienta a una pista que contenga varios segmentos creará (en el mismo archivo) una nueva pista para cada uno de los segmentos.
<DocsNote>
+3 -3
View File
@@ -10,11 +10,11 @@ title: Combinar
# <Group size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
Para usar esta herramienta, necesita [seleccionar](../files-and-stats) múltiples archivos, [pistas o segmentos](../gpx).
To use this tool, you need to [select](../files-and-stats) multiple files, [tracks, segments or routes](../gpx).
- Si su objetivo es crear un único trazado continuo a partir de su selección, use la opción **Conectar los trazados** y valide.
- La segunda opción se puede usar para crear o gestionar archivos con múltiples [pistas o segmentos](../gpx).
Combinar archivos (o pistas) dará como resultado un único archivo (o pista) que contendrá todas las pistas (o segmentos) de la selección.
- The second option can be used to create or manage files with multiple [tracks, segments or routes](../gpx).
Merging files (or tracks) will result in a single file (or track) containing all tracks and routes (or segments) from the selection.
<DocsNote>
+1 -1
View File
@@ -27,6 +27,6 @@ Para dividir el trazado seleccionado en dos partes, haga clic en uno de los marc
Para dividir en un punto específico de su elección, sitúe el cursor sobre el trazado en el mapa.
Las tijeras aparecerán en la posición del cursor, mostrando que puede dividir el trazado en ese punto.
Puede elegir dividir el trazado en dos archivos GPX o mantener las partes divididas en el mismo archivo como [pistas o segmentos](../gpx).
You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks, segments or routes](../gpx).
<DocsImage src="tools/split" alt="Situar el cursor sobre el trazado seleccionado lo convierte en tijeras." />
+1 -1
View File
@@ -50,7 +50,7 @@ Fitxategi fitxa batean eskuin-klik eginez, [editatu menuko](./menu/edit) ekintza
Aipatu bezala, [ikusi atalean](./menu/view), fitxategien zerrendaren zuhaitz diseinura alda dezakezu.
Diseinu hau aproposa da irekitako fitxategi ugari kudeatzeko, maparen eskuinaldean zerrenda bertikal batean antolatzen baitu.
Gainera, fitxategi-zuhaitzaren ikuspegiak aukera ematen du [arrastoak, segmentuak eta interesguneak](./gpx) ikuskatzeko ataletan antolatutako fitxategietan.
In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](./gpx) contained inside the files through collapsible sections.
[Edizio ekintzak](./menu/edit) eta [tresnak](./toolbar) fitxategien barneko elementuetara ere aplikatu ditzakezu.
Gainera, barruko elementuak arrastatu eta jaregin ditzakezu berrantolatzeko edo hierarkian edo beste fitxategi batera eraman.
+17 -1
View File
@@ -3,13 +3,15 @@ title: GPX fitxategi formatua
---
<script>
import { Waypoints, MapPin } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
import { Waypoints, Route, MapPin } from '@lucide/svelte';
</script>
# { title }
<a href="https://www.topografix.com/gpx.asp" target="_blank">GPX fitxategi formatua</a> GPS datuak aplikazioen eta GPS gailuen artean trukatzeko formatu ireki estandarra da.
GPS puntu serie bat GPS arrasto bat edo gehiagotan bildua, eta, aukeran, interesgune batzuk.
These traces can be stored either as tracks or as routes.
GPX fitxategiek metadatuak ere izan ditzakete, horien artean **izena** eta **deskribapena** eremuak erabiltzaileentzat erabilgarrienak izaten dira.
@@ -27,6 +29,20 @@ Egitura hierarkiko batean antolatuta daude, goiko mailan:
Gehienetan, GPX fitxategiek segmentu bakarraz osatutako arrasto edo ibilbide bakarra dute.
Hala ere, goian deskribatutako hierarkiak erabilera kasu aurreratuagoak egiteko aukera ematen du, hala nola egun anitzeko bidaiak antolatzea egun bakoitzeko hainbat aldaerekin.
### <Route size="16" class="inline-block" style="margin-bottom: 2px" /> Routes
A **route** is a single continuous path made of a sequence of **route points**.
These are mandatory checkpoints, which correspond to the anchor points of the [routing tool](./toolbar/routing).
The points in between are only a suggested path connecting the route points.
Like a track, a route can have a **name**, a **description**, and **appearance properties**.
<DocsNote>
Nowadays, most apps and GPS devices work perfectly well with tracks, even for navigation.
However, some of them only work with routes: in that case, you can [convert](./menu/edit) your tracks to routes, or [create](./menu/file) a route file from the start.
</DocsNote>
### <MapPin size="16" class="inline-block" style="margin-bottom: 2px" /> Interesguneak
**Interesguneak** (_bide-puntu_ gisa ere ezagunak) interesgarriak diren kokapenak GPS gailu batean edo mapa digital batean erakusteko erabiltzen dira.
+2 -2
View File
@@ -13,8 +13,8 @@ title: Integrazioa
Behar duzun guztia hau da:
1. GPX files hosted on your server or on Google Drive, or accessible via a public URL;
2. _Optional:_ a <a href="https://cloud.maptiler.com/auth/widget?next=https://cloud.maptiler.com/maps/" target="_blank">MapTiler key</a> to load MapTiler maps.
1. Zure zerbitzarian edo Google Drive-n ostatatutako GPX fitxategiak, edo URL publiko baten bidez eskura daitezke;
2. _Aukerakoa:_ <a href="https://cloud.maptiler.com/auth/widget?next=https://cloud.maptiler.com/maps/" target="_blank">MapTiler tekla</a> MapTiler mapak kargatzeko.
Ondoren, konfiguratzailearekin jolastu dezakezu zure mapa pertsonalizatzeko eta dagokion HTML kodea sortzeko.
+2 -2
View File
@@ -59,7 +59,7 @@ Maparen geruzen botoiak mapa-oinarri desberdinen artean aldatzeko aukera ematen
<DocsLayers />
<span class="text-sm text-center mt-2">
Hover over the map to show the <a href="https://hiking.waymarkedtrails.org" target="_blank">Waymarked Trails hiking</a> overlay on top of the topographic basemap.
Pasa ezazu maparen gainean <a href="https://hiking.waymarkedtrails.org" target="_blank">markatutako mendi-ibiliak</a> gainjartzeko oinarrizko mapa topografikoaren gainean.
</span>
</div>
@@ -69,4 +69,4 @@ Global eta tokiko mapa oinarri bilduma handia dago **gpx.studio**-n eskuragarri,
Ezarpen horietan, gainjartzeen opakutasuna ere kudeatu dezakezu.
For advanced users, it is possible to add custom basemaps and overlays by providing <a href="https://en.wikipedia.org/wiki/Web_Map_Tile_Service" target="_blank">WMTS</a>, <a href="https://en.wikipedia.org/wiki/Web_Map_Service" target="_blank">WMS</a>, or <a href="https://maplibre.org/maplibre-style-spec/" target="_blank">MapLibre style JSON</a> URLs.
Erabiltzaile aurreratuentzat, oinarrizko mapa pertsonalizatuak eta gainjartzeak gehi daitezke <a href="https://en.wikipedia.org/wiki/Web_Map_Tile_Service" target="_blank">WMTS</a>, <a href="https://en.wikipedia.org/wiki/Web_Map_Service" target="_blank">WMS</a> edo <a href="https://maplibre.org/maplibre-style-spec/" target="_blank">MapLibre estiloko JSON</a> URLak.
+20 -10
View File
@@ -3,14 +3,14 @@ title: Edizio ekintzak
---
<script lang="ts">
import { Undo2, Redo2, Info, PaintBucket, EyeOff, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Maximize, Plus } from '@lucide/svelte';
import { Undo2, Redo2, Info, PaintBucket, ArrowRightLeft, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Plus } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
# { title }
Fitxategi-ekintzak ez bezala, edizio-ekintzek hautatutako fitxategien edukia alda dezakete.
Are gehiago, fitxategien zerrendaren zuhaitzaren diseinua gaituta dagoenean (Ikusi [Fitxategiak eta estatistikak](../files-and-stats)), ekintzek [arrasto, segmentu eta interesgunetan](../gpx) ere aplikatu daitezke.
Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, routes, and points of interest](../gpx).
Hori dela eta, ekintza hauek alda dezaketen elementuei _fitxategi-elementuak_ izendatzen ditugu.
Ohar zaitez, desegin eta berregin ekintzak ez, beste guztiak fitxategi-elementu hauen alboko menuaren bidez (eskuin-klik) ere erabili daitezke.
@@ -27,10 +27,6 @@ Ireki hautatutako fitxategiaren elementuaren elkarrizketa-koadroa, non bere izen
Ireki Itxuraren elkarrizketa-koadroa, eta bertan mapan hautatutako elementuaren kolorea, opakotasuna eta zabalera alda ditzakezu.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Ezkutatu/erakutsi
Aldatu mapan hautatutako elementuen ikusgarritasuna.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> Arrasto berria
Sortu arrasto berri bat hautatutako fitxategian.
@@ -42,6 +38,17 @@ Gainera, fitxategi bakarra egon behar du hautatuta.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New route
Create a new route in the selected file.
<DocsNote>
Ekintza hau soilik aktibatuko da fitxategien zerrendako zuhaitz-geruza gaituta dagoenean.
Gainera, fitxategi bakarra egon behar du hautatuta.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> Segmentu berria
Sortu segmentu berri bat hautatutako arrastoan.
@@ -53,14 +60,17 @@ Gainera, arrasto bakarra egon behar du hautatuta.
</DocsNote>
### <ArrowRightLeft size="16" class="inline-block" style="margin-bottom: 2px" /> Convert to route/track
Convert the selected routes to tracks, or the selected tracks or segments to routes.
Since a route has a single segment, a track with multiple segments is converted into one route per segment.
When the selected files contain only routes, or only tracks, you can also convert all of them at once.
### <FileStack size="16" class="inline-block" style="margin-bottom: 2px" /> Hautatu denak
Gehitu hautapenera uneko hierarkia mailako fitxategi elementu guztiak.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Erdiratu
Erdiratu mapan hautatutako fitxategi-elementuak.
### <ClipboardCopy size="16" class="inline-block" style="margin-bottom: 2px" /> Kopiatu
Kopiatu arbelean hautatutako fitxategi-elementuak.
+4
View File
@@ -15,6 +15,10 @@ Fitxategi ekintzen menuan fitxategien eragiketa multzoa dago.
Sortu fitxategi huts berri bat.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New route file
Create a new file containing an empty [route](../gpx).
### <FolderOpen size="16" class="inline-block" style="margin-bottom: 2px" /> Ireki...
Ireki zure ordenagailuko fitxategiak.
+12 -5
View File
@@ -3,7 +3,7 @@ title: Ikusteko aukerak
---
<script lang="ts">
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, Box, Maximize2 } from '@lucide/svelte';
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, EyeOff, Maximize, Box, Maximize2 } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
@@ -19,7 +19,7 @@ Ezkutatu altueren profila mapari lekua egiteko, edo erakutsi uneko hautapena iku
Aldatu [fitxategi zerrendaren](../files-and-stats) zuhaitza ikuspegira.
Diseinu hau aproposa da irekitako fitxategi ugari kudeatzeko, maparen eskuinaldean zerrenda bertikal batean antolatzen baitu.
Fitxategien zuhaitza ikuspegiak fitxategien barneko [ibilbideak, segmentuak eta interesguneak](../gpx) atal gisa erakusten ditu.
In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](../gpx) contained inside the files through collapsible sections.
### <Map size="16" class="inline-block" style="margin-bottom: 2px" /> Aldatu erabilitako aurreko geruzara
@@ -38,6 +38,14 @@ Unean hautatutako ibilbidean ikusten dira, [koten profila bezala](../files-and-s
Aldatu ibilbidearen noranzko gezien ikusgarritasuna.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Hide/unhide
Toggle the visibility of the selected file items on the map.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Center
Center the map on the selected file items.
### <Box size="16" class="inline-block" style="margin-bottom: 2px" /> Aldatu 3D
Aktibatu edo desaktibatu maparen 3D ikuspegia.
@@ -48,7 +56,6 @@ Maparen orientazioa eta okertzea kontrolatzeko, mapa ere arrasta dezakezu <kbd>
</DocsNote>
### <Maximize2 size="16" class="inline-block" style="margin-bottom: 2px" /> Full screen
### <Maximize2 size="16" class="inline-block" style="margin-bottom: 2px" /> Pantaila osoa
Enter or exit full screen mode.
You can also press <kbd>F11</kbd> to toggle, or <kbd>Escape</kbd> to exit.
Sartu edo irten pantaila osoko moduan.<kbd>F11</kbd> ere saka dezakezu txandakatzeko, edo <kbd>Ihes</kbd> irteteko.
@@ -18,7 +18,7 @@ Tresna honen bidez elebazio datuak gehitzen ahal dira ibilbidetan eta [interesgu
<DocsNote>
Elevation data is provided by <a href="https://mapterhorn.com" target="_blank">Mapterhorn</a>.
You can learn more about its origin and accuracy in the <a href="https://mapterhorn.com/attribution/" target="_blank">documentation</a>.
Altueraren datuak <a href="https://mapterhorn.com" target="_blank">Mapterhorn</a>k ematen ditu.
Haren jatorriari eta zehaztasunari buruz gehiago jakin dezakezu <a href="https://mapterhorn.com/attribution/" target="_blank">dokumentazioan</a>.
</DocsNote>
+1 -1
View File
@@ -16,7 +16,7 @@ Tresna honen bidez [ibilbideak (edo segmentuak)](../gpx) erauzi ditzakezu fitxat
<Extract class="text-foreground p-3 border rounded-md shadow-lg" />
</div>
Hainbat ibilbide dituen fitxategi bati aplikatuz gero dituen ibilbide bakoitzarentzat fitxategi bana sortuko du.
Applying the tool to a file containing multiple [tracks or routes](../gpx) will create a new file for each of the tracks and routes it contains.
Era berean, hainbat segmentu duen ibilbide bati aplikatuz gero (fitxategi berean) duen segmentu bakoitzeko ibilbide bana sortuko du.
<DocsNote>
+5 -5
View File
@@ -10,16 +10,16 @@ title: Batu
# <Group size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
Tresna hau erabiltzeko [hautatu](../files-and-stats) behar duzu hainbat fitxategi [ibilbidea, edo segmentuak](../gpx).
To use this tool, you need to [select](../files-and-stats) multiple files, [tracks, segments or routes](../gpx).
- Zure helburua hautaketatik abiatuta etengabeko arrasto bakarra sortzea bada, erabili **Konektatu trazak ** aukera eta balioztatu.
- Bigarren aukera bat izan daiteke hainbat [arrasto edo segmentu](../gpx) biltzen duen fitxategiak sortzea edo kudeatzea.
Fitxategiak (edo arrastoak) batzearen emaitza fitxategi (edo arrasto) bakarra sortzea da zeinak hautapenaren arrastoak (edo segmentuak) biltzen duena.
- The second option can be used to create or manage files with multiple [tracks, segments or routes](../gpx).
Merging files (or tracks) will result in a single file (or track) containing all tracks and routes (or segments) from the selection.
<DocsNote>
Selected items are merged in the order they appear in the files list.
Reorder items by drag-and-drop if needed.
Hautatutako elementuak fitxategien zerrendan agertzen diren ordenan batzen dira.
Berrantolatu elementuak arrastatu eta jareginez, behar izanez gero.
</DocsNote>
+1 -1
View File
@@ -27,6 +27,6 @@ Hautatutako ibilbidea bi zatitan banatzeko, egin klik arrastoko zatiketa markatz
Puntu zehatz batean banatzeko, pasa ezazu maparen arrastoaren gainean.
Artaziak agertuko dira kurtsorearen posizioan, puntu horretan ebaki dezakezula erakutsiz.
Arrastoa bi GPX fitxategietan zatitzea edo fitxategi berean zatiak [arrastoak edo segmentuak](../gpx) gisa gordetzea aukeratu dezakezu.
You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks, segments or routes](../gpx).
<DocsImage src="tools/split" alt="Aukeratutako trazaren gainean kokatzean, zure kurtsorea guraize bihurtuko da." />
+1 -1
View File
@@ -51,7 +51,7 @@ By right-clicking on a file tab, you can access the same actions as in the [edit
As mentioned in the [view options section](./menu/view), you can switch to a tree layout for the files list.
This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map.
In addition, the file tree view enables you to inspect the [tracks, segments, and points of interest](./gpx) contained inside the files through collapsible sections.
In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](./gpx) contained inside the files through collapsible sections.
You can also apply [edit actions](./menu/edit) and [tools](./toolbar) to internal file items.
Furthermore, you can drag and drop the inner items to reorder them, or move them in the hierarchy or even to another file.
+17 -1
View File
@@ -3,13 +3,15 @@ title: GPX file format
---
<script>
import { Waypoints, MapPin } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
import { Waypoints, Route, MapPin } from '@lucide/svelte';
</script>
# { title }
The <a href="https://www.topografix.com/gpx.asp" target="_blank">GPX file format</a> is an open standard for exchanging GPS data between applications and GPS devices.
It essentially consists of a series of GPS points encoding one or multiple GPS traces, and, optionally, some points of interest.
These traces can be stored either as tracks or as routes.
GPX files may also contain metadata, of which the **name** and **description** fields are the most useful for users.
@@ -27,6 +29,20 @@ These are organized in a hierarchical structure, with tracks at the top level.
In most cases, GPX files contain a single track with a single segment.
However, the hierarchy described above allows for more advanced use cases, such as planning multi-day trips with several variants for each day.
### <Route size="16" class="inline-block" style="margin-bottom: 2px" /> Routes
A **route** is a single continuous path made of a sequence of **route points**.
These are mandatory checkpoints, which correspond to the anchor points of the [routing tool](./toolbar/routing).
The points in between are only a suggested path connecting the route points.
Like a track, a route can have a **name**, a **description**, and **appearance properties**.
<DocsNote>
Nowadays, most apps and GPS devices work perfectly well with tracks, even for navigation.
However, some of them only work with routes: in that case, you can [convert](./menu/edit) your tracks to routes, or [create](./menu/file) a route file from the start.
</DocsNote>
### <MapPin size="16" class="inline-block" style="margin-bottom: 2px" /> Points of interest
**Points of interest** (technically called _waypoints_) represent locations of interest to show either on a GPS device or on a digital map.
+20 -10
View File
@@ -3,14 +3,14 @@ title: Edit actions
---
<script lang="ts">
import { Undo2, Redo2, Info, PaintBucket, EyeOff, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Maximize, Plus } from '@lucide/svelte';
import { Undo2, Redo2, Info, PaintBucket, ArrowRightLeft, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Plus } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
# { title }
Unlike the file actions, the edit actions can potentially modify the content of the currently selected files.
Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, and points of interest](../gpx).
Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, routes, and points of interest](../gpx).
Therefore, we will refer to the elements that can be modified by these actions as _file items_.
Note that except for the undo and redo actions, the edit actions are also accessible through the context menu (right-click) of the file items.
@@ -27,10 +27,6 @@ Open the information dialog of the currently selected file item, where you can s
Open the appearance dialog, where you can change the color, opacity, and width of the selected file items on the map.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Hide/unhide
Toggle the visibility of the selected file items on the map.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New track
Create a new track in the selected file.
@@ -42,6 +38,17 @@ Additionally, the selection must be a single file.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New route
Create a new route in the selected file.
<DocsNote>
This action is only available when the tree layout of the files list is enabled.
Additionally, the selection must be a single file.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New segment
Create a new segment in the selected track.
@@ -53,14 +60,17 @@ Additionally, the selection must be a single track.
</DocsNote>
### <ArrowRightLeft size="16" class="inline-block" style="margin-bottom: 2px" /> Convert to route/track
Convert the selected routes to tracks, or the selected tracks or segments to routes.
Since a route has a single segment, a track with multiple segments is converted into one route per segment.
When the selected files contain only routes, or only tracks, you can also convert all of them at once.
### <FileStack size="16" class="inline-block" style="margin-bottom: 2px" /> Select all
Add all file items in the current hierarchy level to the selection.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Center
Center the map on the selected file items.
### <ClipboardCopy size="16" class="inline-block" style="margin-bottom: 2px" /> Copy
Copy the selected file items to the clipboard.
+4
View File
@@ -15,6 +15,10 @@ The file actions menu contains a set of pretty self-explanatory file operations.
Create a new empty file.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New route file
Create a new file containing an empty [route](../gpx).
### <FolderOpen size="16" class="inline-block" style="margin-bottom: 2px" /> Open...
Open files from your computer.
+10 -2
View File
@@ -3,7 +3,7 @@ title: View options
---
<script lang="ts">
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, Box, Maximize2 } from '@lucide/svelte';
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, EyeOff, Maximize, Box, Maximize2 } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
@@ -19,7 +19,7 @@ Hide the elevation profile to make room for the map, or show it to inspect the c
Toggle the tree layout for the [file list](../files-and-stats).
This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map.
In addition, the file tree view enables you to inspect the [tracks, segments, and points of interest](../gpx) contained inside the files through collapsible sections.
In addition, the file tree view enables you to inspect the [tracks, segments, routes, and points of interest](../gpx) contained inside the files through collapsible sections.
### <Map size="16" class="inline-block" style="margin-bottom: 2px" /> Switch to previous basemap
@@ -38,6 +38,14 @@ They are displayed for the current selection, like the [elevation profile](../fi
Toggle the visibility of direction arrows on the map.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Hide/unhide
Toggle the visibility of the selected file items on the map.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Center
Center the map on the selected file items.
### <Box size="16" class="inline-block" style="margin-bottom: 2px" /> Toggle 3D
Enter or exit the 3D map view.
+1 -1
View File
@@ -16,7 +16,7 @@ This tool allows you to extract [tracks (or segments)](../gpx) from files (or tr
<Extract class="text-foreground p-3 border rounded-md shadow-lg" />
</div>
Applying the tool to a file containing multiple tracks will create a new file for each of the tracks it contains.
Applying the tool to a file containing multiple [tracks or routes](../gpx) will create a new file for each of the tracks and routes it contains.
Similarly, applying the tool to a track containing multiple segments will create (in the same file) a new track for each of the segments it contains.
<DocsNote>
+3 -3
View File
@@ -10,11 +10,11 @@ title: Merge
# <Group size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
To use this tool, you need to [select](../files-and-stats) multiple files, [tracks, or segments](../gpx).
To use this tool, you need to [select](../files-and-stats) multiple files, [tracks, segments or routes](../gpx).
- If your goal is to create a single continuous trace from your selection, use the **Connect the traces** option and validate.
- The second option can be used to create or manage files with multiple [tracks or segments](../gpx).
Merging files (or tracks) will result in a single file (or track) containing all tracks (or segments) from the selection.
- The second option can be used to create or manage files with multiple [tracks, segments or routes](../gpx).
Merging files (or tracks) will result in a single file (or track) containing all tracks and routes (or segments) from the selection.
<DocsNote>
+1 -1
View File
@@ -27,6 +27,6 @@ To split the selected trace into two parts, click on one of the split markers di
To split at a specific point of your choice, hover over the trace on the map.
Scissors will appear at the cursor position, showing that you can split the trace at that point.
You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks or segments](../gpx).
You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks, segments or routes](../gpx).
<DocsImage src="tools/split" alt="Hovering over the selected trace turns your cursor into scissors." />
+1 -1
View File
@@ -51,7 +51,7 @@ En faisant un clic droit sur un onglet de fichier, vous pouvez accéder aux mêm
Comme mentionné dans la [section des options d'affichage](./menu/view), vous pouvez basculer vers une présentation en arborescence pour la liste des fichiers.
Cette disposition est idéale pour gérer un grand nombre de fichiers ouverts, car elle les organise en une liste verticale à droite de la carte.
De plus, la vue en arborescence des fichiers vous permet d'inspecter les [traces, segments, et points d'intérêt](./gpx) contenus dans les fichiers grâce à des sections pliables.
De plus, la vue en arborescence des fichiers vous permet d'inspecter les [traces, segments, routes, et points d'intérêt](./gpx) contenus dans les fichiers grâce à des sections pliables.
Vous pouvez également appliquer les [actions d'édition](./menu/edit) et les [outils](./toolbar) aux éléments intérieurs aux fichiers.
De plus, vous pouvez glisser-déposer les éléments intérieurs pour les réorganiser, ou les déplacer dans la hiérarchie ou même dans un autre fichier.
+17 -1
View File
@@ -3,13 +3,15 @@ title: Format de fichier GPX
---
<script>
import { Waypoints, MapPin } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
import { Waypoints, Route, MapPin } from '@lucide/svelte';
</script>
# { title }
Le format de fichier <a href="https://www.topografix.com/gpx.asp" target="_blank">GPX</a> est un standard ouvert pour l'échange de données GPS entre applications et appareils GPS.
Il consiste principalement en une série de points GPS encodant une ou plusieurs traces GPS, et, éventuellement, certains points d'intérêt.
Ces traces peuvent être stockées sous forme de pistes ou de routes.
Les fichiers GPX peuvent aussi contenir des métadonnées, dont les champs **nom** et **description** sont les plus utiles pour les utilisateurs.
@@ -27,6 +29,20 @@ Celles-ci sont organisées en une structure hiérarchique, dont les pistes const
Dans la plupart des cas, les fichiers GPX contiennent une seule piste avec un seul segment.
Cependant, la hiérarchie décrite ci-dessus permet des cas d'utilisation plus avancés, comme la planification de voyages de plusieurs jours avec plusieurs variantes pour chaque jour.
### <Route size="16" class="inline-block" style="margin-bottom: 2px" /> Routes
Une **route** est un chemin continu constitué d'une séquence de **points de passage**.
Ces points de passage correspondent aux points d'ancrage de [l'outil de routage](./toolbar/routing).
Les points intermédiaires entre les points de passage ne sont qu'un chemin suggéré pour les relier.
Comme une piste, une route peut avoir un **nom**, une **description**, et des **propriétés d'apparence**.
<DocsNote>
De nos jours, la plupart des applications et des appareils GPS fonctionnent parfaitement avec les pistes, même pour la navigation.
Cependant, certains d'entre eux ne fonctionnent qu'avec des routes : dans ce cas, vous pouvez [convertir](./menu/edit) vos pistes en routes, ou [créer](./menu/file) un fichier route dès le début.
</DocsNote>
### <MapPin size="16" class="inline-block" style="margin-bottom: 2px" /> Points d'intérêt
Les **points d'intérêt** (techniquement appelés _waypoints_) représentent des lieux d'intérêt à afficher sur un appareil GPS ou sur une carte numérique.
+20 -10
View File
@@ -3,14 +3,14 @@ title: Actions d'édition
---
<script lang="ts">
import { Undo2, Redo2, Info, PaintBucket, EyeOff, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Maximize, Plus } from '@lucide/svelte';
import { Undo2, Redo2, Info, PaintBucket, ArrowRightLeft, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Plus } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
# { title }
Contrairement aux actions sur les fichiers, les actions d'édition peuvent potentiellement modifier le contenu des fichiers sélectionnés.
De plus, lorsque la présentation en arborescence de la liste de fichiers est activée (voir [Fichiers et statistiques](../files-and-stats)), elles peuvent également être appliquées aux [pistes, segments, et points d'intérêt](../gpx).
De plus, lorsque la présentation en arborescence de la liste de fichiers est activée (voir [Fichiers et statistiques](../files-and-stats)), elles peuvent également être appliquées aux [pistes, segments, routes, et points d'intérêt](../gpx).
Par conséquent, nous appellerons les éléments qui peuvent être modifiés par ces actions des _éléments de fichier_.
Notez que, à part les actions annuler et rétablir, les actions d'édition sont également accessibles via le menu contextuel (clic droit) des éléments de fichier.
@@ -27,10 +27,6 @@ Ouvrir la boîte de dialogue d'information de l'élément de fichier actuellemen
Ouvrir la boîte de dialogue d'apparence, où vous pouvez changer la couleur, l'opacité et l'épaisseur des éléments de fichier sélectionnés sur la carte.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Cacher/afficher
Activer/désactiver la visibilité des éléments de fichier sélectionnés sur la carte.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> Nouvelle piste
Créer une nouvelle piste dans le fichier sélectionné.
@@ -42,6 +38,17 @@ De plus, la sélection doit contenir un seul fichier.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> Nouvelle route
Créer une nouvelle route dans le fichier sélectionné.
<DocsNote>
Cette action n'est disponible que lorsque la présentation en arborescence de la liste de fichiers est activée.
De plus, la sélection doit contenir un seul fichier.
</DocsNote>
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> Nouveau segment
Créer un nouveau segment dans la piste sélectionnée.
@@ -53,14 +60,17 @@ De plus, la sélection doit contenir une seule piste.
</DocsNote>
### <ArrowRightLeft size="16" class="inline-block" style="margin-bottom: 2px" /> Convertir en fichier route/piste
Convertir les routes sélectionnées en pistes, ou les pistes ou segments sélectionnés en routes.
Comme une route a un seul segment, une piste avec plusieurs segments est convertie en une route par segment.
Lorsque les fichiers sélectionnés ne contiennent que des routes, ou seulement des pistes, vous pouvez également les convertir tous en même temps.
### <FileStack size="16" class="inline-block" style="margin-bottom: 2px" /> Sélectionner tout
Ajouter tous les éléments de fichier du niveau de hiérarchie actuel à la sélection.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Centrer
Centrer la carte sur les éléments de fichier sélectionnés.
### <ClipboardCopy size="16" class="inline-block" style="margin-bottom: 2px" /> Copier
Copier les éléments de fichier sélectionnés dans le presse-papiers.
+4
View File
@@ -15,6 +15,10 @@ Le menu des actions sur les fichiers contient un ensemble d'opérations de fichi
Créer un nouveau fichier vide.
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" />Nouveau fichier route
Créer un nouveau fichier contenant une [route](../gpx) vide.
### <FolderOpen size="16" class="inline-block" style="margin-bottom: 2px" /> Ouvrir...
Ouvrir des fichiers depuis votre ordinateur.
+10 -2
View File
@@ -3,7 +3,7 @@ title: Options d'affichage
---
<script lang="ts">
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, Box, Maximize2 } from '@lucide/svelte';
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, EyeOff, Maximize, Box, Maximize2 } from '@lucide/svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>
@@ -19,7 +19,7 @@ Masquer le profil altimétrique pour faire de la place pour la carte, ou l'affic
Activer/désactiver la présentation en arborescence pour la [liste de fichiers](../files-and-stats).
Cette disposition est idéale pour gérer un grand nombre de fichiers ouverts, car elle les organise en une liste verticale à droite de la carte.
De plus, la vue en arborescence des fichiers vous permet d'inspecter les [traces, segments, et points d'intérêt](../gpx) contenus dans les fichiers grâce à des sections pliables.
De plus, la vue en arborescence des fichiers vous permet d'inspecter les [traces, segments, routes, et points d'intérêt](../gpx) contenus dans les fichiers grâce à des sections pliables.
### <Map size="16" class="inline-block" style="margin-bottom: 2px" /> Basculer au fond de carte précédent
@@ -38,6 +38,14 @@ Ceux-ci sont affichés pour la sélection courante, comme le [profil altimétriq
Activer/désactiver la visibilité des flèches de direction sur la carte.
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Cacher/afficher
Activer/désactiver la visibilité des éléments de fichier sélectionnés sur la carte.
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Centrer
Centrer la carte sur les éléments de fichier sélectionnés.
### <Box size="16" class="inline-block" style="margin-bottom: 2px" /> Activer/désactiver la 3D
Entrer ou sortir de la vue 3D de la carte.
+1 -1
View File
@@ -16,7 +16,7 @@ Cet outil vous permet d'extraire [des pistes (ou des segments)](../gpx) de fichi
<Extract class="text-foreground p-3 border rounded-md shadow-lg" />
</div>
Appliquer l'outil à un fichier contenant plusieurs pistes créera un nouveau fichier pour chacune des pistes qu'il contient.
Appliquer l'outil à un fichier contenant plusieurs [pistes ou routes](../gpx) créera un nouveau fichier pour chacune des pistes et routes qu'il contient.
De même, appliquer l'outil à une piste contenant plusieurs segments créera (dans le même fichier) une nouvelle piste pour chacun des segments qu'elle contient.
<DocsNote>
+3 -3
View File
@@ -10,11 +10,11 @@ title: Fusionner
# <Group size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
Pour utiliser cet outil, vous devez [sélectionner](../files-and-stats) plusieurs fichiers, [pistes ou segments](../gpx).
Pour utiliser cet outil, vous devez [sélectionner](../files-and-stats) plusieurs fichiers, [pistes, segments ou routes](../gpx).
- Si votre objectif est de créer une seule trace continue à partir de votre sélection, utilisez l'option **Connecter les traces** et validez.
- La deuxième option peut être utilisée pour créer ou gérer des fichiers avec plusieurs [pistes ou segments](../gpx).
Fusionner des fichiers (ou des pistes) résultera en un seul fichier (ou piste) contenant toutes les pistes (ou tous les segments) de la sélection.
- La deuxième option peut être utilisée pour créer ou gérer des fichiers avec plusieurs [pistes, segments ou routes](../gpx).
Fusionner des fichiers (ou des pistes) résultera en un seul fichier (ou piste) contenant toutes les pistes et routes (ou tous les segments) de la sélection.
<DocsNote>
+1 -1
View File
@@ -27,6 +27,6 @@ Pour diviser la trace sélectionnée en deux parties, cliquez sur l'un des marqu
Pour diviser à un point précis de votre choix, survolez la trace sur la carte.
Des ciseaux apparaîtront à la position du curseur, montrant que vous pouvez diviser la trace à ce point.
Vous pouvez choisir de diviser la trace en deux fichiers GPX, ou de créer une [piste ou un segment](../gpx) pour chaque partie dans le même fichier.
Vous pouvez choisir de diviser la trace en deux fichiers GPX, ou de créer [une piste, un segment ou une route](../gpx) pour chaque partie dans le même fichier.
<DocsImage src="tools/split" alt="Survoler la trace sélectionnée transforme votre curseur en ciseaux." />
Loaded 100 of 343 files, more files were not shown because too many files have changed in this diff. Show more