Subversion Repositories mkgmap

Rev

Rev 3081 | View as "text/plain" | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * Copyright (C) 2012.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 or
 * version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 */


package uk.me.parabola.mkgmap.sea.optional;

import java.awt.Rectangle;
import java.awt.geom.Area;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.mkgmap.reader.osm.FakeIdGenerator;
import uk.me.parabola.mkgmap.reader.osm.SeaGenerator;
import uk.me.parabola.mkgmap.reader.osm.Way;
import uk.me.parabola.util.Java2DConverter;

import org.geotools.data.DataStore;
import org.geotools.data.DataStoreFinder;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.filter.text.cql2.CQLException;
import org.geotools.geometry.jts.JTS;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.opengis.feature.Feature;
import org.opengis.feature.GeometryAttribute;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;

/**
 * Converts a shapefile containing land polygons into mkgmap precompiled sea tiles.
 * @author WanMil
 */

public class PrecompSeaGenerator {

        private final ExecutorService service;

        /** the shapefile (.shp) that contains the land polygons */
        private final File shapeFile;
        /** the directory the precomp sea tiles are written to */
        private final File outputDir;

        private SimpleFeatureCollection shapeCollection;
        private SimpleFeatureIterator shapeIterator;

        /** transforms the projection of the shapefile to WGS84 ({@code null} if shape file uses WGS84) */
        private final MathTransform transformation;
        /** {@code true}: sea tiles are created with PBF format; {@code false}: sea tiles are created with .osm.gz format */
        private boolean usePbfFormat;
        /** Number of tiles generated by one full reading of the shapefile. Higher numbers require more memory. */
        private int tilesPerCycle;

        public PrecompSeaGenerator(File shapeFile, String shapeCRS, File outputDir)
                        throws NoSuchAuthorityCodeException, FactoryException {
                this.shapeFile = shapeFile;
                this.outputDir = outputDir;
                this.transformation = createTransformation(shapeCRS);
               
                this.service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
                this.usePbfFormat = true;
                this.tilesPerCycle = 10 * 512;
        }

        /**
         * Sets the flag if pbf format or gzipped osm xml format (.osm.gz) should be used
         * for the precompiled sea tiles.
         * @param usePbf {@code true} use PBF format; {@code false} use .osm.gz format
         */

        public void setUsePbfFormat(boolean usePbf) {
                this.usePbfFormat = usePbf;
        }

        /**
         * Retrieves the transformation that is necessary to transform the
         * data from the shape file to WGS84.
         * @param shapeCRS the projection of the shape file
         * @return the transformation ({@code null} if no transformation required)
         * @throws NoSuchAuthorityCodeException if the given projection of the shape file is not supported
         * @throws FactoryException if the given projection of the shape file is not supported
         */

        private MathTransform createTransformation(String shapeCRS)
                        throws NoSuchAuthorityCodeException, FactoryException {
                if ("WGS84".equals(shapeCRS)) {
                        return null;
                }
                if ("Mercator".equals(shapeCRS)) {
                        shapeCRS = "EPSG:3857";
                }
                CoordinateReferenceSystem crsInShapefile = CRS.decode(shapeCRS);
                CoordinateReferenceSystem targetCRS = DefaultGeographicCRS.WGS84;
                boolean lenient = true; // allow for some error due to different datums
                return CRS.findMathTransform(crsInShapefile, targetCRS, lenient);
        }

        /**
         * Transforms a geometry from the shape file to a geometry with WGS84 projection.
         * @param geometry a geometry from the shape file
         * @return a geometry with WGS84 projection
         * @throws MismatchedDimensionException if the transformation fails
         * @throws TransformException if the geometry could not be transformed
         */

        private Geometry transformToWGS84(Geometry geometry)
                        throws MismatchedDimensionException, TransformException {
                if (transformation == null) {
                        return geometry;
                } else {
                        return JTS.transform(geometry, transformation);
                }
        }

        /**
         * Retrieve the areas of all precompiled tiles that have to be worked out.
         * @return the areas of all tiles
         */

        private List<uk.me.parabola.imgfmt.app.Area> getTiles() {
                uk.me.parabola.imgfmt.app.Area earth = new uk.me.parabola.imgfmt.app.Area(
                                -90.0d, -180.0d, 90.0d, 180.0d);
                return getTiles(earth);
        }

