/*
* Copyright 2010, 2011, 2012 mapsforge.org
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later version.
*
* 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mapsforge.map.writer.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.mapsforge.core.model.CoordinatesUtil;
import org.mapsforge.core.model.GeoPoint;
import org.mapsforge.core.util.MercatorProjection;
import org.mapsforge.map.writer.model.TDNode;
import org.mapsforge.map.writer.model.TDWay;
import org.mapsforge.map.writer.model.TileCoordinate;
import org.mapsforge.map.writer.model.WayDataBlock;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.geom.TopologyException;
import com.vividsolutions.jts.simplify.TopologyPreservingSimplifier;
/**
* Provides utility functions for the maps preprocessing.
*
* @author bross
*/
public final class GeoUtils {
private GeoUtils() {
}
// private static final double DOUGLAS_PEUCKER_SIMPLIFICATION_TOLERANCE = 0.0000188;
// private static final double DOUGLAS_PEUCKER_SIMPLIFICATION_TOLERANCE = 0.00003;
/**
* The minimum amount of nodes required for a valid closed polygon.
*/
public static final int MIN_NODES_POLYGON = 4;
/**
* The minimum amount of coordinates (lat/lon counted separately) required for a valid closed polygon.
*/
public static final int MIN_COORDINATES_POLYGON = 8;
private static final byte SUBTILE_ZOOMLEVEL_DIFFERENCE = 2;
private static final double[] EPSILON_ZERO = new double[] { 0, 0 };
private static final Logger LOGGER = Logger.getLogger(GeoUtils.class.getName());
private static final int[] TILE_BITMASK_VALUES = new int[] { 32768, 16384, 8192, 4096, 2048, 1024, 512, 256, 128,
64, 32, 16, 8, 4, 2, 1 };
// JTS
private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
// **************** WAY OR POI IN TILE *****************
/**
* Computes which tiles on the given base zoom level need to include the given way (which may be a polygon).
*
* @param way
* the way that is mapped to tiles
* @param baseZoomLevel
* the base zoom level which is used in the mapping
* @param enlargementInMeter
* amount of pixels that is used to enlarge the bounding box of the way and the tiles in the mapping
* process
* @return all tiles on the given base zoom level that need to include the given way, an empty set if no tiles are
* matched
*/
public static Set<TileCoordinate> mapWayToTiles(final TDWay way, final byte baseZoomLevel,
final int enlargementInMeter) {
if (way == null) {
LOGGER.fine("way is null in mapping to tiles");
return Collections.emptySet();
}
HashSet<TileCoordinate> matchedTiles = new HashSet<TileCoordinate>();
Geometry wayGeometry = toJTSGeometry(way, !way.isForcePolygonLine());
if (wayGeometry == null) {
LOGGER.fine("unable to create geometry from way: " + way.getId());
return matchedTiles;
}
TileCoordinate[] bbox = getWayBoundingBox(way, baseZoomLevel, enlargementInMeter);
// calculate the tile coordinates and the corresponding bounding boxes
try {
for (int k = bbox[0].getX(); k <= bbox[1].getX(); k++) {
for (int l = bbox[0].getY(); l <= bbox[1].getY(); l++) {
Geometry bboxGeometry = tileToJTSGeometry(k, l, baseZoomLevel, enlargementInMeter);
if (bboxGeometry.intersects(wayGeometry)) {
matchedTiles.add(new TileCoordinate(k, l, baseZoomLevel));
}
}
}
} catch (TopologyException e) {
LOGGER.log(Level.FINE,
"encountered error during mapping of a way to corresponding tiles, way id: " + way.getId());
return Collections.emptySet();
}
return matchedTiles;
}
/**
* @param geoPoint
* the point
* @param tile
* the tile
* @return true if the point is located in the given tile
*/
public static boolean pointInTile(GeoPoint geoPoint, TileCoordinate tile) {
if (geoPoint == null || tile == null) {
return false;
}
double lon1 = MercatorProjection.tileXToLongitude(tile.getX(), tile.getZoomlevel());
double lon2 = MercatorProjection.tileXToLongitude(tile.getX() + 1, tile.getZoomlevel());
double lat1 = MercatorProjection.tileYToLatitude(tile.getY(), tile.getZoomlevel());
double lat2 = MercatorProjection.tileYToLatitude(tile.getY() + 1, tile.getZoomlevel());
return geoPoint.latitude <= lat1 && geoPoint.latitude >= lat2 && geoPoint.longitude >= lon1
&& geoPoint.longitude <= lon2;
}
// *********** PREPROCESSING OF WAYS **************
/**
* Clips a geometry to a tile.
*
* @param way
* the way
* @param geometry
* the geometry
* @param tileCoordinate
* the tile coordinate
* @param enlargementInMeters
* the bounding box buffer
* @return the clipped geometry
*/
public static Geometry clipToTile(TDWay way, Geometry geometry, TileCoordinate tileCoordinate,
int enlargementInMeters) {
// clip geometry?
Geometry tileBBJTS = null;
Geometry ret = null;
// create tile bounding box
tileBBJTS = tileToJTSGeometry(tileCoordinate.getX(), tileCoordinate.getY(), tileCoordinate.getZoomlevel(),
enlargementInMeters);
// clip the polygon/ring by intersection with the bounding box of the tile
// may throw a TopologyException
try {
// geometry = OverlayOp.overlayOp(tileBBJTS, geometry, OverlayOp.INTERSECTION);
ret = tileBBJTS.intersection(geometry);
} catch (TopologyException e) {
LOGGER.log(Level.FINE, "JTS cannot clip way, not storing it in data file: " + way.getId(), e);
way.setInvalid(true);
return null;
}
return ret;
}
/**
* Simplifies a geometry using the Douglas Peucker algorithm.
*
* @param way
* the way
* @param geometry
* the geometry
* @param zoomlevel
* the zoom level
* @param simplificationFactor
* the simplification factor
* @return the simplified geometry
*/
public static Geometry simplifyGeometry(TDWay way, Geometry geometry, byte zoomlevel, double simplificationFactor) {
Geometry ret = null;
Envelope bbox = geometry.getEnvelopeInternal();
// compute maximal absolute latitude (so that we don't need to care if we
// are on northern or southern hemisphere)
double latMax = Math.max(Math.abs(bbox.getMaxY()), Math.abs(bbox.getMinY()));
double deltaLat = MercatorProjection.deltaLat(simplificationFactor, latMax, zoomlevel);
try {
ret = TopologyPreservingSimplifier.simplify(geometry, deltaLat);
} catch (TopologyException e) {
LOGGER.log(Level.FINE,
"JTS cannot simplify way due to an error, not simplifying way with id: " + way.getId(), e);
way.setInvalid(true);
return geometry;
}
return ret;
}
/**
* A tile on zoom level <i>z</i> has exactly 16 sub tiles on zoom level <i>z+2</i>. For each of these 16 sub tiles
* it is analyzed if the given way needs to be included. The result is represented as a 16 bit short value. Each bit
* represents one of the 16 sub tiles. A bit is set to 1 if the sub tile needs to include the way. Representation is
* row-wise.
*
* @param geometry
* the geometry which is analyzed
* @param tile
* the tile which is split into 16 sub tiles
* @param enlargementInMeter
* amount of pixels that is used to enlarge the bounding box of the way and the tiles in the mapping
* process
* @return a 16 bit short value that represents the information which of the sub tiles needs to include the way
*/
public static short computeBitmask(final Geometry geometry, final TileCoordinate tile, final int enlargementInMeter) {
List<TileCoordinate> subtiles = tile
.translateToZoomLevel((byte) (tile.getZoomlevel() + SUBTILE_ZOOMLEVEL_DIFFERENCE));
short bitmask = 0;
int tileCounter = 0;
for (TileCoordinate subtile : subtiles) {
Geometry bbox = tileToJTSGeometry(subtile.getX(), subtile.getY(), subtile.getZoomlevel(),
enlargementInMeter);
if (bbox.intersects(geometry)) {
bitmask |= TILE_BITMASK_VALUES[tileCounter];
}
tileCounter++;
}
return bitmask;
}
/**
* @param geometry
* a JTS {@link Geometry} object representing the OSM entity
* @param tile
* the tile
* @param enlargementInMeter
* the enlargement of the tile in meters
* @return true, if the geometry is covered completely by this tile
*/
public static boolean coveredByTile(final Geometry geometry, final TileCoordinate tile, final int enlargementInMeter) {
Geometry bbox = tileToJTSGeometry(tile.getX(), tile.getY(), tile.getZoomlevel(), enlargementInMeter);
if (bbox.covers(geometry)) {
return true;
}
return false;
}
/**
* @param geometry
* the JTS {@link Geometry} object
* @return the centroid of the given geometry
*/
public static GeoPoint computeCentroid(Geometry geometry) {
Point centroid = geometry.getCentroid();
if (centroid != null) {
return new GeoPoint(centroid.getCoordinate().y, centroid.getCoordinate().x);
}
return null;
}
/**
* Convert a JTS Geometry to a WayDataBlock list.
*
* @param geometry
* a geometry object which should be converted
* @return a list of WayBlocks which you can use to save the way.
*/
public static List<WayDataBlock> toWayDataBlockList(Geometry geometry) {
List<WayDataBlock> res = new ArrayList<WayDataBlock>();
if (geometry instanceof MultiPolygon) {
MultiPolygon mp = (MultiPolygon) geometry;
for (int i = 0; i < mp.getNumGeometries(); i++) {
Polygon p = (Polygon) mp.getGeometryN(i);
List<Integer> outer = toCoordinateList(p.getExteriorRing());
List<List<Integer>> inner = new ArrayList<List<Integer>>();
for (int j = 0; j < p.getNumInteriorRing(); j++) {
inner.add(toCoordinateList(p.getInteriorRingN(j)));
}
res.add(new WayDataBlock(outer, inner));
}
} else if (geometry instanceof Polygon) {
Polygon p = (Polygon) geometry;
List<Integer> outer = toCoordinateList(p.getExteriorRing());
List<List<Integer>> inner = new ArrayList<List<Integer>>();
for (int i = 0; i < p.getNumInteriorRing(); i++) {
inner.add(toCoordinateList(p.getInteriorRingN(i)));
}
res.add(new WayDataBlock(outer, inner));
} else if (geometry instanceof MultiLineString) {
MultiLineString ml = (MultiLineString) geometry;
for (int i = 0; i < ml.getNumGeometries(); i++) {
LineString l = (LineString) ml.getGeometryN(i);
res.add(new WayDataBlock(toCoordinateList(l), null));
}
} else if (geometry instanceof LinearRing || geometry instanceof LineString) {
res.add(new WayDataBlock(toCoordinateList(geometry), null));
} else if (geometry instanceof GeometryCollection) {
GeometryCollection gc = (GeometryCollection) geometry;
for (int i = 0; i < gc.getNumGeometries(); i++) {
List<WayDataBlock> recursiveResult = toWayDataBlockList(gc.getGeometryN(i));
for (WayDataBlock wayDataBlock : recursiveResult) {
res.add(wayDataBlock);
}
}
}
return res;
}
// **************** JTS CONVERSIONS *********************
/**
* Converts a way with potential inner ways to a JTS geometry.
*
* @param way
* the way
* @param innerWays
* the inner ways or null
* @return the JTS geometry
*/
public static Geometry toJtsGeometry(TDWay way, List<TDWay> innerWays) {
Geometry wayGeometry = toJTSGeometry(way, !way.isForcePolygonLine());
if (wayGeometry == null) {
return null;
}
if (innerWays != null) {
List<LinearRing> innerWayGeometries = new ArrayList<LinearRing>();
if (!(wayGeometry instanceof Polygon)) {
LOGGER.warning("outer way of multi polygon is not a polygon, skipping it: " + way.getId());
return null;
}
Polygon outerPolygon = (Polygon) wayGeometry;
for (TDWay innerWay : innerWays) {
// in order to build the polygon with holes, we want to create
// linear rings of the inner ways
Geometry innerWayGeometry = toJTSGeometry(innerWay, false);
if (innerWayGeometry == null) {
continue;
}
if (!(innerWayGeometry instanceof LinearRing)) {
LOGGER.warning("inner way of multi polygon is not a polygon, skipping it, inner id: "
+ innerWay.getId() + ", outer id: " + way.getId());
continue;
}
LinearRing innerRing = (LinearRing) innerWayGeometry;
// check if inner way is completely contained in outer way
if (outerPolygon.covers(innerRing)) {
innerWayGeometries.add(innerRing);
} else {
LOGGER.warning("inner way is not contained in outer way, skipping inner way, inner id: "
+ innerWay.getId() + ", outer id: " + way.getId());
}
}
if (!innerWayGeometries.isEmpty()) {
// make wayGeometry a new Polygon that contains inner ways as holes
LinearRing[] holes = innerWayGeometries.toArray(new LinearRing[innerWayGeometries.size()]);
LinearRing exterior = GEOMETRY_FACTORY
.createLinearRing(outerPolygon.getExteriorRing().getCoordinates());
wayGeometry = new Polygon(exterior, holes, GEOMETRY_FACTORY);
}
}
return wayGeometry;
}
/**
* Internal conversion method to convert our internal data structure for ways to geometry objects in JTS. It will
* care about ways and polygons and will create the right JTS onjects.
*
* @param way
* TDway which will be converted. Null if we were not able to convert the way to a Geometry object.
* @param area
* true, if the way represents an area, i.e. a polygon instead of a linear ring
* @return return Converted way as JTS object.
*/
private static Geometry toJTSGeometry(TDWay way, boolean area) {
if (way.getWayNodes().length < 2) {
LOGGER.fine("way has fewer than 2 nodes: " + way.getId());
return null;
}
Coordinate[] coordinates = new Coordinate[way.getWayNodes().length];
for (int i = 0; i < coordinates.length; i++) {
TDNode currentNode = way.getWayNodes()[i];
coordinates[i] = new Coordinate(CoordinatesUtil.microdegreesToDegrees(currentNode.getLongitude()),
CoordinatesUtil.microdegreesToDegrees(currentNode.getLatitude()));
}
Geometry res = null;
try {
// check for closed polygon
if (way.isPolygon()) {
if (area) {
// polygon
res = GEOMETRY_FACTORY.createPolygon(GEOMETRY_FACTORY.createLinearRing(coordinates), null);
} else {
// linear ring
res = GEOMETRY_FACTORY.createLinearRing(coordinates);
}
} else {
res = GEOMETRY_FACTORY.createLineString(coordinates);
}
} catch (TopologyException e) {
LOGGER.log(Level.FINE, "error creating JTS geometry from way: " + way.getId(), e);
return null;
}
return res;
}
private static List<Integer> toCoordinateList(Geometry jtsGeometry) {
Coordinate[] jtsCoords = jtsGeometry.getCoordinates();
ArrayList<Integer> result = new ArrayList<Integer>();
for (int j = 0; j < jtsCoords.length; j++) {
GeoPoint geoCoord = new GeoPoint(jtsCoords[j].y, jtsCoords[j].x);
result.add(Integer.valueOf(CoordinatesUtil.degreesToMicrodegrees(geoCoord.latitude)));
result.add(Integer.valueOf(CoordinatesUtil.degreesToMicrodegrees(geoCoord.longitude)));
}
return result;
}
private static double[] computeTileEnlargement(double lat, int enlargementInPixel) {
if (enlargementInPixel == 0) {
return EPSILON_ZERO;
}
double[] epsilons = new double[2];
epsilons[0] = GeoPoint.latitudeDistance(enlargementInPixel);
epsilons[1] = GeoPoint.longitudeDistance(enlargementInPixel, lat);
return epsilons;
}
private static double[] bufferInDegrees(long tileY, byte zoom, int enlargementInMeter) {
if (enlargementInMeter == 0) {
return EPSILON_ZERO;
}
double[] epsilons = new double[2];
double lat = MercatorProjection.tileYToLatitude(tileY, zoom);
epsilons[0] = GeoPoint.latitudeDistance(enlargementInMeter);
epsilons[1] = GeoPoint.longitudeDistance(enlargementInMeter, lat);
return epsilons;
}
private static Geometry tileToJTSGeometry(long tileX, long tileY, byte zoom, int enlargementInMeter) {
double minLat = MercatorProjection.tileYToLatitude(tileY + 1, zoom);
double maxLat = MercatorProjection.tileYToLatitude(tileY, zoom);
double minLon = MercatorProjection.tileXToLongitude(tileX, zoom);
double maxLon = MercatorProjection.tileXToLongitude(tileX + 1, zoom);
double[] epsilons = bufferInDegrees(tileY, zoom, enlargementInMeter);
minLon -= epsilons[1];
minLat -= epsilons[0];
maxLon += epsilons[1];
maxLat += epsilons[0];
Coordinate bottomLeft = new Coordinate(minLon, minLat);
Coordinate topRight = new Coordinate(maxLon, maxLat);
return GEOMETRY_FACTORY.createLineString(new Coordinate[] { bottomLeft, topRight }).getEnvelope();
}
private static TileCoordinate[] getWayBoundingBox(final TDWay way, byte zoomlevel, int enlargementInPixel) {
double maxx = Double.NEGATIVE_INFINITY, maxy = Double.NEGATIVE_INFINITY, minx = Double.POSITIVE_INFINITY, miny = Double.POSITIVE_INFINITY;
for (TDNode coordinate : way.getWayNodes()) {
maxy = Math.max(maxy, CoordinatesUtil.microdegreesToDegrees(coordinate.getLatitude()));
miny = Math.min(miny, CoordinatesUtil.microdegreesToDegrees(coordinate.getLatitude()));
maxx = Math.max(maxx, CoordinatesUtil.microdegreesToDegrees(coordinate.getLongitude()));
minx = Math.min(minx, CoordinatesUtil.microdegreesToDegrees(coordinate.getLongitude()));
}
double[] epsilonsTopLeft = computeTileEnlargement(maxy, enlargementInPixel);
double[] epsilonsBottomRight = computeTileEnlargement(miny, enlargementInPixel);
TileCoordinate[] bbox = new TileCoordinate[2];
bbox[0] = new TileCoordinate((int) MercatorProjection.longitudeToTileX(minx - epsilonsTopLeft[1], zoomlevel),
(int) MercatorProjection.latitudeToTileY(maxy + epsilonsTopLeft[0], zoomlevel), zoomlevel);
bbox[1] = new TileCoordinate(
(int) MercatorProjection.longitudeToTileX(maxx + epsilonsBottomRight[1], zoomlevel),
(int) MercatorProjection.latitudeToTileY(miny - epsilonsBottomRight[0], zoomlevel), zoomlevel);
return bbox;
}
}