Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 1534 service window in summary report #1837

Merged
merged 32 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
38cfec4
service window first commit
qcdyx Sep 5, 2024
5c8c4d8
added logic when both calendars.txt and calendar_dates.txt are used
qcdyx Sep 9, 2024
1e269a2
renamed method
qcdyx Sep 9, 2024
e1a3a26
added null check for earliestStartDate and lateEndDate
qcdyx Sep 9, 2024
9bf2721
added edge case logic
qcdyx Sep 9, 2024
594d5d5
added testLoadServiceDateRange and modified loadServiceDateRange logic
qcdyx Sep 10, 2024
a20686a
formatted code
qcdyx Sep 10, 2024
2e5cfa7
Merge branch 'master' into 1534-service-window-in-summary-report
qcdyx Sep 10, 2024
f962997
add null check before loadFeedInfo; use a field noticeContainer in Fe…
qcdyx Sep 12, 2024
6547447
revert back changes
qcdyx Sep 12, 2024
55489e7
called loadServiceDateRange separately from loadFeedInfo
qcdyx Sep 12, 2024
50287f2
checked in feedInfo before calling loadFeedInfo
qcdyx Sep 12, 2024
5de42bb
resolved PR comments
qcdyx Sep 16, 2024
5e1bdbf
Merge branch 'master' into 1534-service-window-in-summary-report
qcdyx Sep 16, 2024
26ff674
added check for epoch time
qcdyx Sep 23, 2024
d1b2db0
removed tripContainer, calendarTable and calendarDateTable from loadF…
qcdyx Sep 23, 2024
32bc9a3
avoided NPE
qcdyx Sep 23, 2024
8e2e1bb
Merge branch 'master' into 1534-service-window-in-summary-report
qcdyx Sep 23, 2024
990eda7
added condition checks
qcdyx Sep 23, 2024
49fc8eb
Merge branch '1534-service-window-in-summary-report' of github.com:Mo…
qcdyx Sep 23, 2024
95b3654
removed commented code
qcdyx Sep 23, 2024
7cea7a4
fixed 1970-01-01 problem
qcdyx Sep 24, 2024
d9d0b74
formatted code
qcdyx Sep 24, 2024
7e1f5d7
compared the values of startDate/endDate and epoch localDate, not the…
qcdyx Sep 24, 2024
a7d71a6
changed "Service Date Range" to "Service Dates"
qcdyx Sep 24, 2024
bd9fc5d
added tooltip for Service Dates
qcdyx Sep 24, 2024
9d12dc9
Merge branch 'master' into 1534-service-window-in-summary-report
qcdyx Sep 24, 2024
ed5392b
Merge branch 'master' into 1534-service-window-in-summary-report
qcdyx Sep 25, 2024
d3da3dc
changed to Service Window
qcdyx Sep 25, 2024
ad398e1
Merge branch '1534-service-window-in-summary-report' of github.com:Mo…
qcdyx Sep 25, 2024
fc89eb0
wrapped load service window with try/catch block
qcdyx Sep 25, 2024
dab5b1c
formatted code
qcdyx Sep 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.flogger.FluentLogger;
import com.vladsch.flexmark.util.misc.Pair;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Function;
import org.mobilitydata.gtfsvalidator.table.*;
import org.mobilitydata.gtfsvalidator.util.CalendarUtil;
import org.mobilitydata.gtfsvalidator.util.ServicePeriod;

