offline-editor-js/lib/tpk/TPKLayer.js

721 lines
36 KiB
JavaScript

/**
* Library for reading an ArcGIS Tile Package (.tpk) file and displaying the tiles
* as a map that can be used both online and offline.
*
* Note: you may have to rename your .tpk file to use .zip in order for it to be recognized.
*
* Author: Andy Gup
* Credits: Mansour Raad for his ArcGIS API for Flex TPKLayer,
* and Jon leighton for his super fast ArrayBuffer to Base64 convert.
*/
define([
"dojo/_base/declare","esri/geometry/Extent","dojo/query","esri/SpatialReference",
"esri/layers/TileInfo","esri/layers/TiledMapServiceLayer",
"dojo/Deferred","dojo/promise/all","dojo/Evented"],
function(declare,Extent,query,SpatialReference,TileInfo,TiledMapServiceLayer,
Deferred,all,Evented){
return declare("O.esri.TPK.TPKLayer",[TiledMapServiceLayer,Evented],{
//
// Public Properties
//
map: null,
store: null, // Reference to the local database store and hooks to it's functionality
MAX_DB_SIZE: 75, // Recommended maximum size in MBs
TILE_PATH:"", // The absolute path to the root of bundle/bundleX files e.g. V101/YOSEMITE_MAP/
RECENTER_DELAY: 350, // Millisecond delay before attempting to recent map after an orientation change
PARSING_ERROR: "parsingError", // An error was encountered while parsing a TPK file.
DB_INIT_ERROR: "dbInitError", // An error occurred while initializing the database.
DB_FULL_ERROR: "dbFullError", // No space left in the database.
NO_SUPPORT_ERROR: "libNotSupportedError", // Error indicating this library is not supported in a particular browser.
PROGRESS_START: "start",
PROGRESS_END: "end",
WINDOW_VALIDATED: "windowValidated", // All window functionality checks have passed
DB_VALIDATED: "dbValidated", // All database functionality checks have passed
//
// Events
//
DATABASE_ERROR_EVENT: "databaseErrorEvent", // An error thrown by the database.
VALIDATION_EVENT: "validationEvent", // Library validation checks.
PROGRESS_EVENT: "progress", // Event dispatched while parsing a bundle file.
//
// Private properties
//
_maxDBSize: 75, // User configurable maximum size in MBs.
_isDBWriteable: true, // Manually allow or stop writing to the database.
_isDBValid: false, // Does the browser support IndexedDB or IndexedDBShim
_autoCenter: null, // Auto center the map
_fileEntriesLength: 0, // Number of files in zip
_inMemTilesObject: null, // Stores unzipped files from tpk
_inMemTilesObjectLength: 0,
_zeroLengthFileCounter: 0, // For counting the number of zero length files in the tpk (e.g. directories)
_imageURL: "data:image/png;base64,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pmk0TROPj48Hn3l4eIimaSJN06O3A4NJwDMxm82ibdtXo4D5fB6r1Sp+//4dz8/Pw5H+6ekpdrtdJEkS8/n8S/9f713ie3vaceo9x557QAB451Sgfyin34HKshweunk5HzAej2MymXz5+f9nbjJyI9L39Wu5XP55+XQZ39uxR4Z3u90wSXjqEV0wAjhjx47oaZq63Me/ZhIQBAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEAAbAJQAAAAQAEABAAQAAAAQAEABAAQAAAAQAEABAAQAAAAQAEABAAQAAAAQAEABAAQAAAAQAEABAAQAAAAQAEABAAQAAAAQAEABAAQAAAAQAEABAAEABAAAABAAQAEABAAAABAAQAEABAAAABAAQAEABAAAABAAQAEABAAAABAAQAEABAAAABAAQAEABAAAABAAQAEABAAAABAAQAEABAAAABAAQAEAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEABAAQAEAAAAEAvqe/BwCeKjUweoA8pQAAAABJRU5ErkJggg==",
constructor:function(){
this._self = this;
this._inMemTilesIndex = [];
this._inMemTilesObject = {};
this.store = new O.esri.Tiles.TilesStore();
this._validate();
},
extend: function(files){
this._fileEntriesLength = files.length;
this.emit(this.PROGRESS_EVENT,this.PROGRESS_START);
this._parseInMemFiles(files,function (){
//Parse conf.xml and conf.cdi to get the required setup info
this._parseConfCdi(function(initExtent){
this.initialExtent = (this.fullExtent = initExtent);
this._parseConfXml(function(result){
this.tileInfo = new TileInfo(result);
this.spatialReference = new SpatialReference({wkid:this.tileInfo.spatialReference.wkid});
this.loaded = true;
this.onLoad(this);
this.emit(this.PROGRESS_EVENT,this.PROGRESS_END);
}.bind(this._self));
}.bind(this._self));
}.bind(this._self));
},
/**
* Overrides getTileUrl method
* @param level
* @param row
* @param col
* @returns {string}
*/
getTileUrl:function(level,row,col){
this.emit(this.PROGRESS_EVENT,this.PROGRESS_START);
var layersDir = this._self.TILE_PATH + "_alllayers";
var url = this._getCacheFilePath(layersDir,level,row,col);
if(this._inMemTilesObject != {}) {
/* temporary URL returned immediately, as we haven't retrieved the image from the indexeddb yet */
var tileid = "void:/" + level + "/" + row + "/" + col;
if(this.map == null) {
this.map = this.getMap();
}
if(this._autoCenter == null) {
this._autoCenter = new O.esri.TPK.autoCenterMap(this.map,this.RECENTER_DELAY);
this._autoCenter.init();
}
this._getInMemTiles(url,layersDir, level, row, col,tileid,function (result,tileid,url) {
var img = query("img[src=" + tileid + "]")[0];
if (typeof img == "undefined") {
img = new Image();
} //create a blank place holder for undefined images
var imgURL;
if (result) {
console.log("found tile offline", url);
var png = "data:image/png;base64,";
switch(this.tileInfo.format) {
case "JPEG":
imgURL = "data:image/jpg;base64," + result;
break;
case "PNG":
imgURL = png + result;
break;
case "PNG8":
imgURL = png + result;
break;
case "PNG24":
imgURL = png + result;
break;
case "PNG32":
imgURL = png + result;
break;
default:
imgURL = "data:image/jpg;base64," + result;
}
img.style.borderColor = "blue";
}
else {
img.style.borderColor = "green";
console.log("tile is not in the offline store", url);
imgURL = this._imageURL;
}
// when we return a nonexistent url to the image, the TiledMapServiceLayer::_tileErrorHandler() method
// sets img visibility to 'hidden', so we need to show the image back once we have put the data:image
img.style.visibility = "visible";
img.src = imgURL;
//console.log("URL length " + imgURL.length + ", image: " + imgURL);
this.emit(this.PROGRESS_EVENT,this.PROGRESS_END);
return "";
/* this result goes nowhere, seriously */
}.bind(this._self));
return tileid;
}
},
/**
* Optional. Set the maximum database size. Recommended maximum for mobile devices is 100MBs.
* Making the database too large can result in browser crashes and slow performance.
* TPKs can contain a lot of data!
* @param size
*/
setMaxDBSize: function(size){
//Make sure the entry is an integer.
var testRegex = /^\d+$/;
if(testRegex.test(size) && size <= this.MAX_DB_SIZE){
this._maxDBSize = size;
}
else{
console.log("setMaxDBSize Error: invalid entry. Integers only and less than " + this.MAX_DB_SIZE + "MBs");
}
},
/**
* Returns the size of the tiles database.
* @param callback {size , error}. Note, size is in bytes.
*/
getDBSize: function(callback){
this.store.usedSpace(function(size,err){
callback(size,err);
}.bind(this));
},
/**
* Sets whether or not tiles can be written to the database. This function
* can help you manage the size of the tiles database.
* Use this in conjunction with getDBSize() on a map pan or zoom event listener.
* @param value
*/
setDBWriteable: function(/* Boolean */ value){
this._isDBWriteable = value;
},
/**
* Validates whether or not the browser supports this library
* @returns {boolean}
*/
isDBValid: function(){
this._validate();
return this._isDBValid;
},
/**
* Reads a tile into tile database. Works with OfflineTilesBasic.js and OfflineTilesAdvanced.js
* saveToFile() functionality.
* IMPORTANT! The tile must confirm to an object using the pattern shown in _storeTile().
* @param file
* @param callback callback( boolean, error)
*/
loadFromURL: function (tile, callback) // callback(success,msg)
{
if (this.isDBValid()) {
this.store.store(tile, function (success,err) {
//Check the result
if (success) {
console.log("loadFromURL() success.");
callback(true, "");
}
else {
console.log("loadFromURL() Failed.");
callback(false, err);
}
});
}
else {
callback(false, "not supported");
}
},
/**
* Runs specific validation tasks. Reserved for future use.
* Currently only throws console errors. Does not stop execution of the library!
* @private
*/
_validate: function(){
//Verify if basic functionality is supported by the browser
if(!window.File && !window.FileReader && !window.Blob && !window.btoa && !window.DataView){
console.log("TPKLayer library is not supported by this browser");
this.emit(this.VALIDATION_EVENT,{msg:this.NO_SUPPORT_ERROR, err : null});
}
else{
this.emit(this.VALIDATION_EVENT,{msg:this.WINDOW_VALIDATED, err : null});
}
//Verify if IndexedDB is supported and initializes properly
if( /*false &&*/ this.store.isSupported() )
{
this.store.init(function(result){
if(result === false){
console.log("There was an error initializing the TPKLayer database");
this.emit(this.DATABASE_ERROR_EVENT,{msg:this.DB_INIT_ERROR, err: null});
}
else{
this.store.usedSpace(function(size,err){
var mb = this._bytes2MBs(size.sizeBytes);
if(mb > this.MAX_DB_SIZE){
console.log("Database is full!");
this.emit(this.DATABASE_ERROR_EVENT,{msg:this.DB_FULL_ERROR,err : err});
}
this.emit(this.VALIDATION_EVENT,{msg:this.DB_VALIDATED,err : null});
console.log("DB size: " + mb + " MBs, Tile count: " + size.tileCount + ", Error: " + err);
this._isDBValid = true;
}.bind(this));
}
}.bind(this));
}
else
{
console.log("IndexedDB is not supported on your browser.");
this.emit(this.VALIDATION_EVENT,{msg:this.NO_SUPPORT_ERROR, err : null});
}
},
/**
* Function for pulling out individual files from the .tpk/zip and storing
* them in memory.
* @param files
* @param callback
* @private
*/
_parseInMemFiles: function(files,callback){
var inMemTilesLength = this._fileEntriesLength;
this._zeroLengthFileCounter = 0;
var promises = [];
for(var i=0;i < inMemTilesLength;i++){
var deferred = new Deferred();
var name = files[i].filename.toLocaleUpperCase();
var index = name.indexOf("_ALLLAYERS",0);
if(index != -1){
this.TILE_PATH = name.slice(0,index);
}
if(files[i].compressedSize === 0) {
this._zeroLengthFileCounter++;
}
var indexCDI = name.indexOf("CONF.CDI",0);
var indexXML = name.indexOf("CONF.XML",0);
var indexBUNDLE = name.indexOf("BUNDLE",0);
var indexBUNDLX = name.indexOf("BUNDLX",0);
if(indexCDI != -1 || indexXML != -1){
this._unzipConfFiles(files,i,deferred,function(/* deferred */ d, /* token */ t){ console.log("CONF FILE");
d.resolve(t);
});
}
else if(indexBUNDLE != -1 || indexBUNDLX != -1){
this._unzipTileFiles(files,i,deferred,function(/* deferred */ d, /* token */ t){
d.resolve(t);
});
}
else{
deferred.resolve(i);
}
promises.push(deferred);
}
all(promises).then( function(results)
{
callback && callback(results); // jshint ignore:line
});
},
/**
* Calculate the size of an Object based on whether or not the item is enumerable.
* Native Objects don't have a built in size property.
* More info: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty
* @param obj
* @returns {number}
* @constructor
*/
ObjectSize: function(obj) {
var size = 0, key;
for (key in obj) {
if (obj.hasOwnProperty(key)) {
size++;
}
}
return size;
},
/**
* Retrieve XML config files
* @param files
* @param token
* @param callback
* @private
*/
_unzipConfFiles: function(files,token,deferred,callback){
files[token].getData(new O.esri.zip.TextWriter(token),function(data){
this._inMemTilesIndex.push("blank");
var name = files[data.token].filename.toLocaleUpperCase();
this._inMemTilesObject[name]= data.string;
var size = this.ObjectSize(this._inMemTilesObject);
if(size > 0){
callback(deferred,data.token);
}
}.bind(this));
},
/**
* Retrieve binary tile files as ArrayBuffers
* @param files
* @param token
* @param callback
* @private
*/
_unzipTileFiles: function(files,token,deferred,callback){
var that = this;
files[token].getData(new O.esri.zip.BlobWriter(token),function(data){
if(data.size !== 0){
var reader = new FileReader();
reader.token = data.token;
reader.onerror = function (event) {
console.error("_unzipTileFiles Error: " + event.target.error.message);
that.emit(that.PARSING_ERROR, {msg: "Error parsing file: ", err: event.target.error});
};
reader.onloadend = function(evt) {
if(reader.token !== undefined){
that._inMemTilesIndex.push("blank");
var name = files[reader.token].filename.toLocaleUpperCase();
that._inMemTilesObject[name]= reader.result;
var size = that.ObjectSize(that._inMemTilesObject);
if(size > 0){
callback(deferred,data.token);
}
}
};
reader.readAsArrayBuffer(data); //open bundleX
}
});
},
/**
* Parse conf.cdi
* @param callback
* @private
*/
_parseConfCdi: function(callback){
var m_conf_i = this._inMemTilesObject[this.TILE_PATH + "CONF.CDI"];
var x2js = new O.esri.TPK.X2JS();
var jsonObj = x2js.xml_str2json( m_conf_i );
var envelopeInfo = jsonObj.EnvelopeN;
var xmin = parseFloat(envelopeInfo.XMin);
var ymin = parseFloat(envelopeInfo.YMin);
var xmax = parseFloat(envelopeInfo.XMax);
var ymax = parseFloat(envelopeInfo.YMax);
var sr = parseInt(envelopeInfo.SpatialReference.WKID);
var initExtent = new Extent(
xmin,ymin,xmax,ymax, new SpatialReference({wkid:sr})
);
callback(initExtent);
},
/**
* Parse conf.xml
* @param callback
* @private
*/
_parseConfXml:function(callback) {
var m_conf = this._inMemTilesObject[this.TILE_PATH + "CONF.XML"];
var x2js = new O.esri.TPK.X2JS();
var jsonObj = x2js.xml_str2json(m_conf);
var cacheInfo = jsonObj.CacheInfo;
var tileInfo = {};
tileInfo.rows = parseInt(cacheInfo.TileCacheInfo.TileRows);
tileInfo.cols = parseInt(cacheInfo.TileCacheInfo.TileCols);
tileInfo.dpi = parseInt(cacheInfo.TileCacheInfo.DPI);
tileInfo.format = cacheInfo.TileImageInfo.CacheTileFormat;
tileInfo.compressionQuality = parseInt(cacheInfo.TileImageInfo.CompressionQuality);
tileInfo.origin = {
x: parseInt(cacheInfo.TileCacheInfo.TileOrigin.X),
y: parseInt(cacheInfo.TileCacheInfo.TileOrigin.Y)
};
tileInfo.spatialReference = {
"wkid": parseInt(cacheInfo.TileCacheInfo.SpatialReference.WKID)
};
var lods = cacheInfo.TileCacheInfo.LODInfos.LODInfo;
var finalLods = [];
for (var i = 0; i < lods.length; i++) {
finalLods.push({
"level": parseFloat(lods[i].LevelID),
"resolution": parseFloat(lods[i].Resolution),
"scale": parseFloat(lods[i].Scale)});
}
tileInfo.lods = finalLods;
callback(tileInfo);
},
/**
* Parses the in-memory tile cache and returns a base64 tile image
* @param layersDir
* @param level
* @param row
* @param col
* @param tileCount - number of tiles in the Extent
* @param tiledId - id of the associated tile being passed
* @param callback
* @private
*/
_getInMemTiles: function(url,layersDir,level,row,col,tileId,callback){
var that = this._self;
var db = this.store;
//First check in the database if the tile exists.
//If not then we store the tile in the database later.
this.store.retrieve(url, function(success, offlineTile){
if( success && offlineTile.img !== "")
{
console.log("Tile found in storage: " + url);
callback(offlineTile.img,tileId,url);
}
else {
console.log("Tile is not in storage: " + url);
var snappedRow = Math.floor(row / 128) * 128;
var snappedCol = Math.floor(col / 128) * 128;
var path = this._getCacheFilePath(layersDir, level, snappedRow, snappedCol).toLocaleUpperCase();
var offset;
var bundleIndex = path + ".BUNDLE";
var bufferI = this._inMemTilesObject[bundleIndex];
var bufferX = this._inMemTilesObject[path + ".BUNDLX"];
if(bufferI !== undefined || bufferX !== undefined) {
offset = this._getOffset(level, row, col, snappedRow, snappedCol);
var pointer = that._getPointer(bufferX, offset);
that._buffer2Base64(bufferI,pointer,function(result){
if (that._isDBWriteable) {
that._storeTile(url, result, db,function(success,err){
if(err){
console.log("TPKLayer - Error writing to database." + err.message);
that.emit(that.DATABASE_ERROR_EVENT,{msg:"Error writing to database. ", err : err});
}
});
}
callback(result,tileId, url);
}.bind(that));
}
else{
console.log("_getInMemTiles Error: Invalid values");
callback(null,tileId,url);
}
}
}.bind(that));
},
/**
* Stores a tile in the local database.
* @param url
* @param base64Str
* @param db
* @param callback
* @private
*/
_storeTile: function(url,base64Str,db,callback){
var tile = {
url: url,
img: base64Str
};
db.store(tile,function(success,err){
callback(success,err);
});
},
/**
* Returns a pointer for reading a BUNDLE binary file as based on the given offset.
* @param buffer
* @param offset
* @returns {Uint8}
* @private
*/
_getPointer: function(/* ArrayBuffer */ buffer,offset){
var snip = buffer.slice(offset);
var dv = new DataView(snip,0,5);
var nume1 = dv.getUint8(0,true);
var nume2 = dv.getUint8(1,true);
var nume3 = dv.getUint8(2,true);
var nume4 = dv.getUint8(3,true);
var nume5 = dv.getUint8(4,true);
var value = nume5;
value = value * 256 + nume4;
value = value * 256 + nume3;
value = value * 256 + nume2;
value = value * 256 + nume1;
return value;
},
/**
* Convert an ArrayBuffer to base64. My testing shows this to be
* much faster than combining Blobs and btoa().
* ALL CREDITS: https://gist.github.com/jonleighton/958841
* NO licensing listed at the gist repo.
* @param arrayBuffer
* @returns {string}
* @private
*/
_base64ArrayBuffer: function(arrayBuffer) {
var base64 = "";
var encodings = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var bytes = new Uint8Array(arrayBuffer);
var byteLength = bytes.byteLength;
var byteRemainder = byteLength % 3;
var mainLength = byteLength - byteRemainder;
var a, b, c, d;
var chunk;
/*jslint bitwise: true */
// Main loop deals with bytes in chunks of 3
for (var i = 0; i < mainLength; i = i + 3) {
// Combine the three bytes into a single integer
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
// Use bitmasks to extract 6-bit segments from the triplet
a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
d = chunk & 63; // 63 = 2^6 - 1
// Convert the raw binary segments to the appropriate ASCII encoding
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
}
// Deal with the remaining bytes and padding
if (byteRemainder == 1) {
chunk = bytes[mainLength];
a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
// Set the 4 least significant bits to zero
b = (chunk & 3) << 4; // 3 = 2^2 - 1
base64 += encodings[a] + encodings[b] + "==";
} else if (byteRemainder == 2) {
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
// Set the 2 least significant bits to zero
c = (chunk & 15) << 2; // 15 = 2^4 - 1
base64 += encodings[a] + encodings[b] + encodings[c] + "=";
}
/*jslint bitwise: false */
return base64;
},
/**
* Given a ArrayBuffer and a position it will return a Base64 tile image
* @param arrayBuffer
* @param position
* @returns {string}
* @private
*/
_buffer2Base64: function(/* ArrayBuffer */arrayBuffer,/* int */ position,callback){
var view = new DataView(arrayBuffer,position);
var chunk = view.getInt32(0,true);
var buffer = view.buffer.slice(position + 4,position + 4 + chunk);
var string = this._base64ArrayBuffer(buffer);
callback(string);
},
/**
* Converts an integer to hex
* @param value
* @returns {string}
* @private
*/
_int2HexString: function(/* int */ value){
var text = value.toString(16).toUpperCase();
if (text.length === 1)
{
return "000" + text;
}
if (text.length === 2)
{
return "00" + text;
}
if (text.length === 3)
{
return "0" + text;
}
return text.substr(0, text.length);
},
/**
* Determines where to start reading a BUNDLEX binary file
* @param level
* @param row
* @param col
* @param startRow
* @param startCol
* @returns {number}
* @private
*/
_getOffset: function(/* int */level, /* number */row,/* number */col, /* number */startRow, /* number */ startCol){
var recordNumber = 128 * (col - startCol) + (row - startRow);
return 16 + recordNumber * 5;
},
/**
* Returns a hexadecimal representation of a cache file path
* @param layerDir
* @param level
* @param row
* @param col
* @returns {string}
* @private
*/
_getCacheFilePath: function(/* String */ layerDir, /* int */level, /* int */row, /* int */ col){
var arr = [];
arr.push(layerDir);
arr.push("/");
arr.push("L");
arr.push(level < 10 ? "0" + level : level);
arr.push("/");
arr.push("R");
arr.push(this._int2HexString(row));
arr.push("C");
arr.push(this._int2HexString(col));
return arr.join("");
},
/**
* Returns database size in MBs.
* @returns {string}
* @private
*/
_bytes2MBs: function(bytes){
return (bytes >>> 20 ) + '.' + ( bytes & (2*0x3FF ) ); // jshint ignore:line
}
});
}
);