Project

General

Profile

Download (30.2 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.util.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
import eu.etaxonomy.cdm.strategy.cache.TaggedText;
56

    
57
/**
58
 * Class implementing the business logic for creating the map service string for
59
 * a given set of distributions. See {@link EditGeoService} as API for the given functionality.
60
 *
61
 * @see EditGeoService
62
 *
63
 * @author a.mueller
64
 * @since 17.11.2008
65
 */
66
public class EditGeoServiceUtilities {
67
    private static final Logger logger = Logger.getLogger(EditGeoServiceUtilities.class);
68

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

    
71
    private static final String SUBENTRY_DELIMITER = ",";
72
    private static final String ID_FROM_VALUES_SEPARATOR = ":";
73
    private static final String VALUE_LIST_ENTRY_SEPARATOR = "|";
74
    private static final String VALUE_SUPER_LIST_ENTRY_SEPARATOR = "||";
75

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

    
86
    private static HashMap<SpecimenOrObservationType, Color> getDefaultSpecimenOrObservationTypeColors() {
87
        return defaultSpecimenOrObservationTypeColors;
88
    }
89

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

    
103
    private static List<UUID>  presenceAbsenceTermVocabularyUuids = null;
104

    
105
    private static HashMap<PresenceAbsenceTerm, Color> getDefaultPresenceAbsenceTermBaseColors() {
106
        return defaultPresenceAbsenceTermBaseColors;
107
    }
108

    
109

    
110
    /**
111
     * @param filteredDistributions
112
     *            A set of distributions a condensed distribution string should
113
     *            be created for.
114
     *            The set should guarantee that for each area not more than
115
     *            1 status exists, otherwise the behavior is not deterministic.
116
     *            For filtering see {@link DescriptionUtility#filterDistributions(
117
     *            Collection, Set, boolean, boolean, boolean, boolean, boolean)}
118
     * @param config
119
     *            The configuration for the condensed distribution string creation.
120
     * @param languages
121
     *            A list of preferred languages in case the status or area symbols are
122
     *            to be taken from the language abbreviations (not really in use)
123
     *            TODO could be moved to configuration or fully removed
124
     * @return
125
     *            A CondensedDistribution object that contains a string representation
126
     *            and a {@link TaggedText} representation of the condensed distribution string.
127
     */
128
    public static CondensedDistribution getCondensedDistribution(Collection<Distribution> filteredDistributions,
129
            CondensedDistributionConfiguration config, List<Language> languages) {
130

    
131
        CondensedDistributionComposer composer = new CondensedDistributionComposer();
132

    
133
        CondensedDistribution condensedDistribution = composer.createCondensedDistribution(
134
                filteredDistributions, languages, config);
135
        return condensedDistribution;
136
    }
137

    
138
    /**
139
     * Returns the parameter String for the EDIT geo webservice to create a
140
     * distribution map.
141
     *
142
     * @param filteredDistributions
143
     *            A set of distributions that should be shown on the map.
144
     *            The set should guarantee that for each area not more than
145
     *            1 status exists, otherwise the behavior is not deterministic.
146
     *            For filtering see {@link DescriptionUtility#filterDistributions(
147
     *            Collection, Set, boolean, boolean, boolean, boolean, boolean)}
148
     * @param mapping
149
     *            Data regarding the mapping of NamedAreas to shape file
150
     *            attribute tables
151
     * @param presenceAbsenceTermColors
152
     *            A map that defines the colors of PresenceAbsenceTerms. The
153
     *            PresenceAbsenceTerms are defined by their uuid. If a
154
     *            PresenceAbsenceTerm is not included in this map, it's default
155
     *            color is taken instead. If the map == null all terms are
156
     *            colored by their default color.
157
     * @param projectToLayer
158
     *            name of a layer which is representing a specific
159
     *            {@link NamedAreaLevel} Supply this parameter if you to project
160
     *            all other distribution area levels to this layer.
161
     * @param languages
162
     *
163
     * @return the parameter string or an empty string if the
164
     *         <code>distributions</code> set was null or empty.
165
     */
166
    @Transient
167
    public static String getDistributionServiceRequestParameterString(
168
            Collection<Distribution> filteredDistributions,
169
            IGeoServiceAreaMapping mapping,
170
            Map<PresenceAbsenceTerm,Color> presenceAbsenceTermColors,
171
            String projectToLayer,
172
            List<Language> languages){
173

    
174
        /*
175
         * generateMultipleAreaDataParameters switches between the two possible styles:
176
         * 1. ad=layername1:area-data||layername2:area-data
177
         * 2. ad=layername1:area-data&ad=layername2:area-data
178
         */
179
        boolean generateMultipleAreaDataParameters = false;
180

    
181
        List<String>  perLayerAreaData = new ArrayList<>();
182
        Map<Integer, String> areaStyles = new HashMap<>();
183
        List<String> legendSortList = new ArrayList<>();
184

    
185
        String borderWidth = "0.1";
186
        String borderColorRgb = "";
187
        String borderDashingPattern = "";
188

    
189
        //handle empty set
190
        if(filteredDistributions == null || filteredDistributions.size() == 0){
191
            return "";
192
        }
193

    
194
        presenceAbsenceTermColors = mergeMaps(getDefaultPresenceAbsenceTermBaseColors(), presenceAbsenceTermColors);
195

    
196
        Map<String, Map<Integer, Set<Distribution>>> layerMap = new HashMap<>();
197
        List<PresenceAbsenceTerm> statusList = new ArrayList<>();
198

    
199
        groupStylesAndLayers(filteredDistributions, layerMap, statusList, mapping);
200

    
201
        Map<String, String> parameters = new HashMap<>();
202

    
203
        //style
204
        int styleCounter = 0;
205
        for (PresenceAbsenceTerm status: statusList){
206

    
207
            char styleCode = getStyleAbbrev(styleCounter);
208

    
209
            //getting the area title
210
            if (languages == null){
211
                languages = new ArrayList<>();
212
            }
213
            if (languages.size() == 0){
214
                languages.add(Language.DEFAULT());
215
            }
216
            Representation statusRepresentation = status.getPreferredRepresentation(languages);
217

    
218
            //getting the area color
219
            Color statusColor = presenceAbsenceTermColors.get(status);
220
            String fillColorRgb;
221
            if (statusColor != null){
222
                fillColorRgb = Integer.toHexString(statusColor.getRGB()).substring(2);
223
            }else{
224
                fillColorRgb = status.getDefaultColor(); //TODO
225
            }
226
            String styleValues = StringUtils.join(new String[]{fillColorRgb, borderColorRgb, borderWidth, borderDashingPattern}, ',');
227

    
228
            areaStyles.put(styleCounter, styleValues);
229

    
230
            String legendEntry = styleCode + ID_FROM_VALUES_SEPARATOR + encode(statusRepresentation.getLabel());
231
            legendSortList.add(StringUtils.leftPad(String.valueOf(status.getOrderIndex()), INT_MAX_LENGTH, '0') + legendEntry );
232
            styleCounter++;
233
        }
234

    
235
        // area data
236
        List<String> styledAreasPerLayer;
237
        List<String> areasPerStyle;
238
        /**
239
         * Map<Integer, Integer> styleUsage
240
         *
241
         * Used to avoid reusing styles in multiple layers
242
         *
243
         * key: the style id
244
         * value: the count of how often the style has been used for different layers, starts with 0 for first time use
245
         */
246
        Map<Integer, Integer> styleUsage = new HashMap<>();
247

    
248
        char styleChar;
249
        for (String layerString : layerMap.keySet()){
250
            // each layer
251
            styledAreasPerLayer = new ArrayList<>();
252
            Map<Integer, Set<Distribution>> styleMap = layerMap.get(layerString);
253
            for (int style: styleMap.keySet()){
254
                // stylesPerLayer
255
                styleChar = getStyleAbbrev(style);
256
                Set<Distribution> distributionSet = styleMap.get(style);
257
                areasPerStyle = new ArrayList<>();
258
                for (Distribution distribution: distributionSet){
259
                    // areasPerStyle
260
                    areasPerStyle.add(encode(getAreaCode(distribution, mapping)));
261
                }
262
                styledAreasPerLayer.add(styleChar + ID_FROM_VALUES_SEPARATOR + StringUtils.join(areasPerStyle.iterator(), SUBENTRY_DELIMITER));
263
            }
264
            perLayerAreaData.add(encode(layerString) + ID_FROM_VALUES_SEPARATOR + StringUtils.join(styledAreasPerLayer.iterator(), VALUE_LIST_ENTRY_SEPARATOR));
265
        }
266

    
267
        if(areaStyles.size() > 0){
268
            ArrayList<Integer> styleIds = new ArrayList<>(areaStyles.size());
269
            styleIds.addAll(areaStyles.keySet());
270
            Collections.sort(styleIds); // why is it necessary to sort here?
271
            StringBuilder db = new StringBuilder();
272
            for(Integer sid : styleIds){
273
                if(db.length() > 0){
274
                    db.append(VALUE_LIST_ENTRY_SEPARATOR);
275
                }
276
                db.append( getStyleAbbrev(sid)).append(ID_FROM_VALUES_SEPARATOR).append(areaStyles.get(sid));
277
            }
278
            parameters.put("as", db.toString());
279
        }
280
        if(legendSortList.size() > 0){
281
            // sort the label entries after the status terms
282
            Collections.sort(legendSortList);
283
            // since the status terms are have an inverse natural order
284
            // (as all other ordered term, see OrderedTermBase.performCompareTo(T orderedTerm, boolean skipVocabularyCheck)
285
            // the sorted list must be reverted
286
//            Collections.reverse(legendSortList);
287
            // remove the prepended order index (like 000000000000001 ) from the legend entries
288
            @SuppressWarnings("unchecked")
289
            Collection<String> legendEntries = CollectionUtils.collect(legendSortList, (o)->{
290
                      String s = ((String) o);
291
                      return s.substring(INT_MAX_LENGTH, s.length());
292
                  });
293

    
294
            parameters.put("title", StringUtils.join(legendEntries.iterator(), VALUE_LIST_ENTRY_SEPARATOR));
295
        }
296

    
297
        if(generateMultipleAreaDataParameters){
298
            // not generically possible since parameters can not contain duplicate keys with value "ad"
299
        } else {
300
            parameters.put("ad", StringUtils.join(perLayerAreaData.iterator(), VALUE_SUPER_LIST_ENTRY_SEPARATOR));
301
        }
302

    
303
        String queryString = makeQueryString(parameters);
304
        logger.debug("getDistributionServiceRequestParameterString(): " + queryString);
305

    
306
        return queryString;
307
    }
308

    
309

    
310
    /**
311
     * Fills the layerMap and the statusList
312
     *
313
     * @param distributions
314
     * @param layerMap see {@link #addAreaToLayerMap(Map, List, Distribution, NamedArea, IGeoServiceAreaMapping)}
315
     * @param statusList
316
     */
317
    private static void groupStylesAndLayers(Collection<Distribution> distributions,
318
            Map<String, Map<Integer,Set<Distribution>>> layerMap,
319
            List<PresenceAbsenceTerm> statusList,
320
            IGeoServiceAreaMapping mapping) {
321

    
322

    
323
        //iterate through distributions and group styles and layers
324
        //and collect necessary information
325
        for (Distribution distribution : distributions){
326
            //collect status
327
            PresenceAbsenceTerm status = distribution.getStatus();
328
            if(status == null){
329
                continue;
330
            }
331
            status = HibernateProxyHelper.deproxy(status);
332
            if (! statusList.contains(status)){
333
                statusList.add(status);
334
            }
335
            //group areas by layers and styles
336
            NamedArea area = distribution.getArea();
337

    
338
            addAreaToLayerMap(layerMap, statusList, distribution, area, mapping);
339
        }
340
    }
341

    
342
    /**
343
     * Adds the areas to the layer map. Areas which do not have layer information
344
     * mapped to them are ignored.
345
     * <p>
346
     * A layer map holds the following information:
347
     *
348
     * <ul>
349
     *   <li><b>String</b>: the WMSLayerName which matches the level of the
350
     *   contained distributions areas</li>
351
     *   <li><b>StyleMap</b>:</li>
352
     *   <ul>
353
     *     <li><b>Integer</b>: the index of the status in the
354
     *     <code>statusList</code></li>
355
     *     <li><b>Set{@code<Distribution>}</b>: the set of distributions having the
356
     *     same Status, the status list is populated in {@link #groupStylesAndLayers(Set, Map, List, IGeoServiceAreaMapping)}</li>
357
     *   </ul>
358
     * </ul>
359
     *
360
     * @param layerMap
361
     * @param statusList
362
     * @param distribution
363
     * @param area
364
     */
365
    private static void addAreaToLayerMap(Map<String, Map<Integer,
366
            Set<Distribution>>> layerMap,
367
            List<PresenceAbsenceTerm> statusList,
368
            Distribution distribution,
369
            NamedArea area,
370
            IGeoServiceAreaMapping mapping) {
371

    
372
        if (area != null){
373
            String geoLayerName = getWMSLayerName(area, mapping);
374

    
375
            if(geoLayerName == null){
376
               logger.warn("no wms layer mapping defined for " + area.getLabel() + " [" + area.getIdInVocabulary() + "]");
377
            } else {
378
                Map<Integer, Set<Distribution>> styleMap = layerMap.get(geoLayerName);
379
                if (styleMap == null) {
380
                    styleMap = new HashMap<>();
381
                    layerMap.put(geoLayerName, styleMap);
382
                }
383
                addDistributionToStyleMap(distribution, styleMap, statusList);
384
            }
385
        }
386
    }
387

    
388
    /**
389
     * URI encode the given String
390
     */
391
    private static String encode(String string) {
392
        String encoded = string;
393
        try {
394
            encoded = URLEncoder.encode(string, "UTF-8");
395
        } catch (UnsupportedEncodingException e) {
396
            logger.error(e);
397
        }
398
        return encoded;
399
    }
400

    
401
    /**
402
     * combine parameter into a URI query string fragment. The values will be
403
     * escaped correctly.
404
     *
405
     * @param parameters
406
     * @return a URI query string fragment
407
     */
408
    private static String makeQueryString(Map<String, String> parameters){
409
        StringBuilder queryString = new StringBuilder();
410
        for (String key : parameters.keySet()) {
411
            if(queryString.length() > 0){
412
                queryString.append('&');
413
            }
414
            if(key.equals("od") || key.equals("os") || key.equals("ms") || key.equals("ad") || key.equals("as") || key.equals("title") || key.equals("bbox")){
415
                queryString.append(key).append('=').append(parameters.get(key));
416
            } else {
417
                queryString.append(key).append('=').append(encode(parameters.get(key)));
418
            }
419
        }
420
        return queryString.toString();
421
    }
422

    
423
    private static String getAreaCode(Distribution distribution, IGeoServiceAreaMapping mapping){
424

    
425
        NamedArea area = distribution.getArea();
426
        TermVocabulary<NamedArea> voc = area.getVocabulary();
427
        String result = null;
428

    
429
        if (voc != null && (voc.getUuid().equals(NamedArea.uuidTdwgAreaVocabulary) ||  voc.getUuid().equals(Country.uuidCountryVocabulary))) {
430
            // TDWG or Country
431
            result = area.getIdInVocabulary();
432
            if (area.getLevel() != null && area.getLevel().equals(NamedAreaLevel.TDWG_LEVEL4())) {
433
                result = result.replace("-", "");
434
            }
435
        } else {
436
            // use generic GeoServiceArea data stored in technical annotations
437
            // of the
438
            // named area
439
            GeoServiceArea areas = mapping.valueOf(area);
440
            if (areas != null && areas.size() > 0) {
441
                // FIXME multiple layers
442
                List<String> values = areas.getAreasMap().values().iterator().next().values().iterator().next();
443
                for (String value : values) {
444
                    result = CdmUtils.concat(SUBENTRY_DELIMITER, result, value);
445
                }
446
            }
447
        }
448
        return CdmUtils.Nz(result, "-");
449
    }
450

    
451
    private static List<String> projectToWMSSubLayer(NamedArea area){
452

    
453
        List<String> layerNames = new ArrayList<>();
454
        String matchedLayerName = null;
455
        TermVocabulary<NamedArea> voc = area.getVocabulary();
456
        //TDWG areas
457
        if (voc.getUuid().equals(NamedArea.uuidTdwgAreaVocabulary)){
458
            NamedAreaLevel level = area.getLevel();
459
            if (level != null) {
460
                //TODO integrate into CDM
461
                if (level.equals(NamedAreaLevel.TDWG_LEVEL1())) {
462
                    matchedLayerName = "tdwg1" ;
463
                } else if (level.equals(NamedAreaLevel.TDWG_LEVEL2())) {
464
                    matchedLayerName = "tdwg2";
465
                }else if (level.equals(NamedAreaLevel.TDWG_LEVEL3())) {
466
                    matchedLayerName = "tdwg3";
467
                }else if (level.equals(NamedAreaLevel.TDWG_LEVEL4())) {
468
                    matchedLayerName = "tdwg4";
469
                }
470
            }
471
            //unrecognized tdwg area
472

    
473
        }
474
        //TODO countries
475

    
476
        // check if the matched layer equals the layer to project to
477
        // if not: recurse into the sub-level in order to find the specified one.
478
        String[] matchedLayerNameTokens = StringUtils.split(matchedLayerName, ':');
479
//		if(matchedLayerNameTokens.length > 0 &&  matchedLayerNameTokens[0] != projectToLayer){
480
//			for (NamedArea subArea : area.getIncludes()){
481
//
482
//			}
483
            //
484
            // add all sub areas
485
//		}
486

    
487
        return null;
488
    }
489

    
490
    private static String getWMSLayerName(NamedArea area, IGeoServiceAreaMapping mapping){
491
        TermVocabulary<NamedArea> voc = area.getVocabulary();
492
        //TDWG areas
493
        if (voc.getUuid().equals(NamedArea.uuidTdwgAreaVocabulary)){
494
            NamedAreaLevel level = area.getLevel();
495
            if (level != null) {
496
                //TODO integrate into CDM
497
                if (level.equals(NamedAreaLevel.TDWG_LEVEL1())) {
498
                    return "tdwg1";
499
                } else if (level.equals(NamedAreaLevel.TDWG_LEVEL2())) {
500
                    return "tdwg2";
501
                }else if (level.equals(NamedAreaLevel.TDWG_LEVEL3())) {
502
                    return "tdwg3";
503
                }else if (level.equals(NamedAreaLevel.TDWG_LEVEL4())) {
504
                    return "tdwg4";
505
                }
506
            }
507
            //unrecognized tdwg area
508
            return null;
509
        }else if (voc.getUuid().equals(Country.uuidCountryVocabulary)){
510
            return "country_earth:gmi_cntry";
511
        }
512

    
513
        GeoServiceArea areas = mapping.valueOf(area);
514
        if (areas != null && areas.getAreasMap().size() > 0){
515
            //FIXME multiple layers
516
            String layer = areas.getAreasMap().keySet().iterator().next();
517
            Map<String, List<String>> fields = areas.getAreasMap().get(layer);
518
            String field = fields.keySet().iterator().next();
519
            String layerString = layer + ":" + field;
520
            return layerString.toLowerCase();
521
        }
522

    
523
        return null;
524
    }
525

    
526
    private static void addDistributionToStyleMap(Distribution distribution, Map<Integer, Set<Distribution>> styleMap,
527
            List<PresenceAbsenceTerm> statusList) {
528
        PresenceAbsenceTerm status = distribution.getStatus();
529
        if (status != null) {
530
            int style = statusList.indexOf(status);
531
            Set<Distribution> distributionSet = styleMap.get(style);
532
            if (distributionSet == null) {
533
                distributionSet = new HashSet<Distribution>();
534
                styleMap.put(style, distributionSet);
535
            }
536
            distributionSet.add(distribution);
537
        }
538
    }
539

    
540
    /**
541
     * @param fieldUnitPoints
542
     * @param derivedUnitPoints
543
     * @param specimenOrObservationTypeColors
544
     * @param width
545
     * @param height
546
     * @param bbox
547
     * @param backLayer
548
     * @return
549
     * e.g.:
550
     * 	l=v%3Aatbi%2Ce_w_0
551
     *  &legend=0
552
     *  &image=false
553
     *  &recalculate=false
554
     *  &ms=400%2C350
555

    
556
     *  &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
557
     *  &os=1%3Ac%2FFFD700%2F10%2FAporrectodea caliginosa
558
     */
559
    public static OccurrenceServiceRequestParameterDto getOccurrenceServiceRequestParameterString(
560
            List<Point> fieldUnitPoints,
561
            List<Point> derivedUnitPoints,
562
            Map<SpecimenOrObservationType, Color> specimenOrObservationTypeColors) {
563

    
564
        OccurrenceServiceRequestParameterDto dto = new OccurrenceServiceRequestParameterDto();
565

    
566
        specimenOrObservationTypeColors = mergeMaps(getDefaultSpecimenOrObservationTypeColors(), specimenOrObservationTypeColors);
567

    
568
        Map<String, String> parameters = new HashMap<>();
569
        parameters.put("legend", "0");
570

    
571
        Map<String, String> styleAndData = new HashMap<>();
572

    
573
        addToStyleAndData(fieldUnitPoints, SpecimenOrObservationType.FieldUnit, specimenOrObservationTypeColors, styleAndData);
574
        addToStyleAndData(derivedUnitPoints, SpecimenOrObservationType.DerivedUnit, specimenOrObservationTypeColors, styleAndData);
575

    
576
        parameters.put("os", StringUtils.join(styleAndData.keySet().iterator(), "||"));
577
        parameters.put("od", StringUtils.join(styleAndData.values().iterator(), "||"));
578

    
579
        String queryString = makeQueryString(parameters);
580

    
581
        dto.setFieldUnitPoints(fieldUnitPoints);
582
        dto.setDerivedUnitPoints(derivedUnitPoints);
583
        dto.setOccurrenceQuery(queryString);
584

    
585
        logger.info(queryString);
586

    
587
        return dto;
588
    }
589

    
590
    private static <T, S> Map<T, S> mergeMaps(Map<T, S> defaultMap, Map<T, S> overrideMap) {
591
        Map<T, S> tmpMap = new HashMap<T, S>();
592
        tmpMap.putAll(defaultMap);
593
        if(overrideMap != null){
594
            tmpMap.putAll(overrideMap);
595
        }
596
        return tmpMap;
597
    }
598

    
599
    private static void addToStyleAndData(
600
            List<Point> points,
601
            SpecimenOrObservationType specimenOrObservationType,
602
            Map<SpecimenOrObservationType, Color> specimenOrObservationTypeColors, Map<String, String> styleAndData) {
603

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

    
606
        if(points != null && points.size()>0){
607
            String style =  "c/" + Integer.toHexString(specimenOrObservationTypeColors.get(specimenOrObservationType).getRGB()).substring(2) + "/10/noLabel";
608
            StringBuilder data = new StringBuilder();
609
            for(Point point : points){
610
                if (point != null){  //should not be null, but just in case
611
                    if(data.length() > 0){
612
                        data.append('|');
613
                    }
614
                    data.append(point.getLatitude() + "," + point.getLongitude());
615
                }
616
            }
617
            int index = styleAndData.size() + 1;
618
            styleAndData.put(index + ":" +style, index + ":" +data.toString());
619
        }
620
    }
621

    
622
    /**
623
     * transform an integer (style counter) into a valid character representing a style.
624
     * 0-25 => a-z<br>
625
     * 26-51 => A-Z<br>
626
     * i not in {0,...,51} is undefined
627
     */
628
    private static char getStyleAbbrev(int i){
629
        i++;
630
        int ascii = 96 + i;
631
        if (i >26){
632
            ascii = 64 + i;
633
        }
634
        return (char)ascii;
635
    }
636

    
637
    /**
638
     * @param statusColorJson for example: {@code {"n":"#ff0000","p":"#ffff00"}}
639
     */
640
    public static Map<PresenceAbsenceTerm, Color> buildStatusColorMap(String statusColorJson,
641
            ITermService termService, IVocabularyService vocabularyService)
642
            throws IOException, JsonParseException, JsonMappingException {
643

    
644
        Map<PresenceAbsenceTerm, Color> presenceAbsenceTermColors = null;
645
        if(StringUtils.isNotEmpty(statusColorJson)){
646

    
647
            ObjectMapper mapper = new ObjectMapper();
648
            // TODO cache the color maps to speed this up?
649

    
650
            TypeFactory typeFactory = mapper.getTypeFactory();
651
            MapType mapType = typeFactory.constructMapType(HashMap.class, String.class, String.class);
652

    
653
            Map<String,String> statusColorMap = mapper.readValue(statusColorJson, mapType);
654
            presenceAbsenceTermColors = new HashMap<>();
655
            PresenceAbsenceTerm paTerm = null;
656
            for(String statusId : statusColorMap.keySet()){
657
                try {
658
                    Color color = Color.decode(statusColorMap.get(statusId));
659
                    // the below loop is  a hack for #4522 (custom status colors not working in cyprus portal)
660
                    // remove it once the ticket is solved
661
                    for(UUID vocabUuid : presenceAbsenceTermVocabularyUuids(vocabularyService)) {
662
                        paTerm = termService.findByIdInVocabulary(statusId, vocabUuid, PresenceAbsenceTerm.class);
663
                        if(paTerm != null) {
664
                            break;
665
                        }
666
                    }
667
                    if(paTerm != null){
668
                        presenceAbsenceTermColors.put(paTerm, color);
669
                    }
670
                } catch (NumberFormatException e){
671
                    logger.error("Cannot decode color", e);
672
                }
673
            }
674
        }
675
        return presenceAbsenceTermColors;
676
    }
677

    
678
    /**
679
     * this is a hack for #4522 (custom status colors not working in cyprus portal)
680
     * remove this method once the ticket is solved
681
     *
682
     * @param vocabularyService
683
     * @return
684
     */
685
    private static List<UUID> presenceAbsenceTermVocabularyUuids(IVocabularyService vocabularyService) {
686

    
687
        if(EditGeoServiceUtilities.presenceAbsenceTermVocabularyUuids == null) {
688

    
689
            List<UUID> uuids = new ArrayList<>();
690
            // the default as first entry
691
            UUID presenceTermVocabUuid = PresenceAbsenceTerm.NATIVE().getVocabulary().getUuid();
692
            uuids.add(presenceTermVocabUuid);
693

    
694
            for(TermVocabulary<?> vocab : vocabularyService.findByTermType(TermType.PresenceAbsenceTerm, null)) {
695
                if(!uuids.contains(vocab.getUuid())) {
696
                    uuids.add(vocab.getUuid());
697
                }
698
            }
699
            EditGeoServiceUtilities.presenceAbsenceTermVocabularyUuids = uuids;
700
        }
701
        return EditGeoServiceUtilities.presenceAbsenceTermVocabularyUuids;
702
    }
703

    
704
}
(5-5/11)