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)
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
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
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
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
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
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
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
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)
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
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)
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
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 )
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