Subversion Repositories mkgmap

Rev

Rev 4594 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * Copyright (C) 2019.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 or
 * 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.
 */

package uk.me.parabola.util;


import java.awt.geom.Path2D;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import java.util.Set;
import java.util.TreeMap;

import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.reader.osm.Way;

/**
 * Implements insideness tests for points, polyline and polygon. We distinguish
 * 3 cases for points: <br>
 * 1: the point is outside the polygon <br>
 * 2: the point is on the boundary of the polygon (or very close to it) <br>
 * 3: the point in inside the polygon
 *
 * We distinguish 6 cases for lines: <br>
 * 1: all of the line is outside the polygon <br>
 * 2: some of the line is outside and the rest touches or runs along the polygon
 * edge <br>
 * 3: all of the line runs along the polygon edge <br>
 * 4: some of the line is inside and the rest touches or runs along. <br>
 * 5: all of the line is inside the polygon <br>
 * 6: some is inside and some outside the polygon. Obviously some point is on
 * the polygon edge but we don't care if runs along the edge.
 *
 * @author Gerd Petermann
 *
 */

public class IsInUtil {
        private static final Logger log = Logger.getLogger(IsInUtil.class);
        public static final int IN = 0x01;
        public static final int ON = 0x02;
        public static final int OUT = 0x04;

        public static final int IN_ON_OUT = IN | ON | OUT;

        private IsInUtil() {
                // hide public constructor
        }

        public static void mergePolygons(Set<Way> polygons, List<List<Coord>> outers, List<List<Coord>> holes) {
                // combine all polygons which intersect the bbox of the element if possible
                Path2D.Double path = new Path2D.Double();
                for (Way polygon : polygons) {
                        path.append(Java2DConverter.createPath2D(polygon.getPoints()), false);
                }
                java.awt.geom.Area polygonsArea = new java.awt.geom.Area(path);
                List<List<Coord>> mergedShapes = Java2DConverter.areaToShapes(polygonsArea);

                // combination of polygons may contain holes. They are counter clockwise.
                for (List<Coord> shape : mergedShapes) {
                        (Way.clockwise(shape) ? outers : holes).add(shape);
                }
        }
                                       
        private enum IntersectionStatus {
                TOUCHING, CROSSING, SPLITTING, JOINING,SIMILAR, DOUBLE_SPIKE
        }
       
        private static final int EPS_HP = 4; // ~0.15 meters at equator
        private static final int EPS_HP_SQRD = EPS_HP * EPS_HP;
        private static final double EPS = 0.15; // meters. needed for distToLineSegment()
        private static final double EPS_OFF = EPS * 2;  

