1 package com.cooliris.media;
3 import java.io.BufferedInputStream;
4 import java.io.BufferedOutputStream;
5 import java.io.ByteArrayInputStream;
6 import java.io.ByteArrayOutputStream;
7 import java.io.DataInputStream;
8 import java.io.DataOutputStream;
9 import java.io.IOException;
10 import java.util.List;
11 import java.util.Locale;
13 import android.content.Context;
14 import android.location.Address;
15 import android.location.Geocoder;
16 import android.os.Process;
18 public final class ReverseGeocoder extends Thread {
19 private static final int MAX_COUNTRY_NAME_LENGTH = 8;
20 // If two points are within 50 miles of each other, use "Around Palo Alto, CA" or "Around Mountain View, CA".
21 // instead of directly jumping to the next level and saying "California, US".
22 private static final int MAX_LOCALITY_MILE_RANGE = 50;
23 private static final Deque<MediaSet> sQueue = new Deque<MediaSet>();
24 private static final DiskCache sGeoCache = new DiskCache("geocoder-cache");
25 private static final String TAG = "ReverseGeocoder";
27 private Geocoder mGeocoder;
28 private final Context mContext;
30 public ReverseGeocoder(Context context) {
36 public void enqueue(MediaSet set) {
37 Deque<MediaSet> inQueue = sQueue;
38 synchronized (inQueue) {
46 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
47 Deque<MediaSet> queue = sQueue;
48 mGeocoder = new Geocoder(mContext);
52 // Wait for the next request.
54 synchronized (queue) {
55 while ((set = queue.pollFirst()) == null) {
59 // Process the request.
62 } catch (InterruptedException e) {
63 // Terminate the thread.
67 public void flushCache() {
71 public void shutdown() {
76 private boolean process(final MediaSet set) {
77 if (!set.mLatLongDetermined) {
78 // No latitude, longitude information available.
79 set.mReverseGeocodedLocationComputed = true;
82 set.mReverseGeocodedLocation = computeMostGranularCommonLocation(set);
83 set.mReverseGeocodedLocationComputed = true;
87 protected String computeMostGranularCommonLocation(final MediaSet set) {
88 // The overall min and max latitudes and longitudes of the set.
89 double setMinLatitude = set.mMinLatLatitude;
90 double setMinLongitude = set.mMinLonLongitude;
91 double setMaxLatitude = set.mMaxLatLatitude;
92 double setMaxLongitude = set.mMaxLonLongitude;
93 Address addr1 = lookupAddress(setMinLatitude, setMinLongitude);
94 Address addr2 = lookupAddress(setMaxLatitude, setMaxLongitude);
95 if (addr1 == null || addr2 == null) {
99 // Look at the first line of the address.
100 String closestCommonLocation = valueIfEqual(addr1.getAddressLine(0), addr2.getAddressLine(0));
101 if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
102 return closestCommonLocation;
105 // Compare thoroughfare (street address) next.
106 closestCommonLocation = valueIfEqual(addr1.getThoroughfare(), addr2.getThoroughfare());
107 if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
108 return closestCommonLocation;
111 // Feature names can sometimes be useful like "Golden Gate Bridge" but can also be
112 // degenerate for street address (like just the house number).
113 closestCommonLocation = valueIfEqual(addr1.getFeatureName(), addr2.getFeatureName());
114 if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
116 Integer.parseInt(closestCommonLocation);
117 closestCommonLocation = null;
118 } catch (final NumberFormatException nfe) {
119 // The feature name is not an integer, allow and continue.
120 return closestCommonLocation;
124 // Compare the locality.
125 closestCommonLocation = valueIfEqual(addr1.getLocality(), addr2.getLocality());
126 if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
127 String adminArea = addr1.getAdminArea();
128 if (adminArea != null && adminArea.length() > 0) {
129 closestCommonLocation += ", " + adminArea;
131 return closestCommonLocation;
134 // Just choose one of the localities if within a 50 mile radius.
135 int distance = (int) LocationMediaFilter.toMile(LocationMediaFilter.distanceBetween(setMinLatitude, setMinLongitude,
136 setMaxLatitude, setMaxLongitude));
137 if (distance < MAX_LOCALITY_MILE_RANGE) {
138 // Try each of the points and just return the first one to have a valid address.
139 Address minLatAddress = lookupAddress(setMinLatitude, set.mMinLatLongitude);
140 closestCommonLocation = getLocalityAdminForAddress(minLatAddress, true);
141 if (closestCommonLocation != null) {
142 return closestCommonLocation;
144 Address minLonAddress = lookupAddress(set.mMinLonLatitude, setMinLongitude);
145 closestCommonLocation = getLocalityAdminForAddress(minLonAddress, true);
146 if (closestCommonLocation != null) {
147 return closestCommonLocation;
149 Address maxLatAddress = lookupAddress(setMaxLatitude, set.mMaxLatLongitude);
150 closestCommonLocation = getLocalityAdminForAddress(maxLatAddress, true);
151 if (closestCommonLocation != null) {
152 return closestCommonLocation;
154 Address maxLonAddress = lookupAddress(set.mMaxLonLatitude, setMaxLongitude);
155 closestCommonLocation = getLocalityAdminForAddress(maxLonAddress, true);
156 if (closestCommonLocation != null) {
157 return closestCommonLocation;
161 // Check the administrative area.
162 closestCommonLocation = valueIfEqual(addr1.getAdminArea(), addr2.getAdminArea());
163 if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
164 String countryCode = addr1.getCountryCode();
165 if (countryCode != null && countryCode.length() > 0) {
166 closestCommonLocation += ", " + countryCode;
168 return closestCommonLocation;
171 // Check the country codes.
172 closestCommonLocation = valueIfEqual(addr1.getCountryCode(), addr2.getCountryCode());
173 if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
174 return closestCommonLocation;
176 // There is no intersection, let's choose a nicer name.
177 String addr1Country = addr1.getCountryName();
178 String addr2Country = addr2.getCountryName();
179 if (addr1Country.length() > MAX_COUNTRY_NAME_LENGTH || addr2Country.length() > MAX_COUNTRY_NAME_LENGTH) {
180 closestCommonLocation = addr1.getCountryCode() + " - " + addr2.getCountryCode();
182 closestCommonLocation = addr1Country + " - " + addr2Country;
184 return closestCommonLocation;
187 protected String getReverseGeocodedLocation(final double latitude, final double longitude, final int desiredNumDetails) {
188 String location = null;
190 Address addr = lookupAddress(latitude, longitude);
193 // Look at the first line of the address, thorough fare and feature name in order and pick one.
194 location = addr.getAddressLine(0);
195 if (location != null && !("null".equals(location))) {
198 location = addr.getThoroughfare();
199 if (location != null && !("null".equals(location))) {
202 location = addr.getFeatureName();
203 if (location != null && !("null".equals(location))) {
209 if (numDetails == desiredNumDetails) {
213 String locality = addr.getLocality();
214 if (locality != null && !("null".equals(locality))) {
215 if (location != null && location.length() > 0) {
216 location += ", " + locality;
223 if (numDetails == desiredNumDetails) {
227 String adminArea = addr.getAdminArea();
228 if (adminArea != null && !("null".equals(adminArea))) {
229 if (location != null && location.length() > 0) {
230 location += ", " + adminArea;
232 location = adminArea;
237 if (numDetails == desiredNumDetails) {
241 String countryCode = addr.getCountryCode();
242 if (countryCode != null && !("null".equals(countryCode))) {
243 if (location != null && location.length() > 0) {
244 location += ", " + countryCode;
246 location = addr.getCountryName();
254 private String getLocalityAdminForAddress(final Address addr, final boolean approxLocation) {
257 String localityAdminStr = addr.getLocality();
258 if (localityAdminStr != null && !("null".equals(localityAdminStr))) {
259 if (approxLocation) {
260 // TODO: Uncomment these lines as soon as we may translations for R.string.around.
261 // localityAdminStr = mContext.getResources().getString(R.string.around) + " " + localityAdminStr;
263 String adminArea = addr.getAdminArea();
264 if (adminArea != null && adminArea.length() > 0) {
265 localityAdminStr += ", " + adminArea;
267 return localityAdminStr;
272 private Address lookupAddress(final double latitude, final double longitude) {
274 long locationKey = (long) (((latitude + LocationMediaFilter.LAT_MAX) * 2 * LocationMediaFilter.LAT_MAX + (longitude + LocationMediaFilter.LON_MAX)) * LocationMediaFilter.EARTH_RADIUS_METERS);
275 byte[] cachedLocation = sGeoCache.get(locationKey, 0);
276 Address address = null;
277 if (cachedLocation == null || cachedLocation.length == 0) {
278 List<Address> addresses = mGeocoder.getFromLocation(latitude, longitude, 1);
279 if (!addresses.isEmpty()) {
280 address = addresses.get(0);
281 ByteArrayOutputStream bos = new ByteArrayOutputStream();
282 DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(bos, 256));
283 Locale locale = address.getLocale();
284 Utils.writeUTF(dos, locale.getLanguage());
285 Utils.writeUTF(dos, locale.getCountry());
286 Utils.writeUTF(dos, locale.getVariant());
288 Utils.writeUTF(dos, address.getThoroughfare());
289 int numAddressLines = address.getMaxAddressLineIndex();
290 dos.writeInt(numAddressLines);
291 for (int i = 0; i < numAddressLines; ++i) {
292 Utils.writeUTF(dos, address.getAddressLine(i));
294 Utils.writeUTF(dos, address.getFeatureName());
295 Utils.writeUTF(dos, address.getLocality());
296 Utils.writeUTF(dos, address.getAdminArea());
297 Utils.writeUTF(dos, address.getSubAdminArea());
299 Utils.writeUTF(dos, address.getCountryName());
300 Utils.writeUTF(dos, address.getCountryCode());
301 Utils.writeUTF(dos, address.getPostalCode());
302 Utils.writeUTF(dos, address.getPhone());
303 Utils.writeUTF(dos, address.getUrl());
306 sGeoCache.put(locationKey, bos.toByteArray());
310 // Parsing the address from the byte stream.
311 DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(cachedLocation), 256));
312 String language = Utils.readUTF(dis);
313 String country = Utils.readUTF(dis);
314 String variant = Utils.readUTF(dis);
315 Locale locale = null;
316 if (language != null) {
317 if (country == null) {
318 locale = new Locale(language);
319 } else if (variant == null) {
320 locale = new Locale(language, country);
322 locale = new Locale(language, country, variant);
325 if (!locale.getLanguage().equals(Locale.getDefault().getLanguage())) {
326 sGeoCache.delete(locationKey);
328 return lookupAddress(latitude, longitude);
330 address = new Address(locale);
332 address.setThoroughfare(Utils.readUTF(dis));
333 int numAddressLines = dis.readInt();
334 for (int i = 0; i < numAddressLines; ++i) {
335 address.setAddressLine(i, Utils.readUTF(dis));
337 address.setFeatureName(Utils.readUTF(dis));
338 address.setLocality(Utils.readUTF(dis));
339 address.setAdminArea(Utils.readUTF(dis));
340 address.setSubAdminArea(Utils.readUTF(dis));
342 address.setCountryName(Utils.readUTF(dis));
343 address.setCountryCode(Utils.readUTF(dis));
344 address.setPostalCode(Utils.readUTF(dis));
345 address.setPhone(Utils.readUTF(dis));
346 address.setUrl(Utils.readUTF(dis));
351 } catch (IOException e) {
357 private String valueIfEqual(String a, String b) {
358 return (a != null && b != null && a.equalsIgnoreCase(b)) ? a : null;