Created
March 17, 2026 06:47
-
-
Save mhaberler/f9f44853c77000adc5710d9766aa963b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Sensor Logger recording – Stiwoll – 2026-03-17 04:43:28 – 49s</title> | |
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> | |
| <link rel="stylesheet" href="https://unpkg.com/uplot@1.6.30/dist/uPlot.min.css" /> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; | |
| background: #f0f2f5; | |
| color: #1f2937; | |
| min-height: 100vh; | |
| } | |
| header { | |
| background: #1e3a5f; | |
| padding: 12px 24px; | |
| border-bottom: 2px solid #15304f; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| header h1 { | |
| font-size: 1.2rem; | |
| color: #ffffff; | |
| font-weight: 600; | |
| } | |
| header .meta { | |
| font-size: 0.8rem; | |
| color: #93c5fd; | |
| } | |
| #app { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| grid-template-rows: 1fr 300px auto; | |
| gap: 8px; | |
| padding: 8px; | |
| height: calc(100vh - 52px); | |
| } | |
| .panel { | |
| background: #ffffff; | |
| border: 1px solid #d1d5db; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.08); | |
| } | |
| .panel-title { | |
| font-size: 0.75rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.1em; | |
| color: #2563eb; | |
| padding: 8px 12px 4px; | |
| font-weight: 600; | |
| } | |
| .panel-body { | |
| flex: 1; | |
| position: relative; | |
| min-height: 0; | |
| } | |
| #map-panel { grid-column: 1; grid-row: 1; } | |
| #three-panel { grid-column: 2; grid-row: 1; } | |
| #chart-panel { grid-column: 1 / -1; grid-row: 2; } | |
| #controls-panel { grid-column: 1 / -1; grid-row: 3; } | |
| #map-container { width: 100%; height: 100%; } | |
| #three-container { width: 100%; height: 100%; } | |
| #chart-container { width: 100%; height: 100%; padding: 8px; } | |
| /* Controls */ | |
| .controls-bar { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 10px 16px; | |
| flex-wrap: wrap; | |
| } | |
| .transport-btn { | |
| background: #1e3a5f; | |
| border: 1px solid #2563eb; | |
| color: #ffffff; | |
| padding: 6px 12px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 0.85rem; | |
| transition: background 0.15s; | |
| } | |
| .transport-btn:hover { background: #2563eb; color: #fff; } | |
| .transport-btn.active { background: #2563eb; color: #fff; } | |
| .slider-group { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| font-size: 0.8rem; | |
| color: #6b7280; | |
| } | |
| .slider-group input[type="range"] { | |
| accent-color: #2563eb; | |
| } | |
| .slider-group label { white-space: nowrap; } | |
| #timeline-slider { width: 200px; } | |
| #speed-slider { width: 120px; } | |
| .stat-field { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 4px; | |
| background: #eff6ff; | |
| padding: 4px 10px; | |
| border-radius: 4px; | |
| font-size: 0.85rem; | |
| border: 1px solid #bfdbfe; | |
| } | |
| .stat-field .stat-label { color: #6b7280; font-size: 0.75rem; } | |
| .stat-field .stat-value { color: #1e3a5f; font-weight: 600; font-variant-numeric: tabular-nums; } | |
| .export-btn { | |
| margin-left: auto; | |
| background: #ffffff; | |
| border: 1px solid #059669; | |
| color: #059669; | |
| padding: 6px 14px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 0.8rem; | |
| } | |
| .export-btn:hover { background: #059669; color: #fff; } | |
| .time-display { | |
| font-variant-numeric: tabular-nums; | |
| color: #1f2937; | |
| font-size: 0.85rem; | |
| min-width: 80px; | |
| } | |
| .leaflet-control-attribution { font-size: 9px !important; } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>Sensor Logger recording – Stiwoll – 2026-03-17 04:43:28 – 49s</h1> | |
| <span class="meta">50 samples · generated 2026-03-17 06:53</span> | |
| </header> | |
| <script type="text/plain" id="csv-data">seconds_elapsed,orientation_qz,orientation_qy,orientation_qx,orientation_qw,orientation_roll,orientation_pitch,orientation_yaw,compass_magneticBearing,barometer_relativeAltitude,barometer_pressure,location_bearingAccuracy,location_speedAccuracy,location_verticalAccuracy,location_horizontalAccuracy,location_speed,location_bearing,location_altitude,location_longitude,location_latitude | |
| 0.0,-0.08644432760775089,0.09119788929820061,0.10179844126105309,0.9856078028678894,0.20429039001464844,-0.18759769201278687,0.19302037358283997,177.7328311634257,0.0,932.005126953125,,,,,,,,, | |
| 1.0,-0.2656877040863037,-0.0881725586950779,0.08336018025875092,0.9558970630168915,-0.127945888787508,-0.20810548961162567,0.5288469195365906,52.369068489628674,1.00408935546875,931.8924560546875,37.57186508178711,0.8182744979858398,3.015794277191162,7.882999897003174,0.537994921207428,160.72206115722656,868.7999877929688,15.2116017,47.1292212 | |
| 2.0,-0.34951353073120117,-0.170734241604805,0.11168552003800869,0.9084132313728333,-0.25794926285743713,-0.33259616792201996,0.6902811378240585,84.19762223528488,0.4993896484375,931.9491577148438,45.0,0.08037494122982025,3.882091760635376,5.835999965667725,0.4797990918159485,159.40882873535156,868.7999877929688,15.2115821,47.1292472 | |
| 3.0,-0.298484206199646,-0.18384123593568802,0.13254473730921745,0.9263474941253662,-0.2847595363855362,-0.3644202947616577,0.5711797773838043,78.72370334743502,0.29327392578125,931.9722900390625,6.321190357208252,0.9181410074234009,3.680576801300049,5.1539998054504395,0.7659105062484741,20.609827041625977,868.7999877929688,15.2115825,47.1292646 | |
| 4.0,-0.3626633286476135,-0.09095422737300396,0.04677151096984744,0.9250454008579254,-0.13655218854546547,-0.1537274233996868,0.738360732793808,61.01869345887174,0.311767578125,931.9700927734375,,,,,,,,, | |
| 5.0,-0.3591948300600052,-0.3363349288702011,-0.03810791647993028,0.8294791579246521,-0.7046690210700035,-0.1570713371038437,0.7226967215538025,71.62350192805549,0.2166748046875,931.9810180664062,10.220641136169434,0.9184688329696655,3.4362270832061768,4.607999801635742,0.8043633699417114,13.683735847473145,868.2592856390431,15.2115918,47.129279 | |
| 6.0,0.15265120193362236,-0.8670699894428253,-0.11374548450112343,0.3768561780452728,-2.3014503717422485,0.40771632036194205,0.11524593830108643,34.46837383126344,0.37518310546875,931.9630126953125,180.0,0.9215114712715149,3.1311709880828857,3.6480000019073486,0.6438695788383484,32.017303466796875,868.2649177779354,15.2115959,47.1292786 | |
| 7.0,0.17792688310146332,-0.87314772605896,-0.01691090688109398,0.43562839925289154,-2.1999318599700928,0.33614711463451385,-0.13303400203585625,30.53666180667298,-0.32501220703125,932.0415649414062,180.0,0.426113486289978,2.7549502849578857,3.5460000038146973,0.4465737044811249,65.8299331665039,868.2159182931128,15.2115979,47.1292761 | |
| 8.0,0.11946653202176094,-0.44578441977500916,-0.5508204698562622,0.630996972322464,-1.8588757514953613,1.0420953631401062,0.9745411276817322,182.30364647608562,-0.298583984375,932.0386352539062,180.0,0.19193719327449799,3.6445882320404053,3.3269999027252197,0.15846461057662964,82.97369384765625,868.1776200395889,15.2115985,47.1292753 | |
| 9.0,-0.43112923949956894,-0.09039062552619725,-0.016592485830187798,0.8656820058822632,-0.1884236002806574,-0.010484195314347744,0.9166986793279648,47.96030743050802,-0.84552001953125,932.0999755859375,180.0,0.18697117269039154,3.6445882320404053,3.253999948501587,0.15922793745994568,89.12895965576172,868.1776200395889,15.2115986,47.1292748 | |
| 10.0,-0.633157879114151,-0.024332723347470164,0.03714566212147474,0.7716073095798492,0.009894995018839836,-0.0865621268749237,1.3750301599502563,86.13796543003136,-0.6500244140625,932.0782470703125,,,,,,,,, | |
| 11.0,-0.6159274876117706,-0.07321419380605221,0.10795105248689651,0.7725454568862915,0.018289506435394287,-0.2531265765428543,1.3464823961257935,95.58300064994052,-0.50201416015625,932.0616455078125,100.66751861572266,0.31332457065582275,2.1511037349700928,3.0759999752044678,0.46323543787002563,104.7138671875,868.1320002837022,15.211608,47.1292789 | |
| 12.0,-0.5950314998626709,-0.1477539837360382,0.045097336173057556,0.7883602976799011,-0.18630049377679825,-0.2490822747349739,1.269599437713623,90.58459222889348,-0.41748046875,932.0521240234375,,,,,,,,, | |
| 13.0,-0.5860764682292938,-0.20904848724603653,0.061616163700819016,0.7767059504985809,-0.2717720530927181,-0.3444140702486038,1.2470147609710693,93.45841191976257,-0.478271484375,932.0587158203125,54.02828598022461,0.2957476079463959,2.1065962314605713,3.1710000038146973,0.7963052988052368,65.15839385986328,867.6814477909006,15.2116332,47.129295 | |
| 14.0,-0.5833109319210052,-0.18473969399929047,0.06331582367420197,0.7880399823188782,-0.23086409270763397,-0.3207301199436188,1.2374227046966553,99.78155152534562,-0.77154541015625,932.0919189453125,30.840513229370117,0.24171197414398193,1.9228384494781494,3.2860000133514404,0.944787859916687,66.83002471923828,867.6200618408612,15.2116542,47.129301 | |
| 15.0,-0.5703683495521545,-0.16183526813983917,-0.048935357481241226,0.8026322424411774,-0.32415594160556793,-0.10628461465239525,1.2167694568634033,83.04845432792024,-0.86138916015625,932.1019897460938,,,,,,,,, | |
| 16.0,-0.5153109133243561,-0.15002311021089554,0.010105494409799576,0.8428919911384583,-0.24973703920841217,-0.17341243475675583,1.0766035318374634,76.07362524407546,-0.998779296875,932.1172485351562,12.99361801147461,0.2596639394760132,1.9329853057861328,3.3570001125335693,1.032220482826233,61.777278900146484,867.4939122731067,15.2116932,47.1293126 | |
| 17.0,-0.5436005592346191,-0.11953375488519669,0.06079643964767456,0.8285613656044006,-0.13606269657611847,-0.23280154168605804,1.145337700843811,79.81162652375653,-0.7398681640625,932.0883178710938,12.333927154541016,0.24544323980808258,1.8340840339660645,3.303999900817871,1.0117696523666382,55.81776809692383,867.4128170995401,15.2117081,47.1293179 | |
| 18.0,-0.5952634513378143,0.013331679627299309,0.02551836147904396,0.801678866147995,0.0520037617534399,-0.02447305154055357,1.2780630588531494,68.7779330875039,-0.92742919921875,932.1093139648438,11.895368576049805,0.22197014093399048,1.9535025358200073,3.2049999237060547,1.0407880544662476,57.68764114379883,867.1824884505564,15.2117196,47.129323 | |
| 19.0,-0.7230762839317322,0.1134573295712471,-0.018382838577963412,0.6770386099815369,0.12856608256697655,0.18705951422452927,1.6250409483909607,58.5869783656531,-1.33172607421875,932.154541015625,,,,,,,,, | |
| 20.0,-0.8136069774627686,0.10272371023893356,-0.09661458805203438,0.5590123534202576,-0.040217586793005466,0.2792619466781616,1.9422898888587952,73.8566315411731,-1.2286376953125,932.1431274414062,11.855274200439453,0.22556258738040924,1.8811010122299194,3.078000068664551,1.0337423086166382,84.93197631835938,867.1588364691092,15.2117474,47.1293285 | |
| 21.0,-0.8913457095623016,0.12092739343643188,-0.08331737294793129,0.42759890854358673,-0.0459861783310771,0.2907884865999222,2.2528178691864014,98.54947341326145,-1.41888427734375,932.1644897460938,,,,,,,,, | |
| 22.0,-0.9399935901165009,0.09276782721281052,-0.024248127825558186,0.31156354397535324,0.011001814156770706,0.18905429542064667,2.5008164644241333,125.53654293630403,-1.45062255859375,932.1679077148438,12.373570442199707,0.22074712812900543,1.879306435585022,3.0250000953674316,0.9790939092636108,129.7823944091797,867.0507139537127,15.2117698,47.1293194 | |
| 23.0,0.007714122533798218,0.010523620992898941,-0.011360416421666741,0.08084711630363017,-0.01618175720795989,0.22949884086847305,-0.15400445461273193,155.38881584871552,-1.788818359375,932.2059326171875,,,,,,,,, | |
| 24.0,-9.631216526031494E-4,0.00571059063076973,0.03567343903705478,0.022116929292678833,-0.09074125485494733,0.20978495478630066,0.004824399948120117,151.04866927964457,-2.1005859375,932.240966796875,11.650644302368164,0.24862560629844666,1.8918834924697876,3.0329999923706055,1.0698206424713135,173.6055145263672,866.8271524388066,15.2117809,47.1292996 | |
| 25.0,-7.608234882354736E-4,0.028091516345739365,0.021979358047246933,0.04648584686219692,-0.2117350995540619,0.12725670263171196,0.02343285083770752,132.64598019619183,-2.1693115234375,932.248779296875,12.121906280517578,0.20379789173603058,1.851344347000122,3.0269999504089355,1.1250724792480469,179.3681640625,866.5686842785983,15.2117792,47.1292883 | |
| 26.0,-0.022554129362106323,-0.0016249381005764008,0.047260135412216187,0.10668070241808891,-0.5957466810941696,0.03749215416610241,0.15861833095550537,118.57855931385927,-2.2750244140625,932.2606811523438,,,,,,,,, | |
| 27.0,0.1986967697739601,0.0021721869707107544,0.006856471300125122,0.31151849403977394,-2.0809521675109863,0.21474294364452362,2.323177218437195,92.91998265320704,-1.59326171875,932.1841430664062,11.915589332580566,0.22216449677944183,1.8170580863952637,3.015000104904175,1.1655666828155518,179.7118377685547,866.3806099337162,15.2117768,47.1292648 | |
| 28.0,-0.823909342288971,-0.019668590277433395,-0.03590157628059387,0.49375396966934204,-0.09512220323085785,0.07812150847166777,2.0524532794952393,135.09526656447218,-2.74530029296875,932.3134155273438,,,,,,,,, | |
| 29.0,-0.9540323615074158,0.04078492149710655,-0.1383989416062832,0.257784403860569,-0.24877072870731354,0.1515682488679886,2.6335697174072266,114.66662082421344,-2.32257080078125,932.2659912109375,11.840937614440918,0.21581318974494934,2.1763815879821777,3.009999990463257,1.120317816734314,168.92613220214844,865.5999755859375,15.2117983,47.1292459 | |
| 30.0,-0.9121628701686859,-0.019356108736246824,-0.18408791720867157,0.36007633805274963,-0.36407747864723206,0.09635015949606895,2.4086015224456787,111.57744789892247,-2.5867919921875,932.2954711914062,25.21116828918457,0.2257736772298813,2.117218494415283,3.0329999923706055,1.1730488538742065,165.0810089111328,865.5999755859375,15.2117964,47.1292383 | |
| 31.0,-0.8768338859081268,-0.07710456848144531,-0.15686850994825363,0.447314515709877,-0.3515230268239975,0.0049224435351789,2.199352502822876,114.61168411699833,-2.68719482421875,932.306884765625,23.936904907226562,0.21576125919818878,2.307523250579834,3.062000036239624,1.2083373069763184,146.8518829345703,865.5999755859375,15.2118027,47.1292291 | |
| 32.0,-0.8582769334316254,-0.08448543399572372,-0.15648549795150757,0.4812740683555603,-0.35759207606315613,0.005511674185981974,2.1205180883407593,109.37844306456527,-2.845703125,932.3247680664062,22.113813400268555,0.21029986441135406,2.3290696144104004,3.1019999980926514,1.1932735443115234,136.0508270263672,865.5999755859375,15.2118146,47.1292221 | |
| 33.0,-0.8298305571079254,-0.10592042282223701,-0.16462994366884232,0.5210076868534088,-0.3934279531240463,-0.004852045327425003,2.018203914165497,105.94506091636364,-2.983154296875,932.3399658203125,20.609983444213867,0.21802867949008942,2.425243377685547,3.114000082015991,1.157045841217041,124.78156280517578,865.5999755859375,15.2118285,47.1292174 | |
| 34.0,-0.8135093748569489,-0.09253000468015671,-0.1488851234316826,0.5539363920688629,-0.35266080498695374,0.014572644256986678,1.9484835863113403,104.35401381879899,-3.00164794921875,932.342041015625,15.663887023925781,0.21917006373405457,2.5242226123809814,3.1679999828338623,1.147882342338562,113.91002655029297,865.5999755859375,15.2118442,47.1292146 | |
| 35.0,-0.8292766511440277,-0.07091749459505081,-0.1532697230577469,0.532610684633255,-0.33642761409282684,0.04555694758892059,2.007509708404541,106.54552738740784,-2.906494140625,932.3316040039062,14.855649948120117,0.17257435619831085,2.4807255268096924,3.2339999675750732,1.1389310359954834,107.69793701171875,865.5999755859375,15.2118611,47.1292136 | |
| 36.0,-0.9024549126625061,-0.031340764835476875,-0.1441398300230503,0.40321606397628784,-0.2908257991075516,0.05887397564947605,2.310780882835388,113.21595546240019,-2.9276123046875,932.3339233398438,14.651507377624512,0.17530427873134613,2.4696004390716553,3.2699999809265137,1.074712872505188,110.24418640136719,865.5999755859375,15.2118764,47.1292124 | |
| 37.0,-0.9326882064342499,-0.025021438486874104,-0.140006672590971,0.32727178931236267,-0.2829953208565712,0.042513612657785416,2.473995089530945,116.63945747512082,-2.65814208984375,932.3036499023438,,,,,,,,, | |
| 38.0,-0.9232697784900665,0.003565677907317877,-0.13175402954220772,0.3538881540298462,-0.24392734467983246,0.09688715264201164,2.4211137294769287,113.12989109802407,-2.2274169921875,932.2552490234375,13.259521484375,0.23056338727474213,2.618950605392456,3.265000104904175,0.9642587900161743,131.54214477539062,865.5999755859375,15.2119,47.1292031 | |
| 39.0,-0.9052609503269196,-7.381148752756417E-4,-0.1197657585144043,0.4060983955860138,-0.22064022719860077,0.09552204608917236,2.308926582336426,116.38977122109088,-1.94732666015625,932.2237548828125,,,,,,,,, | |
| 40.0,-0.9023710787296295,-0.014291089493781328,-0.09359142929315567,0.41897641122341156,-0.18219498544931412,0.05181289650499821,2.276960611343384,118.58458586041564,-1.698974609375,932.196044921875,13.242439270019531,0.262595534324646,2.6441562175750732,3.1710000038146973,0.8387566208839417,122.42157745361328,865.5999755859375,15.2119161,47.1291945 | |
| 41.0,-0.9118027091026306,-0.047511179000139236,-0.021708425134420395,0.4072926342487335,-0.0785570964217186,-0.06901305168867111,2.298684597015381,128.15606235350353,-1.33966064453125,932.1557006835938,10.28044605255127,0.2097874879837036,2.622119665145874,3.109999895095825,0.8593690395355225,123.65911865234375,865.5999755859375,15.2119243,47.1291898 | |
| 42.0,-0.941298246383667,-0.01736861653625965,-0.0649716816842556,0.33011528849601746,-0.1341865435242653,0.010039952583611012,2.467665195465088,129.3633691571963,-0.80853271484375,932.0960693359375,,,,,,,,, | |
| 43.0,-0.9403172731399536,-0.0206897109746933,-0.05378318578004837,0.3344625383615494,-0.11521876230835915,-0.002641818020492792,2.458287000656128,125.33042049620758,-0.75567626953125,932.0899658203125,11.722803115844727,0.18253007531166077,2.7567927837371826,3.0480000972747803,0.8248430490493774,136.92919921875,865.5999755859375,15.2119393,47.1291774 | |
| 44.0,-0.8455488383769989,-0.1077936664223671,0.060347242280840874,0.5077158063650131,-0.01788571197539568,-0.2521427348256111,2.0587417483329773,123.76193588159374,-0.25897216796875,932.0342407226562,12.170211791992188,0.2044217884540558,2.8219363689422607,3.0339999198913574,0.7614890933036804,127.73212432861328,865.5999755859375,15.2119445,47.1291717 | |
| 45.0,-0.7234142124652863,-0.06937270611524582,0.18884819000959396,0.659534364938736,0.19637150317430496,-0.3578534424304962,1.6985156536102295,102.62794978763631,-0.2166748046875,932.0293579101562,12.456461906433105,0.2247518002986908,2.77252197265625,3.002000093460083,0.7225338816642761,100.51154327392578,865.5999755859375,15.2119507,47.1291687 | |
| 46.0,-0.7069641947746277,0.02475636824965477,0.1619688719511032,0.6828266084194183,0.2684032618999481,-0.190738283097744,1.6315068006515503,95.12871748896472,-0.34088134765625,932.0435180664062,12.757087707519531,0.264897882938385,2.8320207595825195,3.010999917984009,0.6936858296394348,81.84463500976562,865.5999755859375,15.2119599,47.1291686 | |
| 47.0,-0.8994994461536407,-0.04712995537556708,0.06272909790277481,0.40150365233421326,0.08668948151171207,-0.1461792290210724,2.3070566654205322,142.8860025679128,-0.253662109375,932.0337524414062,,,,,,,,, | |
| 48.0,-0.9503905773162842,-0.13565251231193542,0.08317969925701618,0.26269135624170303,0.09021434001624584,-0.3081786334514618,2.616485834121704,169.1797877249749,-0.28271484375,932.0369873046875,12.341652870178223,0.23659424483776093,2.849734306335449,3.009999990463257,0.4143098294734955,142.1599884033203,867.3153912702744,15.2119601,47.1291673 | |
| 49.0,-0.9662469625473022,-0.10430022701621056,0.0942748636007309,0.210094153881073,0.1433434784412384,-0.24480533599853516,2.7331552505493164,167.6736902021679,-0.639404296875,932.076904296875,,,,,,,,, | |
| </script> | |
| <div id="app"> | |
| <div class="panel" id="map-panel"> | |
| <div class="panel-title">GPS Track</div> | |
| <div class="panel-body"><div id="map-container"></div></div> | |
| </div> | |
| <div class="panel" id="three-panel"> | |
| <div class="panel-title">Orientation</div> | |
| <div class="panel-body"><div id="three-container"></div></div> | |
| </div> | |
| <div class="panel" id="chart-panel"> | |
| <div class="panel-title">Altitude</div> | |
| <div class="panel-body"><div id="chart-container"></div></div> | |
| </div> | |
| <div class="panel" id="controls-panel"> | |
| <div class="controls-bar"> | |
| <button class="transport-btn" id="btn-begin" title="Go to beginning">⏮</button> | |
| <button class="transport-btn" id="btn-play" title="Play">▶</button> | |
| <button class="transport-btn" id="btn-pause" title="Pause">⏸</button> | |
| <button class="transport-btn" id="btn-stop" title="Stop">■</button> | |
| <button class="transport-btn" id="btn-end" title="Go to end">⏭</button> | |
| <div class="slider-group"> | |
| <label for="timeline-slider">Time:</label> | |
| <input type="range" id="timeline-slider" min="0" max="1" step="1" value="0"> | |
| <span class="time-display" id="time-display">0.0s / 0.0s</span> | |
| </div> | |
| <div class="slider-group"> | |
| <label for="speed-slider">Speed:</label> | |
| <input type="range" id="speed-slider" min="0.1" max="20" step="0.1" value="5"> | |
| <span id="speed-display">5.0x</span> | |
| </div> | |
| <div class="stat-field"> | |
| <span class="stat-label">Speed</span> | |
| <span class="stat-value" id="stat-speed">--</span> | |
| <span style="color:#666; font-size:0.7rem">m/s</span> | |
| </div> | |
| <div class="stat-field"> | |
| <span class="stat-label">Bearing</span> | |
| <span class="stat-value" id="stat-bearing">--</span> | |
| <span style="color:#666; font-size:0.7rem">°</span> | |
| </div> | |
| <button class="export-btn" id="btn-export">Export JSON</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> | |
| <script src="https://unpkg.com/uplot@1.6.30/dist/uPlot.iife.min.js"></script> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://unpkg.com/three@0.170.0/build/three.module.js", | |
| "three/addons/": "https://unpkg.com/three@0.170.0/examples/jsm/" | |
| } | |
| } | |
| </script> | |
| <script type="module"> | |
| import * as THREE from 'three'; | |
| import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; | |
| (function() { | |
| 'use strict'; | |
| // ─── CSV Parsing ─── | |
| const csvText = document.getElementById('csv-data').textContent.trim(); | |
| const lines = csvText.split('\n').filter(Boolean); | |
| const headers = lines[0].split(',').map(h => h.trim()); | |
| const rows = []; | |
| for (let i = 1; i < lines.length; i++) { | |
| const vals = lines[i].split(','); | |
| const row = {}; | |
| for (let j = 0; j < headers.length; j++) { | |
| const v = vals[j]?.trim(); | |
| row[headers[j]] = v === '' || v === undefined ? null : parseFloat(v); | |
| } | |
| rows.push(row); | |
| } | |
| const maxTime = rows.length > 0 ? rows[rows.length - 1].seconds_elapsed : 0; | |
| // ─── Playback State ─── | |
| const state = { | |
| currentIndex: 0, | |
| playing: false, | |
| playbackRate: 5, | |
| lastTimestamp: null, | |
| accum: 0, | |
| }; | |
| function currentRow() { return rows[state.currentIndex] || rows[0]; } | |
| // ─── DOM refs ─── | |
| const timelineSlider = document.getElementById('timeline-slider'); | |
| const speedSlider = document.getElementById('speed-slider'); | |
| const timeDisplay = document.getElementById('time-display'); | |
| const speedDisplay = document.getElementById('speed-display'); | |
| const statSpeed = document.getElementById('stat-speed'); | |
| const statBearing = document.getElementById('stat-bearing'); | |
| timelineSlider.max = rows.length - 1; | |
| timelineSlider.value = 0; | |
| // ─── Leaflet Map ─── | |
| const map = L.map('map-container', { attributionControl: true }); | |
| L.tileLayer('https://tile.openstreetmap.de/{z}/{x}/{y}.png', { | |
| attribution: '© OpenStreetMap contributors', | |
| maxZoom: 19, | |
| }).addTo(map); | |
| const gpsPoints = []; | |
| const gpsIndexMap = []; // maps gps array index → row index | |
| for (let i = 0; i < rows.length; i++) { | |
| const r = rows[i]; | |
| if (r.location_latitude !== null && r.location_longitude !== null) { | |
| gpsPoints.push([r.location_latitude, r.location_longitude]); | |
| gpsIndexMap.push(i); | |
| } | |
| } | |
| let trackLine = null; | |
| let marker = null; | |
| if (gpsPoints.length > 0) { | |
| trackLine = L.polyline(gpsPoints, { color: '#2563eb', weight: 3, opacity: 0.8 }).addTo(map); | |
| map.fitBounds(trackLine.getBounds(), { padding: [20, 20] }); | |
| marker = L.circleMarker(gpsPoints[0], { | |
| radius: 7, color: '#fff', fillColor: '#2563eb', fillOpacity: 1, weight: 2, | |
| }).addTo(map); | |
| } else { | |
| map.setView([47.13, 15.21], 13); | |
| } | |
| function updateMap(row) { | |
| if (!marker) return; | |
| if (row.location_latitude !== null && row.location_longitude !== null) { | |
| marker.setLatLng([row.location_latitude, row.location_longitude]); | |
| } | |
| } | |
| // Invalidate map size after layout settles | |
| setTimeout(() => map.invalidateSize(), 200); | |
| // ─── Three.js Orientation ─── | |
| const threeContainer = document.getElementById('three-container'); | |
| const scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0xf0f2f5); | |
| const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100); | |
| camera.position.set(5, 3.5, 5); | |
| camera.lookAt(0, 0, 0); | |
| const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| threeContainer.appendChild(renderer.domElement); | |
| const controls = new OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.08; | |
| // Grid | |
| scene.add(new THREE.GridHelper(4, 8, 0xd1d5db, 0xe5e7eb)); | |
| // Cylinder-based axes (matching compass shaft thickness) | |
| const axisRadius = 0.015; | |
| const axisLength = 2.0; | |
| function makeAxis(color, direction) { | |
| const geo = new THREE.CylinderGeometry(axisRadius, axisRadius, axisLength, 8); | |
| geo.translate(0, axisLength / 2, 0); | |
| const mat = new THREE.MeshBasicMaterial({ color }); | |
| const mesh = new THREE.Mesh(geo, mat); | |
| if (direction === 'x') mesh.rotation.z = -Math.PI / 2; | |
| if (direction === 'z') mesh.rotation.x = Math.PI / 2; | |
| return mesh; | |
| } | |
| scene.add(makeAxis(0xef4444, 'x')); | |
| scene.add(makeAxis(0x22c55e, 'y')); | |
| scene.add(makeAxis(0x3b82f6, 'z')); | |
| // Axis labels (X=red, Y=green, Z=blue) | |
| function makeLabel(text, color, position) { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = 64; canvas.height = 32; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.font = 'bold 24px sans-serif'; | |
| ctx.fillStyle = color; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText(text, 32, 16); | |
| const tex = new THREE.CanvasTexture(canvas); | |
| const mat = new THREE.SpriteMaterial({ map: tex, depthTest: false }); | |
| const sprite = new THREE.Sprite(mat); | |
| sprite.position.copy(position); | |
| sprite.scale.set(0.5, 0.25, 1); | |
| return sprite; | |
| } | |
| scene.add(makeLabel('X', '#ef4444', new THREE.Vector3(2.3, 0, 0))); | |
| scene.add(makeLabel('Y', '#22c55e', new THREE.Vector3(0, 2.3, 0))); | |
| scene.add(makeLabel('Z', '#3b82f6', new THREE.Vector3(0, 0, 2.3))); | |
| // Device mesh — a box representing the sensor/phone | |
| const deviceGeo = new THREE.BoxGeometry(0.6, 1.0, 0.15); | |
| const deviceMat = new THREE.MeshPhongMaterial({ color: 0x1e3a5f, transparent: true, opacity: 0.85 }); | |
| const deviceMesh = new THREE.Mesh(deviceGeo, deviceMat); | |
| scene.add(deviceMesh); | |
| // Screen indicator on device (small plane on front face) | |
| const screenGeo = new THREE.PlaneGeometry(0.45, 0.7); | |
| const screenMat = new THREE.MeshBasicMaterial({ color: 0x93c5fd, side: THREE.FrontSide }); | |
| const screenMesh = new THREE.Mesh(screenGeo, screenMat); | |
| screenMesh.position.z = 0.076; | |
| deviceMesh.add(screenMesh); | |
| // Compass arrow on ground plane | |
| const arrowDir = new THREE.Vector3(0, 0, -1); | |
| const compassOrigin = new THREE.Vector3(0, -0.5, 0); | |
| const compassArrow = new THREE.ArrowHelper(arrowDir, compassOrigin, 2.0, 0x059669, 0.25, 0.15); | |
| compassArrow.line.material.linewidth = 2; | |
| // Cylinder shaft (half previous thickness) | |
| const shaftGeo = new THREE.CylinderGeometry(0.015, 0.015, 1.75, 8); | |
| shaftGeo.translate(0, 0.875, 0); | |
| shaftGeo.rotateX(Math.PI / 2); | |
| const shaftMat = new THREE.MeshBasicMaterial({ color: 0x059669 }); | |
| const compassShaft = new THREE.Mesh(shaftGeo, shaftMat); | |
| compassShaft.position.copy(compassOrigin); | |
| scene.add(compassShaft); | |
| scene.add(compassArrow); | |
| // "N" marker at compass tip | |
| const compassLabel = makeLabel('N', '#059669', new THREE.Vector3(0, -0.5, -2.3)); | |
| scene.add(compassLabel); | |
| // Lighting | |
| scene.add(new THREE.AmbientLight(0xffffff, 0.6)); | |
| const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| dirLight.position.set(3, 5, 3); | |
| scene.add(dirLight); | |
| function resizeThree() { | |
| const w = threeContainer.clientWidth; | |
| const h = threeContainer.clientHeight; | |
| if (w === 0 || h === 0) return; | |
| camera.aspect = w / h; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(w, h); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| } | |
| new ResizeObserver(resizeThree).observe(threeContainer); | |
| resizeThree(); | |
| // Three.js render loop | |
| function animateThree() { | |
| requestAnimationFrame(animateThree); | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| } | |
| animateThree(); | |
| function updateThree(row) { | |
| // CSV columns: qz, qy, qx, qw → reorder to (qx, qy, qz, qw) | |
| const qx = row.orientation_qx ?? 0; | |
| const qy = row.orientation_qy ?? 0; | |
| const qz = row.orientation_qz ?? 0; | |
| const qw = row.orientation_qw ?? 1; | |
| // iOS Z-up → Three.js Y-up: remap (qx, qy, qz, qw) → (qx, -qz, qy, qw) | |
| deviceMesh.quaternion.set(qx, -qz, qy, qw); | |
| deviceMesh.quaternion.normalize(); | |
| // Compass arrow: rotate around Y axis with noise | |
| const bearing = row.compass_magneticBearing ?? 0; | |
| const noise = (Math.random() - 0.5) * 4; // ±2 degrees jitter | |
| const bearingRad = -(bearing + noise) * Math.PI / 180; | |
| const dir = new THREE.Vector3(Math.sin(bearingRad), 0, -Math.cos(bearingRad)); | |
| compassArrow.setDirection(dir); | |
| // Rotate thick shaft to match | |
| compassShaft.rotation.set(0, 0, 0); | |
| compassShaft.lookAt( | |
| compassShaft.position.x + dir.x, | |
| compassShaft.position.y, | |
| compassShaft.position.z + dir.z | |
| ); | |
| // Move N label to arrow tip | |
| compassLabel.position.set( | |
| compassOrigin.x + dir.x * 2.3, | |
| compassOrigin.y, | |
| compassOrigin.z + dir.z * 2.3 | |
| ); | |
| } | |
| // ─── uPlot Chart ─── | |
| const chartContainer = document.getElementById('chart-container'); | |
| const timeData = rows.map(r => r.seconds_elapsed); | |
| const baroAlt = rows.map(r => r.barometer_relativeAltitude); | |
| const gpsAlt = rows.map(r => r.location_altitude); | |
| let uplot = null; | |
| let cursorUpdating = false; | |
| const cursorPlugin = { | |
| hooks: { | |
| setCursor: [function(u) { | |
| if (cursorUpdating) return; | |
| const idx = u.cursor.idx; | |
| if (idx != null && !state.playing) { | |
| state.currentIndex = idx; | |
| cursorUpdating = true; | |
| updateAll(); | |
| cursorUpdating = false; | |
| } | |
| }], | |
| }, | |
| }; | |
| function initChart() { | |
| const rect = chartContainer.getBoundingClientRect(); | |
| const w = Math.max(rect.width - 16, 200); | |
| const h = Math.max(rect.height - 16, 100); | |
| const opts = { | |
| width: w, | |
| height: h, | |
| plugins: [cursorPlugin], | |
| cursor: { sync: { key: 'sensor' } }, | |
| scales: { | |
| x: { time: false }, | |
| y: { auto: true }, | |
| y2: { auto: true, side: 1 }, | |
| }, | |
| axes: [ | |
| { stroke: '#6b7280', grid: { stroke: '#e5e7eb' }, ticks: { stroke: '#d1d5db' } }, | |
| { stroke: '#2563eb', grid: { stroke: '#e5e7eb' }, ticks: { stroke: '#d1d5db' }, label: 'Baro Rel. Alt (m)' }, | |
| { stroke: '#059669', side: 1, grid: { show: false }, ticks: { stroke: '#d1d5db' }, label: 'GPS Alt (m)', scale: 'y2' }, | |
| ], | |
| series: [ | |
| { label: 'Time (s)' }, | |
| { label: 'Baro Rel. Alt', stroke: '#2563eb', width: 2, scale: 'y' }, | |
| { label: 'GPS Altitude', stroke: '#059669', width: 2, scale: 'y2', | |
| paths: (u, seriesIdx, idx0, idx1) => { | |
| // Skip null gaps | |
| return uPlot.paths.linear()(u, seriesIdx, idx0, idx1); | |
| }, | |
| }, | |
| ], | |
| }; | |
| uplot = new uPlot(opts, [timeData, baroAlt, gpsAlt], chartContainer); | |
| } | |
| function updateChart() { | |
| if (!uplot) return; | |
| const idx = state.currentIndex; | |
| const left = uplot.valToPos(timeData[idx], 'x'); | |
| uplot.setCursor({ left, top: 0 }); | |
| } | |
| new ResizeObserver(() => { | |
| if (!uplot) return; | |
| const rect = chartContainer.getBoundingClientRect(); | |
| uplot.setSize({ width: Math.max(rect.width - 16, 200), height: Math.max(rect.height - 16, 100) }); | |
| }).observe(chartContainer); | |
| setTimeout(initChart, 100); | |
| // ─── Update All Panels ─── | |
| function updateAll() { | |
| const row = currentRow(); | |
| const t = row.seconds_elapsed ?? 0; | |
| // Timeline slider | |
| timelineSlider.value = state.currentIndex; | |
| timeDisplay.textContent = t.toFixed(1) + 's / ' + maxTime.toFixed(1) + 's'; | |
| // Stats | |
| statSpeed.textContent = row.location_speed !== null ? row.location_speed.toFixed(2) : '--'; | |
| statBearing.textContent = row.location_bearing !== null ? row.location_bearing.toFixed(1) : '--'; | |
| // Panels | |
| updateMap(row); | |
| updateThree(row); | |
| updateChart(); | |
| } | |
| // ─── Playback Loop ─── | |
| function playbackLoop(timestamp) { | |
| if (!state.playing) return; | |
| if (state.lastTimestamp !== null) { | |
| const dt = (timestamp - state.lastTimestamp) / 1000; // seconds | |
| state.accum += dt * state.playbackRate; | |
| // Advance index based on seconds_elapsed differences | |
| while (state.accum >= 1 && state.currentIndex < rows.length - 1) { | |
| state.currentIndex++; | |
| state.accum -= 1; | |
| } | |
| if (state.currentIndex >= rows.length - 1) { | |
| state.currentIndex = rows.length - 1; | |
| state.playing = false; | |
| btnPlay.classList.remove('active'); | |
| } | |
| updateAll(); | |
| } | |
| state.lastTimestamp = timestamp; | |
| if (state.playing) requestAnimationFrame(playbackLoop); | |
| } | |
| // ─── Transport Controls ─── | |
| const btnBegin = document.getElementById('btn-begin'); | |
| const btnPlay = document.getElementById('btn-play'); | |
| const btnPause = document.getElementById('btn-pause'); | |
| const btnStop = document.getElementById('btn-stop'); | |
| const btnEnd = document.getElementById('btn-end'); | |
| const btnExport = document.getElementById('btn-export'); | |
| btnPlay.addEventListener('click', () => { | |
| if (state.currentIndex >= rows.length - 1) state.currentIndex = 0; | |
| state.playing = true; | |
| state.lastTimestamp = null; | |
| state.accum = 0; | |
| btnPlay.classList.add('active'); | |
| requestAnimationFrame(playbackLoop); | |
| }); | |
| btnPause.addEventListener('click', () => { | |
| state.playing = false; | |
| state.lastTimestamp = null; | |
| btnPlay.classList.remove('active'); | |
| }); | |
| btnStop.addEventListener('click', () => { | |
| state.playing = false; | |
| state.lastTimestamp = null; | |
| state.currentIndex = 0; | |
| btnPlay.classList.remove('active'); | |
| updateAll(); | |
| }); | |
| btnBegin.addEventListener('click', () => { | |
| state.currentIndex = 0; | |
| if (!state.playing) updateAll(); | |
| }); | |
| btnEnd.addEventListener('click', () => { | |
| state.playing = false; | |
| state.lastTimestamp = null; | |
| state.currentIndex = rows.length - 1; | |
| btnPlay.classList.remove('active'); | |
| updateAll(); | |
| }); | |
| // Timeline slider | |
| timelineSlider.addEventListener('input', () => { | |
| state.currentIndex = parseInt(timelineSlider.value, 10); | |
| if (!state.playing) updateAll(); | |
| }); | |
| // Speed slider | |
| speedSlider.addEventListener('input', () => { | |
| state.playbackRate = parseFloat(speedSlider.value); | |
| speedDisplay.textContent = state.playbackRate.toFixed(1) + 'x'; | |
| }); | |
| // Export JSON | |
| btnExport.addEventListener('click', () => { | |
| const json = JSON.stringify(rows, null, 2); | |
| const blob = new Blob([json], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'Unknown-sensor-data.json'; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| }); | |
| // Initial render | |
| updateAll(); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment