kopia lustrzana https://github.com/robhawkes/vizicities
630 wiersze
17 KiB
JavaScript
630 wiersze
17 KiB
JavaScript
import Tile from './Tile';
|
|
import BoxHelper from '../../vendor/BoxHelper';
|
|
import THREE from 'three';
|
|
import reqwest from 'reqwest';
|
|
import topojson from 'topojson';
|
|
import Point from '../../geo/Point';
|
|
import LatLon from '../../geo/LatLon';
|
|
import earcut from 'earcut';
|
|
import extend from 'lodash.assign';
|
|
import extrudePolygon from '../../util/extrudePolygon';
|
|
import Offset from 'polygon-offset';
|
|
|
|
// 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 GeoJSONTile extends Tile {
|
|
constructor(quadcode, path, layer, options) {
|
|
super(quadcode, path, layer);
|
|
|
|
var defaults = {
|
|
topojson: false,
|
|
filter: null,
|
|
style: {
|
|
color: '#ffffff'
|
|
}
|
|
};
|
|
|
|
this._options = extend(defaults, 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);
|
|
}
|
|
|
|
destroy() {
|
|
// Cancel any pending requests
|
|
this._abortRequest();
|
|
|
|
// Clear request reference
|
|
this._request = 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.position.x = this._center[0];
|
|
mesh.position.z = this._center[1];
|
|
|
|
// var geom = new THREE.PlaneBufferGeometry(this._side, this._side, 1);
|
|
//
|
|
// var material = new THREE.MeshBasicMaterial({
|
|
// depthWrite: false
|
|
// });
|
|
//
|
|
// var localMesh = new THREE.Mesh(geom, material);
|
|
// localMesh.rotation.x = -90 * Math.PI / 180;
|
|
//
|
|
// mesh.add(localMesh);
|
|
//
|
|
// var box = new BoxHelper(localMesh);
|
|
// mesh.add(box);
|
|
//
|
|
// 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;
|
|
});
|
|
}
|
|
|
|
_processTileData(data) {
|
|
console.time(this._tile);
|
|
|
|
var geojson = data;
|
|
|
|
if (this._options.topojson) {
|
|
// TODO: Allow TopoJSON object to be customised so this isn't tied to
|
|
// Mapzen tiles
|
|
geojson = topojson.feature(data, data.objects.vectile);
|
|
}
|
|
|
|
var offset = Point(0, 0);
|
|
offset.x = -1 * this._center[0];
|
|
offset.y = -1 * this._center[1];
|
|
|
|
var coordinates;
|
|
var earcutData;
|
|
var faces;
|
|
|
|
var allVertices = [];
|
|
var allFaces = [];
|
|
var allColours = [];
|
|
var facesCount = 0;
|
|
|
|
var colour = new THREE.Color();
|
|
|
|
var light = new THREE.Color(0xffffff);
|
|
var shadow = new THREE.Color(0x666666);
|
|
|
|
var features = geojson.features;
|
|
|
|
// Run filter, if provided
|
|
if (this._options.filter) {
|
|
features = geojson.features.filter(this._options.filter);
|
|
}
|
|
|
|
var style = this._options.style;
|
|
|
|
var allFlat = true;
|
|
|
|
features.forEach(feature => {
|
|
// feature.geometry, feature.properties
|
|
|
|
// Get style object, if provided
|
|
if (typeof this._options.style === 'function') {
|
|
style = this._options.style(feature);
|
|
}
|
|
|
|
var coordinates = feature.geometry.coordinates;
|
|
|
|
// Skip if geometry is a point
|
|
//
|
|
// This should be a user-defined filter as it would be wrong to assume
|
|
// that people won't want to output points
|
|
//
|
|
// The default use-case should be to output points in a different way
|
|
if (!coordinates[0] || !coordinates[0][0] || !Array.isArray(coordinates[0][0])) {
|
|
return;
|
|
}
|
|
|
|
coordinates = coordinates.map(ring => {
|
|
return ring.map(coordinate => {
|
|
var latlon = LatLon(coordinate[1], coordinate[0]);
|
|
var point = this._layer._world.latLonToPoint(latlon);
|
|
return [point.x, point.y];
|
|
});
|
|
});
|
|
|
|
// Draw footprint on shadow canvas
|
|
//
|
|
// TODO: Disabled for the time-being until it can be sped up / moved to
|
|
// a worker
|
|
// this._addShadow(coordinates);
|
|
|
|
earcutData = this._toEarcut(coordinates);
|
|
|
|
faces = this._triangulate(earcutData.vertices, earcutData.holes, earcutData.dimensions);
|
|
|
|
var groupedVertices = [];
|
|
for (i = 0, il = earcutData.vertices.length; i < il; i += earcutData.dimensions) {
|
|
groupedVertices.push(earcutData.vertices.slice(i, i + earcutData.dimensions));
|
|
}
|
|
|
|
var height = 0;
|
|
|
|
if (style.height) {
|
|
height = this._world.metresToWorld(style.height, this._pointScale);
|
|
}
|
|
|
|
var extruded = extrudePolygon(groupedVertices, faces, {
|
|
bottom: 0,
|
|
top: height
|
|
});
|
|
|
|
colour.set(style.color);
|
|
|
|
var topColor = colour.clone().multiply(light);
|
|
var bottomColor = colour.clone().multiply(shadow);
|
|
|
|
var _faces = [];
|
|
var _colours = [];
|
|
|
|
allVertices.push(extruded.positions);
|
|
|
|
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);
|
|
});
|
|
|
|
if (extruded.sides) {
|
|
if (allFlat) {
|
|
allFlat = 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);
|
|
|
|
allFaces.push(_faces);
|
|
allColours.push(_colours);
|
|
|
|
facesCount += _faces.length;
|
|
});
|
|
|
|
// 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);
|
|
|
|
// Skip if no faces
|
|
//
|
|
// Need to check way before this if there are no faces, before even doing
|
|
// earcut triangulation.
|
|
if (facesCount === 0) {
|
|
this._ready = true;
|
|
return;
|
|
}
|
|
|
|
var geometry = new THREE.BufferGeometry();
|
|
|
|
// Three components per vertex per face (3 x 3 = 9)
|
|
var vertices = new Float32Array(facesCount * 9);
|
|
var normals = new Float32Array(facesCount * 9);
|
|
var colours = new Float32Array(facesCount * 9);
|
|
|
|
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 dim = 2;
|
|
|
|
var index;
|
|
var _faces;
|
|
var _vertices;
|
|
var _colour;
|
|
var lastIndex = 0;
|
|
for (var i = 0; i < allFaces.length; i++) {
|
|
_faces = allFaces[i];
|
|
_vertices = allVertices[i];
|
|
_colour = allColours[i];
|
|
|
|
for (var j = 0; j < _faces.length; j++) {
|
|
// Array of vertex indexes for the face
|
|
index = _faces[j][0];
|
|
|
|
var ax = _vertices[index][0] + offset.x;
|
|
var ay = _vertices[index][1];
|
|
var az = _vertices[index][2] + offset.y;
|
|
|
|
var c1 = _colour[j][0];
|
|
|
|
index = _faces[j][1];
|
|
|
|
var bx = _vertices[index][0] + offset.x;
|
|
var by = _vertices[index][1];
|
|
var bz = _vertices[index][2] + offset.y;
|
|
|
|
var c2 = _colour[j][1];
|
|
|
|
index = _faces[j][2];
|
|
|
|
var cx = _vertices[index][0] + offset.x;
|
|
var cy = _vertices[index][1];
|
|
var cz = _vertices[index][2] + offset.y;
|
|
|
|
var c3 = _colour[j][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;
|
|
|
|
vertices[lastIndex * 9 + 0] = ax;
|
|
vertices[lastIndex * 9 + 1] = ay;
|
|
vertices[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];
|
|
|
|
vertices[lastIndex * 9 + 3] = bx;
|
|
vertices[lastIndex * 9 + 4] = by;
|
|
vertices[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];
|
|
|
|
vertices[lastIndex * 9 + 6] = cx;
|
|
vertices[lastIndex * 9 + 7] = cy;
|
|
vertices[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];
|
|
|
|
lastIndex++;
|
|
}
|
|
}
|
|
|
|
// itemSize = 3 because there are 3 values (components) per vertex
|
|
geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
|
|
geometry.addAttribute('normal', new THREE.BufferAttribute(normals, 3));
|
|
geometry.addAttribute('color', new THREE.BufferAttribute(colours, 3));
|
|
|
|
geometry.computeBoundingBox();
|
|
|
|
var material;
|
|
if (!this._world._environment._skybox) {
|
|
material = new THREE.MeshPhongMaterial({
|
|
vertexColors: THREE.VertexColors,
|
|
side: THREE.BackSide
|
|
});
|
|
} else {
|
|
material = new THREE.MeshStandardMaterial({
|
|
vertexColors: THREE.VertexColors,
|
|
side: THREE.BackSide
|
|
});
|
|
material.roughness = 1;
|
|
material.metalness = 0.1;
|
|
material.envMapIntensity = 3;
|
|
material.envMap = this._world._environment._skybox.getRenderTarget();
|
|
}
|
|
|
|
var mesh = new THREE.Mesh(geometry, material);
|
|
|
|
mesh.castShadow = true;
|
|
mesh.receiveShadow = true;
|
|
|
|
if (allFlat) {
|
|
// This is only useful for flat objects
|
|
mesh.renderOrder = 1;
|
|
}
|
|
|
|
this._mesh.add(mesh);
|
|
|
|
this._ready = true;
|
|
console.timeEnd(this._tile);
|
|
console.log(`${this._tile}: ${features.length} features`);
|
|
}
|
|
|
|
_toEarcut(data) {
|
|
var dim = data[0][0].length;
|
|
var result = {vertices: [], holes: [], dimensions: dim};
|
|
var holeIndex = 0;
|
|
|
|
for (var i = 0; i < data.length; i++) {
|
|
for (var j = 0; j < data[i].length; j++) {
|
|
for (var d = 0; d < dim; d++) {
|
|
result.vertices.push(data[i][j][d]);
|
|
}
|
|
}
|
|
if (i > 0) {
|
|
holeIndex += data[i - 1].length;
|
|
result.holes.push(holeIndex);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
_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;
|
|
}
|
|
|
|
_abortRequest() {
|
|
if (!this._request) {
|
|
return;
|
|
}
|
|
|
|
this._request.abort();
|
|
}
|
|
}
|
|
|
|
// Initialise without requiring new keyword
|
|
export default function(quadcode, path, layer, options) {
|
|
return new GeoJSONTile(quadcode, path, layer, options);
|
|
};
|