1 /***************************************
  2  * Copyright 2011, 2012 GlobWeb contributors.
  3  *
  4  * This file is part of GlobWeb.
  5  *
  6  * GlobWeb is free software: you can redistribute it and/or modify
  7  * it under the terms of the GNU Lesser General Public License as published by
  8  * the Free Software Foundation, version 3 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * GlobWeb is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 14  * GNU Lesser General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU General Public License
 17  * along with GlobWeb. If not, see <http://www.gnu.org/licenses/>.
 18  ***************************************/
 19 
 20 define(['./Tile','./TilePool', './TileRequest', './TileIndexBuffer', './Program', './CoordinateSystem'],
 21 	function (Tile,TilePool,TileRequest,TileIndexBuffer,Program, CoordinateSystem) {
 22 
 23 /** @constructor
 24 	TileManager constructor
 25  */
 26 var TileManager = function( globe )
 27 {
 28 	this.globe = globe;
 29 	this.renderContext = this.globe.renderContext;
 30 	this.tilePool = new TilePool(this.renderContext);
 31 	this.imageryProvider = null;
 32 	this.elevationProvider = null;
 33 	this.tilesToRender = [];
 34 	this.tilesToRequest = [];
 35 	this.postRenderers = [];
 36 	this.level0Tiles = [];
 37 	this.levelZeroTexture = null;
 38 	
 39 	// Tile requests : limit to 4 at a given time
 40 	this.maxRequests = 4;
 41 	this.availableRequests = [];
 42 	for ( var i=0; i < this.maxRequests; i++ )
 43 	{
 44 		this.availableRequests[i] = new TileRequest(this);
 45 	}
 46 	this.completedRequests = [];
 47 				
 48 	this.level0TilesLoaded = false;
 49 	
 50 	// Configuration for tile
 51 	this.tileConfig = {
 52 		tesselation: 9,
 53 		skirt: true,
 54 		cullSign: 1.0,
 55 		imageSize: 256,
 56 		vertexSize: this.renderContext.lighting ? 6 : 3, 
 57 		normals: this.renderContext.lighting
 58 	};
 59 		
 60 	// Shared index and texture coordinate buffer : all tiles uses the same
 61 	this.tcoordBuffer = null;
 62 	this.tileIndexBuffer = new TileIndexBuffer(this.renderContext,this.tileConfig);
 63 	this.identityTextureTransform = [ 1.0, 1.0, 0.0, 0.0 ];
 64 
 65 	// For debug
 66 	this.showWireframe = false;
 67 	this.freeze = false;
 68 
 69 	// Stats
 70 	this.numTilesGenerated = 0;
 71 	this.frameNumber = 0;
 72 
 73 	var vertexShader = "\
 74 	attribute vec3 vertex;\n\
 75 	attribute vec2 tcoord;\n\
 76 	uniform mat4 modelViewMatrix;\n\
 77 	uniform mat4 projectionMatrix;\n\
 78 	uniform vec4 texTransform;\n\
 79 	varying vec2 texCoord;\n";
 80 	if ( this.renderContext.lighting )
 81 		vertexShader += "attribute vec3 normal;\nvarying vec3 color;\n";
 82 	vertexShader += "\
 83 	void main(void) \n\
 84 	{\n\
 85 		gl_Position = projectionMatrix * modelViewMatrix * vec4(vertex, 1.0);\n";
 86 	if ( this.renderContext.lighting )
 87 		vertexShader += "vec4 vn = modelViewMatrix * vec4(normal,0);\ncolor = max( vec3(-vn[2],-vn[2],-vn[2]), 0.0 );\n";
 88 	vertexShader += "\
 89 		texCoord = vec2(tcoord.s * texTransform.x + texTransform.z, tcoord.t * texTransform.y + texTransform.w);\n\
 90 	}\n\
 91 	";
 92 
 93 	var fragmentShader = "\
 94 	precision highp float; \n\
 95 	varying vec2 texCoord;\n";
 96 	if ( this.renderContext.lighting )
 97 		fragmentShader += "varying vec3 color;\n";
 98 	fragmentShader += "\
 99 	uniform sampler2D colorTexture;\n\
100 	void main(void)\n\
101 	{\n\
102 		gl_FragColor.rgb = texture2D(colorTexture, texCoord).rgb;\n";
103 	if ( this.renderContext.lighting )
104 		fragmentShader += "gl_FragColor.rgb *= color;\n";
105 	fragmentShader += "\
106 		gl_FragColor.a = 1.0;\n\
107 	}\n\
108 	";
109 	
110 	this.program = new Program(this.renderContext);
111 	this.program.createFromSource( vertexShader, fragmentShader );
112 }
113 
114 /**************************************************************************************************************/
115 
116 /** 
117 	Add post renderer
118  */
119 TileManager.prototype.addPostRenderer = function(renderer)
120 {	
121 	this.postRenderers.push( renderer );
122 }
123 
124 /**************************************************************************************************************/
125 
126 /** 
127 	Remove a post renderer
128  */
129 TileManager.prototype.removePostRenderer = function(renderer)
130 {
131 	var rendererIndex = this.postRenderers.indexOf(renderer);
132 	if ( rendererIndex != -1 )
133 	{
134 		// Remove the renderer from all the tiles if it has a cleanupTile method
135 		if ( renderer.cleanupTile )
136 			this.visitTiles( function(tile) { renderer.cleanupTile(tile); } );
137 			
138 		// Remove renderer from the list
139 		this.postRenderers.splice( rendererIndex, 1 );
140 	}
141 }
142 
143 /**************************************************************************************************************/
144 
145 /** 
146 	Set the imagery provider to be used
147  */
148 TileManager.prototype.setImageryProvider = function(ip)
149 {
150 	this.reset();
151 	this.imageryProvider = ip;
152 	
153 	if (ip)
154 	{
155 		// Rebuild level zero tiles
156 		this.tileConfig.imageSize = ip.tilePixelSize;
157 		this.level0Tiles = ip.tiling.generateLevelZeroTiles(this.tileConfig,this.tilePool);
158 	}
159 }
160 
161 /**************************************************************************************************************/
162 
163 /** 
164 	Set the elevation provider to be used
165  */
166 TileManager.prototype.setElevationProvider = function(tp)
167 {	
168 	this.reset();
169 	this.elevationProvider = tp;
170 	this.tileConfig.tesselation = tp ? tp.tilePixelSize : 9;
171 }
172 
173 /**************************************************************************************************************/
174 
175 /**
176 	Reset the tile manager : remove all the tiles
177  */
178 TileManager.prototype.reset = function()
179 {
180 	// Reset all level zero tiles : destroy render data, and reset state to NONE
181 	for (var i = 0; i < this.level0Tiles.length; i++)
182 	{
183 		this.level0Tiles[i].deleteChildren(this.renderContext,this.tilePool);
184 		this.level0Tiles[i].dispose(this.renderContext,this.tilePool);
185 	}
186 	
187 	// Reset the shared buffers : texture coordinate and indices
188 	var gl = this.renderContext.gl;
189 	this.tileIndexBuffer.reset();
190 	gl.deleteBuffer( this.tcoordBuffer );
191 	this.tcoordBuffer = null;
192 	
193 	this.levelZeroTexture = null;
194 	
195 	this.level0TilesLoaded = false;
196 }
197 
198 /**************************************************************************************************************/
199 
200 /** 
201 	Tile visitor
202  */
203 TileManager.prototype.visitTiles = function( callback )
204 {
205 	// Store the tiles to process in an array, first copy level0 tiles
206 	var tilesToProcess = this.level0Tiles.concat([]);
207 	
208 	while( tilesToProcess.length > 0 )
209 	{
210 		// Retreive the first tile and remove it from the array
211 		var tile = tilesToProcess.shift();
212 		
213 		callback( tile );
214 		
215 		// Add tile children to array to be processed later
216 		if ( tile.children )
217 		{
218 			tilesToProcess.push( tile.children[0] );
219 			tilesToProcess.push( tile.children[1] );
220 			tilesToProcess.push( tile.children[2] );
221 			tilesToProcess.push( tile.children[3] );
222 		}
223 	}
224 }
225 
226 /**************************************************************************************************************/
227 
228 /**
229 	Traverse tiless tiles
230  */
231  TileManager.prototype.traverseTiles = function()
232  {		
233 	this.tilesToRender.length = 0;
234 	this.tilesToRequest.length = 0;
235 	this.numTraversedTiles = 0;
236 	
237 	// First load level 0 tiles if needed
238 	if ( !this.level0TilesLoaded && !this.levelZeroTexture )
239 	{
240 		this.level0TilesLoaded = true;
241 		for ( var i = 0; i < this.level0Tiles.length; i++ )
242 		{
243 			var tile = this.level0Tiles[i];
244 			var tileIsLoaded = tile.state == Tile.State.LOADED;
245 			
246 			// Update frame number
247 			tile.frameNumber = this.frameNumber;
248 			
249 			this.level0TilesLoaded = this.level0TilesLoaded && tileIsLoaded;
250 			if ( !tileIsLoaded )
251 			{		
252 				// Request tile if necessary
253 				if ( tile.state == Tile.State.NONE )
254 				{
255 					tile.state = Tile.State.REQUESTED;
256 					this.tilesToRequest.push(tile);
257 				}
258 				else if ( tile.state == Tile.State.ERROR )
259 				{
260 					this.globe.publish("baseLayersError");
261 					this.imageryProvider._ready = false;
262 				}
263 			}
264 		}
265 		if ( this.level0TilesLoaded )
266 		{
267 			this.globe.publish("baseLayersReady");
268 		}
269 	}
270 	
271 	// Traverse tiles
272 	if ( this.level0TilesLoaded || this.levelZeroTexture )
273 	{
274 		// Normal traversal, iterate through level zero tiles and process them recursively
275 		for ( var i = 0; i < this.level0Tiles.length; i++ )
276 		{
277 			var tile = this.level0Tiles[i];
278 			if ( !tile.isCulled(this.renderContext) )
279 			{
280 				this.processTile(tile,0);
281 			}
282 			else 
283 			{
284 				var tileIsLoaded = (tile.state == Tile.State.LOADED);
285 				// Remove texture from level 0 tile, only if there is a global level zero texture
286 				if( this.levelZeroTexture && tileIsLoaded )
287 				{
288 						this.tilePool.disposeGLTexture( tile.texture );
289 						tile.texture = null;
290 						tile.state = Tile.State.NONE;
291 				}
292 				// Delete its children
293 				tile.deleteChildren(this.renderContext,this.tilePool);
294 			}
295 		}
296 	}
297 }
298 
299 /**************************************************************************************************************/
300 
301 /**
302 	Process a tile
303  */
304 TileManager.prototype.processTile = function(tile,level)
305 {
306 	this.numTraversedTiles++;
307 	
308 	// Update frame number
309 	tile.frameNumber = this.frameNumber;
310 
311 	// Request the tile if needed
312 	if ( tile.state == Tile.State.NONE )
313 	{
314 		tile.state = Tile.State.REQUESTED;
315 		
316 		// Add it to the request
317 		this.tilesToRequest.push(tile);
318 	}
319 	
320 	// Check if the tiles needs to be refined
321 	if ( (tile.state == Tile.State.LOADED) && (level+1 < this.imageryProvider.numberOfLevels) && (tile.needsToBeRefined(this.renderContext) ) )
322 	{
323 		// Create the children if needed
324 		if ( tile.children == null )
325 		{
326 			tile.createChildren();
327 		}
328 		
329 		for ( var i = 0; i < 4; i++ )
330 		{
331 			if (!tile.children[i].isCulled(this.renderContext))
332 			{
333 				this.processTile(tile.children[i],level+1);
334 			}
335 			else
336 			{
337 				tile.children[i].deleteChildren(this.renderContext,this.tilePool);
338 			}
339 		}
340 	}
341 	else
342 	{
343 		// Push the tiles to render
344 		this.tilesToRender.push( tile );
345 	}
346 }
347 
348 /**************************************************************************************************************/
349 
350 /**
351 	Generate tiles
352  */
353  TileManager.prototype.generateReceivedTiles = function()
354  {
355 	while ( this.completedRequests.length > 0 )
356 	{
357 		var tileRequest = this.completedRequests.pop();
358 		var tile = tileRequest.tile;
359 		if ( tile.frameNumber == this.frameNumber )
360 		{
361 			// Generate the tile using data from tileRequest
362 			if ( this.elevationProvider )
363 			{
364 				tile.generate( this.tilePool, tileRequest.image, this.elevationProvider.parseElevations( tileRequest.elevations ) );
365 			}
366 			else
367 				tile.generate( this.tilePool, tileRequest.image );
368 							
369 			// Now post renderers can generate their data on the new tile
370 			for (var i=0; i < this.postRenderers.length; i++ )
371 			{
372 				if ( this.postRenderers[i].generate )
373 					this.postRenderers[i].generate(tile);
374 			}
375 			
376 			this.numTilesGenerated++;
377 			this.renderContext.requestFrame();
378 		}
379 		else
380 		{
381 			tile.state = Tile.State.NONE;			
382 		}
383 		this.availableRequests.push(tileRequest);
384 	}
385 	
386 	// All requests have been processed, send endBackgroundLoad event
387 	if ( this.availableRequests.length == this.maxRequests )
388 		this.globe.publish("endBackgroundLoad");
389 
390 }
391 
392 /**************************************************************************************************************/
393 
394 /**
395 	Render tiles
396  */
397  TileManager.prototype.renderTiles = function()
398  {	
399 	var rc = this.renderContext;
400 	var gl = rc.gl;
401 	
402 	gl.enable(gl.POLYGON_OFFSET_FILL);
403 	gl.polygonOffset(0,4);
404 	// TODO : remove this
405 	gl.disable(gl.CULL_FACE);
406 	
407     // Setup program
408     this.program.apply();
409 	
410 	var attributes = this.program.attributes;
411 	
412 	// Compute near/far from tiles
413 	var nr;
414 	var fr;
415 	if ( this.tileConfig.cullSign < 0 )
416 	{
417 		// When in "Astro" mode, do not compute near/far from tiles not really needed
418 		// And the code used for "Earth" does not works really well, when the earth is seen from inside...
419 		nr = 0.2 * CoordinateSystem.radius;
420 		fr = 1.1 * CoordinateSystem.radius;
421 	}
422 	else
423 	{
424 		nr = 1e9;
425 		fr = 0.0;
426 		for ( var i = 0; i < this.tilesToRender.length; i++ )
427 		{
428 			var tile = this.tilesToRender[i];
429 			// Update near/far to take into account the tile
430 			nr = Math.min( nr, tile.distance - 2.0 * tile.radius );
431 			fr = Math.max( fr, tile.distance + 2.0 * tile.radius );
432 		}
433 	}
434 	rc.near = Math.max( rc.minNear, Math.min(nr,rc.near) );
435 	rc.far = Math.max( fr, rc.far );
436 	
437 	// Update projection matrix with new near and far values
438 	mat4.perspective(rc.fov, rc.canvas.width / rc.canvas.height, rc.near, rc.far, rc.projectionMatrix);
439 
440 	// Setup state
441 	gl.activeTexture(gl.TEXTURE0);
442 	gl.uniformMatrix4fv(this.program.uniforms["projectionMatrix"], false, rc.projectionMatrix);
443 	gl.uniform1i(this.program.uniforms["colorTexture"], 0);
444 	
445 	// Bind the texture coordinate buffer (shared between all tiles
446 	if ( !this.tcoordBuffer )
447 		this.buildSharedTexCoordBuffer();
448 	gl.bindBuffer(gl.ARRAY_BUFFER, this.tcoordBuffer);
449 	gl.vertexAttribPointer(attributes['tcoord'], 2, gl.FLOAT, false, 0, 0);
450 	
451 	var currentIB = null;
452 	
453 	var currentTextureTransform = null;
454 	
455 	for ( var i = 0; i < this.tilesToRender.length; i++ )
456 	{
457 		var tile = this.tilesToRender[i];
458 		
459 		var isLoaded = ( tile.state == Tile.State.LOADED );
460 		var isLevelZero = ( tile.parentIndex == -1 );
461 		
462 		// Bind tile texture
463 		var textureTransform;
464 		if ( !isLoaded && isLevelZero )
465 		{
466 			// The texture is not yet loaded but there is a full texture to render the tile
467 			gl.bindTexture(gl.TEXTURE_2D, this.levelZeroTexture);
468 			textureTransform = tile.texTransform;
469 		}
470 		else
471 		{
472 			gl.bindTexture(gl.TEXTURE_2D, tile.texture);
473 			textureTransform = this.identityTextureTransform;
474 		}
475 		
476 		// Update texture transform
477 		if ( currentTextureTransform != textureTransform )
478 		{
479 			gl.uniform4f(this.program.uniforms["texTransform"], textureTransform[0], textureTransform[1], textureTransform[2], textureTransform[3]);
480 			currentTextureTransform = textureTransform;
481 		}
482 	
483 		// Update uniforms for modelview matrix
484 		mat4.multiply( rc.viewMatrix, tile.matrix, rc.modelViewMatrix );
485 		gl.uniformMatrix4fv(this.program.uniforms["modelViewMatrix"], false, rc.modelViewMatrix);
486 	
487 		// Bind the vertex buffer
488 		gl.bindBuffer(gl.ARRAY_BUFFER, tile.vertexBuffer);
489 		gl.vertexAttribPointer(attributes['vertex'], 3, gl.FLOAT, false, 4*this.tileConfig.vertexSize, 0);
490 		if (this.tileConfig.normals)
491 			gl.vertexAttribPointer(attributes['normal'], 3, gl.FLOAT, false, 4*this.tileConfig.vertexSize, 12);
492 				
493 		var indexBuffer = ( isLoaded || isLevelZero ) ? this.tileIndexBuffer.getSolid() : this.tileIndexBuffer.getSubSolid(tile.parentIndex);
494 		// Bind the index buffer only if different (index buffer is shared between tiles)
495 		if ( currentIB != indexBuffer )
496 		{	
497 			gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
498 			currentIB = indexBuffer;
499 		}
500 		
501 		// Finally draw the tiles
502 		gl.drawElements(gl.TRIANGLES, currentIB.numIndices, gl.UNSIGNED_SHORT, 0);
503 	}
504 	
505 	for (var i=0; i < this.postRenderers.length; i++ )
506 	{
507 		if (this.postRenderers[i].needsOffset)
508 			this.postRenderers[i].render( this.tilesToRender );
509 	}
510 	
511 	gl.disable(gl.POLYGON_OFFSET_FILL);
512 	
513 	for (var i=0; i < this.postRenderers.length; i++ )
514 	{
515 		if (!this.postRenderers[i].needsOffset)
516 			this.postRenderers[i].render( this.tilesToRender );
517 	}
518 }
519 
520 // Internal function to sort tiles
521 var _sortTilesByDistance = function(t1,t2)
522 {
523 	return t1.distance - t2.distance;
524 };
525 
526 /**************************************************************************************************************/
527 
528 /**
529 	Request tiles
530  */
531  TileManager.prototype.launchRequests = function()
532  {
533 	// Process request
534 	this.tilesToRequest.sort( _sortTilesByDistance );
535 	
536 	var trl = this.tilesToRequest.length; 
537 	for ( var i = 0; i < trl; i++ )
538 	{
539 		var tile = this.tilesToRequest[i];
540 		if ( this.availableRequests.length > 0 ) // Check to limit the number of requests done per frame
541 		{
542 			// First launch request, send an event
543 			if ( this.availableRequests.length == this.maxRequests )
544 				this.globe.publish("startBackgroundLoad");
545 			
546 			var tileRequest = this.availableRequests.pop();
547 			tileRequest.launch( tile );
548 			tile.state = Tile.State.LOADING;
549 		}
550 		else
551 		{
552 			tile.state = Tile.State.NONE;
553 		}
554 	}
555 }
556 
557 /**************************************************************************************************************/
558 
559 /**
560 	Render the tiles
561  */
562 TileManager.prototype.render = function()
563 {
564 	if ( this.imageryProvider == null
565 		|| !this.imageryProvider._ready )
566 	{
567 		return;
568 	}
569 	
570 	// Create the texture for level zero
571 	if ( this.levelZeroTexture == null && this.imageryProvider.levelZeroImage )
572 	{
573 		this.levelZeroTexture = this.renderContext.createNonPowerOfTwoTextureFromImage(this.imageryProvider.levelZeroImage);
574 		this.globe.publish("baseLayersReady");
575 	}
576 
577 	var stats = this.renderContext.stats;
578 	
579 	if (!this.freeze)
580 	{
581 		if (stats) stats.start("traverseTime");
582 		this.traverseTiles();
583 		if (stats) stats.end("traverseTime");
584 	}
585 
586 	if ( this.level0TilesLoaded || this.levelZeroTexture )
587 	{
588 		if (stats) stats.start("renderTime");
589 		this.renderTiles();
590 		if (stats) stats.end("renderTime");
591 	}
592 	
593 	if (stats) stats.start("generateTime");
594 	this.generateReceivedTiles();
595 	if (stats) stats.end("generateTime");
596 	
597 	if (stats) stats.start("requestTime");
598 	this.launchRequests();
599 	if (stats) stats.end("requestTime");
600 		
601 	this.frameNumber++;
602 }
603 
604 /**************************************************************************************************************/
605 
606 /**
607 	Returns visible tile for given longitude/latitude, null otherwise
608  */
609 TileManager.prototype.getVisibleTile = function(lon, lat)
610 {
611 	return this.imageryProvider.tiling.findInsideTile(lon, lat, this.tilesToRender);
612 }
613 
614 /**************************************************************************************************************/
615 
616 /**
617 	Build shared texture coordinate buffer
618  */
619 TileManager.prototype.buildSharedTexCoordBuffer = function()
620 {
621 	var size = this.tileConfig.tesselation;
622 	var skirt = this.tileConfig.skirt;
623 	var bufferSize = 2*size*size;
624 	if (skirt)
625 		bufferSize += 2*size*6;
626 
627 	var tcoords = new Float32Array( bufferSize );
628 
629 	var step = 1.0 / (size-1);
630 	
631 	var offset = 0;
632 	var v = 0.0;
633 	for ( var j=0; j < size; j++)
634 	{
635 		var u = 0.0;
636 		for ( var i=0; i < size; i++)
637 		{
638 			tcoords[offset] = u;
639 			tcoords[offset+1] = v;
640 
641 			offset += 2;
642 			u += step;
643 		}
644 		
645 		v += step;
646 	}
647 	
648 	if ( skirt )
649 	{
650 		// Top skirt
651 		u = 0.0;
652 		v = 0.0;
653 		for ( var i=0; i < size; i++)
654 		{
655 			tcoords[offset] = u;
656 			tcoords[offset+1] = v;
657 			u += step;
658 			offset += 2;
659 		}
660 		// Bottom skirt
661 		u = 0.0;
662 		v = 1.0;
663 		for ( var i=0; i < size; i++)
664 		{
665 			tcoords[offset] = u;
666 			tcoords[offset+1] = v;
667 			u += step;
668 			offset += 2;
669 		}
670 		// Left skirt
671 		u = 0.0;
672 		v = 0.0;
673 		for ( var i=0; i < size; i++)
674 		{
675 			tcoords[offset] = u;
676 			tcoords[offset+1] = v;
677 			v += step;
678 			offset += 2;
679 		}
680 		// Right skirt
681 		u = 1.0;
682 		v = 0.0;
683 		for ( var i=0; i < size; i++)
684 		{
685 			tcoords[offset] = u;
686 			tcoords[offset+1] = v;
687 			v += step;
688 			offset += 2;
689 		}
690 		
691 		// Center skirt
692 		u = 0.0;
693 		v = 0.5;
694 		for ( var i=0; i < size; i++)
695 		{
696 			tcoords[offset] = u;
697 			tcoords[offset+1] = v;
698 			u += step;
699 			offset += 2;
700 		}
701 		
702 		// Middle skirt
703 		u = 0.5;
704 		v = 0.0;
705 		for ( var i=0; i < size; i++)
706 		{
707 			tcoords[offset] = u;
708 			tcoords[offset+1] = v;
709 			v += step;
710 			offset += 2;
711 		}
712 	}
713 	
714 	var gl = this.renderContext.gl;
715 	var tcb = gl.createBuffer();
716 	gl.bindBuffer(gl.ARRAY_BUFFER, tcb);
717 	gl.bufferData(gl.ARRAY_BUFFER, tcoords, gl.STATIC_DRAW);
718 	
719 	this.tcoordBuffer = tcb;
720 }
721 
722 /**************************************************************************************************************/
723 
724 return TileManager;
725 
726 });