Spaces:
Sleeping
Sleeping
Upload index.html
Browse files- index.html +136 -131
index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
| 12 |
top: 0;
|
| 13 |
left: 0;
|
| 14 |
width: 100%;
|
| 15 |
-
height:
|
| 16 |
background: rgba(255, 255, 255, 0.95);
|
| 17 |
display: flex;
|
| 18 |
align-items: center;
|
|
@@ -25,6 +25,14 @@
|
|
| 25 |
font-size: 13px;
|
| 26 |
}
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
.control-group {
|
| 29 |
display: flex;
|
| 30 |
flex-direction: column; /* Stack label over slider */
|
|
@@ -73,8 +81,8 @@
|
|
| 73 |
/* Adjust your Plotly container to make room for the banner */
|
| 74 |
#plot-container {
|
| 75 |
display: none; /* Hidden until data is loaded */
|
| 76 |
-
margin-top:
|
| 77 |
-
height: calc(100vh -
|
| 78 |
}
|
| 79 |
.slider-label { display: flex; justify-content: space-between; margin-bottom: 8px; font-weight: bold; }
|
| 80 |
input[type=range] { width: 100%; }
|
|
@@ -83,19 +91,34 @@
|
|
| 83 |
<body>
|
| 84 |
|
| 85 |
<div id="controls" class="top-banner">
|
| 86 |
-
<div class="
|
| 87 |
-
<
|
|
|
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
-
<button class="btn" id="process-btn"
|
| 95 |
-
style="background: #007bff; color: white; border: 1px solid #007bff; border-radius: 4px 4px 4px 4px; cursor: pointer; white-space: nowrap; margin-left:6px;"
|
| 96 |
-
onclick="uploadAndProcess()">
|
| 97 |
-
Process File
|
| 98 |
-
</button>
|
| 99 |
</div>
|
| 100 |
|
| 101 |
<div class="control-group">
|
|
@@ -107,11 +130,12 @@
|
|
| 107 |
<div class="control-group">
|
| 108 |
<div class="slider-header">
|
| 109 |
<span>Trajectories:</span>
|
| 110 |
-
<span id="
|
| 111 |
</div>
|
| 112 |
-
<input type="range" id="
|
| 113 |
</div>
|
| 114 |
|
|
|
|
| 115 |
<div class="control-group">
|
| 116 |
<div class="slider-header">
|
| 117 |
<span>Opacity:</span>
|
|
@@ -120,8 +144,8 @@
|
|
| 120 |
<input type="range" id="opacity" min="0" max="0.99" step="0.01" value="0.99">
|
| 121 |
</div>
|
| 122 |
|
| 123 |
-
<div class="control-group">
|
| 124 |
-
<button id="reset-view">Reset View</button>
|
| 125 |
</div>
|
| 126 |
|
| 127 |
<div class="info-text">
|
|
@@ -136,8 +160,11 @@
|
|
| 136 |
<script>
|
| 137 |
|
| 138 |
let allData = [];
|
| 139 |
-
let
|
|
|
|
| 140 |
let floorData, wallData, sideData;
|
|
|
|
|
|
|
| 141 |
|
| 142 |
function handleDrop(event) {
|
| 143 |
event.preventDefault();
|
|
@@ -191,26 +218,36 @@
|
|
| 191 |
const allZ = allData.flatMap(t => t.z);
|
| 192 |
|
| 193 |
// Define the boundaries of your "box"
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
| 201 |
|
| 202 |
// 6. Trigger UI
|
| 203 |
document.getElementById('plot-container').style.display = 'block';
|
| 204 |
-
//
|
| 205 |
-
|
| 206 |
-
document.getElementById('
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
}
|
| 211 |
} catch (err) {
|
|
|
|
| 212 |
console.error(err);
|
| 213 |
-
alert("Upload failed. Check file size.");
|
| 214 |
} finally {
|
| 215 |
btn.innerText = "Process File";
|
| 216 |
btn.style.background = "#3498db";
|
|
@@ -218,82 +255,23 @@
|
|
| 218 |
}
|
| 219 |
}
|
| 220 |
|
| 221 |
-
// async function sendPathToJulia() {
|
| 222 |
-
// const path = document.getElementById('path-input').value;
|
| 223 |
-
// const btn = document.getElementById('process-btn');
|
| 224 |
-
|
| 225 |
-
// if (!path) {
|
| 226 |
-
// alert("Please provide a file path.");
|
| 227 |
-
// return;
|
| 228 |
-
// }
|
| 229 |
-
|
| 230 |
-
// btn.innerText = "Processing...";
|
| 231 |
-
// btn.style.background = "#6c757d"; // Change to grey
|
| 232 |
-
// btn.style.pointerEvents = "none"; // Prevent double-clicking
|
| 233 |
-
|
| 234 |
-
// try {
|
| 235 |
-
// const response = await fetch('http://localhost:8080/process-path', {
|
| 236 |
-
// method: 'POST',
|
| 237 |
-
// headers: { 'Content-Type': 'application/json' },
|
| 238 |
-
// body: JSON.stringify({ path: path })
|
| 239 |
-
// });
|
| 240 |
-
|
| 241 |
-
// const data = await response.json();
|
| 242 |
-
|
| 243 |
-
// if (data.error) {
|
| 244 |
-
// alert("Julia Error: " + data.error);
|
| 245 |
-
// } else {
|
| 246 |
-
// console.log("Data received:", data);
|
| 247 |
-
// allData = data.trajectories;
|
| 248 |
-
|
| 249 |
-
// // Re-flatten coordinate arrays for grid calculations
|
| 250 |
-
// const allX = allData.flatMap(t => t.x);
|
| 251 |
-
// const allY = allData.flatMap(t => t.y);
|
| 252 |
-
// const allZ = allData.flatMap(t => t.z);
|
| 253 |
-
|
| 254 |
-
// // Define the boundaries of your "box"
|
| 255 |
-
// xMin = allX.reduce((a, b) => Math.min(a, b), Infinity);
|
| 256 |
-
// yMax = allY.reduce((a, b) => Math.max(a, b), -Infinity);
|
| 257 |
-
// zMin = allZ.reduce((a, b) => Math.min(a, b), Infinity);
|
| 258 |
-
|
| 259 |
-
// floorData = computeDensityGrid(allX, allY);
|
| 260 |
-
// wallData = computeDensityGrid(allX, allZ);
|
| 261 |
-
// sideData = computeDensityGrid(allY, allZ);
|
| 262 |
-
|
| 263 |
-
// // 6. Trigger UI
|
| 264 |
-
// document.getElementById('plot-container').style.display = 'block';
|
| 265 |
-
// // updatePlot(document.getElementById('percent').value);
|
| 266 |
-
// //update slider to 2%
|
| 267 |
-
// document.getElementById('percent').value = 2;
|
| 268 |
-
// percentVal.innerText = "2%";
|
| 269 |
-
// updatePlot(2);
|
| 270 |
-
// console.log("Processed tracks:", allData.length);
|
| 271 |
-
|
| 272 |
-
// }
|
| 273 |
-
// } catch (err) {
|
| 274 |
-
// alert("Could not connect to Julia server. Is server.jl running?");
|
| 275 |
-
// console.error(err);
|
| 276 |
-
// } finally {
|
| 277 |
-
// btn.innerText = "Process File";
|
| 278 |
-
// btn.style.background = "#3498db";
|
| 279 |
-
// btn.style.pointerEvents = "auto"; // Re-enable button
|
| 280 |
-
// }
|
| 281 |
-
// }
|
| 282 |
-
|
| 283 |
// Use the variable name you defined in your Julia export
|
| 284 |
|
| 285 |
const plotDiv = document.getElementById('plot-container');
|
| 286 |
-
const slider = document.getElementById('
|
| 287 |
-
const
|
| 288 |
const opacitySlider = document.getElementById('opacity');
|
| 289 |
const opacityVal = document.getElementById('opacity-val');
|
|
|
|
|
|
|
|
|
|
| 290 |
|
| 291 |
-
function computeDensityGrid(allX, allY, dX = 0.2) {
|
| 292 |
-
// 1. Define the grid boundaries
|
| 293 |
-
const xMin = allX.reduce((a, b) => Math.min(a, b), Infinity);
|
| 294 |
-
const xMax = allX.reduce((a, b) => Math.max(a, b), -Infinity);
|
| 295 |
-
const yMin = allY.reduce((a, b) => Math.min(a, b), Infinity);
|
| 296 |
-
const yMax = allY.reduce((a, b) => Math.max(a, b), -Infinity);
|
| 297 |
|
| 298 |
// 2. Calculate number of bins based on fixed dX
|
| 299 |
// We use Math.ceil to ensure we cover the entire range
|
|
@@ -324,17 +302,16 @@
|
|
| 324 |
return { x: xRange, y: yRange, z: density };
|
| 325 |
}
|
| 326 |
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
let currentOpacity = 0.99;
|
| 330 |
-
let heatmapOpacity = 0.0;
|
| 331 |
|
| 332 |
-
function
|
| 333 |
|
| 334 |
if (!allData || allData.length === 0) return;
|
| 335 |
-
|
|
|
|
|
|
|
|
|
|
| 336 |
const plotDiv = document.getElementById('plot-container');
|
| 337 |
-
|
| 338 |
// 1. Capture the CURRENT camera state
|
| 339 |
let currentCamera = null;
|
| 340 |
if (plotDiv && plotDiv.layout && plotDiv.layout.scene) {
|
|
@@ -342,13 +319,10 @@
|
|
| 342 |
}
|
| 343 |
|
| 344 |
// 1. Calculate how many trajectories to show
|
| 345 |
-
const
|
| 346 |
-
const
|
| 347 |
-
|
| 348 |
-
// console.log("X range:", floorData.x[0], "to", floorData.x[floorData.x.length-1]);
|
| 349 |
-
// console.log("Y range:", floorData.y[0], "to", floorData.y[floorData.y.length-1]);
|
| 350 |
|
| 351 |
-
|
| 352 |
// 2. Map data to Plotly traces
|
| 353 |
const traces = subset.map(t => ({
|
| 354 |
type: 'scatter3d',
|
|
@@ -373,7 +347,7 @@
|
|
| 373 |
type: 'surface',
|
| 374 |
x: floorData.x,
|
| 375 |
y: floorData.y,
|
| 376 |
-
z: floorData.y.map(() => floorData.x.map(() =>
|
| 377 |
surfacecolor: floorData.z,
|
| 378 |
colorscale: 'Portland',
|
| 379 |
opacity: heatmapOpacity,
|
|
@@ -393,7 +367,7 @@
|
|
| 393 |
x: wallData.y.map(() => wallData.x),
|
| 394 |
|
| 395 |
// Y-matrix: Every single point in the grid is at the same 'yMax' (the wall)
|
| 396 |
-
y: wallData.z.map(row => row.map(() =>
|
| 397 |
|
| 398 |
// Z-matrix: Create a column for every X-bin
|
| 399 |
z: wallData.y.map(zVal => Array(wallData.x.length).fill(zVal)),
|
|
@@ -409,7 +383,7 @@
|
|
| 409 |
const sideSurface = {
|
| 410 |
type: 'surface',
|
| 411 |
// X-matrix: Every single point in the grid is at the same 'xMin' (the side wall)
|
| 412 |
-
x: sideData.z.map(row => row.map(() =>
|
| 413 |
|
| 414 |
// Y-matrix: Create a row for every Z-bin
|
| 415 |
y: sideData.y.map(() => sideData.x),
|
|
@@ -431,11 +405,11 @@
|
|
| 431 |
camera: currentCamera || {
|
| 432 |
eye: {x: 1.25, y: 1.25, z: 1.25}
|
| 433 |
},
|
| 434 |
-
xaxis: { title: 'X (m)', backgroundcolor: "#f8f9fa", showbackground: true, showspikes: false, showgrid: true, zeroline: false, gridcolor: '#ffffff', gridwidth: 2 },
|
| 435 |
-
yaxis: { title: 'Y (m)', backgroundcolor: "#f8f9fa", showbackground: true, showspikes: false, showgrid: true, zeroline: false, gridcolor: '#ffffff', gridwidth: 2 },
|
| 436 |
-
zaxis: { title: 'Z (m)', backgroundcolor: "#f8f9fa", showbackground: true, showspikes: false, showgrid: true, zeroline: false, gridcolor: '#ffffff', gridwidth: 2 },
|
| 437 |
-
aspectmode: '
|
| 438 |
-
|
| 439 |
},
|
| 440 |
margin: { l: 0, r: 0, b: 0, t: 0 },
|
| 441 |
showlegend: false,
|
|
@@ -455,18 +429,51 @@
|
|
| 455 |
|
| 456 |
// Event Listeners
|
| 457 |
slider.oninput = function() {
|
| 458 |
-
|
| 459 |
};
|
| 460 |
|
| 461 |
slider.onchange = function() {
|
| 462 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
};
|
| 464 |
|
| 465 |
opacitySlider.oninput = function() {
|
| 466 |
currentOpacity = parseFloat(this.value);
|
| 467 |
opacityVal.innerText = currentOpacity;
|
| 468 |
// Trigger a re-render with the new opacity
|
| 469 |
-
|
| 470 |
};
|
| 471 |
|
| 472 |
// 3. Reset View Listener
|
|
@@ -484,14 +491,12 @@
|
|
| 484 |
showHeatmaps = this.checked;
|
| 485 |
// if checked set heatmapOpacity to 0.6 else 0.0
|
| 486 |
heatmapOpacity = showHeatmaps ? 0.6 : 0.0;
|
| 487 |
-
|
| 488 |
-
const currentPercent = document.getElementById('percent').value;
|
| 489 |
-
updatePlot(currentPercent);
|
| 490 |
};
|
| 491 |
|
| 492 |
|
| 493 |
// Initial render
|
| 494 |
-
window.onload = () =>
|
| 495 |
|
| 496 |
</script>
|
| 497 |
</body>
|
|
|
|
| 12 |
top: 0;
|
| 13 |
left: 0;
|
| 14 |
width: 100%;
|
| 15 |
+
height: 100px; /* Fixed height for a slim look */
|
| 16 |
background: rgba(255, 255, 255, 0.95);
|
| 17 |
display: flex;
|
| 18 |
align-items: center;
|
|
|
|
| 25 |
font-size: 13px;
|
| 26 |
}
|
| 27 |
|
| 28 |
+
/* Container for the sliders to stack them */
|
| 29 |
+
.slider-stack {
|
| 30 |
+
display: flex;
|
| 31 |
+
flex-direction: column;
|
| 32 |
+
gap: 15px; /* Space between the two sliders */
|
| 33 |
+
min-width: 180px;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
.control-group {
|
| 37 |
display: flex;
|
| 38 |
flex-direction: column; /* Stack label over slider */
|
|
|
|
| 81 |
/* Adjust your Plotly container to make room for the banner */
|
| 82 |
#plot-container {
|
| 83 |
display: none; /* Hidden until data is loaded */
|
| 84 |
+
margin-top: 100px;
|
| 85 |
+
height: calc(100vh - 100px);
|
| 86 |
}
|
| 87 |
.slider-label { display: flex; justify-content: space-between; margin-bottom: 8px; font-weight: bold; }
|
| 88 |
input[type=range] { width: 100%; }
|
|
|
|
| 91 |
<body>
|
| 92 |
|
| 93 |
<div id="controls" class="top-banner">
|
| 94 |
+
<div class="slider-stack">
|
| 95 |
+
<div class="control-group" style="min-width: 300px; flex-direction: row">
|
| 96 |
+
<input type="file" id="file-selector" style="display: none;" onchange="updateFilename(this)">
|
| 97 |
|
| 98 |
+
<label for="file-selector" id="drop-zone"
|
| 99 |
+
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 8px; border: 1px solid #ccc; border-radius: 4px 4px 4px 4px; background: white; cursor: pointer; color: #666; display: flex; align-items: center; overflow: hidden;">
|
| 100 |
+
<span id="file-name-display">Click to select or Drop CSV...</span>
|
| 101 |
+
</label>
|
| 102 |
+
|
| 103 |
+
<button class="btn" id="process-btn"
|
| 104 |
+
style="background: #007bff; color: white; border: 1px solid #007bff; border-radius: 4px 4px 4px 4px; cursor: pointer; white-space: nowrap; margin-left:6px;"
|
| 105 |
+
onclick="uploadAndProcess()">
|
| 106 |
+
Process File
|
| 107 |
+
</button>
|
| 108 |
+
</div>
|
| 109 |
+
<div class="control-group" style="display: flex; flex-direction: row; gap: 20px; align-items: center; width: 100%;">
|
| 110 |
+
<div class="slider-header" style="min-width: 90px;">
|
| 111 |
+
<span>Start (min):</span>
|
| 112 |
+
<span id="start-val">0</span>
|
| 113 |
+
</div>
|
| 114 |
+
<input type="range" id="start-slider" min="0" max="60" value="0" oninput="document.getElementById('start-val').innerText = this.value">
|
| 115 |
+
|
| 116 |
+
<div class="slider-header" style="min-width: 150px">
|
| 117 |
+
<span>Window (min):</span>
|
| 118 |
+
<input type="number" id="window-input" value="10" min="1" style="width: 45px; border: 1px solid #ccc; border-radius: 3px; font-size: 11px;">
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
</div>
|
| 123 |
|
| 124 |
<div class="control-group">
|
|
|
|
| 130 |
<div class="control-group">
|
| 131 |
<div class="slider-header">
|
| 132 |
<span>Trajectories:</span>
|
| 133 |
+
<span id="trajnum-val">200</span>
|
| 134 |
</div>
|
| 135 |
+
<input type="range" id="trajnum" min="1" max="1000" value="200" step="1">
|
| 136 |
</div>
|
| 137 |
|
| 138 |
+
|
| 139 |
<div class="control-group">
|
| 140 |
<div class="slider-header">
|
| 141 |
<span>Opacity:</span>
|
|
|
|
| 144 |
<input type="range" id="opacity" min="0" max="0.99" step="0.01" value="0.99">
|
| 145 |
</div>
|
| 146 |
|
| 147 |
+
<div class="control-group" style="max-width: 100px; min-width: 100px">
|
| 148 |
+
<button id="reset-view" style="width: 100%">Reset View</button>
|
| 149 |
</div>
|
| 150 |
|
| 151 |
<div class="info-text">
|
|
|
|
| 160 |
<script>
|
| 161 |
|
| 162 |
let allData = [];
|
| 163 |
+
let selectData = [];
|
| 164 |
+
let xMinAll, xMaxAll, yMinAll, yMaxAll, zMinAll, zMaxAll;
|
| 165 |
let floorData, wallData, sideData;
|
| 166 |
+
let currentOpacity = 0.99;
|
| 167 |
+
let heatmapOpacity = 0.0;
|
| 168 |
|
| 169 |
function handleDrop(event) {
|
| 170 |
event.preventDefault();
|
|
|
|
| 218 |
const allZ = allData.flatMap(t => t.z);
|
| 219 |
|
| 220 |
// Define the boundaries of your "box"
|
| 221 |
+
xMinAll = allX.reduce((a, b) => Math.min(a, b), Infinity);
|
| 222 |
+
xMaxAll = allX.reduce((a, b) => Math.max(a, b), -Infinity);
|
| 223 |
+
yMinAll = allY.reduce((a, b) => Math.min(a, b), Infinity);
|
| 224 |
+
yMaxAll = allY.reduce((a, b) => Math.max(a, b), -Infinity);
|
| 225 |
+
zMinAll = allZ.reduce((a, b) => Math.min(a, b), Infinity);
|
| 226 |
+
zMaxAll = allZ.reduce((a, b) => Math.max(a, b), -Infinity);
|
| 227 |
+
// floorData = computeDensityGrid(allX, allY);
|
| 228 |
+
// wallData = computeDensityGrid(allX, allZ);
|
| 229 |
+
// sideData = computeDensityGrid(allY, allZ);
|
| 230 |
|
| 231 |
// 6. Trigger UI
|
| 232 |
document.getElementById('plot-container').style.display = 'block';
|
| 233 |
+
//render
|
| 234 |
+
const star_min = document.getElementById('start-slider').value;
|
| 235 |
+
const window_min = document.getElementById('window-input').value;
|
| 236 |
+
|
| 237 |
+
selectData = allData.filter(t => t.start_time >= parseFloat(star_min) * 60 && t.start_time < (parseFloat(star_min) + parseFloat(window_min)) * 60);
|
| 238 |
+
const selectX = selectData.flatMap(t => t.x);
|
| 239 |
+
const selectY = selectData.flatMap(t => t.y);
|
| 240 |
+
const selectZ = selectData.flatMap(t => t.z);
|
| 241 |
+
floorData = computeDensityGrid(selectX, selectY, xMinAll, xMaxAll, yMinAll, yMaxAll);
|
| 242 |
+
wallData = computeDensityGrid(selectX, selectZ, xMinAll, xMaxAll, zMinAll, zMaxAll);
|
| 243 |
+
sideData = computeDensityGrid(selectY, selectZ, yMinAll, yMaxAll, zMinAll, zMaxAll);
|
| 244 |
+
|
| 245 |
+
renderCurrentState();
|
| 246 |
+
|
| 247 |
}
|
| 248 |
} catch (err) {
|
| 249 |
+
alert("Could not connect to Julia server. Is server.jl running?");
|
| 250 |
console.error(err);
|
|
|
|
| 251 |
} finally {
|
| 252 |
btn.innerText = "Process File";
|
| 253 |
btn.style.background = "#3498db";
|
|
|
|
| 255 |
}
|
| 256 |
}
|
| 257 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
// Use the variable name you defined in your Julia export
|
| 259 |
|
| 260 |
const plotDiv = document.getElementById('plot-container');
|
| 261 |
+
const slider = document.getElementById('trajnum');
|
| 262 |
+
const trajVal = document.getElementById('trajnum-val');
|
| 263 |
const opacitySlider = document.getElementById('opacity');
|
| 264 |
const opacityVal = document.getElementById('opacity-val');
|
| 265 |
+
const startMinSlider = document.getElementById('start-slider');
|
| 266 |
+
const windowMinInput = document.getElementById('window-input');
|
| 267 |
+
const startMinVal = document.getElementById('start-val');
|
| 268 |
|
| 269 |
+
function computeDensityGrid(allX, allY, xMin, xMax, yMin, yMax, dX = 0.2) {
|
| 270 |
+
// // 1. Define the grid boundaries
|
| 271 |
+
// const xMin = allX.reduce((a, b) => Math.min(a, b), Infinity);
|
| 272 |
+
// const xMax = allX.reduce((a, b) => Math.max(a, b), -Infinity);
|
| 273 |
+
// const yMin = allY.reduce((a, b) => Math.min(a, b), Infinity);
|
| 274 |
+
// const yMax = allY.reduce((a, b) => Math.max(a, b), -Infinity);
|
| 275 |
|
| 276 |
// 2. Calculate number of bins based on fixed dX
|
| 277 |
// We use Math.ceil to ensure we cover the entire range
|
|
|
|
| 302 |
return { x: xRange, y: yRange, z: density };
|
| 303 |
}
|
| 304 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
|
| 306 |
+
function renderCurrentState() {
|
| 307 |
|
| 308 |
if (!allData || allData.length === 0) return;
|
| 309 |
+
if (!selectData || selectData.length === 0) {
|
| 310 |
+
alert("No trajectories found in the specified time window.");
|
| 311 |
+
return;
|
| 312 |
+
}
|
| 313 |
const plotDiv = document.getElementById('plot-container');
|
| 314 |
+
|
| 315 |
// 1. Capture the CURRENT camera state
|
| 316 |
let currentCamera = null;
|
| 317 |
if (plotDiv && plotDiv.layout && plotDiv.layout.scene) {
|
|
|
|
| 319 |
}
|
| 320 |
|
| 321 |
// 1. Calculate how many trajectories to show
|
| 322 |
+
const trajnum = parseInt(document.getElementById('trajnum').value);
|
| 323 |
+
const totalToDisplay = Math.min(trajnum, selectData.length);
|
| 324 |
+
const subset = selectData.slice(0, totalToDisplay);
|
|
|
|
|
|
|
| 325 |
|
|
|
|
| 326 |
// 2. Map data to Plotly traces
|
| 327 |
const traces = subset.map(t => ({
|
| 328 |
type: 'scatter3d',
|
|
|
|
| 347 |
type: 'surface',
|
| 348 |
x: floorData.x,
|
| 349 |
y: floorData.y,
|
| 350 |
+
z: floorData.y.map(() => floorData.x.map(() => zMinAll - 0.1)), // Constant Z
|
| 351 |
surfacecolor: floorData.z,
|
| 352 |
colorscale: 'Portland',
|
| 353 |
opacity: heatmapOpacity,
|
|
|
|
| 367 |
x: wallData.y.map(() => wallData.x),
|
| 368 |
|
| 369 |
// Y-matrix: Every single point in the grid is at the same 'yMax' (the wall)
|
| 370 |
+
y: wallData.z.map(row => row.map(() => yMaxAll + 0.2)),
|
| 371 |
|
| 372 |
// Z-matrix: Create a column for every X-bin
|
| 373 |
z: wallData.y.map(zVal => Array(wallData.x.length).fill(zVal)),
|
|
|
|
| 383 |
const sideSurface = {
|
| 384 |
type: 'surface',
|
| 385 |
// X-matrix: Every single point in the grid is at the same 'xMin' (the side wall)
|
| 386 |
+
x: sideData.z.map(row => row.map(() => xMinAll - 0.1)),
|
| 387 |
|
| 388 |
// Y-matrix: Create a row for every Z-bin
|
| 389 |
y: sideData.y.map(() => sideData.x),
|
|
|
|
| 405 |
camera: currentCamera || {
|
| 406 |
eye: {x: 1.25, y: 1.25, z: 1.25}
|
| 407 |
},
|
| 408 |
+
xaxis: { title: 'X (m)', range: [xMinAll - 0.2, xMaxAll], backgroundcolor: "#f8f9fa", showbackground: true, showspikes: false, showgrid: true, zeroline: false, gridcolor: '#ffffff', gridwidth: 2 },
|
| 409 |
+
yaxis: { title: 'Y (m)', range: [yMinAll, yMaxAll + 0.3], backgroundcolor: "#f8f9fa", showbackground: true, showspikes: false, showgrid: true, zeroline: false, gridcolor: '#ffffff', gridwidth: 2 },
|
| 410 |
+
zaxis: { title: 'Z (m)', range: [zMinAll - 0.2, zMaxAll], backgroundcolor: "#f8f9fa", showbackground: true, showspikes: false, showgrid: true, zeroline: false, gridcolor: '#ffffff', gridwidth: 2 },
|
| 411 |
+
aspectmode: 'manual', // Ensures 1:1:1 spatial scale
|
| 412 |
+
aspectratio: {x: 1, z: (zMaxAll - zMinAll + 0.2) / (xMaxAll - xMinAll + 0.2), y: (yMaxAll - yMinAll + 0.3) / (xMaxAll - xMinAll + 0.2)}
|
| 413 |
},
|
| 414 |
margin: { l: 0, r: 0, b: 0, t: 0 },
|
| 415 |
showlegend: false,
|
|
|
|
| 429 |
|
| 430 |
// Event Listeners
|
| 431 |
slider.oninput = function() {
|
| 432 |
+
trajVal.innerText = this.value;
|
| 433 |
};
|
| 434 |
|
| 435 |
slider.onchange = function() {
|
| 436 |
+
renderCurrentState();
|
| 437 |
+
};
|
| 438 |
+
|
| 439 |
+
startMinSlider.oninput = function() {
|
| 440 |
+
startMinVal.innerText = this.value;
|
| 441 |
+
|
| 442 |
+
const star_min = this.value;
|
| 443 |
+
const window_min = document.getElementById('window-input').value;
|
| 444 |
+
|
| 445 |
+
selectData = allData.filter(t => t.start_time >= parseFloat(star_min) * 60 && t.start_time < (parseFloat(star_min) + parseFloat(window_min)) * 60);
|
| 446 |
+
const selectX = selectData.flatMap(t => t.x);
|
| 447 |
+
const selectY = selectData.flatMap(t => t.y);
|
| 448 |
+
const selectZ = selectData.flatMap(t => t.z);
|
| 449 |
+
floorData = computeDensityGrid(selectX, selectY, xMinAll, xMaxAll, yMinAll, yMaxAll);
|
| 450 |
+
wallData = computeDensityGrid(selectX, selectZ, xMinAll, xMaxAll, zMinAll, zMaxAll);
|
| 451 |
+
sideData = computeDensityGrid(selectY, selectZ, yMinAll, yMaxAll, zMinAll, zMaxAll);
|
| 452 |
+
|
| 453 |
+
renderCurrentState();
|
| 454 |
+
};
|
| 455 |
+
|
| 456 |
+
windowMinInput.onchange = function() {
|
| 457 |
+
|
| 458 |
+
const star_min = document.getElementById('start-slider').value;
|
| 459 |
+
const window_min = this.value;
|
| 460 |
+
|
| 461 |
+
selectData = allData.filter(t => t.start_time >= parseFloat(star_min) * 60 && t.start_time < (parseFloat(star_min) + parseFloat(window_min)) * 60);
|
| 462 |
+
const selectX = selectData.flatMap(t => t.x);
|
| 463 |
+
const selectY = selectData.flatMap(t => t.y);
|
| 464 |
+
const selectZ = selectData.flatMap(t => t.z);
|
| 465 |
+
floorData = computeDensityGrid(selectX, selectY, xMinAll, xMaxAll, yMinAll, yMaxAll);
|
| 466 |
+
wallData = computeDensityGrid(selectX, selectZ, xMinAll, xMaxAll, zMinAll, zMaxAll);
|
| 467 |
+
sideData = computeDensityGrid(selectY, selectZ, yMinAll, yMaxAll, zMinAll, zMaxAll);
|
| 468 |
+
|
| 469 |
+
renderCurrentState();
|
| 470 |
};
|
| 471 |
|
| 472 |
opacitySlider.oninput = function() {
|
| 473 |
currentOpacity = parseFloat(this.value);
|
| 474 |
opacityVal.innerText = currentOpacity;
|
| 475 |
// Trigger a re-render with the new opacity
|
| 476 |
+
renderCurrentState();
|
| 477 |
};
|
| 478 |
|
| 479 |
// 3. Reset View Listener
|
|
|
|
| 491 |
showHeatmaps = this.checked;
|
| 492 |
// if checked set heatmapOpacity to 0.6 else 0.0
|
| 493 |
heatmapOpacity = showHeatmaps ? 0.6 : 0.0;
|
| 494 |
+
renderCurrentState();
|
|
|
|
|
|
|
| 495 |
};
|
| 496 |
|
| 497 |
|
| 498 |
// Initial render
|
| 499 |
+
window.onload = () => renderCurrentState(); // Default to showing 2% of trajectories with start_min=0 and window_min=10
|
| 500 |
|
| 501 |
</script>
|
| 502 |
</body>
|