/**
* Copyright (C) 2010-2014 Morgner UG (haftungsbeschränkt)
*
* This file is part of Structr <http://structr.org>.
*
* Structr 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 3 of the
* License, or (at your option) any later version.
*
* Structr 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 Structr. If not, see <http://www.gnu.org/licenses/>.
*/
package org.structr.common.geo;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.structr.common.geo.GeoCodingResult.Type;
/**
*
* @author Christian Morgner
*/
public class BingGeoCodingProvider extends AbstractGeoCodingProvider {
private static final Logger logger = Logger.getLogger(BingGeoCodingProvider.class.getName());
@Override
public GeoCodingResult geocode(String street, String house, String postalCode, String city, String state, String country, String language) throws IOException {
if (apiKey != null && !apiKey.isEmpty()) {
StringBuilder urlBuffer = new StringBuilder("http://dev.virtualearth.net/REST/v1/Locations");
// api key
urlBuffer.append("?key=").append(apiKey);
// culture for language-specific formatting
urlBuffer.append("&c=").append(language);
// output format: XML
urlBuffer.append("&o=xml");
// address line string
urlBuffer.append("&query=");
// house
if (house != null && !house.isEmpty()) {
urlBuffer.append(encodeURL(house)).append("+");
}
// street
if (street != null && !street.isEmpty()) {
urlBuffer.append(encodeURL(street)).append("+");
}
// city
if (city != null && !city.isEmpty()) {
urlBuffer.append(encodeURL(city)).append("+");
}
// postalCode
if (postalCode != null && !postalCode.isEmpty()) {
urlBuffer.append("&postalCode=").append(encodeURL(postalCode));
}
/* disabled because the ISO country code is required here which we don't have
// countryRegion
if (country != null && !country.isEmpty()) {
urlBuffer.append("&countryRegion=").append(encodeURL(country));
}
*/
// max results
urlBuffer.append("&maxResults=1");
String url = urlBuffer.toString();
try {
logger.log(Level.INFO, "Using url {0}", url);
URL mapsUrl = new URL(urlBuffer.toString());
HttpURLConnection connection = (HttpURLConnection) mapsUrl.openConnection();
connection.connect();
Reader reader = new InputStreamReader(connection.getInputStream());
SAXReader saxReader = new SAXReader();
// skip leading 0xFEFF character if present
if (reader.read() != 65279) {
reader.reset();
}
Document xmlDoc = saxReader.read(reader);
connection.disconnect();
reader.close();
if (xmlDoc != null) {
Map<String, String> data = new LinkedHashMap<>();
Element root = xmlDoc.getRootElement();
try { data.put("lat", root.element("ResourceSets").element("ResourceSet").element("Resources").element("Location").element("Point").element("Latitude").getTextTrim()); } catch (Throwable t) {}
try { data.put("lon", root.element("ResourceSets").element("ResourceSet").element("Resources").element("Location").element("Point").element("Longitude").getTextTrim()); } catch (Throwable t) {}
try { data.put("postalCode", root.element("ResourceSets").element("ResourceSet").element("Resources").element("Location").element("Address").element("PostalCode").getTextTrim()); } catch (Throwable t) {}
try { data.put("adminDistrict", root.element("ResourceSets").element("ResourceSet").element("Resources").element("Location").element("Address").element("AdminDistrict").getTextTrim()); } catch (Throwable t) {}
try { data.put("adminDistrict2", root.element("ResourceSets").element("ResourceSet").element("Resources").element("Location").element("Address").element("AdminDistrict2").getTextTrim()); } catch (Throwable t) {}
try { data.put("locality", root.element("ResourceSets").element("ResourceSet").element("Resources").element("Location").element("Address").element("Locality").getTextTrim()); } catch (Throwable t) {}
try { data.put("countryRegion", root.element("ResourceSets").element("ResourceSet").element("Resources").element("Location").element("Address").element("CountryRegion").getTextTrim()); } catch (Throwable t) {}
if (data.containsKey("lat") && data.containsKey("lon")) {
String address =
(StringUtils.isNotBlank(street) ? street : "") + " " +
(StringUtils.isNotBlank(house) ? house : "") + " " +
(StringUtils.isNotBlank(postalCode) ? postalCode : "" +
(StringUtils.isNotBlank(city) ? city : "") + " " +
(StringUtils.isNotBlank(state) ? state : "") + " " +
(StringUtils.isNotBlank(country) ? country : "") + " "
);
return new BingGeoCodingResult(address, data);
} else {
logger.log(Level.WARNING, "Geocoding result did not contain location information:\n{0}", xmlDoc.asXML());
}
}
} catch (DocumentException dex) {
logger.log(Level.WARNING, "Unable to use Bing geocoding provider: {0}", dex.getMessage());
// maybe not a permanent error => wrap in IOException so the request is retried later
throw new IOException(dex);
}
} else {
logger.log(Level.WARNING, "Unable to use Bing geocoding provider, missing API key. Please supply API key in structr.conf using the key geocoding.apikey.");
}
return null;
}
private static class BingGeoCodingResult implements GeoCodingResult {
private List<AddressComponent> addressComponents = new LinkedList<>();
private Double latitude = null;
private Double longitude = null;
private String address = null;
public BingGeoCodingResult(String address, Map<String, String> data) {
this.address = address;
this.latitude = Double.parseDouble(data.get("lat"));
this.longitude = Double.parseDouble(data.get("lon"));
this.addressComponents.add(new BingAddressComponent(data.get("postalCode"), Type.postal_code));
this.addressComponents.add(new BingAddressComponent(data.get("adminDistrict"), Type.administrative_area_level_1));
this.addressComponents.add(new BingAddressComponent(data.get("adminDistrict2"), Type.administrative_area_level_3));
this.addressComponents.add(new BingAddressComponent(data.get("countryRegion"), Type.country));
this.addressComponents.add(new BingAddressComponent(data.get("locality"), Type.locality));
}
@Override
public String getAddress() {
return address;
}
@Override
public AddressComponent getAddressComponent(Type... types) {
for(AddressComponent addressComponent : addressComponents) {
if(addressComponent.getTypes().containsAll(Arrays.asList(types))) {
return addressComponent;
}
}
return null;
}
@Override
public List<AddressComponent> getAddressComponents() {
return addressComponents;
}
@Override
public double getLatitude() {
return latitude;
}
@Override
public double getLongitude() {
return longitude;
}
@Override
public void setAddress(String address) {
this.address = address;
}
@Override
public void setLatitude(double latitude) {
this.latitude = latitude;
}
@Override
public void setLongitude(double longitude) {
this.longitude = longitude;
}
@Override
public Double[] toArray() {
return new Double[]{ latitude, longitude };
}
}
private static class BingAddressComponent implements AddressComponent {
Set<Type> types = new LinkedHashSet<Type>();
String longValue = null;
String shortValue = null;
public BingAddressComponent(String value, Type... types) {
this.types.addAll(Arrays.asList(types));
this.longValue = value;
this.shortValue = value;
}
@Override
public String getLongValue() {
return longValue;
}
@Override
public String getShortValue() {
return shortValue;
}
@Override
public Set<Type> getTypes() {
return types;
}
}
}