/* * Copyright (C) 2014 Kurt Raschke <kurt@kurtraschke.com> * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.kurtraschke.wmata.gtfsrealtime.services; import org.onebusaway.collections.Min; import org.onebusaway.collections.tuple.T2; import org.onebusaway.collections.tuple.Tuples; import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.gtfs.model.Route; import org.onebusaway.gtfs.model.StopTime; import org.onebusaway.gtfs.model.Trip; import org.onebusaway.gtfs.model.calendar.CalendarServiceData; import org.onebusaway.gtfs.model.calendar.ServiceDate; import org.onebusaway.gtfs.services.GtfsRelationalDao; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.kurtraschke.wmata.gtfsrealtime.AgencyTimeZone; import com.kurtraschke.wmata.gtfsrealtime.DateTimeUtils; import com.kurtraschke.wmata.gtfsrealtime.WMATAAPIException; import com.kurtraschke.wmata.gtfsrealtime.api.buspositions.BusPosition; import com.kurtraschke.wmata.gtfsrealtime.api.routeschedule.RouteSchedule; import com.kurtraschke.wmata.gtfsrealtime.api.routeschedule.WMATAStopTime; import com.kurtraschke.wmata.gtfsrealtime.api.routeschedule.WMATATrip; import com.kurtraschke.wmata.gtfsrealtime.model.TripMapKey; import net.sf.ehcache.Cache; import net.sf.ehcache.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TimeZone; import javax.inject.Inject; import javax.inject.Named; /** * * @author kurt */ public class WMATATripMapperService { private static final Logger _log = LoggerFactory.getLogger(WMATATripMapperService.class); private WMATARouteMapperService _routeMapperService; private WMATAAPIService _api; private Cache _tripCache; private CalendarServiceData _csd; private GtfsRelationalDao _dao; private TimeZone _agencyTimeZone; private int _scoreLimit; @Inject public void setWMATARouteMapperService(WMATARouteMapperService mapperService) { _routeMapperService = mapperService; } @Inject public void setWMATAAPIService(WMATAAPIService api) { _api = api; } @Inject public void setTripCache(@Named("caches.trip") Cache tripCache) { _tripCache = tripCache; } @Inject public void setCalendarServiceData(CalendarServiceData csd) { _csd = csd; } @Inject public void setGtfsRelationalDao(GtfsRelationalDao dao) { _dao = dao; } @Inject public void setAgencyTimeZone(@AgencyTimeZone TimeZone timeZone) { _agencyTimeZone = timeZone; } @Inject public void setScoreLimit(@Named("tripMapping.scoreLimit") int scoreLimit) { _scoreLimit = scoreLimit; } public AgencyAndId getTripMapping(BusPosition bp) throws WMATAAPIException { ServiceDate serviceDate = bp.getServiceDate(); String tripID = bp.getTripID(); TripMapKey k = new TripMapKey(serviceDate, tripID); Element e = _tripCache.get(k); if (e == null) { AgencyAndId mappedTripID = mapTrip(bp); _tripCache.put(new Element(k, mappedTripID)); return mappedTripID; } else { return (AgencyAndId) e.getObjectValue(); } } private AgencyAndId mapTrip(BusPosition bp) throws WMATAAPIException { WMATATrip theTrip = getWMATATrip(bp.getServiceDate(), bp.getRouteID(), bp.getTripStartTime(), bp.getTripEndTime(), bp.getDirectionText()); ServiceDate serviceDate = bp.getServiceDate(); if (theTrip != null) { return mapTrip(serviceDate, theTrip); } else { return null; } } private WMATATrip getWMATATrip(ServiceDate serviceDate, String routeID, Date tripStartTime, Date tripEndTime, String tripDirection) throws WMATAAPIException { RouteSchedule rsi = _api.downloadRouteScheduleInfo(routeID, DateTimeUtils.apiDateStringForServiceDate(serviceDate)); for (WMATATrip t : rsi.getTrips()) { if (t.getStartTime().equals(tripStartTime) && t.getEndTime().equals(tripEndTime) && t.getTripDirectionText().equals(tripDirection)) { return t; } } return null; } private AgencyAndId mapTrip(ServiceDate serviceDate, WMATATrip theTrip) { AgencyAndId mappedRouteID = _routeMapperService.getRouteMapping(theTrip.getRouteID()); if (mappedRouteID != null) { Collection<Trip> candidateTrips = tripsForServiceDateAndRoute( serviceDate, mappedRouteID); if (candidateTrips.size() > 0) { T2<Double, Trip> result = findBestGtfsTripForWMATATrip(theTrip, candidateTrips, serviceDate); double mappingScore = result.getFirst(); Trip mappedTrip = result.getSecond(); if (mappingScore < _scoreLimit) { AgencyAndId mappedTripID = mappedTrip.getId(); _log.info("Mapped WMATA trip " + theTrip.getTripID() + " to GTFS trip " + mappedTripID + " with score " + Math.round(mappingScore)); return mappedTripID; } else { /* * In this case, we had one or more candidate trips from the GTFS * schedule to evaluate, but the best of them produced a score that * was too high to consider a reliable match. */ _log.warn("Could not map WMATA trip " + theTrip.getTripID() + " on route " + theTrip.getRouteID() + " with score " + Math.round(mappingScore)); return null; } } else { /* * This is the case where the GTFS schedule simply doesn't return any * active trips for that route and time. */ _log.warn("Could not map WMATA trip " + theTrip.getTripID() + " on route " + theTrip.getRouteID() + " (no candidates from GTFS schedule)"); return null; } } else { /* * This is the case where we could not map the route; no sense trying to * map the trip when we don't know the route. */ _log.warn("Could not map WMATA trip " + theTrip.getTripID() + " (could not map route " + theTrip.getRouteID() + ")"); return null; } } private Collection<Trip> tripsForServiceDateAndRoute(ServiceDate serviceDate, AgencyAndId route) { Route r = _dao.getRouteForId(route); final Set<AgencyAndId> services = _csd.getServiceIdsForDate(serviceDate); List<Trip> allTrips = _dao.getTripsForRoute(r); return Collections2.<Trip> filter(allTrips, new Predicate<Trip>() { @Override public boolean apply(Trip t) { return services.contains(t.getServiceId()); } }); } private T2<Double, Trip> findBestGtfsTripForWMATATrip(WMATATrip wmataTrip, Collection<Trip> gtfsTrips, ServiceDate serviceDate) { List<WMATAStopTime> wmataStopTimes = wmataTrip.getStopTimes(); Collections.sort(wmataStopTimes); Min<Trip> m = new Min<>(); for (Trip gtfsTrip : gtfsTrips) { double score = computeStopTimeAlignmentScore(wmataStopTimes, _dao.getStopTimesForTrip(gtfsTrip), serviceDate); m.add(score, gtfsTrip); } if (m.getMinValue() > _scoreLimit) { StringBuilder b = new StringBuilder(); for (WMATAStopTime stopTime : wmataTrip.getStopTimes()) { b.append("\n "); b.append(stopTime.getStopID()); b.append(" "); b.append(stopTime.getStopName()); b.append(" "); b.append(stopTime.getTime()); b.append(" "); } b.append("\n-----"); for (StopTime stopTime : _dao.getStopTimesForTrip( m.getMinElement())) { b.append("\n "); b.append(stopTime.getStop().getCode()); b.append(" "); b.append(stopTime.getStop().getName()); b.append(" "); b.append(new Date((getTime(stopTime) * 1000L) + serviceDate.getAsDate(_agencyTimeZone).getTime())); b.append(" "); } _log.warn("no good match found for trip:" + b.toString()); } return Tuples.<Double, Trip> tuple(m.getMinValue(), m.getMinElement()); } private double computeStopTimeAlignmentScore( List<WMATAStopTime> wmataStopTimes, List<StopTime> gtfsStopTimes, ServiceDate serviceDate) { Map<String, StopTimes> gtfsStopIdToStopTimes = new HashMap<>(); for (int index = 0; index < gtfsStopTimes.size(); index++) { StopTime stopTime = gtfsStopTimes.get(index); String stopId = stopTime.getStop().getCode(); StopTimes stopTimes = gtfsStopIdToStopTimes.get(stopId); if (stopTimes == null) { stopTimes = new StopTimes(); gtfsStopIdToStopTimes.put(stopId, stopTimes); } stopTimes.addStopTime(stopTime, index); } for (StopTimes stopTimes : gtfsStopIdToStopTimes.values()) { stopTimes.pack(); } Map<WMATAStopTime, Integer> mapping = new HashMap<>(); for (WMATAStopTime wmataStopTime : wmataStopTimes) { StopTimes stopTimes = gtfsStopIdToStopTimes.get(wmataStopTime.getStopID()); if (stopTimes == null) { mapping.put(wmataStopTime, -1); } else { int bestIndex = stopTimes.computeBestStopTimeIndex((int) ((wmataStopTime.getTime().getTime() - serviceDate.getAsDate( _agencyTimeZone).getTime()) / 1000L)); mapping.put(wmataStopTime, bestIndex); } } int lastIndex = -1; int score = 0; boolean allMisses = true; for (Map.Entry<WMATAStopTime, Integer> entry : mapping.entrySet()) { WMATAStopTime wmataStopTime = entry.getKey(); int index = entry.getValue(); StopTime gtfsStopTime = null; if (0 <= index && index < gtfsStopTimes.size()) { gtfsStopTime = gtfsStopTimes.get(index); } if (gtfsStopTime == null) { score += 15; // A miss is a 15 minute penalty } else { allMisses = false; if (index < lastIndex) { score += 15; // Out of order is a 10 minute penalty } int delta = Math.abs(((int) (wmataStopTime.getTime().getTime() / 1000L)) - (getTime(gtfsStopTime) + (int) (serviceDate.getAsDate(_agencyTimeZone).getTime() / 1000L))) / 60; score += delta; lastIndex = index; } } if (allMisses) { return 4 * 60 * 60; } return score; } private static int getTime(StopTime stopTime) { return ((stopTime.getDepartureTime() + stopTime.getArrivalTime()) / 2); } private static class StopTimes { private List<StopTime> stopTimes = new ArrayList<>(); private List<Integer> indices = new ArrayList<>(); private int[] times; public void addStopTime(StopTime stopTime, int index) { stopTimes.add(stopTime); indices.add(index); } public int computeBestStopTimeIndex(int time) { int index = Arrays.binarySearch(times, time); if (index < 0) { index = -(index + 1); } if (index < 0 || index >= indices.size()) { return -1; } return indices.get(index); } public void pack() { times = new int[stopTimes.size()]; for (int i = 0; i < stopTimes.size(); ++i) { StopTime stopTime = stopTimes.get(i); times[i] = getTime(stopTime); } } } }