Skip to content

Instantly share code, notes, and snippets.

@mhaberler
Created March 17, 2026 06:47
Show Gist options
  • Select an option

  • Save mhaberler/f9f44853c77000adc5710d9766aa963b to your computer and use it in GitHub Desktop.

Select an option

Save mhaberler/f9f44853c77000adc5710d9766aa963b to your computer and use it in GitHub Desktop.
<!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 &middot; 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">&#9198;</button>
<button class="transport-btn" id="btn-play" title="Play">&#9654;</button>
<button class="transport-btn" id="btn-pause" title="Pause">&#9208;</button>
<button class="transport-btn" id="btn-stop" title="Stop">&#9632;</button>
<button class="transport-btn" id="btn-end" title="Go to end">&#9197;</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">&deg;</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: '&copy; 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