/* * 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; } 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 (shapeIterator.hasNext()) { Feature feature = shapeIterator.next(); GeometryAttribute geom = feature.getDefaultGeometryProperty(); Geometry poly = (Geometry) geom.getValue(); if (poly == null){ continue; } try { wgs84Poly = transformToWGS84(poly); } catch (Exception exp) { System.err.println(exp); continue; } 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"); } }