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

org.opentripplanner.routing.algorithm.mapping.GraphPathToItineraryMapper Maven / Gradle / Ivy

The newest version!
package org.opentripplanner.routing.algorithm.mapping;

import static org.opentripplanner.street.search.state.VehicleRentalState.RENTING_FLOATING;

import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
import org.opentripplanner.astar.model.GraphPath;
import org.opentripplanner.ext.flex.FlexibleTransitLeg;
import org.opentripplanner.ext.flex.edgetype.FlexTripEdge;
import org.opentripplanner.framework.application.OTPFeature;
import org.opentripplanner.framework.geometry.GeometryUtils;
import org.opentripplanner.framework.i18n.I18NString;
import org.opentripplanner.framework.time.ZoneIdFallback;
import org.opentripplanner.model.plan.ElevationProfile;
import org.opentripplanner.model.plan.Itinerary;
import org.opentripplanner.model.plan.Leg;
import org.opentripplanner.model.plan.Place;
import org.opentripplanner.model.plan.StreetLeg;
import org.opentripplanner.model.plan.StreetLegBuilder;
import org.opentripplanner.model.plan.WalkStep;
import org.opentripplanner.routing.services.notes.StreetNotesService;
import org.opentripplanner.service.vehiclerental.street.VehicleRentalEdge;
import org.opentripplanner.service.vehiclerental.street.VehicleRentalPlaceVertex;
import org.opentripplanner.street.model.edge.BoardingLocationToStopLink;
import org.opentripplanner.street.model.edge.Edge;
import org.opentripplanner.street.model.edge.StreetEdge;
import org.opentripplanner.street.model.edge.VehicleParkingEdge;
import org.opentripplanner.street.model.note.StreetNote;
import org.opentripplanner.street.model.vertex.StreetVertex;
import org.opentripplanner.street.model.vertex.TemporaryStreetLocation;
import org.opentripplanner.street.model.vertex.TransitStopVertex;
import org.opentripplanner.street.model.vertex.VehicleParkingEntranceVertex;
import org.opentripplanner.street.model.vertex.Vertex;
import org.opentripplanner.street.search.TraverseMode;
import org.opentripplanner.street.search.state.State;

/**
 * A mapper class used in converting internal GraphPaths to Itineraries, which are returned by the
 * OTP APIs. This only produces itineraries for non-transit searches, as well as the non-transit
 * parts of itineraries containing transit, while the whole transit itinerary is produced by
 * {@link RaptorPathToItineraryMapper}.
 */
public class GraphPathToItineraryMapper {

  private final ZoneId timeZone;
  private final StreetNotesService streetNotesService;
  private final double ellipsoidToGeoidDifference;

  public GraphPathToItineraryMapper(
    ZoneId timeZone,
    StreetNotesService streetNotesService,
    double ellipsoidToGeoidDifference
  ) {
    this.timeZone = ZoneIdFallback.zoneId(timeZone);
    this.streetNotesService = streetNotesService;
    this.ellipsoidToGeoidDifference = ellipsoidToGeoidDifference;
  }

  public static boolean isRentalPickUp(State state) {
    return (
      state.getBackEdge() instanceof VehicleRentalEdge &&
      (state.getBackState() == null || !state.getBackState().isRentingVehicle())
    );
  }

  public static boolean isRentalStationDropOff(State state) {
    return (
      state.getBackEdge() instanceof VehicleRentalEdge && state.getBackState().isRentingVehicle()
    );
  }

  /**
   * Dropping of a free-floating vehicle can happen at any edge so be sure to select the correct
   * state (forward, not backward).
   */
  public static boolean isFloatingRentalDropoff(State state) {
    return (
      !state.isRentingVehicle() &&
      (state.getBackState() != null &&
        state.getBackState().getVehicleRentalState() == RENTING_FLOATING)
    );
  }

