Ionic/Cordova Demo: Where did I take that picture?

This post is more than 2 years old.

Every now and then I think of an idea for a cool (aka useless and pointless but fun) app that I think will take me one hour and let me grow my small little empire of demos. Sometimes those "quick little demos" end up turning into multi-hour sessions as I pull my hair out trying to find out why this or that isn't working. That's frustrating as heck while I'm working on it, but in the end it makes me as happy.

smile-kitten-large

Why? Because if I run into problems with my little "toy" demo, most likely you, the poor reader who has to put up with my silly demos, will run into it in a production app. And if my pain helps you avoid issues, then this blog will earn its keep. Ok, so what was the idea?

A few weeks ago I was shopping with my wife. It was the type of store where pretty much nothing in it interests me so I was just kind of mindlessly following along. But when my wife pointed out something she liked, I discretely snapped a picture of the item so I'd remember it as a possible present for her birthday or Christmas. Unfortunately, I couldn't remember the name of the store. I knew roundabout where it was, of course, but not the actual store.

Turns out that many pictures automatically include data that relates to the location where the picture was taken. You can - with a few clicks - get the latitude and longitude of the picture. That's nice - but frankly, I can't translate those values into a 'real' location off the top of my head. I'm sure web apps exist to help with that, but I thought, wouldn't it be nice if I could just select a picture and have it tell me where it was taken - in English? For example:

shot1

For my demo, I decided to build the following:

  • Let the user select a picture.
  • Attempt to read the EXIF data and get a location.
  • Try to Foursquare the location. I figured that would work great for businesses.
  • If that fails, try to reverse geocode it to an address at least.
  • If that fails too, show it on a map at least.

Right away I ran into some interesting issues. First, I needed to read the EXIF data. I found a Cordova plugin for it, but it had not been updated in two years, and I saw multiple issues reported that were not being addressed. So then I simply Googled for "exif javascript" and came across this project: exif-js. This project was also old with outstanding PRs, but I thought it might be safer to try.

For the most part, it just works. Here is a snippet showing it in action:

	
$scope.selectPicture = function() {
	navigator.camera.getPicture(gotPic, errHandler, {
		sourceType:Camera.PictureSourceType.PHOTOLIBRARY,
		destinationType:Camera.DestinationType.NATIVE_URI
	});
};
	
var errHandler = function(e) {
	alert('Error with Camera: '+e);	
};
	
//utility funct based on https://en.wikipedia.org/wiki/Geographic_coordinate_conversion
var convertDegToDec = function(arr) {
	return (arr[0].numerator + arr[1].numerator/60 + (arr[2].numerator/arr[2].denominator)/3600).toFixed(4);
};
	
var gotPic = function(u) {
	console.log('Got image '+u);
	$scope.img.url = u;
	//scope.apply can KMA
	$scope.$apply();
	
};

var img = document.querySelector("#selImage");	

img.addEventListener("load", function() {
	console.log("load event for image "+(new Date()));
	$scope.status.text = "Loading EXIF data for image.";
	EXIF.getData(document.querySelector("#selImage"), function() {
		console.log("in exif");
			
		//console.dir(EXIF.getAllTags(img));
		var long = EXIF.getTag(img,"GPSLongitude");
		var lat = EXIF.getTag(img,"GPSLatitude");
		if(!long || !lat) {
			$scope.status.text = "Unfortunately, I can't find GPS info for the picture";
			return;	
		}
		long = convertDegToDec(long);
		lat = convertDegToDec(lat);
		//handle W/S
		if(EXIF.getTag(this,"GPSLongitudeRef") === "W") long = -1 * long;
		if(EXIF.getTag(this,"GPSLatitudeRef") === "S") lat = -1 * lat;
		console.log(long,lat);
		locateAddress(long,lat);
	});			
}, false);

First thing I discovered was that when you select an image in Cordova, the EXIF data is stripped down to about 4 or so different tags. Turns out this is a known bug (CF-1285) due to the fact that the plugin copies the original image and in that process removes the data. The bug is marked resolved, but obviously it isn't. However, if you switch the camera source to NATIVE_URI then the problem goes away.

So far so good. To work with the code, you need to point it to an image in the DOM, and wait for the image to finish loading. That by itself isn't hard, although I feel dirty when I use the DOM in Angular controllers. (I got over it.) I then discovered an issue with the library. When it loads the EXIF data, it copies the values to the DOM item for caching. I'm using the same image every time you select a new photo, so this meant the tag data was cached. I filed a bug report and in the meantime I simply edited the library to remove the cache check. That's bad - but I got over that too.

The next thing I had to work with was the location stuff. As I said, the idea was to first check Foursquare, fall back to reverse geocoding, and fall back again to a static map. Let's look at the controller code first.


