All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opentripplanner.common.geometry.RecursiveGridIsolineBuilder Maven / Gradle / Ivy

There is a newer version: 2.5.0
Show newest version
package org.opentripplanner.common.geometry;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.locationtech.jts.algorithm.CGAlgorithms;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.Polygon;

/**
 * Compute isoline based on a zFunc and a set of initial coverage points P0={(x,y)} to seed the
 * computation.
 * 
 * This assume we have a z = Fz(x,y) function which can gives a z value for any given point (x,y), z
 * can be undefined for some region.
 * 
 * There are many tricks to reduce to the minimum the numbers of Fz samplings, explained in the code
 * below.
 * 
 * It will compute an isoline for a given z0 value. The isoline is composed of a list of n polygons,
 * CW for normal polygons, CCW for "holes". The isoline computation can be called multiple times on
 * the same builder for different z0 values: this will reduce the number of Fz sampling as they are
 * cached in the builder. Please note that the initial covering points must touch all isolines you
 * want to cover.
 * 
 * @author laurent
 */
public class RecursiveGridIsolineBuilder {

    public interface ZFunc {
        public long z(Coordinate c);
    }

    public enum Direction {
        UP, DOWN, LEFT, RIGHT;
    }

    /**
     * A fast XY-index for tiles, allowing to use maps.
     */
    private static class XYIndex {

        private int xIndex;

        private int yIndex;

        private XYIndex(int xIndex, int yIndex) {
            this.xIndex = xIndex;
            this.yIndex = yIndex;
        }

        private final XYIndex translated(int dx, int dy) {
            return new XYIndex(this.xIndex + dx, this.yIndex + dy);
        }

        @Override
        public final int hashCode() {
            return xIndex >> 16 | yIndex;
        }

        @Override
        public final boolean equals(Object other) {
            if (other instanceof XYIndex) {
                XYIndex otherTileIndex = (XYIndex) other;
                return otherTileIndex.xIndex == this.xIndex && otherTileIndex.yIndex == this.yIndex;
            }
            return false;
        }

        @Override
        public final String toString() {
            return "(" + xIndex + "," + yIndex + ")";
        }
    }

    private static class GridDot {
        private XYIndex index;

        private long z;

        private GridEdge up, down, left, right;

        private GridDot(XYIndex index, long z) {
            this.index = index;
            this.z = z;
        }

        @Override
        public final int hashCode() {
            return index.hashCode();
        }

        @Override
        public final boolean equals(Object other) {
            if (other instanceof GridDot) {
                // Only compare position, not Z value which should be identical
                return this.index.equals(((GridDot) other).index);
            }
            return false;
        }

        @Override
        public final String toString() {
            return "[Dot" + index + "," + z + "]";
        }
    }

    private static class GridEdge {
        // For horizontal edges, A is left and B is right
        // For vertical edges, A is bottom and B is top
        private GridDot A, B;

        boolean horizontal;

        int size;

        boolean used = false;

        private GridEdge(GridDot A, GridDot B, int size, boolean horizontal) {
            // We accept A and B in any order. They must be correctly placed however
            if (horizontal && A.index.xIndex > B.index.xIndex || !horizontal
                    && A.index.yIndex > B.index.yIndex) {
                // Must swap A and B
                this.A = B;
                this.B = A;
            } else {
                this.A = A;
                this.B = B;
            }
            this.size = size;
            this.horizontal = horizontal;
            if (horizontal) {
                if (this.A.index.yIndex != this.B.index.yIndex)
                    throw new AssertionError(
                            "Building horizontal edge with non horizontally-aligned dots");
                if (this.B.index.xIndex - this.A.index.xIndex != size)
                    throw new AssertionError(
                            "Building horizontal edge with incorrect size vs dot spacing");
            } else {
                if (this.A.index.xIndex != this.B.index.xIndex)
                    throw new AssertionError(
                            "Building vertical edge with non vertically-aligned dots");
                if (this.B.index.yIndex - this.A.index.yIndex != size)
                    throw new AssertionError(
                            "Building vertical edge with incorrect size vs dot spacing");
            }
        }

