Skip to content

Commit

Permalink
Merge pull request #7 from goeuropa/feature/stop_reports
Browse files Browse the repository at this point in the history
Report for Stop by ID
  • Loading branch information
wkulesza authored Apr 15, 2024
2 parents b6820dd + 751e0e6 commit 1c15eb1
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 41 deletions.
77 changes: 45 additions & 32 deletions app/src/main/java/org/transitclock/api/resources/TransitimeApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,7 @@
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.servers.Server;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.apache.commons.lang3.StringUtils;
Expand All @@ -36,25 +21,16 @@
import org.transitclock.domain.structs.Agency;
import org.transitclock.domain.structs.ExportTable;
import org.transitclock.domain.structs.Location;
import org.transitclock.service.dto.IpcActiveBlock;
import org.transitclock.service.dto.IpcBlock;
import org.transitclock.service.dto.IpcCalendar;
import org.transitclock.service.dto.IpcDirectionsForRoute;
import org.transitclock.service.dto.IpcPrediction;
import org.transitclock.service.dto.IpcPredictionsForRouteStopDest;
import org.transitclock.service.dto.IpcRoute;
import org.transitclock.service.dto.IpcRouteSummary;
import org.transitclock.service.dto.IpcSchedule;
import org.transitclock.service.dto.IpcServerStatus;
import org.transitclock.service.dto.IpcTrip;
import org.transitclock.service.dto.IpcTripPattern;
import org.transitclock.service.dto.IpcVehicle;
import org.transitclock.service.dto.IpcVehicleConfig;
import org.transitclock.service.contract.ConfigInterface;
import org.transitclock.service.contract.PredictionsInterface;
import org.transitclock.service.contract.PredictionsInterface.RouteStop;
import org.transitclock.service.contract.ServerStatusInterface;
import org.transitclock.service.contract.VehiclesInterface;
import org.transitclock.service.dto.*;

import java.rmi.RemoteException;
import java.util.*;
import java.util.stream.Collectors;