  /**
   * Generates a TripPlan from a set of paths
   */
  public List mapItineraries(List> paths) {
    List itineraries = new LinkedList<>();
    for (GraphPath path : paths) {
      Itinerary itinerary = generateItinerary(path);
      if (itinerary.getLegs().isEmpty()) {
        continue;
      }
      itineraries.add(itinerary);
    }

    return itineraries;
  }

  /**
   * Generate an itinerary from a {@link GraphPath}. This method first slices the list of states at
   * the leg boundaries. These smaller state arrays are then used to generate legs.
   *
   * @param path The graph path to base the itinerary on
   * @return The generated itinerary
   */
  public Itinerary generateItinerary(GraphPath path) {
    List legs = new ArrayList<>();
    WalkStep previousStep = null;
    for (List legStates : sliceStates(path.states)) {
      if (OTPFeature.FlexRouting.isOn() && legStates.get(1).backEdge instanceof FlexTripEdge) {
        legs.add(generateFlexLeg(legStates));
        previousStep = null;
        continue;
      }
      StreetLeg leg = generateLeg(legStates, previousStep);
      legs.add(leg);

      List walkSteps = leg.getWalkSteps();
      if (walkSteps.size() > 0) {
        previousStep = walkSteps.get(walkSteps.size() - 1);
      } else {
        previousStep = null;
      }
    }

    Itinerary itinerary = Itinerary.createDirectItinerary(legs);

    calculateElevations(itinerary, path.edges);

    State lastState = path.states.getLast();
    itinerary.setGeneralizedCost((int) lastState.weight);
    itinerary.setArrivedAtDestinationWithRentedVehicle(lastState.isRentingVehicleFromStation());

    return itinerary;
  }

  /**
   * Slice a {@link State} list at the leg boundaries.
   *
   * @param states The list of input states
   * @return A list of lists of states belonging to a single leg
   */
  private static List> sliceStates(List states) {
    // Trivial case
    if (states.stream().allMatch(state -> state.getBackMode() == null)) {
      return List.of();
    }

    List> legsStates = new LinkedList<>();

    int previousBreak = 0;

    for (int i = 1; i < states.size() - 1; i++) {
      var backState = states.get(i);
      var forwardState = states.get(i + 1);

      var flexChange =
        forwardState.backEdge instanceof FlexTripEdge || backState.backEdge instanceof FlexTripEdge;
      var rentalChange =
        isRentalPickUp(backState) ||
        isRentalStationDropOff(backState) ||
        isFloatingRentalDropoff(backState);
      var parkingChange = backState.isVehicleParked() != forwardState.isVehicleParked();
      var carPickupChange = backState.getCarPickupState() != forwardState.getCarPickupState();

      if (parkingChange || flexChange || rentalChange || carPickupChange) {
        int nextBreak = i;

        if (nextBreak > previousBreak) {
          legsStates.add(states.subList(previousBreak, nextBreak + 1));
        }

        /* Remove the state for actually parking (traversing a VehicleParkingEdge) from the
         * states so that the leg from/to edges correspond to the actual entrances.
         * The actual time for parking is added to the walking leg in generateLeg().
         */
        if (parkingChange) {
          nextBreak++;
        }

        previousBreak = nextBreak;
      }
    }

    // Final leg
    if (states.size() > previousBreak) {
      legsStates.add(states.subList(previousBreak, states.size()));
    }

    return legsStates;
  }

  /**
   * Calculate the elevationGained and elevationLost fields of an {@link Itinerary}.
   *
   * @param itinerary The itinerary to calculate the elevation changes for
   * @param edges     The edges that go with the itinerary
   */
  private static void calculateElevations(Itinerary itinerary, List edges) {
    for (Edge edge : edges) {
      if (!(edge instanceof StreetEdge edgeWithElevation)) {
        continue;
      }
      PackedCoordinateSequence coordinates = edgeWithElevation.getElevationProfile();

      if (coordinates == null) continue;
      // TODO Check the test below, AFAIU current elevation profile has 3 dimensions.
      if (coordinates.getDimension() != 2) continue;

      for (int i = 0; i < coordinates.size() - 1; i++) {
        double change = coordinates.getOrdinate(i + 1, 1) - coordinates.getOrdinate(i, 1);

        if (change > 0) {
          itinerary.setElevationGained(itinerary.getElevationGained() + change);
        } else if (change < 0) {
          itinerary.setElevationLost(itinerary.getElevationLost() - change);
        }
      }
    }
  }