        private final void indexEndPoints() {
            if (size != 1)
                throw new AssertionError("Can't dot-index edge with size != 1");
            if (horizontal) {
                A.right = this;
                B.left = this;
            } else {
                A.up = this;
                B.down = this;
            }
        }

        private final int cut(long z0) {
            if (A.z < z0 && z0 <= B.z)
                return 1;
            if (B.z < z0 && z0 <= A.z)
                return -1;
            return 0;
        }

        @Override
        public final int hashCode() {
            // Normally size does not matter as we won't
            // keep edges from different sizes in the same set/map
            // horizontality matter, though.
            return A.hashCode() + size + (horizontal ? 0x8000 : 0);
        }

        @Override
        public final boolean equals(Object other) {
            if (other instanceof GridEdge) {
                // Only compare A position, size and horizontality
                GridEdge otherEdge = (GridEdge) other;
                return A.equals(otherEdge.A) && size == otherEdge.size
                        && horizontal == otherEdge.horizontal;
            }
            return false;
        }

        @Override
        public final String toString() {
            return "[" + (horizontal ? "H" : "V") + "-Edge" + A + "-" + B + " L=" + size + "]";
        }
    }

    private static final Logger LOG = LoggerFactory.getLogger(RecursiveGridIsolineBuilder.class);

    /**
     * Size (in index-unit) of initial (seed) sampling. This *MUST* be a power of 2. Good values are
     * 4 (slower but miss less small islands) or 8 (faster but miss more small islands).
     */
    private static final int SIZE_0 = 4;

    private ZFunc fz;

    private int fzInterpolateCount;

    private double dX, dY;

    private Coordinate center;

    private Map allDots;

    private Set initialDots;

    private List initialEdges;

    private boolean debugSeedGrid = false;

    private boolean debugCrossingEdges = false;

    private Geometry debugGeometry = null;
        
    // private List p0List;

    /**
     * Create an object to compute isochrones. One may call several time isochronify on the same
     * IsoChronificator object, this will re-use the z = f(x,y) sampling if possible, as they are
     * kept in cache.
     * 
     * @param center Center point (eg origin)
     * @param fz Function returning the z-value for a xy-coordinate
     * @param p0List Initial set of coverage points to seed the heuristics
     */
    public RecursiveGridIsolineBuilder( double dX, double dY, Coordinate center, ZFunc fz,
            List p0List) {
       
    	
    	
    	this.dX = dX;
        this.dY = dY;
        /*
         * Center position only purpose is to serve as a reference value to the XY integer indexing,
         * so it only needs to be not too far off to prevent int indexes from overflowing.
         */
        this.center = center;
        // this.p0List = p0List;

        LOG.debug("Center={} dX={} dY={}", this.center, dX, dY);
        this.fz = fz;
        /* Step 1. SEED (1). Compute initial set of dots. */
        allDots = new HashMap(p0List.size() / 2);
        initialDots = new HashSet(p0List.size() / 2);
        for (Coordinate p0 : p0List) {
            XYIndex index0 = getIndex(p0, SIZE_0);
            for (int dx = -SIZE_0; dx <= SIZE_0; dx += SIZE_0) {
                for (int dy = -SIZE_0; dy <= SIZE_0; dy += SIZE_0) {
                    // This will always create initial dots
                    GridDot A = getOrCreateDot(index0.translated(dx, dy));
                    initialDots.add(A);
                }
            }
        }
        /*
         * Step 2. SEED (2). Create initial edges from initial dots. There will be slightly less
         * edges than dots.
         */
        initialEdges = new ArrayList(initialDots.size());
        for (GridDot A : initialDots) {
            // Horizontal
            GridDot B = allDots.get(A.index.translated(SIZE_0, 0));
            if (B != null) {
                GridEdge e = new GridEdge(A, B, SIZE_0, true);
                initialEdges.add(e);
            }
            // Vertical
            B = allDots.get(A.index.translated(0, SIZE_0));
            if (B != null) {
                GridEdge e = new GridEdge(A, B, SIZE_0, false);
                initialEdges.add(e);
            }
        }
        LOG.debug("Created {} dots and {} edges out of {} initial points.", initialDots.size(),
                initialEdges.size(), p0List.size());
    }