        private List<uk.me.parabola.imgfmt.app.Area> getTiles(
                        uk.me.parabola.imgfmt.app.Area wholeArea) {
                int minLat = wholeArea.getMinLat();
                int maxLat = wholeArea.getMaxLat();
                int minLon = wholeArea.getMinLong();
                int maxLon = wholeArea.getMaxLong();

                List<uk.me.parabola.imgfmt.app.Area> tiles = new ArrayList<uk.me.parabola.imgfmt.app.Area>();
                for (int lon = SeaGenerator.getPrecompTileStart(minLon); lon < maxLon; lon += SeaGenerator.PRECOMP_RASTER) {
                        for (int lat = SeaGenerator.getPrecompTileStart(minLat); lat < maxLat; lat += SeaGenerator.PRECOMP_RASTER) {
                                uk.me.parabola.imgfmt.app.Area tile = new uk.me.parabola.imgfmt.app.Area(
                                                Math.max(lat, minLat), Math.max(lon, minLon), Math.min(
                                                                lat + SeaGenerator.PRECOMP_RASTER, maxLat),
                                                Math.min(lon + SeaGenerator.PRECOMP_RASTER, maxLon));
                                tiles.add(tile);
                        }
                }
                return tiles;
        }


        /**
         * Prints regularly how many tiles are not yet finished.
         * @author WanMil
         */

        private static class ProgressPrinter extends Thread {
                private final CountDownLatch countdown;

                public ProgressPrinter(CountDownLatch countdown) {
                        super("ProgressPrinter");
                        this.countdown = countdown;
                        setDaemon(true);
                }

                public void run() {
                        long count = 0;
                        do {
                                count = countdown.getCount();
                                System.out.println(count + " tiles remaining");
                                try {
                                        Thread.sleep(10000);
                                } catch (InterruptedException exp) {
                                }
                        } while (count > 0);
                }
        }

        /**
         * Converts the given geometry to an {@link Area} object.
         * @param geometry a polygon as {@link Geometry} object
         * @return the polygon converted to an {@link Area} object.
         */

        private Area convertToArea(Geometry geometry) {
                Coordinate[] c = geometry.getCoordinates();
                List<Coord> points = new ArrayList<>(c.length);
                for (int n = 0; n < c.length; n++) {
                        points.add(new Coord(c[n].y, c[n].x));
                }
                return Java2DConverter.createArea(points);
        }

       
        /**
         * Creates the merger threads for the given tiles.
         * @param tiles the areas of the precompiled tiles
         * @param tilesCountdown the countdown that should be decreased after a tile is finished
         * @param saveQueue the queue the merged results should be added to
         * @return the preinitialized but not started mergers
         */

        private List<PrecompSeaMerger> createMergers(
                        Collection<uk.me.parabola.imgfmt.app.Area> tiles,
                        CountDownLatch tilesCountdown,
                        BlockingQueue<Entry<String, List<Way>>> saveQueue) {
                List<PrecompSeaMerger> mergers = new ArrayList<PrecompSeaMerger>();

                for (uk.me.parabola.imgfmt.app.Area bounds : tiles) {

                        Rectangle mergeBounds = new Rectangle(bounds.getMinLong(),
                                        bounds.getMinLat(), bounds.getWidth(), bounds.getHeight());
                        String tileKey = bounds.getMinLat() + "_" + bounds.getMinLong();

                        PrecompSeaMerger merger = new PrecompSeaMerger(mergeBounds,
                                        tileKey, tilesCountdown, saveQueue);
                        merger.setExecutorService(service);
                        mergers.add(merger);
                }
                return mergers;
        }

        private void createShapefileAccess() throws IOException {
                Map<String,URL> map = new HashMap<String, URL>();
                map.put("url", shapeFile.toURI().toURL());
                DataStore dataStore = DataStoreFinder.getDataStore(map);
                String typeName = dataStore.getTypeNames()[0];

                SimpleFeatureSource source = dataStore.getFeatureSource(typeName);
                shapeCollection = source.getFeatures();
        }

        private void openShapefile() {
                shapeIterator = shapeCollection.features();
        }

        private void closeShapefile() {
                shapeIterator.close();
                shapeIterator = null;
        }

        /**
         * Reads the next polygon from the shape file.
         * @return the next polygon (WGS84 projection)
         */

        private Geometry readNextPolygon() {
                if (shapeIterator.hasNext()) {
                        Feature feature = shapeIterator.next();
                        GeometryAttribute geom = feature.getDefaultGeometryProperty();
                        Geometry poly = (Geometry) geom.getValue();
                        try {
                                return transformToWGS84(poly);
                        } catch (Exception exp) {
                                System.err.println(exp);
                                return null;
                        }
                } else {
                        return null;
                }
        }

