Image GPS Extractor Android App

I just wrapped up my first little Android App using Adobe AIR.  As far as development goes, that was one of the smoothest experiences I’ve ever had.

I’m currently developing a mobile app for a project at work.  I’ve never created a mobile app before, and the project app is going to take a few weeks of solid work to complete.  I wanted to see the whole process from dev to release of a mobile app much sooner…. like, now.  So Friday I started writing classes and code that I’m going to need for the project at work, and that I could pull out of the project and use in a small tutorial project that I’m posting here.

I created the Image GPS Extractor app to allow users to extract Exif GPS data from a picture on their phone.  The app really only works if the pictures you are trying to decode were taken with GeoLocation enabled.  So, if a user has his/her GPS disabled for photos, the app is basically useless.

I used Shichiseki.jp’s Exif Library for AS3 to extract the Exif info from JPG files. The library can be found here

We’ll go ahead and jump into the main MobileApplication screen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<s:MobileApplication xmlns:fx="http://ns.adobe.com/mxml/2009" 
			xmlns:s="library://ns.adobe.com/flex/spark" 
                        firstView="views.Home"
			creationComplete="init()"
			splashScreenImage="@Embed('splashscreen.png')"
		        >
	<fx:Declarations>
		<!-- Place non-visual elements (e.g., services, value objects) here -->
	</fx:Declarations>
	<fx:Script>
		<![CDATA[
			import com.zf.common.ZFModel;
 
			private var zfModel : ZFModel = ZFModel.getInstance();
 
			private function init():void  {
				zfModel.navigator = this.navigator;
			}
		]]>
	</fx:Script>
	<s:navigationContent>
		<s:Button label="Home" click="navigator.popToFirstView()" />
	</s:navigationContent>
 
</s:MobileApplication>

Line 3: firstView is specifying which view component in the “views” folder is going to be the first view once the app starts.
Line 5: splashScreenImage is setting the file “splashscreen.png” to be the image that pops up as your app is loading
Line 14: I created a singleton class called ZFModel to be a global reference to things I need between views. Probably not entirely necessary, but it’s there and I like it.
Line 17: I grab a reference to the MobileApplications ViewNavigator so I can use it in other views.
Line 21: Spark’s navigationContent is an “Array of visual elements that are used as the ActionBar’s navigationContent when this view is active.” Which basically means, it’s the buttons, tabs, words, etc up at the top of your view that handle navigation and let the user know where they are located in your app. I created a button, labeled it “Home” and made it ‘pop back to the first view’ (which in Line 3 we specified as views.Home) when clicked.

Lets take a look at our first (and only… really…) view, Home.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009" 
		xmlns:s="library://ns.adobe.com/flex/spark" 
		title="Image GPS Extractor" 
		xmlns:windows="views.windows.*"
		creationComplete="init()"
		>
	<fx:Declarations>
		<!-- Place non-visual elements (e.g., services, value objects) here -->
	</fx:Declarations>
