コード例 #1
0
ファイル: merge_utils.py プロジェクト: woshilapin/kirin
def adjust_trip_update_consistency(trip_update, stus):
    """
    Adjust consistency of TripUpdate and StopTimeUpdates (side-effect).
    NB : using list of StopTimeUpdate provided in stus, as the list is often not
    attached to TripUpdate (to avoid ORM persisting it)
    :param trip_update: TripUpdate to adjust (all but stop_time_updates)
    :param stus: List of StopTimeUpdates to adjust
    :return: None, just update given parameters
    """
    previous_stop_event_dt = datetime.datetime.min
    for res_stu in stus:
        # if trip is added: stops are either added or deleted (both can be for detour) + no delay
        if trip_update.effect == TripEffect.ADDITIONAL_SERVICE.name:
            if res_stu.arrival_status in SIMPLE_MODIF_STATUSES:
                res_stu.arrival_status = ModificationType.add.name
                res_stu.arrival_delay = as_duration(0)
            if res_stu.departure_status in SIMPLE_MODIF_STATUSES:
                res_stu.departure_status = ModificationType.add.name
                res_stu.departure_delay = as_duration(0)
        elif trip_update.effect == TripEffect.NO_SERVICE.name:
            res_stu.arrival_status = ModificationType.delete.name
            res_stu.departure_status = ModificationType.delete.name

        # adjust stop-events considering stop-events surrounding them
        previous_stop_event_dt = adjust_stop_event_consistency(
            res_stu, StopTimeEvent.arrival, previous_stop_event_dt)
        previous_stop_event_dt = adjust_stop_event_consistency(
            res_stu, StopTimeEvent.departure, previous_stop_event_dt)
コード例 #2
0
ファイル: merge_utils.py プロジェクト: woshilapin/kirin
def manage_consistency(trip_update):
    """
    receive a TripUpdate, then adjust its consistency
    """

    previous_stop_event = TimeDelayTuple(time=None, delay=None)
    for current_order, stu in enumerate(trip_update.stop_time_updates):
        # modifications
        if not manage_arrival_consistency(stu, trip_update,
                                          previous_stop_event):
            return False
        if not manage_departure_consistency(stu, trip_update):
            return False

        if stu.arrival_delay is None:
            stu.arrival_delay = as_duration(0)
            log_stu_modif(trip_update, stu,
                          "arrival_delay = {v}".format(v=stu.arrival_delay))

        if stu.departure_delay is None:
            stu.departure_delay = as_duration(0)
            log_stu_modif(
                trip_update, stu,
                "departure_delay = {v}".format(v=stu.departure_delay))

        # store arrival as previous stop-event
        previous_stop_event = manage_stop_event_order("arrival", stu,
                                                      previous_stop_event,
                                                      trip_update)
        # store departure as previous stop-event
        previous_stop_event = manage_stop_event_order("departure", stu,
                                                      previous_stop_event,
                                                      trip_update)

    return True
コード例 #3
0
def get_last_delay_from_added_stu(stu):
    last_delay = None
    # Added stops propagate delay, even if no delay is given (which "stops" previous delay propagation)
    if stu.departure_status not in DELETED_STATUSES:
        last_delay = stu.departure_delay if stu.departure_delay else as_duration(0)
    elif stu.arrival_status not in DELETED_STATUSES:
        last_delay = stu.arrival_delay if stu.arrival_delay else as_duration(0)
    return last_delay
コード例 #4
0
ファイル: merge_utils.py プロジェクト: woshilapin/kirin
def _get_update_info_of_stop_event(base_time, input_time, input_status,
                                   input_delay):
    """
    Process information for a given stop event: given information available, compute info to be stored in db.
    :param base_time: datetime in base_schedule
    :param input_time: datetime in new feed
    :param input_status: status in new feed
    :param input_delay: delay in new feed
    :return: new_time (base-schedule datetime in most case),
             status (update, delete, ...)
             delay (new_time + delay = RT datetime)
    """
    new_time = None
    status = ModificationType.none.name
    delay = as_duration(0)
    if input_status == ModificationType.update.name:
        new_time = base_time if base_time else None
        if new_time is not None and input_delay:
            new_time += input_delay
        status = input_status
        delay = input_delay
    elif input_status in DELETED_STATUSES:
        # passing status 'delete' on the stop_time
        # Note: we keep providing base_schedule stop_time to better identify the stop_time
        # in the vj (for lollipop lines for example)
        status = input_status
    elif input_status in ADDED_STATUSES:
        status = input_status
        new_time = input_time.replace(tzinfo=None) if input_time else None
        if new_time is not None and input_delay:
            new_time += input_delay
    else:
        new_time = base_time
    return new_time, status, delay
コード例 #5
0
def compute_stop_event_status(event, nav_stu, new_delay, new_stu):
    new_status = getattr(new_stu, STATUS_MAP[event])
    if nav_stu is None and new_status in SIMPLE_MODIF_STATUSES:
        new_status = ModificationType.add.name
    elif new_status == ModificationType.none.name and new_delay:
        new_status = ModificationType.update.name  # if there is a delay, change stop status none to update
    elif new_status == ModificationType.update.name and new_delay == as_duration(0):
        new_status = ModificationType.none.name  # if there no delay, change status update to none
    return new_status
コード例 #6
0
def _fill_and_check_one_stop_event(st_update, event_toggle, gtfsrt_stop, previous_rt_stop_event_time):
    """
    Fill considered stop-event in given STU, and check its consistency on the fly
    :param st_update: STU to fill
    :param event_toggle: stop-event to be considered for filling
    :param gtfsrt_stop: GTFSRT stop to process
    :param previous_rt_stop_event_time: datetime of previous stop-event (to check stop-event are well time-sorted)
    :return: datetime of stop-event (or previous one if none is provided)
    """
    if gtfsrt_stop.HasField("schedule_relationship"):
        setattr(
            st_update,
            STATUS_MAP[event_toggle],
            stop_gtfsrt_status_to_kirin_status.get(gtfsrt_stop.schedule_relationship).name,
        )
        if gtfsrt_stop.schedule_relationship == gtfs_realtime_pb2.TripUpdate.StopTimeUpdate.NO_DATA:
            if gtfsrt_stop.HasField(event_toggle):
                raise InvalidArguments(
                    "invalid feed: stop_point's({}) status is NO_DATA, but '{}' field is provided".format(
                        st_update.id, event_toggle
                    )
                )
            # if NO_DATA set delay to 0
            setattr(st_update, DELAY_MAP[event_toggle], as_duration(0))

    if not gtfsrt_stop.HasField(event_toggle):
        return previous_rt_stop_event_time

    event = getattr(gtfsrt_stop, event_toggle)
    if event.HasField("delay"):
        setattr(st_update, DELAY_MAP[event_toggle], as_duration(event.delay))

    if event.HasField("time"):
        setattr(st_update, event_toggle, timestamp_to_utc_naive_dt(event.time))

        if _is_stop_event_served(getattr(st_update, event_toggle), getattr(st_update, STATUS_MAP[event_toggle])):
            if previous_rt_stop_event_time > getattr(st_update, event_toggle):
                raise InvalidArguments(
                    "invalid feed: stop_point's({}) time is not consistent".format(st_update.id)
                )
            previous_rt_stop_event_time = getattr(st_update, event_toggle)

    return previous_rt_stop_event_time
コード例 #7
0
ファイル: merge_utils.py プロジェクト: woshilapin/kirin
def convert_nav_stop_list_to_stu_list(nav_stop_list, circulation_date):
    """
    Convert navitia's json stop list to dated StopTimeUpdate list
    :param nav_stop_list: list of dict containing navitia' stop info (extracted from json)
    :param circulation_date: date of the first stop time event of the list
    :return: A list of not impacted StopTimeUpdates
    """
    stus = []
    previous_stop_event_time = datetime.time.min
    for nav_order, nav_stop in enumerate(nav_stop_list):
        current_stop_arr_time = nav_stop.get("utc_arrival_time")
        current_stop_arr_dt = None
        if current_stop_arr_time is not None:
            if is_past_midnight(previous_stop_event_time,
                                current_stop_arr_time):
                circulation_date += datetime.timedelta(days=1)
            current_stop_arr_dt = datetime.datetime.combine(
                circulation_date, current_stop_arr_time)
            previous_stop_event_time = current_stop_arr_time

        current_stop_dep_time = nav_stop.get("utc_departure_time",
                                             datetime.time.min)
        current_stop_dep_dt = None
        if current_stop_dep_time is not None:
            if is_past_midnight(previous_stop_event_time,
                                current_stop_dep_time):
                circulation_date += datetime.timedelta(days=1)
            current_stop_dep_dt = datetime.datetime.combine(
                circulation_date, current_stop_dep_time)
            previous_stop_event_time = current_stop_dep_time

        stu = StopTimeUpdate(
            nav_stop.get("stop_point"),
            arrival=current_stop_arr_dt,
            arrival_delay=as_duration(0),
            departure=current_stop_dep_dt,
            departure_delay=as_duration(0),
            order=nav_order,
        )
        stus.append(stu)
    return stus
コード例 #8
0
ファイル: model_maker.py プロジェクト: woshilapin/kirin
def _retrieve_stop_event_delay(pdp, arrival_departure_toggle):
    cots_ref_planned = get_value(
        pdp,
        "sourceHoraireProjete{}Reference".format(arrival_departure_toggle),
        nullable=True)
    cots_planned_stop_times = get_value(
        pdp,
        "listeHoraireProjete{}".format(arrival_departure_toggle),
        nullable=True)
    cots_planned_stop_time = _retrieve_projected_time(cots_ref_planned,
                                                      cots_planned_stop_times)

    if cots_planned_stop_time is not None:
        delay = get_value(cots_planned_stop_time, "pronosticIV", nullable=True)
        return as_duration(delay)

    return None
コード例 #9
0
ファイル: merge_utils.py プロジェクト: woshilapin/kirin
def adjust_stop_event_in_time(stu, stop_event, previous_stop_event_dt):
    """
    Compare current stop_event datetime with previous stop_event, and push it to be at the same time if it is earlier
    :param stu: StopTimeUpdate to containing the event to consider
    :param stop_event: StopEvent to consider
    :param previous_stop_event_dt: datetime of the previous StopEvent
    :return:
    """
    stop_event_dt = getattr(stu, stop_event.name)
    # If time-sorted, nothing to be done
    if previous_stop_event_dt <= stop_event_dt:
        return

    # Adjust delay and status: do not affect deleted and added events
    if stu.get_stop_event_status(stop_event.name) in SIMPLE_MODIF_STATUSES:
        delay_attr = DELAY_MAP[stop_event.name]
        stop_event_delay = getattr(stu, delay_attr, as_duration(0))
        setattr(stu, delay_attr,
                stop_event_delay + (previous_stop_event_dt - stop_event_dt))
        setattr(stu, STATUS_MAP[stop_event.name], ModificationType.update.name)
    # Adjust datetime
    setattr(stu, stop_event.name, previous_stop_event_dt)
コード例 #10
0
def build_new_stop_event_info(new_stu, nav_stu, propagated_delay, event):
    """
    Build consistent information concerning a stop-event from a provided new STU and its corresponding navitia STU
    :param new_stu: STU from new feed being processed
    :param nav_stu: STU from base-schedule
    :param propagated_delay: delay to use for possible propagation
    :param event: string "arrival" or "departure" to describe the stop-event to build
    :return: datetime, delay, status of resulting stop-event, and delay to be propagated
    """
    nav_dt = getattr(nav_stu, event) if nav_stu is not None else None
    # compute stop-event datetime
    new_dt = getattr(new_stu, event)
    if new_dt is None and nav_dt is not None:
        tmp_delay = getattr(new_stu, DELAY_MAP[event])
        if tmp_delay is None:
            tmp_delay = propagated_delay
        new_dt = nav_dt + tmp_delay

    # compute stop-event delay
    new_delay = None
    if new_dt is not None:
        new_delay = new_dt - nav_dt if nav_dt is not None else None
    if getattr(new_stu, STATUS_MAP[event]) in DELETED_STATUSES:
        new_delay = None  # do not keep delay if stop-event is deleted (to avoid propagating delay externally)

    new_status = compute_stop_event_status(event, nav_stu, new_delay, new_stu)

    # compute propagated delay
    if new_status in ADDED_STATUSES:
        # a stop-time 'add' always stops/replace delay propagation (new delay or 0)
        propagated_delay = new_delay if new_delay is not None else as_duration(0)
    elif new_status in SIMPLE_MODIF_STATUSES and new_delay is not None:
        # a stop-time 'modification' case stops/replace delay propagation
        propagated_delay = new_delay

    return new_dt, new_delay, new_status, propagated_delay
