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

org.opentripplanner.netex.mapping.StopTimesMapper Maven / Gradle / Ivy

The newest version!
package org.opentripplanner.netex.mapping;

import static org.opentripplanner.model.PickDrop.COORDINATE_WITH_DRIVER;
import static org.opentripplanner.model.PickDrop.NONE;
import static org.opentripplanner.model.PickDrop.SCHEDULED;

import jakarta.xml.bind.JAXBElement;
import java.math.BigInteger;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.opentripplanner.framework.i18n.I18NString;
import org.opentripplanner.framework.i18n.NonLocalizedString;
import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore;
import org.opentripplanner.model.StopTime;
import org.opentripplanner.netex.index.api.ReadOnlyHierarchicalMap;
import org.opentripplanner.netex.index.api.ReadOnlyHierarchicalMapById;
import org.opentripplanner.netex.mapping.support.FeedScopedIdFactory;
import org.opentripplanner.netex.support.JourneyPatternHelper;
import org.opentripplanner.transit.model.framework.ImmutableEntityById;
import org.opentripplanner.transit.model.site.AreaStop;
import org.opentripplanner.transit.model.site.GroupStop;
import org.opentripplanner.transit.model.site.RegularStop;
import org.opentripplanner.transit.model.site.StopLocation;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.model.timetable.booking.BookingInfo;
import org.rutebanken.netex.model.DestinationDisplay;
import org.rutebanken.netex.model.DestinationDisplay_VersionStructure;
import org.rutebanken.netex.model.FlexibleLine;
import org.rutebanken.netex.model.JourneyPattern_VersionStructure;
import org.rutebanken.netex.model.LineRefStructure;
import org.rutebanken.netex.model.MultilingualString;
import org.rutebanken.netex.model.PointInLinkSequence_VersionedChildStructure;
import org.rutebanken.netex.model.Route;
import org.rutebanken.netex.model.ServiceJourney;
import org.rutebanken.netex.model.StopPointInJourneyPattern;
import org.rutebanken.netex.model.TimetabledPassingTime;
import org.rutebanken.netex.model.VersionOfObjectRefStructure;
import org.rutebanken.netex.model.Via_VersionedChildStructure;
import org.rutebanken.netex.model.Vias_RelStructure;

/**
 * This maps a list of TimetabledPassingTimes to a list of StopTimes. It also makes sure the
 * StopTime has a reference to the correct stop. DestinationDisplay is mapped to HeadSign. There is
 * logic to take care of the the fact that DestinationsDisplay is also valid for each subsequent
 * TimeTabledPassingTime, while HeadSign has to be explicitly defined for each StopTime.
 * This class does not take Daylight Saving Time transitions into account, this is an error and
 * should be fixed. See https://github.com/opentripplanner/OpenTripPlanner/issues/5109
 */
class StopTimesMapper {

  private static final int DAY_IN_SECONDS = 3600 * 24;
  private final DataImportIssueStore issueStore;
  private final FeedScopedIdFactory idFactory;

  private final ReadOnlyHierarchicalMap destinationDisplayById;

  private final ImmutableEntityById stopsById;

  private final ImmutableEntityById flexibleStopLocationsById;

  private final ImmutableEntityById groupStopById;

  private final ReadOnlyHierarchicalMap quayIdByStopPointRef;

  private final ReadOnlyHierarchicalMap flexibleStopPlaceIdByStopPointRef;

  private final ReadOnlyHierarchicalMap routeByid;

  private final ReadOnlyHierarchicalMapById flexibleLinesById;

  private I18NString currentHeadSign;

  private List currentHeadSignVias;

