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    }