Created
June 26, 2014 15:28
-
-
Save RhinoLance/e8b06ba45ea10a58ea16 to your computer and use it in GitHub Desktop.
Leaflet offline tile caching
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*global L, LocalFileSystem, RhinoLib, async, FileTransfer, Connection */ | |
var cacheMode = { | |
PRE_CACHE: 0x1, | |
CACHE_IN_BOUNDS: 0x2, | |
CACHE_OUTSIDE_BOUNDS: 0x4 | |
}; | |
L.TileLayer.CachedTiles = L.TileLayer.extend({ | |
_fileStorePath: 'MapTiles', | |
initialize: function (url, options) { | |
options = L.setOptions(this, options); | |
options.cacheThreads = options.cacheThreads || 10; | |
options.cacheBounds = options.cacheBounds || []; | |
options.cacheActions = options.cacheActions || 255; //allow everything | |
options.name = options.name || "defaultMapCache"; | |
// detecting retina displays, adjusting tileSize and zoom levels | |
if (options.detectRetina && L.Browser.retina && options.maxZoom > 0) { | |
options.tileSize = Math.floor(options.tileSize / 2); | |
options.zoomOffset++; | |
if (options.minZoom > 0) { | |
options.minZoom--; | |
} | |
this.options.maxZoom--; | |
} | |
this._url = url; | |
var subdomains = this.options.subdomains; | |
if (typeof subdomains === 'string') { | |
this.options.subdomains = subdomains.split(''); | |
} | |
this._sFileExt = url.substr(url.lastIndexOf('.') + 1); | |
this.tileStore = null; | |
this._tileStoreDefer = null; | |
this.cacheBounds = this.getCacheBounds(options.cacheBounds); | |
}, | |
getFileStore: function () { | |
var that = this; | |
if (that._tileStoreDefer != null) { | |
return this._tileStoreDefer.promise; | |
} | |
that._tileStoreDefer = Q.defer(); | |
var path = that._fileStorePath + '/' + that.options.name.split(' ').join('_'); | |
RhinoLib.File.requestFileSystem(RhinoLib.File.LocalFileSystem.PERSISTENT) | |
.then(function (fs) { | |
that.tileStore = fs.root; | |
return that._buildFolderStructure(path); | |
}) | |
.then(function (dirEntry) { | |
that.tileStore = dirEntry; | |
that._tileStoreDefer.resolve(that.tileStore) | |
}); | |
return that._tileStoreDefer.promise; | |
}, | |
xx_setFileStore: function (fCallback) { | |
var that = this; | |
if (typeof cordova !== 'undefined') { //only do the caching if it's a cordova platform | |
console.log('Setting FileStore'); | |
window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, | |
function (oFs) { | |
that.tileStore = oFs.root; | |
console.log('root: ' + oFs.root) | |
var path = that._fileStorePath + '/' + that.options.name.split(' ').join('_'); | |
that._buildFolderStructure( path, | |
function () { | |
oFs.root.getDirectory(path, { create: true, exclusive: false }, | |
function (oDir) { | |
that.tileStore = oDir; | |
if( fCallback){fCallback();} | |
}, | |
function (oError) { | |
console.error("Unable to get root directory: " + JSON.stringify(oError)); | |
} | |
); | |
} | |
); | |
}, | |
function (oError) { | |
console.error("Unable to get filesystem: " + JSON.stringify(oError)); | |
} | |
); | |
} | |
}, | |
getTileUrl: function (tilePoint, zLayer) { | |
return L.Util.template(this._url, L.extend({ | |
s: this._getSubdomain(tilePoint), | |
z: zLayer || this._getZoomForUrl(), | |
x: tilePoint.x, | |
y: tilePoint.y | |
}, this.options)); | |
}, | |
_loadTile: function (tile, tilePoint) { | |
//try{ | |
var that = this; | |
tile._layer = this; | |
tile.onload = this._tileOnLoad; | |
tile.onerror = this._tileOnError; | |
tilePoint.z = this._getZoomForUrl(); | |
if (!this.tileStore) { | |
//console.log("Getting from web"); | |
tile.src = this.getTileUrl(tilePoint); | |
return; | |
} | |
var sFileName = this._getFilePath({ x: tilePoint.x, y: tilePoint.y, z: tilePoint.z }); | |
//console.log("fileName " + sFileName); | |
this.tileStore.getFile( | |
sFileName, | |
null, | |
function (oFile) { | |
//console.log("Tile loaded from cache "); | |
tile.src = oFile.toURL(); | |
}, | |
function (oError) { | |
//console.log("Cached tile not found, fetching it ..."); | |
try{ | |
if (!that.canCache(tilePoint)) { | |
//console.log('cancache: No'); | |
tile.src = that.getTileUrl(tilePoint); | |
return; | |
} | |
} catch (ex) { console.error("err qwe : " + ex + " \n" + JSON.stringify(ex)); } | |
//console.log('no local tile'); | |
that._downloadTile( | |
{ x: tilePoint.x, y: tilePoint.y, z: tilePoint.z }, | |
function (oResult) { | |
//console.log('download done.'); | |
if (oResult.status == "success") { | |
tile.src = oResult.data.toURL(); | |
//console.log("loaded from new cache" ); | |
} | |
} | |
); | |
} | |
); | |
//} catch (ex) { console.log("err abc: " + ex + " \n" + JSON.stringify(ex)); } | |
}, | |
canCache: function( tilePoint ) { | |
//Check if we're allowed to cache | |
var canCache = false; | |
if (this.options.cacheActions & cacheMode.CACHE_OUTSIDE_BOUNDS) { | |
canCache = true; | |
} | |
else { | |
if (this.options.cacheActions & cacheMode.CACHE_IN_BOUNDS) { | |
if (this.cacheBounds.contains(tilePoint)) { | |
canCache = true; | |
} | |
} | |
} | |
return canCache; | |
}, | |
getCacheBounds: function (cacheSettings) { | |
var cache; | |
var that = this; | |
cache = { | |
bounds: [], | |
contains: function (tilePoint) { | |
for (var cI = 0; cI < that.cacheBounds.bounds.length; cI++) { | |
//Check if it's allowed for the current zoom | |
if (tilePoint.z !== that.cacheBounds.bounds[cI].zoom) { | |
//console.log("outside cache bounds zoom"); | |
return false; | |
} | |
//Check if it's in he onvelope | |
if (tilePoint.x > that.cacheBounds.bounds[cI].topLeft && | |
tilePoint.y > that.cacheBounds.bounds[cI].topLeft && | |
tilePoint.x < that.cacheBounds.bounds[cI].bottomRight && | |
tilePoint.y < that.cacheBounds.bounds[cI].bottomRight) { | |
//console.log("in cache bounds envelope"); | |
return true; | |
} | |
else { | |
//console.log("not in cache bounds envelope"); | |
return false; | |
} | |
} | |
} | |
}; | |
for (var cK = 0; cK < cacheSettings.length; cK++) { //main settings | |
var bounds = cacheSettings[cK].bounds; | |
for (var cI = 0; cI < cacheSettings[cK].zooms.length; cI++) { | |
var iZoom = cacheSettings[cK].zooms[cI]; | |
var topLeft = RhinoLib.GIS.getTilePoint(bounds[0][0], bounds[0][1], iZoom); | |
var bottomRight = RhinoLib.GIS.getTilePoint(bounds[1][0], bounds[1][1], iZoom); | |
//Check that coords have been supplied correctly and skip if not. | |
if (topLeft.y > bottomRight.y || topLeft.x > bottomRight.x) { | |
continue; | |
} | |
cache.bounds.push( | |
{ | |
topLeft: topLeft, | |
bottomRight: bottomRight, | |
zoom: iZoom | |
} | |
); | |
} | |
} | |
return cache; | |
}, | |
getCacheBoundsLayer: function() { | |
if (!this.boundsLayer) { | |
this.boundsLayer = new L.FeatureGroup(); | |
for (var cK = 0; cK < this.options.cacheBounds.length; cK++) { //main settings | |
var zone = L.rectangle(this.options.cacheBounds[cK].bounds, { color: "#ff7800", weight: 1 }); | |
zone.bindPopup("Cache at zooms: " + JSON.stringify(this.options.cacheBounds[cK].zooms)); | |
zone.addTo(this.boundsLayer); | |
} | |
} | |
return this.boundsLayer; | |
}, | |
startPreCache: function( fCallback ) { | |
var that = this; | |
that.getFileStore() | |
.then(function (fs) { | |
that._startPreCache(fCallback); | |
}); | |
}, | |
_startPreCache: function (fCallback) { | |
var that = this; | |
console.log('starting preCache'); | |
fCallback(0); | |
if (typeof cordova === 'undefined') { //only do the caching if it's a cordova platform | |
fCallback(100); | |
return; | |
} | |
if (navigator.connection.type === Connection.NONE) { | |
//No point trying to download if there is no connection. | |
fCallback(100); | |
return; | |
} | |
if (!(this.options.cacheActions & cacheMode.PRE_CACHE)) { | |
//console.log("No permission to pre-cache"); | |
//No permission to pre-cache | |
fCallback(100); | |
return; | |
} | |
var aTiles = []; | |
var folderHash = []; | |
for (var cK = 0; cK < this.cacheBounds.bounds.length; cK++) { | |
var bounds = this.cacheBounds.bounds[cK]; | |
//Create an array with all tiles to cache | |
for (var cJ = bounds.topLeft.x; cJ <= bounds.bottomRight.x; cJ++) { | |
//While we're here, create the folder, so that we can download in parallel later. | |
var folder = this._getFolderPath({ x: cJ, z: bounds.zoom }); | |
if (folderHash.indexOf(folder) === -1) { //We only want to create each folder once. | |
folderHash.push(folder); | |
this._buildFolderStructure( | |
folder | |
); | |
} | |
for (var cI = bounds.topLeft.y; cI <= bounds.bottomRight.y; cI++) { | |
aTiles.push({ x: cJ, y: cI, z: bounds.zoom }); | |
} | |
} | |
} | |
var that = this; | |
var iCount = -1; | |
async.eachLimit( //initiate caching | |
aTiles, | |
that.options.cacheThreads, | |
function (item, fItemCb) { | |
//Let's update our progress | |
if (iCount++ % 10 === 0) { | |
fCallback(Math.floor(iCount * (100 / aTiles.length))); | |
} | |
//Check if we already have the tile. | |
that.tileStore.getFile( | |
that._getFilePath(item), | |
null, | |
function (oFile) { | |
//it exists | |
fItemCb(); | |
}, | |
function (oError) { | |
//console.log('no local tile'); | |
that._downloadTile( | |
item, | |
function (oResult) { | |
if (oResult.status == "fail") { | |
//console.log( "download error: " + JSON.stringify( oResult )); | |
if (oResult.data.code == 3) { //file not found, keep going | |
fItemCb(); | |
} | |
else { | |
fItemCb(oResult.data); | |
} | |
} | |
else { | |
fItemCb(); | |
} | |
} | |
); | |
}); | |
}, | |
function(oErr){ | |
if( oErr ) { | |
//console.log("Cache completed. " + aTiles.length + " tiles cached. Errors: " + JSON.stringify(oErr)); | |
} | |
fCallback( 100 ); | |
} | |
); | |
}, | |
_getFilePath: function(oTile) { | |
return this.tileStore.fullPath + "/" + this._getFolderPath(oTile) + "/" + oTile.y + "." + this._sFileExt; | |
}, | |
_getFolderPath: function(oTile) { | |
return oTile.z + "/" + oTile.x; | |
}, | |
_downloadTile: function( oTile, fCallback ) { | |
if (navigator.connection.type === Connection.NONE) { | |
//No point trying to download if there is no connection. | |
fCallback({ status: "fail", data: {desc: "No network connection"} }); | |
return; | |
} | |
var fileTransfer; | |
try{ | |
fileTransfer = new FileTransfer(); | |
} | |
catch(oErr){ | |
fCallback({ status: "success", data: {} }); | |
return; | |
} | |
var that = this; | |
async.series([ | |
//Let's make sure the directory structure exists for this tile. | |
function(fCbAsync) { | |
that.tileStore.getDirectory( | |
that._getFolderPath(oTile), | |
{ create: true, exclusive: false }, | |
function (oDir) { | |
fCbAsync(); | |
}, | |
function (oErr) { | |
that._buildFolderStructure( that._getFolderPath(oTile)) | |
.then( fCbAsync ); | |
} | |
); | |
}, | |
//Now download it | |
function(fCbAsync) { | |
var src = that.getTileUrl(oTile, oTile.z) | |
var dest = 'cdvfile://localhost/persistent/' + that._getFilePath(oTile) | |
fileTransfer.download( src, dest, | |
function (oFile) { | |
//console.log("Cached " + oFile.name); | |
fCallback({ status: "success", data: oFile }); | |
}, | |
function (oErr) { | |
//console.log("Cache failed for " + that.getTileUrl(oTile, oTile.z) + ": " + JSON.stringify(oErr)); | |
fCallback({ status: "fail", data: oErr }); | |
} | |
); | |
} | |
]); | |
}, | |
_buildFolderStructure: function( sFolderPath ) { | |
var deferred = Q.defer(); | |
//console.log('c1'); | |
var that = this; | |
var aParts = sFolderPath.split('/'); | |
var currItem = null; | |
var currDir = that.tileStore; | |
//console.log('c2'); | |
async.eachSeries( | |
aParts, | |
function (item, fItemCb) { | |
//console.log("Creating directory: " + item); | |
currItem = item; | |
currDir.getDirectory( | |
item, | |
{create: true, exclusive: false}, | |
function (oDir) { | |
//console.log('c3'); | |
currDir = oDir; | |
fItemCb(); | |
}, | |
fItemCb | |
); | |
}, | |
function (oErr) { | |
if (oErr) { | |
console.error("Error creating folder structure: " + currItem + " Errors: " + JSON.stringify(oErr)); | |
deferred.reject(err); | |
} | |
else { | |
//console.log("Folder succesfully created: " + sFolderPath); | |
deferred.resolve(currDir); | |
} | |
} | |
); | |
return deferred.promise; | |
}, | |
deleteCache: function (fCallback) { | |
if( typeof cordova === 'undefined') { //only do the caching if it's a cordova platform | |
fCallback(); | |
return; | |
} | |
var that = this; | |
//console.log('a1'); | |
var fDone = function (oErr) { | |
//console.log('a2'); | |
that.getFileStore(); //Re-create the directory | |
//console.log('a3'); | |
fCallback(oErr); | |
}; | |
that.getFileStore() | |
.then( | |
function () { | |
that.tileStore.removeRecursively(fDone, fDone); | |
} | |
); | |
} | |
}); | |
L.Icon.TextIcon = L.Icon.extend({ | |
options: { | |
/* | |
iconUrl: (String) (required) | |
iconRetinaUrl: (String) (optional, used for retina devices if detected) | |
iconSize: (Point) (can be set through CSS) | |
iconAnchor: (Point) (centered by default, can be set in CSS with negative margins) | |
popupAnchor: (Point) (if not specified, popup opens in the anchor point) | |
shadowUrl: (Point) (no shadow by default) | |
shadowRetinaUrl: (String) (optional, used for retina devices if detected) | |
shadowSize: (Point) | |
shadowAnchor: (Point) | |
*/ | |
className: '' | |
}, | |
createIcon: function () { | |
var options = this.options; | |
options.className += " leaflet-marker-textIcon"; | |
var div = document.createElement('div'); | |
div.innerHTML = "<div data-markerIcon='" + options.dataIcon + "' class='leaflet-marker-textIcon-inner'></div>"; | |
div.style.background = "url(" + options.iconUrl + ") no-repeat no-repeat 0 0"; | |
if (options.bgPos) { | |
div.style.backgroundPosition = | |
(-options.bgPos.x) + 'px ' + (-options.bgPos.y) + 'px'; | |
} | |
div.style.height = options.iconSize[1] + "px"; | |
div.style.width = options.iconSize[0] + "px"; | |
div.style.backgroundSize = "100%"; | |
this._setIconStyles(div, 'icon'); | |
return div; | |
}, | |
createShadow: function () { | |
//return; | |
var div = document.createElement('div'), | |
options = this.options; | |
this._setIconStyles(div, 'shadow'); | |
return div; | |
} | |
}); | |
L.icon.textIcon = function (options) { | |
return new L.Icon.TextIcon(options); | |
}; | |
L.Marker.prototype.animateDragging = function () { | |
var iconMargin, shadowMargin; | |
this.on('dragstart', function () { | |
if (!iconMargin) { | |
iconMargin = parseInt(L.DomUtil.getStyle(this._icon, 'marginTop')); | |
shadowMargin = parseInt(L.DomUtil.getStyle(this._shadow, 'marginLeft')); | |
} | |
this._icon.style.marginTop = (iconMargin - 25) + 'px'; | |
this._shadow.style.marginLeft = (shadowMargin + 8) + 'px'; | |
}); | |
return this.on('dragend', function () { | |
this._icon.style.marginTop = iconMargin + 'px'; | |
this._shadow.style.marginLeft = shadowMargin + 'px'; | |
}); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment