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

com.conveyal.gtfs.util.PolylineUtils Maven / Gradle / Ivy

package com.conveyal.gtfs.util;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;

import java.util.ArrayList;
import java.util.List;

/**
 * Polyline utils class contains method that can decode/encode a polyline, simplify a line, and
 * more. This code is taken/derived from the mapbox-java project (MIT license):
 *
 * https://github.com/mapbox/mapbox-java/blob/master/services-geojson/src/main/java/com/mapbox/geojson/utils/PolylineUtils.java
 */
public final class PolylineUtils {

    private PolylineUtils() {
        // Prevent initialization of this class
    }

    // 1 by default (in the same metric as the point coordinates)
    private static final double SIMPLIFY_DEFAULT_TOLERANCE = 1;

    // False by default (excludes distance-based preprocessing step which leads to highest quality
    // simplification but runs slower)
    private static final boolean SIMPLIFY_DEFAULT_HIGHEST_QUALITY = false;

    /**
     * Decodes an encoded path string into a sequence of {@link Point}.
     *
     * @param encodedPath a String representing an encoded path string
     * @param precision   OSRMv4 uses 6, OSRMv5 and Google uses 5
     * @return list of {@link Point} making up the line
     * @see Part of algorithm came from this source
     * @see Part of algorithm came from this source.
     * @since 1.0.0
     */
    public static List decode(final String encodedPath, int precision) {
        GeometryFactory gf = new GeometryFactory();
        int len = encodedPath.length();

        // OSRM uses precision=6, the default Polyline spec divides by 1E5, capping at precision=5
        double factor = Math.pow(10, precision);

        // For speed we preallocate to an upper bound on the final length, then
        // truncate the array before returning.
        final List path = new ArrayList<>();
        int index = 0;
        int lat = 0;
        int lng = 0;

        while (index < len) {
            int result = 1;
            int shift = 0;
            int temp;
            do {
                temp = encodedPath.charAt(index++) - 63 - 1;
                result += temp << shift;
                shift += 5;
            }
            while (temp >= 0x1f);
            lat += (result & 1) != 0 ? ~(result >> 1) : (result >> 1);

            result = 1;
            shift = 0;
            do {
                temp = encodedPath.charAt(index++) - 63 - 1;
                result += temp << shift;
                shift += 5;
            }
            while (temp >= 0x1f);
            lng += (result & 1) != 0 ? ~(result >> 1) : (result >> 1);

            path.add(gf.createPoint(new Coordinate(lng / factor, lat / factor)));
        }

        return path;
    }

    /**
     * Encodes a sequence of Points into an encoded path string.
     *
     * @param path      list of {@link Point}s making up the line
     * @param precision OSRMv4 uses 6, OSRMv5 and Google uses 5
     * @return a String representing a path string
     * @since 1.0.0
     */
    public static String encode(final List path, int precision) {
        long lastLat = 0;
        long lastLng = 0;

        final StringBuilder result = new StringBuilder();

        // OSRM uses precision=6, the default Polyline spec divides by 1E5, capping at precision=5
        double factor = Math.pow(10, precision);

        for (final Point point : path) {
            long lat = Math.round(point.getY() * factor);
            long lng = Math.round(point.getX() * factor);

            long varLat = lat - lastLat;
            long varLng = lng - lastLng;

            encode(varLat, result);
            encode(varLng, result);

            lastLat = lat;
            lastLng = lng;
        }
        return result.toString();
    }

    private static void encode(long variable, StringBuilder result) {
        variable = variable < 0 ? ~(variable << 1) : variable << 1;
        while (variable >= 0x20) {
            result.append(Character.toChars((int) ((0x20 | (variable & 0x1f)) + 63)));
            variable >>= 5;
        }
        result.append(Character.toChars((int) (variable + 63)));
    }

    /*
     * Polyline simplification method. It's a direct port of simplify.js to Java.
     * See: https://github.com/mourner/simplify-js/blob/master/simplify.js
     */

    /**
     * Reduces the number of points in a polyline while retaining its shape, giving a performance
     * boost when processing it and also reducing visual noise.
     *
     * @param points an array of points
     * @return an array of simplified points
     * @see JavaScript implementation
     * @since 1.2.0
     */
    public static List simplify(List points) {
        return simplify(points, SIMPLIFY_DEFAULT_TOLERANCE, SIMPLIFY_DEFAULT_HIGHEST_QUALITY);
    }

    /**
     * Reduces the number of points in a polyline while retaining its shape, giving a performance
     * boost when processing it and also reducing visual noise.
     *
     * @param points    an array of points
     * @param tolerance affects the amount of simplification (in the same metric as the point
     *                  coordinates)
     * @return an array of simplified points
     * @see JavaScript implementation
     * @since 1.2.0
     */
    public static List simplify(List points, double tolerance) {
        return simplify(points, tolerance, SIMPLIFY_DEFAULT_HIGHEST_QUALITY);
    }

