eu.eubrazilcc.lvl.core.geocoding.GeocodingHelper.java Source code

Java tutorial

Introduction

Here is the source code for eu.eubrazilcc.lvl.core.geocoding.GeocodingHelper.java

Source

/*
 * Copyright 2014 EUBrazilCC (EU?Brazil Cloud Connect)
 * 
 * Licensed under the EUPL, Version 1.1 or - as soon they will be approved by 
 * the European Commission - subsequent versions of the EUPL (the "Licence");
 * You may not use this work except in compliance with the Licence.
 * You may obtain a copy of the Licence at:
 * 
 *   http://ec.europa.eu/idabc/eupl
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the Licence is distributed on an "AS IS" basis,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the Licence for the specific language governing permissions and 
 * limitations under the Licence.
 * 
 * This product combines work with different licenses. See the "NOTICE" text
 * file for details on the various modules and licenses.
 * The "NOTICE" text file is part of the distribution. Any derivative works
 * that you distribute must include a readable copy of the "NOTICE" text file.
 */

package eu.eubrazilcc.lvl.core.geocoding;

import static com.google.common.base.Optional.absent;
import static com.google.common.base.Optional.of;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Splitter.on;
import static com.google.common.cache.CacheBuilder.newBuilder;
import static com.google.common.collect.Iterables.getFirst;
import static eu.eubrazilcc.lvl.core.concurrent.TaskRunner.TASK_RUNNER;
import static java.lang.Thread.sleep;
import static java.util.Locale.ENGLISH;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.slf4j.LoggerFactory.getLogger;

import java.util.Locale;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

import org.slf4j.Logger;

import com.google.code.geocoder.Geocoder;
import com.google.code.geocoder.GeocoderRequestBuilder;
import com.google.code.geocoder.model.GeocodeResponse;
import com.google.code.geocoder.model.GeocoderResult;
import com.google.common.base.Optional;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.ListenableFuture;

import eu.eubrazilcc.lvl.core.geojson.LngLatAlt;
import eu.eubrazilcc.lvl.core.geojson.Point;

/**
 * Converts addresses into geographic coordinates using the Google Geocoding API v3.
 * @author Erik Torres <ertorser@upv.es>
 */
public final class GeocodingHelper {

    private static final Logger LOGGER = getLogger(GeocodingHelper.class);

    public static final int MAX_CACHED_ELEMENTS = 1000;
    public static final char COUNTRY_SEPARATOR = ':';

    public static final int OVER_QUERY_LIMIT_MIN_DELAY = 800;
    public static final int OVER_QUERY_LIMIT_MAX_DELAY = 2000;

    private static final LoadingCache<String, Optional<Point>> CACHE = newBuilder().maximumSize(MAX_CACHED_ELEMENTS)
            .build(new CacheLoader<String, Optional<Point>>() {
                @Override
                public Optional<Point> load(final String key) throws ExecutionException {
                    return geocodeFromGoogle(key, true, true);
                }

                @Override
                public ListenableFuture<Optional<Point>> reload(final String key, final Optional<Point> oldValue)
                        throws Exception {
                    return TASK_RUNNER.submit(new Callable<Optional<Point>>() {
                        public Optional<Point> call() {
                            return geocodeFromGoogle(key, true, true);
                        }
                    });
                }
            });

    /**
     * Converts a Java {@link Locale} to geographic coordinates.
     * @param locale - the locale to be converted
     * @return a {@link Point} that represents a geospatial location in GeoJSON format using WGS84 
     *         coordinate reference system (CRS).
     */
    public static final Optional<Point> geocode(final Locale locale) {
        checkArgument(locale != null, "Uninitialized or invalid locale");
        return geocode(locale.getDisplayCountry(ENGLISH));
    }

    /**
     * Converts an address to geographic coordinates.
     * @param address - the address to be converted
     * @return a {@link Point} that represents a geospatial location in GeoJSON format using WGS84 
     *         coordinate reference system (CRS).
     */
    public static final Optional<Point> geocode(final String address) {
        checkArgument(isNotBlank(address), "Uninitialized or invalid address");
        Optional<Point> point = absent();
        try {
            point = CACHE.get(address.trim());
        } catch (Exception e) {
            LOGGER.error("Failed to get geospatial location from cache", e);
        }
        return point;
    }

