2016-02-19 21:25:16 +00:00
|
|
|
|
import Tile from './Tile';
|
2016-03-14 16:24:47 +00:00
|
|
|
|
import {geoJSONLayer as GeoJSONLayer} from '../GeoJSONLayer';
|
2016-09-07 15:01:02 +00:00
|
|
|
|
import {geoJSONWorkerLayer as GeoJSONWorkerLayer} from '../GeoJSONWorkerLayer';
|
2016-02-19 21:25:16 +00:00
|
|
|
|
import BoxHelper from '../../vendor/BoxHelper';
|
|
|
|
|
import THREE from 'three';
|
|
|
|
|
import reqwest from 'reqwest';
|
2016-03-01 20:34:53 +00:00
|
|
|
|
import {point as Point} from '../../geo/Point';
|
|
|
|
|
import {latLon as LatLon} from '../../geo/LatLon';
|
2016-02-22 13:03:50 +00:00
|
|
|
|
import extend from 'lodash.assign';
|
2016-02-28 11:25:45 +00:00
|
|
|
|
// import Offset from 'polygon-offset';
|
|
|
|
|
import GeoJSON from '../../util/GeoJSON';
|
|
|
|
|
import Buffer from '../../util/Buffer';
|
2016-02-29 18:49:21 +00:00
|
|
|
|
import PickingMaterial from '../../engine/PickingMaterial';
|
2016-02-22 13:03:50 +00:00
|
|
|
|
|
2016-02-29 20:45:10 +00:00
|
|
|
|
// 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
|
|
|
|
|
|
2016-02-28 18:40:51 +00:00
|
|
|
|
// TODO: Make sure nothing is left behind in the heap after calling destroy()
|
|
|
|
|
|
2016-02-22 13:03:50 +00:00
|
|
|
|
// 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.
|
2016-02-19 21:25:16 +00:00
|
|
|
|
|
2016-02-27 14:58:33 +00:00
|
|
|
|
class GeoJSONTile extends Tile {
|
2016-02-22 13:03:50 +00:00
|
|
|
|
constructor(quadcode, path, layer, options) {
|
2016-02-19 21:25:16 +00:00
|
|
|
|
super(quadcode, path, layer);
|
2016-02-22 13:03:50 +00:00
|
|
|
|
|
2016-02-28 20:29:52 +00:00
|
|
|
|
this._defaultStyle = GeoJSON.defaultStyle;
|
2016-02-27 19:31:41 +00:00
|
|
|
|
|
2016-02-22 13:03:50 +00:00
|
|
|
|
var defaults = {
|
2016-09-07 15:01:02 +00:00
|
|
|
|
workers: false,
|
2016-03-14 16:24:47 +00:00
|
|
|
|
output: true,
|
|
|
|
|
outputToScene: false,
|
|
|
|
|
interactive: false,
|
2016-02-27 14:58:33 +00:00
|
|
|
|
topojson: false,
|
2016-02-22 13:03:50 +00:00
|
|
|
|
filter: null,
|
2016-03-14 16:24:47 +00:00
|
|
|
|
onEachFeature: null,
|
|
|
|
|
polygonMaterial: null,
|
|
|
|
|
onPolygonMesh: null,
|
|
|
|
|
onPolygonBufferAttributes: null,
|
|
|
|
|
polylineMaterial: null,
|
|
|
|
|
onPolylineMesh: null,
|
|
|
|
|
onPolylineBufferAttributes: null,
|
|
|
|
|
pointGeometry: null,
|
|
|
|
|
pointMaterial: null,
|
|
|
|
|
onPointMesh: null,
|
2016-03-14 21:46:47 +00:00
|
|
|
|
style: GeoJSON.defaultStyle,
|
|
|
|
|
keepFeatures: false
|
2016-02-22 13:03:50 +00:00
|
|
|
|
};
|
|
|
|
|
|
2016-03-14 16:24:47 +00:00
|
|
|
|
var _options = extend({}, defaults, options);
|
2016-02-28 20:29:52 +00:00
|
|
|
|
|
|
|
|
|
if (typeof options.style === 'function') {
|
2016-03-14 16:24:47 +00:00
|
|
|
|
_options.style = options.style;
|
2016-02-28 20:29:52 +00:00
|
|
|
|
} else {
|
2016-03-14 16:24:47 +00:00
|
|
|
|
_options.style = extend({}, defaults.style, options.style);
|
2016-02-28 20:29:52 +00:00
|
|
|
|
}
|
2016-03-14 16:24:47 +00:00
|
|
|
|
|
|
|
|
|
this._options = _options;
|
2016-02-19 21:25:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Request data for the tile
|
|
|
|
|
requestTileAsync() {
|
|
|
|
|
// Making this asynchronous really speeds up the LOD framerate
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (!this._mesh) {
|
|
|
|
|
this._mesh = this._createMesh();
|
2016-02-26 20:04:32 +00:00
|
|
|
|
// this._shadowCanvas = this._createShadowCanvas();
|
2016-02-19 21:25:16 +00:00
|
|
|
|
}
|
2016-10-06 09:11:32 +00:00
|
|
|
|
|
|
|
|
|
this._requestTile();
|
2016-02-19 21:25:16 +00:00
|
|
|
|
}, 0);
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-14 16:24:47 +00:00
|
|
|
|
// TODO: Destroy GeoJSONLayer
|
2016-02-19 21:25:16 +00:00
|
|
|
|
destroy() {
|
|
|
|
|
// Cancel any pending requests
|
|
|
|
|
this._abortRequest();
|
|
|
|
|
|
|
|
|
|
// Clear request reference
|
|
|
|
|
this._request = null;
|
|
|
|
|
|
2016-03-14 16:24:47 +00:00
|
|
|
|
if (this._geojsonLayer) {
|
|
|
|
|
this._geojsonLayer.destroy();
|
|
|
|
|
this._geojsonLayer = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._mesh = null;
|
|
|
|
|
|
2016-02-29 23:04:07 +00:00
|
|
|
|
// TODO: Properly dispose of picking mesh
|
|
|
|
|
this._pickingMesh = null;
|
|
|
|
|
|
2016-02-19 21:25:16 +00:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-14 16:24:47 +00:00
|
|
|
|
// _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;
|
|
|
|
|
// }
|
2016-02-26 20:04:32 +00:00
|
|
|
|
|
2016-02-28 11:25:45 +00:00
|
|
|
|
// _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();
|
|
|
|
|
// }
|
2016-02-26 20:04:32 +00:00
|
|
|
|
|
2016-02-19 21:25:16 +00:00
|
|
|
|
_requestTile() {
|
|
|
|
|
var urlParams = {
|
|
|
|
|
x: this._tile[0],
|
|
|
|
|
y: this._tile[1],
|
|
|
|
|
z: this._tile[2]
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var url = this._getTileURL(urlParams);
|
|
|
|
|
|
2016-10-06 09:11:32 +00:00
|
|
|
|
this._aborted = false;
|
|
|
|
|
|
2016-09-07 15:01:02 +00:00
|
|
|
|
if (!this._options.workers) {
|
|
|
|
|
this._request = reqwest({
|
|
|
|
|
url: url,
|
|
|
|
|
type: 'json',
|
2016-09-16 09:08:35 +00:00
|
|
|
|
crossOrigin: true,
|
|
|
|
|
headers: this._options.headers
|
2016-09-07 15:01:02 +00:00
|
|
|
|
}).then(res => {
|
|
|
|
|
// Clear request reference
|
|
|
|
|
this._request = null;
|
|
|
|
|
this._processTileData(res);
|
|
|
|
|
}).catch(err => {
|
|
|
|
|
// Clear request reference
|
|
|
|
|
this._request = null;
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
this._processTileData(url);
|
|
|
|
|
}
|
2016-02-19 21:25:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_processTileData(data) {
|
2016-10-06 09:11:32 +00:00
|
|
|
|
// console.time(this._tile);
|
2016-02-22 22:24:57 +00:00
|
|
|
|
|
2016-09-07 15:01:02 +00:00
|
|
|
|
var GeoJSONClass = (!this._options.workers) ? GeoJSONLayer : GeoJSONWorkerLayer;
|
|
|
|
|
|
2016-03-14 16:24:47 +00:00
|
|
|
|
// Using this creates a huge amount of memory due to the quantity of tiles
|
2016-09-07 15:01:02 +00:00
|
|
|
|
this._geojsonLayer = GeoJSONClass(data, this._options);
|
2016-08-30 15:46:04 +00:00
|
|
|
|
this._geojsonLayer.addTo(this._world).then(() => {
|
2016-10-06 09:11:32 +00:00
|
|
|
|
// TODO: This never seems to be called on worker layers. Find out why.
|
|
|
|
|
if (this.isAborted()) {
|
|
|
|
|
// this._geojsonLayer._aborted = true;
|
|
|
|
|
// this._geojsonLayer = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: This is a hack to stop old tile meshes hanging around. Fix or
|
|
|
|
|
// move to somewhere more robust.
|
|
|
|
|
//
|
|
|
|
|
// Could potentially just overwrite mesh on first index each time
|
|
|
|
|
//
|
|
|
|
|
// This makes some worker tiles to not appear properly – showing the
|
|
|
|
|
// points mesh but not the polygon mesh, etc.
|
|
|
|
|
//
|
|
|
|
|
// Only do this for non-worker layers for now as it seems to cause issues
|
|
|
|
|
// with worker tiles showing for a moment and then disappearing forever
|
|
|
|
|
if (!this._options.workers) {
|
|
|
|
|
this.destroyMesh(this._mesh);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TOSO: Work out if the picking mesh needs destroying here
|
|
|
|
|
// this.destroyMesh(this._pickingMesh);
|
|
|
|
|
|
2016-09-16 09:08:35 +00:00
|
|
|
|
this._mesh.add(this._geojsonLayer._object3D);
|
2016-08-30 15:46:04 +00:00
|
|
|
|
this._pickingMesh = this._geojsonLayer._pickingMesh;
|
|
|
|
|
|
|
|
|
|
// Free the GeoJSON memory as we don't need it
|
|
|
|
|
//
|
|
|
|
|
// TODO: This should probably be a method within GeoJSONLayer
|
2016-09-07 15:01:02 +00:00
|
|
|
|
if (this._geojsonLayer._geojson) {
|
|
|
|
|
this._geojsonLayer._geojson = null;
|
|
|
|
|
}
|
2016-08-30 15:46:04 +00:00
|
|
|
|
|
|
|
|
|
// 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;
|
2016-10-06 09:11:32 +00:00
|
|
|
|
// console.timeEnd(this._tile);
|
2016-11-29 17:26:13 +00:00
|
|
|
|
}).catch((err) => {
|
|
|
|
|
console.error(err);
|
|
|
|
|
});
|
2016-02-19 21:25:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_abortRequest() {
|
2016-10-06 09:11:32 +00:00
|
|
|
|
if ((!this._request && !this._options.workers) || this._ready) {
|
2016-02-19 21:25:16 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-06 09:11:32 +00:00
|
|
|
|
this._aborted = true;
|
|
|
|
|
|
|
|
|
|
if (this._request) {
|
|
|
|
|
this._request.abort();
|
|
|
|
|
}
|
2016-02-19 21:25:16 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-01 20:51:38 +00:00
|
|
|
|
export default GeoJSONTile;
|
|
|
|
|
|
|
|
|
|
var noNew = function(quadcode, path, layer, options) {
|
2016-02-27 14:58:33 +00:00
|
|
|
|
return new GeoJSONTile(quadcode, path, layer, options);
|
2016-02-19 21:25:16 +00:00
|
|
|
|
};
|
2016-03-01 20:51:38 +00:00
|
|
|
|
|
|
|
|
|
// Initialise without requiring new keyword
|
|
|
|
|
export {noNew as geoJSONTile};
|