package org.osmdroid.bonuspack.location; import android.location.Address; import android.os.Bundle; import android.util.Log; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; import org.osmdroid.bonuspack.utils.BonusPackHelper; import org.osmdroid.util.BoundingBox; import org.osmdroid.util.GeoPoint; import java.io.IOException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; import java.util.Locale; /** * Implements an equivalent to Android Geocoder class, based on OpenStreetMap data and Nominatim API. <br> * @see <a href="http://wiki.openstreetmap.org/wiki/Nominatim">Nominatim Reference</a> * @see <a href="http://open.mapquestapi.com/nominatim/">Nominatim at MapQuest Open</a> * * Important: to use the public Nominatim service, you will have to define a user agent, * and adhere to the <a href="http://wiki.openstreetmap.org/wiki/Nominatim_usage_policy">Nominatim usage policy</a>. * * @author M.Kergall */ public class GeocoderNominatim { public static final String NOMINATIM_SERVICE_URL = "http://nominatim.openstreetmap.org/"; public static final String MAPQUEST_SERVICE_URL = "http://open.mapquestapi.com/nominatim/v1/"; protected Locale mLocale; protected String mServiceUrl, mKey; protected String mUserAgent; protected boolean mPolygon; public GeocoderNominatim(Locale locale, String userAgent) { mLocale = locale; setOptions(false); setService(NOMINATIM_SERVICE_URL); //default service mUserAgent = userAgent; } public GeocoderNominatim(String userAgent) { this(Locale.getDefault(), userAgent); } static public boolean isPresent(){ return true; } /** * Specify the url of the Nominatim service provider to use. * Can be one of the predefined (NOMINATIM_SERVICE_URL or MAPQUEST_SERVICE_URL), * or another one, your local instance of Nominatim for instance. */ public void setService(String serviceUrl){ mServiceUrl = serviceUrl; } /** * Set AppKey for MapQuest open service */ public void setKey(String appKey) { mKey = appKey; } /** * @param polygon true to get the polygon enclosing the location. */ public void setOptions(boolean polygon){ mPolygon = polygon; } /** * Build an Android Address object from the Nominatim address in JSON format. * Current implementation is mainly targeting french addresses, * and will be quite basic on other countries. * @return Android Address, or null if input is not valid. */ protected Address buildAndroidAddress(JsonObject jResult) throws JsonSyntaxException{ Address gAddress = new Address(mLocale); if (!jResult.has("lat") || !jResult.has("lon") || !jResult.has("address")) return null; gAddress.setLatitude(jResult.get("lat").getAsDouble()); gAddress.setLongitude(jResult.get("lon").getAsDouble()); JsonObject jAddress = jResult.get("address").getAsJsonObject(); int addressIndex = 0; if (jAddress.has("road")){ gAddress.setAddressLine(addressIndex++, jAddress.get("road").getAsString()); gAddress.setThoroughfare(jAddress.get("road").getAsString()); } if (jAddress.has("suburb")){ //gAddress.setAddressLine(addressIndex++, jAddress.getString("suburb")); //not kept => often introduce "noise" in the address. gAddress.setSubLocality(jAddress.get("suburb").getAsString()); } if (jAddress.has("postcode")){ gAddress.setAddressLine(addressIndex++, jAddress.get("postcode").getAsString()); gAddress.setPostalCode(jAddress.get("postcode").getAsString()); } if (jAddress.has("city")){ gAddress.setAddressLine(addressIndex++, jAddress.get("city").getAsString()); gAddress.setLocality(jAddress.get("city").getAsString()); } else if (jAddress.has("town")){ gAddress.setAddressLine(addressIndex++, jAddress.get("town").getAsString()); gAddress.setLocality(jAddress.get("town").getAsString()); } else if (jAddress.has("village")){ gAddress.setAddressLine(addressIndex++, jAddress.get("village").getAsString()); gAddress.setLocality(jAddress.get("village").getAsString()); } if (jAddress.has("county")){ //France: departement gAddress.setSubAdminArea(jAddress.get("county").getAsString()); } if (jAddress.has("state")){ //France: region gAddress.setAdminArea(jAddress.get("state").getAsString()); } if (jAddress.has("country")){ gAddress.setAddressLine(addressIndex++, jAddress.get("country").getAsString()); gAddress.setCountryName(jAddress.get("country").getAsString()); } if (jAddress.has("country_code")) gAddress.setCountryCode(jAddress.get("country_code").getAsString()); /* Other possible OSM tags in Nominatim results not handled yet: * subway, golf_course, bus_stop, parking,... * house, house_number, building * city_district (13e Arrondissement) * road => or highway, ... * sub-city (like suburb) => locality, isolated_dwelling, hamlet ... * state_district */ //Add non-standard (but very useful) information in Extras bundle: Bundle extras = new Bundle(); if (jResult.has("polygonpoints")){ JsonArray jPolygonPoints = jResult.get("polygonpoints").getAsJsonArray(); ArrayList<GeoPoint> polygonPoints = new ArrayList<GeoPoint>(jPolygonPoints.size()); for (int i=0; i<jPolygonPoints.size(); i++){ JsonArray jCoords = jPolygonPoints.get(i).getAsJsonArray(); double lon = jCoords.get(0).getAsDouble(); double lat = jCoords.get(1).getAsDouble(); GeoPoint p = new GeoPoint(lat, lon); polygonPoints.add(p); } extras.putParcelableArrayList("polygonpoints", polygonPoints); } if (jResult.has("boundingbox")){ JsonArray jBoundingBox = jResult.get("boundingbox").getAsJsonArray(); BoundingBox bb = new BoundingBox( jBoundingBox.get(1).getAsDouble(), jBoundingBox.get(2).getAsDouble(), jBoundingBox.get(0).getAsDouble(), jBoundingBox.get(3).getAsDouble()); extras.putParcelable("boundingbox", bb); } if (jResult.has("osm_id")){ long osm_id = jResult.get("osm_id").getAsLong(); extras.putLong("osm_id", osm_id); } if (jResult.has("osm_type")){ String osm_type = jResult.get("osm_type").getAsString(); extras.putString("osm_type", osm_type); } if (jResult.has("display_name")){ String display_name = jResult.get("display_name").getAsString(); extras.putString("display_name", display_name); } gAddress.setExtras(extras); return gAddress; } /** * Equivalent to Geocoder::getFromLocation(double latitude, double longitude, int maxResults). */ public List<Address> getFromLocation(double latitude, double longitude, int maxResults) throws IOException { String url = mServiceUrl + "reverse?"; if (mKey != null) url += "key=" + mKey + "&"; url += "format=json" + "&accept-language=" + mLocale.getLanguage() //+ "&addressdetails=1" + "&lat=" + latitude + "&lon=" + longitude; Log.d(BonusPackHelper.LOG_TAG, "GeocoderNominatim::getFromLocation:"+url); String result = BonusPackHelper.requestStringFromUrl(url, mUserAgent); if (result == null) throw new IOException(); try { JsonParser parser = new JsonParser(); JsonElement json = parser.parse(result); JsonObject jResult = json.getAsJsonObject(); Address gAddress = buildAndroidAddress(jResult); List<Address> list = new ArrayList<Address>(1); if (gAddress != null) list.add(gAddress); return list; } catch (JsonSyntaxException e) { throw new IOException(); } } /** * Equivalent to Geocoder::getFromLocation(String locationName, int maxResults, double lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude, double upperRightLongitude) * but adding bounded parameter. * @param bounded true = return only results which are inside the view box; false = view box is used as a preferred area to find search results. */ public List<Address> getFromLocationName(String locationName, int maxResults, double lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude, double upperRightLongitude, boolean bounded) throws IOException { String url = mServiceUrl + "search?"; if (mKey != null) url += "key=" + mKey + "&"; url += "format=json" + "&accept-language=" + mLocale.getLanguage() + "&addressdetails=1" + "&limit=" + maxResults + "&q=" + URLEncoder.encode(locationName); if (lowerLeftLatitude != 0.0 && upperRightLatitude != 0.0){ //viewbox = left, top, right, bottom: url += "&viewbox=" + lowerLeftLongitude + "," + upperRightLatitude + "," + upperRightLongitude + "," + lowerLeftLatitude + "&bounded="+(bounded ? 1 : 0); } if (mPolygon){ //get polygon outlines for items found: url += "&polygon=1"; //TODO: polygon param is obsolete. Should be replaced by polygon_geojson. //Upgrade is on hold, waiting for MapQuest service to become compatible. } Log.d(BonusPackHelper.LOG_TAG, "GeocoderNominatim::getFromLocationName:"+url); String result = BonusPackHelper.requestStringFromUrl(url, mUserAgent); //Log.d(BonusPackHelper.LOG_TAG, result); if (result == null) throw new IOException(); try { JsonParser parser = new JsonParser(); JsonElement json = parser.parse(result); JsonArray jResults = json.getAsJsonArray(); List<Address> list = new ArrayList<Address>(jResults.size()); for (int i=0; i<jResults.size(); i++){ JsonObject jResult = jResults.get(i).getAsJsonObject(); Address gAddress = buildAndroidAddress(jResult); if (gAddress != null) list.add(gAddress); } //Log.d(BonusPackHelper.LOG_TAG, "done"); return list; } catch (JsonSyntaxException e) { throw new IOException(); } } /** * Equivalent to Geocoder::getFromLocation(String locationName, int maxResults, double lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude, double upperRightLongitude) * @see #getFromLocationName(String locationName, int maxResults) about extra data added in Address results. */ public List<Address> getFromLocationName(String locationName, int maxResults, double lowerLeftLatitude, double lowerLeftLongitude, double upperRightLatitude, double upperRightLongitude) throws IOException { return getFromLocationName(locationName, maxResults, lowerLeftLatitude, lowerLeftLongitude, upperRightLatitude, upperRightLongitude, true); } /** * Equivalent to Geocoder::getFromLocation(String locationName, int maxResults). <br> * * Some useful information, returned by Nominatim, that doesn't fit naturally within Android Address, are added in the bundle Address.getExtras():<br> * "boundingbox": the enclosing bounding box, as a BoundingBox<br> * "osm_id": the OSM id, as a long<br> * "osm_type": one of the 3 OSM types, as a string (node, way, or relation). <br> * "display_name": the address, as a single String<br> * "polygonpoints": the enclosing polygon of the location (depending on setOptions usage), as an ArrayList of GeoPoint<br> */ public List<Address> getFromLocationName(String locationName, int maxResults) throws IOException { return getFromLocationName(locationName, maxResults, 0.0, 0.0, 0.0, 0.0, false); } }