Subversion Repositories mkgmap

Rev

Rev 4133 | 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].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]));
                        new0.setAddedByClipper(true);
                        // 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]));
                        new1.setAddedByClipper(true);
                       
                        // 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;
        }
}