/*
* 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.awt.geom.Path2D;
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.Utils;
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 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();
Path2D.
Double path =
new Path2D.
Double();
path.
moveTo(Utils.
toMapUnit(c
[0].
x), Utils.
toMapUnit(c
[0].
y));
for (int n =
1; n
< c.
length; n++
) {
path.
lineTo(Utils.
toMapUnit(c
[n
].
x), Utils.
toMapUnit(c
[n
].
y));
}
path.
closePath();
return new Area(path
);
}
/**
* 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");
}
}