TrafficLink.java

package org.heigit.ors.routing.graphhopper.extensions.reader.traffic;

import com.graphhopper.util.DistanceCalcEarth;
import org.apache.log4j.Logger;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.opengis.feature.Property;

import java.io.InvalidObjectException;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Map;

public class TrafficLink {
    private static final Logger LOGGER = Logger.getLogger(TrafficLink.class);

    private final int linkId;
    private double linkLength;
    private boolean isTeardrop;

    private LineString linkGeometry;
    private final TrafficLinkMetadata trafficLinkMetadata;
    private final EnumMap<TrafficEnums.WeekDay, Integer> trafficPatternIdsFrom;
    private final EnumMap<TrafficEnums.WeekDay, Integer> trafficPatternIdsTo;

    /**
     * Construct a TrafficLink object used for processing the link traffic data.
     *
     * @param linkId            Link ID of the link
     * @param linkGeometry      Geometry representing the link
     * @param properties        Properties of the link
     * @param distanceCalcEarth Initialized {@link DistanceCalcEarth} object that can easily be reused to calculate the lengths.
     * @throws InvalidObjectException Provides detailed information when a geometry was invalid.
     */
    public TrafficLink(int linkId, Geometry linkGeometry, Collection<Property> properties, DistanceCalcEarth distanceCalcEarth) throws InvalidObjectException {
        this.linkId = linkId;

        this.trafficPatternIdsFrom = new EnumMap<>(TrafficEnums.WeekDay.class);
        this.trafficPatternIdsTo = new EnumMap<>(TrafficEnums.WeekDay.class);

        this.setLinkGeometry(linkGeometry);
        this.setLinkLength(distanceCalcEarth);

        trafficLinkMetadata = new TrafficLinkMetadata(properties);

    }

    private void setLinkLength(DistanceCalcEarth dc) {
        double temporaryLength = 0;

        if (this.getLinkGeometry() != null) {
            LineString ls = this.getLinkGeometry();
            int nPoints = ls.getNumPoints();

            if (nPoints > 1) {
                Coordinate c = ls.getCoordinateN(0);
                double x0 = c.x;
                double y0 = c.y;
                for (int i = 1; i < ls.getNumPoints(); i++) {
                    c = ls.getCoordinateN(i);

                    temporaryLength += dc.calcDist(y0, x0, c.y, c.x);
                    x0 = c.x;
                    y0 = c.y;
                }
            }
        }
        this.linkLength = temporaryLength;
    }

    public double getLinkLength() {
        return this.linkLength;
    }

    public int getLinkId() {
        return this.linkId;
    }

    /**
     * The default orientation for the link geometry is from. See isfromGeometry.
     * When a teardrop is detected, the geometry will be entirely ignored through isPotentialTrafficSegment.
     *
     * @param linkGeometry Geometry to assign to the link line string
     * @throws InvalidObjectException
     */
    private void setLinkGeometry(Geometry linkGeometry) throws InvalidObjectException {
        GeometryFactory gf = new GeometryFactory();
        if (linkGeometry.getGeometryType().equals("LineString")) {
            LineString geometry = gf.createLineString(linkGeometry.getCoordinates());
            isTeardrop = checkTearDrop(geometry);
            this.linkGeometry = geometry;
        } else {
            LOGGER.error("Invalid geometry - " + linkGeometry.getGeometryType());
            throw new InvalidObjectException("Invalid geometry for linkId " + linkId);
        }
    }

    public LineString getLinkGeometry() {
        return this.linkGeometry;
    }

    public void setTrafficPatternId(TrafficEnums.TravelDirection travelDirection, TrafficEnums.WeekDay weekDay, Integer trafficPatternId) {
        if (travelDirection == TrafficEnums.TravelDirection.TO) {
            this.trafficPatternIdsTo.put(weekDay, trafficPatternId);
        } else {
            this.trafficPatternIdsFrom.put(weekDay, trafficPatternId);
        }
    }

    public Map<TrafficEnums.WeekDay, Integer> getTrafficPatternIds(TrafficEnums.TravelDirection travelDirection) {
        if (travelDirection == TrafficEnums.TravelDirection.TO) {
            return this.trafficPatternIdsTo;
        } else {
            return this.trafficPatternIdsFrom;
        }
    }

    public boolean isPotentialTrafficSegment() {
        if (isTeardrop) return false;
        if (trafficPatternIdsTo.isEmpty() && trafficPatternIdsFrom.isEmpty()) return false;
        if (trafficLinkMetadata.isFerry()) return false;
        if (trafficLinkMetadata.isRoundAbout()) return false;
        return trafficLinkMetadata.functionalClass() != TrafficEnums.FunctionalClass.CLASS5;
    }

    public boolean isBothDirections() {
        return trafficLinkMetadata.getTravelDirection() == TrafficEnums.LinkTravelDirection.BOTH;
    }

    public int getFunctionalClass() {
        return this.trafficLinkMetadata.getFunctionalClassWithRamp();
    }

    private boolean checkTearDrop(LineString lineString) {
        double coordinateFirstX = lineString.getCoordinateN(0).x;
        double coordinateFirstY = lineString.getCoordinateN(0).y;
        double coordinateLastX = lineString.getCoordinateN(lineString.getCoordinates().length - 1).x;
        double coordinateLastY = lineString.getCoordinateN(lineString.getCoordinates().length - 1).y;

        if (coordinateFirstY < coordinateLastY) {
            // First coordinate is the reference if its latitude is lower. Most common case!
            return false;
        } else if (coordinateFirstY > coordinateLastY) {
            // Last  coordinate is Reference if its latitude is lower.
            return false;
        } else // First coordinate is the reference if latitudes are equal but its longitude is lower.
            // Teardrop nodes with same Coords. This shouldn't happen with roads from Here!
            if (coordinateFirstX < coordinateLastX) {
                // First coordinate is the reference if latitudes are equal but its longitude is lower.
                // This represents horizontal lines >------>
                return false;
            } else return !(coordinateFirstX > coordinateLastX);
    }

    public Geometry getToGeometry() {
        return linkGeometry.reverse();
    }

    public Geometry getFromGeometry() {
        return linkGeometry;
    }

    public boolean isOnlyFromDirection() {
        return trafficLinkMetadata.getTravelDirection() == TrafficEnums.LinkTravelDirection.FROM;
    }
}