001package de.kiezatlas;
002
003import de.deepamehta.core.Association;
004import de.deepamehta.plugins.accesscontrol.AccessControlService;
005import de.deepamehta.plugins.geomaps.GeomapsService;
006import de.deepamehta.plugins.facets.model.FacetValue;
007import de.deepamehta.plugins.facets.FacetsService;
008
009import de.deepamehta.core.AssociationDefinition;
010import de.deepamehta.core.RelatedTopic;
011import de.deepamehta.core.Topic;
012import de.deepamehta.core.model.AssociationModel;
013import de.deepamehta.core.model.ChildTopicsModel;
014import de.deepamehta.core.model.RelatedTopicModel;
015import de.deepamehta.core.model.TopicModel;
016import de.deepamehta.core.model.SimpleValue;
017import de.deepamehta.core.model.TopicRoleModel;
018import de.deepamehta.core.osgi.PluginActivator;
019import de.deepamehta.core.service.Cookies;
020import de.deepamehta.core.service.Inject;
021import de.deepamehta.core.service.ResultList;
022import de.deepamehta.core.service.Transactional;
023import de.deepamehta.core.service.event.PostUpdateTopicListener;
024import de.deepamehta.core.service.event.PreSendTopicListener;
025import de.deepamehta.core.util.DeepaMehtaUtils;
026import de.deepamehta.plugins.geomaps.model.GeoCoordinate;
027import de.deepamehta.plugins.time.TimeService;
028import de.deepamehta.plugins.workspaces.WorkspacesService;
029import java.io.InputStream;
030
031import javax.ws.rs.*;
032
033import java.util.ArrayList;
034import java.util.Iterator;
035import java.util.List;
036import java.util.logging.Logger;
037import javax.ws.rs.core.MediaType;
038import javax.ws.rs.core.Response;
039
040
041/**
042 * Kiezatlas 2 Plugin Service Implementation.
043 * @author jri & mukil
044 */
045@Path("/site")
046@Consumes("application/json")
047@Produces("application/json")
048public class KiezatlasPlugin extends PluginActivator implements KiezatlasService, PostUpdateTopicListener,
049                                                                                  PreSendTopicListener {
050
051    // ------------------------------------------------------------------------------------------------------- Constants
052
053    // The URIs of KA2 Geo Object topics have this prefix.
054    // The remaining part of the URI is the original KA1 topic id.
055    private static final String KA2_GEO_OBJECT_URI_PREFIX = "de.kiezatlas.topic.";
056    private static final String GEO_OBJECT_OWNER_PROPERTY = "de.kiezatlas.owner";
057    private static final String GEO_OBJECT_KEYWORD_PROPERTY = "de.kiezatlas.key.";
058
059    // Website-Geomap association
060    private static final String WEBSITE_GEOMAP = "dm4.core.association";
061    private static final String ROLE_TYPE_WEBSITE = "dm4.core.default";     // Note: used for both associations
062    private static final String ROLE_TYPE_GEOMAP = "dm4.core.default";
063    // Website-Facet Types association
064    private static final String WEBSITE_FACET_TYPES = "dm4.core.association";
065    private static final String ROLE_TYPE_FACET_TYPE = "dm4.core.default";
066
067    // ---------------------------------------------------------------------------------------------- Instance Variables
068
069    @Inject private GeomapsService geomapsService;
070    @Inject private FacetsService facetsService;
071    @Inject private TimeService timeService;
072    @Inject private AccessControlService accessControlService;
073    @Inject private WorkspacesService workspaceService;
074
075    private Logger logger = Logger.getLogger(getClass().getName());
076
077    // -------------------------------------------------------------------------------------------------- Public Methods
078
079
080
081    /**
082     * Fetches a simple page rendering objects for kiez-administrators.
083     * @return
084     */
085    @GET
086    @Path("/administration")
087    @Produces(MediaType.TEXT_HTML)
088    public InputStream getKiezatlasAdministrationPage() {
089        String username = accessControlService.getUsername();
090        if (username != null && !username.equals("admin")) throw new WebApplicationException(Response.Status.UNAUTHORIZED);
091        return getStaticResource("web/kiez-administration.html");
092    }
093
094
095
096    // **********************
097    // *** Plugin Service ***
098    // **********************
099
100    @POST
101    @Path("/create/{siteName}/{siteUri}")
102    @Transactional
103    @Override
104    public Topic createWebsite(@PathParam("siteName") String siteName, @PathParam("siteUri") String siteUri) {
105        Topic websiteTopic = dms.getTopic("uri", new SimpleValue(siteUri));
106        if (websiteTopic == null) {
107            logger.info("Creating Kiezatlas Website \"" + siteName + " with siteUri=\"" + siteUri + "\"");
108            websiteTopic = dms.createTopic(new TopicModel(siteUri, WEBSITE, new ChildTopicsModel()
109                .put("ka2.website.title", siteName)));
110        } else {
111            logger.info("Kiezatlas Website with siteUri=\"" + siteUri + "\" already exists");
112        }
113        return websiteTopic;
114    }
115
116    @POST
117    @Path("/add/{geoObjectId}/{siteId}")
118    @Transactional
119    @Override
120    public Association addGeoObjectToWebsite(@PathParam("geoObjectId") long geoObjectId, @PathParam("siteId") long siteId) {
121        Topic geoObject = dms.getTopic(geoObjectId);
122        Association relation = null;
123        if (!hasSiteAssociation(geoObject, siteId)) {
124            logger.info("Adding Geo Object \"" + geoObject.getSimpleValue() + "\" to Site Topic: " + siteId);
125            relation = dms.createAssociation(new AssociationModel("dm4.core.association",
126                new TopicRoleModel(geoObjectId, "dm4.core.default"), new TopicRoleModel(siteId, "dm4.core.default")));
127        } else {
128            logger.info("Skipping adding Topic to Site, Association already EXISTS");
129        }
130        return relation;
131    }
132
133    @GET
134    @Path("/geomap/{geomap_id}")
135    @Override
136    public Topic getWebsite(@PathParam("geomap_id") long geomapId) {
137        try {
138            return dms.getTopic(geomapId).getRelatedTopic(WEBSITE_GEOMAP, ROLE_TYPE_WEBSITE, ROLE_TYPE_GEOMAP,
139                WEBSITE);
140        } catch (Exception e) {
141            throw new RuntimeException("Finding the geomap's website topic failed (geomapId=" + geomapId + ")", e);
142        }
143    }
144
145    @GET
146    @Path("/{website_id}/facets")
147    @Override
148    public ResultList<RelatedTopic> getFacetTypes(@PathParam("website_id") long websiteId) {
149        try {
150            return dms.getTopic(websiteId).getRelatedTopics(WEBSITE_FACET_TYPES, ROLE_TYPE_WEBSITE,
151                ROLE_TYPE_FACET_TYPE, "dm4.core.topic_type", 0);
152        } catch (Exception e) {
153            throw new RuntimeException("Finding the website's facet types failed (websiteId=" + websiteId + ")", e);
154        }
155    }
156
157    @GET
158    @Path("/criteria")
159    @Override
160    public List<Topic> getAllCriteria() {
161        List<Topic> criteria = dms.getTopics("uri", new SimpleValue("ka2.criteria.*"));
162        // remove facet types
163        Iterator<Topic> i = criteria.iterator();
164        while (i.hasNext()) {
165            Topic crit = i.next();
166            if (crit.getUri().endsWith(".facet")) {
167                i.remove();
168            }
169        }
170        //
171        return criteria;
172    }
173
174    @GET
175    @Path("/geomap/{geomap_id}/objects")
176    @Override
177    public List<Topic> getGeoObjects(@PathParam("geomap_id") long geomapId) {
178        try {
179            return fetchGeoObjects(geomapId);
180        } catch (Exception e) {
181            throw new RuntimeException("Fetching the geomap's geo objects failed (geomapId=" + geomapId + ")", e);
182        }
183    }
184
185    @GET
186    @Path("/category/{id}/objects")
187    @Override
188    public List<RelatedTopic> getGeoObjectsByCategory(@PathParam("id") long categoryId) {
189        return dms.getTopic(categoryId).getRelatedTopics("dm4.core.aggregation", "dm4.core.child", "dm4.core.parent",
190                GEO_OBJECT, 0).getItems();
191    }
192
193    /** Used by famportal editorial tool. */
194    @GET
195    @Path("/geoobject")
196    @Override
197    public GeoObjects searchGeoObjectNames(@QueryParam("search") String searchTerm, @QueryParam("clock") long clock) {
198        GeoObjects result = new GeoObjects(clock);
199        for (Topic geoObjectName : dms.searchTopics("*" + searchTerm + "*", GEO_OBJECT_NAME)) {
200            result.add(getGeoObject(geoObjectName));
201        }
202        return result;
203    }
204
205    @GET
206    @Path("/einrichtungen/missing-bezirk")
207    public List<SocialInstitutionObject> getInstitutionsMissingBezirk() {
208        logger.info("Fetching Social Institutions without a BEZIRK assignment");
209        List<SocialInstitutionObject> results = new ArrayList<SocialInstitutionObject>();
210        List<Topic> topics = dms.getTopics("type_uri", new SimpleValue("ka2.geo_object"));
211        for (Topic geoObject : topics) {
212            SocialInstitutionObject institution = new SocialInstitutionObject();
213            institution.setName(geoObject.getSimpleValue().toString()); // ### load child topic "ka2.geo_object.name"
214            Topic bezirk = getFacettedBezirkChildTopic(geoObject);
215            if (bezirk == null) {
216                logger.warning("> Bezirks Relation is missing for " + geoObject.getSimpleValue());
217                institution.setCreated(timeService.getCreationTime(geoObject.getId()));
218                institution.setLastModified(timeService.getModificationTime(geoObject.getId()));
219                institution.setUri(geoObject.getUri());
220                results.add(institution);
221            }
222        }
223        return results;
224    }
225
226    @GET
227    @Path("/einrichtungen/missing-link/")
228    public List<SocialInstitutionObject> getInstitutionsMissingBacklink() {
229        logger.info("Fetching Social Institutions without a BEZIRK & BEZIRKSREGION assignment");
230        List<SocialInstitutionObject> results = new ArrayList<SocialInstitutionObject>();
231        List<Topic> topics = dms.getTopics("type_uri", new SimpleValue("ka2.geo_object"));
232        for (Topic geoObject : topics) {
233            SocialInstitutionObject institution = new SocialInstitutionObject();
234            institution.setName(geoObject.getSimpleValue().toString()); // ### load child topic "ka2.geo_object.name"
235            Topic bezirk = getFacettedBezirkChildTopic(geoObject);
236            Topic bezirksregion = getFacettedBezirksregionChildTopic(geoObject);
237            if (bezirk == null && bezirksregion == null) {
238                logger.warning("> Bezirks and Bezirksregion Relation is missing for " + geoObject.getSimpleValue());
239                institution.setCreated(timeService.getCreationTime(geoObject.getId()));
240                institution.setLastModified(timeService.getModificationTime(geoObject.getId()));
241                institution.setUri(geoObject.getUri());
242                results.add(institution);
243            }
244        }
245        return results;
246    }
247
248    // Note: Fetch and build up administrative response objects
249    @GET
250    @Path("/einrichtungen/{id}")
251    public List<SocialInstitutionObject> getSiteInstitutions(@PathParam("id") long topicId) {
252        logger.info("Loading Social Institutions related to super Topic " + topicId);
253        List<SocialInstitutionObject> results = new ArrayList<SocialInstitutionObject>();
254        Topic superTopic = dms.getTopic(topicId);
255        ResultList<RelatedTopic> geoObjects = superTopic.getRelatedTopics("dm4.core.aggregation",
256            "dm4.core.child", "dm4.core.parent", "ka2.geo_object", 0);
257        int missingMailboxes = 0;
258        for (RelatedTopic geoObject : geoObjects) {
259            SocialInstitutionObject institution = new SocialInstitutionObject();
260            geoObject.loadChildTopics("dm4.contacts.address");
261            RelatedTopic address = geoObject.getChildTopics().getTopic("dm4.contacts.address");
262            Topic contactTopic = getFacettedContactChildTopic(geoObject);
263            String contactValue = "k.A.";
264            boolean missesMailbox = false;
265            if (contactTopic != null) {
266                contactTopic.loadChildTopics();
267                Topic eMail = contactTopic.getChildTopics().getTopic("ka2.kontakt.email");
268                if (eMail != null && !eMail.getSimpleValue().toString().isEmpty()) {
269                    contactValue = eMail.getSimpleValue().toString();
270                } else {
271                    missesMailbox = true;
272                    Topic phone = contactTopic.getChildTopics().getTopic("ka2.kontakt.telefon");
273                    if (phone != null && !phone.getSimpleValue().toString().isEmpty()) {
274                        contactValue = phone.getSimpleValue().toString();
275                    }
276                }
277            }
278            GeoCoordinate coordinates = geomapsService.getGeoCoordinate(address);
279            if (coordinates != null) {
280                institution.setGeoCoordinate(coordinates);
281            } else {
282                institution.addClassification("no-coordinates");
283                logger.info("> Geo Coordinates unavailable on \"" + geoObject.getSimpleValue().toString() + "\", Address: " + address.getSimpleValue());
284            }
285            if (missesMailbox) {
286                // logger.info("> Identified missing Email Address at " + geoObject.getSimpleValue().toString());
287                missingMailboxes++;
288                institution.addClassification("no-email");
289            }
290            Topic bezirksregion = getFacettedBezirksregionChildTopic(geoObject);
291            if (bezirksregion == null) {
292                logger.warning("> No Bezirksregion set on \"" + geoObject.getSimpleValue().toString() + "\"");
293                institution.addClassification("no-bezirksregion");
294            }
295            institution.setName(geoObject.getSimpleValue().toString()); // ### load child topic "ka2.geo_object.name"
296            institution.setContact(contactValue);
297            institution.setAddress(address.getSimpleValue().toString());
298            institution.setCreated(timeService.getCreationTime(geoObject.getId()));
299            institution.setLastModified(timeService.getModificationTime(geoObject.getId()));
300            results.add(institution);
301        }
302        logger.info("> Identified " + missingMailboxes +" Geo Objects without an "
303                + "Email-Address in the group of \"" + superTopic.getSimpleValue().toString()
304            + "\" (" + geoObjects.getSize() + ")");
305        return results;
306    }
307
308    @PUT
309    @Path("/geoobject/attribution/{topicId}/{owner}")
310    @Transactional
311    @Consumes(MediaType.TEXT_PLAIN)
312    public Response createGeoObjectAttribution(@PathParam("topicId") long id,
313            @PathParam("owner") String owner, String key) {
314        Topic geoObject = dms.getTopic(id);
315        if (geoObject != null && !owner.isEmpty() && !key.isEmpty()) {
316            String value = owner.trim();
317            String keyValue = key.trim();
318            try {
319                String existingValue = (String) geoObject.getProperty(GEO_OBJECT_OWNER_PROPERTY);
320                logger.warning("Values already set: Updating not allowed, owner=" + existingValue);
321                return Response.status(405).build();
322            } catch (Exception e) {  // ### org.neo4j.graphdb.NotFoundException
323                geoObject.setProperty(GEO_OBJECT_OWNER_PROPERTY, value, true); // ### addToIndex=true?
324                geoObject.setProperty(GEO_OBJECT_KEYWORD_PROPERTY, keyValue, false);
325                logger.info("### Equipped \"" + geoObject.getSimpleValue() + "\" with owner=\"" + owner + "\" and " +
326                        "key=\"" + key + "\"");
327                return Response.status(200).build();
328            }
329        } else if (owner.isEmpty()){
330            logger.warning("Owner and/or key empty - Not allowed");
331            return Response.status(405).build();
332        }
333        return Response.status(404).build();
334    }
335
336    @GET
337    @Path("/category/objects")
338    @Override
339    public GroupedGeoObjects searchCategories(@QueryParam("search") String searchTerm,
340            @QueryParam("clock") long clock) {
341        GroupedGeoObjects result = new GroupedGeoObjects(clock);
342        for (Topic criteria : getAllCriteria()) {
343            for (Topic category : dms.searchTopics("*" + searchTerm + "*", criteria.getUri())) {
344                result.add(criteria, category, getGeoObjectsByCategory(category.getId()));
345            }
346        }
347        return result;
348    }
349
350    @Override
351    public GeoCoordinate getGeoCoordinateByGeoObject(Topic geoObject) {
352        Topic addressTopic = geoObject.getChildTopics().getTopic(GEO_OBJECT_ADDRESS);
353        if (addressTopic != null) return geomapsService.getGeoCoordinate(addressTopic);
354        logger.warning("GeoCoordinate could not be determined becuase no Address is related to Geo Object: "
355            + geoObject.getSimpleValue());
356        return null;
357    }
358
359    @Override
360    public String getGeoObjectAttribution(Topic geoObject) {
361        return (String) geoObject.getProperty(GEO_OBJECT_OWNER_PROPERTY) + ":"
362            + (String) geoObject.getProperty(GEO_OBJECT_KEYWORD_PROPERTY);
363    }
364
365    /** This facet depends/just exists after installation of the dm4-kiezatlas-etl plugin. */
366    @Override
367    public Topic getFacettedBezirksregionChildTopic(Topic facettedTopic) {
368        // Note: Untested
369        return facetsService.getFacet(facettedTopic, "ka2.bezirksregion.facet");
370    }
371
372    /** This facet depends/just exists after installation of the dm4-kiezatlas-etl plugin. */
373    @Override
374    public Topic getImageFileFacetByGeoObject(Topic geoObject) {
375        return facetsService.getFacet(geoObject, "ka2.bild.facet");
376    }
377
378    @Override
379    public ResultList<RelatedTopic> getParentRelatedAggregatedGeoObjects(Topic bezirksFacet) {
380        return bezirksFacet.getRelatedTopics("dm4.core.aggregation", "dm4.core.child",
381            "dm4.core.parent", "ka2.geo_object", 0);
382    }
383
384    @Override
385    public void updateImageFileFacet(Topic geoObject, String imageFilePath) {
386        facetsService.updateFacet(geoObject, "ka2.bild.facet", new FacetValue("ka2.bild.pfad").put(imageFilePath));
387    }
388
389    // ********************************
390    // *** Listener Implementations ***
391    // ********************************
392
393
394
395    @Override
396    public void preSendTopic(Topic topic) {
397        if (!topic.getTypeUri().equals(GEO_OBJECT)) {
398            return;
399        }
400        //
401        ResultList<RelatedTopic> facetTypes = getFacetTypes();
402        if (facetTypes == null) {
403            return;
404        }
405        //
406        enrichWithFacets(topic, facetTypes);
407    }
408
409    @Override
410    public void postUpdateTopic(Topic topic, TopicModel newModel, TopicModel oldModel) {
411        if (!topic.getTypeUri().equals(GEO_OBJECT)) {
412            return;
413        }
414        //
415        ResultList<RelatedTopic> facetTypes = getFacetTypes();
416        if (facetTypes == null) {
417            return;
418        }
419        //
420        updateFacets(topic, facetTypes, newModel);
421    }
422
423
424
425    // ------------------------------------------------------------------------------------------------- Private Methods
426
427    private Topic getFacettedContactChildTopic(Topic facettedTopic) {
428        return facettedTopic.getRelatedTopic("dm4.core.composition", "dm4.core.parent",
429            "dm4.core.child", "ka2.kontakt");
430    }
431
432    private Topic getFacettedBezirkChildTopic(Topic facettedTopic) {
433        return facettedTopic.getRelatedTopic("dm4.core.aggregation", "dm4.core.parent",
434            "dm4.core.child", "ka2.bezirk");
435    }
436
437    private boolean hasSiteAssociation(Topic geoObject, long siteId) {
438        ResultList<RelatedTopic> sites = geoObject.getRelatedTopics("dm4.core.association", "dm4.core.default",
439            "dm4.core.default", WEBSITE, 0);
440        for (RelatedTopic site : sites) {
441            if (site.getId() == siteId) return true;
442        }
443        return false;
444    }
445
446    // === Enrich with facets ===
447
448    private void enrichWithFacets(Topic geoObject, ResultList<RelatedTopic> facetTypes) {
449        for (Topic facetType : facetTypes) {
450            String facetTypeUri = facetType.getUri();
451            if (!isMultiFacet(facetTypeUri)) {
452                enrichWithSingleFacet(geoObject, facetTypeUri);
453            } else {
454                enrichWithMultiFacet(geoObject, facetTypeUri);
455            }
456        }
457    }
458
459    // ---
460
461    private void enrichWithSingleFacet(Topic geoObject, String facetTypeUri) {
462        Topic facetValue = facetsService.getFacet(geoObject, facetTypeUri);
463        // Note: facetValue is null in 2 cases:
464        // 1) The geo object has just been created (no update yet)
465        // 2) The geo object has been created outside a geomap and then being revealed in a geomap.
466        if (facetValue == null) {
467            logger.info("### Enriching geo object " + geoObject.getId() + " with its \"" + facetTypeUri +
468                "\" facet value ABORTED -- no such facet in DB");
469            return;
470        }
471        //
472        logger.info("### Enriching geo object " + geoObject.getId() + " with its \"" + facetTypeUri +
473            "\" facet value (" + facetValue + ")");
474        geoObject.getChildTopics().getModel().put(facetValue.getTypeUri(), facetValue.getModel());
475    }
476
477    private void enrichWithMultiFacet(Topic geoObject, String facetTypeUri) {
478        ResultList<RelatedTopic> facetValues = facetsService.getFacets(geoObject, facetTypeUri);
479        logger.info("### Enriching geo object " + geoObject.getId() + " with its \"" + facetTypeUri +
480            "\" facet values (" + facetValues + ")");
481        String childTypeUri = getChildTypeUri(facetTypeUri);
482        // Note: we set the facet values at once (using put()) instead of iterating (and using add()) as after an geo
483        // object update request the facet values are already set. Using add() would result in having the values twice.
484        geoObject.getChildTopics().getModel().put(childTypeUri, DeepaMehtaUtils.toTopicModels(facetValues));
485    }
486
487
488
489    // === Update facets ===
490
491    private void updateFacets(Topic geoObject, ResultList<RelatedTopic> facetTypes, TopicModel newModel) {
492        for (Topic facetType : facetTypes) {
493            String facetTypeUri = facetType.getUri();
494            String childTypeUri = getChildTypeUri(facetTypeUri);
495            if (!isMultiFacet(facetTypeUri)) {
496                TopicModel facetValue = newModel.getChildTopicsModel().getTopic(childTypeUri);
497                logger.info("### Storing facet of type \"" + facetTypeUri + "\" for geo object " + geoObject.getId() +
498                    " (facetValue=" + facetValue + ")");
499                FacetValue value = new FacetValue(childTypeUri).put(facetValue);
500                facetsService.updateFacet(geoObject, facetTypeUri, value);
501            } else {
502                List<RelatedTopicModel> facetValues = newModel.getChildTopicsModel().getTopics(childTypeUri);
503                logger.info("### Storing facets of type \"" + facetTypeUri + "\" for geo object " + geoObject.getId() +
504                    " (facetValues=" + facetValues + ")");
505                FacetValue value = new FacetValue(childTypeUri).put(facetValues);
506                facetsService.updateFacet(geoObject, facetTypeUri, value);
507            }
508        }
509    }
510
511
512
513    // === Helper ===
514
515    /**
516     * Returns the facet types for the current topicmap, or null if the facet types can't be determined.
517     * There can be several reasons for the latter:
518     *   a) there is no "current topicmap". This can be the case with 3rd-party clients.
519     *   b) the current topicmap is not a geomap.
520     *   c) the geomap is not part of a Kiezatlas Website.
521     *
522     * @return  The facet types (as a result set, may be empty), or <code>null</code>.
523     */
524    private ResultList<RelatedTopic> getFacetTypes() {
525        Cookies cookies = Cookies.get();
526        if (!cookies.has("dm4_topicmap_id")) {
527            logger.fine("### Finding geo object facet types ABORTED -- topicmap is unknown (no \"dm4_topicmap_id\" " +
528                "cookie was sent)");
529            return null;
530        }
531        //
532        long topicmapId = cookies.getLong("dm4_topicmap_id");
533        if (!isGeomap(topicmapId)) {
534            logger.fine("### Finding geo object facet types for topicmap " + topicmapId + " ABORTED -- not a geomap");
535            return null;
536        }
537        //
538        Topic website = getWebsite(topicmapId);
539        if (website == null) {
540            logger.info("### Finding geo object facet types for geomap " + topicmapId + " ABORTED -- not part of a " +
541                "Kiezatlas website");
542            return null;
543        }
544        //
545        logger.info("### Finding geo object facet types for geomap " + topicmapId);
546        return getFacetTypes(website.getId());
547    }
548
549    private List<Topic> fetchGeoObjects(long geomapId) {
550        List<Topic> geoObjects = new ArrayList<Topic>();
551        for (TopicModel geoCoord : geomapsService.getGeomap(geomapId)) {
552            Topic geoObject = geomapsService.getDomainTopic(geoCoord.getId());
553            geoObjects.add(geoObject);
554            // ### TODO: optimization. Include only name and address in returned geo objects.
555            // ### For the moment the entire objects are returned, including composite values and facets.
556        }
557        return geoObjects;
558    }
559
560    // ---
561
562    private Topic getGeoObject(Topic geoObjectName) {
563        return geoObjectName.getRelatedTopic("dm4.core.composition", "dm4.core.child", "dm4.core.parent",
564            GEO_OBJECT);   // ### TODO: Core API should provide type-driven navigation
565    }
566
567    private boolean isGeomap(long topicmapId) {
568        Topic topicmap = dms.getTopic(topicmapId);
569        String rendererUri = topicmap.getChildTopics().getString("dm4.topicmaps.topicmap_renderer_uri");
570        return rendererUri.equals("dm4.geomaps.geomap_renderer");
571    }
572
573    // ---
574
575    // ### FIXME: there is a copy in FacetsPlugin.java
576    private boolean isMultiFacet(String facetTypeUri) {
577        return getAssocDef(facetTypeUri).getChildCardinalityUri().equals("dm4.core.many");
578    }
579
580    // ### FIXME: there is a copy in FacetsPlugin.java
581    private String getChildTypeUri(String facetTypeUri) {
582        return getAssocDef(facetTypeUri).getChildTypeUri();
583    }
584
585    // ### FIXME: there is a copy in FacetsPlugin.java
586    private AssociationDefinition getAssocDef(String facetTypeUri) {
587        // Note: a facet type has exactly *one* association definition
588        return dms.getTopicType(facetTypeUri).getAssocDefs().iterator().next();
589    }
590}