const fs = require('fs');
const path = require('path');
// WGS84 ellipsoid parameters
const a = 6378137.0; // Semi-major axis in meters
const f = 1 / 298.257223563; // Flattening
const b = a * (1 - f); // Semi-minor axis in meters
const e2 = (a * a - b * b) / (a * a); // Eccentricity squared
const e_prime2 = (a * a - b * b) / (b * b); // Second eccentricity squared
/**
* Converts ECEF coordinates to latitude, longitude, and height
* @param {number} x - X coordinate in ECEF
* @param {number} y - Y coordinate in ECEF
* @param {number} z - Z coordinate in ECEF
* @returns {Object} Latitude, longitude, and height
*/
function ecefToLlh(x, y, z) {
let lon = Math.atan2(y, x);
let p = Math.sqrt(x * x + y * y);
let theta = Math.atan2(z * a, p * b);
let lat = Math.atan2(z + e_prime2 * b * Math.sin(theta)**3, p - e2 * a * Math.cos(theta)**3);
// Refine latitude
let N = a / Math.sqrt(1 - e2 * Math.sin(lat)**2);
let h = p / Math.cos(lat) - N;
// Convert radians to degrees
lat = lat * 180 / Math.PI;
lon = lon * 180 / Math.PI;
return { lat, lon, height: h };
}
/**
* Applies a transform matrix to a vertex
* @param {Array} vertex - 3D vertex [x, y, z]
* @param {Array} transform - 4x4 transform matrix
* @returns {Array} Transformed vertex [x, y, z]
*/
function applyTransform(vertex, transform) {
const [x, y, z] = vertex;
// Ensure we have a 4x4 matrix
const t = new Array(16).fill(0);
// Set identity matrix as default
t[0] = 1;
t[5] = 1;
t[10] = 1;
t[15] = 1;
// Copy transform values (handle both 12-element and 16-element matrices)
for (let i = 0; i < Math.min(16, transform.length); i++) {
t[i] = transform[i];
}
// Apply 4x4 matrix
const newX = t[0] * x + t[4] * y + t[8] * z + t[12];
const newY = t[1] * x + t[5] * y + t[9] * z + t[13];
const newZ = t[2] * x + t[6] * y + t[10] * z + t[14];
const w = t[3] * x + t[7] * y + t[11] * z + t[15];
// Normalize if w is not 1
if (w !== 1 && w !== 0) {
return [newX / w, newY / w, newZ / w];
}
return [newX, newY, newZ];
}
/**
* Parses a box from tileset.json
* @param {Array} box - Box array from tileset.json
* @param {Array} transform - Transform matrix from tileset.json
* @returns {Object} Box information including vertices and bounds
*/
function parseBox(box, transform) {
const center = box.slice(0, 3);
const xAxis = box.slice(3, 6);
const yAxis = box.slice(6, 9);
const zAxis = box.slice(9, 12);
// Calculate 8 vertices of the box
const vertices = [
[center[0] - xAxis[0] - yAxis[0] - zAxis[0], center[1] - xAxis[1] - yAxis[1] - zAxis[1], center[2] - xAxis[2] - yAxis[2] - zAxis[2]],
[center[0] + xAxis[0] - yAxis[0] - zAxis[0], center[1] + xAxis[1] - yAxis[1] - zAxis[1], center[2] + xAxis[2] - yAxis[2] - zAxis[2]],
[center[0] - xAxis[0] + yAxis[0] - zAxis[0], center[1] - xAxis[1] + yAxis[1] - zAxis[1], center[2] - xAxis[2] + yAxis[2] - zAxis[2]],
[center[0] + xAxis[0] + yAxis[0] - zAxis[0], center[1] + xAxis[1] + yAxis[1] - zAxis[1], center[2] + xAxis[2] + yAxis[2] - zAxis[2]],
[center[0] - xAxis[0] - yAxis[0] + zAxis[0], center[1] - xAxis[1] - yAxis[1] + zAxis[1], center[2] - xAxis[2] - yAxis[2] + zAxis[2]],
[center[0] + xAxis[0] - yAxis[0] + zAxis[0], center[1] + xAxis[1] - yAxis[1] + zAxis[1], center[2] + xAxis[2] - yAxis[2] + zAxis[2]],
[center[0] - xAxis[0] + yAxis[0] + zAxis[0], center[1] - xAxis[1] + yAxis[1] + zAxis[1], center[2] - xAxis[2] + yAxis[2] + zAxis[2]],
[center[0] + xAxis[0] + yAxis[0] + zAxis[0], center[1] + xAxis[1] + yAxis[1] + zAxis[1], center[2] + xAxis[2] + yAxis[2] + zAxis[2]]
];
// Apply transform to all vertices
const transformedVertices = vertices.map(vertex => applyTransform(vertex, transform));
// Convert to geographic coordinates
const geoVertices = transformedVertices.map(vertex => ecefToLlh(vertex[0], vertex[1], vertex[2]));
// Calculate min and max bounds
const minLon = Math.min(...geoVertices.map(v => v.lon));
const maxLon = Math.max(...geoVertices.map(v => v.lon));
const minLat = Math.min(...geoVertices.map(v => v.lat));
const maxLat = Math.max(...geoVertices.map(v => v.lat));
const minHeight = Math.min(...geoVertices.map(v => v.height));
const maxHeight = Math.max(...geoVertices.map(v => v.height));
return {
center,
axes: { xAxis, yAxis, zAxis },
vertices,
transformedVertices,
geoVertices,
bounds: {
min: { lon: minLon, lat: minLat, height: minHeight },
max: { lon: maxLon, lat: maxLat, height: maxHeight }
}
};
}
/**
* Parses tileset.json
* @param {string} tilesetPath - Path to tileset.json
* @returns {Object} Parsed tileset information
*/
function parseTileset(tilesetPath) {
try {
const tilesetContent = fs.readFileSync(tilesetPath, 'utf8');
const tileset = JSON.parse(tilesetContent);
const result = {
asset: tileset.asset,
extras: tileset.extras,
geometricError: tileset.geometricError,
refine: tileset.refine,
tilesCount: 0,
root: {}
};
// Parse root node
if (tileset.root) {
const root = tileset.root;
const transform = root.transform || [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
result.root = {
geometricError: root.geometricError,
refine: root.refine
};
// Parse bounding volume
if (root.boundingVolume) {
result.root.boundingVolume = {};
if (root.boundingVolume.box) {
result.root.boundingVolume.box = parseBox(root.boundingVolume.box, transform);
}
if (root.boundingVolume.sphere) {
const sphere = root.boundingVolume.sphere;
const center = sphere.slice(0, 3);
const radius = sphere[3];
const transformedCenter = applyTransform(center, transform);
const geoCenter = ecefToLlh(transformedCenter[0], transformedCenter[1], transformedCenter[2]);
result.root.boundingVolume.sphere = {
center,
radius,
transformedCenter,
geoCenter
};
}
if (root.boundingVolume.region) {
const region = root.boundingVolume.region;
const west = region[0] * 180 / Math.PI;
const south = region[1] * 180 / Math.PI;
const east = region[2] * 180 / Math.PI;
const north = region[3] * 180 / Math.PI;
const minHeight = region[4];
const maxHeight = region[5];
result.root.boundingVolume.region = {
west,
south,
east,
north,
minHeight,
maxHeight
};
}
}
// Parse children
if (root.children) {
result.root.children = root.children.map((child, index) => {
const childTransform = child.transform || transform;
const childResult = {
index,
geometricError: child.geometricError,
refine: child.refine,
content: child.content
};
// Parse bounding volume
if (child.boundingVolume) {
childResult.boundingVolume = {};
if (child.boundingVolume.box) {
childResult.boundingVolume.box = parseBox(child.boundingVolume.box, childTransform);
}
if (child.boundingVolume.sphere) {
const sphere = child.boundingVolume.sphere;
const center = sphere.slice(0, 3);
const radius = sphere[3];
const transformedCenter = applyTransform(center, childTransform);
const geoCenter = ecefToLlh(transformedCenter[0], transformedCenter[1], transformedCenter[2]);
childResult.boundingVolume.sphere = {
center,
radius,
transformedCenter,
geoCenter
};
}
}
return childResult;
});
result.tilesCount = root.children.length;
}
}
return result;
} catch (error) {
console.error(`Error parsing tileset.json: ${error.message}`);
return null;
}
}
/**
* Parses scenetree.json
* @param {string} scenetreePath - Path to scenetree.json
* @returns {Object} Parsed scenetree information
*/
function parseScenetree(scenetreePath) {
try {
const scenetreeContent = fs.readFileSync(scenetreePath, 'utf8');
const scenetree = JSON.parse(scenetreeContent);
// Convert sphere centers to geographic coordinates
function processNode(node) {
if (node.sphere) {
const center = node.sphere.slice(0, 3);
const radius = node.sphere[3];
const geoCenter = ecefToLlh(center[0], center[1], center[2]);
node.sphere = {
center,
radius,
geoCenter
};
}
if (node.children) {
node.children = node.children.map(processNode);
}
return node;
}
const processedScenes = scenetree.scenes.map(processNode);
return { scenes: processedScenes };
} catch (error) {
console.error(`Error parsing scenetree.json: ${error.message}`);
return null;
}
}
/**
* Main function
*/
function main() {
// Get command line arguments
const args = process.argv.slice(2);
if (args.length === 0) {
console.error('Usage: node parse_3dtiles.js <tileset.json> [scenetree.json]');
process.exit(1);
}
const tilesetPath = args[0];
const scenetreePath = args[1] || path.join(path.dirname(tilesetPath), 'scenetree.json');
console.log('Parsing 3D Tiles files...');
console.log(`Tileset: ${tilesetPath}`);
console.log(`Scenetree: ${scenetreePath}`);
// Parse tileset.json
const tilesetData = parseTileset(tilesetPath);
if (!tilesetData) {
console.error('Failed to parse tileset.json');
process.exit(1);
}
// Parse scenetree.json if it exists
let scenetreeData = null;
if (fs.existsSync(scenetreePath)) {
scenetreeData = parseScenetree(scenetreePath);
if (!scenetreeData) {
console.warn('Failed to parse scenetree.json, continuing without it');
}
} else {
console.warn('scenetree.json not found, continuing without it');
}
// Combine results
const result = {
tileset: tilesetData,
scenetree: scenetreeData
};
// Output results
const outputPath = path.join(path.dirname(tilesetPath), '3dtiles_info.json');
fs.writeFileSync(outputPath, JSON.stringify(result, null, 2), 'utf8');
console.log('\nParsing completed successfully!');
console.log(`Results saved to: ${outputPath}`);
// Print summary
console.log('\nSummary:');
console.log(`- Asset: ${tilesetData.asset.generatetool || 'Unknown'}, Version: ${tilesetData.asset.version}`);
console.log(`- Total tiles: ${tilesetData.tilesCount}`);
if (tilesetData.root.boundingVolume && tilesetData.root.boundingVolume.box) {
const bounds = tilesetData.root.boundingVolume.box.bounds;
console.log('\nGeographic bounds (root):');
console.log(`- Min: Longitude ${bounds.min.lon.toFixed(6)}, Latitude ${bounds.min.lat.toFixed(6)}, Height ${bounds.min.height.toFixed(2)}m`);
console.log(`- Max: Longitude ${bounds.max.lon.toFixed(6)}, Latitude ${bounds.max.lat.toFixed(6)}, Height ${bounds.max.height.toFixed(2)}m`);
}
if (scenetreeData) {
console.log('\nScenetree nodes:', scenetreeData.scenes.length);
}
}
// Run main function if called directly
if (require.main === module) {
main();
}
// Export functions for use in other modules
module.exports = {
ecefToLlh,
applyTransform,
parseBox,
parseTileset,
parseScenetree
};