001    //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/branches/2.3_testing/src/org/deegree/ogcwebservices/wms/dataaccess/OSMSlippyMapReader.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.ogcwebservices.wms.dataaccess;
038    
039    import java.awt.Graphics;
040    import java.awt.image.BufferedImage;
041    import java.awt.image.renderable.ParameterBlock;
042    import java.io.IOException;
043    import java.io.InputStream;
044    import java.net.URL;
045    import java.util.ArrayList;
046    import java.util.Iterator;
047    import java.util.LinkedHashMap;
048    import java.util.List;
049    import java.util.Map;
050    import java.util.Properties;
051    
052    import javax.media.jai.Interpolation;
053    import javax.media.jai.InterpolationBilinear;
054    import javax.media.jai.JAI;
055    import javax.media.jai.RenderedOp;
056    
057    import org.deegree.framework.log.ILogger;
058    import org.deegree.framework.log.LoggerFactory;
059    import org.deegree.framework.util.ImageUtils;
060    import org.deegree.framework.util.MapUtils;
061    import org.deegree.framework.xml.XMLFragment;
062    import org.deegree.model.crs.CRSFactory;
063    import org.deegree.model.crs.CoordinateSystem;
064    import org.deegree.model.crs.GeoTransformer;
065    import org.deegree.model.crs.UnknownCRSException;
066    import org.deegree.model.spatialschema.Envelope;
067    import org.deegree.model.spatialschema.GeometryFactory;
068    import org.deegree.model.spatialschema.Point;
069    import org.deegree.ogcwebservices.wms.operation.GetFeatureInfo;
070    import org.deegree.ogcwebservices.wms.operation.GetFeatureInfoResult;
071    import org.deegree.ogcwebservices.wms.operation.GetLegendGraphic;
072    import org.deegree.ogcwebservices.wms.operation.GetMap;
073    import org.deegree.ogcwebservices.wms.operation.GetMapResult;
074    
075    /**
076     * TODO add class documentation here.
077     *
078     * @author <a href="mailto:poth@lat-lon.de">Andreas Poth</a>
079     * @author last edited by: $Author: awietschke $
080     *
081     * @version $Revision: 18845 $, $Date: 2009-07-31 16:43:08 +0200 (Fr, 31. Jul 2009) $
082     */
083    public class OSMSlippyMapReader implements ExternalRasterDataAccess {
084    
085        private static final ILogger LOG = LoggerFactory.getLogger( OSMSlippyMapReader.class );
086    
087        private static int tileSize = 256;
088    
089        private static CoordinateSystem crsWGS84;
090    
091        private static CoordinateSystem crsOSM;
092    
093        private static GeoTransformer gt2OSM;
094    
095        private static GeoTransformer gt2WGS84;
096    
097        private int width;
098    
099        private int height;
100    
101        private Envelope envelope;
102    
103        private BufferedImage target;
104    
105        private Envelope targetEnv;
106    
107        private Properties props;
108    
109        private BufferedImage legend;
110    
111        private int minLevel = 0;
112    
113        private int maxLevel = 18;
114    
115        private String extension = ".png";
116    
117        // String base = "http://andy.sandbox.cloudmade.com/tiles/cycle/";
118        // String base = "http://tile.openstreetmap.org/";
119        // String base = "http://andy.sandbox.cloudmade.com/tiles/cycle/";
120    
121        static Map<double[], Integer> levels;
122        static {
123            if ( levels == null ) {
124                levels = new LinkedHashMap<double[], Integer>( 20 );
125                double min = 0;
126                double max = 0.844517829;
127                for ( int i = 0; i < 19; i++ ) {
128                    levels.put( new double[] { min, max }, 18 - i );
129                    min = max;
130                    max *= 2d;
131                }
132                try {
133                    crsWGS84 = CRSFactory.create( "EPSG:4326" );
134                    crsOSM = CRSFactory.create( "OSM_SLIPPY_MAP" );
135                    gt2OSM = new GeoTransformer( crsOSM );
136                    gt2WGS84 = new GeoTransformer( crsWGS84 );
137                } catch ( UnknownCRSException e ) {
138                    // TODO Auto-generated catch block
139                    e.printStackTrace();
140                }
141            }
142        }
143    
144        /**
145         * default initialization of configuration
146         */
147        private void init() {
148            if ( props == null ) {
149                props = new Properties();
150                props.put( "TILEROOT", "http://tile.openstreetmap.org/" );
151                props.put( "LEGEND", props.getProperty( "TILEROOT" ) + "14/8514/5504.png" );
152            }
153        }
154    
155        /**
156         *
157         * @param lat
158         * @param lon
159         * @param zoom
160         * @return
161         */
162        private int[] getTileNumber( final double lon, final double lat, final int zoom ) {
163            int xtile = (int) Math.floor( ( lon + 180 ) / 360 * ( 1 << zoom ) );
164            int ytile = (int) Math.floor( ( 1 - Math.log( Math.tan( lat * Math.PI / 180 ) + 1
165                                                          / Math.cos( lat * Math.PI / 180 ) )
166                                                / Math.PI )
167                                          / 2 * ( 1 << zoom ) );
168    
169            return new int[] { xtile, ytile };
170        }
171    
172        private void readSlippyMaps()
173                                throws Exception {
174    
175            double scale = MapUtils.calcScale( width, height, envelope, crsWGS84, 1 );
176            int level = calculateLevel( scale );
177    
178            List<List<String>> tiles = new ArrayList<List<String>>( 5 );
179    
180            double cx = envelope.getMin().getX();
181            double cy = envelope.getMin().getY();
182            double[] bb = new double[] { 0, 0, 0, 0 };
183            int[] idx = getTileNumber( cx, cy, level );
184            int xx = idx[0];
185            int yy = idx[1];
186            double minx = 9E99;
187            double miny = 9E99;
188            double maxx = -9E99;
189            double maxy = -9E99;
190            while ( cy < envelope.getMax().getY() + ( bb[3] - bb[1] ) ) {
191                List<String> row = new ArrayList<String>( 10 );
192                while ( cx < envelope.getMax().getX() + ( bb[2] - bb[0] ) ) {
193                    bb = getTileBBox( xx, yy, level );
194                    String s = props.getProperty( "TILEROOT" ) + level + "/" + xx + "/" + yy + extension;
195                    xx++;
196                    row.add( s );
197                    if ( bb[0] < minx ) {
198                        minx = bb[0];
199                    }
200                    if ( bb[1] < miny ) {
201                        miny = bb[1];
202                    }
203                    if ( bb[2] > maxx ) {
204                        maxx = bb[2];
205                    }
206                    if ( bb[3] > maxy ) {
207                        maxy = bb[3];
208                    }
209                    cx = cx + ( bb[2] - bb[0] );
210                }
211                cx = envelope.getMin().getX();
212                cy = cy + ( bb[3] - bb[1] );
213                tiles.add( row );
214                xx = idx[0];
215                yy--;
216            }
217    
218            target = new BufferedImage( tiles.get( 0 ).size() * tileSize, tiles.size() * tileSize,
219                                        BufferedImage.TYPE_INT_RGB );
220            Graphics g = target.getGraphics();
221    
222            OSMReader[] threads = new OSMReader[tiles.size()];
223            for ( int i = 0; i < tiles.size(); i++ ) {
224                List<String> row = tiles.get( i );
225                threads[i] = new OSMReader( row, i, g );
226                threads[i].start();
227            }
228            while ( !isFinished( threads ) ) {
229                Thread.sleep( 50 );
230            }
231            g.dispose();
232    
233            // calc result resolution in x- and y-direction ...
234            Point minP = (Point) gt2OSM.transform( GeometryFactory.createPoint( minx, miny, crsWGS84 ) );
235            Point maxP = (Point) gt2OSM.transform( GeometryFactory.createPoint( maxx, maxy, crsWGS84 ) );
236            double resx = ( maxP.getX() - minP.getX() ) / target.getWidth();
237            double resy = ( maxP.getY() - minP.getY() ) / target.getHeight();
238            // ... and rescale image to have same resolutions in both directions because otherwise
239            // deegree and other clients will not be able to render it correctly
240            if ( resx != resy ) {
241                target = scale( target, resx, resy );
242            }
243            targetEnv = GeometryFactory.createEnvelope( minP.getPosition(), maxP.getPosition(), crsOSM );
244        }
245    
246        /**
247         * @param threads
248         * @return
249         */
250        private boolean isFinished( OSMReader[] threads ) {
251            for ( OSMReader reader : threads ) {
252                if ( !reader.isFinished() ) {
253                    return false;
254                }
255            }
256            return true;
257        }
258    
259        /**
260         * @param target
261         * @param resx
262         * @param resy
263         * @return
264         */
265        private BufferedImage scale( BufferedImage target, double resx, double resy ) {
266            Interpolation interpolation = new InterpolationBilinear();
267            double scaleX = 1;
268            double scaleY = 1;
269            if ( resx < resy ) {
270                scaleY = resy / resx;
271            } else {
272                scaleX = resx / resy;
273            }
274            LOG.logDebug( "Scale image: by factors: " + scaleX + ' ' + scaleY );
275            ParameterBlock pb = new ParameterBlock();
276            pb.addSource( target );
277            pb.add( (float) scaleX ); // The xScale
278            pb.add( (float) scaleY ); // The yScale
279            pb.add( 0.0F ); // The x translation
280            pb.add( 0.0F ); // The y translation
281            pb.add( interpolation ); // The interpolation
282            // Create the scale operation
283            RenderedOp ro = JAI.create( "scale", pb, null );
284            try {
285                target = ro.getAsBufferedImage();
286            } catch ( Exception e ) {
287                e.printStackTrace();
288            }
289            return target;
290        }
291    
292        private double[] getTileBBox( final int x, final int y, final int zoom ) {
293            double north = tile2lat( y, zoom );
294            double south = tile2lat( y + 1, zoom );
295            double west = tile2lon( x, zoom );
296            double east = tile2lon( x + 1, zoom );
297            return new double[] { west, south, east, north };
298        }
299    
300        private static double tile2lon( int x, int z ) {
301            return ( x / Math.pow( 2.0, z ) * 360.0 ) - 180;
302        }
303    
304        private static double tile2lat( int y, int z ) {
305            double n = Math.PI - ( ( 2.0 * Math.PI * y ) / Math.pow( 2.0, z ) );
306            return 180.0 / Math.PI * Math.atan( 0.5 * ( Math.exp( n ) - Math.exp( -n ) ) );
307        }
308    
309        /**
310         * @param scale
311         * @return required OSM slippy map level
312         */
313        private int calculateLevel( double scale ) {
314            Iterator<double[]> range = levels.keySet().iterator();
315            while ( range.hasNext() ) {
316                double[] ds = (double[]) range.next();
317                if ( scale > ds[0] && scale <= ds[1] ) {
318                    int l = levels.get( ds );
319                    if ( l < minLevel ) {
320                        l = minLevel;
321                    } else if ( l > maxLevel ) {
322                        l = maxLevel;
323                    }
324                    return l;
325                }
326            }
327            return minLevel;
328        }
329    
330        /*
331         * (non-Javadoc)
332         *
333         * @see
334         * org.deegree.ogcwebservices.wms.dataaccess.ExternalRasterDataAccess#perform(org.deegree.ogcwebservices.wms.operation
335         * .GetMap)
336         */
337        public synchronized GetMapResult perform( GetMap getMap )
338                                throws Exception {
339            width = getMap.getWidth();
340            height = getMap.getHeight();
341            Envelope gmBBox = getMap.getBoundingBox();
342            CoordinateSystem gmCRS = CRSFactory.create( getMap.getSrs() );
343            if ( !gmCRS.equals( crsWGS84 ) ) {
344                envelope = gt2WGS84.transform( gmBBox, gmCRS, true );
345            } else {
346                envelope = gmBBox;
347            }
348            // do the work
349            readSlippyMaps();
350    
351            // create a result map/image that matches the requested one
352            GeoTransformer gt = new GeoTransformer( gmCRS );
353            target = gt.transform( target, targetEnv, gmBBox, width, height, 6, 3,
354                                   Interpolation.getInstance( Interpolation.INTERP_BILINEAR ) );
355            return new GetMapResult( getMap, target );
356        }
357    
358        /*
359         * (non-Javadoc)
360         *
361         * @see
362         * org.deegree.ogcwebservices.wms.dataaccess.ExternalDataAccess#perform(org.deegree.ogcwebservices.wms.operation
363         * .GetFeatureInfo)
364         */
365        public GetFeatureInfoResult perform( GetFeatureInfo gfi ) {
366            // TODO Auto-generated method stub
367            return null;
368        }
369    
370        /*
371         * (non-Javadoc)
372         *
373         * @see
374         * org.deegree.ogcwebservices.wms.dataaccess.ExternalDataAccess#perform(org.deegree.ogcwebservices.wms.operation
375         * .GetLegendGraphic)
376         */
377        public BufferedImage perform( GetLegendGraphic glg ) {
378            return legend;
379        }
380    
381        /*
382         * (non-Javadoc)
383         *
384         * @see org.deegree.ogcwebservices.wms.dataaccess.ExternalDataAccess#setConfigurationFile(java.net.URL)
385         */
386        public void setConfigurationFile( URL url )
387                                throws IOException {
388            if ( url == null ) {
389                init();
390                try {
391                    BufferedImage tmp = ImageUtils.loadImage( new URL( props.getProperty( "LEGEND" ) ) );
392                    legend = new BufferedImage( tileSize, tileSize, BufferedImage.TYPE_INT_RGB );
393                    Graphics g = legend.getGraphics();
394                    g.drawImage( tmp, 0, 0, null );
395                    g.dispose();
396                } catch ( IOException e ) {
397                    LOG.logError( e.getMessage(), e );
398                }
399            } else {
400                props = new Properties();
401                InputStream is = url.openStream();
402                props.load( is );
403                is.close();
404                if ( props.get( "TILEROOT" ) == null ) {
405                    props.put( "TILEROOT", "http://tile.openstreetmap.org/" );
406                }
407                if ( !props.getProperty( "TILEROOT" ).endsWith( "/" ) ) {
408                    props.put( "TILEROOT", props.getProperty( "TILEROOT" ) + "/" );
409                }
410                if ( props.getProperty( "TILEROOT" ).startsWith( "http://tah.openstreetmap.org" ) ) {
411                    maxLevel = 17;
412                } else if ( props.getProperty( "TILEROOT" ).startsWith( "http://tah.openstreetmap.org" ) ) {
413                    minLevel = 12;
414                    maxLevel = 16;
415                } else if ( props.getProperty( "TILEROOT" ).startsWith( "http://richard.dev.openstreetmap.org" ) ) {
416                    minLevel = 13;
417                    maxLevel = 15;
418                    extension = ".jpg";
419                }
420    
421                if ( props.get( "LEGEND" ) == null ) {
422                    props.put( "LEGEND", "http://tile.openstreetmap.org/14/8514/5504.png" );
423                }
424                XMLFragment dummy = new XMLFragment();
425                dummy.setSystemId( url );
426                URL legendURL = dummy.resolve( props.getProperty( "LEGEND" ) );
427                try {
428                    BufferedImage tmp = ImageUtils.loadImage( legendURL );
429                    legend = new BufferedImage( tmp.getWidth(), tmp.getHeight(), BufferedImage.TYPE_INT_RGB );
430                    Graphics g = legend.getGraphics();
431                    g.drawImage( tmp, 0, 0, null );
432                    g.dispose();
433                    return;
434                } catch ( IOException e ) {
435                    LOG.logError( e.getMessage(), e );
436                }
437            }
438            LOG.logDebug( "properties: ", props );
439           
440        }
441    
442        // //////////////////////////////////////////////////////////////////////////////
443        // inner classes
444        // //////////////////////////////////////////////////////////////////////////////
445    
446        private class OSMReader extends Thread {
447    
448            private List<String> row;
449    
450            private int y;
451    
452            private Graphics g;
453    
454            private boolean finished = false;
455    
456            /**
457             *
458             * @param row
459             * @param y
460             */
461            OSMReader( List<String> row, int y, Graphics g ) {
462                this.row = row;
463                this.y = y;
464                this.g = g;
465            }
466    
467            /*
468             * (non-Javadoc)
469             *
470             * @see java.lang.Runnable#run()
471             */
472            public void run() {
473                for ( int j = 0; j < row.size(); j++ ) {
474                    BufferedImage tmp;
475                    try {
476                        tmp = ImageUtils.loadImage( new URL( row.get( j ) ) );
477                    } catch ( IOException e ) {
478                        throw new RuntimeException( e );
479                    }
480                    synchronized ( g ) {
481                        g.drawImage( tmp, j * tileSize, target.getHeight() - ( y * tileSize ) - tileSize, null );
482                    }
483                }
484                finished = true;
485            }
486    
487            /**
488             * @return the finished
489             */
490            public boolean isFinished() {
491                return finished;
492            }
493    
494        }
495    }