diff --git a/VICAR-frame-reader.html b/VICAR-frame-reader.html
new file mode 100644
index 0000000..397eda1
--- /dev/null
+++ b/VICAR-frame-reader.html
@@ -0,0 +1,198 @@
+
+
+
+
+ Coordinate System Parser
+
+
+
+ Caricamento Coordinate
+
+
+
+
+
+ Nome File |
+ MAST_FRAME |
+ MAST Yaw |
+ MAST Pitch |
+ MAST Roll |
+ ROVER_FRAME |
+ ROVER Yaw |
+ ROVER Pitch |
+ ROVER Roll |
+ MAST Delta Yaw |
+ MAST Delta Pitch |
+ MAST Delta Roll |
+ ROVER Delta Yaw |
+ ROVER Delta Pitch |
+ ROVER Delta Roll |
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/VST2X3D-AI.js b/VST2X3D-AI.js
new file mode 100644
index 0000000..b61559a
--- /dev/null
+++ b/VST2X3D-AI.js
@@ -0,0 +1,1228 @@
+BASE_IMG_URL_NAVCAM = "https://planetarydata.jpl.nasa.gov/w10n/mer2-m-navcam-5-disparity-ops-v1.0/mer2no_0xxx/";
+BASE_IMG_URL_PANCAM = "https://planetarydata.jpl.nasa.gov/w10n/mer2-m-pancam-5-disparity-ops-v1.0/mer2po_0xxx/";
+IMG_RAW_FOLDER = "data/";
+IMG_JPG_FOLDER = "browse/";
+PRODUCT_FOLDER = "sol#SOLNUMBER#/rdr/"; // sol must be determined from VST // DEBUG!! (Sol number is in .lbl of .vst file)
+BASE_VST_URL = "https://planetarydata.jpl.nasa.gov/img/data/mer/spirit/mer2mw_0xxx/";
+VST_CAMERA_FOLDER = "data/navcam/"; // (navcam --> mer2no_0xxx for images) // DEBUG: add other cameras!
+VST_FOLDER_SITE_NAME = "site0137/"; // DEBUG! Site number is in filename (137=B1)
+product_name_no_ext = "2n292378085vilb128f0006l0m1"; // 2n292377885vilb126f0006l0m1 = small vst for testing, sol 1870 // DEBUG
+// 2n292377885vilb126f0006l0m1
+// 2n292378085vilb128f0006l0m1
+base_VST_filename = BASE_VST_URL + VST_CAMERA_FOLDER + VST_FOLDER_SITE_NAME + product_name_no_ext;
+
+let angles = 0;
+let x3d = null;
+wireframeEnabled = false;
+
+ proxyURL = "https://win98.altervista.org/space/exploration/myp.php?pass=miapass&mode=native&url=";
+
+x3dShapes = [];
+rotatedShapes = [];
+
+const graphicalProducts = [
+ "RAD",
+ "RAL",
+ "RSL",
+ "EFF",
+ "ESF",
+ "EDN",
+ "ILF",
+ "ILL",
+ "FFL",
+ "MRD",
+ "MRL",
+ "RFD",
+ "RFL",
+ "IOF",
+ "IOL",
+ "IFF",
+ "IFL",
+ "IFS",
+ "CCD",
+ "CCL",
+ "CFD",
+ "CFL",
+ "ITH",
+ "THN"
+];
+
+
+ // Strutture dati VST
+ class VSTHeader {
+ constructor(dataView, offset = 0) {
+ this.label = dataView.getInt32(offset, true);
+ this.byteOrder = dataView.getInt32(offset + 4, true);
+ this.versionMajor = dataView.getInt32(offset + 8, true);
+ this.versionMinor = dataView.getInt32(offset + 12, true);
+ this.implementation = dataView.getInt32(offset + 16, true);
+ this.res1 = dataView.getInt32(offset + 20, true);
+ this.res2 = dataView.getInt32(offset + 24, true);
+ this.textureNum = dataView.getInt32(offset + 28, true);
+ this.vertexNum = dataView.getInt32(offset + 32, true);
+ this.lodNum = dataView.getInt32(offset + 36, true);
+
+ // Gestione byte order
+ if (this.byteOrder === 0x00010203) {
+ this.reverseValues();
+ }
+ }
+
+ reverseValues() {
+ this.versionMajor = this.reverseInt32(this.versionMajor);
+ this.versionMinor = this.reverseInt32(this.versionMinor);
+ this.textureNum = this.reverseInt32(this.textureNum);
+ this.vertexNum = this.reverseInt32(this.vertexNum);
+ this.lodNum = this.reverseInt32(this.lodNum);
+ }
+
+ reverseInt32(value) {
+ return ((value & 0xFF) << 24) |
+ ((value & 0xFF00) << 8) |
+ ((value & 0xFF0000) >>> 8) |
+ ((value & 0xFF000000) >>> 24);
+ }
+ }
+
+ class BoundingBox {
+ constructor(dataView, offset = 0, needsReverse = false) {
+ this.xMin = dataView.getFloat32(offset, true);
+ this.yMin = dataView.getFloat32(offset + 4, true);
+ this.zMin = dataView.getFloat32(offset + 8, true);
+ this.xMax = dataView.getFloat32(offset + 12, true);
+ this.yMax = dataView.getFloat32(offset + 16, true);
+ this.zMax = dataView.getFloat32(offset + 20, true);
+
+ if (needsReverse) {
+ this.reverseValues();
+ }
+ }
+
+ reverseValues() {
+ [this.xMin, this.yMin, this.zMin, this.xMax, this.yMax, this.zMax] =
+ [this.xMin, this.yMin, this.zMin, this.xMax, this.yMax, this.zMax].map(this.reverseFloat32);
+ }
+
+ reverseFloat32(value) {
+ const buffer = new ArrayBuffer(4);
+ const view = new DataView(buffer);
+ view.setFloat32(0, value, true);
+ return view.getFloat32(0, false);
+ }
+ }
+
+ class Vertex {
+ constructor(dataView, offset = 0, needsReverse = false) {
+ this.tx = dataView.getFloat32(offset, true);
+ this.ty = dataView.getFloat32(offset + 4, true);
+ this.x = dataView.getFloat32(offset + 8, true);
+ this.y = dataView.getFloat32(offset + 12, true);
+ this.z = dataView.getFloat32(offset + 16, true);
+
+ if (needsReverse) {
+ this.reverseValues();
+ }
+ }
+
+ reverseValues() {
+ [this.tx, this.ty, this.x, this.y, this.z] =
+ [this.tx, this.ty, this.x, this.y, this.z].map(this.reverseFloat32);
+ }
+
+ reverseFloat32(value) {
+ const buffer = new ArrayBuffer(4);
+ const view = new DataView(buffer);
+ view.setFloat32(0, value, true);
+ return view.getFloat32(0, false);
+ }
+ }
+
+ class LODHeader {
+ constructor(dataView, offset = 0, needsReverse = false) {
+ this.size = dataView.getInt32(offset, true);
+ this.res1 = dataView.getInt32(offset + 4, true);
+ this.res2 = dataView.getInt32(offset + 8, true);
+ this.vertexNum = dataView.getInt32(offset + 12, true);
+ this.distThreshold = dataView.getFloat32(offset + 16, true);
+ this.patchNum = dataView.getInt32(offset + 20, true);
+ this.vertMax = dataView.getInt32(offset + 24, true);
+
+ if (needsReverse) {
+ this.reverseValues();
+ }
+ }
+
+ reverseValues() {
+ this.patchNum = this.reverseInt32(this.patchNum);
+ this.vertexNum = this.reverseInt32(this.vertexNum);
+ this.vertMax = this.reverseInt32(this.vertMax);
+ this.distThreshold = this.reverseFloat32(this.distThreshold);
+ }
+
+ reverseInt32(value) {
+ return ((value & 0xFF) << 24) |
+ ((value & 0xFF00) << 8) |
+ ((value & 0xFF0000) >>> 8) |
+ ((value & 0xFF000000) >>> 24);
+ }
+
+ reverseFloat32(value) {
+ const buffer = new ArrayBuffer(4);
+ const view = new DataView(buffer);
+ view.setFloat32(0, value, true);
+ return view.getFloat32(0, false);
+ }
+ }
+
+ class PatchHeader {
+ constructor(dataView, offset = 0, needsReverse = false) {
+ this.res1 = dataView.getInt32(offset, true);
+ this.res2 = dataView.getInt32(offset + 4, true);
+ this.pointCloud = dataView.getInt32(offset + 8, true);
+ this.texture = dataView.getInt32(offset + 12, true);
+ this.arrayNum = dataView.getInt32(offset + 16, true);
+ this.totalVertexNum = dataView.getInt32(offset + 20, true);
+
+ if (needsReverse) {
+ this.reverseValues();
+ }
+ }
+
+ reverseValues() {
+ this.pointCloud = this.reverseInt32(this.pointCloud);
+ this.arrayNum = this.reverseInt32(this.arrayNum);
+ this.texture = this.reverseInt32(this.texture);
+ this.totalVertexNum = this.reverseInt32(this.totalVertexNum);
+ }
+
+ reverseInt32(value) {
+ return ((value & 0xFF) << 24) |
+ ((value & 0xFF00) << 8) |
+ ((value & 0xFF0000) >>> 8) |
+ ((value & 0xFF000000) >>> 24);
+ }
+ }
+
+ class VSTParser {
+ constructor(arrayBuffer) {
+ this.dataView = new DataView(arrayBuffer);
+ this.offset = 0;
+ this.textureFiles = [];
+ this.vertices = [];
+ }
+
+//////
+
+async parse() {
+ console.log("HEADER - Started parsing...");
+
+ const fileProgressBar = document.getElementById('fileProgressBar');
+ fileProgressBar.value = 0; // Resetta la barra di progresso
+ const totalSteps = 8; // Numero totale di passi significativi (puoi aggiungerne altri)
+
+ let currentStep = 0;
+ const advanceProgress = async (stepMessage) => {
+ currentStep++;
+ fileProgressBar.value = Math.round((currentStep / totalSteps) * 100);
+ await new Promise(resolve => setTimeout(resolve, 0)); // Permetti aggiornamenti in tempo reale
+ };
+
+
+
+ try {
+ // Parse header
+ const header = new VSTHeader(this.dataView, this.offset);
+ this.offset += 40; // Size of VSTHeader
+
+ // Validate VST format
+ if (header.label !== 0x00545356) throw new Error('Invalid VST file format');
+ if (header.implementation !== 0x0052454D) throw new Error('Implementation MER expected');
+await advanceProgress("HEADER2 - Header parsed");
+
+ const needsReverse = header.byteOrder === 0x00010203;
+
+ // Parse bounding box
+ const bbox = new BoundingBox(this.dataView, this.offset, needsReverse);
+ this.offset += 24; // Size of BoundingBox
+
+ // Parse texture references
+ for (let i = 0; i < header.textureNum; i++) {
+ const textureBytes = new Uint8Array(this.dataView.buffer, this.offset, 2048);
+ const textureRef = String.fromCharCode.apply(null, textureBytes);
+
+ let textureName = textureRef.substring(10, 37) + '.img.jpg';
+ let textureUrlLeft = textureName.substring(0, 11);
+ let textureUrlRight = textureName.substring(14);
+ let siteNumberCoded = textureName.substr(14, 2);
+ let siteNumberString = "site0" + siteCodeToString(siteNumberCoded);
+ let solNumber = await getSolNumberFromLabel(txtProductId.value + ".lbl", siteNumberString);
+console.log("SOLNUMBER=",solNumber);
+ angles = await getAltAzFromImgProduct("dummy", siteNumberString, solNumber);
+
+ let textureBASE64 = "error";
+ for (let productIndex = 0; (( productIndex < graphicalProducts.length) && (textureBASE64 === "error")); productIndex++) {
+ let textureUrlProduct = graphicalProducts[productIndex];
+ textureName = textureName.substring(0, 26 ) + "1" + textureName.substring(27); // Force version 1 for IMG product
+
+ const secondChar = textureName.charAt(1).toLowerCase();
+ // Imposta VST_CAMERA_FOLDER in base al secondo carattere
+ if (secondChar === 'n') {
+ BASE_IMG_URL = BASE_IMG_URL_NAVCAM
+ } else if (secondChar === 'p') {
+ BASE_IMG_URL = BASE_IMG_URL_PANCAM
+ } else {
+ throw new Error("Carattere non riconosciuto '" + secondChar + "' per determinare la cartella della fotocamera."); // Gestione errore
+ }
+
+
+ let base_texture_folder = BASE_IMG_URL + IMG_JPG_FOLDER + PRODUCT_FOLDER;
+ let base_texture_folderNew = base_texture_folder.replace("#SOLNUMBER#",solNumber);
+ let textureUrl = base_texture_folderNew + textureUrlLeft + textureUrlProduct + textureUrlRight;
+ textureUrl = textureUrl.toLowerCase();
+ textureBASE64 = await urlToBase64(textureUrl);
+ if (textureBASE64 !== "error") {
+ //
+ }
+ };
+
+ this.textureFiles.push(textureBASE64 || null);
+
+ this.offset += 2048;
+ }
+await advanceProgress("HEADER2 - Textures loaded");
+
+ const coords = {
+ // Memorizza come stringa
+ coordString: (() => {
+ const bytes = new Uint8Array(this.dataView.buffer, this.offset, 4096);
+ return String.fromCharCode.apply(null, bytes);
+ })(),
+
+ // Memorizza come array di byte
+ coordArray: new Uint8Array(this.dataView.buffer, this.offset, 4096),
+
+ // Memorizza come array di float32
+ coordFloat: (() => {
+ const floatArray = [];
+ const float32View = new Float32Array(1);
+ const uint8View = new Uint8Array(float32View.buffer);
+
+ for (let i = 0; i < 4096; i += 4) {
+ // Copia 4 byte alla volta nel buffer temporaneo
+ for (let j = 0; j < 4; j++) {
+ uint8View[j] = this.dataView.getUint8(this.offset + i + j);
+ }
+ // Aggiungi il float32 risultante all'array
+ floatArray.push(float32View[0]);
+ }
+ return floatArray;
+ })(),
+
+ // Memorizza come array di interi a 32 bit
+ coordInt32: (() => {
+ const int32Array = [];
+
+ for (let i = 0; i < 4096; i += 4) {
+ // Leggi un intero a 32 bit
+ const int32 = this.dataView.getInt32(this.offset + i, false); // false per big endian
+ int32Array.push(int32);
+ }
+ return int32Array;
+ })()
+ };
+ this.offset += 4096;
+
+ // Read vertices
+ for (let i = 0; i < header.vertexNum; i++) {
+ const vertex = new Vertex(this.dataView, this.offset, needsReverse);
+ this.vertices.push(vertex);
+ this.offset += 20;
+ }
+await advanceProgress("HEADER2 - Vertex loaded");
+
+ // Process LODs
+ const lods = [];
+ for (let i = 0; i < header.lodNum; i++) {
+ const lod = this.parseLOD(needsReverse);
+ lods.push(lod);
+await advanceProgress("HEADER2 - LOD processed");
+ }
+
+ fileProgressBar.value = 100; // Parsing completato
+
+ return {
+ header,
+ bbox,
+ textureFiles: this.textureFiles,
+ vertices: this.vertices,
+ lods,
+ coordinateSystem : coords
+ };
+ } catch (error) {
+ console.error("Errore durante il parsing:", error);
+ fileProgressBar.value = 0; // Reset in caso di errore
+ throw error;
+ }
+}
+
+
+
+
+//////
+
+
+
+ parseLOD(needsReverse) {
+ const lodHeader = new LODHeader(this.dataView, this.offset, needsReverse);
+ this.offset += 28; // Size of LODHeader
+
+ // Skip texture bounding boxes
+ this.offset += 24 * this.textureFiles.length;
+
+ const patches = [];
+ for (let i = 0; i < lodHeader.patchNum; i++) {
+ const patch = this.parsePatch(needsReverse);
+ patches.push(patch);
+ }
+
+ return {
+ header: lodHeader,
+ patches
+ };
+ }
+
+ parsePatch(needsReverse) {
+ const patchHeader = new PatchHeader(this.dataView, this.offset, needsReverse);
+ this.offset += 24; // Size of PatchHeader
+
+ const arrays = [];
+ let pos1 = this.offset;
+ let pos2 = pos1 + patchHeader.arrayNum * 4;
+
+ for (let i = 0; i < patchHeader.arrayNum; i++) {
+ // Save current position
+ const currentPos = this.offset;
+
+ // Read array length
+ this.offset = pos1;
+ let arrayLen = this.dataView.getInt32(this.offset, true);
+ if (needsReverse) {
+ arrayLen = ((arrayLen & 0xFF) << 24) |
+ ((arrayLen & 0xFF00) << 8) |
+ ((arrayLen & 0xFF0000) >>> 8) |
+ ((arrayLen & 0xFF000000) >>> 24);
+ }
+ pos1 += 4;
+
+ // Read vertex indices
+ this.offset = pos2;
+ const indices = [];
+ for (let j = 0; j < arrayLen; j++) {
+ let index = this.dataView.getInt32(this.offset, true);
+ if (needsReverse) {
+ index = ((index & 0xFF) << 24) |
+ ((index & 0xFF00) << 8) |
+ ((index & 0xFF0000) >>> 8) |
+ ((index & 0xFF000000) >>> 24);
+ }
+ indices.push(index);
+ this.offset += 4;
+ }
+ pos2 = this.offset;
+
+ arrays.push(indices);
+ }
+
+ return {
+ header: patchHeader,
+ arrays
+ };
+ }
+
+ readString(length) {
+ const bytes = new Uint8Array(this.dataView.buffer, this.offset, length);
+ let str = '';
+ for (let i = 0; i < length && bytes[i] !== 0; i++) {
+ str += String.fromCharCode(bytes[i]);
+ }
+ this.offset += length;
+ return str;
+ }
+ }
+
+function generateOBJ_AI(vstData, startLod, endLod) {
+ const vertices = vstData.vertices;
+ const lods = vstData.lods;
+
+ let obj = "";
+
+ // Write vertex data
+ for (let vertex of vertices) {
+ obj += `v ${vertex.x} ${-vertex.z} ${vertex.y}\n`;
+ }
+
+ // Write texture coordinate data
+ for (let vertex of vertices) {
+ obj += `vt ${vertex.tx} ${1 - vertex.ty}\n`;
+ }
+
+ obj += "# Faces\n";
+
+ // Write face data for each LOD from startLod to endLod
+ //for (let lodIndex = startLod; lodIndex <= endLod; lodIndex++) {
+ lodIndex=1; // debug
+ const lod = lods[lodIndex]; // debug
+
+ for (let patch of lod.patches) {
+ if (patch.header.pointCloud === 0) {
+ const textureFile = vstData.textureFiles?.[patch.header.texture];
+ if (textureFile) {
+ obj += `# Material: ${textureFile}\n`;
+ obj += `usemtl mtl_${patch.header.texture}\n`;
+ obj += `mtllib ${textureFile.replace('.png', '.mtl')}\n`;
+ }
+
+ obj += `g shape_lod${lodIndex}\n`; // Aggiungo il numero del LOD al nome del gruppo
+
+ for (let array of patch.arrays) {
+ obj += "f ";
+ for (let index of array) {
+ obj += `${index+1}/${index+1} `;
+ }
+ obj += "\n";
+ }
+ }
+ }
+
+
+ //}
+
+ return obj;
+}
+
+
+function generateX3D(vstData, startLod, endLod) {
+ const bbox = vstData.bbox;
+ const vertices = vstData.vertices;
+ const lods = vstData.lods;
+
+ // Helper to format coordinates for X3D
+ const formatPoint = (vertex) => `${vertex.x} ${-vertex.z} ${vertex.y}`;
+ const formatTexCoord = (vertex) => `${vertex.tx} ${1 - vertex.ty}`;
+
+
+ x3d = `
+
+
+
+
+
+
+
+ `;
+
+ // Calculate bounding box center and size
+ const bboxCenter = {
+ x: (bbox.xMax + bbox.xMin) / 2,
+ y: -(bbox.zMax + bbox.zMin) / 2,
+ z: (bbox.yMax + bbox.yMin) / 2
+ };
+
+ const bboxSize = {
+ x: (bbox.xMax - bbox.xMin) / 2,
+ y: (bbox.zMax - bbox.zMin) / 2,
+ z: (bbox.yMax - bbox.yMin) / 2
+ };
+
+ lods.forEach((lod, lodIndex) => {
+ currentShape = "";
+ wireframe = "";
+ if (( startLod <= lodIndex) && (lodIndex <= endLod)) {
+//console.log("Inserting LOD n. ", lodIndex, ", V=", lod.header.vertexNum, ", T=", lod.patches[0].header.arrayNum);
+ x3d += `\n `;
+ currentShape += `\n `;
+
+ lod.patches.forEach(patch => {
+ if (patch.header.pointCloud === 1) {
+ // Handle point cloud
+ x3d += '\n \n {
+ array.forEach(vertexIndex => {
+ points.push(formatPoint(vertices[vertexIndex]));
+ });
+ });
+
+ x3d += points.join(', ');
+ currentShape += points.join(', ');
+ x3d += '"/>\n ';
+ currentShape += '"/>\n ';
+ } else {
+ // Handle trianglestrip
+ textureOk = false;
+ if (vstData.textureFiles && vstData.textureFiles[patch.header.texture]) {
+ finalUrl = vstData.textureFiles[patch.header.texture];
+ textureOk = true;
+ } else {
+ textureOk = false; // redundant, just to add the closing "else"
+console.log("Creating x3d file without the missing texture.");
+ }
+
+ // Handle textured triangles
+x3d += `\n
+ `;
+
+currentShape += `\n
+ `;
+
+wireframe += `\n
+ `;
+
+ if (textureOk) {
+ x3d += `\n
+ `;
+
+ currentShape += `\n
+ `;
+ }
+
+x3d += `\n
+
+ \n \n \n ';
+ currentShape += '"\n />';
+ wireframe += '"\n />';
+
+ if (textureOk) {
+ // Add texture coordinates
+ x3d += `\n ';
+ currentShape += '"\n />';
+ wireframe += '"\n />';
+ }
+ x3d += '\n ';
+ currentShape += '\n ';
+ wireframe += '\n ';
+
+ x3d += '\n ';
+ currentShape += '\n ';
+ if (wireframeEnabled) {
+
+ x3d += `\n `;
+ currentShape += `\n `;
+ x3d += wireframe;
+ }
+ } // patch cycle, trianglestrip type
+ }); // patch cycle
+
+ if (wireframeEnabled) {
+
+ x3d += '\n ';
+ currentShape += '\n ';
+ }
+ x3dShapes.push(currentShape);
+ currentShape="";
+ } // selected lod
+ }); // lod cycle
+
+ x3d += '\n \n';
+ return x3d;
+ }
+
+
+const dropArea = document.getElementById('dropArea');
+const fileInput = document.getElementById('fileInput');
+const convertBtn = document.getElementById('convertBtn');
+const status = document.getElementById('status');
+
+let selectedFile = null;
+
+// Handler per il click sull'area di drop
+dropArea.addEventListener('click', () => {
+ fileInput.click();
+});
+
+// Handler per il cambio del file input
+fileInput.addEventListener('change', (e) => {
+ handleFile(e.target.files[0]);
+});
+
+// Handlers per il drag and drop
+dropArea.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ dropArea.classList.add('dragover');
+});
+
+dropArea.addEventListener('dragleave', () => {
+ dropArea.classList.remove('dragover');
+});
+
+dropArea.addEventListener('drop', (e) => {
+ e.preventDefault();
+ dropArea.classList.remove('dragover');
+ const files = Array.from(e.dataTransfer.files);
+ handleFiles(files);
+});
+
+// Funzione per gestire i file selezionati
+async function handleFiles(files) {
+ if (!files || files.length === 0) return;
+
+ // Filtra solo i file .vst
+ const vstFiles = files.filter(file => file.name.toLowerCase().endsWith('.vst'));
+
+ if (vstFiles.length === 0) {
+ showStatus('Per favore seleziona almeno un file .vst', 'error');
+ return;
+ }
+
+ if (vstFiles.length !== files.length) {
+ showStatus(`Trovati ${vstFiles.length} file .vst su ${files.length} file selezionati`, 'warning');
+ }
+
+ selectedFiles = vstFiles;
+ convertBtn.disabled = false;
+
+ // Mostra la lista dei file selezionati
+ const fileList = vstFiles.map(file => file.name).join(', ');
+console.log("fileList:", fileList);
+ showStatus(`File selezionati: ${fileList}`, 'success');
+
+ // Ottieni la barra di progresso e resetta il valore
+ const progressBar = document.getElementById('progressBar');
+ progressBar.value = 0;
+ progressBar.max = vstFiles.length;
+
+ // Processa i file in sequenza
+ for (let i = 0; i < vstFiles.length; i++) {
+ try {
+ await processFile(vstFiles[i], i);
+ progressBar.value = i + 1; // Aggiorna la barra di progresso
+ showStatus(`Completato il file ${i + 1} di ${vstFiles.length}: ${vstFiles[i].name}`, 'success');
+ document.getElementById("spnLoaded").innerHTML += vstFiles[i].name+ "
";
+ } catch (error) {
+ showStatus(`Errore nel processare il file ${vstFiles[i].name}: ${error.message}`, 'error');
+ // break; // Decommentare per fermarsi al primo errore
+ }
+ }
+
+ // Mostra il completamento al termine
+ showStatus('Elaborazione completata!', 'success');
+}
+
+
+async function processFile(file, index) {
+ // Aggiorna il product ID per questo file specifico
+ txtProductId.value = file.name.replace(".vst","").replace(".VST","");
+
+ // Avvia la conversione per questo file
+ await startConversion(file, index);
+}
+
+async function startConversion (selectedFile) {
+showStatus("Conversion started", 'success');
+ product_name_no_ext = document.getElementById("txtProductId").value; // 2n292377885vilb126f0006l0m1 = small vst for testing, sol 1870
+
+ const secondChar = product_name_no_ext.charAt(1).toLowerCase();
+
+ // Imposta VST_CAMERA_FOLDER in base al secondo carattere
+ if (secondChar === 'n') {
+ VST_CAMERA_FOLDER = "data/navcam/";
+ } else if (secondChar === 'p') {
+ VST_CAMERA_FOLDER = "data/pancam/";
+ } else {
+ throw new Error("Carattere non riconosciuto '" + secondChar + "' per determinare la cartella della fotocamera."); // Gestione errore
+ }
+
+ base_VST_filename = BASE_VST_URL + VST_CAMERA_FOLDER + VST_FOLDER_SITE_NAME + product_name_no_ext.toLowerCase();
+ let txtVSTurl = base_VST_filename + ".vst"; // DEBUG
+
+ let arrayBuffer;
+
+ // try {
+ if (selectedFile) {
+console.log("==================================");
+console.log("Local:", selectedFile.name);
+ // Se il file e' selezionato dall'utente
+ arrayBuffer = await selectedFile.arrayBuffer();
+ } else if (txtVSTurl) {
+console.log("==================================");
+console.log("Downloading:",txtVSTurl);
+ // Se esiste un URL in txtVSTurl, scarica il file usando il proxy
+ const proxyURL = "https://win98.altervista.org/space/exploration/myp.php?pass=miapass&mode=native&url=";
+ response = await fetch(proxyURL + encodeURIComponent(txtVSTurl));
+ if (!response.ok) throw new Error("Errore nel download del file da URL.");
+ arrayBuffer = await response.arrayBuffer();
+ } else {
+ showStatus("Nessun file selezionato o URL fornito.", "error");
+ return;
+ }
+
+console.log("Starting VST parsing...");
+ // Parsing e generazione del contenuto X3D
+ const parser = new VSTParser(arrayBuffer);
+ vstData = await parser.parse();
+
+let temp = vstData.coordinateSystem.coordArray;
+console.log(temp[0],temp[1],temp[2],temp[3]);
+
+ // Invert Z coordinate (in MER system the positive vertical axis points to ground):
+ const invertedVertices = vstData.vertices.map(item => ({
+ ...item,
+ z: -item.z//,
+ //x: -item.x//,
+ //y: -item.y
+ }));
+ vstData.vertices = invertedVertices;
+
+console.log("Inserting model into scene....");
+ x3dContent = generateX3D(vstData, 1,1); // DEBUG: LOD must be selected by user; // DEBUG: Get the x3d file as returned value to pass to viewer
+console.log("Conversion to x3d completed.");
+//saveX3Dfile(x3dContent);
+//objContent = generateOBJ_AI(vstData, 1,1); // FUNZIONA la pointcloud!
+//saveOBJfile(objContent);
+
+
+ showStatus(selectedFile.name + 'completato.', 'success');
+
+
+ loadX3DFile(x3dContent, angles.pancamAzimuthRad, -angles.pancamElevationRad);
+ //} catch (error) {
+ // showStatus(`Errore durante la conversione: ${error.message}`, 'error');
+ // }
+};
+
+
+convertBtn.addEventListener('click', startConversion);
+
+
+function saveX3Dfile(x3dContent) { // DEBUG: add a caller button
+ // Crea e scarica il file X3D
+ const blob = new Blob([x3dContent], { type: 'model/x3d+xml' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+console.log("Saving to ", product_name_no_ext + '.x3d');
+ a.download = selectedFile ? selectedFile.name.replace('.vst', '.x3d') : product_name_no_ext + '.x3d';
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+}
+
+function saveOBJfile(objContent) { // DEBUG: add a caller button
+ // Crea e scarica il file X3D
+ const blob = new Blob([objContent], { type: 'model/x3d+xml' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+console.log("Saving to ", product_name_no_ext + '.obj');
+ a.download = selectedFile ? selectedFile.name.replace('.vst', '.obj') : product_name_no_ext + '.obj';
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+}
+
+
+// Funzione per mostrare lo status
+function showStatus(message, type) {
+ status.textContent = message;
+ status.style.display = 'block';
+ status.className = type;
+}
+
+async function urlToBase64(imageUrl) {
+ finalUrl = proxyURL + encodeURIComponent(imageUrl);
+
+ try {
+ // Scarica l'immagine come blob
+ const response = await fetch(finalUrl);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const blob = await response.blob();
+
+ // Verifica che il MIME type sia JPEG
+ if (blob.type !== 'image/jpeg') {
+ return ("error");
+ }
+
+ // Determina il tipo MIME corretto
+ const mimeType = blob.type;//'image/jpeg';
+
+ // Converti il blob in base64
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ const base64String = reader.result;
+ const base64Data = base64String.split(',')[1];
+ resolve(`data:${mimeType};base64,${base64Data}`);
+ };
+ reader.onerror = reject;
+ reader.readAsDataURL(blob);
+ });
+ } catch (error) {
+console.log(`Errore durante la creazione di BASE64: ${error.message}`);
+ var manualUrl = prompt("2- No network - Base64url?");
+ return(manualUrl);
+ }
+}
+
+function siteCodeToString(code) {
+ if (code.length !== 2) {
+ throw new Error("Il codice deve essere una stringa di due caratteri.");
+ }
+
+ const firstChar = code[0];
+ const secondChar = code[1];
+
+ // Caso 1: numerico puro ("00" - "99")
+ if (!isNaN(firstChar) && !isNaN(secondChar)) {
+ return parseInt(code, 10);
+ }
+
+ // Caso 2: alfanumerico ("A0" - "ZZ")
+ const alphabetOffset = 'A'.charCodeAt(0); // Offset per calcolare il valore delle lettere
+ const firstValue = firstChar.charCodeAt(0) - alphabetOffset;
+ let secondValue;
+
+ if (isNaN(secondChar)) {
+ // Se il secondo carattere è una lettera
+ secondValue = secondChar.charCodeAt(0) - alphabetOffset + 10;
+ } else {
+ // Se il secondo carattere è un numero
+ secondValue = parseInt(secondChar, 10);
+ }
+
+ // Formula per calcolare l'intervallo corretto (da 100 in poi)
+ return 100 + firstValue * 36 + secondValue;
+}
+
+
+
+
+async function getSolNumberFromLabel(lblFileName, siteNumberString) {
+
+ const secondChar = lblFileName.charAt(1).toLowerCase();
+
+ // Imposta VST_CAMERA_FOLDER in base al secondo carattere
+ if (secondChar === 'n') {
+ VST_CAMERA_FOLDER = "data/navcam/";
+ } else if (secondChar === 'p') {
+ VST_CAMERA_FOLDER = "data/pancam/";
+ } else {
+ throw new Error("Carattere non riconosciuto '" + secondChar + "' per determinare la cartella della fotocamera."); // Gestione errore
+ }
+
+
+ lblUrl = BASE_VST_URL + VST_CAMERA_FOLDER + siteNumberString + "/" + lblFileName ; // DEBUG: valid only up to site 0138, for Spirit!!
+ lblFinalUrl = proxyURL + encodeURIComponent(lblUrl);
+ try {
+ const response = await fetch(lblFinalUrl);
+
+ if (!response.ok) {
+console.log(`HTTP error! status: ${response.status}`);
+ reject( new Error(`HTTP error! status: ${response.status}`));
+ } else {
+//
+ }
+
+ const blob = await response.blob();
+
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ VSTlabel = reader.result;
+ solNumber = extractSol(VSTlabel);
+ // Verifica validita di solNumber
+ if (solNumber == null || isNaN(solNumber) || solNumber < 0) {
+ var solNumber = prompt("1 - Sol not valid in label; Sol?");
+ if (solNumber == null || isNaN(solNumber) || solNumber < 0) {
+console.log(`>>Errore durante la lettura del Sol Number dalla label: ${error.message}`, "URL=", lblUrl);
+ reject(new Error("INVALID SOLNUMBER IN LABEL " ));
+ } else {
+ resolve(solNumber);
+ }
+ } else {
+ resolve(solNumber);
+ }
+ };
+ reader.onerror = reject;
+ reader.readAsText(blob);
+ });
+ } catch (error) {
+ var solNumber = prompt("2- No network - Sol?");
+ if (solNumber == null || isNaN(solNumber) || solNumber < 0) {
+console.log(`>>Errore durante il caricamento della label per leggere il Sol Number: ${error.message}`);
+ return ("NETWORK ERROR ON LABEL");
+ } else {
+ return(solNumber);
+ }
+ }
+}
+
+
+async function getAltAzFromImgProduct(textureName, siteNumberString, solNumber) {
+ let imgFileNameLeft = txtProductId.value.substring(0, 11);
+ let imgFileNameRight = txtProductId.value.substring(14);
+ let imgFileName = imgFileNameLeft + "thn" + imgFileNameRight + ".img"; // thumbnail is the shorter one, and we need just the label
+
+ imgFileName = imgFileName.substring(0, 26) + "1" + imgFileName.substring(27); // Force version 1 for IMG product
+
+ const secondChar = imgFileName.charAt(1).toLowerCase();
+
+ // Imposta VST_CAMERA_FOLDER in base al secondo carattere
+ if (secondChar === 'n') {
+ BASE_IMG_URL = BASE_IMG_URL_NAVCAM
+ } else if (secondChar === 'p') {
+ BASE_IMG_URL = BASE_IMG_URL_PANCAM
+ } else {
+ throw new Error("Carattere non riconosciuto '" + secondChar + "' in nome file '" + imgFileName + "' per determinare la cartella della fotocamera."); // Gestione errore
+ }
+
+
+ base_texture_folderIMG = BASE_IMG_URL + IMG_RAW_FOLDER + PRODUCT_FOLDER;
+
+
+ imgUrl = base_texture_folderIMG.replace("#SOLNUMBER#",solNumber) + imgFileName ; // DEBUG: valid only up to site 0138, for Spirit!!
+ imgFinalUrl = proxyURL + encodeURIComponent(imgUrl);
+
+ try {
+ const response = await fetch(imgFinalUrl);
+ if (!response.ok) {
+ return (new Error(`getAltAzFromImgProduct - HTTP error! status: ${response.status}`));
+ } else {
+//console.log("OK, IMG for AltAz retrieved");
+ }
+
+ const blob = await response.blob();
+
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ imgContents = reader.result;
+ resolve(extractVicarData(imgContents));
+ };
+ reader.onerror = reject;
+ reader.readAsText(blob);
+ });
+ } catch (error) {
+
+ var pancamAzimuthRad = prompt("2- No network - pancamAzimuthRad?");
+ if (pancamAzimuthRad == null || isNaN(pancamAzimuthRad) ) {
+console.log(`>>Errore durante il caricamento di pancamAzimuthRad: ${error.message}`);
+ pancamAzimuthRad = 0;
+ } else {
+// go on
+ }
+
+ var pancamElevationRad = prompt("2- No network - pancamElevationRad?");
+ if (pancamElevationRad == null || isNaN(pancamElevationRad) ) {
+console.log(`>>Errore durante il caricamento di pancamElevationRad: ${error.message}`);
+ pancamElevationRad = 0;
+ } else {
+ //go on
+ }
+
+ return {pancamAzimuthRad: pancamAzimuthRad, pancamElevationRad : pancamElevationRad, azimuthFOVdeg: 45, elevationFOVdeg : 45};
+
+ }
+}
+
+
+function extractSol(labelContent) {
+ const match = labelContent.match(/PLANET_DAY_NUMBER\s*=\s*(\d+)/);
+ if (match) {
+ return parseInt(match[1], 10);
+ } else {
+console.log("SOL NUMBER NOT FOUND: ",labelContent);
+ return null;
+ }
+}
+
+
+function parseQuaternion(quaternionString) {
+ // Remove the surrounding parentheses
+ const quaternionValues = quaternionString.slice(1, -1).split(',');
+
+ // Convert the string values to numbers
+ const [x, y, z, w] = quaternionValues.map(parseFloat);
+
+ return [x, y, z, w];
+}
+
+
+function calculatePancamAngles(roverQuaternion, pancamAzimuth, pancamElevation) {
+ // Estrazione componenti del quaternione
+ const [x, y, z, w] = roverQuaternion;
+
+ // Calcolo azimuth della Pancam
+ const azimuth = Math.atan2(2 * (x * y + w * z), w * w + x * x - y * y - z * z);
+
+ // Calcolo elevazione della Pancam
+ const elevation = Math.asin(-2 * (x * z - w * y));
+
+ // Aggiungi gli angoli della Pancam
+ const finalAzimuth = pancamAzimuth + azimuth;
+ const finalElevation = pancamElevation + elevation; // DEBUG non funziona!
+
+ return { finalAzimuth, finalElevation };
+}
+
+
+
+function extractVicarData(vicarLabelText) {
+ PMA_anglesValuesRaw = readVicarParameter(vicarLabelText, "PMA_ARTICULATION_STATE", "ARTICULATION_DEVICE_ANGLE");
+ PMA_orientationValuesRaw = readVicarParameter(vicarLabelText, "PMA_ARTICULATION_STATE", "ARTICULATION_DEVICE_ANGLE")
+
+ PANCAM_azimuthRaw = PMA_orientationValuesRaw["AZIMUTH-MEASURED"];
+ PANCAM_elevationRaw = PMA_orientationValuesRaw["ELEVATION-MEASURED"];
+ PANCAM_azimuthFOV_Raw = readVicarParameter(vicarLabelText, "INSTRUMENT_STATE_PARMS", "AZIMUTH_FOV")["AZIMUTH_FOV"];
+ PANCAM_elevationFOV_Raw = readVicarParameter(vicarLabelText, "INSTRUMENT_STATE_PARMS", "ELEVATION_FOV")["ELEVATION_FOV"];
+
+
+ pancamAzimuthRad = parseFloat(PANCAM_azimuthRaw.replace("",""));
+ pancamElevationRad = parseFloat(PANCAM_elevationRaw.replace("",""));
+ azimuthFOVdeg = parseFloat(PANCAM_azimuthFOV_Raw.replace("",""));
+ elevationFOVdeg = parseFloat(PANCAM_elevationFOV_Raw.replace("",""));
+
+ quaternionArray = readVicarParameter(vicarLabelText, "ROVER_COORDINATE_SYSTEM", "ORIGIN_ROTATION_QUATERNION").ORIGIN_ROTATION_QUATERNION;
+ result = calculatePancamAngles([quaternionArray[0],quaternionArray[1],quaternionArray[2],quaternionArray[3]], pancamAzimuthRad, pancamElevationRad);
+
+ return {pancamAzimuthRad: result.finalAzimuth, pancamElevationRad : result.finalElevation, azimuthFOVdeg: azimuthFOVdeg, elevationFOVdeg : elevationFOVdeg};
+}
+
+function readVicarParameter(vicarLabel, groupName, paramName) {
+ const data = vicarLabel;
+
+ // Trova il gruppo con regex che include tutte le righe tra il gruppo iniziale e il successivo
+ const groupRegex = new RegExp(`GROUP\\s*=\\s*${groupName}\\s*([\\s\\S]*?)(?:GROUP|$)`, 'i');
+ const groupMatch = data.match(groupRegex);
+ if (!groupMatch) return null; // Gruppo non trovato
+
+ const groupData = groupMatch[1];
+
+ // Trova il parametro specificato nel gruppo
+ const paramRegex = new RegExp(`^\\s*${paramName}\\s*=\\s*(.*)`, 'm');
+ const paramMatch = groupData.match(paramRegex);
+ if (!paramMatch) return null; // Parametro non trovato
+
+ let value = paramMatch[1].trim();
+
+ // Verifica se il valore è una lista
+ if (value.startsWith("(")) {
+ // Regex per estrarre correttamente tutti gli elementi tra parentesi tonde (compresi ritorni a capo)
+ const listRegex = new RegExp(`^\\s*${paramName}\\s*=\\s*\\(([^)]+)\\)`, 'ms');
+ const listMatch = groupData.match(listRegex);
+
+ if (listMatch) {
+ value = listMatch[1].split(',').map(v => v.trim()); // Crea un array con ogni elemento
+ const nameParam = paramName + "_NAME"; // Cerca un array di nomi associati
+ const nameRegex = new RegExp(`^\\s*${nameParam}\\s*=\\s*\\(([^)]+)\\)`, 'ms');
+ const nameMatch = groupData.match(nameRegex);
+
+ // Associa i nomi ai valori, se presente la lista di nomi
+ if (nameMatch) {
+ const names = nameMatch[1].split(',').map(name => name.trim().replace(/"/g, ''));
+ const result = {};
+ names.forEach((name, index) => {
+ result[name] = value[index];
+ });
+ return result;
+ }
+ }
+ }
+
+ // Restituisce un oggetto con chiave e valore singolo se non è una lista
+ return { [paramName.trim()]: value };
+}
+
+
+
+function rotateShape(shapeIndex, rotationType, angleDegrees) {
+ var shapes = document.querySelectorAll("#modelScene Shape");
+ if (shapes && shapes[shapeIndex]) {
+ var shape = shapes[shapeIndex];
+ var trfElevation = shape.parentElement;
+ var trfAzimuth = trfElevation.parentElement;
+ var initialAzimuthDegrees = initialAzimuth[shapeIndex-1]*180/Math.PI;
+ var initialElevationDegrees = initialElevation[shapeIndex-1]*180/Math.PI;
+ if (trfElevation && trfAzimuth) {
+ // Seleziona il transform appropriato e leggi la rotazione corrente
+ var newAngleRadians;
+ if (rotationType === 'azimuth') {
+ newAngleRadians = (initialAzimuthDegrees + angleDegrees) * (Math.PI / 180);
+ trfAzimuth.setAttribute("rotation", `0 1 0 ${newAngleRadians}`);
+ } else if (rotationType === 'elevation') {
+ newAngleRadians = (initialElevationDegrees + angleDegrees) * (Math.PI / 180);
+ trfElevation.setAttribute("rotation", `1 0 0 ${newAngleRadians}`);
+ }
+
+ x3dom.reload();
+ } else {
+ console.error("Transform non trovati.");
+ }
+ } else {
+ console.error("Shape non trovata.");
+ }
+}
diff --git a/test-001.html b/test-001.html
new file mode 100644
index 0000000..c9ea19b
--- /dev/null
+++ b/test-001.html
@@ -0,0 +1,505 @@
+
+
+
+
+ X3D Model Viewer
+
+
+
+
+
+
+ X3D Model Viewer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/x3d-viewer.html b/x3d-viewer.html
new file mode 100644
index 0000000..ceec783
--- /dev/null
+++ b/x3d-viewer.html
@@ -0,0 +1,316 @@
+
+
+
+
+ X3d viewer
+
+
+
+
+
+
+
+
VST to X3D Converter
+
Upload a .vst file to convert it to X3D format.
+
+
+
Drag and drop a .vst file here or click to select
+
+
+
+ Product id:
+
+
+
+Files list:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ |
+
+
+ Double click to set rotation center.
+
+ - View modes
+
+ - N = Turntable (recommended)
+
- E = Examine
+
- W = Walk
+
- G = Immediate pan mode
+
- F = flight mode
+
- I = "Look at" mode
+
+ - A = Full model view
+
- R = Reset view
+
- U = Reset flight mode
+
+ Official X3DOM help
+
+
+
+
+
+