kopia lustrzana https://github.com/robhawkes/vizicities
Working prototype of tile loader with real map tiles
rodzic
9ee293817b
commit
5e8fa09632
Plik diff jest za duży
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -81,6 +81,8 @@
|
|||
"dependencies": {
|
||||
"eventemitter3": "^1.1.1",
|
||||
"lodash.assign": "^4.0.2",
|
||||
"lodash.throttle": "^4.0.0",
|
||||
"lru-cache": "^4.0.0",
|
||||
"three-orbit-controls": "github:robhawkes/three-orbit-controls"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Layer from '../Layer';
|
||||
import Surface from './Surface';
|
||||
import TileCache from './TileCache';
|
||||
import throttle from 'lodash.throttle';
|
||||
import THREE from 'three';
|
||||
|
||||
// TODO: Prevent tiles from being loaded if they are further than a certain
|
||||
|
@ -9,8 +10,12 @@ class GridLayer extends Layer {
|
|||
constructor() {
|
||||
super();
|
||||
|
||||
this._tileCache = new TileCache(1000);
|
||||
|
||||
// TODO: Work out why changing the minLOD causes loads of issues
|
||||
this._minLOD = 3;
|
||||
this._maxLOD = 18;
|
||||
|
||||
this._frustum = new THREE.Frustum();
|
||||
}
|
||||
|
||||
|
@ -27,9 +32,13 @@ class GridLayer extends Layer {
|
|||
}
|
||||
|
||||
_initEvents() {
|
||||
this._world.on('move', latlon => {
|
||||
// Run LOD calculations based on render calls
|
||||
//
|
||||
// TODO: Perhaps don't perform a calculation if nothing has changed in a
|
||||
// frame and there are no tiles waiting to be loaded.
|
||||
this._world.on('preUpdate', throttle(() => {
|
||||
this._calculateLOD();
|
||||
});
|
||||
}, 100));
|
||||
}
|
||||
|
||||
_updateFrustum() {
|
||||
|
@ -41,11 +50,18 @@ class GridLayer extends Layer {
|
|||
this._frustum.setFromMatrix(new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse));
|
||||
}
|
||||
|
||||
_surfaceInFrustum(surface) {
|
||||
return this._frustum.intersectsBox(new THREE.Box3(new THREE.Vector3(surface.bounds[0], 0, surface.bounds[3]), new THREE.Vector3(surface.bounds[2], 0, surface.bounds[1])));
|
||||
_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])));
|
||||
}
|
||||
|
||||
_calculateLOD() {
|
||||
if (this._stop) {
|
||||
return;
|
||||
}
|
||||
|
||||
// var start = performance.now();
|
||||
|
||||
var camera = this._world.getCamera();
|
||||
|
||||
// 1. Update and retrieve camera frustum
|
||||
|
@ -54,44 +70,82 @@ class GridLayer extends Layer {
|
|||
// 2. Add the four root items of the quadtree to a check list
|
||||
var checkList = this._checklist;
|
||||
checkList = [];
|
||||
checkList.push(Surface('0', this._world));
|
||||
checkList.push(Surface('1', this._world));
|
||||
checkList.push(Surface('2', this._world));
|
||||
checkList.push(Surface('3', this._world));
|
||||
checkList.push(this._tileCache.requestTile('0', this));
|
||||
checkList.push(this._tileCache.requestTile('1', this));
|
||||
checkList.push(this._tileCache.requestTile('2', this));
|
||||
checkList.push(this._tileCache.requestTile('3', this));
|
||||
|
||||
// 3. Call Divide, passing in the check list
|
||||
this._divide(checkList);
|
||||
|
||||
// 4. Render the quadtree items remaining in the check list
|
||||
checkList.forEach((surface, index) => {
|
||||
if (!this._surfaceInFrustum(surface)) {
|
||||
// 4. Remove all tiles from layer
|
||||
this._removeTiles();
|
||||
|
||||
var tileCount = 0;
|
||||
|
||||
// 5. Render the tiles remaining in the check list
|
||||
checkList.forEach((tile, index) => {
|
||||
// Skip tile if it's not in the current view frustum
|
||||
if (!this._tileInFrustum(tile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log(surface);
|
||||
// TODO: Can probably speed this up
|
||||
var center = tile.getCenter();
|
||||
var dist = (new THREE.Vector3(center[0], 0, center[1])).sub(camera.position).length();
|
||||
|
||||
// surface.render();
|
||||
this._layer.add(surface.mesh);
|
||||
// Manual distance limit to cut down on tiles so far away
|
||||
if (dist > 8000) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Does the tile have a mesh?
|
||||
//
|
||||
// If yes, continue
|
||||
// If no, generate tile mesh, request texture and skip
|
||||
if (!tile.getMesh()) {
|
||||
tile.requestTileAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
// Are the mesh and texture ready?
|
||||
//
|
||||
// If yes, continue
|
||||
// If no, skip
|
||||
if (!tile.isReady()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add tile to layer (and to scene)
|
||||
this._layer.add(tile.getMesh());
|
||||
|
||||
// Output added tile (for debugging)
|
||||
// console.log(tile);
|
||||
|
||||
tileCount++;
|
||||
});
|
||||
|
||||
// console.log(tileCount);
|
||||
// console.log(performance.now() - start);
|
||||
}
|
||||
|
||||
_divide(checkList) {
|
||||
var count = 0;
|
||||
var currentItem;
|
||||
var quadkey;
|
||||
var quadcode;
|
||||
|
||||
// 1. Loop until count equals check list length
|
||||
while (count != checkList.length) {
|
||||
currentItem = checkList[count];
|
||||
quadkey = currentItem.quadkey;
|
||||
quadcode = currentItem.getQuadcode();
|
||||
|
||||
// 2. Increase count and continue loop if quadkey equals max LOD / zoom
|
||||
// 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 quadkey
|
||||
// 3. Else, calculate screen-space error metric for quadcode
|
||||
if (this._screenSpaceError(currentItem)) {
|
||||
// 4. If error is sufficient...
|
||||
|
||||
|
@ -99,10 +153,10 @@ class GridLayer extends Layer {
|
|||
checkList.splice(count, 1);
|
||||
|
||||
// 4b. Add 4 child items to the check list
|
||||
checkList.push(Surface(quadkey + '0', this._world));
|
||||
checkList.push(Surface(quadkey + '1', this._world));
|
||||
checkList.push(Surface(quadkey + '2', this._world));
|
||||
checkList.push(Surface(quadkey + '3', this._world));
|
||||
checkList.push(this._tileCache.requestTile(quadcode + '0', this));
|
||||
checkList.push(this._tileCache.requestTile(quadcode + '1', this));
|
||||
checkList.push(this._tileCache.requestTile(quadcode + '2', this));
|
||||
checkList.push(this._tileCache.requestTile(quadcode + '3', this));
|
||||
|
||||
// 4d. Continue the loop without increasing count
|
||||
continue;
|
||||
|
@ -113,44 +167,54 @@ class GridLayer extends Layer {
|
|||
}
|
||||
}
|
||||
|
||||
_screenSpaceError(surface) {
|
||||
_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 surface sides before
|
||||
// comparing against the surface distance from camera
|
||||
// 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 quadkey length is greater than maxDepth
|
||||
if (surface.quadkey.length > maxDepth) {
|
||||
// 1. Return false if quadcode length is greater than maxDepth
|
||||
if (quadcode.length > maxDepth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Return true if quadkey length is less than minDepth
|
||||
if (surface.quadkey.length < minDepth) {
|
||||
// 2. Return true if quadcode length is less than minDepth
|
||||
if (quadcode.length < minDepth) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. Return false if quadkey bounds are not in view frustum
|
||||
if (!this._surfaceInFrustum(surface)) {
|
||||
// 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 surface corners
|
||||
var dist = (new THREE.Vector3(surface.center[0], 0, surface.center[1])).sub(camera.position).length();
|
||||
// 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();
|
||||
|
||||
// console.log(surface, dist);
|
||||
|
||||
var error = quality * surface.side / dist;
|
||||
var error = quality * tile.getSide() / dist;
|
||||
|
||||
// 5. Return true if error is greater than 1.0, else return false
|
||||
return (error > 1.0);
|
||||
}
|
||||
|
||||
_removeTiles() {
|
||||
// console.log('Pre:', this._layer.children.length);
|
||||
for (var i = this._layer.children.length - 1; i >= 0; i--) {
|
||||
this._layer.remove(this._layer.children[i]);
|
||||
}
|
||||
// console.log('Post:', this._layer.children.length);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialise without requiring new keyword
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
import LatLon from '../../geo/LatLon';
|
||||
import THREE from 'three';
|
||||
|
||||
// Manages a single tile and its layers
|
||||
|
||||
var r2d = 180 / Math.PI;
|
||||
|
||||
var loader = new THREE.TextureLoader();
|
||||
loader.setCrossOrigin('');
|
||||
|
||||
class Tile {
|
||||
constructor(quadcode, layer) {
|
||||
this._layer = layer;
|
||||
this._quadcode = quadcode;
|
||||
|
||||
this._ready = false;
|
||||
|
||||
this._tile = this._quadcodeToTile(quadcode);
|
||||
|
||||
// Bottom-left and top-right bounds in WGS84 coordinates
|
||||
this._boundsLatLon = this._tileBoundsWGS84(this._tile);
|
||||
|
||||
// Bottom-left and top-right bounds in world coordinates
|
||||
this._boundsWorld = this._tileBoundsFromWGS84(this._boundsLatLon);
|
||||
|
||||
// Tile center in world coordinates
|
||||
this._center = this._boundsToCenter(this._boundsWorld);
|
||||
|
||||
// Length of a tile side in world coorindates
|
||||
this._side = this._getSide(this._boundsWorld);
|
||||
}
|
||||
|
||||
// Returns true if the tile mesh and texture are ready to be used
|
||||
// Otherwise, returns false
|
||||
isReady() {
|
||||
return this._ready;
|
||||
}
|
||||
|
||||
// Request data for the various tile providers
|
||||
//
|
||||
// Providers are provided here and not on instantiation of the class so that
|
||||
// providers can be easily changed in subsequent requests without heavy
|
||||
// management
|
||||
//
|
||||
// If requestData is called more than once then the provider data will be
|
||||
// re-downloaded and the mesh output will be changed
|
||||
//
|
||||
// Being able to update tile data and output like this on-the-fly makes it
|
||||
// appealing for situations where tile data may be dynamic / realtime
|
||||
// (eg. realtime traffic tiles)
|
||||
//
|
||||
// May need to be intelligent about what exactly is updated each time
|
||||
// requestData is called as it doesn't make sense to re-request and
|
||||
// re-generate a mesh each time when only the image provider needs updating,
|
||||
// and likewise it doesn't make sense to update the imagery when only terrain
|
||||
// provider changes
|
||||
requestTileAsync(imageProviders) {
|
||||
// Making this asynchronous really speeds up the LOD framerate
|
||||
setTimeout(() => {
|
||||
if (!this._mesh) {
|
||||
this._mesh = this._createMesh();
|
||||
this._requestTextureAsync();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
getQuadcode() {
|
||||
return this._quadcode;
|
||||
}
|
||||
|
||||
getBounds() {
|
||||
return this._boundsWorld;
|
||||
}
|
||||
|
||||
getCenter() {
|
||||
return this._center;
|
||||
}
|
||||
|
||||
getSide() {
|
||||
return this._side;
|
||||
}
|
||||
|
||||
getMesh() {
|
||||
return this._mesh;
|
||||
}
|
||||
|
||||
// Destroys the tile and removes it from the layer and memory
|
||||
//
|
||||
// Ensure that this leaves no trace of the tile – no textures, no meshes,
|
||||
// nothing in memory or the GPU
|
||||
destroy() {}
|
||||
|
||||
_createMesh() {
|
||||
var mesh = new THREE.Object3D();
|
||||
var geom = new THREE.PlaneGeometry(this._side, this._side, 1);
|
||||
|
||||
var material = new THREE.MeshBasicMaterial();
|
||||
|
||||
var localMesh = new THREE.Mesh(geom, material);
|
||||
localMesh.rotation.x = -90 * Math.PI / 180;
|
||||
|
||||
mesh.add(localMesh);
|
||||
|
||||
mesh.position.x = this._center[0];
|
||||
mesh.position.z = this._center[1];
|
||||
|
||||
var box = new THREE.BoxHelper(localMesh);
|
||||
mesh.add(box);
|
||||
|
||||
// mesh.add(this._createDebugMesh());
|
||||
|
||||
return mesh;
|
||||
}
|
||||
|
||||
_requestTextureAsync() {
|
||||
var letter = String.fromCharCode(97 + Math.floor(Math.random() * 26));
|
||||
var url = 'http://' + letter + '.basemaps.cartocdn.com/light_nolabels/';
|
||||
// var url = 'http://tile.stamen.com/toner-lite/';
|
||||
|
||||
loader.load(url + this._tile[2] + '/' + this._tile[0] + '/' + this._tile[1] + '.png', texture => {
|
||||
// console.log('Loaded');
|
||||
// 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;
|
||||
|
||||
this._mesh.children[0].material.map = texture;
|
||||
this._mesh.children[0].material.needsUpdate = true;
|
||||
this._ready = true;
|
||||
}, null, xhr => {
|
||||
console.log(xhr);
|
||||
});
|
||||
}
|
||||
|
||||
// Convert from quadcode to TMS tile coordinates
|
||||
_quadcodeToTile(quadcode) {
|
||||
var x = 0;
|
||||
var y = 0;
|
||||
var z = quadcode.length;
|
||||
|
||||
for (var i = z; i > 0; i--) {
|
||||
var mask = 1 << (i - 1);
|
||||
var q = +quadcode[z - i];
|
||||
if (q === 1) {
|
||||
x |= mask;
|
||||
}
|
||||
if (q === 2) {
|
||||
y |= mask;
|
||||
}
|
||||
if (q === 3) {
|
||||
x |= mask;
|
||||
y |= mask;
|
||||
}
|
||||
}
|
||||
|
||||
return [x, y, z];
|
||||
}
|
||||
|
||||
// Convert WGS84 tile bounds to world coordinates
|
||||
_tileBoundsFromWGS84(boundsWGS84) {
|
||||
var sw = this._layer._world.latLonToPoint(LatLon(boundsWGS84[1], boundsWGS84[0]));
|
||||
var ne = this._layer._world.latLonToPoint(LatLon(boundsWGS84[3], boundsWGS84[2]));
|
||||
|
||||
return [sw.x, sw.y, ne.x, ne.y];
|
||||
}
|
||||
|
||||
// Get tile bounds in WGS84 coordinates
|
||||
_tileBoundsWGS84(tile) {
|
||||
var e = this._tile2lon(tile[0] + 1, tile[2]);
|
||||
var w = this._tile2lon(tile[0], tile[2]);
|
||||
var s = this._tile2lat(tile[1] + 1, tile[2]);
|
||||
var n = this._tile2lat(tile[1], tile[2]);
|
||||
return [w, s, e, n];
|
||||
}
|
||||
|
||||
_tile2lon(x, z) {
|
||||
return x / Math.pow(2, z) * 360 - 180;
|
||||
}
|
||||
|
||||
_tile2lat(y, z) {
|
||||
var n = Math.PI - 2 * Math.PI * y / Math.pow(2, z);
|
||||
return r2d * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
|
||||
}
|
||||
|
||||
_boundsToCenter(bounds) {
|
||||
var x = bounds[0] + (bounds[2] - bounds[0]) / 2;
|
||||
var y = bounds[1] + (bounds[3] - bounds[1]) / 2;
|
||||
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
_getSide(bounds) {
|
||||
return (new THREE.Vector3(bounds[0], 0, bounds[3])).sub(new THREE.Vector3(bounds[0], 0, bounds[1])).length();
|
||||
}
|
||||
}
|
||||
|
||||
export default Tile;
|
|
@ -0,0 +1,50 @@
|
|||
import LRUCache from 'lru-cache';
|
||||
import Tile from './Tile';
|
||||
|
||||
// This process is based on a similar approach taken by OpenWebGlobe
|
||||
// See: https://github.com/OpenWebGlobe/WebViewer/blob/master/source/core/globecache.js
|
||||
|
||||
class TileCache {
|
||||
constructor(cacheLimit) {
|
||||
this._cache = LRUCache(cacheLimit);
|
||||
}
|
||||
|
||||
// Returns true if all specified tile providers are ready to be used
|
||||
// Otherwise, returns false
|
||||
isReady() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get a cached tile or request a new one if not in cache
|
||||
requestTile(quadcode, layer) {
|
||||
var tile = this._cache.get(quadcode);
|
||||
|
||||
if (!tile) {
|
||||
// Set up a brand new tile
|
||||
tile = new Tile(quadcode, layer);
|
||||
|
||||
// Request data for various tile providers
|
||||
// tile.requestData(imageProviders);
|
||||
|
||||
// Add tile to cache, though it won't be ready yet as the data is being
|
||||
// requested from various places asynchronously
|
||||
this._cache.set(quadcode, tile);
|
||||
}
|
||||
|
||||
return tile;
|
||||
}
|
||||
|
||||
// Get a cached tile without requesting a new one
|
||||
getTile(quadcode) {
|
||||
return this._cache.get(quadcode);
|
||||
}
|
||||
|
||||
// Destroy the cache and remove it from memory
|
||||
//
|
||||
// TODO: Call destroy method on items in cache
|
||||
destroy() {
|
||||
this._cache.reset();
|
||||
}
|
||||
}
|
||||
|
||||
export default TileCache;
|
Ładowanie…
Reference in New Issue