public final class

ReverseGeocoder

extends Thread
/*
 * 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;
    }
}