001 //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/tags/2.1/src/org/deegree/model/csct/ct/GeocentricTransform.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 dependencies (SEAGIS) 054 import java.io.Serializable; 055 056 import javax.media.jai.ParameterList; 057 import javax.media.jai.util.Range; 058 059 import org.deegree.model.csct.cs.Ellipsoid; 060 import org.deegree.model.csct.resources.css.ResourceKeys; 061 import org.deegree.model.csct.resources.css.Resources; 062 import org.deegree.model.csct.units.Unit; 063 064 /** 065 * Transforms three dimensional geographic points to geocentric 066 * coordinate points. Input points must be longitudes, latitudes 067 * and heights above the ellipsoid. 068 * 069 * @version 1.00 070 * @author Frank Warmerdam 071 * @author Martin Desruisseaux 072 */ 073 class GeocentricTransform extends AbstractMathTransform implements Serializable { 074 /** 075 * Serial number for interoperability with different versions. 076 */ 077 private static final long serialVersionUID = -3352045463953828140L; 078 079 /** 080 * Cosine of 67.5 degrees. 081 */ 082 private static final double COS_67P5 = 0.38268343236508977; 083 084 /** 085 * Toms region 1 constant. 086 */ 087 private static final double AD_C = 1.0026000; 088 089 /** 090 * Semi-major axis of ellipsoid in meters. 091 */ 092 private final double a; 093 094 /** 095 * Semi-minor axis of ellipsoid in meters. 096 */ 097 private final double b; 098 099 /** 100 * Square of semi-major axis (@link #a}�). 101 */ 102 private final double a2; 103 104 /** 105 * Square of semi-minor axis ({@link #b}�). 106 */ 107 private final double b2; 108 109 /** 110 * Eccentricity squared. 111 */ 112 private final double e2; 113 114 /** 115 * 2nd eccentricity squared. 116 */ 117 private final double ep2; 118 119 /** 120 * <code>true</code> if geographic coordinates 121 * include an ellipsoidal height (i.e. are 3-D), 122 * or <code>false</code> if they are strictly 2-D. 123 */ 124 private final boolean hasHeight; 125 126 /** 127 * The inverse of this transform. 128 * Will be created only when needed. 129 */ 130 private transient MathTransform inverse; 131 132 /** 133 * Construct a transform. 134 * 135 * @param ellipsoid The ellipsoid. 136 * @param hasHeight <code>true</code> if geographic coordinates 137 * include an ellipsoidal height (i.e. are 3-D), 138 * or <code>false</code> if they are strictly 2-D. 139 */ 140 protected GeocentricTransform( final Ellipsoid ellipsoid, final boolean hasHeight ) { 141 this( ellipsoid.getSemiMajorAxis(), ellipsoid.getSemiMinorAxis(), ellipsoid.getAxisUnit(), 142 hasHeight ); 143 } 144 145 /** 146 * Construct a transform. 147 * 148 * @param semiMajor The semi-major axis length. 149 * @param semiMinor The semi-minor axis length. 150 * @param units The axis units. 151 * @param hasHeight <code>true</code> if geographic coordinates 152 * include an ellipsoidal height (i.e. are 3-D), 153 * or <code>false</code> if they are strictly 2-D. 154 */ 155 protected GeocentricTransform( final double semiMajor, final double semiMinor, 156 final Unit units, final boolean hasHeight ) { 157 this.hasHeight = hasHeight; 158 a = Unit.METRE.convert( semiMajor, units ); 159 b = Unit.METRE.convert( semiMinor, units ); 160 a2 = a * a; 161 b2 = b * b; 162 e2 = ( a2 - b2 ) / a2; 163 ep2 = ( a2 - b2 ) / b2; 164 checkArgument( "a", a, Double.MAX_VALUE ); 165 checkArgument( "b", b, a ); 166 } 167 168 /** 169 * Check an argument value. The argument must be greater 170 * than 0 and finite, otherwise an exception is thrown. 171 * 172 * @param name The argument name. 173 * @param value The argument value. 174 * @param max The maximal legal argument value. 175 */ 176 private static void checkArgument( final String name, final double value, final double max ) 177 throws IllegalArgumentException { 178 if ( !( value >= 0 && value <= max ) ) // Use '!' in order to trap NaN 179 { 180 throw new IllegalArgumentException( 181 Resources.format( 182 ResourceKeys.ERROR_ILLEGAL_ARGUMENT_$2, 183 name, new Double( value ) ) ); 184 } 185 } 186 187 /** 188 * Converts geodetic coordinates (longitude, latitude, height) to 189 * geocentric coordinates (x, y, z) according to the current ellipsoid 190 * parameters. 191 */ 192 public void transform( double[] srcPts, int srcOff, double[] dstPts, int dstOff, int numPts ) { 193 transform( srcPts, srcOff, dstPts, dstOff, numPts, false ); 194 } 195 196 /** 197 * Implementation of geodetic to geocentric conversion. This 198 * implementation allows the caller to use height in computation. 199 */ 200 private void transform( final double[] srcPts, int srcOff, final double[] dstPts, int dstOff, 201 int numPts, boolean hasHeight ) { 202 int step = 0; 203 final int dimSource = getDimSource(); 204 hasHeight |= ( dimSource >= 3 ); 205 if ( srcPts == dstPts && srcOff < dstOff && srcOff + numPts * dimSource > dstOff ) { 206 step = -dimSource; 207 srcOff -= ( numPts - 1 ) * step; 208 dstOff -= ( numPts - 1 ) * step; 209 } 210 while ( --numPts >= 0 ) { 211 final double L = Math.toRadians( srcPts[srcOff++] ); // Longitude 212 final double P = Math.toRadians( srcPts[srcOff++] ); // Latitude 213 final double h = hasHeight ? srcPts[srcOff++] : 0; // Height above the ellipsoid (metres). 214 215 final double cosLat = Math.cos( P ); 216 final double sinLat = Math.sin( P ); 217 final double rn = a / Math.sqrt( 1 - e2 * ( sinLat * sinLat ) ); 218 219 dstPts[dstOff++] = ( rn + h ) * cosLat * Math.cos( L ); // X 220 dstPts[dstOff++] = ( rn + h ) * cosLat * Math.sin( L ); // Y 221 dstPts[dstOff++] = ( rn * ( 1 - e2 ) + h ) * sinLat; // Z 222 srcOff += step; 223 dstOff += step; 224 } 225 } 226 227 /** 228 * Converts geodetic coordinates (longitude, latitude, height) to 229 * geocentric coordinates (x, y, z) according to the current ellipsoid 230 * parameters. 231 */ 232 public void transform( final float[] srcPts, int srcOff, final float[] dstPts, int dstOff, 233 int numPts ) { 234 int step = 0; 235 final int dimSource = getDimSource(); 236 final boolean hasHeight = ( dimSource >= 3 ); 237 if ( srcPts == dstPts && srcOff < dstOff && srcOff + numPts * dimSource > dstOff ) { 238 step = -dimSource; 239 srcOff -= ( numPts - 1 ) * step; 240 dstOff -= ( numPts - 1 ) * step; 241 } 242 while ( --numPts >= 0 ) { 243 final double L = Math.toRadians( srcPts[srcOff++] ); // Longitude 244 final double P = Math.toRadians( srcPts[srcOff++] ); // Latitude 245 final double h = hasHeight ? srcPts[srcOff++] : 0; // Height above the ellipsoid (metres). 246 247 final double cosLat = Math.cos( P ); 248 final double sinLat = Math.sin( P ); 249 final double rn = a / Math.sqrt( 1 - e2 * ( sinLat * sinLat ) ); 250 251 dstPts[dstOff++] = (float) ( ( rn + h ) * cosLat * Math.cos( L ) ); // X 252 dstPts[dstOff++] = (float) ( ( rn + h ) * cosLat * Math.sin( L ) ); // Y 253 dstPts[dstOff++] = (float) ( ( rn * ( 1 - e2 ) + h ) * sinLat ); // Z 254 srcOff += step; 255 dstOff += step; 256 } 257 } 258 259 /** 260 * Converts geocentric coordinates (x, y, z) to geodetic coordinates 261 * (longitude, latitude, height), according to the current ellipsoid 262 * parameters. The method used here is derived from "An Improved 263 * Algorithm for Geocentric to Geodetic Coordinate Conversion", by 264 * Ralph Toms, Feb 1996. 265 */ 266 protected final void inverseTransform( final double[] srcPts, int srcOff, 267 final double[] dstPts, int dstOff, int numPts ) { 268 int step = 0; 269 final int dimSource = getDimSource(); 270 final boolean hasHeight = ( dimSource >= 3 ); 271 boolean computeHeight = hasHeight; 272 273 if ( srcPts == dstPts && srcOff < dstOff && srcOff + numPts * dimSource > dstOff ) { 274 step = -dimSource; 275 srcOff -= ( numPts - 1 ) * step; 276 dstOff -= ( numPts - 1 ) * step; 277 } 278 while ( --numPts >= 0 ) { 279 final double x = srcPts[srcOff++]; 280 final double y = srcPts[srcOff++]; 281 final double z = srcPts[srcOff++]; 282 283 // Note: The Java version of 'atan2' work correctly for x==0. 284 // No need for special handling like in the C version. 285 // No special handling neither for latitude. Formulas 286 // below are generic enough, considering that 'atan' 287 // work correctly with infinities (1/0). 288 289 // Note: Variable names follow the notation used in Toms, Feb 1996 290 final double W2 = x * x + y * y; // square of distance from Z axis 291 final double W = Math.sqrt( W2 ); // distance from Z axis 292 final double T0 = z * AD_C; // initial estimate of vertical component 293 final double S0 = Math.sqrt( T0 * T0 + W2 ); // initial estimate of horizontal component 294 final double sin_B0 = T0 / S0; // sin(B0), B0 is estimate of Bowring aux variable 295 final double cos_B0 = W / S0; // cos(B0) 296 final double sin3_B0 = sin_B0 * sin_B0 * sin_B0; // cube of sin(B0) 297 final double T1 = z + b * ep2 * sin3_B0; // corrected estimate of vertical component 298 final double sum = W - a * e2 * ( cos_B0 * cos_B0 * cos_B0 ); // numerator of cos(phi1) 299 final double S1 = Math.sqrt( T1 * T1 + sum * sum ); // corrected estimate of horizontal component 300 final double sin_p1 = T1 / S1; // sin(phi1), phi1 is estimated latitude 301 final double cos_p1 = sum / S1; // cos(phi1) 302 303 final double longitude = Math.toDegrees( Math.atan2( y, x ) ); 304 final double latitude = Math.toDegrees( Math.atan( sin_p1 / cos_p1 ) ); 305 final double height; 306 307 dstPts[dstOff++] = longitude; 308 dstPts[dstOff++] = latitude; 309 if ( computeHeight ) { 310 final double rn = a / Math.sqrt( 1 - e2 * ( sin_p1 * sin_p1 ) ); // Earth radius at location 311 if ( cos_p1 >= +COS_67P5 ) 312 height = W / +cos_p1 - rn; 313 else if ( cos_p1 <= -COS_67P5 ) 314 height = W / -cos_p1 - rn; 315 else 316 height = z / sin_p1 + rn * ( e2 - 1.0 ); 317 if ( hasHeight ) { 318 dstPts[dstOff++] = height; 319 } 320 } 321 srcOff += step; 322 dstOff += step; 323 } 324 } 325 326 /** 327 * Converts geocentric coordinates (x, y, z) to geodetic coordinates 328 * (longitude, latitude, height), according to the current ellipsoid 329 * parameters. The method used here is derived from "An Improved 330 * Algorithm for Geocentric to Geodetic Coordinate Conversion", by 331 * Ralph Toms, Feb 1996. 332 */ 333 protected final void inverseTransform( final float[] srcPts, int srcOff, final float[] dstPts, 334 int dstOff, int numPts ) { 335 int step = 0; 336 final int dimSource = getDimSource(); 337 final boolean hasHeight = ( dimSource >= 3 ); 338 boolean computeHeight = hasHeight; 339 if ( srcPts == dstPts && srcOff < dstOff && srcOff + numPts * dimSource > dstOff ) { 340 step = -dimSource; 341 srcOff -= ( numPts - 1 ) * step; 342 dstOff -= ( numPts - 1 ) * step; 343 } 344 while ( --numPts >= 0 ) { 345 final double x = srcPts[srcOff++]; 346 final double y = srcPts[srcOff++]; 347 final double z = srcPts[srcOff++]; 348 349 // Note: The Java version of 'atan2' work correctly for x==0. 350 // No need for special handling like in the C version. 351 // No special handling neither for latitude. Formulas 352 // below are generic enough, considering that 'atan' 353 // work correctly with infinities (1/0). 354 355 // Note: Variable names follow the notation used in Toms, Feb 1996 356 final double W2 = x * x + y * y; // square of distance from Z axis 357 final double W = Math.sqrt( W2 ); // distance from Z axis 358 final double T0 = z * AD_C; // initial estimate of vertical component 359 final double S0 = Math.sqrt( T0 * T0 + W2 ); // initial estimate of horizontal component 360 final double sin_B0 = T0 / S0; // sin(B0), B0 is estimate of Bowring aux variable 361 final double cos_B0 = W / S0; // cos(B0) 362 final double sin3_B0 = sin_B0 * sin_B0 * sin_B0; // cube of sin(B0) 363 final double T1 = z + b * ep2 * sin3_B0; // corrected estimate of vertical component 364 final double sum = W - a * e2 * ( cos_B0 * cos_B0 * cos_B0 ); // numerator of cos(phi1) 365 final double S1 = Math.sqrt( T1 * T1 + sum * sum ); // corrected estimate of horizontal component 366 final double sin_p1 = T1 / S1; // sin(phi1), phi1 is estimated latitude 367 final double cos_p1 = sum / S1; // cos(phi1) 368 369 final double longitude = Math.toDegrees( Math.atan2( y, x ) ); 370 final double latitude = Math.toDegrees( Math.atan( sin_p1 / cos_p1 ) ); 371 final double height; 372 373 dstPts[dstOff++] = (float) longitude; 374 dstPts[dstOff++] = (float) latitude; 375 if ( computeHeight ) { 376 final double rn = a / Math.sqrt( 1 - e2 * ( sin_p1 * sin_p1 ) ); // Earth radius at location 377 if ( cos_p1 >= +COS_67P5 ) 378 height = W / +cos_p1 - rn; 379 else if ( cos_p1 <= -COS_67P5 ) 380 height = W / -cos_p1 - rn; 381 else 382 height = z / sin_p1 + rn * ( e2 - 1.0 ); 383 if ( hasHeight ) { 384 dstPts[dstOff++] = (float) height; 385 } 386 } 387 srcOff += step; 388 dstOff += step; 389 } 390 } 391 392 /** 393 * Gets the dimension of input points, which is 2 or 3. 394 */ 395 public int getDimSource() { 396 return hasHeight ? 3 : 2; 397 } 398 399 /** 400 * Gets the dimension of output points, which is 3. 401 */ 402 public final int getDimTarget() { 403 return 3; 404 } 405 406 /** 407 * Tests whether this transform does not move any points. 408 * This method returns always <code>false</code>. 409 */ 410 public final boolean isIdentity() { 411 return false; 412 } 413 414 /** 415 * Returns the inverse of this transform. 416 */ 417 public synchronized MathTransform inverse() { 418 if ( inverse == null ) 419 inverse = new Inverse(); 420 return inverse; 421 } 422 423 /** 424 * Returns a hash value for this transform. 425 */ 426 public final int hashCode() { 427 final long code = Double.doubleToLongBits( a ) 428 + 37 429 * ( Double.doubleToLongBits( b ) + 37 * ( Double.doubleToLongBits( a2 ) + 37 * ( Double.doubleToLongBits( b2 ) + 37 * ( Double.doubleToLongBits( e2 ) + 37 * ( Double.doubleToLongBits( ep2 ) ) ) ) ) ); 430 return (int) code ^ (int) ( code >>> 32 ); 431 } 432 433 /** 434 * Compares the specified object with 435 * this math transform for equality. 436 */ 437 public final boolean equals( final Object object ) { 438 if ( object == this ) 439 return true; // Slight optimization 440 if ( super.equals( object ) ) { 441 final GeocentricTransform that = (GeocentricTransform) object; 442 return Double.doubleToLongBits( this.a ) == Double.doubleToLongBits( that.a ) 443 && Double.doubleToLongBits( this.b ) == Double.doubleToLongBits( that.b ) 444 && Double.doubleToLongBits( this.a2 ) == Double.doubleToLongBits( that.a2 ) 445 && Double.doubleToLongBits( this.b2 ) == Double.doubleToLongBits( that.b2 ) 446 && Double.doubleToLongBits( this.e2 ) == Double.doubleToLongBits( that.e2 ) 447 && Double.doubleToLongBits( this.ep2 ) == Double.doubleToLongBits( that.ep2 ); 448 } 449 return false; 450 } 451 452 /** 453 * Returns the WKT for this math transform. 454 */ 455 public final String toString() { 456 return toString( "Ellipsoid_To_Geocentric" ); 457 } 458 459 /** 460 * Returns the WKT for this math transform with the 461 * specified classification name. The classification 462 * name should be "Ellipsoid_To_Geocentric" or 463 * "Geocentric_To_Ellipsoid". 464 */ 465 final String toString( final String classification ) { 466 final StringBuffer buffer = paramMT( classification ); 467 addParameter( buffer, "semi_major", a ); 468 addParameter( buffer, "semi_minor", b ); 469 buffer.append( ']' ); 470 return buffer.toString(); 471 } 472 473 /** 474 * Inverse of a geocentric transform. 475 * 476 * @version 1.0 477 * @author Martin Desruisseaux 478 */ 479 private final class Inverse extends AbstractMathTransform.Inverse { 480 public Inverse() { 481 GeocentricTransform.this.super(); 482 } 483 484 public void transform( final double[] source, final int srcOffset, final double[] dest, 485 final int dstOffset, final int length ) 486 throws TransformException { 487 GeocentricTransform.this.inverseTransform( source, srcOffset, dest, dstOffset, length ); 488 } 489 490 public void transform( final float[] source, final int srcOffset, final float[] dest, 491 final int dstOffset, final int length ) 492 throws TransformException { 493 GeocentricTransform.this.inverseTransform( source, srcOffset, dest, dstOffset, length ); 494 } 495 496 public final String toString() { 497 return GeocentricTransform.this.toString( "Geocentric_To_Ellipsoid" ); 498 } 499 } 500 501 /** 502 * The provider for {@link GeocentricTransform}. 503 * 504 * @version 1.0 505 * @author Martin Desruisseaux 506 */ 507 static final class Provider extends MathTransformProvider { 508 /** 509 * The range of values for the dimension. 510 */ 511 private static final Range DIM_RANGE = new Range( Integer.class, new Integer( 2 ), 512 new Integer( 3 ) ); 513 514 /** 515 * <code>false</code> for the direct transform, 516 * or <code>true</code> for the inverse transform. 517 */ 518 private final boolean inverse; 519 520 /** 521 * Create a provider. 522 * 523 * @param inverse <code>false</code> for the direct transform, 524 * or <code>true</code> for the inverse transform. 525 */ 526 public Provider( final boolean inverse ) { 527 super( inverse ? "Geocentric_To_Ellipsoid" : "Ellipsoid_To_Geocentric", 528 0/*ResourceKeys.GEOCENTRIC_TRANSFORM*/, null ); // TODO 529 put( "semi_major", Double.NaN, POSITIVE_RANGE ); 530 put( "semi_minor", Double.NaN, POSITIVE_RANGE ); 531 putInt( "dim_geoCS", 3, DIM_RANGE ); // Custom parameter: NOT AN OPENGIS SPECIFICATION 532 this.inverse = inverse; 533 } 534 535 /** 536 * Returns a transform for the specified parameters. 537 * 538 * @param parameters The parameter values in standard units. 539 * @return A {@link MathTransform} object of this classification. 540 */ 541 public MathTransform create( final ParameterList parameters ) { 542 final double semiMajor = parameters.getDoubleParameter( "semi_major" ); 543 final double semiMinor = parameters.getDoubleParameter( "semi_minor" ); 544 int dimGeographic = 3; 545 try { 546 dimGeographic = parameters.getIntParameter( "dim_geoCS" ); 547 } catch ( IllegalArgumentException exception ) { 548 // the "dim_geoCS" parameter is a custom one required 549 // by our SEAGIS implementation. It is NOT an OpenGIS 550 // one. We can't require clients to know it. 551 } 552 GeocentricTransform transform = new GeocentricTransform( semiMajor, semiMinor, 553 Unit.METRE, dimGeographic != 2 ); 554 return ( inverse ) ? transform.inverse() : transform; 555 } 556 } 557 }