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