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.azimuthal;
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.ProjectionException;
046    import org.deegree.crs.projections.ProjectionUtils;
047    
048    /**
049     * The <code>StereographicAzimuthal</code> class allows for Stereographic Projections of the Poles, equator as well as
050     * oblique. This projection has following properties (Snyder p. 154):
051     * <ul>
052     * <li>Azimuthal</li>
053     * <li>Conformal</li>
054     * <li>The central meridian an a particular parallel (if shown) are straight lines.</li>
055     * <li>All meridians on the polar aspects and the equator on the equatorial aspect are straight lines.</li>
056     * <li>All other meidians and parallels are shown as arcs of circles</li>
057     * <li>A perspective projections for the sphere</li>
058     * <li>Directions from the center of the projection are true (except on ellipsoidal oblique and equatorial aspects)</li>
059     * <li>Scale increases away from the center of the projection</li>
060     * <li>Point opposite the center of the projection cannot be plotted</li>
061     * <li>Used for polar maps and miscellaneous special maps</li>
062     * </ul>
063     * 
064     * <p>
065     * Like Orthographic, the stereographic projection is a true perspective in its isSpherical() form. It is the only known
066     * true perspective projection of any kind that is also conformal. Its point of projection is on the the surface of the
067     * sphere at a point jus opposite the oint of tangency of the plane or the center point of the projection. Thus, if the
068     * north pole is the center of the map, the projection is from the south-pole.
069     * </p>
070     * <p>
071     * It is known to be used by following epsg transformations:
072     * <ul>
073     * <li>EPSG:28992</li>
074     * </ul>
075     * </p>
076     * 
077     * @author <a href="mailto:bezema@lat-lon.de">Rutger Bezema</a>
078     * 
079     * @author last edited by: $Author:$
080     * 
081     * @version $Revision:$, $Date:$
082     * 
083     */
084    
085    public class StereographicAzimuthal extends AzimuthalProjection {
086    
087        /**
088         * This variable will hold different values, for the ellipsoidal projection:
089         * <ul>
090         * <li>for Oblique projection it is the first Part (2*a*k_0*m_1, hence it's name) of A (p.160 21-27)</li>
091         * <li>for the north-south it may hold the rho of (21-33) or (21-34)
092         * <li>for the equatorial it holds 2*scale.</li>
093         * <li>For the
094         * </ul>
095         * For the spherical projection:
096         * <ul>
097         * <li>For oblique and equatorial: 2*scale, which is compliant with 2*k_0 from Snyder (p.157 21-4) </li>
098         * <li>For north and south: cos(truelat) / tan( pi/4 - phi/2) ???? </li>
099         * </ul>
100         */
101        private double akm1;
102    
103        private double trueScaleLatitude;
104    
105        private double[] preCalcedPhiSeries;
106    
107        /**
108         * @param trueScaleLatitude
109         *            the latitude (in radians) of a circle around the projection point, which contains the true scale.
110         * @param geographicCRS
111         * @param falseNorthing
112         * @param falseEasting
113         * @param naturalOrigin
114         * @param units
115         * @param scale
116         * @param identifiers
117         * @param names
118         * @param versions
119         * @param descriptions
120         * @param areasOfUse
121         */
122        public StereographicAzimuthal( double trueScaleLatitude, GeographicCRS geographicCRS, double falseNorthing,
123                                       double falseEasting, Point2d naturalOrigin, Unit units, double scale,
124                                       String[] identifiers, String[] names, String[] versions, String[] descriptions,
125                                       String[] areasOfUse ) {
126            super( geographicCRS,
127                   falseNorthing,
128                   falseEasting,
129                   naturalOrigin,
130                   units,
131                   scale,
132                   true,
133                   false,//not equal area.
134                   identifiers,
135                   names,
136                   versions,
137                   descriptions,
138                   areasOfUse );
139    
140            preCalcedPhiSeries = ProjectionUtils.preCalcedThetaSeries( getSquaredEccentricity() );
141    
142            this.trueScaleLatitude = Math.abs( trueScaleLatitude );
143            if ( !isSpherical() ) {
144                // double X;
145                double tmp = 0;
146                switch ( getMode() ) {
147                case NORTH_POLE:
148                case SOUTH_POLE:
149                    /**
150                     * The t from 21-33 and 21-34 is still to be calculated.
151                     */
152                    // If true scale or known scale factor k_0 is to occur at the pole, the following applies:
153                    if ( Math.abs( this.trueScaleLatitude - ProjectionUtils.HALFPI ) < ProjectionUtils.EPS10 ) {
154                        // Snyder (p.161 21-33) akm1 = rho.
155                        akm1 = 2. * getScaleFactor()
156                               / Math.sqrt( Math.pow( 1 + getEccentricity(), 1 + getEccentricity() ) * Math.pow( 1 - getEccentricity(),
157                                                                                                                 1 - getEccentricity() ) );
158    
159                    } else {
160                        // For true scale along the circle represented by trueScaleLatitude (phi_c in snyder)..
161                        // akm1 will hold m_c/t_c
162                        // Calculate the rho from Snyder (p.161 21-34) and place in akm1
163                        tmp = Math.sin( this.trueScaleLatitude );
164    
165                        // First part of m of Snyder (p.160 14-15)
166                        akm1 = Math.cos( this.trueScaleLatitude ) / ProjectionUtils.tanHalfCoLatitude( this.trueScaleLatitude,
167                                                                                                       tmp,
168                                                                                                       getEccentricity() );
169                        tmp *= getEccentricity();
170                        // Second part of m of Snyder (p.160 14-15), t = (e*sin(phi_c))
171                        akm1 /= Math.sqrt( 1. - tmp * tmp );
172    
173                    }
174                    break;
175                case EQUATOR:
176                    akm1 = 2. * getScaleFactor();
177                    break;
178                case OBLIQUE:
179                    tmp = getSinphi0();// Math.sin( getProjectionLatitude() );
180                    // Calculate the ConformalLatitude for this ellipsoid Snyder (p.160 3-1)
181                    double X = ( 2. * Math.atan( ProjectionUtils.conformalLatitudeInnerPart( getProjectionLatitude(),
182                                                                                             tmp,
183                                                                                             getEccentricity() ) ) ) - ProjectionUtils.HALFPI;
184    
185                    tmp *= getEccentricity();
186                    // the first part (2a*k_0*m) of the largeA in Snyder (p.160 21-27) is stored in akm1 it still must be
187                    // devided by the cos X ... etc. to get largeA
188                    akm1 = 2. * getScaleFactor() * getCosphi0() / Math.sqrt( 1. - tmp * tmp );
189    
190                    // Setting the sinPhi0 to the conformal Latitude X.
191                    setProjectionLatitude( X );
192                    break;
193                }
194            } else {
195                akm1 = 2. * getScaleFactor();
196            }
197        }
198    
199        /**
200         * @param trueScaleLatitude
201         *            the latitude (in radians) of a circle around the projection point, which contains the true scale.
202         * @param geographicCRS
203         * @param falseNorthing
204         * @param falseEasting
205         * @param naturalOrigin
206         * @param units
207         * @param scale
208         * @param identifier
209         * @param name
210         * @param version
211         * @param description
212         * @param areaOfUse
213         */
214        public StereographicAzimuthal( double trueScaleLatitude, GeographicCRS geographicCRS, double falseNorthing,
215                                       double falseEasting, Point2d naturalOrigin, Unit units, double scale,
216                                       String identifier, String name, String version, String description, String areaOfUse ) {
217            this( trueScaleLatitude,
218                  geographicCRS,
219                  falseNorthing,
220                  falseEasting,
221                  naturalOrigin,
222                  units,
223                  scale,
224                  new String[] { identifier },
225                  new String[] { name },
226                  new String[] { version },
227                  new String[] { description },
228                  new String[] { areaOfUse } );
229        }
230        
231        /**
232         * @param trueScaleLatitude
233         *            the latitude (in radians) of a circle around the projection point, which contains the true scale.
234         * @param geographicCRS
235         * @param falseNorthing
236         * @param falseEasting
237         * @param naturalOrigin
238         * @param units
239         * @param scale
240         * @param identifiers
241         */
242        public StereographicAzimuthal( double trueScaleLatitude, GeographicCRS geographicCRS, double falseNorthing,
243                                       double falseEasting, Point2d naturalOrigin, Unit units, double scale,
244                                       String[] identifiers ) {
245            this( trueScaleLatitude,
246                  geographicCRS,
247                  falseNorthing,
248                  falseEasting,
249                  naturalOrigin,
250                  units,
251                  scale,
252                  identifiers,
253                  null,
254                  null,
255                  null,
256                  null);
257        }
258    
259        /**
260         * @param trueScaleLatitude
261         *            the latitude (in radians) of a circle around the projection point, which contains the true scale.
262         * @param geographicCRS
263         * @param falseNorthing
264         * @param falseEasting
265         * @param naturalOrigin
266         * @param units
267         * @param scale
268         * @param identifier
269         */
270        public StereographicAzimuthal( double trueScaleLatitude, GeographicCRS geographicCRS, double falseNorthing,
271                                       double falseEasting, Point2d naturalOrigin, Unit units, double scale,
272                                       String identifier ) {
273            this( trueScaleLatitude,
274                  geographicCRS,
275                  falseNorthing,
276                  falseEasting,
277                  naturalOrigin,
278                  units,
279                  scale,
280                  new String[]{identifier} );
281        }
282        
283        /**
284         * Create a {@link StereographicAzimuthal} which has a truescale latitude at MapUtils.HALFPI.
285         * 
286         * @param geographicCRS
287         * @param falseNorthing
288         * @param falseEasting
289         * @param naturalOrigin
290         * @param units
291         * @param scale
292         * @param identifiers
293         */
294        public StereographicAzimuthal( GeographicCRS geographicCRS, double falseNorthing, double falseEasting,
295                                       Point2d naturalOrigin, Unit units, double scale, String[] identifiers) {
296            this( ProjectionUtils.HALFPI,
297                  geographicCRS,
298                  falseNorthing,
299                  falseEasting,
300                  naturalOrigin,
301                  units,
302                  scale,
303                  identifiers );
304        }
305    
306        /**
307         * Create a {@link StereographicAzimuthal} which has a truescale latitude at MapUtils.HALFPI.
308         * 
309         * @param geographicCRS
310         * @param falseNorthing
311         * @param falseEasting
312         * @param naturalOrigin
313         * @param units
314         * @param scale
315         * @param identifier
316         */
317        public StereographicAzimuthal( GeographicCRS geographicCRS, double falseNorthing, double falseEasting,
318                                       Point2d naturalOrigin, Unit units, double scale, String identifier) {
319            this( ProjectionUtils.HALFPI,
320                  geographicCRS,
321                  falseNorthing,
322                  falseEasting,
323                  naturalOrigin,
324                  units,
325                  scale,
326                  identifier );
327        }
328        
329        /**
330         * Create a {@link StereographicAzimuthal} which has a scale of 1 and a truescale latitude,
331         * 
332         * @param trueScaleLatitude
333         *            the latitude (in radians) of a circle around the projection point, which contains the true scale.
334         * @param geographicCRS
335         * @param falseNorthing
336         * @param falseEasting
337         * @param naturalOrigin
338         * @param units
339         * @param identifiers
340         */
341        public StereographicAzimuthal( double trueScaleLatitude, GeographicCRS geographicCRS, double falseNorthing,
342                                       double falseEasting, Point2d naturalOrigin, Unit units, String[] identifiers ) {
343            this( trueScaleLatitude, geographicCRS, falseNorthing, falseEasting, naturalOrigin, units, 1, identifiers );
344        }
345    
346        /**
347         * Create a {@link StereographicAzimuthal} which has a scale of 1 and a truescale latitude,
348         * 
349         * @param trueScaleLatitude
350         *            the latitude (in radians) of a circle around the projection point, which contains the true scale.
351         * @param geographicCRS
352         * @param falseNorthing
353         * @param falseEasting
354         * @param naturalOrigin
355         * @param units
356         * @param identifier
357         */
358        public StereographicAzimuthal( double trueScaleLatitude, GeographicCRS geographicCRS, double falseNorthing,
359                                       double falseEasting, Point2d naturalOrigin, Unit units, String identifier ) {
360            this( trueScaleLatitude, geographicCRS, falseNorthing, falseEasting, naturalOrigin, units, 1, identifier );
361        }
362        
363        /**
364         * Create a {@link StereographicAzimuthal} which is conformal, has a scale of 1 and a truescale latitude at pi*0.5.
365         * 
366         * @param geographicCRS
367         * @param falseNorthing
368         * @param falseEasting
369         * @param naturalOrigin
370         * @param units
371         * @param identifiers
372         */
373        public StereographicAzimuthal( GeographicCRS geographicCRS, double falseNorthing, double falseEasting,
374                                       Point2d naturalOrigin, Unit units, String[] identifiers  ) {
375            this( ProjectionUtils.HALFPI,
376                  geographicCRS,
377                  falseNorthing,
378                  falseEasting,
379                  naturalOrigin,
380                  units,
381                  1,
382                  identifiers );
383        }
384    
385        /**
386         * Create a {@link StereographicAzimuthal} which is conformal, has a scale of 1 and a truescale latitude at pi*0.5.
387         * 
388         * @param geographicCRS
389         * @param falseNorthing
390         * @param falseEasting
391         * @param naturalOrigin
392         * @param units
393         * @param identifier
394         */
395        public StereographicAzimuthal( GeographicCRS geographicCRS, double falseNorthing, double falseEasting,
396                                       Point2d naturalOrigin, Unit units, String identifier  ) {
397            this( ProjectionUtils.HALFPI,
398                  geographicCRS,
399                  falseNorthing,
400                  falseEasting,
401                  naturalOrigin,
402                  units,
403                  1,
404                  identifier );
405        }
406    
407        /*
408         * (non-Javadoc)
409         * 
410         * @see org.deegree.crs.projections.Projection#doInverseProjection(double, double)
411         */
412        @Override
413        public Point2d doInverseProjection( double x, double y ) {
414            Point2d lp = new Point2d( 0, 0 );
415            x -= getFalseEasting();
416            y -= getFalseNorthing();
417            double rho = ProjectionUtils.length( x, y );
418            if ( isSpherical() ) {
419                // akm1 = 2*scale.
420                double c = 2. * Math.atan( rho / akm1 );
421                double sinc = Math.sin( c );
422                double cosc = Math.cos( c );
423                switch ( getMode() ) {
424                case OBLIQUE:
425                    // First find theta from Snyder (p.158 20-14).
426                    if ( Math.abs( rho ) <= ProjectionUtils.EPS10 ) {
427                        lp.y = getProjectionLatitude();
428                    } else {
429                        lp.y = Math.asin( cosc * getSinphi0() + ( ( y * sinc * getCosphi0() ) / rho ) );
430                    }
431                    // And now lambda but instead of using the atan, use atan2. Is this correct????
432                    c = cosc - getSinphi0() * Math.sin( lp.y );
433                    if ( Math.abs( c ) > ProjectionUtils.EPS10 || x != 0. ) {
434                        lp.x = Math.atan2( x * sinc * getCosphi0(), c * rho );
435                    }
436                    break;
437                case EQUATOR:
438                    if ( Math.abs( rho ) > ProjectionUtils.EPS10 ) {
439                        lp.y = Math.asin( y * sinc / rho );
440                    }
441                    if ( cosc != 0. || x != 0. ) {
442                        lp.x = Math.atan2( x * sinc, cosc * rho );
443                    }
444                    break;
445                case NORTH_POLE:
446                    y = -y;
447                case SOUTH_POLE:
448                    if ( Math.abs( rho ) <= ProjectionUtils.EPS10 ) {
449                        lp.y = getProjectionLatitude();
450                    } else {
451                        lp.y = Math.asin( getMode() == SOUTH_POLE ? -cosc : cosc );
452                    }
453                    lp.x = Math.atan2( x, y );
454                    break;
455                }
456            } else {
457                double cosCe = 0;
458                double sinCe = 0;
459                double X = 0;
460                // for oblique and equatorial projections, akm1 = first part of Snyder (p.160 21-27). which is 2*scale*m_1
461                double ce = 2. * Math.atan2( rho * getCosphi0(), akm1 );
462                if ( getMode() == OBLIQUE || getMode() == EQUATOR ) {
463                    cosCe = Math.cos( ce );
464                    sinCe = Math.sin( ce );
465                }
466                switch ( getMode() ) {
467                case OBLIQUE:
468                case EQUATOR:
469                    X = Math.asin( cosCe * getSinphi0() + ( ( y * sinCe * getCosphi0() ) / rho ) );
470                    lp.x = Math.atan2( x * sinCe, rho * getCosphi0() * cosCe - y * getSinphi0() * sinCe );
471                    break;
472                case NORTH_POLE:
473                    y = -y;
474                case SOUTH_POLE:
475                    /**
476                     * akm1 can hold rho from 21-34 or 21-33 (if no truescale latitude was given) without the parameter t.
477                     * Either way it must only be multiplied with t to forfill the formula. Independent of the true scale
478                     * latitude, the value of akm1 must therefore only be inverted to calculate Snyder (p.162 21-39 or
479                     * 21-40).
480                     */
481                    double t = rho * 1. / this.akm1;
482                    X = ProjectionUtils.HALFPI - 2 * Math.atan( t );
483                    lp.x = Math.atan2( x, y );
484                    break;
485                }
486                lp.y = ProjectionUtils.calcPhiFromConformalLatitude( X, preCalcedPhiSeries );
487                lp.x += getProjectionLongitude();
488            }
489            return lp;
490        }
491    
492        /*
493         * (non-Javadoc)
494         * 
495         * @see org.deegree.crs.projections.Projection#doProjection(double, double)
496         */
497        @Override
498        public Point2d doProjection( double lambda, double phi )
499                                                                throws ProjectionException {
500            Point2d xy = new Point2d( 0, 0 );
501            lambda -= getProjectionLongitude();
502            double cosLamda = Math.cos( lambda );
503            double sinLamda = Math.sin( lambda );
504            double sinPhi = Math.sin( phi );
505    
506            if ( isSpherical() ) {
507                double cosphi = Math.cos( phi );
508    
509                switch ( getMode() ) {
510                case OBLIQUE:
511                    // Calc k from Snyder (p.158 21-14) and store in xy.y
512                    xy.y = 1. + getSinphi0() * sinPhi + getCosphi0() * cosphi * cosLamda;
513                    if ( xy.y <= ProjectionUtils.EPS10 ) {
514                        throw new ProjectionException( "Division by zero" );
515                    }
516                    // akm1 was set to 2*scale.
517                    xy.y = akm1 / xy.y;
518    
519                    // Calc x (p.157 21-2) and y (p.157 21-3)
520                    xy.x = xy.y * cosphi * sinLamda;
521                    xy.y *= getCosphi0() * sinPhi - getSinphi0() * cosphi * cosLamda;
522                    break;
523                case EQUATOR:
524                    // Calc k from Snyder (p.158 21-14) and store in xy.y
525                    xy.y = 1. + cosphi * cosLamda;
526                    if ( xy.y <= ProjectionUtils.EPS10 ) {
527                        throw new ProjectionException( "Division by zero" );
528                    }
529                    // akm1 was set to 2*scale.
530                    xy.y = akm1 / xy.y;
531    
532                    // Calc x (p.157 21-2) and y (p.158 21-13)
533                    xy.x = xy.y * cosphi * sinLamda;
534                    xy.y *= sinPhi;
535                    break;
536                case NORTH_POLE:
537                    cosLamda = -cosLamda;
538                    phi = -phi;
539                case SOUTH_POLE:
540                    if ( Math.abs( phi - ProjectionUtils.HALFPI ) < ProjectionUtils.EPS10 ) {
541                        throw new ProjectionException( "The requested phi: " + phi
542                                                       + " lies on the singularity ("
543                                                       + ( ( getMode() == SOUTH_POLE ) ? "South-Pole" : "North-Pole" )
544                                                       + ") of this projection's mappable area." );
545                    }
546                    // akm1 was set to 2*scale.
547                    xy.y = akm1 * Math.tan( ProjectionUtils.QUARTERPI + .5 * phi );
548                    xy.x = sinLamda * ( xy.y  );
549                    xy.y *= cosLamda;
550                    break;
551                }
552            } else {
553                double sinX = 0;
554                double cosX = 0;
555                double largeA;
556    
557                if ( getMode() == OBLIQUE || getMode() == EQUATOR ) {
558                    // Calculate the conformal latitue Snyder (p.160 3-1).
559                    double X = 2. * Math.atan( ProjectionUtils.conformalLatitudeInnerPart( phi, sinPhi, getEccentricity() ) )
560                               - ProjectionUtils.HALFPI;
561                    sinX = Math.sin( X );
562                    cosX = Math.cos( X );
563                }
564                switch ( getMode() ) {
565                case OBLIQUE:
566                    //System.out.println( "Oblique stereographic projection" );
567                    // akm1 was set to the first part of Snyder (p.160 21-27)
568                    // sinPhi0 and cosPhi0 where set to the conformal latitude of the natural origin.
569                    largeA = akm1 / ( getCosphi0() * ( 1. + getSinphi0() * sinX + getCosphi0() * cosX * cosLamda ) );
570                    xy.y = largeA * ( getCosphi0() * sinX - getSinphi0() * cosX * cosLamda );
571                    xy.x = largeA * cosX;
572    
573                    break;
574                case EQUATOR:
575                    //System.out.println( "equatorial stereographic projection" );
576                    // akm1 was set to 2. * scale;
577                    // Calculate the A of Snyder (p.161 21-29).
578                    largeA = 2. * akm1 / ( 1. + cosX * cosLamda );
579                    xy.y = largeA * sinX;
580                    xy.x = largeA * cosX;
581                    break;
582                case SOUTH_POLE:
583                    //System.out.println( "South-pole stereographic projection" );
584                    phi = -phi;
585                    cosLamda = -cosLamda;
586                    sinPhi = -sinPhi;
587                case NORTH_POLE:
588                    //System.out.println( "North-pole stereographic projection" );
589                    // akm1 holds the rho's from 21-33 or 21-34, but without the t (==halfColatitude of the conformal
590                    // latitude).
591                    xy.x = akm1 * ProjectionUtils.tanHalfCoLatitude( phi, sinPhi, getEccentricity() );
592                    xy.y = -xy.x * cosLamda;
593                    break;
594                }
595                // Sinus(Lambda) was still to be multiplied on the x.
596                xy.x = xy.x * sinLamda;
597            }
598            xy.x += getFalseEasting();
599            xy.y += getFalseNorthing();
600            return xy;
601        }
602    
603        @Override
604        public String getDeegreeSpecificName() {
605            return "stereographicAzimuthal";
606        }
607    
608        /**
609         * @return the trueScaleLatitude.
610         */
611        public final double getTrueScaleLatitude() {
612            return trueScaleLatitude;
613        }
614    }