<fx:Script>
<![CDATA[
	import com.zf.utils.ExifGPSUtil;
	import com.zf.common.ZFModel;
 
	import flash.net.navigateToURL;
 
	private var imgFile        : File;
	private var exifGPSUtil   : ExifGPSUtil = new ExifGPSUtil();
	private var camera	: CameraUI;
	private var cameraRoll	: CameraRoll;
 
	private var zfModel	: ZFModel = ZFModel.getInstance();
 
	private function init():void  {
 
		if (CameraUI.isSupported)  {
			camera = new CameraUI();
			camera.addEventListener(MediaEvent.COMPLETE, mediaCaptured);
			camera.addEventListener(ErrorEvent.ERROR, onError);					
		}
 
		if(CameraRoll.supportsBrowseForImage)  {
			cameraRoll = new CameraRoll();
			cameraRoll.addEventListener(MediaEvent.SELECT, mediaSelected);
			cameraRoll.addEventListener(ErrorEvent.ERROR, onError);
		}
	}
 
	private function onError(event:ErrorEvent):void{
		alertMessage.message = "Please select an image from Gallery only";
		alertMessage.showButton=true;
		alertMessage.visible = true;
	}
 
	private function mediaCaptured(event:MediaEvent):void{
		var p:MediaPromise = event.data as MediaPromise;
		imgFile = p.file as File;
		exifGPSUtil.decodeExifData( imgFile.url , onExifDataCompleteHandler );
	}
 
	private function mediaSelected(event:MediaEvent):void{
		var p:MediaPromise = event.data as MediaPromise;
		imgFile = p.file as File;
		exifGPSUtil.decodeExifData( imgFile.url , onExifDataCompleteHandler );
	}
 
	private function browseGallery(event:MouseEvent):void {
		if(CameraRoll.supportsBrowseForImage){
			cameraRoll.browseForImage();
		}
	}
 
	private function takePicture(event:MouseEvent):void {
		if (CameraUI.isSupported){
			camera.launch(MediaType.IMAGE);
		}
	}
 
	public function onExifDataCompleteHandler( o:Object ):void  {
		if( o.success == "true" )
		{
			zfModel.coordinates	= o;
			latDMS.text 		= o.latDMS;
			longDMS.text 		= o.longDMS;
			latDEC.text 		= o.latDEC;
			longDEC.text 		= o.longDEC;
 
			mapCoords.enabled = true;
		}
		else
		{
			alertMessage.message = "No Lat/Long data was gathered for this image.";
			alertMessage.showButton=true;
			alertMessage.visible = true;
		}
	}
 
	private function onMapCoordButtonClick( e:MouseEvent ):void  {
		var lat:Number = zfModel.coordinates.latDEC;
		var lng:Number = zfModel.coordinates.longDEC;
		var url:String = "http://maps.google.com/maps?f=q&source=s_q&hl=en&geocode=&q=" + lat + "," + lng + "&z=14";
		navigateToURL( new URLRequest( url ) );
	}
 
]]>
</fx:Script>
	<s:Group id="content" width="480" height="672">
		<s:Button x="10" y="10" label="Take Photo" click="takePicture(event)"/>
		<s:Button x="247" y="10" label="Browse Gallery" click="browseGallery(event)"/>
 
		<s:Label x="10" y="134" width="460" text="Degrees, Minutes, Seconds:"/>
		<s:Label x="10" y="169" width="172" height="55" text="Latitude:" textAlign="right"
				 verticalAlign="middle"/>
		<s:Label x="10" y="232" width="172" height="55" text="Longitude:" textAlign="right"
				 verticalAlign="middle"/>
		<s:TextInput id="latDMS" x="190" y="169" width="280" />
		<s:TextInput id="longDMS" x="190" y="232" width="280" />
 
		<s:Label x="10" y="333" width="460" text="Decimal:"/>
		<s:Label x="10" y="364" width="172" height="55" text="Latitude:" textAlign="right"
				 verticalAlign="middle"/>
		<s:Label x="10" y="427" width="172" height="55" text="Longitude:" textAlign="right"
				 verticalAlign="middle"/>
		<s:TextInput id="latDEC" x="190" y="364" width="280" maxChars="10"/>
		<s:TextInput id="longDEC" x="190" y="427" width="280" maxChars="10"/>
 
		<s:Button id="mapCoords" x="118" y="589" label="Map Coordinates"
				  click="onMapCoordButtonClick(event)" enabled="false"/>
	</s:Group>
 
	<windows:AlertMessage id="alertMessage" height="250" content="{content}"/>
 
</s:View>

Alrighty… lets dive in.
Line 3: when the user changes to this view, title will be the text that appears at the top of the screen.
Line 12 & 18: I’m importing and instantiating my ExifGPSUtil class that wraps Shichiseki’s Exif Library
Line 24: The init function checks to see if the mobile device has a camera (if yes, creates a new Camera() object ) and if it has a CameraRoll feature (if yes, creates a new CameraRoll() object )
Line 39: onError only occurs with the cameraRoll if the user selects a file that doesn’t come from the device’s “Gallery” area
Line 45: mediaCaptured occurs when a new photo is taken. It takes the image as a MediaPromise, then casts it to a File. Finally we pass it to the ExifGPSUtil to decode. More on that later.
Line 51: mediaSelected occurs when a photo is selected out of the devices Gallery. Again, MediaPromise casted to a File then decoded.
Line 57: browseGallery executes when the “Browse Gallery” button is pressed. If the phone supports browseForImage then lets browseForImage
Line 61: takePicture executes when the “Take Picture” button is pressed. If the phone has a camera, launch it’s native picture taking process
Line 69: onExifDataCompleteHandler is the callback function assigned to exifGPSUtil.decodeExifData on lines 48 and 54. When the ExifGPSUtil is done processing an image, it calls this function.
Line 70: If the image had actual GeoLocation lat/long data with it… otherwise this will return as “false” and we’ll jump down to Line 80.
Line 72 – 76: Passing the data that was returned into our ZFModel for safe keeping then setting the TextFields text properties to the data so it shows up on the screen.
Line 78: We have data now so lets enable the “Map Coordinates” button so users can press it now.
Line 82-84: No coordinates were found in the image so lets display an alert message letting the user know
Line 88-92: User has clicked the “Map Coordinates” button, get the lat/long from the ZFModel and lets navigate to google maps.

You may ask, “Why did you not just use Google’s Maps API for Flash? I did, originally, but then I found that when I clicked Map Coordinates… it took anywhere from 15-40 seconds for the actual map api to appear. During this time I got to see the “Your application is not responding, force close or wait” screen appear a lot. I decided to just not use the API because it was incredibly slow and unreliable on my phone.