  /**
   * Resolve mode from states.
   *
   * @param states The states that go with the leg
   */
  private static TraverseMode resolveMode(List states) {
    return states
      .stream()
      // The first state is part of the previous leg
      .skip(1)
      .map(state -> {
        var mode = state.currentMode();

        if (mode != null) {
          // Resolve correct mode if renting vehicle
          if (state.isRentingVehicle()) {
            return state.stateData.rentalVehicleFormFactor.traverseMode;
          } else {
            return mode;
          }
        }

        return null;
      })
      .filter(Objects::nonNull)
      .findFirst()
      // Fallback to walking
      .orElse(TraverseMode.WALK);
  }

  private static ElevationProfile encodeElevationProfileWithNaN(
    Edge edge,
    double distanceOffset,
    double heightOffset
  ) {
    var elevations = encodeElevationProfile(edge, distanceOffset, heightOffset);
    if (elevations.isEmpty()) {
      return ElevationProfile.of()
        .stepYUnknown(distanceOffset)
        .stepYUnknown(distanceOffset + edge.getDistanceMeters())
        .build();
    }
    return elevations;
  }

  private static ElevationProfile encodeElevationProfile(
    Edge edge,
    double distanceOffset,
    double heightOffset
  ) {
    if (!(edge instanceof StreetEdge elevEdge)) {
      return ElevationProfile.empty();
    }
    if (elevEdge.getElevationProfile() == null) {
      return ElevationProfile.empty();
    }

    var out = ElevationProfile.of();
    Coordinate[] coordArr = elevEdge.getElevationProfile().toCoordinateArray();
    for (final Coordinate coordinate : coordArr) {
      out.step(coordinate.x + distanceOffset, coordinate.y + heightOffset);
    }

    return out.build();
  }

  /**
   * Make a {@link Place} to add to a {@link Leg}.
   *
   * @param state The {@link State}.
   * @return The resulting {@link Place} object.
   */
  private static Place makePlace(State state) {
    Vertex vertex = state.getVertex();
    I18NString name = vertex.getName();

    //This gets nicer names instead of osm:node:id when changing mode of transport
    //Names are generated from all the streets in a corner, same as names in origin and destination
    //We use name in TemporaryStreetLocation since this name generation already happened when temporary location was generated
    if (vertex instanceof StreetVertex && !(vertex instanceof TemporaryStreetLocation)) {
      name = ((StreetVertex) vertex).getIntersectionName();
    }

    if (vertex instanceof TransitStopVertex) {
      return Place.forStop(((TransitStopVertex) vertex).getStop());
    } else if (vertex instanceof VehicleRentalPlaceVertex) {
      return Place.forVehicleRentalPlace((VehicleRentalPlaceVertex) vertex);
    } else if (vertex instanceof VehicleParkingEntranceVertex) {
      return Place.forVehicleParkingEntrance((VehicleParkingEntranceVertex) vertex, state);
    } else {
      return Place.normal(vertex, name);
    }
  }

  /**
   * Generate a flex leg from the states belonging to the flex leg
   */
  private Leg generateFlexLeg(List states) {
    State fromState = states.get(0);
    State toState = states.get(1);
    FlexTripEdge flexEdge = (FlexTripEdge) toState.backEdge;
    ZonedDateTime startTime = fromState.getTime().atZone(timeZone);
    ZonedDateTime endTime = toState.getTime().atZone(timeZone);
    int generalizedCost = (int) (toState.getWeight() - fromState.getWeight());

    return FlexibleTransitLeg.of()
      .withFlexTripEdge(flexEdge)
      .withStartTime(startTime)
      .withEndTime(endTime)
      .withGeneralizedCost(generalizedCost)
      .build();
  }

