/*
* 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;
import uk.me.parabola.mkgmap.reader.osm.FakeIdGenerator;
import net.sharenav.osmToShareNav.model.Bounds;
import net.sharenav.osmToShareNav.model.Node;
/**
* 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<ArrayList<Node>> clip(Bounds a, List<Node> 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)
return null;
boolean inBounds = true;
for (Node n : coords) {
if (!a.isIn(n.lat, n.lon)) {
inBounds = false;
}
}
if (inBounds)
return null;
class LineCollector {
private final ArrayList<ArrayList<Node>> ret = new ArrayList<ArrayList<Node>>(4);
private ArrayList<Node> currentLine;
private Node last;
public void add(Node[] 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<Node>(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++) {
Node[] pair = {coords.get(i), coords.get(i+1)};
Node[] clippedPair = clip(a, pair);
seg.add(clippedPair);
}
return seg.ret;
}
/**
* 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.
* @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 Node[] clip(Bounds a, Node[] ends) {
assert ends.length == 2;
float x0 = ends[0].lon;
float y0 = ends[0].lat;
float x1 = ends[1].lon;
float y1 = ends[1].lat;
float dx = x1 - x0;
float dy = y1 - y0;
double[] t = {0, 1};
float p = -dx;
float q = -(a.minLon - x0);
boolean scrap = checkSide(t, p, q);
if (scrap) return null;
p = dx;
q = a.maxLon - x0;
scrap = checkSide(t, p, q);
if (scrap) return null;
p = -dy;
q = -(a.minLat - y0);
scrap = checkSide(t, p, q);
if (scrap) return null;
p = dy;
q = a.maxLat - y0;
scrap = checkSide(t, p, q);
if (scrap) return null;
assert t[0] >= 0;
assert t[1] <= 1;
Node orig0 = ends[0];
Node orig1 = ends[1];
if (a.isOnBoundary(ends[0])) {
// consistency check
//assert a.onBoundary(ends[0]) : "Point marked as boundary node at " + ends[0].toString() + " not on boundary of [" + a.minLat + ", " + a.minLon + ", " + a.maxLat + ", " + a.maxLon + "]";
}
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
Node new0 = new Node(calcCoord(y0, dy, t[0]), calcCoord(x0, dx, t[0]), FakeIdGenerator.makeFakeId());
// check the maths worked out
if(!new0.equals(orig0))
ends[0] = new0;
//ends[0].setOnBoundary(true);
}
else if (a.isOnBoundary(ends[0])) {
// point lies on the boundary so it's a boundary node
//ends[0].setOnBoundary(true);
}
if (a.isOnBoundary(ends[1])) {
// consistency check
//assert a.onBoundary(ends[1]) : "Point marked as boundary node at " + ends[1].toString() + " not on boundary of [" + a.minLat + ", " + a.minLon + ", " + a.maxLat + ", " + a.maxLon + "]";
}
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
Node new1 = new Node(calcCoord(y0, dy, t[1]), calcCoord(x0, dx, t[1]), FakeIdGenerator.makeFakeId());
// check the maths worked out
//assert a.isOnBoundary(new1) : "New boundary point at " + new1.toString() + " not on boundary of [" + a.minLat + ", " + a.minLon + ", " + a.maxLat + ", " + a.maxLon + "]";
if(!new1.equals(orig1))
ends[1] = new1;
//ends[1].setOnBoundary(true);
}
else if(a.isOnBoundary(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].equals(ends[1]))
return null;
return ends;
}
private static float calcCoord(float base, float delta, double t) {
double d = 0.5;
double y = (base + t * delta);
return ((float) ((y >= 0f) ? 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;
}
}