001 //$HeadURL: svn+ssh://jwilden@svn.wald.intevation.org/deegree/base/branches/2.5_testing/src/org/deegree/crs/projections/cylindric/TransverseMercator.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.cylindric; 038 039 import static org.deegree.crs.projections.ProjectionUtils.EPS10; 040 import static org.deegree.crs.projections.ProjectionUtils.HALFPI; 041 import static org.deegree.crs.projections.ProjectionUtils.acosScaled; 042 import static org.deegree.crs.projections.ProjectionUtils.asinScaled; 043 import static org.deegree.crs.projections.ProjectionUtils.calcPhiFromMeridianDistance; 044 import static org.deegree.crs.projections.ProjectionUtils.getDistanceAlongMeridian; 045 import static org.deegree.crs.projections.ProjectionUtils.getRectifiyingLatitudeValues; 046 import static org.deegree.crs.projections.ProjectionUtils.normalizeLatitude; 047 import static org.deegree.crs.projections.ProjectionUtils.normalizeLongitude; 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 import org.deegree.framework.log.ILogger; 056 import org.deegree.framework.log.LoggerFactory; 057 058 /** 059 * The <code>TransverseMercator</code> projection has following properties: 060 * <ul> 061 * <li>Cylindrical (transverse)</li> 062 * <li>Conformal</li> 063 * <li>The central meridian, each meridian 90° from central meridian and the equator are straight lines</li> 064 * <li>All other meridians and parallels are complex curves</li> 065 * <li>Scale is true along central meridian or along two straight lines equidistant from and parallel to central 066 * merdian. (These lines are only approximately straight for the ellipsoid)</li> 067 * <li>Scale becomes infinite on sphere 90° from central meridian</li> 068 * <li>Used extensively for quadrangle maps at scales from 1:24.000 to 1:250.000</li> 069 * <li>Often used to show regions with greater north-south extent</li> 070 * <li>presented by lambert in 1772</li> 071 * </ul> 072 * 073 * @author <a href="mailto:bezema@lat-lon.de">Rutger Bezema</a> 074 * 075 * @author last edited by: $Author: mschneider $ 076 * 077 * @version $Revision: 18195 $, $Date: 2009-06-18 17:55:39 +0200 (Do, 18 Jun 2009) $ 078 * 079 */ 080 081 public class TransverseMercator extends CylindricalProjection { 082 083 private static final long serialVersionUID = -8648170171776619756L; 084 085 private static ILogger LOG = LoggerFactory.getLogger( TransverseMercator.class ); 086 087 /** 088 * Constants used for the forward and inverse transform for the elliptical case of the Transverse Mercator. 089 */ 090 private static final double FC1 = 1., // 1/1 091 FC2 = 0.5, // 1/2 092 FC3 = 0.16666666666666666666666, // 1/6 093 FC4 = 0.08333333333333333333333, // 1/12 094 FC5 = 0.05, // 1/20 095 FC6 = 0.03333333333333333333333, // 1/30 096 FC7 = 0.02380952380952380952380, // 1/42 097 FC8 = 0.01785714285714285714285; // 1/56 098 099 // 1 for northern hemisphere -1 for southern. 100 private int hemisphere; 101 102 // esp will can hold two values, for the sphere it will hold the scale, for the ellipsoid Snyder (p.61 8-12). 103 private double esp; 104 105 private double ml0; 106 107 private double[] en; 108 109 /** 110 * @param northernHemisphere 111 * true if on the northern hemisphere false otherwise. 112 * @param geographicCRS 113 * @param falseNorthing 114 * @param falseEasting 115 * @param naturalOrigin 116 * @param units 117 * @param scale 118 * @param id 119 * an identifiable instance containing information about this projection 120 */ 121 public TransverseMercator( boolean northernHemisphere, GeographicCRS geographicCRS, double falseNorthing, 122 double falseEasting, Point2d naturalOrigin, Unit units, double scale, Identifiable id ) { 123 super( geographicCRS, falseNorthing, falseEasting, naturalOrigin, units, scale, true,// always conformal 124 false/* not equalArea */, id ); 125 this.hemisphere = ( northernHemisphere ) ? 1 : -1; 126 if ( isSpherical() ) { 127 esp = getScale(); 128 ml0 = .5 * esp; 129 } else { 130 en = getRectifiyingLatitudeValues( getSquaredEccentricity() ); 131 ml0 = getDistanceAlongMeridian( getProjectionLatitude(), getSinphi0(), getCosphi0(), en ); 132 esp = getSquaredEccentricity() / ( 1. - getSquaredEccentricity() ); 133 // esp = ( ( getSemiMajorAxis() * getSemiMajorAxis() ) / ( getSemiMinorAxis() * getSemiMinorAxis() ) ) - 134 // 1.0; 135 } 136 137 } 138 139 /** 140 * Sets the id to EPSG:9807 141 * 142 * @param northernHemisphere 143 * true if on the northern hemisphere false otherwise. 144 * @param geographicCRS 145 * @param falseNorthing 146 * @param falseEasting 147 * @param naturalOrigin 148 * @param units 149 * @param scale 150 */ 151 public TransverseMercator( boolean northernHemisphere, GeographicCRS geographicCRS, double falseNorthing, 152 double falseEasting, Point2d naturalOrigin, Unit units, double scale ) { 153 this( northernHemisphere, geographicCRS, falseNorthing, falseEasting, naturalOrigin, units, scale, 154 new Identifiable( "EPSG::9807" ) ); 155 } 156 157 /** 158 * Sets the false-easting to 50000, false-northing to 0 or 10000000 (depending on the hemisphere), the 159 * projection-longitude is calculated from the zone and the projection-latitude is set to 0. The scale will be 160 * 0.9996. 161 * 162 * @param zone 163 * to add 164 * @param northernHemisphere 165 * true if the projection is on the northern hemisphere 166 * @param geographicCRS 167 * @param units 168 * @param id 169 * an identifiable instance containing information about this projection 170 */ 171 public TransverseMercator( int zone, boolean northernHemisphere, GeographicCRS geographicCRS, Unit units, 172 Identifiable id ) { 173 super( geographicCRS, ( northernHemisphere ? 0 : 10000000 ), 500000, new Point2d( ( --zone + .5 ) * Math.PI 174 / 30. - Math.PI, 0 ), units, 175 0.9996, true /* always conformal */, false /* not equalArea */, id ); 176 this.hemisphere = ( northernHemisphere ) ? 1 : -1; 177 if ( isSpherical() ) { 178 esp = getScale(); 179 ml0 = .5 * esp; 180 } else { 181 // recalculate the rectifying latitudes and the distance along the meridian. 182 en = getRectifiyingLatitudeValues( getSquaredEccentricity() ); 183 ml0 = getDistanceAlongMeridian( getProjectionLatitude(), getSinphi0(), getCosphi0(), en ); 184 esp = getSquaredEccentricity() / ( 1. - getSquaredEccentricity() ); 185 } 186 187 } 188 189 /** 190 * Sets the false-easting to 50000, false-northing to 0 or 10000000 (depending on the hemisphere), the 191 * projection-longitude is calculated from the zone and the projection-latitude is set to 0. The scale will be 192 * 0.9996. 193 * 194 * @param zone 195 * to add 196 * @param northernHemisphere 197 * true if the projection is on the northern hemisphere 198 * @param geographicCRS 199 * @param units 200 */ 201 public TransverseMercator( int zone, boolean northernHemisphere, GeographicCRS geographicCRS, Unit units ) { 202 this( zone, northernHemisphere, geographicCRS, units, new Identifiable( "EPSG::9807" ) ); 203 } 204 205 /** 206 * A northern hemisphere conformal transverse mercator projection with a scale of one. Using the given datum. 207 * 208 * @param geographicCRS 209 * @param falseNorthing 210 * @param falseEasting 211 * @param naturalOrigin 212 * @param units 213 */ 214 public TransverseMercator( GeographicCRS geographicCRS, double falseNorthing, double falseEasting, 215 Point2d naturalOrigin, Unit units ) { 216 this( true, geographicCRS, falseNorthing, falseEasting, naturalOrigin, units, 1. ); 217 } 218 219 /** 220 * A northern hemisphere conformal transverse mercator projection with a scale of one. Using the given datum. 221 * 222 * @param geographicCRS 223 * @param falseNorthing 224 * @param falseEasting 225 * @param naturalOrigin 226 * @param units 227 * @param id 228 * an identifiable instance containing information about this projection 229 */ 230 public TransverseMercator( GeographicCRS geographicCRS, double falseNorthing, double falseEasting, 231 Point2d naturalOrigin, Unit units, Identifiable id ) { 232 this( true, geographicCRS, falseNorthing, falseEasting, naturalOrigin, units, 1., id ); 233 } 234 235 @Override 236 public Point2d doInverseProjection( double x, double y ) 237 throws ProjectionException { 238 Point2d result = new Point2d( 0, 0 ); 239 LOG.logDebug( "InverseProjection, incoming points x: " + x + " y: " + y ); 240 x = ( x - getFalseEasting() ) / getScaleFactor(); 241 y = ( y - getFalseNorthing() ) / getScaleFactor(); 242 y *= hemisphere; 243 244 if ( isSpherical() ) { 245 // h holds e^x, the sinh = 0.5*(e^x - e^-x), cosh = 0.5(e^x + e^-x) 246 double h = Math.exp( x / getScaleFactor() ); 247 // sinh holds the sinh from Snyder (p.60 8-7) 248 double sinh = .5 * ( h - 1. / h ); 249 250 // Snyder (p.60 8-8) 251 // reuse variable 252 double cosD = Math.cos( getProjectionLatitude() + ( y/* / getScale() */) ); 253 /** 254 * To calc phi from Snyder (p.60 8-6), use following trick! sin^2(D) + cos^2(D) = 1 => sin(D) = sqrt( 1- 255 * cos^2(D) ) and cosh^2(x) - sin^2(x) = 1 => cosh(x) = sqrt( 1+sin^2(x) ) 256 */ 257 result.y = asinScaled( Math.sqrt( ( 1. - cosD * cosD ) / ( 1. + sinh * sinh ) ) ); 258 // if ( y < 0 ) {// southern hemisphere 259 // out.y = -out.y; 260 // } 261 result.x = Math.atan2( sinh, cosD ); 262 } else { 263 // out.y will hold the phi_1 from Snyder (p.63 8-18). 264 result.y = calcPhiFromMeridianDistance( ml0 + ( y/* / getScale() */), getSquaredEccentricity(), en ); 265 // result.y = calcPhiFromMeridianDistance( ml0 + ( y / getScale() ), 266 // getSquaredEccentricity(), 267 // en ); 268 if ( Math.abs( result.y ) >= HALFPI ) { 269 result.y = y < 0. ? -HALFPI : HALFPI; 270 result.x = 0; 271 } else { 272 273 double sinphi = Math.sin( result.y ); 274 double cosphi = Math.cos( result.y ); 275 // largeT Will hold the tan^2(phi) Snyder (p.64 8-22). 276 double largeT = ( Math.abs( cosphi ) > EPS10 ) ? sinphi / cosphi : 0; 277 278 // will hold the C_1 from Synder (p.64 8-21) 279 double largeC = esp * cosphi * cosphi; 280 281 // Holds a modified N from Synder (p.64 8-23), multiplied with the largeT, it is the first term fo the 282 // calculation of phi e.g. N*T/R 283 double con = 1. - ( getSquaredEccentricity() * sinphi * sinphi ); 284 // largeD holds the D from Snyder (p.64 8-25). (x/(1/N) = x*N) 285 // double largeD = x * Math.sqrt( con ) / getScaleFactor(); 286 double largeD = x * Math.sqrt( con )/* / getScale() */; 287 con *= largeT; 288 largeT *= largeT; 289 double ds = largeD * largeD; 290 291 /** 292 * As for the forward projection, I'm not sure if this is correct, this should be checked! 293 */ 294 result.y -= ( con * ds / ( 1. - getSquaredEccentricity() ) ) 295 * FC2 296 * ( 1. - ds 297 * FC4 298 * ( 5. + largeT * ( 3. - 9. * largeC ) + largeC * ( 1. - 4 * largeC ) - ds 299 * FC6 300 * ( 61. 301 + largeT 302 * ( 90. - 252. * largeC + 45. * largeT ) 303 + 46. 304 * largeC - ds 305 * FC8 306 * ( 1385. + largeT 307 * ( 3633. + largeT 308 * ( 4095. + 1574. * largeT ) ) ) ) ) ); 309 result.x = largeD 310 * ( FC1 - ds 311 * FC3 312 * ( 1. + 2. * largeT + largeC - ds 313 * FC5 314 * ( 5. + largeT 315 * ( 28. + 24. * largeT + 8. * largeC ) + 6. 316 * largeC - ds 317 * FC7 318 * ( 61. + largeT 319 * ( 662. + largeT 320 * ( 1320. + 720. * largeT ) ) ) ) ) ) 321 / cosphi; 322 } 323 } 324 // result.y += getProjectionLatitude(); 325 result.x += getProjectionLongitude(); 326 327 return result; 328 } 329 330 /* 331 * (non-Javadoc) 332 * 333 * @see org.deegree.crs.projections.Projection#doProjection(double, double) 334 */ 335 @Override 336 public Point2d doProjection( double lambda, double phi ) 337 throws ProjectionException { 338 // LOG.logDebug( "Projection, incoming points lambda: " + lambda + " phi: " + phi ); 339 LOG.logDebug( "Projection, incoming points lambda: " + Math.toDegrees( lambda ) + " phi: " 340 + Math.toDegrees( phi ) ); 341 Point2d result = new Point2d( 0, 0 ); 342 lambda -= getProjectionLongitude(); 343 // phi -= getProjectionLatitude(); 344 345 phi *= hemisphere; 346 double cosphi = Math.cos( phi ); 347 if ( isSpherical() ) { 348 double b = cosphi * Math.sin( lambda ); 349 350 // Snyder (p.58 8-1) 351 result.x = ml0 * getScaleFactor() * Math.log( ( 1. + b ) / ( 1. - b ) ); 352 353 // reformed and inserted the k from (p.58 8-4), so no tangens has to be calculated. 354 double ty = cosphi * Math.cos( lambda ) / Math.sqrt( 1. - b * b ); 355 ty = acosScaled( ty ); 356 if ( phi < 0.0 ) { 357 ty = -ty; 358 } 359 // esp just holds the scale 360 result.y = esp * ( ty - getProjectionLatitude() ); 361 } else { 362 double sinphi = Math.sin( phi ); 363 double largeT = ( Math.abs( cosphi ) > EPS10 ) ? sinphi / cosphi : 0.0; 364 // largeT holds Snyder (p.61 8-13). 365 largeT *= largeT; 366 double largeA = cosphi * lambda; 367 double squaredLargeA = largeA * largeA; 368 // largeA now holds A/N Snyder (p.61 4-20 and 8-15) 369 largeA /= Math.sqrt( 1. - ( getSquaredEccentricity() * sinphi * sinphi ) ); 370 371 // largeA *= getSemiMajorAxis(); 372 373 // largeC will hold Snyder (p.61 8-14), esp holds Snyder (p.61 8-12). 374 double largeC = esp * cosphi * cosphi; 375 double largeM = getDistanceAlongMeridian( phi, sinphi, cosphi, en ); 376 377 result.x = largeA 378 * ( FC1 + FC3 379 * squaredLargeA 380 * ( 1. - largeT + largeC + FC5 381 * squaredLargeA 382 * ( 5. + largeT * ( largeT - 18. ) + largeC 383 * ( 14. - 58. * largeT ) + FC7 384 * squaredLargeA 385 * ( 61. + largeT 386 * ( largeT 387 * ( 179. - largeT ) - 479. ) ) ) ) ); 388 389 result.y = ( largeM - ml0 ) 390 + sinphi 391 * largeA 392 * lambda 393 * FC2 394 * ( 1. + FC4 395 * squaredLargeA 396 * ( 5. - largeT + largeC * ( 9. + 4. * largeC ) + FC6 397 * squaredLargeA 398 * ( 61. + largeT * ( largeT - 58. ) 399 + largeC * ( 270. - 330 * largeT ) + FC8 400 * squaredLargeA 401 * ( 1385. + largeT 402 * ( largeT 403 * ( 543. - largeT ) - 3111. ) ) ) ) ); 404 405 } 406 407 result.x = ( result.x * getScaleFactor() ) + getFalseEasting(); 408 result.y = ( result.y * getScaleFactor() ) + getFalseNorthing(); 409 410 return result; 411 } 412 413 /** 414 * @param latitude 415 * to get the nearest paralles to. 416 * @return the nearest parallel in radians of given latitude 417 */ 418 public int getRowFromNearestParallel( double latitude ) { 419 int degrees = (int) Math.round( Math.toDegrees( normalizeLatitude( latitude ) ) ); 420 if ( degrees < -80 || degrees > 84 ) { 421 return 0; 422 } 423 if ( degrees > 80 ) { 424 return 24; // last parallel 425 } 426 return ( ( degrees + 80 ) / 8 ) + 3; 427 } 428 429 /** 430 * the utm zone from a given meridian 431 * 432 * @param longitude 433 * in radians 434 * @return the utm zone. 435 */ 436 public int getZoneFromNearestMeridian( double longitude ) { 437 int zone = (int) Math.floor( ( normalizeLongitude( longitude ) + Math.PI ) * 30.0 / Math.PI ) + 1; 438 if ( zone < 1 ) { 439 zone = 1; 440 } else if ( zone > 60 ) { 441 zone = 60; 442 } 443 return zone; 444 } 445 446 @Override 447 public String getImplementationName() { 448 return "transverseMercator"; 449 } 450 451 /** 452 * @return the true if defined on the northern hemisphere. 453 */ 454 public final boolean getHemisphere() { 455 return ( hemisphere == 1 ); 456 } 457 }