5f20ab72909d9ba5f9cf59df7a1f8592f5dba7a3
[cdmlib.git] / cdmlib-services / src / main / java / eu / etaxonomy / cdm / api / service / geo / DistributionServiceImpl.java
1 /**
2 * Copyright (C) 2023 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 package eu.etaxonomy.cdm.api.service.geo;
10
11 import java.awt.Color;
12 import java.io.IOException;
13 import java.io.Reader;
14 import java.util.ArrayList;
15 import java.util.Arrays;
16 import java.util.Collection;
17 import java.util.Collections;
18 import java.util.EnumSet;
19 import java.util.HashSet;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Set;
23 import java.util.UUID;
24 import java.util.stream.Collectors;
25
26 import javax.persistence.EntityNotFoundException;
27
28 import org.apache.logging.log4j.LogManager;
29 import org.apache.logging.log4j.Logger;
30 import org.springframework.beans.factory.annotation.Autowired;
31 import org.springframework.stereotype.Service;
32 import org.springframework.transaction.annotation.Transactional;
33
34 import eu.etaxonomy.cdm.api.dto.portal.DistributionDto;
35 import eu.etaxonomy.cdm.api.dto.portal.DistributionInfoDto;
36 import eu.etaxonomy.cdm.api.dto.portal.DistributionInfoDto.InfoPart;
37 import eu.etaxonomy.cdm.api.dto.portal.IDistributionTree;
38 import eu.etaxonomy.cdm.api.dto.portal.config.DistributionInfoConfiguration;
39 import eu.etaxonomy.cdm.api.dto.portal.config.DistributionOrder;
40 import eu.etaxonomy.cdm.api.service.portal.PortalDtoLoader;
41 import eu.etaxonomy.cdm.common.CdmUtils;
42 import eu.etaxonomy.cdm.common.SetMap;
43 import eu.etaxonomy.cdm.format.description.distribution.CondensedDistribution;
44 import eu.etaxonomy.cdm.format.description.distribution.CondensedDistributionConfiguration;
45 import eu.etaxonomy.cdm.model.common.CdmBase;
46 import eu.etaxonomy.cdm.model.common.Language;
47 import eu.etaxonomy.cdm.model.common.MarkerType;
48 import eu.etaxonomy.cdm.model.description.DescriptionElementBase;
49 import eu.etaxonomy.cdm.model.description.Distribution;
50 import eu.etaxonomy.cdm.model.description.Feature;
51 import eu.etaxonomy.cdm.model.description.PresenceAbsenceTerm;
52 import eu.etaxonomy.cdm.model.description.TaxonDescription;
53 import eu.etaxonomy.cdm.model.location.NamedArea;
54 import eu.etaxonomy.cdm.model.location.NamedAreaLevel;
55 import eu.etaxonomy.cdm.model.term.DefinedTermBase;
56 import eu.etaxonomy.cdm.model.term.TermNode;
57 import eu.etaxonomy.cdm.model.term.TermTree;
58 import eu.etaxonomy.cdm.model.term.TermVocabulary;
59 import eu.etaxonomy.cdm.persistence.dao.description.IDescriptionDao;
60 import eu.etaxonomy.cdm.persistence.dao.term.IDefinedTermDao;
61 import eu.etaxonomy.cdm.persistence.dao.term.ITermTreeDao;
62 import eu.etaxonomy.cdm.persistence.dao.term.ITermVocabularyDao;
63
64 /**
65 * @author a.mueller
66 * @date 08.02.2023
67 */
68 @Service
69 @Transactional(readOnly = true)
70 public class DistributionServiceImpl implements IDistributionService {
71
72 @SuppressWarnings("unused")
73 private static final Logger logger = LogManager.getLogger();
74
75 @Autowired
76 private IDescriptionDao dao;
77
78 @Autowired
79 private IDefinedTermDao termDao;
80
81 @Autowired
82 private ITermTreeDao termTreeDao;
83
84 @Autowired
85 private ITermVocabularyDao vocabDao;
86
87 @Autowired
88 private IGeoServiceAreaMapping areaMapping;
89
90 @Override
91 public DistributionInfoDto composeDistributionInfoFor(DistributionInfoConfiguration config, UUID taxonUUID,
92 boolean neverUseFallbackAreaAsParent,
93 Map<PresenceAbsenceTerm, Color> presenceAbsenceTermColors,
94 List<Language> languages, List<String> propertyPaths){
95
96 Set<UUID> featureUuids = config.getFeatures();
97 Set<Feature> features = null;
98 if (!CdmUtils.isNullSafeEmpty(featureUuids)) {
99 features = termDao.list(featureUuids, null, null, null, null)
100 .stream().filter(t->t.isInstanceOf(Feature.class)).map(t->(Feature)t).collect(Collectors.toSet());
101 }
102
103 if (propertyPaths == null){
104 propertyPaths = Arrays.asList(new String []{});
105 }
106 // Adding default initStrategies to improve the performance of this method
107 // adding 'status' and 'area' has a good positive effect:
108 // filterDistributions() only takes 21% of the total method time (before it was 46%)
109 // at the same time the cost of the getDescriptionElementForTaxon is not increased at all!
110 //
111 // adding 'markers.markerType' is not improving the performance since it only
112 // moved the load from the filter method to the getDescriptionElementForTaxon()
113 // method.
114 // overall improvement by this means is by 42% (from 77,711 ms to 44,868 ms)
115 ArrayList<String> initStrategy = new ArrayList<>(propertyPaths);
116 if(!initStrategy.contains("status")) {
117 initStrategy.add("status");
118 }
119 if(!initStrategy.contains("area")) {
120 initStrategy.add("area");
121 }
122 if(!initStrategy.contains("markers.markerType")) {
123 initStrategy.add("markers.markerType");
124 }
125
126 List<Distribution> distributions = dao.getDescriptionElementForTaxon(
127 taxonUUID, features, Distribution.class, config.isIncludeUnpublished(), null, null, initStrategy);
128
129 return composeDistributionInfoFor(config, distributions, neverUseFallbackAreaAsParent, presenceAbsenceTermColors, languages);
130 }
131
132 @Override
133 public DistributionInfoDto composeDistributionInfoFor(DistributionInfoConfiguration config, List<Distribution> distributions,
134 boolean neverUseFallbackAreaAsParent, Map<PresenceAbsenceTerm, Color> presenceAbsenceTermColors,
135 List<Language> languages){
136
137 EnumSet<DistributionInfoDto.InfoPart> parts = config.getInfoParts();
138 boolean subAreaPreference = config.isPreferSubareas();
139 boolean statusOrderPreference = config.isStatusOrderPreference();
140 Set<MarkerType> fallbackAreaMarkerTypes = config.getFallbackAreaMarkerTypeList();
141 Set<MarkerType> alternativeRootAreaMarkerTypes = config.getAlternativeRootAreaMarkerTypes();
142 Set<NamedAreaLevel> omitLevels = config.getOmitLevels();
143 CondensedDistributionConfiguration condensedDistConfig = config.getCondensedDistributionConfiguration();
144 DistributionOrder distributionOrder = config.getDistributionOrder();
145
146 final boolean PREFER_AGGREGATED = true;
147 final boolean PREFER_SUBAREA = true;
148
149 DistributionInfoDto dto = new DistributionInfoDto();
150
151 if(omitLevels == null) {
152 @SuppressWarnings("unchecked") //2 lines to allow unchecked annotation
153 Set<NamedAreaLevel> emptySet = Collections.EMPTY_SET;
154 omitLevels = emptySet;
155 }
156
157 //area tree
158 TermTree<NamedArea> areaTree = getPersistentAreaTree(distributions, config);
159 if (areaTree == null) {
160 //TODO better use areaTree created within filterDistributions(...) but how to get it easily?
161 areaTree = DistributionServiceUtilities.getAreaTree(distributions, fallbackAreaMarkerTypes);
162 }
163
164 //TODO unify to use only the node map
165 SetMap<NamedArea, NamedArea> parentAreaMap = areaTree.getParentMap();
166 SetMap<NamedArea, TermNode<NamedArea>> parentAreaNodeMap = areaTree.getParentNodeMap();
167 SetMap<NamedArea, TermNode<NamedArea>> area2TermNodesMap = areaTree.getTermNodesMap();
168
169 //status tree
170 TermTree<PresenceAbsenceTerm> statusTree = getPersistentStatusTree(config);
171
172 // for all later applications apply the rules statusOrderPreference, hideHiddenArea
173 // and ignoreUndefinedStatus to all distributions, but KEEP fallback area distributions
174 boolean keepFallBackOnlyIfNoSubareaDataExists = false;
175 Set<Distribution> filteredDistributions = DistributionServiceUtilities.filterDistributions(distributions,
176 areaTree, statusTree, fallbackAreaMarkerTypes, !PREFER_AGGREGATED, statusOrderPreference, !PREFER_SUBAREA,
177 keepFallBackOnlyIfNoSubareaDataExists);
178
179 if(parts.contains(InfoPart.elements)) {
180 dto.setElements(filteredDistributions);
181 }
182
183 if(parts.contains(InfoPart.tree)) {
184 IDistributionTree tree;
185 if (config.isUseTreeDto()) {
186
187 //transform distributions to distribution DTOs
188 Set<DistributionDto> filteredDtoDistributions = new HashSet<>();
189 for (Distribution distribution : filteredDistributions) {
190 DistributionDto distDto = new DistributionDto(distribution, parentAreaMap);
191 PortalDtoLoader.loadBaseData(distribution, distDto);
192 distDto.setTimeperiod(distribution.getTimeperiod() == null ? null : distribution.getTimeperiod().toString());
193 filteredDtoDistributions.add(distDto);
194 }
195
196 boolean useSecondMethod = false;
197 tree = DistributionServiceUtilities.buildOrderedTreeDto(omitLevels,
198 filteredDtoDistributions, parentAreaMap, areaTree, fallbackAreaMarkerTypes,
199 alternativeRootAreaMarkerTypes, neverUseFallbackAreaAsParent,
200 distributionOrder, termDao, useSecondMethod);
201 }else {
202 //version with model entities as used in direct webservice (not taxon page DTO)
203 tree = DistributionServiceUtilities.buildOrderedTree(omitLevels,
204 filteredDistributions, parentAreaMap, fallbackAreaMarkerTypes,
205 alternativeRootAreaMarkerTypes, neverUseFallbackAreaAsParent,
206 distributionOrder, termDao);
207 }
208 dto.setTree(tree);
209 }
210
211 if(parts.contains(InfoPart.condensedDistribution)) {
212 CondensedDistribution condensedDistribution = DistributionServiceUtilities.getCondensedDistribution(
213 filteredDistributions, area2TermNodesMap, condensedDistConfig, languages);
214 dto.setCondensedDistribution(condensedDistribution);
215 }
216
217 if (parts.contains(InfoPart.mapUriParams)) {
218 boolean IGNORE_STATUS_ORDER_PREF = false;
219 Set<MarkerType> fallbackAreaMarkerType = null;
220 // only apply the subAreaPreference rule for the maps
221 keepFallBackOnlyIfNoSubareaDataExists = true;
222 Set<Distribution> filteredMapDistributions = DistributionServiceUtilities.filterDistributions(
223 filteredDistributions, areaTree, statusTree, fallbackAreaMarkerType, !PREFER_AGGREGATED,
224 IGNORE_STATUS_ORDER_PREF, subAreaPreference, keepFallBackOnlyIfNoSubareaDataExists);
225
226 String mapUri = DistributionServiceUtilities.getDistributionServiceRequestParameterString(
227 filteredMapDistributions,
228 areaMapping,
229 presenceAbsenceTermColors,
230 null, languages);
231 dto.setMapUriParams(mapUri);
232 }
233
234 return dto;
235 }
236
237 private TermTree<NamedArea> getPersistentAreaTree(List<Distribution> distributions, DistributionInfoConfiguration config) {
238 UUID areaTreeUuid = config.getAreaTree();
239 if (areaTreeUuid == null) {
240 return null;
241 }
242 //TODO property path
243 String[] propertyPath = new String[] {};
244 @SuppressWarnings("unchecked")
245 TermTree<NamedArea> areaTree = termTreeDao.load(areaTreeUuid, Arrays.asList(propertyPath));
246 return areaTree;
247 }
248
249 private TermTree<PresenceAbsenceTerm> getPersistentStatusTree(DistributionInfoConfiguration config) {
250 UUID statusTreeUuid = config.getStatusTree();
251 if (statusTreeUuid == null) {
252 return null;
253 }
254 //TODO property path
255 String[] propertyPath = new String[] {};
256 @SuppressWarnings("unchecked")
257 TermTree<PresenceAbsenceTerm> statusTree = termTreeDao.load(statusTreeUuid, Arrays.asList(propertyPath));
258 return statusTree;
259 }
260
261 @Override
262 public CondensedDistribution getCondensedDistribution(Set<Distribution> distributions,
263 TermTree<NamedArea> areaTree,
264 TermTree<PresenceAbsenceTerm> statusTree,
265 boolean statusOrderPreference,
266 Set<MarkerType> fallbackAreaMarkerTypes,
267 CondensedDistributionConfiguration config,
268 List<Language> langs) {
269
270 //TODO exclude "undefined" status as long as status tree is not yet
271 areaTree = areaTree == null ? DistributionServiceUtilities.getAreaTree(distributions, fallbackAreaMarkerTypes) : areaTree;
272 SetMap<NamedArea,TermNode<NamedArea>> parentNodeMap = areaTree.getParentNodeMap();
273 Collection<Distribution> filteredDistributions = DistributionServiceUtilities.filterDistributions(
274 distributions, null, statusTree, fallbackAreaMarkerTypes, false, statusOrderPreference, false, false);
275 CondensedDistribution condensedDistribution = DistributionServiceUtilities.getCondensedDistribution(
276 filteredDistributions,
277 parentNodeMap,
278 config,
279 langs);
280 return condensedDistribution;
281 }
282
283 @Override
284 public void setMapping(NamedArea area, GeoServiceArea geoServiceArea) {
285 areaMapping.set(area, geoServiceArea);
286 }
287
288 @Override
289 public String getDistributionServiceRequestParameterString(List<TaxonDescription> taxonDescriptions,
290 boolean subAreaPreference,
291 boolean statusOrderPreference,
292 Set<MarkerType> hideMarkedAreas,
293 Map<PresenceAbsenceTerm, Color> presenceAbsenceTermColors,
294 List<Language> langs,
295 boolean includeUnpublished) {
296
297 Set<Feature> features = new HashSet<>();
298 features.add(Feature.DISTRIBUTION()); //for now only this one
299 Set<Distribution> distributions = getDistributionsOf(taxonDescriptions, features, includeUnpublished);
300
301 String uriParams = getDistributionServiceRequestParameterString(distributions,
302 subAreaPreference,
303 statusOrderPreference,
304 hideMarkedAreas,
305 presenceAbsenceTermColors,
306 langs);
307
308 return uriParams;
309 }
310
311 private Set<Distribution> getDistributionsOf(List<TaxonDescription> taxonDescriptions, Set<Feature> features, boolean includeUnpublished) {
312 Set<Distribution> result = new HashSet<>();
313
314 for (TaxonDescription taxonDescription : taxonDescriptions) {
315 List<Distribution> distributions;
316 if (taxonDescription.getId() > 0){
317 distributions = dao.getDescriptionElements(taxonDescription,
318 null, features, Distribution.class, includeUnpublished, null, null, null);
319 }else{
320 distributions = new ArrayList<>();
321 for (DescriptionElementBase deb : taxonDescription.getElements()){
322 if (deb.isInstanceOf(Distribution.class)){
323 if (features == null || features.isEmpty()
324 || features.contains(deb.getFeature())) {
325 distributions.add(CdmBase.deproxy(deb, Distribution.class));
326 }
327 }
328 }
329 }
330 result.addAll(distributions);
331 }
332 return result;
333 }
334
335 @Override
336 public String getDistributionServiceRequestParameterString(
337 Set<Distribution> distributions,
338 boolean subAreaPreference,
339 boolean statusOrderPreference,
340 Set<MarkerType> hideMarkedAreas,
341 Map<PresenceAbsenceTerm, Color> presenceAbsenceTermColors,
342 List<Language> langs) {
343
344 TermTree<NamedArea> areaTree = null;
345 TermTree<PresenceAbsenceTerm> statusTree = null;
346 boolean keepFallbackOnlyIfNoSubareaDataExists = true;
347 Collection<Distribution> filteredDistributions = DistributionServiceUtilities.filterDistributions(
348 distributions, areaTree, statusTree,
349 hideMarkedAreas, false, statusOrderPreference,
350 subAreaPreference, keepFallbackOnlyIfNoSubareaDataExists);
351
352 String uriParams = DistributionServiceUtilities.getDistributionServiceRequestParameterString(
353 filteredDistributions,
354 areaMapping,
355 presenceAbsenceTermColors,
356 null, langs);
357 return uriParams;
358 }
359
360 @Override
361 @Transactional(readOnly=false)
362 public Map<NamedArea, String> mapShapeFileToNamedAreas(Reader csvReader,
363 List<String> idSearchFields, String wmsLayerName, UUID areaVocabularyUuid,
364 Set<UUID> namedAreaUuids) throws IOException {
365
366 Set<NamedArea> areas = new HashSet<>();
367
368 if(areaVocabularyUuid != null){
369 @SuppressWarnings("unchecked")
370 TermVocabulary<NamedArea> areaVocabulary = vocabDao.load(areaVocabularyUuid);
371 if(areaVocabulary == null){
372 throw new EntityNotFoundException("No Vocabulary found for uuid " + areaVocabularyUuid);
373 }
374 areas.addAll(areaVocabulary.getTerms());
375 }
376 if(namedAreaUuids != null && !namedAreaUuids.isEmpty()){
377 for(DefinedTermBase<?> dtb : termDao.list(namedAreaUuids, null, null, null, null)){
378 areas.add((NamedArea)CdmBase.deproxy(dtb));
379 }
380 }
381
382 ShpAttributesToNamedAreaMapper mapper = new ShpAttributesToNamedAreaMapper(areas, areaMapping);
383 Map<NamedArea, String> resultMap = mapper.readCsv(csvReader, idSearchFields, wmsLayerName);
384 termDao.saveOrUpdateAll((Collection)areas);
385 return resultMap;
386 }
387 }