コード例 #11
0
    def merge_trip_updates(self, navitia_vj, db_trip_update, new_trip_update):
        """
        Steps:
        1. Build TripUpdate info resulting of merge
        2. Adjust TripUpdate info to have it be self-consistent (call adjust_trip_update_consistency())
        3. If resulting TripUpdate info are new compared to the one from previous RT info: send it

        NB:
        * Working with ORM objects directly: no persistence of new object before knowing it's final version and
          it's not a duplicate (performance and db unicity constraints)
        """
        res_stus = [
        ]  # final list of StopTimeUpdates (to be attached to res in the end)

        circulation_date = new_trip_update.vj.get_circulation_date()
        # last info known about stops in trip (before processing new feed):
        # either from previous RT feed or base-schedule
        old_stus = (db_trip_update.stop_time_updates
                    if db_trip_update else convert_nav_stop_list_to_stu_list(
                        navitia_vj.get("stop_times", []), circulation_date))

        # build resulting STU list
        old_stus_unprocessed_start = 0  # keeps track of what was processed in previous STU info

        for new_order, new_stu in enumerate(new_trip_update.stop_time_updates):
            is_new_stu_last = new_order == len(
                new_trip_update.stop_time_updates) - 1

            # find corresponding stop in last known information
            match_old_stu_order, match_old_stu = find_enumerate_stu_in_stus(
                new_stu, old_stus, start=old_stus_unprocessed_start)
            is_match_old_stu_last = match_old_stu_order is not None and match_old_stu_order == len(
                old_stus) - 1

            # If new stop-time is not added, or if it is and was already added in last known information
            # Progress on stop-times from last-known information and process them
            if not new_stu.is_fully_added(
                    index=new_order, is_last=is_new_stu_last) or (
                        match_old_stu is not None
                        and match_old_stu.is_fully_added(
                            index=match_old_stu_order,
                            is_last=is_match_old_stu_last)):
                # Populate with old stops not found in new feed (the ones skipped to find current new_stu)
                populate_res_stus_with_unfound_old_stus(
                    res_stus=res_stus,
                    old_stus=old_stus,
                    unfound_start=old_stus_unprocessed_start,
                    unfound_end=match_old_stu_order
                    if match_old_stu_order is not None else len(old_stus),
                )
                # Remember progress (to avoid matching the same old stop-time for 2 different new stop-times)
                old_stus_unprocessed_start = match_old_stu_order + 1 if match_old_stu_order is not None else None

            # mark new stop-time as added if it was not known previously
            # Ex: a "delay" on a stop that doesn't exist in base-schedule is actually an "add"
            if match_old_stu is None:
                if new_stu.arrival_status in SIMPLE_MODIF_STATUSES:
                    new_stu.arrival_status = ModificationType.add.name
                if new_stu.departure_status in SIMPLE_MODIF_STATUSES:
                    new_stu.departure_status = ModificationType.add.name

            if new_stu.departure_delay is None:
                new_stu.departure_delay = as_duration(0)
            if new_stu.arrival_delay is None:
                new_stu.arrival_delay = as_duration(0)
            # add stop currently processed
            # GOTCHA: need a StopTimeUpdate detached of new_trip_update to avoid persisting new_trip_update
            # (this would lead to 2 TripUpdates for the same trip on the same day, forbidden by unicity constraint)
            res_stus.append(
                model.StopTimeUpdate(
                    navitia_stop=new_stu.navitia_stop,
                    departure=new_stu.departure,
                    arrival=new_stu.arrival,
                    departure_delay=new_stu.departure_delay,
                    arrival_delay=new_stu.arrival_delay,
                    dep_status=new_stu.departure_status,
                    arr_status=new_stu.arrival_status,
                    message=new_stu.message,
                    order=len(res_stus),
                ))
        # Finish populating with old stops not found in new feed
        # (the ones at the end after searching stops from new feed, if any)
        populate_res_stus_with_unfound_old_stus(
            res_stus=res_stus,
            old_stus=old_stus,
            unfound_start=old_stus_unprocessed_start,
            unfound_end=len(old_stus),
        )

        # if navitia_vj is empty, it's a creation
        if not navitia_vj.get("stop_times", []):
            new_trip_update.effect = TripEffect.ADDITIONAL_SERVICE.name
            new_trip_update.status = ModificationType.add.name

        # adjust consistency for resulting trip_update
        adjust_trip_update_consistency(new_trip_update, res_stus)

        return build_trip_update_if_modified(db_trip_update, old_stus,
                                             new_trip_update, res_stus,
                                             self.contributor.id)
コード例 #12
0
    def _make_trip_update(self, json_train, vj):
        trip_update = model.TripUpdate(vj=vj,
                                       contributor_id=self.contributor.id)
        trip_update.headsign = json_train.get("numero")
        company_id = json_train.get("operateur").get("codeOperateur")
        trip_update.company_id = self._get_navitia_company_id(company_id)
        physical_mode = json_train.get("modeTransport").get("typeMode")
        trip_update.physical_mode_id = self._get_navitia_physical_mode_id(
            physical_mode)
        trip_status_type = trip_piv_status_to_effect.get(
            json_train.get("evenement").get("type"), TripEffect.UNDEFINED)
        trip_update.message = json_train.get("evenement").get("texte")
        trip_update.effect = trip_status_type.name
        if trip_status_type == TripEffect.NO_SERVICE:
            # the whole trip is deleted
            trip_update.status = ModificationType.delete.name
            trip_update.stop_time_updates = []
            return trip_update
        elif trip_status_type == TripEffect.ADDITIONAL_SERVICE:
            trip_update.status = ModificationType.add.name
        else:
            trip_update.status = ModificationType.update.name

        highest_st_status = ModificationType.none.name
        ads = get_value(get_value(json_train, "listeArretsDesserte"), "arret")

        # this variable is used to store the last stop_time's departure in order to check the stop_time consistency
        # ex. stop_time[i].arrival/departure must be greater than stop_time[i-1].departure
        previous_rt_stop_event_time = datetime.datetime.utcfromtimestamp(0)
        for arret in ads:
            # retrieve navitia's stop_point corresponding to the current PIV ad
            nav_stop, log_dict = self._get_navitia_stop_point(
                arret, vj.navitia_vj)

            if log_dict:
                record_internal_failure(log_dict["log"],
                                        contributor=self.contributor.id)
                log_dict.update({"contributor": self.contributor.id})
                logging.getLogger(__name__).info("metrology", extra=log_dict)

            if nav_stop is None:
                continue  # simply skip stop_times at unknown stop areas

            st_update = model.StopTimeUpdate(nav_stop)
            trip_update.stop_time_updates.append(st_update)

            st_update.message = _get_message(arret)
            for event_toggle in ["arrivee", "depart"]:
                event = get_value(arret, event_toggle, nullable=True)
                if event is None:
                    continue

                piv_disruption = event.get("evenement", {})
                piv_event_status = event.get("statutModification",
                                             piv_disruption.get("type", None))

                if not piv_event_status or piv_event_status in MANAGED_STOP_EVENTS:
                    piv_event_datetime = get_value(event,
                                                   "dateHeureReelle",
                                                   nullable=True)
                    event_datetime = str_to_utc_naive_dt(
                        piv_event_datetime) if piv_event_datetime else None
                    if event_datetime:
                        setattr(st_update,
                                STOP_EVENT_DATETIME_MAP[event_toggle],
                                event_datetime)

                    if piv_event_status in ["SUPPRESSION_PARTIELLE"]:
                        setattr(st_update, STATUS_MAP[event_toggle],
                                ModificationType.delete.name)
                    elif piv_event_status in ["SUPPRESSION_DETOURNEMENT"]:
                        setattr(st_update, STATUS_MAP[event_toggle],
                                ModificationType.deleted_for_detour.name)
                    elif piv_event_status in ["CREATION"]:
                        setattr(st_update, STATUS_MAP[event_toggle],
                                ModificationType.add.name)
                    elif piv_event_status in ["CREATION_DETOURNEMENT"]:
                        setattr(st_update, STATUS_MAP[event_toggle],
                                ModificationType.added_for_detour.name)
                    elif trip_status_type == TripEffect.ADDITIONAL_SERVICE:
                        # In this case we want to create schedules taking into account the delay
                        # without adding the delay a second time
                        setattr(st_update, STATUS_MAP[event_toggle],
                                ModificationType.add.name)
                    elif piv_event_status in [
                            "RETARD_OBSERVE", "RETARD_PROJETE"
                    ]:
                        setattr(st_update, STATUS_MAP[event_toggle],
                                ModificationType.update.name)
                        if piv_disruption:
                            piv_event_delay = piv_disruption.get(
                                "retard", {}).get("duree", 0)
                            setattr(st_update, DELAY_MAP[event_toggle],
                                    as_duration(piv_event_delay *
                                                60))  # minutes
                    else:
                        setattr(st_update, STATUS_MAP[event_toggle],
                                ModificationType.none.name)
                    # otherwise let those be none

                else:
                    raise InvalidArguments(
                        "invalid value {s} for field {t}/statutModification or {t}/evenement/type"
                        .format(s=piv_event_status, t=event_toggle))

                event_status = getattr(st_update, STATUS_MAP[event_toggle],
                                       ModificationType.none.name)
                highest_st_status = get_higher_status(highest_st_status,
                                                      event_status)

                mdi = (arret[STOP_EVENT_MDI_MAP[event_toggle]] if arret.get(
                    STOP_EVENT_MDI_MAP[event_toggle]) else False)
                if _is_stop_event_served(event_datetime, event_status, mdi):
                    if previous_rt_stop_event_time > event_datetime:
                        raise InvalidArguments(
                            "invalid feed: stop_point's({}) time is not consistent"
                            .format(
                                get_value(get_value(arret, "emplacement"),
                                          "code")))
                    previous_rt_stop_event_time = event_datetime

        # Calculates effect from stop_time status list (this work is also done in kraken and has to be deleted there)
        if trip_update.effect == TripEffect.MODIFIED_SERVICE.name:
            trip_update.effect = get_effect_from_modification_type(
                highest_st_status)
        return trip_update
コード例 #13
0
    def merge_trip_updates(self, navitia_vj, db_trip_update, new_trip_update):
        """
        Steps:
        1. Build TripUpdate info resulting of merge
        2. Adjust TripUpdate info to have it be self-consistent (call adjust_trip_update_consistency())
        3. If resulting TripUpdate info are new compared to the one from previous RT info: send it

        NB:
        * Working with ORM objects directly: no persistence of new object before knowing it's final version and
          it's not a duplicate (performance and db unicity constraints)
        """
        # * Retrieve information available in the 3 StopTimeUpdate lists *
        circulation_date = new_trip_update.vj.get_circulation_date()
        new_stus = new_trip_update.stop_time_updates

        # base-schedule STU list
        nav_stus = convert_nav_stop_list_to_stu_list(navitia_vj.get("stop_times", []), circulation_date)

        # last info known about STU in trip (before processing new feed):
        # either from previous RT feed or base-schedule
        old_stus = db_trip_update.stop_time_updates if db_trip_update else nav_stus

        # * Build matches between stus in different lists *
        old_to_nav = map_old_stu_to_nav_stu(old_stus=old_stus, nav_stus=nav_stus)
        if old_to_nav is None:
            # If nav_stus is not strictly contained into old_stus, then base-schedule was probably reloaded.
            # So let's restart from scratch using base-schedule
            fail_str = "Warning: previous RT result doesn't contain base-schedule"
            log_str = "{f}: navitia_vj.id={id}".format(f=fail_str, id=navitia_vj.get("id"))
            self.log.warning(log_str)
            record_internal_failure(fail_str, contributor=self.contributor.id, reason=log_str)

            old_stus = nav_stus
            old_to_nav = map_old_stu_to_nav_stu(old_stus=old_stus, nav_stus=nav_stus)

        new_to_old = list_match_new_stu_to_old_stu(new_stus=new_stus, old_stus=old_stus)

        # * Build resulting STU list *
        # keeps track of what was processed in previous STU info
        old_stus_unprocessed_start = new_to_old[0][1] if new_to_old else len(old_stus)

        # Old STUs before the first STU mentioned in new_stus:
        # Regular behavior is to keep them as-is (considered "archived")
        res_stus = old_stus[:old_stus_unprocessed_start]
        # Only exception is if trip is entirely deleted: keep only stops in nav_stus
        if new_trip_update.effect == TripEffect.NO_SERVICE.name:
            res_stus = [
                old_stus[idx] for idx in filter(lambda i: i in old_to_nav, range(0, old_stus_unprocessed_start))
            ]

        new_stus_unprocessed_start = 0
        propagated_delay = as_duration(0)
        # populate with new_stus (iterating on matches and filling voids in matches-intervals)
        for new_index, old_index in new_to_old:
            stus_until_match, last_delay = build_stops_until_match(
                match_new_index=new_index,
                match_old_index=old_index,
                new_stus_unprocessed_start=new_stus_unprocessed_start,
                old_stus_unprocessed_start=old_stus_unprocessed_start,
                new_stus=new_stus,
                nav_stus=nav_stus,
                old_to_nav=old_to_nav,
                result_idx_offset=len(res_stus),
                propagated_delay=propagated_delay,
            )
            res_stus.extend(stus_until_match)
            propagated_delay = last_delay

            # Remember progress
            new_stus_unprocessed_start = new_index + 1
            old_stus_unprocessed_start = old_index + 1

        # Finalize populating unmatched STUs in old_stus and reversely those unmatched in new_stus
        stus_until_end, _last_delay = create_intermediate_stus_before_match(
            match_new_index=len(new_stus),
            match_old_index=len(old_stus),
            new_stus_unprocessed_start=new_stus_unprocessed_start,
            old_stus_unprocessed_start=old_stus_unprocessed_start,
            new_stus=new_stus,
            nav_stus=nav_stus,
            old_to_nav=old_to_nav,
            result_idx_offset=len(res_stus),
            propagated_delay=propagated_delay,
        )
        res_stus.extend(stus_until_end)

        # adjust consistency for resulting trip_update
        adjust_gtfsrt_trip_update_consistency(new_trip_update, res_stus, nav_stus)

        return build_trip_update_if_modified(
            db_trip_update, old_stus, new_trip_update, res_stus, self.contributor.id
        )
