Merge pull request #170 from UDST/feature/web-workers-polygons-dedupe

Remove duplicated code within Web Worker and non-Web Worker areas
feature/threejs-update
Robin Hawkes 2016-09-12 09:28:00 +01:00 zatwierdzone przez GitHub
commit 2179088399
10 zmienionych plików z 270 dodań i 1042 usunięć

Wyświetl plik

@ -21,7 +21,8 @@ world.createWorkers(7).then(() => {
});;
// Buildings from Mapzen
VIZI.topoJSONWorkerTileLayer('https://vector.mapzen.com/osm/buildings/{z}/{x}/{y}.topojson?api_key=vector-tiles-NT5Emiw', {
VIZI.topoJSONTileLayer('https://vector.mapzen.com/osm/buildings/{z}/{x}/{y}.topojson?api_key=vector-tiles-NT5Emiw', {
workers: true,
interactive: false,
style: function(feature) {
var height;

Wyświetl plik

@ -98,197 +98,153 @@ class GeoJSONLayer extends LayerGroup {
// Need to be careful as to not make it impossible to fork this off into a
// worker script at a later stage
_processData(data) {
// Collects features into a single FeatureCollection
//
// Also converts TopoJSON to GeoJSON if instructed
this._geojson = GeoJSON.collectFeatures(data, this._options.topojson);
return new Promise((resolve) => {
// Collects features into a single FeatureCollection
//
// Also converts TopoJSON to GeoJSON if instructed
this._geojson = GeoJSON.collectFeatures(data, this._options.topojson);
// TODO: Check that GeoJSON is valid / usable
// TODO: Check that GeoJSON is valid / usable
var features = this._geojson.features;
var features = this._geojson.features;
// Run filter, if provided
if (this._options.filter) {
features = this._geojson.features.filter(this._options.filter);
}
var defaults = {};
// Assume that a style won't be set per feature
var style = this._options.style;
var options;
features.forEach(feature => {
// Get per-feature style object, if provided
if (typeof this._options.style === 'function') {
style = extend({}, GeoJSON.defaultStyle, this._options.style(feature));
// Run filter, if provided
if (this._options.filter) {
features = this._geojson.features.filter(this._options.filter);
}
options = extend({}, defaults, {
// If merging feature layers, stop them outputting themselves
// If not, let feature layers output themselves to the world
output: !this.isOutput(),
interactive: this._options.interactive,
style: style
var defaults = {};
// Assume that a style won't be set per feature
var style = this._options.style;
var layerPromises = [];
var options;
features.forEach(feature => {
// Get per-feature style object, if provided
if (typeof this._options.style === 'function') {
style = extend({}, GeoJSON.defaultStyle, this._options.style(feature));
}
options = extend({}, defaults, {
// If merging feature layers, stop them outputting themselves
// If not, let feature layers output themselves to the world
output: !this.isOutput(),
interactive: this._options.interactive,
style: style
});
var layer = this._featureToLayer(feature, options);
if (!layer) {
return;
}
// Sometimes you don't want to store a reference to the feature
//
// For example, to save memory when being used by tile layers
if (this._options.keepFeatures) {
layer.feature = feature;
}
// If defined, call a function for each feature
//
// This is commonly used for adding event listeners from the user script
if (this._options.onEachFeature) {
this._options.onEachFeature(feature, layer);
}
// TODO: Make this a promise array and only continue on completion
layerPromises.push(this.addLayer(layer));
});
var layer = this._featureToLayer(feature, options);
if (!layer) {
return;
}
// Sometimes you don't want to store a reference to the feature
//
// For example, to save memory when being used by tile layers
if (this._options.keepFeatures) {
layer.feature = feature;
}
// If defined, call a function for each feature
//
// This is commonly used for adding event listeners from the user script
if (this._options.onEachFeature) {
this._options.onEachFeature(feature, layer);
}
// TODO: Make this a promise array and only continue on completion
this.addLayer(layer);
});
// If merging layers do that now, otherwise skip as the geometry layers
// should have already outputted themselves
if (!this.isOutput()) {
return;
}
// From here on we can assume that we want to merge the layers
var polygonAttributes = [];
var polygonFlat = true;
var polylineAttributes = [];
var pointAttributes = [];
this._layers.forEach(layer => {
if (layer instanceof PolygonLayer) {
polygonAttributes.push(layer.getBufferAttributes());
if (polygonFlat && !layer.isFlat()) {
polygonFlat = false;
Promise.all(layerPromises).then((results) => {
// If merging layers do that now, otherwise skip as the geometry layers
// should have already outputted themselves
if (!this.isOutput()) {
resolve();
return;
}
} else if (layer instanceof PolylineLayer) {
polylineAttributes.push(layer.getBufferAttributes());
} else if (layer instanceof PointLayer) {
pointAttributes.push(layer.getBufferAttributes());
}
// From here on we can assume that we want to merge the layers
var polygonAttributes = [];
var polygonAttributeLengths = {
positions: 3,
normals: 3,
colors: 3
};
var polygonFlat = true;
var polylineAttributes = [];
var pointAttributes = [];
this._layers.forEach(layer => {
if (layer instanceof PolygonLayer) {
polygonAttributes.push(layer.getBufferAttributes());
if (polygonFlat && !layer.isFlat()) {
polygonFlat = false;
}
if (this._options.interactive) {
polygonAttributeLengths.pickingIds = 1;
}
} else if (layer instanceof PolylineLayer) {
polylineAttributes.push(layer.getBufferAttributes());
} else if (layer instanceof PointLayer) {
pointAttributes.push(layer.getBufferAttributes());
}
});
if (polygonAttributes.length > 0) {
var mergedPolygonAttributes = Buffer.mergeAttributes(polygonAttributes);
this._setPolygonMesh(mergedPolygonAttributes, polygonAttributeLengths, polygonFlat).then((result) => {
this._polygonMesh = result.mesh;
this.add(this._polygonMesh);
if (result.pickingMesh) {
this._pickingMesh.add(pickingMesh);
}
});
}
if (polylineAttributes.length > 0) {
var mergedPolylineAttributes = Buffer.mergeAttributes(polylineAttributes);
this._setPolylineMesh(mergedPolylineAttributes);
this.add(this._polylineMesh);
}
if (pointAttributes.length > 0) {
var mergedPointAttributes = Buffer.mergeAttributes(pointAttributes);
this._setPointMesh(mergedPointAttributes);
this.add(this._pointMesh);
}
// Clean up layers
//
// TODO: Are there ever situations where the unmerged buffer attributes
// and coordinates would still be required?
this._layers.forEach(layer => {
layer.clearBufferAttributes();
layer.clearCoordinates();
});
resolve();
});
});
if (polygonAttributes.length > 0) {
var mergedPolygonAttributes = Buffer.mergeAttributes(polygonAttributes);
this._setPolygonMesh(mergedPolygonAttributes, polygonFlat);
this.add(this._polygonMesh);
}
if (polylineAttributes.length > 0) {
var mergedPolylineAttributes = Buffer.mergeAttributes(polylineAttributes);
this._setPolylineMesh(mergedPolylineAttributes);
this.add(this._polylineMesh);
}
if (pointAttributes.length > 0) {
var mergedPointAttributes = Buffer.mergeAttributes(pointAttributes);
this._setPointMesh(mergedPointAttributes);
this.add(this._pointMesh);
}
// Clean up layers
//
// TODO: Are there ever situations where the unmerged buffer attributes
// and coordinates would still be required?
this._layers.forEach(layer => {
layer.clearBufferAttributes();
layer.clearCoordinates();
});
return Promise.resolve();
}
// Create and store mesh from buffer attributes
//
// TODO: De-dupe this from the individual mesh creation logic within each
// geometry layer (materials, settings, etc)
//
// Could make this an abstract method for each geometry layer
_setPolygonMesh(attributes, flat) {
var geometry = new THREE.BufferGeometry();
// itemSize = 3 because there are 3 values (components) per vertex
geometry.addAttribute('position', new THREE.BufferAttribute(attributes.vertices, 3));
geometry.addAttribute('normal', new THREE.BufferAttribute(attributes.normals, 3));
geometry.addAttribute('color', new THREE.BufferAttribute(attributes.colours, 3));
if (attributes.pickingIds) {
geometry.addAttribute('pickingId', new THREE.BufferAttribute(attributes.pickingIds, 1));
}
geometry.computeBoundingBox();
// TODO: Probably remove this and call static method directly as it's just a proxy
_setPolygonMesh(attributes, attributeLengths, flat) {
// TODO: Make this work when style is a function per feature
var style = (typeof this._options.style === 'function') ? this._options.style(this._geojson.features[0]) : this._options.style;
style = extend({}, GeoJSON.defaultStyle, style);
var material;
if (this._options.polygonMaterial && this._options.polygonMaterial instanceof THREE.Material) {
material = this._options.polygonMaterial;
} else if (!this._world._environment._skybox) {
material = new THREE.MeshPhongMaterial({
vertexColors: THREE.VertexColors,
side: THREE.BackSide,
transparent: style.transparent,
opacity: style.opacity,
blending: style.blending
});
} else {
material = new THREE.MeshStandardMaterial({
vertexColors: THREE.VertexColors,
side: THREE.BackSide,
transparent: style.transparent,
opacity: style.opacity,
blending: style.blending
});
material.roughness = 1;
material.metalness = 0.1;
material.envMapIntensity = 3;
material.envMap = this._world._environment._skybox.getRenderTarget();
}
var mesh;
// Pass mesh through callback, if defined
if (typeof this._options.onPolygonMesh === 'function') {
mesh = this._options.onPolygonMesh(geometry, material);
} else {
mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
mesh.receiveShadow = true;
}
if (flat) {
material.depthWrite = false;
mesh.renderOrder = 1;
}
if (this._options.interactive && this._pickingMesh) {
material = new PickingMaterial();
material.side = THREE.BackSide;
var pickingMesh = new THREE.Mesh(geometry, material);
this._pickingMesh.add(pickingMesh);
}
this._polygonMesh = mesh;
return PolygonLayer.SetMesh(attributes, attributeLengths, flat, style, this._options, this._world._environment._skybox);
}
_setPolylineMesh(attributes) {

Wyświetl plik

@ -97,20 +97,15 @@ class GeoJSONWorkerLayer extends Layer {
Worker.exec('GeoJSONWorkerLayer.Process', [geojson, topojson, headers, originPoint, style, interactive], transferrables).then((results) => {
console.timeEnd('Worker round trip');
var splitVertices = Buffer.splitFloat32Array(results.attributes.vertices);
var splitPositions = Buffer.splitFloat32Array(results.attributes.positions);
var splitNormals = Buffer.splitFloat32Array(results.attributes.normals);
var splitColours = Buffer.splitFloat32Array(results.attributes.colours);
var splitColors = Buffer.splitFloat32Array(results.attributes.colors);
var splitProperties;
if (results.properties) {
splitProperties = Buffer.splitUint8Array(results.properties);
}
// var splitPickingIds;
// if (results.pickingIds) {
// splitPickingIds = Buffer.splitFloat32Array(results.attributes.pickingIds);
// }
var flats = results.flats;
var objects = [];
@ -125,7 +120,7 @@ class GeoJSONWorkerLayer extends Layer {
colors: 3
};
for (var i = 0; i < splitVertices.length; i++) {
for (var i = 0; i < splitPositions.length; i++) {
if (splitProperties && splitProperties[i]) {
properties = JSON.parse(Buffer.uint8ArrayToString(splitProperties[i]));
} else {
@ -136,24 +131,20 @@ class GeoJSONWorkerLayer extends Layer {
// the feature, though the current logic isn't aware of that
obj = {
attributes: [{
positions: splitVertices[i],
positions: splitPositions[i],
normals: splitNormals[i],
colors: splitColours[i]
colors: splitColors[i]
}],
properties: properties,
flat: flats[i]
};
// if (splitPickingIds && splitPickingIds[i]) {
// obj.attributes.pickingIds = splitPickingIds[i];
// }
// WORKERS: If interactive, generate unique ID for each feature, create
// the buffer attributes and set up event listeners
if (this._options.interactive) {
pickingId = this.getPickingId();
pickingIds = new Float32Array(splitVertices[i].length / 3);
pickingIds = new Float32Array(splitPositions[i].length / 3);
pickingIds.fill(pickingId);
obj.attributes[0].pickingIds = pickingIds;
@ -192,28 +183,6 @@ class GeoJSONWorkerLayer extends Layer {
polygonAttributes.push(bufferAttributes);
};
// console.log(splitVertices, splitNormals, splitColours, splitPickingIds);
// var layer;
// var polygonAttributes = [];
// var polygonFlat = true;
// objects.forEach((obj, index) => {
// layer = polygonWorkerLayers[index];
// layer.createGeometry(obj);
// if (layer.isOutput()) {
// return;
// }
// polygonAttributes.push(layer.getBufferAttributes());
// if (polygonFlat && !layer.isFlat()) {
// polygonFlat = false;
// }
// });
if (polygonAttributes.length > 0) {
var mergedPolygonAttributes = Buffer.mergeAttributes(polygonAttributes);
@ -221,8 +190,14 @@ class GeoJSONWorkerLayer extends Layer {
var style = (typeof this._options.style === 'function') ? this._options.style(objects[0]) : this._options.style;
style = extend({}, GeoJSON.defaultStyle, style);
this._setPolygonMesh(mergedPolygonAttributes, polygonAttributeLengths, style, polygonFlat);
this.add(this._polygonMesh);
this._setPolygonMesh(mergedPolygonAttributes, polygonAttributeLengths, style, polygonFlat).then((result) => {
this._polygonMesh = result.mesh;
this.add(this._polygonMesh);
if (result.pickingMesh) {
this._pickingMesh.add(pickingMesh);
}
});
}
resolve();
@ -337,9 +312,9 @@ class GeoJSONWorkerLayer extends Layer {
var transferrables = [];
var transferrablesSize = 0;
var vertices = [];
var positions = [];
var normals = [];
var colours = [];
var colors = [];
// var pickingIds = [];
var properties = [];
@ -364,14 +339,9 @@ class GeoJSONWorkerLayer extends Layer {
for (var j = 0; j < result.attributes.length; j++) {
attributes = result.attributes[j];
vertices.push(attributes.vertices);
positions.push(attributes.positions);
normals.push(attributes.normals);
colours.push(attributes.colours);
// WORKERS: Handle interaction back in the main thread
// if (attributes.pickingIds) {
// pickingIds.push(attributes.pickingIds);
// }
colors.push(attributes.colors);
if (_properties) {
properties.push(Buffer.stringToUint8Array(JSON.stringify(polygon.properties)));
@ -380,19 +350,19 @@ class GeoJSONWorkerLayer extends Layer {
};
var mergedAttributes = {
vertices: Buffer.mergeFloat32Arrays(vertices),
positions: Buffer.mergeFloat32Arrays(positions),
normals: Buffer.mergeFloat32Arrays(normals),
colours: Buffer.mergeFloat32Arrays(colours)
colors: Buffer.mergeFloat32Arrays(colors)
};
transferrables.push(mergedAttributes.vertices[0].buffer);
transferrables.push(mergedAttributes.vertices[1].buffer);
transferrables.push(mergedAttributes.positions[0].buffer);
transferrables.push(mergedAttributes.positions[1].buffer);
transferrables.push(mergedAttributes.normals[0].buffer);
transferrables.push(mergedAttributes.normals[1].buffer);
transferrables.push(mergedAttributes.colours[0].buffer);
transferrables.push(mergedAttributes.colours[1].buffer);
transferrables.push(mergedAttributes.colors[0].buffer);
transferrables.push(mergedAttributes.colors[1].buffer);
var mergedProperties;
if (_properties) {
@ -402,13 +372,6 @@ class GeoJSONWorkerLayer extends Layer {
transferrables.push(mergedProperties[1].buffer);
}
// WORKERS: Handle interaction back in the main thread
// if (pickingIds.length > 0) {
// mergedAttributes.pickingIds = Buffer.mergeFloat32Arrays(pickingIds);
// transferrables.push(mergedAttributes.pickingIds[0].buffer);
// transferrables.push(mergedAttributes.pickingIds[1].buffer);
// }
var output = {
attributes: mergedAttributes,
flats: flats
@ -452,70 +415,7 @@ class GeoJSONWorkerLayer extends Layer {
//
// Could make this an abstract method for each geometry layer
_setPolygonMesh(attributes, attributeLengths, style, flat) {
var geometry = new THREE.BufferGeometry();
for (var key in attributes) {
geometry.addAttribute(key.slice(0, -1), new THREE.BufferAttribute(attributes[key], attributeLengths[key]));
}
geometry.computeBoundingBox();
// Temporary until the above style logic is fixed for workers
// var style = extend({}, GeoJSON.defaultStyle);
var material;
if (this._options.polygonMaterial && this._options.polygonMaterial instanceof THREE.Material) {
material = this._options.polygonMaterial;
} else if (!this._world._environment._skybox) {
material = new THREE.MeshPhongMaterial({
vertexColors: THREE.VertexColors,
side: THREE.BackSide,
transparent: style.transparent,
opacity: style.opacity,
blending: style.blending
});
} else {
material = new THREE.MeshStandardMaterial({
vertexColors: THREE.VertexColors,
side: THREE.BackSide,
transparent: style.transparent,
opacity: style.opacity,
blending: style.blending
});
material.roughness = 1;
material.metalness = 0.1;
material.envMapIntensity = 3;
material.envMap = this._world._environment._skybox.getRenderTarget();
}
var mesh;
// Pass mesh through callback, if defined
if (typeof this._options.onPolygonMesh === 'function') {
mesh = this._options.onPolygonMesh(geometry, material);
} else {
mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
mesh.receiveShadow = true;
}
if (flat) {
material.depthWrite = false;
mesh.renderOrder = 1;
}
if (this._options.interactive && this._pickingMesh) {
material = new PickingMaterial();
material.side = THREE.BackSide;
var pickingMesh = new THREE.Mesh(geometry, material);
this._pickingMesh.add(pickingMesh);
this.addToPicking(this._pickingMesh);
}
this._polygonMesh = mesh;
return PolygonLayer.SetMesh(attributes, attributeLengths, flat, style, this._options, this._world._environment._skybox);
}
// Set up and re-emit interaction events

Wyświetl plik

@ -57,34 +57,57 @@ class PolygonLayer extends Layer {
}
_onAdd(world) {
this._setCoordinates();
return new Promise((resolve, reject) => {
this._setCoordinates();
if (this._options.interactive) {
// Only add to picking mesh if this layer is controlling output
//
// Otherwise, assume another component will eventually add a mesh to
// the picking scene
if (this.isOutput()) {
this._pickingMesh = new THREE.Object3D();
this.addToPicking(this._pickingMesh);
if (this._options.interactive) {
// Only add to picking mesh if this layer is controlling output
//
// Otherwise, assume another component will eventually add a mesh to
// the picking scene
if (this.isOutput()) {
this._pickingMesh = new THREE.Object3D();
this.addToPicking(this._pickingMesh);
}
this._setPickingId();
this._addPickingEvents();
}
this._setPickingId();
this._addPickingEvents();
}
PolygonLayer.SetBufferAttributes(this._projectedCoordinates, this._options).then((result) => {
this._bufferAttributes = Buffer.mergeAttributes(result.attributes);
this._flat = result.flat;
// Store geometry representation as instances of THREE.BufferAttribute
this._setBufferAttributes();
var attributeLengths = {
positions: 3,
normals: 3,
colors: 3
};
if (this.isOutput()) {
// Set mesh if not merging elsewhere
this._setMesh(this._bufferAttributes);
if (this._options.interactive) {
attributeLengths.pickingIds = 1;
}
// Output mesh
this.add(this._mesh);
}
if (this.isOutput()) {
var style = this._options.style;
return Promise.resolve(this);
// Set mesh if not merging elsewhere
PolygonLayer.SetMesh(this._bufferAttributes, attributeLengths, this._flat, style, this._options, this._world._environment._skybox).then((result) => {
// Output mesh
this.add(result.mesh);
if (result.pickingMesh) {
this._pickingMesh.add(pickingMesh);
}
});
}
result.attributes = null;
result = null;
resolve(this);
}).catch(reject);
});
}
// Return center of polygon as a LatLon
@ -117,122 +140,6 @@ class PolygonLayer extends Layer {
}
// Create and store reference to THREE.BufferAttribute data for this layer
//
// TODO: Remove this and instead use the SetBufferAttributes static method
_setBufferAttributes() {
var attributes;
// Only use this if you know what you're doing
if (typeof this._options.onBufferAttributes === 'function') {
// TODO: Probably want to pass something less general as arguments,
// though passing the instance will do for now (it's everything)
attributes = this._options.onBufferAttributes(this);
} else {
var height = 0;
// Convert height into world units
if (this._options.style.height && this._options.style.height !== 0) {
height = this._world.metresToWorld(this._options.style.height, this._pointScale);
}
var colour = new THREE.Color();
colour.set(this._options.style.color);
// Light and dark colours used for poor-mans AO gradient on object sides
var light = new THREE.Color(0xffffff);
var shadow = new THREE.Color(0x666666);
// For each polygon
attributes = this._projectedCoordinates.map(_projectedCoordinates => {
// Convert coordinates to earcut format
var _earcut = PolygonLayer.ToEarcut(_projectedCoordinates);
// Triangulate faces using earcut
var faces = PolygonLayer.Triangulate(_earcut.vertices, _earcut.holes, _earcut.dimensions);
var groupedVertices = [];
for (i = 0, il = _earcut.vertices.length; i < il; i += _earcut.dimensions) {
groupedVertices.push(_earcut.vertices.slice(i, i + _earcut.dimensions));
}
var extruded = extrudePolygon(groupedVertices, faces, {
bottom: 0,
top: height
});
var topColor = colour.clone().multiply(light);
var bottomColor = colour.clone().multiply(shadow);
var _vertices = extruded.positions;
var _faces = [];
var _colours = [];
var _colour;
extruded.top.forEach((face, fi) => {
_colour = [];
_colour.push([colour.r, colour.g, colour.b]);
_colour.push([colour.r, colour.g, colour.b]);
_colour.push([colour.r, colour.g, colour.b]);
_faces.push(face);
_colours.push(_colour);
});
this._flat = true;
if (extruded.sides) {
this._flat = false;
// Set up colours for every vertex with poor-mans AO on the sides
extruded.sides.forEach((face, fi) => {
_colour = [];
// First face is always bottom-bottom-top
if (fi % 2 === 0) {
_colour.push([bottomColor.r, bottomColor.g, bottomColor.b]);
_colour.push([bottomColor.r, bottomColor.g, bottomColor.b]);
_colour.push([topColor.r, topColor.g, topColor.b]);
// Reverse winding for the second face
// top-top-bottom
} else {
_colour.push([topColor.r, topColor.g, topColor.b]);
_colour.push([topColor.r, topColor.g, topColor.b]);
_colour.push([bottomColor.r, bottomColor.g, bottomColor.b]);
}
_faces.push(face);
_colours.push(_colour);
});
}
// Skip bottom as there's no point rendering it
// allFaces.push(extruded.faces);
var polygon = {
vertices: _vertices,
faces: _faces,
colours: _colours,
facesCount: _faces.length
};
if (this._options.interactive && this._pickingId) {
// Inject picking ID
polygon.pickingId = this._pickingId;
}
// Convert polygon representation to proper attribute arrays
return PolygonLayer.ToAttributes(polygon);
});
}
this._bufferAttributes = Buffer.mergeAttributes(attributes);
// Original attributes are no longer required so free the memory
attributes = null;
}
// TODO: Ensure that this has feature parity with the non-static method
static SetBufferAttributes(coordinates, options) {
return new Promise((resolve) => {
var height = 0;
@ -362,53 +269,45 @@ class PolygonLayer extends Layer {
this._projectedCoordinates = null;
}
// Create and store mesh from buffer attributes
//
// This is only called if the layer is controlling its own output
_setMesh(attributes) {
static SetMesh(attributes, attributeLengths, flat, style, options, skybox) {
var geometry = new THREE.BufferGeometry();
// itemSize = 3 because there are 3 values (components) per vertex
geometry.addAttribute('position', new THREE.BufferAttribute(attributes.vertices, 3));
geometry.addAttribute('normal', new THREE.BufferAttribute(attributes.normals, 3));
geometry.addAttribute('color', new THREE.BufferAttribute(attributes.colours, 3));
if (attributes.pickingIds) {
geometry.addAttribute('pickingId', new THREE.BufferAttribute(attributes.pickingIds, 1));
for (var key in attributes) {
geometry.addAttribute(key.slice(0, -1), new THREE.BufferAttribute(attributes[key], attributeLengths[key]));
}
geometry.computeBoundingBox();
var material;
if (this._options.material && this._options.material instanceof THREE.Material) {
material = this._options.material;
} else if (!this._world._environment._skybox) {
if (options.polygonMaterial && options.polygonMaterial instanceof THREE.Material) {
material = options.polygonMaterial;
} else if (!skybox) {
material = new THREE.MeshPhongMaterial({
vertexColors: THREE.VertexColors,
side: THREE.BackSide,
transparent: this._options.style.transparent,
opacity: this._options.style.opacity,
blending: this._options.style.blending
transparent: style.transparent,
opacity: style.opacity,
blending: style.blending
});
} else {
material = new THREE.MeshStandardMaterial({
vertexColors: THREE.VertexColors,
side: THREE.BackSide,
transparent: this._options.style.transparent,
opacity: this._options.style.opacity,
blending: this._options.style.blending
transparent: style.transparent,
opacity: style.opacity,
blending: style.blending
});
material.roughness = 1;
material.metalness = 0.1;
material.envMapIntensity = 3;
material.envMap = this._world._environment._skybox.getRenderTarget();
material.envMap = skybox.getRenderTarget();
}
var mesh;
// Pass mesh through callback, if defined
if (typeof this._options.onMesh === 'function') {
mesh = this._options.onMesh(geometry, material);
if (typeof options.onPolygonMesh === 'function') {
mesh = options.onPolygonMesh(geometry, material);
} else {
mesh = new THREE.Mesh(geometry, material);
@ -416,20 +315,22 @@ class PolygonLayer extends Layer {
mesh.receiveShadow = true;
}
if (this.isFlat()) {
if (flat) {
material.depthWrite = false;
mesh.renderOrder = 1;
}
if (this._options.interactive && this._pickingMesh) {
if (options.interactive) {
material = new PickingMaterial();
material.side = THREE.BackSide;
var pickingMesh = new THREE.Mesh(geometry, material);
this._pickingMesh.add(pickingMesh);
}
this._mesh = mesh;
return Promise.resolve({
mesh: mesh,
pickingMesh: pickingMesh
});
}
// Convert and project coordinates
@ -531,7 +432,7 @@ class PolygonLayer extends Layer {
// TODO: Can this be simplified? It's messy and huge
static ToAttributes(polygon) {
// Three components per vertex per face (3 x 3 = 9)
var vertices = new Float32Array(polygon.facesCount * 9);
var positions = new Float32Array(polygon.facesCount * 9);
var normals = new Float32Array(polygon.facesCount * 9);
var colours = new Float32Array(polygon.facesCount * 9);
@ -603,9 +504,9 @@ class PolygonLayer extends Layer {
var ny = cb.y;
var nz = cb.z;
vertices[lastIndex * 9 + 0] = ax;
vertices[lastIndex * 9 + 1] = ay;
vertices[lastIndex * 9 + 2] = az;
positions[lastIndex * 9 + 0] = ax;
positions[lastIndex * 9 + 1] = ay;
positions[lastIndex * 9 + 2] = az;
normals[lastIndex * 9 + 0] = nx;
normals[lastIndex * 9 + 1] = ny;
@ -615,9 +516,9 @@ class PolygonLayer extends Layer {
colours[lastIndex * 9 + 1] = c1[1];
colours[lastIndex * 9 + 2] = c1[2];
vertices[lastIndex * 9 + 3] = bx;
vertices[lastIndex * 9 + 4] = by;
vertices[lastIndex * 9 + 5] = bz;
positions[lastIndex * 9 + 3] = bx;
positions[lastIndex * 9 + 4] = by;
positions[lastIndex * 9 + 5] = bz;
normals[lastIndex * 9 + 3] = nx;
normals[lastIndex * 9 + 4] = ny;
@ -627,9 +528,9 @@ class PolygonLayer extends Layer {
colours[lastIndex * 9 + 4] = c2[1];
colours[lastIndex * 9 + 5] = c2[2];
vertices[lastIndex * 9 + 6] = cx;
vertices[lastIndex * 9 + 7] = cy;
vertices[lastIndex * 9 + 8] = cz;
positions[lastIndex * 9 + 6] = cx;
positions[lastIndex * 9 + 7] = cy;
positions[lastIndex * 9 + 8] = cz;
normals[lastIndex * 9 + 6] = nx;
normals[lastIndex * 9 + 7] = ny;
@ -649,9 +550,9 @@ class PolygonLayer extends Layer {
}
var attributes = {
vertices: vertices,
positions: positions,
normals: normals,
colours: colours
colors: colours
};
if (pickingIds) {

Wyświetl plik

@ -1,5 +1,6 @@
import Tile from './Tile';
import {geoJSONLayer as GeoJSONLayer} from '../GeoJSONLayer';
import {geoJSONWorkerLayer as GeoJSONWorkerLayer} from '../GeoJSONWorkerLayer';
import BoxHelper from '../../vendor/BoxHelper';
import THREE from 'three';
import reqwest from 'reqwest';
@ -51,6 +52,7 @@ class GeoJSONTile extends Tile {
this._defaultStyle = GeoJSON.defaultStyle;
var defaults = {
workers: false,
output: true,
outputToScene: false,
interactive: false,
@ -231,27 +233,33 @@ class GeoJSONTile extends Tile {
var url = this._getTileURL(urlParams);
this._request = reqwest({
url: url,
type: 'json',
crossOrigin: true
}).then(res => {
// Clear request reference
this._request = null;
this._processTileData(res);
}).catch(err => {
console.error(err);
if (!this._options.workers) {
this._request = reqwest({
url: url,
type: 'json',
crossOrigin: true
}).then(res => {
// Clear request reference
this._request = null;
this._processTileData(res);
}).catch(err => {
console.error(err);
// Clear request reference
this._request = null;
});
// Clear request reference
this._request = null;
});
} else {
this._processTileData(url);
}
}
_processTileData(data) {
console.time(this._tile);
var GeoJSONClass = (!this._options.workers) ? GeoJSONLayer : GeoJSONWorkerLayer;
// Using this creates a huge amount of memory due to the quantity of tiles
this._geojsonLayer = GeoJSONLayer(data, this._options);
this._geojsonLayer = GeoJSONClass(data, this._options);
this._geojsonLayer.addTo(this._world).then(() => {
this._mesh = this._geojsonLayer._object3D;
this._pickingMesh = this._geojsonLayer._pickingMesh;
@ -259,7 +267,9 @@ class GeoJSONTile extends Tile {
// Free the GeoJSON memory as we don't need it
//
// TODO: This should probably be a method within GeoJSONLayer
this._geojsonLayer._geojson = null;
if (this._geojsonLayer._geojson) {
this._geojsonLayer._geojson = null;
}
// TODO: Fix or store shadow canvas stuff and get rid of this code
// Draw footprint on shadow canvas

Wyświetl plik

@ -38,7 +38,8 @@ class GeoJSONTileLayer extends TileLayer {
constructor(path, options) {
var defaults = {
maxLOD: 14,
distance: 30000
distance: 30000,
workers: false
};
options = extend({}, defaults, options);
@ -78,7 +79,7 @@ class GeoJSONTileLayer extends TileLayer {
// Update and output tiles each frame (throttled)
_onWorldUpdate() {
if (this._pauseOutput) {
if (this._pauseOutput || this._disableOutput) {
return;
}
@ -87,38 +88,24 @@ class GeoJSONTileLayer extends TileLayer {
// Update tiles grid after world move, but don't output them
_onWorldMove(latlon, point) {
if (this._disableOutput) {
return;
}
this._pauseOutput = false;
this._calculateLOD();
}
// Pause updates during control movement for less visual jank
_onControlsMove() {
if (this._disableOutput) {
return;
}
this._pauseOutput = true;
}
_createTile(quadcode, layer) {
var options = {};
// if (this._options.filter) {
// options.filter = this._options.filter;
// }
//
// if (this._options.style) {
// options.style = this._options.style;
// }
//
// if (this._options.topojson) {
// options.topojson = true;
// }
//
// if (this._options.interactive) {
// options.interactive = true;
// }
//
// if (this._options.onClick) {
// options.onClick = this._options.onClick;
// }
return new GeoJSONTile(quadcode, this._path, layer, this._options);
}

Wyświetl plik

@ -1,341 +0,0 @@
import Tile from './Tile';
import {geoJSONWorkerLayer as GeoJSONWorkerLayer} from '../GeoJSONWorkerLayer';
import BoxHelper from '../../vendor/BoxHelper';
import THREE from 'three';
import reqwest from 'reqwest';
import {point as Point} from '../../geo/Point';
import {latLon as LatLon} from '../../geo/LatLon';
import extend from 'lodash.assign';
// import Offset from 'polygon-offset';
import GeoJSON from '../../util/GeoJSON';
import Buffer from '../../util/Buffer';
import PickingMaterial from '../../engine/PickingMaterial';
// TODO: Map picking IDs to some reference within the tile data / geometry so
// that something useful can be done when an object is picked / clicked on
// TODO: Make sure nothing is left behind in the heap after calling destroy()
// TODO: Perform tile request and processing in a Web Worker
//
// Use Operative (https://github.com/padolsey/operative)
//
// Would it make sense to have the worker functionality defined in a static
// method so it only gets initialised once and not on every tile instance?
//
// Otherwise, worker processing logic would have to go in the tile layer so not
// to waste loads of time setting up a brand new worker with three.js for each
// tile every single time.
//
// Unsure of the best way to get three.js and VIZI into the worker
//
// Would need to set up a CRS / projection identical to the world instance
//
// Is it possible to bypass requirements on external script by having multiple
// simple worker methods that each take enough inputs to perform a single task
// without requiring VIZI or three.js? So long as the heaviest logic is done in
// the worker and transferrable objects are used then it should be better than
// nothing. Would probably still need things like earcut...
//
// After all, the three.js logic and object creation will still need to be
// done on the main thread regardless so the worker should try to do as much as
// possible with as few dependencies as possible.
//
// Have a look at how this is done in Tangram before implementing anything as
// the approach there is pretty similar and robust.
class GeoJSONWorkerTile extends Tile {
constructor(quadcode, path, layer, options) {
super(quadcode, path, layer);
this._defaultStyle = GeoJSON.defaultStyle;
var defaults = {
output: true,
outputToScene: false,
interactive: false,
topojson: false,
filter: null,
onEachFeature: null,
polygonMaterial: null,
onPolygonMesh: null,
onPolygonBufferAttributes: null,
polylineMaterial: null,
onPolylineMesh: null,
onPolylineBufferAttributes: null,
pointGeometry: null,
pointMaterial: null,
onPointMesh: null,
style: GeoJSON.defaultStyle,
keepFeatures: false
};
var _options = extend({}, defaults, options);
if (typeof options.style === 'function') {
_options.style = options.style;
} else {
_options.style = extend({}, defaults.style, options.style);
}
this._options = _options;
}
// Request data for the tile
requestTileAsync() {
// Making this asynchronous really speeds up the LOD framerate
setTimeout(() => {
if (!this._mesh) {
this._mesh = this._createMesh();
// this._shadowCanvas = this._createShadowCanvas();
this._requestTile();
}
}, 0);
}
// TODO: Destroy GeoJSONLayer
destroy() {
// Cancel any pending requests
this._abortRequest();
// Clear request reference
this._request = null;
if (this._geojsonLayer) {
this._geojsonLayer.destroy();
this._geojsonLayer = null;
}
this._mesh = null;
// TODO: Properly dispose of picking mesh
this._pickingMesh = null;
super.destroy();
}
_createMesh() {
// Something went wrong and the tile
//
// Possibly removed by the cache before loaded
if (!this._center) {
return;
}
var mesh = new THREE.Object3D();
// mesh.add(this._createDebugMesh());
return mesh;
}
_createDebugMesh() {
var canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
var context = canvas.getContext('2d');
context.font = 'Bold 20px Helvetica Neue, Verdana, Arial';
context.fillStyle = '#ff0000';
context.fillText(this._quadcode, 20, canvas.width / 2 - 5);
context.fillText(this._tile.toString(), 20, canvas.width / 2 + 25);
var texture = new THREE.Texture(canvas);
// Silky smooth images when tilted
texture.magFilter = THREE.LinearFilter;
texture.minFilter = THREE.LinearMipMapLinearFilter;
// TODO: Set this to renderer.getMaxAnisotropy() / 4
texture.anisotropy = 4;
texture.needsUpdate = true;
var material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
depthWrite: false
});
var geom = new THREE.PlaneBufferGeometry(this._side, this._side, 1);
var mesh = new THREE.Mesh(geom, material);
mesh.rotation.x = -90 * Math.PI / 180;
mesh.position.y = 0.1;
return mesh;
}
// _createShadowCanvas() {
// var canvas = document.createElement('canvas');
//
// // Rendered at a low resolution and later scaled up for a low-quality blur
// canvas.width = 512;
// canvas.height = 512;
//
// return canvas;
// }
// _addShadow(coordinates) {
// var ctx = this._shadowCanvas.getContext('2d');
// var width = this._shadowCanvas.width;
// var height = this._shadowCanvas.height;
//
// var _coords;
// var _offset;
// var offset = new Offset();
//
// // Transform coordinates to shadowCanvas space and draw on canvas
// coordinates.forEach((ring, index) => {
// ctx.beginPath();
//
// _coords = ring.map(coord => {
// var xFrac = (coord[0] - this._boundsWorld[0]) / this._side;
// var yFrac = (coord[1] - this._boundsWorld[3]) / this._side;
// return [xFrac * width, yFrac * height];
// });
//
// if (index > 0) {
// _offset = _coords;
// } else {
// _offset = offset.data(_coords).padding(1.3);
// }
//
// // TODO: This is super flaky and crashes the browser if run on anything
// // put the outer ring (potentially due to winding)
// _offset.forEach((coord, index) => {
// // var xFrac = (coord[0] - this._boundsWorld[0]) / this._side;
// // var yFrac = (coord[1] - this._boundsWorld[3]) / this._side;
//
// if (index === 0) {
// ctx.moveTo(coord[0], coord[1]);
// } else {
// ctx.lineTo(coord[0], coord[1]);
// }
// });
//
// ctx.closePath();
// });
//
// ctx.fillStyle = 'rgba(80, 80, 80, 0.7)';
// ctx.fill();
// }
_requestTile() {
var urlParams = {
x: this._tile[0],
y: this._tile[1],
z: this._tile[2]
};
var url = this._getTileURL(urlParams);
// this._request = reqwest({
// url: url,
// type: 'json',
// crossOrigin: true
// }).then(res => {
// // Clear request reference
// this._request = null;
// this._processTileData(res);
// }).catch(err => {
// console.error(err);
// // Clear request reference
// this._request = null;
// });
this._processTileData(url);
}
_processTileData(data) {
console.time(this._tile);
// Using this creates a huge amount of memory due to the quantity of tiles
var geojsonLayer = GeoJSONWorkerLayer(data, this._options);
geojsonLayer.addTo(this._world).then(() => {
this._geojsonLayer = geojsonLayer;
this._mesh = this._geojsonLayer._object3D;
this._pickingMesh = this._geojsonLayer._pickingMesh;
// Free the GeoJSON memory as we don't need it
//
// TODO: This should probably be a method within GeoJSONLayer
// WORKERS: Disabled for now as it's needed with sync promises
// this._geojsonLayer._geojson = null;
// TODO: Fix or store shadow canvas stuff and get rid of this code
// Draw footprint on shadow canvas
//
// TODO: Disabled for the time-being until it can be sped up / moved to
// a worker
// this._addShadow(coordinates);
// Output shadow canvas
// TODO: Disabled for the time-being until it can be sped up / moved to
// a worker
// var texture = new THREE.Texture(this._shadowCanvas);
//
// // Silky smooth images when tilted
// texture.magFilter = THREE.LinearFilter;
// texture.minFilter = THREE.LinearMipMapLinearFilter;
//
// // TODO: Set this to renderer.getMaxAnisotropy() / 4
// texture.anisotropy = 4;
//
// texture.needsUpdate = true;
//
// var material;
// if (!this._world._environment._skybox) {
// material = new THREE.MeshBasicMaterial({
// map: texture,
// transparent: true,
// depthWrite: false
// });
// } else {
// material = new THREE.MeshStandardMaterial({
// map: texture,
// transparent: true,
// depthWrite: false
// });
// material.roughness = 1;
// material.metalness = 0.1;
// material.envMap = this._world._environment._skybox.getRenderTarget();
// }
//
// var geom = new THREE.PlaneBufferGeometry(this._side, this._side, 1);
// var mesh = new THREE.Mesh(geom, material);
//
// mesh.castShadow = false;
// mesh.receiveShadow = false;
// mesh.renderOrder = 1;
//
// mesh.rotation.x = -90 * Math.PI / 180;
//
// this._mesh.add(mesh);
this._ready = true;
console.timeEnd(this._tile);
});
}
_abortRequest() {
if (!this._request) {
return;
}
this._request.abort();
}
}
export default GeoJSONWorkerTile;
var noNew = function(quadcode, path, layer, options) {
return new GeoJSONWorkerTile(quadcode, path, layer, options);
};
// Initialise without requiring new keyword
export {noNew as geoJSONWorkerTile};

Wyświetl plik

@ -1,158 +0,0 @@
import TileLayer from './TileLayer';
import extend from 'lodash.assign';
import GeoJSONWorkerTile from './GeoJSONWorkerTile';
import throttle from 'lodash.throttle';
import THREE from 'three';
// TODO: Offer on-the-fly slicing of static, non-tile-based GeoJSON files into a
// tile grid using geojson-vt
//
// See: https://github.com/mapbox/geojson-vt
// TODO: Make sure nothing is left behind in the heap after calling destroy()
// TODO: Consider pausing per-frame output during movement so there's little to
// no jank caused by previous tiles still processing
// This tile layer only updates the quadtree after world movement has occurred
//
// Tiles from previous quadtree updates are updated and outputted every frame
// (or at least every frame, throttled to some amount)
//
// This is because the complexity of TopoJSON tiles requires a lot of processing
// and so makes movement janky if updates occur every frame – only updating
// after movement means frame drops are less obvious due to heavy processing
// occurring while the view is generally stationary
//
// The downside is that until new tiles are requested and outputted you will
// see blank spaces as you orbit and move around
//
// An added benefit is that it dramatically reduces the number of tiles being
// requested over a period of time and the time it takes to go from request to
// screen output
//
// It may be possible to perform these updates per-frame once Web Worker
// processing is added
class GeoJSONWorkerTileLayer extends TileLayer {
constructor(path, options) {
var defaults = {
maxLOD: 14,
distance: 30000
};
options = extend({}, defaults, options);
super(options);
this._path = path;
}
_onAdd(world) {
return super._onAdd(world).then(() => {
// Trigger initial quadtree calculation on the next frame
//
// TODO: This is a hack to ensure the camera is all set up - a better
// solution should be found
setTimeout(() => {
this._calculateLOD();
this._initEvents();
}, 0);
});
}
_initEvents() {
// Run LOD calculations based on render calls
//
// Throttled to 1 LOD calculation per 100ms
this._throttledWorldUpdate = throttle(this._onWorldUpdate, 100);
this._world.on('preUpdate', this._throttledWorldUpdate, this);
this._world.on('move', this._onWorldMove, this);
this._world.on('controlsMove', this._onControlsMove, this);
}
// Update and output tiles each frame (throttled)
_onWorldUpdate() {
if (this._pauseOutput || this._disableOutput) {
return;
}
this._outputTiles();
}
// Update tiles grid after world move, but don't output them
_onWorldMove(latlon, point) {
if (this._disableOutput) {
return;
}
this._pauseOutput = false;
this._calculateLOD();
}
// Pause updates during control movement for less visual jank
_onControlsMove() {
if (this._disableOutput) {
return;
}
this._pauseOutput = true;
}
_createTile(quadcode, layer) {
var options = {};
// if (this._options.filter) {
// options.filter = this._options.filter;
// }
//
// if (this._options.style) {
// options.style = this._options.style;
// }
//
// if (this._options.topojson) {
// options.topojson = true;
// }
//
// if (this._options.interactive) {
// options.interactive = true;
// }
//
// if (this._options.onClick) {
// options.onClick = this._options.onClick;
// }
var workerTile = new GeoJSONWorkerTile(quadcode, this._path, layer, this._options);
// workerTile.on('click', (properties, point2d, point3d, intersects) => {
// console.log(properties, point2d, point3d, intersects);
// });
// workerTile.on('hover', (properties, point2d, point3d, intersects) => {
// });
return workerTile;
}
// Destroys the layer and removes it from the scene and memory
destroy() {
this._world.off('preUpdate', this._throttledWorldUpdate);
this._world.off('move', this._onWorldMove);
this._throttledWorldUpdate = null;
// Run common destruction logic from parent
super.destroy();
}
}
export default GeoJSONWorkerTileLayer;
var noNew = function(path, options) {
return new GeoJSONWorkerTileLayer(path, options);
};
// Initialise without requiring new keyword
export {noNew as geoJSONWorkerTileLayer};

Wyświetl plik

@ -1,22 +0,0 @@
import GeoJSONWorkerTileLayer from './GeoJSONWorkerTileLayer';
import extend from 'lodash.assign';
class TopoJSONWorkerTileLayer extends GeoJSONWorkerTileLayer {
constructor(path, options) {
var defaults = {
topojson: true
};
options = extend({}, defaults, options);
super(path, options);
}
}
export default TopoJSONWorkerTileLayer;
var noNew = function(path, options) {
return new TopoJSONWorkerTileLayer(path, options);
};
export {noNew as topoJSONWorkerTileLayer};

Wyświetl plik

@ -8,8 +8,6 @@ import EnvironmentLayer, {environmentLayer} from './layer/environment/Environmen
import ImageTileLayer, {imageTileLayer} from './layer/tile/ImageTileLayer';
import GeoJSONTileLayer, {geoJSONTileLayer} from './layer/tile/GeoJSONTileLayer';
import TopoJSONTileLayer, {topoJSONTileLayer} from './layer/tile/TopoJSONTileLayer';
import GeoJSONWorkerTileLayer, {geoJSONWorkerTileLayer} from './layer/tile/GeoJSONWorkerTileLayer';
import TopoJSONWorkerTileLayer, {topoJSONWorkerTileLayer} from './layer/tile/TopoJSONWorkerTileLayer';
import GeoJSONLayer, {geoJSONLayer} from './layer/GeoJSONLayer';
import TopoJSONLayer, {topoJSONLayer} from './layer/TopoJSONLayer';
import GeoJSONWorkerLayer, {geoJSONWorkerLayer} from './layer/GeoJSONWorkerLayer';
@ -43,10 +41,6 @@ const VIZI = {
geoJSONTileLayer: geoJSONTileLayer,
TopoJSONTileLayer: TopoJSONTileLayer,
topoJSONTileLayer: topoJSONTileLayer,
GeoJSONWorkerTileLayer: GeoJSONWorkerTileLayer,
geoJSONWorkerTileLayer: geoJSONWorkerTileLayer,
TopoJSONWorkerTileLayer: TopoJSONWorkerTileLayer,
topoJSONWorkerTileLayer: topoJSONWorkerTileLayer,
GeoJSONLayer: GeoJSONLayer,
geoJSONLayer: geoJSONLayer,
TopoJSONLayer: TopoJSONLayer,