mirror of
https://github.com/Esri/offline-editor-js.git
synced 2025-12-15 15:20:05 +00:00
721 lines
36 KiB
JavaScript
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
|
|
}
|
|
});
|
|
}
|
|
); |