001 //$HeadURL: svn+ssh://jwilden@svn.wald.intevation.org/deegree/base/branches/2.5_testing/src/org/deegree/crs/projections/azimuthal/StereographicAzimuthal.java $
002 /*----------------------------------------------------------------------------
003 This file is part of deegree, http://deegree.org/
004 Copyright (C) 2001-2009 by:
005 Department of Geography, University of Bonn
006 and
007 lat/lon GmbH
008
009 This library is free software; you can redistribute it and/or modify it under
010 the terms of the GNU Lesser General Public License as published by the Free
011 Software Foundation; either version 2.1 of the License, or (at your option)
012 any later version.
013 This library is distributed in the hope that it will be useful, but WITHOUT
014 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
015 FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
016 details.
017 You should have received a copy of the GNU Lesser General Public License
018 along with this library; if not, write to the Free Software Foundation, Inc.,
019 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
020
021 Contact information:
022
023 lat/lon GmbH
024 Aennchenstr. 19, 53177 Bonn
025 Germany
026 http://lat-lon.de/
027
028 Department of Geography, University of Bonn
029 Prof. Dr. Klaus Greve
030 Postfach 1147, 53001 Bonn
031 Germany
032 http://www.geographie.uni-bonn.de/deegree/
033
034 e-mail: info@deegree.org
035 ----------------------------------------------------------------------------*/
036
037 package org.deegree.crs.projections.azimuthal;
038
039 import static org.deegree.crs.projections.ProjectionUtils.EPS10;
040 import static org.deegree.crs.projections.ProjectionUtils.EPS11;
041 import static org.deegree.crs.projections.ProjectionUtils.HALFPI;
042 import static org.deegree.crs.projections.ProjectionUtils.QUARTERPI;
043 import static org.deegree.crs.projections.ProjectionUtils.calcPhiFromConformalLatitude;
044 import static org.deegree.crs.projections.ProjectionUtils.conformalLatitudeInnerPart;
045 import static org.deegree.crs.projections.ProjectionUtils.length;
046 import static org.deegree.crs.projections.ProjectionUtils.preCalcedThetaSeries;
047 import static org.deegree.crs.projections.ProjectionUtils.tanHalfCoLatitude;
048
049 import javax.vecmath.Point2d;
050
051 import org.deegree.crs.Identifiable;
052 import org.deegree.crs.components.Unit;
053 import org.deegree.crs.coordinatesystems.GeographicCRS;
054 import org.deegree.crs.exceptions.ProjectionException;
055
056 /**
057 * The <code>StereographicAzimuthal</code> class allows for Stereographic Projections of the Poles, equator as well as
058 * oblique. This projection has following properties (Snyder p. 154):
059 * <ul>
060 * <li>Azimuthal</li>
061 * <li>Conformal</li>
062 * <li>The central meridian an a particular parallel (if shown) are straight lines.</li>
063 * <li>All meridians on the polar aspects and the equator on the equatorial aspect are straight lines.</li>
064 * <li>All other meidians and parallels are shown as arcs of circles</li>
065 * <li>A perspective projections for the sphere</li>
066 * <li>Directions from the center of the projection are true (except on ellipsoidal oblique and equatorial aspects)</li>
067 * <li>Scale increases away from the center of the projection</li>
068 * <li>Point opposite the center of the projection cannot be plotted</li>
069 * <li>Used for polar maps and miscellaneous special maps</li>
070 * </ul>
071 *
072 * <p>
073 * Like Orthographic, the stereographic projection is a true perspective in its isSpherical() form. It is the only known
074 * true perspective projection of any kind that is also conformal. Its point of projection is on the the surface of the
075 * sphere at a point jus opposite the oint of tangency of the plane or the center point of the projection. Thus, if the
076 * north pole is the center of the map, the projection is from the south-pole.
077 * </p>
078 * <p>
079 * It is known to be used by following epsg transformations:
080 * <ul>
081 * <li>EPSG:28992</li>
082 * </ul>
083 * </p>
084 *
085 * @author <a href="mailto:bezema@lat-lon.de">Rutger Bezema</a>
086 *
087 * @author last edited by: $Author: rbezema $
088 *
089 * @version $Revision: 18388 $, $Date: 2009-07-08 15:50:37 +0200 (Mi, 08 Jul 2009) $
090 *
091 */
092
093 public class StereographicAzimuthal extends AzimuthalProjection {
094
095 private static final long serialVersionUID = 5399110969291553925L;
096
097 /**
098 * This variable will hold different values, for the ellipsoidal projection:
099 * <ul>
100 * <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>
101 * <li>for the north-south it may hold the rho of (21-33) or (21-34)
102 * <li>for the equatorial it holds 2*scale.</li>
103 * <li>For the
104 * </ul>
105 * For the spherical projection:
106 * <ul>
107 * <li>For oblique and equatorial: 2*scale, which is compliant with 2*k_0 from Snyder (p.157 21-4)</li>
108 * <li>For north and south: cos(truelat) / tan( pi/4 - phi/2) ????</li>
109 * </ul>
110 */
111 private double akm1;
112
113 private double trueScaleLatitude;
114
115 private double[] preCalcedPhiSeries;
116
117 // use for the OBLIQUE projection instead of the projectionLatitude
118 private double conformalLatitude = 0;
119
120 // sine of the conformal latitude
121 private double sinCL = 0;
122
123 // cosine of the conformal latitude
124 private double cosCL = 0;
125
126 /**
127 * @param trueScaleLatitude
128 * the latitude (in radians) of a circle around the projection point, which contains the true scale.
129 * @param geographicCRS
130 * @param falseNorthing
131 * @param falseEasting
132 * @param naturalOrigin
133 * @param units
134 * @param scale
135 * @param id
136 * an identifiable instance containing information about this projection
137 */
138 public StereographicAzimuthal( double trueScaleLatitude, GeographicCRS geographicCRS, double falseNorthing,
139 double falseEasting, Point2d naturalOrigin, Unit units, double scale, Identifiable id ) {
140 super( geographicCRS, falseNorthing, falseEasting, naturalOrigin, units, scale, true,
141 false/* not equal area */, id );
142
143 preCalcedPhiSeries = preCalcedThetaSeries( getSquaredEccentricity() );
144
145 this.trueScaleLatitude = trueScaleLatitude;
146 if ( !isSpherical() ) {
147 // double X;
148 double tmp = 0;
149 switch ( getMode() ) {
150 case NORTH_POLE:
151 case SOUTH_POLE:
152 /**
153 * The t from 21-33 and 21-34 is still to be calculated.
154 */
155 // If true scale or known scale factor k_0 is to occur at the pole, the following applies:
156 if ( Double.isNaN( trueScaleLatitude )
157 || Math.abs( Math.abs( this.trueScaleLatitude ) - HALFPI ) < EPS10 ) {
158 // Snyder (p.161 21-33) akm1 = rho .
159 akm1 = 2.
160 * getScaleFactor()
161 / Math.sqrt( Math.pow( 1 + getEccentricity(), 1 + getEccentricity() )
162 * Math.pow( 1 - getEccentricity(), 1 - getEccentricity() ) );
163
164 } else {
165 // For true scale along the circle represented by trueScaleLatitude (phi_c in snyder)..
166 // akm1 will hold m_c/t_c
167 // Calculate the rho from Snyder (p.161 21-34) and place in akm1
168 tmp = Math.sin( this.trueScaleLatitude );
169
170 // First part of m of Snyder (p.160 14-15)
171 akm1 = Math.cos( this.trueScaleLatitude )
172 / tanHalfCoLatitude( this.trueScaleLatitude, tmp, getEccentricity() );
173 tmp *= getEccentricity();
174 // Second part of m of Snyder (p.160 14-15), t = (e*sin(phi_c))
175 akm1 /= Math.sqrt( 1. - tmp * tmp );
176
177 }
178 break;
179 case EQUATOR:
180 akm1 = 2. * getScaleFactor();
181 break;
182 case OBLIQUE:
183 tmp = getSinphi0();// Math.sin( getProjectionLatitude() );
184 // Calculate the ConformalLatitude for this ellipsoid Snyder (p.160 3-1) the X
185 conformalLatitude = ( 2. * Math.atan( conformalLatitudeInnerPart( getProjectionLatitude(), tmp,
186 getEccentricity() ) ) )
187 - HALFPI;
188
189 sinCL = Math.sin( conformalLatitude );
190 cosCL = Math.cos( conformalLatitude );
191
192 tmp *= getEccentricity();
193 // the first part (2a*k_0*m) of the largeA in Snyder (p.160 21-27) is stored in akm1 it still must be
194 // devided by the cos X ... etc. to get largeA
195 akm1 = 2. * getScaleFactor() * getCosphi0() / Math.sqrt( 1. - tmp * tmp );
196
197 // Setting the sinPhi0 to the conformal Latitude X.
198 // setProjectionLatitude( X );
199 break;
200 }
201 } else {
202 akm1 = 2. * getScaleFactor();
203 }
204 }
205
206 /**
207 * Sets the id to "Snyder-StereoGraphic"
208 *
209 * @param trueScaleLatitude
210 * the latitude (in radians) of a circle around the projection point, which contains the true scale.
211 * @param geographicCRS
212 * @param falseNorthing
213 * @param falseEasting
214 * @param naturalOrigin
215 * @param units
216 * @param scale
217 */
218 public StereographicAzimuthal( double trueScaleLatitude, GeographicCRS geographicCRS, double falseNorthing,
219 double falseEasting, Point2d naturalOrigin, Unit units, double scale ) {
220 this( trueScaleLatitude, geographicCRS, falseNorthing, falseEasting, naturalOrigin, units, scale,
221 new Identifiable( "Snyder-StereoGraphic" ) );
222 }
223
224 /**
225 * Create a {@link StereographicAzimuthal} which has a true scale latitude at MapUtils.HALFPI.
226 *
227 * @param geographicCRS
228 * @param falseNorthing
229 * @param falseEasting
230 * @param naturalOrigin
231 * @param units
232 * @param scale
233 * @param id
234 * an identifiable instance containing information about this projection
235 */
236 public StereographicAzimuthal( GeographicCRS geographicCRS, double falseNorthing, double falseEasting,
237 Point2d naturalOrigin, Unit units, double scale, Identifiable id ) {
238 this( HALFPI, geographicCRS, falseNorthing, falseEasting, naturalOrigin, units, scale, id );
239 }
240
241 /**
242 * Create a {@link StereographicAzimuthal} which has a true scale latitude at MapUtils.HALFPI. Sets the id to
243 * "Snyder-StereoGraphic"
244 *
245 * @param geographicCRS
246 * @param falseNorthing
247 * @param falseEasting
248 * @param naturalOrigin
249 * @param units
250 * @param scale
251 */
252 public StereographicAzimuthal( GeographicCRS geographicCRS, double falseNorthing, double falseEasting,
253 Point2d naturalOrigin, Unit units, double scale ) {
254 this( HALFPI, geographicCRS, falseNorthing, falseEasting, naturalOrigin, units, scale );
255 }
256
257 /**
258 * Create a {@link StereographicAzimuthal} which has a scale of 1 and a true scale latitude,
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 id
268 * an identifiable instance containing information about this projection
269 */
270 public StereographicAzimuthal( double trueScaleLatitude, GeographicCRS geographicCRS, double falseNorthing,
271 double falseEasting, Point2d naturalOrigin, Unit units, Identifiable id ) {
272 this( trueScaleLatitude, geographicCRS, falseNorthing, falseEasting, naturalOrigin, units, 1, id );
273 }
274
275 /**
276 * Create a {@link StereographicAzimuthal} which has a scale of 1 and a true scale latitude. Sets the id to
277 * "Snyder-StereoGraphic".
278 *
279 * @param trueScaleLatitude
280 * the latitude (in radians) of a circle around the projection point, which contains the true scale.
281 * @param geographicCRS
282 * @param falseNorthing
283 * @param falseEasting
284 * @param naturalOrigin
285 * @param units
286 */
287 public StereographicAzimuthal( double trueScaleLatitude, GeographicCRS geographicCRS, double falseNorthing,
288 double falseEasting, Point2d naturalOrigin, Unit units ) {
289 this( trueScaleLatitude, geographicCRS, falseNorthing, falseEasting, naturalOrigin, units, 1 );
290 }
291
292 /**
293 * Create a {@link StereographicAzimuthal} which is conformal, has a scale of 1 and a truescale latitude at pi*0.5.
294 *
295 * @param geographicCRS
296 * @param falseNorthing
297 * @param falseEasting
298 * @param naturalOrigin
299 * @param units
300 * @param id
301 * an identifiable instance containing information about this projection
302 */
303 public StereographicAzimuthal( GeographicCRS geographicCRS, double falseNorthing, double falseEasting,
304 Point2d naturalOrigin, Unit units, Identifiable id ) {
305 this( HALFPI, geographicCRS, falseNorthing, falseEasting, naturalOrigin, units, 1, id );
306 }
307
308 /**
309 * Create a {@link StereographicAzimuthal} which is conformal, has a scale of 1 and a truescale latitude at pi*0.5.
310 * Sets the id to "Snyder-StereoGraphic".
311 *
312 * @param geographicCRS
313 * @param falseNorthing
314 * @param falseEasting
315 * @param naturalOrigin
316 * @param units
317 */
318 public StereographicAzimuthal( GeographicCRS geographicCRS, double falseNorthing, double falseEasting,
319 Point2d naturalOrigin, Unit units ) {
320 this( HALFPI, geographicCRS, falseNorthing, falseEasting, naturalOrigin, units, 1 );
321 }
322
323 @Override
324 public Point2d doInverseProjection( double x, double y ) {
325 Point2d result = new Point2d( 0, 0 );
326 x -= getFalseEasting();
327 y -= getFalseNorthing();
328 double rho = length( x, y );
329 if ( isSpherical() ) {
330 // akm1 = 2*scale.
331 double c = 2. * Math.atan( rho / akm1 );
332 double sinc = Math.sin( c );
333 double cosc = Math.cos( c );
334 switch ( getMode() ) {
335 case OBLIQUE:
336 // First find theta from Snyder (p.158 20-14).
337 if ( Math.abs( rho ) <= EPS10 ) {
338 result.y = getProjectionLatitude();
339 } else {
340 result.y = Math.asin( cosc * getSinphi0() + ( ( y * sinc * getCosphi0() ) / rho ) );
341 }
342 // And now lambda but instead of using the atan, use atan2. Is this correct????
343 c = cosc - getSinphi0() * Math.sin( result.y );
344 if ( Math.abs( c ) > EPS10 || x != 0. ) {
345 result.x = Math.atan2( x * sinc * getCosphi0(), c * rho );
346 }
347 break;
348 case EQUATOR:
349 if ( Math.abs( rho ) > EPS10 ) {
350 result.y = Math.asin( y * sinc / rho );
351 }
352 if ( cosc != 0. || x != 0. ) {
353 result.x = Math.atan2( x * sinc, cosc * rho );
354 }
355 break;
356 case NORTH_POLE:
357 y = -y;
358 case SOUTH_POLE:
359 if ( Math.abs( rho ) <= EPS10 ) {
360 result.y = getProjectionLatitude();
361 } else {
362 result.y = Math.asin( getMode() == SOUTH_POLE ? -cosc : cosc );
363 }
364 result.x = Math.atan2( x, y );
365 break;
366 }
367 } else {
368 double cosCe = 0;
369 double sinCe = 0;
370 double X = 0;
371 // for oblique and equatorial projections, akm1 = first part of Snyder (p.160 21-27). which is 2*scale*m_1
372 // double ce = 2. * Math.atan2( rho * getCosphi0(), akm1 );
373 // if ( getMode() == OBLIQUE || getMode() == EQUATOR ) {
374 // cosCe = Math.cos( ce );
375 // sinCe = Math.sin( ce );
376 // }
377 double ce = 0;
378 switch ( getMode() ) {
379 case OBLIQUE:
380 ce = 2. * Math.atan2( rho * cosCL, akm1 );
381 cosCe = Math.cos( ce );
382 sinCe = Math.sin( ce );
383 X = Math.asin( cosCe * sinCL + ( ( y * sinCe * cosCL ) / rho ) );
384 result.x = Math.atan2( x * sinCe, rho * cosCL * cosCe - y * sinCL * sinCe );
385 break;
386 case EQUATOR:
387 ce = 2. * Math.atan2( rho * getCosphi0(), akm1 );
388 cosCe = Math.cos( ce );
389 sinCe = Math.sin( ce );
390 X = Math.asin( cosCe * getSinphi0() + ( ( y * sinCe * getCosphi0() ) / rho ) );
391 result.x = Math.atan2( x * sinCe, rho * getCosphi0() * cosCe - y * getSinphi0() * sinCe );
392 break;
393 case NORTH_POLE:
394 y = -y;
395 case SOUTH_POLE:
396 /**
397 * akm1 can hold rho from 21-34 or 21-33 (if no truescale latitude was given) without the parameter t.
398 * Either way it must only be multiplied with t to fulfill the formula. Independent of the true scale
399 * latitude, the value of akm1 must therefore only be inverted to calculate Snyder (p.162 21-39 or
400 * 21-40).
401 */
402 double t = rho * 1. / this.akm1;
403 X = HALFPI - 2 * Math.atan( t );
404 if ( getMode() == SOUTH_POLE ) {
405 X *= -1;
406 }
407 result.x = Math.atan2( x, y );
408 break;
409 }
410 result.y = calcPhiFromConformalLatitude( X, preCalcedPhiSeries );
411 result.x += getProjectionLongitude();
412 }
413 return result;
414 }
415
416 @Override
417 public Point2d doProjection( double lambda, double phi )
418 throws ProjectionException {
419 Point2d result = new Point2d( 0, 0 );
420 lambda -= getProjectionLongitude();
421 double cosLamda = Math.cos( lambda );
422 double sinLamda = Math.sin( lambda );
423 double sinPhi = Math.sin( phi );
424
425 if ( isSpherical() ) {
426 double cosphi = Math.cos( phi );
427
428 switch ( getMode() ) {
429 case OBLIQUE:
430 // Calc k from Snyder (p.158 21-14) and store in xy.y
431 result.y = 1. + getSinphi0() * sinPhi + getCosphi0() * cosphi * cosLamda;
432 if ( result.y <= EPS11 ) {
433 throw new ProjectionException( "Division by zero" );
434 }
435 // akm1 was set to 2*scale.
436 result.y = akm1 / result.y;
437
438 // Calc x (p.157 21-2) and y (p.157 21-3)
439 result.x = result.y * cosphi * sinLamda;
440 result.y *= getCosphi0() * sinPhi - getSinphi0() * cosphi * cosLamda;
441 break;
442 case EQUATOR:
443 // Calc k from Snyder (p.158 21-14) and store in xy.y
444 result.y = 1. + cosphi * cosLamda;
445 if ( result.y <= EPS11 ) {
446 throw new ProjectionException( "Division by zero" );
447 }
448 // akm1 was set to 2*scale.
449 result.y = akm1 / result.y;
450
451 // Calc x (p.157 21-2) and y (p.158 21-13)
452 result.x = result.y * cosphi * sinLamda;
453 result.y *= sinPhi;
454 break;
455 case NORTH_POLE:
456 cosLamda = -cosLamda;
457 phi = -phi;
458 case SOUTH_POLE:
459 if ( Math.abs( phi - HALFPI ) < EPS10 ) {
460 throw new ProjectionException( "The requested phi: " + phi + " lies on the singularity ("
461 + ( ( getMode() == SOUTH_POLE ) ? "South-Pole" : "North-Pole" )
462 + ") of this projection's mappable area." );
463 }
464 // akm1 was set to 2*scale.
465 result.y = akm1 * Math.tan( QUARTERPI + .5 * phi );
466 result.x = sinLamda * ( result.y );
467 result.y *= cosLamda;
468 break;
469 }
470 } else {
471 double sinX = 0;
472 double cosX = 0;
473 double largeA;
474
475 if ( getMode() == OBLIQUE || getMode() == EQUATOR ) {
476 // Calculate the conformal latitue Snyder (p.160 3-1).
477 double X = 2. * Math.atan( conformalLatitudeInnerPart( phi, sinPhi, getEccentricity() ) ) - HALFPI;
478 sinX = Math.sin( X );
479 cosX = Math.cos( X );
480 }
481 switch ( getMode() ) {
482 case OBLIQUE:
483 // akm1 was set to the first part of Snyder (p.160 21-27)
484 // sinPhi0 and cosPhi0 where set to the conformal latitude of the natural origin.
485 largeA = akm1 / ( cosCL * ( 1. + sinCL * sinX + cosCL * cosX * cosLamda ) );
486 result.y = largeA * ( cosCL * sinX - sinCL * cosX * cosLamda );
487 result.x = largeA * cosX;
488 break;
489 case EQUATOR:
490 // akm1 was set to 2. * scale;
491 // Calculate the A of Snyder (p.161 21-29).
492 largeA = 2. * akm1 / ( 1. + cosX * cosLamda );
493 result.y = largeA * sinX;
494 result.x = largeA * cosX;
495 break;
496 case SOUTH_POLE:
497 phi = -phi;
498 cosLamda = -cosLamda;
499 sinPhi = -sinPhi;
500 case NORTH_POLE:
501 // akm1 holds the rho's from 21-33 or 21-34, but without the t (==halfColatitude of the conformal
502 // latitude).
503 result.x = akm1 * tanHalfCoLatitude( phi, sinPhi, getEccentricity() );
504 result.y = -result.x * cosLamda;
505 break;
506 }
507 // Sinus(Lambda) was still to be multiplied on the x.
508 result.x = result.x * sinLamda;
509 }
510 result.x += getFalseEasting();
511 result.y += getFalseNorthing();
512 return result;
513 }
514
515 @Override
516 public String getImplementationName() {
517 return "stereographicAzimuthal";
518 }
519
520 /**
521 * @return the trueScaleLatitude.
522 */
523 public final double getTrueScaleLatitude() {
524 return trueScaleLatitude;
525 }
526 }