mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-12-02 18:12:11 +00:00
Compare commits
548 Commits
30e72db5ea
...
l10n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fe6565527 | ||
|
|
69b018022d | ||
|
|
467cb2e589 | ||
|
|
f13d8c1e22 | ||
|
|
e230d55b82 | ||
|
|
46fcdb4bb2 | ||
|
|
429212ef23 | ||
|
|
4ea0ea6a7a | ||
|
|
2e3ce83605 | ||
|
|
fda908dd0d | ||
|
|
cad77e2b10 | ||
|
|
3542a7c24d | ||
|
|
0d6d161e23 | ||
|
|
89a2e0086b | ||
|
|
cd443faf61 | ||
|
|
bfc56b02a8 | ||
|
|
25bafc6bf1 | ||
|
|
6387580626 | ||
|
|
09b8aa65fc | ||
|
|
6c15193f32 | ||
|
|
4442e29b66 | ||
|
|
b6f96d9f4d | ||
|
|
36b66100f9 | ||
|
|
49d8143cc6 | ||
|
|
fc279fecaf | ||
|
|
bd307daa57 | ||
|
|
7a72f44722 | ||
|
|
8e63fc6946 | ||
|
|
3a65f8dc16 | ||
|
|
d624352a0b | ||
|
|
3fb597a774 | ||
|
|
85e46bc524 | ||
|
|
d3790878b3 | ||
|
|
e648e50f1b | ||
|
|
cb6cede0b6 | ||
|
|
a0157ddc2a | ||
|
|
8f595fbc7b | ||
|
|
2e3b22c5fa | ||
|
|
42b968372b | ||
|
|
415e7b492f | ||
|
|
2ea8e46723 | ||
|
|
977c6c6dde | ||
|
|
1e5db9dc6c | ||
|
|
252dc10e61 | ||
|
|
f9e2315ba1 | ||
|
|
0eca588280 | ||
|
|
33523bbfb9 | ||
|
|
110f23bdf1 | ||
|
|
50a5cb23f5 | ||
|
|
df39350d7d | ||
|
|
5daacd3ed4 | ||
|
|
f3270e19df | ||
|
|
1b9ad41c87 | ||
|
|
c6586f0eed | ||
|
|
f40bdc8ed9 | ||
|
|
e5ad8bbb70 | ||
|
|
7f6acbfdbc | ||
|
|
2e070529e0 | ||
|
|
f4b31e5f0a | ||
|
|
f7f093a464 | ||
|
|
95cc340de5 | ||
|
|
51a003c816 | ||
|
|
977152139f | ||
|
|
78833df95e | ||
|
|
099d941d2e | ||
|
|
ea58f378a9 | ||
|
|
4060884909 | ||
|
|
d9277c11d2 | ||
|
|
bcbc90820a | ||
|
|
e9caa95673 | ||
|
|
9cd6703b05 | ||
|
|
4233bd7771 | ||
|
|
0e2db441f2 | ||
|
|
571b101ea4 | ||
|
|
0b9dca61ab | ||
|
|
d8fa76d076 | ||
|
|
6116eef513 | ||
|
|
fabe987f2c | ||
|
|
af20880f37 | ||
|
|
44eeab0d4b | ||
|
|
b331900158 | ||
|
|
d74380404c | ||
|
|
3833a9cd6b | ||
|
|
74fdd943c9 | ||
|
|
ad5b772502 | ||
|
|
9bc941aa31 | ||
|
|
705df43047 | ||
|
|
b31e3bb710 | ||
|
|
4082d0a368 | ||
|
|
ce85286cdf | ||
|
|
415cf1a777 | ||
|
|
f249919ec8 | ||
|
|
054c9787d3 | ||
|
|
c1e88e2b5a | ||
|
|
f5efeb16c4 | ||
|
|
59afae7bca | ||
|
|
92c6339064 | ||
|
|
582ae233f2 | ||
|
|
3c3016a211 | ||
|
|
e24e1d9d3c | ||
|
|
85fb564be7 | ||
|
|
18f7db9eee | ||
|
|
ea0770fd11 | ||
|
|
9c28fab3f9 | ||
|
|
bac332a8c4 | ||
|
|
86d2542bd9 | ||
|
|
5f63ec884f | ||
|
|
0f87b33354 | ||
|
|
c7dc99a12f | ||
|
|
05c3c3f8f3 | ||
|
|
2437a43471 | ||
|
|
16cd812ba0 | ||
|
|
c036128720 | ||
|
|
645db15848 | ||
|
|
21e142ffc3 | ||
|
|
23a6b3db72 | ||
|
|
b87d109625 | ||
|
|
651bd295b9 | ||
|
|
01607e92b9 | ||
|
|
f40d54adbb | ||
|
|
60fb387495 | ||
|
|
a7df18723c | ||
|
|
4e39cc937a | ||
|
|
fe513c17ab | ||
|
|
f7fb88ed3d | ||
|
|
b3247c7cbe | ||
|
|
97e79c23f6 | ||
|
|
7803a29875 | ||
|
|
08093beed1 | ||
|
|
ae46ae89bc | ||
|
|
f7977dca39 | ||
|
|
0b344f3e08 | ||
|
|
6058154eca | ||
|
|
941016f04b | ||
|
|
7a15898087 | ||
|
|
6b4effa90f | ||
|
|
77d56e4a4c | ||
|
|
c4dd622e7b | ||
|
|
9f24535142 | ||
|
|
a45161fcb8 | ||
|
|
4379763a73 | ||
|
|
a25b376dfd | ||
|
|
fb429f6777 | ||
|
|
ae33227c3a | ||
|
|
2fedc61d1e | ||
|
|
6988a36dd1 | ||
|
|
60e1124970 | ||
|
|
a007684006 | ||
|
|
090a709522 | ||
|
|
9e06655214 | ||
|
|
3651825e79 | ||
|
|
dcf3160b58 | ||
|
|
fd014f42cd | ||
|
|
a760f2f7fc | ||
|
|
262c114b7d | ||
|
|
c90bdd83bb | ||
|
|
fdb2d37b12 | ||
|
|
7951837ecf | ||
|
|
d2e112a672 | ||
|
|
90e86077ba | ||
|
|
f69dfcdefe | ||
|
|
5cff7c4e72 | ||
|
|
0eaa833cdf | ||
|
|
db015d925e | ||
|
|
69fd47601e | ||
|
|
17521574f0 | ||
|
|
56650a76ae | ||
|
|
984a98e792 | ||
|
|
49eb0cf202 | ||
|
|
8e24a6b036 | ||
|
|
d07bf6d699 | ||
|
|
877d12c676 | ||
|
|
ca629f625a | ||
|
|
087dd9a4b6 | ||
|
|
d9a967c072 | ||
|
|
6d522c82c3 | ||
|
|
867af98083 | ||
|
|
1be058d831 | ||
|
|
71bc044ae5 | ||
|
|
d660c50ade | ||
|
|
3fd733d903 | ||
|
|
7703b2361d | ||
|
|
68fdb9ebc6 | ||
|
|
b6513343be | ||
|
|
cc95ff1c55 | ||
|
|
c954ee0fde | ||
|
|
1ada5e5d18 | ||
|
|
f5244c3d93 | ||
|
|
1f17776cd4 | ||
|
|
ebd4f36f94 | ||
|
|
dbb9b5f254 | ||
|
|
4301472cb2 | ||
|
|
15e7954321 | ||
|
|
32d1de08e9 | ||
|
|
ea88663dce | ||
|
|
97aecaf890 | ||
|
|
b16688e1b7 | ||
|
|
5ac856251b | ||
|
|
62a9aacd85 | ||
|
|
dff0b55a8c | ||
|
|
3b21c75b13 | ||
|
|
2c7cc4b8e5 | ||
|
|
bcf8c0e35c | ||
|
|
11d2936fca | ||
|
|
5e84429e24 | ||
|
|
48c88b2d7e | ||
|
|
e5185c0b77 | ||
|
|
15773d3aba | ||
|
|
b577837446 | ||
|
|
324e234b2a | ||
|
|
d40fefb0ea | ||
|
|
7e05568549 | ||
|
|
0249a52d1c | ||
|
|
5df1c5b09b | ||
|
|
953ec8fe31 | ||
|
|
6054afebdc | ||
|
|
04f356e119 | ||
|
|
a6ebefbb30 | ||
|
|
9a1edbe1fa | ||
|
|
c46d74be54 | ||
|
|
72e949586a | ||
|
|
68dacad741 | ||
|
|
7e6505ca73 | ||
|
|
362d83f504 | ||
|
|
e4879736b7 | ||
|
|
68fe6628c7 | ||
|
|
f39ef569be | ||
|
|
42c199376a | ||
|
|
cd6b774a8c | ||
|
|
62598f94f8 | ||
|
|
aa3b46141f | ||
|
|
dcaa2aaeab | ||
|
|
e36f5e47da | ||
|
|
578d7b41b4 | ||
|
|
c4f81ce279 | ||
|
|
02a7dbea85 | ||
|
|
0305d3fe36 | ||
|
|
84fd034197 | ||
|
|
61b48e2048 | ||
|
|
c91f85389c | ||
|
|
f3683355a9 | ||
|
|
a86da886f4 | ||
|
|
7bf8d42eb8 | ||
|
|
88533e29b9 | ||
|
|
8299dcc881 | ||
|
|
0d0c250fea | ||
|
|
eb8d86616b | ||
|
|
1edc90810f | ||
|
|
c011d84a35 | ||
|
|
b97f62ac12 | ||
|
|
200a38bdc0 | ||
|
|
e12c53a90e | ||
|
|
7892916f56 | ||
|
|
5d681beab3 | ||
|
|
a54a2affb3 | ||
|
|
120062bb85 | ||
|
|
a0754d2bd7 | ||
|
|
f4b13a84d4 | ||
|
|
1c98e27e4d | ||
|
|
6067e25df8 | ||
|
|
304c50a247 | ||
|
|
274ad8eac2 | ||
|
|
9946a3fc0e | ||
|
|
622d6db968 | ||
|
|
d756ce0656 | ||
|
|
316e776355 | ||
|
|
f861b7ad99 | ||
|
|
01382e98f3 | ||
|
|
3371442bbc | ||
|
|
a81a804364 | ||
|
|
b24cf5c946 | ||
|
|
3d60213644 | ||
|
|
a23822c9df | ||
|
|
9fa69758f1 | ||
|
|
c892d3f134 | ||
|
|
73271501dc | ||
|
|
a62ea520e7 | ||
|
|
a2a0b3c71e | ||
|
|
51bedbe003 | ||
|
|
7062a3e657 | ||
|
|
209d95d5da | ||
|
|
95ee74ab2b | ||
|
|
9f6b268dce | ||
|
|
96e8ebcc10 | ||
|
|
ca261c7037 | ||
|
|
a95fe13b31 | ||
|
|
c3fe76adf9 | ||
|
|
e9b9d51a6e | ||
|
|
aff66205e9 | ||
|
|
aa0c9d65ae | ||
|
|
423bd2122f | ||
|
|
7533910114 | ||
|
|
0d4d377c22 | ||
|
|
3a576b3ea5 | ||
|
|
27f37744a0 | ||
|
|
012ecad0bb | ||
|
|
49b5d5e961 | ||
|
|
01ca48fe96 | ||
|
|
5abe5045b2 | ||
|
|
8c79a577b9 | ||
|
|
5b18f1016f | ||
|
|
bda58312bb | ||
|
|
eff352a003 | ||
|
|
3109d4ff82 | ||
|
|
c44eda3af6 | ||
|
|
33e8c41c6f | ||
|
|
e0b515ba2b | ||
|
|
c6b6ad41fb | ||
|
|
99576238cd | ||
|
|
db712650ef | ||
|
|
d77e7b07eb | ||
|
|
47b35fad3f | ||
|
|
720105ca92 | ||
|
|
ce656068e3 | ||
|
|
e69ba76e7d | ||
|
|
d1bab213a3 | ||
|
|
016a9341ad | ||
|
|
81f407ad5f | ||
|
|
37ff2f6bdb | ||
|
|
13cf1a9551 | ||
|
|
799aaac449 | ||
|
|
fbe430e4cf | ||
|
|
511746620e | ||
|
|
fed5b648f8 | ||
|
|
1f601743f6 | ||
|
|
db3ccf5da7 | ||
|
|
038898d580 | ||
|
|
dafbc6bc51 | ||
|
|
3d8271e7a1 | ||
|
|
e2ade95219 | ||
|
|
604971a576 | ||
|
|
76173103af | ||
|
|
3fcbc60232 | ||
|
|
835bdb39d0 | ||
|
|
200f09c5d2 | ||
|
|
99a008f122 | ||
|
|
9b71e89ba0 | ||
|
|
9955369cfa | ||
|
|
6ef6ffab66 | ||
|
|
0dd46f0f20 | ||
|
|
cce0d85dd4 | ||
|
|
1ed3eae038 | ||
|
|
2854c24197 | ||
|
|
4b90ddda2a | ||
|
|
552c39e583 | ||
|
|
5176e459b5 | ||
|
|
39515d0600 | ||
|
|
5314949394 | ||
|
|
59e5be749c | ||
|
|
da252a0070 | ||
|
|
b6199e430c | ||
|
|
e260a68c26 | ||
|
|
570cb2deaf | ||
|
|
7a36f03fb5 | ||
|
|
6c3058ba97 | ||
|
|
48a1034c12 | ||
|
|
aaddb50ab9 | ||
|
|
a947586cfe | ||
|
|
f3cfa14a59 | ||
|
|
a2c0a77c53 | ||
|
|
c078f9d5cb | ||
|
|
08eab8a157 | ||
|
|
9c36f234bc | ||
|
|
36b81d0e2a | ||
|
|
4432c14377 | ||
|
|
99e6dd5ca3 | ||
|
|
6327a25aec | ||
|
|
40989de7f5 | ||
|
|
58af44e795 | ||
|
|
4910cc05f8 | ||
|
|
164ee24d16 | ||
|
|
0ffda4ab7c | ||
|
|
4694a6271d | ||
|
|
2f50bc747a | ||
|
|
a13f621a81 | ||
|
|
0cc520cc67 | ||
|
|
c9451c3f2d | ||
|
|
8da53ffda2 | ||
|
|
4319761687 | ||
|
|
a1f3227cd9 | ||
|
|
b07f87c920 | ||
|
|
9c8f23eb64 | ||
|
|
2d232b3c4b | ||
|
|
712dc9bb34 | ||
|
|
5c338d53ae | ||
|
|
8d26842aab | ||
|
|
76e654304b | ||
|
|
32ba679719 | ||
|
|
cac0fefcdb | ||
|
|
498c76dd96 | ||
|
|
7526182304 | ||
|
|
d46bbd9cbf | ||
|
|
e98b537499 | ||
|
|
fc9d8509e5 | ||
|
|
7c6bbb61b5 | ||
|
|
8501ddc87f | ||
|
|
7d9b94525e | ||
|
|
eb02f0eadf | ||
|
|
69a8ba5aec | ||
|
|
fe49b8e618 | ||
|
|
26bf4dde5f | ||
|
|
e9b73050ba | ||
|
|
bacd0ab43f | ||
|
|
e438051371 | ||
|
|
314155593d | ||
|
|
787f819ce0 | ||
|
|
3632a62ea3 | ||
|
|
c7294df007 | ||
|
|
e3ad7fe3c0 | ||
|
|
6213683ddf | ||
|
|
a4ddfc9970 | ||
|
|
7ff271f9b9 | ||
|
|
d75cdd63a9 | ||
|
|
0a7575d1e4 | ||
|
|
ec3022d8ad | ||
|
|
d42103b91b | ||
|
|
00f7d08b04 | ||
|
|
408cc383cb | ||
|
|
5c926d0ac6 | ||
|
|
5cb88782fc | ||
|
|
5eef4e9ece | ||
|
|
04a2124141 | ||
|
|
1b6229b2a1 | ||
|
|
bca6db50a7 | ||
|
|
f3aae26996 | ||
|
|
f3c17a8e0f | ||
|
|
d6b24f8753 | ||
|
|
253db0a303 | ||
|
|
8499e52461 | ||
|
|
d0153179a9 | ||
|
|
264d03727e | ||
|
|
544405d9b9 | ||
|
|
24488a3b67 | ||
|
|
ae78185b29 | ||
|
|
7f682b24ef | ||
|
|
d42a52d8cf | ||
|
|
b85df15890 | ||
|
|
393499f34f | ||
|
|
c656d0f9b5 | ||
|
|
32017a8859 | ||
|
|
d87c5b1140 | ||
|
|
f59f783d3f | ||
|
|
ec298eac61 | ||
|
|
81a25bb4ee | ||
|
|
e99f044e45 | ||
|
|
5ae25a5fd9 | ||
|
|
e9d1cb4907 | ||
|
|
99f8ca2dca | ||
|
|
ddea5d38b5 | ||
|
|
31d2b83550 | ||
|
|
5535e56ed2 | ||
|
|
d740b95dbc | ||
|
|
ae92e9a945 | ||
|
|
29730c3896 | ||
|
|
a5ae8270f0 | ||
|
|
54f5fa6432 | ||
|
|
0260644063 | ||
|
|
267fc03a82 | ||
|
|
bf1537584c | ||
|
|
9ee7825022 | ||
|
|
2be0c42dd1 | ||
|
|
3423c053a2 | ||
|
|
26923cca00 | ||
|
|
36e027659c | ||
|
|
f447dccdb4 | ||
|
|
69eae32851 | ||
|
|
aa2fcfb8cb | ||
|
|
fae5ef2a41 | ||
|
|
7251ca7d2d | ||
|
|
7cdbd919bf | ||
|
|
d450f95602 | ||
|
|
5a65201971 | ||
|
|
d303b8db3e | ||
|
|
06baa33827 | ||
|
|
42743e637e | ||
|
|
9969fd7dec | ||
|
|
fc6d5c2a1d | ||
|
|
f8abb1ca24 | ||
|
|
a5af38ae3d | ||
|
|
aab70951dc | ||
|
|
334cacf93c | ||
|
|
53024012fc | ||
|
|
86a72f77c1 | ||
|
|
bc11a5ad0a | ||
|
|
8f2d217fd4 | ||
|
|
183727cd50 | ||
|
|
676e87591a | ||
|
|
8c05fc4da0 | ||
|
|
2bab06561e | ||
|
|
dfa7e2f5bb | ||
|
|
78bece5616 | ||
|
|
eeea15e373 | ||
|
|
80cd513ab7 | ||
|
|
942ef1615e | ||
|
|
a354698022 | ||
|
|
0cdea488c9 | ||
|
|
4f4291ac47 | ||
|
|
bf0cf03091 | ||
|
|
f7da09f20f | ||
|
|
be1529331c | ||
|
|
301d658a29 | ||
|
|
1cc54e5b2c | ||
|
|
65a7fd21e7 | ||
|
|
856537c0cd | ||
|
|
b2a88e0063 | ||
|
|
85a7068785 | ||
|
|
cbb733d99a | ||
|
|
ce88c94a19 | ||
|
|
16516915d8 | ||
|
|
6addb8da23 | ||
|
|
bc7f664fd8 | ||
|
|
aac17aa33c | ||
|
|
825500e207 | ||
|
|
4d42016c72 | ||
|
|
9d665df602 | ||
|
|
9087f69fb0 | ||
|
|
2a06f6a214 | ||
|
|
78a8428bd0 | ||
|
|
0d235768fa | ||
|
|
af092bbdec | ||
|
|
4961630d62 | ||
|
|
81920b9ab9 | ||
|
|
9e031d3b5b | ||
|
|
7ae3ed6d2a | ||
|
|
05d79f2b51 | ||
|
|
274e591354 | ||
|
|
95fd152b3d | ||
|
|
ffc91ed6d8 | ||
|
|
de0b759875 | ||
|
|
f041dcf944 | ||
|
|
946b9bd9d1 | ||
|
|
db77a69838 | ||
|
|
d10f4d26e2 | ||
|
|
6b62d686ba | ||
|
|
065826e64d | ||
|
|
a3b096343f | ||
|
|
b33be91b06 | ||
|
|
a94a1816c5 | ||
|
|
9a9e7fea07 | ||
|
|
9a03042077 | ||
|
|
704d3b2d6b | ||
|
|
e5c2be238d | ||
|
|
9feea07527 | ||
|
|
b0967d03b8 | ||
|
|
d33fd71f93 | ||
|
|
226b5b2682 | ||
|
|
f8879b0223 | ||
|
|
ada09d96c4 |
98
.github/workflows/deploy.yml
vendored
98
.github/workflows/deploy.yml
vendored
@@ -1,63 +1,63 @@
|
||||
name: Deploy to GitHub Pages
|
||||
on:
|
||||
push:
|
||||
branches: 'main'
|
||||
push:
|
||||
branches: 'main'
|
||||
|
||||
jobs:
|
||||
build_site:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
build_site:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
cache-dependency-path: |
|
||||
gpx/package-lock.json
|
||||
website/package-lock.json
|
||||
|
||||
- name: Install dependencies for gpx
|
||||
run: npm install --prefix gpx
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: |
|
||||
gpx/package-lock.json
|
||||
website/package-lock.json
|
||||
|
||||
- name: Build gpx
|
||||
run: npm run build --prefix gpx
|
||||
- name: Install dependencies for gpx
|
||||
run: npm install --prefix gpx
|
||||
|
||||
- name: Install dependencies for website
|
||||
run: npm install --prefix website
|
||||
- name: Build gpx
|
||||
run: npm run build --prefix gpx
|
||||
|
||||
- name: Create env file
|
||||
run: |
|
||||
touch website/.env
|
||||
echo PUBLIC_MAPBOX_TOKEN=${{ secrets.PUBLIC_MAPBOX_TOKEN }} >> website/.env
|
||||
cat website/.env
|
||||
- name: Install dependencies for website
|
||||
run: npm install --prefix website
|
||||
|
||||
- name: Build website
|
||||
env:
|
||||
BASE_PATH: ''
|
||||
run: |
|
||||
npm run build --prefix website
|
||||
- name: Create env file
|
||||
run: |
|
||||
touch website/.env
|
||||
echo PUBLIC_MAPBOX_TOKEN=${{ secrets.PUBLIC_MAPBOX_TOKEN }} >> website/.env
|
||||
cat website/.env
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: 'website/build/'
|
||||
- name: Build website
|
||||
env:
|
||||
BASE_PATH: ''
|
||||
run: |
|
||||
npm run build --prefix website
|
||||
|
||||
deploy:
|
||||
needs: build_site
|
||||
runs-on: ubuntu-latest
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: 'website/build/'
|
||||
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
deploy:
|
||||
needs: build_site
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Deploy
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
steps:
|
||||
- name: Deploy
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 gpx.studio
|
||||
Copyright (c) 2025 gpx.studio
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
246
gpx/src/gpx.ts
246
gpx/src/gpx.ts
@@ -17,6 +17,9 @@ import {
|
||||
import { immerable, isDraft, original, freeze } from 'immer';
|
||||
|
||||
function cloneJSON<T>(obj: T): T {
|
||||
if (obj === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return null;
|
||||
}
|
||||
@@ -818,9 +821,6 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
|
||||
statistics.local.points = this.trkpt.map((point) => point);
|
||||
|
||||
statistics.local.elevation.smoothed = this._computeSmoothedElevation();
|
||||
statistics.local.slope.at = this._computeSlope();
|
||||
|
||||
const points = this.trkpt;
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
points[i]._data['index'] = i;
|
||||
@@ -835,21 +835,6 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
|
||||
statistics.local.distance.total.push(statistics.global.distance.total);
|
||||
|
||||
// elevation
|
||||
if (i > 0) {
|
||||
const ele =
|
||||
statistics.local.elevation.smoothed[i] -
|
||||
statistics.local.elevation.smoothed[i - 1];
|
||||
if (ele > 0) {
|
||||
statistics.global.elevation.gain += ele;
|
||||
} else if (ele < 0) {
|
||||
statistics.global.elevation.loss -= ele;
|
||||
}
|
||||
}
|
||||
|
||||
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
|
||||
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
|
||||
|
||||
// time
|
||||
if (points[i].time === undefined) {
|
||||
statistics.local.time.total.push(0);
|
||||
@@ -960,8 +945,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
}
|
||||
}
|
||||
|
||||
[statistics.local.slope.segment, statistics.local.slope.length] =
|
||||
this._computeSlopeSegments(statistics);
|
||||
this._elevationComputation(statistics);
|
||||
|
||||
statistics.global.time.total =
|
||||
statistics.global.time.start && statistics.global.time.end
|
||||
@@ -977,59 +961,63 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
? statistics.global.distance.moving / (statistics.global.time.moving / 3600)
|
||||
: 0;
|
||||
|
||||
statistics.local.speed = distanceWindowSmoothingWithDistanceAccumulator(
|
||||
points,
|
||||
200,
|
||||
(accumulated, start, end) =>
|
||||
points[start].time && points[end].time
|
||||
? (3600 * accumulated) /
|
||||
(points[end].time.getTime() - points[start].time.getTime())
|
||||
: undefined
|
||||
statistics.local.speed = timeWindowSmoothing(points, 10000, (start, end) =>
|
||||
points[start].time && points[end].time
|
||||
? (3600 *
|
||||
(statistics.local.distance.total[end] -
|
||||
statistics.local.distance.total[start])) /
|
||||
Math.max((points[end].time.getTime() - points[start].time.getTime()) / 1000, 1)
|
||||
: undefined
|
||||
);
|
||||
|
||||
return statistics;
|
||||
}
|
||||
|
||||
_computeSmoothedElevation(): number[] {
|
||||
const points = this.trkpt;
|
||||
|
||||
let smoothed = distanceWindowSmoothing(
|
||||
points,
|
||||
100,
|
||||
(index) => points[index].ele ?? 0,
|
||||
(accumulated, start, end) => accumulated / (end - start + 1)
|
||||
);
|
||||
|
||||
if (points.length > 0) {
|
||||
smoothed[0] = points[0].ele ?? 0;
|
||||
smoothed[points.length - 1] = points[points.length - 1].ele ?? 0;
|
||||
}
|
||||
|
||||
return smoothed;
|
||||
}
|
||||
|
||||
_computeSlope(): number[] {
|
||||
const points = this.trkpt;
|
||||
|
||||
return distanceWindowSmoothingWithDistanceAccumulator(
|
||||
points,
|
||||
50,
|
||||
(accumulated, start, end) =>
|
||||
(100 * ((points[end].ele ?? 0) - (points[start].ele ?? 0))) /
|
||||
(accumulated > 0 ? accumulated : 1)
|
||||
);
|
||||
}
|
||||
|
||||
_computeSlopeSegments(statistics: GPXStatistics): [number[], number[]] {
|
||||
_elevationComputation(statistics: GPXStatistics) {
|
||||
let simplified = ramerDouglasPeucker(
|
||||
this.trkpt,
|
||||
20,
|
||||
getElevationDistanceFunction(statistics)
|
||||
);
|
||||
|
||||
for (let i = 0; i < simplified.length - 1; i++) {
|
||||
let start = simplified[i].point._data.index;
|
||||
let end = simplified[i + 1].point._data.index;
|
||||
|
||||
let cumulEle = 0;
|
||||
let currentStart = start;
|
||||
let currentEnd = start;
|
||||
let smoothedEle = distanceWindowSmoothing(start, end + 1, statistics, 0.1, (s, e) => {
|
||||
for (let i = currentStart; i < s; i++) {
|
||||
cumulEle -= this.trkpt[i].ele ?? 0;
|
||||
}
|
||||
for (let i = currentEnd; i <= e; i++) {
|
||||
cumulEle += this.trkpt[i].ele ?? 0;
|
||||
}
|
||||
currentStart = s;
|
||||
currentEnd = e + 1;
|
||||
return cumulEle / (e - s + 1);
|
||||
});
|
||||
smoothedEle[0] = this.trkpt[start].ele ?? 0;
|
||||
smoothedEle[smoothedEle.length - 1] = this.trkpt[end].ele ?? 0;
|
||||
|
||||
for (let j = start; j < end; j++) {
|
||||
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
|
||||
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
|
||||
|
||||
const ele = smoothedEle[j - start + 1] - smoothedEle[j - start];
|
||||
if (ele > 0) {
|
||||
statistics.global.elevation.gain += ele;
|
||||
} else if (ele < 0) {
|
||||
statistics.global.elevation.loss -= ele;
|
||||
}
|
||||
}
|
||||
}
|
||||
statistics.local.elevation.gain.push(statistics.global.elevation.gain);
|
||||
statistics.local.elevation.loss.push(statistics.global.elevation.loss);
|
||||
|
||||
let slope = [];
|
||||
let length = [];
|
||||
|
||||
for (let i = 0; i < simplified.length - 1; i++) {
|
||||
let start = simplified[i].point._data.index;
|
||||
let end = simplified[i + 1].point._data.index;
|
||||
@@ -1043,7 +1031,20 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
}
|
||||
}
|
||||
|
||||
return [slope, length];
|
||||
statistics.local.slope.segment = slope;
|
||||
statistics.local.slope.length = length;
|
||||
statistics.local.slope.at = distanceWindowSmoothing(
|
||||
0,
|
||||
this.trkpt.length,
|
||||
statistics,
|
||||
0.05,
|
||||
(start, end) => {
|
||||
const ele = this.trkpt[end].ele - this.trkpt[start].ele || 0;
|
||||
const dist =
|
||||
statistics.local.distance.total[end] - statistics.local.distance.total[start];
|
||||
return dist > 0 ? (0.1 * ele) / dist : 0;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getNumberOfTrackPoints(): number {
|
||||
@@ -1290,8 +1291,14 @@ export class TrackSegment extends GPXTreeLeaf {
|
||||
lastPoint: TrackPoint | undefined
|
||||
) {
|
||||
let og = getOriginal(this); // Read as much as possible from the original object because it is faster
|
||||
let slope = og._computeSlope();
|
||||
let trkpt = withArtificialTimestamps(og.trkpt, totalTime, lastPoint, startTime, slope);
|
||||
let statistics = og._computeStatistics();
|
||||
let trkpt = withArtificialTimestamps(
|
||||
og.trkpt,
|
||||
totalTime,
|
||||
lastPoint,
|
||||
startTime,
|
||||
statistics.local.slope.at
|
||||
);
|
||||
this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
||||
}
|
||||
|
||||
@@ -1488,12 +1495,18 @@ export class Waypoint {
|
||||
this.attributes = waypoint.attributes;
|
||||
this.ele = waypoint.ele;
|
||||
this.time = waypoint.time;
|
||||
this.name = waypoint.name;
|
||||
this.cmt = waypoint.cmt;
|
||||
this.desc = waypoint.desc;
|
||||
this.link = waypoint.link;
|
||||
this.sym = waypoint.sym;
|
||||
this.type = waypoint.type;
|
||||
this.name = waypoint.name === '' ? undefined : waypoint.name;
|
||||
this.cmt = waypoint.cmt === '' ? undefined : waypoint.cmt;
|
||||
this.desc = waypoint.desc === '' ? undefined : waypoint.desc;
|
||||
this.link =
|
||||
!waypoint.link ||
|
||||
!waypoint.link.attributes ||
|
||||
!waypoint.link.attributes.href ||
|
||||
waypoint.link.attributes.href === ''
|
||||
? undefined
|
||||
: waypoint.link;
|
||||
this.sym = waypoint.sym === '' ? undefined : waypoint.sym;
|
||||
this.type = waypoint.type === '' ? undefined : waypoint.type;
|
||||
if (waypoint.hasOwnProperty('_data')) {
|
||||
this._data = waypoint._data;
|
||||
}
|
||||
@@ -1647,7 +1660,6 @@ export class GPXStatistics {
|
||||
};
|
||||
speed: number[];
|
||||
elevation: {
|
||||
smoothed: number[];
|
||||
gain: number[];
|
||||
loss: number[];
|
||||
};
|
||||
@@ -1718,7 +1730,6 @@ export class GPXStatistics {
|
||||
},
|
||||
speed: [],
|
||||
elevation: {
|
||||
smoothed: [],
|
||||
gain: [],
|
||||
loss: [],
|
||||
},
|
||||
@@ -1753,9 +1764,6 @@ export class GPXStatistics {
|
||||
);
|
||||
|
||||
this.local.speed = this.local.speed.concat(other.local.speed);
|
||||
this.local.elevation.smoothed = this.local.elevation.smoothed.concat(
|
||||
other.local.elevation.smoothed
|
||||
);
|
||||
this.local.slope.at = this.local.slope.at.concat(other.local.slope.at);
|
||||
this.local.slope.segment = this.local.slope.segment.concat(other.local.slope.segment);
|
||||
this.local.slope.length = this.local.slope.length.concat(other.local.slope.length);
|
||||
@@ -1911,11 +1919,15 @@ export function distance(
|
||||
const rad = Math.PI / 180;
|
||||
const lat1 = coord1.lat * rad;
|
||||
const lat2 = coord2.lat * rad;
|
||||
const dLat = lat2 - lat1;
|
||||
const dLon = (coord2.lon - coord1.lon) * rad;
|
||||
|
||||
// Haversine formula - better numerical stability for small distances
|
||||
const a =
|
||||
Math.sin(lat1) * Math.sin(lat2) +
|
||||
Math.cos(lat1) * Math.cos(lat2) * Math.cos((coord2.lon - coord1.lon) * rad);
|
||||
const maxMeters = earthRadius * Math.acos(Math.min(a, 1));
|
||||
return maxMeters;
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
const c = 2 * Math.asin(Math.sqrt(Math.min(a, 1)));
|
||||
return earthRadius * c;
|
||||
}
|
||||
|
||||
export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
||||
@@ -1942,57 +1954,59 @@ export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
||||
};
|
||||
}
|
||||
|
||||
function distanceWindowSmoothing(
|
||||
points: TrackPoint[],
|
||||
distanceWindow: number,
|
||||
accumulate: (index: number) => number,
|
||||
compute: (accumulated: number, start: number, end: number) => number,
|
||||
remove?: (index: number) => number
|
||||
function windowSmoothing(
|
||||
left: number,
|
||||
right: number,
|
||||
distance: (index1: number, index2: number) => number,
|
||||
window: number,
|
||||
compute: (start: number, end: number) => number
|
||||
): number[] {
|
||||
let result = [];
|
||||
|
||||
let start = 0,
|
||||
end = 0,
|
||||
accumulated = 0;
|
||||
for (var i = 0; i < points.length; i++) {
|
||||
while (
|
||||
start + 1 < i &&
|
||||
distance(points[start].getCoordinates(), points[i].getCoordinates()) > distanceWindow
|
||||
) {
|
||||
if (remove) {
|
||||
accumulated -= remove(start);
|
||||
} else {
|
||||
accumulated -= accumulate(start);
|
||||
}
|
||||
let start = left;
|
||||
for (var i = left; i < right; i++) {
|
||||
while (start + 1 < i && distance(start, i) > window) {
|
||||
start++;
|
||||
}
|
||||
while (
|
||||
end < points.length &&
|
||||
distance(points[i].getCoordinates(), points[end].getCoordinates()) <= distanceWindow
|
||||
) {
|
||||
accumulated += accumulate(end);
|
||||
let end = Math.min(i + 2, right);
|
||||
while (end < right && distance(i, end) <= window) {
|
||||
end++;
|
||||
}
|
||||
result[i] = compute(accumulated, start, end - 1);
|
||||
result.push(compute(start, end - 1));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function distanceWindowSmoothingWithDistanceAccumulator(
|
||||
points: TrackPoint[],
|
||||
distanceWindow: number,
|
||||
compute: (accumulated: number, start: number, end: number) => number
|
||||
function distanceWindowSmoothing(
|
||||
left: number,
|
||||
right: number,
|
||||
statistics: GPXStatistics,
|
||||
window: number,
|
||||
compute: (start: number, end: number) => number
|
||||
): number[] {
|
||||
return distanceWindowSmoothing(
|
||||
points,
|
||||
distanceWindow,
|
||||
(index) =>
|
||||
index > 0
|
||||
? distance(points[index - 1].getCoordinates(), points[index].getCoordinates())
|
||||
: 0,
|
||||
compute,
|
||||
(index) => distance(points[index].getCoordinates(), points[index + 1].getCoordinates())
|
||||
return windowSmoothing(
|
||||
left,
|
||||
right,
|
||||
(index1, index2) =>
|
||||
statistics.local.distance.total[index2] - statistics.local.distance.total[index1],
|
||||
window,
|
||||
compute
|
||||
);
|
||||
}
|
||||
|
||||
function timeWindowSmoothing(
|
||||
points: TrackPoint[],
|
||||
window: number,
|
||||
compute: (start: number, end: number) => number
|
||||
): number[] {
|
||||
return windowSmoothing(
|
||||
0,
|
||||
points.length,
|
||||
(index1, index2) =>
|
||||
points[index2].time?.getTime() - points[index1].time?.getTime() || 2 * window,
|
||||
window,
|
||||
compute
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ import { Coordinates } from './types';
|
||||
|
||||
export type SimplifiedTrackPoint = { point: TrackPoint; distance?: number };
|
||||
|
||||
const earthRadius = 6371008.8;
|
||||
|
||||
export function ramerDouglasPeucker(
|
||||
points: TrackPoint[],
|
||||
epsilon: number = 50,
|
||||
@@ -72,65 +70,45 @@ export function crossarcDistance(
|
||||
);
|
||||
}
|
||||
|
||||
const metersPerLatitudeDegree = 111320;
|
||||
|
||||
function getMetersPerLongitudeDegree(latitude: number): number {
|
||||
return Math.cos((latitude * Math.PI) / 180) * metersPerLatitudeDegree;
|
||||
}
|
||||
|
||||
function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number {
|
||||
// Calculates the shortest distance in meters
|
||||
// between an arc (defined by p1 and p2) and a third point, p3.
|
||||
// Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees.
|
||||
// Calculates the perpendicular distance in meters
|
||||
// between a line segment (defined by p1 and p2) and a third point, p3.
|
||||
// Uses simple planar geometry (ignores earth curvature).
|
||||
|
||||
const rad = Math.PI / 180;
|
||||
const lat1 = coord1.lat * rad;
|
||||
const lat2 = coord2.lat * rad;
|
||||
const lat3 = coord3.lat * rad;
|
||||
// Convert to meters using approximate scaling
|
||||
const metersPerLongitudeDegree = getMetersPerLongitudeDegree(coord1.lat);
|
||||
|
||||
const lon1 = coord1.lon * rad;
|
||||
const lon2 = coord2.lon * rad;
|
||||
const lon3 = coord3.lon * rad;
|
||||
const x1 = coord1.lon * metersPerLongitudeDegree;
|
||||
const y1 = coord1.lat * metersPerLatitudeDegree;
|
||||
const x2 = coord2.lon * metersPerLongitudeDegree;
|
||||
const y2 = coord2.lat * metersPerLatitudeDegree;
|
||||
const x3 = coord3.lon * metersPerLongitudeDegree;
|
||||
const y3 = coord3.lat * metersPerLatitudeDegree;
|
||||
|
||||
// Prerequisites for the formulas
|
||||
const bear12 = bearing(lat1, lon1, lat2, lon2);
|
||||
const bear13 = bearing(lat1, lon1, lat3, lon3);
|
||||
let dis13 = distance(lat1, lon1, lat3, lon3);
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const segmentLengthSquared = dx * dx + dy * dy;
|
||||
|
||||
let diff = Math.abs(bear13 - bear12);
|
||||
if (diff > Math.PI) {
|
||||
diff = 2 * Math.PI - diff;
|
||||
if (segmentLengthSquared === 0) {
|
||||
// p1 and p2 are the same point
|
||||
return Math.sqrt((x3 - x1) * (x3 - x1) + (y3 - y1) * (y3 - y1));
|
||||
}
|
||||
|
||||
// Is relative bearing obtuse?
|
||||
if (diff > Math.PI / 2) {
|
||||
return dis13;
|
||||
}
|
||||
// Project p3 onto the line defined by p1-p2
|
||||
const t = Math.max(0, Math.min(1, ((x3 - x1) * dx + (y3 - y1) * dy) / segmentLengthSquared));
|
||||
|
||||
// Find the cross-track distance.
|
||||
let dxt = Math.asin(Math.sin(dis13 / earthRadius) * Math.sin(bear13 - bear12)) * earthRadius;
|
||||
// Find the closest point on the segment
|
||||
const projX = x1 + t * dx;
|
||||
const projY = y1 + t * dy;
|
||||
|
||||
// Is p4 beyond the arc?
|
||||
let dis12 = distance(lat1, lon1, lat2, lon2);
|
||||
let dis14 =
|
||||
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
|
||||
if (dis14 > dis12) {
|
||||
return distance(lat2, lon2, lat3, lon3);
|
||||
} else {
|
||||
return Math.abs(dxt);
|
||||
}
|
||||
}
|
||||
|
||||
function distance(latA: number, lonA: number, latB: number, lonB: number): number {
|
||||
// Finds the distance between two lat / lon points.
|
||||
return (
|
||||
Math.acos(
|
||||
Math.sin(latA) * Math.sin(latB) +
|
||||
Math.cos(latA) * Math.cos(latB) * Math.cos(lonB - lonA)
|
||||
) * earthRadius
|
||||
);
|
||||
}
|
||||
|
||||
function bearing(latA: number, lonA: number, latB: number, lonB: number): number {
|
||||
// Finds the bearing from one lat / lon point to another.
|
||||
return Math.atan2(
|
||||
Math.sin(lonB - lonA) * Math.cos(latB),
|
||||
Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA)
|
||||
);
|
||||
// Return distance from p3 to the projected point
|
||||
return Math.sqrt((x3 - projX) * (x3 - projX) + (y3 - projY) * (y3 - projY));
|
||||
}
|
||||
|
||||
export function projectedPoint(
|
||||
@@ -146,56 +124,39 @@ export function projectedPoint(
|
||||
}
|
||||
|
||||
function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates {
|
||||
// Calculates the point on the line defined by p1 and p2
|
||||
// Calculates the point on the line segment defined by p1 and p2
|
||||
// that is closest to the third point, p3.
|
||||
// Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees.
|
||||
// Uses simple planar geometry (ignores earth curvature).
|
||||
|
||||
const rad = Math.PI / 180;
|
||||
const lat1 = coord1.lat * rad;
|
||||
const lat2 = coord2.lat * rad;
|
||||
const lat3 = coord3.lat * rad;
|
||||
// Convert to meters using approximate scaling
|
||||
const metersPerLongitudeDegree = getMetersPerLongitudeDegree(coord1.lat);
|
||||
|
||||
const lon1 = coord1.lon * rad;
|
||||
const lon2 = coord2.lon * rad;
|
||||
const lon3 = coord3.lon * rad;
|
||||
const x1 = coord1.lon * metersPerLongitudeDegree;
|
||||
const y1 = coord1.lat * metersPerLatitudeDegree;
|
||||
const x2 = coord2.lon * metersPerLongitudeDegree;
|
||||
const y2 = coord2.lat * metersPerLatitudeDegree;
|
||||
const x3 = coord3.lon * metersPerLongitudeDegree;
|
||||
const y3 = coord3.lat * metersPerLatitudeDegree;
|
||||
|
||||
// Prerequisites for the formulas
|
||||
const bear12 = bearing(lat1, lon1, lat2, lon2);
|
||||
const bear13 = bearing(lat1, lon1, lat3, lon3);
|
||||
let dis13 = distance(lat1, lon1, lat3, lon3);
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const segmentLengthSquared = dx * dx + dy * dy;
|
||||
|
||||
let diff = Math.abs(bear13 - bear12);
|
||||
if (diff > Math.PI) {
|
||||
diff = 2 * Math.PI - diff;
|
||||
}
|
||||
|
||||
// Is relative bearing obtuse?
|
||||
if (diff > Math.PI / 2) {
|
||||
if (segmentLengthSquared === 0) {
|
||||
// p1 and p2 are the same point
|
||||
return coord1;
|
||||
}
|
||||
|
||||
// Find the cross-track distance.
|
||||
let dxt = Math.asin(Math.sin(dis13 / earthRadius) * Math.sin(bear13 - bear12)) * earthRadius;
|
||||
// Project p3 onto the line defined by p1-p2
|
||||
const t = Math.max(0, Math.min(1, ((x3 - x1) * dx + (y3 - y1) * dy) / segmentLengthSquared));
|
||||
|
||||
// Is p4 beyond the arc?
|
||||
let dis12 = distance(lat1, lon1, lat2, lon2);
|
||||
let dis14 =
|
||||
Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
|
||||
if (dis14 > dis12) {
|
||||
return coord2;
|
||||
} else {
|
||||
// Determine the closest point (p4) on the great circle
|
||||
const f = dis14 / earthRadius;
|
||||
const lat4 = Math.asin(
|
||||
Math.sin(lat1) * Math.cos(f) + Math.cos(lat1) * Math.sin(f) * Math.cos(bear12)
|
||||
);
|
||||
const lon4 =
|
||||
lon1 +
|
||||
Math.atan2(
|
||||
Math.sin(bear12) * Math.sin(f) * Math.cos(lat1),
|
||||
Math.cos(f) - Math.sin(lat1) * Math.sin(lat4)
|
||||
);
|
||||
// Find the closest point on the segment
|
||||
const projX = x1 + t * dx;
|
||||
const projY = y1 + t * dy;
|
||||
|
||||
return { lat: lat4 / rad, lon: lon4 / rad };
|
||||
}
|
||||
// Convert back to degrees
|
||||
return {
|
||||
lat: projY / metersPerLatitudeDegree,
|
||||
lon: projX / metersPerLongitudeDegree,
|
||||
};
|
||||
}
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "gpx.studio",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
92
website/package-lock.json
generated
92
website/package-lock.json
generated
@@ -22,7 +22,7 @@
|
||||
"gpx": "file:../gpx",
|
||||
"immer": "^10.1.1",
|
||||
"jszip": "^3.10.1",
|
||||
"mapbox-gl": "^3.12.0",
|
||||
"mapbox-gl": "^3.16.0",
|
||||
"mapillary-js": "^4.1.2",
|
||||
"png.js": "^0.2.1",
|
||||
"sanitize-html": "^2.17.0",
|
||||
@@ -1701,9 +1701,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/point-geometry": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
|
||||
"integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ=="
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz",
|
||||
"integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@mapbox/polyline": {
|
||||
"version": "1.2.1",
|
||||
@@ -1738,11 +1739,26 @@
|
||||
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw=="
|
||||
},
|
||||
"node_modules/@mapbox/vector-tile": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz",
|
||||
"integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz",
|
||||
"integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "~0.1.0"
|
||||
"@mapbox/point-geometry": "~1.1.0",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"pbf": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/vector-tile/node_modules/pbf": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
|
||||
"integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"resolve-protobuf-schema": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbf": "bin/pbf"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/whoots-js": {
|
||||
@@ -2644,7 +2660,8 @@
|
||||
"node_modules/@types/mapbox__point-geometry": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
|
||||
"integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA=="
|
||||
"integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mapbox__sphericalmercator": {
|
||||
"version": "1.2.3",
|
||||
@@ -2660,16 +2677,6 @@
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mapbox__vector-tile": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz",
|
||||
"integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*",
|
||||
"@types/mapbox__point-geometry": "*",
|
||||
"@types/pbf": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mapbox-gl": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz",
|
||||
@@ -4947,9 +4954,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/gl-matrix": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
|
||||
"integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
|
||||
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "11.0.2",
|
||||
@@ -6061,9 +6069,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mapbox-gl": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.12.0.tgz",
|
||||
"integrity": "sha512-DV6TRr+xoPrLSKuGiUcbyLVkoLdNaNNpn6O7+ZC27yQH7BOOIF7l6JKbTCMhfMJuZBVJfL8YRJjlMJ6MZCTggA==",
|
||||
"version": "3.16.0",
|
||||
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.16.0.tgz",
|
||||
"integrity": "sha512-rluV1Zp/0oHf1Y9BV+nePRNnKyTdljko3E19CzO5rBqtQaNUYS0ePCMPRtxOuWRwSdKp3f9NWJkOCjemM8nmjw==",
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"workspaces": [
|
||||
"src/style-spec",
|
||||
@@ -6072,33 +6080,43 @@
|
||||
"dependencies": {
|
||||
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
||||
"@mapbox/mapbox-gl-supported": "^3.0.0",
|
||||
"@mapbox/point-geometry": "^0.1.0",
|
||||
"@mapbox/point-geometry": "^1.1.0",
|
||||
"@mapbox/tiny-sdf": "^2.0.6",
|
||||
"@mapbox/unitbezier": "^0.0.1",
|
||||
"@mapbox/vector-tile": "^1.3.1",
|
||||
"@mapbox/vector-tile": "^2.0.4",
|
||||
"@mapbox/whoots-js": "^3.1.0",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"@types/geojson-vt": "^3.2.5",
|
||||
"@types/mapbox__point-geometry": "^0.1.4",
|
||||
"@types/mapbox__vector-tile": "^1.3.4",
|
||||
"@types/pbf": "^3.0.5",
|
||||
"@types/supercluster": "^7.1.3",
|
||||
"cheap-ruler": "^4.0.0",
|
||||
"csscolorparser": "~1.0.3",
|
||||
"earcut": "^3.0.1",
|
||||
"geojson-vt": "^4.0.2",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"gl-matrix": "^3.4.4",
|
||||
"grid-index": "^1.1.0",
|
||||
"kdbush": "^4.0.2",
|
||||
"martinez-polygon-clipping": "^0.7.4",
|
||||
"murmurhash-js": "^1.0.0",
|
||||
"pbf": "^3.2.1",
|
||||
"pbf": "^4.0.1",
|
||||
"potpack": "^2.0.0",
|
||||
"quickselect": "^3.0.0",
|
||||
"serialize-to-js": "^3.1.2",
|
||||
"supercluster": "^8.0.1",
|
||||
"tinyqueue": "^3.0.0",
|
||||
"vt-pbf": "^3.1.3"
|
||||
"tinyqueue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mapbox-gl/node_modules/pbf": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
|
||||
"integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"resolve-protobuf-schema": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbf": "bin/pbf"
|
||||
}
|
||||
},
|
||||
"node_modules/mapillary-js": {
|
||||
@@ -9021,16 +9039,6 @@
|
||||
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vt-pbf": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
|
||||
"integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "0.1.0",
|
||||
"@mapbox/vector-tile": "^1.3.1",
|
||||
"pbf": "^3.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
"gpx": "file:../gpx",
|
||||
"immer": "^10.1.1",
|
||||
"jszip": "^3.10.1",
|
||||
"mapbox-gl": "^3.12.0",
|
||||
"mapbox-gl": "^3.16.0",
|
||||
"mapillary-js": "^4.1.2",
|
||||
"png.js": "^0.2.1",
|
||||
"sanitize-html": "^2.17.0",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
--support: rgb(220 15 130);
|
||||
--link: rgb(0 110 180);
|
||||
--selection: hsl(240 4.8% 93%);
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
@@ -69,6 +70,7 @@
|
||||
|
||||
--support: rgb(255 110 190);
|
||||
--link: rgb(80 190 255);
|
||||
--selection: hsl(240 3.7% 22%);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
{i18n._('homepage.home')}
|
||||
</Button>
|
||||
<Button
|
||||
data-sveltekit-reload
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href={getURLForLanguage(i18n.lang, '/app')}
|
||||
@@ -70,15 +71,6 @@
|
||||
<Logo company="facebook" class="h-4 fill-muted-foreground" />
|
||||
{i18n._('homepage.facebook')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href="https://x.com/gpxstudio"
|
||||
target="_blank"
|
||||
>
|
||||
<Logo company="x" class="h-4 fill-muted-foreground" />
|
||||
{i18n._('homepage.x')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
...others
|
||||
}: {
|
||||
iconOnly?: boolean;
|
||||
company?: 'gpx.studio' | 'mapbox' | 'github' | 'crowdin' | 'facebook' | 'x' | 'reddit';
|
||||
company?: 'gpx.studio' | 'mapbox' | 'github' | 'crowdin' | 'facebook' | 'reddit';
|
||||
[key: string]: any;
|
||||
} = $props();
|
||||
</script>
|
||||
@@ -55,16 +55,6 @@
|
||||
d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"
|
||||
/></svg
|
||||
>
|
||||
{:else if company === 'x'}
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="fill-foreground {others.class ?? ''}"
|
||||
><title>X</title><path
|
||||
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
|
||||
/></svg
|
||||
>
|
||||
{:else if company === 'reddit'}
|
||||
<svg
|
||||
role="img"
|
||||
|
||||
@@ -644,6 +644,19 @@
|
||||
} else if (e.key === 'F5') {
|
||||
$routing = !$routing;
|
||||
e.preventDefault();
|
||||
} else if (
|
||||
e.key === 'ArrowRight' ||
|
||||
e.key === 'ArrowDown' ||
|
||||
e.key === 'ArrowLeft' ||
|
||||
e.key === 'ArrowUp'
|
||||
) {
|
||||
if (!targetInput) {
|
||||
selection.updateFromKey(
|
||||
e.key === 'ArrowRight' || e.key === 'ArrowDown',
|
||||
e.shiftKey
|
||||
);
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}}
|
||||
on:dragover={(e) => e.preventDefault()}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
{i18n._('homepage.home')}
|
||||
</Button>
|
||||
<Button
|
||||
data-sveltekit-reload
|
||||
variant="link"
|
||||
class="text-base px-0 has-[>svg]:px-0"
|
||||
href={getURLForLanguage(i18n.lang, '/app')}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import { loadFile } from '$lib/logic/file-actions';
|
||||
import { selection } from '$lib/logic/selection';
|
||||
import { untrack } from 'svelte';
|
||||
import { isSelected, toggle } from '$lib/components/map/layer-control/utils';
|
||||
|
||||
let {
|
||||
useHash = true,
|
||||
@@ -32,6 +33,7 @@
|
||||
|
||||
const {
|
||||
currentBasemap,
|
||||
selectedBasemapTree,
|
||||
distanceUnits,
|
||||
velocityUnits,
|
||||
temperatureUnits,
|
||||
@@ -66,6 +68,9 @@
|
||||
if (allowedEmbeddingBasemaps.includes(options.basemap)) {
|
||||
$currentBasemap = options.basemap;
|
||||
}
|
||||
if (!isSelected($selectedBasemapTree, options.basemap)) {
|
||||
$selectedBasemapTree = toggle($selectedBasemapTree, options.basemap);
|
||||
}
|
||||
$distanceMarkers = options.distanceMarkers;
|
||||
$directionMarkers = options.directionMarkers;
|
||||
$distanceUnits = options.distanceUnits;
|
||||
|
||||
@@ -121,20 +121,16 @@
|
||||
}
|
||||
|
||||
.vertical :global(button) {
|
||||
@apply hover:bg-muted;
|
||||
}
|
||||
|
||||
.vertical :global(.sortable-selected button) {
|
||||
@apply hover:bg-accent;
|
||||
@apply hover:bg-[var(--selection)];
|
||||
}
|
||||
|
||||
.vertical :global(.sortable-selected) {
|
||||
@apply bg-accent;
|
||||
@apply bg-[var(--selection)];
|
||||
}
|
||||
|
||||
.horizontal :global(button) {
|
||||
@apply bg-accent;
|
||||
@apply hover:bg-muted;
|
||||
@apply bg-[var(--selection)];
|
||||
@apply hover:bg-background;
|
||||
}
|
||||
|
||||
.horizontal :global(.sortable-selected button) {
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
let file = fileStateCollection.getFile(item.getFileId());
|
||||
if (layer && file) {
|
||||
let waypoint = file.wpt[item.getWaypointIndex()];
|
||||
if (waypoint) {
|
||||
if (waypoint && !waypoint._data.hidden) {
|
||||
waypointPopup?.setItem({
|
||||
item: waypoint,
|
||||
fileId: item.getFileId(),
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||
import { fileActions } from '$lib/logic/file-actions';
|
||||
import type { PopupItem } from '$lib/components/map/map-popup';
|
||||
import { selection } from '$lib/logic/selection';
|
||||
import { ListFileItem } from '$lib/components/file-list/file-list';
|
||||
|
||||
let {
|
||||
waypoint,
|
||||
@@ -20,6 +22,9 @@
|
||||
waypoint: PopupItem<Waypoint>;
|
||||
} = $props();
|
||||
|
||||
let selected = $derived(
|
||||
waypoint.fileId ? $selection.hasAnyChildren(new ListFileItem(waypoint.fileId)) : false
|
||||
);
|
||||
let symbolKey = $derived(waypoint ? getSymbolKey(waypoint.item.sym) : undefined);
|
||||
|
||||
function sanitize(text: string | undefined): string {
|
||||
@@ -81,7 +86,7 @@
|
||||
</ScrollArea>
|
||||
<div class="mt-2 flex flex-col gap-1">
|
||||
<CopyCoordinates coordinates={waypoint.item.attributes} />
|
||||
{#if $currentTool === Tool.WAYPOINT}
|
||||
{#if $currentTool === Tool.WAYPOINT && selected}
|
||||
<Button
|
||||
class="p-1 has-[>svg]:px-2 h-8"
|
||||
variant="outline"
|
||||
|
||||
@@ -8,14 +8,7 @@ import { allHidden } from '$lib/logic/hidden';
|
||||
|
||||
const { distanceMarkers, distanceUnits } = settings;
|
||||
|
||||
const stops = [
|
||||
[100, 0],
|
||||
[50, 7],
|
||||
[25, 8, 10],
|
||||
[10, 10],
|
||||
[5, 11],
|
||||
[1, 13],
|
||||
];
|
||||
const levels = [100, 50, 25, 10, 5, 1];
|
||||
|
||||
export class DistanceMarkers {
|
||||
updateBinded: () => void = this.update.bind(this);
|
||||
@@ -50,43 +43,50 @@ export class DistanceMarkers {
|
||||
data: this.getDistanceMarkersGeoJSON(),
|
||||
});
|
||||
}
|
||||
stops.forEach(([d, minzoom, maxzoom]) => {
|
||||
if (!map_.getLayer(`distance-markers-${d}`)) {
|
||||
map_.addLayer({
|
||||
id: `distance-markers-${d}`,
|
||||
type: 'symbol',
|
||||
source: 'distance-markers',
|
||||
filter:
|
||||
d === 5
|
||||
? [
|
||||
'any',
|
||||
['==', ['get', 'level'], 5],
|
||||
['==', ['get', 'level'], 25],
|
||||
]
|
||||
: ['==', ['get', 'level'], d],
|
||||
minzoom: minzoom,
|
||||
maxzoom: maxzoom ?? 24,
|
||||
layout: {
|
||||
'text-field': ['get', 'distance'],
|
||||
'text-size': 14,
|
||||
'text-font': ['Open Sans Bold'],
|
||||
},
|
||||
paint: {
|
||||
'text-color': 'black',
|
||||
'text-halo-width': 2,
|
||||
'text-halo-color': 'white',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
map_.moveLayer(`distance-markers-${d}`);
|
||||
}
|
||||
});
|
||||
if (!map_.getLayer('distance-markers')) {
|
||||
map_.addLayer({
|
||||
id: 'distance-markers',
|
||||
type: 'symbol',
|
||||
source: 'distance-markers',
|
||||
filter: [
|
||||
'match',
|
||||
['get', 'level'],
|
||||
100,
|
||||
['>=', ['zoom'], 0],
|
||||
50,
|
||||
['>=', ['zoom'], 7],
|
||||
25,
|
||||
[
|
||||
'any',
|
||||
['all', ['>=', ['zoom'], 8], ['<=', ['zoom'], 9]],
|
||||
['>=', ['zoom'], 11],
|
||||
],
|
||||
10,
|
||||
['>=', ['zoom'], 10],
|
||||
5,
|
||||
['>=', ['zoom'], 11],
|
||||
1,
|
||||
['>=', ['zoom'], 13],
|
||||
false,
|
||||
],
|
||||
layout: {
|
||||
'text-field': ['get', 'distance'],
|
||||
'text-size': 14,
|
||||
'text-font': ['Open Sans Bold'],
|
||||
},
|
||||
paint: {
|
||||
'text-color': 'black',
|
||||
'text-halo-width': 2,
|
||||
'text-halo-color': 'white',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
map_.moveLayer('distance-markers');
|
||||
}
|
||||
} else {
|
||||
stops.forEach(([d]) => {
|
||||
if (map_.getLayer(`distance-markers-${d}`)) {
|
||||
map_.removeLayer(`distance-markers-${d}`);
|
||||
}
|
||||
});
|
||||
if (map_.getLayer('distance-markers')) {
|
||||
map_.removeLayer('distance-markers');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// No reliable way to check if the map is ready to add sources and layers
|
||||
@@ -109,9 +109,7 @@ export class DistanceMarkers {
|
||||
getConvertedDistanceToKilometers(currentTargetDistance)
|
||||
) {
|
||||
let distance = currentTargetDistance.toFixed(0);
|
||||
let [level, minzoom] = stops.find(([d]) => currentTargetDistance % d === 0) ?? [
|
||||
0, 0,
|
||||
];
|
||||
let level = levels.find((level) => currentTargetDistance % level === 0) || 1;
|
||||
features.push({
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
@@ -124,7 +122,6 @@ export class DistanceMarkers {
|
||||
properties: {
|
||||
distance,
|
||||
level,
|
||||
minzoom,
|
||||
},
|
||||
} as GeoJSON.Feature);
|
||||
currentTargetDistance += 1;
|
||||
|
||||
@@ -55,14 +55,18 @@ function decrementColor(color: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
|
||||
export function getSvgForSymbol(symbol?: string | undefined, layerColor?: string | undefined) {
|
||||
let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined;
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
${Square.replace('width="24"', 'width="12"')
|
||||
.replace('height="24"', 'height="12"')
|
||||
.replace('stroke="currentColor"', 'stroke="SteelBlue"')
|
||||
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
|
||||
.replace('fill="none"', `fill="${layerColor}"`)}
|
||||
${
|
||||
layerColor
|
||||
? Square.replace('width="24"', 'width="12"')
|
||||
.replace('height="24"', 'height="12"')
|
||||
.replace('stroke="currentColor"', 'stroke="SteelBlue"')
|
||||
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
|
||||
.replace('fill="none"', `fill="${layerColor}"`)
|
||||
: ''
|
||||
}
|
||||
${MapPin.replace('width="24"', '')
|
||||
.replace('height="24"', '')
|
||||
.replace('stroke="currentColor"', '')
|
||||
@@ -87,9 +91,10 @@ export class GPXLayer {
|
||||
fileId: string;
|
||||
file: Readable<GPXFileWithStatistics | undefined>;
|
||||
layerColor: string;
|
||||
markers: mapboxgl.Marker[] = [];
|
||||
selected: boolean = false;
|
||||
draggable: boolean;
|
||||
currentWaypointData: GeoJSON.FeatureCollection | null = null;
|
||||
draggedWaypointIndex: number | null = null;
|
||||
draggingStartingPosition: mapboxgl.Point = new mapboxgl.Point(0, 0);
|
||||
unsubscribe: Function[] = [];
|
||||
|
||||
updateBinded: () => void = this.update.bind(this);
|
||||
@@ -98,6 +103,20 @@ export class GPXLayer {
|
||||
layerOnMouseMoveBinded: (e: any) => void = this.layerOnMouseMove.bind(this);
|
||||
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
|
||||
layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this);
|
||||
waypointLayerOnMouseEnterBinded: (e: mapboxgl.MapMouseEvent) => void =
|
||||
this.waypointLayerOnMouseEnter.bind(this);
|
||||
waypointLayerOnMouseLeaveBinded: (e: mapboxgl.MapMouseEvent) => void =
|
||||
this.waypointLayerOnMouseLeave.bind(this);
|
||||
waypointLayerOnClickBinded: (e: mapboxgl.MapMouseEvent) => void =
|
||||
this.waypointLayerOnClick.bind(this);
|
||||
waypointLayerOnMouseDownBinded: (e: mapboxgl.MapMouseEvent) => void =
|
||||
this.waypointLayerOnMouseDown.bind(this);
|
||||
waypointLayerOnTouchStartBinded: (e: mapboxgl.MapTouchEvent) => void =
|
||||
this.waypointLayerOnTouchStart.bind(this);
|
||||
waypointLayerOnMouseMoveBinded: (e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) => void =
|
||||
this.waypointLayerOnMouseMove.bind(this);
|
||||
waypointLayerOnMouseUpBinded: (e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) => void =
|
||||
this.waypointLayerOnMouseUp.bind(this);
|
||||
|
||||
constructor(fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
|
||||
this.fileId = fileId;
|
||||
@@ -125,18 +144,6 @@ export class GPXLayer {
|
||||
})
|
||||
);
|
||||
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded));
|
||||
this.unsubscribe.push(
|
||||
currentTool.subscribe((tool) => {
|
||||
if (tool === Tool.WAYPOINT && !this.draggable) {
|
||||
this.draggable = true;
|
||||
this.markers.forEach((marker) => marker.setDraggable(true));
|
||||
} else if (tool !== Tool.WAYPOINT && this.draggable) {
|
||||
this.draggable = false;
|
||||
this.markers.forEach((marker) => marker.setDraggable(false));
|
||||
}
|
||||
})
|
||||
);
|
||||
this.draggable = get(currentTool) === Tool.WAYPOINT;
|
||||
}
|
||||
|
||||
update() {
|
||||
@@ -146,6 +153,8 @@ export class GPXLayer {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadIcons();
|
||||
|
||||
if (
|
||||
file._data.style &&
|
||||
file._data.style.color &&
|
||||
@@ -189,6 +198,56 @@ export class GPXLayer {
|
||||
_map.on('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
||||
}
|
||||
|
||||
let waypointSource = _map.getSource(this.fileId + '-waypoints') as
|
||||
| mapboxgl.GeoJSONSource
|
||||
| undefined;
|
||||
this.currentWaypointData = this.getWaypointsGeoJSON();
|
||||
if (waypointSource) {
|
||||
waypointSource.setData(this.currentWaypointData);
|
||||
} else {
|
||||
_map.addSource(this.fileId + '-waypoints', {
|
||||
type: 'geojson',
|
||||
data: this.currentWaypointData,
|
||||
});
|
||||
}
|
||||
|
||||
if (!_map.getLayer(this.fileId + '-waypoints')) {
|
||||
_map.addLayer({
|
||||
id: this.fileId + '-waypoints',
|
||||
type: 'symbol',
|
||||
source: this.fileId + '-waypoints',
|
||||
layout: {
|
||||
'icon-image': ['get', 'icon'],
|
||||
'icon-size': 0.3,
|
||||
'icon-anchor': 'bottom',
|
||||
'icon-padding': 0,
|
||||
'icon-allow-overlap': true,
|
||||
},
|
||||
});
|
||||
|
||||
_map.on(
|
||||
'mouseenter',
|
||||
this.fileId + '-waypoints',
|
||||
this.waypointLayerOnMouseEnterBinded
|
||||
);
|
||||
_map.on(
|
||||
'mouseleave',
|
||||
this.fileId + '-waypoints',
|
||||
this.waypointLayerOnMouseLeaveBinded
|
||||
);
|
||||
_map.on('click', this.fileId + '-waypoints', this.waypointLayerOnClickBinded);
|
||||
_map.on(
|
||||
'mousedown',
|
||||
this.fileId + '-waypoints',
|
||||
this.waypointLayerOnMouseDownBinded
|
||||
);
|
||||
_map.on(
|
||||
'touchstart',
|
||||
this.fileId + '-waypoints',
|
||||
this.waypointLayerOnTouchStartBinded
|
||||
);
|
||||
}
|
||||
|
||||
if (get(directionMarkers)) {
|
||||
if (!_map.getLayer(this.fileId + '-direction')) {
|
||||
_map.addLayer(
|
||||
@@ -213,7 +272,7 @@ export class GPXLayer {
|
||||
'text-halo-color': 'white',
|
||||
},
|
||||
},
|
||||
_map.getLayer('distance-markers-100') ? 'distance-markers-100' : undefined
|
||||
_map.getLayer('distance-markers') ? 'distance-markers' : undefined
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -222,10 +281,10 @@ export class GPXLayer {
|
||||
}
|
||||
}
|
||||
|
||||
let visibleItems: [number, number][] = [];
|
||||
let visibleSegments: [number, number][] = [];
|
||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||
if (!segment._data.hidden) {
|
||||
visibleItems.push([trackIndex, segmentIndex]);
|
||||
visibleSegments.push([trackIndex, segmentIndex]);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -233,7 +292,7 @@ export class GPXLayer {
|
||||
this.fileId,
|
||||
[
|
||||
'any',
|
||||
...visibleItems.map(([trackIndex, segmentIndex]) => [
|
||||
...visibleSegments.map(([trackIndex, segmentIndex]) => [
|
||||
'all',
|
||||
['==', 'trackIndex', trackIndex],
|
||||
['==', 'segmentIndex', segmentIndex],
|
||||
@@ -241,12 +300,26 @@ export class GPXLayer {
|
||||
],
|
||||
{ validate: false }
|
||||
);
|
||||
|
||||
let visibleWaypoints: number[] = [];
|
||||
file.wpt.forEach((waypoint, waypointIndex) => {
|
||||
if (!waypoint._data.hidden) {
|
||||
visibleWaypoints.push(waypointIndex);
|
||||
}
|
||||
});
|
||||
|
||||
_map.setFilter(
|
||||
this.fileId + '-waypoints',
|
||||
['in', ['get', 'waypointIndex'], ['literal', visibleWaypoints]],
|
||||
{ validate: false }
|
||||
);
|
||||
|
||||
if (_map.getLayer(this.fileId + '-direction')) {
|
||||
_map.setFilter(
|
||||
this.fileId + '-direction',
|
||||
[
|
||||
'any',
|
||||
...visibleItems.map(([trackIndex, segmentIndex]) => [
|
||||
...visibleSegments.map(([trackIndex, segmentIndex]) => [
|
||||
'all',
|
||||
['==', 'trackIndex', trackIndex],
|
||||
['==', 'segmentIndex', segmentIndex],
|
||||
@@ -259,114 +332,6 @@ export class GPXLayer {
|
||||
// No reliable way to check if the map is ready to add sources and layers
|
||||
return;
|
||||
}
|
||||
|
||||
let markerIndex = 0;
|
||||
|
||||
if (get(selection).hasAnyChildren(new ListFileItem(this.fileId))) {
|
||||
file.wpt.forEach((waypoint) => {
|
||||
// Update markers
|
||||
let symbolKey = getSymbolKey(waypoint.sym);
|
||||
if (markerIndex < this.markers.length) {
|
||||
this.markers[markerIndex].getElement().innerHTML = getMarkerForSymbol(
|
||||
symbolKey,
|
||||
this.layerColor
|
||||
);
|
||||
this.markers[markerIndex].setLngLat(waypoint.getCoordinates());
|
||||
Object.defineProperty(this.markers[markerIndex], '_waypoint', {
|
||||
value: waypoint,
|
||||
writable: true,
|
||||
});
|
||||
} else {
|
||||
let element = document.createElement('div');
|
||||
element.classList.add('w-8', 'h-8', 'drop-shadow-xl');
|
||||
element.innerHTML = getMarkerForSymbol(symbolKey, this.layerColor);
|
||||
let marker = new mapboxgl.Marker({
|
||||
draggable: this.draggable,
|
||||
element,
|
||||
anchor: 'bottom',
|
||||
}).setLngLat(waypoint.getCoordinates());
|
||||
Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true });
|
||||
let dragEndTimestamp = 0;
|
||||
marker.getElement().addEventListener('mousemove', (e) => {
|
||||
if (marker._isDragging) {
|
||||
return;
|
||||
}
|
||||
waypointPopup?.setItem({ item: marker._waypoint, fileId: this.fileId });
|
||||
e.stopPropagation();
|
||||
});
|
||||
marker.getElement().addEventListener('click', (e) => {
|
||||
if (dragEndTimestamp && Date.now() - dragEndTimestamp < 1000) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (get(currentTool) === Tool.WAYPOINT && e.shiftKey) {
|
||||
fileActions.deleteWaypoint(this.fileId, marker._waypoint._data.index);
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (get(treeFileView)) {
|
||||
if (
|
||||
(e.ctrlKey || e.metaKey) &&
|
||||
get(selection).hasAnyChildren(
|
||||
new ListWaypointsItem(this.fileId),
|
||||
false
|
||||
)
|
||||
) {
|
||||
selection.addSelectItem(
|
||||
new ListWaypointItem(this.fileId, marker._waypoint._data.index)
|
||||
);
|
||||
} else {
|
||||
selection.selectItem(
|
||||
new ListWaypointItem(this.fileId, marker._waypoint._data.index)
|
||||
);
|
||||
}
|
||||
} else if (get(currentTool) === Tool.WAYPOINT) {
|
||||
selectedWaypoint.set([marker._waypoint, this.fileId]);
|
||||
} else {
|
||||
waypointPopup?.setItem({ item: marker._waypoint, fileId: this.fileId });
|
||||
}
|
||||
e.stopPropagation();
|
||||
});
|
||||
marker.on('dragstart', () => {
|
||||
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, true);
|
||||
marker.getElement().style.cursor = 'grabbing';
|
||||
waypointPopup?.hide();
|
||||
});
|
||||
marker.on('dragend', (e) => {
|
||||
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
|
||||
marker.getElement().style.cursor = '';
|
||||
getElevation([marker._waypoint]).then((ele) => {
|
||||
fileActionManager.applyToFile(this.fileId, (file) => {
|
||||
let latLng = marker.getLngLat();
|
||||
let wpt = file.wpt[marker._waypoint._data.index];
|
||||
wpt.setCoordinates({
|
||||
lat: latLng.lat,
|
||||
lon: latLng.lng,
|
||||
});
|
||||
wpt.ele = ele[0];
|
||||
});
|
||||
});
|
||||
dragEndTimestamp = Date.now();
|
||||
});
|
||||
this.markers.push(marker);
|
||||
}
|
||||
markerIndex++;
|
||||
});
|
||||
}
|
||||
|
||||
while (markerIndex < this.markers.length) {
|
||||
// Remove extra markers
|
||||
this.markers.pop()?.remove();
|
||||
}
|
||||
|
||||
this.markers.forEach((marker) => {
|
||||
if (!marker._waypoint._data.hidden) {
|
||||
marker.addTo(_map);
|
||||
} else {
|
||||
marker.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
remove() {
|
||||
@@ -379,6 +344,24 @@ export class GPXLayer {
|
||||
_map.off('mousemove', this.fileId, this.layerOnMouseMoveBinded);
|
||||
_map.off('style.import.load', this.updateBinded);
|
||||
|
||||
_map.off(
|
||||
'mouseenter',
|
||||
this.fileId + '-waypoints',
|
||||
this.waypointLayerOnMouseEnterBinded
|
||||
);
|
||||
_map.off(
|
||||
'mouseleave',
|
||||
this.fileId + '-waypoints',
|
||||
this.waypointLayerOnMouseLeaveBinded
|
||||
);
|
||||
_map.off('click', this.fileId + '-waypoints', this.waypointLayerOnClickBinded);
|
||||
_map.off('mousedown', this.fileId + '-waypoints', this.waypointLayerOnMouseDownBinded);
|
||||
_map.off(
|
||||
'touchstart',
|
||||
this.fileId + '-waypoints',
|
||||
this.waypointLayerOnTouchStartBinded
|
||||
);
|
||||
|
||||
if (_map.getLayer(this.fileId + '-direction')) {
|
||||
_map.removeLayer(this.fileId + '-direction');
|
||||
}
|
||||
@@ -388,12 +371,14 @@ export class GPXLayer {
|
||||
if (_map.getSource(this.fileId)) {
|
||||
_map.removeSource(this.fileId);
|
||||
}
|
||||
if (_map.getLayer(this.fileId + '-waypoints')) {
|
||||
_map.removeLayer(this.fileId + '-waypoints');
|
||||
}
|
||||
if (_map.getSource(this.fileId + '-waypoints')) {
|
||||
_map.removeSource(this.fileId + '-waypoints');
|
||||
}
|
||||
}
|
||||
|
||||
this.markers.forEach((marker) => {
|
||||
marker.remove();
|
||||
});
|
||||
|
||||
this.unsubscribe.forEach((unsubscribe) => unsubscribe());
|
||||
|
||||
decrementColor(this.layerColor);
|
||||
@@ -407,6 +392,9 @@ export class GPXLayer {
|
||||
if (_map.getLayer(this.fileId)) {
|
||||
_map.moveLayer(this.fileId);
|
||||
}
|
||||
if (_map.getLayer(this.fileId + '-waypoints')) {
|
||||
_map.moveLayer(this.fileId + '-waypoints');
|
||||
}
|
||||
if (_map.getLayer(this.fileId + '-direction')) {
|
||||
_map.moveLayer(this.fileId + '-direction');
|
||||
}
|
||||
@@ -449,7 +437,7 @@ export class GPXLayer {
|
||||
}
|
||||
}
|
||||
|
||||
layerOnClick(e: any) {
|
||||
layerOnClick(e: mapboxgl.MapMouseEvent) {
|
||||
if (
|
||||
get(currentTool) === Tool.ROUTING &&
|
||||
get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])
|
||||
@@ -457,8 +445,8 @@ export class GPXLayer {
|
||||
return;
|
||||
}
|
||||
|
||||
let trackIndex = e.features[0].properties.trackIndex;
|
||||
let segmentIndex = e.features[0].properties.segmentIndex;
|
||||
let trackIndex = e.features![0].properties!.trackIndex;
|
||||
let segmentIndex = e.features![0].properties!.segmentIndex;
|
||||
|
||||
if (
|
||||
get(currentTool) === Tool.SCISSORS &&
|
||||
@@ -466,6 +454,11 @@ export class GPXLayer {
|
||||
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
|
||||
)
|
||||
) {
|
||||
if (get(map)?.queryRenderedFeatures(e.point, { layers: ['split-controls'] }).length) {
|
||||
// Clicked on split control, ignoring
|
||||
return;
|
||||
}
|
||||
|
||||
fileActions.split(get(splitAs), this.fileId, trackIndex, segmentIndex, {
|
||||
lat: e.lngLat.lat,
|
||||
lon: e.lngLat.lng,
|
||||
@@ -502,6 +495,160 @@ export class GPXLayer {
|
||||
}
|
||||
}
|
||||
|
||||
waypointLayerOnMouseEnter(e: mapboxgl.MapMouseEvent) {
|
||||
if (this.draggedWaypointIndex !== null) {
|
||||
return;
|
||||
}
|
||||
let file = get(this.file)?.file;
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
let waypointIndex = e.features![0].properties!.waypointIndex;
|
||||
let waypoint = file.wpt[waypointIndex];
|
||||
waypointPopup?.setItem({ item: waypoint, fileId: this.fileId });
|
||||
|
||||
mapCursor.notify(MapCursorState.WAYPOINT_HOVER, true);
|
||||
}
|
||||
|
||||
waypointLayerOnMouseLeave() {
|
||||
mapCursor.notify(MapCursorState.WAYPOINT_HOVER, false);
|
||||
}
|
||||
|
||||
waypointLayerOnClick(e: mapboxgl.MapMouseEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
let waypointIndex = e.features![0].properties!.waypointIndex;
|
||||
let file = get(this.file)?.file;
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
let waypoint = file.wpt[waypointIndex];
|
||||
if (get(currentTool) === Tool.WAYPOINT) {
|
||||
if (this.selected) {
|
||||
if (e.originalEvent.shiftKey) {
|
||||
fileActions.deleteWaypoint(this.fileId, waypointIndex);
|
||||
} else {
|
||||
selection.selectItem(new ListWaypointItem(this.fileId, waypointIndex));
|
||||
selectedWaypoint.set([waypoint, this.fileId]);
|
||||
}
|
||||
} else {
|
||||
if (get(treeFileView)) {
|
||||
selection.selectItem(new ListWaypointItem(this.fileId, waypointIndex));
|
||||
} else {
|
||||
selection.selectItem(new ListFileItem(this.fileId));
|
||||
}
|
||||
selectedWaypoint.set([waypoint, this.fileId]);
|
||||
}
|
||||
} else {
|
||||
if (get(treeFileView)) {
|
||||
if ((e.originalEvent.ctrlKey || e.originalEvent.metaKey) && this.selected) {
|
||||
selection.addSelectItem(new ListWaypointItem(this.fileId, waypointIndex));
|
||||
} else {
|
||||
selection.selectItem(new ListWaypointItem(this.fileId, waypointIndex));
|
||||
}
|
||||
} else {
|
||||
if (!this.selected) {
|
||||
selection.selectItem(new ListFileItem(this.fileId));
|
||||
}
|
||||
waypointPopup?.setItem({ item: waypoint, fileId: this.fileId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
waypointLayerOnMouseDown(e: mapboxgl.MapMouseEvent) {
|
||||
if (get(currentTool) !== Tool.WAYPOINT || !this.selected) {
|
||||
return;
|
||||
}
|
||||
const _map = get(map);
|
||||
if (!_map) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
this.draggedWaypointIndex = e.features![0].properties!.waypointIndex;
|
||||
this.draggingStartingPosition = e.point;
|
||||
waypointPopup?.hide();
|
||||
|
||||
_map.on('mousemove', this.waypointLayerOnMouseMoveBinded);
|
||||
_map.once('mouseup', this.waypointLayerOnMouseUpBinded);
|
||||
}
|
||||
|
||||
waypointLayerOnTouchStart(e: mapboxgl.MapTouchEvent) {
|
||||
if (e.points.length !== 1 || get(currentTool) !== Tool.WAYPOINT || !this.selected) {
|
||||
return;
|
||||
}
|
||||
const _map = get(map);
|
||||
if (!_map) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.draggedWaypointIndex = e.features![0].properties!.waypointIndex;
|
||||
this.draggingStartingPosition = e.point;
|
||||
waypointPopup?.hide();
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
_map.on('touchmove', this.waypointLayerOnMouseMoveBinded);
|
||||
_map.once('touchend', this.waypointLayerOnMouseUpBinded);
|
||||
}
|
||||
|
||||
waypointLayerOnMouseMove(e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) {
|
||||
if (this.draggedWaypointIndex === null || e.point.equals(this.draggingStartingPosition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, true);
|
||||
|
||||
(
|
||||
this.currentWaypointData!.features[this.draggedWaypointIndex].geometry as GeoJSON.Point
|
||||
).coordinates = [e.lngLat.lng, e.lngLat.lat];
|
||||
|
||||
let waypointSource = get(map)?.getSource(this.fileId + '-waypoints') as
|
||||
| mapboxgl.GeoJSONSource
|
||||
| undefined;
|
||||
if (waypointSource) {
|
||||
waypointSource.setData(this.currentWaypointData!);
|
||||
}
|
||||
}
|
||||
|
||||
waypointLayerOnMouseUp(e: mapboxgl.MapMouseEvent | mapboxgl.MapTouchEvent) {
|
||||
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
|
||||
|
||||
get(map)?.off('mousemove', this.waypointLayerOnMouseMoveBinded);
|
||||
get(map)?.off('touchmove', this.waypointLayerOnMouseMoveBinded);
|
||||
|
||||
if (this.draggedWaypointIndex === null) {
|
||||
return;
|
||||
}
|
||||
if (e.point.equals(this.draggingStartingPosition)) {
|
||||
this.draggedWaypointIndex = null;
|
||||
return;
|
||||
}
|
||||
|
||||
getElevation([
|
||||
{
|
||||
lat: e.lngLat.lat,
|
||||
lon: e.lngLat.lng,
|
||||
},
|
||||
]).then((ele) => {
|
||||
if (this.draggedWaypointIndex === null) {
|
||||
return;
|
||||
}
|
||||
fileActionManager.applyToFile(this.fileId, (file) => {
|
||||
let wpt = file.wpt[this.draggedWaypointIndex!];
|
||||
wpt.setCoordinates({
|
||||
lat: e.lngLat.lat,
|
||||
lon: e.lngLat.lng,
|
||||
});
|
||||
wpt.ele = ele[0];
|
||||
});
|
||||
this.draggedWaypointIndex = null;
|
||||
});
|
||||
}
|
||||
|
||||
getGeoJSON(): GeoJSON.FeatureCollection {
|
||||
let file = get(this.file)?.file;
|
||||
if (!file) {
|
||||
@@ -548,4 +695,65 @@ export class GPXLayer {
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
getWaypointsGeoJSON(): GeoJSON.FeatureCollection {
|
||||
let file = get(this.file)?.file;
|
||||
|
||||
let data: GeoJSON.FeatureCollection = {
|
||||
type: 'FeatureCollection',
|
||||
features: [],
|
||||
};
|
||||
|
||||
if (!file) {
|
||||
return data;
|
||||
}
|
||||
|
||||
file.wpt.forEach((waypoint, index) => {
|
||||
data.features.push({
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [waypoint.getLongitude(), waypoint.getLatitude()],
|
||||
},
|
||||
properties: {
|
||||
fileId: this.fileId,
|
||||
waypointIndex: index,
|
||||
icon: `${this.fileId}-waypoint-${getSymbolKey(waypoint.sym) ?? 'default'}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
loadIcons() {
|
||||
const _map = get(map);
|
||||
let file = get(this.file)?.file;
|
||||
if (!_map || !file) {
|
||||
return;
|
||||
}
|
||||
|
||||
let symbols = new Set<string | undefined>();
|
||||
file.wpt.forEach((waypoint) => {
|
||||
symbols.add(getSymbolKey(waypoint.sym));
|
||||
});
|
||||
|
||||
symbols.forEach((symbol) => {
|
||||
const iconId = `${this.fileId}-waypoint-${symbol ?? 'default'}`;
|
||||
if (!_map.hasImage(iconId)) {
|
||||
let icon = new Image(100, 100);
|
||||
icon.onload = () => {
|
||||
if (!_map.hasImage(iconId)) {
|
||||
_map.addImage(iconId, icon);
|
||||
}
|
||||
};
|
||||
|
||||
// Lucide icons are SVG files with a 24x24 viewBox
|
||||
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
|
||||
icon.src =
|
||||
'data:image/svg+xml,' +
|
||||
encodeURIComponent(getSvgForSymbol(symbol, this.layerColor));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import CustomLayers from './CustomLayers.svelte';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { untrack } from 'svelte';
|
||||
import { extensionAPI } from './extension-api';
|
||||
import { extensionAPI } from '$lib/components/map/layer-control/extension-api';
|
||||
|
||||
const {
|
||||
selectedBasemapTree,
|
||||
@@ -33,6 +33,8 @@
|
||||
opacities,
|
||||
} = settings;
|
||||
|
||||
const { isLayerFromExtension, getLayerName } = extensionAPI;
|
||||
|
||||
let { open = $bindable() }: { open: boolean } = $props();
|
||||
|
||||
let accordionValue: string | undefined = $state(undefined);
|
||||
@@ -161,8 +163,8 @@
|
||||
<Select.Trigger class="h-8 mr-1 w-full">
|
||||
{#if selectedOverlay}
|
||||
{#if isSelected($selectedOverlayTree, selectedOverlay)}
|
||||
{#if extensionAPI.isLayerFromExtension(selectedOverlay)}
|
||||
{extensionAPI.getLayerName(selectedOverlay)}
|
||||
{#if $isLayerFromExtension(selectedOverlay)}
|
||||
{$getLayerName(selectedOverlay)}
|
||||
{:else}
|
||||
{i18n._(`layers.label.${selectedOverlay}`)}
|
||||
{/if}
|
||||
@@ -175,8 +177,8 @@
|
||||
{#each Object.keys(overlays) as id}
|
||||
{#if isSelected($selectedOverlayTree, id)}
|
||||
<Select.Item value={id}>
|
||||
{#if extensionAPI.isLayerFromExtension(id)}
|
||||
{extensionAPI.getLayerName(id)}
|
||||
{#if $isLayerFromExtension(id)}
|
||||
{$getLayerName(id)}
|
||||
{:else}
|
||||
{i18n._(`layers.label.${id}`)}
|
||||
{/if}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
} = $props();
|
||||
|
||||
const { customLayers } = settings;
|
||||
const { isLayerFromExtension, getLayerName } = extensionAPI;
|
||||
|
||||
$effect.pre(() => {
|
||||
if (checked !== undefined) {
|
||||
@@ -73,8 +74,8 @@
|
||||
<Label for="{name}-{id}" class="flex flex-row items-center gap-1">
|
||||
{#if $customLayers.hasOwnProperty(id)}
|
||||
{$customLayers[id].name}
|
||||
{:else if extensionAPI.isLayerFromExtension(id)}
|
||||
{extensionAPI.getLayerName(id)}
|
||||
{:else if $isLayerFromExtension(id)}
|
||||
{$getLayerName(id)}
|
||||
{:else}
|
||||
{i18n._(`layers.label.${id}`)}
|
||||
{/if}
|
||||
@@ -84,7 +85,7 @@
|
||||
{:else if anySelectedLayer(node[id])}
|
||||
<CollapsibleTreeNode {id}>
|
||||
{#snippet trigger()}
|
||||
<span>{i18n._(`layers.label.${id}`)}</span>
|
||||
<span>{i18n._(`layers.label.${id}`, id)}</span>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="ml-2">
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { map, type MapboxGLMap } from '$lib/components/map/map';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { get } from 'svelte/store';
|
||||
import { isSelected, remove, removeByPrefix, toggle } from './utils';
|
||||
import { derived, get, writable, type Writable } from 'svelte/store';
|
||||
import { isSelected, remove, removeAll } from './utils';
|
||||
import { overlays, overlayTree } from '$lib/assets/layers';
|
||||
import { browser } from '$app/environment';
|
||||
import { map } from '$lib/components/map/map';
|
||||
|
||||
const { currentOverlays, previousOverlays, selectedOverlayTree } = settings;
|
||||
|
||||
export type CustomOverlay = {
|
||||
extensionName: string;
|
||||
id: string;
|
||||
name: string;
|
||||
tileUrls: string[];
|
||||
@@ -15,11 +16,9 @@ export type CustomOverlay = {
|
||||
};
|
||||
|
||||
export class ExtensionAPI {
|
||||
private _map: MapboxGLMap;
|
||||
private _overlays: Map<string, CustomOverlay> = new Map();
|
||||
private _overlays: Writable<Map<string, CustomOverlay>> = writable(new Map());
|
||||
|
||||
constructor(map: MapboxGLMap) {
|
||||
this._map = map;
|
||||
init() {
|
||||
if (browser && !window.hasOwnProperty('gpxstudio')) {
|
||||
Object.defineProperty(window, 'gpxstudio', {
|
||||
value: this,
|
||||
@@ -30,21 +29,41 @@ export class ExtensionAPI {
|
||||
}
|
||||
}
|
||||
|
||||
async ensureLoaded(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
this._map.onLoad(() => {
|
||||
resolve();
|
||||
ensureLoaded(): Promise<void> {
|
||||
let unsubscribe: () => void;
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
map.onLoad(() => {
|
||||
unsubscribe = currentOverlays.subscribe((current) => {
|
||||
if (current) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
promise.finally(() => {
|
||||
unsubscribe?.();
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
addOrUpdateOverlay(overlay: CustomOverlay) {
|
||||
if (!overlay.id || !overlay.tileUrls || overlay.tileUrls.length === 0) {
|
||||
throw new Error('Overlay must have an id and at least one tile URL.');
|
||||
if (
|
||||
!overlay.extensionName ||
|
||||
!overlay.id ||
|
||||
!overlay.name ||
|
||||
!overlay.tileUrls ||
|
||||
overlay.tileUrls.length === 0
|
||||
) {
|
||||
throw new Error(
|
||||
'Overlay must have an extensionName, id, name, and at least one tile URL.'
|
||||
);
|
||||
}
|
||||
overlay.id = this.getOverlayId(overlay.id);
|
||||
|
||||
this._overlays.set(overlay.id, overlay);
|
||||
this._overlays.update(($overlays) => {
|
||||
$overlays.set(overlay.id, overlay);
|
||||
return $overlays;
|
||||
});
|
||||
|
||||
overlays[overlay.id] = {
|
||||
version: 8,
|
||||
@@ -65,103 +84,130 @@ export class ExtensionAPI {
|
||||
],
|
||||
};
|
||||
|
||||
overlayTree.overlays.world[overlay.id] = true;
|
||||
if (!overlayTree.overlays.hasOwnProperty(overlay.extensionName)) {
|
||||
overlayTree.overlays[overlay.extensionName] = {};
|
||||
}
|
||||
|
||||
overlayTree.overlays[overlay.extensionName][overlay.id] = true;
|
||||
|
||||
selectedOverlayTree.update((selected) => {
|
||||
selected.overlays.world[overlay.id] = true;
|
||||
if (!selected.overlays.hasOwnProperty(overlay.extensionName)) {
|
||||
selected.overlays[overlay.extensionName] = {};
|
||||
}
|
||||
selected.overlays[overlay.extensionName][overlay.id] = true;
|
||||
return selected;
|
||||
});
|
||||
|
||||
const current = get(currentOverlays);
|
||||
let show = false;
|
||||
if (current && isSelected(current, overlay.id)) {
|
||||
show = true;
|
||||
try {
|
||||
get(this._map)?.removeImport(overlay.id);
|
||||
get(map)?.removeImport(overlay.id);
|
||||
} catch (e) {
|
||||
// No reliable way to check if the map is ready to remove sources and layers
|
||||
}
|
||||
}
|
||||
|
||||
currentOverlays.update((current) => {
|
||||
current.overlays.world[overlay.id] = true;
|
||||
if (!current.overlays.hasOwnProperty(overlay.extensionName)) {
|
||||
current.overlays[overlay.extensionName] = {};
|
||||
}
|
||||
current.overlays[overlay.extensionName][overlay.id] = show;
|
||||
return current;
|
||||
});
|
||||
}
|
||||
|
||||
removeOverlaysWithPrefix(prefix: string) {
|
||||
prefix = this.getOverlayId(prefix);
|
||||
filterOverlays(ids: string[]) {
|
||||
ids = ids.map((id) => this.getOverlayId(id));
|
||||
const idsToRemove = Array.from(get(this._overlays).keys()).filter(
|
||||
(id) => !ids.includes(id)
|
||||
);
|
||||
|
||||
currentOverlays.update((overlays) => {
|
||||
removeByPrefix(overlays, prefix);
|
||||
return overlays;
|
||||
currentOverlays.update((current) => {
|
||||
removeAll(current, idsToRemove);
|
||||
return current;
|
||||
});
|
||||
previousOverlays.update((overlays) => {
|
||||
removeByPrefix(overlays, prefix);
|
||||
return overlays;
|
||||
previousOverlays.update((previous) => {
|
||||
removeAll(previous, idsToRemove);
|
||||
return previous;
|
||||
});
|
||||
selectedOverlayTree.update((overlays) => {
|
||||
removeByPrefix(overlays, prefix);
|
||||
return overlays;
|
||||
selectedOverlayTree.update((selected) => {
|
||||
removeAll(selected, idsToRemove);
|
||||
return selected;
|
||||
});
|
||||
Object.keys(overlays).forEach((id) => {
|
||||
if (id.startsWith(prefix)) {
|
||||
if (idsToRemove.includes(id)) {
|
||||
delete overlays[id];
|
||||
}
|
||||
});
|
||||
Object.keys(overlayTree.overlays.world).forEach((id) => {
|
||||
if (id.startsWith(prefix)) {
|
||||
delete overlayTree.overlays.world[id];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleOverlay(id: string) {
|
||||
id = this.getOverlayId(id);
|
||||
|
||||
currentOverlays.update((overlays) => {
|
||||
toggle(overlays, id);
|
||||
return overlays;
|
||||
});
|
||||
if (!isSelected(get(selectedOverlayTree), id)) {
|
||||
selectedOverlayTree.update((overlays) => {
|
||||
toggle(overlays, id);
|
||||
return overlays;
|
||||
removeAll(overlayTree, idsToRemove);
|
||||
this._overlays.update(($overlays) => {
|
||||
$overlays.forEach((_, id) => {
|
||||
if (idsToRemove.includes(id)) {
|
||||
$overlays.delete(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
return $overlays;
|
||||
});
|
||||
}
|
||||
|
||||
isLayerFromExtension(id: string): boolean {
|
||||
return this._overlays.has(id);
|
||||
updateOverlaysOrder(ids: string[]) {
|
||||
ids = ids.map((id) => this.getOverlayId(id));
|
||||
selectedOverlayTree.update((selected) => {
|
||||
let isSelected: Record<string, boolean> = {};
|
||||
ids.forEach((id) => {
|
||||
const overlay = get(this._overlays).get(id);
|
||||
if (
|
||||
overlay &&
|
||||
selected.overlays.hasOwnProperty(overlay.extensionName) &&
|
||||
selected.overlays[overlay.extensionName].hasOwnProperty(id)
|
||||
) {
|
||||
isSelected[id] = selected.overlays[overlay.extensionName][id];
|
||||
delete selected.overlays[overlay.extensionName][id];
|
||||
}
|
||||
});
|
||||
Object.entries(isSelected).forEach(([id, value]) => {
|
||||
const overlay = get(this._overlays).get(id)!;
|
||||
selected.overlays[overlay.extensionName][id] = value;
|
||||
});
|
||||
return selected;
|
||||
});
|
||||
}
|
||||
|
||||
getLayerName(id: string): string {
|
||||
const overlay = this._overlays.get(id);
|
||||
return overlay ? overlay.name : '';
|
||||
}
|
||||
isLayerFromExtension = derived(this._overlays, ($overlays) => {
|
||||
return (id: string) => $overlays.has(id);
|
||||
});
|
||||
|
||||
getLayerName = derived(this._overlays, ($overlays) => {
|
||||
return (id: string) => $overlays.get(id)?.name || '';
|
||||
});
|
||||
|
||||
private getOverlayId(id: string): string {
|
||||
return `extension-${id}`;
|
||||
}
|
||||
|
||||
private destroy() {
|
||||
currentOverlays.update((overlays) => {
|
||||
this._overlays.forEach((_, id) => {
|
||||
remove(overlays, id);
|
||||
const ids = Array.from(get(this._overlays).keys());
|
||||
currentOverlays.update((current) => {
|
||||
ids.forEach((id) => {
|
||||
remove(current, id);
|
||||
});
|
||||
return overlays;
|
||||
return current;
|
||||
});
|
||||
previousOverlays.update((overlays) => {
|
||||
this._overlays.forEach((_, id) => {
|
||||
remove(overlays, id);
|
||||
previousOverlays.update((previous) => {
|
||||
ids.forEach((id) => {
|
||||
remove(previous, id);
|
||||
});
|
||||
return overlays;
|
||||
return previous;
|
||||
});
|
||||
selectedOverlayTree.update((overlays) => {
|
||||
this._overlays.forEach((_, id) => {
|
||||
remove(overlays, id);
|
||||
selectedOverlayTree.update((selected) => {
|
||||
ids.forEach((id) => {
|
||||
remove(selected, id);
|
||||
});
|
||||
return overlays;
|
||||
return selected;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionAPI = new ExtensionAPI(map);
|
||||
export const extensionAPI = new ExtensionAPI();
|
||||
|
||||
@@ -101,7 +101,9 @@ export class OverpassLayer {
|
||||
this.map.on('click', 'overpass', this.onHoverBinded);
|
||||
}
|
||||
|
||||
this.map.setFilter('overpass', ['in', 'query', ...getCurrentQueries()]);
|
||||
this.map.setFilter('overpass', ['in', 'query', ...getCurrentQueries()], {
|
||||
validate: false,
|
||||
});
|
||||
} catch (e) {
|
||||
// No reliable way to check if the map is ready to add sources and layers
|
||||
}
|
||||
|
||||
@@ -66,12 +66,12 @@ export function remove(node: LayerTreeType, id: string) {
|
||||
return node;
|
||||
}
|
||||
|
||||
export function removeByPrefix(node: LayerTreeType, prefix: string) {
|
||||
export function removeAll(node: LayerTreeType, ids: string[]) {
|
||||
Object.keys(node).forEach((key) => {
|
||||
if (key.startsWith(prefix)) {
|
||||
if (ids.includes(key)) {
|
||||
delete node[key];
|
||||
} else if (typeof node[key] !== 'boolean') {
|
||||
remove(node[key], prefix);
|
||||
removeAll(node[key], ids);
|
||||
}
|
||||
});
|
||||
return node;
|
||||
|
||||
@@ -43,7 +43,7 @@ export class MapboxGLMap {
|
||||
sources: {},
|
||||
layers: [],
|
||||
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
|
||||
sprite: `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${accessToken}`,
|
||||
sprite: 'mapbox://sprites/mapbox/outdoors-v12',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
let endTime: string | undefined = $state(undefined);
|
||||
let movingTime: number | undefined = $state(undefined);
|
||||
let speed: number | undefined = $state(undefined);
|
||||
let artificial = $state(false);
|
||||
let artificial = $state(true);
|
||||
|
||||
function toCalendarDate(date: Date): CalendarDate {
|
||||
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
||||
@@ -346,7 +346,7 @@
|
||||
let fileId = item.getFileId();
|
||||
fileActionManager.applyToFile(fileId, (file) => {
|
||||
if (item instanceof ListFileItem) {
|
||||
if (artificial || !$gpxStatistics.global.time.moving) {
|
||||
if (artificial && !$gpxStatistics.global.time.moving) {
|
||||
file.createArtificialTimestamps(
|
||||
getDate(startDate!, startTime!),
|
||||
movingTime!
|
||||
@@ -359,7 +359,7 @@
|
||||
);
|
||||
}
|
||||
} else if (item instanceof ListTrackItem) {
|
||||
if (artificial || !$gpxStatistics.global.time.moving) {
|
||||
if (artificial && !$gpxStatistics.global.time.moving) {
|
||||
file.createArtificialTimestamps(
|
||||
getDate(startDate!, startTime!),
|
||||
movingTime!,
|
||||
@@ -374,7 +374,7 @@
|
||||
);
|
||||
}
|
||||
} else if (item instanceof ListTrackSegmentItem) {
|
||||
if (artificial || !$gpxStatistics.global.time.moving) {
|
||||
if (artificial && !$gpxStatistics.global.time.moving) {
|
||||
file.createArtificialTimestamps(
|
||||
getDate(startDate!, startTime!),
|
||||
movingTime!,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import { onDestroy } from 'svelte';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
import { selection } from '$lib/logic/selection';
|
||||
import { minTolerance, ReducedGPXLayerCollection, tolerance } from './reduce.svelte';
|
||||
import { minTolerance, ReducedGPXLayerCollection, tolerance } from './utils.svelte';
|
||||
|
||||
let props: { class?: string } = $props();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { GPXFileStateCollectionObserver, type GPXFileState } from '$lib/logic/fi
|
||||
import { selection } from '$lib/logic/selection';
|
||||
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
|
||||
import type { GeoJSONSource } from 'mapbox-gl';
|
||||
import { get, writable, type Writable } from 'svelte/store';
|
||||
import { get, writable } from 'svelte/store';
|
||||
|
||||
export const minTolerance = 0.1;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { TrackPoint, TrackSegment } from 'gpx';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
|
||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
||||
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
|
||||
@@ -9,20 +7,41 @@ import { gpxStatistics } from '$lib/logic/statistics';
|
||||
import { get } from 'svelte/store';
|
||||
import { fileStateCollection } from '$lib/logic/file-state';
|
||||
import { fileActions } from '$lib/logic/file-actions';
|
||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||
|
||||
export class SplitControls {
|
||||
active: boolean = false;
|
||||
map: mapboxgl.Map;
|
||||
controls: ControlWithMarker[] = [];
|
||||
shownControls: ControlWithMarker[] = [];
|
||||
unsubscribes: Function[] = [];
|
||||
|
||||
toggleControlsForZoomLevelAndBoundsBinded: () => void =
|
||||
this.toggleControlsForZoomLevelAndBounds.bind(this);
|
||||
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
|
||||
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
|
||||
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
|
||||
|
||||
constructor(map: mapboxgl.Map) {
|
||||
this.map = map;
|
||||
|
||||
if (!this.map.hasImage('split-control')) {
|
||||
let icon = new Image(100, 100);
|
||||
icon.onload = () => {
|
||||
if (!this.map.hasImage('split-control')) {
|
||||
this.map.addImage('split-control', icon);
|
||||
}
|
||||
};
|
||||
|
||||
// Lucide icons are SVG files with a 24x24 viewBox
|
||||
// Create a new SVG with a 32x32 viewBox and center the icon in a circle
|
||||
icon.src =
|
||||
'data:image/svg+xml,' +
|
||||
encodeURIComponent(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
||||
<circle cx="20" cy="20" r="20" fill="white" />
|
||||
<g transform="translate(8 8)">
|
||||
${Scissors.replace('stroke="currentColor"', 'stroke="black"')}
|
||||
</g>
|
||||
</svg>
|
||||
`);
|
||||
}
|
||||
|
||||
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
|
||||
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
|
||||
this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this)));
|
||||
@@ -31,29 +50,18 @@ export class SplitControls {
|
||||
addIfNeeded() {
|
||||
let scissors = get(currentTool) === Tool.SCISSORS;
|
||||
if (!scissors) {
|
||||
if (this.active) {
|
||||
this.remove();
|
||||
}
|
||||
this.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.active) {
|
||||
this.updateControls();
|
||||
} else {
|
||||
this.add();
|
||||
}
|
||||
}
|
||||
|
||||
add() {
|
||||
this.active = true;
|
||||
|
||||
this.map.on('zoom', this.toggleControlsForZoomLevelAndBoundsBinded);
|
||||
this.map.on('move', this.toggleControlsForZoomLevelAndBoundsBinded);
|
||||
this.updateControls();
|
||||
}
|
||||
|
||||
updateControls() {
|
||||
// Update the markers when the files change
|
||||
let controlIndex = 0;
|
||||
let data: GeoJSON.FeatureCollection = {
|
||||
type: 'FeatureCollection',
|
||||
features: [],
|
||||
};
|
||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||
let file = fileStateCollection.getFile(fileId);
|
||||
|
||||
@@ -64,30 +72,23 @@ export class SplitControls {
|
||||
new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
|
||||
)
|
||||
) {
|
||||
for (let point of segment.trkpt.slice(1, -1)) {
|
||||
// Update the existing controls (could be improved by matching the existing controls with the new ones?)
|
||||
for (let i = 1; i < segment.trkpt.length - 1; i++) {
|
||||
let point = segment.trkpt[i];
|
||||
if (point._data.anchor) {
|
||||
if (controlIndex < this.controls.length) {
|
||||
this.controls[controlIndex].fileId = fileId;
|
||||
this.controls[controlIndex].point = point;
|
||||
this.controls[controlIndex].segment = segment;
|
||||
this.controls[controlIndex].trackIndex = trackIndex;
|
||||
this.controls[controlIndex].segmentIndex = segmentIndex;
|
||||
this.controls[controlIndex].marker.setLngLat(
|
||||
point.getCoordinates()
|
||||
);
|
||||
} else {
|
||||
this.controls.push(
|
||||
this.createControl(
|
||||
point,
|
||||
segment,
|
||||
fileId,
|
||||
trackIndex,
|
||||
segmentIndex
|
||||
)
|
||||
);
|
||||
}
|
||||
controlIndex++;
|
||||
data.features.push({
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [point.getLongitude(), point.getLatitude()],
|
||||
},
|
||||
properties: {
|
||||
fileId: fileId,
|
||||
trackIndex: trackIndex,
|
||||
segmentIndex: segmentIndex,
|
||||
pointIndex: i,
|
||||
minZoom: point._data.zoom,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,86 +96,77 @@ export class SplitControls {
|
||||
}
|
||||
}, false);
|
||||
|
||||
while (controlIndex < this.controls.length) {
|
||||
// Remove the extra controls
|
||||
this.controls.pop()?.marker.remove();
|
||||
}
|
||||
try {
|
||||
let source = this.map.getSource('split-controls') as mapboxgl.GeoJSONSource | undefined;
|
||||
if (source) {
|
||||
source.setData(data);
|
||||
} else {
|
||||
this.map.addSource('split-controls', {
|
||||
type: 'geojson',
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
this.toggleControlsForZoomLevelAndBounds();
|
||||
if (!this.map.getLayer('split-controls')) {
|
||||
this.map.addLayer({
|
||||
id: 'split-controls',
|
||||
type: 'symbol',
|
||||
source: 'split-controls',
|
||||
layout: {
|
||||
'icon-image': 'split-control',
|
||||
'icon-size': 0.25,
|
||||
'icon-padding': 0,
|
||||
},
|
||||
filter: ['<=', ['get', 'minZoom'], ['zoom']],
|
||||
});
|
||||
|
||||
this.map.on('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
|
||||
this.map.on('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
|
||||
this.map.on('click', 'split-controls', this.layerOnClickBinded);
|
||||
}
|
||||
|
||||
this.map.moveLayer('split-controls');
|
||||
} catch (e) {
|
||||
// No reliable way to check if the map is ready to add sources and layers
|
||||
}
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.active = false;
|
||||
this.map.off('mouseenter', 'split-controls', this.layerOnMouseEnterBinded);
|
||||
this.map.off('mouseleave', 'split-controls', this.layerOnMouseLeaveBinded);
|
||||
this.map.off('click', 'split-controls', this.layerOnClickBinded);
|
||||
|
||||
for (let control of this.controls) {
|
||||
control.marker.remove();
|
||||
}
|
||||
this.map.off('zoom', this.toggleControlsForZoomLevelAndBoundsBinded);
|
||||
this.map.off('move', this.toggleControlsForZoomLevelAndBoundsBinded);
|
||||
}
|
||||
|
||||
toggleControlsForZoomLevelAndBounds() {
|
||||
// Show markers only if they are in the current zoom level and bounds
|
||||
this.shownControls.splice(0, this.shownControls.length);
|
||||
|
||||
let southWest = this.map.unproject([0, this.map.getCanvas().height]);
|
||||
let northEast = this.map.unproject([this.map.getCanvas().width, 0]);
|
||||
let bounds = new mapboxgl.LngLatBounds(southWest, northEast);
|
||||
|
||||
let zoom = this.map.getZoom();
|
||||
this.controls.forEach((control) => {
|
||||
control.inZoom = control.point._data.zoom <= zoom;
|
||||
if (control.inZoom && bounds.contains(control.marker.getLngLat())) {
|
||||
control.marker.addTo(this.map);
|
||||
this.shownControls.push(control);
|
||||
} else {
|
||||
control.marker.remove();
|
||||
try {
|
||||
if (this.map.getLayer('split-controls')) {
|
||||
this.map.removeLayer('split-controls');
|
||||
}
|
||||
});
|
||||
|
||||
if (this.map.getSource('split-controls')) {
|
||||
this.map.removeSource('split-controls');
|
||||
}
|
||||
} catch (e) {
|
||||
// No reliable way to check if the map is ready to remove sources and layers
|
||||
}
|
||||
}
|
||||
|
||||
createControl(
|
||||
point: TrackPoint,
|
||||
segment: TrackSegment,
|
||||
fileId: string,
|
||||
trackIndex: number,
|
||||
segmentIndex: number
|
||||
): ControlWithMarker {
|
||||
let element = document.createElement('div');
|
||||
element.className = `h-6 w-6 p-0.5 rounded-full bg-white border-2 border-black cursor-pointer`;
|
||||
element.innerHTML = Scissors.replace('width="24"', '')
|
||||
.replace('height="24"', '')
|
||||
.replace('stroke="currentColor"', 'stroke="black"');
|
||||
layerOnMouseEnter(e: any) {
|
||||
mapCursor.notify(MapCursorState.SPLIT_CONTROL, true);
|
||||
}
|
||||
|
||||
let marker = new mapboxgl.Marker({
|
||||
draggable: true,
|
||||
className: 'z-10',
|
||||
element,
|
||||
}).setLngLat(point.getCoordinates());
|
||||
layerOnMouseLeave() {
|
||||
mapCursor.notify(MapCursorState.SPLIT_CONTROL, false);
|
||||
}
|
||||
|
||||
let control = {
|
||||
point,
|
||||
segment,
|
||||
fileId,
|
||||
trackIndex,
|
||||
segmentIndex,
|
||||
marker,
|
||||
inZoom: false,
|
||||
};
|
||||
|
||||
marker.getElement().addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
fileActions.split(
|
||||
get(splitAs),
|
||||
control.fileId,
|
||||
control.trackIndex,
|
||||
control.segmentIndex,
|
||||
control.point.getCoordinates(),
|
||||
control.point._data.index
|
||||
);
|
||||
});
|
||||
|
||||
return control;
|
||||
layerOnClick(e: mapboxgl.MapMouseEvent) {
|
||||
let coordinates = (e.features![0].geometry as GeoJSON.Point).coordinates;
|
||||
fileActions.split(
|
||||
get(splitAs),
|
||||
e.features![0].properties!.fileId,
|
||||
e.features![0].properties!.trackIndex,
|
||||
e.features![0].properties!.segmentIndex,
|
||||
{ lon: coordinates[0], lat: coordinates[1] },
|
||||
e.features![0].properties!.pointIndex
|
||||
);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@@ -182,16 +174,3 @@ export class SplitControls {
|
||||
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
}
|
||||
}
|
||||
|
||||
type Control = {
|
||||
segment: TrackSegment;
|
||||
fileId: string;
|
||||
trackIndex: number;
|
||||
segmentIndex: number;
|
||||
point: TrackPoint;
|
||||
};
|
||||
|
||||
type ControlWithMarker = Control & {
|
||||
marker: mapboxgl.Marker;
|
||||
inZoom: boolean;
|
||||
};
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
import { fileActions } from '$lib/logic/file-actions';
|
||||
import { map } from '$lib/components/map/map';
|
||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { getSvgForSymbol } from '$lib/components/map/gpx-layer/gpx-layer';
|
||||
|
||||
let props: {
|
||||
class?: string;
|
||||
@@ -39,6 +41,21 @@
|
||||
})
|
||||
);
|
||||
|
||||
let marker: mapboxgl.Marker | null = null;
|
||||
|
||||
function reset() {
|
||||
if ($selectedWaypoint) {
|
||||
selectedWaypoint.reset();
|
||||
} else {
|
||||
name = '';
|
||||
description = '';
|
||||
link = '';
|
||||
sym = '';
|
||||
longitude = 0;
|
||||
latitude = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if ($selectedWaypoint) {
|
||||
const wpt = $selectedWaypoint[0];
|
||||
@@ -54,14 +71,7 @@
|
||||
latitude = parseFloat(wpt.getLatitude().toFixed(6));
|
||||
});
|
||||
} else {
|
||||
untrack(() => {
|
||||
name = '';
|
||||
description = '';
|
||||
link = '';
|
||||
sym = '';
|
||||
longitude = 0;
|
||||
latitude = 0;
|
||||
});
|
||||
untrack(reset);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -85,14 +95,14 @@
|
||||
desc: description.length > 0 ? description : undefined,
|
||||
cmt: description.length > 0 ? description : undefined,
|
||||
link: link.length > 0 ? { attributes: { href: link } } : undefined,
|
||||
sym: sym,
|
||||
sym: sym.length > 0 ? sym : undefined,
|
||||
},
|
||||
selectedWaypoint.wpt && selectedWaypoint.fileId
|
||||
? new ListWaypointItem(selectedWaypoint.fileId, selectedWaypoint.wpt._data.index)
|
||||
: undefined
|
||||
);
|
||||
|
||||
selectedWaypoint.reset();
|
||||
reset();
|
||||
}
|
||||
|
||||
function setCoordinates(e: any) {
|
||||
@@ -100,6 +110,37 @@
|
||||
longitude = e.lngLat.lng.toFixed(6);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if ($selectedWaypoint) {
|
||||
if (marker) {
|
||||
marker.remove();
|
||||
marker = null;
|
||||
}
|
||||
} else if (latitude != 0 || longitude != 0) {
|
||||
if ($map) {
|
||||
if (marker) {
|
||||
marker.setLngLat([longitude, latitude]).getElement().innerHTML =
|
||||
getSvgForSymbol(symbolKey);
|
||||
} else {
|
||||
let element = document.createElement('div');
|
||||
element.classList.add('w-8', 'h-8');
|
||||
element.innerHTML = getSvgForSymbol(symbolKey);
|
||||
marker = new mapboxgl.Marker({
|
||||
element,
|
||||
anchor: 'bottom',
|
||||
})
|
||||
.setLngLat([longitude, latitude])
|
||||
.addTo($map);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (marker) {
|
||||
marker.remove();
|
||||
marker = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if ($map) {
|
||||
$map.on('click', setCoordinates);
|
||||
@@ -112,6 +153,10 @@
|
||||
$map.off('click', setCoordinates);
|
||||
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false);
|
||||
}
|
||||
if (marker) {
|
||||
marker.remove();
|
||||
marker = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -210,7 +255,7 @@
|
||||
{i18n._('toolbar.waypoint.create')}
|
||||
{/if}
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" onclick={() => selectedWaypoint.reset()}>
|
||||
<Button variant="outline" size="icon" onclick={reset}>
|
||||
<CircleX size="16" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { HeartHandshake } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||
|
||||
Each time you add or move GPS points, our servers calculate the best route on the road network.
|
||||
We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Languages } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Translation
|
||||
## <Languages size="18" class="inline-block align-baseline" /> Translation
|
||||
|
||||
The website is translated by volunteers using a collaborative translation platform.
|
||||
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
|
||||
|
||||
@@ -29,13 +29,13 @@ You can also drag and drop files directly from your file system into the window.
|
||||
|
||||
Create a copy of the currently selected files.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||
|
||||
Close the currently selected files.
|
||||
Delete the currently selected files.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close all
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||
|
||||
Close all files.
|
||||
Delete all files.
|
||||
|
||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Route planning and editing
|
||||
---
|
||||
|
||||
<script>
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||
@@ -71,7 +71,7 @@ The following tools automate some common route modification operations.
|
||||
|
||||
Reverse the direction of the route.
|
||||
|
||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
||||
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
||||
|
||||
Connect the last point of the route with the starting point, using the chosen routing settings.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { HeartHandshake } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Ajuda a mantenir aquesta pàgina web gratuïta (i sense anuncis)
|
||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Ajuda a mantenir aquesta pàgina web gratuïta (i sense anuncis)
|
||||
|
||||
Cada cop que afegeixes o mous un punt GPS, els nostres servidors calculen la millor ruta possible.
|
||||
També utilitzen l'API de <a href="https://mapbox.com" target="_blank">Mapbox</a> per ensenyar mapes bonics, donar informació sobre l'altitud i permetre la cerca de llocs d'interès.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Languages } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <Languages size="18" class="mr-1 inline-block align-baseline" />Traducció
|
||||
## <Languages size="18" class="inline-block align-baseline" /> Traducció
|
||||
|
||||
Aquesta pàgina web ha estat traduïda per voluntaris utilitzant una plataforma de traducció col·laborativa.
|
||||
Tu també pots contribuir-hi afegint o millorant les traduccions al nostre <a href="https://crowdin.com/project/gpxstudio" target="_blank">projecte de Crowdin</a>.
|
||||
|
||||
@@ -29,13 +29,13 @@ Pots arrossegar y deixar arxius directament des del seu sistema d'arxius cap a l
|
||||
|
||||
Crear una còpia dels arxius seleccionats.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Tanca
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||
|
||||
Tanca els arxius seleccionats.
|
||||
Delete the currently selected files.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Tanca tot
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||
|
||||
Tanca tots els arxius.
|
||||
Delete all files.
|
||||
|
||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportar...
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Planificació i edició de rutes
|
||||
---
|
||||
|
||||
<script>
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||
@@ -71,7 +71,7 @@ Les eines següents automatitzen algunes operacions comunes de modificació de r
|
||||
|
||||
Inverteix el sentit de la ruta.
|
||||
|
||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Tornar a l'inici
|
||||
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Tornar a l'inici
|
||||
|
||||
Connecta l'últim punt de la ruta amb el punt d'inici, utilitzant la configuració de ruta escollida.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { HeartHandshake } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Pomozte udržet web zdarma (a bez reklam)
|
||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Pomozte udržet web zdarma (a bez reklam)
|
||||
|
||||
Vždy, když přidáte nebo přesunete GPS body, naše servery vypočítají nejlepší cestu po silniční síti.
|
||||
Používáme také API z <a href="https://mapbox.com" target="_blank">Mapboxu</a> pro zobrazení krásných map, získání dat o nadmořské výšce a vyhledávání míst.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Languages } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Překlad
|
||||
## <Languages size="18" class="inline-block align-baseline" /> Překlad
|
||||
|
||||
Tento web je překládán dobrovolníky prostřednictvím kolaborativní překladatelské platformy.
|
||||
Ke zlepšení překladů můžete přispět na našem <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin projektu</a>.
|
||||
|
||||
@@ -29,13 +29,13 @@ Soubory můžete také přetáhnout přímo ze souborového systému do okna.
|
||||
|
||||
Vytvořit kopii aktuálně vybraných souborů.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Zavřít
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Smazat
|
||||
|
||||
Zavřít aktuálně vybrané soubory.
|
||||
Smazat aktuálně vybrané soubory.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Zavřít vše
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Smazat vše
|
||||
|
||||
Zavřít všechny soubory.
|
||||
Smazat všechny soubory.
|
||||
|
||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Plánování a úpravy tras
|
||||
---
|
||||
|
||||
<script>
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||
@@ -24,7 +24,7 @@ Dialog lze minimalizovat kliknutím na <button><SquareArrowUpLeft size="16" clas
|
||||
|
||||
### <Route size="16" class="inline-block" style="margin-bottom: 2px" /> Plánování tras
|
||||
|
||||
Když je plánování trasy aktivní, kotevní body umístěné nebo přesunuté po mapě budou projenou trasou vypočítanou v silniční síti <a href="https://www.openstreetmap.org" target="_blank">OpenStreetMap</a>.
|
||||
Když je plánování trasy aktivní, kotevní body umístěné nebo přesunuté po mapě budou propojeny trasou vypočítanou v silniční síti <a href="https://www.openstreetmap.org" target="_blank">OpenStreetMap</a>.
|
||||
Zakázat plánování trasy pro propojení kotevních bodů přímými čarami.
|
||||
Toto nastavení lze také přepnout stisknutím <kbd>F5</kbd>.
|
||||
|
||||
@@ -71,7 +71,7 @@ Následující nástroje automatizují některé běžné operace úpravy trasy.
|
||||
|
||||
Obrátit směr trasy.
|
||||
|
||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Zpět na začátek
|
||||
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Zpět na začátek
|
||||
|
||||
Propojit poslední bod trasy s výchozím bodem pomocí zvoleného nastavení směrování.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { HeartHandshake } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||
|
||||
Each time you add or move GPS points, our servers calculate the best route on the road network.
|
||||
We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Languages } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Oversættelse
|
||||
## <Languages size="18" class="inline-block align-baseline" /> Translation
|
||||
|
||||
Hjemmesiden er oversat af frivillige ved hjælp af en kollaborativ oversættelsesplatform.
|
||||
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
|
||||
|
||||
@@ -29,13 +29,13 @@ You can also drag and drop files directly from your file system into the window.
|
||||
|
||||
Create a copy of the currently selected files.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||
|
||||
Close the currently selected files.
|
||||
Delete the currently selected files.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close all
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||
|
||||
Close all files.
|
||||
Delete all files.
|
||||
|
||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" />Eksporter...
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Route planning and editing
|
||||
---
|
||||
|
||||
<script>
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||
@@ -71,7 +71,7 @@ The following tools automate some common route modification operations.
|
||||
|
||||
Reverse the direction of the route.
|
||||
|
||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
||||
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
||||
|
||||
Connect the last point of the route with the starting point, using the chosen routing settings.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { HeartHandshake } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Helfen Sie, die Website kostenlos (und werbefrei) zu erhalten
|
||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Helfen Sie, die Website kostenlos (und werbefrei) zu erhalten
|
||||
|
||||
Jedes Mal, wenn Sie GPS-Punkte hinzufügen oder verschieben, berechnen unsere Server die beste Route im Straßennetz.
|
||||
Wir verwenden auch APIs von <a href="https://mapbox.com" target="_blank">Mapbox</a>, um schöne Karten anzuzeigen, Höhendaten abzurufen und Ihnen die Suche nach Orten zu ermöglichen.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Languages } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Übersetzung
|
||||
## <Languages size="18" class="inline-block align-baseline" /> Übersetzung
|
||||
|
||||
Die Webseite wird von Freiwilligen mit einer gemeinsamen Übersetzungsplattform übersetzt.
|
||||
Sie können dazu beitragen, indem Sie Übersetzungen in unserem <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin Projekt</a> hinzufügen oder verbessern.
|
||||
|
||||
@@ -29,13 +29,13 @@ Sie können auch Dateien per Drag-and-Drop aus Ihrem Dateisystem in das Fenster
|
||||
|
||||
Erstelle eine Kopie der aktuell ausgewählten Dateien.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Schließen
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||
|
||||
Die aktuell ausgewählten Dateien schließen.
|
||||
Delete the currently selected files.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Alle schließen
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||
|
||||
Schließe alle Dateien.
|
||||
Delete all files.
|
||||
|
||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportieren...
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Routenplanung und Bearbeitung
|
||||
---
|
||||
|
||||
<script>
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||
@@ -71,7 +71,7 @@ Die folgenden Tools automatisieren einige gemeinsame Routenmodifikationsoperatio
|
||||
|
||||
Die Richtung der Route umkehren.
|
||||
|
||||
### Zurück zum Start
|
||||
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Zurück zum Start
|
||||
|
||||
Verbinden Sie den letzten Punkt der Route mit dem Startpunkt mit den gewählten Routing-Einstellungen.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { HeartHandshake } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||
|
||||
Each time you add or move GPS points, our servers calculate the best route on the road network.
|
||||
We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Languages } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Translation
|
||||
## <Languages size="18" class="inline-block align-baseline" /> Translation
|
||||
|
||||
The website is translated by volunteers using a collaborative translation platform.
|
||||
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
|
||||
|
||||
@@ -29,13 +29,13 @@ You can also drag and drop files directly from your file system into the window.
|
||||
|
||||
Create a copy of the currently selected files.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||
|
||||
Close the currently selected files.
|
||||
Delete the currently selected files.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close all
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||
|
||||
Close all files.
|
||||
Delete all files.
|
||||
|
||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Route planning and editing
|
||||
---
|
||||
|
||||
<script>
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||
@@ -71,7 +71,7 @@ The following tools automate some common route modification operations.
|
||||
|
||||
Reverse the direction of the route.
|
||||
|
||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
||||
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
||||
|
||||
Connect the last point of the route with the starting point, using the chosen routing settings.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { HeartHandshake } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Ayude a mantener este sitio gratis (y libre de anuncios)
|
||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Ayude a mantener este sitio gratis (y libre de anuncios)
|
||||
|
||||
Cada vez que añade o mueve puntos GPS, nuestros servidores calculan la mejor ruta en la red de carreteras.
|
||||
También usamos APIs de <a href="https://mapbox.com" target="_blank">Mapbox</a> para mostrar hermosos mapas, obtener datos de elevación y permitirle buscar lugares.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Languages } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Traducción
|
||||
## <Languages size="18" class="inline-block align-baseline" /> Traducción
|
||||
|
||||
Este sitio está traducido por voluntarios usando una plataforma de traducción colaborativa.
|
||||
Puede contribuir añadiendo o mejorando las traducciones en nuestro <a href="https://crowdin.com/project/gpxstudio" target="_blank">proyecto Crowdin</a>.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Integración
|
||||
title: Integraciones
|
||||
---
|
||||
|
||||
<script>
|
||||
|
||||
@@ -29,13 +29,13 @@ Puede arrastrar y soltar archivos directamente desde su sistema de archivos a la
|
||||
|
||||
Crear una copia de los archivos seleccionados actualmente.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Cerrar
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Borrar
|
||||
|
||||
Cerrar los archivos seleccionados actualmente.
|
||||
Borrar los archivos seleccionados.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Cerrar todo
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Borrar todos
|
||||
|
||||
Cerrar todos los archivos.
|
||||
Borrar todos los archivos.
|
||||
|
||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportar...
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Opciones de vista
|
||||
title: View options
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Planificación y edición de rutas
|
||||
---
|
||||
|
||||
<script>
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||
@@ -71,7 +71,7 @@ Las siguientes herramientas automatizan algunas operaciones comunes de modificac
|
||||
|
||||
Invierte el sentido de la ruta.
|
||||
|
||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Volver al inicio
|
||||
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Volver al inicio
|
||||
|
||||
Conecta el último punto de la ruta con el punto de inicio, usando los ajustes de ruta elegidos.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { HeartHandshake } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Lagundu mantentzen webgunea doan (eta propagandarik gabe)
|
||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Lagundu mantentzen webgunea doan (eta propagandarik gabe)
|
||||
|
||||
GPS puntuak gehitzen edo mugitzen dituzun bakoitzean, gure zerbitzariek bide sareko ibilbide onena kalkulatzen dute.<a href="https://mapbox.com" target="_blank">Mapbox</a>en APIak erabiltzen ditugu erakusteko mapa argiak, kota-datuak eskaintzeko eta ahalbidetzeko lekuen bilaketa.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Languages } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Itzulpena
|
||||
## <Languages size="18" class="inline-block align-baseline" /> Itzulpena
|
||||
|
||||
Webgune hau itzulpenerako elkarlaneko plataforma bat erabiltzen duten boluntarioen lanari esker itzultzen da.
|
||||
Itzulpenean lagundu dezakezu gure <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin proiektu</a>an.
|
||||
|
||||
@@ -29,13 +29,13 @@ Beste era batez, fitxategiak zuzenean arrastatu eta jaregin ditzakezu zure fitxa
|
||||
|
||||
Sortu hautatutako fitxategien kopia bat.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Itxi
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||
|
||||
Itxi hautatutako fitxategiak.
|
||||
Delete the currently selected files.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Itxi guztiak
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||
|
||||
Itxi fitxategi guztiak.
|
||||
Delete all files.
|
||||
|
||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Esportatu...
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Ibilbideak planifikatzea eta editatzea
|
||||
---
|
||||
|
||||
<script>
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||
@@ -70,7 +70,7 @@ Hurrengo tresnek ibilbidearen aldaketa arrunt batzuk automatizatzen dituzte.
|
||||
|
||||
Ibilbidearen norabidea alderantzikatu.
|
||||
|
||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" />Bueltatu abiapuntura
|
||||
### <House size="16" class="inline-block" style="margin-bottom: 2px" />Bueltatu abiapuntura
|
||||
|
||||
Konektatu ibilbidearen azken puntua abiapuntuarekin, aukeratutako bideratze ezarpenak erabiliz.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { HeartHandshake } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||
|
||||
Each time you add or move GPS points, our servers calculate the best route on the road network.
|
||||
We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Languages } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Translation
|
||||
## <Languages size="18" class="inline-block align-baseline" /> Translation
|
||||
|
||||
The website is translated by volunteers using a collaborative translation platform.
|
||||
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
|
||||
|
||||
@@ -29,13 +29,13 @@ You can also drag and drop files directly from your file system into the window.
|
||||
|
||||
Create a copy of the currently selected files.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||
|
||||
Close the currently selected files.
|
||||
Delete the currently selected files.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close all
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||
|
||||
Close all files.
|
||||
Delete all files.
|
||||
|
||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Route planning and editing
|
||||
---
|
||||
|
||||
<script>
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||
@@ -71,7 +71,7 @@ The following tools automate some common route modification operations.
|
||||
|
||||
Reverse the direction of the route.
|
||||
|
||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
||||
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
||||
|
||||
Connect the last point of the route with the starting point, using the chosen routing settings.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { HeartHandshake } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <HeartHandshake size="18" class="mr-1 inline-block align-baseline" /> Aidez à garder le site gratuit (et sans pub)
|
||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Aidez à garder le site gratuit (et sans pub)
|
||||
|
||||
Chaque fois que vous ajoutez ou déplacez des points GPS, nos serveurs calculent le meilleur itinéraire sur le réseau routier.
|
||||
Nous utilisons également des services de <a href="https://mapbox.com" target="_blank">Mapbox</a> pour afficher de magnifiques cartes, récupérer des données d'altitude et vous permettre de rechercher des adresses.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Languages } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Traduction
|
||||
## <Languages size="18" class="inline-block align-baseline" /> Traduction
|
||||
|
||||
Le site est traduit par des bénévoles sur une plate-forme de traduction collaborative.
|
||||
Vous pouvez contribuer en ajoutant ou en améliorant des traductions sur notre projet <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin</a>.
|
||||
|
||||
@@ -29,13 +29,13 @@ Vous pouvez également glisser-déposer des fichiers directement depuis votre sy
|
||||
|
||||
Créer une copie des fichiers sélectionnés.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Fermer
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Supprimer
|
||||
|
||||
Fermer les fichiers sélectionnés.
|
||||
Supprimer les fichiers sélectionnés.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Fermer tout
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Supprimer tout
|
||||
|
||||
Fermer tous les fichiers.
|
||||
Supprimer toutes les fichiers.
|
||||
|
||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exporter...
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Planification et édition d'itinéraires
|
||||
---
|
||||
|
||||
<script>
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||
@@ -71,7 +71,7 @@ Les outils suivants automatisent certaines opérations communes de modification
|
||||
|
||||
Inverser le sens de l'itinéraire.
|
||||
|
||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Retour au départ
|
||||
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Retour au départ
|
||||
|
||||
Connecter le dernier point de l'itinéraire avec le point de départ, en utilisant les paramètres de routage choisis.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { HeartHandshake } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## עזור לשמור על אתר זה נקי מפרסומות
|
||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||
|
||||
בכל פעם שאתה מוסיף או מעביר נקודות GPS, השרתים שלנו מחשבים את המסלול הטוב ביותר ברשת הדרכים.
|
||||
We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Languages } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <Languages size="18" class="mr-1 inline-block align-baseline" /> Translation
|
||||
## <Languages size="18" class="inline-block align-baseline" /> Translation
|
||||
|
||||
The website is translated by volunteers using a collaborative translation platform.
|
||||
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
|
||||
|
||||
@@ -29,13 +29,13 @@ You can also drag and drop files directly from your file system into the window.
|
||||
|
||||
Create a copy of the currently selected files.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||
|
||||
Close the currently selected files.
|
||||
Delete the currently selected files.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close all
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||
|
||||
Close all files.
|
||||
Delete all files.
|
||||
|
||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Route planning and editing
|
||||
---
|
||||
|
||||
<script>
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||
@@ -71,7 +71,7 @@ The following tools automate some common route modification operations.
|
||||
|
||||
Reverse the direction of the route.
|
||||
|
||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
||||
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
||||
|
||||
Connect the last point of the route with the starting point, using the chosen routing settings.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { HeartHandshake } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## Segítsen megőrizni a webhely ingyenességét (és reklám mentességét)
|
||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||
|
||||
Minden alkalommal, amikor GPS-pontokat ad hozzá vagy mozgat, szervereink kiszámítják a legjobb útvonalat az úthálózaton.<a href="https://mapbox.com" target="_blank">Mapbox</a> API-jait használjuk a gyönyörű térképek megjelenítésére, a magassági adatok lekérésére és a helyek keresésére.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Languages } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## Fordítás
|
||||
## <Languages size="18" class="inline-block align-baseline" /> Translation
|
||||
|
||||
A weboldalt önkéntesek fordítják egy közös fordítói platformon.
|
||||
Hozzájárulhat fordítások hozzáadásával vagy javításával <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin projektünkben</a>.
|
||||
|
||||
@@ -29,13 +29,13 @@ A fájlokat közvetlenül a fájlrendszerből is áthúzhatja az ablakba.
|
||||
|
||||
Készítsen másolatot az aktuálisan kiválasztott fájlokról.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Bezárás
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||
|
||||
Zárja be az aktuálisan kiválasztott fájlokat.
|
||||
Delete the currently selected files.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Az összes bezárása
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||
|
||||
Zárja be az összes fájlt.
|
||||
Delete all files.
|
||||
|
||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Exportálás...
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Útvonal tervezés és szerkesztés
|
||||
---
|
||||
|
||||
<script>
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, House, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from '@lucide/svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||
@@ -71,7 +71,7 @@ A következő eszközök automatizálnak néhány gyakori útvonal-módosítási
|
||||
|
||||
Fordítsa meg az útvonal irányát.
|
||||
|
||||
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Vissza a kezdéshez
|
||||
### <House size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
||||
|
||||
Csatlakoztassa az útvonal utolsó pontját a kiindulási ponthoz a kiválasztott útvonalbeállítások segítségével.
|
||||
|
||||
|
||||
35
website/src/lib/docs/id/faq.mdx
Normal file
35
website/src/lib/docs/id/faq.mdx
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: FAQ
|
||||
---
|
||||
|
||||
<script>
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
</script>
|
||||
|
||||
# { title }
|
||||
|
||||
### Do I need to donate to use the website?
|
||||
|
||||
No.
|
||||
The website is free to use and always will be (as long as it is financially sustainable).
|
||||
However, donations are appreciated and help keep the website running.
|
||||
|
||||
### Why is this route chosen over that one? _Or_ how can I add something to the map?
|
||||
|
||||
**gpx.studio** uses data from <a href="https://www.openstreetmap.org/" target="_blank">OpenStreetMap</a>, which is an open and collaborative world map.
|
||||
This means you can contribute to the map by adding or editing data on OpenStreetMap.
|
||||
|
||||
If you have never contributed to OpenStreetMap before, here is how you can suggest changes:
|
||||
|
||||
1. Go to the location where you want to add or edit data on the <a href="https://www.openstreetmap.org/" target="_blank">map</a>.
|
||||
2. Use the <button>Query features</button> tool on the right to inspect the existing data.
|
||||
3. Right-click on the location and select <button>Add a note here</button>.
|
||||
4. Explain what is incorrect or missing in the note and click <button>Add note</button> to submit it.
|
||||
|
||||
Someone more experienced with OpenStreetMap will then review your note and make the necessary changes.
|
||||
|
||||
<DocsNote>
|
||||
|
||||
More information on how to contribute to OpenStreetMap can be found <a href="https://wiki.openstreetmap.org/wiki/How_to_contribute" target="_blank">here</a>.
|
||||
|
||||
</DocsNote>
|
||||
110
website/src/lib/docs/id/files-and-stats.mdx
Normal file
110
website/src/lib/docs/id/files-and-stats.mdx
Normal file
@@ -0,0 +1,110 @@
|
||||
---
|
||||
title: Files and statistics
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import { ChartNoAxesColumn } from '@lucide/svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
import ElevationProfile from '$lib/components/elevation-profile/ElevationProfile.svelte';
|
||||
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
|
||||
import { exampleGPXFile } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
</script>
|
||||
|
||||
# { title }
|
||||
|
||||
## File list
|
||||
|
||||
Once you have [opened](./menu/file) files, they will be shown as tabs in the file list located at the bottom of the map.
|
||||
You can reorder them by dragging and dropping the tabs.
|
||||
And when many files are open, you can scroll through the list of tabs to navigate between them.
|
||||
|
||||
<DocsNote>
|
||||
|
||||
When using a mouse, you need to hold <kbd>Shift</kbd> to scroll horizontally.
|
||||
|
||||
</DocsNote>
|
||||
|
||||
### File selection
|
||||
|
||||
By clicking on a tab, you can switch between the files to inspect their statistics, and apply [edit actions](./menu/edit) and [tools](./toolbar) to them.
|
||||
By holding the <kbd>Ctrl/Cmd</kbd> key, you can add files to the selection or remove them, and by holding <kbd>Shift</kbd>, you can select a range of files.
|
||||
Most of the [edit actions](./menu/edit) and [tools](./toolbar) can be applied to multiple files at once.
|
||||
|
||||
<DocsNote>
|
||||
|
||||
You can also navigate through the files using the arrow keys on your keyboard, and use <kbd>Shift</kbd> to add files to the selection.
|
||||
|
||||
</DocsNote>
|
||||
|
||||
### Edit actions
|
||||
|
||||
By right-clicking on a file tab, you can access the same actions as in the [edit menu](./menu/edit).
|
||||
|
||||
### Tree layout
|
||||
|
||||
As mentioned in the [view options section](./menu/view), you can switch to a tree layout for the files list.
|
||||
This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map.
|
||||
In addition, the file tree view enables you to inspect the [tracks, segments, and points of interest](./gpx) contained inside the files through collapsible sections.
|
||||
|
||||
You can also apply [edit actions](./menu/edit) and [tools](./toolbar) to internal file items.
|
||||
Furthermore, you can drag and drop the inner items to reorder them, or move them in the hierarchy or even to another file.
|
||||
|
||||
<DocsNote>
|
||||
|
||||
The size of the file list can be adjusted by dragging the separator between the map and the file list.
|
||||
|
||||
</DocsNote>
|
||||
|
||||
## Elevation profile and statistics
|
||||
|
||||
At the bottom of the interface, you can find the elevation profile and statistics for the current selection.
|
||||
|
||||
<DocsNote>
|
||||
|
||||
The size of the elevation profile can be adjusted by dragging the separator between the map and the elevation profile.
|
||||
|
||||
</DocsNote>
|
||||
|
||||
### Interactive statistics
|
||||
|
||||
When hovering over the elevation profile, a tooltip will show statistics at the cursor position.
|
||||
|
||||
To get the statistics for a specific section of the elevation profile, you can drag a selection rectangle on the profile.
|
||||
Click on the profile to reset the selection.
|
||||
|
||||
You can also use the mouse wheel to zoom in and out on the elevation profile, and move left and right by dragging the profile while holding the <kbd>Shift</kbd> key.
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-center -mt-6">
|
||||
<div class="h-10 w-fit">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
panelSize={120}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### Additional data
|
||||
|
||||
Using the <kbd><ChartNoAxesColumn size="16" class="inline-block" style="margin-bottom: 2px"/></kbd> button at the bottom-right of the elevation profile, you can optionally color the elevation profile by:
|
||||
|
||||
- **slope** information computed from the elevation data, or
|
||||
- **surface** or **category** data coming from <a href="https://www.openstreetmap.org/" target="_blank">OpenStreetMap</a>'s <a href="https://wiki.openstreetmap.org/wiki/Key:surface" target="_blank">surface</a> and <a href="https://wiki.openstreetmap.org/wiki/Key:highway" target="_blank">highway</a> tags.
|
||||
This is only available for files created with **gpx.studio**.
|
||||
|
||||
If your selection includes it, you can also visualize: **speed**, **heart rate**, **cadence**, **temperature** and **power** data on the elevation profile.
|
||||
37
website/src/lib/docs/id/getting-started.mdx
Normal file
37
website/src/lib/docs/id/getting-started.mdx
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Getting started
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||
</script>
|
||||
|
||||
# { title }
|
||||
|
||||
Welcome to the official guide for **gpx.studio**!
|
||||
This guide will walk you through all the components and tools of the interface, helping you become a proficient user of the application.
|
||||
|
||||
<DocsImage src="getting-started/interface" alt="The gpx.studio interface." />
|
||||
|
||||
As shown in the screenshot above, the interface is divided into four main sections organized around the map.
|
||||
Before we dive into the details of each section, let's have a quick overview of the interface.
|
||||
|
||||
## Menu
|
||||
|
||||
At the top of the interface, you will find the [main menu](./menu).
|
||||
This is where you can access common actions such as opening, closing, and exporting files, undoing and redoing actions, and adjusting the application settings.
|
||||
|
||||
## Files and statistics
|
||||
|
||||
At the bottom of the interface, you will find the list of files currently open in the application.
|
||||
You can click on a file to select it and display its statistics below the list.
|
||||
In the [dedicated section](./files-and-stats), we will explain how to select multiple files and switch to a tree layout for advanced file management.
|
||||
|
||||
## Toolbar
|
||||
|
||||
On the left side of the interface, you will find the [toolbar](./toolbar), which contains all the tools you can use to edit your files.
|
||||
|
||||
## Map controls
|
||||
|
||||
Finally, on the right side of the interface, you will find the [map controls](./map-controls).
|
||||
These controls allow you to navigate the map, zoom in and out, and switch between different map styles.
|
||||
34
website/src/lib/docs/id/gpx.mdx
Normal file
34
website/src/lib/docs/id/gpx.mdx
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: GPX file format
|
||||
---
|
||||
|
||||
<script>
|
||||
import { Waypoints, 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.
|
||||
|
||||
GPX files may also contain metadata, of which the **name** and **description** fields are the most useful for users.
|
||||
|
||||
### <Waypoints size="16" class="inline-block" style="margin-bottom: 2px" /> Tracks, segments, and GPS points
|
||||
|
||||
As mentioned above, a GPX file can contain multiple GPS traces.
|
||||
These are organized in a hierarchical structure, with tracks at the top level.
|
||||
|
||||
- A **track** is made of a sequence of disconnected segments.
|
||||
Furthermore, it can contain metadata such as a **name**, a **description**, and **appearance properties**.
|
||||
- A **segment** is a sequence of GPS points that form a continuous path.
|
||||
- A **GPS point** is a location with a latitude, a longitude, and optionally a timestamp and an altitude.
|
||||
Some devices also store additional information such as heart rate, cadence, temperature, and power.
|
||||
|
||||
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.
|
||||
|
||||
### <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.
|
||||
|
||||
In addition to its coordinates, a point of interest can have a **name** and a **description**.
|
||||
13
website/src/lib/docs/id/home/funding.mdx
Normal file
13
website/src/lib/docs/id/home/funding.mdx
Normal file
@@ -0,0 +1,13 @@
|
||||
<script>
|
||||
import { HeartHandshake } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||
|
||||
Each time you add or move GPS points, our servers calculate the best route on the road network.
|
||||
We also use APIs from <a href="https://mapbox.com" target="_blank">Mapbox</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
||||
|
||||
Unfortunately, this is expensive.
|
||||
If you enjoy using this tool and find it valuable, please consider making a small donation to help keep the website free and ad-free.
|
||||
|
||||
Thank you very much for your support! ❤️
|
||||
5
website/src/lib/docs/id/home/mapbox.mdx
Normal file
5
website/src/lib/docs/id/home/mapbox.mdx
Normal file
@@ -0,0 +1,5 @@
|
||||
Mapbox is the company that provides some of the beautiful maps on this website.
|
||||
They also develop the <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">map engine</a> which powers **gpx.studio**.
|
||||
|
||||
We are incredibly fortunate and grateful to be part of their <a href="https://mapbox.com/community" target="_blank">Community</a> program, which supports nonprofits, educational institutions, and positive impact organizations.
|
||||
This partnership allows **gpx.studio** to benefit from Mapbox tools at discounted prices, greatly contributing to the financial viability of the project and enabling us to offer the best possible user experience.
|
||||
12
website/src/lib/docs/id/home/translation.mdx
Normal file
12
website/src/lib/docs/id/home/translation.mdx
Normal file
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
import { Languages } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <Languages size="18" class="inline-block align-baseline" /> Translation
|
||||
|
||||
The website is translated by volunteers using a collaborative translation platform.
|
||||
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
|
||||
|
||||
If you would like to start translating into a new language, please <a href="#contact">get in touch</a>.
|
||||
|
||||
Any help is greatly appreciated!
|
||||
27
website/src/lib/docs/id/integration.mdx
Normal file
27
website/src/lib/docs/id/integration.mdx
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: Integration
|
||||
---
|
||||
|
||||
<script>
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
import EmbeddingPlayground from '$lib/components/embedding/EmbeddingPlayground.svelte';
|
||||
</script>
|
||||
|
||||
# { title }
|
||||
|
||||
You can use **gpx.studio** to create maps showing your GPX files and embed them in your website.
|
||||
|
||||
All you need is:
|
||||
|
||||
1. A <a href="https://account.mapbox.com/auth/signup" target="_blank">Mapbox access token</a> to load the map, and
|
||||
2. GPX files hosted on your server or on Google Drive, or accessible via a public URL.
|
||||
|
||||
You can then play with the configurator below to customize your map and generate the corresponding HTML code.
|
||||
|
||||
<DocsNote type="warning">
|
||||
|
||||
You will need to set up <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" target="_blank">Cross-Origin Resource Sharing (CORS)</a> headers on your server to allow <b>gpx.studio</b> to load your GPX files.
|
||||
|
||||
</DocsNote>
|
||||
|
||||
<EmbeddingPlayground />
|
||||
70
website/src/lib/docs/id/map-controls.mdx
Normal file
70
website/src/lib/docs/id/map-controls.mdx
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: Map controls
|
||||
---
|
||||
|
||||
<script>
|
||||
import { Plus, Minus, Diff, Compass, Search, LocateFixed, PersonStanding, Layers } from '@lucide/svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
import DocsLayers from '$lib/components/docs/DocsLayers.svelte';
|
||||
</script>
|
||||
|
||||
# { title }
|
||||
|
||||
The map controls are located on the right side of the interface.
|
||||
These controls allow you to navigate the map, zoom in and out, and switch between different map styles.
|
||||
|
||||
### <Diff size="16" class="inline-block" style="margin-bottom: 2px" /> Map navigation
|
||||
|
||||
The controls at the top allow you to zoom in <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> and out <Minus size="16" class="inline-block" style="margin-bottom: 2px" />, and to change the orientation and tilt of the map <Compass size="16" class="inline-block" style="margin-bottom: 2px" />.
|
||||
|
||||
<DocsNote>
|
||||
|
||||
To control the orientation and tilt of the map, you can also drag the map while holding <kbd>Ctrl</kbd>.
|
||||
|
||||
</DocsNote>
|
||||
|
||||
### <Search size="16" class="inline-block" style="margin-bottom: 2px" /> Search bar
|
||||
|
||||
You can use the search bar to look for an address and navigate to it on the map.
|
||||
|
||||
### <LocateFixed size="16" class="inline-block" style="margin-bottom: 2px" /> Locate button
|
||||
|
||||
The locate button centers the map on your current location.
|
||||
|
||||
<DocsNote>
|
||||
|
||||
This only works if you have allowed your browser and <b>gpx.studio</b> to access your location.
|
||||
|
||||
</DocsNote>
|
||||
|
||||
### <PersonStanding size="16" class="inline-block" style="margin-bottom: 2px" /> Street view
|
||||
|
||||
This button can be used to enable street view mode on the map.
|
||||
Depending on the street view source chosen in the [settings](./menu/settings), street view imagery can be accessed differently.
|
||||
|
||||
- <a href="https://www.mapillary.com/" target="_blank">Mapillary</a>: the street view coverage will appear as green lines on the map. When zoomed in enough, green dots will show the exact locations where street view imagery is available. Hovering over a green dot will show the street view image at that location.
|
||||
- <a href="https://www.google.com/streetview/" target="_blank">Google Street View</a>: click on the map to open a new tab with the street view imagery at that location.
|
||||
|
||||
### <Layers size="16" class="inline-block" style="margin-bottom: 2px" /> Map layers
|
||||
|
||||
The map layers button allows you to switch between different basemaps, and toggle map overlays and categories of points of interest.
|
||||
|
||||
- **Basemaps** are background maps that present the main geographic features of the world.
|
||||
Depending on their purpose, basemaps have different styles and levels of detail.
|
||||
Only one basemap can be displayed at a time.
|
||||
- **Overlays** are additional layers that can be displayed on top of the basemap to provide complementary information.
|
||||
- **Points of interest** can be added to the map to show different categories of places, such as shops, restaurants, or accommodations.
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<DocsLayers />
|
||||
<span class="text-sm text-center mt-2">
|
||||
Hover over the map to show the <a href="https://hiking.waymarkedtrails.org" target="_blank">Waymarked Trails hiking</a> overlay on top of the <a href="https://www.mapbox.com/maps/outdoors" target="_blank">Mapbox Outdoors</a> basemap.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
A large collection of global and local basemaps and overlays is available in **gpx.studio**, as well as a selection of point-of-interest categories.
|
||||
They can be enabled in the [map layer settings dialog](./menu/settings).
|
||||
|
||||
In these settings, you can also manage the opacity of the overlays.
|
||||
|
||||
For advanced users, it is possible to add custom basemaps and overlays by providing <a href="https://en.wikipedia.org/wiki/Web_Map_Tile_Service" target="_blank">WMTS</a>, <a href="https://en.wikipedia.org/wiki/Web_Map_Service" target="_blank">WMS</a>, or <a href="https://docs.mapbox.com/help/glossary/style/" target="_blank">Mapbox style JSON</a> URLs.
|
||||
17
website/src/lib/docs/id/menu.mdx
Normal file
17
website/src/lib/docs/id/menu.mdx
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Menu
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
</script>
|
||||
|
||||
# { title }
|
||||
|
||||
The main menu, located at the top of the interface, provides access to actions, options, and settings divided into several categories, explained separately in the following sections.
|
||||
|
||||
<DocsNote>
|
||||
|
||||
Most of the menu actions can also be performed using the keyboard shortcuts displayed in the menu.
|
||||
|
||||
</DocsNote>
|
||||
96
website/src/lib/docs/id/menu/edit.mdx
Normal file
96
website/src/lib/docs/id/menu/edit.mdx
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: Edit actions
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import { Undo2, Redo2, Info, PaintBucket, EyeOff, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Maximize, 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).
|
||||
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.
|
||||
|
||||
### <Undo2 size="16" class="inline-block" style="margin-bottom: 2px" /><Redo2 size="16" class="inline-block" style="margin-bottom: 2px" /> Undo and redo
|
||||
|
||||
Using these buttons, you can undo or redo the last actions you performed.
|
||||
This applies to all actions of the interface but not to view options, application settings, or map navigation.
|
||||
|
||||
### <Info size="16" class="inline-block" style="margin-bottom: 2px" /> Info...
|
||||
|
||||
Open the information dialog of the currently selected file item, where you can see and edit its name and description.
|
||||
|
||||
### <PaintBucket size="16" class="inline-block" style="margin-bottom: 2px" /> Appearance...
|
||||
|
||||
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.
|
||||
|
||||
<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.
|
||||
|
||||
<DocsNote>
|
||||
|
||||
This action is only available when the tree layout of the files list is enabled.
|
||||
Additionally, the selection must be a single track.
|
||||
|
||||
</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.
|
||||
|
||||
### <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.
|
||||
|
||||
<DocsNote>
|
||||
|
||||
This action is only available when the tree layout of the files list is enabled.
|
||||
|
||||
</DocsNote>
|
||||
|
||||
### <Scissors size="16" class="inline-block" style="margin-bottom: 2px" /> Cut
|
||||
|
||||
Cut the selected file items to the clipboard.
|
||||
|
||||
<DocsNote>
|
||||
|
||||
This action is only available when the tree layout of the files list is enabled.
|
||||
|
||||
</DocsNote>
|
||||
|
||||
### <ClipboardPaste size="16" class="inline-block" style="margin-bottom: 2px" /> Paste
|
||||
|
||||
Paste the file items from the clipboard to the current hierarchy level if they are compatible with it.
|
||||
|
||||
<DocsNote>
|
||||
|
||||
This action is only available when the tree layout of the files list is enabled.
|
||||
|
||||
</DocsNote>
|
||||
|
||||
### <Trash2 size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||
|
||||
Delete the selected file items.
|
||||
52
website/src/lib/docs/id/menu/file.mdx
Normal file
52
website/src/lib/docs/id/menu/file.mdx
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: File actions
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import { Plus, FolderOpen, Copy, FileX, Download } from '@lucide/svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
</script>
|
||||
|
||||
# { title }
|
||||
|
||||
The file actions menu contains a set of pretty self-explanatory file operations.
|
||||
|
||||
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New
|
||||
|
||||
Create a new empty file.
|
||||
|
||||
### <FolderOpen size="16" class="inline-block" style="margin-bottom: 2px" /> Open...
|
||||
|
||||
Open files from your computer.
|
||||
|
||||
<DocsNote>
|
||||
|
||||
You can also drag and drop files directly from your file system into the window.
|
||||
|
||||
</DocsNote>
|
||||
|
||||
### <Copy size="16" class="inline-block" style="margin-bottom: 2px" /> Duplicate
|
||||
|
||||
Create a copy of the currently selected files.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||
|
||||
Delete the currently selected files.
|
||||
|
||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
||||
|
||||
Delete all files.
|
||||
|
||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
||||
|
||||
Open the export dialog to save the currently selected files to your computer.
|
||||
|
||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export all...
|
||||
|
||||
Open the export dialog to save all files to your computer.
|
||||
|
||||
<DocsNote type="warning">
|
||||
|
||||
If your download does not start after clicking the download button, please check your browser settings to allow downloads from <b>gpx.studio</b>.
|
||||
|
||||
</DocsNote>
|
||||
50
website/src/lib/docs/id/menu/settings.mdx
Normal file
50
website/src/lib/docs/id/menu/settings.mdx
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: Settings
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import { Ruler, Zap, Thermometer, Languages, Sun, PersonStanding, Layers } from '@lucide/svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
</script>
|
||||
|
||||
# { title }
|
||||
|
||||
### <Ruler size="16" class="inline-block" style="margin-bottom: 2px" /> Distance units
|
||||
|
||||
Change the units used to display distances in the interface.
|
||||
|
||||
### <Zap size="16" class="inline-block" style="margin-bottom: 2px" /> Velocity units
|
||||
|
||||
Change the units used to display velocities in the interface.
|
||||
You can choose between distance per hour or minutes per distance, which can be more suitable for running activities.
|
||||
|
||||
### <Thermometer size="16" class="inline-block" style="margin-bottom: 2px" /> Temperature units
|
||||
|
||||
Change the units used to display temperatures in the interface.
|
||||
|
||||
### <Languages size="16" class="inline-block" style="margin-bottom: 2px" /> Language
|
||||
|
||||
Change the language used in the interface.
|
||||
|
||||
<DocsNote>
|
||||
|
||||
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
|
||||
If you would like to start translating into a new language, please <a href="#contact">get in touch</a>.
|
||||
Any help is greatly appreciated!
|
||||
|
||||
</DocsNote>
|
||||
|
||||
### <Sun size="16" class="inline-block" style="margin-bottom: 2px" /> Theme
|
||||
|
||||
Change the theme used in the interface.
|
||||
|
||||
### <PersonStanding size="16" class="inline-block" style="margin-bottom: 2px" /> Street view source
|
||||
|
||||
Change the source used for the [street view control](../map-controls).
|
||||
The default one is <a href="https://www.mapillary.com" target="_blank">Mapillary</a>, but you can also use <a href="https://www.google.com/streetview/" target="_blank">Google Street View</a>.
|
||||
Learn more about how to use the street view control in the [map controls section](../map-controls).
|
||||
|
||||
### <Layers size="16" class="inline-block" style="margin-bottom: 2px" /> Map layers...
|
||||
|
||||
Open a dialog where you can enable or disable map layers, add custom ones, change the opacity of overlays, and more.
|
||||
More information about map layers can be found in the [map controls section](../map-controls).
|
||||
49
website/src/lib/docs/id/menu/view.mdx
Normal file
49
website/src/lib/docs/id/menu/view.mdx
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: View options
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import { ChartArea, ListTree, Map, Layers2, Coins, Milestone, Box } from '@lucide/svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
</script>
|
||||
|
||||
# { title }
|
||||
|
||||
This menu provides options to rearrange the interface and the map view.
|
||||
|
||||
### <ChartArea size="16" class="inline-block" style="margin-bottom: 2px" /> Elevation profile
|
||||
|
||||
Hide the elevation profile to make room for the map, or show it to inspect the current selection.
|
||||
|
||||
### <ListTree size="16" class="inline-block" style="margin-bottom: 2px" /> File tree
|
||||
|
||||
Toggle the tree layout for the [file list](../files-and-stats).
|
||||
This layout is ideal for managing a large number of open files, as it organizes them into a vertical list on the right side of the map.
|
||||
In addition, the file tree view enables you to inspect the [tracks, segments, and points of interest](../gpx) contained inside the files through collapsible sections.
|
||||
|
||||
### <Map size="16" class="inline-block" style="margin-bottom: 2px" /> Switch to previous basemap
|
||||
|
||||
Change the basemap to the one previously selected through the [map layer control](../map-controls).
|
||||
|
||||
### <Layers2 size="16" class="inline-block" style="margin-bottom: 2px" /> Toggle overlays
|
||||
|
||||
Toggle the visibility of the map overlays selected through the [map layer control](../map-controls).
|
||||
|
||||
### <Coins size="16" class="inline-block" style="margin-bottom: 2px" /> Distance markers
|
||||
|
||||
Toggle the visibility of distance markers on the map.
|
||||
They are displayed for the current selection, like the [elevation profile](../files-and-stats).
|
||||
|
||||
### <Milestone size="16" class="inline-block" style="margin-bottom: 2px" /> Direction arrows
|
||||
|
||||
Toggle the visibility of direction arrows on the map.
|
||||
|
||||
### <Box size="16" class="inline-block" style="margin-bottom: 2px" /> Toggle 3D
|
||||
|
||||
Enter or exit the 3D map view.
|
||||
|
||||
<DocsNote>
|
||||
|
||||
To control the orientation and tilt of the map, you can also drag the map while holding <kbd>Ctrl</kbd>.
|
||||
|
||||
</DocsNote>
|
||||
32
website/src/lib/docs/id/toolbar.mdx
Normal file
32
website/src/lib/docs/id/toolbar.mdx
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Toolbar
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
|
||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
currentTool.set(Tool.ROUTING);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
currentTool.set(null);
|
||||
});
|
||||
</script>
|
||||
|
||||
# { title }
|
||||
|
||||
The toolbar is located on the left side of the map and is the heart of the application, as it provides access to the main features of **gpx.studio**.
|
||||
Each tool is represented by an icon and can be activated by clicking on it.
|
||||
|
||||
<div class="flex flex-row justify-center text-foreground">
|
||||
<div>
|
||||
<Toolbar class="border rounded-md shadow-lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
As with [edit actions](./menu/edit), most tools can be applied to multiple files at once and to [inner tracks and segments](./gpx).
|
||||
|
||||
The next sections describe each tool in detail.
|
||||
18
website/src/lib/docs/id/toolbar/clean.mdx
Normal file
18
website/src/lib/docs/id/toolbar/clean.mdx
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: Clean
|
||||
---
|
||||
|
||||
<script>
|
||||
import { SquareDashedMousePointer } from '@lucide/svelte';
|
||||
import Clean from '$lib/components/toolbar/tools/Clean.svelte';
|
||||
</script>
|
||||
|
||||
# <SquareDashedMousePointer size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
||||
|
||||
When the clean tool is selected, dragging the map will create a rectangular selection.
|
||||
|
||||
Depending on the options selected in the dialog shown below, clicking the delete button will remove GPS points and/or [points of interest](../gpx) located either inside or outside the selection.
|
||||
|
||||
<div class="flex flex-row justify-center">
|
||||
<Clean class="text-foreground p-3 border rounded-md shadow-lg" />
|
||||
</div>
|
||||
24
website/src/lib/docs/id/toolbar/elevation.mdx
Normal file
24
website/src/lib/docs/id/toolbar/elevation.mdx
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: Elevation
|
||||
---
|
||||
|
||||
<script>
|
||||
import { MountainSnow } from '@lucide/svelte';
|
||||
import Elevation from '$lib/components/toolbar/tools/Elevation.svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
</script>
|
||||
|
||||
# <MountainSnow size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
||||
|
||||
This tool allows you to add elevation data to traces and [points of interest](../gpx), or to replace the existing data.
|
||||
|
||||
<div class="flex flex-row justify-center">
|
||||
<Elevation class="text-foreground p-3 border rounded-md shadow-lg" />
|
||||
</div>
|
||||
|
||||
<DocsNote>
|
||||
|
||||
Elevation data is provided by <a href="https://mapbox.com" target="_blank">Mapbox</a>.
|
||||
You can learn more about its origin and accuracy in the <a href="https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data" target="_blank">documentation</a>.
|
||||
|
||||
</DocsNote>
|
||||
26
website/src/lib/docs/id/toolbar/extract.mdx
Normal file
26
website/src/lib/docs/id/toolbar/extract.mdx
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Extract
|
||||
---
|
||||
|
||||
<script>
|
||||
import { Ungroup } from '@lucide/svelte';
|
||||
import Extract from '$lib/components/toolbar/tools/Extract.svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
</script>
|
||||
|
||||
# <Ungroup size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
||||
|
||||
This tool allows you to extract [tracks (or segments)](../gpx) from files (or tracks) containing multiple of them.
|
||||
|
||||
<div class="flex flex-row justify-center">
|
||||
<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.
|
||||
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>
|
||||
|
||||
When extracting the tracks from a file containing <a href="../gpx">points of interest</a>, the tool will automatically assign each point of interest to the track it is closest to.
|
||||
|
||||
</DocsNote>
|
||||
20
website/src/lib/docs/id/toolbar/merge.mdx
Normal file
20
website/src/lib/docs/id/toolbar/merge.mdx
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Merge
|
||||
---
|
||||
|
||||
<script>
|
||||
import { Group } from '@lucide/svelte';
|
||||
import Merge from '$lib/components/toolbar/tools/Merge.svelte';
|
||||
</script>
|
||||
|
||||
# <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).
|
||||
|
||||
- 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.
|
||||
|
||||
<div class="flex flex-row justify-center">
|
||||
<Merge class="text-foreground p-3 border rounded-md shadow-lg" />
|
||||
</div>
|
||||
26
website/src/lib/docs/id/toolbar/minify.mdx
Normal file
26
website/src/lib/docs/id/toolbar/minify.mdx
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: Minify
|
||||
---
|
||||
|
||||
<script>
|
||||
import { Funnel } from '@lucide/svelte';
|
||||
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
</script>
|
||||
|
||||
# <Funnel size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
||||
|
||||
This tool can be used to reduce the number of GPS points in a trace, which can be useful for decreasing its size.
|
||||
|
||||
You can adjust the tolerance of the simplification algorithm using the slider, and see the number of points that will be kept, as well as the simplified trace on the map.
|
||||
|
||||
<div class="flex flex-row justify-center">
|
||||
<Reduce class="text-foreground p-3 border rounded-md shadow-lg" />
|
||||
</div>
|
||||
|
||||
<DocsNote>
|
||||
|
||||
The tolerance value represents the maximum distance allowed between the original trace and the simplified trace.
|
||||
You can read more about the algorithm used <a href="https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm" target="_blank">here</a>.
|
||||
|
||||
</DocsNote>
|
||||
27
website/src/lib/docs/id/toolbar/poi.mdx
Normal file
27
website/src/lib/docs/id/toolbar/poi.mdx
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: Points of interest
|
||||
---
|
||||
|
||||
<script>
|
||||
import { MapPin } from '@lucide/svelte';
|
||||
import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
|
||||
</script>
|
||||
|
||||
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
||||
|
||||
[Points of interest](../gpx) can be added to GPX files to mark locations of interest on the map and display them on your GPS device.
|
||||
|
||||
### Creating a point of interest
|
||||
|
||||
To create a point of interest, fill in the form shown below.
|
||||
You can choose the location of the point of interest either by clicking on the map or by entering the coordinates manually.
|
||||
Validate the form when you are done.
|
||||
|
||||
<div class="flex flex-row justify-center">
|
||||
<Waypoint class="text-foreground p-3 border rounded-md shadow-lg" />
|
||||
</div>
|
||||
|
||||
### Editing a point of interest
|
||||
|
||||
The form above can also be used to edit an existing point of interest after selecting it on the map.
|
||||
If you only need to move the point of interest, you can drag it to the desired location.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user