  StopTimesMapper(
    DataImportIssueStore issueStore,
    FeedScopedIdFactory idFactory,
    ImmutableEntityById stopsById,
    ImmutableEntityById areaStopById,
    ImmutableEntityById groupStopById,
    ReadOnlyHierarchicalMap destinationDisplayById,
    ReadOnlyHierarchicalMap quayIdByStopPointRef,
    ReadOnlyHierarchicalMap flexibleStopPlaceIdByStopPointRef,
    ReadOnlyHierarchicalMapById flexibleLinesById,
    ReadOnlyHierarchicalMap routeById
  ) {
    this.issueStore = issueStore;
    this.idFactory = idFactory;
    this.destinationDisplayById = destinationDisplayById;
    this.stopsById = stopsById;
    this.flexibleStopLocationsById = areaStopById;
    this.groupStopById = groupStopById;
    this.quayIdByStopPointRef = quayIdByStopPointRef;
    this.flexibleStopPlaceIdByStopPointRef = flexibleStopPlaceIdByStopPointRef;
    this.flexibleLinesById = flexibleLinesById;
    this.routeByid = routeById;
  }

  static int calculateOtpTime(LocalTime time, BigInteger dayOffset) {
    int otpTime = time.toSecondOfDay();
    if (dayOffset != null) {
      otpTime += DAY_IN_SECONDS * dayOffset.intValue();
    }
    return otpTime;
  }

  /**
   * @return a map of stop-times indexed by the TimetabledPassingTime id.
   */
  @Nullable
  StopTimesMapperResult mapToStopTimes(
    JourneyPattern_VersionStructure journeyPattern,
    Trip trip,
    List passingTimes,
    ServiceJourney serviceJourney
  ) {
    StopTimesMapperResult result = new StopTimesMapperResult();
    List scheduledStopPointIds = new ArrayList<>();

    for (int i = 0; i < passingTimes.size(); i++) {
      TimetabledPassingTime currentPassingTime = passingTimes.get(i);

      String pointInJourneyPattern = currentPassingTime
        .getPointInJourneyPatternRef()
        .getValue()
        .getRef();

      StopPointInJourneyPattern stopPoint = findStopPoint(pointInJourneyPattern, journeyPattern);

      StopLocation stop = lookUpStopLocation(stopPoint);

      if (stop == null) {
        if (
          stopPoint != null &&
          isFalse(stopPoint.isForAlighting()) &&
          isFalse(stopPoint.isForBoarding())
        ) {
          continue;
        }
        issueStore.add(
          "JourneyPatternStopNotFound",
          "Stop with id %s not found for StopPoint %s in JourneyPattern %s. " +
          "Trip %s will not be mapped.",
          stopPoint != null && stopPoint.getScheduledStopPointRef() != null
            ? stopPoint.getScheduledStopPointRef().getValue().getRef()
            : "null",
          stopPoint != null ? stopPoint.getId() : "null",
          journeyPattern.getId(),
          trip.getId()
        );
        return null;
      }

      scheduledStopPointIds.add(stopPoint.getScheduledStopPointRef().getValue().getRef());

      StopTime stopTime = mapToStopTime(trip, stopPoint, stop, currentPassingTime, i);
      if (stopTime == null) {
        return null;
      }

      BookingInfo bookingInfo = new BookingInfoMapper(issueStore).map(
        stopPoint,
        serviceJourney,
        lookUpFlexibleLine(serviceJourney, journeyPattern)
      );
      stopTime.setDropOffBookingInfo(bookingInfo);
      stopTime.setPickupBookingInfo(bookingInfo);

      result.addStopTime(currentPassingTime.getId(), stopTime);
    }
    result.setScheduledStopPointIds(scheduledStopPointIds);

    return result;
  }

  /**
   * @return a map of stop-times indexed by the TimetabledPassingTime id.
   */
  @Nullable
  String findTripHeadsign(
    JourneyPattern_VersionStructure journeyPattern,
    TimetabledPassingTime firstPassingTime
  ) {
    String pointInJourneyPattern = firstPassingTime
      .getPointInJourneyPatternRef()
      .getValue()
      .getRef();

    var stopPoint = findStopPoint(pointInJourneyPattern, journeyPattern);

    if (stopPoint == null) {
      return null;
    }

    if (stopPoint.getDestinationDisplayRef() == null) {
      return null;
    }

    var destinationDisplay = destinationDisplayById.lookup(
      stopPoint.getDestinationDisplayRef().getRef()
    );

    return destinationDisplay == null
      ? null
      : MultilingualStringMapper.nullableValueOf(destinationDisplay.getFrontText());
  }

