Project

General

Profile

Download (29.8 KB) Statistics
| Branch: | Tag: | Revision:
1
/**
2
* Copyright (C) 2007 EDIT
3
* European Distributed Institute of Taxonomy
4
* http://www.e-taxonomy.eu
5
*
6
* The contents of this file are subject to the Mozilla Public License Version 1.1
7
* See LICENSE.TXT at the top of this package for the full license terms.
8
*/
9

    
10
package eu.etaxonomy.cdm.ext.geo;
11

    
12
import java.awt.Color;
13
import java.io.IOException;
14
import java.io.UnsupportedEncodingException;
15
import java.net.URLEncoder;
16
import java.util.ArrayList;
17
import java.util.Collection;
18
import java.util.Collections;
19
import java.util.HashMap;
20
import java.util.HashSet;
21
import java.util.List;
22
import java.util.Map;
23
import java.util.Set;
24
import java.util.UUID;
25

    
26
import javax.persistence.Transient;
27

    
28
import org.apache.commons.collections.CollectionUtils;
29
import org.apache.commons.lang.StringUtils;
30
import org.apache.log4j.Logger;
31

    
32
import com.fasterxml.jackson.core.JsonParseException;
33
import com.fasterxml.jackson.databind.JsonMappingException;
34
import com.fasterxml.jackson.databind.ObjectMapper;
35
import com.fasterxml.jackson.databind.type.MapType;
36
import com.fasterxml.jackson.databind.type.TypeFactory;
37

    
38
import eu.etaxonomy.cdm.api.service.ITermService;
39
import eu.etaxonomy.cdm.api.service.IVocabularyService;
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.hibernate.HibernateProxyHelper;
44
import eu.etaxonomy.cdm.model.common.Language;
45
import eu.etaxonomy.cdm.model.description.Distribution;
46
import eu.etaxonomy.cdm.model.description.PresenceAbsenceTerm;
47
import eu.etaxonomy.cdm.model.location.Country;
48
import eu.etaxonomy.cdm.model.location.NamedArea;
49
import eu.etaxonomy.cdm.model.location.NamedAreaLevel;
50
import eu.etaxonomy.cdm.model.location.Point;
51
import eu.etaxonomy.cdm.model.occurrence.SpecimenOrObservationType;
52
import eu.etaxonomy.cdm.model.term.Representation;
53
import eu.etaxonomy.cdm.model.term.TermType;
54
import eu.etaxonomy.cdm.model.term.TermVocabulary;
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
 * @since 17.11.2008
64
 */