  /**
   * Generate one leg of an itinerary from a list of {@link State}.
   *
   * @param states       The list of states to base the leg on
   * @param previousStep the previous walk step, so that the first relative turn direction is
   *                     calculated correctly
   * @return The generated leg
   */
  private StreetLeg generateLeg(List states, WalkStep previousStep) {
    List edges = states
      .stream()
      // The first back edge is part of the previous leg, skip it
      .skip(1)
      // when linking an OSM boarding location, like a platform centroid, we create a link edge
      // so we can see it in the debug UI's traversal permission layer but we don't want to show the
      // link to the user so we remove it here
      .filter(e -> !(e.backEdge instanceof BoardingLocationToStopLink))
      .map(State::getBackEdge)
      .toList();

    State firstState = states.get(0);
    State lastState = states.get(states.size() - 1);

    double distanceMeters = edges.stream().mapToDouble(Edge::getDistanceMeters).sum();

    LineString geometry = GeometryUtils.concatenateLineStrings(edges, Edge::getGeometry);

    var statesToWalkStepsMapper = new StatesToWalkStepsMapper(
      states,
      previousStep,
      streetNotesService,
      ellipsoidToGeoidDifference
    );
    List walkSteps = statesToWalkStepsMapper.generateWalkSteps();

    /* For the from/to vertices to be in the correct place for vehicle parking
     * the state for actually parking (traversing the VehicleParkEdge) is excluded
     * from the list of states.
     * This adds the time for parking to the walking leg.
     */
    var previousStateIsVehicleParking =
      firstState.getBackState() != null && firstState.getBackEdge() instanceof VehicleParkingEdge;

    State startTimeState = previousStateIsVehicleParking ? firstState.getBackState() : firstState;

    StreetLegBuilder leg = StreetLeg.create()
      .withMode(resolveMode(states))
      .withStartTime(startTimeState.getTime().atZone(timeZone))
      .withEndTime(lastState.getTime().atZone(timeZone))
      .withFrom(makePlace(firstState))
      .withTo(makePlace(lastState))
      .withDistanceMeters(distanceMeters)
      .withGeneralizedCost((int) (lastState.getWeight() - firstState.getWeight()))
      .withGeometry(geometry)
      .withElevationProfile(
        makeElevation(edges, firstState.getPreferences().system().geoidElevation())
      )
      .withWalkSteps(walkSteps)
      .withRentedVehicle(firstState.isRentingVehicle())
      .withWalkingBike(false);

    if (firstState.isRentingVehicle()) {
      String vehicleRentalNetwork = firstState.getVehicleRentalNetwork();
      if (vehicleRentalNetwork != null) {
        leg.withVehicleRentalNetwork(vehicleRentalNetwork);
      }
    }

    addStreetNotes(leg, states);

    return leg.build();
  }

  /**
   * Add mode and alerts fields to a {@link StreetLeg}.
   *
   * @param leg    The leg to add the mode and alerts to
   * @param states The states that go with the leg
   */
  private void addStreetNotes(StreetLegBuilder leg, List states) {
    for (State state : states) {
      Set streetNotes = streetNotesService.getNotes(state);

      if (streetNotes != null) {
        leg.withStreetNotes(streetNotes);
      }
    }
  }

  private ElevationProfile makeElevation(List edges, boolean geoidElevation) {
    var builder = ElevationProfile.of();

    double heightOffset = geoidElevation ? ellipsoidToGeoidDifference : 0;

    double distanceOffset = 0;
    for (final Edge edge : edges) {
      if (edge.getDistanceMeters() > 0) {
        builder.add(encodeElevationProfileWithNaN(edge, distanceOffset, heightOffset));
        distanceOffset += edge.getDistanceMeters();
      }
    }

    var p = builder.build();

    return p.isAllYUnknown() ? null : p;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy