I'm implementing a Cordova application (3.2) where I want to use LeafletJS and a map tile provider together with a local filesystem cache of the tiles.
My approach in an overview is the following:
Extend the Leaflet TileLayer
Overwrite the _loadTile method to retrieve the tile either from local filesystem or from remote
My code:
var StorageTileLayer = L.TileLayer.extend({
log: function (text) {
if (this.options.log)
this.options.log(text);
else
console.log("[StorageTileLayer]: " + text);
},
_setUpTile: function (tile, key, value, cache) {
try {
tile._layer = this;
tile.onload = this._tileOnLoad;
tile.onerror = this._tileOnError;
this._adjustTilePoint(tile);
tile.src = value;
this.fire('tileloadstart', {
tile: tile,
url: tile.src
});
this.log("Setting url to " + tile.src);
}
catch (e) {
this.log("ERROR in setUpTile: " + e.message);
}
},
_loadTile: function (tile, tilePoint) {
this._adjustTilePoint(tilePoint);
var key = tilePoint.z + ',' + tilePoint.y + ',' + tilePoint.x;
var self = this;
var tileUrl = self.getTileUrl(tilePoint);
console.log(tileUrl);
console.log(typeof tileUrl);
if (this.options.storage) {
this.log("Load Tile with storage");
this.options.storage.get(key, tileUrl).then(function (value) {
self.log("Tile URL to load: " + value.url);
self._setUpTile(tile, key, value.url, true);
});
} else {
this.log("Load Tile without storage");
self._setUpTile(tile, key, tileUrl, false);
}
}
});
options.storage is a storage which has the method get(key, remoteUrl) and returns either the cached tile from local filestorage (this implementation actual works fine, so here is not the problem) or the remote url but downloads the tile in the background, so that it will be available from local file storage on the next call.
Unfortunately I can see on my device when I use Charles (Web Debugging Proxy) that although the local map tiles are loaded (I can see it from the logs) that there are still a couple of requests to the map tiles provider.
Does anyone have an idea what I am doing wrong and what else I have to overwrite in my StorageTileLayer to prevent the calls to the remote? The real problem is, that the map should work in offline mode as well, but it is not.
Thanks for your help.
Libraries in the environment:
Leaflet (0.7.3)
angularJS (1.2.16)
Cordova (3.2)
I basically fixed it with this code (angular js):
(function (window, L) {
var isDebug = false;
var StorageTileLayer = L.TileLayer.extend({
log: function (text) {
if (!isDebug)
return;
if (this.options.log)
this.options.log(text);
else
console.log("[StorageTileLayer]: " + text);
},
_setUpTile: function (tile, key, value, cache) {
try {
tile._layer = this;
tile.onload = this._tileOnLoad;
tile.onerror = this._tileOnError;
this._adjustTilePoint(tile);
tile.src = value;
this.fire('tileloadstart', {
tile: tile,
url: tile.src
});
}
catch (e) {
this.log("ERROR in setUpTile: " + e.message);
}
},
_loadTile: function (tile, tilePoint) {
this._adjustTilePoint(tilePoint);
var key = tilePoint.z + ',' + tilePoint.y + ',' + tilePoint.x;
var self = this;
var tileUrl = self.getTileUrl(tilePoint);
if (isNaN(tilePoint.x) || isNaN(tilePoint.y)) {
this.log("TilePoint x or y is nan: " + tilePoint.x + "-" + tilePoint.y);
return;
}
if (this.options.storage) {
this.options.storage.get(key, tileUrl).then(function (value) {
self.log("Tile URL to load: " + value.url);
self._setUpTile(tile, key, value.url, true);
});
} else {
this.log("Load Tile without storage");
self._setUpTile(tile, key, tileUrl, false);
}
}
});
window.StorageTileLayer = StorageTileLayer;
})(window, L);
Adding the tile layer to the leaflet map is the important part! you have to prevent the load balancer from getting different urls for each tile. I did it by setting the url of the tole layer to a fixed value:
var url = 'https://a.tiles.mapbox.com/v3/<<YOUR ACCESS CODE>>/{z}/{x}/{y}.png';
var layer = new StorageTileLayer(url, {
storage: TileStorage
});
Of course you still have to implement the TileStorage in my case it has a single method get(key, url) and returns a $q-defer which is resolved with either the local available file. If the file is not available in the local storage it will be downloaded and then the promise is resolved.
Unfortunately this TileStorage is not public available because its an in-house development of my company so I can't share it.
Nevertheless I hope this helps you.
Related
I came across several questions on this subject. I'm trying to select the rear camera on an Android device running Chrome.
So, after some reading :
var selector = document.getElementById('video-source-selector');
navigator.mediaDevices.enumerateDevices()
.then(function(devices) {
var videoDevices = devices.map(function (item) {
if(item.kind === 'videoinput'){
return item;
}
}).filter(function( element ) {
return element !== undefined;
});
var max = videoDevices.length;
videoDevices.forEach(function(device, i) {
var html = '';
var div = document.createElement('div');
if(i === max-1){ // last element reached
html += '<option value="'+device.deviceId+'" selected>'+ device.label +'</option>';
}
else {
html += '<option value="'+device.deviceId+'">'+ device.label +'</option>';
}
div.innerHTML = html;
selector.appendChild(div.childNodes[0]);
console.log(device.kind + ": " + device.label +
" id = " + device.deviceId);
});
})
.catch(function(err) {
console.log(err.name + ": " + err.message);
});
selector.addEventListener("change", function(){
console.log(selector.value); // Works as supposed : returns the ID of the selected device
});
Then, as I'm using Three.js in this app, I'm binding this ID to Jerome Etienne three extension WebcamGrabbing (https://github.com/jeromeetienne/threex.webar):
var videoGrabbing = new THREEx.WebcamGrabbing(selector.value);
Then I had to modify THREEx.WebcamGrabbing class this way (I removed the irrelevant parts):
THREEx.WebcamGrabbing = function(sourceDeviceId){
...
console.log('webcamgrabbing : ', sourceDeviceId); // returns the expected ID
var constraints = {
video: {
optional: [{
sourceId: sourceDeviceId
}]
}
}
// try to get user media
navigator.getUserMedia( constraints, function(stream){
domElement.src = URL.createObjectURL(stream);
}, function(error) {
console.error("Cant getUserMedia()! due to ", error);
});
...
}
But still, Chrome on Android is still giving me the stream of the face camera, whatever device I select...
What do I miss?
EDIT : Based on this topic (GetUserMedia - facingmode), I came up with some logs to see what's happening here :
var constraints = {
audio: false,
video: { facingMode: { exact: "environment" } }
}
console.log('Try to get stream with constraints:', constraints);
navigator.getUserMedia( constraints, function(stream){
var videoTracks = stream.getVideoTracks();
console.log('Got stream with constraints:', constraints); // Ok
console.log('Using video device: ' + videoTracks[0].label); // > Using video device: camera 0, facing back
for(var i = 0; i < videoTracks.length; i++){
console.log('Found video device with contraints : ', videoTracks[i].label); // Found video device with contraints : camera 0, facing back
}
domElement.src = URL.createObjectURL(stream);
}, function(error) {
console.error("Cant getUserMedia()! due to ", error);
});
An alternative way to select the back camera on chrome is to use the enumerateDevices method.
First get all the video input id's:
navigator.mediaDevices.enumerateDevices().then(function(devices) {
devices.forEach(function(device) {
if(device.kind=="videoinput"){
//If device is a video input add to array.
}
});
Then the first element of the array will contain the id of the front camera, the second element will contain the id of the back camera.
Finally put the id of the camera that you want to use
navigator.getUserMedia({audio: false, video: { sourceId: VideoId } }, successCallback, errorCallback);
i downloaded the SDK of Wikitude 3.1, installed the samples into my Eclipse workspace and on my Gnexus everything works. I divide the questions in points so could be easier to answer:
I added the "onMarkerSelectedFn" method into the "limitingVisiblePois.js" file because i wanted to have my POIs clickable and when clicked, appear the information page like the 5.1 example. I added the method but it doesn't work and i haven't understand where i'm making mistakes. Each other file is the same for the 5.x samples.
Code of "limitingVisiblePois.js" edited by me
var World = {
markerDrawable_idle: new AR.ImageResource("assets/marker_idle.png"),
markerDrawable_selected: new AR.ImageResource("assets/marker_selected.png"),
markerDrawable_directionIndicator: new AR.ImageResource("assets/indi.png"),
markerList: [],
// called to inject new POI data
loadPoisFromJsonData: function loadPoisFromJsonDataFn(poiData) {
PoiRadar.show();
document.getElementById("statusElement").innerHTML = 'Loading JSON objects';
var poiImage = new AR.ImageResource("img/marker.png", {
onError: World.errorLoadingImage
});
// TODO: call single POI-creation statement instead
for (var i = 0; i < poiData.length; i++) {
var singlePoi = {
//EDIT BRUS: adding the ID of each POIs
"id": poiData[i].id,
"latitude": parseFloat(poiData[i].latitude),
"longitude": parseFloat(poiData[i].longitude),
"altitude": parseFloat(poiData[i].altitude),
"title": poiData[i].name,
"description": poiData[i].description
};
World.markerList.push(new Marker(singlePoi));
}
document.getElementById("statusElement").innerHTML = 'JSON objects loaded properly';
},
// user's latest known location, accessible via userLocation.latitude, userLocation.longitude, userLocation.altitude
userLocation: null,
// location updates
locationChanged: function locationChangedFn(lat, lon, alt, acc) {
World.userLocation = {
'latitude': lat,
'longitude': lon,
'altitude': alt,
'accuracy': acc
};
},
//EDIT BRUS: Adding onMarkerSelected function
onMarkerSelected: function onMarkerSelectedFn(marker) {
// notify native environment
document.location = "architectsdk://markerselected?id=" + marker.poiData.id;
},
// called from slider.js every time the slider value changes
onSliderChanged: function onSliderChangedFn(value) {
if (value > 0) {
var valueMeters = value * 1000;
PoiRadar.setMaxDistance(valueMeters);
AR.context.scene.cullingDistance = valueMeters;
}
}
};
// forward locationChanges to custom function
AR.context.onLocationChanged = World.locationChanged;
2) I wasn't able to understand where the POIs latlong coordinates where declareated. In the same code posted ahead, there is the function
loadPoisFromJsonData: function loadPoisFromJsonDataFn(poiData)
but i don't understand how the poiData are taken.
I used the last 3.1 SDK in Android and phonegap.
thanks in advance,
Kind regards
Brus
PhoneGap Plugin samples are yet not in sync with those from the native SDK.
Whereat the current "HelloWorld" sample in PhoneGap Plugin requests POI-Data from a webservice the sample you pointed out is from the Android SDK and passes POI-data from native Android via "architectView.callJavaScript('loadPoisFromJsonData(...)')".
Both are making use of the same method to parse POI data, PhoneGap sample uses it e.g. that way
// request POI data
requestDataFromServer: function requestDataFromServerFn(lat, lon) {
var serverUrl = ServerInformation.POIDATA_SERVER + "?" + ServerInformation.POIDATA_SERVER_ARG_LAT + "=" + lat + "&" + ServerInformation.POIDATA_SERVER_ARG_LON + "=" + lon + "&" + ServerInformation.POIDATA_SERVER_ARG_NR_POIS + "=20";
var jqxhr = $.getJSON(serverUrl, function(data) {
World.loadPoisFromJsonData(data);
})
.error(function(err) {
alert("JSON error occured! " + err.message);
})
.complete(function() {});
}
You may just add these lines in your locationChanged implementation to use places from a dummy-webserver (don't forget to define 'alreadyRequestedData= false;' )
if (!World.alreadyRequestedData) {
World.requestDataFromServer(lat, lon);
World.alreadyRequestedData = true;
}
Kind regards,
Andreas
I have the following piece of code:
function download_img(imgToDownload, imgToRemove){
var url = remote_url+imgToDownload; // image url
root_path = get_root_path();
var flag = "working";
var flag_delete = false;
var imageToDownloadPath = root_path + "/" + imgToDownload; // full file path
var imageToRemovePath = root_path + "/" + imgToRemove; // full file path
try{
var fileTransfer = new FileTransfer();
fileTransfer.download(url, imageToDownloadPath,
function () {
if(imgToRemove != "" && imgToRemove != null){
var entry = new FileEntry("foo", imageToRemovePath);
entry.remove(function (){alert("fine");flag_delete = true;}, function (){alert("marron");flag_delete = true;});
}
else{
flag_delete = true;
}
flag = "done";
},
function (error) {
flag = "done";
flag_delete = true;
}
);
}catch(error){
alert("Error capturado: "+error.message);
}
while(flag=="working" && !flag_delete){
try{
setTimeout(
function() {
/* Código */
},
300
);
}
catch(error){
alert("Error en el bucle: " + error.message);
}
}
}
I have had problems downloading the files which apparently seemed to be a sync conflict, I mean, looks like something was avorting the execution before the file/s was/were downloaded.
I have used two flags to make sure not to continue until files are downloaded (and old ones deleted if necessary). The idea is to change flag's values when the action is completed and keep the code waiting in a loop.
The results this code is giving is as follows:
It never seems to enter the success fileTransfer.download sucess method (I used an alert which never triggered) even though the first file downloads properly.
The flags are never changed so the code stays stuck in the loop, and it does not continue downloading other files.
I think it might be a very basic jQuery behaviour but I am just starting with this technologies and I am a little lost. If anyone could give me a clue on that I would really appreciate it.
Thanks!!
I have an AJAX function I am building which makes a call on scroll or click/touchtstart to append some content to my HTML. Everything works great for me in the desktop environment, both on click and on scroll events. However neither of them work on my Android 2.3.7 HTC EVO or my Nexus 7 on Android 4.1.1.
Here is my click JavaScript event handler:
$('.loadMore').each(function() {
//Set URL and start on the first page
var self = this;
var path2root = ".";
var loc = window.location.origin;
var url = loc + "/process.php";
var subcategoryId = $(this).parent().attr('data-subcategoryId');
var page = 1;
// Build the updated URL
$(self).bind('touchstart', function(e) {
e.preventDefault();
page++;
// AJAX function for processing JSON
$.ajax({
url: url + "?subcategoryId=" + subcategoryId + "&page=" + page,
type: "GET",
contentType: "application/json; charset=utf-8",
dataType: "json",
async: false,
success: function (data) {
var i = 0;
for(i = 0 ; i < data.length; i++) {
var articleId = data[i].articleResult.articleId;
var title = data[i].articleResult.title;
var subhead = data[i].articleResult.subhead;
var image = data[i].articleResult.image;
var response = "<td><div class='articleResult'><a href='" + path2root + "/article/article.php?articleId=" + articleId + "'><img src='" + image + "' width='120'/></a><br><h3><a href='" + path2root + "/article/article.php?articleId=" + articleId + "'>" + title.substring(0,25) + "</a></h3></div></td>";
$("table tr td:nth-last-child(2)").after(response);
};
}
});
});
});
My scroll function is very similar, only I bind the event to a different element, and on scroll:
// Collecting scroll info
$('.overthrow').each(function() {
// SAME VARIABLES
$(this).scroll(function () {
if ($(self).scrollLeft() >= parseInt($(document).width() - 50)) {
if (finished == 1) {
page++;
finished = 0;
// SAME AJAX FUNCTION
};
};
});
});
Keep in mind this is a mobile optimized webpage, not a native PhoneGap app, and I am using regular jQuery 1.8.0, not jQuery Mobile or PhoneGap.
I had found THIS issue over on Google Code in regards to Android 2.3 not receiving touchstart events very effectively, which led me to start building the on scroll function instead.
According to your link from Google Code the fix would be to include an e.preventDefault() at the start of your touchstart event like so:
$(self).bind('touchstart', function(e) { // include e as a reference to the event
e.preventDefault(); // prevents the default behaviour on this element
....
});
The above works on my Android 2.X devices; I hope it helps!
So I was able to get the AJAX call functioning properly for the on scroll event. I have abandoned the click/touchstart event all together as that seems to be buggy in Android.
For whatever reason, Android was not reading dynamic URL correctly when uploaded to the webserver:
var loc = window.location.origin;
var url = loc + "/process.php";
So when I was building my ajax call with the compiled URL:
$.ajax({
url: url + "?subcategoryId=" + subcategoryId + "&page=" + page,
the window.origin.location was being misinterpreted. Instead I needed to hard-code the location of my process.php file:
url: "process.php?subcategoryId=" + subcategoryId + "&page=" + page,
I discovered my solution through another SO post HERE
The only catch being my code was working on iPhone, but not on Android. If anyone knows the reason behind this, that would be great information to have.
I am crunntly working on Hybrid Android App using JQM + PhoneGap
I am doing a JSONP request with AJAX
It works well on all Chrome PC browser, also works well on my android application when WiFi is connected,
but when changing network connection to 3G the AJAX request is not responding.
I found #BruceHill related post that wrote the following :
"mobile operators do content modification before delivering it to the phone and this modification breaks jQuery"
Jquery mobile page not loading correctly on Netherlands 3G connections
Although I am not living in Holland, I tried doing what he suggests by locating all the JS files on a remote server and called it via CDN, but unfortunately it didn't help.
I will be happy to get some help on this one...
this is my AJAX request:
$.mobile.allowCrossDomainPages = true;
$('#expertsListPage').live('pageshow', function (event) {
$.mobile.showPageLoadingMsg();
getExpertsList();
});
var catId;
var catName
function getExpertsList() {
$('#expertsList li').remove();
catId = getUrlVars()["id"];
catName = getUrlVars()["cat"] ;
$('h1').text( unescape(catName) );
var url = 'http://apis.mydomain.com/mydata.svc/jsonp'
$.ajax({
cache: true,
type: 'GET',
url: url,
dataType: 'jsonp' ,
jsonp: 'callback',
success:api_do
});
}
var expertss;
function api_do(obj) {
$('#expertsList li').remove();
expertss = obj.experts;
$.each(expertss, function (index, expert) {
$('#expertsList').append('<li><a href="ExpertDetails.html?id=' + expert.id + '&catid=' + catId +'&catName=' + catName + '">' +
'<img style="width:160px;height:160px;" src="' + expert.thumbnail + '"/>' +
'<h4>' + expert.name + '</h4>' +
'<p>' + expert.description + '</p>' +
'</a></li>');
});
$('#expertsList').listview('refresh');
$.mobile.hidePageLoadingMsg();
}
function getUrlVars() {
var varsExperts = [], hash;
var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
for (var i = 0; i < hashes.length; i++) {
hash = hashes[i].split('=');
varsExperts.push(hash[0]);
varsExperts[hash[0]] = hash[1];
}
return varsExperts;
}
Try adding this code to your javascript, might help.
$( document ).on( "mobileinit", function() {
// Make your jQuery Mobile framework configuration changes here!
$.support.cors = true;
$.mobile.allowCrossDomainPages = true;
});