Lets look at the ExifGPSUtil.as next…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package com.zf.utils  {
public class ExifGPSUtil  {
 
	import flash.events.Event;
	import flash.net.URLLoader;
	import flash.net.URLRequest;
 
	import jp.shichiseki.exif.*;
 
	private var _exifLoader : ExifLoader = new ExifLoader();
	private var _callBack	: Function;
	private var _type		: String;
 
	public function ExifGPSUtil()  {
	}
 
	/***
	 * Pulls the Exif data out of a JPG image
	 * 
	 * @param url 	String 	URL of the image to be decoded
	 * @param callBack	Function	Function to call back to once image exif info is decoded
	 **/
	public function decodeExifData( url:String , callBack:Function ):void  {
 
		_callBack = callBack;
 
		_exifLoader.addEventListener(Event.COMPLETE, onExifComplete);
		_exifLoader.load( new URLRequest( url ) );
 
	}
 
	private function onExifComplete( e:Event ):void  {
		var obj:Object = new Object();
 
		if( _exifLoader.exif.ifds.gps != null )  
		{
			var rawLat	: String = _exifLoader.exif.ifds.gps["GPSLatitude"];
			var rawLong	: String = _exifLoader.exif.ifds.gps["GPSLongitude"];
 
			var posNegLat	: String = "+";
			var posNegLong	: String = "+";
 
			if( _exifLoader.exif.ifds.gps["GPSLatitudeRef"] == "S" )
				posNegLat = "-";
			if( _exifLoader.exif.ifds.gps["GPSLongitudeRef"] == "W" )
				posNegLong = "-";
 
			obj.success = "true";
 
			obj.latDMS 	= posNegLat + parseLatLong( _exifLoader.exif.ifds.gps["GPSLatitude"] );
			obj.longDMS	= posNegLong + parseLatLong( _exifLoader.exif.ifds.gps["GPSLongitude"] );
 
			obj.latDEC	= posNegLat + parseLatLongToDecimal( _exifLoader.exif.ifds.gps["GPSLatitude"] ).toString();
			obj.longDEC	= posNegLong + parseLatLongToDecimal( _exifLoader.exif.ifds.gps["GPSLongitude"] ).toString();
 
			_callBack( obj );
		}  
		else  
		{
			obj.success = "false";
			_callBack( obj );
		}
	}
 
	private function parseLatLong( val:String ) :String  {
		var v : Array = val.split( ',' );
		return v[0] + "º " + v[1] + "' " + v[2] + "''";
	}
 
	private function parseLatLongToDecimal( val:String ):Number  {
		var v : Array = val.split( ',' );
		var d : Number = v[0];
		var m : Number = v[1];
		var s : Number = v[2];
 
		var decimal:Number = d + ( m / 60 ) + ( s / 60 / 60 );
		return decimal;
	}
}
}

Line 8: Importing Shichiseki’s libraries
Line 23: This is the sole public function of this class. decodeExifData takes a url string and a callback function as params. Shichiseki’s ExifLoader class is basically just a URLLoader with some of his own functions in there so it operates just like a URLLoader.
Line 35: _exifLoader.exif.ifds.gps is how far you have to drill into _exifLoader to find the gps data. If it is null, that means there were no lat/long nor any GPS data found on the image.
Line 37 – 38: gps[“GPSLongitude”] and gps[“GPSLatitude”] contain a string that looks like: “39,48,2”. These numbers represent Degrees, Minutes, and Seconds all separated by commas. I am putting them in “rawLat/Long” because we’re soon going to convert that to a more user-friendly string.
Line 40 – 46: These lines contain the solution to the only real laughable development bug in this whole process. The coordinates the Exif Library returns are unsigned. They do not have + or – signs in front of them. And it was really, really curious to me as to why an image taken in the middle of the United States got mapped to coordinates in the middle of China. Instead, the Library gives us gps[“GPSLatitudeRef”] which contains “N” or “S” and gps[“GPSLongitudeRef”] which contain either “E” or “W”. If your reference is “S” or “W” you need to make that Lat or Long a negative number.
Line 50-54: Parsing values and setting up my obj to be returned.
Line 56: calling the callback function and passing it my coordinates object
Line 61 – 62: If gps is null, set the object success to “false” and call the callback function
Line 65: parseLatLong takes the comma separated string “39,48,2” and parses it to look like: 39º 48′ 2”
Line 70: parseLatLongToDecimal takes the comma separated string “39,48,2” and parses it to be a decimal value that looks like: 39.800555

—-
Project ZIP | Project FXP

FacebookTwitterGoogle+Share

Leave a Comment

Your email address will not be published.