    /**
     * Reduces the number of points in a polyline while retaining its shape, giving a performance
     * boost when processing it and also reducing visual noise.
     *
     * @param points         an array of points
     * @param highestQuality excludes distance-based preprocessing step which leads to highest quality
     *                       simplification
     * @return an array of simplified points
     * @see JavaScript implementation
     * @since 1.2.0
     */
    public static List simplify(List points, boolean highestQuality) {
        return simplify(points, SIMPLIFY_DEFAULT_TOLERANCE, highestQuality);
    }

    /**
     * Reduces the number of points in a polyline while retaining its shape, giving a performance
     * boost when processing it and also reducing visual noise.
     *
     * @param points         an array of points
     * @param tolerance      affects the amount of simplification (in the same metric as the point
     *                       coordinates)
     * @param highestQuality excludes distance-based preprocessing step which leads to highest quality
     *                       simplification
     * @return an array of simplified points
     * @see JavaScript implementation
     * @since 1.2.0
     */
    public static List simplify(List points, double tolerance,
                                       boolean highestQuality) {
        if (points.size() <= 2) {
            return points;
        }

        double sqTolerance = tolerance * tolerance;

        points = highestQuality ? points : simplifyRadialDist(points, sqTolerance);
        points = simplifyDouglasPeucker(points, sqTolerance);

        return points;
    }

    /**
     * Square distance between 2 points.
     *
     * @param p1 first {@link Point}
     * @param p2 second Point
     * @return square of the distance between two input points
     */
    private static double getSqDist(Point p1, Point p2) {
        double dx = p1.getX() - p2.getX();
        double dy = p1.getY() - p2.getY();
        return dx * dx + dy * dy;
    }

    /**
     * Square distance from a point to a segment.
     *
     * @param point {@link Point} whose distance from segment needs to be determined
     * @param p1,p2 points defining the segment
     * @return square of the distance between first input point and segment defined by
     *   other two input points
     */
    private static double getSqSegDist(Point point, Point p1, Point p2) {
        double horizontal = p1.getX();
        double vertical = p1.getY();
        double diffHorizontal = p2.getX() - horizontal;
        double diffVertical = p2.getY() - vertical;

        if (diffHorizontal != 0 || diffVertical != 0) {
            double total = ((point.getX() - horizontal) * diffHorizontal + (point.getY()
                - vertical) * diffVertical) / (diffHorizontal * diffHorizontal + diffVertical
                * diffVertical);
            if (total > 1) {
                horizontal = p2.getX();
                vertical = p2.getY();

            } else if (total > 0) {
                horizontal += diffHorizontal * total;
                vertical += diffVertical * total;
            }
        }

        diffHorizontal = point.getX() - horizontal;
        diffVertical = point.getY() - vertical;

        return diffHorizontal * diffHorizontal + diffVertical * diffVertical;
    }

    /**
     * Basic distance-based simplification.
     *
     * @param points a list of points to be simplified
     * @param sqTolerance square of amount of simplification
     * @return a list of simplified points
     */
    private static List simplifyRadialDist(List points, double sqTolerance) {
        Point prevPoint = points.get(0);
        ArrayList newPoints = new ArrayList<>();
        newPoints.add(prevPoint);
        Point point = null;

        for (int i = 1, len = points.size(); i < len; i++) {
            point = points.get(i);

            if (getSqDist(point, prevPoint) > sqTolerance) {
                newPoints.add(point);
                prevPoint = point;
            }
        }

        if (!prevPoint.equals(point)) {
            newPoints.add(point);
        }
        return newPoints;
    }

    private static List simplifyDpStep(
        List points, int first, int last, double sqTolerance, List simplified) {
        double maxSqDist = sqTolerance;
        int index = 0;

        ArrayList stepList = new ArrayList<>();

        for (int i = first + 1; i < last; i++) {
            double sqDist = getSqSegDist(points.get(i), points.get(first), points.get(last));
            if (sqDist > maxSqDist) {
                index = i;
                maxSqDist = sqDist;
            }
        }

        if (maxSqDist > sqTolerance) {
            if (index - first > 1) {
                stepList.addAll(simplifyDpStep(points, first, index, sqTolerance, simplified));
            }

            stepList.add(points.get(index));

            if (last - index > 1) {
                stepList.addAll(simplifyDpStep(points, index, last, sqTolerance, simplified));
            }
        }

        return stepList;
    }

    /**
     * Simplification using Ramer-Douglas-Peucker algorithm.
     *
     * @param points a list of points to be simplified
     * @param sqTolerance square of amount of simplification
     * @return a list of simplified points
     */
    private static List simplifyDouglasPeucker(List points, double sqTolerance) {
        int last = points.size() - 1;
        ArrayList simplified = new ArrayList<>();
        simplified.add(points.get(0));
        simplified.addAll(simplifyDpStep(points, 0, last, sqTolerance, simplified));
        simplified.add(points.get(last));
        return simplified;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy