import { McapIndexedReader } from '@mcap/core';
import { BlobReadable } from '@mcap/browser';
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const status = document.getElementById('status');
const results = document.getElementById('results');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
if (!dropZone.classList.contains('disabled')) {
dropZone.style.backgroundColor = '#f0f0f0';
}
});
dropZone.addEventListener('dragleave', () => {
dropZone.style.backgroundColor = 'white';
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
if (dropZone.classList.contains('disabled')) {
return;
}
dropZone.style.backgroundColor = 'white';
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFile(files[0]);
}
});
dropZone.addEventListener('click', () => {
if (!dropZone.classList.contains('disabled')) {
fileInput.click();
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFile(e.target.files[0]);
}
});
function disableDropZone() {
dropZone.classList.add('disabled');
dropZone.innerHTML = '⏳ Processing file... Please wait';
}
function enableDropZone() {
dropZone.classList.remove('disabled');
dropZone.innerHTML = '📁 Drag your rosbag here or click to select file<input type="file" id="fileInput" accept=".mcap" style="display: none;">';
const newFileInput = document.getElementById('fileInput');
newFileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFile(e.target.files[0]);
}
});
}
async function handleFile(file) {
if (!file.name.endsWith('.mcap')) {
status.innerHTML = 'Error: Please select an MCAP file';
return;
}
disableDropZone();
status.innerHTML = 'Analyzing file...';
results.innerHTML = '';
try {
const reader = await McapIndexedReader.Initialize({
readable: new BlobReadable(file),
});
await analyzeFile(reader, file);
} catch (error) {
console.error('Error:', error);
status.innerHTML = 'Error reading MCAP file: ' + error.message;
} finally {
enableDropZone();
}
}
async function analyzeFile(reader, file) {
const targetSchema = 'sensor_msgs/msg/PointCloud2';
let foundChannels = [];
let totalChannels = 0;
const allSchemas = new Set();
status.innerHTML = 'Loading WASM module...';
let wasmModule;
try {
const script = document.createElement('script');
script.src = '/cloudini_wasm.js';
await new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
wasmModule = await CloudiniModule();
console.log('WASM module loaded. Available properties:', Object.keys(wasmModule));
console.log('Has HEAPU8:', !!wasmModule.HEAPU8);
console.log('Has ccall:', !!wasmModule.ccall);
console.log('Has _malloc:', !!wasmModule._malloc);
console.log('Has _free:', !!wasmModule._free);
console.log('Functions starting with _:', Object.keys(wasmModule).filter(k => k.startsWith('_')));
} catch (error) {
console.error('Failed to load WASM module:', error);
status.innerHTML = 'Error loading WASM module: ' + error.message;
return;
}
for (const [channelId, channel] of reader.channelsById) {
totalChannels++;
const schema = reader.schemasById.get(channel.schemaId);
if (schema) {
allSchemas.add(schema.name);
if (schema.name === targetSchema) {
foundChannels.push({
channelId,
topic: channel.topic,
schema: schema.name,
encoding: schema.encoding
});
}
}
}
status.innerHTML = `Processing PointCloud2 channels inside the rosbag...`;
if (foundChannels.length > 0) {
const channelResults = [];
for (const channel of foundChannels) {
let messageCount = 0;
let totalSize = 0;
let totalCompressedSize = 0;
for await (const message of reader.readMessages({
startTime: reader.start,
endTime: reader.end,
topics: [channel.topic]
})) {
messageCount++;
const dataSize = message.data.length;
totalSize += dataSize;
try {
const dataView = new Uint8Array(message.data);
if (wasmModule.HEAPU8) {
const maxAllowedSize = wasmModule.HEAPU8.length / 4; if (dataSize > maxAllowedSize) {
console.warn(`Message too large (${dataSize} bytes > ${maxAllowedSize}), skipping...`);
continue;
}
}
let compressedSize;
if (wasmModule._malloc && wasmModule._free && wasmModule.HEAPU8) {
const dataPtr = wasmModule._malloc(dataSize);
const wasmView = new Uint8Array(wasmModule.HEAPU8.buffer, dataPtr, dataSize);
wasmView.set(dataView);
compressedSize = wasmModule._cldn_ComputeCompressedSize(dataPtr, dataSize, 0.001);
wasmModule._free(dataPtr);
} else {
compressedSize = wasmModule.ccall(
'cldn_ComputeCompressedSize',
'number',
['array', 'number', 'number'],
[dataView, dataSize, 0.001]
);
}
totalCompressedSize += compressedSize;
} catch (error) {
console.error('Error calling WASM function:', error);
console.error('Data size:', dataSize);
console.error('WASM memory size:', wasmModule.HEAPU8?.length || 'unknown');
}
}
channelResults.push({
...channel,
messageCount,
totalSize,
totalCompressedSize,
compressionRatio: totalSize > 0 ? (totalCompressedSize / totalSize).toFixed(3) : 0
});
}
status.innerHTML = `File: ${file.name} | Channels: ${totalChannels} | Schemas: ${allSchemas.size}`;
const grandTotalSize = channelResults.reduce((sum, ch) => sum + ch.totalSize, 0);
const grandTotalCompressed = channelResults.reduce((sum, ch) => sum + ch.totalCompressedSize, 0);
const grandCompressionRatio = grandTotalSize > 0 ? (grandTotalCompressed / grandTotalSize).toFixed(3) : 0;
results.innerHTML = `
<div class="results-container">
<h3 class="results-title">
✅ Found ${foundChannels.length} PointCloud2 Channel${foundChannels.length !== 1 ? 's' : ''}
</h3>
<div class="channels-grid">
${channelResults.map(ch =>
`<div class="channel-card">
<div class="channel-content">
<div>
<div class="channel-topic">${ch.topic}</div>
<div class="channel-details">
<div><strong>Schema:</strong> ${ch.schema}</div>
<div><strong>Encoding:</strong> ${ch.encoding}</div>
<div><strong>Channel ID:</strong> ${ch.channelId}</div>
<div><strong>Messages:</strong> <span class="message-count">${ch.messageCount.toLocaleString()}</span></div>
</div>
</div>
</div>
</div>`
).join('')}
</div>
<div class="compression-analysis">
<h3 class="compression-title">📊 Compression Analysis</h3>
<div class="compression-note">
This includes ONLY pointclouds, other messages in the rosbag are ignored.</div>
<div class="compression-quantization">
Quantization used: 1 millimeter</div>
<div class="compression-stats">
<div class="stat-card">
<div class="stat-label">Original Size (uncompressed)</div>
<div class="stat-value">${(grandTotalSize / (1024 * 1024)).toFixed(1)} MB</div>
</div>
<div class="stat-card">
<div class="stat-label">Compressed Size</div>
<div class="stat-value">${(grandTotalCompressed / (1024 * 1024)).toFixed(1)} MB</div>
</div>
<div class="stat-card">
<div class="stat-label">Compression Ratio</div>
<div class="stat-value">${grandCompressionRatio}</div>
</div>
</div>
</div>
</div>
`;
} else {
status.innerHTML = `File: ${file.name} | Channels: ${totalChannels} | Schemas: ${allSchemas.size}`;
results.innerHTML = `
<div class="no-results">
<div class="no-results-card">
<div class="no-results-icon">🔍</div>
<h3 class="no-results-title">No PointCloud2 Channels Found</h3>
<p class="no-results-text">This MCAP file doesn't contain any sensor_msgs/msg/PointCloud2 data.</p>
</div>
<div class="schemas-info">
<h4 class="schemas-title">📋 Available Schemas in this file:</h4>
<div class="schemas-list">
${Array.from(allSchemas).sort().map(schema =>
`<span class="schema-tag">${schema}</span>`
).join('')}
</div>
</div>
</div>
`;
}
}