kopia lustrzana https://github.com/robhawkes/vizicities
				
				
				
			
		
			
				
	
	
		
			406 wiersze
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
			
		
		
	
	
			406 wiersze
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
| import Layer from '../Layer';
 | ||
| import extend from 'lodash.assign';
 | ||
| import TileCache from './TileCache';
 | ||
| import 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 = 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;
 |