composition of condensed distribution fully implemented - #3907
[cdmlib.git] / cdmlib-ext / src / main / java / eu / etaxonomy / cdm / ext / geo / EuroPlusMedCondensedDistributionComposer.java
1 // $Id$
2 /**
3 * Copyright (C) 2015 EDIT
4 * European Distributed Institute of Taxonomy
5 * http://www.e-taxonomy.eu
6 *
7 * The contents of this file are subject to the Mozilla Public License Version 1.1
8 * See LICENSE.TXT at the top of this package for the full license terms.
9 */
10 package eu.etaxonomy.cdm.ext.geo;
11
12 import java.util.ArrayList;
13 import java.util.Collection;
14 import java.util.Collections;
15 import java.util.HashMap;
16 import java.util.HashSet;
17 import java.util.List;
18 import java.util.Map;
19 import java.util.Set;
20 import java.util.UUID;
21
22 import org.apache.commons.lang.StringUtils;
23 import org.apache.log4j.Logger;
24
25 import eu.etaxonomy.cdm.api.service.dto.CondensedDistribution;
26 import eu.etaxonomy.cdm.model.common.Language;
27 import eu.etaxonomy.cdm.model.description.Distribution;
28 import eu.etaxonomy.cdm.model.description.PresenceAbsenceTerm;
29 import eu.etaxonomy.cdm.model.location.NamedArea;
30
31 /**
32 * @author a.kohlbecker
33 * @date Jun 24, 2015
34 *
35 */
36 public class EuroPlusMedCondensedDistributionComposer implements ICondensedDistributionComposer {
37
38 private static final Logger logger = Logger.getLogger(EuroPlusMedCondensedDistributionComposer.class);
39
40 private final CondensedDistribution condensedDistribution;
41
42 private static Map<UUID, String> statusSymbols;
43
44 private static Set<UUID> foreignStatusUuids;
45
46 // these status uuids are special for EuroPlusMed and might also be used
47 private final static UUID REPORTED_IN_ERROR_UUID = UUID.fromString("38604788-cf05-4607-b155-86db456f7680");
48
49 static {
50
51 // ==================================================
52 // Mapping as defined in ticket http://dev.e-taxonomy.eu/trac/ticket/3907
53 // ==================================================
54
55 statusSymbols = new HashMap<UUID, String> ();
56 // ● endemic (U+25CF BLACK CIRCLE)
57 statusSymbols.put(PresenceAbsenceTerm.ENDEMIC_FOR_THE_RELEVANT_AREA().getUuid(), "\u25CF");
58
59 // Lu native (incl. archaeophytes) TODO status archaeophytes?
60 statusSymbols.put(PresenceAbsenceTerm.NATIVE().getUuid(), "");
61 statusSymbols.put(PresenceAbsenceTerm.NATIVE_FORMERLY_NATIVE().getUuid(), "");
62
63 // ?Lu doubtfully present (U+3F QUESTION MARK)
64 statusSymbols.put(PresenceAbsenceTerm.INTRODUCED_PRESENCE_QUESTIONABLE().getUuid(), "?");
65 statusSymbols.put(PresenceAbsenceTerm.NATIVE_PRESENCE_QUESTIONABLE().getUuid(), "?");
66 statusSymbols.put(PresenceAbsenceTerm.PRESENT_DOUBTFULLY().getUuid(), "?");
67
68 // dLu doubtfully native
69 statusSymbols.put(PresenceAbsenceTerm.NATIVE_DOUBTFULLY_NATIVE().getUuid(), "d");
70
71 // -Lu absent but reported in error (U+2D HYPHEN-MINUS)
72 statusSymbols.put(PresenceAbsenceTerm.INTRODUCED_REPORTED_IN_ERROR().getUuid(), "-");
73 statusSymbols.put(PresenceAbsenceTerm.NATIVE_REPORTED_IN_ERROR().getUuid(), "-");
74 statusSymbols.put(REPORTED_IN_ERROR_UUID, "-");
75
76 // †Lu (presumably) extinct (U+2020 DAGGER)
77 // no such status in database!!!
78
79 // [Lu] introduced (casual or naturalized) = introduced, introduced: naturalized
80 statusSymbols.put(PresenceAbsenceTerm.INTRODUCED().getUuid(), "");
81 statusSymbols.put(PresenceAbsenceTerm.INTRODUCED_NATURALIZED().getUuid(), "");
82
83 // [aLu] casual alien = introduced: adventitious (casual)
84 statusSymbols.put(PresenceAbsenceTerm.INTRODUCED_ADVENTITIOUS().getUuid(), "a");
85
86 // [cLu] cultivated
87 statusSymbols.put(PresenceAbsenceTerm.CULTIVATED() .getUuid(), "c");
88 statusSymbols.put(PresenceAbsenceTerm.INTRODUCED_CULTIVATED().getUuid(), "c");
89
90 // [nLu] naturalized
91 statusSymbols.put(PresenceAbsenceTerm.NATURALISED().getUuid(), "n");
92
93 foreignStatusUuids = new HashSet<UUID>();
94 foreignStatusUuids.add(PresenceAbsenceTerm.INTRODUCED().getUuid());
95 foreignStatusUuids.add(PresenceAbsenceTerm.INTRODUCED_NATURALIZED().getUuid());
96 foreignStatusUuids.add(PresenceAbsenceTerm.INTRODUCED_ADVENTITIOUS().getUuid());
97 // foreignStatusUuids.add(PresenceAbsenceTerm.INTRODUCED_CULTIVATED().getUuid()); // how about this?
98 foreignStatusUuids.add(PresenceAbsenceTerm.NATURALISED().getUuid());
99 foreignStatusUuids.add(PresenceAbsenceTerm.CULTIVATED().getUuid());
100
101 }
102
103 public EuroPlusMedCondensedDistributionComposer() {
104 condensedDistribution = new CondensedDistribution();
105 }
106
107 /**
108 * {@inheritDoc}
109 * @return
110 */
111 @Override
112 public CondensedDistribution createCondensedDistribution(Collection<Distribution> filteredDistributions,
113 List<Language> langs) {
114
115 //1. order by PresenceAbsenceTerms
116 Map<PresenceAbsenceTerm, Collection<NamedArea>> byStatus = new HashMap<PresenceAbsenceTerm, Collection<NamedArea>>();
117 for(Distribution d : filteredDistributions) {
118 if(!byStatus.containsKey(d.getStatus())) {
119 byStatus.put(d.getStatus(), new HashSet<NamedArea>());
120 }
121 byStatus.get(d.getStatus()).add(d.getArea());
122 }
123
124 //2. build the area hierarchy
125 for(PresenceAbsenceTerm status : byStatus.keySet()) {
126
127 Map<NamedArea, AreaNode> areaNodeMap = new HashMap<NamedArea, AreaNode>();
128
129 for(NamedArea area : byStatus.get(status)) {
130 AreaNode node;
131 if(!areaNodeMap.containsKey(area)) {
132 // putting area into hierarchy as node
133 node = new AreaNode(area);
134 areaNodeMap.put(area, node);
135 } else {
136 // is parent of another and thus already has a node
137 node = areaNodeMap.get(area);
138 }
139
140 NamedArea parent = findParentIn(area, byStatus.get(status));
141 if(parent != null) {
142 AreaNode parentNode;
143 if(!areaNodeMap.containsKey(parent)) {
144 parentNode = new AreaNode(parent);
145 areaNodeMap.put(parent, parentNode);
146 } else {
147 parentNode = areaNodeMap.get(parent);
148 }
149 parentNode.addSubArea(node);
150 }
151 }
152
153 //3. find root nodes
154 Set<AreaNode>hierarchy = new HashSet<AreaNode>();
155 for(AreaNode node : areaNodeMap.values()) {
156 if(!node.hasParent()) {
157 hierarchy.add(node);
158 }
159 }
160
161 //4. replace the area by the abbreviated representation and add symbols
162 for(AreaNode node : hierarchy) {
163
164 StringBuilder areaStatusString = new StringBuilder();
165
166 String statusSymbol = statusSymbol(status);
167 areaStatusString.append(statusSymbol);
168
169 String areaLabel = node.area.getPreferredRepresentation(langs).getAbbreviatedLabel();
170 areaStatusString.append(areaLabel);
171
172 if(!node.subAreas.isEmpty()) {
173 areaStatusString.append('(');
174 subAreaLabels(langs, node.subAreas, areaStatusString, statusSymbol, areaLabel);
175 areaStatusString.append(')');
176 }
177
178 if(isForeignStatus(status)) {
179 condensedDistribution.addForeignDistributionItem(status, areaStatusString.toString(), areaLabel);
180 } else {
181 condensedDistribution.addIndigenousDistributionItem(status, areaStatusString.toString(), areaLabel);
182 }
183
184 }
185
186 }
187 //5. order the condensedDistributions alphabetically
188 condensedDistribution.sortForeign();
189 condensedDistribution.sortIndigenous();
190
191 return condensedDistribution;
192 }
193
194 /**
195 * @param status
196 * @return
197 */
198 private String statusSymbol(PresenceAbsenceTerm status) {
199 String symbol = statusSymbols.get(status.getUuid());
200 if(symbol == null) {
201 symbol = "";
202 }
203 return symbol ;
204 }
205
206 private boolean isForeignStatus(PresenceAbsenceTerm status) {
207 return foreignStatusUuids.contains(status.getUuid());
208 }
209
210 /**
211 * @param langs
212 * @param node
213 * @param areaString
214 * @param statusSymbol
215 */
216 private void subAreaLabels(List<Language> langs, Collection<AreaNode> nodes, StringBuilder areaString, String statusSymbol, String parentLabel) {
217
218 List<String> subAreaLabels = new ArrayList<String>();
219
220 for(AreaNode node : nodes) {
221
222 StringBuilder subAreaString = new StringBuilder();
223
224 subAreaString.append(statusSymbol);
225
226 String areaLabel = node.area.getPreferredRepresentation(langs).getAbbreviatedLabel();
227 subAreaString.append(StringUtils.remove(areaLabel, parentLabel + "-"));
228
229 if(!node.subAreas.isEmpty()) {
230 subAreaString.append('(');
231 subAreaLabels(langs, node.subAreas, subAreaString, statusSymbol, areaLabel);
232 subAreaString.append(')');
233 }
234
235 subAreaLabels.add(subAreaString.toString());
236 }
237 Collections.sort(subAreaLabels);
238 areaString.append(StringUtils.join(subAreaLabels, " "));
239
240 }
241
242 /**
243 * Searches for the parent are of the area given as parameter in
244 * the Collection of areas.
245 *
246 * @parent area
247 * The area whose parent area is to be searched
248 * @param collection
249 * The areas to search in.
250 *
251 * @return
252 * Either the parent if it has been found or null.
253 */
254 private NamedArea findParentIn(NamedArea area, Collection<NamedArea> areas) {
255 NamedArea parent = area.getPartOf();
256 if(parent != null && areas.contains(parent)){
257 return parent;
258 }
259 return null;
260 }
261
262 class AreaNode {
263
264 private final NamedArea area;
265 private AreaNode parent = null;
266 private final Set<AreaNode> subAreas = new HashSet<AreaNode>();
267
268 /**
269 * @param area
270 */
271 public AreaNode(NamedArea area) {
272 this.area = area;
273 }
274
275 public void addSubArea(AreaNode subArea) {
276 subAreas.add(subArea);
277 subArea.parent = this;
278 }
279
280 public AreaNode getParent() {
281 return parent;
282 }
283
284 public boolean hasParent() {
285 return getParent() != null;
286 }
287
288 public Collection<NamedArea> getSubareas() {
289 Collection<NamedArea> areas = new HashSet<NamedArea>();
290 for(AreaNode node : subAreas) {
291 areas.add(node.area);
292 }
293 return areas;
294 }
295 }
296
297 }