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