Skip to content

Commit

Permalink
MTA-50 add service interval support to calendar
Browse files Browse the repository at this point in the history
  • Loading branch information
sheldonabrown committed Jan 8, 2024
1 parent f849874 commit 166e24c
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,12 @@ public List<Date> getServiceDateArrivalsWithinRange(LocalizedServiceId serviceId
return _calendarService.getServiceDateArrivalsWithinRange(serviceId, interval, from, to);
}

@Override
public boolean isLocalizedServiceIdActiveInRange(LocalizedServiceId serviceId,
ServiceInterval scheduledService,
AgencyServiceInterval serviceInterval) {
return _calendarService.isLocalizedServiceIdActiveInRange(serviceId, scheduledService, serviceInterval);
}
@Override
public Map<LocalizedServiceId, List<Date>> getServiceDateArrivalsWithinRange(ServiceIdIntervals serviceIdIntervals, Date from, Date to) {
return _calendarService.getServiceDateArrivalsWithinRange(serviceIdIntervals, from, to);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,7 @@
import java.util.TimeZone;

import org.onebusaway.gtfs.model.AgencyAndId;
import org.onebusaway.gtfs.model.calendar.CalendarServiceData;
import org.onebusaway.gtfs.model.calendar.LocalizedServiceId;
import org.onebusaway.gtfs.model.calendar.ServiceDate;
import org.onebusaway.gtfs.model.calendar.ServiceIdIntervals;
import org.onebusaway.gtfs.model.calendar.ServiceInterval;
import org.onebusaway.gtfs.model.calendar.*;
import org.onebusaway.gtfs.services.calendar.CalendarService;
import org.onebusaway.gtfs.services.calendar.CalendarServiceDataFactory;

Expand Down Expand Up @@ -126,6 +122,34 @@ public boolean isLocalizedServiceIdActiveOnDate(
return Collections.binarySearch(dates, serviceDate) >= 0;
}

/**
* test if the given calendar servieId is active in the union of the activeService
* window and the agencyServiceInterval.
* @param localizedServiceId
* @param activeService
* @param agencyServiceInterval
* @return
*/
public boolean isLocalizedServiceIdActiveInRange(LocalizedServiceId localizedServiceId,
ServiceInterval activeService,
AgencyServiceInterval agencyServiceInterval) {
if (agencyServiceInterval == null || agencyServiceInterval.getServiceDate() == null) {
throw new IllegalStateException("agencyServiceInterval cannot be null");
}
ServiceInterval serviceInterval = agencyServiceInterval.getServiceInterval(localizedServiceId.getId().getAgencyId());

boolean active = isLocalizedServiceIdActiveOnDate(localizedServiceId, agencyServiceInterval.getServiceDate().getAsDate());
if (active) {
// even if a match is found enforce overlap in service intervals
if ( Math.max(activeService.getMinArrival(), serviceInterval.getMinArrival())
< Math.min(activeService.getMaxDeparture(), serviceInterval.getMaxDeparture())) {
return true;
}
}

return false;
}

@Override
public List<Date> getServiceDateArrivalsWithinRange(
LocalizedServiceId serviceId, ServiceInterval interval, Date from, Date to) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* Copyright (C) 2024 Cambridge Systematics, Inc.
*
* 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.gtfs.model.calendar;

import java.io.Serializable;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
* Represent a service interval (a period of defined transit both scheduled and dynamic)
* that exists on a given service date and window relative to that date. The times may differ
* based on the specified agency. The net effect is that the period of consideration can
* be smaller for more frequent service (such as a subway/BRT provider) vs a traditional
* fixed bus service.
*/
public class AgencyServiceInterval implements Serializable {

public static final int SECONDS_IN_DAY = 24 * 60 * 60;

private final long _referenceTime;
private final ServiceDate _serviceDate;

/**
* Map of overrides that have a different window of applicability of service relative
* to the reference time. The default is the entire service date. Values should be
* AGENCY_ID, MINUTES_AFTER_REFERENCE_TIME.
*
*/
private final Map<String, Integer> _overridesByAgencyId = new HashMap<>();


public AgencyServiceInterval(long referenceTime) {
_referenceTime = referenceTime;
_serviceDate = new ServiceDate(new Date(referenceTime));
}

public AgencyServiceInterval(ServiceDate serviceDate) {
_referenceTime = serviceDate.getAsDate().getTime();
_serviceDate = serviceDate;
}

public AgencyServiceInterval(long referenceTime, Map<String, Integer> agencyIdOverrides) {
_referenceTime = referenceTime;
_serviceDate = new ServiceDate(new Date(referenceTime));
if (agencyIdOverrides != null)
_overridesByAgencyId.putAll(agencyIdOverrides);
}

public ServiceDate getServiceDate() {
return _serviceDate;
}

public ServiceInterval getServiceInterval(String agencyId) {

if (_overridesByAgencyId.containsKey(agencyId)) {
// override will be referenceTime, referenceTime+window (in minutes)
ServiceDate serviceDate = new ServiceDate(new Date(_referenceTime));
int startSecondsIntoDay = Math.toIntExact(_referenceTime - serviceDate.getAsDate().getTime()) / 1000;
int endSecondsIntoDay = startSecondsIntoDay + (_overridesByAgencyId.get(agencyId) * 60);
return new ServiceInterval(startSecondsIntoDay, endSecondsIntoDay);
}
// default will be 0, endOfDay (aka entire service day)
return new ServiceInterval(0, SECONDS_IN_DAY);
}
public Date getFrom(String agencyId) {
if (_overridesByAgencyId.containsKey(agencyId))
return new Date(_referenceTime);
return new Date(_referenceTime);
}

public Date getTo(String agencyId) {
if (_overridesByAgencyId.containsKey(agencyId))
return new Date(_referenceTime + _overridesByAgencyId.get(agencyId) * 60 * 1000);
return endOfDay(_serviceDate);

}

private Date endOfDay(ServiceDate serviceDate) {
final Calendar cal = Calendar.getInstance();
cal.setTime(serviceDate.getAsDate());
cal.set(Calendar.HOUR_OF_DAY, 23);
cal.set(Calendar.MINUTE, 59);
cal.set(Calendar.SECOND, 59);
cal.set(Calendar.MILLISECOND, 999);
return cal.getTime();
}
private Date startOfDay(ServiceDate serviceDate) {
final Calendar cal = Calendar.getInstance();
cal.setTime(serviceDate.getAsDate());
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 000);
return cal.getTime();

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,7 @@
import org.onebusaway.gtfs.model.ServiceCalendarDate;
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.LocalizedServiceId;
import org.onebusaway.gtfs.model.calendar.ServiceDate;
import org.onebusaway.gtfs.model.calendar.ServiceIdIntervals;
import org.onebusaway.gtfs.model.calendar.ServiceInterval;
import org.onebusaway.gtfs.model.calendar.*;

/**
* While the set of {@link ServiceCalendar} and {@link ServiceCalendarDate}
Expand Down Expand Up @@ -129,6 +125,9 @@ public LocalizedServiceId getLocalizedServiceIdForAgencyAndServiceId(
public boolean isLocalizedServiceIdActiveOnDate(
LocalizedServiceId localizedServiceId, Date serviceDate);

public boolean isLocalizedServiceIdActiveInRange(
LocalizedServiceId localizedServiceId, ServiceInterval activeService, AgencyServiceInterval serviceInterval);

/**
* Given the specified localized service id, which has a corresponding set of
* localized service dates, determine the sublist of service dates that, when
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,19 @@
import static org.onebusaway.gtfs.DateSupport.date;
import static org.onebusaway.gtfs.DateSupport.hourToSec;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.*;

import org.junit.Before;
import org.junit.Test;
import org.onebusaway.gtfs.model.AgencyAndId;
import org.onebusaway.gtfs.model.calendar.CalendarServiceData;
import org.onebusaway.gtfs.model.calendar.LocalizedServiceId;
import org.onebusaway.gtfs.model.calendar.ServiceDate;
import org.onebusaway.gtfs.model.calendar.ServiceIdIntervals;
import org.onebusaway.gtfs.model.calendar.*;

public class CalendarServiceImplSyntheticTest {

static {
// avoid timezone conversions
TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
}
private TimeZone tz = TimeZone.getTimeZone("America/Los_Angeles");

private ServiceDate d1 = new ServiceDate(2010, 02, 01);
Expand Down Expand Up @@ -447,6 +442,64 @@ public void testGetPreviousArrivalServiceDates05() {
assertTrue(dates.contains(d1.getAsDate(tz)));
}

@Test
public void testAgencyOverrideOverlap() {
long baseTime = d1.getAsDate().getTime();

Map<String, Integer> oneHourOverride = new HashMap<>();
oneHourOverride.put("A", 60); // 60 minute window instead of entire service day

Map<String, Integer> twoHourOverride = new HashMap<>();
twoHourOverride.put("A", 120); // 2 hour window instead of entire service day

// active service 6:00 -> 7:00
// 6:00 + 1 service interval
ServiceInterval sixOclockScheudledService = new ServiceInterval(hourToSec(6), hourToSec(7));
AgencyServiceInterval sixOclock1HourQuery = new AgencyServiceInterval(baseTime + hourToSec(6)*1000, oneHourOverride);
boolean active = service.isLocalizedServiceIdActiveInRange(lsid1, sixOclockScheudledService, sixOclock1HourQuery);
assertTrue(active); // this should be an exact match

// active service 6:00 -> 7:00
// 5 + 1 service interval
AgencyServiceInterval fiveOclock1HourQuery = new AgencyServiceInterval(baseTime + hourToSec(5)*1000, oneHourOverride);
active = service.isLocalizedServiceIdActiveInRange(lsid1, sixOclockScheudledService, fiveOclock1HourQuery);
assertFalse(active);

// active service 6:00 -> 7:00
// 7 + 1 service interval
AgencyServiceInterval sevenOclock1HourQuery = new AgencyServiceInterval(baseTime + hourToSec(7)*1000, oneHourOverride);
active = service.isLocalizedServiceIdActiveInRange(lsid1, sixOclockScheudledService, sevenOclock1HourQuery);
assertFalse(active);

// active service 6:00 -> 7:00
// 5 + 2 service interval
AgencyServiceInterval fiveOclock2HourQuery = new AgencyServiceInterval(baseTime + hourToSec(5)*1000, twoHourOverride);
active = service.isLocalizedServiceIdActiveInRange(lsid1, sixOclockScheudledService, fiveOclock2HourQuery);
assertTrue(active);

// active service 6:00 -> 25:00
// 12 + 1
AgencyServiceInterval lunchInterval1HourOverride = new AgencyServiceInterval(baseTime + hourToSec(12)*1000, oneHourOverride);
ServiceInterval exactMatchInterval = new ServiceInterval(hourToSec(6), hourToSec(25));
active = service.isLocalizedServiceIdActiveInRange(lsid1, exactMatchInterval, lunchInterval1HourOverride);
assertTrue(active);
}

@Test
public void testOverlapNoOverride() {
long baseTime = d1.getAsDate().getTime();
AgencyServiceInterval defaultInterval = new AgencyServiceInterval(baseTime);
// active service 6:00 -> 7:00
ServiceInterval sixOclockScheudledService = new ServiceInterval(hourToSec(6), hourToSec(7));
boolean active = service.isLocalizedServiceIdActiveInRange(lsid1, sixOclockScheudledService, defaultInterval);
assertTrue(active);

long yesterday = d1.getAsDate().getTime() - hourToSec(24) * 1000;
AgencyServiceInterval tomorrowInterval = new AgencyServiceInterval(yesterday);
active = service.isLocalizedServiceIdActiveInRange(lsid1, sixOclockScheudledService, tomorrowInterval);
assertFalse(active);
}

/****
* Private Methods
****/
Expand Down

0 comments on commit 166e24c

Please sign in to comment.