65
public class EditGeoServiceUtilities {
66
    private static final Logger logger = Logger.getLogger(EditGeoServiceUtilities.class);
67

    
68
    private static final int INT_MAX_LENGTH = String.valueOf(Integer.MAX_VALUE).length();
69

    
70
    private static HashMap<SpecimenOrObservationType, Color> defaultSpecimenOrObservationTypeColors = null;
71

    
72
    private static HashMap<SpecimenOrObservationType, Color> getDefaultSpecimenOrObservationTypeColors() {
73
        if(defaultSpecimenOrObservationTypeColors == null){
74
            defaultSpecimenOrObservationTypeColors = new HashMap<>();
75
            defaultSpecimenOrObservationTypeColors.put(SpecimenOrObservationType.FieldUnit, Color.ORANGE);
76
            defaultSpecimenOrObservationTypeColors.put(SpecimenOrObservationType.DerivedUnit, Color.RED);
77
            defaultSpecimenOrObservationTypeColors.put(SpecimenOrObservationType.LivingSpecimen, Color.GREEN);
78
            defaultSpecimenOrObservationTypeColors.put(SpecimenOrObservationType.Observation, Color.ORANGE);
79
            defaultSpecimenOrObservationTypeColors.put(SpecimenOrObservationType.PreservedSpecimen, Color.GRAY);
80
            defaultSpecimenOrObservationTypeColors.put(SpecimenOrObservationType.Media, Color.BLUE);
81
        }
82
        return defaultSpecimenOrObservationTypeColors;
83
    }
84

    
85
    private static HashMap<PresenceAbsenceTerm, Color> defaultPresenceAbsenceTermBaseColors = null;
86

    
87
    private static List<UUID>  presenceAbsenceTermVocabularyUuids = null;
88

    
89
    private static HashMap<PresenceAbsenceTerm, Color> getDefaultPresenceAbsenceTermBaseColors() {
90
        if(defaultPresenceAbsenceTermBaseColors == null){
91
            defaultPresenceAbsenceTermBaseColors = new HashMap<>();
92
            defaultPresenceAbsenceTermBaseColors.put(PresenceAbsenceTerm.PRESENT(), Color.decode("0x4daf4a"));
93
            defaultPresenceAbsenceTermBaseColors.put(PresenceAbsenceTerm.NATIVE(), Color.decode("0x4daf4a"));
94
            defaultPresenceAbsenceTermBaseColors.put(PresenceAbsenceTerm.NATIVE_DOUBTFULLY_NATIVE(), Color.decode("0x377eb8"));
95
            defaultPresenceAbsenceTermBaseColors.put(PresenceAbsenceTerm.CULTIVATED(), Color.decode("0x984ea3"));
96
            defaultPresenceAbsenceTermBaseColors.put(PresenceAbsenceTerm.INTRODUCED(), Color.decode("0xff7f00"));
97
            defaultPresenceAbsenceTermBaseColors.put(PresenceAbsenceTerm.CASUAL(), Color.decode("0xffff33"));
98
            defaultPresenceAbsenceTermBaseColors.put(PresenceAbsenceTerm.INTRODUCED_CULTIVATED(), Color.decode("0xa65628"));
99
            defaultPresenceAbsenceTermBaseColors.put(PresenceAbsenceTerm.NATURALISED(), Color.decode("0xf781bf"));
100
        }
101
        return defaultPresenceAbsenceTermBaseColors;
102
    }
103

    
104
    private static final String SUBENTRY_DELIMITER = ",";
105
    private static final String ENTRY_DELIMITER = ";";
106
    static final String ID_FROM_VALUES_SEPARATOR = ":";
107
    static final String VALUE_LIST_ENTRY_SEPARATOR = "|";
108
    static final String VALUE_SUPER_LIST_ENTRY_SEPARATOR = "||";
109

    
110
    /**
111
     * Returns the parameter String for the EDIT geo webservice to create a
112
     * distribution map.
113
     *
114
     * @param distributions
115
     *            A set of distributions that should be shown on the map
116
     *            The {@link DescriptionUtility} class provides a method for
117
     *            filtering a set of Distributions :
118
     *
119
     *            {@code
120
     *            Collection<Distribution> filteredDistributions =
121
     *            DescriptionUtility.filterDistributions(distributions,
122
     *            subAreaPreference, statusOrderPreference, hideMarkedAreas);
123
     *            }
124
     * @param mapping
125
     *            Data regarding the mapping of NamedAreas to shape file
126
     *            attribute tables
127
     * @param presenceAbsenceTermColors
128
     *            A map that defines the colors of PresenceAbsenceTerms. The
129
     *            PresenceAbsenceTerms are defined by their uuid. If a
130
     *            PresenceAbsenceTerm is not included in this map, it's default
131
     *            color is taken instead. If the map == null all terms are
132
     *            colored by their default color.
133
     * @param projectToLayer
134
     *            name of a layer which is representing a specific
135
     *            {@link NamedAreaLevel} Supply this parameter if you to project
136
     *            all other distribution area levels to this layer.
137
     * @param languages
138
     *
139
     * @return the parameter string or an empty string if the
140
     *         <code>distributions</code> set was null or empty.
141
     */
142
    @Transient
143
    public static String getDistributionServiceRequestParameterString(
144
            Collection<Distribution> filteredDistributions,
145
            IGeoServiceAreaMapping mapping,
146
            Map<PresenceAbsenceTerm,Color> presenceAbsenceTermColors,
147
            String projectToLayer,
148
            List<Language> languages){
149

    
150
        /*
151
         * generateMultipleAreaDataParameters switches between the two possible styles:
152
         * 1. ad=layername1:area-data||layername2:area-data
153
         * 2. ad=layername1:area-data&ad=layername2:area-data
154
         */
155
        boolean generateMultipleAreaDataParameters = false;
156

    
157
        List<String>  perLayerAreaData = new ArrayList<>();
158
        Map<Integer, String> areaStyles = new HashMap<>();
159
        List<String> legendSortList = new ArrayList<>();
160

    
161
        String borderWidth = "0.1";
162
        String borderColorRgb = "";
163
        String borderDashingPattern = "";
164

    
165
        //handle empty set
166
        if(filteredDistributions == null || filteredDistributions.size() == 0){
167
            return "";
168
        }
169

    
170
        presenceAbsenceTermColors = mergeMaps(getDefaultPresenceAbsenceTermBaseColors(), presenceAbsenceTermColors);
171

    
172
        Map<String, Map<Integer, Set<Distribution>>> layerMap = new HashMap<>();
173
        List<PresenceAbsenceTerm> statusList = new ArrayList<>();
174

    
175
        groupStylesAndLayers(filteredDistributions, layerMap, statusList, mapping);
176

    
177
        Map<String, String> parameters = new HashMap<>();
178

    
179
        //style
180
        int styleCounter = 0;
181
        for (PresenceAbsenceTerm status: statusList){
182

    
183
            char styleCode = getStyleAbbrev(styleCounter);
184

    
185
            //getting the area title
186
            if (languages == null){
187
                languages = new ArrayList<>();
188
            }
189
            if (languages.size() == 0){
190
                languages.add(Language.DEFAULT());
191
            }
192
            Representation statusRepresentation = status.getPreferredRepresentation(languages);
193

    
194
            //getting the area color
195
            Color statusColor = presenceAbsenceTermColors.get(status);
196
            String fillColorRgb;
197
            if (statusColor != null){
198
                fillColorRgb = Integer.toHexString(statusColor.getRGB()).substring(2);
199
            }else{
200
                fillColorRgb = status.getDefaultColor(); //TODO
201
            }
202
            String styleValues = StringUtils.join(new String[]{fillColorRgb, borderColorRgb, borderWidth, borderDashingPattern}, ',');
203

    
204
            areaStyles.put(styleCounter, styleValues);
205

    
206
            String legendEntry = styleCode + ID_FROM_VALUES_SEPARATOR + encode(statusRepresentation.getLabel());
207
            legendSortList.add(StringUtils.leftPad(String.valueOf(status.getOrderIndex()), INT_MAX_LENGTH, '0') + legendEntry );
208
            styleCounter++;
209
        }
210

    
211
        // area data
212
        List<String> styledAreasPerLayer;
213
        List<String> areasPerStyle;
214
        /**
215
         * Map<Integer, Integer> styleUsage
216
         *
217
         * Used to avoid reusing styles in multiple layers
218
         *
219
         * key: the style id
220
         * value: the count of how often the style has been used for different layers, starts with 0 for first time use
221
         */
222
        Map<Integer, Integer> styleUsage = new HashMap<>();
223

    
224
        char styleChar;
225
        for (String layerString : layerMap.keySet()){
226
            // each layer
227
            styledAreasPerLayer = new ArrayList<>();
228
            Map<Integer, Set<Distribution>> styleMap = layerMap.get(layerString);
229
            for (int style: styleMap.keySet()){
230
                // stylesPerLayer
231
                styleChar = getStyleAbbrev(style);
232
                Set<Distribution> distributionSet = styleMap.get(style);
233
                areasPerStyle = new ArrayList<>();
234
                for (Distribution distribution: distributionSet){
235
                    // areasPerStyle
236
                    areasPerStyle.add(encode(getAreaCode(distribution, mapping)));
237
                }
238
                styledAreasPerLayer.add(styleChar + ID_FROM_VALUES_SEPARATOR + StringUtils.join(areasPerStyle.iterator(), SUBENTRY_DELIMITER));
239
            }
240
            perLayerAreaData.add(encode(layerString) + ID_FROM_VALUES_SEPARATOR + StringUtils.join(styledAreasPerLayer.iterator(), VALUE_LIST_ENTRY_SEPARATOR));
241
        }
242

    
243
        if(areaStyles.size() > 0){
244
            ArrayList<Integer> styleIds = new ArrayList<>(areaStyles.size());
245
            styleIds.addAll(areaStyles.keySet());
246
            Collections.sort(styleIds); // why is it necessary to sort here?
247
            StringBuilder db = new StringBuilder();
248
            for(Integer sid : styleIds){
249
                if(db.length() > 0){
250
                    db.append(VALUE_LIST_ENTRY_SEPARATOR);
251
                }
252
                db.append( getStyleAbbrev(sid)).append(ID_FROM_VALUES_SEPARATOR).append(areaStyles.get(sid));
253
            }
254
            parameters.put("as", db.toString());
255
        }
256
        if(legendSortList.size() > 0){
257
            // sort the label entries after the status terms
258
            Collections.sort(legendSortList);
259
            // since the status terms are have an inverse natural order
260
            // (as all other ordered term, see OrderedTermBase.performCompareTo(T orderedTerm, boolean skipVocabularyCheck)
261
            // the sorted list must be reverted
262
//            Collections.reverse(legendSortList);
263
            // remove the prepended order index (like 000000000000001 ) from the legend entries
264
            @SuppressWarnings("unchecked")
265
            Collection<String> legendEntries = CollectionUtils.collect(legendSortList, (o)->{
266
                      String s = ((String) o);
267
                      return s.substring(INT_MAX_LENGTH, s.length());
268
                  });
269

    
270
            parameters.put("title", StringUtils.join(legendEntries.iterator(), VALUE_LIST_ENTRY_SEPARATOR));
271
        }
272

    
273
        if(generateMultipleAreaDataParameters){
274
            // not generically possible since parameters can not contain duplicate keys with value "ad"
275
        } else {
276
            parameters.put("ad", StringUtils.join(perLayerAreaData.iterator(), VALUE_SUPER_LIST_ENTRY_SEPARATOR));
277
        }
278

    
279
        String queryString = makeQueryString(parameters);
280
        logger.debug("getDistributionServiceRequestParameterString(): " + queryString);
281

    
282
        return queryString;
283
    }
284

    
285

    
286
    /**
287
     * Fills the layerMap and the statusList
288
     *
289
     * @param distributions
290
     * @param layerMap see {@link #addAreaToLayerMap(Map, List, Distribution, NamedArea, IGeoServiceAreaMapping)}
291
     * @param statusList
292
     */
293
    private static void groupStylesAndLayers(Collection<Distribution> distributions,
294
            Map<String, Map<Integer,Set<Distribution>>> layerMap,
295
            List<PresenceAbsenceTerm> statusList,
296
            IGeoServiceAreaMapping mapping) {
297

    
298

    
299
        //iterate through distributions and group styles and layers
300
        //and collect necessary information
301
        for (Distribution distribution : distributions){
302
            //collect status
303
            PresenceAbsenceTerm status = distribution.getStatus();
304
            if(status == null){
305
                continue;
306
            }
307
            status = HibernateProxyHelper.deproxy(status);
308
            if (! statusList.contains(status)){
309
                statusList.add(status);
310
            }
311
            //group areas by layers and styles
312
            NamedArea area = distribution.getArea();
313

    
314
            addAreaToLayerMap(layerMap, statusList, distribution, area, mapping);
315
        }
316
    }
317

    
318
    /**
319
     * Adds the areas to the layer map. Areas which do not have layer information
320
     * mapped to them are ignored.
321
     * <p>
322
     * A layer map holds the following information:
323
     *
324
     * <ul>
325
     *   <li><b>String</b>: the WMSLayerName which matches the level of the
326
     *   contained distributions areas</li>
327
     *   <li><b>StyleMap</b>:</li>
328
     *   <ul>
329
     *     <li><b>Integer</b>: the index of the status in the
330
     *     <code>statusList</code></li>
331
     *     <li><b>Set{@code<Distribution>}</b>: the set of distributions having the
332
     *     same Status, the status list is populated in {@link #groupStylesAndLayers(Set, Map, List, IGeoServiceAreaMapping)}</li>
333
     *   </ul>
334
     * </ul>
335
     *
336
     * @param layerMap
337
     * @param statusList
338
     * @param distribution
339
     * @param area
340
     */
341
    private static void addAreaToLayerMap(Map<String, Map<Integer,
342
            Set<Distribution>>> layerMap,
343
            List<PresenceAbsenceTerm> statusList,
344
            Distribution distribution,
345
            NamedArea area,
346
            IGeoServiceAreaMapping mapping) {
347

    
348
        if (area != null){
349
            String geoLayerName = getWMSLayerName(area, mapping);
350

    
351
            if(geoLayerName == null){
352
               logger.warn("no wms layer mapping defined for " + area.getLabel() + " [" + area.getIdInVocabulary() + "]");
353
            } else {
354
                Map<Integer, Set<Distribution>> styleMap = layerMap.get(geoLayerName);
355
                if (styleMap == null) {
356
                    styleMap = new HashMap<Integer, Set<Distribution>>();
357
                    layerMap.put(geoLayerName, styleMap);
358
                }
359
                addDistributionToStyleMap(distribution, styleMap, statusList);
360
            }
361
        }
362
    }
363

    
364
    /**
365
     * URI encode the given String
366
     */
367
    private static String encode(String string) {
368
        String encoded = string;
369
        try {
370
            encoded = URLEncoder.encode(string, "UTF-8");
371
        } catch (UnsupportedEncodingException e) {
372
            logger.error(e);
373
        }
374
        return encoded;
375
    }
376

    
377
    /**
378
     * combine parameter into a URI query string fragment. The values will be
379
     * escaped correctly.
380
     *
381
     * @param parameters
382
     * @return a URI query string fragment
383
     */
384
    private static String makeQueryString(Map<String, String> parameters){
385
        StringBuilder queryString = new StringBuilder();
386
        for (String key : parameters.keySet()) {
387
            if(queryString.length() > 0){
388
                queryString.append('&');
389
            }
390
            if(key.equals("od") || key.equals("os") || key.equals("ms") || key.equals("ad") || key.equals("as") || key.equals("title") || key.equals("bbox")){
391
                queryString.append(key).append('=').append(parameters.get(key));
392
            } else {
393
                queryString.append(key).append('=').append(encode(parameters.get(key)));
394
            }
395
        }
396
        return queryString.toString();
397
    }
398

    
399
    private static String getAreaCode(Distribution distribution, IGeoServiceAreaMapping mapping){
400

    
401
        NamedArea area = distribution.getArea();
402
        TermVocabulary<NamedArea> voc = area.getVocabulary();
403
        String result = null;
404

    
405
        if (voc != null && voc.getUuid().equals(NamedArea.uuidTdwgAreaVocabulary) ||  voc.getUuid().equals(Country.uuidCountryVocabulary)) {
406
            // TDWG or Country
407
            result = area.getIdInVocabulary();
408
            if (area.getLevel() != null && area.getLevel().equals(NamedAreaLevel.TDWG_LEVEL4())) {
409
                result = result.replace("-", "");
410
            }
411
        } else {
412
            // use generic GeoServiceArea data stored in technical annotations
413
            // of the
414
            // named area
415
            GeoServiceArea areas = mapping.valueOf(area);
416
            if ((areas != null) && areas.size() > 0) {
417
                // FIXME multiple layers
418
                List<String> values = areas.getAreasMap().values().iterator().next().values().iterator().next();
419
                for (String value : values) {
420
                    result = CdmUtils.concat(SUBENTRY_DELIMITER, result, value);
421
                }
422
            }
423
        }
424
        return CdmUtils.Nz(result, "-");
425
    }
426

    
427
    private static List<String> projectToWMSSubLayer(NamedArea area){
428

    
429
        List<String> layerNames = new ArrayList<>();
430
        String matchedLayerName = null;
431
        TermVocabulary<NamedArea> voc = area.getVocabulary();
432
        //TDWG areas
433
        if (voc.getUuid().equals(NamedArea.uuidTdwgAreaVocabulary)){
434
            NamedAreaLevel level = area.getLevel();
435
            if (level != null) {
436
                //TODO integrate into CDM
437
                if (level.equals(NamedAreaLevel.TDWG_LEVEL1())) {
438
                    matchedLayerName = "tdwg1" ;
439
                } else if (level.equals(NamedAreaLevel.TDWG_LEVEL2())) {
440
                    matchedLayerName = "tdwg2";
441
                }else if (level.equals(NamedAreaLevel.TDWG_LEVEL3())) {
442
                    matchedLayerName = "tdwg3";
443
                }else if (level.equals(NamedAreaLevel.TDWG_LEVEL4())) {
444
                    matchedLayerName = "tdwg4";
445
                }
446
            }
447
            //unrecognized tdwg area
448

    
449
        }
450
        //TODO countries
451

    
452
        // check if the matched layer equals the layer to project to
453
        // if not: recurse into the sub-level in order to find the specified one.
454
        String[] matchedLayerNameTokens = StringUtils.split(matchedLayerName, ':');
455
//		if(matchedLayerNameTokens.length > 0 &&  matchedLayerNameTokens[0] != projectToLayer){
456
//			for (NamedArea subArea : area.getIncludes()){
457
//
458
//			}
459
            //
460
            // add all sub areas
461
//		}
462

    
463
        return null;
464
    }
465

    
466
    private static String getWMSLayerName(NamedArea area, IGeoServiceAreaMapping mapping){
467
        TermVocabulary<NamedArea> voc = area.getVocabulary();
468
        //TDWG areas
469
        if (voc.getUuid().equals(NamedArea.uuidTdwgAreaVocabulary)){
470
            NamedAreaLevel level = area.getLevel();
471
            if (level != null) {
472
                //TODO integrate into CDM
473
                if (level.equals(NamedAreaLevel.TDWG_LEVEL1())) {
474
                    return "tdwg1";
475
                } else if (level.equals(NamedAreaLevel.TDWG_LEVEL2())) {
476
                    return "tdwg2";
477
                }else if (level.equals(NamedAreaLevel.TDWG_LEVEL3())) {
478
                    return "tdwg3";
479
                }else if (level.equals(NamedAreaLevel.TDWG_LEVEL4())) {
480
                    return "tdwg4";
481
                }
482
            }
483
            //unrecognized tdwg area
484
            return null;
485

    
486
        }else if (voc.getUuid().equals(Country.uuidCountryVocabulary)){
487
            return "country_earth:gmi_cntry";
488
        }
489

    
490
        GeoServiceArea areas = mapping.valueOf(area);
491
        if (areas != null && areas.getAreasMap().size() > 0){
492
            //FIXME multiple layers
493
            String layer = areas.getAreasMap().keySet().iterator().next();
494
            Map<String, List<String>> fields = areas.getAreasMap().get(layer);
495
            String field = fields.keySet().iterator().next();
496
            String layerString = layer + ":" + field;
497
            return layerString.toLowerCase();
498
        }
499

    
500
        return null;
501
    }
502

    
503
    private static void addDistributionToStyleMap(Distribution distribution, Map<Integer, Set<Distribution>> styleMap,
504
            List<PresenceAbsenceTerm> statusList) {
505
        PresenceAbsenceTerm status = distribution.getStatus();
506
        if (status != null) {
507
            int style = statusList.indexOf(status);
508
            Set<Distribution> distributionSet = styleMap.get(style);
509
            if (distributionSet == null) {
510
                distributionSet = new HashSet<Distribution>();
511
                styleMap.put(style, distributionSet);
512
            }
513
            distributionSet.add(distribution);
514
        }
515
    }
516

    
517
    /**
518
     * @param fieldUnitPoints
519
     * @param derivedUnitPoints
520
     * @param specimenOrObservationTypeColors
521
     * @param width
522
     * @param height
523
     * @param bbox
524
     * @param backLayer
525
     * @return
526
     * e.g.:
527
     * 	l=v%3Aatbi%2Ce_w_0
528
     *  &legend=0
529
     *  &image=false
530
     *  &recalculate=false
531
     *  &ms=400%2C350
532

    
533
     *  &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
534
     *  &os=1%3Ac%2FFFD700%2F10%2FAporrectodea caliginosa
535
     */
536
    public static OccurrenceServiceRequestParameterDto getOccurrenceServiceRequestParameterString(
537
            List<Point> fieldUnitPoints,
538
            List<Point> derivedUnitPoints,
539
            Map<SpecimenOrObservationType, Color> specimenOrObservationTypeColors) {
540

    
541
        OccurrenceServiceRequestParameterDto dto = new OccurrenceServiceRequestParameterDto();
542

    
543
        specimenOrObservationTypeColors = mergeMaps(getDefaultSpecimenOrObservationTypeColors(), specimenOrObservationTypeColors);
544

    
545
        Map<String, String> parameters = new HashMap<>();
546
        parameters.put("legend", "0");
547

    
548
        Map<String, String> styleAndData = new HashMap<>();
549

    
550
        addToStyleAndData(fieldUnitPoints, SpecimenOrObservationType.FieldUnit, specimenOrObservationTypeColors, styleAndData);
551
        addToStyleAndData(derivedUnitPoints, SpecimenOrObservationType.DerivedUnit, specimenOrObservationTypeColors, styleAndData);
552

    
553
        parameters.put("os", StringUtils.join(styleAndData.keySet().iterator(), "||"));
554
        parameters.put("od", StringUtils.join(styleAndData.values().iterator(), "||"));
555

    
556
        String queryString = makeQueryString(parameters);
557

    
558
        dto.setFieldUnitPoints(fieldUnitPoints);
559
        dto.setDerivedUnitPoints(derivedUnitPoints);
560
        dto.setOccurrenceQuery(queryString);
561

    
562
        logger.info(queryString);
563

    
564
        return dto;
565
    }
566

    
567
    private static <T, S> Map<T, S> mergeMaps(Map<T, S> defaultMap, Map<T, S> overrideMap) {
568
        Map<T, S> tmpMap = new HashMap<T, S>();
569
        tmpMap.putAll(defaultMap);
570
        if(overrideMap != null){
571
            tmpMap.putAll(overrideMap);
572
        }
573
        return tmpMap;
574
    }
575

    
576
    private static void addToStyleAndData(
577
            List<Point> points,
578
            SpecimenOrObservationType specimenOrObservationType,
579
            Map<SpecimenOrObservationType, Color> specimenOrObservationTypeColors, Map<String, String> styleAndData) {
580

    
581
        //TODO add markerShape and size and Label to specimenOrObservationTypeColors -> Map<Class<SpecimenOrObservationBase<?>>, MapStyle>
582

    
583
        if(points != null && points.size()>0){
584
            String style =  "c/" + Integer.toHexString(specimenOrObservationTypeColors.get(specimenOrObservationType).getRGB()).substring(2) + "/10/noLabel";
585
            StringBuilder data = new StringBuilder();
586
            for(Point point : points){
587
                if (point != null){  //should not be null, but just in case
588
                    if(data.length() > 0){
589
                        data.append('|');
590
                    }
591
                    data.append(point.getLatitude() + "," + point.getLongitude());
592
                }
593
            }
594
            int index = styleAndData.size() + 1;
595
            styleAndData.put(index + ":" +style, index + ":" +data.toString());
596
        }
597
    }
598

    
599
    /**
600
     * transform an integer (style counter) into a valid character representing a style.
601
     * 0-25 => a-z<br>
602
     * 26-51 => A-Z<br>
603
     * i not in {0,...,51} is undefined
604
     */
605
    private static char getStyleAbbrev(int i){
606
        i++;
607
        int ascii = 96 + i;
608
        if (i >26){
609
            ascii = 64 + i;
610
        }
611
        return (char)ascii;
612
    }
613

    
614
    /**
615
     * @param statusColorJson for example: {@code {"n":"#ff0000","p":"#ffff00"}}
616
     * @param vocabularyService TODO
617
     * @return
618
     * @throws IOException
619
     * @throws JsonParseException
620
     * @throws JsonMappingException
621
     */
622
    public static Map<PresenceAbsenceTerm, Color> buildStatusColorMap(String statusColorJson,
623
            ITermService termService, IVocabularyService vocabularyService)
624
            throws IOException, JsonParseException, JsonMappingException {
625

    
626
        Map<PresenceAbsenceTerm, Color> presenceAbsenceTermColors = null;
627
        if(StringUtils.isNotEmpty(statusColorJson)){
628

    
629
            ObjectMapper mapper = new ObjectMapper();
630
            // TODO cache the color maps to speed this up?
631

    
632
            TypeFactory typeFactory = mapper.getTypeFactory();
633
            MapType mapType = typeFactory.constructMapType(HashMap.class, String.class, String.class);
634

    
635
            Map<String,String> statusColorMap = mapper.readValue(statusColorJson, mapType);
636
            presenceAbsenceTermColors = new HashMap<>();
637
            PresenceAbsenceTerm paTerm = null;
638
            for(String statusId : statusColorMap.keySet()){
639
                try {
640
                    Color color = Color.decode(statusColorMap.get(statusId));
641
                    // the below loop is  a hack for #4522 (custom status colors not working in cyprus portal)
642
                    // remove it once the ticket is solved
643
                    for(UUID vocabUuid : presenceAbsenceTermVocabularyUuids(vocabularyService)) {
644
                        paTerm = termService.findByIdInVocabulary(statusId, vocabUuid, PresenceAbsenceTerm.class);
645
                        if(paTerm != null) {
646
                            break;
647
                        }
648
                    }
649
                    if(paTerm != null){
650
                        presenceAbsenceTermColors.put(paTerm, color);
651
                    }
652
                } catch (NumberFormatException e){
653
                    logger.error("Cannot decode color", e);
654
                }
655
            }
656
        }
657
        return presenceAbsenceTermColors;
658
    }
659

    
660
    /**
661
     * this is a hack for #4522 (custom status colors not working in cyprus portal)
662
     * remove this method once the ticket is solved
663
     *
664
     * @param vocabularyService
665
     * @return
666
     */
667
    private static List<UUID> presenceAbsenceTermVocabularyUuids(IVocabularyService vocabularyService) {
668

    
669
        if(EditGeoServiceUtilities.presenceAbsenceTermVocabularyUuids == null) {
670

    
671
            List<UUID> uuids = new ArrayList<>();
672
            // the default as first entry
673
            UUID presenceTermVocabUuid = PresenceAbsenceTerm.NATIVE().getVocabulary().getUuid();
674
            uuids.add(presenceTermVocabUuid);
675

    
676
            for(TermVocabulary<?> vocab : vocabularyService.findByTermType(TermType.PresenceAbsenceTerm, null)) {
677
                if(!uuids.contains(vocab.getUuid())) {
678
                    uuids.add(vocab.getUuid());
679
                }
680
            }
681
            EditGeoServiceUtilities.presenceAbsenceTermVocabularyUuids = uuids;
682
        }
683
        return EditGeoServiceUtilities.presenceAbsenceTermVocabularyUuids;
684
    }
685

    
686
    public static CondensedDistribution getCondensedDistribution(Collection<Distribution> filteredDistributions,
687
            CondensedDistributionRecipe recipe, List<Language> langs) {
688

    
689
        ICondensedDistributionComposer composer;
690
        if(recipe == null) {
691
            throw new NullPointerException("parameter recipe must not be null");
692
        }
693
        try {
694
            composer = recipe.newCondensedDistributionComposerInstance();
695
        } catch (InstantiationException|IllegalAccessException e) {
696
            throw new RuntimeException(e);
697
        }
698
        CondensedDistribution condensedDistribution = composer.createCondensedDistribution(
699
                filteredDistributions,  langs);
700
        return condensedDistribution;
701
    }
702
}
(4-4/14)