        public void runSeaGeneration() throws MismatchedDimensionException,
                        TransformException, IOException, InterruptedException {
                createShapefileAccess();

                // get all tiles that need to be processed
                List<uk.me.parabola.imgfmt.app.Area> remainingTiles = getTiles();

                // initialize the count down so that it is possible to get the
                // information when all tiles are finished
                CountDownLatch tilesCountdown = new CountDownLatch(remainingTiles.size());
               
                // start a printer that outputs how many tiles still need to be
                // processed
                new ProgressPrinter(tilesCountdown).start();

                // start the saver thread that stores the tiles to disc and creates
                // the index file
                PrecompSeaSaver precompSaver = new PrecompSeaSaver(outputDir, usePbfFormat);
                new Thread(precompSaver, "SaveThread").start();

                // perform several cycles which is necessary to reduce memory
                // requirements
                while (remainingTiles.isEmpty() == false) {

                        // create a list with all tiles that are processed within this cycle
                        List<uk.me.parabola.imgfmt.app.Area> tiles = new ArrayList<uk.me.parabola.imgfmt.app.Area>();
                        tiles.addAll(remainingTiles.subList(0,
                                        Math.min(tilesPerCycle, remainingTiles.size())));
                        remainingTiles.subList(0,
                                        Math.min(tilesPerCycle, remainingTiles.size())).clear();


                        // create the mergers that merge the data of one tile
                        List<PrecompSeaMerger> mergers = createMergers(tiles, tilesCountdown, precompSaver.getQueue());

                        // create an overall area for a simple check if a polygon read from the
                        // shape file intersects one of the currently processed sea tiles
                        Area tileArea = new Area();
                        for (PrecompSeaMerger m : mergers) {
                                tileArea.add(new Area(m.getTileBounds()));
                                // start the mergers
                                service.execute(m);
                        }

                        openShapefile();

                        int numPolygon = 0;
                        long lastInfo = System.currentTimeMillis();

                        // read all polygons from the shape file and add them to the queues of the
                        // merger threads
                        Geometry wgs84Poly = null;
                        while ((wgs84Poly = readNextPolygon()) != null) {

                                if (wgs84Poly.getNumGeometries() != 1) {
                                        // only simple polygons are supported by now
                                        // maybe this could be changed in future?
                                        System.err.println("Polygon from shapefile has "
                                                        + wgs84Poly.getNumGeometries()
                                                        + " geometries. Only one geometry is supported.");
                                        System.err.println("Skip polygon.");
                                        continue;
                                }

                                Geometry bounds = wgs84Poly.getEnvelope();
                                if (bounds.isEmpty()) {
                                        System.err.println("Empty or non polygon: " + bounds);
                                } else {
                                        Area polyBounds = convertToArea(bounds);

                                        // easy check if the polygon is used by any tile that is
                                        // currently processed
                                        if (polyBounds.intersects(tileArea.getBounds2D())) {
                                               
                                                // yes it touches at least one tile => convert it to
                                                // a java.awt.geom.Area object
                                                Area polyAsArea = convertToArea(wgs84Poly.getGeometryN(0));
                                               
                                                // go through all current merger threads and add the
                                                // polygon to the queues of them
                                                for (PrecompSeaMerger mThread : mergers) {
                                                        if (mThread.getTileBounds().intersects(polyAsArea.getBounds2D())) {
                                                                try {
                                                                        mThread.getQueue().put(polyAsArea);
                                                                } catch (InterruptedException exp) {
                                                                        exp.printStackTrace();
                                                                }
                                                        }
                                                }
                                        }

                                        numPolygon++;
                                        if ((numPolygon) % 50000 == 0
                                                        || System.currentTimeMillis() - lastInfo > 30000) {
                                                // print out the current number of polygons already processed
                                                System.out.println("Worked out " + (numPolygon) + " polygons");
                                                lastInfo = System.currentTimeMillis();
                                        }

                                }
                        }
                        closeShapefile();

                        System.out.println("Reading shapefile finished");

                        // signal all mergers that all polygons have been read
                        for (PrecompSeaMerger mThread : mergers) {
                                mThread.signalInputComplete();
                        }

                        // Wait until not more than twice the number of tiles per cycle
                        // are waiting for processing. Otherwise OutOfMemory problems
                        // may occurr
                        while (tilesCountdown.getCount() > remainingTiles.size()
                                        + 2*tilesPerCycle) {
                                Thread.sleep(50L);
                        }
                }
                // wait until all tiles have been merged
                tilesCountdown.await();
                // wait until the saver for the tiles is finished
                precompSaver.waitForFinish();
                // shutdown the executor service
                service.shutdown();
        }

        public static void main(String[] args) throws MismatchedDimensionException,
                        TransformException, IOException, FactoryException, CQLException,
                        InterruptedException {
                long t1 = System.currentTimeMillis();
               
                File shapeFile = new File(args[0]);
                String shapeCRS = args[1];
                File outputDir = new File(args[2]);

                if (shapeFile.exists() == false) {
                        throw new FileNotFoundException("File "+shapeFile+" does not exist.");
                }
               
                // use small fake ids so that the xml files become smaller
                FakeIdGenerator.setStartId(0);
               
                PrecompSeaGenerator seaGenerator = new PrecompSeaGenerator(shapeFile,
                                shapeCRS, outputDir);
                seaGenerator.runSeaGeneration();

                System.out.println("Generation took "+(System.currentTimeMillis()-t1)+" ms");
               
        }
}