/** * Copyright (C) 2011 Brian Ferris <bdferris@onebusaway.org> * * 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 org.onebusaway.transit_data_federation.impl; import java.io.Serializable; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.PostConstruct; import net.sf.ehcache.Cache; import net.sf.ehcache.Element; import org.onebusaway.collections.CollectionsLibrary; import org.onebusaway.container.cache.Cacheable; import org.onebusaway.container.refresh.Refreshable; import org.onebusaway.gtfs.model.calendar.LocalizedServiceId; import org.onebusaway.gtfs.model.calendar.ServiceDate; import org.onebusaway.gtfs.model.calendar.ServiceInterval; import org.onebusaway.gtfs.services.calendar.CalendarService; import org.onebusaway.transit_data_federation.services.ExtendedCalendarService; import org.onebusaway.transit_data_federation.services.transit_graph.BlockConfigurationEntry; import org.onebusaway.transit_data_federation.services.transit_graph.BlockEntry; import org.onebusaway.transit_data_federation.services.transit_graph.ServiceIdActivation; import org.onebusaway.transit_data_federation.services.transit_graph.TransitGraphDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class ExtendedCalendarServiceImpl implements ExtendedCalendarService { private CalendarService _calendarService; private TransitGraphDao _transitGraphDao; private Map<ServiceIdActivation, List<Date>> _serviceDatesByServiceIds = new HashMap<ServiceIdActivation, List<Date>>(); private double _serviceDateRangeCacheInterval = 4 * 60 * 60; private Cache _serviceDateRangeCache; private int _serviceDateLowerBoundsInWeeks = -1; private int _serviceDateUpperBoundsInWeeks = -1; public void setServiceDateLowerBoundsInWeeks(int serviceDateLowerBoundsInWeeks) { _serviceDateLowerBoundsInWeeks = serviceDateLowerBoundsInWeeks; } public void setServiceDateUpperBoundsInWeeks(int serviceDateUpperBoundsInWeeks) { _serviceDateUpperBoundsInWeeks = serviceDateUpperBoundsInWeeks; } @Autowired public void setCalendarService(CalendarService calendarService) { _calendarService = calendarService; } @Autowired public void setTransitGraphDao(TransitGraphDao transitGraphDao) { _transitGraphDao = transitGraphDao; } public void setServiceDateRangeCacheInterval(int hours) { _serviceDateRangeCacheInterval = hours * 60 * 60; } public void setServiceDateRangeCache(Cache serviceDateRangeCache) { _serviceDateRangeCache = serviceDateRangeCache; } @PostConstruct @Refreshable(dependsOn = RefreshableResources.CALENDAR_DATA) public void start() { cacheServiceDatesForServiceIds(); } @Cacheable @Override public Set<ServiceDate> getServiceDatesForServiceIds( ServiceIdActivation serviceIds) { Set<ServiceDate> serviceDates = null; List<LocalizedServiceId> activeServiceIds = serviceIds.getActiveServiceIds(); List<LocalizedServiceId> inactiveServiceIds = serviceIds.getInactiveServiceIds(); for (LocalizedServiceId activeServiceId : activeServiceIds) { Set<ServiceDate> dates = _calendarService.getServiceDatesForServiceId(activeServiceId.getId()); if (dates.isEmpty()) return Collections.emptySet(); if (serviceDates == null) serviceDates = new HashSet<ServiceDate>(dates); else serviceDates.retainAll(dates); if (serviceDates.isEmpty()) return Collections.emptySet(); } for (LocalizedServiceId inactiveServiceId : inactiveServiceIds) { Set<ServiceDate> dates = _calendarService.getServiceDatesForServiceId(inactiveServiceId.getId()); serviceDates.removeAll(dates); } return serviceDates; } @Cacheable @Override public Set<Date> getDatesForServiceIds(ServiceIdActivation serviceIds) { Set<Date> serviceDates = null; List<LocalizedServiceId> activeServiceIds = serviceIds.getActiveServiceIds(); List<LocalizedServiceId> inactiveServiceIds = serviceIds.getInactiveServiceIds(); for (LocalizedServiceId activeServiceId : activeServiceIds) { List<Date> dates = _calendarService.getDatesForLocalizedServiceId(activeServiceId); if (dates.isEmpty()) return Collections.emptySet(); if (serviceDates == null) serviceDates = new HashSet<Date>(dates); else serviceDates.retainAll(dates); if (serviceDates.isEmpty()) return Collections.emptySet(); } for (LocalizedServiceId inactiveServiceId : inactiveServiceIds) { List<Date> dates = _calendarService.getDatesForLocalizedServiceId(inactiveServiceId); serviceDates.removeAll(dates); } return serviceDates; } @Cacheable public List<Date> getDatesForServiceIdsAsOrderedList( ServiceIdActivation serviceIds) { Set<Date> dates = getDatesForServiceIds(serviceIds); List<Date> list = new ArrayList<Date>(dates); Collections.sort(list); return list; } @SuppressWarnings("unchecked") @Override public Collection<Date> getServiceDatesWithinRange( ServiceIdActivation serviceIds, ServiceInterval interval, Date from, Date to) { if (_serviceDateRangeCache == null) return getServiceDatesWithinRangeExact(serviceIds, interval, from, to); ServiceDateRangeKey key = getCacheKey(serviceIds, interval, from, to); Element element = _serviceDateRangeCache.get(key); if (element == null) { serviceIds = key.getServiceIds(); interval = key.getInterval(); from = key.getFromTime(); to = key.getToTime(); Collection<Date> values = getServiceDatesWithinRangeExact(serviceIds, interval, from, to); element = new Element(key, values); _serviceDateRangeCache.put(element); } return (Collection<Date>) element.getValue(); } @Override @Cacheable public boolean areServiceIdsActiveOnServiceDate( ServiceIdActivation serviceIds, Date serviceDate) { List<LocalizedServiceId> activeServiceIds = serviceIds.getActiveServiceIds(); List<LocalizedServiceId> inactiveServiceIds = serviceIds.getInactiveServiceIds(); // 95% of configs look like this if (activeServiceIds.size() == 1 && inactiveServiceIds.isEmpty()) { LocalizedServiceId lsid = activeServiceIds.get(0); return _calendarService.isLocalizedServiceIdActiveOnDate(lsid, serviceDate); } for (LocalizedServiceId lsid : activeServiceIds) { if (!_calendarService.isLocalizedServiceIdActiveOnDate(lsid, serviceDate)) return false; } for (LocalizedServiceId lsid : inactiveServiceIds) { if (_calendarService.isLocalizedServiceIdActiveOnDate(lsid, serviceDate)) return false; } return true; } @Override public List<Date> getServiceDatesForInterval(ServiceIdActivation serviceIds, ServiceInterval serviceInterval, long time, boolean findDepartures) { if (findDepartures) return getNextServiceDatesForDepartureInterval(serviceIds, serviceInterval, time); else return getPreviousServiceDatesForArrivalInterval(serviceIds, serviceInterval, time); } @Override public List<Date> getNextServiceDatesForDepartureInterval( ServiceIdActivation serviceIds, ServiceInterval serviceInterval, long time) { List<Date> serviceDates = _serviceDatesByServiceIds.get(serviceIds); if (CollectionsLibrary.isEmpty(serviceDates)) return Collections.emptyList(); int offset = (serviceInterval.getMaxDeparture() - serviceInterval.getMinDeparture()) * 1000; Date offsetDate = new Date(time - offset); int startIndex = Collections.binarySearch(serviceDates, offsetDate); if (startIndex < 0) startIndex = -(startIndex + 1); startIndex = Math.max(0, startIndex - 1); List<Date> serviceDatesToReturn = new ArrayList<Date>(); boolean directHit = false; for (int index = startIndex; index < serviceDates.size(); index++) { Date serviceDate = serviceDates.get(index); long timeFrom = serviceDate.getTime() + serviceInterval.getMinDeparture() * 1000; long timeTo = serviceDate.getTime() + serviceInterval.getMaxDeparture() * 1000; if (time < timeFrom) { if (!directHit) { serviceDatesToReturn.add(serviceDate); } return serviceDatesToReturn; } if (timeFrom <= time && time <= timeTo) { serviceDatesToReturn.add(serviceDate); directHit = true; } } return serviceDatesToReturn; } @Override public List<Date> getPreviousServiceDatesForArrivalInterval( ServiceIdActivation serviceIds, ServiceInterval serviceInterval, long time) { List<Date> serviceDates = _serviceDatesByServiceIds.get(serviceIds); if (CollectionsLibrary.isEmpty(serviceDates)) return Collections.emptyList(); int offset = (serviceInterval.getMaxDeparture() - serviceInterval.getMinDeparture()) * 1000; Date offsetDate = new Date(time + offset); int endIndex = Collections.binarySearch(serviceDates, offsetDate); if (endIndex < 0) endIndex = -(endIndex + 1); endIndex = Math.min(serviceDates.size() - 1, endIndex + 1); List<Date> serviceDatesToReturn = new ArrayList<Date>(); boolean directHit = false; for (int index = endIndex; index >= 0; index--) { Date serviceDate = serviceDates.get(index); long timeFrom = serviceDate.getTime() + serviceInterval.getMinDeparture() * 1000; long timeTo = serviceDate.getTime() + serviceInterval.getMaxDeparture() * 1000; if (time > timeTo) { if (!directHit) { serviceDatesToReturn.add(serviceDate); } return serviceDatesToReturn; } if (timeFrom <= time && time <= timeTo) { serviceDatesToReturn.add(serviceDate); directHit = true; } } return serviceDatesToReturn; } /**** * Private Methods ****/ private ServiceDateRangeKey getCacheKey(ServiceIdActivation serviceIds, ServiceInterval interval, Date from, Date to) { Serializable serviceIdsKey = getServiceIdsKey(serviceIds); int fromStopTime = (int) (Math.floor(interval.getMinArrival() / _serviceDateRangeCacheInterval) * _serviceDateRangeCacheInterval); int toStopTime = (int) (Math.ceil(interval.getMaxDeparture() / _serviceDateRangeCacheInterval) * _serviceDateRangeCacheInterval); double m = _serviceDateRangeCacheInterval * 1000; long fromTime = (long) (Math.floor(from.getTime() / m) * m); long toTime = (long) (Math.ceil(to.getTime() / m) * m); return new ServiceDateRangeKey(serviceIdsKey, fromStopTime, toStopTime, fromTime, toTime); } private Serializable getServiceIdsKey(ServiceIdActivation serviceIds) { List<LocalizedServiceId> activeServiceIds = serviceIds.getActiveServiceIds(); List<LocalizedServiceId> inactiveServiceIds = serviceIds.getInactiveServiceIds(); if (activeServiceIds.size() == 1 && inactiveServiceIds.isEmpty()) return activeServiceIds.get(0); return serviceIds; } private Collection<Date> getServiceDatesWithinRangeExact( ServiceIdActivation serviceIds, ServiceInterval interval, Date from, Date to) { Set<Date> serviceDates = null; List<LocalizedServiceId> activeServiceIds = serviceIds.getActiveServiceIds(); List<LocalizedServiceId> inactiveServiceIds = serviceIds.getInactiveServiceIds(); // System.out.println(serviceIds + " " + interval + " " + from + " " + to); // 95% of configs look like this if (activeServiceIds.size() == 1 && inactiveServiceIds.isEmpty()) return _calendarService.getServiceDatesWithinRange( activeServiceIds.get(0), interval, from, to); for (LocalizedServiceId serviceId : activeServiceIds) { List<Date> dates = _calendarService.getServiceDatesWithinRange(serviceId, interval, from, to); // If the dates are ever empty here, we can short circuit to no dates if (dates.isEmpty()) return Collections.emptyList(); if (serviceDates == null) serviceDates = new HashSet<Date>(dates); else serviceDates.retainAll(serviceDates); // If the dates are empty here after the intersection operation, we can // short circuit to no dates if (serviceDates.isEmpty()) return Collections.emptyList(); } if (!inactiveServiceIds.isEmpty()) { for (LocalizedServiceId serviceId : inactiveServiceIds) { List<Date> dates = _calendarService.getServiceDatesWithinRange( serviceId, interval, from, to); serviceDates.removeAll(dates); } } return serviceDates; } private void cacheServiceDatesForServiceIds() { if(_serviceDateRangeCache != null) { _serviceDateRangeCache.removeAll(); } _serviceDatesByServiceIds.clear(); Set<ServiceIdActivation> allServiceIds = determineAllServiceIds(); Date lowerBounds = null; if (_serviceDateLowerBoundsInWeeks != -1) { Calendar c = Calendar.getInstance(); c.add(Calendar.WEEK_OF_YEAR, -_serviceDateLowerBoundsInWeeks); lowerBounds = c.getTime(); } Date upperBounds = null; if (_serviceDateUpperBoundsInWeeks != -1) { Calendar c = Calendar.getInstance(); c.add(Calendar.WEEK_OF_YEAR, _serviceDateUpperBoundsInWeeks); upperBounds = c.getTime(); } for (ServiceIdActivation serviceIds : allServiceIds) { List<Date> dates = computeServiceDatesForServiceIds(serviceIds, lowerBounds, upperBounds); _serviceDatesByServiceIds.put(serviceIds, dates); } } private Set<ServiceIdActivation> determineAllServiceIds() { Set<ServiceIdActivation> allServiceIds = new HashSet<ServiceIdActivation>(); for (BlockEntry block : _transitGraphDao.getAllBlocks()) { for (BlockConfigurationEntry blockConfig : block.getConfigurations()) { ServiceIdActivation serviceIds = blockConfig.getServiceIds(); allServiceIds.add(serviceIds); } } return allServiceIds; } private List<Date> computeServiceDatesForServiceIds( ServiceIdActivation serviceIds, Date lowerBounds, Date upperBounds) { Set<Date> serviceDates = null; for (LocalizedServiceId lsid : serviceIds.getActiveServiceIds()) { List<Date> dates = _calendarService.getDatesForLocalizedServiceId(lsid); if (dates == null) dates = Collections.emptyList(); if (serviceDates == null) serviceDates = new HashSet<Date>(dates); else serviceDates.retainAll(dates); } for (LocalizedServiceId lsid : serviceIds.getInactiveServiceIds()) { List<Date> dates = _calendarService.getDatesForLocalizedServiceId(lsid); if (serviceDates != null) serviceDates.removeAll(dates); } List<Date> dates = new ArrayList<Date>(); if (serviceDates != null) { for (Date serviceDate : serviceDates) { if ((lowerBounds == null || lowerBounds.before(serviceDate)) && (upperBounds == null || serviceDate.before(upperBounds))) dates.add(serviceDate); } } Collections.sort(dates); return dates; } private class ServiceDateRangeKey { private final Serializable _serviceIds; private final int _fromStopTime; private final int _toStopTime; private final long _fromTime; private final long _toTime; public ServiceDateRangeKey(Serializable serviceIds, int fromStopTime, int toStopTime, long fromTime, long toTime) { if (serviceIds == null) throw new IllegalStateException("serviceIds cannot be null"); _serviceIds = serviceIds; _fromStopTime = fromStopTime; _toStopTime = toStopTime; _fromTime = fromTime; _toTime = toTime; } public ServiceIdActivation getServiceIds() { if (_serviceIds instanceof ServiceIdActivation) { return (ServiceIdActivation) _serviceIds; } else if (_serviceIds instanceof LocalizedServiceId) { return new ServiceIdActivation((LocalizedServiceId) _serviceIds); } else { throw new IllegalStateException("unknown service id type: " + _serviceIds); } } public ServiceInterval getInterval() { return new ServiceInterval(_fromStopTime, _toStopTime); } public Date getFromTime() { return new Date(_fromTime); } public Date getToTime() { return new Date(_toTime); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + _fromStopTime; result = prime * result + (int) (_fromTime ^ (_fromTime >>> 32)); result = prime * result + _serviceIds.hashCode(); result = prime * result + _toStopTime; result = prime * result + (int) (_toTime ^ (_toTime >>> 32)); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ServiceDateRangeKey other = (ServiceDateRangeKey) obj; if (_fromStopTime != other._fromStopTime) return false; if (_fromTime != other._fromTime) return false; if (!_serviceIds.equals(other._serviceIds)) return false; if (_toStopTime != other._toStopTime) return false; if (_toTime != other._toTime) return false; return true; } } }