001    //$HeadURL: $
002    /*----------------    FILE HEADER  ------------------------------------------
003     This file is part of deegree.
004     Copyright (C) 2001-2008 by:
005     Department of Geography, University of Bonn
006     http://www.giub.uni-bonn.de/deegree/
007     lat/lon GmbH
008     http://www.lat-lon.de
009    
010     This library is free software; you can redistribute it and/or
011     modify it under the terms of the GNU Lesser General Public
012     License as published by the Free Software Foundation; either
013     version 2.1 of the License, or (at your option) any later version.
014     This library is distributed in the hope that it will be useful,
015     but WITHOUT ANY WARRANTY; without even the implied warranty of
016     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017     Lesser General Public License for more details.
018     You should have received a copy of the GNU Lesser General Public
019     License along with this library; if not, write to the Free Software
020     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
021     Contact:
022    
023     Andreas Poth
024     lat/lon GmbH
025     Aennchenstr. 19
026     53177 Bonn
027     Germany
028     E-Mail: poth@lat-lon.de
029    
030     Prof. Dr. Klaus Greve
031     Department of Geography
032     University of Bonn
033     Meckenheimer Allee 166
034     53115 Bonn
035     Germany
036     E-Mail: greve@giub.uni-bonn.de
037     ---------------------------------------------------------------------------*/
038    
039    package org.deegree.crs.projections.cylindric;
040    
041    import javax.vecmath.Point2d;
042    
043    import org.deegree.crs.components.Unit;
044    import org.deegree.crs.coordinatesystems.GeographicCRS;
045    import org.deegree.crs.exceptions.CRSException;
046    import org.deegree.crs.projections.ProjectionUtils;
047    import org.deegree.framework.log.ILogger;
048    import org.deegree.framework.log.LoggerFactory;
049    
050    /**
051     * The <code>TransverseMercator</code> projection has following properties:
052     * <ul>
053     * <li>Cylindrical (transverse)</li>
054     * <li>Conformal</li>
055     * <li>The central meridian, each meridian 90° from central meridian and the equator are straight lines</li>
056     * <li>All other meridians and parallels are complex curves</li>
057     * <li>Scale is true along central meridian or along two straight lines equidistant from and parallel to central
058     * merdian. (These lines are only approximately straight for the ellipsoid)</li>
059     * <li>Scale becomes infinite on sphere 90° from central meridian</li>
060     * <li>Used extensively for quadrangle maps at scales from 1:24.000 to 1:250.000</li>
061     * <li>Often used to show regions with greater north-south extent</li>
062     * <li>presented by lambert in 1772</li>
063     * </ul>
064     * 
065     * @author <a href="mailto:bezema@lat-lon.de">Rutger Bezema</a>
066     * 
067     * @author last edited by: $Author:$
068     * 
069     * @version $Revision:$, $Date:$
070     * 
071     */
072    
073    public class TransverseMercator extends CylindricalProjection {
074    
075        private static ILogger LOG = LoggerFactory.getLogger( TransverseMercator.class );
076        /**
077         * Contants used for the forward and inverse transform for the eliptical case of the Transverse Mercator.
078         */
079        private static final double FC1 = 1., // 1/1
080                                FC2 = 0.5, // 1/2
081                                FC3 = 0.16666666666666666666666, // 1/6
082                                FC4 = 0.08333333333333333333333, // 1/12
083                                FC5 = 0.05, // 1/20
084                                FC6 = 0.03333333333333333333333, // 1/30
085                                FC7 = 0.02380952380952380952380, // 1/42
086                                FC8 = 0.01785714285714285714285; // 1/56
087    
088        // 1 for northern hemisphere -1 for southern.
089        private int hemisphere;
090    
091        // esp will can hold two values, for the sphere it will hold the scale, for the ellipsoid Snyder (p.61 8-12).
092        private double esp;
093    
094        private double ml0;
095    
096        private double[] en;
097    
098        /**
099         * @param northernHemisphere
100         *            true if on the northernhemisphere false otherwhise.
101         * @param geographicCRS
102         * @param falseNorthing
103         * @param falseEasting
104         * @param naturalOrigin
105         * @param units
106         * @param scale
107         * @param identifiers
108         * @param names
109         * @param versions
110         * @param descriptions
111         * @param areasOfUse
112         */
113        public TransverseMercator( boolean northernHemisphere, GeographicCRS geographicCRS, double falseNorthing,
114                                   double falseEasting, Point2d naturalOrigin, Unit units, double scale,
115                                   String[] identifiers, String[] names, String[] versions, String[] descriptions,
116                                   String[] areasOfUse ) {
117            super( geographicCRS, falseNorthing, falseEasting, naturalOrigin, units, scale, true,// always conformal
118                   false, // not equalArea
119                   identifiers,
120                   names,
121                   versions,
122                   descriptions,
123                   areasOfUse );
124            this.hemisphere = ( northernHemisphere ) ? 1 : -1;
125            if ( isSpherical() ) {
126                esp = getScale();
127                ml0 = .5 * esp;
128            } else {
129                en = ProjectionUtils.getRectifiyingLatitudeValues( getSquaredEccentricity() );
130                ml0 = ProjectionUtils.getDistanceAlongMeridian( getProjectionLatitude(), getSinphi0(), getCosphi0(), en );
131                esp = getSquaredEccentricity() / ( 1. - getSquaredEccentricity() );
132                // esp = ( ( getSemiMajorAxis() * getSemiMajorAxis() ) / ( getSemiMinorAxis() * getSemiMinorAxis() ) ) -
133                // 1.0;
134            }
135    
136        }
137    
138        /**
139         * @param northernHemisphere
140         *            true if on the northernhemisphere false otherwhise.
141         * @param geographicCRS
142         * @param falseNorthing
143         * @param falseEasting
144         * @param naturalOrigin
145         * @param units
146         * @param scale
147         * @param identifier
148         * @param name
149         * @param version
150         * @param description
151         * @param areaOfUse
152         */
153        public TransverseMercator( boolean northernHemisphere, GeographicCRS geographicCRS, double falseNorthing,
154                                   double falseEasting, Point2d naturalOrigin, Unit units, double scale, String identifier,
155                                   String name, String version, String description, String areaOfUse ) {
156            this( northernHemisphere,
157                  geographicCRS,
158                  falseNorthing,
159                  falseEasting,
160                  naturalOrigin,
161                  units,
162                  scale,
163                  new String[] { identifier },
164                  new String[] { name },
165                  new String[] { version },
166                  new String[] { description },
167                  new String[] { areaOfUse } );
168        }
169    
170        /**
171         * @param northernHemisphere
172         *            true if on the northernhemisphere false otherwhise.
173         * @param geographicCRS
174         * @param falseNorthing
175         * @param falseEasting
176         * @param naturalOrigin
177         * @param units
178         * @param scale
179         * @param identifiers
180         */
181        public TransverseMercator( boolean northernHemisphere, GeographicCRS geographicCRS, double falseNorthing,
182                                   double falseEasting, Point2d naturalOrigin, Unit units, double scale,
183                                   String[] identifiers ) {
184            this( northernHemisphere,
185                  geographicCRS,
186                  falseNorthing,
187                  falseEasting,
188                  naturalOrigin,
189                  units,
190                  scale,
191                  identifiers,
192                  null,
193                  null,
194                  null,
195                  null );
196        }
197    
198        /**
199         * @param northernHemisphere
200         *            true if on the northernhemisphere false otherwhise.
201         * @param geographicCRS
202         * @param falseNorthing
203         * @param falseEasting
204         * @param naturalOrigin
205         * @param units
206         * @param scale
207         * @param identifier
208         */
209        public TransverseMercator( boolean northernHemisphere, GeographicCRS geographicCRS, double falseNorthing,
210                                   double falseEasting, Point2d naturalOrigin, Unit units, double scale, String identifier ) {
211            this( northernHemisphere,
212                  geographicCRS,
213                  falseNorthing,
214                  falseEasting,
215                  naturalOrigin,
216                  units,
217                  scale,
218                  new String[] { identifier } );
219        }
220    
221        /**
222         * A northern hemisphere conformal transverse mercator projection with a scale of one. Using the given datum.
223         * 
224         * @param geographicCRS
225         * @param falseNorthing
226         * @param falseEasting
227         * @param naturalOrigin
228         * @param units
229         * @param identifiers
230         * @param name
231         */
232        public TransverseMercator( GeographicCRS geographicCRS, double falseNorthing, double falseEasting,
233                                   Point2d naturalOrigin, Unit units, String[] identifiers ) {
234            this( true, geographicCRS, falseNorthing, falseEasting, naturalOrigin, units, 1., identifiers );
235        }
236    
237        /**
238         * A northern hemisphere conformal transverse mercator projection with a scale of one. Using the given datum.
239         * 
240         * @param geographicCRS
241         * @param falseNorthing
242         * @param falseEasting
243         * @param naturalOrigin
244         * @param units
245         * @param identifier
246         * @param name
247         */
248        public TransverseMercator( GeographicCRS geographicCRS, double falseNorthing, double falseEasting,
249                                   Point2d naturalOrigin, Unit units, String identifier ) {
250            this( true, geographicCRS, falseNorthing, falseEasting, naturalOrigin, units, 1., identifier );
251        }
252    
253        @Override
254        public Point2d doInverseProjection( double x, double y )
255                                                                throws CRSException {
256            Point2d result = new Point2d( 0, 0 );
257            LOG.logDebug( "InverseProjection, incoming points x: " + x + " y: " + y );
258            x -= getFalseEasting();
259            y -= getFalseNorthing();
260            y *= hemisphere;
261    
262            if ( isSpherical() ) {
263                // h holds e^x, the sinh = 0.5*(e^x - e^-x), cosh = 0.5(e^x + e^-x)
264                double h = Math.exp( x / getScaleFactor() );
265                // sinh holds the sinh from Snyder (p.60 8-7)
266                double sinh = .5 * ( h - 1. / h );
267    
268                // Snyder (p.60 8-8)
269                // reuse variable
270                double cosD = Math.cos( getProjectionLatitude() + ( y / getScaleFactor() ) );
271                /**
272                 * To calc phi from Snyder (p.60 8-6), use following trick! sin^2(D) + cos^2(D) = 1 => sin(D) = sqrt( 1-
273                 * cos^2(D) ) and cosh^2(x) - sin^2(x) = 1 => cosh(x) = sqrt( 1+sin^2(x) )
274                 */
275                result.y = ProjectionUtils.asinScaled( Math.sqrt( ( 1. - cosD * cosD ) / ( 1. + sinh * sinh ) ) );
276                // if ( y < 0 ) {// southern hemisphere
277                // out.y = -out.y;
278                // }
279                result.x = Math.atan2( sinh, cosD );
280            } else {
281                // out.y will hold the phi_1 from Snyder (p.63 8-18).
282                result.y = ProjectionUtils.calcPhiFromMeridianDistance( ml0 + ( y / getScaleFactor() ),
283                                                                        getSquaredEccentricity(),
284                                                                        en );
285                if ( Math.abs( result.y ) >= ProjectionUtils.HALFPI ) {
286                    result.y = y < 0. ? -ProjectionUtils.HALFPI : ProjectionUtils.HALFPI;
287                    result.x = 0;
288                } else {
289    
290                    double sinphi = Math.sin( result.y );
291                    double cosphi = Math.cos( result.y );
292                    // largeT Will hold the tan^2(phi) Snyder (p.64 8-22).
293                    double largeT = ( Math.abs( cosphi ) > ProjectionUtils.EPS10 ) ? sinphi / cosphi : 0;
294    
295                    // will hold the C_1 from Synder (p.64 8-21)
296                    double largeC = esp * cosphi * cosphi;
297    
298                    // Holds a modified N from Synder (p.64 8-23), multiplied with the largeT, it is the first term fo the
299                    // calculation of phi e.g. N*T/R
300                    double con = 1. - ( getSquaredEccentricity() * sinphi * sinphi );
301                    // largeD holds the D from Snyder (p.64 8-25). (x/(1/N) = x*N)
302                    double largeD = x * Math.sqrt( con ) / getScaleFactor();
303                    con *= largeT;
304                    largeT *= largeT;
305                    double ds = largeD * largeD;
306    
307                    /**
308                     * As for the forward projection, I'm not sure if this is correct, this should be checked!
309                     */
310                    result.y -= ( con * ds / ( 1. - getSquaredEccentricity() ) ) * FC2
311                                * ( 1. - ds * FC4
312                                         * ( 5. + largeT * ( 3. - 9. * largeC ) + largeC * ( 1. - 4 * largeC ) - ds * FC6
313                                                                                                                 * ( 61. + largeT
314                                                                                                                     * ( 90. - 252. * largeC + 45. * largeT )
315                                                                                                                     + 46.
316                                                                                                                     * largeC - ds * FC8
317                                                                                                                                * ( 1385. + largeT * ( 3633. + largeT * ( 4095. + 1574. * largeT ) ) ) ) ) );
318                    result.x = largeD * ( FC1 - ds * FC3
319                                                * ( 1. + 2. * largeT + largeC - ds * FC5
320                                                                                * ( 5. + largeT
321                                                                                    * ( 28. + 24. * largeT + 8. * largeC )
322                                                                                    + 6.
323                                                                                    * largeC - ds * FC7
324                                                                                               * ( 61. + largeT * ( 662. + largeT * ( 1320. + 720. * largeT ) ) ) ) ) )
325                               / cosphi;
326                }
327            }
328            result.y += getProjectionLatitude();
329            result.x += getProjectionLongitude();
330    
331            return result;
332        }
333    
334        /*
335         * (non-Javadoc)
336         * 
337         * @see org.deegree.crs.projections.Projection#doProjection(double, double)
338         */
339        @Override
340        public Point2d doProjection( double lambda, double phi )
341                                                                throws CRSException {
342            LOG.logDebug( "Projection, incoming points lambda: " + lambda + " phi: " + phi );
343            Point2d result = new Point2d( 0, 0 );
344            lambda -= getProjectionLongitude();
345            phi -= getProjectionLatitude();
346            phi *= hemisphere;
347            double cosphi = Math.cos( phi );
348            if ( isSpherical() ) {
349                double b = cosphi * Math.sin( lambda );
350    
351                // Snyder (p.58 8-1)
352                result.x = ml0 * getScaleFactor() * Math.log( ( 1. + b ) / ( 1. - b ) );
353    
354                // reformed and inserted the k from (p.58 8-4), so no tangens has to be calculated.
355                double ty = cosphi * Math.cos( lambda ) / Math.sqrt( 1. - b * b );
356                ty = ProjectionUtils.acosScaled( ty );
357                if ( phi < 0.0 ) {
358                    ty = -ty;
359                }
360                // esp just holds the scale
361                result.y = esp * ( ty - getProjectionLatitude() );
362            } else {
363                double sinphi = Math.sin( phi );
364                double largeT = ( Math.abs( cosphi ) > ProjectionUtils.EPS10 ) ? sinphi / cosphi : 0.0;
365                // largeT holds Snyder (p.61 8-13).
366                largeT *= largeT;
367                double largeA = cosphi * lambda;
368                double squaredLargeA = largeA * largeA;
369                // largeA now holds A/N Snyder (p.61 4-20 and 8-15)
370                largeA /= Math.sqrt( 1. - getSquaredEccentricity() * sinphi * sinphi );
371    
372                // largeC will hold Snyder (p.61 8-14), esp holds Snyder (p.61 8-12).
373                double largeC = esp * cosphi * cosphi;
374                double largeM = ProjectionUtils.getDistanceAlongMeridian( phi, sinphi, cosphi, en );
375    
376                result.x = getScaleFactor() * largeA
377                           * ( FC1 + FC3 * squaredLargeA
378                                     * ( 1. - largeT + largeC + FC5 * squaredLargeA
379                                                                * ( 5. + largeT
380                                                                    * ( largeT - 18. )
381                                                                    + largeC
382                                                                    * ( 14. - 58. * largeT ) + FC7 * squaredLargeA
383                                                                                               * ( 61. + largeT * ( largeT * ( 179. - largeT ) - 479. ) ) ) ) );
384    
385                result.y = getScaleFactor() * ( largeM - ml0 + sinphi * largeA
386                                                               * lambda
387                                                               * FC2
388                                                               * ( 1. + FC4 * squaredLargeA
389                                                                        * ( 5. - largeT + largeC * ( 9. + 4. * largeC ) + FC6 * squaredLargeA
390                                                                                                                          * ( 61. + largeT
391                                                                                                                              * ( largeT - 58. )
392                                                                                                                              + largeC
393                                                                                                                              * ( 270. - 330 * largeT ) + FC8 * squaredLargeA
394                                                                                                                                                          * ( 1385. + largeT * ( largeT * ( 543. - largeT ) - 3111. ) ) ) ) ) );
395    
396            }
397            result.x += getFalseEasting();
398            result.y += getFalseNorthing();
399            return result;
400        }
401    
402        /**
403         * @param latitude
404         *            to get the nearest paralles to.
405         * @return the nearest parallel in radians of given latitude
406         */
407        public int getRowFromNearestParallel( double latitude ) {
408            int degrees = (int) Math.round( Math.toDegrees( ProjectionUtils.normalizeLatitude( latitude ) ) );
409            if ( degrees < -80 || degrees > 84 ) {
410                return 0;
411            }
412            if ( degrees > 80 ) {
413                return 24; // last parallel
414            }
415            return ( ( degrees + 80 ) / 8 ) + 3;
416        }
417    
418        /**
419         * the utm zone from a given meridian
420         * 
421         * @param longitude
422         *            in radians
423         * @return the utm zone.
424         */
425        public int getZoneFromNearestMeridian( double longitude ) {
426            int zone = (int) Math.floor( ( ProjectionUtils.normalizeLongitude( longitude ) + Math.PI ) * 30.0 / Math.PI ) + 1;
427            if ( zone < 1 ) {
428                zone = 1;
429            } else if ( zone > 60 ) {
430                zone = 60;
431            }
432            return zone;
433        }
434    
435        /**
436         * Set the zone for this transverse mercator, not yet used.
437         * 
438         * @param zone
439         *            the new utm zone.
440         */
441        public void setUTMZone( int zone ) {
442            zone--;
443            setProjectionLongitude( ( zone + .5 ) * Math.PI / 30. - Math.PI );
444    
445            setProjectionLatitude( 0.0 );
446            if ( getScale() != 0.9996 ) {
447                setScale( 0.9996 );
448            }
449            if ( Math.abs( getFalseEasting() - 500000 ) > 0.000001 ) {
450                setFalseEasting( 500000 );
451            }
452            if ( !isSpherical() ) {
453                // recalculate the rectifying latitudes and the distance along the meridian.
454                en = ProjectionUtils.getRectifiyingLatitudeValues( getSquaredEccentricity() );
455                ml0 = ProjectionUtils.getDistanceAlongMeridian( getProjectionLatitude(), getSinphi0(), getCosphi0(), en );
456            }
457        }
458    
459        @Override
460        public String getDeegreeSpecificName() {
461            return "transverseMercator";
462        }
463    
464        /**
465         * @return the true if defined on the northern hemisphere.
466         */
467        public final boolean getHemisphere() {
468            return ( hemisphere == 1 );
469        }
470    }