public class FeedMetadata {
/*
Expand All @@ -20,6 +24,9 @@ public class FeedMetadata {
public static final String FEED_INFO_FEED_LANGUAGE = "Feed Language";
public static final String FEED_INFO_FEED_START_DATE = "Feed Start Date";
public static final String FEED_INFO_FEED_END_DATE = "Feed End Date";
public static final String FEED_INFO_SERVICE_WINDOW = "Service Window";

private static final FluentLogger logger = FluentLogger.forEnclosingClass();

/*
* Use these strings as keys in the counts map. Also used to specify the info that will appear in
Expand Down Expand Up @@ -84,9 +91,22 @@ public static FeedMetadata from(GtfsFeedContainer feedContainer, ImmutableSet<St
(GtfsTableContainer<GtfsFeedInfo>)
feedContainer.getTableForFilename(GtfsFeedInfo.FILENAME).get());
}

feedMetadata.loadAgencyData(
(GtfsTableContainer<GtfsAgency>)
feedContainer.getTableForFilename(GtfsAgency.FILENAME).get());

if (feedContainer.getTableForFilename(GtfsTrip.FILENAME).isPresent()
&& (feedContainer.getTableForFilename(GtfsCalendar.FILENAME).isPresent()
|| feedContainer.getTableForFilename(GtfsCalendarDate.FILENAME).isPresent())) {
feedMetadata.loadServiceWindow(
(GtfsTableContainer<GtfsTrip>) feedContainer.getTableForFilename(GtfsTrip.FILENAME).get(),
(GtfsTableContainer<GtfsCalendar>)
feedContainer.getTableForFilename(GtfsCalendar.FILENAME).get(),
(GtfsTableContainer<GtfsCalendarDate>)
feedContainer.getTableForFilename(GtfsCalendarDate.FILENAME).get());
}

feedMetadata.loadSpecFeatures(feedContainer);
return feedMetadata;
}
Expand Down Expand Up @@ -368,16 +388,148 @@ private void loadFeedInfo(GtfsTableContainer<GtfsFeedInfo> feedTable) {
feedInfo.put(
FEED_INFO_FEED_LANGUAGE, info == null ? "N/A" : info.feedLang().getDisplayLanguage());
if (feedTable.hasColumn(GtfsFeedInfo.FEED_START_DATE_FIELD_NAME)) {
LocalDate localDate = info.feedStartDate().getLocalDate();
String displayDate =
localDate.equals(GtfsFeedInfo.DEFAULT_FEED_START_DATE) ? "N/A" : localDate.toString();
feedInfo.put(FEED_INFO_FEED_START_DATE, info == null ? "N/A" : displayDate);
if (info != null) {
LocalDate localDate = info.feedStartDate().getLocalDate();
feedInfo.put(FEED_INFO_FEED_START_DATE, checkLocalDate(localDate));
}
}
if (feedTable.hasColumn(GtfsFeedInfo.FEED_END_DATE_FIELD_NAME)) {
LocalDate localDate = info.feedEndDate().getLocalDate();
String displayDate =
localDate.equals(GtfsFeedInfo.DEFAULT_FEED_END_DATE) ? "N/A" : localDate.toString();
feedInfo.put(FEED_INFO_FEED_END_DATE, info == null ? "N/A" : displayDate);
if (info != null) {
LocalDate localDate = info.feedEndDate().getLocalDate();
feedInfo.put(FEED_INFO_FEED_END_DATE, checkLocalDate(localDate));
}
}
}

private String checkLocalDate(LocalDate localDate) {
String displayDate;
if (localDate.toString().equals(LocalDate.EPOCH.toString())) {
displayDate = "N/A";
} else {
displayDate = localDate.toString();
}
return displayDate;
}

/**
* Loads the service date range by determining the earliest start date and the latest end date for
* all services referenced with a trip\_id in `trips.txt`. It handles three cases: 1. When only
* `calendars.txt` is used. 2. When only `calendar\_dates.txt` is used. 3. When both
* `calendars.txt` and `calendar\_dates.txt` are used.
*
* @param tripContainer the container for `trips.txt` data
* @param calendarTable the container for `calendars.txt` data
* @param calendarDateTable the container for `calendar\_dates.txt` data
*/
public void loadServiceWindow(
GtfsTableContainer<GtfsTrip> tripContainer,
GtfsTableContainer<GtfsCalendar> calendarTable,
GtfsTableContainer<GtfsCalendarDate> calendarDateTable) {
List<GtfsTrip> trips = tripContainer.getEntities();
qcdyx marked this conversation as resolved.
Show resolved Hide resolved

LocalDate earliestStartDate = null;
LocalDate latestEndDate = null;
try {
if ((calendarDateTable == null) && (calendarTable != null)) {
// When only calendars.txt is used
List<GtfsCalendar> calendars = calendarTable.getEntities();
for (GtfsTrip trip : trips) {
String serviceId = trip.serviceId();
for (GtfsCalendar calendar : calendars) {
if (calendar.serviceId().equals(serviceId)) {
LocalDate startDate = calendar.startDate().getLocalDate();
LocalDate endDate = calendar.endDate().getLocalDate();
if (startDate != null || endDate != null) {
if (startDate.toString().equals(LocalDate.EPOCH.toString())
|| endDate.toString().equals(LocalDate.EPOCH.toString())) {
continue;
}
if (earliestStartDate == null || startDate.isBefore(earliestStartDate)) {
earliestStartDate = startDate;
}
if (latestEndDate == null || endDate.isAfter(latestEndDate)) {
latestEndDate = endDate;
}
}
}
}
}
} else if ((calendarDateTable != null) && (calendarTable == null)) {
// When only calendar_dates.txt is used
List<GtfsCalendarDate> calendarDates = calendarDateTable.getEntities();
for (GtfsTrip trip : trips) {
String serviceId = trip.serviceId();
for (GtfsCalendarDate calendarDate : calendarDates) {
if (calendarDate.serviceId().equals(serviceId)) {
LocalDate date = calendarDate.date().getLocalDate();
if (date != null && !date.toString().equals(LocalDate.EPOCH.toString())) {
if (earliestStartDate == null || date.isBefore(earliestStartDate)) {
earliestStartDate = date;
}
if (latestEndDate == null || date.isAfter(latestEndDate)) {
latestEndDate = date;
}
}
}
}
}
} else if ((calendarTable != null) && (calendarDateTable != null)) {
// When both calendars.txt and calendar_dates.txt are used
Map<String, ServicePeriod> servicePeriods =
CalendarUtil.buildServicePeriodMap(
(GtfsCalendarTableContainer) calendarTable,
(GtfsCalendarDateTableContainer) calendarDateTable);
List<LocalDate> removedDates = new ArrayList<>();
for (GtfsTrip trip : trips) {
String serviceId = trip.serviceId();
ServicePeriod servicePeriod = servicePeriods.get(serviceId);
LocalDate startDate = servicePeriod.getServiceStart();
LocalDate endDate = servicePeriod.getServiceEnd();
if (startDate != null && endDate != null) {
if (startDate.toString().equals(LocalDate.EPOCH.toString())
|| endDate.toString().equals(LocalDate.EPOCH.toString())) {
continue;
}
if (earliestStartDate == null || startDate.isBefore(earliestStartDate)) {
earliestStartDate = startDate;
}
if (latestEndDate == null || endDate.isAfter(latestEndDate)) {
latestEndDate = endDate;
}
}
removedDates.addAll(servicePeriod.getRemovedDays());
}

for (LocalDate date : removedDates) {
if (date.isEqual(earliestStartDate)) {
earliestStartDate = date.plusDays(1);
}
if (date.isEqual(latestEndDate)) {
latestEndDate = date.minusDays(1);
}
}
}
} catch (Exception e) {
logger.atSevere().withCause(e).log("Error while loading Service Window");
} finally {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMMM d, yyyy");
if ((earliestStartDate == null) && (latestEndDate == null)) {
feedInfo.put(FEED_INFO_SERVICE_WINDOW, "N/A");
} else if (earliestStartDate == null && latestEndDate != null) {
feedInfo.put(FEED_INFO_SERVICE_WINDOW, latestEndDate.format(formatter));
} else if (latestEndDate == null && earliestStartDate != null) {
if (earliestStartDate.isAfter(latestEndDate)) {
feedInfo.put(FEED_INFO_SERVICE_WINDOW, "N/A");
} else {
feedInfo.put(FEED_INFO_SERVICE_WINDOW, earliestStartDate.format(formatter));
}
} else {
StringBuilder serviceWindow = new StringBuilder();
serviceWindow.append(earliestStartDate);
serviceWindow.append(" to ");
serviceWindow.append(latestEndDate);
feedInfo.put(FEED_INFO_SERVICE_WINDOW, serviceWindow.toString());
}
}
}

Expand Down
7 changes: 6 additions & 1 deletion main/src/main/resources/report.html
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,12 @@ <h4>Feed Info</h4>
<a th:href="${info.value}" target="_blank" th:text="${info.value}" />
</span>
<span th:if="${info.key.contains('URL') and info.value == 'N/A'}" th:text="${info.value}"></span>
<span th:unless="${info.key.contains('URL')}" th:text="${info.value}" />
<span th:unless="${info.key.contains('URL')}" th:text="${info.value}"/>
<span th:if="${info.key.contains('Service Window')}" >
<a href="#" class="tooltip" onclick="event.preventDefault();"><span>(?)</span>
<span class="tooltiptext" style="transform: translateX(-100%)">The range of service dates covered by the feed, based on trips with an associated service_id in calendar.txt and/or calendar_dates.txt</span>
</a>
</span>
</dt>
</div>
</dl>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package org.mobilitydata.gtfsvalidator.report.model;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;

import com.google.common.collect.ImmutableList;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.time.LocalDate;
import java.util.List;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
Expand All @@ -17,6 +19,7 @@
import org.mobilitydata.gtfsvalidator.input.GtfsInput;
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
import org.mobilitydata.gtfsvalidator.table.*;
import org.mobilitydata.gtfsvalidator.type.GtfsDate;
import org.mobilitydata.gtfsvalidator.validator.*;

public class FeedMetadataTest {
Expand All @@ -30,6 +33,12 @@ public class FeedMetadataTest {
.build();
ValidatorLoader validatorLoader;
File rootDir;
NoticeContainer noticeContainer = new NoticeContainer();

private GtfsTableContainer<GtfsTrip> tripContainer;
private GtfsTableContainer<GtfsCalendar> calendarTable;
private GtfsTableContainer<GtfsCalendarDate> calendarDateTable;
private FeedMetadata feedMetadata = new FeedMetadata();

private void createDataFile(String filename, String content) throws IOException {
File dataFile = tmpDir.newFile("data/" + filename);
Expand All @@ -50,12 +59,85 @@ public void setup() throws IOException, ValidatorLoaderException {
ValidatorLoader.createForClasses(ClassGraphDiscovery.discoverValidatorsInDefaultPackage());
}

public static GtfsTrip createTrip(int csvRowNumber, String serviceId) {
return new GtfsTrip.Builder().setCsvRowNumber(csvRowNumber).setServiceId(serviceId).build();
}

public static GtfsCalendar createCalendar(
int csvRowNumber, String serviceId, GtfsDate startDate, GtfsDate endDate) {
return new GtfsCalendar.Builder()
.setCsvRowNumber(csvRowNumber)
.setServiceId(serviceId)
.setStartDate(startDate)
.setEndDate(endDate)
.build();
}

public static GtfsCalendarDate createCalendarDate(
int csvRowNumber,
String serviceId,
GtfsDate date,
GtfsCalendarDateExceptionType exceptionType) {
return new GtfsCalendarDate.Builder()
.setCsvRowNumber(csvRowNumber)
.setServiceId(serviceId)
.setDate(date)
.setExceptionType(exceptionType)
.build();
}

@Test
public void testLoadServiceWindow() {
GtfsTrip trip1 = createTrip(1, "JUN24-MVS-SUB-Weekday-01");
GtfsTrip trip2 = createTrip(2, "JUN24-MVS-SUB-Weekday-02");
// when(tripContainer.getEntities()).thenReturn(List.of(trip1, trip2));
tripContainer = GtfsTripTableContainer.forEntities(List.of(trip1, trip2), noticeContainer);
GtfsCalendar calendar1 =
createCalendar(
1,
"JUN24-MVS-SUB-Weekday-01",
GtfsDate.fromLocalDate(LocalDate.of(2024, 1, 1)),
GtfsDate.fromLocalDate(LocalDate.of(2024, 12, 20)));
GtfsCalendar calendar2 =
createCalendar(
2,
"JUN24-MVS-SUB-Weekday-02",
GtfsDate.fromLocalDate(LocalDate.of(2024, 6, 1)),
GtfsDate.fromLocalDate(LocalDate.of(2024, 12, 31)));
// when(calendarTable.getEntities()).thenReturn(List.of(calendar1, calendar2));
calendarTable =
GtfsCalendarTableContainer.forEntities(List.of(calendar1, calendar2), noticeContainer);
GtfsCalendarDate calendarDate1 =
createCalendarDate(
1,
"JUN24-MVS-SUB-Weekday-01",
GtfsDate.fromLocalDate(LocalDate.of(2024, 1, 1)),
GtfsCalendarDateExceptionType.SERVICE_REMOVED);
GtfsCalendarDate calendarDate2 =
createCalendarDate(
2,
"JUN24-MVS-SUB-Weekday-02",
GtfsDate.fromLocalDate(LocalDate.of(2024, 6, 1)),
GtfsCalendarDateExceptionType.SERVICE_ADDED);
// when(calendarDateTable.getEntities()).thenReturn(List.of(calendarDate1, calendarDate2));
calendarDateTable =
GtfsCalendarDateTableContainer.forEntities(
List.of(calendarDate1, calendarDate2), noticeContainer);

// Call the method
feedMetadata.loadServiceWindow(tripContainer, calendarTable, calendarDateTable);

// Verify the result
String expectedServiceWindow = "2024-01-02 to 2024-12-31";
assertEquals(
expectedServiceWindow, feedMetadata.feedInfo.get(FeedMetadata.FEED_INFO_SERVICE_WINDOW));
}

private void validateSpecFeature(
String specFeature,
Boolean expectedValue,
ImmutableList<Class<? extends GtfsTableDescriptor<?>>> tableDescriptors)
throws IOException, InterruptedException {
NoticeContainer noticeContainer = new NoticeContainer();
feedLoaderMock = new GtfsFeedLoader(tableDescriptors);
try (GtfsInput gtfsInput = GtfsInput.createFromPath(rootDir.toPath(), noticeContainer)) {
GtfsFeedContainer feedContainer =
Expand Down
Loading