    /**
     * Returns the geographic coordinates associated with the specified address in this cache, or {@code null} 
     * if there is no cached value for address.
     * @param address - the address to be searched for
     * @return the geographic coordinates associated with the specified address in this cache, or {@code null} 
     *         if there is no cached value for address.
     */
    public static final Optional<Point> getIfPresent(final String address) {
        return CACHE.getIfPresent(address);
    }

    /**
     * Converts an address to geographic coordinates. The expected address format is: <code>'country:region'</code>. 
     * @param address - the address to be converted
     * @param recoverZeroResults - when the query fails due to an error in the region, the method will try a second 
     *        search using only the country part of the address
     * @param recoverOverQueryLimit - when the maximum number of queries is reached, the method will suspend for a
     *        predefined time and will retry the same search one more ocassion
     * @return a {@link Point} that represents a geospatial location in GeoJSON format using WGS84 
     *         coordinate reference system (CRS), or {@code null} if the address cannot be found.
     */
    private static final Optional<Point> geocodeFromGoogle(final String address, final boolean recoverZeroResults,
            final boolean recoverOverQueryLimit) {
        checkArgument(isNotBlank(address), "Uninitialized or invalid address");
        Optional<Point> point = absent();
        try {
            final Geocoder geocoder = new Geocoder();
            final GeocodeResponse response = geocoder.geocode(
                    new GeocoderRequestBuilder().setAddress(address).setLanguage("en").getGeocoderRequest());
            checkState(response != null, "No response received from Google Geocoding service");
            checkState(response.getStatus() != null,
                    "No status code included in the response received from Google Geocoding service");
            switch (response.getStatus()) {
            case OK:
                break;
            case ZERO_RESULTS:
                if (recoverZeroResults) {
                    final String country = countryName(address);
                    checkState(isNotBlank(country),
                            "Invalid address format, expected 'country:region' but found: " + address);
                    point = geocodeFromGoogle(country, false, recoverOverQueryLimit);
                }
                break;
            case OVER_QUERY_LIMIT:
                if (recoverOverQueryLimit) {
                    try {
                        sleep(new Random().nextInt(OVER_QUERY_LIMIT_MAX_DELAY - OVER_QUERY_LIMIT_MIN_DELAY + 1)
                                + OVER_QUERY_LIMIT_MIN_DELAY);
                        point = geocodeFromGoogle(address, recoverZeroResults, false);
                    } catch (InterruptedException ie) {
                    }
                } else {
                    // throwing an exception will prevent this value to be cached               
                    throw new IllegalStateException(
                            "Search failed, maximum number of Google Geocoding queries exceeded");
                }
                break;
            default:
                throw new IllegalStateException("Searching for '" + address
                        + "' produces invalid Google Geocoding server response: " + response.getStatus());
            }
            if (!point.isPresent() && response.getResults() != null) {
                for (int i = 0; i < response.getResults().size() && !point.isPresent(); i++) {
                    final GeocoderResult result = response.getResults().get(i);
                    if (result != null && result.getGeometry() != null && result.getGeometry().getLocation() != null
                            && result.getGeometry().getLocation().getLng() != null
                            && result.getGeometry().getLocation().getLat() != null) {
                        point = of(Point.builder().coordinates(LngLatAlt.builder()
                                .longitude(result.getGeometry().getLocation().getLng().doubleValue())
                                .latitude(result.getGeometry().getLocation().getLat().doubleValue()).build())
                                .build());
                    }
                }
            }
        } catch (Exception e) {
            LOGGER.error("Failed to convert address to geographic coordinates", e);
        }
        return point;
    }

    private static final String countryName(final String address) {
        return getFirst(on(COUNTRY_SEPARATOR).trimResults().omitEmptyStrings().split(address), "");
    }

}