/*
* Copyright (C) 2008 Steve Ratcliffe
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License 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.
*
*
* Author: Steve Ratcliffe
* Create date: 30-Jun-2008
*/
package uk.me.parabola.mkgmap.general;
import java.util.ArrayList;
import java.util.List;
import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
/**
* Routine to clip a polyline to a given bounding box.
* @author Steve Ratcliffe
* @see <a href="http://www.skytopia.com/project/articles/compsci/clipping.html">A very clear explaination of the Liang-Barsky algorithm</a>
*/
public class LineClipper {
/**
* Clips a polyline by the given bounding box. This may produce several
* separate lines if the line meanders in and out of the box.
* This will work even if no point is actually inside the box.
* @param a The bounding area.
* @param coords A list of the points in the line.
* @return Returns null if the line is completely in the bounding box and
* this is expected to be the normal case.
* If clipping is needed then an array of point lists is returned.
*/
public static List<List<Coord>> clip(Area a, List<Coord> coords) {
// If all the points are inside the box then we just return null
// to show that nothing was done and the line can be used. This
// is expected to be the normal case.
if (a == null || a.allInsideBoundary(coords))
return null;
class LineCollector {
private final List<List<Coord>> ret = new ArrayList<List<Coord>>(4);
private List<Coord> currentLine;
private Coord last;
public void add(Coord[] segment) {
if (segment == null) {
currentLine = null;
} else {
// we start a new line if there isn't a current one, or if the first
// point of the segment is not equal to the last one in the line.
if (currentLine == null || !segment[0].equals(last)) {
currentLine = new ArrayList<Coord>(5);
currentLine.add(segment[0]);
currentLine.add(segment[1]);
ret.add(currentLine);
} else {
currentLine.add(segment[1]);
}
last = segment[1];
}
}
}
LineCollector seg = new LineCollector();
// Step through each segment, clip it if necessary and create a list of
// lines from it.
for (int i = 0; i <= coords.size() - 2; i++) {
Coord[] pair = {coords.get(i), coords.get(i+1)};
if (pair[0].highPrecEquals(pair[1])) {
continue;
}
Coord[] clippedPair = clip(a, pair);
seg.add(clippedPair);
}
// in case the coords build a closed way the first and the last clipped line
// might have to be joined
if (seg.ret.size() >= 2 && coords.get(0) == coords.get(coords.size()-1)) {
List<Coord> firstSeg = seg.ret.get(0);
List<Coord> lastSeg = seg.ret.get(seg.ret.size()-1);
// compare the first point of the first segment with the last point of
// the last segment
if (firstSeg.get(0).equals(lastSeg.get(lastSeg.size()-1))) { //TODO : equal, ident or highPrecEqual?
// they are the same so the two segments should be joined
lastSeg.addAll(firstSeg.subList(1, firstSeg.size()));
seg.ret.remove(0);
}
}
return seg.ret;
}
public static Coord[] clip(Area a, Coord[] ends) {
return clip(a,ends,false);
}
/**
* A straight forward implementation of the Liang-Barsky algorithm as described
* in the referenced web page.
* @param a The clipping area.
* @param ends The start and end of the line the contents of this will
* be changed if the line is clipped to contain the new start and end
* points. A point that was inside the box will not be changed.
* @param nullIfInside true: returns null if all points are within the given area
* @return An array of the new start and end points if any of the line is
* within the box. If the line is wholly outside then null is returned.
* If a point is within the box then the same coordinate object will
* be returned as was passed in.
* @see <a href="http://www.skytopia.com/project/articles/compsci/clipping.html">Liang-Barsky algorithm</a>
*/
public static Coord[] clip(Area a, Coord[] ends, boolean nullIfInside) {
assert ends.length == 2;
if (a.insideBoundary(ends[0]) && a.insideBoundary(ends[1])) {
return (nullIfInside ? null : ends);
}
Coord lowerLeft = new Coord(a.getMinLat(),a.getMinLong());
Coord upperRight = new Coord(a.getMaxLat(),a.getMaxLong());
int x0 = ends[0].getHighPrecLon();
int y0 = ends[0].getHighPrecLat();
int x1 = ends[1].getHighPrecLon();
int y1 = ends[1].getHighPrecLat();
int dx = x1 - x0;
int dy = y1 - y0;
double[] t = {0, 1};
int p = -dx;
int q = -(lowerLeft.getHighPrecLon() - x0);
boolean scrap = checkSide(t, p, q);
if (scrap) return null;
p = dx;
q = upperRight.getHighPrecLon() - x0;
scrap = checkSide(t, p, q);
if (scrap) return null;
p = -dy;
q = -(lowerLeft.getHighPrecLat() - y0);
scrap = checkSide(t, p, q);
if (scrap) return null;
p = dy;
q = upperRight.getHighPrecLat() - y0;
scrap = checkSide(t, p, q);
if (scrap) return null;
assert t[0] >= 0;
assert t[1] <= 1;
Coord orig0 = ends[0];
Coord orig1 = ends[1];
if(ends[0].getOnBoundary()) {
// consistency check
assert a.onBoundary(ends[0]) : "Point marked as boundary node at " + ends[0].toString() + " not on boundary of [" + a.getMinLat() + ", " + a.getMinLong() + ", " + a.getMaxLat() + ", " + a.getMaxLong() + "]";
}
else if (t[0] > 0) {
// line requires clipping so create a new end point and if
// its position (in map coordinates) is different from the
// original point, use the new point as a boundary node
Coord new0 = Coord.makeHighPrecCoord(calcCoord(y0, dy, t[0]), calcCoord(x0, dx, t[0]));
// check the maths worked out
assert a.onBoundary(new0) : "New boundary point at " + new0.toString() + " not on boundary of [" + a.getMinLat() + ", " + a.getMinLong() + ", " + a.getMaxLat() + ", " + a.getMaxLong() + "]";
if(!new0.highPrecEquals(orig0))
ends[0] = new0;
ends[0].setOnBoundary(true);
}
else if(a.onBoundary(ends[0])) {
// point lies on the boundary so it's a boundary node
ends[0].setOnBoundary(true);
}
if(ends[1].getOnBoundary()) {
// consistency check
assert a.onBoundary(ends[1]) : "Point marked as boundary node at " + ends[1].toString() + " not on boundary of [" + a.getMinLat() + ", " + a.getMinLong() + ", " + a.getMaxLat() + ", " + a.getMaxLong() + "]";
}
else if (t[1] < 1) {
// line requires clipping so create a new end point and if
// its position (in map coordinates) is different from the
// original point, use the new point as a boundary node
Coord new1 = Coord.makeHighPrecCoord(calcCoord(y0, dy, t[1]), calcCoord(x0, dx, t[1]));
// check the maths worked out
assert a.onBoundary(new1) : "New boundary point at " + new1.toString() + " not on boundary of [" + a.getMinLat() + ", " + a.getMinLong() + ", " + a.getMaxLat() + ", " + a.getMaxLong() + "]";
if(!new1.highPrecEquals(orig1))
ends[1] = new1;
ends[1].setOnBoundary(true);
}
else if(a.onBoundary(ends[1])) {
// point lies on the boundary so it's a boundary node
ends[1].setOnBoundary(true);
}
// zero length segments can be created if one point lies on
// the boundary and the other is outside of the area
// try really hard to catch these as they will break the
// routing
// the check for t[0] >= t[1] should quickly find all the zero
// length segments but the extra check to see if the points
// are equal could catch the situation where although t[0] and
// t[1] differ, the coordinates come out the same for both
// points
if(t[0] >= t[1] || ends[0].highPrecEquals(ends[1]))
return null;
return ends;
}
private static int calcCoord(int base, int delta, double t) {
double d = 0.5;
double y = (base + t * delta);
return (int) ((y >= 0) ? y + d : y - d);
}
private static boolean checkSide(double[] t, double p, double q) {
double r = q/p;
if (p == 0) {
if (q < 0)
return true;
} else if (p < 0) {
if (r > t[1])
return true;
else if (r > t[0])
t[0] = r;
} else {
if (r < t[0])
return true;
else if (r < t[1])
t[1] = r;
}
return false;
}
}