Project

General

Profile

Download (11.5 KB) Statistics
| Branch: | Tag: | Revision:
1
/**
2
 * Copyright (C) 2020 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.kml;
11

    
12
import java.util.ArrayList;
13
import java.util.Arrays;
14
import java.util.HashMap;
15
import java.util.HashSet;
16
import java.util.List;
17
import java.util.Map;
18
import java.util.Optional;
19
import java.util.Set;
20

    
21
import org.apache.commons.lang3.StringUtils;
22
import org.apache.log4j.Logger;
23
import org.geotools.feature.SchemaException;
24
import org.locationtech.jts.geom.Coordinate;
25
import org.opengis.feature.simple.SimpleFeature;
26
import org.opengis.referencing.FactoryException;
27

    
28
import de.micromata.opengis.kml.v_2_2_0.AltitudeMode;
29
import de.micromata.opengis.kml.v_2_2_0.Document;
30
import de.micromata.opengis.kml.v_2_2_0.ExtendedData;
31
import de.micromata.opengis.kml.v_2_2_0.Feature;
32
import de.micromata.opengis.kml.v_2_2_0.Kml;
33
import de.micromata.opengis.kml.v_2_2_0.KmlFactory;
34
import de.micromata.opengis.kml.v_2_2_0.LineStyle;
35
import de.micromata.opengis.kml.v_2_2_0.LinearRing;
36
import de.micromata.opengis.kml.v_2_2_0.Placemark;
37
import de.micromata.opengis.kml.v_2_2_0.PolyStyle;
38
import de.micromata.opengis.kml.v_2_2_0.Polygon;
39
import de.micromata.opengis.kml.v_2_2_0.Style;
40
import eu.etaxonomy.cdm.model.location.Point;
41
import eu.etaxonomy.cdm.model.occurrence.DerivedUnit;
42
import eu.etaxonomy.cdm.model.occurrence.FieldUnit;
43
import eu.etaxonomy.cdm.model.occurrence.GatheringEvent;
44
import eu.etaxonomy.cdm.model.occurrence.SpecimenOrObservationBase;
45
import eu.etaxonomy.cdm.model.occurrence.SpecimenOrObservationType;
46
import eu.etaxonomy.cdm.model.term.DefinedTerm;
47
import eu.etaxonomy.cdm.model.term.TermType;
48
import si.uom.SI;
49
import tec.uom.se.quantity.Quantities;
50

    
51
/**
52
 *
53
 * @author Andreas Kohlbecker
54
 * @since Apr 21, 2020
55
 */
