Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 8 additions & 7 deletions src/main/java/pl/agh/transit/StopController.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,13 @@ public ResponseEntity<List<NextDepartureDTO>> getNextDepartures(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.TIME) LocalTime time
) {
//TODO: consider handling it in the service layer

LocalDate travelDate = date != null ? date : LocalDate.now();
LocalTime travelTime = time != null ? time : LocalTime.now();
List<NextDepartureDTO> departures = stopService.getNextDeparturesForStopAndRoute(
stopName, routeName, travelDate, travelTime, 5);
List<NextDepartureDTO> departures;
if (date != null && time != null) {
departures = stopService.getNextDeparturesForStopAndRoute(stopName, routeName, date, time, 5);
}
else{
departures = stopService.getNextDeparturesForStopAndRoute(stopName, routeName,5);
}
return ResponseEntity.ok(departures);
}
}
}
96 changes: 48 additions & 48 deletions src/main/java/pl/agh/transit/StopService.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,15 @@
import pl.agh.transit.gtfs_static.model.Trip;
import pl.agh.transit.gtfs_static.model.Route;
import pl.agh.transit.gtfs_static.repository.StaticTripRepository;
import pl.agh.transit.gtfs_static.repository.StopRepository;
import pl.agh.transit.gtfs_static.repository.RouteRepository;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.time.LocalTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
Expand All @@ -37,7 +34,6 @@
@AllArgsConstructor
public class StopService {

private final StopRepository stopRepository;
private final JdbcTemplate jdbcTemplate;
private final ConnectionServiceHelper connectionServiceHelper;
private final StaticTripRepository staticTripRepository;
Expand Down Expand Up @@ -69,7 +65,7 @@ public PagedResponseDTO<StopDTO> getAllStops(int page, int size, String sortBy)

List<StopDTO> stopDTOs = stops.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
.toList();

log.debug("Found {} stops on page {} of {}", stopDTOs.size(), page, totalPages);

Expand All @@ -84,6 +80,49 @@ public PagedResponseDTO<StopDTO> getAllStops(int page, int size, String sortBy)
.build();
}

public List<NextDepartureDTO> getNextDeparturesForStopAndRoute(String stopName, String routeName, int limit) {
LocalDate travelDate = LocalDate.now();
LocalTime travelTime = LocalTime.now();

return getNextDeparturesForStopAndRoute(stopName, routeName, travelDate, travelTime, limit);
}

/**
* Get next departures for a stop and route.
* Merges realtime and static data, filters by service calendar.
* Accepts names instead of IDs.
* Supplements results from next day if less than limit.
*/
public List<NextDepartureDTO> getNextDeparturesForStopAndRoute(
String stopName,
String routeName,
LocalDate travelDate,
LocalTime travelTime,
int limit
) {
log.debug("[0] Getting next {} departures for stop: {}, route: {}, date: {}, time: {}",
limit, stopName, routeName, travelDate, travelTime);

List<NextDepartureDTO> results = getNextDeparturesForDate(stopName, routeName, travelDate, travelTime, limit);

// If insufficient results, fetch from next day
if (results.size() < limit) {
int remainingCount = limit - results.size();
log.debug("[0c] Only {} results for initial date, fetching {} more from next day", results.size(), remainingCount);

LocalDate nextDate = travelDate.plusDays(1);
List<NextDepartureDTO> nextDayResults = getNextDeparturesForDate(stopName, routeName, nextDate, travelTime, remainingCount);
results = Stream.concat(results.stream(), nextDayResults.stream())
.limit(limit)
.toList();

log.debug("[0d] Added {} results from next day, total: {}", nextDayResults.size(), results.size());
}

log.debug("[0e] Final results (including next day if needed): {}", results.size());
return results;
}

/**
* Sanitize sort column to prevent SQL injection
*/
Expand Down Expand Up @@ -125,43 +164,6 @@ private StopDTO convertToDTO(Stop stop) {
.build();
}

/**
* Get next departures for a stop and route.
* Merges realtime and static data, filters by service calendar.
* Accepts names instead of IDs.
* Supplements results from next day if less than limit.
*/
public List<NextDepartureDTO> getNextDeparturesForStopAndRoute(
String stopName,
String routeName,
LocalDate travelDate,
java.time.LocalTime travelTime,
int limit
) {
log.debug("[0] Getting next {} departures for stop: {}, route: {}, date: {}, time: {}",
limit, stopName, routeName, travelDate, travelTime);

List<NextDepartureDTO> results = getNextDeparturesForDate(stopName, routeName, travelDate, travelTime, limit);

// If insufficient results, fetch from next day
if (results.size() < limit) {
int remainingCount = limit - results.size();
log.debug("[0c] Only {} results for initial date, fetching {} more from next day", results.size(), remainingCount);

LocalDate nextDate = travelDate.plusDays(1);
List<NextDepartureDTO> nextDayResults = getNextDeparturesForDate(stopName, routeName, nextDate, travelTime, remainingCount);
results = Stream.concat(results.stream(), nextDayResults.stream())
.limit(limit)
.toList();

log.debug("[0d] Added {} results from next day, total: {}", nextDayResults.size(), results.size());
}

log.debug("[0e] Final results (including next day if needed): {}", results.size());
return results;
}