コード例 #14
0
ファイル: merge_utils.py プロジェクト: woshilapin/kirin
def merge(navitia_vj, db_trip_update, new_trip_update, is_new_complete):
    """
    We need to merge the info from 3 sources:
        * the navitia base schedule
        * the trip update already in the db (potentially nonexistent)
        * the incoming trip update

    The result is either the db_trip_update if it exists, or the new_trip_update (it is updated as a side
    effect)

    The mechanism is quite simple:
        * the result trip status is the new_trip_update's status
            (ie if in the db the trip was cancelled, and a new update is only an update, the trip update is
            not cancelled anymore, only updated)

        * for each navitia's stop_time and for departure|arrival:
            - if there is an update on this stoptime (in new_trip_update):
                we compute the new datetime based on the new information and the navitia's base schedule
            - else if there is the stoptime in the db:
                we keep this db stoptime
            - else we keep the navitia's base schedule

    Note that the results is either 'db_trip_update' or 'new_trip_update'. Side effects on this object are
    thus wanted because of database persistency (update or creation of new objects)

    If is_new_complete==True, then new_trip_update is considered as a complete trip, so it will erase and
    replace the (old) db_trip_update.
    Detail: is_new_complete changes the way None is interpreted in the new_trip_update:
        - if is_new_complete==False, None means there is no new information, so we keep old ones
        - if is_new_complete==True, None means we are back to normal, so we keep the new None
          (for now it only impacts messages to allow removal)


    ** Important Note **:
    we DO NOT HANDLE changes in navitia's schedule for the moment
    it will need to be handled, but it will be done after
    """
    res = db_trip_update if db_trip_update else new_trip_update
    res_stoptime_updates = []

    res.status = new_trip_update.status
    res.effect = new_trip_update.effect
    if new_trip_update.message is not None or is_new_complete:
        res.message = new_trip_update.message

    if res.status == ModificationType.delete.name:
        # for trip cancellation, we delete all StopTimeUpdates
        res.stop_time_updates = []
        return res

    has_changes = False
    previous_stop_event = TimeDelayTuple(time=None, delay=None)
    last_departure = None
    circulation_date = new_trip_update.vj.get_circulation_date()

    yield_next_stop = (yield_next_stop_from_trip_update(
        new_trip_update, db_trip_update) if is_new_complete else
                       yield_next_stop_from_base_schedule_vj(navitia_vj))

    for nav_order, navitia_stop in yield_next_stop:
        if navitia_stop is None:
            logging.getLogger(__name__).warning(
                "No stop point found (order:{}".format(nav_order))
            continue

        # TODO handle forbidden pickup/drop-off (in those case set departure/arrival at None)
        nav_departure_time = navitia_stop.get("utc_departure_time")
        nav_arrival_time = navitia_stop.get("utc_arrival_time")

        # we compute the arrival time and departure time on base schedule and take past mid-night into
        # consideration
        base_arrival = base_departure = None
        stop_id = navitia_stop.get("stop_point", {}).get("id")
        new_st = new_trip_update.find_stop(stop_id, nav_order)

        # considering only served arrival
        if is_stop_event_served(
                event_name="arrival",
                stop_id=stop_id,
                stop_order=nav_order,
                nav_stop=navitia_stop,
                db_tu=db_trip_update,
                new_stu=new_st,
        ):
            arrival_delay = new_st.arrival_delay if (
                new_st and new_st.arrival_delay) else as_duration(0)
            arrival_stop_event = TimeDelayTuple(time=nav_arrival_time,
                                                delay=arrival_delay)

            # For arrival we need to compare arrival time and delay with previous departure time and delay
            if nav_arrival_time is not None:
                if is_past_midnight_event(previous_stop_event,
                                          arrival_stop_event):
                    # last departure is after arrival, it's a past-midnight
                    circulation_date += datetime.timedelta(days=1)
                base_arrival = datetime.datetime.combine(
                    circulation_date, nav_arrival_time)

            # store arrival as previous stop-event
            previous_stop_event = arrival_stop_event

        # considering only served departure (same logic as before)
        if is_stop_event_served(
                event_name="departure",
                stop_id=stop_id,
                stop_order=nav_order,
                nav_stop=navitia_stop,
                db_tu=db_trip_update,
                new_stu=new_st,
        ):
            departure_delay = new_st.departure_delay if (
                new_st and new_st.departure_delay) else as_duration(0)
            departure_stop_event = TimeDelayTuple(time=nav_departure_time,
                                                  delay=departure_delay)

            if nav_departure_time is not None:
                if is_past_midnight_event(previous_stop_event,
                                          departure_stop_event):
                    # departure is before arrival, it's a past-midnight
                    circulation_date += datetime.timedelta(days=1)
                base_departure = datetime.datetime.combine(
                    circulation_date, nav_departure_time)

            # store departure as previous stop-event
            previous_stop_event = departure_stop_event

        if db_trip_update is not None and new_st is not None:
            """
            First case: we already have recorded the delay and we find update info in the new trip update
            Then      : we should probably update it or not if the input info is exactly the same as the
                        one in db.
            """
            db_st = db_trip_update.find_stop(stop_id, nav_order)
            new_st_update = _make_stop_time_update(base_arrival,
                                                   base_departure,
                                                   last_departure,
                                                   new_st,
                                                   navitia_stop["stop_point"],
                                                   order=nav_order)
            has_changes |= (db_st is None) or not db_st.is_equal(new_st_update)
            res_st = new_st_update if has_changes else db_st

        elif db_trip_update is None and new_st is not None:
            """
            Second case: we have not yet recorded the delay
            Then       : it's time to create one in the db
            """
            has_changes = True
            res_st = _make_stop_time_update(base_arrival,
                                            base_departure,
                                            last_departure,
                                            new_st,
                                            navitia_stop["stop_point"],
                                            order=nav_order)
            res_st.message = new_st.message

        elif db_trip_update is not None and new_st is None:
            """
            Third case: we have already recorded a delay but nothing is mentioned in the new trip update
            Then      : For cots, we do nothing but only update stop time's order
                        For gtfs-rt, according to the specification, we should use the delay from the previous
                        stop time, which will be handled sooner by the connector-specified model maker

                        *** Here, we MUST NOT do anything, only update stop time's order ***
            """
            db_st = db_trip_update.find_stop(stop_id, nav_order)
            res_st = (db_st if db_st is not None else StopTimeUpdate(
                navitia_stop["stop_point"],
                departure=base_departure,
                arrival=base_arrival,
                order=nav_order))
            has_changes |= db_st is None
        else:
            """
            Last case: nothing is recorded yet and there is no update info in the new trip update
            Then     : take the base schedule's arrival/departure time and let's create a whole new world!
            """
            has_changes = True
            res_st = StopTimeUpdate(navitia_stop["stop_point"],
                                    departure=base_departure,
                                    arrival=base_arrival,
                                    order=nav_order)

        last_departure = res_st.departure
        res_stoptime_updates.append(res_st)

    # Use effect inside the new trip_update (input data feed).
    # It is already computed inside build function (KirinModelBuilder)
    # TODO: process this effect after the merge, as effect should have memory of what's been done before
    #       in case of differential RT feed (that's the case on GTFS-RT)
    res.effect = new_trip_update.effect

    if has_changes:
        res.stop_time_updates = res_stoptime_updates
        if manage_consistency(res):
            return res

    return None