001/*
002 * JScience - Java(TM) Tools and Libraries for the Advancement of Sciences.
003 * Copyright (C) 2006 - JScience (http://jscience.org/)
004 * All rights reserved.
005 *
006 * Permission to use, copy, modify, and distribute this software is
007 * freely granted, provided that this notice is preserved.
008 */
009package org.jscience.geography.coordinates;
010
011import static org.jscience.geography.coordinates.crs.ReferenceEllipsoid.WGS84;
012
013import javax.measure.Measure;
014import javax.measure.converter.UnitConverter;
015import javax.measure.quantity.*;
016import javax.measure.unit.*;
017import static javax.measure.unit.SI.*;
018import static javax.measure.unit.NonSI.*;
019
020import javolution.context.ObjectFactory;
021import javolution.xml.XMLFormat;
022import javolution.xml.stream.XMLStreamException;
023
024import org.jscience.geography.coordinates.crs.*;
025import org.opengis.referencing.cs.CoordinateSystem;
026
027/**
028 * This class represents the {@link ProjectedCRS projected}
029 * Universal Transverse Mercator (UTM) coordinates onto the WGS84 ellipsoid.
030 *
031 * <p>The UTM system is limited to values between -80 and +84 degrees latitude.
032 * Values beyond these limits (i.e., the polar regions) are projected
033 * using the Universal Polar Stereographic (UPS) projection. Although
034 * mathematically distinct, the two projections are represented identically.
035 * This class returns correct results for both UTM and UPS projections.
036 *
037 * The conversion routines for this class were derived from formulas described
038 * in the
039 * <a href="http://earth-info.nga.mil/GandG/publications/tm8358.2/TM8358_2.pdf">
040 * Defense Mapping Agency Technical Manual 8358.2.</a>
041 *
042 * @author Paul D. Anderson
043 * @version 3.0, February 25, 2006
044 * @see <a href="http://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system">
045 *      Wikipedia: Universal Transverse Mercator Coordinate System</a>
046 */
047public final class UTM extends Coordinates<ProjectedCRS<?>> {
048
049    /**
050     * The UTM scale factor. This the exact scale factor only on a pair of
051     * lines lying either side of the central meridian, but the effect is to
052     * reduce overall distortion within the UTM zone to less than one part per
053     * thousand.
054     */
055    public static final double UTM_SCALE_FACTOR = 0.9996;
056
057    /**
058     * The UTM "false easting" value. This quantity is added to the true
059     * easting to avoid using negative numbers in the coordinates.
060     */
061    public static Measure<Integer, Length> UTM_FALSE_EASTING = Measure.valueOf(
062            500000, METRE);
063
064    /**
065     * The UTM "false northing" value. This quantity is added to the true
066     * northing for coordinates <b>in the southern hemisphere only</b>
067     * to avoid using negative numbers in the coordinates.
068     */
069    public static Measure<Integer, Length> UTM_FALSE_NORTHING = Measure
070            .valueOf(10000000, METRE);
071
072    /**
073     * The northern limit of the UTM grid. Beyond this limit the distortion
074     * introduced by the transverse Mercator projection is impractically
075     * large, and the UPS grid is used instead.
076     */
077    public static final Measure<Integer, Angle> UTM_NORTHERN_LIMIT = Measure
078            .valueOf(84, DEGREE_ANGLE);
079
080    /**
081     * The southern limit of the UTM grid. Beyond this limit the distortion
082     * introduced by the transverse Mercator projection is impractically
083     * large, and the UPS grid is used instead.
084     */
085    public static final Measure<Integer, Angle> UTM_SOUTHERN_LIMIT = Measure
086            .valueOf(-80, DEGREE_ANGLE);
087
088    /**
089     * The UPS scale factor.
090     */
091    public static final double UPS_SCALE_FACTOR = 0.994;
092
093    /**
094     * The UPS "false easting" value. This quantity is added to the true
095     * easting to avoid using negative numbers in the coordinates.
096     */
097    public static Measure<Integer, Length> UPS_FALSE_EASTING = Measure.valueOf(
098            2000000, METRE);
099
100    /**
101     * The UPS "false northing" value. This quantity is added to the true
102     * northing to avoid using negative numbers in the coordinates.
103     * The UPS system, unlike the UTM system, always includes the false northing.
104     */
105    public static Measure<Integer, Length> UPS_FALSE_NORTHING = Measure
106            .valueOf(2000000, METRE);
107
108    /*
109     * NOTE: The calculations in this class use power series expansions.
110     * The naming convention is to include the power in the name
111     * of the term, so that the square of K0 is 'K02', the cube
112     * is 'K03', etc.
113     */
114    private static final double K0 = UTM_SCALE_FACTOR;
115
116    private static final double K02 = K0 * K0;
117
118    private static final double K03 = K02 * K0;
119
120    private static final double K04 = K03 * K0;
121
122    private static final double K05 = K04 * K0;
123
124    private static final double K06 = K05 * K0;
125
126    private static final double K07 = K06 * K0;
127
128    private static final double K08 = K07 * K0;
129
130    /**
131     * Holds the coordinate reference system for all instances of this class.
132     */
133    public static final ProjectedCRS<UTM> CRS = new ProjectedCRS<UTM>() {
134
135        private final double NORTHERN_LIMIT_IN_DEGREES = UTM_NORTHERN_LIMIT
136                .doubleValue(NonSI.DEGREE_ANGLE);
137
138        private final double SOUTHERN_LIMIT_IN_DEGREES = UTM_SOUTHERN_LIMIT
139                .doubleValue(NonSI.DEGREE_ANGLE);
140
141        @Override
142        protected UTM coordinatesOf(AbsolutePosition position) {
143            LatLong latLong = LatLong.valueOf(position.latitudeWGS84
144                    .doubleValue(SI.RADIAN), position.longitudeWGS84
145                    .doubleValue(SI.RADIAN), SI.RADIAN);
146            // UTM or UPS?
147            final double latitude = position.latitudeWGS84
148                    .doubleValue(NonSI.DEGREE_ANGLE);
149            if (latitude > SOUTHERN_LIMIT_IN_DEGREES
150                    && latitude < NORTHERN_LIMIT_IN_DEGREES) {
151                return latLongToUtm(latLong, WGS84);
152            } else {
153                return latLongToUps(latLong, WGS84);
154            }
155        }
156
157        @Override
158        protected AbsolutePosition positionOf(UTM coordinates,
159                AbsolutePosition position) {
160            final LatLong latLong;
161            if (coordinates.latitudeZone() < 'C'
162                    || coordinates.latitudeZone() > 'X') {
163                latLong = upsToLatLong(coordinates, WGS84);
164            } else {
165                latLong = utmToLatLong(coordinates, WGS84);
166            }
167            position.latitudeWGS84 = Measure.valueOf(latLong
168                    .latitudeValue(SI.RADIAN), SI.RADIAN);
169            position.longitudeWGS84 = Measure.valueOf(latLong
170                    .longitudeValue(SI.RADIAN), SI.RADIAN);
171            return position;
172        }
173
174        @Override
175        public CoordinateSystem getCoordinateSystem() {
176            return ProjectedCRS.EASTING_NORTHING_CS;
177        }
178    };
179
180    /**
181     * Holds the longitude zone identifier.
182     */
183    private int _longitudeZone;
184
185    /**
186     * Holds the latitude zone identifier.
187     */
188    private char _latitudeZone;
189
190    /**
191     * Holds the easting in meters.
192     */
193    private double _easting;
194
195    /**
196     * Holds the northing in meters.
197     */
198    private double _northing;
199
200    /**
201     * Returns the projected UTM position corresponding to the specified
202     * coordinates.
203     *
204     * @param longitudeZone the longitude zone number.
205     * @param latitudeZone  the longitude zone character.
206     * @param easting       the easting value stated in the specified unit.
207     * @param northing      the northing value stated in the specified unit.
208     * @param unit          the easting/northing length unit.
209     * @return the corresponding surface position.
210     */
211    public static UTM valueOf(int longitudeZone, char latitudeZone,
212            double easting, double northing, Unit<Length> unit) {
213        UTM utm = FACTORY.object();
214        utm._longitudeZone = longitudeZone;
215        utm._latitudeZone = latitudeZone;
216        if (unit == METRE) {
217            utm._easting = easting;
218            utm._northing = northing;
219        } else {
220            UnitConverter toMeter = unit.getConverterTo(METRE);
221            utm._easting = toMeter.convert(easting);
222            utm._northing = toMeter.convert(northing);
223        }
224        return utm;
225    }
226
227    private static final ObjectFactory<UTM> FACTORY = new ObjectFactory<UTM>() {
228
229        @Override
230        protected UTM create() {
231            return new UTM();
232        }
233    };
234
235    private UTM() {
236    }
237
238    /**
239     * Returns the longitude zone identifier.
240     *
241     * @return the longitude zone number.
242     */
243    public final int longitudeZone() {
244        return _longitudeZone;
245    }
246
247    /**
248     * Returns the latitude zone identifier.
249     *
250     * @return the latitude zone character.
251     */
252    public final char latitudeZone() {
253        return _latitudeZone;
254    }
255
256    /**
257     * Returns the projected distance of the position from the central meridian.
258     *
259     * @param unit the length unit of the easting to return.
260     * @return the easting stated in the specified unit.
261     */
262    public final double eastingValue(Unit<Length> unit) {
263        return unit.equals(METRE) ? _easting : METRE.getConverterTo(unit)
264                .convert(_easting);
265    }
266
267    /**
268     * Returns the projected distance of the point from the equator.
269     *
270     * @param unit the length unit of the northing to return.
271     * @return the northing stated in the specified unit.
272     */
273    public final double northingValue(Unit<Length> unit) {
274        return unit.equals(METRE) ? _northing : METRE.getConverterTo(unit)
275                .convert(_northing);
276    }
277
278    @Override
279    public ProjectedCRS<UTM> getCoordinateReferenceSystem() {
280        return CRS;
281    }
282
283    // OpenGIS Interface.
284    public int getDimension() {
285        return 2;
286    }
287
288    // OpenGIS Interface.
289    public double getOrdinate(int dimension) throws IndexOutOfBoundsException {
290        if (dimension == 0) {
291            Unit<?> u = ProjectedCRS.EASTING_NORTHING_CS.getAxis(0).getUnit();
292            return METRE.getConverterTo(u).convert(_easting);
293        } else if (dimension == 1) {
294            Unit<?> u = ProjectedCRS.EASTING_NORTHING_CS.getAxis(1).getUnit();
295            return METRE.getConverterTo(u).convert(_northing);
296        } else {
297            throw new IndexOutOfBoundsException();
298        }
299    }
300
301    /**
302     * Returns true if the position indicated by the coordinates is
303     * north of the northern limit of the UTM grid (84 degrees).
304     *
305     * @param latLong The coordinates.
306     * @return True if the latitude is greater than 84 degrees.
307     */
308    public static boolean isNorthPolar(final LatLong latLong) {
309        return latLong.latitudeValue(DEGREE_ANGLE) > 84.0;
310    }
311
312    /**
313     * Returns true if the position indicated by the coordinates is
314     * south of the southern limit of the UTM grid (-80 degrees).
315     *
316     * @param latLong The coordinates.
317     * @return True if the latitude is less than -80 degrees.
318     */
319    public static boolean isSouthPolar(final LatLong latLong) {
320        return latLong.latitudeValue(DEGREE_ANGLE) < -80.0;
321    }
322
323    /**
324     * Returns the UTM/UPS latitude zone identifier for the specified coordinates.
325     *
326     * @param latLong The coordinates.
327     * @return the latitude zone character.
328     */
329    public static char getLatitudeZone(final LatLong latLong) {
330        if (isNorthPolar(latLong)) {
331            if (latLong.longitudeValue(RADIAN) < 0) {
332                return 'Y';
333            } else {
334                return 'Z';
335            }
336        }
337        if (isSouthPolar(latLong)) {
338            if (latLong.longitudeValue(RADIAN) < 0) {
339                return 'A';
340            } else {
341                return 'B';
342            }
343        }
344        final int degreesLatitude = (int) latLong.latitudeValue(DEGREE_ANGLE);
345        char zone = (char) ((degreesLatitude + 80) / 8 + 'C');
346        if (zone > 'H') {
347            zone++;
348        }
349        if (zone > 'N') {
350            zone++;
351        }
352        if (zone > 'X') {
353            zone = 'X';
354        }
355        return zone;
356    }
357
358    /**
359     * Returns the UTM/UPS longitude zone number for the specified
360     * coordinates.
361     *
362     * @param latLong  The coordinates.
363     * @return the longitude zone number.
364     */
365    public static int getLongitudeZone(LatLong latLong) {
366
367        final double degreesLongitude = latLong.longitudeValue(DEGREE_ANGLE);
368
369        // UPS longitude zones
370        if (isNorthPolar(latLong) || isSouthPolar(latLong)) {
371            if (degreesLongitude < 0.0) {
372                return 30;
373            } else {
374                return 31;
375            }
376        }
377
378        final char latitudeZone = getLatitudeZone(latLong);
379        // X latitude exceptions
380        if (latitudeZone == 'X' && degreesLongitude > 0.0
381                && degreesLongitude < 42.0) {
382            if (degreesLongitude < 9.0) {
383                return 31;
384            }
385            if (degreesLongitude < 21.0) {
386                return 33;
387            }
388            if (degreesLongitude < 33.0) {
389                return 35;
390            } else {
391                return 37;
392            }
393        }
394        // V latitude exceptions
395        if (latitudeZone == 'V' && degreesLongitude > 0.0
396                && degreesLongitude < 12.0) {
397            if (degreesLongitude < 3.0) {
398                return 31;
399            } else {
400                return 32;
401            }
402        }
403
404        return (int) ((degreesLongitude + 180) / 6) + 1;
405    }
406
407    /**
408     * Returns the central meridian (in radians) for the specified
409     * UTM/UPS zone.
410     * @param longitudeZone The UTM/UPS longitude zone number.
411     * @param latitudeZone  The UTM/UPS latitude zone character.
412     * @return The central meridian for the specified zone.
413     */
414    public static double getCentralMeridian(int longitudeZone, char latitudeZone) {
415        // polar zones
416        if (latitudeZone < 'C' || latitudeZone > 'X') {
417            return 0.0;
418        }
419        // X latitude zone exceptions
420        if (latitudeZone == 'X' && longitudeZone > 31 && longitudeZone <= 37) {
421            return Math.toRadians((longitudeZone - 1) * 6 - 180 + 4.5);
422        }
423        // V latitude zone exceptions
424        if (longitudeZone == 'V') {
425            if (latitudeZone == 31) {
426                return Math.toRadians(1.5);
427            } else if (latitudeZone == 32) {
428                return Math.toRadians(7.5);
429            }
430        }
431        return Math.toRadians((longitudeZone - 1) * 6 - 180 + 3);
432    }
433
434    /**
435     * Converts latitude/longitude coordinates to UTM coordinates based on
436     * the specified reference ellipsoid.
437     *
438     * @param latLong   The latitude/longitude coordinates.
439     * @param ellipsoid The reference ellipsoid.
440     * @return  The UTM coordinates.
441     */
442    public static UTM latLongToUtm(LatLong latLong, ReferenceEllipsoid ellipsoid) {
443        final char latitudeZone = getLatitudeZone(latLong);
444        final int longitudeZone = getLongitudeZone(latLong);
445
446        final double phi = latLong.latitudeValue(RADIAN);
447
448        final double cosPhi = Math.cos(phi);
449        final double cos2Phi = cosPhi * cosPhi;
450        final double cos3Phi = cos2Phi * cosPhi;
451        final double cos4Phi = cos3Phi * cosPhi;
452        final double cos5Phi = cos4Phi * cosPhi;
453        final double cos6Phi = cos5Phi * cosPhi;
454        final double cos7Phi = cos6Phi * cosPhi;
455        final double cos8Phi = cos7Phi * cosPhi;
456
457        final double tanPhi = Math.tan(phi);
458        final double tan2Phi = tanPhi * tanPhi;
459        final double tan4Phi = tan2Phi * tan2Phi;
460        final double tan6Phi = tan4Phi * tan2Phi;
461
462        final double eb2 = ellipsoid.getSecondEccentricitySquared();
463        final double eb4 = eb2 * eb2;
464        final double eb6 = eb4 * eb2;
465        final double eb8 = eb6 * eb2;
466
467        final double e2c2 = eb2 * cos2Phi;
468        final double e4c4 = eb4 * cos4Phi;
469        final double e6c6 = eb6 * cos6Phi;
470        final double e8c8 = eb8 * cos8Phi;
471
472        final double t2e2c2 = tan2Phi * e2c2;
473        final double t2e4c4 = tan2Phi * e4c4;
474        final double t2e6c6 = tan2Phi * e6c6;
475        final double t2e8c8 = tan2Phi * e8c8;
476
477        final double nu = ellipsoid.verticalRadiusOfCurvature(phi);
478        final double kn1 = K0 * nu * Math.sin(phi);
479        final double t1 = K0 * ellipsoid.meridionalArc(phi);
480        final double t2 = kn1 * cosPhi / 2.0;
481        final double t3 = (kn1 * cos3Phi / 24.0)
482                * (5.0 - tan2Phi + 9.0 * e2c2 + 4.0 * e4c4);
483        final double t4 = (kn1 * cos5Phi / 720.0)
484                * (61.0 - 58.0 * tan2Phi + tan4Phi + 270.0 * e2c2 - 330.0
485                        * t2e2c2 + 445.0 * e4c4 - 680.0 * t2e4c4 + 324.0 * e6c6
486                        - 600.0 * t2e6c6 + 88.0 * e8c8 - 192.0 * t2e8c8);
487        final double t5 = (kn1 * cos7Phi / 40320.0)
488                * (1385.0 - 3111.0 * tan2Phi + 543.0 * tan4Phi - tan6Phi);
489
490        final double kn2 = K0 * nu;
491        final double t6 = kn2 * cosPhi;
492        final double t7 = (kn2 * cos3Phi / 6.0) * (1.0 - tan2Phi + e2c2);
493        final double t8 = (kn2 * cos5Phi / 120.0)
494                * (5.0 - 18.0 * tan2Phi + tan4Phi + 14.0 * e2c2 - 58.0 * t2e2c2
495                        + 13.0 * e4c4 - 64.0 * t2e4c4 + 4.0 * e6c6 - 24.0 * t2e6c6);
496        final double t9 = (kn2 * cos7Phi / 50.40)
497                * (61.0 - 479.0 * tan2Phi + 179.0 * tan4Phi - tan6Phi);
498
499        final double lambda = latLong.longitudeValue(RADIAN);
500        final double lambda0 = getCentralMeridian(longitudeZone, latitudeZone);
501        final double dL = lambda - lambda0;
502        final double dL2 = dL * dL;
503        final double dL3 = dL2 * dL;
504        final double dL4 = dL3 * dL;
505        final double dL5 = dL4 * dL;
506        final double dL6 = dL5 * dL;
507        final double dL7 = dL6 * dL;
508        final double dL8 = dL7 * dL;
509
510        final double falseNorthing;
511        if ((phi < 0.0)) {
512            // southern hemisphere -- add false northing
513            falseNorthing = UTM_FALSE_NORTHING.doubleValue(METRE);
514        } else {
515            // northern hemisphere -- no false northing
516            falseNorthing = 0.0;
517        }
518        final double falseEasting = UTM_FALSE_EASTING.doubleValue(METRE);
519        final double northing = falseNorthing + t1 + dL2 * t2 + dL4 * t3 + dL6
520                * t4 + dL8 * t5;
521        final double easting = falseEasting + dL * t6 + dL3 * t7 + dL5 * t8
522                + dL7 * t9;
523
524        return UTM.valueOf(longitudeZone, latitudeZone, easting, northing,
525                METRE);
526    }
527
528    /**
529     * Converts latitude/longitude coordinates to UPS coordinates based on
530     * the specified reference ellipsoid.
531     *
532     * @param latLong   The latitude/longitude coordinates.
533     * @param ellipsoid The reference ellipsoid.
534     * @return  The UPS coordinates.
535     */
536    public static UTM latLongToUps(LatLong latLong, ReferenceEllipsoid ellipsoid) {
537
538        final char latitudeZone = getLatitudeZone(latLong);
539        final int longitudeZone = getLongitudeZone(latLong);
540
541        final double latitude = latLong.latitudeValue(RADIAN);
542        final double sign = Math.signum(latitude);
543        final double phi = Math.abs(latitude);
544        final double lambda = latLong.longitudeValue(RADIAN);
545
546        final double a = ellipsoid.getSemimajorAxis().doubleValue(METRE);
547        final double e = ellipsoid.getEccentricity();
548        final double e2 = ellipsoid.getEccentricitySquared();
549
550        final double c0 = ((2.0 * a) / Math.sqrt(1.0 - e2))
551                * Math.pow((1.0 - e) / (1.0 + e), e / 2.0);
552        final double eSinPhi = e * Math.sin(phi);
553        final double tz = Math.pow((1 + eSinPhi) / (1 - eSinPhi), e / 2.0)
554                * Math.tan(Math.PI / 4.0 - phi / 2.0);
555        final double radius = UPS_SCALE_FACTOR * c0 * tz;
556        final double falseNorthing = UPS_FALSE_NORTHING.doubleValue(METRE);
557        final double northing;
558        if (sign > 0) {
559            northing = falseNorthing - radius * Math.cos(lambda);
560        } else {
561            northing = falseNorthing + radius * Math.cos(lambda);
562        }
563        final double falseEasting = UPS_FALSE_EASTING.doubleValue(METRE);
564        final double easting = falseEasting + radius * Math.sin(lambda);
565
566        return UTM.valueOf(longitudeZone, latitudeZone, easting, northing,
567                METRE);
568    }
569
570    /**
571     * Converts the UTM coordinates to latitude/longitude coordinates,
572     * based on the specified reference ellipsoid.
573     *
574     * @param utm   The UTM coordinates.
575     * @param ellipsoid The reference ellipsoid.
576     * @return  The latitude/longitude coordinates.
577     */
578    public static LatLong utmToLatLong(UTM utm, ReferenceEllipsoid ellipsoid) {
579        final double northing;
580        if ((utm.latitudeZone() < 'N')) {
581            // southern hemisphere
582            northing = utm._northing - UTM_FALSE_NORTHING.doubleValue(METRE);
583        } else {
584            // northern hemisphere
585            northing = utm._northing;
586        }
587
588        // footpoint latitude
589        final double arc0 = northing / K0;
590        double rho = ellipsoid.meridionalRadiusOfCurvature(0.0);
591        double phi = arc0 / rho;
592        for (int i = 0; i < 5; i++) {
593            double arc = ellipsoid.meridionalArc(phi);
594            rho = ellipsoid.meridionalRadiusOfCurvature(phi);
595            double diff = (arc0 - arc) / rho;
596            if (Math.abs(diff) < Math.ulp(phi)) {
597                break;
598            }
599            phi += diff;
600        }
601
602        final double cosPhi = Math.cos(phi);
603        final double cos2Phi = cosPhi * cosPhi;
604        final double cos3Phi = cos2Phi * cosPhi;
605        final double cos4Phi = cos3Phi * cosPhi;
606        final double cos5Phi = cos4Phi * cosPhi;
607        final double cos6Phi = cos5Phi * cosPhi;
608        final double cos7Phi = cos6Phi * cosPhi;
609        final double cos8Phi = cos7Phi * cosPhi;
610
611        final double tanPhi = Math.tan(phi);
612        final double tan2Phi = tanPhi * tanPhi;
613        final double tan4Phi = tan2Phi * tan2Phi;
614        final double tan6Phi = tan4Phi * tan2Phi;
615
616        final double eb2 = ellipsoid.getSecondEccentricitySquared();
617        final double eb4 = eb2 * eb2;
618        final double eb6 = eb4 * eb2;
619        final double eb8 = eb6 * eb2;
620        final double e2c2 = eb2 * cos2Phi;
621        final double e4c4 = eb4 * cos4Phi;
622        final double e6c6 = eb6 * cos6Phi;
623        final double e8c8 = eb8 * cos8Phi;
624
625        final double t2e2c2 = tan2Phi * e2c2;
626        final double t2e4c4 = tan2Phi * e4c4;
627        final double t2e6c6 = tan2Phi * e6c6;
628        final double t2e8c8 = tan2Phi * e8c8;
629        final double t4e2c2 = tan4Phi * e2c2;
630        final double t4e4c4 = tan4Phi * e4c4;
631
632        final double nu = ellipsoid.verticalRadiusOfCurvature(phi);
633        final double nu2 = nu * nu;
634        final double nu3 = nu2 * nu;
635        final double nu5 = nu3 * nu2;
636        final double nu7 = nu5 * nu2;
637
638        final double lambda0 = getCentralMeridian(utm.longitudeZone(), utm
639                .latitudeZone());
640        final double dE = utm._easting - UTM_FALSE_EASTING.doubleValue(METRE);
641        final double dE2 = dE * dE;
642        final double dE3 = dE2 * dE;
643        final double dE4 = dE3 * dE;
644        final double dE5 = dE4 * dE;
645        final double dE6 = dE5 * dE;
646        final double dE7 = dE6 * dE;
647        final double dE8 = dE7 * dE;
648
649        final double t10 = tanPhi / (2.0 * rho * nu * K02);
650        final double t11 = tanPhi / (24.0 * rho * nu3 * K04)
651                * (5.0 + 3.0 * tan2Phi + e2c2 - 9.0 * t2e2c2 - 4.0 * e4c4);
652        final double t12 = tanPhi
653                / (720.0 * rho * nu5 * K06)
654                * (61.0 + 90.0 * tan2Phi + 45.0 * tan4Phi + 46.0 * e2c2 - 252.0
655                        * t2e2c2 - 90.0 * t4e2c2 - 3.0 * e4c4 - 66.0 * t2e4c4
656                        + 225.0 * t4e4c4 + 100.0 * e6c6 + 84.0 * t2e6c6 + 88.0
657                        * e8c8 - 192.0 * t2e8c8);
658        final double t13 = tanPhi
659                / (40320.0 * rho * nu7 * K08)
660                * (1385.0 + 3633.0 * tan2Phi + 4095.0 * tan4Phi + 1575.0 * tan6Phi);
661        final double t14 = 1.0 / (cosPhi * nu * K0);
662        final double t15 = 1.0 / (6.0 * cosPhi * nu3 * K03)
663                * (1.0 + 2.0 * tan2Phi + e2c2);
664        final double t16 = 1.0
665                / (120.0 * cosPhi * nu5 * K05)
666                * (5.0 + 28.0 * tan2Phi + 24.0 * tan4Phi + 6.0 * e2c2 + 8.0
667                        * t2e2c2 - 3.0 * e4c4 + 4.0 * t2e4c4 - 4.0 * e6c6 + 24.0 * t2e6c6);
668        final double t17 = 1.0 / (5040.0 * cosPhi * nu7 * K07)
669                * (61.0 + 662.0 * tan2Phi + 1320.0 * tan4Phi + 720.0 * tan6Phi);
670
671        final double latitude = phi - dE2 * t10 + dE4 * t11 - dE6 * t12 + dE8
672                * t13;
673        final double longitude = lambda0 + dE * t14 - dE3 * t15 + dE5 * t16
674                - dE7 * t17;
675        return LatLong.valueOf(latitude, longitude, RADIAN);
676    }
677
678    /**
679     * Converts the UPS coordinates to latitude/longitude coordinates,
680     * based on the specified reference ellipsoid.
681     *
682     * @param ups   The UPS coordinates.
683     * @param ellipsoid The reference ellipsoid.
684     * @return  The latitude/longitude coordinates.
685     */
686    public static LatLong upsToLatLong(UTM ups, ReferenceEllipsoid ellipsoid) {
687        final boolean northernHemisphere = ups.latitudeZone() > 'N';
688        final double dN = ups.northingValue(METRE)
689                - UPS_FALSE_NORTHING.doubleValue(METRE);
690        final double dE = ups.eastingValue(METRE)
691                - UPS_FALSE_EASTING.doubleValue(METRE);
692        // check for zeroes (the poles)
693        if (dE == 0.0 && dN == 0.0) {
694            if (northernHemisphere) {
695                return LatLong.valueOf(90.0, 0.0, DEGREE_ANGLE);
696            } else {
697                return LatLong.valueOf(-90.0, 0.0, DEGREE_ANGLE);
698            }
699        }
700        // compute longitude
701        final double longitude;
702        if (northernHemisphere) {
703            longitude = Math.atan2(dE, -dN);
704        } else {
705            longitude = Math.atan2(dE, dN);
706        }
707
708        // compute latitude
709        final double a = ellipsoid.getSemimajorAxis().doubleValue(METRE);
710        final double e = ellipsoid.getEccentricity();
711        final double e2 = ellipsoid.getEccentricitySquared();
712        final double e4 = e2 * e2;
713        final double e6 = e4 * e2;
714        final double e8 = e6 * e2;
715        final double aBar = e2 / 2.0 + 5.0 * e4 / 24.0 + e6 / 12.0 + 13 * e8
716                / 360.0;
717        final double bBar = 7.0 * e4 / 48.0 + 29.0 * e6 / 240.0 + 811.0 * e8
718                / 11520.0;
719        final double cBar = 7.0 * e6 / 120.0 + 81.0 * e8 / 1120.0;
720        final double dBar = 4279 * e8 / 161280.0;
721        final double c0 = ((2.0 * a) / Math.sqrt(1.0 - e2))
722                * Math.pow((1.0 - e) / (1.0 + e), e / 2.0);
723        final double r;
724        if (dE == 0.0) {
725            r = dN;
726        } else if (dN == 0.0) {
727            r = dE;
728        } else if (dN < dE) {
729            r = dE / Math.sin(longitude);
730        } else {
731            r = dN / Math.cos(longitude);
732        }
733        final double radius = Math.abs(r);
734
735        final double chi = (Math.PI / 2.0) - 2.0
736                * Math.atan2(radius, UPS_SCALE_FACTOR * c0);
737        final double phi = chi + aBar * Math.sin(2.0 * chi) + bBar
738                * Math.sin(4.0 * chi) + cBar * Math.sin(6.0 * chi) + dBar
739                * Math.sin(8.0 * chi);
740        final double latitude;
741        if (northernHemisphere) {
742            latitude = phi;
743        } else {
744            latitude = -phi;
745        }
746        return LatLong.valueOf(latitude, longitude, RADIAN);
747    }
748
749    @Override
750    public UTM copy() {
751        return UTM.valueOf(_longitudeZone, _latitudeZone, _easting, _northing,
752                METRE);
753    }
754
755    // Default serialization.
756    //
757
758    static final XMLFormat<UTM> XML = new XMLFormat<UTM>(UTM.class) {
759
760        @Override
761        public UTM newInstance(Class<UTM> cls, InputElement xml)
762                throws XMLStreamException {
763            return FACTORY.object();
764        }
765
766        public void write(UTM utm, OutputElement xml) throws XMLStreamException {
767            xml.setAttribute("latitudeZone", utm._latitudeZone);
768            xml.setAttribute("longitudeZone", utm._longitudeZone);
769            xml.setAttribute("easting", utm._easting);
770            xml.setAttribute("northing", utm._northing);
771        }
772
773        public void read(InputElement xml, UTM UTM) throws XMLStreamException {
774            UTM._latitudeZone = xml.getAttribute("latitudeZone", ' ');
775            UTM._longitudeZone = xml.getAttribute("longitudeZone", 0);
776            UTM._easting = xml.getAttribute("easting", 0.0);
777            UTM._northing = xml.getAttribute("northing", 0.0);
778        }
779    };
780
781    private static final long serialVersionUID = 1L;
782
783}