cleanup
[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 //TODO this is probably not in use anymore
204 tree = DistributionServiceUtilities.buildOrderedTree(omitLevels,
205 filteredDistributions, parentAreaMap, fallbackAreaMarkerTypes,
206 alternativeRootAreaMarkerTypes, neverUseFallbackAreaAsParent,
207 distributionOrder, termDao);
208 }
209 dto.setTree(tree);
210 }
211
212 if(parts.contains(InfoPart.condensedDistribution)) {
213 CondensedDistribution condensedDistribution = DistributionServiceUtilities.getCondensedDistribution(
214 filteredDistributions, area2TermNodesMap, condensedDistConfig, languages);
215 dto.setCondensedDistribution(condensedDistribution);
216 }
217
218 if (parts.contains(InfoPart.mapUriParams)) {
219 boolean IGNORE_STATUS_ORDER_PREF = false;
220 Set<MarkerType> fallbackAreaMarkerType = null;
221 // only apply the subAreaPreference rule for the maps
222 keepFallBackOnlyIfNoSubareaDataExists = true;
223 Set<Distribution> filteredMapDistributions = DistributionServiceUtilities.filterDistributions(
224 filteredDistributions, areaTree, statusTree, fallbackAreaMarkerType, !PREFER_AGGREGATED,
225 IGNORE_STATUS_ORDER_PREF, subAreaPreference, keepFallBackOnlyIfNoSubareaDataExists);
226
227 String mapUri = DistributionServiceUtilities.getDistributionServiceRequestParameterString(
228 filteredMapDistributions,
229 areaMapping,
230 presenceAbsenceTermColors,
231 null, languages);
232 dto.setMapUriParams(mapUri);
233 }
234
235 return dto;
236 }
237
238 private TermTree<NamedArea> getPersistentAreaTree(List<Distribution> distributions, DistributionInfoConfiguration config) {
239 UUID areaTreeUuid = config.getAreaTree();
240 if (areaTreeUuid == null) {
241 return null;
242 }
243 //TODO property path
244 String[] propertyPath = new String[] {};
245 @SuppressWarnings("unchecked")
246 TermTree<NamedArea> areaTree = termTreeDao.load(areaTreeUuid, Arrays.asList(propertyPath));
247 return areaTree;
248 }
249
250 private TermTree<PresenceAbsenceTerm> getPersistentStatusTree(DistributionInfoConfiguration config) {
251 UUID statusTreeUuid = config.getStatusTree();
252 if (statusTreeUuid == null) {
253 return null;
254 }
255 //TODO property path
256 String[] propertyPath = new String[] {};
257 @SuppressWarnings("unchecked")
258 TermTree<PresenceAbsenceTerm> statusTree = termTreeDao.load(statusTreeUuid, Arrays.asList(propertyPath));
259 return statusTree;
260 }
261
262 @Override
263 public CondensedDistribution getCondensedDistribution(Set<Distribution> distributions,
264 TermTree<NamedArea> areaTree,
265 TermTree<PresenceAbsenceTerm> statusTree,
266 boolean statusOrderPreference,
267 Set<MarkerType> fallbackAreaMarkerTypes,
268 CondensedDistributionConfiguration config,
269 List<Language> langs) {
270
271 //TODO exclude "undefined" status as long as status tree is not yet
272 areaTree = areaTree == null ? DistributionServiceUtilities.getAreaTree(distributions, fallbackAreaMarkerTypes) : areaTree;
273 SetMap<NamedArea,TermNode<NamedArea>> parentNodeMap = areaTree.getParentNodeMap();
274 Collection<Distribution> filteredDistributions = DistributionServiceUtilities.filterDistributions(
275 distributions, null, statusTree, fallbackAreaMarkerTypes, false, statusOrderPreference, false, false);
276 CondensedDistribution condensedDistribution = DistributionServiceUtilities.getCondensedDistribution(
277 filteredDistributions,
278 parentNodeMap,
279 config,
280 langs);
281 return condensedDistribution;
282 }
283
284 @Override
285 public void setMapping(NamedArea area, GeoServiceArea geoServiceArea) {
286 areaMapping.set(area, geoServiceArea);
287 }
288
289 @Override
290 public String getDistributionServiceRequestParameterString(List<TaxonDescription> taxonDescriptions,
291 boolean subAreaPreference,
292 boolean statusOrderPreference,
293 Set<MarkerType> hideMarkedAreas,
294 Map<PresenceAbsenceTerm, Color> presenceAbsenceTermColors,
295 List<Language> langs,
296 boolean includeUnpublished) {
297
298 Set<Feature> features = new HashSet<>();
299 features.add(Feature.DISTRIBUTION()); //for now only this one
300 Set<Distribution> distributions = getDistributionsOf(taxonDescriptions, features, includeUnpublished);
301
302 String uriParams = getDistributionServiceRequestParameterString(distributions,
303 subAreaPreference,
304 statusOrderPreference,
305 hideMarkedAreas,
306 presenceAbsenceTermColors,
307 langs);
308
309 return uriParams;
310 }
311
312 private Set<Distribution> getDistributionsOf(List<TaxonDescription> taxonDescriptions, Set<Feature> features, boolean includeUnpublished) {
313 Set<Distribution> result = new HashSet<>();
314
315 for (TaxonDescription taxonDescription : taxonDescriptions) {
316 List<Distribution> distributions;
317 if (taxonDescription.getId() > 0){
318 distributions = dao.getDescriptionElements(taxonDescription,
319 null, features, Distribution.class, includeUnpublished, null, null, null);
320 }else{
321 distributions = new ArrayList<>();
322 for (DescriptionElementBase deb : taxonDescription.getElements()){
323 if (deb.isInstanceOf(Distribution.class)){
324 if (features == null || features.isEmpty()
325 || features.contains(deb.getFeature())) {
326 distributions.add(CdmBase.deproxy(deb, Distribution.class));
327 }
328 }
329 }
330 }
331 result.addAll(distributions);
332 }
333 return result;
334 }
335
336 @Override
337 public String getDistributionServiceRequestParameterString(
338 Set<Distribution> distributions,
339 boolean subAreaPreference,
340 boolean statusOrderPreference,
341 Set<MarkerType> hideMarkedAreas,
342 Map<PresenceAbsenceTerm, Color> presenceAbsenceTermColors,
343 List<Language> langs) {
344
345 TermTree<NamedArea> areaTree = null;
346 TermTree<PresenceAbsenceTerm> statusTree = null;
347 boolean keepFallbackOnlyIfNoSubareaDataExists = true;
348 Collection<Distribution> filteredDistributions = DistributionServiceUtilities.filterDistributions(
349 distributions, areaTree, statusTree,
350 hideMarkedAreas, false, statusOrderPreference,
351 subAreaPreference, keepFallbackOnlyIfNoSubareaDataExists);
352
353 String uriParams = DistributionServiceUtilities.getDistributionServiceRequestParameterString(
354 filteredDistributions,
355 areaMapping,
356 presenceAbsenceTermColors,
357 null, langs);
358 return uriParams;
359 }
360
361 @Override
362 @Transactional(readOnly=false)
363 public Map<NamedArea, String> mapShapeFileToNamedAreas(Reader csvReader,
364 List<String> idSearchFields, String wmsLayerName, UUID areaVocabularyUuid,
365 Set<UUID> namedAreaUuids) throws IOException {
366
367 Set<NamedArea> areas = new HashSet<>();
368
369 if(areaVocabularyUuid != null){
370 @SuppressWarnings("unchecked")
371 TermVocabulary<NamedArea> areaVocabulary = vocabDao.load(areaVocabularyUuid);
372 if(areaVocabulary == null){
373 throw new EntityNotFoundException("No Vocabulary found for uuid " + areaVocabularyUuid);
374 }
375 areas.addAll(areaVocabulary.getTerms());
376 }
377 if(namedAreaUuids != null && !namedAreaUuids.isEmpty()){
378 for(DefinedTermBase<?> dtb : termDao.list(namedAreaUuids, null, null, null, null)){
379 areas.add((NamedArea)CdmBase.deproxy(dtb));
380 }
381 }
382
383 ShpAttributesToNamedAreaMapper mapper = new ShpAttributesToNamedAreaMapper(areas, areaMapping);
384 Map<NamedArea, String> resultMap = mapper.readCsv(csvReader, idSearchFields, wmsLayerName);
385 termDao.saveOrUpdateAll((Collection)areas);
386 return resultMap;
387 }
388 }