var locateAddress = function(long,lat) {

	$scope.status.text = "Trying to locate the photo.";

	Location.getInfo(long, lat).then(function(result) {
		console.log('Result was '+JSON.stringify(result));
		if(result.type === 'foursquare') {
			$scope.status.text = 'Your photo was taken at ' + result.name + ' located at ' + result.address;
		} else if (result.type === 'geocode') {
			$scope.status.text = 'Your photo appears to have been taken at ' + result.address;
		} else {
			var map = 'https://maps.googleapis.com/maps/api/staticmap?center='+lat+','+long+'zoom=13&size=300x300&maptype=roadmap&markers=color:blue%7Clabel:X%7C'+lat+','+long;
			$scope.status.text = 'Sorry, I\'ve got nothing. But here is a map!
'; } }); };

Not too complex, right? I just run my service and deal with the result. The service is a bit complex, but really just makes use of the various APIs I'm hitting.

angular.module('starter.services', [])

.factory('Foursquare', function($http) {

	var CLIENT_ID = 'mahsecretismahsecret';
	var CLIENT_SECRET = 'soylentgreenispeople';
	
	function whatsAt(long,lat) {
		return $http.get('https://api.foursquare.com/v2/venues/search?ll='+lat+','+long+'&intent=browse&radius=30&client_id='+CLIENT_ID+'&client_secret='+CLIENT_SECRET+'&v=20151201');		
	}

	return {
		whatsAt:whatsAt
	};
})
.factory('Geocode', function($http) {
	var KEY = 'google should let me geocode for free';
	
	function lookup(long,lat) {
		return $http.get('https://maps.googleapis.com/maps/api/geocode/json?latlng='+lat+','+long+'&key='+KEY);
	}
	
	return {
		lookup:lookup	
	};

})
.factory('Location', function($q,Foursquare,Geocode) {
	
	function getInfo(long,lat) {
		console.log('ok, in getInfo with '+long+','+lat);
		var deferred = $q.defer();
		Foursquare.whatsAt(long,lat).then(function(result) {
			//console.log('back from fq with '+JSON.stringify(result));
			if(result.status === 200 && result.data.response.venues.length >= 1) {
				var bestMatch = result.data.response.venues[0];
				//convert the result to something the caller can use consistently
				var result = {
					type:"foursquare",
					name:bestMatch.name,
					address:bestMatch.location.formattedAddress.join(", ")
				}
				console.dir(bestMatch);
				deferred.resolve(result);
			} else {
				//ok, time to try google
				Geocode.lookup(long,lat).then(function(result) {
					console.log('back from google with ');
					if(result.data && result.data.results && result.data.results.length >= 1) {
						console.log('did i come in here?');
						var bestMatch = result.data.results[0];
						console.log(JSON.stringify(bestMatch));	
						var result = {
							type:"geocode",
							address:bestMatch.formatted_address	
						}
						deferred.resolve(result);
					}
				});	
			}
		});
		
		return deferred.promise;	
	}
	return {
		getInfo:getInfo	
	};
	
});

In both cases, I'm assuming the first result from the API is the best result. That may not always be true, but it works for now. You've seen an example of Foursquare working, here is an example of the reverse geocode.

geocode

And here it is with the last fallback. Yes, this is the same picture, I just temporarily disabled the Geocode service for a quick test.

map

All in all, this was a fun little app to build, and as I said, I'm glad I ran into the EXIF issues. I know I'll need that in the future. You can find the complete source code for this demo here: https://github.com/cfjedimaster/Cordova-Examples/tree/master/photolocate

Raymond Camden's Picture

About Raymond Camden

Raymond is a developer advocate for HERE Technologies. He focuses on JavaScript, serverless and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Gary F posted on 12/4/2015 at 2:25 AM

This was a more realistic journey encountered when developing an app! Dev, dev, dev, BAM! A brick wall. But you find a workaround and continue until the next one. I like the app, a good use of EXIF data.

Comment 2 (In reply to #1) by Raymond Camden posted on 12/4/2015 at 2:31 AM

Thanks, glad you liked it!

Comment 3 by Casey McLaughlin posted on 12/10/2015 at 6:27 PM

Try using 3 words instead: http://esri.what3words.com/

Oh and don't forget that your cell location isn't the same as GPS or it might be, but you can't tell unless you verify it on a map. Cryptic geospeak. There are a bunch of apps doing this and for some real purposes like reporting civic information like "Graffiti is here" or for folks tracking infrastructure status (This fire hydrant is in good condition, see the picture with timestamp and location).

Cool stuff with grabbing the exif, but you also need to leave it enabled, depending on what your personal privacy concerns may or may not be. (http://www.howtogeek.com/20....

Remember the 3 rules of real-estate: location location location

Comment 4 by Simon Prickett posted on 12/10/2015 at 7:52 PM

Nice demo app - I've been working on a commercial project that uses location info to find things about your immediate location and present them to you - can't talk about that, but on the way we found factual.com and use their API for finding what place you are at, then deriving that place's Foursquare, Instagram, Facebook, Yelp etc pages / API IDs from Factual's API, so we can then present a lot of information about the place. If you're looking for places too they have a great full text search. The $1 to get a key API tier is good for demos. I made a small demo here https://github.com/simonpri... and there's lots of docs at http://developer.factual.com/ - I'm not connected with them, just found their service fun to use and it solved a data quality problem we had when relying on things like Foursquare or Facebook places.

Comment 5 by Ed posted on 11/17/2016 at 1:26 PM

This is a great tutorial Raymond.

Any ideas how you could insert your own custom text into an image? For example: "My cat, Felix, his first Xmas" tagged in the image as "Event". Perhaps via an input box on the same page as your taken pic?

So when you download the image, come back to it in a year and view in Windows Explorer, in the properties the "Event" is there to view.

Comment 6 (In reply to #5) by Raymond Camden posted on 11/17/2016 at 2:11 PM

It's possible - see this article for a bit of help: http://developer.telerik.co...

Comment 7 by dinorb posted on 2/6/2017 at 1:46 PM

Does it work with Ionic2 / Angular 2 ?

Comment 8 (In reply to #7) by Raymond Camden posted on 2/6/2017 at 3:32 PM

This app was written for Ionic 1, not 2. Obviously you could rebuild it in 2 using the methods I used, but you couldn't just copy and paste code/files.

Comment 9 by Isha Nur Fajar posted on 5/17/2017 at 7:06 PM

sir this subject is where can you explain when did i get photo?
all about time

Comment 10 (In reply to #9) by Raymond Camden posted on 5/17/2017 at 7:14 PM

Oh - the date the picture was taken should definitely be in the EXIF data. Just check the docs. :)