001 package de.deepamehta.plugins.geomaps; 002 003 import de.deepamehta.plugins.geomaps.model.Geomap; 004 import de.deepamehta.plugins.geomaps.service.GeomapsService; 005 import de.deepamehta.plugins.topicmaps.service.TopicmapsService; 006 import de.deepamehta.plugins.facets.service.FacetsService; 007 008 import de.deepamehta.core.Association; 009 import de.deepamehta.core.AssociationDefinition; 010 import de.deepamehta.core.RelatedTopic; 011 import de.deepamehta.core.Topic; 012 import de.deepamehta.core.TopicType; 013 import de.deepamehta.core.model.AssociationModel; 014 import de.deepamehta.core.model.CompositeValueModel; 015 import de.deepamehta.core.model.TopicModel; 016 import de.deepamehta.core.model.TopicRoleModel; 017 import de.deepamehta.core.osgi.PluginActivator; 018 import de.deepamehta.core.service.ClientState; 019 import de.deepamehta.core.service.Directives; 020 import de.deepamehta.core.service.PluginService; 021 import de.deepamehta.core.service.ResultList; 022 import de.deepamehta.core.service.annotation.ConsumesService; 023 import de.deepamehta.core.service.event.PostCreateTopicListener; 024 import de.deepamehta.core.service.event.PostUpdateTopicListener; 025 import de.deepamehta.core.service.event.PreSendTopicListener; 026 import de.deepamehta.core.util.JavaUtils; 027 028 import org.codehaus.jettison.json.JSONObject; 029 030 import javax.ws.rs.GET; 031 import javax.ws.rs.PUT; 032 import javax.ws.rs.HeaderParam; 033 import javax.ws.rs.Path; 034 import javax.ws.rs.PathParam; 035 import javax.ws.rs.Produces; 036 import javax.ws.rs.Consumes; 037 038 import java.net.URL; 039 import java.util.List; 040 import java.util.logging.Level; 041 import java.util.logging.Logger; 042 043 044 045 @Path("/geomap") 046 @Consumes("application/json") 047 @Produces("application/json") 048 public class GeomapsPlugin extends PluginActivator implements GeomapsService, PostCreateTopicListener, 049 PostUpdateTopicListener, 050 PreSendTopicListener { 051 052 private static final String GEOCODER_URL = "http://maps.googleapis.com/maps/api/geocode/json?" + 053 "address=%s&sensor=false"; 054 055 // ---------------------------------------------------------------------------------------------- Instance Variables 056 057 private TopicmapsService topicmapsService; 058 private FacetsService facetsService; 059 060 private Logger logger = Logger.getLogger(getClass().getName()); 061 062 // -------------------------------------------------------------------------------------------------- Public Methods 063 064 065 066 // ************************************* 067 // *** GeomapsService Implementation *** 068 // ************************************* 069 070 071 072 @GET 073 @Path("/{id}") 074 @Override 075 public Geomap getGeomap(@PathParam("id") long geomapId) { 076 return new Geomap(geomapId, dms); 077 } 078 079 @GET 080 @Path("/topic/{id}") 081 @Override 082 public Topic getGeoTopic(@PathParam("id") long topicId) { 083 try { 084 Topic topic = dms.getTopic(topicId, true); 085 RelatedTopic parentTopic; 086 while ((parentTopic = topic.getRelatedTopic(null, "dm4.core.child", "dm4.core.parent", null, 087 true, false)) != null) { 088 topic = parentTopic; 089 } 090 return topic; 091 } catch (Exception e) { 092 throw new RuntimeException("Finding the geo coordinate's parent topic failed (topicId=" + topicId + ")", e); 093 } 094 } 095 096 @GET 097 @Path("/{id}/topics") 098 @Override 099 public ResultList<RelatedTopic> getGeomapTopics(@PathParam("id") long geomapId) { 100 return Geomap.fetchGeomapTopics(geomapId, dms); 101 } 102 103 @PUT 104 @Path("/{id}/topic/{topic_id}") 105 @Override 106 public void addTopicToGeomap(@PathParam("id") long geomapId, @PathParam("topic_id") long topicId) { 107 logger.info("### Adding topic " + topicId + " to geomap " + geomapId); 108 AssociationModel model = new AssociationModel("dm4.geomaps.geotopic_mapcontext", 109 new TopicRoleModel(geomapId, "dm4.core.default"), 110 new TopicRoleModel(topicId, "dm4.topicmaps.topicmap_topic") 111 ); 112 Association refAssoc = dms.createAssociation(model, null); // FIXME: clientState=null 113 // ### return refAssoc.getId(); 114 } 115 116 @PUT 117 @Path("/{id}/center/{lon}/{lat}/zoom/{zoom}") 118 @Override 119 public void setGeomapState(@PathParam("id") long geomapId, @PathParam("lon") double lon, 120 @PathParam("lat") double lat, @PathParam("zoom") int zoom) { 121 CompositeValueModel geomapState = new CompositeValueModel().put( 122 "dm4.topicmaps.state", new CompositeValueModel().put( 123 "dm4.topicmaps.translation", new CompositeValueModel().put( 124 "dm4.topicmaps.translation_x", lon).put( 125 "dm4.topicmaps.translation_y", lat)).put( 126 "dm4.topicmaps.zoom_level", zoom) 127 ); 128 dms.updateTopic(new TopicModel(geomapId, geomapState), null); 129 } 130 131 132 133 // **************************** 134 // *** Hook Implementations *** 135 // **************************** 136 137 138 139 @Override 140 public void init() { 141 topicmapsService.registerTopicmapRenderer(new GeomapRenderer()); 142 } 143 144 // --- 145 146 @Override 147 @ConsumesService({ 148 "de.deepamehta.plugins.topicmaps.service.TopicmapsService", 149 "de.deepamehta.plugins.facets.service.FacetsService" 150 }) 151 public void serviceArrived(PluginService service) { 152 if (service instanceof FacetsService) { 153 facetsService = (FacetsService) service; 154 } else if (service instanceof TopicmapsService) { 155 topicmapsService = (TopicmapsService) service; 156 } 157 } 158 159 @Override 160 public void serviceGone(PluginService service) { 161 if (service == facetsService) { 162 facetsService = null; 163 } else if (service == topicmapsService) { 164 topicmapsService = null; 165 } 166 } 167 168 169 170 // ******************************** 171 // *** Listener Implementations *** 172 // ******************************** 173 174 175 176 @Override 177 public void postCreateTopic(Topic topic, ClientState clientState, Directives directives) { 178 if (topic.getTypeUri().equals("dm4.contacts.address")) { 179 // 180 facetsService.addFacetTypeToTopic(topic.getId(), "dm4.geomaps.geo_coordinate_facet"); 181 // 182 Address address = new Address(topic.getCompositeValue().getModel()); 183 if (!address.isEmpty()) { 184 logger.info("### New " + address); 185 geocodeAndStoreFacet(address, topic, clientState, directives); 186 } else { 187 logger.info("### New empty address"); 188 } 189 } 190 } 191 192 @Override 193 public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel, ClientState clientState, 194 Directives directives) { 195 if (topic.getTypeUri().equals("dm4.contacts.address")) { 196 Address address = new Address(topic.getCompositeValue().getModel()); 197 Address oldAddress = new Address(oldModel.getCompositeValueModel()); 198 if (!address.equals(oldAddress)) { 199 logger.info("### Address changed:" + address.changeReport(oldAddress)); 200 geocodeAndStoreFacet(address, topic, clientState, directives); 201 } else { 202 logger.info("### Address not changed"); 203 } 204 } 205 } 206 207 // --- 208 209 /** 210 * Enriches an Address topic with its Geo Coordinate facet. 211 */ 212 @Override 213 public void preSendTopic(Topic topic, ClientState clientState) { 214 TopicModel address = findAddress(topic); 215 if (address == null) { 216 return; 217 } 218 // 219 Topic geoFacet = facetsService.getFacet(address.getId(), "dm4.geomaps.geo_coordinate_facet"); 220 if (geoFacet != null) { 221 logger.info("### Enriching address " + address.getId() + " with its geo facet"); 222 address.getCompositeValueModel().put("dm4.geomaps.geo_coordinate", geoFacet.getModel()); 223 } else { 224 logger.info("### Enriching address " + address.getId() + " with its geo facet ABORTED " + 225 "-- no geo facet in DB"); 226 } 227 } 228 229 230 231 // ------------------------------------------------------------------------------------------------- Private Methods 232 233 private void geocodeAndStoreFacet(Address address, Topic topic, ClientState clientState, Directives directives) { 234 try { 235 LonLat geoCoordinate = address.geocode(); 236 storeGeoFacet(topic, geoCoordinate, clientState, directives); 237 } catch (Exception e) { 238 // ### TODO: show to the user? 239 logger.log(Level.WARNING, "Adding geo facet to " + address + " failed", e); 240 } 241 } 242 243 /** 244 * Stores a geo facet for an address topic in the DB. 245 */ 246 private void storeGeoFacet(Topic address, LonLat geoCoordinate, ClientState clientState, Directives directives) { 247 try { 248 logger.info("Storing geo facet (" + geoCoordinate + ") of address " + address); 249 TopicModel geoFacet = new TopicModel("dm4.geomaps.geo_coordinate", new CompositeValueModel() 250 .put("dm4.geomaps.longitude", geoCoordinate.lon) 251 .put("dm4.geomaps.latitude", geoCoordinate.lat) 252 ); 253 facetsService.updateFacet(address, "dm4.geomaps.geo_coordinate_facet", geoFacet, clientState, directives); 254 } catch (Exception e) { 255 throw new RuntimeException("Storing geo facet of address " + address.getId() + " failed", e); 256 } 257 } 258 259 // --- 260 261 private TopicModel findAddress(Topic topic) { 262 return findChildTopic(topic.getModel(), "dm4.contacts.address"); 263 } 264 265 /** 266 * Searches a topic's composite value for a topic of a given type. 267 * The search is driven by the topic's type definition. In other words, composite value entries which do not 268 * adhere to the topic's type definition are not found. 269 * Note: this is an in-memory search; the DB is not accessed. 270 * <p> 271 * The first topic found is returned, according to a depth-first search. 272 * For multiple-value fields only the first topic is returned. 273 * <p> 274 * TODO: make this a generally available method by adding it to the Topic interface? 275 */ 276 private TopicModel findChildTopic(TopicModel topic, String topicTypeUri) { 277 String typeUri = topic.getTypeUri(); 278 if (typeUri.equals(topicTypeUri)) { 279 return topic; 280 } 281 // 282 CompositeValueModel comp = topic.getCompositeValueModel(); 283 TopicType topicType = dms.getTopicType(typeUri); 284 for (AssociationDefinition assocDef : topicType.getAssocDefs()) { 285 String childTypeUri = assocDef.getChildTypeUri(); 286 String cardinalityUri = assocDef.getChildCardinalityUri(); 287 TopicModel childTopic = null; 288 if (cardinalityUri.equals("dm4.core.one")) { 289 childTopic = comp.getTopic(childTypeUri, null); 290 } else if (cardinalityUri.equals("dm4.core.many")) { 291 List<TopicModel> childTopics = comp.getTopics(childTypeUri, null); 292 if (childTopics != null && !childTopics.isEmpty()) { 293 childTopic = childTopics.get(0); 294 } 295 } else { 296 throw new RuntimeException("\"" + cardinalityUri + "\" is an unexpected cardinality URI"); 297 } 298 // Note: topics just created have no child topics yet 299 if (childTopic == null) { 300 continue; 301 } 302 // recursion 303 childTopic = findChildTopic(childTopic, topicTypeUri); 304 if (childTopic != null) { 305 return childTopic; 306 } 307 } 308 return null; 309 } 310 311 // ------------------------------------------------------------------------------------------------- Private Classes 312 313 private class Address { 314 315 String street, postalCode, city, country; 316 317 // --- 318 319 Address(CompositeValueModel address) { 320 // Note: some Address child topics might be deleted (resp. do not exist), so we use "" 321 // as defaults here. Otherwise "Invalid access to CompositeValueModel" would be thrown. 322 street = address.getString("dm4.contacts.street", ""); 323 postalCode = address.getString("dm4.contacts.postal_code", ""); 324 city = address.getString("dm4.contacts.city", ""); 325 country = address.getString("dm4.contacts.country", ""); 326 } 327 328 // --- 329 330 LonLat geocode() { 331 URL url = null; 332 try { 333 // perform request 334 String address = street + ", " + postalCode + " " + city + ", " + country; 335 url = new URL(String.format(GEOCODER_URL, JavaUtils.encodeURIComponent(address))); 336 logger.info("### Geocoding \"" + address + "\"\n url=\"" + url + "\""); 337 JSONObject response = new JSONObject(JavaUtils.readTextURL(url)); 338 // check response status 339 String status = response.getString("status"); 340 if (!status.equals("OK")) { 341 throw new RuntimeException(status); 342 } 343 // parse response 344 JSONObject location = response.getJSONArray("results").getJSONObject(0).getJSONObject("geometry") 345 .getJSONObject("location"); 346 double lng = location.getDouble("lng"); 347 double lat = location.getDouble("lat"); 348 // create result 349 LonLat geoCoordinate = new LonLat(lng, lat); 350 logger.info("=> " + geoCoordinate); 351 return geoCoordinate; 352 } catch (Exception e) { 353 throw new RuntimeException("Geocoding failed (url=\"" + url + "\")", e); 354 } 355 } 356 357 boolean isEmpty() { 358 return street.equals("") && postalCode.equals("") && city.equals("") && country.equals(""); 359 } 360 361 String changeReport(Address oldAddr) { 362 StringBuilder report = new StringBuilder(); 363 if (!street.equals(oldAddr.street)) { 364 report.append("\n Street: \"" + oldAddr.street + "\" -> \"" + street + "\""); 365 } 366 if (!postalCode.equals(oldAddr.postalCode)) { 367 report.append("\n Postal Code: \"" + oldAddr.postalCode + "\" -> \"" + postalCode + "\""); 368 } 369 if (!city.equals(oldAddr.city)) { 370 report.append("\n City: \"" + oldAddr.city + "\" -> \"" + city + "\""); 371 } 372 if (!country.equals(oldAddr.country)) { 373 report.append("\n Country: \"" + oldAddr.country + "\" -> \"" + country + "\""); 374 } 375 return report.toString(); 376 } 377 378 // === Java API === 379 380 @Override 381 public boolean equals(Object o) { 382 if (o instanceof Address) { 383 Address addr = (Address) o; 384 return street.equals(addr.street) && postalCode.equals(addr.postalCode) && 385 city.equals(addr.city) && country.equals(addr.country); 386 } 387 return false; 388 } 389 390 @Override 391 public int hashCode() { 392 return (street + postalCode + city + country).hashCode(); 393 } 394 395 @Override 396 public String toString() { 397 return "address (street=\"" + street + "\", postalCode=\"" + postalCode + 398 "\", city=\"" + city + "\", country=\"" + country + "\")"; 399 } 400 } 401 402 private class LonLat { 403 404 double lon, lat; 405 406 LonLat(double lon, double lat) { 407 this.lon = lon; 408 this.lat = lat; 409 } 410 411 public String toString() { 412 return "long=" + lon + ", lat=" + lat; 413 } 414 } 415 }