001 //$HeadURL: http://svn.wald.intevation.org/svn/deegree/base/trunk/src/org/deegree/framework/util/MapUtils.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 package org.deegree.framework.util; 037 038 import static java.lang.Math.sqrt; 039 040 import java.awt.BasicStroke; 041 import java.awt.Color; 042 import java.awt.Dimension; 043 import java.awt.Font; 044 import java.awt.Graphics2D; 045 import java.text.DecimalFormat; 046 047 import org.deegree.framework.log.ILogger; 048 import org.deegree.framework.log.LoggerFactory; 049 import org.deegree.graphics.transformation.GeoTransform; 050 import org.deegree.graphics.transformation.WorldToScreenTransform; 051 import org.deegree.i18n.Messages; 052 import org.deegree.model.crs.CRSFactory; 053 import org.deegree.model.crs.CoordinateSystem; 054 import org.deegree.model.crs.GeoTransformer; 055 import org.deegree.model.spatialschema.Envelope; 056 import org.deegree.model.spatialschema.GeometryFactory; 057 import org.deegree.model.spatialschema.Position; 058 059 /** 060 * 061 * 062 * @version $Revision: 29842 $ 063 * @author <a href="mailto:poth@lat-lon.de">Andreas Poth</a> 064 * @author last edited by: $Author: apoth $ 065 * 066 * @version 1.0. $Revision: 29842 $, $Date: 2011-03-03 10:51:42 +0100 (Thu, 03 Mar 2011) $ 067 * 068 * @since 2.0 069 */ 070 public class MapUtils { 071 072 private static ILogger LOG = LoggerFactory.getLogger( MapUtils.class ); 073 074 /** 075 * The value of sqrt(2) 076 */ 077 public static final double SQRT2 = sqrt( 2 ); 078 079 /** 080 * The Value of a PixelSize 081 */ 082 public static final double DEFAULT_PIXEL_SIZE = 0.00028; 083 084 /** 085 * @param mapWidth 086 * @param mapHeight 087 * @param bbox 088 * @param crs 089 * @return the WMS 1.1.1 scale (size of the diagonal pixel) 090 */ 091 public static double calcScaleWMS111( int mapWidth, int mapHeight, Envelope bbox, CoordinateSystem crs ) { 092 if ( mapWidth == 0 || mapHeight == 0 ) { 093 return 0; 094 } 095 double scale = 0; 096 097 if ( crs == null ) { 098 throw new RuntimeException( "Invalid crs: " + crs ); 099 } 100 101 try { 102 if ( "m".equalsIgnoreCase( crs.getAxisUnits()[0].toString() ) ) { 103 /* 104 * this method to calculate a maps scale as defined in OGC WMS and SLD specification is not required for 105 * maps having a projected reference system. Direct calculation of scale avoids uncertainties 106 */ 107 double dx = bbox.getWidth() / mapWidth; 108 double dy = bbox.getHeight() / mapHeight; 109 scale = sqrt( dx * dx + dy * dy ); 110 } else { 111 112 if ( !crs.getIdentifier().equalsIgnoreCase( "EPSG:4326" ) ) { 113 // transform the bounding box of the request to EPSG:4326 114 GeoTransformer trans = new GeoTransformer( CRSFactory.create( "EPSG:4326" ) ); 115 bbox = trans.transform( bbox, crs ); 116 } 117 double dx = bbox.getWidth() / mapWidth; 118 double dy = bbox.getHeight() / mapHeight; 119 Position min = GeometryFactory.createPosition( bbox.getMin().getX() + dx * ( mapWidth / 2d - 1 ), 120 bbox.getMin().getY() + dy * ( mapHeight / 2d - 1 ) ); 121 Position max = GeometryFactory.createPosition( bbox.getMin().getX() + dx * ( mapWidth / 2d ), 122 bbox.getMin().getY() + dy * ( mapHeight / 2d ) ); 123 124 double distance = calcDistance( min.getX(), min.getY(), max.getX(), max.getY() ); 125 126 scale = distance / SQRT2; 127 128 } 129 } catch ( Exception e ) { 130 LOG.logError( e.getMessage(), e ); 131 throw new RuntimeException( Messages.getMessage( "FRAMEWORK_ERROR_SCALE_CALC", e.getMessage() ) ); 132 } 133 134 return scale; 135 } 136 137 /** 138 * @param mapWidth 139 * @param mapHeight 140 * @param bbox 141 * @param crs 142 * @return the WMS 1.3.0 scale (horizontal size of the pixel, pixel size == 0.28mm) 143 */ 144 public static double calcScaleWMS130( int mapWidth, int mapHeight, Envelope bbox, CoordinateSystem crs ) { 145 if ( mapWidth == 0 || mapHeight == 0 ) { 146 return 0; 147 } 148 149 double scale = 0; 150 151 if ( crs == null ) { 152 throw new RuntimeException( "Invalid crs: " + crs ); 153 } 154 155 try { 156 if ( "m".equalsIgnoreCase( crs.getAxisUnits()[0].toString() ) ) { 157 /* 158 * this method to calculate a maps scale as defined in OGC WMS and SLD specification is not required for 159 * maps having a projected reference system. Direct calculation of scale avoids uncertainties 160 */ 161 double dx = bbox.getWidth() / mapWidth; 162 scale = dx / DEFAULT_PIXEL_SIZE; 163 } else { 164 165 if ( !crs.getIdentifier().equalsIgnoreCase( "EPSG:4326" ) ) { 166 // transform the bounding box of the request to EPSG:4326 167 GeoTransformer trans = new GeoTransformer( CRSFactory.create( "EPSG:4326" ) ); 168 bbox = trans.transform( bbox, crs ); 169 } 170 double dx = bbox.getWidth() / mapWidth; 171 double dy = bbox.getHeight() / mapHeight; 172 173 double minx = bbox.getMin().getX() + dx * ( mapWidth / 2d - 1 ); 174 double miny = bbox.getMin().getY() + dy * ( mapHeight / 2d - 1 ); 175 double maxx = bbox.getMin().getX() + dx * ( mapWidth / 2d ); 176 double maxy = bbox.getMin().getY() + dy * ( mapHeight / 2d - 1 ); 177 178 double distance = calcDistance( minx, miny, maxx, maxy ); 179 180 scale = distance / SQRT2 / DEFAULT_PIXEL_SIZE; 181 182 } 183 } catch ( Exception e ) { 184 LOG.logError( e.getMessage(), e ); 185 throw new RuntimeException( Messages.getMessage( "FRAMEWORK_ERROR_SCALE_CALC", e.getMessage() ) ); 186 } 187 188 return scale; 189 } 190 191 /** 192 * calculates the map scale (denominator) as defined in the OGC SLD 1.0.0 specification 193 * 194 * @param mapWidth 195 * map width in pixel 196 * @param mapHeight 197 * map height in pixel 198 * @param bbox 199 * bounding box of the map 200 * @param crs 201 * coordinate reference system of the map 202 * @param pixelSize 203 * size of one pixel of the map measured in meter 204 * 205 * @return a maps scale based on the diagonal size of a pixel at the center of the map in meter. 206 * @throws RuntimeException 207 */ 208 public static double calcScale( int mapWidth, int mapHeight, Envelope bbox, CoordinateSystem crs, double pixelSize ) 209 throws RuntimeException { 210 211 double sqpxsize; 212 if ( pixelSize == 1d ) { 213 LOG.logDebug( "Calculating WMS 1.1.1 scale." ); 214 return calcScaleWMS111( mapWidth, mapHeight, bbox, crs ); 215 } else if ( pixelSize == DEFAULT_PIXEL_SIZE ) { 216 LOG.logDebug( "Calculating WMS 1.3.0 scale." ); 217 return calcScaleWMS130( mapWidth, mapHeight, bbox, crs ); 218 } else { 219 sqpxsize = pixelSize * pixelSize; 220 sqpxsize += sqpxsize; 221 sqpxsize = sqrt( sqpxsize ); 222 } 223 224 if ( mapWidth == 0 || mapHeight == 0 ) { 225 return 0; 226 } 227 228 double scale = 0; 229 230 CoordinateSystem cs = crs; 231 232 if ( cs == null ) { 233 throw new RuntimeException( "Invalid crs: " + crs ); 234 } 235 236 try { 237 if ( "m".equalsIgnoreCase( cs.getAxisUnits()[0].toString() ) ) { 238 /* 239 * this method to calculate a maps scale as defined in OGC WMS and SLD specification is not required for 240 * maps having a projected reference system. Direct calculation of scale avoids uncertainties 241 */ 242 double dx = bbox.getWidth() / mapWidth; 243 double dy = bbox.getHeight() / mapHeight; 244 scale = Math.sqrt( dx * dx + dy * dy ) / sqpxsize; 245 } else { 246 247 if ( !crs.getIdentifier().equalsIgnoreCase( "EPSG:4326" ) ) { 248 // transform the bounding box of the request to EPSG:4326 249 GeoTransformer trans = new GeoTransformer( CRSFactory.create( "EPSG:4326" ) ); 250 bbox = trans.transform( bbox, crs ); 251 } 252 double dx = bbox.getWidth() / mapWidth; 253 double dy = bbox.getHeight() / mapHeight; 254 Position min = GeometryFactory.createPosition( bbox.getMin().getX() + dx * ( mapWidth / 2d - 1 ), 255 bbox.getMin().getY() + dy * ( mapHeight / 2d - 1 ) ); 256 Position max = GeometryFactory.createPosition( bbox.getMin().getX() + dx * ( mapWidth / 2d ), 257 bbox.getMin().getY() + dy * ( mapHeight / 2d ) ); 258 259 double distance = calcDistance( min.getX(), min.getY(), max.getX(), max.getY() ); 260 261 scale = distance / sqpxsize; 262 263 } 264 } catch ( Exception e ) { 265 LOG.logError( e.getMessage(), e ); 266 throw new RuntimeException( Messages.getMessage( "FRAMEWORK_ERROR_SCALE_CALC", e.getMessage() ) ); 267 } 268 269 return scale; 270 271 } 272 273 /** 274 * calculates the distance in meters between two points in EPSG:4326 coodinates. this is a convenience method 275 * assuming the world is a ball 276 * 277 * @param lon1 278 * @param lat1 279 * @param lon2 280 * @param lat2 281 * @return the distance in meters between two points in EPSG:4326 coords 282 */ 283 public static double calcDistance( double lon1, double lat1, double lon2, double lat2 ) { 284 double r = 6378.137; 285 double rad = Math.PI / 180d; 286 double cose = Math.sin( rad * lon1 ) * Math.sin( rad * lon2 ) + Math.cos( rad * lon1 ) * Math.cos( rad * lon2 ) 287 * Math.cos( rad * ( lat1 - lat2 ) ); 288 double dist = r * Math.acos( cose ) * Math.cos( rad * Math.min( lat1, lat2 ) ); 289 290 // * 0.835 is just an heuristic correction factor 291 return dist * 1000 * 0.835; 292 } 293 294 /** 295 * The method calculates a new Envelope from the <code>requestedBarValue</code> It will either zoom in or zoom out 296 * of the <code>actualBBOX<code> depending 297 * on the ratio of the <code>requestedBarValue</code> to the <code>actualBarValue</code> 298 * 299 * @param currentEnvelope 300 * current Envelope 301 * @param currentScale 302 * the scale of the current envelope 303 * @param requestedScale 304 * requested scale value 305 * @return a new Envelope 306 */ 307 public static Envelope scaleEnvelope( Envelope currentEnvelope, double currentScale, double requestedScale ) { 308 309 double ratio = requestedScale / currentScale; 310 double newWidth = currentEnvelope.getWidth() * ratio; 311 double newHeight = currentEnvelope.getHeight() * ratio; 312 double midX = currentEnvelope.getMin().getX() + ( currentEnvelope.getWidth() / 2d ); 313 double midY = currentEnvelope.getMin().getY() + ( currentEnvelope.getHeight() / 2d ); 314 315 double minx = midX - newWidth / 2d; 316 double maxx = midX + newWidth / 2d; 317 double miny = midY - newHeight / 2d; 318 double maxy = midY + newHeight / 2d; 319 320 return GeometryFactory.createEnvelope( minx, miny, maxx, maxy, currentEnvelope.getCoordinateSystem() ); 321 322 } 323 324 /** 325 * This method ensures the bbox is resized (shrunk) to match the aspect ratio defined by mapHeight/mapWidth 326 * 327 * @param bbox 328 * @param mapWith 329 * @param mapHeight 330 * @return a new bounding box with the aspect ratio given my mapHeight/mapWidth 331 */ 332 public static final Envelope ensureAspectRatio( Envelope bbox, double mapWith, double mapHeight ) { 333 334 double minx = bbox.getMin().getX(); 335 double miny = bbox.getMin().getY(); 336 double maxx = bbox.getMax().getX(); 337 double maxy = bbox.getMax().getY(); 338 339 double dx = maxx - minx; 340 double dy = maxy - miny; 341 342 double ratio = mapHeight / mapWith; 343 344 if ( dx >= dy ) { 345 // height has to be corrected 346 double[] normCoords = getNormalizedCoords( dx, ratio, miny, maxy ); 347 miny = normCoords[0]; 348 maxy = normCoords[1]; 349 } else { 350 // width has to be corrected 351 ratio = mapWith / mapHeight; 352 double[] normCoords = getNormalizedCoords( dy, ratio, minx, maxx ); 353 minx = normCoords[0]; 354 maxx = normCoords[1]; 355 } 356 CoordinateSystem crs = bbox.getCoordinateSystem(); 357 358 return GeometryFactory.createEnvelope( minx, miny, maxx, maxy, crs ); 359 } 360 361 private static final double[] getNormalizedCoords( double normLen, double ratio, double min, double max ) { 362 double mid = ( max - min ) / 2 + min; 363 min = mid - ( normLen / 2 ) * ratio; 364 max = mid + ( normLen / 2 ) * ratio; 365 double[] newCoords = { min, max }; 366 return newCoords; 367 } 368 369 /** 370 * 371 * @param img 372 * @param bbox 373 * @param mapSize 374 * @param fontName 375 * @param fontSize 376 */ 377 public static void drawScalbar( Graphics2D g, int desiredSize, Envelope bbox, Dimension mapSize, String fontName, 378 int fontSize ) { 379 380 desiredSize -= 30; 381 GeoTransform gt = new WorldToScreenTransform( bbox.getMin().getX(), bbox.getMin().getY(), bbox.getMax().getX(), 382 bbox.getMax().getY(), 0, 0, mapSize.getWidth() - 1, 383 mapSize.getHeight() - 1 ); 384 385 // calculate scale bar max scale and size 386 int length = 0; 387 double lx = gt.getDestX( bbox.getMin().getX() ); 388 double scale = 0; 389 for ( int i = 0; i < 100; i++ ) { 390 double k = 0; 391 double dec = 30 * Math.pow( 10, i ); 392 for ( int j = 0; j < 9; j++ ) { 393 k += dec; 394 double tx = gt.getDestX( bbox.getMin().getX() + k ); 395 if ( Math.abs( tx - lx ) < desiredSize ) { 396 length = (int) Math.round( Math.abs( tx - lx ) ); 397 scale = k; 398 } else { 399 break; 400 } 401 } 402 } 403 404 // draw scale bar base line 405 g.setStroke( new BasicStroke( ( desiredSize + 30 ) / 250 ) ); 406 g.setColor( Color.black ); 407 g.drawLine( 10, 30, length + 10, 30 ); 408 double dx = length / 3d; 409 double vdx = scale / 3; 410 double div = 1; 411 String uom = "m"; 412 if ( scale > 1000 ) { 413 div = 1000; 414 uom = "km"; 415 } 416 // draw scale bar scales 417 if ( fontName == null ) { 418 fontName = "SANS SERIF"; 419 } 420 g.setFont( new Font( fontName, Font.PLAIN, fontSize ) ); 421 DecimalFormat df = new DecimalFormat( "##.# " + uom ); 422 for ( int i = 0; i < 4; i++ ) { 423 double val = ( vdx * i ) / div; 424 g.drawString( df.format( val ), (int) Math.round( 10 + i * dx ) - 8, 10 ); 425 g.drawLine( (int) Math.round( 10 + i * dx ), 30, (int) Math.round( 10 + i * dx ), 20 ); 426 } 427 for ( int i = 0; i < 7; i++ ) { 428 g.drawLine( (int) Math.round( 10 + i * dx / 2d ), 30, (int) Math.round( 10 + i * dx / 2d ), 25 ); 429 } 430 431 g.dispose(); 432 433 } 434 435 }