        public static int isLineInShape(List<Coord> lineToTest, List<Coord> shape, Area elementBbox) {
                final int n = lineToTest.size();
                int status = isPointInShape(lineToTest.get(0), shape);
                BitSet onBoundary = new BitSet();
               
                for (int i = 0; i < shape.size() - 1; i++) {
                        Coord p11 = shape.get(i);
                        Coord p12 = shape.get(i + 1);
                        if (p11.distanceInHighPrecSquared(p12) < EPS_HP_SQRD) { // skip very short segments
                                continue;
                        }
                        // check if shape segment is clearly below, above, right or left of bbox
                        if ((Math.min(p11.getLatitude(), p12.getLatitude()) > elementBbox.getMaxLat() + 1)
                                        || (Math.max(p11.getLatitude(), p12.getLatitude()) < elementBbox.getMinLat() - 1)
                                        || (Math.min(p11.getLongitude(), p12.getLongitude()) > elementBbox.getMaxLong() + 1)
                                        || (Math.max(p11.getLongitude(), p12.getLongitude()) < elementBbox.getMinLong() - 1))
                                continue;
                        for (int k = 0; k < n - 1; k++) {
                                Coord p21 = lineToTest.get(k);
                                Coord p22 = lineToTest.get(k + 1);
                                if (p21.distanceInHighPrecSquared(p22) < EPS_HP_SQRD) { // skip very short segments
                                        continue;
                                }
                                Coord inter = Utils.getSegmentSegmentIntersection(p11, p12, p21, p22);
                                if (inter != null) {
                                        // segments have at least one common point
                                        boolean isCrossing = false;
                                        if (inter.distanceInHighPrecSquared(p21) < EPS_HP_SQRD) {
                                                onBoundary.set(k);
                                                if (k == 0) {
                                                        status |= ON;
                                                } else {
                                                        if (p21.distanceInHighPrecSquared(p11) < EPS_HP_SQRD) {
                                                                Coord p20 = lineToTest.get(k - 1);
                                                                Coord p10 = shape.get(i - 1 >= 0 ? i - 1 : shape.size() - 2);
                                                                IntersectionStatus x = analyseCrossingInPoint(p11, p20, p22, p10, p12);
                                                                Coord pTest = null;
                                                                if (x == IntersectionStatus.CROSSING) {
                                                                        isCrossing = true;
                                                                } else if (x == IntersectionStatus.JOINING) {
                                                                        if (!isOnOrCloseToEdgeOfShape(shape, p21, p20)) {
                                                                                pTest = p21.destOnRhumbLine(EPS_OFF, p21.bearingTo(p20));
                                                                        }
                                                                } else if (x == IntersectionStatus.SPLITTING) {
                                                                        if (!isOnOrCloseToEdgeOfShape(shape, p21, p22)) {
                                                                                pTest = p21.destOnRhumbLine(EPS_OFF, p21.bearingTo(p22));
                                                                        }
                                                                }
                                                                if (pTest != null) {
                                                                        int testStat = isPointInShape(pTest, shape);
                                                                        status |= testStat;
                                                                        if ((status | ON) == IN_ON_OUT)
                                                                                return IN_ON_OUT;
                                                                }
                                                        } else if (p21.distanceInHighPrecSquared(p12) < EPS_HP_SQRD) {
                                                                // handled in next iteration (k+1) or (i+1)b
                                                        } else {
                                                                // way segment starts on a shape segment
                                                                // somewhere between p11 and p12
                                                                // it may cross the shape or just touch it,
                                                                // check if previous way segment is on the same
                                                                // side or not
                                                                long isLeftPrev = lineToTest.get(k-1).isLeft(p11, p12);
                                                                long isLeftNext = p22.isLeft(p11, p12);
                                                                if (isLeftPrev< 0 && isLeftNext > 0 || isLeftPrev > 0 && isLeftNext < 0) {
                                                                        // both way segments are not on the shape
                                                                        // segment and they are on different sides
                                                                        isCrossing = true;
                                                                }
                                                        }
                                                }
                                        } else if (inter.distanceInHighPrecSquared(p22) < EPS_HP_SQRD) {
                                                onBoundary.set(k + 1);
                                                // handle intersection on next iteration
                                        } else if (inter.distanceInHighPrecSquared(p11) < EPS_HP_SQRD || inter.distanceInHighPrecSquared(p12) < EPS_HP_SQRD) {
                                                // intersection is very close to end of shape segment
                                                if (inter.distToLineSegment(p21, p22) > EPS)
                                                        isCrossing = true;
                                        } else {
                                                isCrossing = true;
                                        }
                                        if (isCrossing) {
                                                // real intersection found
                                                return IN_ON_OUT;
                                        }
                                }
                        }
                }
               
                if (!onBoundary.isEmpty())
                        status |= ON;
                if (status == ON) {
                        // found no intersection and first point is on boundary
                        while (onBoundary.cardinality() != n) {
                                int pos = onBoundary.nextClearBit(0);
                                // return result for first point which is not on boundary
                                Coord pTest = lineToTest.get(pos);
                                status |= isPointInShape(pTest, shape);
                                if (status == ON) {
                                        onBoundary.set(pos);
                                        continue;
                                }
                                return status;
                        }
                        status |= checkAllOn(lineToTest, shape);
                }
                return status;
        }


        /**
         * Handle special case that all points of {@code lineToTest} are on the edge of shape
         * @param lineToTest
         * @param shape
         * @return
         */

        private static int checkAllOn(List<Coord> lineToTest, List<Coord> shape) {
                int n = lineToTest.size();
                // all points are on boundary
                for (int i = 0; i < n-1; i++) {
                        Coord p1 = lineToTest.get(i);
                        Coord p2 = lineToTest.get(i + 1);
                        if (!isOnOrCloseToEdgeOfShape(shape, p1, p2)) {
                                Coord pTest = p1.destOnRhumbLine(EPS_OFF, p1.bearingTo(p2));
                                int resMidPoint = isPointInShape(pTest, shape);
                                if (resMidPoint != ON)
                                        return resMidPoint;
                        }
                }
                return ON;
        }

        /**
         * two line-strings a-s-c and x-s-y the same mid point. Check if they are crossing. This is the case
         * if a-s-c is between x-s-y or if x-s-y is between a-s-c.
         * @param s the share point
         * @param a 1st point 1st line-string
         * @param b 2nd point 1st line-string
         * @param x 1st point 2nd line-string
         * @param y 2nd point 2nd line-string
         * @return kind of crossing or touching
         */