  @Nullable
  private static StopPointInJourneyPattern findStopPoint(
    String pointInJourneyPatterRef,
    JourneyPattern_VersionStructure journeyPattern
  ) {
    var points = journeyPattern
      .getPointsInSequence()
      .getPointInJourneyPatternOrStopPointInJourneyPatternOrTimingPointInJourneyPattern();

    for (PointInLinkSequence_VersionedChildStructure point : points) {
      if (point instanceof StopPointInJourneyPattern stopPoint) {
        if (stopPoint.getId().equals(pointInJourneyPatterRef)) {
          return stopPoint;
        }
      }
    }
    return null;
  }

  private static int calculateOtpTime(
    LocalTime time,
    BigInteger dayOffset,
    LocalTime fallbackTime,
    BigInteger fallbackDayOffset
  ) {
    return time != null
      ? calculateOtpTime(time, dayOffset)
      : calculateOtpTime(fallbackTime, fallbackDayOffset);
  }

  private static boolean isFalse(Boolean value) {
    return value != null && !value;
  }

  private StopTime mapToStopTime(
    Trip trip,
    StopPointInJourneyPattern stopPoint,
    StopLocation stop,
    TimetabledPassingTime passingTime,
    int stopSequence
  ) {
    StopTime stopTime = new StopTime();
    stopTime.setTrip(trip);
    stopTime.setStopSequence(stopSequence);
    stopTime.setStop(stop);
    if (passingTime.getArrivalTime() != null || passingTime.getDepartureTime() != null) {
      stopTime.setArrivalTime(
        calculateOtpTime(
          passingTime.getArrivalTime(),
          passingTime.getArrivalDayOffset(),
          passingTime.getDepartureTime(),
          passingTime.getDepartureDayOffset()
        )
      );
      stopTime.setDepartureTime(
        calculateOtpTime(
          passingTime.getDepartureTime(),
          passingTime.getDepartureDayOffset(),
          passingTime.getArrivalTime(),
          passingTime.getArrivalDayOffset()
        )
      );

      // From NeTEx we define timepoint as a waitpoint with waiting time defined (also 0)
      if (Boolean.TRUE.equals(stopPoint.isIsWaitPoint()) && passingTime.getWaitingTime() != null) {
        stopTime.setTimepoint(1);
      }
    } else if (
      passingTime.getEarliestDepartureTime() != null && passingTime.getLatestArrivalTime() != null
    ) {
      stopTime.setFlexWindowStart(
        calculateOtpTime(
          passingTime.getEarliestDepartureTime(),
          passingTime.getEarliestDepartureDayOffset()
        )
      );
      stopTime.setFlexWindowEnd(
        calculateOtpTime(
          passingTime.getLatestArrivalTime(),
          passingTime.getLatestArrivalDayOffset()
        )
      );
    } else {
      return null;
    }

    if (stopPoint != null) {
      if (isFalse(stopPoint.isForAlighting())) {
        stopTime.setDropOffType(NONE);
      } else if (Boolean.TRUE.equals(stopPoint.isRequestStop())) {
        stopTime.setDropOffType(COORDINATE_WITH_DRIVER);
      } else {
        stopTime.setDropOffType(SCHEDULED);
      }

      if (isFalse(stopPoint.isForBoarding())) {
        stopTime.setPickupType(NONE);
      } else if (Boolean.TRUE.equals(stopPoint.isRequestStop())) {
        stopTime.setPickupType(COORDINATE_WITH_DRIVER);
      } else {
        stopTime.setPickupType(SCHEDULED);
      }

      if (stopPoint.getDestinationDisplayRef() != null) {
        DestinationDisplay destinationDisplay = destinationDisplayById.lookup(
          stopPoint.getDestinationDisplayRef().getRef()
        );

        if (destinationDisplay != null) {
          currentHeadSign = new NonLocalizedString(destinationDisplay.getFrontText().getValue());
          Vias_RelStructure viaValues = destinationDisplay.getVias();
          if (viaValues != null && viaValues.getVia() != null) {
            currentHeadSignVias = viaValues
              .getVia()
              .stream()
              .map(Via_VersionedChildStructure::getDestinationDisplayRef)
              .filter(Objects::nonNull)
              .map(VersionOfObjectRefStructure::getRef)
              .filter(Objects::nonNull)
              .map(destinationDisplayById::lookup)
              .filter(Objects::nonNull)
              .map(DestinationDisplay_VersionStructure::getFrontText)
              .filter(Objects::nonNull)
              .map(MultilingualString::getValue)
              .filter(Objects::nonNull)
              .collect(Collectors.toList());

            if (currentHeadSignVias.isEmpty()) {
              currentHeadSignVias = null;
            }
          } else {
            currentHeadSignVias = null;
          }
        }
      }
    }

    if (
      passingTime.getArrivalTime() == null &&
      passingTime.getDepartureTime() == null &&
      passingTime.getEarliestDepartureTime() == null &&
      passingTime.getLatestArrivalTime() == null
    ) {
      issueStore.add("TripWithoutTime", "Time missing for trip %s", trip.getId());
    }

    if (currentHeadSign != null) {
      stopTime.setStopHeadsign(currentHeadSign);
    }
    if (currentHeadSignVias != null) {
      stopTime.setHeadsignVias(currentHeadSignVias);
    }
    return stopTime;
  }

