Working prototype of tile loader with real map tiles

master
Robin Hawkes 2016-02-15 22:55:25 +00:00
rodzic 9ee293817b
commit 5e8fa09632
7 zmienionych plików z 2835 dodań i 189 usunięć

2626
dist/vizicities.js vendored

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

Wyświetl plik

@ -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"
}
}

Wyświetl plik

@ -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

Wyświetl plik

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

Wyświetl plik

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