001package de.deepamehta.geomaps; 002 003import de.deepamehta.geomaps.model.GeoCoordinate; 004import de.deepamehta.geomaps.model.Geomap; 005import de.deepamehta.topicmaps.TopicmapsService; 006import de.deepamehta.facets.FacetsService; 007 008import de.deepamehta.core.Association; 009import de.deepamehta.core.AssociationDefinition; 010import de.deepamehta.core.ChildTopics; 011import de.deepamehta.core.RelatedTopic; 012import de.deepamehta.core.Topic; 013import de.deepamehta.core.TopicType; 014import de.deepamehta.core.model.AssociationModel; 015import de.deepamehta.core.model.ChildTopicsModel; 016import de.deepamehta.core.model.TopicModel; 017import de.deepamehta.core.osgi.PluginActivator; 018import de.deepamehta.core.service.Cookies; 019import de.deepamehta.core.service.Inject; 020import de.deepamehta.core.service.Transactional; 021import de.deepamehta.core.service.event.PostCreateTopicListener; 022import de.deepamehta.core.service.event.PostUpdateTopicListener; 023import de.deepamehta.core.service.event.PreSendTopicListener; 024import de.deepamehta.core.util.JavaUtils; 025 026import org.codehaus.jettison.json.JSONObject; 027 028import javax.ws.rs.GET; 029import javax.ws.rs.PUT; 030import javax.ws.rs.HeaderParam; 031import javax.ws.rs.Path; 032import javax.ws.rs.PathParam; 033import javax.ws.rs.QueryParam; 034import javax.ws.rs.Produces; 035import javax.ws.rs.Consumes; 036 037import java.net.URL; 038import java.util.List; 039import java.util.logging.Level; 040import java.util.logging.Logger; 041 042 043 044@Path("/geomap") 045@Consumes("application/json") 046@Produces("application/json") 047public class GeomapsPlugin extends PluginActivator implements GeomapsService, PostCreateTopicListener, 048 PostUpdateTopicListener, 049 PreSendTopicListener { 050 051 private static final String GEOCODER_URL = "http://maps.googleapis.com/maps/api/geocode/json?" + 052 "address=%s&sensor=false"; 053 054 private static final String COOKIE_NO_GEOCODING = "dm4_no_geocoding"; 055 056 private static final double EARTH_RADIUS_KM = 6371.009; 057 058 // ---------------------------------------------------------------------------------------------- Instance Variables 059 060 @Inject private TopicmapsService topicmapsService; 061 @Inject private FacetsService facetsService; 062 063 private Logger logger = Logger.getLogger(getClass().getName()); 064 065 // -------------------------------------------------------------------------------------------------- Public Methods 066 067 068 069 // ************************************* 070 // *** GeomapsService Implementation *** 071 // ************************************* 072 073 074 075 @GET 076 @Path("/{id}") 077 @Override 078 public Geomap getGeomap(@PathParam("id") long geomapId) { 079 return new Geomap(geomapId, dm4); 080 } 081 082 // Note: the "include_childs" query paramter is handled by the core's JerseyResponseFilter 083 @GET 084 @Path("/topic/{id}") 085 @Override 086 public Topic getDomainTopic(@PathParam("id") long geoCoordId) { 087 try { 088 Topic topic = dm4.getTopic(geoCoordId); 089 RelatedTopic parentTopic; 090 while ((parentTopic = topic.getRelatedTopic(null, "dm4.core.child", "dm4.core.parent", null)) != null) { 091 topic = parentTopic; 092 } 093 return topic; 094 } catch (Exception e) { 095 throw new RuntimeException("Finding the geo coordinate's domain topic failed (geoCoordId=" + 096 geoCoordId + ")", e); 097 } 098 } 099 100 @Override 101 public GeoCoordinate getGeoCoordinate(Topic geoTopic) { 102 try { 103 Topic geoCoordTopic = getGeoCoordinateTopic(geoTopic); 104 if (geoCoordTopic != null) { 105 return geoCoordinate(geoCoordTopic); 106 } else { 107 return null; 108 } 109 } catch (Exception e) { 110 throw new RuntimeException("Getting the geo coordinate failed (geoTopic=" + geoTopic + ")", e); 111 } 112 } 113 114 @Override 115 public GeoCoordinate geoCoordinate(Topic geoCoordTopic) { 116 ChildTopics childTopics = geoCoordTopic.getChildTopics(); 117 return new GeoCoordinate( 118 childTopics.getDouble("dm4.geomaps.longitude"), 119 childTopics.getDouble("dm4.geomaps.latitude") 120 ); 121 } 122 123 @PUT 124 @Path("/{id}/topic/{geo_coord_id}") 125 @Transactional 126 @Override 127 public void addCoordinateToGeomap(@PathParam("id") long geomapId, @PathParam("geo_coord_id") long geoCoordId) { 128 logger.info("### Adding geo coordinate topic " + geoCoordId + " to geomap " + geomapId); 129 AssociationModel model = mf.newAssociationModel("dm4.geomaps.geotopic_mapcontext", 130 mf.newTopicRoleModel(geomapId, "dm4.core.default"), 131 mf.newTopicRoleModel(geoCoordId, "dm4.topicmaps.topicmap_topic") 132 ); 133 dm4.createAssociation(model); 134 } 135 136 @PUT 137 @Path("/{id}/center/{lon}/{lat}/zoom/{zoom}") 138 @Transactional 139 @Override 140 public void setGeomapState(@PathParam("id") long geomapId, @PathParam("lon") double lon, 141 @PathParam("lat") double lat, @PathParam("zoom") int zoom) { 142 ChildTopicsModel geomapState = mf.newChildTopicsModel().put( 143 "dm4.topicmaps.state", mf.newChildTopicsModel().put( 144 "dm4.topicmaps.translation", mf.newChildTopicsModel().put( 145 "dm4.topicmaps.translation_x", lon).put( 146 "dm4.topicmaps.translation_y", lat)).put( 147 "dm4.topicmaps.zoom_level", zoom) 148 ); 149 dm4.updateTopic(mf.newTopicModel(geomapId, geomapState)); 150 } 151 152 @GET 153 @Path("/distance") 154 @Override 155 public double getDistance(@QueryParam("coord1") GeoCoordinate coord1, 156 @QueryParam("coord2") GeoCoordinate coord2) { 157 // calculate distance by the flat-surface formula for a "Spherical Earth projected to a plane" 158 // http://en.wikipedia.org/wiki/Geographical_distance#Flat-surface_formulae 159 double lonDiff = Math.toRadians(coord2.lon - coord1.lon); 160 double latDiff = Math.toRadians(coord2.lat - coord1.lat); 161 double latMean = Math.toRadians((coord1.lat + coord2.lat) / 2); 162 return EARTH_RADIUS_KM * Math.sqrt(Math.pow(latDiff, 2) + Math.pow(Math.cos(latMean) * lonDiff, 2)); 163 } 164 165 166 167 // **************************** 168 // *** Hook Implementations *** 169 // **************************** 170 171 172 173 @Override 174 public void init() { 175 topicmapsService.registerTopicmapRenderer(new GeomapRenderer()); 176 } 177 178 179 180 // ******************************** 181 // *** Listener Implementations *** 182 // ******************************** 183 184 185 186 @Override 187 public void postCreateTopic(Topic topic) { 188 if (topic.getTypeUri().equals("dm4.contacts.address")) { 189 if (!abortGeocoding()) { 190 // 191 facetsService.addFacetTypeToTopic(topic.getId(), "dm4.geomaps.geo_coordinate_facet"); 192 // 193 Address address = new Address(topic.getChildTopics().getModel()); 194 if (!address.isEmpty()) { 195 logger.info("### New " + address); 196 geocodeAndStoreFacet(address, topic); 197 } else { 198 logger.info("### New empty address"); 199 } 200 } 201 } 202 } 203 204 @Override 205 public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel) { 206 if (topic.getTypeUri().equals("dm4.contacts.address")) { 207 if (!abortGeocoding()) { 208 Address address = new Address(topic.getChildTopics().getModel()); 209 Address oldAddress = new Address(oldModel.getChildTopicsModel()); 210 if (!address.equals(oldAddress)) { 211 logger.info("### Address changed:" + address.changeReport(oldAddress)); 212 geocodeAndStoreFacet(address, topic); 213 } else { 214 logger.info("### Address not changed"); 215 } 216 } 217 } 218 } 219 220 // --- 221 222 /** 223 * Enriches an Address topic with its geo coordinate. 224 */ 225 @Override 226 public void preSendTopic(Topic topic) { 227 Topic address = topic.findChildTopic("dm4.contacts.address"); 228 if (address != null) { 229 String operation = "### Enriching address " + address.getId() + " with its geo coordinate"; 230 Topic geoCoordTopic = getGeoCoordinateTopic(address); 231 if (geoCoordTopic != null) { 232 logger.info(operation); 233 address.getChildTopics().getModel().put("dm4.geomaps.geo_coordinate", geoCoordTopic.getModel()); 234 } else { 235 logger.info(operation + " ABORTED -- no geo coordinate in DB"); 236 } 237 } 238 } 239 240 241 242 // ------------------------------------------------------------------------------------------------- Private Methods 243 244 /** 245 * Returns the Geo Coordinate topic (including its child topics) of a geo-facetted topic (e.g. an Address), 246 * or <code>null</code> if no geo coordinate is stored. 247 */ 248 private Topic getGeoCoordinateTopic(Topic geoTopic) { 249 Topic geoCoordTopic = facetsService.getFacet(geoTopic, "dm4.geomaps.geo_coordinate_facet"); 250 return geoCoordTopic != null ? geoCoordTopic.loadChildTopics() : null; 251 } 252 253 // --- 254 255 /** 256 * Geocodes the given address and stores the resulting coordinate as a facet value of the given Address topic. 257 * If geocoding (or storing the coordinate) fails a warning is logged; no exception is thrown. 258 * 259 * @param topic the Address topic to be facetted. 260 */ 261 private void geocodeAndStoreFacet(Address address, Topic topic) { 262 try { 263 GeoCoordinate geoCoord = address.geocode(); 264 storeGeoCoordinate(topic, geoCoord); 265 } catch (Exception e) { 266 // ### TODO: show to the user? 267 logger.log(Level.WARNING, "Adding geo coordinate to " + address + " failed", e); 268 } 269 } 270 271 /** 272 * Stores a geo coordinate for an address topic in the DB. 273 */ 274 private void storeGeoCoordinate(Topic address, GeoCoordinate geoCoord) { 275 try { 276 logger.info("Storing geo coordinate (" + geoCoord + ") of address " + address); 277 facetsService.updateFacet(address, "dm4.geomaps.geo_coordinate_facet", 278 mf.newFacetValueModel("dm4.geomaps.geo_coordinate") 279 .put(mf.newChildTopicsModel() 280 .put("dm4.geomaps.longitude", geoCoord.lon) 281 .put("dm4.geomaps.latitude", geoCoord.lat) 282 ) 283 ); 284 } catch (Exception e) { 285 throw new RuntimeException("Storing geo coordinate of address " + address.getId() + " failed", e); 286 } 287 } 288 289 // --- 290 291 private boolean abortGeocoding() { 292 Cookies cookies = Cookies.get(); 293 if (!cookies.has(COOKIE_NO_GEOCODING)) { 294 return false; 295 } 296 String value = cookies.get(COOKIE_NO_GEOCODING); 297 if (!value.equals("false") && !value.equals("true")) { 298 throw new RuntimeException("\"" + value + "\" is an unexpected value for the \"" + COOKIE_NO_GEOCODING + 299 "\" cookie (expected are \"false\" or \"true\")"); 300 } 301 boolean abort = value.equals("true"); 302 if (abort) { 303 logger.info("### Geocoding ABORTED -- \"" + COOKIE_NO_GEOCODING + "\" cookie detected"); 304 } 305 return abort; 306 } 307 308 // ------------------------------------------------------------------------------------------------- Private Classes 309 310 private class Address { 311 312 String street, postalCode, city, country; 313 314 // --- 315 316 Address(ChildTopicsModel address) { 317 // Note: some Address child topics might be deleted (resp. do not exist), so we use "" 318 // as defaults here. Otherwise "Invalid access to ChildTopicsModel" would be thrown. 319 street = address.getString("dm4.contacts.street", ""); 320 postalCode = address.getString("dm4.contacts.postal_code", ""); 321 city = address.getString("dm4.contacts.city", ""); 322 country = address.getString("dm4.contacts.country", ""); 323 } 324 325 // --- 326 327 GeoCoordinate geocode() { 328 URL url = null; 329 try { 330 // perform request 331 String address = street + ", " + postalCode + " " + city + ", " + country; 332 url = new URL(String.format(GEOCODER_URL, JavaUtils.encodeURIComponent(address))); 333 logger.info("### Geocoding \"" + address + "\"\n url=\"" + url + "\""); 334 JSONObject response = new JSONObject(JavaUtils.readTextURL(url)); 335 // check response status 336 String status = response.getString("status"); 337 if (!status.equals("OK")) { 338 throw new RuntimeException(status); 339 } 340 // parse response 341 JSONObject location = response.getJSONArray("results").getJSONObject(0).getJSONObject("geometry") 342 .getJSONObject("location"); 343 double lng = location.getDouble("lng"); 344 double lat = location.getDouble("lat"); 345 // create result 346 GeoCoordinate geoCoord = new GeoCoordinate(lng, lat); 347 logger.info("=> " + geoCoord); 348 return geoCoord; 349 } catch (Exception e) { 350 throw new RuntimeException("Geocoding failed (url=\"" + url + "\")", e); 351 } 352 } 353 354 boolean isEmpty() { 355 return street.equals("") && postalCode.equals("") && city.equals("") && country.equals(""); 356 } 357 358 String changeReport(Address oldAddr) { 359 StringBuilder report = new StringBuilder(); 360 if (!street.equals(oldAddr.street)) { 361 report.append("\n Street: \"" + oldAddr.street + "\" -> \"" + street + "\""); 362 } 363 if (!postalCode.equals(oldAddr.postalCode)) { 364 report.append("\n Postal Code: \"" + oldAddr.postalCode + "\" -> \"" + postalCode + "\""); 365 } 366 if (!city.equals(oldAddr.city)) { 367 report.append("\n City: \"" + oldAddr.city + "\" -> \"" + city + "\""); 368 } 369 if (!country.equals(oldAddr.country)) { 370 report.append("\n Country: \"" + oldAddr.country + "\" -> \"" + country + "\""); 371 } 372 return report.toString(); 373 } 374 375 // === Java API === 376 377 @Override 378 public boolean equals(Object o) { 379 if (o instanceof Address) { 380 Address addr = (Address) o; 381 return street.equals(addr.street) && postalCode.equals(addr.postalCode) && 382 city.equals(addr.city) && country.equals(addr.country); 383 } 384 return false; 385 } 386 387 @Override 388 public int hashCode() { 389 return (street + postalCode + city + country).hashCode(); 390 } 391 392 @Override 393 public String toString() { 394 return "address (street=\"" + street + "\", postalCode=\"" + postalCode + 395 "\", city=\"" + city + "\", country=\"" + country + "\")"; 396 } 397 } 398}