package org.osmdroid.bonuspack.location; 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.kml.KmlFolder; import org.osmdroid.bonuspack.kml.KmlGeometry; import org.osmdroid.bonuspack.kml.KmlLineString; import org.osmdroid.bonuspack.kml.KmlMultiGeometry; import org.osmdroid.bonuspack.kml.KmlPlacemark; import org.osmdroid.bonuspack.kml.KmlPoint; import org.osmdroid.bonuspack.kml.KmlPolygon; import org.osmdroid.bonuspack.utils.BonusPackHelper; import org.osmdroid.util.BoundingBox; import org.osmdroid.util.GeoPoint; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Map; import java.util.Set; /** * Access to Overpass API, a super-powerful search API on OpenStreetMap data. <br> * * Two strategies are implemented: <br> * - Get result as POIs, as simplified content and geometry (one point), using {@link #getPOIsFromUrl(String)}<br> * - Get results with full content and geometry as KML, using {@link #addInKmlFolder(KmlFolder, String)}<br> * * Helper methods are provided to build URLs for usual search requests. <br> * * TODO Improve/revise the API => add an API targeting boundaries? <br> * * @see <a href="http://wiki.openstreetmap.org/wiki/Overpass_API">Overpass API Reference</a> * @author M.Kergall */ public class OverpassAPIProvider { public static final String OVERPASS_API_DE_SERVICE = "http://overpass-api.de/api/interpreter"; public static final String OVERPASS_API_SERVICE = "http://api.openstreetmap.fr/oapi/interpreter"; protected String mService; public OverpassAPIProvider(){ setService(OVERPASS_API_DE_SERVICE); //good default, as it seems fast and reliable. } /** * Allows to change the OverPass API service * @param serviceUrl */ public void setService(String serviceUrl){ mService = serviceUrl; } /** * Build the URL to search for elements having a specific OSM Tag (key=value), within a bounding box. * Elements will be OSM nodes, ways and relations. Ways and relations will have no geometry, only their center. <br> * Usage: urlForPOISearch("amenity=cinema", map.getBoundingBox(), 200, 30);<br> * @param tag OpenStreetMap tag to search. Can be either "key=value", or "key". * @param bb bounding box * @param limit max number of results. * @param timeout in seconds * @return the url for this request. * @see <a href="http://wiki.openstreetmap.org/wiki/Tags">OSM Tags</a> */ public String urlForPOISearch(String tag, BoundingBox bb, int limit, int timeout){ StringBuilder s = new StringBuilder(); s.append(mService+"?data="); String sBB = "("+bb.getLatSouth()+","+bb.getLonWest()+","+bb.getLatNorth()+","+bb.getLonEast()+")"; String data = "[out:json][timeout:"+timeout+"];(" + "node["+tag+"]"+sBB+";" + "way["+tag+"]"+sBB+";" + "relation["+tag+"]"+sBB+";" + ");out qt center "+ limit + " tags;"; Log.d(BonusPackHelper.LOG_TAG, "data="+data); s.append(URLEncoder.encode(data)); return s.toString(); } protected GeoPoint geoPointFromJson(JsonObject jLatLon){ double lat = jLatLon.get("lat").getAsDouble(); double lon = jLatLon.get("lon").getAsDouble(); return new GeoPoint(lat, lon); } protected String tagValueFromJson(String key, JsonObject jTags){ JsonElement jTag = jTags.get(key); if (jTag == null) return null; else return jTag.getAsString(); } protected String tagValueFromJsonNotNull(String key, JsonObject jTags){ String v = tagValueFromJson(key, jTags); return (v != null ? ","+v : ""); } /** * Search for POI. * @param url full URL request, built with #urlForPOISearch or equivalent. * Main requirements: <br> * - Content must be in JSON format<br> * - ways and relations must contain the "center" element. <br> * @return elements as a list of POI */ public ArrayList<POI> getPOIsFromUrl(String url){ Log.d(BonusPackHelper.LOG_TAG, "OverpassAPIProvider:getPOIsFromUrl:"+url); String jString = BonusPackHelper.requestStringFromUrl(url); if (jString == null) { Log.e(BonusPackHelper.LOG_TAG, "OverpassAPIProvider: request failed."); return null; } try { //parse JSON and build POIs JsonParser parser = new JsonParser(); JsonElement json = parser.parse(jString); JsonObject jResult = json.getAsJsonObject(); JsonArray jElements = jResult.get("elements").getAsJsonArray(); ArrayList<POI> pois = new ArrayList<POI>(jElements.size()); for (JsonElement j:jElements){ JsonObject jo = j.getAsJsonObject(); POI poi = new POI(POI.POI_SERVICE_OVERPASS_API); poi.mId = jo.get("id").getAsLong(); poi.mCategory = jo.get("type").getAsString(); if (jo.has("tags")){ JsonObject jTags = jo.get("tags").getAsJsonObject(); poi.mType = tagValueFromJson("name", jTags); //Try to set a relevant POI type by searching for an OSM commonly used tag key, and getting its value: poi.mDescription = tagValueFromJsonNotNull("amenity", jTags) + tagValueFromJsonNotNull("boundary", jTags) + tagValueFromJsonNotNull("building", jTags) + tagValueFromJsonNotNull("craft", jTags) + tagValueFromJsonNotNull("emergency", jTags) + tagValueFromJsonNotNull("highway", jTags) + tagValueFromJsonNotNull("historic", jTags) + tagValueFromJsonNotNull("landuse", jTags) + tagValueFromJsonNotNull("leisure", jTags) + tagValueFromJsonNotNull("natural", jTags) + tagValueFromJsonNotNull("shop", jTags) + tagValueFromJsonNotNull("sport", jTags) + tagValueFromJsonNotNull("tourism", jTags); //remove first "," (quite ugly, I know) if (poi.mDescription.length()>0) poi.mDescription = poi.mDescription.substring(1); //TODO: try to set a relevant thumbnail image, according to key/value tags. //We could try to replicate Nominatim/lib/lib.php/getClassTypes(), but it sounds crazy for the added value. poi.mUrl = tagValueFromJson("website", jTags); if (poi.mUrl != null){ //normalize the url (often needed): if (!poi.mUrl.startsWith("http://") && !poi.mUrl.startsWith("https://")) poi.mUrl = "http://" + poi.mUrl; } } if ("node".equals(poi.mCategory)){ poi.mLocation = geoPointFromJson(jo); } else { if (jo.has("center")){ JsonObject jCenter = jo.get("center").getAsJsonObject(); poi.mLocation = geoPointFromJson(jCenter); } } if (poi.mLocation != null) pois.add(poi); } return pois; } catch (JsonSyntaxException e) { Log.e(BonusPackHelper.LOG_TAG, "OverpassAPIProvider: parsing error."); return null; } } /** * Build the URL to search for elements having a specific OSM Tag (key=value), within a bounding box. * Similar to {@link #urlForPOISearch}, but here the request is built to retrieve the full geometry. * @param tag * @param bb bounding box * @param limit max number of results. * @param timeout in seconds * @return the url for this request. */ public String urlForTagSearchKml(String tag, BoundingBox bb, int limit, int timeout){ StringBuilder s = new StringBuilder(); s.append(mService+"?data="); String sBB = "("+bb.getLatSouth()+","+bb.getLonWest()+","+bb.getLatNorth()+","+bb.getLonEast()+")"; String data = "[out:json][timeout:"+timeout+"];" + "(node["+tag+"]"+sBB+";" + "way["+tag+"]"+sBB+";);" + "out qt geom tags "+ limit + ";" + "relation["+tag+"]"+sBB+";out qt geom body "+ limit+";"; //relation isolated to get geometry with body option //TODO: see issue https://github.com/drolbr/Overpass-API/issues/134#issuecomment-58847362 //When solved, simplify. Log.d(BonusPackHelper.LOG_TAG, "data="+data); s.append(URLEncoder.encode(data)); return s.toString(); } /** * Attempt to detect if a way is an area. * Assume that a closed way is an area, without handling very specific OSM exceptions. */ protected boolean isAnArea(ArrayList<GeoPoint> coords){ return (coords!=null) && (coords.size()>=3) && (coords.get(0).equals(coords.get(coords.size()-1))); } protected ArrayList<GeoPoint> parseGeometry(JsonObject jo){ JsonArray jGeometry = jo.get("geometry").getAsJsonArray(); ArrayList<GeoPoint> coords = new ArrayList<GeoPoint>(jGeometry.size()); for (JsonElement j:jGeometry){ JsonObject jLatLon = j.getAsJsonObject(); GeoPoint p = geoPointFromJson(jLatLon); coords.add(p); } return coords; } protected KmlMultiGeometry buildMultiGeometry(JsonArray jMembers){ KmlMultiGeometry geometry = new KmlMultiGeometry(); for (JsonElement j:jMembers){ JsonObject jMember = j.getAsJsonObject(); KmlGeometry item = buildGeometry(jMember); geometry.addItem(item); } return geometry; } protected KmlGeometry buildGeometry(JsonObject jo){ KmlGeometry geometry; String type = jo.get("type").getAsString(); if ("node".equals(type)){ geometry = new KmlPoint(geoPointFromJson(jo)); } else if ("way".equals(type)){ ArrayList<GeoPoint> coords = parseGeometry(jo); if (isAnArea(coords)){ geometry = new KmlPolygon(); geometry.mCoordinates = coords; } else { geometry = new KmlLineString(); geometry.mCoordinates = coords; } } else { //relation: JsonArray jMembers = jo.get("members").getAsJsonArray(); geometry = buildMultiGeometry(jMembers); } return geometry; } /** * Retrieve elements from url, and add them in a KML Folder, as KML Placemarks: Point, LineString, Polygon, or MultiGeometry. * @param kmlFolder KML folder in which elements will be added * @param url OverPass API url to retrieve elements. * Main requirements:<br> * - Content must be in JSON format<br> * - ways and relations must have the "geometry" element<br> * @return true if ok, false if technical error. */ public boolean addInKmlFolder(KmlFolder kmlFolder, String url){ Log.d(BonusPackHelper.LOG_TAG, "OverpassAPIProvider:addInKmlFolder:"+url); String jString = BonusPackHelper.requestStringFromUrl(url); if (jString == null) { Log.e(BonusPackHelper.LOG_TAG, "OverpassAPIProvider: request failed."); return false; } try { //parse JSON and build KML JsonParser parser = new JsonParser(); JsonElement json = parser.parse(jString); JsonObject jResult = json.getAsJsonObject(); JsonArray jElements = jResult.get("elements").getAsJsonArray(); for (JsonElement j:jElements){ JsonObject jo = j.getAsJsonObject(); KmlPlacemark placemark = new KmlPlacemark(); placemark.mGeometry = buildGeometry(jo); placemark.mId = jo.get("id").getAsString(); //Tags: if (jo.has("tags")){ JsonObject jTags = jo.get("tags").getAsJsonObject(); if (jTags.has("name")) placemark.mName = jTags.get("name").getAsString(); //copy all tags as KML Extended Data: Set<Map.Entry<String,JsonElement>> entrySet = jTags.entrySet(); for (Map.Entry<String,JsonElement> entry:entrySet){ String key = entry.getKey(); String value = entry.getValue().getAsString(); placemark.setExtendedData(key, value); } } kmlFolder.add(placemark); } return true; } catch (JsonSyntaxException e) { Log.e(BonusPackHelper.LOG_TAG, "OverpassAPIProvider: parsing error."); return false; } } }