        private static IntersectionStatus analyseCrossingInPoint(Coord s, Coord a, Coord b, Coord x, Coord y) {
                TreeMap<Long, Character> map = new TreeMap<>();
                long ba = Math.round(s.bearingTo(a) * 1000);
                long bb = Math.round(s.bearingTo(b) * 1000);
                long bx = Math.round(s.bearingTo(x) * 1000);
                long by = Math.round(s.bearingTo(y) * 1000);
                map.put(ba, 'a');
                map.put(bb, 'b');
                map.put(bx, 'x');
                map.put(by, 'y');
                List<Character> sortedByBearing = new ArrayList<>(map.values());
                int apos = sortedByBearing.indexOf('a');
                int bpos = sortedByBearing.indexOf('b');
                int xpos = sortedByBearing.indexOf('x');
                int ypos = sortedByBearing.indexOf('y');
               
                if (map.size() == 4) {
                        if (Math.abs(xpos-ypos) == 2) {
                                // pair xy is either on 0 and 2 or 1 and 3, so only one of a and b is between them
                                // shape segments x-s-y is nether between nor outside of way segments a-s-b
                                return IntersectionStatus.CROSSING;
                        }
                        return IntersectionStatus.TOUCHING;
                }

                if (map.size() == 3) {
                        if (xpos < 0) {
                                // x-s-y is a spike that touches a-s-b
                                return IntersectionStatus.TOUCHING;
                        }
                        if (bpos < 0) {
                                // either s-x or s-y is overlaps s-b
                                return IntersectionStatus.JOINING;
                        }
                        if (ba == bx || ba == by) {
                                return IntersectionStatus.SPLITTING;
                        }
                        return IntersectionStatus.TOUCHING;
                }
                if (map.size() == 2) {
                        if (apos > 0 || bpos > 0) {
                                // two spikes meeting
                                return IntersectionStatus.TOUCHING;
                        }
                        // a-s-b and x-s-y are overlapping (maybe have different directions)
                        return IntersectionStatus.SIMILAR;
                }
                // both a-s-b and x-s-y come from and go to the same direction
                return IntersectionStatus.DOUBLE_SPIKE;
        }

        /**
         * Check if the sequence p1-p2 or p2-p1 appears in the shape or if there is only one point c between and the sequence p1-c-p2
         * is nearly straight.
         * @param shape list of points describing the shape
         * @param p1 first point
         * @param p2 second point
         * @return true if the sequence p1-p2 or p2-p1 appears in the shape or if there is only one point c between and the sequence p1-c-p2
         * is nearly straight, else false.
         */

        private static boolean isOnOrCloseToEdgeOfShape(List<Coord> shape, Coord p1, Coord p2) {
                for (int i  = 0; i < shape.size(); i++) {
                        Coord p = shape.get(i);
                        if (p.distanceInHighPrecSquared(p1) >= EPS_HP_SQRD)
                                continue;
                       
                        int posPrev = i > 0 ? i - 1 : shape.size() - 2;
                        int posNext = i < shape.size() - 1 ? i + 1 : 1;
                        if (shape.get(posPrev).distanceInHighPrecSquared(p2) < EPS_HP_SQRD || shape.get(posNext).distanceInHighPrecSquared(p2) < EPS_HP_SQRD)
                                return true;

                        int posPrev2 = posPrev > 0 ? posPrev - 1 : shape.size() - 2;
                        int posNext2 = posNext < shape.size() - 1 ? posNext + 1 : 1;
                        if (shape.get(posPrev2).distanceInHighPrecSquared(p2) < EPS_HP_SQRD && Math.abs(Utils.getAngle(p1, shape.get(posPrev), p2)) < 0.1) {
                                // shape segments between p1 and p2 are almost straight
                                return true;
                        }
                        if (shape.get(posNext2).distanceInHighPrecSquared(p2) < EPS_HP_SQRD && Math.abs(Utils.getAngle(p1, shape.get(posNext), p2)) < 0.1) {
                                // shape segments between p1 and p2 are almost straight
                                return true;
                        }
                }
               
                return false;
        }

        /**
         * Check if node is in polygon using crossing number counter, with some some tolerance
         * @param node the point to test
         * @param shape list of points describing the polygon
         * @return IN/ON/OUT
         */

