Closed ryaa closed 7 years ago
Below is the method to download and save tiles for offline mode
_downloadOfflineMap(ticketDetail: TicketDetail) {
return new Promise<TicketDetail>((resolve: Function, reject: Function) => {
if (this.isOnline && ticketDetail && ticketDetail.work_area) {
let ZOOM_MIN = 16;
let ZOOM_MAX = 20;
let FOLDER_NAME = 'OfflineTileLayer';
let streetsLayer = null;
let satelliteLayer = null;
let promiseForStreetsLayerInit = new Promise((resolve: Function, reject: Function) => {
try {
streetsLayer = L.tileLayerCordova('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', {
attribution: '© <a href="https://www.mapbox.com/map-feedback/">Mapbox</a> | © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
minZoon: ZOOM_MIN,
maxZoom: ZOOM_MAX,
id: 'mapbox.streets',
accessToken: MAPBOX_API_KEY,
// these are specific to L.TileLayer.Cordova and mostly specify where to store the tiles on disk
folder: FOLDER_NAME,
name: 'streetsLayer',
debug: true
}, () => {
resolve();
});
} catch (error) {
this.logger.error('TicketsList: error occurred while settings up downloading map tiles for offline support: ' + JSON.stringify(error));
let appError = new AppError(false, 'Downloading map tiles for offline support failed.');
reject(appError);
}
});
let promiseForSatelliteLayerInit = new Promise((resolve: Function, reject: Function) => {
try {
satelliteLayer = L.tileLayerCordova('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', {
attribution: '© <a href="https://www.mapbox.com/map-feedback/">Mapbox</a> | © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
minZoon: ZOOM_MIN,
maxZoom: ZOOM_MAX,
id: 'mapbox.streets-satellite',
accessToken: MAPBOX_API_KEY,
// these are specific to L.TileLayer.Cordova and mostly specify where to store the tiles on disk
folder: FOLDER_NAME,
name: 'satelliteLayer',
debug: true
}, () => {
resolve();
});
} catch (error) {
this.logger.error('TicketsList: error occurred while settings up downloading map tiles for offline support: ' + JSON.stringify(error));
let appError = new AppError(false, 'Downloading map tiles for offline support failed.');
reject(appError);
}
});
Promise.all([promiseForStreetsLayerInit, promiseForSatelliteLayerInit])
.then(() => {
let geoJSONLayer = L.geoJSON(ticketDetail.work_area);
let _bounds = geoJSONLayer.getBounds();
// calculate tiles to cache based on the bounds and going down to a stated range of zoom levels
let tile_list = streetsLayer.calculateXYZListFromBounds(_bounds, ZOOM_MIN, ZOOM_MAX);
//let center = _bounds.getCenter();
//let tile_list = streetsLayer.calculateXYZListFromPyramid(center.lat, center.lng, ZOOM_MIN, ZOOM_MAX);
let promiseForStreetsLayerDownload = new Promise((resolve: Function, reject: Function) => {
streetsLayer.downloadXYZList(
// 1st param: a list of XYZ objects indicating tiles to download
tile_list,
// 2nd param: overwrite existing tiles on disk?
// if no then a tile already on disk will be kept, which can be a big time saver
false, //true,
// 3rd param: progress callback
// receives the number of tiles downloaded and the number of tiles total
// caller can calculate a percentage, update progress bar, etc.
function (done, total) {
var percent = Math.round(100 * done / total);
console.log(done + " / " + total + " = " + percent + "%");
},
// 4th param: complete callback
// no parameters are given, but we know we're done!
function () {
// for this demo, on success we use another L.TileLayer.Cordova feature and show the disk usage!
streetsLayer.getDiskUsage(function (filecount, bytes) {
var kilobytes = Math.round(bytes / 1024);
console.log("Done " + filecount + " files " + kilobytes + " kB");
});
resolve();
},
// 5th param: error callback
// parameter is the error message string
function (error) {
console.error("Failed; Error code: " + error.code);
reject();
}
);
});
let promiseForSatelliteLayerDownload = new Promise((resolve: Function, reject: Function) => {
satelliteLayer.downloadXYZList(
// 1st param: a list of XYZ objects indicating tiles to download
tile_list,
// 2nd param: overwrite existing tiles on disk?
// if no then a tile already on disk will be kept, which can be a big time saver
false, //true,
// 3rd param: progress callback
// receives the number of tiles downloaded and the number of tiles total
// caller can calculate a percentage, update progress bar, etc.
function (done, total) {
var percent = Math.round(100 * done / total);
console.log(done + " / " + total + " = " + percent + "%");
},
// 4th param: complete callback
// no parameters are given, but we know we're done!
function () {
// for this demo, on success we use another L.TileLayer.Cordova feature and show the disk usage!
streetsLayer.getDiskUsage(function (filecount, bytes) {
var kilobytes = Math.round(bytes / 1024);
console.log("Done " + filecount + " files " + kilobytes + " kB");
});
resolve();
},
// 5th param: error callback
// parameter is the error message string
function (error) {
console.error("Failed; Error code: " + error.code);
reject(error);
}
);
});
Promise.all([promiseForStreetsLayerDownload, promiseForSatelliteLayerDownload])
.then(() => {
resolve(ticketDetail);
})
.catch((error: any) => {
this.logger.error('TicketsList: error occurred while downloading map tiles for offline support: ' + JSON.stringify(error));
let appError = new AppError(false, 'Downloading map tiles for offline support failed.');
reject(appError);
});
})
.catch((appError: AppError) => {
reject(appError);
});
} else {
resolve(ticketDetail);
}
});
}
and here is the code to load the map on a different page
loadMap() {
if (this.item && this.item.work_area) {
let ZOOM_MIN = 16;
let ZOOM_MAX = 20;
let FOLDER_NAME = 'OfflineTileLayer';
this.map = L.map("map", {
minZoom: ZOOM_MIN,
maxZoom: ZOOM_MAX,
zoomControl: false
});
let streetsLayer = null;
let satelliteLayer = null;
let promiseForStreetsLayerInit = new Promise((resolve: Function, reject: Function) => {
try {
streetsLayer = L.tileLayerCordova('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', {
attribution: '© <a href="https://www.mapbox.com/map-feedback/">Mapbox</a> | © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
minZoon: ZOOM_MIN,
maxZoom: ZOOM_MAX,
id: 'mapbox.streets',
accessToken: MAPBOX_API_KEY,
// these are specific to L.TileLayer.Cordova and mostly specify where to store the tiles on disk
folder: FOLDER_NAME,
name: 'streetsLayer',
debug: true
}, () => {
resolve();
});
} catch (error) {
this.logger.error('TicketsList: error occurred while settings up downloading map tiles for offline support: ' + JSON.stringify(error));
let appError = new AppError(false, 'Downloading map tiles for offline support failed.');
reject(appError);
}
});
let promiseForSatelliteLayerInit = new Promise((resolve: Function, reject: Function) => {
try {
satelliteLayer = L.tileLayerCordova('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}', {
attribution: '© <a href="https://www.mapbox.com/map-feedback/">Mapbox</a> | © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
minZoon: ZOOM_MIN,
maxZoom: ZOOM_MAX,
id: 'mapbox.streets-satellite',
accessToken: MAPBOX_API_KEY,
// these are specific to L.TileLayer.Cordova and mostly specify where to store the tiles on disk
folder: FOLDER_NAME,
name: 'satelliteLayer',
debug: true
}, () => {
resolve();
});
} catch (error) {
this.logger.error('TicketsList: error occurred while settings up downloading map tiles for offline support: ' + JSON.stringify(error));
let appError = new AppError(false, 'Downloading map tiles for offline support failed.');
reject(appError);
}
});
Promise.all([promiseForStreetsLayerInit, promiseForSatelliteLayerInit])
.then(() => {
if (this.isOnline) {
streetsLayer.goOnline();
satelliteLayer.goOnline();
} else {
streetsLayer.goOffline();
satelliteLayer.goOffline();
}
streetsLayer.addTo(this.map);
let layerControl = L.control.layers({}, {});
layerControl.addBaseLayer(streetsLayer, 'Streets');
layerControl.addBaseLayer(satelliteLayer, 'Satellite');
this.map.layerControl = layerControl.addTo(this.map);
let geoJSONLayer = L.geoJSON(this.item.work_area);
let _bounds = geoJSONLayer.getBounds();
if (this.item.work_area.type == 'Point') {
this.map.fitBounds(_bounds);
geoJSONLayer.addTo(this.map);
} else {
let layerStyle = { fillColor: '#2190c4', color: '#25A0DA', weight: 3 };
geoJSONLayer.setStyle(() => { return layerStyle });
this.map.fitBounds(_bounds);
geoJSONLayer.addTo(this.map);
}
if (!this.isOnline) {
this.map.dragging.disable();
this.map.touchZoom.disable();
this.map.doubleClickZoom.disable();
this.map.scrollWheelZoom.disable();
this.map.boxZoom.disable();
}
}
}
Interesting, I wonder if there's some rounding error someplace.
Can you tell me, the Lat-Lng bounds you're using here? I'll want to set up a map of the same bounding box and compare some of the tile-numbering outputs.
To get bounds I use let geoJSONLayer = L.geoJSON(this.item.work_area); (this is in my code above)
For example I can reproduce the problem for the below work_area
{
"type": "Polygon",
"coordinates": [
[
[
-96.372664221291,
33.201604114837
],
[
-96.375364221291,
33.201864114837
],
[
-96.375824221291,
33.199524114837
],
[
-96.372964221291,
33.199304114837
],
[
-96.372664221291,
33.201604114837
]
]
]
}
When i save tiles for offline mode using the above _downloadOfflineMap method and then view the map in offline mode I get the below (note missing files)
The online mode shows fine since the same files get downloaded from the server
If this is easier, I can provide cordova android application built in debug to see the problem
Thank you very much for you feedback
Sitting down with one of my own applications here, I am not getting the same problem when I download from that same area. This other one (a MobileMapStarter) downloads the tiles and does not show any Not Found errors.
You said that you could provide the functioning app with debugging enabled? That would be helpful, if you are able to provide it.
Hi, thank you for prompt feedback. The cordova app for Android showing the problem with debug enabled can be found at https://www.dropbox.com/sh/fxek905zxjlo6qy/AAARgEo4KcPUUEvBdkX8U8gsa?dl=0 There are several installers for different platforms.
Steps to reproduce the problem: 1) install the app 2) login as (you may need to skip first time launch tutorial) domain: first username: mobiledev@example.com password: 123123123 3) when on the main page (tickets list) click search and enter 1654702483 to find the ticket with this number 4) for this ticket card, please click on the right bottom cloud icon to add this ticket to offline mode This will save the ticket and all the related details into the offline storage. This includes saving the map for this ticket (more on this later) 5) when save is successfully complete (no errors shows and the cloud icon turns blue) click on the ticket to open it 6) in the ticket details click on the last/third tab to see the map with a polygon displayed (this is the ticket work area as I described earlier) Note that it displays fine in online mode 7) return to ticket list 8) disable the network (I usually turn off wifi on my device) 9) open the same ticket 1654702483 details 10) in the ticket details click on the last/third tab to see the map with a polygon displayed Note that the map shown has some tiles missing
Now, if you connect your device to the dev machine, open Chrome, enter chrome://inspect and start debugging session you will see these not found error for the missing tiles
Here are some tech details (see attached screenshot as well):
1) map tiles are saved for offline mode in
Note that this problem: a) reproducible for many tickets. The one above is just one of the samples b) i tried to change min and max zoom and the problem is gone only when min is very small and max is 20. However this triggers a very large download For min/max zoom combinations 17-19, 16-20 etc the problem is reproducible
Please let me know if you have any questions. If it is easier for you I'm available to support in skype alex_ryaa
Thank you very much
I'm finally getting a chance to sit down with your app, to see what I can figure out. Also, I tried contacting you via Skype, but have not received a reply.
I'm guessing it's a rounding error only significant at very-close ranges and very-small areas. Some of the JSON shapes are smaller than a single, which was not a condition I had tested previously. I'll let you know what I find.
I'm not sure that there really is an error here.
The shape above, does not extend into the tile above (17-30446-52707) so it's correct that this tile was not downloaded. The latitude range seems to correspond to 52708 and 52709, but not 52707.
function long2tile(lon,zoom) { return (Math.floor((lon+180)/360*Math.pow(2,zoom))); }
function lat2tile(lat,zoom) { return (Math.floor((1-Math.log(Math.tan(lat*Math.PI/180) +
1/Math.cos(lat*Math.PI/180))/Math.PI)/2 *Math.pow(2,zoom))); }
lat2tile(33.199304, 17) 52709
lat2tile(33.2018641, 17) 52708
As such, I don't think this is a bug at all. L.TileLayer.Cordova does not attempt to download a buffered area around your extent, but when you pan the map Leaflet would still try to load 52707 and fail since it's not downloaded.
I further confirmed this in the inspector (GapDebug) and the screenshot shows the tiles that are currently in DOM. Specifically of note, is that the 52707 tiles are indeed further north than the extent of your shape.
My advice for an immediate workaround to get a surrounding area, is that you pad()
the _bounds
to include a bit of surrounding area.
// see leaflet LatLngBounds pad() method
var _bounds = geoJSONLayer.getBounds().pad(0.25);
If I am misunderstanding the problem, please let me know. But given that L.TileLayer.Cordova really does restrict to only the bounds given, it does seem proper that 52707 would be excluded here and that northern tile should be absent from the offline cache.
Greg,
Thank you very much for your help. I was under assumption that if i download tiles for specific bounds for offline these will cover the entire area when the map shows in offline mode. Anyway, i added the suggested workaround and it seems to be working. I will need to test more to see if 0.25 enough or it needs to be increased to cover all scenarios.
There is one more issue that I found and I'm not sure if this is bug in this library or leaflet itself. When viewing the map saved for offline mode and changing orientation the map shows with tiles missing and it seems that it does not even try to fetch these tiles - see attached This limitation exists for both online and offline mode on iOS and for offline mode on Android so this could not be this library bug. Please let me know your thoughts and i can create another ticket if necessary
"changing orientation the map shows with tiles missing and it seems that it does not even try to fetch these tiles - see attached"
You need to detect resize
event on the map DIV, and trigger the L.Map's invalidateSize()
function. That's not related to Cordova nor Android specifically, as much "just a Leaflet thing"
This is exactly what i did however it did not fix the problem. Anyway, this is a different problem, not related to the library. Thank you very much!
In my solution i need to store tiles for offline map support. I get bounds, invoke calculateXYZListFromBounds and download tiles using downloadXYZList. Everything is fine. However when I open the map in offline mode later, and leaflet tries to load tiles (from offline storage), some of the tiles are found and displayed on and some are not found For example, I see a lot of errors are below (/data/user/0/com.mobile.XXXXXXXXX/files/files/OfflineTileLayer//streetsLayer-17-30447-52707.png:1 GET file:///data/user/0/com.mobile.XXXXXX/files/files/OfflineTileLayer//streetsLayer-17-30447-52707.png net::ERR_FILE_NOT_FOUND).
I checked and the problem is that the urls generated by leaflet sometimes has different Y's For example for the above error, the first URL created by the lib is as below (there is not URL with ...52707.png
Note the difference in y - the leaflet asks for 52707 and calculateXYZListFromBounds/downloadXYZList used 52708
I use leaflet 1.0.2
Btw, if i set to calculate/download tiles for wide zoom range (for example 1-20) everything seems to be working fine but it takes too much tiles to download
Thanks you very much