56
public class KMLDocumentBuilder {
57

    
58
	private final static Logger logger = Logger.getLogger(KMLDocumentBuilder.class);
59

    
60
	private Set<SpecimenOrObservationBase> occSet = new HashSet<>();
61

    
62
	private Map<FieldUnit, Set<SpecimenOrObservationBase>> fieldUnitMap = new HashMap<>();
63
	private Map<FieldUnit, Set<SpecimenOrObservationType>> fieldUnitRecordBases = new HashMap<>();
64

    
65
	private Map<String, Style> styles = new HashMap<>();
66

    
67
	private static final DefinedTerm KIND_OF_UNIT_UNSET = DefinedTerm.NewInstance(TermType.KindOfUnit);
68

    
69
	public KMLDocumentBuilder addSpecimenOrObservationBase(SpecimenOrObservationBase occurrence) {
70
		occSet.add(occurrence);
71
		return this;
72
	}
73

    
74
	public Kml build() {
75

    
76
		Kml kml = KmlFactory.createKml();
77
		List<Feature> documentFeatures = new ArrayList<>();
78

    
79
		for (SpecimenOrObservationBase sob : occSet) {
80
			mapFieldUnit(sob, null, null);
81
		}
82
		for (FieldUnit fu : fieldUnitMap.keySet()) {
83
			createFieldUnitPlacemarks(documentFeatures, fu);
84
		}
85

    
86
		Document doc = kml.createAndSetDocument();
87
		doc.getFeature().addAll(documentFeatures);
88
		doc.getStyleSelector().addAll(styles.values());
89
		return kml;
90
	}
91

    
92
	private void mapFieldUnit(SpecimenOrObservationBase unitOfInterest, SpecimenOrObservationBase original, Set<SpecimenOrObservationType> recordBases) {
93

    
94
		if(original == null) {
95
			original = unitOfInterest;
96
		}
97
		if(recordBases == null) {
98
			recordBases = new HashSet<>();
99
		}
100

    
101
		if (original instanceof FieldUnit) {
102
			FieldUnit fu = (FieldUnit)original;
103
			if(!fieldUnitMap.containsKey(fu)) {
104
				fieldUnitMap.put(fu, new HashSet<>());
105
			}
106
			fieldUnitMap.get(fu).add(unitOfInterest);
107
			if(!fieldUnitRecordBases.containsKey(fu)) {
108
				fieldUnitRecordBases.put(fu, new HashSet<>());
109
			}
110
			fieldUnitRecordBases.get(fu).addAll(recordBases);
111
		} else if (original instanceof DerivedUnit) {
112
			Set<SpecimenOrObservationBase> originals = ((DerivedUnit)original).getOriginals();
113
			if (originals != null) {
114
				for (SpecimenOrObservationBase parentOriginal : originals) {
115
					mapFieldUnit(original, parentOriginal, recordBases);
116
				}
117
			}
118
		}
119
	}
120

    
121
	private void createFieldUnitPlacemarks(List<Feature> documentFeatures, FieldUnit fieldUnit) {
122

    
123
		GatheringEvent gatherEvent = fieldUnit.getGatheringEvent();
124
		if (gatherEvent != null && isValidPoint(gatherEvent.getExactLocation())) {
125
			Placemark mapMarker = fieldUnitLocationMarker(gatherEvent.getExactLocation(),
126
					gatherEvent.getAbsoluteElevation(), fieldUnitRecordBases.get(fieldUnit));
127
			documentFeatures.add(mapMarker);
128
			addExtendedData(mapMarker, fieldUnit, gatherEvent);
129
			errorRadiusPlacemark(gatherEvent.getExactLocation()).ifPresent(pm -> documentFeatures.add(pm));
130
		}
131
	}
132

    
133
	private Placemark fieldUnitLocationMarker(Point exactLocation, Integer altitude, Set<SpecimenOrObservationType> recordBases) {
134

    
135
		Placemark mapMarker = KmlFactory.createPlacemark();
136
		de.micromata.opengis.kml.v_2_2_0.Point point = KmlFactory.createPoint();
137
		point.setAltitudeMode(AltitudeMode.ABSOLUTE);
138
		if (altitude != null) {
139
			point.setCoordinates(Arrays.asList(KmlFactory.createCoordinate(exactLocation.getLongitude(),
140
					exactLocation.getLatitude(), altitude.doubleValue())));
141
		} else {
142
			point.setCoordinates(Arrays
143
					.asList(KmlFactory.createCoordinate(exactLocation.getLongitude(), exactLocation.getLatitude())));
144
		}
145
		mapMarker.setGeometry(point);
146
		mapMarker.setStyleUrl(styleURL(recordBases));
147

    
148
		return mapMarker;
149
	}
150

    
151
	private Optional<Placemark> errorRadiusPlacemark(Point exactLocation) {
152

    
153
		// exactLocation.setErrorRadius(25 * 1000); // METER // uncomment for debugging
154

    
155
		Placemark errorRadiusCicle = null;
156
		if (exactLocation.getErrorRadius() != null && exactLocation.getErrorRadius() > 0) {
157
			errorRadiusCicle = KmlFactory.createPlacemark();
158
			LinearRing cirle = createKMLCircle(exactLocation.getLongitude(), exactLocation.getLatitude(),
159
					exactLocation.getErrorRadius());
160
			Polygon polygon = errorRadiusCicle.createAndSetPolygon();
161
			polygon.createAndSetOuterBoundaryIs().setLinearRing(cirle);
162
			polygon.setExtrude(true);
163
			errorRadiusCicle.setStyleUrl(errorRadiusStyleURL());
164
		}
165

    
166
		return Optional.ofNullable(errorRadiusCicle);
167
	}
168

    
169

    
170
	/**
171
	 * @param longitude
172
	 * @param latitude
173
	 * @param errorRadiusMeter
174
	 * @return
175
	 */
176
	private LinearRing createKMLCircle(Double longitude, Double latitude, Integer errorRadiusMeter) {
177

    
178
		GeometryBuilder.CircleMethod method = GeometryBuilder.CircleMethod.reprojectedCircle;
179
		LinearRing lineString = KmlFactory.createLinearRing();
180
		lineString.setAltitudeMode(AltitudeMode.RELATIVE_TO_GROUND);
181

    
182
		org.locationtech.jts.geom.Geometry polygonGeom = null;
183
		GeometryBuilder gb = new GeometryBuilder();
184
		try {
185
			switch (method) {
186
			case simpleCircle:
187
				// this is the best method so far !!!!
188
				polygonGeom = gb.simpleCircle(Quantities.getQuantity(errorRadiusMeter.doubleValue(), SI.METRE),
189
						latitude, longitude);
190
				break;
191
			case simpleCircleSmall:
192
				// Only suitable for small radius (> 1000 m) as the circles are heavily distorted
193
				// otherwise.
194
				polygonGeom = gb.simpleCircleSmall(errorRadiusMeter.doubleValue(), latitude, longitude);
195
				break;
196
			case circle:
197
				// fails
198
				polygonGeom = gb.circle(errorRadiusMeter.doubleValue(), latitude, longitude);
199
				break;
200
			case reprojectedCircle:
201
				// incomplete
202
				SimpleFeature pointFeature = gb.createSimplePointFeature(longitude, latitude);
203
				polygonGeom = gb.bufferFeature(pointFeature, errorRadiusMeter.doubleValue());
204
				break;
205
			}
206
			for (Coordinate coordinate : polygonGeom.getCoordinates()) {
207
				lineString.addToCoordinates(coordinate.getX(), coordinate.getY());
208
			}
209
		} catch (FactoryException e) {
210
			logger.error("Polygon creation for error radius failed", e);
211
		} catch (SchemaException e) {
212
			logger.error("SimplePointFeature creation failed", e);
213
		}
214
		return lineString;
215
	}
216

    
217
	private void addExtendedData(Placemark mapMarker, FieldUnit fieldUnit, GatheringEvent gatherEvent) {
218

    
219
		String name = fieldUnit.toString();
220
		String titleCache = fieldUnit.getTitleCache();
221
		String locationString = null;
222
		if(gatherEvent.getExactLocation() != null) {
223
			locationString = gatherEvent.getExactLocation().toSexagesimalString(false,  true);
224
			titleCache = titleCache.replace(locationString + ", ", "");
225
		}
226
		String description = "<p class=\"title-cache\">" + titleCache;
227
		if(locationString != null) {
228
			// see https://www.mediawiki.org/wiki/GeoHack
229
		    String geohackUrl = String.format(
230
		            "https://geohack.toolforge.org/geohack.php?language=en&params=%f;%f&pagename=%s",
231
		            gatherEvent.getExactLocation().getLatitude(),
232
		            gatherEvent.getExactLocation().getLongitude(),
233
		            name);
234
			description +=  "<br/><a class=\"exact-location\" target=\"geohack\" href=\"" + geohackUrl + "\">" + locationString + "</a>";
235
		}
236
		description += "</p>";
237
		description += "<figure><figcaption>Specimens and observations:</figcaption><ul>";
238
		for(SpecimenOrObservationBase sob : fieldUnitMap.get(fieldUnit)) {
239
			SpecimenOrObservationType type = sob.getRecordBasis() != null ? sob.getRecordBasis() : SpecimenOrObservationType.Unknown;
240
			String unitTitle = type.name();
241
			if(sob instanceof DerivedUnit) {
242
				DerivedUnit du = ((DerivedUnit)sob);
243
				if(StringUtils.isNotBlank(du.getMostSignificantIdentifier())){
244
					unitTitle += ": " + du.getMostSignificantIdentifier();
245
				}
246
			}
247
			description += "<li><a class=\"occurrence-link occurrence-link-" + sob.getUuid() + " \" href=\"${occurrence-link-base-url}/" + sob.getUuid() + "\">" + unitTitle + "</a></li>";
248
		}
249
		description += "</ul></figure>";
250
		// mapMarker.setName(name);
251
		mapMarker.setDescription(description);
252

    
253
		ExtendedData extendedData = mapMarker.createAndSetExtendedData();
254
		extendedData.createAndAddData(fieldUnit.getTitleCache()).setName("titleCache");
255
		if(mapMarker.getGeometry() != null && mapMarker.getGeometry() instanceof de.micromata.opengis.kml.v_2_2_0.Point) {
256
			extendedData.createAndAddData(((de.micromata.opengis.kml.v_2_2_0.Point)mapMarker.getGeometry()).getCoordinates().toString()).setName("Location");
257
		}
258

    
259
	}
260

    
261

    
262
	private String styleURL(Set<SpecimenOrObservationType> recordBases) {
263
		String key = "DEFAULT";
264
		// TODO determine key and style on base of the recordBases
265
		if (!styles.containsKey(key)) {
266
			Style style = KmlFactory.createStyle().withIconStyle(MapMarkerIcons.red_blank.asIconStyle());
267
			style.setId(key);
268
			styles.put(key, style);
269
		}
270
		return "#" + key;
271
	}
272

    
273
	private String errorRadiusStyleURL() {
274
		String key = "ERROR_RADIUS";
275
		if (!styles.containsKey(key)) {
276
			Style style = KmlFactory.createStyle();
277
			PolyStyle polyStyle = style.createAndSetPolyStyle();
278
			polyStyle.setColor("100000ee"); // aabbggrr, where aa=alpha (00 to ff); bb=blue (00 to ff); gg=green (00 to ff); rr=red (00 to ff).
279
			polyStyle.setFill(true);
280
			polyStyle.setOutline(true);
281
			style.setId(key);
282
			LineStyle lineStyle = style.createAndSetLineStyle();
283
			// lineStyle.setColor("ff880088"); // aabbggrr, where aa=alpha (00 to ff);
284
			// bb=blue (00 to ff); gg=green (00 to ff); rr=red (00 to ff).
285
			lineStyle.setWidth(1);
286
			styles.put(key, style);
287
		}
288
		return "#" + key;
289
	}
290

    
291

    
292
	// TODO use also for .EditGeoService.registerDerivedUnitLocations(DerivedUnit
293
	// derivedUnit, List<Point> derivedUnitPoints) !!!!!!!!!
294
	public boolean isValidPoint(Point point) {
295

    
296
		if (point == null) {
297
			return false;
298
		}
299
		// points with no longitude or latitude should not exist
300
		// see #4173 ([Rule] Longitude and Latitude in Point must not be null)
301
		if (point.getLatitude() == null || point.getLongitude() == null) {
302
			return false;
303
		}
304
		// FIXME: remove next statement after
305
		// DerivedUnitFacade or ABCD import is fixed
306
		//
307
		if (point.getLatitude() == 0.0 || point.getLongitude() == 0.0) {
308
			return false;
309
		}
310
		return true;
311
	}
312
}
(2-2/3)