        public static int isPointInShape(Coord node, List<Coord> shape) {
                        final int nodeLat = node.getHighPrecLat();
                final int nodeLon = node.getHighPrecLon();
                if (log.isDebugEnabled()) {
                        log.debug("node", node, nodeLon, nodeLat, shape.size(), shape);
                }
                int trailLat = 0, trailLon = 0;
                int lhsCount = 0, rhsCount = 0; // count both, to be sure
                int minLat, maxLat, minLon, maxLon;
                double lonDif, latDif, distSqrd;
                boolean subsequent = false;
                for (Coord leadCoord : shape) {
                        final int leadLat = leadCoord.getHighPrecLat();
                        final int leadLon = leadCoord.getHighPrecLon();
                        if (subsequent) { // use first point as trailing (poly is closed)
                                if (leadCoord.distanceInHighPrecSquared(node) < EPS_HP_SQRD)
                                        return ON;
                                if (leadLat < trailLat) {
                                        minLat = leadLat;
                                        maxLat = trailLat;
                                } else {
                                        minLat = trailLat;
                                        maxLat = leadLat;
                                }
                                if (leadLon < trailLon) {
                                        minLon = leadLon;
                                        maxLon = trailLon;
                                } else {
                                        minLon = trailLon;
                                        maxLon = leadLon;
                                }
                                if (minLat - EPS_HP > nodeLat) {
                                        // line segment is all slightly above, ignore
                                } else if (maxLat + EPS_HP < nodeLat) {
                                        // line segment is all slightly below, ignore
                                } else if (minLon - EPS_HP > nodeLon && minLat < nodeLat && maxLat > nodeLat) {
                                        ++rhsCount; // definite line segment all slightly to the right
                                } else if (maxLon + EPS_HP < nodeLon && minLat < nodeLat && maxLat > nodeLat) {
                                        ++lhsCount; // definite line segment all slightly to the left
                                } else { // need to consider this segment more carefully.
                                        if (leadLat == trailLat)
                                                lonDif = Double.POSITIVE_INFINITY; // horizontal lines ignored in crossing calc, infinity handled in distToLine calc
                                        else
                                                lonDif = nodeLon - trailLon - (double)(nodeLat - trailLat) / (leadLat - trailLat) * (leadLon - trailLon);
                                        if ((minLon - EPS_HP <= nodeLon) && (maxLon + EPS_HP >= nodeLon)) { // check if the point is ON the line
                                                if (leadLon == trailLon)
                                                        latDif = Double.POSITIVE_INFINITY; // handled in distToLine calc
                                                else
                                                        latDif = nodeLat - trailLat - (double)(nodeLon - trailLon) / (leadLon - trailLon) * (leadLat - trailLat);
                                                // calculate distance to segment using right-angle attitude theorem
                                                log.debug("inBox", leadLon-nodeLon, leadLat-nodeLat, trailLon-nodeLon, trailLat-nodeLat, lonDif, latDif, lhsCount, rhsCount);
                                                // There can be a small area, within the square EPS_HP*2 around the node, but is not in the circle radius EPS_HP where a polygon
                                                // vertix meets this square, that will be incorrectly calculated as ON
                                                if (Double.isInfinite(lonDif))
                                                        distSqrd = latDif*latDif;
                                                else if (Double.isInfinite(latDif))
                                                        distSqrd = lonDif*lonDif;
                                                else if (Math.abs(lonDif) < EPS_HP || Math.abs(latDif) < EPS_HP)
                                                        return ON;
                                                else
                                                        distSqrd = lonDif*lonDif * latDif*latDif / (lonDif*lonDif + latDif*latDif);
                                                if (distSqrd < EPS_HP_SQRD)
                                                        return ON;
                                        } else
                                                log.debug("inSlice", leadLon-nodeLon, leadLat-nodeLat, trailLon-nodeLon, trailLat-nodeLat, lonDif, "N/A", lhsCount, rhsCount);
                                        if ((trailLat <= nodeLat && leadLat >  nodeLat) || //  an upward crossing
                                            (trailLat >  nodeLat && leadLat <= nodeLat)) { // a downward crossing
                                                if (lonDif < 0)
                                                        ++rhsCount; // a valid crossing right of nodeLon
                                                else
                                                        ++lhsCount;
                                        }
                                }
                        } // if not first Coord
                        subsequent = true;
                        trailLat = leadLat;
                        trailLon = leadLon;
                } // for leadCoord
                log.debug("lhs | rhs", lhsCount, rhsCount);
                assert (lhsCount & 1) == (rhsCount & 1) : "LHS: " + lhsCount + " RHS: " + rhsCount;
                return (rhsCount & 1) == 1 ? IN : OUT;
        }

}