potree
Version:
WebGL point cloud viewer - WORK IN PROGRESS
353 lines (296 loc) • 11.3 kB
JavaScript
/**
* @class Loads greyhound metadata and returns a PointcloudOctree
*
* @author Maarten van Meersbergen
* @author Oscar Martinez Rubi
* @author Connor Manning
*/
class GreyhoundUtils {
static getQueryParam (name) {
name = name.replace(/[[\]]/g, '\\$&');
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
var results = regex.exec(window.location.href);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}
static createSchema (attributes) {
var schema = [
{ 'name': 'X', 'size': 4, 'type': 'signed' },
{ 'name': 'Y', 'size': 4, 'type': 'signed' },
{ 'name': 'Z', 'size': 4, 'type': 'signed' }
];
// Once we include options in the UI to load a dynamic list of available
// attributes for visualization (f.e. Classification, Intensity etc.)
// we will be able to ask for that specific attribute from the server,
// where we are now requesting all attributes for all points all the time.
// If we do that though, we also need to tell Potree to redraw the points
// that are already loaded (with different attributes).
// This is not default behaviour.
attributes.forEach(function (item) {
if (item === 'COLOR_PACKED') {
schema.push({ 'name': 'Red', 'size': 2, 'type': 'unsigned' });
schema.push({ 'name': 'Green', 'size': 2, 'type': 'unsigned' });
schema.push({ 'name': 'Blue', 'size': 2, 'type': 'unsigned' });
} else if (item === 'INTENSITY') {
schema.push({ 'name': 'Intensity', 'size': 2, 'type': 'unsigned' });
} else if (item === 'CLASSIFICATION') {
schema.push({ 'name': 'Classification', 'size': 1, 'type': 'unsigned' });
}
});
return schema;
}
static fetch (url, cb) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200 || xhr.status === 0) {
cb(null, xhr.responseText);
} else {
cb(xhr.responseText);
}
}
};
xhr.send(null);
};
static fetchBinary (url, cb) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'arraybuffer';
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200 || xhr.status === 0) {
cb(null, xhr.response);
} else {
cb(xhr.responseText);
}
}
};
xhr.send(null);
};
static pointSizeFrom (schema) {
return schema.reduce((p, c) => p + c.size, 0);
};
static getNormalization (serverURL, baseDepth, cb) {
var s = [
{ 'name': 'X', 'size': 4, 'type': 'floating' },
{ 'name': 'Y', 'size': 4, 'type': 'floating' },
{ 'name': 'Z', 'size': 4, 'type': 'floating' },
{ 'name': 'Red', 'size': 2, 'type': 'unsigned' },
{ 'name': 'Green', 'size': 2, 'type': 'unsigned' },
{ 'name': 'Blue', 'size': 2, 'type': 'unsigned' },
{ 'name': 'Intensity', 'size': 2, 'type': 'unsigned' }
];
var url = serverURL + 'read?depth=' + baseDepth +
'&schema=' + JSON.stringify(s);
GreyhoundUtils.fetchBinary(url, function (err, buffer) {
if (err) throw new Error(err);
var view = new DataView(buffer);
var numBytes = buffer.byteLength - 4;
// TODO Unused: var numPoints = view.getUint32(numBytes, true);
var pointSize = GreyhoundUtils.pointSizeFrom(s);
var colorNorm = false;
var intensityNorm = false;
for (var offset = 0; offset < numBytes; offset += pointSize) {
if (view.getUint16(offset + 12, true) > 255 ||
view.getUint16(offset + 14, true) > 255 ||
view.getUint16(offset + 16, true) > 255) {
colorNorm = true;
}
if (view.getUint16(offset + 18, true) > 255) {
intensityNorm = true;
}
if (colorNorm && intensityNorm) break;
}
if (colorNorm) console.log('Normalizing color');
if (intensityNorm) console.log('Normalizing intensity');
cb(null, { color: colorNorm, intensity: intensityNorm });
});
};
};
Potree.GreyhoundLoader = function () { };
Potree.GreyhoundLoader.loadInfoJSON = function load (url, callback) { };
/**
* @return a point cloud octree with the root node data loaded.
* loading of descendants happens asynchronously when they're needed
*
* @param url
* @param loadingFinishedListener executed after loading the binary has been
* finished
*/
Potree.GreyhoundLoader.load = function load (url, callback) {
var HIERARCHY_STEP_SIZE = 5;
try {
// We assume everything ater the string 'greyhound://' is the server url
var serverURL = url.split('greyhound://')[1];
if (serverURL.split('http://').length === 1) {
serverURL = 'http://' + serverURL;
}
GreyhoundUtils.fetch(serverURL + 'info', function (err, data) {
if (err) throw new Error(err);
/* We parse the result of the info query, which should be a JSON
* datastructure somewhat like:
{
"bounds": [635577, 848882, -1000, 639004, 853538, 2000],
"numPoints": 10653336,
"schema": [
{ "name": "X", "size": 8, "type": "floating" },
{ "name": "Y", "size": 8, "type": "floating" },
{ "name": "Z", "size": 8, "type": "floating" },
{ "name": "Intensity", "size": 2, "type": "unsigned" },
{ "name": "OriginId", "size": 4, "type": "unsigned" },
{ "name": "Red", "size": 2, "type": "unsigned" },
{ "name": "Green", "size": 2, "type": "unsigned" },
{ "name": "Blue", "size": 2, "type": "unsigned" }
],
"srs": "<omitted for brevity>",
"type": "octree"
}
*/
var greyhoundInfo = JSON.parse(data);
var version = new Potree.Version('1.4');
var bounds = greyhoundInfo.bounds;
// TODO Unused: var boundsConforming = greyhoundInfo.boundsConforming;
// TODO Unused: var width = bounds[3] - bounds[0];
// TODO Unused: var depth = bounds[4] - bounds[1];
// TODO Unused: var height = bounds[5] - bounds[2];
// TODO Unused: var radius = width / 2;
var scale = greyhoundInfo.scale || 0.01;
if (Array.isArray(scale)) {
scale = Math.min(scale[0], scale[1], scale[2]);
}
if (GreyhoundUtils.getQueryParam('scale')) {
scale = parseFloat(GreyhoundUtils.getQueryParam('scale'));
}
var baseDepth = Math.max(8, greyhoundInfo.baseDepth);
// Ideally we want to change this bit completely, since
// greyhound's options are wider than the default options for
// visualizing pointclouds. If someone ever has time to build a
// custom ui element for greyhound, the schema options from
// this info request should be given to the UI, so the user can
// choose between them. The selected option can then be
// directly requested from the server in the
// PointCloudGreyhoundGeometryNode without asking for
// attributes that we are not currently visualizing. We assume
// XYZ are always available.
var attributes = ['POSITION_CARTESIAN'];
// To be careful, we only add COLOR_PACKED as an option if all
// colors are actually found.
var red = false;
var green = false;
var blue = false;
greyhoundInfo.schema.forEach(function (entry) {
// Intensity and Classification are optional.
if (entry.name === 'Intensity') {
attributes.push('INTENSITY');
}
if (entry.name === 'Classification') {
attributes.push('CLASSIFICATION');
}
if (entry.name === 'Red') red = true;
else if (entry.name === 'Green') green = true;
else if (entry.name === 'Blue') blue = true;
});
if (red && green && blue) attributes.push('COLOR_PACKED');
// Fill in geometry fields.
var pgg = new Potree.PointCloudGreyhoundGeometry();
pgg.serverURL = serverURL;
pgg.spacing = (bounds[3] - bounds[0]) / Math.pow(2, baseDepth);
pgg.baseDepth = baseDepth;
pgg.hierarchyStepSize = HIERARCHY_STEP_SIZE;
pgg.schema = GreyhoundUtils.createSchema(attributes);
var pointSize = GreyhoundUtils.pointSizeFrom(pgg.schema);
pgg.pointAttributes = new Potree.PointAttributes(attributes);
pgg.pointAttributes.byteSize = pointSize;
var boundingBox = new THREE.Box3(
new THREE.Vector3().fromArray(bounds, 0),
new THREE.Vector3().fromArray(bounds, 3)
);
var offset = boundingBox.min.clone();
boundingBox.max.sub(boundingBox.min);
boundingBox.min.set(0, 0, 0);
pgg.projection = greyhoundInfo.srs;
pgg.boundingBox = boundingBox;
pgg.boundingSphere = boundingBox.getBoundingSphere();
pgg.scale = scale;
pgg.offset = offset;
console.log('Scale:', scale);
console.log('Offset:', offset);
console.log('Bounds:', boundingBox);
pgg.loader = new Potree.GreyhoundBinaryLoader(version, boundingBox, pgg.scale);
var nodes = {};
{ // load root
var name = 'r';
var root = new Potree.PointCloudGreyhoundGeometryNode(
name, pgg, boundingBox,
scale, offset
);
root.level = 0;
root.hasChildren = true;
root.numPoints = greyhoundInfo.numPoints;
root.spacing = pgg.spacing;
pgg.root = root;
pgg.root.load();
nodes[name] = root;
}
pgg.nodes = nodes;
GreyhoundUtils.getNormalization(serverURL, greyhoundInfo.baseDepth,
function (_, normalize) {
if (normalize.color) pgg.normalize.color = true;
if (normalize.intensity) pgg.normalize.intensity = true;
callback(pgg);
}
);
});
} catch (e) {
console.log("loading failed: '" + url + "'");
console.log(e);
callback();
}
};
Potree.GreyhoundLoader.loadPointAttributes = function (mno) {
var fpa = mno.pointAttributes;
var pa = new Potree.PointAttributes();
for (var i = 0; i < fpa.length; i++) {
var pointAttribute = Potree.PointAttribute[fpa[i]];
pa.add(pointAttribute);
}
return pa;
};
Potree.GreyhoundLoader.createChildAABB = function (aabb, childIndex) {
var min = aabb.min;
var max = aabb.max;
var dHalfLength = new THREE.Vector3().copy(max).sub(min).multiplyScalar(0.5);
var xHalfLength = new THREE.Vector3(dHalfLength.x, 0, 0);
var yHalfLength = new THREE.Vector3(0, dHalfLength.y, 0);
var zHalfLength = new THREE.Vector3(0, 0, dHalfLength.z);
var cmin = min;
var cmax = new THREE.Vector3().add(min).add(dHalfLength);
if (childIndex === 1) {
min = new THREE.Vector3().copy(cmin).add(zHalfLength);
max = new THREE.Vector3().copy(cmax).add(zHalfLength);
} else if (childIndex === 3) {
min = new THREE.Vector3().copy(cmin).add(zHalfLength).add(yHalfLength);
max = new THREE.Vector3().copy(cmax).add(zHalfLength).add(yHalfLength);
} else if (childIndex === 0) {
min = cmin;
max = cmax;
} else if (childIndex === 2) {
min = new THREE.Vector3().copy(cmin).add(yHalfLength);
max = new THREE.Vector3().copy(cmax).add(yHalfLength);
} else if (childIndex === 5) {
min = new THREE.Vector3().copy(cmin).add(zHalfLength).add(xHalfLength);
max = new THREE.Vector3().copy(cmax).add(zHalfLength).add(xHalfLength);
} else if (childIndex === 7) {
min = new THREE.Vector3().copy(cmin).add(dHalfLength);
max = new THREE.Vector3().copy(cmax).add(dHalfLength);
} else if (childIndex === 4) {
min = new THREE.Vector3().copy(cmin).add(xHalfLength);
max = new THREE.Vector3().copy(cmax).add(xHalfLength);
} else if (childIndex === 6) {
min = new THREE.Vector3().copy(cmin).add(xHalfLength).add(yHalfLength);
max = new THREE.Vector3().copy(cmax).add(xHalfLength).add(yHalfLength);
}
return new THREE.Box3(min, max);
};