kopia lustrzana https://github.com/robhawkes/vizicities
378 wiersze
11 KiB
JavaScript
Executable File
378 wiersze
11 KiB
JavaScript
Executable File
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';
|
||
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 GeoJSONTile extends Tile {
|
||
constructor(quadcode, path, layer, options) {
|
||
super(quadcode, path, layer);
|
||
|
||
this._defaultStyle = GeoJSON.defaultStyle;
|
||
|
||
var defaults = {
|
||
workers: false,
|
||
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._aborted = false;
|
||
|
||
if (!this._options.workers) {
|
||
this._request = reqwest({
|
||
url: url,
|
||
type: 'json',
|
||
crossOrigin: true,
|
||
headers: this._options.headers
|
||
}).then(res => {
|
||
// Clear request reference
|
||
this._request = null;
|
||
this._processTileData(res);
|
||
}).catch(err => {
|
||
// 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 = GeoJSONClass(data, this._options);
|
||
this._geojsonLayer.addTo(this._world).then(() => {
|
||
// 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);
|
||
|
||
this._mesh.add(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
|
||
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
|
||
//
|
||
// 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);
|
||
}).catch((err) => {
|
||
console.error(err);
|
||
});
|
||
}
|
||
|
||
_abortRequest() {
|
||
if ((!this._request && !this._options.workers) || this._ready) {
|
||
return;
|
||
}
|
||
|
||
this._aborted = true;
|
||
|
||
if (this._request) {
|
||
this._request.abort();
|
||
}
|
||
}
|
||
}
|
||
|
||
export default GeoJSONTile;
|
||
|
||
var noNew = function(quadcode, path, layer, options) {
|
||
return new GeoJSONTile(quadcode, path, layer, options);
|
||
};
|
||
|
||
// Initialise without requiring new keyword
|
||
export {noNew as geoJSONTile};
|