Project

General

Profile

Download (21.8 KB) Statistics
| Branch: | Tag: | Revision:
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;import org.apache.logging.log4j.Logger;
35
import org.hibernate.search.annotations.Field;
36
import org.hibernate.search.annotations.Latitude;
37
import org.hibernate.search.annotations.Longitude;
38
import org.hibernate.search.annotations.NumericField;
39
import org.hibernate.search.annotations.Spatial;
40
import org.hibernate.search.annotations.SpatialMode;
41

    
42
import eu.etaxonomy.cdm.common.CdmUtils;
43
import eu.etaxonomy.cdm.strategy.parser.location.CoordinateConverter;
44
import eu.etaxonomy.cdm.strategy.parser.location.CoordinateConverter.ConversionResults;
45
import eu.etaxonomy.cdm.validation.Level2;
46

    
47
/**
48
 * @author m.doering
49
 * @since 08-Nov-2007 13:06:44
50
 */
51
@XmlAccessorType(XmlAccessType.FIELD)
52
@XmlType(name = "Point", propOrder = {
53
    "longitude",
54
    "latitude",
55
    "errorRadius",
56
    "referenceSystem"
57
})
58
@XmlRootElement(name = "Point")
59
@Embeddable
60
@Spatial(spatialMode=SpatialMode.RANGE, name="point")
61
public class Point implements Cloneable, Serializable {
62
    private static final long serialVersionUID = 531030660792800636L;
63
    private static final Logger logger = LogManager.getLogger(Point.class);
64

    
65
    //TODO was Float but H2 threw errors, maybe we should also use BigDecimal for exactness, see #8978
66
    @XmlElement(name = "Longitude")
67
    @Longitude(of="point")
68
    @NotNull(groups = Level2.class)
69
    private Double longitude;
70

    
71
    @XmlElement(name = "Latitude")
72
    @Latitude(of="point")
73
    @NotNull(groups = Level2.class)
74
    private Double latitude;
75

    
76
    /**
77
     * Error radius in meters
78
     */
79
    @XmlElement(name = "ErrorRadius")
80
    @Field
81
    @NumericField
82
    private Integer errorRadius;
83

    
84
    @XmlElement(name = "ReferenceSystem")
85
    @XmlIDREF
86
    @XmlSchemaType(name = "IDREF")
87
    @ManyToOne(fetch = FetchType.LAZY)
88
    private ReferenceSystem referenceSystem;
89

    
90

    
91
//******************** FACTORY METHODS ****************************
92

    
93
    public static Point NewInstance(){
94
        return new Point();
95
    }
96

    
97
    public static Point NewInstance(Double longitude, Double latitude, ReferenceSystem referenceSystem, Integer errorRadius){
98
        Point result = new Point();
99
        result.setLongitude(longitude);
100
        result.setLatitude(latitude);
101
        result.setReferenceSystem(referenceSystem);
102
        result.setErrorRadius(errorRadius);
103
        return result;
104
    }
105

    
106
// ******************** CONSTRUCTOR ***************************
107

    
108
    public Point() {
109
    }
110

    
111
//************** Sexagesimal /decimal METHODS *******************
112

    
113
    public enum Direction {
114
        WEST {
115

    
116
            @Override
117
            public String toString() {
118
                return "W";
119
            }
120
        },
121
        EAST {
122

    
123
            @Override
124
            public String toString() {
125
                return "E";
126
            }
127
        },
128
        NORTH {
129

    
130
            @Override
131
            public String toString() {
132
                return "N";
133
            }
134
        },
135
        SOUTH {
136

    
137
            @Override
138
            public String toString() {
139
                return "S";
140
            }
141
        };
142
    }
143

    
144
    public static final class CoordinateParser {
145

    
146
        /**
147
         * Pattern zum parsen von Sexagesimalen Grad: 145°
148
         */
149
        private static final String DEGREE_REGEX = "([0-9]*)\u00B0";
150
        /**
151
         * Pattern zum parsen von Sexagesimalen Minuten: 65'
152
         */
153
        private static final String MINUTES_REGEX = "(?:([0-9]*)')?";
154
        /**
155
         * Pattern zum parsen von Sexagesimalen Sekunden: 17"
156
         */
157
        private static final String SECONDS_REGEX = "(?:([0-9]*)(?:''|\"))?";
158
        /**
159
         * Himmelsrichtung Längengrad
160
         */
161
        private static final String LONGITUDE_DIRECTION_REGEX = "([OEW])";
162
        /**
163
         * Himmelsrichtung Breitengrad
164
         */
165
        private static final String LATITUDE_DIRECTION_REGEX = "([NS])";
166

    
167
        /**
168
         * Pattern zum Parsen von Breitengraden.
169
         */
170
        private static final Pattern LATITUDE_PATTERN = Pattern
171
                .compile(DEGREE_REGEX + MINUTES_REGEX + SECONDS_REGEX
172
                        + LATITUDE_DIRECTION_REGEX);
173

    
174
        /**
175
         * Pattern zum Parsen von Längengraden.
176
         */
177
        private static final Pattern LONGITUDE_PATTERN = Pattern
178
                .compile(DEGREE_REGEX + MINUTES_REGEX + SECONDS_REGEX
179
                        + LONGITUDE_DIRECTION_REGEX);
180

    
181
        private CoordinateParser() {
182
            throw new AssertionError( );
183
        }
184

    
185
        /**
186
         * Parst einen Breitengrad der Form<br>
187
         * G°M'S""(OEW)<br>
188
         * Die Formen<br>
189
         * G°(OEW)<br>
190
         * G°M'(OEW)<br>
191
         * sind ebenfalls erlaubt.
192
         *
193
         * @param strg
194
         * @return Die geparsten Koordinaten
195
         * @throws ParseException
196
         *             Wenn eine Fehler beim Parsen aufgetreten ist.
197
         */
198
        public static Sexagesimal parseLatitude(final String strg)
199
                throws ParseException {
200
            return parseCoordinates(strg, LATITUDE_PATTERN);
201
        }
202

    
203
        /**
204
         * Parst einen Längengrad der Form<br>
205
         * G°M'S"(NS)<br>
206
         * Die Formen<br>
207
         * G°(NS)<br>
208
         * G°M'(NS)<br>
209
         * sind ebenfalls erlaubt.
210
         *
211
         * @param strg
212
         * @return Die geparsten Koordinaten
213
         * @throws ParseException
214
         *             Wenn eine Fehler beim Parsen aufgetreten ist.
215
         */
216
        public static Sexagesimal parseLongitude(final String strg)
217
                throws ParseException {
218
            return parseCoordinates(strg, LONGITUDE_PATTERN);
219
        }
220

    
221

    
222
        /**
223
         * Not used at the moment. Use CoordinateConverter instead.
224
         * @param strg
225
         * @param pattern
226
         * @return
227
         * @throws ParseException
228
         */
229
        private static Sexagesimal parseCoordinates(final String strg, final Pattern pattern) throws ParseException {
230
            if (strg == null) {
231
                throw new java.text.ParseException("Keine Koordinaten gegeben.", -1);
232
            }
233
            final Matcher matcher = pattern.matcher(strg);
234
            if (matcher.matches( )) {
235
                if (matcher.groupCount( ) == 4) {
236
                    // Grad
237
                    String tmp = matcher.group(1);
238
                    int degree = Integer.parseInt(tmp);
239

    
240
                    // Optional minutes
241
                    tmp = matcher.group(2);
242
                    int minutes = Sexagesimal.NONE;
243
                    if (tmp != null) {
244
                        minutes = Integer.parseInt(tmp);
245
                    }
246

    
247
                    // Optional seconds
248
                    tmp = matcher.group(3);
249
                    int seconds = Sexagesimal.NONE;
250
                    if (tmp != null) {
251
                        seconds = Integer.parseInt(tmp);
252
                    }
253

    
254
                    // directions
255
                    tmp = matcher.group(4);
256
                    final Direction direction;
257
                    if (tmp.equals("N")) {
258
                        direction = Direction.NORTH;
259
                    }
260
                    else if (tmp.equals("S")) {
261
                        direction = Direction.SOUTH;
262
                    }
263
                    else if (tmp.equals("E") || tmp.equals("O")) {
264
                        direction = Direction.EAST;
265
                    }
266
                    else if (tmp.equals("W")) {
267
                        direction = Direction.WEST;
268
                    }
269
                    else {
270
                        direction = null;
271
                    }
272
                    return Sexagesimal.NewInstance(degree, minutes, seconds, direction);
273
                }
274
                else {
275
                    throw new java.text.ParseException(
276
                            "Die Koordinaten-Darstellung ist fehlerhaft: " + strg,
277
                            -1);
278
                }
279
            }
280
            else {
281
                throw new java.text.ParseException(
282
                        "Die Koordinaten-Darstellung ist fehlerhaft: " + strg, -1);
283
            }
284
        }
285

    
286
    }
287

    
288

    
289
    private static final BigDecimal SIXTY = BigDecimal.valueOf(60.0);
290
    private static final MathContext MC = new MathContext(34, RoundingMode.HALF_UP);
291
    private static final double HALF_SECOND = 1. / 7200.;
292

    
293
    //see http://www.tutorials.de/forum/archiv/348596-quiz-10-zeja-java.html
294
    public static class Sexagesimal{
295
        public static Sexagesimal NewInstance(Integer degree, Integer minutes, Integer seconds, Direction direction){
296
            Sexagesimal result = new Sexagesimal();
297
            result.degree = degree; result.minutes = minutes; result.seconds = seconds;
298
            return result;
299
        }
300

    
301
        public static final int NONE = 0;
302
        public Integer degree;
303
        public Integer minutes;
304
        public Integer seconds;
305
        public Double tertiers;
306

    
307
        public Direction direction;
308

    
309

    
310
        public boolean isLatitude(){
311
            return (direction == Direction.WEST) || (direction == Direction.EAST) ;
312
        }
313
        public boolean isLongitude(){
314
            return ! isLatitude();
315
        }
316

    
317

    
318
        public static Sexagesimal valueOf(Double decimal, boolean isLatitude){
319
            return valueOf(decimal, isLatitude, false, false, true);
320
        }
321

    
322
        public static Sexagesimal valueOf(Double decimal, boolean isLatitude, boolean nullSecondsToNull, boolean nullMinutesToNull, boolean allowTertiers){
323
            if(decimal == null){
324
                return null;
325
            }
326
            Sexagesimal sexagesimal = new Sexagesimal();
327
            Double decimalDegree = decimal;
328
                if (isLatitude) {
329
                    if (decimalDegree < 0) {
330
                        sexagesimal.direction = Direction.SOUTH;
331
                    }
332
                    else {
333
                        sexagesimal.direction = Direction.NORTH;
334
                    }
335
                }
336
                else {
337
                    if (decimalDegree < 0) {
338
                           sexagesimal.direction = Direction.WEST;
339
                        }
340
                        else {
341
                            sexagesimal.direction = Direction.EAST;
342
                        }
343
                }
344

    
345
                // Decimal in \u00B0'" umrechnen
346
                double d = Math.abs(decimalDegree);
347
                if (! allowTertiers){
348
                    d += HALF_SECOND; // add half a second for rounding
349
                }else{
350
                    d += HALF_SECOND / 10000;  //to avoid rounding errors
351
                }
352
                sexagesimal.degree = (int) Math.floor(d);
353
                sexagesimal.minutes = (int) Math.floor((d - sexagesimal.degree) * 60.0);
354
                sexagesimal.seconds = (int) Math.floor((d - sexagesimal.degree - sexagesimal.minutes / 60.0) * 3600.0);
355
                sexagesimal.tertiers = (d - sexagesimal.degree - sexagesimal.minutes / 60.0 - sexagesimal.seconds / 3600.0) * 3600.0;
356

    
357
                if (sexagesimal.seconds == 0 && nullSecondsToNull){
358
                    sexagesimal.seconds = null;
359
                }
360
                if (sexagesimal.seconds == null && sexagesimal.minutes == 0 && nullMinutesToNull){
361
                    sexagesimal.minutes = null;
362
                }
363

    
364
               // sexagesimal.decimalRadian = Math.toRadians(this.decimalDegree);
365
                return sexagesimal;
366
        }
367

    
368

    
369

    
370
        private Double toDecimal(){
371
            BigDecimal value = BigDecimal.valueOf(CdmUtils.Nz(this.seconds)).divide(SIXTY, MC).add
372
                (BigDecimal.valueOf(CdmUtils.Nz(this.minutes))).divide(SIXTY, MC).add
373
                (BigDecimal.valueOf(CdmUtils.Nz(this.degree)));
374

    
375
            if (this.direction == Direction.WEST || this.direction == Direction.SOUTH) {
376
                value = value.negate( );
377
            }
378
            return value.doubleValue( );
379
        }
380

    
381
        @Override
382
        public String toString(){
383
            return toString(false, false);
384
        }
385
        public String toString(boolean includeEmptySeconds){
386
            return toString(includeEmptySeconds, false);
387
        }
388

    
389
        public String toString(boolean includeEmptySeconds, boolean removeTertiers){
390
            String result;
391
            result = String.valueOf(CdmUtils.Nz(degree)) + "\u00B0";
392
            if (seconds != null || minutes != null){
393
                result += String.valueOf(CdmUtils.Nz(minutes)) + "'";
394
            }
395
            if (seconds != null ){
396
                if (seconds != 0 || includeEmptySeconds){
397
                    result += String.valueOf(CdmUtils.Nz(seconds)) + getTertiersString(tertiers, removeTertiers) + "\"";
398
                }
399
            }
400
            result += direction;
401
            return result;
402
        }
403
        private String getTertiersString(Double tertiers, boolean removeTertiers) {
404
            if (tertiers == null || removeTertiers){
405
                return "";
406
            }else{
407
                if (tertiers >= 1.0 || tertiers < 0.0){
408
                    throw new IllegalStateException("Tertiers should be 0.0 <= tertiers < 1.0 but are '" + tertiers + "'");
409
                }
410
                String result = tertiers.toString();
411
                int pos = result.indexOf("E");
412
                if (pos > -1){
413
                    int exp = - Integer.valueOf(result.substring(pos + 1));
414
                    result = result.substring(0, pos).replace(".", "");
415
                    result = "0." + StringUtils.leftPad("", exp - 1, "0") +  result;
416

    
417
                }
418

    
419
                if (result.length() > 5){
420
                    result = result.substring(0, 5);
421
                }
422
                while (result.endsWith("0")){
423
                    result = result.substring(0, result.length() -1);
424
                }
425
                result = result.substring(1);
426
                if (result.equals(".")){
427
                    result = "";
428
                }
429
                return result;
430
            }
431

    
432
        }
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
}
(5-5/8)