Rev 2370 |
Blame |
Compare with Previous |
Last modification |
View Log
| RSS feed
/*
* 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].
equals(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).
equals(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))) {
// 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
);
}
int x0 = ends
[0].
getLongitude();
int y0 = ends
[0].
getLatitude();
int x1 = ends
[1].
getLongitude();
int y1 = ends
[1].
getLatitude();
int dx = x1 - x0
;
int dy = y1 - y0
;
double[] t =
{0,
1};
int p = -dx
;
int q = -
(a.
getMinLong() - x0
);
boolean scrap = checkSide
(t, p, q
);
if (scrap
) return null;
p = dx
;
q = a.
getMaxLong() - x0
;
scrap = checkSide
(t, p, q
);
if (scrap
) return null;
p = -dy
;
q = -
(a.
getMinLat() - y0
);
scrap = checkSide
(t, p, q
);
if (scrap
) return null;
p = dy
;
q = a.
getMaxLat() - 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 =
new Coord
(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.
equals(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 =
new Coord
(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.
equals(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].
equals(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;
}
}