/* * Copyright (C) 2010-2012 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * * Akvo FLOW is free software: you can redistribute it and modify it under the terms of * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, * either version 3 of the License or any later version. * * Akvo FLOW 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 Affero General Public License included below for more details. * * The full license text can also be seen at <http://www.gnu.org/licenses/agpl.html>. */ package com.gallatinsystems.gis.location; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.StringReader; import java.net.URL; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import com.gallatinsystems.gis.map.dao.OGRFeatureDao; import com.gallatinsystems.gis.map.domain.Geometry; import com.gallatinsystems.gis.map.domain.Geometry.GeometryType; import com.gallatinsystems.gis.map.domain.OGRFeature; import com.gallatinsystems.gis.map.domain.OGRFeature.FeatureType; import com.gallatinsystems.survey.xml.SurveyXMLAdapter; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; import com.vividsolutions.jts.io.ParseException; import com.vividsolutions.jts.io.WKTReader; /** * This service utilizes the geonames web services (ws.geonames.org) to look up geographic * information given latitude/longitude coordinates. * * @author Christopher Fagiani */ public class GeoLocationServiceGeonamesImpl implements GeoLocationService { private static final Logger log = Logger.getLogger(SurveyXMLAdapter.class .getName()); // private static final String COUNTRY_SERVICE_URL = "http://ws.geonames.org/countryCode?"; private static final String PLACE_SERVICE_URL = "http://api.geonames.org/findNearbyPlaceName?username=akvo&"; private static final String LAT_PARAM = "lat"; private static final String LON_PARAM = "lng"; /** * bounding box map of countries we care about order of coordinates: top left, bottom right */ private Map<String, Double[]> COUNTRY_MBR = new HashMap<String, Double[]>() { private static final long serialVersionUID = 9163559500580094769L; { put("GT", new Double[] { 17.8152, -92.2414, 13.7373, -88.2232 }); put("HN", new Double[] { 17.4505, -89.3508, 12.9824, -82.4995 }); put("DO", new Double[] { 19.9298, -72.0035, 17.4693, -68.3200 }); put("NI", new Double[] { 15.0259, -87.6903, 10.7075, -82.5921 }); // TODO: make sure we check ecuador before peru since they overlap put("EC", new Double[] { 1.6504, -92.0005, -4.9988, -75.1846, }); put("BO", new Double[] { -9.6806, -69.6408, -22.8961, -57.4581 }); put("PE", new Double[] { -0.0130, -81.3267, -18.3497, -68.6780, }); put("RW", new Double[] { -1.064, 28.871, -2.867, 30.97 }); put("MW", new Double[] { -9.3675, 32.6740, -17.1250, 35.9168 }); put("UG", new Double[] { 4.2144, 29.5732, -1.4840, 35.0360 }); put("IN", new Double[] { 36.2617, 68.0323, 6.7471, 97.4030 }); put("LR", // new Double[] { 4.353060, -11.492080, 8.551790, -7.365110 }); new Double[] { 3.0, -14.7, 8.551790, -7.365110 }); } }; private Map<String, String> COUNTRY_NAME = new HashMap<String, String>() { private static final long serialVersionUID = -6506773226209066480L; { put("GT", "Guatemala"); put("HN", "Honduras"); put("NI", "Nicaragua"); put("DO", "Dominican Republic"); put("EC", "Ecuador"); put("BO", "Bolivia"); put("PE", "Peru"); put("RW", "Rwanda"); put("MW", "Malawi"); put("UG", "Uganda"); put("IN", "India"); } }; /** * returns the 2-letter country code for the lat/lon location passed in */ public String getCountryCodeForPoint(String lat, String lon) { OGRFeatureDao ogrFeatureDao = new OGRFeatureDao(); List<OGRFeature> ogrList = ogrFeatureDao.listByExtentAndType( Double.parseDouble(lon), Double.parseDouble(lat), FeatureType.COUNTRY, "x1", "asc", "all"); String countryCode = null; for (OGRFeature item : ogrList) { Geometry geo = item.getGeometry(); GeometryFactory geometryFactory = new GeometryFactory(); WKTReader reader = new WKTReader(geometryFactory); com.vividsolutions.jts.geom.Geometry shape = null; if (geo != null && geo.getType() != null) { try { if (geo.getType().equals(GeometryType.POLYGON)) { shape = (Polygon) reader.read(geo.getWktText()); } else if (geo.getType().equals(GeometryType.MULITPOLYGON)) { shape = (MultiPolygon) reader.read(geo.getWktText()); } } catch (ParseException e) { log.log(Level.SEVERE, e.getMessage()); } Coordinate coord = new Coordinate(Double.parseDouble(lon), Double.parseDouble(lat)); Point point = geometryFactory.createPoint(coord); if (shape != null && shape.contains(point)) { countryCode = item.getCountryCode(); break; } } else { log.log(Level.INFO, item.getCountryCode() + " has a null geometry"); } } return countryCode; } // /** // * returns the 2-letter country code for the lat/lon location passed in // */ // public String getCountryCodeForPoint(String lat, String lon) { // String countryCode = null; // countryCode = callApi(COUNTRY_SERVICE_URL, lat, lon, true); // if (countryCode != null) { // countryCode = countryCode.trim(); // } else { // GeoPlace p = manualLookup(lat, lon); // if (p != null) { // countryCode = p.getCountryCode(); // } // } // return countryCode; // } /** * returns a geo place object that is closest to the lat/lon passed in. */ public GeoPlace findGeoPlace(String lat, String lon) { GeoPlaces places = parseXml(callApi(PLACE_SERVICE_URL, lat, lon, false)); if (places != null && places.getGeoname() != null) { return places.getGeoname().get(0); } else { return manualLookup(lat, lon); } } /** * Forms a service url and calls the api, returning the entire response body as a string */ private String callApi(String base, String lat, String lon, boolean retry) { String result = null; try { result = invokeApi(base, lat, lon); } catch (IOException ie) { // retry because of timeout log.log(Level.WARNING, "Timeout for " + base, ie); if (retry) { try { result = invokeApi(base, lat, lon); } catch (Exception e) { log.log(Level.WARNING, "Could not invoke geonames api via url " + base, e); } } } catch (Exception e) { log.log(Level.WARNING, "Could not invoke geonames api via url " + base, e); } return result; } private String invokeApi(String base, String lat, String lon) throws IOException, Exception { URL url = new URL(base + LAT_PARAM + "=" + lat + "&" + LON_PARAM + "=" + lon); BufferedReader reader = new BufferedReader(new InputStreamReader( url.openStream())); String line = null; StringBuilder builder = new StringBuilder(); while ((line = reader.readLine()) != null) { builder.append(line); } reader.close(); return builder.toString(); } private GeoPlaces parseXml(String xmlString) { GeoPlaces places = null; if (xmlString != null) { try { JAXBContext jc = JAXBContext.newInstance(GeoPlaces.class, GeoPlace.class); Unmarshaller unmarshaller; unmarshaller = jc.createUnmarshaller(); StringReader sr = new StringReader(xmlString); places = (GeoPlaces) unmarshaller.unmarshal(sr); } catch (JAXBException e) { log.log(Level.SEVERE, "Could not parse api response", e); } } else { log.log(Level.SEVERE, "Geonames response xml was null"); } return places; } /** * absolute fall-back lookup for geo places that uses a statically intialized bounding box * * @param latStr * @param lonStr * @return */ private GeoPlace primitiveLookup(String latStr, String lonStr) { GeoPlace place = null; try { if (latStr != null && lonStr != null) { double lat = Double.parseDouble(latStr); double lon = Double.parseDouble(lonStr); for (Entry<String, Double[]> entry : COUNTRY_MBR.entrySet()) { if (lat <= entry.getValue()[0] && lat >= entry.getValue()[2] && lon >= entry.getValue()[1] && lon <= entry.getValue()[3]) { place = new GeoPlace(); place.setCountryCode(entry.getKey()); place.setCountryName(COUNTRY_NAME.get(entry.getKey())); break; } } } else { log.log(Level.SEVERE, "Lat or lon is null"); } } catch (Exception e) { log.log(Level.SEVERE, "Lat/Lon are non numeric: ", e); } return place; } public GeoPlace manualLookup(String latStr, String lonStr) { return manualLookup(latStr, lonStr, FeatureType.COUNTRY); } public GeoPlace manualLookup(String latStr, String lonStr, OGRFeature.FeatureType type) { log.log(Level.INFO, "Inside Manaual Lookup for " + latStr + ":" + lonStr + ":" + type.toString()); GeoPlace place = null; OGRFeatureDao ogrFeatureDao = new OGRFeatureDao(); // create a list of candidates, based on a bounding box search List<OGRFeature> ogrList = ogrFeatureDao.listByExtentAndType( Double.parseDouble(lonStr), Double.parseDouble(latStr), type, "x1", "asc", "all"); String countryCode = null; // now we have to check if one of the candidates is the one by looking at the real GIS data for (OGRFeature item : ogrList) { Geometry geo = item.getGeometry(); GeometryFactory geometryFactory = new GeometryFactory(); WKTReader reader = new WKTReader(geometryFactory); com.vividsolutions.jts.geom.Geometry shape = null; if (geo != null && geo.getType() != null) { try { if (geo.getType().equals(GeometryType.POLYGON)) { shape = (Polygon) reader.read(geo.getWktText()); } else if (geo.getType().equals(GeometryType.MULITPOLYGON)) { shape = (MultiPolygon) reader.read(geo.getWktText()); } } catch (ParseException e) { log.log(Level.SEVERE, e.getMessage()); } Coordinate coord = new Coordinate(Double.parseDouble(lonStr), Double.parseDouble(latStr)); Point point = geometryFactory.createPoint(coord); Boolean containsFlag = false; if (shape != null) { // we iterate over the number of geometries in the shape, // as their might be disjoined items such as islands for (int i = 0; i < shape.getNumGeometries(); i++) { // this is where we check if the point actually lies in the geometry if (shape.getGeometryN(i).contains(point)) { containsFlag = true; } } if (containsFlag) { place = new GeoPlace(); countryCode = item.getCountryCode(); if (countryCode != null) { place.setCountryCode(countryCode); place.setCountryName(item.getName()); } place.setSub1(item.getSub1()); place.setSub2(item.getSub2()); place.setSub3(item.getSub3()); place.setSub4(item.getSub4()); place.setSub5(item.getSub5()); place.setSub6(item.getSub6()); log.log(Level.INFO, "Found point inside " + item.getCountryCode() + " " + item.toString()); } } } else { log.log(Level.INFO, item.getCountryCode() + " has a null geometry"); } } // if we are trying to find the subcountry, and can't find it // try the country if (place == null && type.equals(OGRFeature.FeatureType.SUB_COUNTRY_OTHER)) { place = manualLookup(latStr, lonStr, OGRFeature.FeatureType.COUNTRY); } // if we can't find the country, search on bounding box if (place == null) { place = primitiveLookup(latStr, lonStr); } return place; } public GeoPlace resolveSubCountry(String latStr, String lonStr, String countryCode) { GeoPlace place = null; if (countryCode != null && latStr != null && lonStr != null) { OGRFeatureDao ogrFeatureDao = new OGRFeatureDao(); List<OGRFeature> ogrList = ogrFeatureDao.listByExtentTypeCountry( Double.parseDouble(lonStr), Double.parseDouble(latStr), countryCode, "x1", "asc", "all"); for (OGRFeature item : ogrList) { Geometry geo = item.getGeometry(); GeometryFactory geometryFactory = new GeometryFactory(); WKTReader reader = new WKTReader(geometryFactory); com.vividsolutions.jts.geom.Geometry shape = null; if (geo != null && geo.getType() != null) { try { if (geo.getType().equals(GeometryType.POLYGON)) { shape = (Polygon) reader.read(geo.getWktText()); } else if (geo.getType().equals( GeometryType.MULITPOLYGON)) { shape = (MultiPolygon) reader .read(geo.getWktText()); } } catch (ParseException e) { log.log(Level.SEVERE, e.getMessage()); } Coordinate coord = new Coordinate( Double.parseDouble(lonStr), Double.parseDouble(latStr)); Point point = geometryFactory.createPoint(coord); if (shape != null && shape.contains(point)) { place = new GeoPlace(); countryCode = item.getCountryCode(); place.setCountryCode(countryCode); place.setCountryName(item.getName()); place.setSub1(item.getSub1()); place.setSub2(item.getSub2()); place.setSub3(item.getSub3()); place.setSub4(item.getSub4()); place.setSub5(item.getSub5()); place.setSub6(item.getSub6()); log.log(Level.INFO, "Found point inside " + item.getCountryCode() + " " + item.toString()); } } else { log.log(Level.INFO, item.getCountryCode() + " has a null geometry"); } } } return place; } /** * tries 4 different things to resolve the lat/lon to a geo place (as soon as a method returns a * non-null place, execution short-circuits: * <ul> * <li>Look in the shapefile for a sub_country feature</li> * <li>try to resolve the country using the statically configured bounding boxes</li> * <li>Look in the shapefiles for a country feature</li> * <li>invoke the geonames service to find a place</li> * </ul> */ @Override public GeoPlace findDetailedGeoPlace(String lat, String lon) { // first, check the shape files GeoPlace gp = manualLookup(lat, lon, OGRFeature.FeatureType.SUB_COUNTRY_OTHER); // if we don't find the sub_country, try the country // This is also already tried in the manualLookup method if (gp == null) { gp = manualLookup(lat, lon); } // if it's still null, then try calling the geonames service if (gp == null) { gp = findGeoPlace(lat, lon); } return gp; } }