/*
This file is part of RouteConverter.
RouteConverter is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
RouteConverter is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with RouteConverter; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Copyright (C) 2007 Christian Pesch. All Rights Reserved.
*/
package slash.navigation.googlemaps;
import slash.navigation.common.BoundingBox;
import slash.navigation.common.LongitudeAndLatitude;
import slash.navigation.common.NavigationPosition;
import slash.navigation.common.SimpleNavigationPosition;
import slash.navigation.elevation.ElevationService;
import slash.navigation.geocoding.GeocodingService;
import slash.navigation.googlemaps.elevation.ElevationResponse;
import slash.navigation.googlemaps.geocode.GeocodeResponse;
import slash.navigation.rest.Get;
import slash.navigation.rest.exception.ServiceUnavailableException;
import javax.xml.bind.JAXBException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;
import static java.util.Arrays.sort;
import static slash.common.io.Transfer.encodeUri;
import static slash.navigation.common.Bearing.calculateBearing;
import static slash.navigation.googlemaps.GoogleMapsAPIKey.getAPIKey;
import static slash.navigation.googlemaps.GoogleMapsServer.getGoogleMapsServer;
import static slash.navigation.googlemaps.GoogleMapsUtil.unmarshalElevation;
import static slash.navigation.googlemaps.GoogleMapsUtil.unmarshalGeocode;
import static slash.navigation.rest.HttpRequest.USER_AGENT;
/**
* Encapsulates REST access to the Google Maps API Elevation and Geocoding Services.
*
* @author Christian Pesch
*/
public class GoogleMapsService implements ElevationService, GeocodingService {
private static final Logger log = Logger.getLogger(GoogleMapsService.class.getName());
private int overQueryLimitCount = 0;
public String getName() {
return "Google Maps";
}
public boolean isOverQueryLimit() {
return overQueryLimitCount > 0;
}
private String getGoogleMapsApiUrl(String api, String payload) {
String language = Locale.getDefault().getLanguage();
return getGoogleMapsServer().getApiUrl() + "/maps/api/" + api + "/xml?" + payload +
"&sensor=false&language=" + language + "&key=" + getAPIKey(api);
}
private String getElevationUrl(String payload) {
return getGoogleMapsApiUrl("elevation", payload);
}
private String getGeocodingUrl(String payload) {
return getGoogleMapsApiUrl("geocode", payload);
}
private Get get(String url) {
Get get = new Get(url);
get.setUserAgent(USER_AGENT);
return get;
}
private void checkForError(String url, String status) throws ServiceUnavailableException {
if (status.equals("OVER_QUERY_LIMIT")) {
overQueryLimitCount++;
log.warning("Google API is over query limit, count: " + overQueryLimitCount + ", url: " + url);
throw new ServiceUnavailableException(getClass().getSimpleName(), url, status);
}
if (status.equals("REQUEST_DENIED")) {
log.warning("Google API access is denied, url: " + url);
throw new ServiceUnavailableException(getClass().getSimpleName(), url, status);
}
}
public String getAddressFor(NavigationPosition position) throws IOException {
String url = getGeocodingUrl("latlng=" + position.getLatitude() + "," + position.getLongitude());
Get get = get(url);
log.info("Getting location for " + position.getLongitude() + "," + position.getLatitude());
String result = get.executeAsString();
if (get.isSuccessful())
try {
GeocodeResponse geocodeResponse = unmarshalGeocode(result);
if (geocodeResponse != null) {
String status = geocodeResponse.getStatus();
checkForError(url, status);
return extractClosestLocation(geocodeResponse.getResult(), position.getLongitude(), position.getLatitude());
}
} catch (JAXBException e) {
throw new IOException("Cannot unmarshall " + result + ": " + e, e);
}
return null;
}
private String extractClosestLocation(List<GeocodeResponse.Result> results,
final double longitude, final double latitude) {
GeocodeResponse.Result[] resultsArray = results.toArray(new GeocodeResponse.Result[results.size()]);
sort(resultsArray, new Comparator<GeocodeResponse.Result>() {
public int compare(GeocodeResponse.Result p1, GeocodeResponse.Result p2) {
GeocodeResponse.Result.Geometry.Location l1 = p1.getGeometry().getLocation();
GeocodeResponse.Result.Geometry.Location l2 = p2.getGeometry().getLocation();
double distance1 = calculateBearing(longitude, latitude, l1.getLng().doubleValue(), l1.getLat().doubleValue()).getDistance();
double distance2 = calculateBearing(longitude, latitude, l2.getLng().doubleValue(), l2.getLat().doubleValue()).getDistance();
return (int) (distance1 - distance2);
}
});
return resultsArray.length > 0 ? resultsArray[0].getFormattedAddress() : null;
}
public List<NavigationPosition> getPositionsFor(String address) throws IOException {
String url = getGeocodingUrl("address=" + encodeUri(address));
Get get = get(url);
log.info("Getting positions for " + address);
String result = get.executeAsString();
if (get.isSuccessful())
try {
GeocodeResponse geocodeResponse = unmarshalGeocode(result);
if (geocodeResponse != null) {
String status = geocodeResponse.getStatus();
checkForError(url, status);
return extractAdresses(geocodeResponse.getResult());
}
} catch (JAXBException e) {
throw new IOException("Cannot unmarshall " + result + ": " + e, e);
}
return null;
}
private List<NavigationPosition> extractAdresses(List<GeocodeResponse.Result> responses) {
List<NavigationPosition> result = new ArrayList<>(responses.size());
for (GeocodeResponse.Result response : responses) {
GeocodeResponse.Result.Geometry.Location location = response.getGeometry().getLocation();
result.add(new SimpleNavigationPosition(location.getLng().doubleValue(), location.getLat().doubleValue(),
null, response.getFormattedAddress()));
}
return result;
}
public Double getElevationFor(double longitude, double latitude) throws IOException {
String url = getElevationUrl("locations=" + latitude + "," + longitude); // could be up to 512 locations
Get get = get(url);
log.info("Getting elevation for " + longitude + "," + latitude);
String result = get.executeAsString();
if (get.isSuccessful())
try {
ElevationResponse elevationResponse = unmarshalElevation(result);
if (elevationResponse != null) {
String status = elevationResponse.getStatus();
checkForError(url, status);
List<Double> elevations = extractElevations(elevationResponse.getResult());
return elevations != null && elevations.size() > 0 ? elevations.get(0) : null;
}
} catch (JAXBException e) {
throw new IOException("Cannot unmarshall " + result + ": " + e, e);
}
return null;
}
private List<Double> extractElevations(List<ElevationResponse.Result> responses) {
List<Double> results = new ArrayList<>(responses.size());
for (ElevationResponse.Result response : responses) {
results.add(response.getElevation().doubleValue());
}
return results;
}
public boolean isDownload() {
return false;
}
public boolean isSupportsPath() {
return false;
}
public String getPath() {
throw new UnsupportedOperationException();
}
public void setPath(String path) {
throw new UnsupportedOperationException();
}
public void downloadElevationDataFor(List<LongitudeAndLatitude> longitudeAndLatitudes, boolean waitForDownload) {
throw new UnsupportedOperationException();
}
public long calculateRemainingDownloadSize(List<BoundingBox> boundingBoxes) {
throw new UnsupportedOperationException();
}
public void downloadElevationData(List<BoundingBox> boundingBoxes) {
throw new UnsupportedOperationException();
}
}