  @Nullable
  private StopLocation lookUpStopLocation(StopPointInJourneyPattern stopPointInJourneyPattern) {
    if (stopPointInJourneyPattern == null) {
      return null;
    }

    String stopPointRef = stopPointInJourneyPattern.getScheduledStopPointRef().getValue().getRef();

    String stopId = quayIdByStopPointRef.lookup(stopPointRef);
    String flexibleStopPlaceId = flexibleStopPlaceIdByStopPointRef.lookup(stopPointRef);

    if (stopId == null && flexibleStopPlaceId == null) {
      issueStore.add(
        "PassengerStopAssignmentNotFound",
        "No passengerStopAssignment found for %s",
        stopPointRef
      );
      return null;
    }

    StopLocation stopLocation;
    if (stopId != null) {
      stopLocation = stopsById.get(idFactory.createId(stopId));
    } else {
      AreaStop areaStop = flexibleStopLocationsById.get(idFactory.createId(flexibleStopPlaceId));
      GroupStop groupStop = groupStopById.get(idFactory.createId(flexibleStopPlaceId));

      if (areaStop != null) {
        stopLocation = areaStop;
      } else stopLocation = groupStop;
    }

    if (stopLocation == null) {
      issueStore.add(
        "StopPointInJourneyPatternMissingStopLocation",
        "No Quay or FlexibleStopPlace found for %s",
        stopPointRef
      );
    }

    return stopLocation;
  }

  private FlexibleLine lookUpFlexibleLine(
    ServiceJourney serviceJourney,
    JourneyPattern_VersionStructure journeyPattern
  ) {
    if (serviceJourney == null) {
      return null;
    }
    String lineRef = null;
    // Check for direct connection to Line
    JAXBElement lineRefStruct = serviceJourney.getLineRef();

    if (lineRefStruct != null) {
      // Connect to Line referenced directly from ServiceJourney
      lineRef = lineRefStruct.getValue().getRef();
    } else if (serviceJourney.getJourneyPatternRef() != null) {
      // Connect to Line referenced through JourneyPattern->Route
      lineRef = JourneyPatternHelper.getLineFromRoute(routeByid, journeyPattern);
    }
    return flexibleLinesById.lookup(lineRef);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy