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>LambertAzimuthalEqualArea</code> projection has following properties (From J.S. Snyder, Map Projections a 050 * Working Manual p. 182): 051 * <ul> 052 * <li>Azimuthal</li> 053 * <li>Equal-Area</li> 054 * <li>All meridians in the polar aspect, the central meridian in other aspects, and the Equator in the equatorial 055 * aspect are straight lines</li> 056 * <li>The outer meridian of a hemisphere in the equatorial aspect (for the sphere) and the parallels in the polar 057 * aspect (sphere or ellipsoid) are circles.</li> 058 * <li>All other meridians and the parallels are complex curves</li> 059 * <li>Not a perspective projection</li> 060 * <li>Scale decreases radially as the distance increases from the center, the only point without distortion</li> 061 * <li>Directions from the center are true for the sphere and the polar ellipsoidal forms.</li> 062 * <li>Point opposite the center is shown as a circle surrounding the map (for the sphere).</li> 063 * <li>Used for maps of continents and hemispheres</li> 064 * <li>presented by lambert in 1772</li> 065 * </ul> 066 * 067 * <p> 068 * The difference to orthographic and stereographic projection, comes from the spacing between the parallels. The space 069 * decreases with increasing distance from the pole. The opposite pole not visible on either the orthographic or 070 * stereographic may be shown on the lambert as a large circle surrounding the map, almost half again as far as the 071 * equator from the center. Normally the projectction is not shown beyond one hemisphere (or beyond the equator in the 072 * polar aspect). 073 * </p> 074 * 075 * <p> 076 * It is known to be used by following epsg transformations: 077 * <ul> 078 * <li>EPSG:3035</li> 079 * </ul> 080 * </p> 081 * 082 * @author <a href="mailto:bezema@lat-lon.de">Rutger Bezema</a> 083 * 084 * @author last edited by: $Author:$ 085 * 086 * @version $Revision:$, $Date:$ 087 * 088 */ 089 090 public class LambertAzimuthalEqualArea extends AzimuthalProjection { 091 092 private double sinb1; 093 094 private double cosb1; 095 096 /** 097 * qp is q (needed for authalicLatitude Snyder 3-12) evaluated for a phi of 90°. 098 */ 099 private double qp; 100 101 /** 102 * Will hold the value D (A slide adjustment for the standardpoint, to achieve a correct scale in all directions at 103 * the center of the projection) calculated by Snyder (24-20). 104 */ 105 private double dd; 106 107 /** 108 * Radius for the sphere having the same surface area as the ellipsoid. Calculated with Snyder (3-13). 109 */ 110 private double rq; 111 112 /** 113 * precalculated series values to calculate the authalic latitude value from. 114 */ 115 private double[] apa; 116 117 /** 118 * A variable to hold a precalculated value for the x parameter of the oblique projection 119 */ 120 private double xMultiplyForward; 121 122 /** 123 * A variable to hold a precalculated value for the y parameter of the oblique or equatorial projection 124 */ 125 private double yMultiplyForward; 126 127 /** 128 * @param geographicCRS 129 * @param falseNorthing 130 * @param falseEasting 131 * @param naturalOrigin 132 * @param units 133 * @param scale 134 * @param identifiers 135 * @param names 136 * @param versions 137 * @param descriptions 138 * @param areasOfUse 139 */ 140 public LambertAzimuthalEqualArea( GeographicCRS geographicCRS, double falseNorthing, double falseEasting, 141 Point2d naturalOrigin, Unit units, double scale, 142 String[] identifiers, String[] names, String[] versions, String[] descriptions, 143 String[] areasOfUse ) { 144 super( geographicCRS, 145 falseNorthing, 146 falseEasting, 147 naturalOrigin, 148 units, 149 scale, 150 false,//not conformal 151 true,//equal-area 152 identifiers, 153 names, 154 versions, 155 descriptions, 156 areasOfUse ); 157 if ( !isSpherical() ) { 158 // sin(rad(90)) = 1; 159 qp = ProjectionUtils.calcQForAuthalicLatitude( 1., getEccentricity() ); 160 rq = getSemiMajorAxis() * Math.sqrt( .5 * qp );// Snyder (3-13) 161 apa = ProjectionUtils.getAuthalicLatitudeSeriesValues( getSquaredEccentricity() ); 162 163 switch ( getMode() ) { 164 case NORTH_POLE: 165 case SOUTH_POLE: 166 xMultiplyForward = getSemiMajorAxis(); 167 yMultiplyForward = ( getMode() == NORTH_POLE ) ? -getSemiMajorAxis() : getSemiMajorAxis(); 168 dd = 1.; 169 break; 170 case EQUATOR: 171 dd = 1. / ( rq ); 172 xMultiplyForward = getSemiMajorAxis(); 173 yMultiplyForward = getSemiMajorAxis() * .5 * qp; 174 break; 175 case OBLIQUE: 176 double sinphi = getSinphi0(); 177 // arcsin( q/ qp) = beta . Snyder (3-11) 178 sinb1 = ProjectionUtils.calcQForAuthalicLatitude( sinphi, getEccentricity() ) / qp; 179 // sin*sin + cos*cos = 1 180 cosb1 = Math.sqrt( 1. - sinb1 * sinb1 ); 181 // (24-20) D = a*m_1 / (Rq*cos(beta_1) ) 182 double m_1 = getCosphi0() / Math.sqrt( 1. - getSquaredEccentricity() * sinphi * sinphi ); 183 dd = getSemiMajorAxis() * m_1 / ( rq * cosb1 ); 184 xMultiplyForward = rq * dd; 185 yMultiplyForward = rq / dd; 186 break; 187 } 188 } 189 } 190 191 /** 192 * @param geographicCRS 193 * @param falseNorthing 194 * @param falseEasting 195 * @param naturalOrigin 196 * @param units 197 * @param scale 198 * @param identifier 199 * @param name 200 * @param version 201 * @param description 202 * @param areaOfUse 203 */ 204 public LambertAzimuthalEqualArea( GeographicCRS geographicCRS, double falseNorthing, double falseEasting, 205 Point2d naturalOrigin, Unit units, double scale, 206 String identifier, String name, String version, String description, 207 String areaOfUse ) { 208 this( geographicCRS, 209 falseNorthing, 210 falseEasting, 211 naturalOrigin, 212 units, 213 scale, 214 new String[] { identifier }, 215 new String[] { name }, 216 new String[] { version }, 217 new String[] { description }, 218 new String[] { areaOfUse } ); 219 } 220 221 /** 222 * @param geographicCRS 223 * @param falseNorthing 224 * @param falseEasting 225 * @param naturalOrigin 226 * @param units 227 * @param scale 228 * @param identifiers 229 */ 230 public LambertAzimuthalEqualArea( GeographicCRS geographicCRS, double falseNorthing, double falseEasting, 231 Point2d naturalOrigin, Unit units, double scale, 232 String[] identifiers ) { 233 this( geographicCRS, 234 falseNorthing, 235 falseEasting, 236 naturalOrigin, 237 units, 238 scale, 239 identifiers, 240 null, 241 null, 242 null, 243 null ); 244 } 245 246 /** 247 * @param geographicCRS 248 * @param falseNorthing 249 * @param falseEasting 250 * @param naturalOrigin 251 * @param units 252 * @param scale 253 * @param identifier 254 */ 255 public LambertAzimuthalEqualArea( GeographicCRS geographicCRS, double falseNorthing, double falseEasting, 256 Point2d naturalOrigin, Unit units, double scale, 257 String identifier ) { 258 this( geographicCRS, 259 falseNorthing, 260 falseEasting, 261 naturalOrigin, 262 units, 263 scale, 264 new String[]{identifier} ); 265 } 266 267 268 /** 269 * @param geographicCRS 270 * @param falseNorthing 271 * @param falseEasting 272 * @param naturalOrigin 273 * @param units 274 * @param identifier 275 */ 276 public LambertAzimuthalEqualArea( GeographicCRS geographicCRS, double falseNorthing, double falseEasting, 277 Point2d naturalOrigin, Unit units, String identifier ) { 278 this( geographicCRS, falseNorthing, falseEasting, naturalOrigin, units, 1, identifier ); 279 } 280 281 /* 282 * (non-Javadoc) 283 * 284 * @see org.deegree.crs.projections.Projection#doInverseProjection(double, double) 285 */ 286 @Override 287 public Point2d doInverseProjection( double x, double y ) 288 throws ProjectionException { 289 Point2d lp = new Point2d( 0, 0 ); 290 x -= getFalseEasting(); 291 y -= getFalseNorthing(); 292 // Snyder (20-18) 293 double rho = ProjectionUtils.length( x, y ); 294 if ( isSpherical() ) { 295 double cosC = 0; 296 double sinC = 0; 297 298 // Snyder (20-18) 299 if ( rho * .5 > 1. ) { 300 throw new ProjectionException( "The Y value is beyond the maximum mappable area" ); 301 } 302 // lp.y = 2. * Math.asin( rho*.5 ); 303 // Snyder (24-16) 304 double c = 2 * Math.asin( rho * 0.5 ); 305 306 if ( getMode() == OBLIQUE || getMode() == EQUATOR ) { 307 sinC = Math.sin( c ); 308 cosC = Math.cos( c ); 309 } 310 switch ( getMode() ) { 311 case OBLIQUE: 312 // the theta from Snyder (20-14) 313 lp.y = ( rho <= ProjectionUtils.EPS10 ) ? getProjectionLatitude() 314 : Math.asin( cosC * getSinphi0() 315 + ( ( y * sinC * getCosphi0() ) / rho ) ); 316 317 // For the calculation of the Lamda (Snyder[20-15]) proj4 obviously uses the atan2 method, I don't know 318 // if this is correct. 319 320 // x = the radial coordinate (usually denoted as r) it denotes the point's distance from a central point 321 // known as the pole (equivalent to the origin in the Cartesian system) 322 x *= sinC * getCosphi0(); 323 324 // y = The angular coordinate (also known as the polar angle or the azimuth angle, and usually denoted 325 // by θ or t) denotes the positive or anticlockwise (counterclockwise) angle required to reach the point 326 // from the 0° ray or polar axis (which is equivalent to the positive x-axis in the Cartesian coordinate 327 // plane). 328 y = ( cosC - Math.sin( lp.y ) * getSinphi0() ) * rho; 329 /** 330 * it could be something like this too. 331 */ 332 // lp.x = Math.atan( ( x * sinC ) / ( ( rho * getCosphi0() * cosC ) - ( y * sinC * getSinphi0() ) ) ); 333 break; 334 case EQUATOR: 335 lp.y = ( rho <= ProjectionUtils.EPS10 ) ? 0. : Math.asin( y * sinC / rho ); 336 x *= sinC; 337 y = cosC * rho; 338 break; 339 case NORTH_POLE: 340 y = -y; 341 // cos(90 or -90) = 0, therefore the last term from (20-14) is null 342 // sin( 90 or -90 = + or - 1 what is 343 // left is asin( (-or+) cosC ) 344 // from cos(c) = sin( HALFPI - c ) follows 345 lp.y = ProjectionUtils.HALFPI - c; 346 break; 347 case SOUTH_POLE: 348 lp.y = c - ProjectionUtils.HALFPI; 349 break; 350 } 351 // calculation of the Lamda (Snyder[20-15]) is this correct??? 352 lp.x = ( ( y == 0. && ( getMode() == EQUATOR || getMode() == OBLIQUE ) ) ? 0. : Math.atan2( x, y ) ); 353 } else { 354 double q = 0; 355 double arcSinusBeta = 0; 356 switch ( getMode() ) { 357 case EQUATOR: 358 case OBLIQUE: 359 // Snyder (p.189 24-28) 360 x /= dd; 361 y *= dd; 362 rho = ProjectionUtils.length( x, y ); 363 if ( rho < ProjectionUtils.EPS10 ) { 364 return new Point2d( 0, getProjectionLatitude() ); 365 } 366 // Snyder (p.189 24-29). 367 double ce = 2. * Math.asin( ( .5 * rho ) / rq ); 368 double cosinusCe = Math.cos( ce ); 369 double sinusCe = Math.sin( ce ); 370 371 x *= sinusCe; 372 if ( getMode() == OBLIQUE ) { 373 // Snyder (p.189 24-30) 374 arcSinusBeta = cosinusCe * sinb1 + ( ( y * sinusCe * cosb1 ) / rho ); 375 // Snyder (p.188 24-27) 376 q = qp * ( arcSinusBeta ); 377 // calculate the angular coordinate to be used in the atan2 method. 378 y = rho * cosb1 * cosinusCe - y * sinb1 * sinusCe; 379 } else { 380 arcSinusBeta = y * sinusCe / rho; 381 q = qp * ( arcSinusBeta ); 382 y = rho * cosinusCe; 383 } 384 break; 385 case NORTH_POLE: 386 y = -y; 387 case SOUTH_POLE: 388 // will be used to calc q. 389 q = ( x * x + y * y ); 390 if ( Math.abs( q ) < ProjectionUtils.EPS10 ) { 391 return new Point2d( 0, getProjectionLatitude() ); 392 } 393 // Simplified Snyder (p.190 24-32), because sin(phi) = 1, the qp can be used to calc arcSinusBeta. 394 // xMultiplyForward = getSemiMajorAxis(); 395 double aSquare = xMultiplyForward * xMultiplyForward; 396 arcSinusBeta = 1. - ( q / ( aSquare * qp ) ); 397 if ( getMode() == SOUTH_POLE ) { 398 arcSinusBeta = -arcSinusBeta; 399 } 400 break; 401 } 402 lp.x = Math.atan2( x, y ); 403 lp.y = ProjectionUtils.calcPhiFromAuthalicLatitude( Math.asin( arcSinusBeta ), apa ); 404 } 405 lp.x += getProjectionLongitude(); 406 return lp; 407 } 408 409 /* 410 * (non-Javadoc) 411 * 412 * @see org.deegree.crs.projections.Projection#doProjection(double, double) 413 */ 414 @Override 415 public Point2d doProjection( double lambda, double phi ) 416 throws ProjectionException { 417 Point2d result = new Point2d( 0, 0 ); 418 lambda -= getProjectionLongitude(); 419 double sinphi = Math.sin( phi ); 420 double cosLamda = Math.cos( lambda ); 421 double sinLambda = Math.sin( lambda ); 422 423 if ( isSpherical() ) { 424 double cosphi = Math.cos( phi ); 425 double kAccent = 0; 426 switch ( getMode() ) { 427 case OBLIQUE: 428 // Calculation of k' Snyder (24-2) 429 kAccent = 1. + ( getSinphi0() * sinphi ) + ( getCosphi0() * cosphi * cosLamda ); 430 if ( Math.abs( kAccent ) <= ProjectionUtils.EPS11 ) { 431 throw new ProjectionException( "The scalefactor (k') in the perpendicular direction to the radius from the center of the map equals: " + kAccent 432 + " this will cause a divide by zero." ); 433 } 434 kAccent = Math.sqrt( 2. / kAccent ); 435 436 result.x = kAccent * cosphi * sinLambda; 437 result.y = kAccent * ( ( getCosphi0() * sinphi ) - ( getSinphi0() * cosphi * cosLamda ) ); 438 break; 439 case EQUATOR: 440 kAccent = 1. + cosphi * cosLamda; 441 if ( kAccent <= ProjectionUtils.EPS11 ) { 442 throw new ProjectionException( "The scalefactor (k') in the perpendicular direction to the radius from the center of the map equals: " + kAccent 443 + " this will cause a divide by zero." ); 444 } 445 kAccent = Math.sqrt( 2. / kAccent ); 446 result.x = kAccent * cosphi * sinLambda; 447 result.y = kAccent * sinphi; 448 break; 449 case NORTH_POLE: 450 cosLamda = -cosLamda; 451 case SOUTH_POLE: 452 if ( Math.abs( phi + getProjectionLatitude() ) < ProjectionUtils.EPS11 ) { 453 throw new ProjectionException( "The requested phi: " + phi 454 + " lies on the singularity (" 455 + ( ( getMode() == SOUTH_POLE ) ? "South-Pole" : "North-Pole" ) 456 + ") of this projection's mappable area." ); 457 } 458 result.y = ProjectionUtils.QUARTERPI - ( phi * .5 ); 459 result.y = 2. * ( getMode() == SOUTH_POLE ? Math.cos( result.y ) : Math.sin( result.y ) ); 460 result.x = result.y * sinLambda; 461 result.y *= cosLamda; 462 break; 463 } 464 // the radius is stil to be multiplied. 465 result.x *= getSemiMajorAxis(); 466 result.y *= getSemiMajorAxis(); 467 } else { 468 double sinb = 0; 469 double cosb = 0; 470 471 // The big B of snyder (24-19), but will also be used as a place holder for the none oblique calculations. 472 double bigB = 0; 473 474 double q = ProjectionUtils.calcQForAuthalicLatitude( sinphi, getEccentricity() ); 475 if ( getMode() == OBLIQUE || getMode() == EQUATOR ) { 476 // snyder ( 3-11 ) 477 sinb = q / qp; 478 cosb = Math.sqrt( 1. - sinb * sinb ); 479 } 480 switch ( getMode() ) { 481 case OBLIQUE: 482 // Snyder (24-19) 483 bigB = 1. + sinb1 * sinb + cosb1 * cosb * cosLamda; 484 break; 485 case EQUATOR: 486 // dd, sin as well as cos(beta1) fall out Snyder (24-21). 487 bigB = 1. + cosb * cosLamda; 488 break; 489 case NORTH_POLE: 490 bigB = ProjectionUtils.HALFPI + phi; 491 q = qp - q; 492 break; 493 case SOUTH_POLE: 494 bigB = phi - ProjectionUtils.HALFPI; 495 q = qp + q; 496 break; 497 } 498 /** 499 * Test to see if the projection point is 0, -> divide by zero. 500 */ 501 if ( Math.abs( bigB ) < ProjectionUtils.EPS10 ) { 502 throw new ProjectionException( "The projectionPoint B from the authalic latitude beta: " + ( Math.toDegrees( Math.asin( sinb ) ) ) 503 + "° lies on the singularity of this projection's mappable area, resulting in a divide by zero." ); 504 } 505 switch ( getMode() ) { 506 case OBLIQUE: 507 bigB = Math.sqrt( 2 / bigB ); 508 result.x = xMultiplyForward * bigB * cosb * sinLambda; 509 result.y = yMultiplyForward * bigB * ( cosb1 * sinb - sinb1 * cosb * cosLamda ); 510 break; 511 case EQUATOR: 512 bigB = Math.sqrt( 2 / bigB ); 513 // dd, sin as well as cosbeta1 fall out Snyder (24-21), xMulti = getSemimajorAxis(), yMulti = 514 // getSemiMajorAxsis() * 0.5 * qp 515 result.x = xMultiplyForward * bigB * cosb * sinLambda; 516 result.y = yMultiplyForward * bigB * sinb; 517 break; 518 case NORTH_POLE: 519 case SOUTH_POLE: 520 if ( q >= 0. ) { 521 bigB = Math.sqrt( q ); 522 // xMulti = yMulti = getSemimajorAxis() 523 result.x = xMultiplyForward * bigB * sinLambda; 524 // if NORTH, yMultiplyForward = -getSemiMajorAxis. 525 result.y = yMultiplyForward * cosLamda * bigB; 526 } else { 527 result.x = 0; 528 result.y = 0; 529 } 530 break; 531 } 532 } 533 result.x += getFalseEasting(); 534 result.y += getFalseNorthing(); 535 return result; 536 } 537 538 @Override 539 public String getDeegreeSpecificName() { 540 return "lambertAzimuthalEqualArea"; 541 } 542 543 }