vizicities/src/layer/tile/TileLayer.js

406 wiersze
12 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 Layer from '../Layer';
import extend from 'lodash.assign';
import TileCache from './TileCache';
import * as THREE from 'three';
// TODO: Consider removing picking from TileLayer instances as there aren't
// (m)any situations where it would be practical
//
// For example, how would you even know what picking IDs to listen to and what
// to do with them?
// TODO: Make sure nothing is left behind in the heap after calling destroy()
// TODO: Consider keeping a single TileLayer / LOD instance running by default
// that keeps a standard LOD grid for other layers to utilise, rather than
// having to create their own, unique LOD grid and duplicate calculations when
// they're going to use the same grid setup anyway
//
// It still makes sense to be able to have a custom LOD grid for some layers as
// they may want to customise things, maybe not even using a quadtree at all!
//
// Perhaps it makes sense to split out the quadtree stuff into a singleton and
// pass in the necessary parameters each time for the calculation step.
//
// Either way, it seems silly to force layers to have to create a new LOD grid
// each time and create extra, duplicated processing every frame.
// TODO: Allow passing in of options to define min/max LOD and a distance to use
// for culling tiles beyond that distance.
// DONE: Prevent tiles from being loaded if they are further than a certain
// distance from the camera and are unlikely to be seen anyway
// TODO: Avoid performing LOD calculation when it isn't required. For example,
// when nothing has changed since the last frame and there are no tiles to be
// loaded or in need of rendering
// TODO: Only remove tiles from the layer that aren't to be rendered in the
// current frame – it seems excessive to remove all tiles and re-add them on
// every single frame, even if it's just array manipulation
// TODO: Fix LOD calculation so min and max LOD can be changed without causing
// problems (eg. making min above 5 causes all sorts of issues)
// TODO: Reuse THREE objects where possible instead of creating new instances
// on every LOD calculation
// TODO: Consider not using THREE or LatLon / Point objects in LOD calculations
// to avoid creating unnecessary memory for garbage collection
// TODO: Prioritise loading of tiles at highest level in the quadtree (those
// closest to the camera) so visual inconsistancies during loading are minimised
class TileLayer extends Layer {
constructor(options) {
var defaults = {
picking: false,
maxCache: 1000,
maxLOD: 18
};
var _options = extend({}, defaults, options);
super(_options);
this._destroy = false;
this._tileCache = new TileCache(this._options.maxCache, tile => {
this._destroyTile(tile);
});
// List of tiles from the previous LOD calculation
this._tileList = [];
// TODO: Work out why changing the minLOD causes loads of issues
this._minLOD = this._options.minLOD || 3;
this._maxLOD = this._options.maxLOD;
this._frustum = new THREE.Frustum();
this._tiles = new THREE.Object3D();
this._tilesPicking = new THREE.Object3D();
}
_onAdd(world) {
this.addToPicking(this._tilesPicking);
this.add(this._tiles);
return Promise.resolve();
}
_updateFrustum() {
var camera = this._world.getCamera();
var projScreenMatrix = new THREE.Matrix4();
projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
this._frustum.setFromMatrix(camera.projectionMatrix);
this._frustum.setFromMatrix(new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse));
}
_tileInFrustum(tile) {
var bounds = tile.getBounds();
return this._frustum.intersectsBox(new THREE.Box3(new THREE.Vector3(bounds[0], 0, bounds[3]), new THREE.Vector3(bounds[2], 0, bounds[1])));
}
// Update and output tiles from the previous LOD checklist
_outputTiles() {
if (!this._tiles || this._destroy) {
return;
}
// Remove all tiles from layer
this._removeTiles();
// Add / re-add tiles
this._tileList.forEach(tile => {
// Are the mesh and texture ready?
//
// If yes, continue
// If no, skip
if (!tile.isReady()) {
return;
}
// Add tile to layer (and to scene) if not already there
this._tiles.add(tile.getMesh());
if (tile.getPickingMesh()) {
this._tilesPicking.add(tile.getPickingMesh());
}
});
// Emit event notifying of new tiles
this.emit('tilesList', this._tileList.map((tile) => tile));
}
// Works out tiles in the view frustum and stores them in an array
//
// Does not output the tiles, deferring this to _outputTiles()
_calculateLOD() {
if (this._stop || !this._world || this._destroy) {
return;
}
// var start = (performance || Date).now();
var camera = this._world.getCamera();
// 1. Update and retrieve camera frustum
this._updateFrustum(this._frustum, camera);
// 2. Add the four root items of the quadtree to a check list
var checkList = this._checklist;
checkList = [];
checkList.push(this._requestTile('0', this));
checkList.push(this._requestTile('1', this));
checkList.push(this._requestTile('2', this));
checkList.push(this._requestTile('3', this));
// 3. Call Divide, passing in the check list
this._divide(checkList);
// // 4. Remove all tiles from layer
//
// Moved to _outputTiles() for now
// this._removeTiles();
// Order tile-list by zoom so nearest tiles are requested first
checkList.sort((a, b) => {
return a._quadcode.length < b._quadcode.length;
});
// 5. Filter the tiles remaining in the check list
var tileList = checkList.filter((tile, index) => {
// Skip tile if it's not in the current view frustum
if (!this._tileInFrustum(tile)) {
return false;
}
if (this._options.distance && this._options.distance > 0) {
// TODO: Can probably speed this up
var center = tile.getCenter();
var dist = (new THREE.Vector3(center[0], 0, center[1])).sub(camera.position).length();
// Manual distance limit to cut down on tiles so far away
if (dist > this._options.distance) {
return false;
}
}
// Does the tile have a mesh?
//
// If yes, continue
// If no, generate tile mesh, request texture and skip
if (!tile.getMesh() || tile.isAborted()) {
tile.requestTileAsync();
}
return true;
// Are the mesh and texture ready?
//
// If yes, continue
// If no, skip
// if (!tile.isReady()) {
// return;
// }
//
// // Add tile to layer (and to scene)
// this._tiles.add(tile.getMesh());
});
// Get list of tiles that were in the previous update but not the
// current one (for aborting requests, etc)
var missingTiles = this._tileList.filter((item) => {
return tileList.indexOf(item) < 0;
});
// Abort tiles that are no longer in view
missingTiles.forEach((tile) => tile._abortRequest());
this._tileList = tileList;
// console.log((performance || Date).now() - start);
}
_divide(checkList) {
var count = 0;
var currentItem;
var quadcode;
// 1. Loop until count equals check list length
while (count != checkList.length) {
currentItem = checkList[count];
quadcode = currentItem.getQuadcode();
// 2. Increase count and continue loop if quadcode equals max LOD / zoom
if (currentItem.length === this._maxLOD) {
count++;
continue;
}
// 3. Else, calculate screen-space error metric for quadcode
if (this._screenSpaceError(currentItem)) {
// 4. If error is sufficient...
// 4a. Remove parent item from the check list
checkList.splice(count, 1);
// 4b. Add 4 child items to the check list
checkList.push(this._requestTile(quadcode + '0', this));
checkList.push(this._requestTile(quadcode + '1', this));
checkList.push(this._requestTile(quadcode + '2', this));
checkList.push(this._requestTile(quadcode + '3', this));
// 4d. Continue the loop without increasing count
continue;
} else {
// 5. Else, increase count and continue loop
count++;
}
}
}
_screenSpaceError(tile) {
var minDepth = this._minLOD;
var maxDepth = this._maxLOD;
var quadcode = tile.getQuadcode();
var camera = this._world.getCamera();
// Tweak this value to refine specific point that each quad is subdivided
//
// It's used to multiple the dimensions of the tile sides before
// comparing against the tile distance from camera
var quality = 3.0;
// 1. Return false if quadcode length equals maxDepth (stop dividing)
if (quadcode.length === maxDepth) {
return false;
}
// 2. Return true if quadcode length is less than minDepth
if (quadcode.length < minDepth) {
return true;
}
// 3. Return false if quadcode bounds are not in view frustum
if (!this._tileInFrustum(tile)) {
return false;
}
var center = tile.getCenter();
// 4. Calculate screen-space error metric
// TODO: Use closest distance to one of the 4 tile corners
var dist = (new THREE.Vector3(center[0], 0, center[1])).sub(camera.position).length();
var error = quality * tile.getSide() / dist;
// 5. Return true if error is greater than 1.0, else return false
return (error > 1.0);
}
_removeTiles() {
if (!this._tiles || !this._tiles.children) {
return;
}
for (var i = this._tiles.children.length - 1; i >= 0; i--) {
this._tiles.remove(this._tiles.children[i]);
}
if (!this._tilesPicking || !this._tilesPicking.children) {
return;
}
for (var i = this._tilesPicking.children.length - 1; i >= 0; i--) {
this._tilesPicking.remove(this._tilesPicking.children[i]);
}
}
// Return a new tile instance
_createTile(quadcode, layer) {}
// Get a cached tile or request a new one if not in cache
_requestTile(quadcode, layer) {
var tile = this._tileCache.getTile(quadcode);
if (!tile) {
// Set up a brand new tile
tile = this._createTile(quadcode, layer);
// Add tile to cache, though it won't be ready yet as the data is being
// requested from various places asynchronously
this._tileCache.setTile(quadcode, tile);
}
return tile;
}
_destroyTile(tile) {
// Remove tile from scene
this._tiles.remove(tile.getMesh());
// Delete any references to the tile within this component
// Call destory on tile instance
tile.destroy();
}
show() {
this._stop = false;
if (this._tilesPicking) {
this._tilesPicking.visible = true;
}
this._calculateLOD();
super.show();
}
hide() {
this._stop = true;
if (this._tilesPicking) {
this._tilesPicking.visible = false;
}
super.hide();
}
// Destroys the layer and removes it from the scene and memory
destroy() {
this._destroy = true;
if (this._tiles.children) {
// Remove all tiles
for (var i = this._tiles.children.length - 1; i >= 0; i--) {
this._tiles.remove(this._tiles.children[i]);
}
}
// Remove tile from picking scene
this.removeFromPicking(this._tilesPicking);
if (this._tilesPicking.children) {
// Remove all tiles
for (var i = this._tilesPicking.children.length - 1; i >= 0; i--) {
this._tilesPicking.remove(this._tilesPicking.children[i]);
}
}
this._tileCache.destroy();
this._tileCache = null;
this._tiles = null;
this._tilesPicking = null;
this._frustum = null;
super.destroy();
}
}
export default TileLayer;