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(() => {}); } _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};