Merge branch 'release/5.45.0'
[cdmlib.git] / cdmlib-model / src / main / java / eu / etaxonomy / cdm / model / location / Point.java
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.model.location;
11
12 import java.io.Serializable;
13 import java.math.BigDecimal;
14 import java.math.MathContext;
15 import java.math.RoundingMode;
16 import java.text.ParseException;
17 import java.util.regex.Matcher;
18 import java.util.regex.Pattern;
19
20 import javax.persistence.Embeddable;
21 import javax.persistence.FetchType;
22 import javax.persistence.ManyToOne;
23 import javax.persistence.Transient;
24 import javax.validation.constraints.NotNull;
25 import javax.xml.bind.annotation.XmlAccessType;
26 import javax.xml.bind.annotation.XmlAccessorType;
27 import javax.xml.bind.annotation.XmlElement;
28 import javax.xml.bind.annotation.XmlIDREF;
29 import javax.xml.bind.annotation.XmlRootElement;
30 import javax.xml.bind.annotation.XmlSchemaType;
31 import javax.xml.bind.annotation.XmlType;
32
33 import org.apache.commons.lang3.StringUtils;
34 import org.apache.logging.log4j.LogManager;
35 import org.apache.logging.log4j.Logger;
36 import org.hibernate.search.annotations.Field;
37 import org.hibernate.search.annotations.Latitude;
38 import org.hibernate.search.annotations.Longitude;
39 import org.hibernate.search.annotations.NumericField;
40 import org.hibernate.search.annotations.Spatial;
41 import org.hibernate.search.annotations.SpatialMode;
42
43 import eu.etaxonomy.cdm.common.CdmUtils;
44 import eu.etaxonomy.cdm.strategy.parser.location.CoordinateConverter;
45 import eu.etaxonomy.cdm.strategy.parser.location.CoordinateConverter.ConversionResults;
46 import eu.etaxonomy.cdm.validation.Level2;
47
48 /**
49 * @author m.doering
50 * @since 08-Nov-2007 13:06:44
51 */
52 @XmlAccessorType(XmlAccessType.FIELD)
53 @XmlType(name = "Point", propOrder = {
54 "longitude",
55 "latitude",
56 "errorRadius",
57 "referenceSystem"
58 })
59 @XmlRootElement(name = "Point")
60 @Embeddable
61 @Spatial(spatialMode=SpatialMode.RANGE, name="point")
62 public class Point implements Cloneable, Serializable {
63
64 private static final long serialVersionUID = 531030660792800636L;
65 private static final Logger logger = LogManager.getLogger();
66
67 //TODO was Float but H2 threw errors, maybe we should also use BigDecimal for exactness, see #8978
68 @XmlElement(name = "Longitude")
69 @Longitude(of="point")
70 @NotNull(groups = Level2.class)
71 private Double longitude;
72
73 @XmlElement(name = "Latitude")
74 @Latitude(of="point")
75 @NotNull(groups = Level2.class)
76 private Double latitude;
77
78 /**
79 * Error radius in meters
80 */
81 @XmlElement(name = "ErrorRadius")
82 @Field
83 @NumericField
84 private Integer errorRadius;
85
86 @XmlElement(name = "ReferenceSystem")
87 @XmlIDREF
88 @XmlSchemaType(name = "IDREF")
89 @ManyToOne(fetch = FetchType.LAZY)
90 private ReferenceSystem referenceSystem;
91
92
93 //******************** FACTORY METHODS ****************************
94
95 public static Point NewInstance(){
96 return new Point();
97 }
98
99 public static Point NewInstance(Double longitude, Double latitude, ReferenceSystem referenceSystem, Integer errorRadius){
100 Point result = new Point();
101 result.setLongitude(longitude);
102 result.setLatitude(latitude);
103 result.setReferenceSystem(referenceSystem);
104 result.setErrorRadius(errorRadius);
105 return result;
106 }
107
108 // ******************** CONSTRUCTOR ***************************
109
110 public Point() {
111 }
112
113 //************** Sexagesimal /decimal METHODS *******************
114
115 public enum Direction {
116 WEST {
117
118 @Override
119 public String toString() {
120 return "W";
121 }
122 },
123 EAST {
124
125 @Override
126 public String toString() {
127 return "E";
128 }
129 },
130 NORTH {
131
132 @Override
133 public String toString() {
134 return "N";
135 }
136 },
137 SOUTH {
138
139 @Override
140 public String toString() {
141 return "S";
142 }
143 };
144 }
145
146 public static final class CoordinateParser {
147
148 /**
149 * Pattern zum parsen von Sexagesimalen Grad: 145°
150 */
151 private static final String DEGREE_REGEX = "([0-9]*)\u00B0";
152 /**
153 * Pattern zum parsen von Sexagesimalen Minuten: 65'
154 */
155 private static final String MINUTES_REGEX = "(?:([0-9]*)')?";
156 /**
157 * Pattern zum parsen von Sexagesimalen Sekunden: 17"
158 */
159 private static final String SECONDS_REGEX = "(?:([0-9]*)(?:''|\"))?";
160 /**
161 * Himmelsrichtung Längengrad
162 */
163 private static final String LONGITUDE_DIRECTION_REGEX = "([OEW])";
164 /**
165 * Himmelsrichtung Breitengrad
166 */
167 private static final String LATITUDE_DIRECTION_REGEX = "([NS])";
168
169 /**
170 * Pattern zum Parsen von Breitengraden.
171 */
172 private static final Pattern LATITUDE_PATTERN = Pattern
173 .compile(DEGREE_REGEX + MINUTES_REGEX + SECONDS_REGEX
174 + LATITUDE_DIRECTION_REGEX);
175
176 /**
177 * Pattern zum Parsen von Längengraden.
178 */
179 private static final Pattern LONGITUDE_PATTERN = Pattern
180 .compile(DEGREE_REGEX + MINUTES_REGEX + SECONDS_REGEX
181 + LONGITUDE_DIRECTION_REGEX);
182
183 private CoordinateParser() {
184 throw new AssertionError( );
185 }
186
187 /**
188 * Parst einen Breitengrad der Form<br>
189 * G°M'S""(OEW)<br>
190 * Die Formen<br>
191 * G°(OEW)<br>
192 * G°M'(OEW)<br>
193 * sind ebenfalls erlaubt.
194 *
195 * @param strg
196 * @return Die geparsten Koordinaten
197 * @throws ParseException
198 * Wenn eine Fehler beim Parsen aufgetreten ist.
199 */
200 public static Sexagesimal parseLatitude(final String strg)
201 throws ParseException {
202 return parseCoordinates(strg, LATITUDE_PATTERN);
203 }
204
205 /**
206 * Parst einen Längengrad der Form<br>
207 * G°M'S"(NS)<br>
208 * Die Formen<br>
209 * G°(NS)<br>
210 * G°M'(NS)<br>
211 * sind ebenfalls erlaubt.
212 *
213 * @param strg
214 * @return Die geparsten Koordinaten
215 * @throws ParseException
216 * Wenn eine Fehler beim Parsen aufgetreten ist.
217 */
218 public static Sexagesimal parseLongitude(final String strg)
219 throws ParseException {
220 return parseCoordinates(strg, LONGITUDE_PATTERN);
221 }
222
223
224 /**
225 * Not used at the moment. Use CoordinateConverter instead.
226 * @param strg
227 * @param pattern
228 * @return
229 * @throws ParseException
230 */
231 private static Sexagesimal parseCoordinates(final String strg, final Pattern pattern) throws ParseException {
232 if (strg == null) {
233 throw new java.text.ParseException("Keine Koordinaten gegeben.", -1);
234 }
235 final Matcher matcher = pattern.matcher(strg);
236 if (matcher.matches( )) {
237 if (matcher.groupCount( ) == 4) {
238 // Grad
239 String tmp = matcher.group(1);
240 int degree = Integer.parseInt(tmp);
241
242 // Optional minutes
243 tmp = matcher.group(2);
244 int minutes = Sexagesimal.NONE;
245 if (tmp != null) {
246 minutes = Integer.parseInt(tmp);
247 }
248
249 // Optional seconds
250 tmp = matcher.group(3);
251 int seconds = Sexagesimal.NONE;
252 if (tmp != null) {
253 seconds = Integer.parseInt(tmp);
254 }
255
256 // directions
257 tmp = matcher.group(4);
258 final Direction direction;
259 if (tmp.equals("N")) {
260 direction = Direction.NORTH;
261 }
262 else if (tmp.equals("S")) {
263 direction = Direction.SOUTH;
264 }
265 else if (tmp.equals("E") || tmp.equals("O")) {
266 direction = Direction.EAST;
267 }
268 else if (tmp.equals("W")) {
269 direction = Direction.WEST;
270 }
271 else {
272 direction = null;
273 }
274 return Sexagesimal.NewInstance(degree, minutes, seconds, direction);
275 }
276 else {
277 throw new java.text.ParseException(
278 "Die Koordinaten-Darstellung ist fehlerhaft: " + strg,
279 -1);
280 }
281 }
282 else {
283 throw new java.text.ParseException(
284 "Die Koordinaten-Darstellung ist fehlerhaft: " + strg, -1);
285 }
286 }
287
288 }
289
290
291 private static final BigDecimal SIXTY = BigDecimal.valueOf(60.0);
292 private static final MathContext MC = new MathContext(34, RoundingMode.HALF_UP);
293 private static final double HALF_SECOND = 1. / 7200.;
294
295 //see http://www.tutorials.de/forum/archiv/348596-quiz-10-zeja-java.html
296 public static class Sexagesimal{
297 public static Sexagesimal NewInstance(Integer degree, Integer minutes, Integer seconds, Direction direction){
298 Sexagesimal result = new Sexagesimal();
299 result.degree = degree; result.minutes = minutes; result.seconds = seconds;
300 return result;
301 }
302
303 public static final int NONE = 0;
304 public Integer degree;
305 public Integer minutes;
306 public Integer seconds;
307 public Double tertiers;
308
309 public Direction direction;
310
311
312 public boolean isLatitude(){
313 return (direction == Direction.WEST) || (direction == Direction.EAST) ;
314 }
315 public boolean isLongitude(){
316 return ! isLatitude();
317 }
318
319
320 public static Sexagesimal valueOf(Double decimal, boolean isLatitude){
321 return valueOf(decimal, isLatitude, false, false, true);
322 }
323
324 public static Sexagesimal valueOf(Double decimal, boolean isLatitude, boolean nullSecondsToNull, boolean nullMinutesToNull, boolean allowTertiers){
325 if(decimal == null){
326 return null;
327 }
328 Sexagesimal sexagesimal = new Sexagesimal();
329 Double decimalDegree = decimal;
330 if (isLatitude) {
331 if (decimalDegree < 0) {
332 sexagesimal.direction = Direction.SOUTH;
333 }
334 else {
335 sexagesimal.direction = Direction.NORTH;
336 }
337 }
338 else {
339 if (decimalDegree < 0) {
340 sexagesimal.direction = Direction.WEST;
341 }
342 else {
343 sexagesimal.direction = Direction.EAST;
344 }
345 }
346
347 // Decimal in \u00B0'" umrechnen
348 double d = Math.abs(decimalDegree);
349 if (! allowTertiers){
350 d += HALF_SECOND; // add half a second for rounding
351 }else{
352 d += HALF_SECOND / 10000; //to avoid rounding errors
353 }
354 sexagesimal.degree = (int) Math.floor(d);
355 sexagesimal.minutes = (int) Math.floor((d - sexagesimal.degree) * 60.0);
356 sexagesimal.seconds = (int) Math.floor((d - sexagesimal.degree - sexagesimal.minutes / 60.0) * 3600.0);
357 sexagesimal.tertiers = (d - sexagesimal.degree - sexagesimal.minutes / 60.0 - sexagesimal.seconds / 3600.0) * 3600.0;
358
359 if (sexagesimal.seconds == 0 && nullSecondsToNull){
360 sexagesimal.seconds = null;
361 }
362 if (sexagesimal.seconds == null && sexagesimal.minutes == 0 && nullMinutesToNull){
363 sexagesimal.minutes = null;
364 }
365
366 // sexagesimal.decimalRadian = Math.toRadians(this.decimalDegree);
367 return sexagesimal;
368 }
369
370
371
372 private Double toDecimal(){
373 BigDecimal value = BigDecimal.valueOf(CdmUtils.Nz(this.seconds)).divide(SIXTY, MC).add
374 (BigDecimal.valueOf(CdmUtils.Nz(this.minutes))).divide(SIXTY, MC).add
375 (BigDecimal.valueOf(CdmUtils.Nz(this.degree)));
376
377 if (this.direction == Direction.WEST || this.direction == Direction.SOUTH) {
378 value = value.negate( );
379 }
380 return value.doubleValue( );
381 }
382
383 @Override
384 public String toString(){
385 return toString(false, false);
386 }
387 public String toString(boolean includeEmptySeconds){
388 return toString(includeEmptySeconds, false);
389 }
390
391 public String toString(boolean includeEmptySeconds, boolean removeTertiers){
392 String result;
393 result = String.valueOf(CdmUtils.Nz(degree)) + "\u00B0";
394 if (seconds != null || minutes != null){
395 result += String.valueOf(CdmUtils.Nz(minutes)) + "'";
396 }
397 if (seconds != null ){
398 if (seconds != 0 || includeEmptySeconds){
399 result += String.valueOf(CdmUtils.Nz(seconds)) + getTertiersString(tertiers, removeTertiers) + "\"";
400 }
401 }
402 result += direction;
403 return result;
404 }
405 private String getTertiersString(Double tertiers, boolean removeTertiers) {
406
407 if (tertiers == null || removeTertiers){
408 return "";
409 }else{
410 if (tertiers >= 1.0 || tertiers < 0.0){
411 throw new IllegalStateException("Tertiers should be 0.0 <= tertiers < 1.0 but are '" + tertiers + "'");
412 }
413 String result = tertiers.toString();
414 int pos = result.indexOf("E");
415 if (pos > -1){
416 int exp = - Integer.valueOf(result.substring(pos + 1));
417 result = result.substring(0, pos).replace(".", "");
418 result = "0." + StringUtils.leftPad("", exp - 1, "0") + result;
419
420 }
421
422 if (result.length() > 5){
423 result = result.substring(0, 5);
424 }
425 while (result.endsWith("0")){
426 result = result.substring(0, result.length() -1);
427 }
428 result = result.substring(1);
429 if (result.equals(".")){
430 result = "";
431 }
432 return result;
433 }
434 }
435 }
436
437 @Transient
438 public Sexagesimal getLongitudeSexagesimal (){
439 boolean isLatitude = false;
440 return Sexagesimal.valueOf(longitude, isLatitude);
441 }
442
443 @Transient
444 public Sexagesimal getLatitudeSexagesimal (){
445 boolean isLatitude = true;
446 return Sexagesimal.valueOf(latitude, isLatitude);
447 }
448
449 @Transient
450 public void setLatitudeSexagesimal(Sexagesimal sexagesimalLatitude){
451 this.latitude = sexagesimalLatitude.toDecimal();
452 }
453 @Transient
454 public void setLongitudeSexagesimal(Sexagesimal sexagesimalLongitude){
455 this.longitude = sexagesimalLongitude.toDecimal();
456 }
457
458 @Transient
459 public void setLatitudeByParsing(String string) throws ParseException{
460 this.setLatitude(parseLatitude(string));
461 }
462
463 @Transient
464 public void setLongitudeByParsing(String string) throws ParseException{
465 this.setLongitude(parseLongitude(string));
466 }
467
468
469 public static Double parseLatitude(String string) throws ParseException{
470 try{
471 if (string == null || string.isEmpty()){
472 return null;
473 }
474 string = setCurrentDoubleSeparator(string);
475 if (isDouble(string)){
476 Double result = Double.valueOf(string);
477 if (Math.abs(result) > 90.0){
478 throw new ParseException("Latitude could not be parsed", 0);
479 }
480 return result;
481 }else{
482 CoordinateConverter converter = new CoordinateConverter();
483 ConversionResults result = converter.tryConvert(string);
484 if (! result.conversionSuccessful || (result.isLongitude != null && result.isLongitude) ){
485 throw new ParseException("Latitude could not be parsed", 0);
486 }else{
487 return result.convertedCoord;
488 }
489 }
490 } catch (Exception e) {
491 String message = "Latitude %s could not be parsed";
492 message = String.format(message, string);
493 throw new ParseException(message, 0);
494 }
495 }
496
497 public static Double parseLongitude(String string) throws ParseException{
498 try {
499 if (string == null || string.isEmpty()){
500 return null;
501 }
502 string = setCurrentDoubleSeparator(string);
503 if (isDouble(string)){
504 Double result = Double.valueOf(string);
505 if (Math.abs(result) > 180.0){
506 throw new ParseException("Longitude could not be parsed", 0);
507 }
508 return result;
509 }else{
510 CoordinateConverter converter = new CoordinateConverter();
511 ConversionResults result = converter.tryConvert(string);
512 if (! result.conversionSuccessful || (result.isLongitude != null && ! result.isLongitude)){
513 throw new ParseException("Longitude could not be parsed", 0);
514 }else{
515 return result.convertedCoord;
516 }
517 }
518 } catch (Exception e) {
519 String message = "Longitude %s could not be parsed";
520 message = String.format(message, string);
521 throw new ParseException(message, 0);
522 }
523 }
524
525 private static String setCurrentDoubleSeparator(String string) {
526 String regExReplaceComma = "(\\,|\\.)";
527 string = string.replaceAll(regExReplaceComma,".");
528 return string;
529
530 }
531
532 private static boolean isDouble(String string) {
533 try {
534 Double.valueOf(string);
535 return true;
536
537 } catch (NumberFormatException e) {
538 return false;
539 } catch (Exception e) {
540 return false;
541 }
542
543 }
544
545 /**
546 * <code>true</code>, if none of the attributes (lat, long, errRadius, refSys) is set.
547 */
548 @Transient
549 public boolean isEmpty(){
550 if (errorRadius == null && latitude == null && longitude == null
551 && referenceSystem == null){
552 return true;
553 }else{
554 return false;
555 }
556 }
557
558 // ******************** GETTER / SETTER ********************************
559
560 public ReferenceSystem getReferenceSystem(){
561 return this.referenceSystem;
562 }
563
564 /**
565 *
566 * @param referenceSystem referenceSystem
567 */
568 public void setReferenceSystem(ReferenceSystem referenceSystem){
569 this.referenceSystem = referenceSystem;
570 }
571
572 public Double getLongitude(){
573 return this.longitude;
574 }
575
576 /**
577 *
578 * @param longitude longitude
579 */
580 public void setLongitude(Double longitude){
581 this.longitude = longitude;
582 }
583
584 public Double getLatitude(){
585 return this.latitude;
586 }
587
588 /**
589 *
590 * @param latitude latitude
591 */
592 public void setLatitude(Double latitude){
593 this.latitude = latitude;
594 }
595
596 /**
597 * Error radius in Meters
598 */
599 public Integer getErrorRadius(){
600 return this.errorRadius;
601 }
602
603 /**
604 *
605 * @param errorRadius errorRadius
606 */
607 public void setErrorRadius(Integer errorRadius){
608 this.errorRadius = errorRadius;
609 }
610
611 // **************** toString *************************/
612
613
614 /**
615 * Returns a string representation in sexagesimal coordinates.
616 * @return
617 */
618 public String toSexagesimalString(boolean includeEmptySeconds, boolean includeReferenceSystem){
619 String result = "";
620 result += getLatitudeSexagesimal() == null ? "" : getLatitudeSexagesimal().toString(includeEmptySeconds);
621 result = CdmUtils.concat(", ", result, getLongitudeSexagesimal() == null ? "" : getLongitudeSexagesimal().toString(includeEmptySeconds));
622 if (includeReferenceSystem && getReferenceSystem() != null){
623 String refSys = CdmUtils.isBlank(getReferenceSystem().getLabel()) ? "" : "(" + getReferenceSystem().getLabel() + ")";
624 result = CdmUtils.concat(" ", result, refSys);
625 }
626 return result;
627 }
628
629 @Override
630 public String toString(){
631 String result = "";
632 boolean includeEmptySeconds = true;
633 result += getLatitudeSexagesimal() == null ? "" : getLatitudeSexagesimal().toString(includeEmptySeconds);
634 result = CdmUtils.concat(", ", result, getLongitudeSexagesimal() == null ? "" : getLongitudeSexagesimal().toString(includeEmptySeconds));
635 return result;
636 }
637
638
639 //*********** CLONE **********************************/
640
641 /**
642 * Clones <i>this</i> point. This is a shortcut that enables to
643 * create a new instance that differs only slightly from <i>this</i> point
644 * by modifying only some of the attributes.<BR>
645 *
646 * @see java.lang.Object#clone()
647 */
648 @Override
649 public Point clone(){
650 try{
651 Point result = (Point)super.clone();
652 result.setReferenceSystem(this.referenceSystem);
653 //no changes to: errorRadius, latitude, longitude
654 return result;
655 } catch (CloneNotSupportedException e) {
656 logger.warn("Object does not implement cloneable");
657 e.printStackTrace();
658 return null;
659 }
660 }
661 }