001    //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/tags/2.1/src/org/deegree/model/csct/ct/StereographicProjection.java $
002    /*----------------    FILE HEADER  ------------------------------------------
003    
004     This file is part of deegree.
005     Copyright (C) 2001 by:
006     EXSE, Department of Geography, University of Bonn
007     http://www.giub.uni-bonn.de/exse/
008     lat/lon GmbH
009     http://www.lat-lon.de
010    
011     It has been implemented within SEAGIS - An OpenSource implementation of OpenGIS specification
012     (C) 2001, Institut de Recherche pour le D�veloppement (http://sourceforge.net/projects/seagis/)
013     SEAGIS Contacts:  Surveillance de l'Environnement Assist�e par Satellite
014     Institut de Recherche pour le D�veloppement / US-Espace
015     mailto:seasnet@teledetection.fr
016    
017    
018     This library is free software; you can redistribute it and/or
019     modify it under the terms of the GNU Lesser General Public
020     License as published by the Free Software Foundation; either
021     version 2.1 of the License, or (at your option) any later version.
022    
023     This library is distributed in the hope that it will be useful,
024     but WITHOUT ANY WARRANTY; without even the implied warranty of
025     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
026     Lesser General Public License for more details.
027    
028     You should have received a copy of the GNU Lesser General Public
029     License along with this library; if not, write to the Free Software
030     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
031    
032     Contact:
033    
034     Andreas Poth
035     lat/lon GmbH
036     Aennchenstr. 19
037     53115 Bonn
038     Germany
039     E-Mail: poth@lat-lon.de
040    
041     Klaus Greve
042     Department of Geography
043     University of Bonn
044     Meckenheimer Allee 166
045     53115 Bonn
046     Germany
047     E-Mail: klaus.greve@uni-bonn.de
048    
049     
050     ---------------------------------------------------------------------------*/
051    package org.deegree.model.csct.ct;
052    
053    // OpenGIS (SEAS) dependencies
054    import java.awt.geom.Point2D;
055    
056    import org.deegree.model.csct.cs.Projection;
057    import org.deegree.model.csct.resources.css.ResourceKeys;
058    import org.deegree.model.csct.resources.css.Resources;
059    
060    /**
061     * Projection st�r�ographique. Les directions � partir du point central sont vrais, mais
062     * les aires et les longueurs deviennent de plus en plus d�form�es � mesure que l'on
063     * s'�loigne du centre. Cette projection est utilis�e pour repr�senter des r�gions
064     * polaires. Elle peut �tre appropri�e pour d'autres r�gions ayant une forme circulaire.
065     * <br>
066     * <br>
067     * 
068     * R�f�rence: John P. Snyder (Map Projections - A Working Manual, U.S. Geological Survey
069     * Professional Paper 1395, 1987)
070     * 
071     * @version 1.0
072     * @author Andr� Gosselin
073     * @author Martin Desruisseaux
074     */
075    final class StereographicProjection extends PlanarProjection {
076        /**
077         * Nombre maximal d'it�rations permises lors du calcul de la projection inverse.
078         */
079        private static final int MAX_ITER = 10;
080    
081        /** Projection mode for switch statement. */
082        private static final int SPHERICAL_NORTH = 0;
083    
084        /** Projection mode for switch statement. */
085        private static final int SPHERICAL_SOUTH = 1;
086    
087        /** Projection mode for switch statement. */
088        private static final int ELLIPSOIDAL_SOUTH = 2;
089    
090        /** Projection mode for switch statement. */
091        private static final int ELLIPSOIDAL_NORTH = 3;
092    
093        /** Projection mode for switch statement. */
094        private static final int SPHERICAL_OBLIQUE = 4;
095    
096        /** Projection mode for switch statement. */
097        private static final int SPHERICAL_EQUATORIAL = 5;
098    
099        /** Projection mode for switch statement. */
100        private static final int ELLIPSOIDAL_EQUATORIAL = 6;
101    
102        /** Projection mode for switch statement. */
103        private static final int ELLIPSOIDAL_OBLIQUE = 7;
104    
105        /**
106         * Projection mode. It must be one of the following constants:
107         * {@link #SPHERICAL_NORTH},{@link #SPHERICAL_SOUTH},{@link #ELLIPSOIDAL_NORTH},
108         * {@link #ELLIPSOIDAL_SOUTH}.{@link #SPHERICAL_OBLIQUE},
109         * {@link #SPHERICAL_EQUATORIAL},{@link #ELLIPSOIDAL_OBLIQUE}or
110         * {@link #ELLIPSOIDAL_EQUATORIAL}.
111         */
112        private final int mode;
113    
114        /**
115         * Global scale factor. Value <code>ak0</code> is equals to
116         * <code>{@link #a}*k0</code>.
117         */
118        private final double k0, ak0;
119    
120        /**
121         * Facteurs utilis�s lors des projections obliques et equatorialles.
122         */
123        private final double sinphi0, cosphi0, chi1, sinChi1, cosChi1;
124    
125        /**
126         * Latitude of true scale, in radians. Used for {@link #toString}implementation.
127         */
128        private final double latitudeTrueScale;
129    
130        /**
131         * Construct a new map projection from the suplied parameters.
132         * 
133         * @param parameters The parameter values in standard units.
134         * @throws MissingParameterException if a mandatory parameter is missing.
135         */
136        protected StereographicProjection( final Projection parameters )
137                                throws MissingParameterException {
138            this( parameters, true, true );
139        }
140    
141        /**
142         * Construct a new map projection from the suplied parameters.
143         * 
144         * @param parameters The parameter values in standard units.
145         * @param polar <code>true</code> for polar projection.
146         * @param auto <code>true</code> if projection (polar vs oblique) can be selected
147         *            automatically.
148         * @throws MissingParameterException if a mandatory parameter is missing.
149         */
150        private StereographicProjection( final Projection parameters, final boolean polar,
151                                        final boolean auto ) throws MissingParameterException {
152            //////////////////////////
153            //   Fetch parameters //
154            //////////////////////////
155            super( parameters );
156            final double defaultLatitude = parameters.getValue( "latitude_of_origin", polar ? 90 : 0 );
157            latitudeTrueScale = Math.abs( latitudeToRadians(
158                                                             parameters.getValue(
159                                                                                  "latitude_true_scale",
160                                                                                  defaultLatitude ),
161                                                             true ) );
162    
163            //////////////////////////
164            //  Compute constants //
165            //////////////////////////
166            if ( auto ? ( Math.abs( Math.abs( centralLatitude ) - ( Math.PI / 2 ) ) < EPS ) : polar ) {
167                if ( centralLatitude < 0 ) {
168                    centralLatitude = -( Math.PI / 2 );
169                    mode = ( isSpherical ) ? SPHERICAL_SOUTH : ELLIPSOIDAL_SOUTH;
170                } else {
171                    centralLatitude = +( Math.PI / 2 );
172                    mode = ( isSpherical ) ? SPHERICAL_NORTH : ELLIPSOIDAL_NORTH;
173                }
174            } else if ( Math.abs( centralLatitude ) < EPS ) {
175                centralLatitude = 0;
176                mode = ( isSpherical ) ? SPHERICAL_EQUATORIAL : ELLIPSOIDAL_EQUATORIAL;
177            } else {
178                mode = ( isSpherical ) ? SPHERICAL_OBLIQUE : ELLIPSOIDAL_OBLIQUE;
179            }
180            switch ( mode ) {
181            default: {
182                cosphi0 = Math.cos( centralLatitude );
183                sinphi0 = Math.sin( centralLatitude );
184                chi1 = 2.0 * Math.atan( ssfn( centralLatitude, sinphi0 ) ) - ( Math.PI / 2 );
185                cosChi1 = Math.cos( chi1 );
186                sinChi1 = Math.sin( chi1 );
187                break;
188            }
189            case SPHERICAL_EQUATORIAL:
190            case ELLIPSOIDAL_EQUATORIAL: {
191                cosphi0 = 1.0;
192                sinphi0 = 0.0;
193                chi1 = 0.0;
194                cosChi1 = 1.0;
195                sinChi1 = 0.0;
196                break;
197            }
198            }
199    
200            //////////////////////////
201            //  Compute k0 and ak0 //
202            //////////////////////////
203            switch ( mode ) {
204            default: {
205                System.out.println( "StereographicProjection: line 201: should not happen" );
206            }
207            case ELLIPSOIDAL_NORTH:
208            case ELLIPSOIDAL_SOUTH: {
209                if ( Math.abs( latitudeTrueScale - ( Math.PI / 2 ) ) >= EPS ) {
210                    final double t = Math.sin( latitudeTrueScale );
211                    k0 = ( Math.cos( latitudeTrueScale ) / ( Math.sqrt( 1 - es * t * t ) ) )
212                         / tsfn( latitudeTrueScale, t );
213                } else {
214                    // True scale at pole
215                    k0 = 2.0 / Math.sqrt( Math.pow( 1 + e, 1 + e ) * Math.pow( 1 - e, 1 - e ) );
216                }
217                break;
218            }
219    
220            case SPHERICAL_NORTH:
221            case SPHERICAL_SOUTH: {
222                if ( Math.abs( latitudeTrueScale - ( Math.PI / 2 ) ) >= EPS )
223                    k0 = 1 + Math.sin( latitudeTrueScale );
224                else
225                    k0 = 2;
226                break;
227            }
228    
229            case ELLIPSOIDAL_OBLIQUE:
230            case ELLIPSOIDAL_EQUATORIAL: {
231                k0 = 2.0 * Math.cos( centralLatitude ) / Math.sqrt( 1 - es * sinphi0 * sinphi0 );
232                break;
233            }
234    
235            case SPHERICAL_OBLIQUE:
236            case SPHERICAL_EQUATORIAL: {
237                k0 = 2;
238                break;
239            }
240            }
241            ak0 = a * k0;
242        }
243    
244        /**
245         * Returns a human readable name localized for the specified locale.
246         */
247        public String getName(  ) {
248            return Resources.getResources( null ).getString( ResourceKeys.STEREOGRAPHIC_PROJECTION );
249        }
250    
251        /**
252         * Transforms the specified ( <var>x </var>, <var>y </var>) coordinate and stores the
253         * result in <code>ptDst</code>.
254         */
255        protected Point2D transform( double x, double y, final Point2D ptDst )
256                                throws TransformException {
257            x -= centralMeridian;
258            final double coslat = Math.cos( y );
259            final double sinlat = Math.sin( y );
260            final double coslon = Math.cos( x );
261            final double sinlon = Math.sin( x );
262    
263            switch ( mode ) {
264            default: {
265                System.out.println( "StereographicProjection: line 262: should not happen" );
266            }
267            case ELLIPSOIDAL_NORTH: {
268                final double rho = ak0 * tsfn( y, sinlat );
269                x = rho * sinlon;
270                y = -rho * coslon;
271                break;
272            }
273            case ELLIPSOIDAL_SOUTH: {
274                final double rho = ak0 * tsfn( -y, -sinlat );
275                x = rho * sinlon;
276                y = rho * coslon;
277                break;
278            }
279            case SPHERICAL_NORTH: {
280                if ( !( Math.abs( 1 + sinlat ) >= TOL ) ) {
281                    throw new TransformException(
282                                                  Resources.format( ResourceKeys.ERROR_VALUE_TEND_TOWARD_INFINITY ) );
283                }
284                // (21-8)
285                final double f = ak0 * coslat / ( 1 + sinlat );// == tan (pi/4 - phi/2)
286                x = f * sinlon; // (21-5)
287                y = -f * coslon; // (21-6)
288                break;
289            }
290            case SPHERICAL_SOUTH: {
291                if ( !( Math.abs( 1 - sinlat ) >= TOL ) ) {
292                    throw new TransformException(
293                                                  Resources.format( ResourceKeys.ERROR_VALUE_TEND_TOWARD_INFINITY ) );
294                }
295                // (21-12)
296                final double f = ak0 * coslat / ( 1 - sinlat );// == tan (pi/4 + phi/2)
297                x = f * sinlon; // (21-9)
298                y = f * coslon; // (21-10)
299                break;
300            }
301            case SPHERICAL_EQUATORIAL: {
302                double f = 1.0 + coslat * coslon;
303                if ( !( f >= TOL ) ) {
304                    throw new TransformException(
305                                                  Resources.format( ResourceKeys.ERROR_VALUE_TEND_TOWARD_INFINITY ) );
306                }
307                f = ak0 / f;
308                x = f * coslat * sinlon;
309                y = f * sinlat;
310                break;
311            }
312            case SPHERICAL_OBLIQUE: {
313                double f = 1.0 + sinphi0 * sinlat + cosphi0 * coslat * coslon; // (21-4)
314                if ( !( f >= TOL ) ) {
315                    throw new TransformException(
316                                                  Resources.format( ResourceKeys.ERROR_VALUE_TEND_TOWARD_INFINITY ) );
317                }
318                f = ak0 / f;
319                x = f * coslat * sinlon; // (21-2)
320                y = f * ( cosphi0 * sinlat - sinphi0 * coslat * coslon );// (21-3)
321                break;
322            }
323            case ELLIPSOIDAL_EQUATORIAL: {
324                final double chi = 2.0 * Math.atan( ssfn( y, sinlat ) ) - ( Math.PI / 2 );
325                final double sinChi = Math.sin( chi );
326                final double cosChi = Math.cos( chi );
327                final double A = ak0 / ( 1.0 + cosChi * coslon );
328                x = A * cosChi * sinlon;
329                y = A * sinChi;
330                break;
331            }
332            case ELLIPSOIDAL_OBLIQUE: {
333                final double chi = 2.0 * Math.atan( ssfn( y, sinlat ) ) - ( Math.PI / 2 );
334                final double sinChi = Math.sin( chi );
335                final double cosChi = Math.cos( chi );
336                final double cosChi_coslon = cosChi * coslon;
337                final double A = ak0 / cosChi1 / ( 1 + sinChi1 * sinChi + cosChi1 * cosChi_coslon );
338                x = A * cosChi * sinlon;
339                y = A * ( cosChi1 * sinChi - sinChi1 * cosChi_coslon );
340                break;
341            }
342            }
343            y += false_northing;
344            x += false_easting;
345            if ( ptDst != null ) {
346                ptDst.setLocation( x, y );
347                return ptDst;
348            }
349            return new Point2D.Double( x, y );
350        }
351    
352        /**
353         * Transforms the specified ( <var>x </var>, <var>y </var>) coordinate and stores the
354         * result in <code>ptDst</code>.
355         */
356        protected Point2D inverseTransform( double x, double y, final Point2D ptDst )
357                                throws TransformException {
358            x -= false_easting;
359            y -= false_northing;
360            x /= a;
361            y /= a;
362            final double rho = Math.sqrt( x * x + y * y );
363            choice: switch ( mode ) {
364            default: {
365    
366            }
367            case SPHERICAL_NORTH: {
368                y = -y;
369                // fallthrough
370            }
371            case SPHERICAL_SOUTH: {
372                // (20-17) call atan2(x,y) to properly deal with y==0
373                x = ( Math.abs( x ) < TOL && Math.abs( y ) < TOL ) ? centralMeridian
374                                                                  : Math.atan2( x, y )
375                                                                    + centralMeridian;
376                if ( Math.abs( rho ) < TOL )
377                    y = centralLatitude;
378                else {
379                    final double c = 2.0 * Math.atan( rho / k0 );
380                    final double cosc = Math.cos( c );
381                    y = ( mode == SPHERICAL_NORTH ) ? Math.asin( cosc ) : Math.asin( -cosc ); // (20-14)
382                    // with
383                    // phi1=90
384                }
385                break;
386            }
387            case SPHERICAL_EQUATORIAL: {
388                if ( Math.abs( rho ) < TOL ) {
389                    y = 0.0;
390                    x = centralMeridian;
391                } else {
392                    final double c = 2.0 * Math.atan( rho / k0 );
393                    final double cosc = Math.cos( c );
394                    final double sinc = Math.sin( c );
395                    y = Math.asin( y * sinc / rho ); // (20-14) with phi1=0
396                    final double t = x * sinc;
397                    final double ct = rho * cosc;
398                    x = ( Math.abs( t ) < TOL && Math.abs( ct ) < TOL ) ? centralMeridian
399                                                                       : Math.atan2( t, ct )
400                                                                         + centralMeridian;
401                }
402                break;
403            }
404            case SPHERICAL_OBLIQUE: {
405                if ( Math.abs( rho ) < TOL ) {
406                    y = centralLatitude;
407                    x = centralMeridian;
408                } else {
409                    final double c = 2.0 * Math.atan( rho / k0 );
410                    final double cosc = Math.cos( c );
411                    final double sinc = Math.sin( c );
412                    final double ct = rho * cosphi0 * cosc - y * sinphi0 * sinc; // (20-15)
413                    final double t = x * sinc;
414                    y = Math.asin( cosc * sinphi0 + y * sinc * cosphi0 / rho );
415                    x = ( Math.abs( ct ) < TOL && Math.abs( t ) < TOL ) ? centralMeridian
416                                                                       : Math.atan2( t, ct )
417                                                                         + centralMeridian;
418                }
419                break;
420            }
421            case ELLIPSOIDAL_SOUTH: {
422                y = -y;
423                // fallthrough
424            }
425            case ELLIPSOIDAL_NORTH: {
426                final double t = rho / k0;
427                /*
428                 * Compute lat using iterative technique.
429                 */
430                final double halfe = e / 2.0;
431                double phi0 = 0;
432                for ( int i = MAX_ITER; --i >= 0; ) {
433                    final double esinphi = e * Math.sin( phi0 );
434                    final double phi = ( Math.PI / 2 )
435                                       - 2.0
436                                       * Math.atan( t
437                                                    * Math.pow( ( 1 - esinphi ) / ( 1 + esinphi ),
438                                                                halfe ) );
439                    if ( Math.abs( phi - phi0 ) < TOL ) {
440                        x = ( Math.abs( rho ) < TOL ) ? centralMeridian : Math.atan2( x, -y )
441                                                                          + centralMeridian;
442                        y = ( mode == ELLIPSOIDAL_NORTH ) ? phi : -phi;
443                        break choice;
444                    }
445                    phi0 = phi;
446                }
447                throw new TransformException( Resources.format( ResourceKeys.ERROR_NO_CONVERGENCE ) );
448            }
449            case ELLIPSOIDAL_OBLIQUE: {
450                // fallthrough
451            }
452            case ELLIPSOIDAL_EQUATORIAL: {
453                final double ce = 2.0 * Math.atan2( rho * cosChi1, k0 );
454                final double cosce = Math.cos( ce );
455                final double since = Math.sin( ce );
456                final double chi = ( Math.abs( rho ) >= TOL ) ? Math.asin( cosce
457                                                                           * sinChi1
458                                                                           + ( y * since * cosChi1 / rho ) )
459                                                             : chi1;
460                final double t = Math.tan( Math.PI / 4.0 + chi / 2.0 );
461                /*
462                 * Compute lat using iterative technique.
463                 */
464                final double halfe = e / 2.0;
465                double phi0 = chi;
466                for ( int i = MAX_ITER; --i >= 0; ) {
467                    final double esinphi = e * Math.sin( phi0 );
468                    final double phi = 2.0
469                                       * Math.atan( t
470                                                    * Math.pow( ( 1 + esinphi ) / ( 1 - esinphi ),
471                                                                halfe ) ) - ( Math.PI / 2 );
472                    if ( Math.abs( phi - phi0 ) < TOL ) {
473                        x = ( Math.abs( rho ) < TOL ) ? centralMeridian
474                                                     : Math.atan2( x * since, rho * cosChi1 * cosce - y
475                                                                              * sinChi1 * since )
476                                                       + centralMeridian;
477                        y = phi;
478                        break choice;
479                    }
480                    phi0 = phi;
481                }
482                throw new TransformException( Resources.format( ResourceKeys.ERROR_NO_CONVERGENCE ) );
483            }
484            }
485            if ( ptDst != null ) {
486                ptDst.setLocation( x, y );
487                return ptDst;
488            }
489            return new Point2D.Double( x, y );
490        }
491    
492        /**
493         * Compute part of function (3-1) from Snyder
494         */
495        private double ssfn( double phi, double sinphi ) {
496            sinphi *= e;
497            return Math.tan( ( Math.PI / 4.0 ) + phi / 2.0 )
498                   * Math.pow( ( 1 - sinphi ) / ( 1 + sinphi ), e / 2.0 );
499        }
500    
501        /**
502         * Returns a hash value for this map projection.
503         */
504        public int hashCode() {
505            final long code = Double.doubleToLongBits( k0 );
506            return ( (int) code ^ (int) ( code >>> 32 ) ) + 37 * super.hashCode();
507        }
508    
509        /**
510         * Compares the specified object with this map projection for equality.
511         */
512        public boolean equals( final Object object ) {
513            if ( object == this )
514                return true; // Slight optimization
515            if ( super.equals( object ) ) {
516                final StereographicProjection that = (StereographicProjection) object;
517                return Double.doubleToLongBits( this.k0 ) == Double.doubleToLongBits( that.k0 )
518                       && Double.doubleToLongBits( this.ak0 ) == Double.doubleToLongBits( that.ak0 )
519                       && Double.doubleToLongBits( this.sinphi0 ) == Double.doubleToLongBits( that.sinphi0 )
520                       && Double.doubleToLongBits( this.cosphi0 ) == Double.doubleToLongBits( that.cosphi0 )
521                       && Double.doubleToLongBits( this.chi1 ) == Double.doubleToLongBits( that.chi1 )
522                       && Double.doubleToLongBits( this.sinChi1 ) == Double.doubleToLongBits( that.sinChi1 )
523                       && Double.doubleToLongBits( this.cosChi1 ) == Double.doubleToLongBits( that.cosChi1 );
524            }
525            return false;
526        }
527    
528        /**
529         * Impl�mentation de la partie entre crochets de la cha�ne retourn�e par
530         * {@link #toString()}.
531         */
532        void toString( final StringBuffer buffer ) {
533            super.toString( buffer );
534            addParameter( buffer, "latitude_true_scale", Math.toDegrees( latitudeTrueScale ) );
535        }
536    
537        /**
538         * Informations about a {@link StereographicProjection}.
539         * 
540         * @version 1.0
541         * @author Martin Desruisseaux
542         */
543        static final class Provider extends MapProjection.Provider {
544            /**
545             * <code>true</code> for polar stereographic, or <code>false</code> for
546             * equatorial and oblique stereographic.
547             */
548            private final boolean polar;
549    
550            /**
551             * <code>true</code> if polar/oblique/equatorial stereographic can be
552             * automatically choosen.
553             */
554            private final boolean auto;
555    
556            /**
557             * Construct a new provider. The type (polar, oblique or equatorial) will be
558             * choosen automatically according the latitude or origin.
559             */
560            public Provider() {
561                super( "Stereographic", ResourceKeys.STEREOGRAPHIC_PROJECTION );
562                put( "latitude_true_scale", 90.0, LATITUDE_RANGE );
563                polar = true;
564                auto = true;
565            }
566    
567            /**
568             * Construct an object for polar or oblique stereographic.
569             * 
570             * @param polar <code>true</code> for polar stereographic, or <code>false</code>
571             *            for equatorial and oblique stereographic.
572             */
573            public Provider( final boolean polar ) {
574                super( polar ? "Polar_Stereographic" : "Oblique_Stereographic",
575                       ResourceKeys.STEREOGRAPHIC_PROJECTION );
576                put( "latitude_true_scale", polar ? 90.0 : 0.0, LATITUDE_RANGE );
577                this.polar = polar;
578                this.auto = false;
579            }
580    
581            /**
582             * Create a new map projection.
583             */
584            protected Object create( final Projection parameters ) {
585                return new StereographicProjection( parameters, polar, auto );
586            }
587        }
588    }