/**
* Contains the API commands for the Transitime API for getting real-time vehicle and prediction
Expand Down Expand Up @@ -215,7 +191,7 @@ public Response getVehiclesToBlock(
public Response getAvlReport(
@BeanParam StandardParameters stdParameters,
@Parameter(description = "Vehicle id") @QueryParam(value = "v") String vehicleId,
@Parameter(description = "Begin date(MM-DD-YYYY.") @QueryParam(value = "beginDate") String beginDate,
@Parameter(description = "Begin date(MM-DD-YYYY or YYYY-MM-DD)") @QueryParam(value = "beginDate") String beginDate,
@Parameter(description = "Num days.", required = false) @QueryParam(value = "numDays") int numDays,
@Parameter(description = "Begin time(HH:MM)") @QueryParam(value = "beginTime") String beginTime,
@Parameter(description = "End time(HH:MM)") @QueryParam(value = "endTime") String endTime)
Expand Down Expand Up @@ -310,7 +286,7 @@ public Response getTrips(
public Response scheduleAdhReport(
@BeanParam StandardParameters stdParameters,
@Parameter(description = "Route id") @QueryParam(value = "r") String routeId,
@Parameter(description = "Begin date(MM-DD-YYYY.") @QueryParam(value = "beginDate") String beginDate,
@Parameter(description = "Begin date(MM-DD-YYYY or YYYY-MM-DD)") @QueryParam(value = "beginDate") String beginDate,
@Parameter(description = "Num days.", required = false) @QueryParam(value = "numDays") int numDays,
@Parameter(description = "Begin time(HH:MM)") @QueryParam(value = "beginTime") String beginTime,
@Parameter(description = "End time(HH:MM)") @QueryParam(value = "endTime") String endTime,
Expand Down Expand Up @@ -357,6 +333,43 @@ public Response getLastAvlJsonData(@BeanParam StandardParameters stdParameters)
}
}

@Operation(
summary = "Returns schedule adherence report for single stop.",
description = "Returns schedule adherence report for single stop.",
tags = {"report", "stop"})
@Path("/reports/schedStopAdh")
@GET
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public Response reportForStopById(
@BeanParam StandardParameters stdParameters,
@Parameter(description = "Stop Id", required = true) @QueryParam(value = "id") String stopId,
@Parameter(description = "Begin date(MM-DD-YYYY or YYYY-MM-DD)", required = true) @QueryParam(value = "beginDate") String beginDate,
@Parameter(description = "Num days.", required = true) @QueryParam(value = "numDays") int numDays,
@Parameter(description = "Begin time(HH:MM)") @QueryParam(value = "beginTime") String beginTime,
@Parameter(description = "End time(HH:MM)") @QueryParam(value = "endTime") String endTime,
@Parameter(description = "Allowable early in mins(default 1.0)") @QueryParam(value = "allowableEarly")
String allowableEarly,
@Parameter(description = "Allowable late in mins(default 4.0") @QueryParam(value = "allowableLate")
String allowableLate)
throws WebApplicationException {
stdParameters.validate();
try {
String response = Reports.getReportForStopById(
stdParameters.getAgencyId(),
stopId,
beginDate,
allowableEarly,
allowableLate,
beginTime,
endTime,
numDays);
return stdParameters.createResponse(response);
} catch (Exception e) {
// If problem getting data then return a Bad Request
throw WebUtils.badRequestException(e);
}
}

/**
* Handles the vehicleIds command. Returns list of vehicle IDs.
*
Expand Down
127 changes: 119 additions & 8 deletions core/src/main/java/org/transitclock/core/reports/Reports.java
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ public static String getScheduleAdhByStops(
// Specifies which routes to provide data for
+ SqlUtils.routeClause(route, "ad")
+ "\n"
+ SqlUtils.timeRangeClause(agencyId, "ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate)
+ SqlUtils.timeRangeClause("ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate)
+ "\n"
+ " AND scheduled_time-time > "
+ allowableEarlyMinutesStr
Expand All @@ -290,7 +290,7 @@ public static String getScheduleAdhByStops(
// Specifies which routes to provide data for
+ SqlUtils.routeClause(route, "ad")
+ "\n"
+ SqlUtils.timeRangeClause(agencyId, "ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate)
+ SqlUtils.timeRangeClause("ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate)
+ "\n"
+ " AND time-scheduled_time > "
+ allowableLateMinutesStr
Expand Down Expand Up @@ -345,7 +345,7 @@ public static String getScheduleAdhByStops(
// Specifies which routes to provide data for
+ SqlUtils.routeClause(route, "ad")
+ "\n"
+ SqlUtils.timeRangeClause(agencyId, "ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate)
+ SqlUtils.timeRangeClause("ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate)
+ "\n"
// Grouping and ordering is a bit complicated since might also be looking
// at old arrival/departure data that doen't have stoporder defined. Also,
Expand Down Expand Up @@ -486,7 +486,7 @@ public static String getScheduleAdhByStops_v2(
// Specifies which routes to provide data for
+ SqlUtils.routeClause(route, "ad")
+ "\n"
+ SqlUtils.timeRangeClause(agencyId, "ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate)
+ SqlUtils.timeRangeClause("ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate)
+ "\n"
+ " AND scheduled_time-time > "
+ allowableEarlyMinutesStr
Expand All @@ -511,7 +511,7 @@ public static String getScheduleAdhByStops_v2(
// Specifies which routes to provide data for
+ SqlUtils.routeClause(route, "ad")
+ "\n"
+ SqlUtils.timeRangeClause(agencyId, "ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate)
+ SqlUtils.timeRangeClause("ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate)
+ "\n"
+ " AND time-scheduled_time > "
+ allowableLateMinutesStr
Expand Down Expand Up @@ -567,7 +567,7 @@ public static String getScheduleAdhByStops_v2(
// Specifies which routes to provide data for
+ SqlUtils.routeClause(route, "ad")
+ "\n"
+ SqlUtils.timeRangeClause(agencyId, "ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate)
+ SqlUtils.timeRangeClause("ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate)
+ "\n"
// Grouping and ordering is a bit complicated since might also be looking
// at old arrival/departure data that doen't have stoporder defined. Also,
Expand Down Expand Up @@ -597,7 +597,7 @@ public static String getScheduleAdhByStops_v2(
// Specifies which routes to provide data for
+ SqlUtils.routeClause(route, "ad")
+ "\n"
+ SqlUtils.timeRangeClause(agencyId, "ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate)
+ SqlUtils.timeRangeClause("ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate)
+ "\n"
+ " AND scheduled_time-time > "
+ allowableEarlyMinutesStr
Expand All @@ -621,7 +621,7 @@ public static String getScheduleAdhByStops_v2(
// Specifies which routes to provide data for
+ SqlUtils.routeClause(route, "ad")
+ "\n"
+ SqlUtils.timeRangeClause(agencyId, "ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate)
+ SqlUtils.timeRangeClause("ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate)
+ "\n"
+ " AND time-scheduled_time > "
+ allowableLateMinutesStr
Expand All @@ -643,6 +643,116 @@ public static String getScheduleAdhByStops_v2(
return jsonString;
}


/**
* Queries agency for Stop ID and returns result as a JSON string. Limited to returning
* MAX_ROWS (50,000) data points.
*
* @return Stop reports in JSON format. Can be empty JSON array if no data meets criteria.
*/
public static String getReportForStopById (String agencyId,
String stop,
String beginDate,
String allowableEarly,
String allowableLate,
String beginTime,
String endTime,
int numDays) {
if (allowableEarly == null || allowableEarly.isEmpty()) allowableEarly = "1.0";
String allowableEarlyMinutesStr = "'" + SqlUtils.convertMinutesToSecs(allowableEarly) + " seconds'";

if (allowableLate == null || allowableLate.isEmpty()) allowableLate = "4.0";
String allowableLateMinutesStr = "'" + SqlUtils.convertMinutesToSecs(allowableLate) + " seconds'";

String sql = " WITH early AS (SELECT time AS early,\n" +
" s.name AS name,\n" +
" ad.route_id AS route,\n" +
" ad.trip_id AS trip,\n" +
" ad.block_id AS block,\n" +
" ad.vehicle_id AS vehicle,\n" +
" ad.scheduled_time AS schedule,\n" +
" regexp_replace(CAST(DATE_TRUNC('second', ad.scheduled_time::timestamp) - " +
" DATE_TRUNC('second', ad.time::timestamp) AS VARCHAR),\n" +
" '^00:', ''\n" +
" ) AS difference\n" +
" FROM arrivals_departures ad,\n" +
" stops s\n" +
" WHERE ad.config_rev = s.config_rev\n" +
" AND ad.stop_id = s.id\n" +
" AND ad.scheduled_time IS NOT NULL \n" +
// Specifies which stops to provide data for
SqlUtils.stopClause(stop, "ad") +
" \n" +
// Defines time range
SqlUtils.timeRangeClause("ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate) +
" \n" +
" AND scheduled_time - time > " + allowableEarlyMinutesStr +
" \n" +
" ORDER BY early),\n" +
" on_time AS (SELECT time AS on_time,\n" +
" s.name AS name,\n" +
" ad.route_id AS route,\n" +
" ad.trip_id AS trip,\n" +
" ad.block_id AS block,\n" +
" ad.vehicle_id AS vehicle,\n" +
" ad.scheduled_time AS schedule,\n" +
" regexp_replace(\n" +
" CAST(DATE_TRUNC('second', ad.scheduled_time::timestamp) - " +
" DATE_TRUNC('second', ad.time::timestamp) AS VARCHAR),\n" +
" '^(-)?00:', '\\1'\n" +
" ) AS difference\n" +
" FROM arrivals_departures ad,\n" +
" stops s\n" +
" WHERE ad.config_rev = s.config_rev\n" +
" AND ad.stop_id = s.id\n" +
" AND ad.scheduled_time IS NOT NULL \n" +
// Specifies which stops to provide data for
SqlUtils.stopClause(stop, "ad") +
" \n" +
// Defines time range
SqlUtils.timeRangeClause("ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate) +
" \n" +
" AND scheduled_time - time <= " + allowableEarlyMinutesStr +
" \n" +
" AND time - scheduled_time <= " + allowableLateMinutesStr +
" \n" +
" ORDER BY on_time),\n" +
" late AS (SELECT time AS late,\n" +
" s.name AS name,\n" +
" ad.route_id AS route,\n" +
" ad.trip_id AS trip,\n" +
" ad.block_id AS block,\n" +
" ad.vehicle_id AS vehicle,\n" +
" ad.scheduled_time AS schedule,\n" +
" regexp_replace(\n" +
" CAST(DATE_TRUNC('second', ad.scheduled_time::timestamp) - " +
" DATE_TRUNC('second', ad.time::timestamp) AS VARCHAR),\n" +
" '^(-)00:', '\\1'\n" +
" ) AS difference\n" +
" FROM arrivals_departures ad,\n" +
" stops s\n" +
" WHERE ad.config_rev = s.config_rev\n" +
" AND ad.stop_id = s.id\n" +
" AND ad.scheduled_time IS NOT NULL \n" +
// Specifies which stops to provide data for
SqlUtils.stopClause(stop, "ad") +
" \n" +
// Defines time range
SqlUtils.timeRangeClause("ad.time", MAX_NUM_DAYS, numDays, beginTime, endTime, beginDate) +
" \n" +
" AND time - scheduled_time > " + allowableLateMinutesStr +
" \n" +
" ORDER BY late) \n" +
"SELECT * FROM \n" +
" early\n" +
" FULL OUTER JOIN\n" +
" on_time ON early.early = on_time.on_time\n" +
" FULL OUTER JOIN\n" +
" late ON on_time.on_time = late.late\n";

return GenericJsonQuery.getJsonString(agencyId, sql).replaceFirst("\\bdata\\b", stop);
}

/**
* Queries agency for AVL data and returns result as a JSON string. Limited to returning
* MAX_ROWS (50,000) data points.
Expand Down Expand Up @@ -693,3 +803,4 @@ public static boolean hasLastAvlJsonInHours(String agencyId, String vehicleId, i
return json.length() > 50;
}
}

13 changes: 12 additions & 1 deletion core/src/main/java/org/transitclock/core/reports/SqlUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ public static String routeClause(String r, String tableAliasName) {
return " AND " + tableAlias + "route_short_name IN " + routeIdentifiers;
}

public static String stopClause(String id, String tableAliasName) {
if (id == null || id.isEmpty())
return "";

String tableAlias = "";
if (tableAliasName != null && !tableAliasName.isEmpty()) tableAlias = tableAliasName + ".";

return " AND " + tableAlias + "stop_id = '" + id + "'";
}

/**
* Creates a SQL clause for specifying a time range. Looks at the request parameters
* "beginDate", "numDays", "beginTime", and "endTime"
Expand Down Expand Up @@ -189,7 +199,6 @@ public static String timeRangeClause(HttpServletRequest request, String timeColu
* INTERVAL '1 day' AND time::time BETWEEN '12:00' AND '24:00'"
*/
public static String timeRangeClause(
String agencyId,
String timeColumnName,
int maxNumDays,
int numDays,
Expand Down Expand Up @@ -217,10 +226,12 @@ public static String timeRangeClause(

SimpleDateFormat currentFormat = new SimpleDateFormat("MM-dd-yyyy");
SimpleDateFormat requiredFormat = new SimpleDateFormat("yyyy-MM-dd");
if (beginDate.charAt(4) != '-') {
try {
beginDate = requiredFormat.format(currentFormat.parse(beginDate));
} catch (ParseException e) {
logger.error("Exception happened while processing time-range clause", e);
}
}
return " AND %s BETWEEN '%s' AND TIMESTAMP '%s' + INTERVAL '%d day' %s "
.formatted(timeColumnName, beginDate, beginDate, numDays, timeSql);
Expand Down

0 comments on commit 1c15eb1

Please sign in to comment.