private List<NextDepartureDTO> getNextDeparturesForDate(
String stopName,
String routeName,
Expand All @@ -177,7 +179,7 @@ private List<NextDepartureDTO> getNextDeparturesForDate(
log.debug("[0a] Stop not found: {}", stopName);
return List.of();
}
Stop stop = stops.get(0);
Stop stop = stops.getFirst();
String stopId = stop.getId();

Optional<Route> routeOptional = routeRepository.findByRouteShortName(routeName);
Expand Down Expand Up @@ -283,16 +285,14 @@ private Stream<NextDepartureDTO> findStopInRealtimeTrip(

return stopTimeUpdates.stream()
.filter(stopUpdate -> stopId.equals(stopUpdate.getStopId()))
.filter(stopUpdate -> isStopUpdateValid(stopUpdate, travelDateTime, travelDate, trip.getTripId(), stopId))
.filter(stopUpdate -> isStopUpdateValid(stopUpdate, travelDateTime, travelDate))
.map(stopUpdate -> buildNextDepartureDTOFromUpdate(stopUpdate, stopName, routeName, trip.getTripId(), stopId));
}

private boolean isStopUpdateValid(
StopTimeUpdateDTO stopUpdate,
LocalDateTime travelDateTime,
LocalDate travelDate,
String tripId,
String stopId
LocalDate travelDate
) {
LocalDateTime departureTime = connectionServiceHelper.parseTimeWithDelay(
stopUpdate.getDepartureTime(),
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/pl/agh/transit/TripUpdateScheduler.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jakarta.annotation.PostConstruct;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import pl.agh.transit.gtfs_static.GtfsStaticService;
Expand All @@ -11,6 +12,7 @@
@Component
@AllArgsConstructor
@Slf4j
@ConditionalOnProperty(name = "gtfs.scheduler.enabled", havingValue = "true", matchIfMissing = true)
public class TripUpdateScheduler {

private final TripDataFetcher fetcher;
Expand Down
11 changes: 6 additions & 5 deletions src/main/java/pl/agh/transit/mcp/StopMcpTools.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ public List<NextDepartureDTO> getNextDepartures(
@ToolParam(description = "Date to check schedule for (YYYY-MM-DD). If omitted, defaults to today.", required = false) LocalDate date,
@ToolParam(description = "Time to check schedule for (HH:mm). If omitted, defaults to right now.", required = false) LocalTime time
) {
//TODO: consider handling it in the service layer
LocalDate travelDate = (date != null) ? date : LocalDate.now();
LocalTime travelTime = time != null ? time : LocalTime.now();

return stopService.getNextDeparturesForStopAndRoute(stopName, routeName, travelDate, travelTime, 5);
if (date != null && time != null) {
return stopService.getNextDeparturesForStopAndRoute(stopName, routeName, date, time, 5);
}
else{
return stopService.getNextDeparturesForStopAndRoute(stopName, routeName,5);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import java.time.LocalTime;
import java.util.List;

import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
import pl.agh.transit.gtfs_static.repository.RouteRepository;
import pl.agh.transit.gtfs_static.repository.StaticTripRepository;
Expand All @@ -26,6 +28,8 @@

@SpringBootTest
@Transactional
@ActiveProfiles("test")
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
@DisplayName("DirectConnectionService Integration Tests")
class DirectConnectionServiceIntegrationTest {

Expand All @@ -47,6 +51,7 @@ class DirectConnectionServiceIntegrationTest {
@Autowired
private CalendarRepository calendarRepository;

private static final LocalTime TEST_TIME = LocalTime.of(22, 0);

@BeforeEach
void setUp() {
Expand Down Expand Up @@ -152,7 +157,7 @@ private void setupTestData() {
@DisplayName("Should find fastest direct connection from static data")
void findFastestDirectConnection_staticData_returnsFastestRoute() {
List<TransportTypeDTO> result = directConnectionService
.findFastestDirectConnections("Main Station", "City Center", LocalDate.now(), LocalTime.now(), 1);
.findFastestDirectConnections("Main Station", "City Center", LocalDate.now(), TEST_TIME, 1);

assertThat(result).hasSize(1);
assertThat(result.get(0).getRouteName()).isEqualTo("1A");
Expand All @@ -162,7 +167,7 @@ void findFastestDirectConnection_staticData_returnsFastestRoute() {
@DisplayName("Should return empty when no direct connection exists")
void findFastestDirectConnection_noDirectConnections_returnsEmpty() {
List<TransportTypeDTO> result = directConnectionService
.findFastestDirectConnections("Main Station", "Final Stop", LocalDate.now(), LocalTime.now(), 1);
.findFastestDirectConnections("Main Station", "Final Stop", LocalDate.now(), TEST_TIME, 1);

assertThat(result).isEmpty();
}
Expand All @@ -171,7 +176,7 @@ void findFastestDirectConnection_noDirectConnections_returnsEmpty() {
@DisplayName("Should return empty when source stop does not exist")
void findFastestDirectConnections_sourceStopNotFound_returnsEmpty() {
List<TransportTypeDTO> result = directConnectionService
.findFastestDirectConnections("Unknown Station", "City Center", LocalDate.now(), LocalTime.now(), 1);
.findFastestDirectConnections("Unknown Station", "City Center", LocalDate.now(), TEST_TIME, 1);

assertThat(result).isEmpty();
}
Expand All @@ -180,7 +185,7 @@ void findFastestDirectConnections_sourceStopNotFound_returnsEmpty() {
@DisplayName("Should return empty when destination stop does not exist")
void findFastestDirectConnections_destinationStopNotFound_returnsEmpty() {
List<TransportTypeDTO> result = directConnectionService
.findFastestDirectConnections("Main Station", "Unknown Station", LocalDate.now(), LocalTime.now(), 1);
.findFastestDirectConnections("Main Station", "Unknown Station", LocalDate.now(), TEST_TIME, 1);

assertThat(result).isEmpty();
}
Expand All @@ -189,7 +194,7 @@ void findFastestDirectConnections_destinationStopNotFound_returnsEmpty() {
@DisplayName("Should return empty when both stops are the same")
void findFastestDirectConnections_sameSourceAndDestination_returnsEmpty() {
List<TransportTypeDTO> result = directConnectionService
.findFastestDirectConnections("Main Station", "Main Station", LocalDate.now(), LocalTime.now(), 1);
.findFastestDirectConnections("Main Station", "Main Station", LocalDate.now(), TEST_TIME, 1);

assertThat(result).isEmpty();
}
Expand All @@ -200,7 +205,7 @@ void findFastestDirectConnections_travelDateOutsideCalendar_returnsEmpty() {
LocalDate futureDate = LocalDate.now().plusYears(1);

List<TransportTypeDTO> result = directConnectionService
.findFastestDirectConnections("Main Station", "City Center", futureDate, LocalTime.now(), 1);
.findFastestDirectConnections("Main Station", "City Center", futureDate, TEST_TIME, 1);

assertThat(result).isEmpty();
}
Expand All @@ -211,7 +216,7 @@ void findFastestDirectConnection_validTravelDate_returnsConnections() {
LocalDate validDate = LocalDate.now();

List<TransportTypeDTO> result = directConnectionService
.findFastestDirectConnections("Main Station", "City Center", validDate, LocalTime.now(), 1);
.findFastestDirectConnections("Main Station", "City Center", validDate, TEST_TIME, 1);

assertThat(result).hasSize(1);
assertThat(result.get(0).getRouteName()).isEqualTo("1A");
Expand All @@ -225,7 +230,7 @@ void findFastestDirectConnections_comparesDepartureTimes_returnsEarliestDepartur
// Should select trip 1 because it departs earliest

List<TransportTypeDTO> result = directConnectionService
.findFastestDirectConnections("Main Station", "City Center", LocalDate.now(), LocalTime.now(), 1);
.findFastestDirectConnections("Main Station", "City Center", LocalDate.now(), TEST_TIME, 1);

assertThat(result).hasSize(1);
assertThat(result.get(0).getRouteName()).isEqualTo("1A");
Expand Down Expand Up @@ -278,7 +283,7 @@ void findFastestDirectConnections_inactiveServicesIgnored_returnsActiveRoute() {
stopTimeRepository.save(stopTime3To);

List<TransportTypeDTO> result = directConnectionService
.findFastestDirectConnections("Main Station", "City Center", LocalDate.now(), LocalTime.now(), 1);
.findFastestDirectConnections("Main Station", "City Center", LocalDate.now(), TEST_TIME, 1);

// Should select route 1A (active, earliest departure), not 3C (inactive)
assertThat(result).hasSize(1);
Expand All @@ -297,7 +302,7 @@ void findFastestDirectConnections_multipleStopsSameName_handlesCorrectly() {
stopRepository.save(duplicateStop);

List<TransportTypeDTO> result = directConnectionService
.findFastestDirectConnections("Main Station", "City Center", LocalDate.now(), LocalTime.now(), 1);
.findFastestDirectConnections("Main Station", "City Center", LocalDate.now(), TEST_TIME, 1);

// Should find connection even with multiple stops with same name
assertThat(result).hasSize(1);
Expand All @@ -308,7 +313,7 @@ void findFastestDirectConnections_multipleStopsSameName_handlesCorrectly() {
@DisplayName("Should return valid arrival time for connection")
void findFastestDirectConnections_returnsNonNullArrivalTime() {
List<TransportTypeDTO> result = directConnectionService
.findFastestDirectConnections("Main Station", "City Center", LocalDate.now(), LocalTime.now(), 1);
.findFastestDirectConnections("Main Station", "City Center", LocalDate.now(), TEST_TIME, 1);

assertThat(result).hasSize(1);
assertThat(result.get(0).getArrivalTime()).isNotNull();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@
import java.time.LocalTime;
import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.eq;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class TripControllerIntegrationTest {

Expand Down
Loading