    public void setDebugSeedGrid(boolean debugSeedGrid) {
        this.debugSeedGrid = debugSeedGrid;
    }

    public void setDebugCrossingEdges(boolean debugCrossingEdges) {
        this.debugCrossingEdges = debugCrossingEdges;
    }

    public Geometry computeIsoline(long z0) {
        fzInterpolateCount = 0;
        GeometryFactory geomFactory = new GeometryFactory();

        /*
         * Step 3. DIVIDE. While there are cutting edges from size > 1 to divide, divide them in
         * two. Note: edgesToExpand only contains cutting edges.
         */
        Queue edgesToDivide = new ArrayDeque();
        edgesToDivide.addAll(initialEdges);
        Queue edgesToExpand = new ArrayDeque();
        while (!edgesToDivide.isEmpty()) {
            GridEdge e = edgesToDivide.remove();
            if (e.cut(z0) == 0)
                continue;
            int size2 = e.size / 2;
            XYIndex iC = e.horizontal ? e.A.index.translated(size2, 0) : e.A.index.translated(0,
                    size2);
            GridDot C = getOrCreateDot(iC);
            GridEdge e1 = new GridEdge(e.A, C, size2, e.horizontal);
            GridEdge e2 = new GridEdge(C, e.B, size2, e.horizontal);
            GridEdge eCut = e1.cut(z0) != 0 ? e1 : e2;
            if (eCut.cut(z0) == 0)
                throw new AssertionError("Edge MUST cut!");
            if (size2 == 1) {
                edgesToExpand.add(eCut);
            } else {
                edgesToDivide.add(eCut);
            }
        }

        /*
         * Step 4. EXPAND. While there are unprocessed cutting edges, expand them by looking around
         * them for cutting edges.
         */
        Set finalEdges = new HashSet();
        Set finalNonCuttingEdges = new HashSet();
        while (!edgesToExpand.isEmpty()) {
            GridEdge e = edgesToExpand.remove();
            if (finalEdges.add(e) == false) {
                // Since we may have duplicates in the toExpand Q,
                // we prune-out early processed elements.
                continue;
            }
            /**
             * Build the 6 remaining edges (e1..e6) around the 2 squares ABDC and DBFE touching e
             * 
             * 
             *   [Horizontal e]     [Vertical e]
             *   
             *    C--(e2)--D       D--(e3)--B--(e6)--F
             *    |        |       |        |        |
             *   (e1)     (e3)    (e2)     (e)      (e5)   
             *    |        |       |        |        |
             *    A--(e)---B       C--(e1)--A--(e4)--E   
             *    |        |       
             *   (e4)     (e6)   
             *    |        |
             *    E--(e5)--F
             * 
*/ XYIndex iC = e.horizontal ? e.A.index.translated(0, 1) : e.A.index.translated(-1, 0); XYIndex iD = e.horizontal ? e.B.index.translated(0, 1) : e.B.index.translated(-1, 0); XYIndex iE = e.horizontal ? e.A.index.translated(0, -1) : e.A.index.translated(1, 0); XYIndex iF = e.horizontal ? e.B.index.translated(0, -1) : e.B.index.translated(1, 0); GridDot C = getOrCreateDot(iC); GridDot D = getOrCreateDot(iD); GridDot E = getOrCreateDot(iE); GridDot F = getOrCreateDot(iF); GridEdge[] e2List = new GridEdge[] { new GridEdge(e.A, C, 1, !e.horizontal), new GridEdge(C, D, 1, e.horizontal), new GridEdge(e.B, D, 1, !e.horizontal), new GridEdge(e.A, E, 1, !e.horizontal), new GridEdge(E, F, 1, e.horizontal), new GridEdge(e.B, F, 1, !e.horizontal) }; for (GridEdge e2 : e2List) { if (e2.cut(z0) == 0) { finalNonCuttingEdges.add(e2); continue; // Do not cut, OK } if (finalEdges.contains(e2)) continue; // Already processed edgesToExpand.add(e2); } } /* * Note: Here finalEdges only contains cutting edges, and should end up containing all * cutting edges that are discoverable. */ for (GridEdge e : finalEdges) { e.indexEndPoints(); } for (GridEdge e : finalNonCuttingEdges) { e.indexEndPoints(); } // For later logs. int finalEdgesSize = finalEdges.size(); int finalNonCuttingEdgesSize = finalNonCuttingEdges.size(); /* * Step 5. BUILD. Build polygons from finalEdges set. */ List debugGeom = new ArrayList(); if (debugSeedGrid) { for (GridEdge e : initialEdges) { Coordinate A = getCoordinate(e.A.index); Coordinate B = getCoordinate(e.B.index); debugGeom.add(geomFactory.createLineString(new Coordinate[] { A, B })); } // for (Coordinate p0 : p0List) { // debugGeom.add(geomFactory.createPoint(p0)); // } } if (debugCrossingEdges) { for (GridEdge e : finalEdges) { Coordinate A = getCoordinate(e.A.index); Coordinate B = getCoordinate(e.B.index); Coordinate C = interpolate(A, B, e.A.z, e.B.z, z0); Coordinate C1 = new Coordinate(C.x + (B.y - A.y) * 0.1, C.y + (B.x - A.x) * 0.1); Coordinate C2 = new Coordinate(C.x - (B.y - A.y) * 0.1, C.y - (B.x - A.x) * 0.1); debugGeom .add(geomFactory.createLineString(new Coordinate[] { A, C, C1, C2, C, B })); } } List retval = new ArrayList(); List rings = new ArrayList(); while (!finalEdges.isEmpty()) { GridEdge e0 = finalEdges.iterator().next(); List polyPoints = new ArrayList(); int cut = e0.cut(z0); Direction direction = e0.horizontal ? (cut > 0 ? Direction.UP : Direction.DOWN) : (cut > 0 ? Direction.LEFT : Direction.RIGHT); GridEdge e = e0; while (true) { // Add a point to polyline Coordinate cA = getCoordinate(e.A.index); Coordinate cB = getCoordinate(e.B.index); Coordinate cC = interpolate(cA, cB, e.A.z, e.B.z, z0); polyPoints.add(cC); e.used = true; finalEdges.remove(e); // Compute next edge from adjacent tile // Here e1 is always on e.A side, e2 on e.B side // and e3 on opposite side of tile from e. GridEdge e1, e2, e3; Direction d1, d2; // d3: same direction. switch (direction) { default: // Never happen case UP: e1 = e.A.up; d1 = Direction.LEFT; e2 = e.B.up; d2 = Direction.RIGHT; e3 = e1.B.right; break; case DOWN: e1 = e.A.down; d1 = Direction.LEFT; e2 = e.B.down; d2 = Direction.RIGHT; e3 = e1.A.right; break; case LEFT: e1 = e.A.left; d1 = Direction.DOWN; e2 = e.B.left; d2 = Direction.UP; e3 = e1.A.up; break; case RIGHT: e1 = e.A.right; d1 = Direction.DOWN; e2 = e.B.right; d2 = Direction.UP; e3 = e1.B.up; break; } boolean ok1 = e1.cut(z0) != 0 && !e1.used; boolean ok2 = e2.cut(z0) != 0 && !e2.used; boolean ok3 = e3.cut(z0) != 0 && !e3.used; if (ok1 && ok2) { /* * We can go either turn, pick the best one from e1 or e2 by looking if C lies * closer to e.A or e.B. Please note this is approximate only, as we should take * into account real interpolated position on e1 and e2 to compute segment * lenght. But this gives good approximated results and is probably sufficient * given the approximated solution anyway. */ double dA = Math.max(Math.abs(cA.x - cC.x), Math.abs(cA.y - cC.y)); double dB = Math.max(Math.abs(cB.x - cC.x), Math.abs(cB.y - cC.y)); if (dA <= dB) { // C closer to A e = e1; direction = d1; } else { // C closer to B e = e2; direction = d2; } } else if (ok1) { e = e1; direction = d1; } else if (ok2) { e = e2; direction = d2; } else if (ok3) { e = e3; // Same direction as before } else { // This must be the end of the polyline... break; } } // Close the polyline polyPoints.add(polyPoints.get(0)); LinearRing ring = geomFactory.createLinearRing(polyPoints .toArray(new Coordinate[polyPoints.size()])); rings.add(ring); } retval.addAll(punchHoles(geomFactory, rings)); LOG.info("Isochrones: {}+{} Fz samples, {} cutting edges, {} non-cutting edges", allDots.size(), fzInterpolateCount, finalEdgesSize, finalNonCuttingEdgesSize); /* * Step 6. CLEAN. Remove edge index from dots. */ for (GridDot A : allDots.values()) { A.up = A.down = A.right = A.left = null; } debugGeometry = geomFactory.createGeometryCollection(debugGeom .toArray(new Geometry[debugGeom.size()])); return geomFactory.createGeometryCollection(retval.toArray(new Geometry[retval.size()])); } public final Geometry getDebugGeometry() { return debugGeometry; } private final XYIndex getIndex(Coordinate p, int size) { int xIndex = (int) Math.round((p.x - center.x) / dX); int yIndex = (int) Math.round((p.y - center.y) / dY); if (size != 1) { int size2 = size / 2; xIndex = (xIndex + (xIndex > 0 ? size2 : -size2)) / size * size; yIndex = (yIndex + (yIndex > 0 ? size2 : -size2)) / size * size; } return new XYIndex(xIndex, yIndex); } private final Coordinate getCoordinate(XYIndex index) { return new Coordinate(index.xIndex * dX + center.x, index.yIndex * dY + center.y); } private GridDot getOrCreateDot(XYIndex xyIndex) { GridDot A = allDots.get(xyIndex); if (A == null) { A = new GridDot(xyIndex, fz.z(getCoordinate(xyIndex))); allDots.put(xyIndex, A); } return A; } private final Coordinate interpolate(Coordinate A, Coordinate B, long zA, long zB, long z0) { int n = 0; while (n < 3 && (zA == Long.MAX_VALUE || zB == Long.MAX_VALUE)) { Coordinate C = new Coordinate((A.x + B.x) / 2.0, (A.y + B.y) / 2.0); long zC = fz.z(A); fzInterpolateCount++; if (zA == Long.MAX_VALUE && z0 <= zC) { A = C; zA = zC; } else { B = C; zB = zC; } n++; } // Take as fallback position if we are still at +inf at one end z0 * 2 if (zA == Long.MAX_VALUE) zA = z0 * 2; if (zB == Long.MAX_VALUE) zB = z0 * 2; double k = zB == zA ? 0.5 : (z0 - zA) / (double) (zB - zA); Coordinate C = new Coordinate(A.x * (1.0 - k) + B.x * k, A.y * (1.0 - k) + B.y * k); return C; } @SuppressWarnings("unchecked") private final List punchHoles(GeometryFactory geomFactory, List rings) { List shells = new ArrayList(rings.size()); List holes = new ArrayList(rings.size() / 2); // 1. Split the polygon list in two: shells and holes (CCW and CW) for (LinearRing ring : rings) { if (CGAlgorithms.signedArea(ring.getCoordinateSequence()) > 0.0) holes.add(ring); else shells.add(geomFactory.createPolygon(ring)); } // 2. Sort the shells based on number of points to optimize step 3. Collections.sort(shells, new Comparator() { @Override public int compare(Polygon o1, Polygon o2) { return o2.getNumPoints() - o1.getNumPoints(); } }); for (Polygon shell : shells) { shell.setUserData(new ArrayList()); } // 3. For each hole, determine which shell it fits in. for (LinearRing hole : holes) { outer: { // Probably most of the time, the first shell will be the one for (Polygon shell : shells) { if (shell.contains(hole)) { ((List) shell.getUserData()).add(hole); break outer; } } // This should not happen, but do not break bad here // as loosing a hole is not critical, we still have // sensible data to return. LOG.error("Cannot find fitting shell for a hole!"); } } // 4. Build the list of punched polygons List punched = new ArrayList(shells.size()); for (Polygon shell : shells) { List shellHoles = ((List) shell.getUserData()); punched.add(geomFactory.createPolygon((LinearRing) (shell.getExteriorRing()), shellHoles.toArray(new LinearRing[shellHoles.size()]))); } return punched; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy