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