2 * Copyright (C) 2007 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
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.
10 package eu
.etaxonomy
.cdm
.model
.location
;
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
;
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
;
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
;
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
;
50 * @since 08-Nov-2007 13:06:44
52 @XmlAccessorType(XmlAccessType
.FIELD
)
53 @XmlType(name
= "Point", propOrder
= {
59 @XmlRootElement(name
= "Point")
61 @Spatial(spatialMode
=SpatialMode
.RANGE
, name
="point")
62 public class Point
implements Cloneable
, Serializable
{
64 private static final long serialVersionUID
= 531030660792800636L;
65 private static final Logger logger
= LogManager
.getLogger();
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
;
73 @XmlElement(name
= "Latitude")
75 @NotNull(groups
= Level2
.class)
76 private Double latitude
;
79 * Error radius in meters
81 @XmlElement(name
= "ErrorRadius")
84 private Integer errorRadius
;
86 @XmlElement(name
= "ReferenceSystem")
88 @XmlSchemaType(name
= "IDREF")
89 @ManyToOne(fetch
= FetchType
.LAZY
)
90 private ReferenceSystem referenceSystem
;
93 //******************** FACTORY METHODS ****************************
95 public static Point
NewInstance(){
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
);
108 // ******************** CONSTRUCTOR ***************************
113 //************** Sexagesimal /decimal METHODS *******************
115 public enum Direction
{
119 public String
toString() {
126 public String
toString() {
133 public String
toString() {
140 public String
toString() {
146 public static final class CoordinateParser
{
149 * Pattern zum parsen von Sexagesimalen Grad: 145°
151 private static final String DEGREE_REGEX
= "([0-9]*)\u00B0";
153 * Pattern zum parsen von Sexagesimalen Minuten: 65'
155 private static final String MINUTES_REGEX
= "(?:([0-9]*)')?";
157 * Pattern zum parsen von Sexagesimalen Sekunden: 17"
159 private static final String SECONDS_REGEX
= "(?:([0-9]*)(?:''|\"))?";
161 * Himmelsrichtung Längengrad
163 private static final String LONGITUDE_DIRECTION_REGEX
= "([OEW])";
165 * Himmelsrichtung Breitengrad
167 private static final String LATITUDE_DIRECTION_REGEX
= "([NS])";
170 * Pattern zum Parsen von Breitengraden.
172 private static final Pattern LATITUDE_PATTERN
= Pattern
173 .compile(DEGREE_REGEX
+ MINUTES_REGEX
+ SECONDS_REGEX
174 + LATITUDE_DIRECTION_REGEX
);
177 * Pattern zum Parsen von Längengraden.
179 private static final Pattern LONGITUDE_PATTERN
= Pattern
180 .compile(DEGREE_REGEX
+ MINUTES_REGEX
+ SECONDS_REGEX
181 + LONGITUDE_DIRECTION_REGEX
);
183 private CoordinateParser() {
184 throw new AssertionError( );
188 * Parst einen Breitengrad der Form<br>
193 * sind ebenfalls erlaubt.
196 * @return Die geparsten Koordinaten
197 * @throws ParseException
198 * Wenn eine Fehler beim Parsen aufgetreten ist.
200 public static Sexagesimal
parseLatitude(final String strg
)
201 throws ParseException
{
202 return parseCoordinates(strg
, LATITUDE_PATTERN
);
206 * Parst einen Längengrad der Form<br>
211 * sind ebenfalls erlaubt.
214 * @return Die geparsten Koordinaten
215 * @throws ParseException
216 * Wenn eine Fehler beim Parsen aufgetreten ist.
218 public static Sexagesimal
parseLongitude(final String strg
)
219 throws ParseException
{
220 return parseCoordinates(strg
, LONGITUDE_PATTERN
);
225 * Not used at the moment. Use CoordinateConverter instead.
229 * @throws ParseException
231 private static Sexagesimal
parseCoordinates(final String strg
, final Pattern pattern
) throws ParseException
{
233 throw new java
.text
.ParseException("Keine Koordinaten gegeben.", -1);
235 final Matcher matcher
= pattern
.matcher(strg
);
236 if (matcher
.matches( )) {
237 if (matcher
.groupCount( ) == 4) {
239 String tmp
= matcher
.group(1);
240 int degree
= Integer
.parseInt(tmp
);
243 tmp
= matcher
.group(2);
244 int minutes
= Sexagesimal
.NONE
;
246 minutes
= Integer
.parseInt(tmp
);
250 tmp
= matcher
.group(3);
251 int seconds
= Sexagesimal
.NONE
;
253 seconds
= Integer
.parseInt(tmp
);
257 tmp
= matcher
.group(4);
258 final Direction direction
;
259 if (tmp
.equals("N")) {
260 direction
= Direction
.NORTH
;
262 else if (tmp
.equals("S")) {
263 direction
= Direction
.SOUTH
;
265 else if (tmp
.equals("E") || tmp
.equals("O")) {
266 direction
= Direction
.EAST
;
268 else if (tmp
.equals("W")) {
269 direction
= Direction
.WEST
;
274 return Sexagesimal
.NewInstance(degree
, minutes
, seconds
, direction
);
277 throw new java
.text
.ParseException(
278 "Die Koordinaten-Darstellung ist fehlerhaft: " + strg
,
283 throw new java
.text
.ParseException(
284 "Die Koordinaten-Darstellung ist fehlerhaft: " + strg
, -1);
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.;
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
;
303 public static final int NONE
= 0;
304 public Integer degree
;
305 public Integer minutes
;
306 public Integer seconds
;
307 public Double tertiers
;
309 public Direction direction
;
312 public boolean isLatitude(){
313 return (direction
== Direction
.WEST
) || (direction
== Direction
.EAST
) ;
315 public boolean isLongitude(){
316 return ! isLatitude();
320 public static Sexagesimal
valueOf(Double decimal
, boolean isLatitude
){
321 return valueOf(decimal
, isLatitude
, false, false, true);
324 public static Sexagesimal
valueOf(Double decimal
, boolean isLatitude
, boolean nullSecondsToNull
, boolean nullMinutesToNull
, boolean allowTertiers
){
328 Sexagesimal sexagesimal
= new Sexagesimal();
329 Double decimalDegree
= decimal
;
331 if (decimalDegree
< 0) {
332 sexagesimal
.direction
= Direction
.SOUTH
;
335 sexagesimal
.direction
= Direction
.NORTH
;
339 if (decimalDegree
< 0) {
340 sexagesimal
.direction
= Direction
.WEST
;
343 sexagesimal
.direction
= Direction
.EAST
;
347 // Decimal in \u00B0'" umrechnen
348 double d
= Math
.abs(decimalDegree
);
349 if (! allowTertiers
){
350 d
+= HALF_SECOND
; // add half a second for rounding
352 d
+= HALF_SECOND
/ 10000; //to avoid rounding errors
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;
359 if (sexagesimal
.seconds
== 0 && nullSecondsToNull
){
360 sexagesimal
.seconds
= null;
362 if (sexagesimal
.seconds
== null && sexagesimal
.minutes
== 0 && nullMinutesToNull
){
363 sexagesimal
.minutes
= null;
366 // sexagesimal.decimalRadian = Math.toRadians(this.decimalDegree);
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
)));
377 if (this.direction
== Direction
.WEST
|| this.direction
== Direction
.SOUTH
) {
378 value
= value
.negate( );
380 return value
.doubleValue( );
384 public String
toString(){
385 return toString(false, false);
387 public String
toString(boolean includeEmptySeconds
){
388 return toString(includeEmptySeconds
, false);
391 public String
toString(boolean includeEmptySeconds
, boolean removeTertiers
){
393 result
= String
.valueOf(CdmUtils
.Nz(degree
)) + "\u00B0";
394 if (seconds
!= null || minutes
!= null){
395 result
+= String
.valueOf(CdmUtils
.Nz(minutes
)) + "'";
397 if (seconds
!= null ){
398 if (seconds
!= 0 || includeEmptySeconds
){
399 result
+= String
.valueOf(CdmUtils
.Nz(seconds
)) + getTertiersString(tertiers
, removeTertiers
) + "\"";
405 private String
getTertiersString(Double tertiers
, boolean removeTertiers
) {
407 if (tertiers
== null || removeTertiers
){
410 if (tertiers
>= 1.0 || tertiers
< 0.0){
411 throw new IllegalStateException("Tertiers should be 0.0 <= tertiers < 1.0 but are '" + tertiers
+ "'");
413 String result
= tertiers
.toString();
414 int pos
= result
.indexOf("E");
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
;
422 if (result
.length() > 5){
423 result
= result
.substring(0, 5);
425 while (result
.endsWith("0")){
426 result
= result
.substring(0, result
.length() -1);
428 result
= result
.substring(1);
429 if (result
.equals(".")){
438 public Sexagesimal
getLongitudeSexagesimal (){
439 boolean isLatitude
= false;
440 return Sexagesimal
.valueOf(longitude
, isLatitude
);
444 public Sexagesimal
getLatitudeSexagesimal (){
445 boolean isLatitude
= true;
446 return Sexagesimal
.valueOf(latitude
, isLatitude
);
450 public void setLatitudeSexagesimal(Sexagesimal sexagesimalLatitude
){
451 this.latitude
= sexagesimalLatitude
.toDecimal();
454 public void setLongitudeSexagesimal(Sexagesimal sexagesimalLongitude
){
455 this.longitude
= sexagesimalLongitude
.toDecimal();
459 public void setLatitudeByParsing(String string
) throws ParseException
{
460 this.setLatitude(parseLatitude(string
));
464 public void setLongitudeByParsing(String string
) throws ParseException
{
465 this.setLongitude(parseLongitude(string
));
469 public static Double
parseLatitude(String string
) throws ParseException
{
471 if (string
== null || string
.isEmpty()){
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);
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);
487 return result
.convertedCoord
;
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);
497 public static Double
parseLongitude(String string
) throws ParseException
{
499 if (string
== null || string
.isEmpty()){
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);
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);
515 return result
.convertedCoord
;
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);
525 private static String
setCurrentDoubleSeparator(String string
) {
526 String regExReplaceComma
= "(\\,|\\.)";
527 string
= string
.replaceAll(regExReplaceComma
,".");
532 private static boolean isDouble(String string
) {
534 Double
.valueOf(string
);
537 } catch (NumberFormatException e
) {
539 } catch (Exception e
) {
546 * <code>true</code>, if none of the attributes (lat, long, errRadius, refSys) is set.
549 public boolean isEmpty(){
550 if (errorRadius
== null && latitude
== null && longitude
== null
551 && referenceSystem
== null){
558 // ******************** GETTER / SETTER ********************************
560 public ReferenceSystem
getReferenceSystem(){
561 return this.referenceSystem
;
566 * @param referenceSystem referenceSystem
568 public void setReferenceSystem(ReferenceSystem referenceSystem
){
569 this.referenceSystem
= referenceSystem
;
572 public Double
getLongitude(){
573 return this.longitude
;
578 * @param longitude longitude
580 public void setLongitude(Double longitude
){
581 this.longitude
= longitude
;
584 public Double
getLatitude(){
585 return this.latitude
;
590 * @param latitude latitude
592 public void setLatitude(Double latitude
){
593 this.latitude
= latitude
;
597 * Error radius in Meters
599 public Integer
getErrorRadius(){
600 return this.errorRadius
;
605 * @param errorRadius errorRadius
607 public void setErrorRadius(Integer errorRadius
){
608 this.errorRadius
= errorRadius
;
611 // **************** toString *************************/
615 * Returns a string representation in sexagesimal coordinates.
618 public String
toSexagesimalString(boolean includeEmptySeconds
, boolean includeReferenceSystem
){
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
);
630 public String
toString(){
632 boolean includeEmptySeconds
= true;
633 result
+= getLatitudeSexagesimal() == null ?
"" : getLatitudeSexagesimal().toString(includeEmptySeconds
);
634 result
= CdmUtils
.concat(", ", result
, getLongitudeSexagesimal() == null ?
"" : getLongitudeSexagesimal().toString(includeEmptySeconds
));
639 //*********** CLONE **********************************/
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>
646 * @see java.lang.Object#clone()
649 public Point
clone(){
651 Point result
= (Point
)super.clone();
652 result
.setReferenceSystem(this.referenceSystem
);
653 //no changes to: errorRadius, latitude, longitude
655 } catch (CloneNotSupportedException e
) {
656 logger
.warn("Object does not implement cloneable");