/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cooliris.media;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import android.content.Context;
import android.location.Address;
import android.location.Criteria;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationManager;
import android.os.Process;
public final class ReverseGeocoder extends Thread {
private static final int MAX_COUNTRY_NAME_LENGTH = 8;
// If two points are within 20 miles of each other, use
// "Around Palo Alto, CA" or "Around Mountain View, CA".
// instead of directly jumping to the next level and saying
// "California, US".
private static final int MAX_LOCALITY_MILE_RANGE = 20;
private static final Deque<MediaSet> sQueue = new Deque<MediaSet>();
private static final DiskCache sGeoCache = new DiskCache("geocoder-cache");
private static final String TAG = "ReverseGeocoder";
private static Criteria LOCATION_CRITERIA = new Criteria();
private static Address sCurrentAddress; // last known address
static {
LOCATION_CRITERIA.setAccuracy(Criteria.ACCURACY_COARSE);
LOCATION_CRITERIA.setPowerRequirement(Criteria.NO_REQUIREMENT);
LOCATION_CRITERIA.setBearingRequired(false);
LOCATION_CRITERIA.setSpeedRequired(false);
LOCATION_CRITERIA.setAltitudeRequired(false);
}
private Geocoder mGeocoder;
private final Context mContext;
public ReverseGeocoder(Context context) {
super(TAG);
mContext = context;
start();
}
public void enqueue(MediaSet set) {
Deque<MediaSet> inQueue = sQueue;
synchronized (inQueue) {
inQueue.addFirst(set);
inQueue.notify();
}
}
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
Deque<MediaSet> queue = sQueue;
mGeocoder = new Geocoder(mContext);
queue.clear();
try {
for (;;) {
// Wait for the next request.
MediaSet set;
synchronized (queue) {
while ((set = queue.pollFirst()) == null) {
queue.wait();
}
}
// Process the request.
process(set);
}
} catch (InterruptedException e) {
// Terminate the thread.
}
}
public void flushCache() {
sGeoCache.flush();
}
public void shutdown() {
flushCache();
this.interrupt();
}
private boolean process(final MediaSet set) {
if (!set.mLatLongDetermined) {
// No latitude, longitude information available.
set.mReverseGeocodedLocationComputed = true;
return false;
}
set.mReverseGeocodedLocation = computeMostGranularCommonLocation(set);
set.mReverseGeocodedLocationComputed = true;
return true;
}
protected String computeMostGranularCommonLocation(final MediaSet set) {
// The overall min and max latitudes and longitudes of the set.
double setMinLatitude = set.mMinLatLatitude;
double setMinLongitude = set.mMinLatLongitude;
double setMaxLatitude = set.mMaxLatLatitude;
double setMaxLongitude = set.mMaxLatLongitude;
if (Math.abs(set.mMaxLatLatitude - set.mMinLatLatitude) < Math.abs(set.mMaxLonLongitude - set.mMinLonLongitude)) {
setMinLatitude = set.mMinLonLatitude;
setMinLongitude = set.mMinLonLongitude;
setMaxLatitude = set.mMaxLonLatitude;
setMaxLongitude = set.mMaxLonLongitude;
}
Address addr1 = lookupAddress(setMinLatitude, setMinLongitude);
Address addr2 = lookupAddress(setMaxLatitude, setMaxLongitude);
if (addr1 == null)
addr1 = addr2;
if (addr2 == null)
addr2 = addr1;
if (addr1 == null || addr2 == null) {
return null;
}
// Get current location, we decide the granularity of the string based
// on this.
LocationManager locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
Location location = null;
List<String> providers = locationManager.getAllProviders();
for (int i = 0; i < providers.size(); ++i) {
String provider = providers.get(i);
location = (provider != null) ? locationManager.getLastKnownLocation(provider) : null;
if (location != null)
break;
}
String currentCity = "";
String currentAdminArea = "";
String currentCountry = Locale.getDefault().getCountry();
if (location != null) {
Address currentAddress = lookupAddress(location.getLatitude(), location.getLongitude());
if (currentAddress == null) {
currentAddress = sCurrentAddress;
} else {
sCurrentAddress = currentAddress;
}
if (currentAddress != null && currentAddress.getCountryCode() != null) {
currentCity = checkNull(currentAddress.getLocality());
currentCountry = checkNull(currentAddress.getCountryCode());
currentAdminArea = checkNull(currentAddress.getAdminArea());
}
}
String closestCommonLocation = null;
String addr1Locality = checkNull(addr1.getLocality());
String addr2Locality = checkNull(addr2.getLocality());
String addr1AdminArea = checkNull(addr1.getAdminArea());
String addr2AdminArea = checkNull(addr2.getAdminArea());
String addr1CountryCode = checkNull(addr1.getCountryCode());
String addr2CountryCode = checkNull(addr2.getCountryCode());
if (currentCity.equals(addr1Locality) && currentCity.equals(addr2Locality)) {
String otherCity = currentCity;
if (currentCity.equals(addr1Locality)) {
otherCity = addr2Locality;
if (otherCity.length() == 0) {
otherCity = addr2AdminArea;
if (!currentCountry.equals(addr2CountryCode)) {
otherCity += " " + addr2CountryCode;
}
}
addr2Locality = addr1Locality;
addr2AdminArea = addr1AdminArea;
addr2CountryCode = addr1CountryCode;
} else {
otherCity = addr1Locality;
if (otherCity.length() == 0) {
otherCity = addr1AdminArea + " " + addr1CountryCode;
;
if (!currentCountry.equals(addr1CountryCode)) {
otherCity += " " + addr1CountryCode;
}
}
addr1Locality = addr2Locality;
addr1AdminArea = addr2AdminArea;
addr1CountryCode = addr2CountryCode;
}
closestCommonLocation = valueIfEqual(addr1.getAddressLine(0), addr2.getAddressLine(0));
if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
if (!currentCity.equals(otherCity)) {
closestCommonLocation += " - " + otherCity;
}
return closestCommonLocation;
}
// Compare thoroughfare (street address) next.
closestCommonLocation = valueIfEqual(addr1.getThoroughfare(), addr2.getThoroughfare());
if (closestCommonLocation != null && !("null".equals(closestCommonLocation))) {
return closestCommonLocation;
}
}
// Compare the locality.
closestCommonLocation = valueIfEqual(addr1Locality, addr2Locality);
if (closestCommonLocation != null && !("".equals(closestCommonLocation))) {
String adminArea = addr1AdminArea;
String countryCode = addr1CountryCode;
if (adminArea != null && adminArea.length() > 0) {
if (!countryCode.equals(currentCountry)) {
closestCommonLocation += ", " + adminArea + " " + countryCode;
} else {
closestCommonLocation += ", " + adminArea;
}
}
return closestCommonLocation;
}
// If the admin area is the same as the current location, we hide it and
// instead show the city name.
if (currentAdminArea.equals(addr1AdminArea) && currentAdminArea.equals(addr2AdminArea)) {
if ("".equals(addr1Locality)) {
addr1Locality = addr2Locality;
}
if ("".equals(addr2Locality)) {
addr2Locality = addr1Locality;
}
if (!"".equals(addr1Locality)) {
if (addr1Locality.equals(addr2Locality)) {
closestCommonLocation = addr1Locality + ", " + currentAdminArea;
} else {
closestCommonLocation = addr1Locality + " - " + addr2Locality;
}
return closestCommonLocation;
}
}
// Just choose one of the localities if within a MAX_LOCALITY_MILE_RANGE
// mile radius.
int distance = (int) LocationMediaFilter.toMile(LocationMediaFilter.distanceBetween(setMinLatitude, setMinLongitude,
setMaxLatitude, setMaxLongitude));
if (distance < MAX_LOCALITY_MILE_RANGE) {
// Try each of the points and just return the first one to have a
// valid address.
closestCommonLocation = getLocalityAdminForAddress(addr1, true);
if (closestCommonLocation != null) {
return closestCommonLocation;
}
closestCommonLocation = getLocalityAdminForAddress(addr2, true);
if (closestCommonLocation != null) {
return closestCommonLocation;
}
}
// Check the administrative area.
closestCommonLocation = valueIfEqual(addr1AdminArea, addr2AdminArea);
if (closestCommonLocation != null && !("".equals(closestCommonLocation))) {
String countryCode = addr1CountryCode;
if (!countryCode.equals(currentCountry)) {
if (countryCode != null && countryCode.length() > 0) {
closestCommonLocation += " " + countryCode;
}
}
return closestCommonLocation;
}
// Check the country codes.
closestCommonLocation = valueIfEqual(addr1CountryCode, addr2CountryCode);
if (closestCommonLocation != null && !("".equals(closestCommonLocation))) {
return closestCommonLocation;
}
// There is no intersection, let's choose a nicer name.
String addr1Country = addr1.getCountryName();
String addr2Country = addr2.getCountryName();
if (addr1Country == null)
addr1Country = addr1CountryCode;
if (addr2Country == null)
addr2Country = addr2CountryCode;
if (addr1Country == null || addr2Country == null)
return null;
if (addr1Country.length() > MAX_COUNTRY_NAME_LENGTH || addr2Country.length() > MAX_COUNTRY_NAME_LENGTH) {
closestCommonLocation = addr1CountryCode + " - " + addr2CountryCode;
} else {
closestCommonLocation = addr1Country + " - " + addr2Country;
}
return closestCommonLocation;
}
private String checkNull(String locality) {
if (locality == null)
return "";
if (locality.equals("null"))
return "";
return locality;
}
protected String getReverseGeocodedLocation(final double latitude, final double longitude, final int desiredNumDetails) {
String location = null;
int numDetails = 0;
try {
Address addr = lookupAddress(latitude, longitude);
if (addr != null) {
// Look at the first line of the address, thorough fare and
// feature
// name in order and pick one.
location = addr.getAddressLine(0);
if (location != null && !("null".equals(location))) {
numDetails++;
} else {
location = addr.getThoroughfare();
if (location != null && !("null".equals(location))) {
numDetails++;
} else {
location = addr.getFeatureName();
if (location != null && !("null".equals(location))) {
numDetails++;
}
}
}
if (numDetails == desiredNumDetails) {
return location;
}
String locality = addr.getLocality();
if (locality != null && !("null".equals(locality))) {
if (location != null && location.length() > 0) {
location += ", " + locality;
} else {
location = locality;
}
numDetails++;
}
if (numDetails == desiredNumDetails) {
return location;
}
String adminArea = addr.getAdminArea();
if (adminArea != null && !("null".equals(adminArea))) {
if (location != null && location.length() > 0) {
location += ", " + adminArea;
} else {
location = adminArea;
}
numDetails++;
}
if (numDetails == desiredNumDetails) {
return location;
}
String countryCode = addr.getCountryCode();
if (countryCode != null && !("null".equals(countryCode))) {
if (location != null && location.length() > 0) {
location += ", " + countryCode;
} else {
location = addr.getCountryName();
}
}
}
return location;
} catch (Exception e) {
return null;
}
}
private String getLocalityAdminForAddress(final Address addr, final boolean approxLocation) {
if (addr == null)
return "";
String localityAdminStr = addr.getLocality();
if (localityAdminStr != null && !("null".equals(localityAdminStr))) {
if (approxLocation) {
// TODO: Uncomment these lines as soon as we may translations
// for Res.string.around.
// localityAdminStr =
// mContext.getResources().getString(Res.string.around) + " " +
// localityAdminStr;
}
String adminArea = addr.getAdminArea();
if (adminArea != null && adminArea.length() > 0) {
localityAdminStr += ", " + adminArea;
}
return localityAdminStr;
}
return null;
}
private Address lookupAddress(final double latitude, final double longitude) {
try {
long locationKey = (long) (((latitude + LocationMediaFilter.LAT_MAX) * 2 * LocationMediaFilter.LAT_MAX + (longitude + LocationMediaFilter.LON_MAX)) * LocationMediaFilter.EARTH_RADIUS_METERS);
byte[] cachedLocation = sGeoCache.get(locationKey, 0);
Address address = null;
if (cachedLocation == null || cachedLocation.length == 0) {
try {
List<Address> addresses = mGeocoder.getFromLocation(latitude, longitude, 1);
if (!addresses.isEmpty()) {
address = addresses.get(0);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(bos, 256));
Locale locale = address.getLocale();
Utils.writeUTF(dos, locale.getLanguage());
Utils.writeUTF(dos, locale.getCountry());
Utils.writeUTF(dos, locale.getVariant());
Utils.writeUTF(dos, address.getThoroughfare());
int numAddressLines = address.getMaxAddressLineIndex();
dos.writeInt(numAddressLines);
for (int i = 0; i < numAddressLines; ++i) {
Utils.writeUTF(dos, address.getAddressLine(i));
}
Utils.writeUTF(dos, address.getFeatureName());
Utils.writeUTF(dos, address.getLocality());
Utils.writeUTF(dos, address.getAdminArea());
Utils.writeUTF(dos, address.getSubAdminArea());
Utils.writeUTF(dos, address.getCountryName());
Utils.writeUTF(dos, address.getCountryCode());
Utils.writeUTF(dos, address.getPostalCode());
Utils.writeUTF(dos, address.getPhone());
Utils.writeUTF(dos, address.getUrl());
dos.flush();
sGeoCache.put(locationKey, bos.toByteArray(), 0);
dos.close();
}
} finally {
}
} else {
// Parsing the address from the byte stream.
DataInputStream dis = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(cachedLocation), 256));
String language = Utils.readUTF(dis);
String country = Utils.readUTF(dis);
String variant = Utils.readUTF(dis);
Locale locale = null;
if (language != null) {
if (country == null) {
locale = new Locale(language);
} else if (variant == null) {
locale = new Locale(language, country);
} else {
locale = new Locale(language, country, variant);
}
}
if (!locale.getLanguage().equals(Locale.getDefault().getLanguage())) {
sGeoCache.delete(locationKey);
dis.close();
return lookupAddress(latitude, longitude);
}
address = new Address(locale);
address.setThoroughfare(Utils.readUTF(dis));
int numAddressLines = dis.readInt();
for (int i = 0; i < numAddressLines; ++i) {
address.setAddressLine(i, Utils.readUTF(dis));
}
address.setFeatureName(Utils.readUTF(dis));
address.setLocality(Utils.readUTF(dis));
address.setAdminArea(Utils.readUTF(dis));
address.setSubAdminArea(Utils.readUTF(dis));
address.setCountryName(Utils.readUTF(dis));
address.setCountryCode(Utils.readUTF(dis));
address.setPostalCode(Utils.readUTF(dis));
address.setPhone(Utils.readUTF(dis));
address.setUrl(Utils.readUTF(dis));
dis.close();
}
return address;
} catch (Exception e) {
// Ignore.
}
return null;
}
private String valueIfEqual(String a, String b) {
return (a != null && b != null && a.equalsIgnoreCase(b)) ? a : null;
}
}