merge PresenceTerm and AbsenceTerm #4521
[cdmlib.git] / cdmlib-ext / src / main / java / eu / etaxonomy / cdm / ext / geo / EditGeoServiceUtilities.java
1 // $Id$
2 /**
3 * Copyright (C) 2007 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
11 package eu.etaxonomy.cdm.ext.geo;
12
13 import java.awt.Color;
14 import java.io.IOException;
15 import java.io.UnsupportedEncodingException;
16 import java.net.URLEncoder;
17 import java.util.ArrayList;
18 import java.util.Collection;
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Set;
25 import java.util.UUID;
26
27 import javax.persistence.Transient;
28
29 import org.apache.commons.collections.CollectionUtils;
30 import org.apache.commons.collections.Transformer;
31 import org.apache.commons.lang.StringUtils;
32 import org.apache.log4j.Logger;
33 import org.codehaus.jackson.JsonParseException;
34 import org.codehaus.jackson.map.JsonMappingException;
35 import org.codehaus.jackson.map.ObjectMapper;
36 import org.codehaus.jackson.map.type.MapType;
37 import org.codehaus.jackson.map.type.TypeFactory;
38
39 import eu.etaxonomy.cdm.api.service.ITermService;
40 import eu.etaxonomy.cdm.api.utility.DescriptionUtility;
41 import eu.etaxonomy.cdm.common.CdmUtils;
42 import eu.etaxonomy.cdm.model.common.Language;
43 import eu.etaxonomy.cdm.model.common.Marker;
44 import eu.etaxonomy.cdm.model.common.MarkerType;
45 import eu.etaxonomy.cdm.model.common.Representation;
46 import eu.etaxonomy.cdm.model.common.TermVocabulary;
47 import eu.etaxonomy.cdm.model.description.Distribution;
48 import eu.etaxonomy.cdm.model.description.PresenceAbsenceTerm;
49 import eu.etaxonomy.cdm.model.location.Country;
50 import eu.etaxonomy.cdm.model.location.NamedArea;
51 import eu.etaxonomy.cdm.model.location.NamedAreaLevel;
52 import eu.etaxonomy.cdm.model.location.Point;
53 import eu.etaxonomy.cdm.model.occurrence.SpecimenOrObservationType;
54 import eu.etaxonomy.cdm.persistence.dao.common.IDefinedTermDao;
55
56 /**
57 * Class implementing the business logic for creating the map service string for
58 * a given set of distributions. See {@link EditGeoService} as API for the given functionality.
59 *
60 * @see EditGeoService
61 *
62 * @author a.mueller
63 * @created 17.11.2008
64 */
65 public class EditGeoServiceUtilities {
66 private static final int INT_MAX_LENGTH = String.valueOf(Integer.MAX_VALUE).length();
67
68 private static final Logger logger = Logger.getLogger(EditGeoServiceUtilities.class);
69
70 private static PresenceAbsenceTerm defaultStatus = PresenceAbsenceTerm.PRESENT();
71
72 private static IDefinedTermDao termDao;
73
74
75
76 /**
77 * @param termDao
78 */
79 public static void setTermDao(IDefinedTermDao termDao) {
80 EditGeoServiceUtilities.termDao= termDao;
81 }
82
83
84 private static HashMap<SpecimenOrObservationType, Color> defaultSpecimenOrObservationTypeColors = null;
85
86 private static HashMap<SpecimenOrObservationType, Color> getDefaultSpecimenOrObservationTypeColors() {
87 if(defaultSpecimenOrObservationTypeColors == null){
88 defaultSpecimenOrObservationTypeColors = new HashMap<SpecimenOrObservationType, Color>();
89 defaultSpecimenOrObservationTypeColors.put(SpecimenOrObservationType.FieldUnit, Color.ORANGE);
90 defaultSpecimenOrObservationTypeColors.put(SpecimenOrObservationType.DerivedUnit, Color.RED);
91 defaultSpecimenOrObservationTypeColors.put(SpecimenOrObservationType.LivingSpecimen, Color.GREEN);
92 defaultSpecimenOrObservationTypeColors.put(SpecimenOrObservationType.Observation, Color.ORANGE);
93 defaultSpecimenOrObservationTypeColors.put(SpecimenOrObservationType.PreservedSpecimen, Color.GRAY);
94 defaultSpecimenOrObservationTypeColors.put(SpecimenOrObservationType.Media, Color.BLUE);
95 }
96 return defaultSpecimenOrObservationTypeColors;
97 }
98
99
100 private static HashMap<PresenceAbsenceTerm, Color> defaultPresenceAbsenceTermBaseColors = null;
101
102 private static HashMap<PresenceAbsenceTerm, Color> getDefaultPresenceAbsenceTermBaseColors() {
103 if(defaultPresenceAbsenceTermBaseColors == null){
104 defaultPresenceAbsenceTermBaseColors = new HashMap<PresenceAbsenceTerm, Color>();
105 defaultPresenceAbsenceTermBaseColors.put(PresenceAbsenceTerm.PRESENT(), Color.decode("0x4daf4a"));
106 defaultPresenceAbsenceTermBaseColors.put(PresenceAbsenceTerm.NATIVE(), Color.decode("0x4daf4a"));
107 defaultPresenceAbsenceTermBaseColors.put(PresenceAbsenceTerm.NATIVE_DOUBTFULLY_NATIVE(), Color.decode("0x377eb8"));
108 defaultPresenceAbsenceTermBaseColors.put(PresenceAbsenceTerm.CULTIVATED(), Color.decode("0x984ea3"));
109 defaultPresenceAbsenceTermBaseColors.put(PresenceAbsenceTerm.INTRODUCED(), Color.decode("0xff7f00"));
110 defaultPresenceAbsenceTermBaseColors.put(PresenceAbsenceTerm.INTRODUCED_ADVENTITIOUS(), Color.decode("0xffff33"));
111 defaultPresenceAbsenceTermBaseColors.put(PresenceAbsenceTerm.INTRODUCED_CULTIVATED(), Color.decode("0xa65628"));
112 defaultPresenceAbsenceTermBaseColors.put(PresenceAbsenceTerm.INTRODUCED_NATURALIZED(), Color.decode("0xf781bf"));
113
114 /*
115 * and now something very hacky ...
116 * ONLY-A-TEST is set by the Test class EditGeoServiceTest
117 *
118 * FIXME remove according line from
119 * EditGeoServiceTest.setUp() since the hardcoded colors for flora of
120 * cyprus should no longer be needed : #4268 (allow defining custom presence and absence term colors for EditGeoServiceUtilities)
121 */
122 String onlyTest = System.getProperty("ONLY-A-TEST"); //
123 if(onlyTest != null && onlyTest.equals("TRUE")){
124 return defaultPresenceAbsenceTermBaseColors;
125 }
126 //special colors for flora of cyprus !!! see HACK above !!!
127 UUID indigenousUuid = UUID.fromString("b325859b-504b-45e0-9ef0-d5c1602fcc0f");
128 UUID indigenousQUuid = UUID.fromString("17bc601f-53eb-4997-a4bc-c03ce5bfd1d3");
129
130 UUID cultivatedQUuid = UUID.fromString("4f31bfc8-3058-4d83-aea5-3a1fe9773f9f");
131
132 UUID casualUuid = UUID.fromString("5e81353c-38a3-4ca6-b979-0d9abc93b877");
133 UUID casualQUuid = UUID.fromString("73f75493-1185-4a3e-af1e-9a1f2e8dadb7");
134
135 UUID naturalizedNonInvasiveUuid = UUID.fromString("1b025e8b-901a-42e8-9739-119b410c6f03");
136 UUID naturalizedNonInvasiveQUuid = UUID.fromString("11f56e2f-c16c-4b3d-a870-bb5d3b20e624");
137
138 UUID naturalizedInvasiveUuid = UUID.fromString("faf2d271-868a-4bf7-b0b8-a1c5ab309de2");
139 UUID naturalizedInvasiveQUuid = UUID.fromString("ac429d5f-e8ad-49ae-a41c-e4779b58b96a");
140
141 UUID questionablelUuid = UUID.fromString("4b48f675-a6cf-49f3-a5ba-77e2c2979eb3");
142 UUID questionableQUuid = UUID.fromString("914e7393-1314-4632-bc45-5eff3dc1e424");
143
144 UUID reportedInErrorUuid = UUID.fromString("38604788-cf05-4607-b155-86db456f7680");
145
146 defaultPresenceAbsenceTermBaseColors.put((PresenceAbsenceTerm) termDao.load(indigenousUuid), Color.decode("0x339966"));
147 defaultPresenceAbsenceTermBaseColors.put((PresenceAbsenceTerm) termDao.load(indigenousQUuid), Color.decode("0x339966"));
148
149 defaultPresenceAbsenceTermBaseColors.put((PresenceAbsenceTerm) termDao.load(cultivatedQUuid), Color.decode("0xbdb76b"));
150
151 defaultPresenceAbsenceTermBaseColors.put((PresenceAbsenceTerm) termDao.load(casualUuid), Color.decode("0xffff00"));
152 defaultPresenceAbsenceTermBaseColors.put((PresenceAbsenceTerm) termDao.load(casualQUuid), Color.decode("0xffff00"));
153
154 defaultPresenceAbsenceTermBaseColors.put((PresenceAbsenceTerm) termDao.load(naturalizedNonInvasiveUuid), Color.decode("0xff9900"));
155 defaultPresenceAbsenceTermBaseColors.put((PresenceAbsenceTerm) termDao.load(naturalizedNonInvasiveQUuid), Color.decode("0xff9900"));
156
157 defaultPresenceAbsenceTermBaseColors.put((PresenceAbsenceTerm) termDao.load(naturalizedInvasiveUuid), Color.decode("0xff0000"));
158 defaultPresenceAbsenceTermBaseColors.put((PresenceAbsenceTerm) termDao.load(naturalizedInvasiveQUuid), Color.decode("0xff0000"));
159
160 defaultPresenceAbsenceTermBaseColors.put((PresenceAbsenceTerm) termDao.load(questionablelUuid), Color.decode("0x00ccff"));
161 defaultPresenceAbsenceTermBaseColors.put((PresenceAbsenceTerm) termDao.load(questionableQUuid), Color.decode("0x00ccff"));
162
163 defaultPresenceAbsenceTermBaseColors.put((PresenceAbsenceTerm) termDao.load(reportedInErrorUuid), Color.decode("0xcccccc"));
164
165 }
166 return defaultPresenceAbsenceTermBaseColors;
167 }
168
169
170
171 private static final String SUBENTRY_DELIMITER = ",";
172 private static final String ENTRY_DELIMITER = ";";
173 static final String ID_FROM_VALUES_SEPARATOR = ":";
174 static final String VALUE_LIST_ENTRY_SEPARATOR = "|";
175 static final String VALUE_SUPER_LIST_ENTRY_SEPARATOR = "||";
176
177
178
179 //preliminary implementation for TDWG areas
180 /**
181 * Returns the parameter String for the EDIT geo webservice to create a
182 * dsitribution map.
183 *
184 * @param distributions
185 * A set of distributions that should be shown on the map
186 * @param subAreaPreference
187 * enables the <b>Sub area preference rule</b> if set to true,
188 * see {@link DescriptionUtility#filterDistributions(Collection,
189 * boolean, boolean}
190 *
191 * @param statusOrderPreference
192 * enables the <b>Status order preference rule</b> if set to
193 * true, see {@link
194 * DescriptionUtility#filterDistributions(Collection, boolean,
195 * boolean}
196 * @param hideMarkedAreas
197 * distributions where the area has a {@link Marker} with one of
198 * the specified {@link MarkerType}s will be skipped, see
199 * {@link DescriptionUtility#filterDistributions(Collection, boolean, boolean, Set)}
200 * @param presenceAbsenceTermColors
201 * A map that defines the colors of PresenceAbsenceTerms. The
202 * PresenceAbsenceTerms are defined by their uuid. If a
203 * PresenceAbsenceTerm is not included in this map, it's default
204 * color is taken instead. If the map == null all terms are
205 * colored by their default color.
206 * @param width
207 * The maps width
208 * @param height
209 * The maps height
210 * @param bbox
211 * The maps bounding box (e.g. "-180,-90,180,90" for the whole
212 * world)
213 * @param projectToLayer
214 * name of a layer which is representing a specific
215 * {@link NamedAreaLevel} Supply this parameter if you to project
216 * all other distribution area levels to this layer.
217 * @param layer
218 * The layer that is responsible for background borders and
219 * colors. Use the name for the layer. If null 'earth' is taken
220 * as default.
221 * @return the parameter string or an empty string if the
222 * <code>distributions</code> set was null or empty.
223 */
224 @Transient
225 public static String getDistributionServiceRequestParameterString(
226 Set<Distribution> distributions,
227 boolean subAreaPreference,
228 boolean statusOrderPreference,
229 Set<MarkerType> hideMarkedAreas,
230 IGeoServiceAreaMapping mapping,
231 Map<PresenceAbsenceTerm,Color> presenceAbsenceTermColors,
232 String projectToLayer, List<Language> languages){
233
234
235 /*
236 * generateMultipleAreaDataParameters switches between the two possible styles:
237 * 1. ad=layername1:area-data||layername2:area-data
238 * 2. ad=layername1:area-data&ad=layername2:area-data
239 */
240 boolean generateMultipleAreaDataParameters = false;
241
242 /*
243 * doNotReuseStyles is a workaround for a problem in the EDIT MapService,
244 * see https://dev.e-taxonomy.eu/trac/ticket/2707#comment:24
245 *
246 * a.kohlbecker 2014-07-02 :This bug in the map service has been
247 * fixed now so reusing styles is now possible setting this flag to false.
248 */
249 boolean doNotReuseStyles = false;
250
251 List<String> perLayerAreaData = new ArrayList<String>();
252 Map<Integer, String> areaStyles = new HashMap<Integer, String>();
253 List<String> legendSortList = new ArrayList<String>();
254
255 String borderWidth = "0.1";
256 String borderColorRgb = "";
257 String borderDashingPattern = "";
258
259
260 //handle empty set
261 if(distributions == null || distributions.size() == 0){
262 return "";
263 }
264
265 presenceAbsenceTermColors = mergeMaps(getDefaultPresenceAbsenceTermBaseColors(), presenceAbsenceTermColors);
266
267 Collection<Distribution> filteredDistributions = DescriptionUtility.filterDistributions(distributions, subAreaPreference, statusOrderPreference, hideMarkedAreas);
268
269 Map<String, Map<Integer, Set<Distribution>>> layerMap = new HashMap<String, Map<Integer, Set<Distribution>>>();
270 List<PresenceAbsenceTerm> statusList = new ArrayList<PresenceAbsenceTerm>();
271
272 groupStylesAndLayers(filteredDistributions, layerMap, statusList, mapping);
273
274 Map<String, String> parameters = new HashMap<String, String>();
275
276 //style
277 int styleCounter = 0;
278 for (PresenceAbsenceTerm status: statusList){
279
280 char styleCode = getStyleAbbrev(styleCounter);
281
282 //getting the area title
283 if (languages == null){
284 languages = new ArrayList<Language>();
285 }
286 if (languages.size() == 0){
287 languages.add(Language.DEFAULT());
288 }
289 Representation statusRepresentation = status.getPreferredRepresentation(languages);
290 String statusLabel = statusRepresentation.getLabel();
291 //statusLabel.replace('introduced: ', '');
292 statusLabel = statusLabel.replace("introduced: ", "introduced, ");
293 statusLabel = statusLabel.replace("native: ", "native, ");
294
295 //getting the area color
296 Color statusColor = presenceAbsenceTermColors.get(status);
297 String fillColorRgb;
298 if (statusColor != null){
299 fillColorRgb = Integer.toHexString(statusColor.getRGB()).substring(2);
300 }else{
301 if(status != null){
302 fillColorRgb = status.getDefaultColor(); //TODO
303 } else {
304 fillColorRgb = defaultStatus.getDefaultColor();
305 }
306 }
307 String styleValues = StringUtils.join(new String[]{fillColorRgb, borderColorRgb, borderWidth, borderDashingPattern}, ',');
308
309 areaStyles.put(styleCounter, styleValues);
310
311 String legendEntry = styleCode + ID_FROM_VALUES_SEPARATOR + encode(statusLabel);
312 legendSortList.add(StringUtils.leftPad(String.valueOf(status.getOrderIndex()), INT_MAX_LENGTH, '0') + legendEntry );
313 styleCounter++;
314 }
315
316 // area data
317 List<String> styledAreasPerLayer;
318 List<String> areasPerStyle;
319 /**
320 * Map<Integer, Integer> styleUsage
321 *
322 * Used to avoid reusing styles in multiple layers
323 *
324 * key: the style id
325 * value: the count of how often the style has been used for different layers, starts with 0 for first time use
326 */
327 Map<Integer, Integer> styleUsage = new HashMap<Integer, Integer>();
328
329 char styleChar;
330 for (String layerString : layerMap.keySet()){
331 // each layer
332 styledAreasPerLayer = new ArrayList<String>();
333 Map<Integer, Set<Distribution>> styleMap = layerMap.get(layerString);
334 for (int style: styleMap.keySet()){
335 // stylesPerLayer
336 if(doNotReuseStyles) {
337 if(!styleUsage.containsKey(style)){
338 styleUsage.put(style, 0);
339 } else {
340 // increment by 1
341 styleUsage.put(style, styleUsage.get(style) + 1);
342 }
343 Integer styleIncrement = styleUsage.get(style);
344 if(styleIncrement > 0){
345 // style code has been used before!
346 styleChar = getStyleAbbrev(style + styleIncrement + styleCounter);
347 //for debugging sometimes failing test #3831
348 logger.warn("style: " + style + ", styleIncrement: " + styleIncrement + ", styleCounter: " + styleCounter);
349 areaStyles.put(style + styleIncrement + styleCounter, areaStyles.get(style));
350 } else {
351 styleChar = getStyleAbbrev(style);
352 }
353 } else {
354 styleChar = getStyleAbbrev(style);
355 }
356 Set<Distribution> distributionSet = styleMap.get(style);
357 areasPerStyle = new ArrayList<String>();
358 for (Distribution distribution: distributionSet){
359 // areasPerStyle
360 areasPerStyle.add(encode(getAreaCode(distribution, mapping)));
361 }
362 styledAreasPerLayer.add(styleChar + ID_FROM_VALUES_SEPARATOR + StringUtils.join(areasPerStyle.iterator(), SUBENTRY_DELIMITER));
363 }
364 perLayerAreaData.add(encode(layerString) + ID_FROM_VALUES_SEPARATOR + StringUtils.join(styledAreasPerLayer.iterator(), VALUE_LIST_ENTRY_SEPARATOR));
365 }
366
367 if(areaStyles.size() > 0){
368 ArrayList<Integer> styleIds = new ArrayList<Integer>(areaStyles.size());
369 styleIds.addAll(areaStyles.keySet());
370 Collections.sort(styleIds); // why is it necessary to sort here?
371 StringBuilder db = new StringBuilder();
372 for(Integer sid : styleIds){
373 if(db.length() > 0){
374 db.append(VALUE_LIST_ENTRY_SEPARATOR);
375 }
376 db.append( getStyleAbbrev(sid)).append(ID_FROM_VALUES_SEPARATOR).append(areaStyles.get(sid));
377 }
378 parameters.put("as", db.toString());
379 }
380 if(legendSortList.size() > 0){
381 // sort the label entries after the status terms
382 Collections.sort(legendSortList);
383 // since the status terms are have an inverse natural order
384 // (as all other ordered term, see OrderedTermBase.performCompareTo(T orderedTerm, boolean skipVocabularyCheck)
385 // the sorted list must be reverted
386 // Collections.reverse(legendSortList);
387 // remove the prepended order index (like 000000000000001 ) from the legend entries
388 @SuppressWarnings("unchecked")
389 Collection<String> legendEntries = CollectionUtils.collect(legendSortList, new Transformer()
390 {
391 @Override
392 public String transform(Object o)
393 {
394 String s = ((String) o);
395 return s.substring(INT_MAX_LENGTH, s.length());
396 }
397 });
398
399 parameters.put("title", StringUtils.join(legendEntries.iterator(), VALUE_LIST_ENTRY_SEPARATOR));
400 }
401
402 if(generateMultipleAreaDataParameters){
403 // not generically possible since parameters can not contain duplicate keys with value "ad"
404 } else {
405 parameters.put("ad", StringUtils.join(perLayerAreaData.iterator(), VALUE_SUPER_LIST_ENTRY_SEPARATOR));
406 }
407
408 String queryString = makeQueryString(parameters);
409 logger.debug("getDistributionServiceRequestParameterString(): " + queryString);
410
411 return queryString;
412 }
413
414
415 /**
416 * Fills the layerMap and the statusList
417 *
418 * @param distributions
419 * @param layerMap see {@link #addAreaToLayerMap(Map, List, Distribution, NamedArea, IGeoServiceAreaMapping)}
420 * @param statusList
421 */
422 private static void groupStylesAndLayers(Collection<Distribution> distributions,
423 Map<String, Map<Integer,Set<Distribution>>> layerMap,
424 List<PresenceAbsenceTerm> statusList,
425 IGeoServiceAreaMapping mapping) {
426
427
428 //iterate through distributions and group styles and layers
429 //and collect necessary information
430 for (Distribution distribution : distributions){
431 //collect status
432 PresenceAbsenceTerm status = distribution.getStatus();
433 if(status == null){
434 status = defaultStatus;
435 }
436 if (! statusList.contains(status)){
437 statusList.add(status);
438 }
439 //group areas by layers and styles
440 NamedArea area = distribution.getArea();
441
442 addAreaToLayerMap(layerMap, statusList, distribution, area, mapping);
443 }
444 }
445
446 /**
447 * Adds the areas to the layer map. Areas which do not have layer information
448 * mapped to them are ignored.
449 * <p>
450 * A layer map holds the following information:
451 *
452 * <ul>
453 * <li><b>String</b>: the WMSLayerName which matches the level of the
454 * contained distributions areas</li>
455 * <li><b>StyleMap</b>:</li>
456 * <ul>
457 * <li><b>Integer</b>: the index of the status in the
458 * <code>statusList</code></li>
459 * <li><b>Set{@code<Distribution>}</b>: the set of distributions having the
460 * same Status, the status list is populated in {@link #groupStylesAndLayers(Set, Map, List, IGeoServiceAreaMapping)}</li>
461 * </ul>
462 * </ul>
463 *
464 * @param layerMap
465 * @param statusList
466 * @param distribution
467 * @param area
468 */
469 private static void addAreaToLayerMap(Map<String, Map<Integer,
470 Set<Distribution>>> layerMap,
471 List<PresenceAbsenceTerm> statusList,
472 Distribution distribution,
473 NamedArea area,
474 IGeoServiceAreaMapping mapping) {
475
476 if (area != null){
477 String geoLayerName = getWMSLayerName(area, mapping);
478
479 if(geoLayerName == null){
480 /* IGNORE areas for which no layer is mapped */
481 } else {
482 Map<Integer, Set<Distribution>> styleMap = layerMap.get(geoLayerName);
483 if (styleMap == null) {
484 styleMap = new HashMap<Integer, Set<Distribution>>();
485 layerMap.put(geoLayerName, styleMap);
486 }
487 addDistributionToStyleMap(distribution, styleMap, statusList);
488 }
489 }
490 }
491
492
493
494 /**
495 * URI encode the given String
496 * @param string
497 * @return
498 */
499 private static String encode(String string) {
500 String encoded = string;
501 try {
502 encoded = URLEncoder.encode(string, "UTF-8");
503 } catch (UnsupportedEncodingException e) {
504 logger.error(e);
505 }
506 return encoded;
507 }
508
509 /**
510 * combine parameter into a URI query string fragment. The values will be
511 * escaped correctly.
512 *
513 * @param parameters
514 * @return a URI query string fragment
515 */
516 private static String makeQueryString(Map<String, String> parameters){
517 StringBuilder queryString = new StringBuilder();
518 for (String key : parameters.keySet()) {
519 if(queryString.length() > 0){
520 queryString.append('&');
521 }
522 if(key.equals("od") || key.equals("os") || key.equals("ms") || key.equals("ad") || key.equals("as") || key.equals("title") || key.equals("bbox")){
523 queryString.append(key).append('=').append(parameters.get(key));
524 } else {
525 queryString.append(key).append('=').append(encode(parameters.get(key)));
526 }
527 }
528 return queryString.toString();
529 }
530
531 private static String getAreaCode(Distribution distribution, IGeoServiceAreaMapping mapping){
532 NamedArea area = distribution.getArea();
533 TermVocabulary<NamedArea> voc = area.getVocabulary();
534 String result = null;
535
536 if (voc != null && voc.getUuid().equals(NamedArea.uuidTdwgAreaVocabulary) || voc.getUuid().equals(Country.uuidCountryVocabulary)) {
537 // TDWG or Country
538 result = area.getIdInVocabulary();
539 if (area.getLevel() != null && area.getLevel().equals(NamedAreaLevel.TDWG_LEVEL4())) {
540 result = result.replace("-", "");
541 }
542 } else {
543 // use generic GeoServiceArea data stored in technical annotations
544 // of the
545 // named area
546 GeoServiceArea areas = mapping.valueOf(area);
547 if ((areas != null) && areas.size() > 0) {
548 // FIXME multiple layers
549 List<String> values = areas.getAreasMap().values().iterator().next().values().iterator().next();
550 for (String value : values) {
551 result = CdmUtils.concat(SUBENTRY_DELIMITER, result, value);
552 }
553 }
554
555 }
556 return CdmUtils.Nz(result, "-");
557
558 }
559
560 private static List<String> projectToWMSSubLayer(NamedArea area){
561
562 List<String> layerNames = new ArrayList<String>();
563 String matchedLayerName = null;
564 TermVocabulary<NamedArea> voc = area.getVocabulary();
565 //TDWG areas
566 if (voc.getUuid().equals(NamedArea.uuidTdwgAreaVocabulary)){
567 NamedAreaLevel level = area.getLevel();
568 if (level != null) {
569 //TODO integrate into CDM
570 if (level.equals(NamedAreaLevel.TDWG_LEVEL1())) {
571 matchedLayerName = "tdwg1" ;
572 } else if (level.equals(NamedAreaLevel.TDWG_LEVEL2())) {
573 matchedLayerName = "tdwg2";
574 }else if (level.equals(NamedAreaLevel.TDWG_LEVEL3())) {
575 matchedLayerName = "tdwg3";
576 }else if (level.equals(NamedAreaLevel.TDWG_LEVEL4())) {
577 matchedLayerName = "tdwg4";
578 }
579 }
580 //unrecognized tdwg area
581
582 }
583 //TODO countries
584
585 // check if the matched layer equals the layer to project to
586 // if not: recurse into the sub-level in order to find the specified one.
587 String[] matchedLayerNameTokens = StringUtils.split(matchedLayerName, ':');
588 // if(matchedLayerNameTokens.length > 0 && matchedLayerNameTokens[0] != projectToLayer){
589 // for (NamedArea subArea : area.getIncludes()){
590 //
591 // }
592 //
593 // add all sub areas
594 // }
595
596 return null;
597 }
598
599 private static String getWMSLayerName(NamedArea area, IGeoServiceAreaMapping mapping){
600 TermVocabulary<NamedArea> voc = area.getVocabulary();
601 //TDWG areas
602 if (voc.getUuid().equals(NamedArea.uuidTdwgAreaVocabulary)){
603 NamedAreaLevel level = area.getLevel();
604 if (level != null) {
605 //TODO integrate into CDM
606 if (level.equals(NamedAreaLevel.TDWG_LEVEL1())) {
607 return "tdwg1";
608 } else if (level.equals(NamedAreaLevel.TDWG_LEVEL2())) {
609 return "tdwg2";
610 }else if (level.equals(NamedAreaLevel.TDWG_LEVEL3())) {
611 return "tdwg3";
612 }else if (level.equals(NamedAreaLevel.TDWG_LEVEL4())) {
613 return "tdwg4";
614 }
615 }
616 //unrecognized tdwg area
617 return null;
618
619 }else if (voc.getUuid().equals(Country.uuidCountryVocabulary)){
620 return "country_earth:gmi_cntry";
621 }
622
623 GeoServiceArea areas = mapping.valueOf(area);
624 if (areas != null && areas.getAreasMap().size() > 0){
625 //FIXME multiple layers
626 String layer = areas.getAreasMap().keySet().iterator().next();
627 Map<String, List<String>> fields = areas.getAreasMap().get(layer);
628 String field = fields.keySet().iterator().next();
629 String layerString = layer + ":" + field;
630 return layerString.toLowerCase();
631 }
632
633 return null;
634 }
635
636
637 private static void addDistributionToStyleMap(Distribution distribution, Map<Integer, Set<Distribution>> styleMap,
638 List<PresenceAbsenceTerm> statusList) {
639 PresenceAbsenceTerm status = distribution.getStatus();
640 if (status == null) {
641 status = defaultStatus;
642 }
643 int style = statusList.indexOf(status);
644 Set<Distribution> distributionSet = styleMap.get(style);
645 if (distributionSet == null) {
646 distributionSet = new HashSet<Distribution>();
647 styleMap.put(style, distributionSet);
648 }
649 distributionSet.add(distribution);
650 }
651
652 /**
653 * @param fieldUnitPoints
654 * @param derivedUnitPoints
655 * @param specimenOrObservationTypeColors
656 * @param width
657 * @param height
658 * @param bbox
659 * @param backLayer
660 * @return
661 * e.g.:
662 * l=v%3Aatbi%2Ce_w_0
663 * &legend=0
664 * &image=false
665 * &recalculate=false
666 * &ms=400%2C350
667
668 * &od=1%3A44.29481%2C6.82161|44.29252%2C6.822873|44.29247%2C6.82346|44.29279%2C6.823678|44.29269%2C6.82394|44.28482%2C6.887252|44.11469%2C7.287144|44.11468%2C7.289168
669 * &os=1%3Ac%2FFFD700%2F10%2FAporrectodea caliginosa
670 */
671 public static String getOccurrenceServiceRequestParameterString(
672 List<Point> fieldUnitPoints,
673 List<Point> derivedUnitPoints,
674 Map<SpecimenOrObservationType, Color> specimenOrObservationTypeColors) {
675
676 specimenOrObservationTypeColors = mergeMaps(getDefaultSpecimenOrObservationTypeColors(), specimenOrObservationTypeColors);
677
678 Map<String, String> parameters = new HashMap<String, String>();
679 parameters.put("legend", "0");
680
681 Map<String, String> styleAndData = new HashMap<String, String>();
682
683 addToStyleAndData(fieldUnitPoints, SpecimenOrObservationType.FieldUnit, specimenOrObservationTypeColors, styleAndData);
684 addToStyleAndData(derivedUnitPoints, SpecimenOrObservationType.DerivedUnit, specimenOrObservationTypeColors, styleAndData);
685
686 parameters.put("os", StringUtils.join(styleAndData.keySet().iterator(), "||"));
687 parameters.put("od", StringUtils.join(styleAndData.values().iterator(), "||"));
688
689 String queryString = makeQueryString(parameters);
690
691 logger.info(queryString);
692
693 return queryString;
694 }
695
696 /**
697 * @param <T>
698 * @param <S>
699 * @param defaultMap
700 * @param overrideMap
701 * @return
702 */
703 private static <T, S> Map<T, S> mergeMaps(Map<T, S> defaultMap, Map<T, S> overrideMap) {
704 Map<T, S> tmpMap = new HashMap<T, S>();
705 tmpMap.putAll(defaultMap);
706 if(overrideMap != null){
707 tmpMap.putAll(overrideMap);
708 }
709 return tmpMap;
710 }
711
712 private static void addToStyleAndData(
713 List<Point> points,
714 SpecimenOrObservationType specimenOrObservationType,
715 Map<SpecimenOrObservationType, Color> specimenOrObservationTypeColors, Map<String, String> styleAndData) {
716
717 //TODO add markerShape and size and Label to specimenOrObservationTypeColors -> Map<Class<SpecimenOrObservationBase<?>>, MapStyle>
718
719 if(points != null && points.size()>0){
720 String style = "c/" + Integer.toHexString(specimenOrObservationTypeColors.get(specimenOrObservationType).getRGB()).substring(2) + "/10/noLabel";
721 StringBuilder data = new StringBuilder();
722 for(Point point : points){
723 if(data.length() > 0){
724 data.append('|');
725 }
726 data.append(point.getLatitude() + "," + point.getLongitude());
727 }
728 int index = styleAndData.size() + 1;
729 styleAndData.put(index + ":" +style, index + ":" +data.toString());
730 }
731 }
732
733
734 /**
735 * transform an integer (style counter) into a valid character representing a style.
736 * 0-25 => a-z<br>
737 * 26-51 => A-Z<br>
738 * i not in {0,...,51} is undefined
739 * @param i
740 * @return
741 */
742 private static char getStyleAbbrev(int i){
743 i++;
744 int ascii = 96 + i;
745 if (i >26){
746 ascii = 64 + i;
747 }
748 return (char)ascii;
749 }
750
751 /**
752 * @param statusColorJson for example: {@code {"n":"#ff0000","p":"#ffff00"}}
753 * @return
754 * @throws IOException
755 * @throws JsonParseException
756 * @throws JsonMappingException
757 */
758 public static Map<PresenceAbsenceTerm, Color> buildStatusColorMap(String statusColorJson, ITermService termService) throws IOException, JsonParseException,
759 JsonMappingException {
760
761 Map<PresenceAbsenceTerm, Color> presenceAbsenceTermColors = null;
762 if(StringUtils.isNotEmpty(statusColorJson)){
763
764 ObjectMapper mapper = new ObjectMapper();
765 // TODO cache the color maps to speed this up?
766
767 TypeFactory typeFactory = mapper.getTypeFactory();
768 MapType mapType = typeFactory.constructMapType(HashMap.class, String.class, String.class);
769
770 Map<String,String> statusColorMap = mapper.readValue(statusColorJson, mapType);
771 UUID presenceTermVocabUuid = PresenceAbsenceTerm.NATIVE().getVocabulary().getUuid();
772 presenceAbsenceTermColors = new HashMap<PresenceAbsenceTerm, Color>();
773 PresenceAbsenceTerm paTerm = null;
774 for(String statusId : statusColorMap.keySet()){
775 try {
776 Color color = Color.decode(statusColorMap.get(statusId));
777 paTerm = termService.findByIdInVocabulary(statusId, presenceTermVocabUuid, PresenceAbsenceTerm.class);
778 if(paTerm != null){
779 presenceAbsenceTermColors.put(paTerm, color);
780 }
781 } catch (NumberFormatException e){
782 logger.error("Cannot decode color", e);
783 }
784 }
785 }
786 return presenceAbsenceTermColors;
787 }
788
789 }