vizicities/src/layer/tile/GeoJSONTile.js

378 wiersze
11 KiB
JavaScript
Executable File
Czysty Wina Historia

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

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};