vizicities/src/layer/geometry/PolygonLayer.js

702 wiersze
19 KiB
JavaScript
Executable File

// TODO: Move duplicated logic between geometry layrs into GeometryLayer
// TODO: Look at ways to drop unneeded references to array buffers, etc to
// reduce memory footprint
// TODO: Support dynamic updating / hiding / animation of geometry
//
// This could be pretty hard as it's all packed away within BufferGeometry and
// may even be merged by another layer (eg. GeoJSONLayer)
//
// How much control should this layer support? Perhaps a different or custom
// layer would be better suited for animation, for example.
// TODO: Allow _setBufferAttributes to use a custom function passed in to
// generate a custom mesh
import Layer from '../Layer';
import extend from 'lodash.assign';
import THREE from 'three';
import Geo from '../../geo/Geo';
import {latLon as LatLon} from '../../geo/LatLon';
import {point as Point} from '../../geo/Point';
import earcut from 'earcut';
import extrudePolygon from '../../util/extrudePolygon';
import PickingMaterial from '../../engine/PickingMaterial';
import Buffer from '../../util/Buffer';
class PolygonLayer extends Layer {
constructor(coordinates, options) {
var defaults = {
output: true,
interactive: false,
// Custom material override
//
// TODO: Should this be in the style object?
polygonMaterial: null,
onPolygonMesh: null,
onBufferAttributes: null,
// This default style is separate to Util.GeoJSON.defaultStyle
style: {
color: '#ffffff',
transparent: false,
opacity: 1,
blending: THREE.NormalBlending,
height: 0
}
};
var _options = extend({}, defaults, options);
super(_options);
// Return coordinates as array of polygons so it's easy to support
// MultiPolygon features (a single polygon would be a MultiPolygon with a
// single polygon in the array)
this._coordinates = (PolygonLayer.isSingle(coordinates)) ? [coordinates] : coordinates;
}
_onAdd(world) {
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);
}
this._setPickingId();
this._addPickingEvents();
}
PolygonLayer.SetBufferAttributes(this._projectedCoordinates, this._options).then((result) => {
this._bufferAttributes = Buffer.mergeAttributes(result.attributes);
if (result.outlineAttributes.length > 0) {
this._outlineBufferAttributes = Buffer.mergeAttributes(result.outlineAttributes);
}
this._flat = result.flat;
if (this.isOutput()) {
var attributeLengths = {
positions: 3,
normals: 3,
colors: 3,
tops: 1
};
if (this._options.interactive) {
attributeLengths.pickingIds = 1;
}
var style = this._options.style;
// 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(result.pickingMesh);
}
});
}
result.attributes = null;
result.outlineAttributes = null;
result = null;
resolve(this);
}).catch(reject);
});
}
// Return center of polygon as a LatLon
//
// This is used for things like placing popups / UI elements on the layer
//
// TODO: Find proper center position instead of returning first coordinate
// SEE: https://github.com/Leaflet/Leaflet/blob/master/src/layer/vector/Polygon.js#L15
getCenter() {
return this._center;
}
// Return polygon bounds in geographic coordinates
//
// TODO: Implement getBounds()
getBounds() {}
// Get unique ID for picking interaction
_setPickingId() {
this._pickingId = this.getPickingId();
}
// Set up and re-emit interaction events
_addPickingEvents() {
// TODO: Find a way to properly remove this listener on destroy
this._world.on('pick-' + this._pickingId, (point2d, point3d, intersects) => {
// Re-emit click event from the layer
this.emit('click', this, point2d, point3d, intersects);
});
}
// Create and store reference to THREE.BufferAttribute data for this layer
static SetBufferAttributes(coordinates, options) {
return new Promise((resolve) => {
var height = 0;
// Convert height into world units
if (options.style.height && options.style.height !== 0) {
height = Geo.metresToWorld(options.style.height, options.pointScale);
}
var colour = new THREE.Color();
colour.set(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);
var flat = true;
var outlineAttributes = [];
// For each polygon
var attributes = coordinates.map(_coordinates => {
// Convert coordinates to earcut format
var _earcut = PolygonLayer.ToEarcut(_coordinates);
// 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 _tops = [];
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]);
_tops.push([true, true, true]);
_faces.push(face);
_colours.push(_colour);
});
if (extruded.sides) {
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]);
_tops.push([false, false, true]);
// 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]);
_tops.push([true, true, false]);
}
_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,
tops: _tops,
facesCount: _faces.length
};
if (options.style.outline) {
var outlineColour = new THREE.Color();
outlineColour.set(options.style.outlineColor || 0x000000);
outlineAttributes.push(PolygonLayer.Set2DOutline(_coordinates, outlineColour));
}
if (options.interactive && options.pickingId) {
// Inject picking ID
polygon.pickingId = options.pickingId;
}
// Convert polygon representation to proper attribute arrays
return PolygonLayer.ToAttributes(polygon);
});
resolve({
attributes: attributes,
outlineAttributes: outlineAttributes,
flat: flat
});
});
}
getBufferAttributes() {
return this._bufferAttributes;
}
getOutlineBufferAttributes() {
return this._outlineBufferAttributes;
}
// Used by external components to clear some memory when the attributes
// are no longer required to be stored in this layer
//
// For example, you would want to clear the attributes here after merging them
// using something like the GeoJSONLayer
clearBufferAttributes() {
this._bufferAttributes = null;
this._outlineBufferAttributes = null;
}
// Threshold angle is currently in rads
static Set2DOutline(coordinates, colour) {
var _vertices = [];
coordinates.forEach((ring) => {
var _ring = ring.map((coordinate) => {
return [coordinate.x, 0, coordinate.y];
});
// Add in duplicate vertices for line segments to work
var verticeCount = _ring.length;
var first = true;
while (--verticeCount) {
if (first || verticeCount === 0) {
first = false;
continue;
}
_ring.splice(verticeCount + 1, 0, _ring[verticeCount]);
}
_vertices = _vertices.concat(_ring);
});
_colour = [colour.r, colour.g, colour.b];
var vertices = new Float32Array(_vertices.length * 3);
var colours = new Float32Array(_vertices.length * 3);
var lastIndex = 0;
for (var i = 0; i < _vertices.length; i++) {
var ax = _vertices[i][0];
var ay = _vertices[i][1];
var az = _vertices[i][2];
var c1 = _colour;
vertices[lastIndex * 3 + 0] = ax;
vertices[lastIndex * 3 + 1] = ay;
vertices[lastIndex * 3 + 2] = az;
colours[lastIndex * 3 + 0] = c1[0];
colours[lastIndex * 3 + 1] = c1[1];
colours[lastIndex * 3 + 2] = c1[2];
lastIndex++;
}
var attributes = {
positions: vertices,
colors: colours
};
return attributes;
}
// Used by external components to clear some memory when the coordinates
// are no longer required to be stored in this layer
//
// For example, you would want to clear the coordinates here after this
// layer is merged in something like the GeoJSONLayer
clearCoordinates() {
this._coordinates = null;
this._projectedCoordinates = null;
}
static SetMesh(attributes, attributeLengths, flat, style, options, skybox) {
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();
var material;
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: 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 = skybox.getRenderTarget();
}
var mesh;
// Pass mesh through callback, if defined
if (typeof options.onPolygonMesh === 'function') {
mesh = options.onPolygonMesh(geometry, material);
} else {
mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
mesh.receiveShadow = true;
}
if (flat || style.renderOrder !== undefined) {
if (!style.ignoreDepth) {
material.depthWrite = false;
}
var renderOrder = (style.renderOrder !== undefined) ? style.renderOrder : 3;
mesh.renderOrder = renderOrder;
}
if (options.interactive) {
material = new PickingMaterial();
material.side = THREE.BackSide;
var pickingMesh = new THREE.Mesh(geometry, material);
}
return Promise.resolve({
mesh: mesh,
pickingMesh: pickingMesh
});
}
// Convert and project coordinates
//
// TODO: Calculate bounds
_setCoordinates() {
this._bounds = [];
this._coordinates = this._convertCoordinates(this._coordinates);
this._projectedBounds = [];
this._projectedCoordinates = this._projectCoordinates();
this._center = this._coordinates[0][0][0];
}
// Recursively convert input coordinates into LatLon objects
//
// Calculate geographic bounds at the same time
//
// TODO: Calculate geographic bounds
_convertCoordinates(coordinates) {
return coordinates.map(_coordinates => {
return _coordinates.map(ring => {
return ring.map(coordinate => {
return LatLon(coordinate[1], coordinate[0]);
});
});
});
}
// Recursively project coordinates into world positions
//
// Calculate world bounds, offset and pointScale at the same time
//
// TODO: Calculate world bounds
_projectCoordinates() {
var point;
return this._coordinates.map(_coordinates => {
return _coordinates.map(ring => {
return ring.map(latlon => {
point = this._world.latLonToPoint(latlon);
// TODO: Is offset ever being used or needed?
if (!this._offset) {
this._offset = Point(0, 0);
this._offset.x = -1 * point.x;
this._offset.y = -1 * point.y;
this._options.pointScale = this._world.pointScale(latlon);
}
return point;
});
});
});
}
// Convert coordinates array to something earcut can understand
static ToEarcut(coordinates) {
var dim = 2;
var result = {vertices: [], holes: [], dimensions: dim};
var holeIndex = 0;
for (var i = 0; i < coordinates.length; i++) {
for (var j = 0; j < coordinates[i].length; j++) {
// for (var d = 0; d < dim; d++) {
result.vertices.push(coordinates[i][j].x);
result.vertices.push(coordinates[i][j].y);
// }
}
if (i > 0) {
holeIndex += coordinates[i - 1].length;
result.holes.push(holeIndex);
}
}
return result;
}
// Triangulate earcut-based input using earcut
static Triangulate(contour, holes, dim) {
// console.time('earcut');
var faces = earcut(contour, holes, dim);
var result = [];
for (i = 0, il = faces.length; i < il; i += 3) {
result.push(faces.slice(i, i + 3));
}
// console.timeEnd('earcut');
return result;
}
// Transform polygon representation into attribute arrays that can be used by
// THREE.BufferGeometry
//
// TODO: Can this be simplified? It's messy and huge
static ToAttributes(polygon) {
// Three components per vertex per face (3 x 3 = 9)
var positions = new Float32Array(polygon.facesCount * 9);
var normals = new Float32Array(polygon.facesCount * 9);
var colours = new Float32Array(polygon.facesCount * 9);
// One component per vertex per face (1 x 3 = 3)
var tops = new Float32Array(polygon.facesCount * 3);
var pickingIds;
if (polygon.pickingId) {
// One component per vertex per face (1 x 3 = 3)
pickingIds = new Float32Array(polygon.facesCount * 3);
}
var pA = new THREE.Vector3();
var pB = new THREE.Vector3();
var pC = new THREE.Vector3();
var cb = new THREE.Vector3();
var ab = new THREE.Vector3();
var index;
var _faces = polygon.faces;
var _vertices = polygon.vertices;
var _colour = polygon.colours;
var _tops = polygon.tops;
var _pickingId;
if (pickingIds) {
_pickingId = polygon.pickingId;
}
var lastIndex = 0;
for (var i = 0; i < _faces.length; i++) {
// Array of vertex indexes for the face
index = _faces[i][0];
var ax = _vertices[index][0];
var ay = _vertices[index][1];
var az = _vertices[index][2];
var c1 = _colour[i][0];
var t1 = _tops[i][0];
index = _faces[i][1];
var bx = _vertices[index][0];
var by = _vertices[index][1];
var bz = _vertices[index][2];
var c2 = _colour[i][1];
var t2 = _tops[i][1];
index = _faces[i][2];
var cx = _vertices[index][0];
var cy = _vertices[index][1];
var cz = _vertices[index][2];
var c3 = _colour[i][2];
var t3 = _tops[i][2];
// Flat face normals
// From: http://threejs.org/examples/webgl_buffergeometry.html
pA.set(ax, ay, az);
pB.set(bx, by, bz);
pC.set(cx, cy, cz);
cb.subVectors(pC, pB);
ab.subVectors(pA, pB);
cb.cross(ab);
cb.normalize();
var nx = cb.x;
var ny = cb.y;
var nz = cb.z;
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;
normals[lastIndex * 9 + 2] = nz;
colours[lastIndex * 9 + 0] = c1[0];
colours[lastIndex * 9 + 1] = c1[1];
colours[lastIndex * 9 + 2] = c1[2];
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;
normals[lastIndex * 9 + 5] = nz;
colours[lastIndex * 9 + 3] = c2[0];
colours[lastIndex * 9 + 4] = c2[1];
colours[lastIndex * 9 + 5] = c2[2];
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;
normals[lastIndex * 9 + 8] = nz;
colours[lastIndex * 9 + 6] = c3[0];
colours[lastIndex * 9 + 7] = c3[1];
colours[lastIndex * 9 + 8] = c3[2];
tops[lastIndex * 3 + 0] = t1;
tops[lastIndex * 3 + 1] = t2;
tops[lastIndex * 3 + 2] = t3;
if (pickingIds) {
pickingIds[lastIndex * 3 + 0] = _pickingId;
pickingIds[lastIndex * 3 + 1] = _pickingId;
pickingIds[lastIndex * 3 + 2] = _pickingId;
}
lastIndex++;
}
var attributes = {
positions: positions,
normals: normals,
colors: colours,
tops: tops
};
if (pickingIds) {
attributes.pickingIds = pickingIds;
}
return attributes;
}
// Returns true if the polygon is flat (has no height)
isFlat() {
return this._flat;
}
// Returns true if coordinates refer to a single geometry
//
// For example, not coordinates for a MultiPolygon GeoJSON feature
static isSingle(coordinates) {
return !Array.isArray(coordinates[0][0][0]);
}
// TODO: Make sure this is cleaning everything
destroy() {
if (this._pickingMesh) {
// TODO: Properly dispose of picking mesh
this._pickingMesh = null;
}
this.clearCoordinates();
this.clearBufferAttributes();
// Run common destruction logic from parent
super.destroy();
}
}
export default PolygonLayer;
var noNew = function(coordinates, options) {
return new PolygonLayer(coordinates, options);
};
export {noNew as polygonLayer};