mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-09 23:26:31 +00:00
progress
This commit is contained in:
1 parent
606a0e974a
commit
67a4c090bd
107 files changed
+3474
-3948
No files matched your search
Generated
+1
-1570
File diff suppressed because it is too large.
Load diff
@@ -10,6 +10,7 @@
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"test": "node --import tsx --test \"src/**/*.test.ts\"",
|
||||
"generate:example": "node --import tsx src/lib/scripts/example-statistics.ts",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore && eslint .",
|
||||
"format": "prettier --write . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore"
|
||||
@@ -71,11 +72,8 @@
|
||||
"@maplibre/maplibre-gl-geocoder": "^1.9.4",
|
||||
"chart.js": "^4.5.1",
|
||||
"chartjs-plugin-zoom": "^2.2.0",
|
||||
"dexie": "^4.0.11",
|
||||
"file-saver": "^2.0.5",
|
||||
"gpx": "file:../gpx",
|
||||
"gpx-rs": "file:../gpx-rs/pkg",
|
||||
"immer": "^10.1.1",
|
||||
"jszip": "^3.10.1",
|
||||
"mapillary-js": "^4.1.2",
|
||||
"maplibre-gl": "^5.21.1",
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
// Generated by src/lib/scripts/example-statistics.ts from example.gpx: do not edit.
|
||||
export const exampleData = {
|
||||
global: {
|
||||
totalDistance: 35.97253714756412,
|
||||
movingDistance: 35.97253714756412,
|
||||
totalTime: 11033,
|
||||
movingTime: 11033,
|
||||
elevationGain: 860.2000000000002,
|
||||
elevationLoss: 537.8000000000003,
|
||||
startTime: 1672527600024,
|
||||
endTime: 1672538633024,
|
||||
totalSpeed: 11.737617486742575,
|
||||
movingSpeed: 11.737617486742575,
|
||||
atemp: { avg: 17.676470588235293, count: 102 },
|
||||
bounds: { west: -66.660451, south: 48.745764, east: -66.510044, north: 48.92168 },
|
||||
},
|
||||
totalDistance: [
|
||||
0, 0.04328068848542093, 0.11650720502525283, 0.3351587959345754, 1.0682436029275066,
|
||||
1.5026156340883394, 1.7821196261503274, 2.106207216285154, 2.2410399811984894,
|
||||
2.9453552435682093, 3.6025169832366335, 3.9844079748662025, 4.853369564407712,
|
||||
5.245189634628891, 5.701904241715643, 5.8426474906816495, 6.569912055579026,
|
||||
6.653689376713522, 6.7837648541758915, 6.977468888973151, 7.12513222708653,
|
||||
7.250970625751078, 7.327345186465471, 7.523057936506123, 7.870270298566722,
|
||||
8.331907238547238, 8.49966250663381, 8.712146310893873, 9.145642024727252,
|
||||
9.591804416173956, 9.867268606811916, 10.354226519938479, 10.4995033343533,
|
||||
10.768320772932757, 10.945182629048222, 11.179142591075422, 11.44408361293813,
|
||||
11.958196546084086, 12.320595371530443, 12.674140395065313, 12.985892133977105,
|
||||
13.620373921093831, 13.907784560535925, 14.347390028106815, 14.710337166798299,
|
||||
15.20320527974895, 15.87459256056528, 18.06487313488486, 19.45780363453859,
|
||||
19.576216563691453, 20.04629122790926, 20.11508563990313, 20.57906851024942,
|
||||
20.967882401756427, 21.174446555588826, 21.44870285726096, 21.70815037486119,
|
||||
21.853596645545945, 22.18038572464859, 22.673623863657042, 23.255471531923366,
|
||||
23.804171560172605, 24.12646319419839, 24.583516514019728, 24.96563101163832,
|
||||
25.28996993026779, 25.6464275900608, 25.83865311967249, 25.924206335752725,
|
||||
26.46043835304079, 26.66269792060754, 26.868668538885316, 26.951359258371486,
|
||||
27.47113438147256, 27.75981601401171, 27.946424773943168, 28.10186807045419,
|
||||
28.737895463790068, 28.87567424451052, 29.35605997508892, 29.60491379421725,
|
||||
29.786435932558835, 30.26638431743931, 30.60806722475413, 30.78584552536327,
|
||||
30.913622065558272, 31.059459055705133, 31.303159892950628, 31.570611362268927,
|
||||
31.794495167895366, 32.24998958790923, 32.37413113102769, 32.67948450835431,
|
||||
32.985096351049044, 33.56292688668583, 33.93674728522087, 34.064219803714735,
|
||||
34.54278128265628, 34.723139141379185, 35.335211178826825, 35.78464553975441,
|
||||
35.97253714756412,
|
||||
],
|
||||
movingDistance: [
|
||||
0, 0.04328068848542093, 0.11650720502525283, 0.3351587959345754, 1.0682436029275066,
|
||||
1.5026156340883394, 1.7821196261503274, 2.106207216285154, 2.2410399811984894,
|
||||
2.9453552435682093, 3.6025169832366335, 3.9844079748662025, 4.853369564407712,
|
||||
5.245189634628891, 5.701904241715643, 5.8426474906816495, 6.569912055579026,
|
||||
6.653689376713522, 6.7837648541758915, 6.977468888973151, 7.12513222708653,
|
||||
7.250970625751078, 7.327345186465471, 7.523057936506123, 7.870270298566722,
|
||||
8.331907238547238, 8.49966250663381, 8.712146310893873, 9.145642024727252,
|
||||
9.591804416173956, 9.867268606811916, 10.354226519938479, 10.4995033343533,
|
||||
10.768320772932757, 10.945182629048222, 11.179142591075422, 11.44408361293813,
|
||||
11.958196546084086, 12.320595371530443, 12.674140395065313, 12.985892133977105,
|
||||
13.620373921093831, 13.907784560535925, 14.347390028106815, 14.710337166798299,
|
||||
15.20320527974895, 15.87459256056528, 18.06487313488486, 19.45780363453859,
|
||||
19.576216563691453, 20.04629122790926, 20.11508563990313, 20.57906851024942,
|
||||
20.967882401756427, 21.174446555588826, 21.44870285726096, 21.70815037486119,
|
||||
21.853596645545945, 22.18038572464859, 22.673623863657042, 23.255471531923366,
|
||||
23.804171560172605, 24.12646319419839, 24.583516514019728, 24.96563101163832,
|
||||
25.28996993026779, 25.6464275900608, 25.83865311967249, 25.924206335752725,
|
||||
26.46043835304079, 26.66269792060754, 26.868668538885316, 26.951359258371486,
|
||||
27.47113438147256, 27.75981601401171, 27.946424773943168, 28.10186807045419,
|
||||
28.737895463790068, 28.87567424451052, 29.35605997508892, 29.60491379421725,
|
||||
29.786435932558835, 30.26638431743931, 30.60806722475413, 30.78584552536327,
|
||||
30.913622065558272, 31.059459055705133, 31.303159892950628, 31.570611362268927,
|
||||
31.794495167895366, 32.24998958790923, 32.37413113102769, 32.67948450835431,
|
||||
32.985096351049044, 33.56292688668583, 33.93674728522087, 34.064219803714735,
|
||||
34.54278128265628, 34.723139141379185, 35.335211178826825, 35.78464553975441,
|
||||
35.97253714756412,
|
||||
],
|
||||
totalTime: [
|
||||
0, 14000, 46000, 151000, 346000, 424000, 481000, 541000, 567000, 683000, 878000, 942000,
|
||||
1181000, 1285000, 1384000, 1412000, 1565000, 1588000, 1618000, 1660000, 1693000, 1720000,
|
||||
1734000, 1774000, 1841000, 1921000, 1952000, 1990000, 2063000, 2137000, 2183000, 2332000,
|
||||
2375000, 2476000, 2537000, 2628000, 2733000, 2955000, 3078000, 3210000, 3330000, 3757000,
|
||||
3916000, 4078000, 4146000, 4324000, 4571000, 5400000, 5824000, 6419000, 6578000, 6590000,
|
||||
6726000, 6847000, 7063000, 7278000, 7578000, 7785000, 7865000, 7950000, 8263000, 8417000,
|
||||
8482000, 8617000, 8682000, 8723000, 8788000, 8830000, 8853000, 8978000, 9012000, 9050000,
|
||||
9063000, 9154000, 9211000, 9248000, 9278000, 9403000, 9435000, 9528000, 9571000, 9603000,
|
||||
9685000, 9736000, 9780000, 9811000, 9843000, 9895000, 9940000, 9995000, 10126000, 10153000,
|
||||
10216000, 10270000, 10389000, 10475000, 10511000, 10618000, 10653000, 10908000, 11003000,
|
||||
11033000,
|
||||
],
|
||||
movingTime: [
|
||||
0, 14000, 46000, 151000, 346000, 424000, 481000, 541000, 567000, 683000, 878000, 942000,
|
||||
1181000, 1285000, 1384000, 1412000, 1565000, 1588000, 1618000, 1660000, 1693000, 1720000,
|
||||
1734000, 1774000, 1841000, 1921000, 1952000, 1990000, 2063000, 2137000, 2183000, 2332000,
|
||||
2375000, 2476000, 2537000, 2628000, 2733000, 2955000, 3078000, 3210000, 3330000, 3757000,
|
||||
3916000, 4078000, 4146000, 4324000, 4571000, 5400000, 5824000, 6419000, 6578000, 6590000,
|
||||
6726000, 6847000, 7063000, 7278000, 7578000, 7785000, 7865000, 7950000, 8263000, 8417000,
|
||||
8482000, 8617000, 8682000, 8723000, 8788000, 8830000, 8853000, 8978000, 9012000, 9050000,
|
||||
9063000, 9154000, 9211000, 9248000, 9278000, 9403000, 9435000, 9528000, 9571000, 9603000,
|
||||
9685000, 9736000, 9780000, 9811000, 9843000, 9895000, 9940000, 9995000, 10126000, 10153000,
|
||||
10216000, 10270000, 10389000, 10475000, 10511000, 10618000, 10653000, 10908000, 11003000,
|
||||
11033000,
|
||||
],
|
||||
elevationGain: [
|
||||
0, 5.2000000000000455, 19.000000000000057, 43.400000000000034, 75.60000000000002,
|
||||
80.66666666666669, 84.2000000000001, 86.33333333333343, 86.60000000000008,
|
||||
86.60000000000008, 120.40000000000009, 120.40000000000009, 143.40000000000015,
|
||||
160.0000000000001, 160.4666666666668, 161.80000000000013, 163.6000000000002,
|
||||
165.0666666666669, 165.26666666666682, 165.26666666666682, 165.26666666666682,
|
||||
165.26666666666682, 165.26666666666682, 165.26666666666682, 165.26666666666682,
|
||||
165.26666666666682, 165.26666666666682, 165.26666666666682, 165.26666666666682,
|
||||
165.26666666666682, 165.26666666666682, 179.66666666666686, 194.4000000000002,
|
||||
206.4000000000002, 221.9333333333335, 237.40000000000015, 263.73333333333346,
|
||||
289.6000000000001, 318.0666666666668, 340.46666666666675, 372.6000000000001,
|
||||
405.1333333333334, 438.86666666666684, 446.86666666666684, 448.86666666666684,
|
||||
489.7333333333335, 552.4000000000001, 636.8666666666668, 667.7333333333335,
|
||||
680.4000000000001, 680.4000000000001, 682.7333333333335, 692.2666666666668,
|
||||
706.9333333333335, 725.2000000000002, 754.2000000000002, 782.6000000000003,
|
||||
803.1333333333334, 803.1333333333334, 803.1333333333334, 843.5333333333334,
|
||||
843.5333333333334, 843.5333333333334, 843.5333333333334, 843.5333333333334,
|
||||
843.5333333333334, 843.5333333333334, 843.5333333333334, 845.2, 845.2, 845.2, 845.2, 845.2,
|
||||
845.2, 845.2, 845.2, 845.2, 845.2, 845.2, 845.2, 845.2, 845.2, 845.2, 845.2,
|
||||
846.9333333333333, 848.2666666666667, 848.4666666666666, 848.4666666666666,
|
||||
848.4666666666666, 850.4, 854.1333333333332, 856.2666666666668, 856.2666666666668,
|
||||
856.2666666666668, 856.2666666666668, 856.2666666666668, 858.4000000000001,
|
||||
860.2000000000002, 860.2000000000002, 860.2000000000002, 860.2000000000002,
|
||||
860.2000000000002,
|
||||
],
|
||||
elevationLoss: [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0.20000000000004547, 0.20000000000004547, 20.40000000000009,
|
||||
20.40000000000009, 20.40000000000009, 20.40000000000009, 20.40000000000009,
|
||||
20.40000000000009, 20.40000000000009, 20.40000000000009, 20.866666666666788,
|
||||
21.333333333333485, 21.800000000000182, 22.26666666666688, 23.133333333333496,
|
||||
25.26666666666688, 27.933333333333508, 31.06666666666689, 34.46666666666681,
|
||||
39.46666666666681, 45.400000000000205, 51.26666666666682, 51.26666666666682,
|
||||
51.26666666666682, 51.26666666666682, 51.26666666666682, 51.26666666666682,
|
||||
51.26666666666682, 51.26666666666682, 51.26666666666682, 51.26666666666682,
|
||||
51.26666666666682, 51.26666666666682, 51.26666666666682, 51.26666666666682,
|
||||
51.26666666666682, 51.26666666666682, 51.26666666666682, 51.26666666666682,
|
||||
51.26666666666682, 51.26666666666682, 54.73333333333352, 54.73333333333352,
|
||||
54.73333333333352, 54.73333333333352, 54.73333333333352, 54.73333333333352,
|
||||
54.73333333333352, 54.73333333333352, 71.33333333333354, 90.93333333333345,
|
||||
90.93333333333345, 103.26666666666671, 118.93333333333345, 137.93333333333345,
|
||||
182.53333333333347, 231.86666666666684, 275.6666666666669, 313.5333333333335,
|
||||
313.5333333333335, 314.5333333333335, 317.6666666666668, 323.80000000000024,
|
||||
332.40000000000026, 340.1333333333335, 346.5333333333335, 349.00000000000006,
|
||||
355.8666666666666, 361.7999999999999, 367.59999999999997, 371.13333333333327,
|
||||
378.59999999999997, 389.06666666666666, 400.3999999999999, 412.2000000000001,
|
||||
412.2000000000001, 412.2000000000001, 412.2000000000001, 414.0666666666668,
|
||||
415.2666666666668, 415.2666666666668, 415.2666666666668, 415.2666666666668,
|
||||
440.0666666666668, 462.2666666666668, 469.6000000000001, 472.2666666666667,
|
||||
472.2666666666667, 472.2666666666667, 473.40000000000015, 497.93333333333356,
|
||||
519.4000000000003, 537.8000000000003,
|
||||
],
|
||||
slope: [
|
||||
5.54519829509753, 11.329771405243958, 13.361742112430905, 6.556437280063828,
|
||||
3.3748571006965706, 1.4008034553191204, 0.8946446834706256, 0.34864437421436856,
|
||||
0.023833697214010865, 2.4238382250011337, 1.3088844858730833, 0.7354983423695479,
|
||||
3.1409086334150653, 1.2256423076302692, 0.20085098686760294, 0.437784077436669,
|
||||
0.5425120546520237, 0.7481781910257762, -0.3088521546718895, -0.4101153511136872,
|
||||
-0.1462513564675528, -0.4945281465002597, -0.5145407171378057, -0.2946999437560697,
|
||||
-0.7417945450232967, -1.0804074014330973, -1.2097652068240252, -1.269390092150544,
|
||||
-1.4096385772574478, -1.6906250826786617, 1.6526278471972953, 3.985861405936538,
|
||||
6.327061969720709, 6.462045767621801, 6.718241042985547, 7.336125039207492,
|
||||
7.855681831346254, 6.708409718819214, 6.0060576064991755, 7.635690246436876,
|
||||
7.376614554438903, 7.961883391751326, 5.7495287376423185, 1.246024238407464,
|
||||
4.2065153584168264, 7.094663287987479, 5.1718091508901205, 4.191770925552759,
|
||||
3.162749054180518, -1.155504394077654, -2.486689363250133, 3.1532876038197415,
|
||||
3.7758117104036595, 3.829499622714933, 9.10942941353019, 14.240107950438869,
|
||||
12.941665571593896, -0.9317388039630531, -4.414487617055606, 1.6556817957741452,
|
||||
2.4589851525664814, -2.8014045434450368, -4.413963268792533, -7.960267137482532,
|
||||
-12.683072647006348, -15.24684513552079, -15.673890080960359, -9.647966381748613,
|
||||
0.6754743880371754, -0.5145623969089836, -3.135485920405431, -3.879979247223856,
|
||||
-3.352887180002204, -2.176987189136962, -0.9678293676606319, -1.2863539093324792,
|
||||
-2.400594266311405, -1.938469929926933, -0.19412308181177693, -1.6181239765102629,
|
||||
-4.647099739859, -3.3259229591723516, -2.84799279461206, -1.7710658389233644,
|
||||
0.785456382783462, 0.4385748023866403, 0.25671447794793106, -1.4085821207384774,
|
||||
-0.6105810335595012, 1.8251983257651685, 1.3111677817452918, -6.658984457921569,
|
||||
-7.692745586787598, -3.282613638567991, -1.324014882722225, 0.2792778338655941,
|
||||
1.5840695473562374, 0.1214109154598013, -4.214883885805801, -4.182735032776085,
|
||||
-5.523076373263165, -10.644434966065772,
|
||||
],
|
||||
slopeSegmentSlope: [
|
||||
7.077037465314024, 7.077037465314024, 7.077037465314024, 7.077037465314024,
|
||||
0.5753520337401837, 0.5753520337401837, 0.5753520337401837, 0.5753520337401837,
|
||||
0.5753520337401837, 5.143330470981779, -5.289467529413177, 3.1409086334150653,
|
||||
3.1409086334150653, -0.553863318953831, -0.553863318953831, -0.553863318953831,
|
||||
-0.553863318953831, -0.553863318953831, -0.553863318953831, -0.553863318953831,
|
||||
-0.553863318953831, -0.553863318953831, -0.553863318953831, -0.553863318953831,
|
||||
-0.553863318953831, -0.553863318953831, -0.553863318953831, -0.553863318953831,
|
||||
-0.553863318953831, -0.553863318953831, 6.771412441716311, 6.771412441716311,
|
||||
6.771412441716311, 6.771412441716311, 6.771412441716311, 6.771412441716311,
|
||||
6.771412441716311, 6.771412441716311, 6.771412441716311, 6.771412441716311,
|
||||
6.771412441716311, 6.771412441716311, 1.246024238407464, 1.246024238407464,
|
||||
5.604351892140715, 5.604351892140715, 5.604351892140715, 2.2941711127147597,
|
||||
2.2941711127147597, 2.2941711127147597, 2.2941711127147597, 2.2941711127147597,
|
||||
2.2941711127147597, 10.861290836693483, 10.861290836693483, 10.861290836693483,
|
||||
10.861290836693483, -4.414487617055606, -4.414487617055606, 6.94339810974512,
|
||||
-3.539036752039, -3.539036752039, -3.539036752039, -13.990509017835178, -13.990509017835178,
|
||||
-13.990509017835178, -13.990509017835178, -2.033792785924166, -2.033792785924166,
|
||||
-2.033792785924166, -2.033792785924166, -2.033792785924166, -2.033792785924166,
|
||||
-2.033792785924166, -2.033792785924166, -2.033792785924166, -2.033792785924166,
|
||||
-2.033792785924166, -2.033792785924166, -2.033792785924166, -2.033792785924166,
|
||||
-2.033792785924166, -2.033792785924166, 0.45298474033593794, 0.45298474033593794,
|
||||
0.45298474033593794, 0.45298474033593794, 0.45298474033593794, 0.45298474033593794,
|
||||
0.45298474033593794, 0.45298474033593794, -7.692745586787598, -7.692745586787598,
|
||||
-0.4142590757867528, -0.4142590757867528, -0.4142590757867528, -0.4142590757867528,
|
||||
-0.4142590757867528, -5.154482373206842, -5.154482373206842, -5.154482373206842,
|
||||
-5.154482373206842,
|
||||
],
|
||||
slopeSegmentDistance: [
|
||||
1.0682436029275066, 1.0682436029275066, 1.0682436029275066, 1.0682436029275066,
|
||||
1.8771116406407027, 1.8771116406407027, 1.8771116406407027, 1.8771116406407027,
|
||||
1.8771116406407027, 0.6571617396684242, 0.38189099162956897, 1.2607816597626882,
|
||||
1.2607816597626882, 4.622078972183025, 4.622078972183025, 4.622078972183025,
|
||||
4.622078972183025, 4.622078972183025, 4.622078972183025, 4.622078972183025,
|
||||
4.622078972183025, 4.622078972183025, 4.622078972183025, 4.622078972183025,
|
||||
4.622078972183025, 4.622078972183025, 4.622078972183025, 4.622078972183025,
|
||||
4.622078972183025, 4.622078972183025, 4.040515953724009, 4.040515953724009,
|
||||
4.040515953724009, 4.040515953724009, 4.040515953724009, 4.040515953724009,
|
||||
4.040515953724009, 4.040515953724009, 4.040515953724009, 4.040515953724009,
|
||||
4.040515953724009, 4.040515953724009, 0.802552606262374, 0.802552606262374,
|
||||
3.3545359680865605, 3.3545359680865605, 3.3545359680865605, 2.9030092668715675,
|
||||
2.9030092668715675, 2.9030092668715675, 2.9030092668715675, 2.9030092668715675,
|
||||
2.9030092668715675, 0.8857142437895185, 0.8857142437895185, 0.8857142437895185,
|
||||
0.8857142437895185, 0.8200272181110968, 0.8200272181110968, 0.5818476682663238,
|
||||
1.3280449820963618, 1.3280449820963618, 1.3280449820963618, 1.255136605652762,
|
||||
1.255136605652762, 1.255136605652762, 1.255136605652762, 4.7694141050816405,
|
||||
4.7694141050816405, 4.7694141050816405, 4.7694141050816405, 4.7694141050816405,
|
||||
4.7694141050816405, 4.7694141050816405, 4.7694141050816405, 4.7694141050816405,
|
||||
4.7694141050816405, 4.7694141050816405, 4.7694141050816405, 4.7694141050816405,
|
||||
4.7694141050816405, 4.7694141050816405, 4.7694141050816405, 1.7660639062735584,
|
||||
1.7660639062735584, 1.7660639062735584, 1.7660639062735584, 1.7660639062735584,
|
||||
1.7660639062735584, 1.7660639062735584, 1.7660639062735584, 0.610965220021356,
|
||||
0.610965220021356, 1.738042790330141, 1.738042790330141, 1.738042790330141,
|
||||
1.738042790330141, 1.738042790330141, 1.2493980061849328, 1.2493980061849328,
|
||||
1.2493980061849328, 1.2493980061849328,
|
||||
],
|
||||
lng: [
|
||||
-66.658624, -66.658127, -66.658472, -66.660451, -66.650629, -66.646183, -66.646388,
|
||||
-66.642677, -66.640854, -66.633175, -66.635593, -66.630508, -66.633652, -66.633982,
|
||||
-66.636621, -66.636833, -66.632505, -66.632642, -66.63365, -66.636288, -66.637602,
|
||||
-66.639281, -66.640209, -66.641257, -66.645941, -66.648858, -66.647392, -66.647989,
|
||||
-66.643139, -66.637565, -66.63382, -66.627346, -66.627095, -66.623563, -66.622373,
|
||||
-66.619597, -66.617774, -66.61598, -66.612289, -66.612662, -66.611191, -66.610542,
|
||||
-66.611253, -66.614945, -66.615775, -66.613768, -66.608474, -66.608964, -66.610557,
|
||||
-66.609899, -66.604874, -66.605225, -66.610658, -66.609756, -66.610351, -66.609261,
|
||||
-66.612153, -66.61214, -66.607951, -66.604558, -66.598406, -66.591258, -66.586899,
|
||||
-66.580708, -66.575533, -66.571339, -66.567595, -66.564982, -66.56396, -66.558605,
|
||||
-66.557755, -66.555736, -66.555848, -66.551324, -66.549922, -66.547886, -66.54855,
|
||||
-66.545275, -66.543694, -66.53717, -66.533767, -66.531437, -66.524936, -66.520542,
|
||||
-66.518785, -66.517736, -66.517472, -66.518888, -66.516545, -66.5175, -66.514672,
|
||||
-66.515141, -66.517794, -66.51814, -66.515673, -66.512786, -66.513151, -66.5121, -66.513039,
|
||||
-66.513625, -66.512188, -66.510044,
|
||||
],
|
||||
lat: [
|
||||
48.745764, 48.745974, 48.746592, 48.748063, 48.749297, 48.751879, 48.754389, 48.755973,
|
||||
48.756134, 48.759941, 48.765632, 48.764883, 48.772418, 48.775935, 48.779656, 48.780914,
|
||||
48.7868, 48.787548, 48.788511, 48.788629, 48.789636, 48.789875, 48.789562, 48.787943,
|
||||
48.787467, 48.791147, 48.792306, 48.794176, 48.79641, 48.798028, 48.798255, 48.797259,
|
||||
48.798555, 48.799212, 48.800596, 48.801637, 48.803695, 48.808165, 48.810336, 48.813506,
|
||||
48.816137, 48.821827, 48.824369, 48.827487, 48.830705, 48.834936, 48.839867, 48.859562,
|
||||
48.872045, 48.873018, 48.875654, 48.876228, 48.878383, 48.881829, 48.883645, 48.886005,
|
||||
48.887357, 48.888665, 48.88969, 48.893524, 48.896844, 48.89835, 48.897915, 48.89849,
|
||||
48.898005, 48.898957, 48.901011, 48.900816, 48.901191, 48.897895, 48.896164, 48.894872,
|
||||
48.894132, 48.890526, 48.888099, 48.887087, 48.885759, 48.88046, 48.879786, 48.87928,
|
||||
48.879289, 48.879852, 48.879259, 48.880304, 48.879199, 48.880118, 48.881418, 48.883402,
|
||||
48.885249, 48.887162, 48.890812, 48.891885, 48.894006, 48.896745, 48.901682, 48.904457,
|
||||
48.905578, 48.909826, 48.911326, 48.916817, 48.920747, 48.92168,
|
||||
],
|
||||
ele: [
|
||||
290.4, 292.8, 303.6, 331.8, 366, 371.2, 376, 376.6, 377.6, 376.8, 410.6, 390.4, 419.8, 430,
|
||||
430.2, 431.2, 434, 435.6, 435.6, 434.6, 434.2, 434.2, 433.2, 432.8, 431.6, 426.8, 424.8,
|
||||
422.2, 416.6, 409.8, 404.4, 422.4, 429.6, 448.6, 458.4, 476.2, 495, 537.4, 553.8, 580.4,
|
||||
604.6, 650.2, 678, 692, 688, 728, 770.6, 876, 920.8, 923.8, 914, 910.4, 930.8, 942.6, 953.6,
|
||||
986.4, 1029.6, 1038.8, 1025.2, 1002.6, 1043, 1030.4, 1018.6, 996, 951.8, 906.4, 848, 820.4,
|
||||
821.2, 824.6, 817.4, 811.8, 806.2, 791.6, 788.6, 787, 784.2, 768, 769.2, 766.8, 757.4,
|
||||
746.8, 735.4, 723.4, 726.2, 725.8, 727.4, 726.8, 720.2, 723.8, 732.6, 731.4, 704, 684.4,
|
||||
675, 671.8, 676.4, 681.4, 677.2, 648, 632.8, 612.8,
|
||||
],
|
||||
timestamps: [
|
||||
1672527600024, 1672527614024, 1672527646024, 1672527751024, 1672527946024, 1672528024024,
|
||||
1672528081024, 1672528141024, 1672528167024, 1672528283024, 1672528478024, 1672528542024,
|
||||
1672528781024, 1672528885024, 1672528984024, 1672529012024, 1672529165024, 1672529188024,
|
||||
1672529218024, 1672529260024, 1672529293024, 1672529320024, 1672529334024, 1672529374024,
|
||||
1672529441024, 1672529521024, 1672529552024, 1672529590024, 1672529663024, 1672529737024,
|
||||
1672529783024, 1672529932024, 1672529975024, 1672530076024, 1672530137024, 1672530228024,
|
||||
1672530333024, 1672530555024, 1672530678024, 1672530810024, 1672530930024, 1672531357024,
|
||||
1672531516024, 1672531678024, 1672531746024, 1672531924024, 1672532171024, 1672533000024,
|
||||
1672533424024, 1672534019024, 1672534178024, 1672534190024, 1672534326024, 1672534447024,
|
||||
1672534663024, 1672534878024, 1672535178024, 1672535385024, 1672535465024, 1672535550024,
|
||||
1672535863024, 1672536017024, 1672536082024, 1672536217024, 1672536282024, 1672536323024,
|
||||
1672536388024, 1672536430024, 1672536453024, 1672536578024, 1672536612024, 1672536650024,
|
||||
1672536663024, 1672536754024, 1672536811024, 1672536848024, 1672536878024, 1672537003024,
|
||||
1672537035024, 1672537128024, 1672537171024, 1672537203024, 1672537285024, 1672537336024,
|
||||
1672537380024, 1672537411024, 1672537443024, 1672537495024, 1672537540024, 1672537595024,
|
||||
1672537726024, 1672537753024, 1672537816024, 1672537870024, 1672537989024, 1672538075024,
|
||||
1672538111024, 1672538218024, 1672538253024, 1672538508024, 1672538603024, 1672538633024,
|
||||
],
|
||||
speed: [
|
||||
11.129319896251097, 9.117955175889351, 7.6697896847953, 11.420836774827047,
|
||||
15.395035228401284, 19.036693952608555, 18.572048682978913, 19.210619513643987,
|
||||
21.274175339570412, 15.759862403013887, 14.442431786381372, 14.861614825794996,
|
||||
13.232693805089438, 15.04790560743128, 16.935813242440407, 17.264243811647404,
|
||||
16.589493123379203, 14.52585047073047, 16.18897561298147, 16.38563389971064,
|
||||
16.410104206675626, 17.755284238150917, 18.13915405033632, 18.266639285649575,
|
||||
19.80855433570078, 20.41272026163526, 19.83856029634617, 20.950687073300834,
|
||||
21.54264747624692, 21.64879746253991, 14.075484992575811, 11.854401141400949,
|
||||
10.35235632485696, 9.903984326553841, 9.729990429694698, 9.163487459202377,
|
||||
8.576740789086204, 9.146209654876312, 10.107442573852628, 9.50423946352374,
|
||||
6.2274966978110875, 5.663502961794799, 8.153451667435336, 12.561692967584985,
|
||||
12.524125633787333, 9.861928041320306, 9.574353418670329, 10.294940036954442,
|
||||
5.339387972231342, 2.8097550877114332, 11.34461213077211, 12.95944740827415,
|
||||
11.945791216622078, 6.360121552587119, 4.016133735072683, 3.7307451444281803,
|
||||
2.874985478946628, 5.923509614057983, 17.89150294060575, 9.724394236656252,
|
||||
8.715142842518263, 14.317671160685329, 14.0282091692482, 15.10502071391872,
|
||||
23.992757532953096, 23.121393229442734, 18.46036898931696, 15.384668992168189,
|
||||
15.124505676526212, 16.720564185392, 20.41150929222635, 20.376094430396314,
|
||||
20.854586858789187, 19.665164326383863, 18.202610775470102, 18.378916465566082,
|
||||
18.382545054508643, 17.743326284094163, 17.803137925406997, 19.303399845178138,
|
||||
20.658045958555817, 20.88854283859139, 22.239643999271156, 19.684845773960607,
|
||||
14.666632358598804, 15.635058876677832, 16.694478316815264, 18.97060106834699,
|
||||
17.688069898010582, 13.14925598013496, 13.206895362508604, 17.179796817802924,
|
||||
18.798929846810953, 18.383772034644412, 16.711918843992997, 14.792250010689015,
|
||||
15.256799935436915, 16.704997292901556, 9.837060780048157, 10.918351526145143,
|
||||
18.354987899634057, 22.54699293716527,
|
||||
],
|
||||
atemp: [
|
||||
18, 18, 18, 19, 22, 22, 22, 21, 20, 20, 23, 23, 19, 19, 18, 18, 17, 17, 18, 18, 18, 19, 19,
|
||||
19, 19, 19, 18, 18, 17, 17, 16, 16, 16, 17, 18, 17, 17, 18, 18, 18, 19, 22, 23, 23, 23, 22,
|
||||
19, 18, 20, 20, 18, 18, 17, 19, 20, 21, 22, 22, 21, 19, 18, 16, 16, 16, 16, 16, 15, 15, 15,
|
||||
15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 15, 15,
|
||||
15, 15, 15, 15, 16, 16, 16, 18, 17, 17,
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><gpx creator="StravaGPX" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 http://www.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd" version="1.1" xmlns="http://www.topografix.com/GPX/1/1" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns:gpxpx="http://www.garmin.com/xmlschemas/PowerExtension/v1" xmlns:gpx_style="http://www.topografix.com/GPX/gpx_style/0/2"><metadata><name>Example</name><author><name>gpx.studio</name><link href="https://gpx.studio"/></author><time>2023-08-20T12:42:07.000Z</time></metadata><trk><name>Example</name><type>cycling</type><trkseg><trkpt lat="48.745764" lon="-66.658624"><ele>290.4</ele><time>2022-12-31T23:00:00.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>18</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.745974" lon="-66.658127"><ele>292.8</ele><time>2022-12-31T23:00:14.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>18</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.746592" lon="-66.658472"><ele>303.6</ele><time>2022-12-31T23:00:46.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>18</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.748063" lon="-66.660451"><ele>331.8</ele><time>2022-12-31T23:02:31.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>19</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.749297" lon="-66.650629"><ele>366</ele><time>2022-12-31T23:05:46.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>22</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.751879" lon="-66.646183"><ele>371.2</ele><time>2022-12-31T23:07:04.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>22</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.754389" lon="-66.646388"><ele>376</ele><time>2022-12-31T23:08:01.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>22</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.755973" lon="-66.642677"><ele>376.6</ele><time>2022-12-31T23:09:01.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>21</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.756134" lon="-66.640854"><ele>377.6</ele><time>2022-12-31T23:09:27.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>20</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.759941" lon="-66.633175"><ele>376.8</ele><time>2022-12-31T23:11:23.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>20</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.765632" lon="-66.635593"><ele>410.6</ele><time>2022-12-31T23:14:38.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>23</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.764883" lon="-66.630508"><ele>390.4</ele><time>2022-12-31T23:15:42.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>23</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.772418" lon="-66.633652"><ele>419.8</ele><time>2022-12-31T23:19:41.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>19</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.775935" lon="-66.633982"><ele>430</ele><time>2022-12-31T23:21:25.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>19</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.779656" lon="-66.636621"><ele>430.2</ele><time>2022-12-31T23:23:04.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>18</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.780914" lon="-66.636833"><ele>431.2</ele><time>2022-12-31T23:23:32.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>18</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.7868" lon="-66.632505"><ele>434</ele><time>2022-12-31T23:26:05.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>17</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.787548" lon="-66.632642"><ele>435.6</ele><time>2022-12-31T23:26:28.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>17</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkLine truncated
|
||||
@@ -0,0 +1,48 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, it } from 'node:test';
|
||||
import { computeExampleData, engineIsBuilt } from '../scripts/example-statistics';
|
||||
import { exampleData } from './example-data';
|
||||
import { exampleStatistics } from './example';
|
||||
|
||||
describe('exampleStatistics', () => {
|
||||
it('has the statistics of the whole example for the whole range', () => {
|
||||
const whole = exampleStatistics.slice(0, exampleStatistics.length - 1);
|
||||
const global = exampleStatistics.global;
|
||||
assert.ok(whole);
|
||||
for (const key of [
|
||||
'totalDistance',
|
||||
'movingDistance',
|
||||
'totalTime',
|
||||
'movingTime',
|
||||
'elevationGain',
|
||||
'elevationLoss',
|
||||
'startTime',
|
||||
'endTime',
|
||||
'totalSpeed',
|
||||
'movingSpeed',
|
||||
] as const) {
|
||||
assert.ok(Math.abs((whole[key] as number) - (global[key] as number)) < 1e-6, key);
|
||||
}
|
||||
});
|
||||
|
||||
it('has no speed for a range that does not last', () => {
|
||||
const point = exampleStatistics.slice(20, 20);
|
||||
assert.equal(point?.totalTime, 0);
|
||||
assert.equal(point?.totalSpeed, undefined);
|
||||
assert.equal(point?.movingSpeed, undefined);
|
||||
});
|
||||
|
||||
it('has nothing for ranges that are not in the example', () => {
|
||||
assert.equal(exampleStatistics.slice(5, 500), undefined);
|
||||
assert.equal(exampleStatistics.slice(-1, 3), undefined);
|
||||
assert.equal(exampleStatistics.slice(10, 5), undefined);
|
||||
});
|
||||
|
||||
it(
|
||||
'is what the engine computes (npm run generate:example if not)',
|
||||
{ skip: !engineIsBuilt() && 'the engine is not built' },
|
||||
async () => {
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(await computeExampleData())), exampleData);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -1,4 +1,63 @@
|
||||
import { parseGPX } from 'gpx';
|
||||
export const exampleGPXFile = parseGPX(
|
||||
`<?xml version="1.0" encoding="UTF-8"?><gpx creator="StravaGPX" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 http://www.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd" version="1.1" xmlns="http://www.topografix.com/GPX/1/1" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns:gpxpx="http://www.garmin.com/xmlschemas/PowerExtension/v1" xmlns:gpx_style="http://www.topografix.com/GPX/gpx_style/0/2"><metadata><name>Example</name><author><name>gpx.studio</name><link href="https://gpx.studio"/></author><time>2023-08-20T12:42:07.000Z</time></metadata><trk><name>Example</name><type>cycling</type><trkseg><trkpt lat="48.745764" lon="-66.658624"><ele>290.4</ele><time>2022-12-31T23:00:00.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>18</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.745974" lon="-66.658127"><ele>292.8</ele><time>2022-12-31T23:00:14.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>18</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.746592" lon="-66.658472"><ele>303.6</ele><time>2022-12-31T23:00:46.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>18</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.748063" lon="-66.660451"><ele>331.8</ele><time>2022-12-31T23:02:31.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>19</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.749297" lon="-66.650629"><ele>366</ele><time>2022-12-31T23:05:46.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>22</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.751879" lon="-66.646183"><ele>371.2</ele><time>2022-12-31T23:07:04.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>22</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.754389" lon="-66.646388"><ele>376</ele><time>2022-12-31T23:08:01.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>22</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.755973" lon="-66.642677"><ele>376.6</ele><time>2022-12-31T23:09:01.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>21</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.756134" lon="-66.640854"><ele>377.6</ele><time>2022-12-31T23:09:27.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>20</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.759941" lon="-66.633175"><ele>376.8</ele><time>2022-12-31T23:11:23.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>20</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.765632" lon="-66.635593"><ele>410.6</ele><time>2022-12-31T23:14:38.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>23</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.764883" lon="-66.630508"><ele>390.4</ele><time>2022-12-31T23:15:42.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>23</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.772418" lon="-66.633652"><ele>419.8</ele><time>2022-12-31T23:19:41.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>19</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.775935" lon="-66.633982"><ele>430</ele><time>2022-12-31T23:21:25.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>19</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.779656" lon="-66.636621"><ele>430.2</ele><time>2022-12-31T23:23:04.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>18</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.780914" lon="-66.636833"><ele>431.2</ele><time>2022-12-31T23:23:32.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>18</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.7868" lon="-66.632505"><ele>434</ele><time>2022-12-31T23:26:05.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>17</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions></trkpt><trkpt lat="48.787548" lon="-66.632642"><ele>435.6</ele><time>2022-12-31T23:26:28.024Z</time><extensions><gpxtpx:TrackPointExtension><gpxtpx:atemp>17</gpxtpx:atemp></gpxtpx:TrackPointExtension></extensions>
Line truncated
|
||||
);
|
||||
/**
|
||||
* The statistics of an example file (a ride of 102 trackpoints with timestamps and temperatures),
|
||||
* for the pages that show the elevation profile and the statistics without a file: the home page
|
||||
* and the documentation. They are the ones that the engine computes for `example.gpx`, kept as
|
||||
* they are (see `example-data.ts`) so that the example does not need the engine.
|
||||
*/
|
||||
import type { GlobalStatistics, SelectionStatistics } from '$lib/engine';
|
||||
import { exampleData as data } from './example-data';
|
||||
|
||||
const length = data.lng.length;
|
||||
|
||||
// the values that are cumulative over the selection: those of a range are the difference of its ends
|
||||
const cumulative = {
|
||||
totalDistance: data.totalDistance,
|
||||
movingDistance: data.movingDistance,
|
||||
totalTime: data.totalTime,
|
||||
movingTime: data.movingTime,
|
||||
elevationGain: data.elevationGain,
|
||||
elevationLoss: data.elevationLoss,
|
||||
};
|
||||
|
||||
/** The statistics of the trackpoints from `start` to `end` (both included), as the engine does. */
|
||||
function slice(start: number, end: number): GlobalStatistics | undefined {
|
||||
if (!(start >= 0 && start <= end && end < length)) {
|
||||
return undefined;
|
||||
}
|
||||
const delta = (values: number[]) => values[end] - values[start];
|
||||
// the times are in ms, in seconds in the statistics; a range cannot last a negative time
|
||||
const totalTime = Math.max(0, delta(cumulative.totalTime));
|
||||
const movingTime = Math.max(0, delta(cumulative.movingTime));
|
||||
const totalDistance = delta(cumulative.totalDistance);
|
||||
const movingDistance = delta(cumulative.movingDistance);
|
||||
const hours = (ms: number) => ms / 3_600_000;
|
||||
return {
|
||||
...data.global,
|
||||
totalDistance,
|
||||
movingDistance,
|
||||
totalTime: totalTime / 1000,
|
||||
movingTime: movingTime / 1000,
|
||||
elevationGain: delta(cumulative.elevationGain),
|
||||
elevationLoss: delta(cumulative.elevationLoss),
|
||||
startTime: data.timestamps[start],
|
||||
endTime: data.timestamps[end],
|
||||
totalSpeed: totalTime > 0 ? totalDistance / hours(totalTime) : undefined,
|
||||
movingSpeed: movingTime > 0 ? movingDistance / hours(movingTime) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export const exampleStatistics: SelectionStatistics = {
|
||||
global: data.global,
|
||||
length,
|
||||
totalDistance: Float64Array.from(data.totalDistance),
|
||||
slope: Float64Array.from(data.slope),
|
||||
lng: Float64Array.from(data.lng),
|
||||
lat: Float64Array.from(data.lat),
|
||||
ele: Float64Array.from(data.ele),
|
||||
timestamps: BigInt64Array.from(data.timestamps, (time) => BigInt(time)),
|
||||
speed: Float64Array.from(data.speed),
|
||||
atemp: Float64Array.from(data.atemp),
|
||||
slopeSegmentSlope: Float64Array.from(data.slopeSegmentSlope),
|
||||
slopeSegmentDistance: Float64Array.from(data.slopeSegmentDistance),
|
||||
slice,
|
||||
};
|
||||
@@ -0,0 +1,143 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { afterEach, beforeEach, describe, it, mock } from 'node:test';
|
||||
import { cachedFetch, pruneCache } from './cached-fetch';
|
||||
|
||||
const keyOf = (request: RequestInfo | URL) =>
|
||||
request instanceof Request ? request.url : String(request);
|
||||
|
||||
/** A Cache API in memory: only what `cachedFetch` uses. */
|
||||
class FakeCache {
|
||||
entries = new Map<string, Response>();
|
||||
failToPut = false;
|
||||
|
||||
async match(request: RequestInfo | URL) {
|
||||
return this.entries.get(keyOf(request))?.clone();
|
||||
}
|
||||
|
||||
async put(request: RequestInfo | URL, response: Response) {
|
||||
if (this.failToPut) {
|
||||
throw new Error('quota');
|
||||
}
|
||||
this.entries.set(keyOf(request), response);
|
||||
}
|
||||
|
||||
async delete(request: RequestInfo | URL) {
|
||||
return this.entries.delete(keyOf(request));
|
||||
}
|
||||
|
||||
async keys() {
|
||||
return [...this.entries.keys()].map((url) => new Request(url));
|
||||
}
|
||||
}
|
||||
|
||||
const global = globalThis as unknown as { caches?: unknown; fetch: unknown };
|
||||
const original = { caches: global.caches, fetch: global.fetch };
|
||||
|
||||
describe('cachedFetch', () => {
|
||||
let cache: FakeCache;
|
||||
let fetched: string[];
|
||||
let names: string[];
|
||||
let options: { name: string; maxAge: number };
|
||||
let counter = 0;
|
||||
|
||||
beforeEach(() => {
|
||||
cache = new FakeCache();
|
||||
fetched = [];
|
||||
names = [];
|
||||
// the caches are cleaned once per session and name: a new name for each test
|
||||
options = { name: `test-${counter++}`, maxAge: 60_000 };
|
||||
global.caches = {
|
||||
open: async (name: string) => {
|
||||
names.push(name);
|
||||
return cache;
|
||||
},
|
||||
};
|
||||
global.fetch = async (url: string) => {
|
||||
fetched.push(url);
|
||||
return new Response(JSON.stringify({ url }), {
|
||||
status: url.includes('missing') ? 404 : 200,
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.caches = original.caches;
|
||||
global.fetch = original.fetch;
|
||||
mock.timers.reset();
|
||||
});
|
||||
|
||||
it('asks the network once, then uses what it kept', async () => {
|
||||
const first = await cachedFetch('https://example.com/a', options);
|
||||
assert.deepEqual(await first.json(), { url: 'https://example.com/a' });
|
||||
const second = await cachedFetch('https://example.com/a', options);
|
||||
assert.deepEqual(await second.json(), { url: 'https://example.com/a' });
|
||||
assert.deepEqual(fetched, ['https://example.com/a']);
|
||||
assert.ok(names.every((name) => name === options.name));
|
||||
|
||||
await cachedFetch('https://example.com/b', options);
|
||||
assert.equal(fetched.length, 2);
|
||||
});
|
||||
|
||||
it('asks the network again for what is too old, and keeps the new response', async () => {
|
||||
mock.timers.enable({ apis: ['Date'], now: 1_000_000 });
|
||||
await cachedFetch('https://example.com/a', options);
|
||||
mock.timers.setTime(1_000_000 + options.maxAge - 1);
|
||||
await cachedFetch('https://example.com/a', options);
|
||||
assert.equal(fetched.length, 1);
|
||||
|
||||
mock.timers.setTime(1_000_000 + options.maxAge);
|
||||
await cachedFetch('https://example.com/a', options);
|
||||
assert.equal(fetched.length, 2);
|
||||
// the new one is the one that is kept: it is not too old a minute later
|
||||
mock.timers.setTime(1_000_000 + options.maxAge + 30_000);
|
||||
await cachedFetch('https://example.com/a', options);
|
||||
assert.equal(fetched.length, 2);
|
||||
});
|
||||
|
||||
it('does not keep what failed', async () => {
|
||||
const response = await cachedFetch('https://example.com/missing', options);
|
||||
assert.equal(response.status, 404);
|
||||
assert.equal(cache.entries.size, 0);
|
||||
await cachedFetch('https://example.com/missing', options);
|
||||
assert.equal(fetched.length, 2);
|
||||
});
|
||||
|
||||
it('answers even if the response cannot be kept', async () => {
|
||||
cache.failToPut = true;
|
||||
const response = await cachedFetch('https://example.com/a', options);
|
||||
assert.deepEqual(await response.json(), { url: 'https://example.com/a' });
|
||||
// let the failed put settle: it must not throw
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
it('works without the Cache API', async () => {
|
||||
global.caches = undefined;
|
||||
const response = await cachedFetch('https://example.com/a', options);
|
||||
assert.deepEqual(await response.json(), { url: 'https://example.com/a' });
|
||||
await cachedFetch('https://example.com/a', options);
|
||||
assert.equal(fetched.length, 2);
|
||||
});
|
||||
|
||||
it('works when the Cache API cannot be opened', async () => {
|
||||
global.caches = {
|
||||
open: async () => {
|
||||
throw new Error('blocked');
|
||||
},
|
||||
};
|
||||
const response = await cachedFetch('https://example.com/a', options);
|
||||
assert.equal(response.status, 200);
|
||||
});
|
||||
|
||||
it('cleans what is too old', async () => {
|
||||
mock.timers.enable({ apis: ['Date'], now: 5_000_000 });
|
||||
await cachedFetch('https://example.com/old', options);
|
||||
mock.timers.setTime(5_000_000 + options.maxAge / 2);
|
||||
await cachedFetch('https://example.com/recent', options);
|
||||
// a response that was kept by something else is not trusted
|
||||
await cache.put('https://example.com/foreign', new Response('{}'));
|
||||
|
||||
mock.timers.setTime(5_000_000 + options.maxAge + 1);
|
||||
await pruneCache(options);
|
||||
assert.deepEqual([...cache.entries.keys()], ['https://example.com/recent']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* `fetch` that keeps the responses in the Cache API, which the browser does whatever the headers
|
||||
* of the server are: the HTTP cache needs them to keep a response (and `cache: 'force-cache'`
|
||||
* can only use what it kept). A kept response is used until it is older than `maxAge`.
|
||||
*
|
||||
* It works without the Cache API (insecure pages, private windows, blocked storage): the
|
||||
* responses are then just not kept.
|
||||
*/
|
||||
|
||||
export type CachedFetchOptions = {
|
||||
/** Name of the cache the responses are kept in. */
|
||||
name: string;
|
||||
/** How long a kept response is used, in milliseconds. */
|
||||
maxAge: number;
|
||||
};
|
||||
|
||||
/** When a response was kept (ms since the epoch): the server's date can be wrong or missing. */
|
||||
const KEPT_AT = 'x-kept-at';
|
||||
|
||||
/** The caches that were cleaned of their old responses in this session. */
|
||||
const pruned = new Set<string>();
|
||||
|
||||
async function openCache(name: string): Promise<Cache | undefined> {
|
||||
try {
|
||||
return typeof caches === 'undefined' ? undefined : await caches.open(name);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function age(response: Response): number {
|
||||
const keptAt = Number(response.headers.get(KEPT_AT));
|
||||
return keptAt > 0 ? Date.now() - keptAt : Infinity;
|
||||
}
|
||||
|
||||
/** Deletes the responses that are too old to be used. */
|
||||
export async function pruneCache({ name, maxAge }: CachedFetchOptions) {
|
||||
const cache = await openCache(name);
|
||||
if (!cache) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
for (const request of await cache.keys()) {
|
||||
const response = await cache.match(request);
|
||||
if (!response || !(age(response) < maxAge)) {
|
||||
await cache.delete(request);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// what is left is cleaned another time
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The response for `url`: the kept one if there is one that is not too old, otherwise the one of
|
||||
* the network, which is kept if it is successful.
|
||||
*/
|
||||
export async function cachedFetch(url: string, options: CachedFetchOptions): Promise<Response> {
|
||||
const cache = await openCache(options.name);
|
||||
if (cache && !pruned.has(options.name)) {
|
||||
pruned.add(options.name);
|
||||
void pruneCache(options);
|
||||
}
|
||||
|
||||
try {
|
||||
const kept = await cache?.match(url);
|
||||
if (kept && age(kept) < options.maxAge) {
|
||||
return kept;
|
||||
}
|
||||
} catch {
|
||||
// as if there was nothing
|
||||
}
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok || !cache) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const headers = new Headers(response.headers);
|
||||
headers.set(KEPT_AT, String(Date.now()));
|
||||
const stamped = new Response(await response.arrayBuffer(), {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
});
|
||||
// best effort: the storage can be full
|
||||
cache.put(url, stamped.clone()).catch(() => undefined);
|
||||
return stamped;
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
Construction,
|
||||
} from '@lucide/svelte';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import type { Coordinates } from 'gpx';
|
||||
import type { Coordinates } from '$lib/geo';
|
||||
import type { SelectionStatistics } from '$lib/engine';
|
||||
import type { SlicedStatistics } from '$lib/logic/selection-statistics';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
|
||||
@@ -21,7 +21,7 @@ import Chart, {
|
||||
type TooltipItem,
|
||||
} from 'chart.js/auto';
|
||||
import { get, type Readable, type Writable } from 'svelte/store';
|
||||
import type { Coordinates } from 'gpx';
|
||||
import type { Coordinates } from '$lib/geo';
|
||||
import {
|
||||
engine,
|
||||
NO_TIME,
|
||||
@@ -456,7 +456,7 @@ export class ElevationProfile {
|
||||
highway: highways[index],
|
||||
sacScale: sacScales[index],
|
||||
mtbScale: mtbScales[index],
|
||||
coordinates: { lat: data.lat[index], lon: data.lng[index] },
|
||||
coordinates: { lat: data.lat[index], lng: data.lng[index] },
|
||||
index,
|
||||
});
|
||||
if (data.speed && (global.totalTime ?? 0) > 0) {
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
import { untrack } from 'svelte';
|
||||
import { isSelected, toggle } from '$lib/components/map/layer-control/utils';
|
||||
import { engine } from '$lib/engine';
|
||||
import { hoveredPoint } from '$lib/logic/statistics';
|
||||
import { slicedStatistics } from '$lib/logic/selection-statistics';
|
||||
import { hoveredPoint, slicedStatistics } from '$lib/logic/selection-statistics';
|
||||
|
||||
let {
|
||||
useHash = true,
|
||||
@@ -39,7 +38,6 @@
|
||||
distanceUnits,
|
||||
velocityUnits,
|
||||
temperatureUnits,
|
||||
fileOrder,
|
||||
distanceMarkers,
|
||||
directionMarkers,
|
||||
} = settings;
|
||||
|
||||
@@ -63,10 +63,10 @@
|
||||
let manualCamera = $state(false);
|
||||
let zoom = $state('0');
|
||||
let lat = $state('0');
|
||||
let lon = $state('0');
|
||||
let lng = $state('0');
|
||||
let bearing = $state('0');
|
||||
let pitch = $state('0');
|
||||
let hash = $derived(manualCamera ? `#${zoom}/${lat}/${lon}/${bearing}/${pitch}` : '');
|
||||
let hash = $derived(manualCamera ? `#${zoom}/${lat}/${lng}/${bearing}/${pitch}` : '');
|
||||
|
||||
$effect(() => {
|
||||
if (options.elevation.show || options.elevation.height) {
|
||||
@@ -78,7 +78,7 @@
|
||||
if ($map) {
|
||||
let center = $map.getCenter();
|
||||
lat = center.lat.toFixed(4);
|
||||
lon = center.lng.toFixed(4);
|
||||
lng = center.lng.toFixed(4);
|
||||
zoom = $map.getZoom().toFixed(2);
|
||||
bearing = $map.getBearing().toFixed(1);
|
||||
pitch = $map.getPitch().toFixed(0);
|
||||
@@ -293,7 +293,7 @@
|
||||
</Label>
|
||||
<Label class="flex flex-col gap-1">
|
||||
<span>{i18n._('embedding.longitude')}</span>
|
||||
<span>{lon}</span>
|
||||
<span>{lng}</span>
|
||||
</Label>
|
||||
<Label class="flex flex-col gap-1">
|
||||
<span>{i18n._('embedding.zoom')}</span>
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
import FileListNodeStore from './FileListNodeStore.svelte';
|
||||
import FileListNode from './FileListNode.svelte';
|
||||
import FileListNodeContent from './FileListNodeContent.svelte';
|
||||
import { ListLevel } from './file-list';
|
||||
import { allowedMoves, dragging, SortableFileList } from './sortable-file-list';
|
||||
import { allowedMoves, dragging, SortableFileList, type ListLevel } from './sortable-file-list';
|
||||
import { engine, type FileState } from '$lib/engine';
|
||||
import type { FileTreeNode } from '$lib/selection-helpers';
|
||||
|
||||
@@ -26,14 +25,14 @@
|
||||
let container: HTMLElement;
|
||||
let sortableLevel: ListLevel =
|
||||
node === null
|
||||
? ListLevel.FILE
|
||||
? 'file'
|
||||
: node.type === 'file'
|
||||
? waypointRoot
|
||||
? ListLevel.WAYPOINTS
|
||||
: ListLevel.TRACK
|
||||
? 'waypoints'
|
||||
: 'track'
|
||||
: node.type === 'waypoints'
|
||||
? ListLevel.WAYPOINT
|
||||
: ListLevel.SEGMENT;
|
||||
? 'waypoint'
|
||||
: 'segment';
|
||||
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
|
||||
|
||||
let canDrop = $derived($dragging !== null && allowedMoves[$dragging].includes(sortableLevel));
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
Scissors,
|
||||
FileStack,
|
||||
} from '@lucide/svelte';
|
||||
import { ListLevel } from './file-list';
|
||||
import { getContext } from 'svelte';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import MetadataDialog from '$lib/components/file-list/metadata/MetadataDialog.svelte';
|
||||
@@ -56,14 +55,7 @@
|
||||
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
|
||||
let embedding = getContext<boolean>('embedding');
|
||||
|
||||
const levels = {
|
||||
file: ListLevel.FILE,
|
||||
track: ListLevel.TRACK,
|
||||
segment: ListLevel.SEGMENT,
|
||||
waypoints: ListLevel.WAYPOINTS,
|
||||
waypoint: ListLevel.WAYPOINT,
|
||||
};
|
||||
let level = $derived(levels[node.type]);
|
||||
let level = $derived(node.type);
|
||||
|
||||
let singleSelection = $derived(selectionSize($selection) === 1);
|
||||
|
||||
@@ -128,7 +120,7 @@
|
||||
<MetadataDialog bind:open={openEditMetadata} {fileState} {node} />
|
||||
<StyleDialog bind:open={openEditStyle} {fileState} {node} />
|
||||
{/if}
|
||||
{#if level === ListLevel.FILE || level === ListLevel.TRACK}
|
||||
{#if level === 'file' || level === 'track'}
|
||||
<div
|
||||
class="absolute {orientation === 'vertical'
|
||||
? 'top-0 bottom-0 right-0 w-1'
|
||||
@@ -178,9 +170,9 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if level === ListLevel.SEGMENT}
|
||||
{#if level === 'segment'}
|
||||
<Waypoints size="16" class="mx-1 shrink-0" />
|
||||
{:else if level === ListLevel.WAYPOINT}
|
||||
{:else if level === 'waypoint'}
|
||||
{#if symbolKey && symbols[symbolKey].icon}
|
||||
{@const SymbolIcon = symbols[symbolKey].icon}
|
||||
<SymbolIcon size="16" class="mx-1 shrink-0" />
|
||||
@@ -250,7 +242,7 @@
|
||||
<ContextMenu.Separator />
|
||||
{/if}
|
||||
{/if}
|
||||
{#if level !== ListLevel.WAYPOINTS}
|
||||
{#if level !== 'waypoints'}
|
||||
<ContextMenu.Item onclick={() => engine.selectAll()}>
|
||||
<FileStack size="16" />
|
||||
{i18n._('menu.select_all')}
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
export enum ListLevel {
|
||||
ROOT,
|
||||
FILE,
|
||||
TRACK,
|
||||
SEGMENT,
|
||||
WAYPOINTS,
|
||||
WAYPOINT,
|
||||
}
|
||||
|
||||
export abstract class ListItem {
|
||||
[x: string]: any;
|
||||
level: ListLevel;
|
||||
|
||||
constructor(level: ListLevel) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
abstract getId(): string | number;
|
||||
abstract getFullId(): string;
|
||||
abstract getIdAtLevel(level: ListLevel): string | number | undefined;
|
||||
abstract getFileId(): string;
|
||||
abstract getParent(): ListItem;
|
||||
abstract extend(id: string | number): ListItem;
|
||||
}
|
||||
|
||||
export class ListRootItem extends ListItem {
|
||||
constructor() {
|
||||
super(ListLevel.ROOT);
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return 'root';
|
||||
}
|
||||
|
||||
getFullId(): string {
|
||||
return 'root';
|
||||
}
|
||||
|
||||
getIdAtLevel(level: ListLevel): string | number | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getFileId(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
getParent(): ListItem {
|
||||
return this;
|
||||
}
|
||||
|
||||
extend(id: string): ListFileItem {
|
||||
return new ListFileItem(id);
|
||||
}
|
||||
}
|
||||
|
||||
export class ListFileItem extends ListItem {
|
||||
fileId: string;
|
||||
|
||||
constructor(fileId: string) {
|
||||
super(ListLevel.FILE);
|
||||
this.fileId = fileId;
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return this.fileId;
|
||||
}
|
||||
|
||||
getFullId(): string {
|
||||
return this.fileId;
|
||||
}
|
||||
|
||||
getIdAtLevel(level: ListLevel): string | number | undefined {
|
||||
switch (level) {
|
||||
case ListLevel.ROOT:
|
||||
return this.fileId;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
getFileId(): string {
|
||||
return this.fileId;
|
||||
}
|
||||
|
||||
getParent(): ListItem {
|
||||
return new ListRootItem();
|
||||
}
|
||||
|
||||
extend(id: number | 'waypoints'): ListTrackItem | ListWaypointsItem {
|
||||
if (id === 'waypoints') {
|
||||
return new ListWaypointsItem(this.fileId);
|
||||
} else {
|
||||
return new ListTrackItem(this.fileId, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ListTrackItem extends ListItem {
|
||||
fileId: string;
|
||||
trackIndex: number;
|
||||
|
||||
constructor(fileId: string, trackIndex: number) {
|
||||
super(ListLevel.TRACK);
|
||||
this.fileId = fileId;
|
||||
this.trackIndex = trackIndex;
|
||||
}
|
||||
|
||||
getId(): number {
|
||||
return this.trackIndex;
|
||||
}
|
||||
|
||||
getFullId(): string {
|
||||
return `${this.fileId}-track-${this.trackIndex}`;
|
||||
}
|
||||
|
||||
getIdAtLevel(level: ListLevel): string | number | undefined {
|
||||
switch (level) {
|
||||
case ListLevel.ROOT:
|
||||
return this.fileId;
|
||||
case ListLevel.FILE:
|
||||
return this.trackIndex;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
getFileId(): string {
|
||||
return this.fileId;
|
||||
}
|
||||
|
||||
getTrackIndex(): number {
|
||||
return this.trackIndex;
|
||||
}
|
||||
|
||||
getParent(): ListItem {
|
||||
return new ListFileItem(this.fileId);
|
||||
}
|
||||
|
||||
extend(id: number): ListTrackSegmentItem {
|
||||
return new ListTrackSegmentItem(this.fileId, this.trackIndex, id);
|
||||
}
|
||||
}
|
||||
|
||||
export class ListTrackSegmentItem extends ListItem {
|
||||
fileId: string;
|
||||
trackIndex: number;
|
||||
segmentIndex: number;
|
||||
|
||||
constructor(fileId: string, trackIndex: number, segmentIndex: number) {
|
||||
super(ListLevel.SEGMENT);
|
||||
this.fileId = fileId;
|
||||
this.trackIndex = trackIndex;
|
||||
this.segmentIndex = segmentIndex;
|
||||
}
|
||||
|
||||
getId(): number {
|
||||
return this.segmentIndex;
|
||||
}
|
||||
|
||||
getFullId(): string {
|
||||
return `${this.fileId}-track-${this.trackIndex}--${this.segmentIndex}`;
|
||||
}
|
||||
|
||||
getIdAtLevel(level: ListLevel): string | number | undefined {
|
||||
switch (level) {
|
||||
case ListLevel.ROOT:
|
||||
return this.fileId;
|
||||
case ListLevel.FILE:
|
||||
return this.trackIndex;
|
||||
case ListLevel.TRACK:
|
||||
return this.segmentIndex;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
getFileId(): string {
|
||||
return this.fileId;
|
||||
}
|
||||
|
||||
getTrackIndex(): number {
|
||||
return this.trackIndex;
|
||||
}
|
||||
|
||||
getSegmentIndex(): number {
|
||||
return this.segmentIndex;
|
||||
}
|
||||
|
||||
getParent(): ListItem {
|
||||
return new ListTrackItem(this.fileId, this.trackIndex);
|
||||
}
|
||||
|
||||
extend(): ListTrackSegmentItem {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class ListWaypointsItem extends ListItem {
|
||||
fileId: string;
|
||||
|
||||
constructor(fileId: string) {
|
||||
super(ListLevel.WAYPOINTS);
|
||||
this.fileId = fileId;
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return 'waypoints';
|
||||
}
|
||||
|
||||
getFullId(): string {
|
||||
return `${this.fileId}-waypoints`;
|
||||
}
|
||||
|
||||
getIdAtLevel(level: ListLevel): string | number | undefined {
|
||||
switch (level) {
|
||||
case ListLevel.ROOT:
|
||||
return this.fileId;
|
||||
case ListLevel.FILE:
|
||||
return 'waypoints';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
getFileId(): string {
|
||||
return this.fileId;
|
||||
}
|
||||
|
||||
getParent(): ListItem {
|
||||
return new ListFileItem(this.fileId);
|
||||
}
|
||||
|
||||
extend(id: number): ListWaypointItem {
|
||||
return new ListWaypointItem(this.fileId, id);
|
||||
}
|
||||
}
|
||||
|
||||
export class ListWaypointItem extends ListItem {
|
||||
fileId: string;
|
||||
waypointIndex: number;
|
||||
|
||||
constructor(fileId: string, waypointIndex: number) {
|
||||
super(ListLevel.WAYPOINT);
|
||||
this.fileId = fileId;
|
||||
this.waypointIndex = waypointIndex;
|
||||
}
|
||||
|
||||
getId(): number {
|
||||
return this.waypointIndex;
|
||||
}
|
||||
|
||||
getFullId(): string {
|
||||
return `${this.fileId}-waypoint-${this.waypointIndex}`;
|
||||
}
|
||||
|
||||
getIdAtLevel(level: ListLevel): string | number | undefined {
|
||||
switch (level) {
|
||||
case ListLevel.ROOT:
|
||||
return this.fileId;
|
||||
case ListLevel.FILE:
|
||||
return 'waypoints';
|
||||
case ListLevel.WAYPOINTS:
|
||||
return this.waypointIndex;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
getFileId(): string {
|
||||
return this.fileId;
|
||||
}
|
||||
|
||||
getWaypointIndex(): number {
|
||||
return this.waypointIndex;
|
||||
}
|
||||
|
||||
getParent(): ListItem {
|
||||
return new ListWaypointsItem(this.fileId);
|
||||
}
|
||||
|
||||
extend(): ListWaypointItem {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export function sortItems(items: ListItem[], reverse: boolean = false) {
|
||||
items.sort((a, b) => {
|
||||
if (a instanceof ListTrackItem && b instanceof ListTrackItem) {
|
||||
return a.getTrackIndex() - b.getTrackIndex();
|
||||
} else if (a instanceof ListTrackSegmentItem && b instanceof ListTrackSegmentItem) {
|
||||
return a.getSegmentIndex() - b.getSegmentIndex();
|
||||
} else if (a instanceof ListWaypointItem && b instanceof ListWaypointItem) {
|
||||
return a.getWaypointIndex() - b.getWaypointIndex();
|
||||
}
|
||||
return a.level - b.level;
|
||||
});
|
||||
if (reverse) {
|
||||
items.reverse();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
import { isMac } from '$lib/utils';
|
||||
import Sortable, { type Direction } from 'sortablejs/Sortable';
|
||||
import { ListLevel } from './file-list';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { tick } from 'svelte';
|
||||
import { engine, type MoveTarget, type Selection } from '$lib/engine';
|
||||
import { isSelected, type FileTreeNode } from '$lib/selection-helpers';
|
||||
|
||||
/** What a list holds: the files, the tracks of a file, the segments of a track, or the waypoints. */
|
||||
export type ListLevel = 'file' | 'track' | 'segment' | 'waypoints' | 'waypoint';
|
||||
|
||||
export const allowedMoves: Record<ListLevel, ListLevel[]> = {
|
||||
[ListLevel.ROOT]: [],
|
||||
[ListLevel.FILE]: [ListLevel.FILE],
|
||||
[ListLevel.TRACK]: [ListLevel.FILE, ListLevel.TRACK],
|
||||
[ListLevel.SEGMENT]: [ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
|
||||
[ListLevel.WAYPOINTS]: [ListLevel.WAYPOINTS],
|
||||
[ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
|
||||
file: ['file'],
|
||||
track: ['file', 'track'],
|
||||
segment: ['file', 'track', 'segment'],
|
||||
waypoints: ['waypoints'],
|
||||
waypoint: ['waypoints', 'waypoint'],
|
||||
};
|
||||
|
||||
export const dragging = writable<ListLevel | null>(null);
|
||||
@@ -84,11 +85,11 @@ export class SortableFileList {
|
||||
childNode(id: string): FileTreeNode | undefined {
|
||||
const parent = this._parent;
|
||||
switch (this._sortableLevel) {
|
||||
case ListLevel.FILE:
|
||||
case 'file':
|
||||
return { type: 'file', fileId: id };
|
||||
case ListLevel.TRACK:
|
||||
case 'track':
|
||||
return parent ? { type: 'track', fileId: parent.fileId, trackId: id } : undefined;
|
||||
case ListLevel.SEGMENT:
|
||||
case 'segment':
|
||||
return parent?.type === 'track'
|
||||
? {
|
||||
type: 'segment',
|
||||
@@ -97,9 +98,9 @@ export class SortableFileList {
|
||||
segmentId: id,
|
||||
}
|
||||
: undefined;
|
||||
case ListLevel.WAYPOINTS:
|
||||
case 'waypoints':
|
||||
return parent ? { type: 'waypoints', fileId: parent.fileId } : undefined;
|
||||
case ListLevel.WAYPOINT:
|
||||
case 'waypoint':
|
||||
return parent
|
||||
? { type: 'waypoint', fileId: parent.fileId, waypointId: id }
|
||||
: undefined;
|
||||
@@ -110,11 +111,11 @@ export class SortableFileList {
|
||||
private selectionOf(ids: string[]): Selection | undefined {
|
||||
const parent = this._parent;
|
||||
switch (this._sortableLevel) {
|
||||
case ListLevel.FILE:
|
||||
case 'file':
|
||||
return { type: 'file', fileIds: ids };
|
||||
case ListLevel.TRACK:
|
||||
case 'track':
|
||||
return parent ? { type: 'track', fileId: parent.fileId, trackIds: ids } : undefined;
|
||||
case ListLevel.SEGMENT:
|
||||
case 'segment':
|
||||
return parent?.type === 'track'
|
||||
? {
|
||||
type: 'segment',
|
||||
@@ -123,9 +124,9 @@ export class SortableFileList {
|
||||
segmentIds: ids,
|
||||
}
|
||||
: undefined;
|
||||
case ListLevel.WAYPOINTS:
|
||||
case 'waypoints':
|
||||
return parent ? { type: 'waypoints', fileId: parent.fileId } : undefined;
|
||||
case ListLevel.WAYPOINT:
|
||||
case 'waypoint':
|
||||
return parent
|
||||
? { type: 'waypoint', fileId: parent.fileId, waypointIds: ids }
|
||||
: undefined;
|
||||
@@ -136,20 +137,20 @@ export class SortableFileList {
|
||||
private targetAt(index: number): MoveTarget | undefined {
|
||||
const parent = this._parent;
|
||||
switch (this._sortableLevel) {
|
||||
case ListLevel.FILE:
|
||||
case 'file':
|
||||
return { type: 'files', index };
|
||||
case ListLevel.TRACK:
|
||||
case 'track':
|
||||
return parent ? { type: 'tracks', fileId: parent.fileId, index } : undefined;
|
||||
case ListLevel.SEGMENT:
|
||||
case 'segment':
|
||||
return parent?.type === 'track'
|
||||
? { type: 'segments', fileId: parent.fileId, trackId: parent.trackId, index }
|
||||
: undefined;
|
||||
case ListLevel.WAYPOINTS:
|
||||
case 'waypoints':
|
||||
// the list holds the node standing for the waypoints: they go after the others
|
||||
return parent
|
||||
? { type: 'waypoints', fileId: parent.fileId, index: Infinity }
|
||||
: undefined;
|
||||
case ListLevel.WAYPOINT:
|
||||
case 'waypoint':
|
||||
return parent ? { type: 'waypoints', fileId: parent.fileId, index } : undefined;
|
||||
}
|
||||
}
|
||||
@@ -161,7 +162,7 @@ export class SortableFileList {
|
||||
return;
|
||||
}
|
||||
|
||||
if (from === to && this._sortableLevel === ListLevel.FILE) {
|
||||
if (from === to && this._sortableLevel === 'file') {
|
||||
// the order of the files is the one of the list
|
||||
this.updateToFileOrder();
|
||||
return;
|
||||
@@ -169,7 +170,7 @@ export class SortableFileList {
|
||||
|
||||
// The event is triggered on the source and on the destination list: handle it once.
|
||||
// Nothing to move in the list of the single node of the waypoints.
|
||||
if (to !== this || (from === to && this._sortableLevel === ListLevel.WAYPOINTS)) {
|
||||
if (to !== this || (from === to && this._sortableLevel === 'waypoints')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -245,19 +246,19 @@ export class SortableFileList {
|
||||
engine.select([]);
|
||||
} else {
|
||||
switch (this._sortableLevel) {
|
||||
case ListLevel.FILE:
|
||||
case 'file':
|
||||
engine.select(selected);
|
||||
break;
|
||||
case ListLevel.TRACK:
|
||||
case 'track':
|
||||
engine.selectTracks(parent!.fileId, selected);
|
||||
break;
|
||||
case ListLevel.SEGMENT:
|
||||
case 'segment':
|
||||
engine.selectSegments(parent!.fileId, (parent as any).trackId, selected);
|
||||
break;
|
||||
case ListLevel.WAYPOINTS:
|
||||
case 'waypoints':
|
||||
engine.selectWaypointGroup(parent!.fileId);
|
||||
break;
|
||||
case ListLevel.WAYPOINT:
|
||||
case 'waypoint':
|
||||
engine.selectWaypoints(parent!.fileId, selected);
|
||||
break;
|
||||
}
|
||||
@@ -266,7 +267,7 @@ export class SortableFileList {
|
||||
}
|
||||
|
||||
updateFromFileOrder() {
|
||||
if (!this._sortable || this._sortableLevel !== ListLevel.FILE) {
|
||||
if (!this._sortable || this._sortableLevel !== 'file') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -282,7 +283,7 @@ export class SortableFileList {
|
||||
}
|
||||
|
||||
updateToFileOrder() {
|
||||
if (!this._sortable || this._sortableLevel !== ListLevel.FILE) {
|
||||
if (!this._sortable || this._sortableLevel !== 'file') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -304,7 +305,7 @@ export class SortableFileList {
|
||||
if (element instanceof HTMLElement) {
|
||||
let attr = element.getAttribute('data-id');
|
||||
if (attr) {
|
||||
if (this._sortableLevel === ListLevel.FILE && !files.has(attr)) {
|
||||
if (this._sortableLevel === 'file' && !files.has(attr)) {
|
||||
element.remove();
|
||||
} else {
|
||||
this._elements[attr] = element;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { get, type Readable } from 'svelte/store';
|
||||
import type { GPXFile } from 'gpx';
|
||||
import maplibregl, {
|
||||
type GeoJSONSource,
|
||||
type FilterSpecification,
|
||||
@@ -13,7 +12,6 @@ import { getElevation, loadSVGIcon } from '$lib/utils';
|
||||
import { selectedWaypoint } from '$lib/components/toolbar/tools/waypoint/waypoint';
|
||||
import { MapPin, Square } from 'lucide-static';
|
||||
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
||||
import { fileStateCollection } from '$lib/logic/file-state';
|
||||
import { engine, type FileState, type Selection } from '$lib/engine';
|
||||
import { isCovered, hasSelectionWithin, type FileTreeNode } from '$lib/selection-helpers';
|
||||
import { isSegmentHidden, isWaypointHidden } from '$lib/file-visibility';
|
||||
@@ -615,7 +613,7 @@ export class GPXLayer {
|
||||
getElevation([
|
||||
{
|
||||
lat: e.lngLat.lat,
|
||||
lon: e.lngLat.lng,
|
||||
lng: e.lngLat.lng,
|
||||
},
|
||||
]).then(async (ele) => {
|
||||
if (this.draggedWaypointIndex === null) {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
||||
import { hoveredPoint } from '$lib/logic/statistics';
|
||||
import type { GeoJSONSource } from 'maplibre-gl';
|
||||
import { get } from 'svelte/store';
|
||||
import { map } from '$lib/components/map/map';
|
||||
import { allHidden } from '$lib/all-hidden';
|
||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
||||
import { loadSVGIcon } from '$lib/utils';
|
||||
import { slicedStatistics } from '$lib/logic/selection-statistics';
|
||||
import { hoveredPoint, slicedStatistics } from '$lib/logic/selection-statistics';
|
||||
import { engine } from '$lib/engine';
|
||||
|
||||
const startMarkerSVG = `<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -93,7 +92,7 @@ export class StartEndMarkers {
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [hovered.lon, hovered.lat],
|
||||
coordinates: [hovered.lng, hovered.lat],
|
||||
},
|
||||
properties: {
|
||||
icon: 'hover-marker',
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
const desc = Object.entries(tags)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join('\n');
|
||||
const [ele] = await getElevation([{ lat: poi.item.lat, lon: poi.item.lng }]);
|
||||
const [ele] = await getElevation([{ lat: poi.item.lat, lng: poi.item.lng }]);
|
||||
await engine.newWaypoint({
|
||||
lng: poi.item.lng,
|
||||
lat: poi.item.lat,
|
||||
|
||||
@@ -1,39 +1,38 @@
|
||||
import { SphericalMercator } from '@mapbox/sphericalmercator';
|
||||
import { getLayers } from './utils';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { liveQuery } from 'dexie';
|
||||
import { overpassQueryData } from '$lib/assets/layers';
|
||||
import { MapPopup } from '$lib/components/map/map-popup';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { db } from '$lib/db';
|
||||
import type { GeoJSONSource } from 'maplibre-gl';
|
||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
||||
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
||||
import { loadSVGIcon } from '$lib/utils';
|
||||
import { cachedFetch } from '$lib/cached-fetch';
|
||||
|
||||
const { currentOverpassQueries } = settings;
|
||||
|
||||
/** The responses of Overpass are kept in the browser for a week. */
|
||||
const CACHE = { name: 'overpass', maxAge: 7 * 24 * 3600 * 1000 };
|
||||
|
||||
const mercator = new SphericalMercator({
|
||||
size: 256,
|
||||
});
|
||||
|
||||
let data = writable<GeoJSON.FeatureCollection>({ type: 'FeatureCollection', features: [] });
|
||||
|
||||
liveQuery(() => db.overpassdata.toArray()).subscribe((pois) => {
|
||||
data.set({ type: 'FeatureCollection', features: pois.map((poi) => poi.poi) });
|
||||
});
|
||||
|
||||
export class OverpassLayer {
|
||||
overpassUrl = 'https://overpass.gpx.studio/api/interpreter';
|
||||
minZoom = 12;
|
||||
queryZoom = 12;
|
||||
expirationTime = 7 * 24 * 3600 * 1000;
|
||||
map: maplibregl.Map;
|
||||
layerEventManager: MapLayerEventManager;
|
||||
popup: MapPopup;
|
||||
|
||||
data = writable<GeoJSON.FeatureCollection>({ type: 'FeatureCollection', features: [] });
|
||||
|
||||
// What was queried this session: the queries that were made for each tile. The responses are
|
||||
// kept longer, see `cachedFetch`.
|
||||
queriedTiles = new Map<string, Set<string>>();
|
||||
currentQueries: Set<string> = new Set();
|
||||
nextQueries: Map<string, { x: number; y: number; queries: string[] }> = new Map();
|
||||
|
||||
unsubscribes: (() => void)[] = [];
|
||||
queryIfNeededBinded = this.queryIfNeeded.bind(this);
|
||||
@@ -54,7 +53,7 @@ export class OverpassLayer {
|
||||
add() {
|
||||
this.map.on('moveend', this.queryIfNeededBinded);
|
||||
this.map.on('style.load', this.updateBinded);
|
||||
this.unsubscribes.push(data.subscribe(this.updateBinded));
|
||||
this.unsubscribes.push(this.data.subscribe(this.updateBinded));
|
||||
this.unsubscribes.push(
|
||||
currentOverpassQueries.subscribe(() => {
|
||||
this.updateBinded();
|
||||
@@ -77,7 +76,7 @@ export class OverpassLayer {
|
||||
update() {
|
||||
this.loadIcons();
|
||||
|
||||
const fullData = get(data);
|
||||
const fullData = get(this.data);
|
||||
const queries = getCurrentQueries();
|
||||
const d: GeoJSON.FeatureCollection = {
|
||||
type: 'FeatureCollection',
|
||||
@@ -158,7 +157,6 @@ export class OverpassLayer {
|
||||
}
|
||||
|
||||
let tileLimits = mercator.xyz(bbox, this.queryZoom);
|
||||
let time = Date.now();
|
||||
|
||||
for (let x = tileLimits.minX; x <= tileLimits.maxX; x++) {
|
||||
for (let y = tileLimits.minY; y <= tileLimits.maxY; y++) {
|
||||
@@ -166,66 +164,35 @@ export class OverpassLayer {
|
||||
continue;
|
||||
}
|
||||
|
||||
db.overpasstiles
|
||||
.where('[x+y]')
|
||||
.equals([x, y])
|
||||
.toArray()
|
||||
.then((querytiles) => {
|
||||
let missingQueries = queries.filter(
|
||||
(query) =>
|
||||
!querytiles.some(
|
||||
(querytile) =>
|
||||
querytile.query === query &&
|
||||
time - querytile.time < this.expirationTime
|
||||
)
|
||||
);
|
||||
if (missingQueries.length > 0) {
|
||||
this.queryTile(x, y, missingQueries);
|
||||
}
|
||||
});
|
||||
const queried = this.queriedTiles.get(`${x},${y}`);
|
||||
const missingQueries = queries.filter((query) => !queried?.has(query));
|
||||
if (missingQueries.length > 0) {
|
||||
this.queryTile(x, y, missingQueries);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queryTile(x: number, y: number, queries: string[]) {
|
||||
if (this.currentQueries.size > 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentQueries.add(`${x},${y}`);
|
||||
|
||||
const bounds = mercator.bbox(x, y, this.queryZoom);
|
||||
fetch(`${this.overpassUrl}?data=${getQueryForBounds(bounds, queries)}`)
|
||||
.then(
|
||||
(response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
this.currentQueries.delete(`${x},${y}`);
|
||||
return Promise.reject();
|
||||
},
|
||||
() => this.currentQueries.delete(`${x},${y}`)
|
||||
)
|
||||
cachedFetch(`${this.overpassUrl}?data=${getQueryForBounds(bounds, queries)}`, CACHE)
|
||||
.then((response) => (response.ok ? response.json() : Promise.reject()))
|
||||
.then((data) => this.storeOverpassData(x, y, queries, data))
|
||||
.catch(() => this.currentQueries.delete(`${x},${y}`));
|
||||
}
|
||||
|
||||
storeOverpassData(x: number, y: number, queries: string[], data: any) {
|
||||
let time = Date.now();
|
||||
let queryTiles = queries.map((query) => ({ x, y, query, time }));
|
||||
let pois: { query: string; id: number; poi: GeoJSON.Feature }[] = [];
|
||||
|
||||
if (data.elements === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let element of data.elements) {
|
||||
for (let query of queries) {
|
||||
if (belongsToQuery(element, query)) {
|
||||
pois.push({
|
||||
query,
|
||||
id: element.id,
|
||||
poi: {
|
||||
this.data.update((updatedData) => {
|
||||
for (let element of data.elements) {
|
||||
for (let query of queries) {
|
||||
if (belongsToQuery(element, query)) {
|
||||
updatedData.features.push({
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
@@ -242,17 +209,17 @@ export class OverpassLayer {
|
||||
tags: element.tags,
|
||||
type: element.type,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.transaction('rw', db.overpasstiles, db.overpassdata, async () => {
|
||||
await db.overpasstiles.bulkPut(queryTiles);
|
||||
await db.overpassdata.bulkPut(pois);
|
||||
return updatedData;
|
||||
});
|
||||
|
||||
const queried = this.queriedTiles.get(`${x},${y}`) ?? new Set<string>();
|
||||
queries.forEach((query) => queried.add(query));
|
||||
this.queriedTiles.set(`${x},${y}`, queried);
|
||||
|
||||
this.currentQueries.delete(`${x},${y}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fileStateCollection } from '$lib/logic/file-state';
|
||||
import { engine } from '$lib/engine';
|
||||
import { get } from 'svelte/store';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
|
||||
type MapLayerMouseEventListener = (e: maplibregl.MapLayerMouseEvent) => void;
|
||||
@@ -246,17 +247,23 @@ export class MapLayerEventManager {
|
||||
layerIds: string[],
|
||||
bounds: maplibregl.LngLatBounds
|
||||
): string[] {
|
||||
const files = get(engine.files);
|
||||
let result = layerIds.filter((layerId) => {
|
||||
if (!this._map.getLayer(layerId)) return false;
|
||||
const fileId = layerId.replace('-waypoints', '');
|
||||
if (fileId === layerId) {
|
||||
return fileStateCollection.getStatistics(fileId)?.intersectsBBox(bounds) ?? true;
|
||||
} else {
|
||||
return (
|
||||
fileStateCollection.getStatistics(fileId)?.intersectsWaypointBBox(bounds) ??
|
||||
true
|
||||
);
|
||||
// the tracks of a file are all within its bounds
|
||||
const file = files.get(fileId);
|
||||
const box = file && get(file).statistics.bounds;
|
||||
return box
|
||||
? bounds.getWest() <= box.east &&
|
||||
bounds.getEast() >= box.west &&
|
||||
bounds.getSouth() <= box.north &&
|
||||
bounds.getNorth() >= box.south
|
||||
: true;
|
||||
}
|
||||
// the waypoints have no bounds of their own
|
||||
return true;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
async function addElevation() {
|
||||
const stats = get(statistics);
|
||||
const points = Array.from(stats.lng, (lon, i) => ({ lon, lat: stats.lat[i] }));
|
||||
const points = Array.from(stats.lng, (lng, i) => ({ lng, lat: stats.lat[i] }));
|
||||
const ele = await getElevation(points);
|
||||
|
||||
// the selection or the files may have changed while the elevation was loading
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { distance, type Coordinates } from 'gpx';
|
||||
import { distance, type Coordinates } from '$lib/geo';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import maplibregl, {
|
||||
type MapMouseEvent,
|
||||
@@ -50,17 +50,11 @@ type Anchor = {
|
||||
type AnchorProperties = { anchorIndex: number; minZoom: number };
|
||||
type AnchorFeature = GeoJSON.Feature<GeoJSON.Point, AnchorProperties>;
|
||||
|
||||
type Position = { lng: number; lat: number };
|
||||
|
||||
/** Whether the tool has to show controls for a selection: it covers segments, or can get some. */
|
||||
function isRoutable(selection: Selection) {
|
||||
return selection.type === 'file' || selection.type === 'track' || selection.type === 'segment';
|
||||
}
|
||||
|
||||
function toCoordinates({ lng, lat }: Position): Coordinates {
|
||||
return { lat, lon: lng };
|
||||
}
|
||||
|
||||
/**
|
||||
* The anchors of the selection of the engine, shown on the map where the user can drag them to
|
||||
* reroute the segments, or click on the segments to add some. Every interaction is turned into a
|
||||
@@ -103,7 +97,7 @@ export class RoutingControls {
|
||||
* Where the pointer hovers a segment of the selection: an anchor that can be dragged or
|
||||
* clicked to be added.
|
||||
*/
|
||||
temporaryAnchor: (Position & { segment: number }) | null = null;
|
||||
temporaryAnchor: (Coordinates & { segment: number }) | null = null;
|
||||
showTemporaryAnchorBinded: (e: MapLayerMouseEvent) => void =
|
||||
this.showTemporaryAnchor.bind(this);
|
||||
updateTemporaryAnchorBinded: (e: MapMouseEvent) => void = this.updateTemporaryAnchor.bind(this);
|
||||
@@ -328,11 +322,11 @@ export class RoutingControls {
|
||||
this.popup.remove();
|
||||
}
|
||||
|
||||
position(index: number): Position {
|
||||
position(index: number): Coordinates {
|
||||
return { lng: this.statistics!.lng[index], lat: this.statistics!.lat[index] };
|
||||
}
|
||||
|
||||
async moveAnchor(anchor: Anchor, coordinates: Position) {
|
||||
async moveAnchor(anchor: Anchor, coordinates: Coordinates) {
|
||||
// Move the anchor and update the route from and to the neighbouring anchors
|
||||
const initialAnchor = anchor;
|
||||
const initialCoordinates = { lng: anchor.lng, lat: anchor.lat };
|
||||
@@ -349,7 +343,7 @@ export class RoutingControls {
|
||||
const [previousAnchor, nextAnchor] = this.getNeighbouringAnchors(anchor);
|
||||
|
||||
const chain: Anchor[] = [];
|
||||
const targets: Position[] = [];
|
||||
const targets: Coordinates[] = [];
|
||||
if (previousAnchor) {
|
||||
chain.push(previousAnchor);
|
||||
targets.push(this.position(previousAnchor.index));
|
||||
@@ -457,7 +451,7 @@ export class RoutingControls {
|
||||
const first = this.position(start);
|
||||
const last = this.position(end - 1);
|
||||
// the end of the segment has to be close to its start, up to a kilometer
|
||||
return distance(toCoordinates(first), toCoordinates(last)) <= 1000;
|
||||
return distance(first, last) <= 1000;
|
||||
}
|
||||
|
||||
async appendAnchor(e: maplibregl.MapMouseEvent) {
|
||||
@@ -480,7 +474,7 @@ export class RoutingControls {
|
||||
this.appendAnchorWithCoordinates({ lng: e.lngLat.lng, lat: e.lngLat.lat });
|
||||
}
|
||||
|
||||
async appendAnchorWithCoordinates(coordinates: Position) {
|
||||
async appendAnchorWithCoordinates(coordinates: Coordinates) {
|
||||
// Add a new anchor to the end of the last segment
|
||||
const statistics = this.statistics;
|
||||
const revision = this.anchorData?.revision;
|
||||
@@ -546,7 +540,7 @@ export class RoutingControls {
|
||||
* single segment) or where they were moved to, and replaces what is between the first and
|
||||
* the last anchor by the route.
|
||||
*/
|
||||
async routeBetweenAnchors(anchors: Anchor[], targets: Position[]): Promise<boolean> {
|
||||
async routeBetweenAnchors(anchors: Anchor[], targets: Coordinates[]): Promise<boolean> {
|
||||
const revision = this.anchorData?.revision;
|
||||
if (revision === undefined) {
|
||||
return false;
|
||||
@@ -565,7 +559,7 @@ export class RoutingControls {
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await route(targets.map(toCoordinates));
|
||||
response = await route(targets);
|
||||
} catch (e: any) {
|
||||
toast.error(i18n._(e.message, e.message));
|
||||
return false;
|
||||
@@ -581,9 +575,9 @@ export class RoutingControls {
|
||||
const keepFirst =
|
||||
first.index !== segmentStart &&
|
||||
(first.index !== lastIndex ||
|
||||
distance(toCoordinates(targets[0]), {
|
||||
distance(targets[0], {
|
||||
lat: response.lat[0],
|
||||
lon: response.lng[0],
|
||||
lng: response.lng[0],
|
||||
}) > 1);
|
||||
const keepLast = last.index !== lastIndex;
|
||||
|
||||
@@ -843,7 +837,7 @@ export class RoutingControls {
|
||||
return false;
|
||||
}
|
||||
|
||||
moveAnchorFeature(anchor: Anchor, coordinates: Position) {
|
||||
moveAnchorFeature(anchor: Anchor, coordinates: Coordinates) {
|
||||
const source = get(map)?.getSource(
|
||||
this.layers.get(anchor.id === this.anchors.length ? 0 : anchor.zoom)?.id ?? ''
|
||||
) as GeoJSONSource | undefined;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { distance, type Coordinates } from 'gpx';
|
||||
import { distance, type Coordinates } from '$lib/geo';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { getElevation } from '$lib/utils';
|
||||
import { get } from 'svelte/store';
|
||||
@@ -143,7 +143,7 @@ async function getGraphHopperRoute(
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
points: points.map((point) => [point.lon, point.lat]),
|
||||
points: points.map((point) => [point.lng, point.lat]),
|
||||
profile: graphHopperProfile,
|
||||
elevation: true,
|
||||
points_encoded: false,
|
||||
@@ -209,7 +209,7 @@ async function getBRouterRoute(
|
||||
points: Coordinates[],
|
||||
brouterProfile: string
|
||||
): Promise<RoutedPoints> {
|
||||
let url = `https://brouter.de/brouter?lonlats=${points.map((point) => `${point.lon.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile}&format=geojson&alternativeidx=0`;
|
||||
let url = `https://brouter.de/brouter?lonlats=${points.map((point) => `${point.lng.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile}&format=geojson&alternativeidx=0`;
|
||||
|
||||
let response = await fetch(url);
|
||||
|
||||
@@ -283,15 +283,15 @@ function getIntermediatePoints(points: Coordinates[]): Promise<RoutedPoints> {
|
||||
const dist = distance(points[i], points[i + 1]) / 1000;
|
||||
for (let d = 0; d < dist; d += step) {
|
||||
const lat = points[i].lat + (d / dist) * (points[i + 1].lat - points[i].lat);
|
||||
const lon = points[i].lon + (d / dist) * (points[i + 1].lon - points[i].lon);
|
||||
addPoint(route, lon, lat, 0);
|
||||
const lng = points[i].lng + (d / dist) * (points[i + 1].lng - points[i].lng);
|
||||
addPoint(route, lng, lat, 0);
|
||||
}
|
||||
}
|
||||
|
||||
const last = points[points.length - 1];
|
||||
addPoint(route, last.lon, last.lat, 0);
|
||||
addPoint(route, last.lng, last.lat, 0);
|
||||
|
||||
return getElevation(route.lng.map((lng, i) => ({ lon: lng, lat: route.lat[i] }))).then(
|
||||
return getElevation(route.lng.map((lng, i) => ({ lng, lat: route.lat[i] }))).then(
|
||||
(elevations) => {
|
||||
route.ele = elevations;
|
||||
return route;
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { ramerDouglasPeucker, type GPXFile, type TrackSegment } from 'gpx';
|
||||
|
||||
const earthRadius = 6371008.8;
|
||||
|
||||
export const MIN_ANCHOR_ZOOM = 0;
|
||||
export const MAX_ANCHOR_ZOOM = 22;
|
||||
|
||||
export function getZoomLevelForDistance(latitude: number, distance?: number): number {
|
||||
if (distance === undefined) {
|
||||
return MIN_ANCHOR_ZOOM;
|
||||
}
|
||||
|
||||
const rad = Math.PI / 180;
|
||||
const lat = latitude * rad;
|
||||
|
||||
return Math.min(
|
||||
MAX_ANCHOR_ZOOM,
|
||||
Math.max(MIN_ANCHOR_ZOOM, Math.round(Math.log2((earthRadius * Math.cos(lat)) / distance)))
|
||||
);
|
||||
}
|
||||
|
||||
export function updateAnchorPoints(file: GPXFile) {
|
||||
let segments = file.getSegments();
|
||||
|
||||
for (let segment of segments) {
|
||||
if (!segment._data.anchors) {
|
||||
// New segment, compute anchor points for it
|
||||
computeAnchorPoints(segment);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (segment.trkpt.length > 0) {
|
||||
// Ensure first and last points are anchors and always visible
|
||||
segment.trkpt[0]._data.anchor = true;
|
||||
segment.trkpt[0]._data.zoom = 0;
|
||||
segment.trkpt[segment.trkpt.length - 1]._data.anchor = true;
|
||||
segment.trkpt[segment.trkpt.length - 1]._data.zoom = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function computeAnchorPoints(segment: TrackSegment) {
|
||||
let points = segment.trkpt;
|
||||
let anchors = ramerDouglasPeucker(points, 1);
|
||||
anchors.forEach((anchor) => {
|
||||
let point = anchor.point;
|
||||
point._data.anchor = true;
|
||||
point._data.zoom = getZoomLevelForDistance(point.getLatitude(), anchor.distance);
|
||||
});
|
||||
segment._data.anchors = true;
|
||||
}
|
||||
@@ -88,7 +88,7 @@
|
||||
longitude = parseFloat(longitude.toFixed(6));
|
||||
|
||||
const target = $selectedWaypoint;
|
||||
const [ele] = await getElevation([{ lat: latitude, lon: longitude }]);
|
||||
const [ele] = await getElevation([{ lat: latitude, lng: longitude }]);
|
||||
const waypoint = {
|
||||
lng: longitude,
|
||||
lat: latitude,
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import Dexie from 'dexie';
|
||||
import type { GPXFile } from 'gpx';
|
||||
import { enableMapSet, enablePatches, type Patch } from 'immer';
|
||||
|
||||
enableMapSet();
|
||||
enablePatches();
|
||||
|
||||
export class Database extends Dexie {
|
||||
fileids!: Dexie.Table<string, string>;
|
||||
files!: Dexie.Table<GPXFile, string>;
|
||||
patches!: Dexie.Table<{ patch: Patch[]; inversePatch: Patch[]; index: number }, number>;
|
||||
settings!: Dexie.Table<any, string>;
|
||||
overpasstiles!: Dexie.Table<
|
||||
{ query: string; x: number; y: number; time: number },
|
||||
[string, number, number]
|
||||
>;
|
||||
overpassdata!: Dexie.Table<
|
||||
{ query: string; id: number; poi: GeoJSON.Feature },
|
||||
[string, number]
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
super('Database', {
|
||||
cache: 'immutable',
|
||||
});
|
||||
this.version(1).stores({
|
||||
fileids: ',&fileid',
|
||||
files: '',
|
||||
patches: ',patch',
|
||||
settings: '',
|
||||
overpasstiles: '[query+x+y],[x+y]',
|
||||
overpassdata: '[query+id]',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const db = new Database();
|
||||
@@ -7,11 +7,11 @@ title: Files and statistics
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Fitxers i estadístiques
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ També pots utilitzar la rodeta del ratolí per apropar o allunyar el perfil d'e
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ També pots utilitzar la rodeta del ratolí per apropar o allunyar el perfil d'e
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Soubory a statistiky
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ Pomocí kolečka myši můžete také výškový profil přiblížit a oddálit
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ Pomocí kolečka myši můžete také výškový profil přiblížit a oddálit
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Files and statistics
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Dateien und Statistiken
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ Sie können auch das Mausrad verwenden, um auf dem Höhenprofil heranzuzoomen un
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ Sie können auch das Mausrad verwenden, um auf dem Höhenprofil heranzuzoomen un
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Files and statistics
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Files and statistics
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Archivos y estadísticas
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ Puede usar el ratón para acercar o alejar el perfil de elevación y moverse hac
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ Puede usar el ratón para acercar o alejar el perfil de elevación y moverse hac
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Fitxategiak eta estatistikak
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -82,8 +82,8 @@ Saguaren gurpila ere erabil dezakezu altueren profila handitzeko eta mugitzeko e
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -91,8 +91,8 @@ Saguaren gurpila ere erabil dezakezu altueren profila handitzeko eta mugitzeko e
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Files and statistics
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Fichiers et statistiques
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ Vous pouvez également utiliser la molette de la souris pour zoomer ou dézoomer
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ Vous pouvez également utiliser la molette de la souris pour zoomer ou dézoomer
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Files and statistics
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Fájlok és statisztikák
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ Az egérgörgővel is méretezheti a magassági profilt. Balra és jobbra mozogh
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ Az egérgörgővel is méretezheti a magassági profilt. Balra és jobbra mozogh
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Files and statistics
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: File e statistiche
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ Fare clic sul profilo per resettare la selezione.
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ Fare clic sul profilo per resettare la selezione.
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Files and statistics
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Puslapis nerastas
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Files and statistics
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Bestanden en statistieken
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ Je kunt het muiswiel ook gebruiken om in en uit te zoomen op het hoogte profiel,
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ Je kunt het muiswiel ook gebruiken om in en uit te zoomen op het hoogte profiel,
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Files and statistics
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Pliki i statystyki
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Arquivos e estatísticas
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ Você também pode usar a roda do mouse para aumentar e diminuir o zoom no perfi
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ Você também pode usar a roda do mouse para aumentar e diminuir o zoom no perfi
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Files and statistics
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Files and statistics
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Файлы и статистика
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ title: Файлы и статистика
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ title: Файлы и статистика
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Datoteke i statistike
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ Takođe možete da koristite točkić miša da zumirate i umanjite profil nadmo
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ Takođe možete da koristite točkić miša da zumirate i umanjite profil nadmo
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Files and statistics
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ Du kan också använda mushjulet för att zooma in och ut på höjdprofilen och
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ Du kan också använda mushjulet för att zooma in och ut på höjdprofilen och
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Files and statistics
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Dosyalar ve istatistikler
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -82,8 +82,8 @@ Ayrıca, fare tekerleği ile yükselti profiline yakınlaşıp uzaklaşabilir ve
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -91,8 +91,8 @@ Ayrıca, fare tekerleği ile yükselti profiline yakınlaşıp uzaklaşabilir ve
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Файли та статистика
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: Files and statistics
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: 檔案和統計
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,11 +7,11 @@ title: 文件和统计
|
||||
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 { exampleStatistics } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
let elevationFill = writable(undefined);
|
||||
@@ -83,8 +83,8 @@ title: 文件和统计
|
||||
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
@@ -92,8 +92,8 @@ title: 文件和统计
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{statistics}
|
||||
{slicedStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+102
-1
@@ -1,7 +1,14 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { LEGACY_IMPORTED_KEY, readLegacyData } from '$lib/legacy-import';
|
||||
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
||||
import { FileColorAllocator, normalizeColor } from '$lib/file-colors';
|
||||
import { setHidden, type Visibility } from '$lib/file-visibility';
|
||||
import {
|
||||
parseVisibility,
|
||||
setHidden,
|
||||
stringifyVisibility,
|
||||
VISIBILITY_KEY,
|
||||
type Visibility,
|
||||
} from '$lib/file-visibility';
|
||||
import { categoryIntervals, type CategoryIntervals } from '$lib/trackpoint-categories';
|
||||
import { selectedElementIds, type FileTreeNode } from '$lib/selection-helpers';
|
||||
import type { Feature, FeatureCollection, LineString, Point } from 'geojson';
|
||||
@@ -32,6 +39,9 @@ export type {
|
||||
|
||||
type Wasm = typeof import('gpx-rs');
|
||||
|
||||
/** Name of the IndexedDB database that keeps the files and the settings. */
|
||||
const STORAGE_NAME = 'gpx-studio';
|
||||
|
||||
/**
|
||||
* How a merge puts the selected elements together: `connect` makes a single segment of their
|
||||
* trackpoints, `group` keeps them as they are, in the first one.
|
||||
@@ -345,6 +355,85 @@ class Engine {
|
||||
: new Promise(() => {});
|
||||
}
|
||||
|
||||
// Storage
|
||||
|
||||
private _storage?: Promise<Record<string, string>>;
|
||||
|
||||
/**
|
||||
* Opens the storage of the browser: the files that it holds are put in the engine, and the
|
||||
* changes of the files are kept from then on. Resolves to the settings, as JSON strings by
|
||||
* key. Only the app does it (the embedded map has no files of its own), once.
|
||||
*
|
||||
* The first time, what the previous versions of the app kept in the browser is imported.
|
||||
*/
|
||||
openStorage(): Promise<Record<string, string>> {
|
||||
this._storage ??= this.openStorageOnce();
|
||||
return this._storage;
|
||||
}
|
||||
|
||||
private async openStorageOnce(): Promise<Record<string, string>> {
|
||||
await this.ready;
|
||||
const wasm = this.wasm!;
|
||||
let settings: Record<string, string>;
|
||||
try {
|
||||
settings = (await wasm.open_storage(STORAGE_NAME)) as Record<string, string>;
|
||||
} catch (error) {
|
||||
console.error('The files cannot be kept in this browser', error);
|
||||
return {};
|
||||
}
|
||||
// what was hidden, which the files that were restored need when they are read
|
||||
parseVisibility(settings[VISIBILITY_KEY]).forEach((visibility, fileId) =>
|
||||
this._visibility.set(fileId, visibility)
|
||||
);
|
||||
this.sync(wasm);
|
||||
|
||||
if (settings[LEGACY_IMPORTED_KEY] === undefined && get(this._order).length === 0) {
|
||||
settings = { ...settings, ...(await this.importLegacyData(wasm)) };
|
||||
await wasm.flush_storage();
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
/** Puts the files and the settings of the former database in the engine. */
|
||||
private async importLegacyData(wasm: Wasm): Promise<Record<string, string>> {
|
||||
const imported: Record<string, string> = {};
|
||||
try {
|
||||
const legacy = await readLegacyData();
|
||||
if (legacy) {
|
||||
const encoder = new TextEncoder();
|
||||
if (legacy.files.length > 0) {
|
||||
await this.loadFiles(
|
||||
legacy.files.map((file) => ({
|
||||
data: encoder.encode(file.gpx),
|
||||
name: file.name,
|
||||
}))
|
||||
);
|
||||
await this.select([]);
|
||||
}
|
||||
for (const [key, value] of Object.entries(legacy.settings)) {
|
||||
wasm.set_setting(key, value);
|
||||
imported[key] = value;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// it is tried again next time
|
||||
console.error('The files of the previous version could not be imported', error);
|
||||
return imported;
|
||||
}
|
||||
wasm.set_setting(LEGACY_IMPORTED_KEY, 'true');
|
||||
imported[LEGACY_IMPORTED_KEY] = 'true';
|
||||
return imported;
|
||||
}
|
||||
|
||||
/** Keeps a setting, `json` being its JSON. Nothing is kept before `openStorage` is done. */
|
||||
setSetting(key: string, json: string) {
|
||||
this.wasm?.set_setting(key, json);
|
||||
}
|
||||
|
||||
deleteSetting(key: string) {
|
||||
this.wasm?.delete_setting(key);
|
||||
}
|
||||
|
||||
// Actions. Each one resolves to whether the engine changed something.
|
||||
|
||||
/**
|
||||
@@ -845,6 +934,18 @@ class Engine {
|
||||
);
|
||||
this._visibility.set(fileId, visibility);
|
||||
store.update((state) => ({ ...state, visibility }));
|
||||
this.saveVisibility();
|
||||
}
|
||||
|
||||
/** Keeps what is hidden, for the files that exist (the others are not coming back). */
|
||||
private saveVisibility() {
|
||||
const files = get(this._files);
|
||||
const json = stringifyVisibility(this._visibility, (fileId) => files.has(fileId));
|
||||
if (json === undefined) {
|
||||
this.deleteSetting(VISIBILITY_KEY);
|
||||
} else {
|
||||
this.setSetting(VISIBILITY_KEY, json);
|
||||
}
|
||||
}
|
||||
|
||||
// Coordinates, as flat [lng, lat, ...] arrays. The getters copy them out of the WASM memory.
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
isHidden,
|
||||
isSegmentHidden,
|
||||
isWaypointHidden,
|
||||
parseVisibility,
|
||||
setHidden,
|
||||
stringifyVisibility,
|
||||
waypointsKey,
|
||||
type Visibility,
|
||||
type VisibilityState,
|
||||
@@ -142,3 +144,54 @@ describe('file visibility', () => {
|
||||
assert.deepEqual([...before], [['S1', false]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('kept visibility', () => {
|
||||
const visibility = (entries: [string, boolean][]): Visibility => new Map(entries);
|
||||
|
||||
it('comes back as it was kept', () => {
|
||||
const files = new Map([
|
||||
[
|
||||
'F',
|
||||
visibility([
|
||||
['F', false],
|
||||
['S1', true],
|
||||
[WPTS, false],
|
||||
]),
|
||||
],
|
||||
['G', visibility([['G', false]])],
|
||||
]);
|
||||
const json = stringifyVisibility(files, () => true);
|
||||
assert.deepEqual(parseVisibility(json), files);
|
||||
});
|
||||
|
||||
it('leaves out the files that are gone and the ones with nothing set', () => {
|
||||
const files = new Map([
|
||||
['F', visibility([['F', false]])],
|
||||
['G', visibility([['G', false]])],
|
||||
['H', visibility([])],
|
||||
]);
|
||||
const json = stringifyVisibility(files, (fileId) => fileId !== 'G');
|
||||
assert.deepEqual([...parseVisibility(json).keys()], ['F']);
|
||||
});
|
||||
|
||||
it('is nothing when there is nothing to keep', () => {
|
||||
assert.equal(
|
||||
stringifyVisibility(new Map(), () => true),
|
||||
undefined
|
||||
);
|
||||
assert.equal(
|
||||
stringifyVisibility(new Map([['F', visibility([['F', false]])]]), () => false),
|
||||
undefined
|
||||
);
|
||||
assert.equal(parseVisibility(undefined).size, 0);
|
||||
});
|
||||
|
||||
it('ignores what is not visibility', () => {
|
||||
assert.equal(parseVisibility('not json').size, 0);
|
||||
assert.equal(parseVisibility('3').size, 0);
|
||||
assert.equal(parseVisibility('null').size, 0);
|
||||
const parsed = parseVisibility('{"F":{"F":false,"S1":"no"},"G":3,"H":{}}');
|
||||
assert.deepEqual([...parsed.keys()], ['F']);
|
||||
assert.deepEqual([...parsed.get('F')!], [['F', false]]);
|
||||
});
|
||||
});
|
||||
@@ -110,3 +110,55 @@ export function setHidden(
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
/** Name under which the visibility of the files is kept in the storage. */
|
||||
export const VISIBILITY_KEY = 'visibility';
|
||||
|
||||
/**
|
||||
* The visibility of the files as JSON, to keep it: the explicit visibility of each file by id,
|
||||
* leaving out the files that `keep` refuses (the ones that are gone) and the ones that have none.
|
||||
* `undefined` when there is nothing to keep.
|
||||
*/
|
||||
export function stringifyVisibility(
|
||||
files: ReadonlyMap<string, Visibility>,
|
||||
keep: (fileId: string) => boolean
|
||||
): string | undefined {
|
||||
const data: Record<string, Record<string, boolean>> = {};
|
||||
files.forEach((visibility, fileId) => {
|
||||
if (visibility.size > 0 && keep(fileId)) {
|
||||
data[fileId] = Object.fromEntries(visibility);
|
||||
}
|
||||
});
|
||||
return Object.keys(data).length > 0 ? JSON.stringify(data) : undefined;
|
||||
}
|
||||
|
||||
/** What `stringifyVisibility` gave back. What does not make sense is left out. */
|
||||
export function parseVisibility(json: string | undefined): Map<string, Visibility> {
|
||||
const files = new Map<string, Visibility>();
|
||||
if (json === undefined) {
|
||||
return files;
|
||||
}
|
||||
try {
|
||||
const data: unknown = JSON.parse(json);
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
return files;
|
||||
}
|
||||
for (const [fileId, entries] of Object.entries(data)) {
|
||||
if (typeof entries !== 'object' || entries === null) {
|
||||
continue;
|
||||
}
|
||||
const visibility = new Map<string, boolean>();
|
||||
for (const [id, shown] of Object.entries(entries)) {
|
||||
if (typeof shown === 'boolean') {
|
||||
visibility.set(id, shown);
|
||||
}
|
||||
}
|
||||
if (visibility.size > 0) {
|
||||
files.set(fileId, visibility);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// not something that was kept
|
||||
}
|
||||
return files;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export type Coordinates = {
|
||||
lat: number;
|
||||
lng: number;
|
||||
};
|
||||
|
||||
const EARTH_RADIUS = 6371008.8;
|
||||
|
||||
/** The distance in meters between two coordinates (haversine formula). */
|
||||
export function distance(a: Coordinates, b: Coordinates): number {
|
||||
const rad = Math.PI / 180;
|
||||
const lat1 = a.lat * rad;
|
||||
const lat2 = b.lat * rad;
|
||||
const dLat = lat2 - lat1;
|
||||
const dLng = (b.lng - a.lng) * rad;
|
||||
const h =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
||||
return EARTH_RADIUS * 2 * Math.asin(Math.sqrt(Math.min(h, 1)));
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* One-time import of what the previous versions of the app kept in the browser: the files and the
|
||||
* settings of an IndexedDB database named `Database` (managed with Dexie, which is not needed to
|
||||
* read it). The files were stored as the objects of the former `gpx` library; they are written
|
||||
* back as GPX, which the engine loads. The old database is left as it was.
|
||||
*/
|
||||
|
||||
const LEGACY_DATABASE = 'Database';
|
||||
|
||||
/** Setting that tells that the import is done (or that there was nothing to import). */
|
||||
export const LEGACY_IMPORTED_KEY = 'legacyImported';
|
||||
|
||||
/** Settings of the old database that mean nothing anymore. */
|
||||
const DROPPED_SETTINGS = new Set(['patchIndex', 'fileOrder']);
|
||||
|
||||
type Json = any;
|
||||
|
||||
function request<T>(request: IDBRequest<T>): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
/** Opens the old database, `undefined` if there is none (and none is created). */
|
||||
async function openLegacyDatabase(): Promise<IDBDatabase | undefined> {
|
||||
if (typeof indexedDB === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof indexedDB.databases === 'function') {
|
||||
const databases = await indexedDB.databases();
|
||||
if (!databases.some((database) => database.name === LEGACY_DATABASE)) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const open = indexedDB.open(LEGACY_DATABASE);
|
||||
let created = false;
|
||||
open.onupgradeneeded = () => {
|
||||
// it did not exist: do not leave an empty database behind
|
||||
created = true;
|
||||
open.transaction?.abort();
|
||||
};
|
||||
open.onsuccess = () => resolve(open.result);
|
||||
open.onerror = () => {
|
||||
if (created) {
|
||||
indexedDB.deleteDatabase(LEGACY_DATABASE);
|
||||
}
|
||||
resolve(undefined);
|
||||
};
|
||||
open.onblocked = () => resolve(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
async function readAll(
|
||||
database: IDBDatabase,
|
||||
store: string
|
||||
): Promise<Map<string, Json> | undefined> {
|
||||
if (!database.objectStoreNames.contains(store)) {
|
||||
return undefined;
|
||||
}
|
||||
const objectStore = database.transaction(store, 'readonly').objectStore(store);
|
||||
const [keys, values] = await Promise.all([
|
||||
request(objectStore.getAllKeys()),
|
||||
request(objectStore.getAll()),
|
||||
]);
|
||||
return new Map(keys.map((key, i) => [String(key), values[i]]));
|
||||
}
|
||||
|
||||
// GPX, from the stored objects
|
||||
|
||||
function escape(text: unknown): string {
|
||||
return String(text).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
||||
}
|
||||
|
||||
function element(name: string, value: unknown): string {
|
||||
return value === undefined || value === null || value === ''
|
||||
? ''
|
||||
: `<${name}>${escape(value)}</${name}>`;
|
||||
}
|
||||
|
||||
function timeElement(value: unknown): string {
|
||||
const date = value instanceof Date ? value : new Date(value as string);
|
||||
return isNaN(date.getTime()) ? '' : element('time', date.toISOString());
|
||||
}
|
||||
|
||||
function link(value: Json): string {
|
||||
const href = value?.attributes?.href;
|
||||
return href
|
||||
? `<link href="${escape(href).replaceAll('"', '"')}">${element('text', value.text)}</link>`
|
||||
: '';
|
||||
}
|
||||
|
||||
function coordinates(point: Json): string {
|
||||
const { lat, lon } = point.attributes ?? {};
|
||||
return `lat="${Number(lat)}" lon="${Number(lon)}"`;
|
||||
}
|
||||
|
||||
function trackpoint(point: Json): string {
|
||||
const extensions = point.extensions ?? {};
|
||||
const trackPointExtension = extensions['gpxtpx:TrackPointExtension'] ?? {};
|
||||
const osm = Object.entries(trackPointExtension['gpxtpx:Extensions'] ?? {})
|
||||
.map(([key, value]) => element(key, value))
|
||||
.join('');
|
||||
const inner = [
|
||||
element('gpxtpx:atemp', trackPointExtension['gpxtpx:atemp']),
|
||||
element('gpxtpx:hr', trackPointExtension['gpxtpx:hr']),
|
||||
element('gpxtpx:cad', trackPointExtension['gpxtpx:cad']),
|
||||
osm ? `<gpxtpx:Extensions>${osm}</gpxtpx:Extensions>` : '',
|
||||
].join('');
|
||||
const power = element(
|
||||
'gpxpx:PowerInWatts',
|
||||
extensions['gpxpx:PowerExtension']?.['gpxpx:PowerInWatts']
|
||||
);
|
||||
const extension =
|
||||
inner || power
|
||||
? `<extensions>${inner ? `<gpxtpx:TrackPointExtension>${inner}</gpxtpx:TrackPointExtension>` : ''}${power ? `<gpxpx:PowerExtension>${power}</gpxpx:PowerExtension>` : ''}</extensions>`
|
||||
: '';
|
||||
return `<trkpt ${coordinates(point)}>${element('ele', point.ele)}${point.time ? timeElement(point.time) : ''}${extension}</trkpt>`;
|
||||
}
|
||||
|
||||
function waypoint(point: Json): string {
|
||||
return `<wpt ${coordinates(point)}>${element('ele', point.ele)}${point.time ? timeElement(point.time) : ''}${element('name', point.name)}${element('cmt', point.cmt)}${element('desc', point.desc)}${link(point.link)}${element('sym', point.sym)}${element('type', point.type)}</wpt>`;
|
||||
}
|
||||
|
||||
function track(track: Json): string {
|
||||
const style = track.extensions?.['gpx_style:line'];
|
||||
const line = style
|
||||
? `<extensions><gpx_style:line>${element('gpx_style:color', style['gpx_style:color'])}${element('gpx_style:opacity', style['gpx_style:opacity'])}${element('gpx_style:width', style['gpx_style:width'])}</gpx_style:line></extensions>`
|
||||
: '';
|
||||
const segments = (track.trkseg ?? [])
|
||||
.map(
|
||||
(segment: Json) => `<trkseg>${(segment.trkpt ?? []).map(trackpoint).join('')}</trkseg>`
|
||||
)
|
||||
.join('');
|
||||
return `<trk>${element('name', track.name)}${element('cmt', track.cmt)}${element('desc', track.desc)}${element('src', track.src)}${link(track.link)}${element('type', track.type)}${line}${segments}</trk>`;
|
||||
}
|
||||
|
||||
/** A file as the old versions stored it, as GPX. */
|
||||
export function legacyFileToGPX(file: Json): string {
|
||||
const metadata = file.metadata ?? {};
|
||||
const author = metadata.author;
|
||||
const authorXml = author
|
||||
? `<author>${element('name', author.name)}${link(author.link)}</author>`
|
||||
: '';
|
||||
const metadataXml = `<metadata>${element('name', metadata.name)}${element('desc', metadata.desc)}${authorXml}${link(metadata.link)}${metadata.time ? timeElement(metadata.time) : ''}</metadata>`;
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx version="1.1" creator="https://gpx.studio" xmlns="http://www.topografix.com/GPX/1/1" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxpx="http://www.garmin.com/xmlschemas/PowerExtension/v1" xmlns:gpx_style="http://www.topografix.com/GPX/gpx_style/0/2">${metadataXml}${(file.wpt ?? []).map(waypoint).join('')}${(file.trk ?? []).map(track).join('')}</gpx>`;
|
||||
}
|
||||
|
||||
/** What the old database holds. */
|
||||
export type LegacyData = {
|
||||
/** The files as GPX, in the order they had. */
|
||||
files: { gpx: string; name: string }[];
|
||||
/** The settings, as JSON strings by key. */
|
||||
settings: Record<string, string>;
|
||||
};
|
||||
|
||||
/** Reads the old database, `undefined` if there is none. */
|
||||
export async function readLegacyData(): Promise<LegacyData | undefined> {
|
||||
const database = await openLegacyDatabase();
|
||||
if (!database) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const [files, settings] = await Promise.all([
|
||||
readAll(database, 'files'),
|
||||
readAll(database, 'settings'),
|
||||
]);
|
||||
if (!files && !settings) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// the order of the files was a setting; the ids (`gpx-<n>`) tell the order they were added in
|
||||
const byAge = [...(files?.keys() ?? [])].sort(
|
||||
(a, b) => parseInt(a.split('-')[1]) - parseInt(b.split('-')[1])
|
||||
);
|
||||
const order: string[] = Array.isArray(settings?.get('fileOrder'))
|
||||
? settings!.get('fileOrder')
|
||||
: [];
|
||||
const ids = [
|
||||
...order.filter((id) => files?.has(id)),
|
||||
...byAge.filter((id) => !order.includes(id)),
|
||||
];
|
||||
|
||||
const result: LegacyData = { files: [], settings: {} };
|
||||
for (const id of ids) {
|
||||
try {
|
||||
const file = files!.get(id);
|
||||
result.files.push({
|
||||
gpx: legacyFileToGPX(file),
|
||||
name: file?.metadata?.name || 'file',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Could not import the file ${id}`, error);
|
||||
}
|
||||
}
|
||||
for (const [key, value] of settings ?? []) {
|
||||
if (!DROPPED_SETTINGS.has(key) && value !== undefined) {
|
||||
try {
|
||||
result.settings[key] = JSON.stringify(value);
|
||||
} catch {
|
||||
// not something that can be kept
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
import { db, type Database } from '$lib/db';
|
||||
import { liveQuery } from 'dexie';
|
||||
import type { GPXFile } from 'gpx';
|
||||
import { applyPatches, produceWithPatches, type Patch, type WritableDraft } from 'immer';
|
||||
import { GPXFileStateCollectionObserver } from '$lib/logic/file-state';
|
||||
import {
|
||||
derived,
|
||||
get,
|
||||
writable,
|
||||
type Readable,
|
||||
type Unsubscriber,
|
||||
type Writable,
|
||||
} from 'svelte/store';
|
||||
import { selection } from '$lib/logic/selection';
|
||||
|
||||
const MAX_PATCHES = 100;
|
||||
|
||||
export class FileActionManager {
|
||||
private _db: Database;
|
||||
private _files: Map<string, GPXFile>;
|
||||
private _fileSubscriptions: Map<string, Unsubscriber>;
|
||||
private _fileStateCollectionObserver: GPXFileStateCollectionObserver;
|
||||
private _patchIndex: Writable<number>;
|
||||
private _patchMinIndex: Writable<number>;
|
||||
private _patchMaxIndex: Writable<number>;
|
||||
private _canUndo: Readable<boolean>;
|
||||
private _canRedo: Readable<boolean>;
|
||||
|
||||
constructor(db: Database) {
|
||||
this._db = db;
|
||||
this._files = new Map();
|
||||
this._fileSubscriptions = new Map();
|
||||
this._fileStateCollectionObserver = new GPXFileStateCollectionObserver(
|
||||
(newFiles) => {
|
||||
newFiles.forEach((fileState, fileId) => {
|
||||
this._fileSubscriptions.set(
|
||||
fileId,
|
||||
fileState.subscribe((fileWithStatistics) => {
|
||||
if (fileWithStatistics) {
|
||||
this._files.set(fileId, fileWithStatistics.file);
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
(fileId) => {
|
||||
let unsubscribe = this._fileSubscriptions.get(fileId);
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
this._fileSubscriptions.delete(fileId);
|
||||
}
|
||||
this._files.delete(fileId);
|
||||
},
|
||||
() => {
|
||||
this._fileSubscriptions.forEach((unsubscribe) => unsubscribe());
|
||||
this._fileSubscriptions.clear();
|
||||
this._files.clear();
|
||||
}
|
||||
);
|
||||
|
||||
this._patchIndex = writable(-1);
|
||||
this._patchMinIndex = writable(0);
|
||||
this._patchMaxIndex = writable(0);
|
||||
|
||||
liveQuery(() => db.settings.get('patchIndex')).subscribe((value) => {
|
||||
if (value !== undefined) {
|
||||
this._patchIndex.set(value);
|
||||
}
|
||||
});
|
||||
liveQuery(() =>
|
||||
(db.patches.orderBy(':id').keys() as Promise<number[]>).then((keys) => {
|
||||
if (keys.length === 0) {
|
||||
return { min: 0, max: 0 };
|
||||
} else {
|
||||
return { min: keys[0], max: keys[keys.length - 1] + 1 };
|
||||
}
|
||||
})
|
||||
).subscribe((value) => {
|
||||
this._patchMinIndex.set(value.min);
|
||||
this._patchMaxIndex.set(value.max);
|
||||
});
|
||||
|
||||
this._canUndo = derived(
|
||||
[this._patchIndex, this._patchMinIndex],
|
||||
([$patchIndex, $patchMinIndex]) => {
|
||||
return $patchIndex >= $patchMinIndex;
|
||||
}
|
||||
);
|
||||
this._canRedo = derived(
|
||||
[this._patchIndex, this._patchMaxIndex],
|
||||
([$patchIndex, $patchMaxIndex]) => {
|
||||
return $patchIndex < $patchMaxIndex - 1;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get canUndo(): Readable<boolean> {
|
||||
return this._canUndo;
|
||||
}
|
||||
|
||||
get canRedo(): Readable<boolean> {
|
||||
return this._canRedo;
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (get(this.canUndo)) {
|
||||
const patchIndex = get(this._patchIndex);
|
||||
this._db.patches.get(patchIndex).then((patch) => {
|
||||
if (patch) {
|
||||
this.apply(patch.inversePatch);
|
||||
this._db.settings.put(patchIndex - 1, 'patchIndex');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (get(this.canRedo)) {
|
||||
const patchIndex = get(this._patchIndex) + 1;
|
||||
this._db.patches.get(patchIndex).then((patch) => {
|
||||
if (patch) {
|
||||
this.apply(patch.patch);
|
||||
this._db.settings.put(patchIndex, 'patchIndex');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
apply(patch: Patch[]) {
|
||||
let newFiles = applyPatches(this._files, patch);
|
||||
return this.commitFileStateChange(newFiles, patch);
|
||||
}
|
||||
|
||||
commitFileStateChange(newFiles: ReadonlyMap<string, GPXFile>, patch: Patch[]) {
|
||||
let changedFileIds = getChangedFileIds(patch);
|
||||
let updatedFileIds: string[] = [],
|
||||
deletedFileIds: string[] = [];
|
||||
|
||||
changedFileIds.forEach((id) => {
|
||||
if (newFiles.has(id)) {
|
||||
updatedFileIds.push(id);
|
||||
} else {
|
||||
deletedFileIds.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
let updatedFiles = updatedFileIds
|
||||
.map((id) => newFiles.get(id))
|
||||
.filter((file) => file !== undefined) as GPXFile[];
|
||||
updatedFileIds = updatedFiles.map((file) => file._data.id);
|
||||
|
||||
selection.updateFiles(updatedFiles, deletedFileIds);
|
||||
|
||||
// @ts-ignore
|
||||
return this._db.transaction('rw', this._db.fileids, this._db.files, async () => {
|
||||
if (updatedFileIds.length > 0) {
|
||||
await this._db.fileids.bulkPut(updatedFileIds, updatedFileIds);
|
||||
await this._db.files.bulkPut(updatedFiles, updatedFileIds);
|
||||
}
|
||||
if (deletedFileIds.length > 0) {
|
||||
await this._db.fileids.bulkDelete(deletedFileIds);
|
||||
await this._db.files.bulkDelete(deletedFileIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
applyGlobal(callback: (files: Map<string, GPXFile>) => void) {
|
||||
const [newFileCollection, patch, inversePatch] = produceWithPatches(this._files, callback);
|
||||
|
||||
this.storePatches(patch, inversePatch);
|
||||
|
||||
return this.commitFileStateChange(newFileCollection, patch);
|
||||
}
|
||||
|
||||
applyToFiles(fileIds: string[], callback: (file: WritableDraft<GPXFile>) => void) {
|
||||
const [newFileCollection, patch, inversePatch] = produceWithPatches(
|
||||
this._files,
|
||||
(draft) => {
|
||||
fileIds.forEach((fileId) => {
|
||||
let file = draft.get(fileId);
|
||||
if (file) {
|
||||
callback(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.storePatches(patch, inversePatch);
|
||||
|
||||
return this.commitFileStateChange(newFileCollection, patch);
|
||||
}
|
||||
|
||||
applyToFile(fileId: string, callback: (file: WritableDraft<GPXFile>) => void) {
|
||||
return this.applyToFiles([fileId], callback);
|
||||
}
|
||||
|
||||
applyEachToFilesAndGlobal(
|
||||
fileIds: string[],
|
||||
callbacks: ((file: WritableDraft<GPXFile>, context?: any) => void)[],
|
||||
globalCallback: (files: Map<string, GPXFile>, context?: any) => void,
|
||||
context?: any
|
||||
) {
|
||||
const [newFileCollection, patch, inversePatch] = produceWithPatches(
|
||||
this._files,
|
||||
(draft) => {
|
||||
fileIds.forEach((fileId, index) => {
|
||||
let file = draft.get(fileId);
|
||||
if (file) {
|
||||
callbacks[index](file, context);
|
||||
}
|
||||
});
|
||||
globalCallback(draft, context);
|
||||
}
|
||||
);
|
||||
|
||||
this.storePatches(patch, inversePatch);
|
||||
|
||||
return this.commitFileStateChange(newFileCollection, patch);
|
||||
}
|
||||
|
||||
async storePatches(patch: Patch[], inversePatch: Patch[]) {
|
||||
this._db.patches.where(':id').above(get(this._patchIndex)).delete(); // Delete all patches after the current patch to avoid redoing them
|
||||
if (get(this._patchMaxIndex) - get(this._patchMinIndex) + 1 > MAX_PATCHES) {
|
||||
this._db.patches
|
||||
.where(':id')
|
||||
.belowOrEqual(get(this._patchMaxIndex) - MAX_PATCHES)
|
||||
.delete();
|
||||
}
|
||||
this._db.transaction('rw', this._db.patches, this._db.settings, async () => {
|
||||
let index = get(this._patchIndex) + 1;
|
||||
await this._db.patches.put(
|
||||
{
|
||||
patch,
|
||||
inversePatch,
|
||||
index,
|
||||
},
|
||||
index
|
||||
);
|
||||
await this._db.settings.put(index, 'patchIndex');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get the file ids of the files that have changed in the patch
|
||||
function getChangedFileIds(patch: Patch[]): string[] {
|
||||
let changedFileIds = new Set<string>();
|
||||
for (let p of patch) {
|
||||
changedFileIds.add(p.path[0] as string);
|
||||
}
|
||||
return Array.from(changedFileIds);
|
||||
}
|
||||
|
||||
export const fileActionManager = new FileActionManager(db);
|
||||
@@ -1,59 +1,16 @@
|
||||
import { fileStateCollection } from '$lib/logic/file-state';
|
||||
import { fileActionManager } from '$lib/logic/file-action-manager';
|
||||
import { applyToOrderedItemsFromFile, copied, cut, selection } from '$lib/logic/selection';
|
||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
||||
import {
|
||||
ListFileItem,
|
||||
ListLevel,
|
||||
ListRootItem,
|
||||
ListTrackItem,
|
||||
ListTrackSegmentItem,
|
||||
ListWaypointItem,
|
||||
type ListItem,
|
||||
} from '$lib/components/file-list/file-list';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { freeze } from 'immer';
|
||||
import {
|
||||
GPXFile,
|
||||
Track,
|
||||
TrackPoint,
|
||||
TrackSegment,
|
||||
Waypoint,
|
||||
type LineStyleExtension,
|
||||
type WaypointType,
|
||||
} from 'gpx';
|
||||
import { get } from 'svelte/store';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { getClosestTrackSegments, getElevation } from '$lib/utils';
|
||||
import { gpxStatistics } from '$lib/logic/statistics';
|
||||
import { boundsManager } from './bounds';
|
||||
import { engine } from '$lib/engine';
|
||||
import { defaultFileName } from '$lib/default-file-name';
|
||||
|
||||
// Generate unique file ids, different from the ones in the database
|
||||
export function getFileIds(n: number) {
|
||||
let ids = [];
|
||||
for (let index = 0; ids.length < n; index++) {
|
||||
let id = `gpx-${index}`;
|
||||
if (!fileStateCollection.getFile(id)) {
|
||||
ids.push(id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
/** The name of a new file: the translated default name, numbered if other files have it. */
|
||||
export function newFileName() {
|
||||
const names = [...get(engine.files).values()].map((file) => get(file).structure.name);
|
||||
return defaultFileName(i18n._('menu.new_file'), names);
|
||||
}
|
||||
|
||||
export function newGPXFile() {
|
||||
let file = new GPXFile();
|
||||
file.metadata.name = newFileName();
|
||||
return file;
|
||||
}
|
||||
|
||||
export function createFile() {
|
||||
// the engine selects the new file
|
||||
engine.newFile(newFileName());
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/simplify';
|
||||
import { type Database } from '$lib/db';
|
||||
import { liveQuery } from 'dexie';
|
||||
import { GPXFile } from 'gpx';
|
||||
import { GPXStatisticsTree, type GPXFileWithStatistics } from '$lib/logic/statistics-tree';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { get, writable, type Subscriber, type Writable } from 'svelte/store';
|
||||
|
||||
// Observe a single file from the database, and maintain its statistics
|
||||
export class GPXFileState {
|
||||
private _fileId: string;
|
||||
private _file: Writable<GPXFileWithStatistics | undefined>;
|
||||
private _subscription: { unsubscribe: () => void } | undefined;
|
||||
|
||||
constructor(fileId: string, file?: GPXFile) {
|
||||
this._fileId = fileId;
|
||||
this._file = writable(file ? { file, statistics: new GPXStatisticsTree(file) } : undefined);
|
||||
}
|
||||
|
||||
connectToDatabase(db: Database) {
|
||||
if (this._subscription) return;
|
||||
this._subscription = liveQuery(() => db.files.get(this._fileId)).subscribe((value) => {
|
||||
if (value !== undefined) {
|
||||
let file = new GPXFile(value);
|
||||
updateAnchorPoints(file);
|
||||
let statistics = new GPXStatisticsTree(file);
|
||||
this._file.set({ file, statistics });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
subscribe(run: Subscriber<GPXFileWithStatistics | undefined>, invalidate?: () => void) {
|
||||
return this._file.subscribe(run, invalidate);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._subscription?.unsubscribe();
|
||||
this._subscription = undefined;
|
||||
}
|
||||
|
||||
get file(): GPXFile | undefined {
|
||||
return get(this._file)?.file;
|
||||
}
|
||||
|
||||
get statistics(): GPXStatisticsTree | undefined {
|
||||
return get(this._file)?.statistics;
|
||||
}
|
||||
}
|
||||
|
||||
// Observe the file ids in the database, and maintain a map of file states for the corresponding files
|
||||
export class GPXFileStateCollection {
|
||||
private _files: Writable<Map<string, GPXFileState>>;
|
||||
private _subscription: { unsubscribe: () => void } | null = null;
|
||||
|
||||
constructor() {
|
||||
this._files = writable(new Map());
|
||||
}
|
||||
|
||||
connectToDatabase(db: Database): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (this._subscription) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
this._subscription = liveQuery(() => db.fileids.toArray()).subscribe((dbFileIds) => {
|
||||
const currentFiles = get(this._files);
|
||||
// Find new files to observe
|
||||
let newFiles = dbFileIds
|
||||
.filter((id) => !currentFiles.has(id))
|
||||
.sort((a, b) => parseInt(a.split('-')[1]) - parseInt(b.split('-')[1]));
|
||||
// Find deleted files to stop observing
|
||||
let deletedFiles = Array.from(currentFiles.keys()).filter(
|
||||
(id) => !dbFileIds.find((fileId) => fileId === id)
|
||||
);
|
||||
|
||||
if (newFiles.length > 0 || deletedFiles.length > 0) {
|
||||
// Update the map of file states
|
||||
this._files.update(($files) => {
|
||||
newFiles.forEach((id) => {
|
||||
const fileState = new GPXFileState(id);
|
||||
fileState.connectToDatabase(db);
|
||||
$files.set(id, fileState);
|
||||
});
|
||||
deletedFiles.forEach((id) => {
|
||||
$files.get(id)?.destroy();
|
||||
$files.delete(id);
|
||||
});
|
||||
return $files;
|
||||
});
|
||||
|
||||
// Update the file order
|
||||
let fileOrder = get(settings.fileOrder).filter(
|
||||
(id) => !deletedFiles.includes(id)
|
||||
);
|
||||
newFiles.forEach((id) => {
|
||||
if (!fileOrder.includes(id)) {
|
||||
fileOrder.push(id);
|
||||
}
|
||||
});
|
||||
settings.fileOrder.set(fileOrder);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
disconnectFromDatabase() {
|
||||
this._subscription?.unsubscribe();
|
||||
this._subscription = null;
|
||||
this._files.update(($files) => {
|
||||
$files.forEach((fileState) => {
|
||||
fileState.destroy();
|
||||
});
|
||||
return new Map();
|
||||
});
|
||||
}
|
||||
|
||||
setEmbeddedFiles(files: GPXFile[]) {
|
||||
this._files.update(($files) => {
|
||||
$files.clear();
|
||||
files.forEach((file) => {
|
||||
const id = file._data.id;
|
||||
if (!$files.has(id)) {
|
||||
const fileState = new GPXFileState(id, file);
|
||||
$files.set(id, fileState);
|
||||
}
|
||||
});
|
||||
return $files;
|
||||
});
|
||||
}
|
||||
|
||||
subscribe(run: Subscriber<Map<string, GPXFileState>>, invalidate?: () => void) {
|
||||
return this._files.subscribe(run, invalidate);
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return get(this._files).size;
|
||||
}
|
||||
|
||||
getFileState(fileId: string): GPXFileState | undefined {
|
||||
return get(this._files).get(fileId);
|
||||
}
|
||||
|
||||
getFile(fileId: string): GPXFile | undefined {
|
||||
let fileState = get(this._files).get(fileId);
|
||||
return fileState?.file;
|
||||
}
|
||||
|
||||
getStatistics(fileId: string): GPXStatisticsTree | undefined {
|
||||
let fileState = get(this._files).get(fileId);
|
||||
return fileState?.statistics;
|
||||
}
|
||||
|
||||
forEach(callback: (fileId: string, file: GPXFile) => void) {
|
||||
get(this._files).forEach((fileState, fileId) => {
|
||||
if (fileState.file) {
|
||||
callback(fileId, fileState.file);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Collection of all file states
|
||||
export const fileStateCollection = new GPXFileStateCollection();
|
||||
|
||||
export type GPXFileStateCallback = (files: Map<string, GPXFileState>) => void;
|
||||
export class GPXFileStateCollectionObserver {
|
||||
private _fileIds: Set<string>;
|
||||
private _onFilesAdded: GPXFileStateCallback;
|
||||
private _onFileRemoved: (fileId: string) => void;
|
||||
private _onDestroy: () => void;
|
||||
private _unsubscribe: () => void;
|
||||
|
||||
constructor(
|
||||
onFilesAdded: GPXFileStateCallback,
|
||||
onFileRemoved: (fileId: string) => void,
|
||||
onDestroy: () => void
|
||||
) {
|
||||
this._fileIds = new Set();
|
||||
this._onFilesAdded = onFilesAdded;
|
||||
this._onFileRemoved = onFileRemoved;
|
||||
this._onDestroy = onDestroy;
|
||||
|
||||
this._unsubscribe = fileStateCollection.subscribe((files) => {
|
||||
this._fileIds.forEach((fileId) => {
|
||||
if (!files.has(fileId)) {
|
||||
this._onFileRemoved(fileId);
|
||||
this._fileIds.delete(fileId);
|
||||
}
|
||||
});
|
||||
let newFiles = new Map<string, GPXFileState>();
|
||||
files.forEach((file: GPXFileState, fileId: string) => {
|
||||
if (!this._fileIds.has(fileId)) {
|
||||
newFiles.set(fileId, file);
|
||||
this._fileIds.add(fileId);
|
||||
}
|
||||
});
|
||||
if (newFiles.size > 0) {
|
||||
this._onFilesAdded(newFiles);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._onDestroy();
|
||||
this._unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { get, writable, type Writable } from 'svelte/store';
|
||||
import { SelectedGPXFilesObserver, selection } from '$lib/logic/selection';
|
||||
import { fileStateCollection } from '$lib/logic/file-state';
|
||||
import {
|
||||
ListFileItem,
|
||||
ListTrackItem,
|
||||
ListTrackSegmentItem,
|
||||
ListWaypointItem,
|
||||
ListWaypointsItem,
|
||||
} from '$lib/components/file-list/file-list';
|
||||
|
||||
export class AllHidden {
|
||||
private _value: Writable<boolean>;
|
||||
|
||||
constructor() {
|
||||
this._value = writable(false);
|
||||
new SelectedGPXFilesObserver(() => this.update());
|
||||
}
|
||||
|
||||
subscribe(run: (value: boolean) => void, invalidate?: () => void) {
|
||||
return this._value.subscribe(run, invalidate);
|
||||
}
|
||||
|
||||
update() {
|
||||
let hidden = true;
|
||||
selection.applyToSelectedItemsFromFile((fileId, level, items) => {
|
||||
let file = fileStateCollection.getFile(fileId);
|
||||
if (file) {
|
||||
for (let item of items) {
|
||||
if (!hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item instanceof ListFileItem) {
|
||||
hidden = hidden && file._data.hidden === true;
|
||||
} else if (
|
||||
item instanceof ListTrackItem &&
|
||||
item.getTrackIndex() < file.trk.length
|
||||
) {
|
||||
hidden = hidden && file.trk[item.getTrackIndex()]._data.hidden === true;
|
||||
} else if (
|
||||
item instanceof ListTrackSegmentItem &&
|
||||
item.getTrackIndex() < file.trk.length &&
|
||||
item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length
|
||||
) {
|
||||
hidden =
|
||||
hidden &&
|
||||
file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()]._data
|
||||
.hidden === true;
|
||||
} else if (item instanceof ListWaypointsItem) {
|
||||
hidden = hidden && file._data.hiddenWpt === true;
|
||||
} else if (
|
||||
item instanceof ListWaypointItem &&
|
||||
item.getWaypointIndex() < file.wpt.length
|
||||
) {
|
||||
hidden = hidden && file.wpt[item.getWaypointIndex()]._data.hidden === true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (hidden != get(this._value)) {
|
||||
this._value.set(hidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const allHidden = new AllHidden();
|
||||
@@ -1,5 +1,6 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import { engine, type GlobalStatistics } from '$lib/engine';
|
||||
import type { Coordinates } from '$lib/geo';
|
||||
|
||||
/** The part of the selection that is highlighted in the elevation profile. */
|
||||
export type SlicedStatistics = {
|
||||
@@ -14,3 +15,6 @@ export const slicedStatistics = writable<SlicedStatistics | undefined>(undefined
|
||||
|
||||
// the indices only make sense for the selection they were made on
|
||||
engine.statistics.subscribe(() => slicedStatistics.set(undefined));
|
||||
|
||||
/** The point of the elevation profile that the pointer is over, shown on the map. */
|
||||
export const hoveredPoint: Writable<Coordinates | null> = writable(null);
|
||||
@@ -1,140 +0,0 @@
|
||||
import type { ListItem } from '$lib/components/file-list/file-list';
|
||||
|
||||
export class SelectionTreeType {
|
||||
item: ListItem;
|
||||
selected: boolean;
|
||||
children: {
|
||||
[key: string | number]: SelectionTreeType;
|
||||
};
|
||||
size: number = 0;
|
||||
|
||||
constructor(item: ListItem) {
|
||||
this.item = item;
|
||||
this.selected = false;
|
||||
this.children = {};
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.selected = false;
|
||||
for (let key in this.children) {
|
||||
this.children[key].clear();
|
||||
}
|
||||
this.size = 0;
|
||||
}
|
||||
|
||||
_setOrToggle(item: ListItem, value?: boolean) {
|
||||
if (item.level === this.item.level) {
|
||||
let newSelected = value === undefined ? !this.selected : value;
|
||||
if (this.selected !== newSelected) {
|
||||
this.selected = newSelected;
|
||||
this.size += this.selected ? 1 : -1;
|
||||
}
|
||||
} else {
|
||||
let id = item.getIdAtLevel(this.item.level);
|
||||
if (id !== undefined) {
|
||||
if (!this.children.hasOwnProperty(id)) {
|
||||
this.children[id] = new SelectionTreeType(this.item.extend(id));
|
||||
}
|
||||
this.size -= this.children[id].size;
|
||||
this.children[id]._setOrToggle(item, value);
|
||||
this.size += this.children[id].size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set(item: ListItem, value: boolean) {
|
||||
this._setOrToggle(item, value);
|
||||
}
|
||||
|
||||
toggle(item: ListItem) {
|
||||
this._setOrToggle(item);
|
||||
}
|
||||
|
||||
has(item: ListItem): boolean {
|
||||
if (item.level === this.item.level) {
|
||||
return this.selected;
|
||||
} else {
|
||||
let id = item.getIdAtLevel(this.item.level);
|
||||
if (id !== undefined) {
|
||||
if (this.children.hasOwnProperty(id)) {
|
||||
return this.children[id].has(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
hasAnyParent(item: ListItem, self: boolean = true): boolean {
|
||||
if (
|
||||
this.selected &&
|
||||
this.item.level <= item.level &&
|
||||
(self || this.item.level < item.level)
|
||||
) {
|
||||
return this.selected;
|
||||
}
|
||||
let id = item.getIdAtLevel(this.item.level);
|
||||
if (id !== undefined) {
|
||||
if (this.children.hasOwnProperty(id)) {
|
||||
return this.children[id].hasAnyParent(item, self);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
hasAnyChildren(item: ListItem, self: boolean = true, ignoreIds?: (string | number)[]): boolean {
|
||||
if (
|
||||
this.selected &&
|
||||
this.item.level >= item.level &&
|
||||
(self || this.item.level > item.level)
|
||||
) {
|
||||
return this.selected;
|
||||
}
|
||||
let id = item.getIdAtLevel(this.item.level);
|
||||
if (id !== undefined) {
|
||||
if (ignoreIds === undefined || ignoreIds.indexOf(id) === -1) {
|
||||
if (this.children.hasOwnProperty(id)) {
|
||||
return this.children[id].hasAnyChildren(item, self, ignoreIds);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let key in this.children) {
|
||||
if (ignoreIds === undefined || ignoreIds.indexOf(key) === -1) {
|
||||
if (this.children[key].hasAnyChildren(item, self, ignoreIds)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getSelected(selection: ListItem[] = []): ListItem[] {
|
||||
if (this.selected) {
|
||||
selection.push(this.item);
|
||||
}
|
||||
for (let key in this.children) {
|
||||
this.children[key].getSelected(selection);
|
||||
}
|
||||
return selection;
|
||||
}
|
||||
|
||||
forEach(callback: (item: ListItem) => void) {
|
||||
if (this.selected) {
|
||||
callback(this.item);
|
||||
}
|
||||
for (let key in this.children) {
|
||||
this.children[key].forEach(callback);
|
||||
}
|
||||
}
|
||||
|
||||
getChild(id: string | number): SelectionTreeType | undefined {
|
||||
return this.children[id];
|
||||
}
|
||||
|
||||
deleteChild(id: string | number) {
|
||||
if (this.children.hasOwnProperty(id)) {
|
||||
this.size -= this.children[id].size;
|
||||
delete this.children[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,424 +0,0 @@
|
||||
import {
|
||||
ListFileItem,
|
||||
ListItem,
|
||||
ListRootItem,
|
||||
ListTrackItem,
|
||||
ListTrackSegmentItem,
|
||||
ListWaypointItem,
|
||||
ListLevel,
|
||||
sortItems,
|
||||
ListWaypointsItem,
|
||||
} from '$lib/components/file-list/file-list';
|
||||
import { fileStateCollection, GPXFileStateCollectionObserver } from '$lib/logic/file-state';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import type { GPXFile } from 'gpx';
|
||||
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
||||
import { SelectionTreeType } from '$lib/logic/selection-tree';
|
||||
|
||||
export class Selection {
|
||||
private _selection: Writable<SelectionTreeType>;
|
||||
private _copied: Writable<ListItem[] | undefined>;
|
||||
private _cut: Writable<boolean>;
|
||||
|
||||
constructor() {
|
||||
this._selection = writable(new SelectionTreeType(new ListRootItem()));
|
||||
this._copied = writable(undefined);
|
||||
this._cut = writable(false);
|
||||
}
|
||||
|
||||
subscribe(
|
||||
run: (value: SelectionTreeType) => void,
|
||||
invalidate?: (value?: SelectionTreeType) => void
|
||||
) {
|
||||
return this._selection.subscribe(run, invalidate);
|
||||
}
|
||||
|
||||
update(updater: (value: SelectionTreeType) => SelectionTreeType) {
|
||||
this._selection.update(updater);
|
||||
}
|
||||
|
||||
selectItem(item: ListItem) {
|
||||
this._selection.update(($selection) => {
|
||||
$selection.clear();
|
||||
$selection.set(item, true);
|
||||
return $selection;
|
||||
});
|
||||
}
|
||||
|
||||
selectFile(fileId: string) {
|
||||
this.selectItem(new ListFileItem(fileId));
|
||||
}
|
||||
|
||||
addSelectItem(item: ListItem) {
|
||||
this._selection.update(($selection) => {
|
||||
$selection.toggle(item);
|
||||
return $selection;
|
||||
});
|
||||
}
|
||||
|
||||
addSelectFile(fileId: string) {
|
||||
this.addSelectItem(new ListFileItem(fileId));
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
let item: ListItem = new ListRootItem();
|
||||
get(this._selection).forEach((i) => {
|
||||
item = i;
|
||||
});
|
||||
this._selection.update(($selection) => {
|
||||
$selection.clear();
|
||||
if (item instanceof ListRootItem || item instanceof ListFileItem) {
|
||||
fileStateCollection.forEach((fileId, file) => {
|
||||
$selection.set(new ListFileItem(fileId), true);
|
||||
});
|
||||
} else if (item instanceof ListTrackItem) {
|
||||
let file = fileStateCollection.getFile(item.getFileId());
|
||||
if (file) {
|
||||
file.trk.forEach((_track, trackId) => {
|
||||
$selection.set(new ListTrackItem(item.getFileId(), trackId), true);
|
||||
});
|
||||
}
|
||||
} else if (item instanceof ListTrackSegmentItem) {
|
||||
let file = fileStateCollection.getFile(item.getFileId());
|
||||
if (file) {
|
||||
file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => {
|
||||
$selection.set(
|
||||
new ListTrackSegmentItem(
|
||||
item.getFileId(),
|
||||
item.getTrackIndex(),
|
||||
segmentId
|
||||
),
|
||||
true
|
||||
);
|
||||
});
|
||||
}
|
||||
} else if (item instanceof ListWaypointItem) {
|
||||
let file = fileStateCollection.getFile(item.getFileId());
|
||||
if (file) {
|
||||
file.wpt.forEach((_waypoint, waypointId) => {
|
||||
$selection.set(new ListWaypointItem(item.getFileId(), waypointId), true);
|
||||
});
|
||||
}
|
||||
}
|
||||
return $selection;
|
||||
});
|
||||
}
|
||||
|
||||
selectFileWhenLoaded(fileId: string) {
|
||||
const unsubscribe = fileStateCollection.subscribe((files) => {
|
||||
if (files.has(fileId)) {
|
||||
this.selectFile(fileId);
|
||||
unsubscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
set(items: ListItem[]) {
|
||||
this._selection.update(($selection) => {
|
||||
$selection.clear();
|
||||
items.forEach((item) => {
|
||||
$selection.set(item, true);
|
||||
});
|
||||
return $selection;
|
||||
});
|
||||
}
|
||||
|
||||
updateFiles(updatedFiles: GPXFile[], deletedFileIds: string[]) {
|
||||
let removedItems: ListItem[] = [];
|
||||
applyToOrderedItemsFromFile(get(this._selection).getSelected(), (fileId, level, items) => {
|
||||
let file = updatedFiles.find((file) => file._data.id === fileId);
|
||||
if (file) {
|
||||
items.forEach((item) => {
|
||||
if (item instanceof ListTrackItem) {
|
||||
let newTrackIndex = file.trk.findIndex(
|
||||
(track) => track._data.trackIndex === item.getTrackIndex()
|
||||
);
|
||||
if (newTrackIndex === -1) {
|
||||
removedItems.push(item);
|
||||
}
|
||||
} else if (item instanceof ListTrackSegmentItem) {
|
||||
let newTrackIndex = file.trk.findIndex(
|
||||
(track) => track._data.trackIndex === item.getTrackIndex()
|
||||
);
|
||||
if (newTrackIndex === -1) {
|
||||
removedItems.push(item);
|
||||
} else {
|
||||
let newSegmentIndex = file.trk[newTrackIndex].trkseg.findIndex(
|
||||
(segment) => segment._data.segmentIndex === item.getSegmentIndex()
|
||||
);
|
||||
if (newSegmentIndex === -1) {
|
||||
removedItems.push(item);
|
||||
}
|
||||
}
|
||||
} else if (item instanceof ListWaypointItem) {
|
||||
let newWaypointIndex = file.wpt.findIndex(
|
||||
(wpt) => wpt._data.index === item.getWaypointIndex()
|
||||
);
|
||||
if (newWaypointIndex === -1) {
|
||||
removedItems.push(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (deletedFileIds.includes(fileId)) {
|
||||
items.forEach((item) => {
|
||||
removedItems.push(item);
|
||||
});
|
||||
}
|
||||
});
|
||||
if (removedItems.length > 0) {
|
||||
this._selection.update(($selection) => {
|
||||
removedItems.forEach((item) => {
|
||||
if (item instanceof ListFileItem) {
|
||||
$selection.deleteChild(item.getFileId());
|
||||
} else {
|
||||
$selection.set(item, false);
|
||||
}
|
||||
});
|
||||
return $selection;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateFromKey(down: boolean, shift: boolean) {
|
||||
let selected = get(this._selection).getSelected();
|
||||
if (selected.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let next: ListItem | undefined = undefined;
|
||||
if (selected[0] instanceof ListFileItem) {
|
||||
let order = get(settings.fileOrder);
|
||||
let limitIndex: number | undefined = undefined;
|
||||
selected.forEach((item) => {
|
||||
let index = order.indexOf(item.getFileId());
|
||||
if (
|
||||
limitIndex === undefined ||
|
||||
(down && index > limitIndex) ||
|
||||
(!down && index < limitIndex)
|
||||
) {
|
||||
limitIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
if (limitIndex !== undefined) {
|
||||
let nextIndex = down ? limitIndex + 1 : limitIndex - 1;
|
||||
|
||||
while (true) {
|
||||
if (nextIndex < 0) {
|
||||
nextIndex = order.length - 1;
|
||||
} else if (nextIndex >= order.length) {
|
||||
nextIndex = 0;
|
||||
}
|
||||
|
||||
if (nextIndex === limitIndex) {
|
||||
break;
|
||||
}
|
||||
|
||||
next = new ListFileItem(order[nextIndex]);
|
||||
if (!get(selection).has(next)) {
|
||||
break;
|
||||
}
|
||||
|
||||
nextIndex += down ? 1 : -1;
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
selected[0] instanceof ListTrackItem &&
|
||||
selected[selected.length - 1] instanceof ListTrackItem
|
||||
) {
|
||||
let fileId = selected[0].getFileId();
|
||||
let file = fileStateCollection.getFile(fileId);
|
||||
if (file) {
|
||||
let numberOfTracks = file.trk.length;
|
||||
let trackIndex = down
|
||||
? selected[selected.length - 1].getTrackIndex()
|
||||
: selected[0].getTrackIndex();
|
||||
if (down && trackIndex < numberOfTracks - 1) {
|
||||
next = new ListTrackItem(fileId, trackIndex + 1);
|
||||
} else if (!down && trackIndex > 0) {
|
||||
next = new ListTrackItem(fileId, trackIndex - 1);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
selected[0] instanceof ListTrackSegmentItem &&
|
||||
selected[selected.length - 1] instanceof ListTrackSegmentItem
|
||||
) {
|
||||
let fileId = selected[0].getFileId();
|
||||
let file = fileStateCollection.getFile(fileId);
|
||||
if (file) {
|
||||
let trackIndex = selected[0].getTrackIndex();
|
||||
let numberOfSegments = file.trk[trackIndex].trkseg.length;
|
||||
let segmentIndex = down
|
||||
? selected[selected.length - 1].getSegmentIndex()
|
||||
: selected[0].getSegmentIndex();
|
||||
if (down && segmentIndex < numberOfSegments - 1) {
|
||||
next = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex + 1);
|
||||
} else if (!down && segmentIndex > 0) {
|
||||
next = new ListTrackSegmentItem(fileId, trackIndex, segmentIndex - 1);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
selected[0] instanceof ListWaypointItem &&
|
||||
selected[selected.length - 1] instanceof ListWaypointItem
|
||||
) {
|
||||
let fileId = selected[0].getFileId();
|
||||
let file = fileStateCollection.getFile(fileId);
|
||||
if (file) {
|
||||
let numberOfWaypoints = file.wpt.length;
|
||||
let waypointIndex = down
|
||||
? selected[selected.length - 1].getWaypointIndex()
|
||||
: selected[0].getWaypointIndex();
|
||||
if (down && waypointIndex < numberOfWaypoints - 1) {
|
||||
next = new ListWaypointItem(fileId, waypointIndex + 1);
|
||||
} else if (!down && waypointIndex > 0) {
|
||||
next = new ListWaypointItem(fileId, waypointIndex - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (next && (!get(this._selection).has(next) || !shift)) {
|
||||
if (shift) {
|
||||
this.addSelectItem(next);
|
||||
} else {
|
||||
this.selectItem(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getOrderedSelection(reverse: boolean = false): ListItem[] {
|
||||
let selected: ListItem[] = [];
|
||||
this.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||
selected.push(...items);
|
||||
}, reverse);
|
||||
return selected;
|
||||
}
|
||||
|
||||
applyToSelectedItemsFromFile(
|
||||
callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void
|
||||
) {
|
||||
let selectedItems = get(this._selection).getSelected();
|
||||
get(fileStateCollection).forEach((_, fileId) => {
|
||||
let level: ListLevel | undefined = undefined;
|
||||
let items: ListItem[] = [];
|
||||
selectedItems.forEach((item) => {
|
||||
if (item.getFileId() === fileId) {
|
||||
level = item.level;
|
||||
if (
|
||||
item instanceof ListFileItem ||
|
||||
item instanceof ListTrackItem ||
|
||||
item instanceof ListTrackSegmentItem ||
|
||||
item instanceof ListWaypointsItem ||
|
||||
item instanceof ListWaypointItem
|
||||
) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (items.length > 0) {
|
||||
callback(fileId, level, items);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
applyToOrderedSelectedItemsFromFile(
|
||||
callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void,
|
||||
reverse: boolean = true
|
||||
) {
|
||||
applyToOrderedItemsFromFile(get(this._selection).getSelected(), callback, reverse);
|
||||
}
|
||||
|
||||
copySelection(): boolean {
|
||||
let selected = get(this._selection).getSelected();
|
||||
if (selected.length > 0) {
|
||||
this._copied.set(selected);
|
||||
this._cut.set(false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
cutSelection() {
|
||||
if (this.copySelection()) {
|
||||
this._cut.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
resetCopied() {
|
||||
this._copied.set(undefined);
|
||||
this._cut.set(false);
|
||||
}
|
||||
|
||||
get copied(): Readable<ListItem[] | undefined> {
|
||||
return this._copied;
|
||||
}
|
||||
|
||||
get cut(): Readable<boolean> {
|
||||
return this._cut;
|
||||
}
|
||||
}
|
||||
|
||||
export const selection = new Selection();
|
||||
export const copied = selection.copied;
|
||||
export const cut = selection.cut;
|
||||
|
||||
export function applyToOrderedItemsFromFile(
|
||||
selectedItems: ListItem[],
|
||||
callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void,
|
||||
reverse: boolean = true
|
||||
) {
|
||||
get(settings.fileOrder).forEach((fileId) => {
|
||||
let level: ListLevel | undefined = undefined;
|
||||
let items: ListItem[] = [];
|
||||
selectedItems.forEach((item) => {
|
||||
if (item.getFileId() === fileId) {
|
||||
level = item.level;
|
||||
if (
|
||||
item instanceof ListFileItem ||
|
||||
item instanceof ListTrackItem ||
|
||||
item instanceof ListTrackSegmentItem ||
|
||||
item instanceof ListWaypointsItem ||
|
||||
item instanceof ListWaypointItem
|
||||
) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (items.length > 0) {
|
||||
sortItems(items, reverse);
|
||||
callback(fileId, level, items);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export class SelectedGPXFilesObserver {
|
||||
private _fileStateCollectionObserver: GPXFileStateCollectionObserver;
|
||||
private _unsubscribes: Map<string, () => void> = new Map();
|
||||
|
||||
constructor(onSelectedFileChange: () => void) {
|
||||
this._unsubscribes = new Map();
|
||||
this._fileStateCollectionObserver = new GPXFileStateCollectionObserver(
|
||||
(newFiles) => {
|
||||
newFiles.forEach((fileState, fileId) => {
|
||||
this._unsubscribes.set(
|
||||
fileId,
|
||||
fileState.subscribe(() => {
|
||||
if (get(selection).hasAnyChildren(new ListFileItem(fileId))) {
|
||||
onSelectedFileChange();
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
(fileId) => {
|
||||
this._unsubscribes.get(fileId)?.();
|
||||
this._unsubscribes.delete(fileId);
|
||||
},
|
||||
() => {
|
||||
this._unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
this._unsubscribes.clear();
|
||||
}
|
||||
);
|
||||
selection.subscribe(() => onSelectedFileChange());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import { type Database } from '$lib/db';
|
||||
import { liveQuery } from 'dexie';
|
||||
import {
|
||||
basemaps,
|
||||
defaultBasemap,
|
||||
@@ -15,11 +13,35 @@ import {
|
||||
type LayerTreeType,
|
||||
} from '$lib/assets/layers';
|
||||
import { browser } from '$app/environment';
|
||||
import { engine } from '$lib/engine';
|
||||
import { get, writable, type Writable } from 'svelte/store';
|
||||
|
||||
/** What the settings were saved as, by key: JSON strings (see `Engine.openStorage`). */
|
||||
export type StoredSettings = Record<string, string>;
|
||||
|
||||
/** Reads the saved value of a setting, `undefined` if there is none or if it cannot be read. */
|
||||
function storedValue(stored: StoredSettings, key: string): { value: any } | undefined {
|
||||
if (!Object.hasOwn(stored, key)) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return { value: JSON.parse(stored[key]) };
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Keeps a value; `undefined` is not something JSON can hold, it means no value. */
|
||||
function save(key: string, value: unknown) {
|
||||
const json = JSON.stringify(value);
|
||||
if (json === undefined) {
|
||||
engine.deleteSetting(key);
|
||||
} else {
|
||||
engine.setSetting(key, json);
|
||||
}
|
||||
}
|
||||
|
||||
export class Setting<V> {
|
||||
private _db: Database | null = null;
|
||||
private _subscription: { unsubscribe: () => void } | null = null;
|
||||
private _key: string;
|
||||
private _value: Writable<V>;
|
||||
private _validator?: (value: V) => V;
|
||||
@@ -30,30 +52,12 @@ export class Setting<V> {
|
||||
this._validator = validator;
|
||||
}
|
||||
|
||||
connectToDatabase(db: Database) {
|
||||
if (this._db) return;
|
||||
this._db = db;
|
||||
|
||||
let first = true;
|
||||
this._subscription = liveQuery(() => db.settings.get(this._key)).subscribe((value) => {
|
||||
if (value === undefined) {
|
||||
if (!first) {
|
||||
this._value.set(value);
|
||||
}
|
||||
} else {
|
||||
if (this._validator) {
|
||||
value = this._validator(value);
|
||||
}
|
||||
this._value.set(value);
|
||||
}
|
||||
first = false;
|
||||
});
|
||||
}
|
||||
|
||||
disconnectFromDatabase() {
|
||||
this._subscription?.unsubscribe();
|
||||
this._subscription = null;
|
||||
this._db = null;
|
||||
/** Takes the saved value, if there is one. */
|
||||
connect(stored: StoredSettings) {
|
||||
const saved = storedValue(stored, this._key);
|
||||
if (saved) {
|
||||
this._value.set(this._validator ? this._validator(saved.value) : saved.value);
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(run: (value: V) => void, invalidate?: (value?: V) => void) {
|
||||
@@ -62,11 +66,8 @@ export class Setting<V> {
|
||||
|
||||
set(value: V) {
|
||||
if (typeof value === 'object' || value !== get(this._value)) {
|
||||
if (this._db) {
|
||||
this._db.settings.put(value, this._key);
|
||||
} else {
|
||||
this._value.set(value);
|
||||
}
|
||||
this._value.set(value);
|
||||
save(this._key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,9 +76,8 @@ export class Setting<V> {
|
||||
}
|
||||
}
|
||||
|
||||
/** A setting that has no value until it is connected: then the saved one, or the initial one. */
|
||||
export class SettingInitOnFirstRead<V> {
|
||||
private _db: Database | null = null;
|
||||
private _subscription: { unsubscribe: () => void } | null = null;
|
||||
private _key: string;
|
||||
private _value: Writable<V | undefined>;
|
||||
private _initial: V;
|
||||
@@ -90,49 +90,27 @@ export class SettingInitOnFirstRead<V> {
|
||||
this._validator = validator;
|
||||
}
|
||||
|
||||
connectToDatabase(db: Database) {
|
||||
if (this._db) return;
|
||||
this._db = db;
|
||||
|
||||
let first = true;
|
||||
this._subscription = liveQuery(() => db.settings.get(this._key)).subscribe((value) => {
|
||||
if (value === undefined) {
|
||||
if (first) {
|
||||
this._value.set(this._initial);
|
||||
} else {
|
||||
this._value.set(value);
|
||||
}
|
||||
} else {
|
||||
if (this._validator) {
|
||||
value = this._validator(value);
|
||||
}
|
||||
this._value.set(value);
|
||||
}
|
||||
first = false;
|
||||
});
|
||||
connect(stored: StoredSettings) {
|
||||
const saved = storedValue(stored, this._key);
|
||||
if (saved) {
|
||||
this._value.set(this._validator ? this._validator(saved.value) : saved.value);
|
||||
} else {
|
||||
this._value.set(this._initial);
|
||||
}
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.set(this._initial);
|
||||
}
|
||||
|
||||
disconnectFromDatabase() {
|
||||
this._subscription?.unsubscribe();
|
||||
this._subscription = null;
|
||||
this._db = null;
|
||||
}
|
||||
|
||||
subscribe(run: (value: V | undefined) => void, invalidate?: (value?: V | undefined) => void) {
|
||||
return this._value.subscribe(run, invalidate);
|
||||
}
|
||||
|
||||
set(value: V) {
|
||||
if (typeof value === 'object' || value !== get(this._value)) {
|
||||
if (this._db) {
|
||||
this._db.settings.put(value, this._key);
|
||||
} else {
|
||||
this._value.set(value);
|
||||
}
|
||||
this._value.set(value);
|
||||
save(this._key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,19 +300,12 @@ export const settings = {
|
||||
defaultWidth: new Setting('defaultWidth', browser && window.innerWidth < 600 ? 8 : 5),
|
||||
bottomPanelSize: new Setting('bottomPanelSize', 170),
|
||||
rightPanelSize: new Setting('rightPanelSize', 240),
|
||||
connectToDatabase(db: Database) {
|
||||
/** Takes the saved values (see `Engine.openStorage`). */
|
||||
connect(stored: StoredSettings) {
|
||||
for (const key in settings) {
|
||||
const setting = (settings as any)[key];
|
||||
if (setting instanceof Setting || setting instanceof SettingInitOnFirstRead) {
|
||||
setting.connectToDatabase(db);
|
||||
}
|
||||
}
|
||||
},
|
||||
disconnectFromDatabase() {
|
||||
for (const key in settings) {
|
||||
const setting = (settings as any)[key];
|
||||
if (setting instanceof Setting || setting instanceof SettingInitOnFirstRead) {
|
||||
setting.disconnectFromDatabase();
|
||||
setting.connect(stored);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { ListItem, ListLevel } from '$lib/components/file-list/file-list';
|
||||
import { GPXFile, GPXStatistics, GPXStatisticsGroup, type Track } from 'gpx';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
|
||||
export class GPXStatisticsTree {
|
||||
level: ListLevel;
|
||||
statistics: {
|
||||
[key: string]: GPXStatisticsTree | GPXStatistics;
|
||||
} = {};
|
||||
wptBounds: maplibregl.LngLatBounds;
|
||||
|
||||
constructor(element: GPXFile | Track) {
|
||||
this.wptBounds = new maplibregl.LngLatBounds();
|
||||
if (element instanceof GPXFile) {
|
||||
this.level = ListLevel.FILE;
|
||||
element.children.forEach((child, index) => {
|
||||
this.statistics[index] = new GPXStatisticsTree(child);
|
||||
});
|
||||
element.wpt.forEach((wpt) => {
|
||||
this.wptBounds.extend(wpt.getCoordinates());
|
||||
});
|
||||
} else {
|
||||
this.level = ListLevel.TRACK;
|
||||
element.children.forEach((child, index) => {
|
||||
this.statistics[index] = child.getStatistics();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getStatisticsFor(item: ListItem): GPXStatisticsGroup {
|
||||
let statistics = new GPXStatisticsGroup();
|
||||
let id = item.getIdAtLevel(this.level);
|
||||
if (id === undefined || id === 'waypoints') {
|
||||
Object.keys(this.statistics).forEach((key) => {
|
||||
if (this.statistics[key] instanceof GPXStatistics) {
|
||||
statistics.add(this.statistics[key]);
|
||||
} else {
|
||||
statistics.add(this.statistics[key].getStatisticsFor(item));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let child = this.statistics[id];
|
||||
if (child instanceof GPXStatistics) {
|
||||
statistics.add(child);
|
||||
} else if (child !== undefined) {
|
||||
statistics.add(child.getStatisticsFor(item));
|
||||
}
|
||||
}
|
||||
return statistics;
|
||||
}
|
||||
|
||||
intersectsBBox(bounds: maplibregl.LngLatBounds): boolean {
|
||||
for (let key in this.statistics) {
|
||||
const stats = this.statistics[key];
|
||||
if (stats instanceof GPXStatistics) {
|
||||
const bbox = new maplibregl.LngLatBounds(
|
||||
stats.global.bounds.southWest,
|
||||
stats.global.bounds.northEast
|
||||
);
|
||||
if (!bbox.isEmpty() && bbox.intersects(bounds)) {
|
||||
return true;
|
||||
}
|
||||
} else if (stats.intersectsBBox(bounds)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
intersectsWaypointBBox(bounds: maplibregl.LngLatBounds): boolean {
|
||||
return !this.wptBounds.isEmpty() && this.wptBounds.intersects(bounds);
|
||||
}
|
||||
}
|
||||
export type GPXFileWithStatistics = { file: GPXFile; statistics: GPXStatisticsTree };
|
||||
@@ -1,89 +0,0 @@
|
||||
import { selection } from '$lib/logic/selection';
|
||||
import { GPXGlobalStatistics, GPXStatisticsGroup, type Coordinates } from 'gpx';
|
||||
import { fileStateCollection, GPXFileState } from '$lib/logic/file-state';
|
||||
import {
|
||||
ListFileItem,
|
||||
ListWaypointItem,
|
||||
ListWaypointsItem,
|
||||
} from '$lib/components/file-list/file-list';
|
||||
import { get, writable, type Writable } from 'svelte/store';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
|
||||
const { fileOrder } = settings;
|
||||
|
||||
export class SelectedGPXStatistics {
|
||||
private _statistics: Writable<GPXStatisticsGroup>;
|
||||
private _files: Map<
|
||||
string,
|
||||
{
|
||||
file: GPXFileState;
|
||||
unsubscribe: () => void;
|
||||
}
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
this._statistics = writable(new GPXStatisticsGroup());
|
||||
this._files = new Map();
|
||||
selection.subscribe(() => this.update());
|
||||
fileOrder.subscribe(() => this.update());
|
||||
}
|
||||
|
||||
subscribe(
|
||||
run: (value: GPXStatisticsGroup) => void,
|
||||
invalidate?: (value?: GPXStatisticsGroup) => void
|
||||
) {
|
||||
return this._statistics.subscribe(run, invalidate);
|
||||
}
|
||||
|
||||
update() {
|
||||
let statistics = new GPXStatisticsGroup();
|
||||
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||
let stats = fileStateCollection.getStatistics(fileId);
|
||||
if (stats) {
|
||||
let first = true;
|
||||
items.forEach((item) => {
|
||||
if (
|
||||
!(item instanceof ListWaypointItem || item instanceof ListWaypointsItem) ||
|
||||
first
|
||||
) {
|
||||
statistics.add(stats.getStatisticsFor(item));
|
||||
first = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!this._files.has(fileId)) {
|
||||
let file = fileStateCollection.getFileState(fileId);
|
||||
if (file) {
|
||||
let first = true;
|
||||
let unsubscribe = file.subscribe(() => {
|
||||
if (first) first = false;
|
||||
else this.update();
|
||||
});
|
||||
this._files.set(fileId, { file, unsubscribe });
|
||||
}
|
||||
}
|
||||
}, false);
|
||||
this._statistics.set(statistics);
|
||||
for (let [fileId, entry] of this._files) {
|
||||
if (
|
||||
!get(fileStateCollection).has(fileId) ||
|
||||
!get(selection).hasAnyChildren(new ListFileItem(fileId))
|
||||
) {
|
||||
entry.unsubscribe();
|
||||
this._files.delete(fileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const gpxStatistics = new SelectedGPXStatistics();
|
||||
|
||||
export const slicedGPXStatistics: Writable<[GPXGlobalStatistics, number, number] | undefined> =
|
||||
writable(undefined);
|
||||
|
||||
export const hoveredPoint: Writable<Coordinates | null> = writable(null);
|
||||
|
||||
gpxStatistics.subscribe(() => {
|
||||
slicedGPXStatistics.set(undefined);
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Computes the statistics of `assets/example.gpx` with the engine, and writes them to
|
||||
* `assets/example-data.ts`, where the pages that show an example (the home page, the
|
||||
* documentation) take them from. Run it when the example, or the way the engine computes the
|
||||
* statistics, changes:
|
||||
*
|
||||
* npm run generate:example
|
||||
*
|
||||
* The engine has to be built (`wasm-pack build wasm --target bundler --out-dir ../pkg`, in
|
||||
* `gpx-rs`).
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath, pathToFileURL } from 'url';
|
||||
import prettier from 'prettier';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const assets = path.resolve(here, '../assets');
|
||||
const packageDirectory = path.resolve(here, '../../../../gpx-rs/pkg');
|
||||
|
||||
export const exampleGPX = path.join(assets, 'example.gpx');
|
||||
export const exampleData = path.join(assets, 'example-data.ts');
|
||||
|
||||
/** Whether the engine is built. */
|
||||
export function engineIsBuilt() {
|
||||
return fs.existsSync(path.join(packageDirectory, 'gpx_rs_bg.wasm'));
|
||||
}
|
||||
|
||||
/** Loads the engine in Node: the package is made for bundlers, which import the wasm file. */
|
||||
async function loadEngine() {
|
||||
const glue = await import(pathToFileURL(path.join(packageDirectory, 'gpx_rs_bg.js')).href);
|
||||
const { instance } = await WebAssembly.instantiate(
|
||||
fs.readFileSync(path.join(packageDirectory, 'gpx_rs_bg.wasm')),
|
||||
{ './gpx_rs_bg.js': glue }
|
||||
);
|
||||
glue.__wbg_set_wasm(instance.exports);
|
||||
(instance.exports.__wbindgen_start as () => void)();
|
||||
return glue as typeof import('gpx-rs');
|
||||
}
|
||||
|
||||
type Values = Float64Array | BigInt64Array | undefined;
|
||||
|
||||
/** The arrays that the engine has for the example, which are all the pages need. */
|
||||
export async function computeExampleData() {
|
||||
const engine = await loadEngine();
|
||||
engine.start();
|
||||
const data = fs.readFileSync(exampleGPX);
|
||||
engine.load_files(new Uint8Array(data), Uint32Array.from([data.length]), ['example']);
|
||||
|
||||
// numbers are enough for the times, which are milliseconds
|
||||
const numbers = (values: Values) =>
|
||||
values && Array.from(values as ArrayLike<number | bigint>, (value) => Number(value));
|
||||
return {
|
||||
global: engine.selection_statistics(),
|
||||
totalDistance: numbers(engine.total_distance()),
|
||||
movingDistance: numbers(engine.moving_distance()),
|
||||
totalTime: numbers(engine.total_time()),
|
||||
movingTime: numbers(engine.moving_time()),
|
||||
elevationGain: numbers(engine.elevation_gain()),
|
||||
elevationLoss: numbers(engine.elevation_loss()),
|
||||
slope: numbers(engine.slope()),
|
||||
slopeSegmentSlope: numbers(engine.slope_segment_slope()),
|
||||
slopeSegmentDistance: numbers(engine.slope_segment_distance()),
|
||||
lng: numbers(engine.lng()),
|
||||
lat: numbers(engine.lat()),
|
||||
ele: numbers(engine.ele()),
|
||||
timestamps: numbers(engine.timestamps()),
|
||||
speed: numbers(engine.speed()),
|
||||
atemp: numbers(engine.atemp()),
|
||||
};
|
||||
}
|
||||
|
||||
async function generate() {
|
||||
const data = await computeExampleData();
|
||||
const source = `// Generated by src/lib/scripts/example-statistics.ts from example.gpx: do not edit.
|
||||
export const exampleData = ${JSON.stringify(data)};
|
||||
`;
|
||||
const options = (await prettier.resolveConfig(exampleData)) ?? {};
|
||||
fs.writeFileSync(
|
||||
exampleData,
|
||||
await prettier.format(source, { ...options, parser: 'typescript' })
|
||||
);
|
||||
console.log(`Wrote ${path.relative(process.cwd(), exampleData)}`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
||||
generate();
|
||||
}
|
||||
@@ -2,11 +2,9 @@ import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { base } from '$app/paths';
|
||||
import { languages } from '$lib/languages';
|
||||
import { TrackPoint, Waypoint, type Coordinates, crossarcDistance, distance, GPXFile } from 'gpx';
|
||||
import type { Coordinates } from '$lib/geo';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
import { pointToTile, pointToTileFraction } from '@mapbox/tilebelt';
|
||||
import type { GPXStatisticsTree } from '$lib/logic/statistics-tree';
|
||||
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
@@ -19,97 +17,16 @@ export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, 'childre
|
||||
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||
|
||||
export function getClosestLinePoint(
|
||||
points: TrackPoint[],
|
||||
point: TrackPoint | Coordinates,
|
||||
details: any = {}
|
||||
): TrackPoint {
|
||||
let closest = points[0];
|
||||
let closestDist = Number.MAX_VALUE;
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
let dist = crossarcDistance(points[i], points[i + 1], point);
|
||||
if (dist < closestDist) {
|
||||
closestDist = dist;
|
||||
if (distance(points[i], point) <= distance(points[i + 1], point)) {
|
||||
closest = points[i];
|
||||
details['before'] = true;
|
||||
details['index'] = i;
|
||||
} else {
|
||||
closest = points[i + 1];
|
||||
details['before'] = false;
|
||||
details['index'] = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
details['distance'] = closestDist;
|
||||
return closest;
|
||||
}
|
||||
|
||||
export function getClosestTrackSegments(
|
||||
file: GPXFile,
|
||||
statistics: GPXStatisticsTree,
|
||||
point: Coordinates
|
||||
): [number, number][] {
|
||||
let segmentBoundsDistances: [number, number, number][] = [];
|
||||
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||
let segmentStatistics = statistics.getStatisticsFor(
|
||||
new ListTrackSegmentItem(file._data.id, trackIndex, segmentIndex)
|
||||
);
|
||||
let segmentBounds = segmentStatistics.global.bounds;
|
||||
let northEast = segmentBounds.northEast;
|
||||
let southWest = segmentBounds.southWest;
|
||||
let bounds = new maplibregl.LngLatBounds(southWest, northEast);
|
||||
if (bounds.contains(point)) {
|
||||
segmentBoundsDistances.push([0, trackIndex, segmentIndex]);
|
||||
} else {
|
||||
let northWest: Coordinates = { lat: northEast.lat, lon: southWest.lon };
|
||||
let southEast: Coordinates = { lat: southWest.lat, lon: northEast.lon };
|
||||
let distanceToBounds = Math.min(
|
||||
crossarcDistance(northWest, northEast, point),
|
||||
crossarcDistance(northEast, southEast, point),
|
||||
crossarcDistance(southEast, southWest, point),
|
||||
crossarcDistance(southWest, northWest, point)
|
||||
);
|
||||
segmentBoundsDistances.push([distanceToBounds, trackIndex, segmentIndex]);
|
||||
}
|
||||
});
|
||||
segmentBoundsDistances.sort((a, b) => a[0] - b[0]);
|
||||
|
||||
let closest: { distance: number; indices: [number, number][] } = {
|
||||
distance: Number.MAX_VALUE,
|
||||
indices: [],
|
||||
};
|
||||
for (let s = 0; s < segmentBoundsDistances.length; s++) {
|
||||
if (segmentBoundsDistances[s][0] > closest.distance) {
|
||||
break;
|
||||
}
|
||||
const segment = file.getSegment(segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]);
|
||||
segment.trkpt.forEach((pt) => {
|
||||
let dist = distance(pt.getCoordinates(), point);
|
||||
if (dist < closest.distance) {
|
||||
closest.distance = dist;
|
||||
closest.indices = [[segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]]];
|
||||
} else if (dist === closest.distance) {
|
||||
closest.indices.push([segmentBoundsDistances[s][1], segmentBoundsDistances[s][2]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return closest.indices;
|
||||
}
|
||||
|
||||
export function getElevation(
|
||||
points: (TrackPoint | Waypoint | Coordinates)[],
|
||||
points: Coordinates[],
|
||||
ELEVATION_ZOOM: number = 12,
|
||||
tileSize = 512
|
||||
): Promise<number[]> {
|
||||
let coordinates = points.map((point) =>
|
||||
point instanceof TrackPoint || point instanceof Waypoint ? point.getCoordinates() : point
|
||||
);
|
||||
let coordinates = points;
|
||||
let bbox = new maplibregl.LngLatBounds();
|
||||
coordinates.forEach((coord) => bbox.extend(coord));
|
||||
|
||||
let tiles = coordinates.map((coord) => pointToTile(coord.lon, coord.lat, ELEVATION_ZOOM));
|
||||
let tiles = coordinates.map((coord) => pointToTile(coord.lng, coord.lat, ELEVATION_ZOOM));
|
||||
let uniqueTiles = Array.from(new Set(tiles.map((tile) => tile.join(',')))).map((tile) =>
|
||||
tile.split(',').map((x) => parseInt(x))
|
||||
);
|
||||
@@ -161,7 +78,7 @@ export function getElevation(
|
||||
return 0;
|
||||
}
|
||||
|
||||
let tf = pointToTileFraction(coord.lon, coord.lat, ELEVATION_ZOOM);
|
||||
let tf = pointToTileFraction(coord.lng, coord.lat, ELEVATION_ZOOM);
|
||||
let x = tileSize * (tf[0] - tile[0]);
|
||||
let y = tileSize * (tf[1] - tile[1]);
|
||||
let _x = Math.floor(x);
|
||||
|
||||
@@ -17,24 +17,14 @@
|
||||
ExternalLink,
|
||||
} from '@lucide/svelte';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import type { SelectionStatistics } from '$lib/engine';
|
||||
import { exampleStatistics } from '$lib/assets/example';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
import { writable } from 'svelte/store';
|
||||
import Scissors from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
|
||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
// TODO the example comes from the previous implementation
|
||||
let statistics = writable<SelectionStatistics>({
|
||||
global: { totalDistance: 0, elevationGain: 0, elevationLoss: 0 },
|
||||
length: 0,
|
||||
totalDistance: new Float64Array(),
|
||||
slope: new Float64Array(),
|
||||
lng: new Float64Array(),
|
||||
lat: new Float64Array(),
|
||||
ele: new Float64Array(),
|
||||
slice: () => undefined,
|
||||
});
|
||||
let statistics = writable(exampleStatistics);
|
||||
let slicedStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
let additionalDatasets = writable(['speed', 'atemp']);
|
||||
|
||||
@@ -14,15 +14,11 @@
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { loadFiles } from '$lib/logic/file-actions';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import { hoveredPoint } from '$lib/logic/statistics';
|
||||
import { slicedStatistics } from '$lib/logic/selection-statistics';
|
||||
import { hoveredPoint, slicedStatistics } from '$lib/logic/selection-statistics';
|
||||
import { engine } from '$lib/engine';
|
||||
import { getURLForGoogleDriveFile } from '$lib/components/embedding/embedding';
|
||||
import { db } from '$lib/db';
|
||||
import { fileStateCollection } from '$lib/logic/file-state';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
const {
|
||||
treeFileView,
|
||||
@@ -39,32 +35,26 @@
|
||||
);
|
||||
|
||||
onMount(async () => {
|
||||
settings.connectToDatabase(db);
|
||||
fileStateCollection.connectToDatabase(db).then(() => {
|
||||
let files: string[] = JSON.parse(page.url.searchParams.get('files') || '[]');
|
||||
let ids: string[] = JSON.parse(page.url.searchParams.get('ids') || '[]');
|
||||
let urls: string[] = files.concat(ids.map(getURLForGoogleDriveFile));
|
||||
settings.connect(await engine.openStorage());
|
||||
|
||||
if (urls.length > 0) {
|
||||
let downloads: Promise<File | null>[] = [];
|
||||
urls.forEach((url) => {
|
||||
downloads.push(
|
||||
fetch(url)
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => new File([blob], url.split('/').pop() ?? ''))
|
||||
);
|
||||
});
|
||||
let files: string[] = JSON.parse(page.url.searchParams.get('files') || '[]');
|
||||
let ids: string[] = JSON.parse(page.url.searchParams.get('ids') || '[]');
|
||||
let urls: string[] = files.concat(ids.map(getURLForGoogleDriveFile));
|
||||
|
||||
Promise.all(downloads).then((files) => {
|
||||
loadFiles(files.filter((file) => file !== null));
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
if (urls.length > 0) {
|
||||
let downloads: Promise<File | null>[] = [];
|
||||
urls.forEach((url) => {
|
||||
downloads.push(
|
||||
fetch(url)
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => new File([blob], url.split('/').pop() ?? ''))
|
||||
);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
settings.disconnectFromDatabase();
|
||||
fileStateCollection.disconnectFromDatabase();
|
||||
Promise.all(downloads).then((files) => {
|
||||
loadFiles(files.filter((file) => file !== null));
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -5,8 +5,5 @@ import tailwindcss from '@tailwindcss/vite';
|
||||
import wasm from 'vite-plugin-wasm';
|
||||
|
||||
export default defineConfig({
|
||||
ssr: {
|
||||
noExternal: ['gpx'],
|
||||
},
|
||||
plugins: [enhancedImages(), tailwindcss(), wasm(), sveltekit()],
|
||||
});
|
||||
Reference in new issue
Block a user