package bs.bs2d.geom;
import bs.bs2d.gui.plot.ColorMap;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.CoordinateFilter;
import com.vividsolutions.jts.geom.CoordinateSequence;
import com.vividsolutions.jts.geom.CoordinateSequenceFilter;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryComponentFilter;
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.util.AffineTransformation;
import com.vividsolutions.jts.operation.polygonize.Polygonizer;
import com.vividsolutions.jts.operation.valid.ConnectedInteriorTester;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.util.ArrayList;
import java.util.Collection;
import org.jaitools.jts.LineSmoother;
/**
*
* @author Djen
*/
public class AreaSet {
private static GeometryFactory GF = JTSUtils.getFactory();
private final MultiPolygon shell;
private final MultiPolygon[] polygons;
private final Shape[] shapes;
/**
* Creates a new AreaSet from the given Shapes. The relative area threshold
* will be used to compute the absolute threshold by multiplying it with the
* total area of this AreaSet.
* @param shapes the shapes
* @param thrsh the relative area threshold in the interval [0, 1] (Default
* is 0.04)
* @param filterDegree the degree of the moving average filter that is
* applied to smooth the area outlines. Set to 0 to disable filter. *Filtering is not currently supported
*/
public AreaSet(Shape[] shapes, float thrsh, int filterDegree){
// create polygons
polygons = new MultiPolygon[shapes.length];
for (int i = 0; i < shapes.length; i++) {
polygons[i] = JTSUtils.createMultiPolygon(shapes[i]);
}
shell = (MultiPolygon)polygons[0].clone();
// apply filter
// Geometry preserve;
// preserve = GF.createMultiLineString(JTSUtils.getLineStrings(shell));
// preserve = preserve.buffer(0.0001);
// for (int i = 0; i < polygons.length; i++) {
// polygons[i] = applyMAFilter(polygons[i], filterDegree, preserve);
// }
thrsh *= shell.getArea();
computeSubAreas(polygons, thrsh);
// update the shapes for painting
this.shapes = createShapes(polygons);
}
public AreaSet(MultiPolygon[] polygons) {
this.shell = (MultiPolygon) polygons[0].clone();
this.polygons = polygons;
this.shapes = createShapes(polygons);
// if(!isValid())
// throw new RuntimeException("invalid area set!");
}
/**
* Creates an area set with no subareas.
* @return A new area set that equals the shell of this area set.
*/
public AreaSet createSingularAreaSet(){
MultiPolygon mp = (MultiPolygon) shell.clone();
return new AreaSet(new MultiPolygon[]{mp});
}
/**
* Applys a moving average filter to the polygon outlines. If filtering
* results in an invalid geometry the filter degree is lowered successively.
* If no filter could be applied or filter degree is 0 the original polygon
* is returned.
* @param poly the polygon to be smoothed
* @param degree the filter degree. Set 0 to disable filter.
* @param preserve a geometry containing all the points that are to be
* preserved. No vertices conained in the preserve geometry will be changed
* by the filter.
* @return a new polygon with the filter applied or the original polygon if
* no filter was applied
*/
private MultiPolygon applyMAFilter(MultiPolygon poly, int degree, Geometry preserve){
MultiPolygon filtered = null;
for(int deg = degree; deg > 0; deg--){
filtered = (MultiPolygon) poly.clone();
filtered.apply(new ComponentMovingAverageFilter(deg, preserve));
if(filtered.isValid()){
ConnectedInteriorTester t1 = new ConnectedInteriorTester(null);
System.err.println("Applied filter degree: " + deg);
break;
}
}
if(filtered != null && filtered.isValid()){
return filtered;
} else {
if(degree > 0)
System.err.println("No filter applied to polygon");
return poly;
}
}
private MultiPolygon applyCIFilter(MultiPolygon poly, Geometry preserve){
GeometryFactory gf = JTSUtils.getFactory();
// split lines trings along preserve geometry and seperate segments to
// keep and to smooth
MultiLineString lines = gf.createMultiLineString(JTSUtils.getLineStrings(poly));
MultiLineString prsrvLines = gf.createMultiLineString(JTSUtils.getLineStrings(preserve));
Geometry segments = lines.difference(prsrvLines);
LineString[] segLines = JTSUtils.getLineStrings(segments);
ArrayList<LineString> smooth = new ArrayList<>();
ArrayList<LineString> keep = new ArrayList<>();
for (LineString ls : segLines) {
if(preserve.contains(ls))
keep.add(ls);
else
smooth.add(ls);
}
// apply smoothing
LineSmoother smoother = new LineSmoother(gf);
Polygonizer polygonizer = new Polygonizer();
for (int i = 0; i < smooth.size(); i++) {
polygonizer.add(smoother.smooth(smooth.get(i), 0.5));
}
polygonizer.add(keep);
Collection<Polygon> polys = (Collection<Polygon>) polygonizer.getPolygons();
poly = gf.createMultiPolygon(polys.toArray(new Polygon[polys.size()]));
if(!polygonizer.getInvalidRingLines().isEmpty())
System.err.println("invalid ring lines found!");
if(!polygonizer.getDangles().isEmpty())
System.err.println("dangles found!");
if(!polygonizer.getCutEdges().isEmpty())
System.err.println("cut edges found!");
if(!poly.isValid())
throw new RuntimeException("invalid polygon!");
return poly;
}
/**
* Applys the given threshold to the set of MultiPolygons removing any
* regions smaller than the thresholdvalue. At the same time higher order
* areas are subtracted from lower order areas to eliminate any overlapping
* regions.
* As a result the given set of Multipolygons will contain no Polygons with
* an area smaller that thrsh and any geometry in the set may touch, but
* never intersect.
* @param polygons the original overlapping set of geomtetries
* @param thrsh the area threshold
*/
private void computeSubAreas(MultiPolygon[] polygons, float thrsh){
ArrayList<Polygon> subtract = new ArrayList<>();
//start with highest order area
for (int i = polygons.length - 1; i > 0; i--) {
// apply threshold to polygons
Polygon[] polys = JTSUtils.getPolygons(polygons[i]);
for (int j = 0; j < polys.length; j++) {
polys[j] = getThresholdedPolygon(polys[j], thrsh);
subtract.add(polys[j]);
}
polygons[i] = GF.createMultiPolygon(polys);
// subtract accumulated polys from next highest area
Geometry diff = polygons[i-1];
for (Polygon p : subtract)
diff = diff.difference(p);
polygons[i-1] = JTSUtils.toMultiPolygon(diff);
}
// sort out bottom area
// can't have any holes smaller than thrsh
ArrayList<Polygon> add = new ArrayList<>();
ArrayList<Polygon> remain = new ArrayList<>();
Polygon[] polys = JTSUtils.getPolygons(polygons[0]);
for (Polygon p : polys) {
if(p.getArea() < thrsh){
add.add(p);
} else {
remain.add(p);
}
}
// add small polys to higher order
for (Polygon p : add) {
for (int i = 1; i < polygons.length; i++) {
MultiPolygon poly = polygons[i];
if(!poly.disjoint(p) || i == polygons.length - 1){
// make sure p gets added somewhere
polygons[i] = JTSUtils.toMultiPolygon(polygons[i].union(p));
break;
}
}
}
// group remaining first order polygons
polygons[0] = GF.createMultiPolygon(remain.toArray(new Polygon[0]));
}
private Polygon getThresholdedPolygon(Polygon poly, float thrsh){
// sort out holes
int rings = poly.getNumInteriorRing();
ArrayList<LinearRing> holes = new ArrayList<>(rings);
for (int j = 0; j < rings; j++) {
Coordinate[] coords = poly.getInteriorRingN(j).getCoordinates();
Polygon hole = GF.createPolygon(coords);
if(hole.getArea() >= thrsh)
holes.add(GF.createLinearRing(coords));
}
// create new polygon if holes changed
if(rings != holes.size()){
Coordinate[] coords = poly.getExteriorRing().getCoordinates();
LinearRing outline = GF.createLinearRing(coords);
LinearRing[] newHoles = holes.toArray(new LinearRing[0]);
poly = GF.createPolygon(outline, newHoles);
}
//check polygon
if(poly.getArea() < thrsh){
return GF.createPolygon(new Coordinate[0]);
}
return poly;
}
private Shape[] createShapes(MultiPolygon[] polygons){
Shape[] shps = new Shape[polygons.length];
for (int i = 0; i < polygons.length; i++) {
MultiPolygon poly = polygons[i];
shps[i] = JTSUtils.toShape(poly);
}
return shps;
}
public Shape[] getShapes() {
return shapes;
}
public MultiPolygon[] getPolygons(){
return polygons;
}
public void paint(Graphics2D g, AffineTransform transform, ColorMap colorMap){
for (int i = 0; i < shapes.length; i++) {
Shape s = shapes[i];
g.setColor(colorMap.getColor(i));
g.fill(transform.createTransformedShape(s));
}
}
/**
* @return the shell
*/
public MultiPolygon getShell() {
return shell;
}
public Envelope getEnvelope(){
return getShell().getEnvelopeInternal();
}
// public AreaSet getTranslated(final double dx, final double dy){
// System.out.println("before: " + isValid());
// System.out.println("translate - x: " + dx + " y: " + dy);
// AffineTransformation t;
//// t = AffineTransformation.translationInstance(dx, dy);
////// AffineTransform t;
////// t = AffineTransform.getTranslateInstance(dx, dy);
//// AreaSet as = getTransformed(t);
//
// CoordinateFilter cf = new CoordinateFilter() {
//
// @Override
// public void filter(Coordinate coord) {
// coord.x += dx;
// coord.y += dy;
// }
// };
//
// AreaSet as = copy();
// for (MultiPolygon poly : as.polygons) {
// poly.apply(cf);
// }
// as = new AreaSet(as.polygons); // rebuild shell and shapes
//
// System.out.println("after: " + as.isValid());
//
// return as;
// }
//
// public AreaSet getTransformed(AffineTransform t){
// System.out.println("before t: " + isValid());
// MultiPolygon[] tPolys = new MultiPolygon[shapes.length];
// for (int i = 0; i < shapes.length; i++) {
// tPolys[i] = JTSUtils.createMultiPolygon(t.createTransformedShape(shapes[i]));
// }
//
// return new AreaSet(tPolys);
// }
//
// public AreaSet getTransformed(AffineTransformation t){
// MultiPolygon[] tPolys = new MultiPolygon[polygons.length];
// for (int i = 0; i < polygons.length; i++) {
//// System.out.println("poly " + i + " valid: " + polygons[i].isValid());
// tPolys[i] = JTSUtils.toMultiPolygon(t.transform(polygons[i]));
//// System.out.println("poly still valid: " + tPolys[i].isValid());
// }
//
// return new AreaSet(tPolys);
// }
//
/**
* Creates an exact independent copy of this AreaSet.
* @return a copy of this AreaSet
*/
public AreaSet copy(){
MultiPolygon[] plgns = new MultiPolygon[polygons.length];
for (int i = 0; i < plgns.length; i++)
plgns[i] = (MultiPolygon) polygons[i].clone();
return new AreaSet(plgns);
}
public boolean isValid(){
if(!shell.isValid())
return false;
for (MultiPolygon poly : polygons) {
LineString[] l = JTSUtils.getLineStrings(poly);
for (LineString ls : l) {
if(!ls.isValid())
return false;
}
}
return true;
}
// the number of subareas in this area set
public int getResolution(){
return polygons.length;
}
/**
* First order moving average filter for closed loops.
*/
private class ComponentMovingAverageFilter implements GeometryComponentFilter {
Geometry preserve;
int order;
/**
* Creates a new moving average filter with the given order.
* Any points that intersect the preserve shape are left unchanged.
* @param preserve the preserve shape
*/
public ComponentMovingAverageFilter(int order, Geometry preserve) {
this.preserve = preserve;
this.order = order;
}
@Override
public void filter(Geometry geom) {
geom.apply(new CoordinateMovingAverageFilter(preserve, geom, order));
}
}
private class CoordinateMovingAverageFilter implements CoordinateSequenceFilter{
Geometry preserve;
Coordinate[] cCoords;
final int order;
boolean done = false;
boolean closed;
int window;
public CoordinateMovingAverageFilter(Geometry preserve, Geometry geom, int order) {
cCoords = ((Geometry)geom.clone()).getCoordinates();
if(cCoords.length > 0)
closed = cCoords[0].equals(cCoords[cCoords.length - 1]);
else
closed = false;
this.preserve = preserve;
this.order = order;
}
@Override
public void filter(CoordinateSequence seq, int i) {
done = i == seq.size() - 1;
if(done){
if(closed){
seq.setOrdinate(i, 0, seq.getX(0));
seq.setOrdinate(i, 1, seq.getY(0));
}
return;
}
Point p = GF.createPoint(seq.getCoordinate(i));
if(preserve.contains(p))
return;
window = order;
window = Math.min(window, seq.size() / 2 - 1); // restric for small loops
if(closed){
window = Math.min(window, i);
window = Math.min(window, seq.size() - i - 2);
}
double xMean = cCoords[i].x;
double yMean = cCoords[i].y;
int index;
for (int j = 1; j <= window; j++) {
index = i - j;
if(index < 0)
index += seq.size() - 1;
Coordinate cDown = cCoords[index];
xMean += cDown.x;
yMean += cDown.y;
index = i + j;
if(index >= seq.size())
index -= seq.size() - 1;
Coordinate cUp = cCoords[index];
xMean += cUp.x;
yMean += cUp.y;
if(preserve.contains(GF.createPoint(cDown)) ||
preserve.contains(GF.createPoint(cUp))) {
window = j;
break;
}
}
int div = 2 * window + 1;
xMean /= div;
yMean /= div;
seq.setOrdinate(i, 0, xMean);
seq.setOrdinate(i, 1, yMean);
}
@Override
public boolean isDone() {
return done;
}
@Override
public boolean isGeometryChanged() {
return true;
}
}
}