/*
* Copyright (C) 2006, 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.util;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;
/**
* This is a tool class that provides static methods to convert between mkgmap
* objects and Java2D objects. The Java2D objects provide some optimized polygon
* algorithms that are quite useful so that it makes sense to perform the
* conversion.
*
* @author WanMil
*/
public class Java2DConverter {
private static final Logger log = Logger.getLogger(Java2DConverter.class);
/**
* Creates a Java2D {@link Area} object from the given mkgmap rectangular
* {@link uk.me.parabola.imgfmt.app.Area} object.
*
* @param bbox a rectangular bounding box
* @return the converted Java2D area
*/
public static Area createBoundsArea(uk.me.parabola.imgfmt.app.Area bbox) {
return createArea(bbox.toCoords());
}
/**
* Converts the bounding box of a Java2D {@link Shape} object to an mkgmap
* {@link uk.me.parabola.imgfmt.app.Area} object.
*
* @param shape a Java2D Shape (Area, Path2D, ...)
* @return the bounding box
*/
public static uk.me.parabola.imgfmt.app.Area createBbox(Shape shape) {
Rectangle areaBounds = shape.getBounds();
return new uk.me.parabola.imgfmt.app.Area(areaBounds.y,areaBounds.x,(int) areaBounds.getMaxY(),(int) areaBounds.getMaxX());
}
/**
* Creates a Java2D {@link Area} object from a polygon given as a list of
* {@link Coord} objects. This list should describe a closed polygon.
*
* @param polygonPoints a list of points that describe a closed polygon
* @return the converted Java2D area
*/
public static Area createArea(List<Coord> polygonPoints) {
return new Area(createPath2D(polygonPoints));
}
/**
* Creates a Java2D {@link Path2D} object from a polygon given as a list of
* {@link Coord} objects. This list should describe a closed polygon.
*
* @param polygonPoints a list of points that describe a closed polygon
* @return the converted Java2D path
*/
public static Path2D createPath2D (List<Coord> polygonPoints) {
int n = polygonPoints.size();
if (n < 3)
return new Path2D.Double();
Path2D path = new Path2D.Double(PathIterator.WIND_NON_ZERO, n);
if (polygonPoints.get(0).highPrecEquals(polygonPoints.get(n-1))){
// if first and last point are high-prec-equal, ignore last point
// because we use closePath() to signal that
--n;
}
double lastLat = Integer.MAX_VALUE,lastLon = Integer.MAX_VALUE;
for (int i = 0; i < n; i++){
Coord co = polygonPoints.get(i);
int lat30 = co.getHighPrecLat();
int lon30 = co.getHighPrecLon();
double x = (double)lon30 / (1<<Coord.DELTA_SHIFT);
double y = (double)lat30 / (1<<Coord.DELTA_SHIFT);
if (i == 0)
path.moveTo(x, y);
else {
if (lastLon != lon30 || lastLat != lat30)
path.lineTo(x, y);
}
lastLon = lon30;
lastLat = lat30;
}
path.closePath();
return path;
}
public static Polygon createHighPrecPolygon(List<Coord> points) {
Polygon polygon = new Polygon();
for (Coord co : points) {
polygon.addPoint(co.getHighPrecLon(), co.getHighPrecLat());
}
return polygon;
}
public static List<Area> areaToSingularAreas(Area area) {
return areaToSingularAreas(0, area);
}
/**
* Convert an area that may contains multiple areas to a list of singular
* areas keeping the highest possible precision.
*
* @param area an area
* @return list of singular areas
*/
private static List<Area> areaToSingularAreas(int depth, Area area) {
if (area.isEmpty()) {
return Collections.emptyList();
} else if (area.isSingular()) {
return Collections.singletonList(area);
} else {
List<Area> singularAreas = new ArrayList<>();
// all ways in the area MUST define outer areas
// it is not possible that one of the areas define an inner segment
double[] res = new double[6];
PathIterator pit = area.getPathIterator(null);
Path2D path = null;
while (!pit.isDone()) {
int type = pit.currentSegment(res);
double lat = res[1];
double lon = res[0];
switch (type) {
case PathIterator.SEG_LINETO:
path.lineTo(lon, lat);
break;
case PathIterator.SEG_CLOSE:
path.closePath();
Area a = new Area(path);
if (!a.isEmpty()) {
if (depth < 10 && !a.isSingular()){
// should not happen, but it does. Error in Area code?
singularAreas.addAll(areaToSingularAreas(depth+1,a));
}
else
singularAreas.add(a);
}
path = null;
break;
case PathIterator.SEG_MOVETO:
path = new Path2D.Double();
path.moveTo(lon, lat);
break;
default:
log.error("Unsupported path iterator type " + type
+ ". This is an mkgmap error.");
}
pit.next();
}
return singularAreas;
}
}
/**
* Convert an area to an mkgmap way. The caller must ensure that the area is
* singular. Otherwise only the first non-empty part of the area is converted.
*
* @param area the area
* @return a new mkgmap way
*/
public static List<Coord> singularAreaToPoints(Area area) {
if (area.isEmpty()) {
return null;
}
List<Coord> points = null;
double[] res = new double[6];
PathIterator pit = area.getPathIterator(null);
int prevLat30 = Integer.MIN_VALUE;
int prevLong30 = Integer.MIN_VALUE;
while (!pit.isDone()) {
int type = pit.currentSegment(res);
int lat30 = (int)Math.round(res[1] * (1<<Coord.DELTA_SHIFT));
int lon30 = (int)Math.round(res[0] * (1<<Coord.DELTA_SHIFT));
switch (type) {
case PathIterator.SEG_MOVETO:
if (points != null)
log.error("area not singular");
points = new ArrayList<>();
points.add(Coord.makeHighPrecCoord(lat30, lon30));
break;
case PathIterator.SEG_LINETO:
assert points != null;
if (prevLat30 != lat30 || prevLong30 != lon30) {
points.add(Coord.makeHighPrecCoord(lat30, lon30));
}
break;
case PathIterator.SEG_CLOSE:
assert points != null;
if (points.size() < 3)
points = null;
else {
if (points.get(0).highPrecEquals(points.get(points.size() - 1))) {
// replace equal last with closing point
points.set(points.size() - 1, points.get(0));
}
else
points.add(points.get(0)); // add closing point
if (points.size() < 4)
points = null;
else
return points;
}
break;
default:
log.error("Unsupported path iterator type " + type
+ ". This is an mkgmap error.");
}
prevLat30 = lat30;
prevLong30 = lon30;
pit.next();
}
return points;
}
/**
* Convert the area back into a list of polygons each represented by a list
* of coords. It is possible that the area contains multiple discontinuous
* polygons, so you may append more than one shape to the output list.<br/>
* <b>Attention:</b> The outline of the polygon is has clockwise order whereas
* holes in the polygon have counterclockwise order.
*
* @param area The area to be converted.
* @param useHighPrec false: round coordinates to map units
* @return a list of closed polygons
*/
public static List<List<Coord>> areaToShapes(java.awt.geom.Area area) {
List<List<Coord>> outputs = new ArrayList<>(4);
double[] res = new double[6];
PathIterator pit = area.getPathIterator(null);
List<Coord> coords = null;
int prevLat30 = Integer.MIN_VALUE;
int prevLong30 = Integer.MIN_VALUE;
while (!pit.isDone()) {
int type = pit.currentSegment(res);
int lat30 = (int) Math.round(res[1] * (1<<Coord.DELTA_SHIFT));
int lon30 = (int) Math.round(res[0] * (1<<Coord.DELTA_SHIFT));
switch (type) {
case PathIterator.SEG_LINETO:
if (prevLat30 != lat30 || prevLong30 != lon30)
coords.add(Coord.makeHighPrecCoord(lat30, lon30));
prevLat30 = lat30;
prevLong30 = lon30;
break;
case PathIterator.SEG_MOVETO:
case PathIterator.SEG_CLOSE:
if ((type == PathIterator.SEG_MOVETO && coords != null) || type == PathIterator.SEG_CLOSE) {
if (coords.size() > 2){
if (coords.get(0).highPrecEquals(coords.get(coords.size() - 1))){
// replace equal last with closing point
coords.set(coords.size() - 1, coords.get(0));
}
else
coords.add(coords.get(0)); // add closing point
}
if (coords.size() > 3){
outputs.add(coords);
}
}
if (type == PathIterator.SEG_MOVETO){
coords = new ArrayList<>();
coords.add(Coord.makeHighPrecCoord(lat30, lon30));
prevLat30 = lat30;
prevLong30 = lon30;
} else {
coords = null;
prevLat30 = Integer.MIN_VALUE;
prevLong30 = Integer.MIN_VALUE;
}
break;
default:
log.error("Unsupported path iterator type " + type
+ ". This is an mkgmap error.");
}
pit.next();
}
return outputs;
}
}