diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..ad733c083
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,4 @@
+logs
+.git
+.github
+.idea
\ No newline at end of file
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
new file mode 100644
index 000000000..13e05ae20
--- /dev/null
+++ b/.github/workflows/maven.yml
@@ -0,0 +1,34 @@
+# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
+
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+name: Java CI with Maven
+
+on:
+ push:
+ branches: [ "main", "refactoring" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ name: Checkout
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: maven
+
+ - name: Build with Maven
+ run: mvn -B package --file pom.xml
diff --git a/.gitignore b/.gitignore
index cb27ee84f..d25be10c9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
*.class
*.project
*.classpath
+.fastRequest
*.iml
.idea/
# Package Files #
@@ -8,7 +9,46 @@
*.war
*.ear
*.log
-/bin/
+/docker/
/.settings/
.DS_Store
+.flattened-pom.xml
+dependency-reduced-pom.xml
+target
+logs
+
+HELP.md
+target/
+!../.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+/.jpb/jpb-settings.xml
+config/**/*
diff --git a/.mvn/jvm.config b/.mvn/jvm.config
new file mode 100644
index 000000000..be15efcfc
--- /dev/null
+++ b/.mvn/jvm.config
@@ -0,0 +1 @@
+-Xmx2048m -Djava.awt.headless=true -XX:ParallelGCThreads=4 -XX:ConcGCThreads=4 -Djava.util.concurrent.ForkJoinPool.common.parallelism=4 -XX:CICompilerCount=4 -XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=40 -XX:+ExitOnOutOfMemoryError
\ No newline at end of file
diff --git a/.mvn/maven.config b/.mvn/maven.config
new file mode 100644
index 000000000..3e8e28895
--- /dev/null
+++ b/.mvn/maven.config
@@ -0,0 +1 @@
+-Drevision=2.0.0-SNAPSHOT
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 000000000..5f0536eb7
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
diff --git a/.sdkmanrc b/.sdkmanrc
new file mode 100644
index 000000000..00541f3b0
--- /dev/null
+++ b/.sdkmanrc
@@ -0,0 +1,4 @@
+# Enable auto-env through the sdkman_auto_env config
+# Add key=value pairs of SDKs to use below
+java=17.0.9-tem
+maven=3.9.6
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 68a107639..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-language: java
-jdk: openjdk8
-cache:
- directories:
- - "$HOME/.m2"
-script: mvn install -DskipTests
-deploy:
- provider: releases
- api_key:
- secure: gT/9kJv4oLzhXUsVOpASZi41BTiXRDohrryYg7o8txZG6caeki2B5lsLCYQlONe09IR3Mv2BwY6hcPX+GJlJa1Npvfn3Q9Th5IbYLAglRQoqDN/d3iyY8WhEY/ZqbUZ7vJXdpPNjwcyoA0dd2aA8N9pOC8QaFK43n47LCDAe+F16uGyxQN72TX4jCIANOvagVq8iUZtJUByFyubIGEHv0boEQNdODu5NdwzHsd0anCSd+Z/8zIu/rV5Ck1J5ecslqVJFSCs0LRrA3aFu31tf87KgT4+HomC79WHeo3hqrzCZ+PaqPJGrxYQ5j02Lw7rDBX3C8U9SqiP8piKKTXoHflNTWp4rrBT7Z/KehRChfXPvj3l+Oo6Nlz8e49du9HGpMbDRF6IUA5NiCrUotMR+MRMPrCmJcyLwaupPPr4Ha0+Tiy72T7Rp4oYpByuBvIrp8lzEZjKgKv4kpIkSiYhep7N3SoU5S98y6/S/FPWJvLceJrusi/LNFGCcMSY9Ntn4q1qKNt426eqksymcSy5G36DYopRDYg/z3vlTX1lyzPEWEkQv7PPlZu9MFJh0+7pUpadxuRLSiIpL9PtJ1sDPUvGVYSKGnBH2H64Wd8D13tnfPbtqPAsT+9bLFBk0mUbEGqXp93vMSs/Q9n+LC5+29rSdsO2IIR7gbLEquH/9rGo=
- file:
- - "transitclock/target/Core.jar"
- - "transitclock/target/CreateAPIKey.jar"
- - "transitclock/target/CreateWebAgency.jar"
- - "transitclock/target/GtfsFileProcessor.jar"
- - "transitclock/target/RmiQuery.jar"
- - "transitclock/target/ScheduleGenerator.jar"
- - "transitclock/target/SchemaGenerator.jar"
- - "transitclock/target/transitclockCore-2.0.0-SNAPSHOT.jar"
- - "transitclock/target/UpdateTravelTimes.jar"
- - "transitclockWebapp/target/web.war"
- - "transitclockApi/target/api.war"
- skip_cleanup: true
- on:
- repo: TheTransitClock/transitime
- tags: false
- branch: develop
diff --git a/BUILD.md b/BUILD.md
deleted file mode 100644
index 026d4bfa7..000000000
--- a/BUILD.md
+++ /dev/null
@@ -1,9 +0,0 @@
-The software is made up of three modules which can each be built with maven.
-
-The core fuctionality is in the transitime project. The REST api is in transitimeApi and the user Web applicaton is in transitimeWebapp.
-
-1. transitime
-2. transitimeApi
-3. transitimeWebapp
-
-The project can be built from the root "core" directory by running "mvn install -DskipTests".
diff --git a/README.md b/README.md
index 52ccac75f..f6c283f7f 100644
--- a/README.md
+++ b/README.md
@@ -1,35 +1,213 @@
-core [](https://travis-ci.org/TheTransitClock/transitime)
-====
+# :fire::fire: TransitTime :fire::fire:
The complete core Java software for the Transitime real-time transit information project. The goal is to use any type of real-time GPS data to generate useful public transportation information. The system is for both letting passengers know the status of their vehicles and helping agencies more effectively manage their systems. By providing a complete open-source system, agencies can have a cost effective system and have full ownership of it.
The software is currently being used in a production environment for MBTA Commuter Rail and for several smaller agencies.
-Build
+## :hammer: Setup
+In order to build & run the application make sure you have the following requirements fulfilled:
+- [ ] you have maven installed
+- [ ] you have java 17 or later installed
+- [ ] you have docker installed
-The software is made up of three modules which can each be built with maven. See BUILD.md
+### :wrench: Building
+The software is made up of multiple modules which can each be built with maven.
+The core functionality is in the [core](core) project. The REST api & webapp is in [app](app) folder.
-The core functionality is in the transitime project. The REST api is in transitimeApi and the user Web applicaton is in transitimeWebapp.
+```shell
+# this will build all the modules of the application
+mvn clean install
+```
-Setup
+### :runner: Running
+In order to run the application you need a postgres database available, or you could use docker to start an instance
+```shell
+docker run --rm -e POSTGRES_PASSWORD=transitclock -e POSTGRES_DB=transitclock -p 5432:5432 postgres:15-alpine
+```
-The main module is transitTime. This has several standalone programs in the org.transitime.applications package.
+#### :blue_book: Configuration
+- **Properties file**
-SchemaGenerator.java will generate the SQL to create the database structures you need to run on.
-DBTest.java can be used to test that the database can be connected to.
-GTFSFileProcessor.java will read a GTFS file into this database structure.
-Core.java is as the name implies is the workhorse of the system.
-RmiQuery.java allows you make queries to the server run in core from the command line.
-CreateAPIKey.java a test app to allow you create test/demo key to access REST api webapp.
-CreateWebAgency.java is used to create and agency that will work in transitimeWebapp.
+After docker has started, and you have postgress running you need to fine tune the application properties
+Please adjust the fields were you see comments starting with ```##!!!```.
-Details on how to run each of these and their respective parameters are in the README for the transitime module.
+```properties
+##!!! STATIC GTFS FEED
+transitclock.gtfs.url=
+transitclock.gtfs.dirName=/var/transitclock/cache
+transitclock.gtfs.intervalMsec=86400000
+transitclock.autoBlockAssigner.autoAssignerEnabled=true
+transitclock.autoBlockAssigner.ignoreAvlAssignments=false
+transitclock.autoBlockAssigner.allowableEarlySeconds=600
+transitclock.autoBlockAssigner.allowableLateSeconds=600
-Once this is set up the next step is to set up the transitimeApi which is a RESTful API. This API makes RMI calls to the RMI Server started by Core.java to provide results. This is a war file which can be deployed into Tomcat.
+transitclock.avl.feedTimeoutInMSecs=30000
+##!!! RT FEED URI
+transitclock.avl.gtfsRealtimeFeedURI=
+transitclock.avl.maxSpeed=100
+transitclock.avl.numThreads=1
+transitclock.avl.queueSize=2400
+transitclock.avl.minLatitude=43
+transitclock.avl.maxLatitude=48
+transitclock.avl.maxLongitude=30
+transitclock.avl.minLongitude=20
+transitclock.avl.feedPollingRateSecs=15
-The transitimeWebapp in turn is a web application which uses the transitTimeAPI to provided a user interface. This is a war file which can be deployed into Tomcat. This connects to the database and the connection information is configured in hibernate.cfg.xml in the src/main/resources directory. Currently this needs to be deployed on the same server as the API.
+transitclock.blockLoading.agressive=false
-The transitimeQuickStart can be built with mvn install and ran using java -jar transitimeQuickStart it is currently a work in progress but the gui elements can be seen.
+transitclock.cache.core.daysPopulateHistoricalCache=0
-[](https://zenodo.org/record/3550975#.XdgmVedKjOQ)
+transitclock.core.agencyId=stpt
+transitclock.core.allowableEarlyDepartureTimeForLoggingEvent=180
+transitclock.core.allowableEarlyForLayoverSeconds=1800
+transitclock.core.allowableEarlySecondsForInitialMatching=1200
+transitclock.core.allowableEarlyTimeForEarlyDepartureSecs=180
+transitclock.core.allowableLateAtTerminalForLoggingEvent=240
+transitclock.core.allowableLateDepartureTimeForLoggingEvent=360
+transitclock.core.allowableLateSeconds=2700
+transitclock.core.allowableNumberOfBadMatches=4
+transitclock.core.afterStopDistance=100
+transitclock.core.beforeStopDistance=100
+transitclock.core.cache.tripDataHistoryCache=org.transitclock.core.dataCache.ehcache.scheduled.TripDataHistoryCache
+transitclock.core.cache.errorCacheClass=org.transitclock.core.dataCache.ehcache.KalmanErrorCache
+transitclock.core.cache.stopArrivalDepartureCache=org.transitclock.core.dataCache.ehcache.StopArrivalDepartureCache
+transitclock.core.cache.stopPathPredictionCache=org.transitclock.core.dataCache.StopPathPredictionCache
+transitclock.core.cache.dwellTimeModelCache=org.transitclock.core.dataCache.ehcache.scheduled.DwellTimeModelCache
+transitclock.core.distanceFromLayoverForEarlyDeparture=250
+transitclock.core.dwelltime.model=org.transitclock.core.predictiongenerator.scheduled.dwell.DwellAverage
+transitclock.core.dwelltime.headwayGeneratorClass=org.transitclock.core.headwaygenerator.LastArrivalsHeadwayGenerator
+transitclock.core.exclusiveBlockAssignments=true
+transitclock.core.fillHistoricalCaches=0
+transitclock.core.layoverDistance=1000
+transitclock.core.longDistanceDeadheadingSpeed=20
+transitclock.core.matchHistoryMaxSize=40
+transitclock.core.maxHeadingOffsetFromSegment=200
+transitclock.core.maxPredictionTimeForDbSecs=1200
+transitclock.core.maxPredictionsTimeSecs=1800
+transitclock.core.minDistanceForDelayed=60
+transitclock.core.minDistanceForNoProgress=60
+transitclock.core.onlyNeedArrivalDepartures=false
+#transitclock.core.predictionGeneratorClass=org.transitclock.core.predictiongenerator.scheduled.traveltime.kalman.KalmanPredictionGeneratorImpl
+transitclock.core.shortDistanceDeadheadingSpeed=10
+transitclock.core.timeForDeterminingDelayedSecs=300
+transitclock.core.timeForDeterminingNoProgress=360000
+transitclock.core.trackHistoricalCaches=false
+transitclock.core.useArrivalPredictionsForNormalStops=false
+transitclock.db.batchSize=4000
+transitclock.db.dbName=STPT
+transitclock.db.dbPassword=transitclock
+transitclock.db.dbUserName=transitclock
+transitclock.db.dbHost=localhost:5432
+transitclock.db.storeDataInDatabase=true
+transitclock.db.dbType=postgresql
+
+
+transitclock.predAccuracy.stopsPerTrip=1000
+transitclock.predAccuracy.maxPredTimeMinutes=30
+
+transitclock.prediction.data.kalman.mindays=3
+transitclock.prediction.data.kalman.maxdays=5
+transitclock.prediction.data.kalman.maxdaystosearch=21
+transitclock.prediction.data.kalman.percentagePredictionMethodDifferencene=50
+transitclock.prediction.data.kalman.tresholdForDifferenceEventLog=60000
+transitclock.prediction.rls.lambda=0.9
+
+transitclock.timeout.pollingRateSecs=60
+
+transitclock.modules.optionalModulesList=org.transitclock.core.avl.GtfsRealtimeModule;org.transitclock.core.predAccuracy.PredictionAccuracyModule;org.transitclock.gtfs.GtfsUpdatedModule
+##!!! absolute location to the hibernate.cfg.xml file
+transitclock.hibernate.configFile=
+transitclock.logging.dir=/var/transitclock/logs
+transitclock.web.mapTileUrl=http://tile.openstreetmap.org/{z}/{x}/{y}.png
+```
+
+- **hibernate config file sample**
+```xml
+
+
+
+
+
+
+ org.postgresql.Driver
+
+ false
+
+
+
+
+
+ create-drop
+ 5
+ 20
+ 30000
+ 25
+
+
+```
+
+#### :bomb: Starting the application
+```shell
+JAVA_OPTS="-Dtransitclock.configFiles=/location-to-properties-file/transitclock.properties -Dtransitclock.apikey=f78a2e9a"
+GTFS_TO_IMPORT="url to gtfs you want to import"
+
+java $JAVA_OPTS -jar app/target/transitclock.jar --gtfs-url $GTFS_TO_IMPORT
+```
+
+### :whale: Running using Docker
+Simplest way to run the transitclock would be using ```docker```, actually ```docker compose```. For it to happen you would be to map your config into the transitclock container using a configuration as follows:
+```yaml
+version: "3.18"
+services:
+ db:
+ image: postgres:15-alpine
+ restart: always
+ environment:
+ POSTGRES_PASSWORD: transitclock
+ POSTGRES_DB: transitclock
+ ports:
+ - "5432:5432"
+ gtfsrt-validator:
+ image: ghcr.io/mobilitydata/gtfs-realtime-validator:latest
+ ports:
+ - "9090:8080"
+ transitclock:
+ image: otrro/transitclock-server:latest
+ depends_on:
+ - db
+ environment:
+ AGENCYID: transitclock # not that relevant anymore since will be coming from your file
+ AGENCYNAME: transitclock # has to match what is configured in the file for transitclock.db.dbName and should match POSTGRES_DB specified for postgress
+ GTFS_URL: https://your feed location
+ GTFSRTVEHICLEPOSITIONS: https://rt feed location # can be configured in the file directly
+ PGPASSWORD: transitclock
+ POSTGRES_PORT_5432_TCP_ADDR: db
+ POSTGRES_PORT_5432_TCP_PORT: 5432
+ volumes:
+ # this mapps the transitclock.properties file that is near to docker-compose file to the one in container that is used by the app
+ ### IMPORTANT NOTE: make sure you have this config transitclock.hibernate.configFile=/app/config/hibernate.cfg.xml
+ - ./transitclock.properties:/app/config/transitclock.properties
+ ports:
+ - "8080:8080"
+ command:
+ - --gtfs-url
+ - https://your feed location
+```
+
+After doing this you just simply need to:
+```shell
+# if you have compose plugin for docker installed
+docker compose up # in the folder where you have save the previous content as docker-compose.yaml
+
+# if you have the actual docker-compose application
+docker-compose up
+```
\ No newline at end of file
diff --git a/transitclockApi/README.md b/app/README.md
similarity index 100%
rename from transitclockApi/README.md
rename to app/README.md
diff --git a/app/pom.xml b/app/pom.xml
new file mode 100755
index 000000000..7a8b3181e
--- /dev/null
+++ b/app/pom.xml
@@ -0,0 +1,180 @@
+
+
+ 4.0.0
+
+
+ ro.vladvesa.transitclock
+ transitclock
+ ${revision}
+
+
+ app
+ ${revision}
+ jar
+
+
+
+ org.glassfish.jersey.media
+ jersey-media-json-jackson
+
+
+ org.glassfish.jersey.containers
+ jersey-container-jetty-http
+
+
+ org.glassfish.jersey.containers
+ jersey-container-servlet
+
+
+ org.glassfish.jersey.inject
+ jersey-hk2
+
+
+ ro.vladvesa.transitclock
+ core
+ ${revision}
+
+
+ com.google.guava
+ guava
+
+
+ io.swagger.core.v3
+ swagger-jaxrs2-jakarta
+ 2.2.20
+
+
+
+
+ org.eclipse.jetty
+ jetty-server
+
+
+ org.eclipse.jetty.ee10
+ jetty-ee10-webapp
+
+
+ org.eclipse.jetty.ee10
+ jetty-ee10-servlet
+
+
+ org.eclipse.jetty
+ jetty-util
+
+
+ org.eclipse.jetty.ee10
+ jetty-ee10-apache-jsp
+ jar
+
+
+ org.eclipse.jetty.ee10
+ jetty-ee10-glassfish-jstl
+ 12.0.5
+ pom
+
+
+
+
+
+ jakarta.servlet.jsp.jstl
+ jakarta.servlet.jsp.jstl-api
+ 3.0.0
+
+
+
+ org.glassfish.web
+ jakarta.servlet.jsp.jstl
+ 3.0.1
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ com.google.cloud.tools
+ jib-maven-plugin
+
+ false
+
+ org.transitclock.Application
+ /docker-entrypoint.sh
+
+
+ /var/transitclock/
+
+
+
+ 8080
+
+
+
+ eclipse-temurin:17-jre
+
+
+ amd64
+ linux
+
+
+ arm64
+ linux
+
+
+
+
+ ${env.CONTAINER_REPO}
+
+ ${env.CONTAINER_REGISTRY_USER}
+ ${env.CONTAINER_REGISTRY_PASSWORD}
+
+
+
+
+
+ /docker-entrypoint.sh
+ 755
+
+
+ /waitforit
+ 755
+
+
+ /app/config/
+ 777
+
+
+ /var/transitclock/
+ 755
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+
+
+ shade
+
+ shade
+
+ package
+
+ false
+
+
+ org.transitclock.Application
+
+
+ target/transitclock.jar
+
+
+
+
+
+
+
diff --git a/app/src/main/java/org/transitclock/Application.java b/app/src/main/java/org/transitclock/Application.java
new file mode 100644
index 000000000..368c411c5
--- /dev/null
+++ b/app/src/main/java/org/transitclock/Application.java
@@ -0,0 +1,408 @@
+package org.transitclock;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.ParameterException;
+import jakarta.servlet.DispatcherType;
+import lombok.Getter;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.time.DateUtils;
+import org.apache.tomcat.InstanceManager;
+import org.apache.tomcat.SimpleInstanceManager;
+import org.eclipse.jetty.ee10.jsp.JettyJspServlet;
+import org.eclipse.jetty.ee10.servlet.DefaultServlet;
+import org.eclipse.jetty.ee10.servlet.ErrorHandler;
+import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
+import org.eclipse.jetty.ee10.servlet.ServletHolder;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.flywaydb.core.Flyway;
+import org.glassfish.jersey.servlet.ServletContainer;
+import org.hibernate.Session;
+import org.transitclock.api.EmbeddedJspStarter;
+import org.transitclock.api.utils.ApiLoggingFilter;
+import org.transitclock.api.utils.ApiOriginFilter;
+import org.transitclock.api.utils.WebLoggingFilter;
+import org.transitclock.api.utils.XSSFilter;
+import org.transitclock.config.ConfigFileReader;
+import org.transitclock.config.data.AgencyConfig;
+import org.transitclock.config.data.CoreConfig;
+import org.transitclock.config.data.DbSetupConfig;
+import org.transitclock.core.dataCache.StopArrivalDepartureCacheFactory;
+import org.transitclock.core.dataCache.TripDataHistoryCacheFactory;
+import org.transitclock.core.dataCache.ehcache.CacheManagerFactory;
+import org.transitclock.core.dataCache.frequency.FrequencyBasedHistoricalAverageCache;
+import org.transitclock.core.dataCache.scheduled.ScheduleBasedHistoricalAverageCache;
+import org.transitclock.domain.ApiKeyManager;
+import org.transitclock.domain.hibernate.HibernateUtils;
+import org.transitclock.domain.webstructs.WebAgency;
+import org.transitclock.utils.Time;
+import org.transitclock.utils.threading.UncaughtExceptionHandler;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.TimeZone;
+
+import static org.transitclock.utils.ApplicationShutdownSupport.addShutdownHook;
+
+@Slf4j
+@Getter
+public class Application {
+ private static final String WEBROOT_INDEX = "/webroot/";
+ private final CommandLineParameters cli;
+ private final ApplicationContext context;
+
+ @SneakyThrows
+ public static void main(String[] args) {
+ TimeZone aDefault = TimeZone.getDefault();
+ logger.warn("Application started using Timezone [{}, offset={}, daylight={}]", aDefault.getID(), aDefault.getRawOffset(), aDefault.useDaylightTime());
+ var uncaughtExceptionHandler = new UncaughtExceptionHandler();
+ Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
+
+ var currentThread = Thread.currentThread();
+ currentThread.setName("main");
+ currentThread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
+
+ ConfigFileReader.processConfig();
+
+ var app = new Application(args);
+ app.start();
+ }
+
+ public Application(String[] args) throws IOException, URISyntaxException {
+ this(parseAndValidateCmdLine(args));
+ }
+
+ public Application(CommandLineParameters commandLineParameters) throws IOException, URISyntaxException {
+ this.cli = commandLineParameters;
+ this.context = ApplicationContext.createDefaultContext(AgencyConfig.getAgencyId());
+ }
+
+ public void start() {
+ // init cache manager
+ CacheManagerFactory.getInstance();
+ // instantiate flyway & migrate
+ migrate();
+ if(cli.shouldLoadGtfs()) {
+ loadGtfs();
+ }
+ createApiKey();
+ createWebAgency();
+ run();
+ }
+
+ private void migrate() {
+ Flyway flyway = Flyway.configure()
+ .loggers("slf4j")
+ .dataSource(DbSetupConfig.getConnectionUrl(), DbSetupConfig.getDbUserName(), DbSetupConfig.getDbPassword())
+ .load();
+ flyway.migrate();
+ }
+
+ private void loadGtfs() {
+ GtfsFileProcessor processor = GtfsFileProcessor.createGtfsFileProcessor(cli);
+ processor.process();
+ }
+
+ private void run() {
+ String agencyId = AgencyConfig.getAgencyId();
+ try {
+ try {
+ populateCaches();
+ } catch (Exception e) {
+ logger.error("Failed to populate cache.", e);
+ }
+
+ addShutdownHook("close-cache", () -> {
+ try {
+ logger.info("Closing cache.");
+ CacheManagerFactory.getInstance().close();
+ logger.info("Cache closed.");
+ } catch (Exception e) {
+ logger.error("Cache close failed...", e);
+ }
+ });
+
+ // Initialize the core now
+ Core core = Core.createCore(agencyId, context.getModuleRegistry());
+
+ Server server = createWebserver();
+ server.start();
+ logger.info("Go to http://localhost:{} in your browser", cli.port);
+ server.join();
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ }
+ }
+
+ private void createApiKey() {
+ try {
+ ApiKeyManager.getInstance()
+ .generateApiKey(
+ "Sean Og Crudden",
+ "http://www.transitclock.org",
+ "og.crudden@gmail.com",
+ "123456",
+ "foo");
+ } catch (IllegalArgumentException ignored) {
+
+ }
+ }
+
+ private void createWebAgency() {
+ String agencyId = AgencyConfig.getAgencyId();
+ WebAgency webAgency = new WebAgency(agencyId,
+ "127.0.0.1",
+ true,
+ DbSetupConfig.getDbName(),
+ DbSetupConfig.getDbType(),
+ DbSetupConfig.getDbHost(),
+ DbSetupConfig.getDbUserName(),
+ DbSetupConfig.getDbPassword());
+
+ try {
+ // Store the WebAgency
+ webAgency.store(agencyId);
+ } catch (IllegalArgumentException ignored) {
+
+ }
+ }
+
+ private void populateCaches() throws Exception {
+ Session session = HibernateUtils.getSession();
+
+ Date endDate = Calendar.getInstance().getTime();
+
+ if (!CoreConfig.cacheReloadStartTimeStr.getValue().isEmpty() && !CoreConfig.cacheReloadEndTimeStr.getValue().isEmpty()) {
+ if (TripDataHistoryCacheFactory.getInstance() != null) {
+ logger.debug(
+ "Populating TripDataHistoryCache cache for period {} to {}",
+ CoreConfig.cacheReloadStartTimeStr.getValue(),
+ CoreConfig.cacheReloadEndTimeStr.getValue());
+ TripDataHistoryCacheFactory.getInstance()
+ .populateCacheFromDb(
+ session,
+ new Date(Time.parse(CoreConfig.cacheReloadStartTimeStr.getValue()).getTime()),
+ new Date(Time.parse(CoreConfig.cacheReloadEndTimeStr.getValue()).getTime())
+ );
+ }
+
+ if (FrequencyBasedHistoricalAverageCache.getInstance() != null) {
+ logger.debug(
+ "Populating FrequencyBasedHistoricalAverageCache cache for period {} to {}",
+ CoreConfig.cacheReloadStartTimeStr.getValue(),
+ CoreConfig.cacheReloadEndTimeStr.getValue());
+ FrequencyBasedHistoricalAverageCache.getInstance()
+ .populateCacheFromDb(
+ session,
+ new Date(Time.parse(CoreConfig.cacheReloadStartTimeStr.getValue()).getTime()),
+ new Date(Time.parse(CoreConfig.cacheReloadEndTimeStr.getValue()).getTime())
+ );
+ }
+
+ if (StopArrivalDepartureCacheFactory.getInstance() != null) {
+ logger.debug(
+ "Populating StopArrivalDepartureCache cache for period {} to {}",
+ CoreConfig.cacheReloadStartTimeStr.getValue(),
+ CoreConfig.cacheReloadEndTimeStr.getValue());
+ StopArrivalDepartureCacheFactory.getInstance()
+ .populateCacheFromDb(
+ session,
+ new Date(Time.parse(CoreConfig.cacheReloadStartTimeStr.getValue()).getTime()),
+ new Date(Time.parse(CoreConfig.cacheReloadEndTimeStr.getValue()).getTime())
+ );
+ }
+ /*
+ if(ScheduleBasedHistoricalAverageCache.getInstance()!=null)
+ {
+ logger.debug("Populating ScheduleBasedHistoricalAverageCache cache for period {} to {}",cacheReloadStartTimeStr.getValue(),cacheReloadEndTimeStr.getValue());
+ ScheduleBasedHistoricalAverageCache.getInstance().populateCacheFromDb(session, new Date(Time.parse(cacheReloadStartTimeStr.getValue()).getTime()), new Date(Time.parse(cacheReloadEndTimeStr.getValue()).getTime()));
+ }
+ */
+ } else {
+ for (int i = 0; i < CoreConfig.getDaysPopulateHistoricalCache(); i++) {
+ Date startDate = DateUtils.addDays(endDate, -1);
+
+ if (TripDataHistoryCacheFactory.getInstance() != null) {
+ logger.debug("Populating TripDataHistoryCache cache for period {} to {}", startDate, endDate);
+ TripDataHistoryCacheFactory.getInstance().populateCacheFromDb(session, startDate, endDate);
+ }
+
+ if (FrequencyBasedHistoricalAverageCache.getInstance() != null) {
+ logger.debug(
+ "Populating FrequencyBasedHistoricalAverageCache cache for period {} to" + " {}",
+ startDate,
+ endDate);
+ FrequencyBasedHistoricalAverageCache.getInstance().populateCacheFromDb(session, startDate, endDate);
+ }
+
+ endDate = startDate;
+ }
+
+ endDate = Calendar.getInstance().getTime();
+
+ /* populate one day at a time to avoid memory issue */
+ for (int i = 0; i < CoreConfig.getDaysPopulateHistoricalCache(); i++) {
+ Date startDate = DateUtils.addDays(endDate, -1);
+ if (StopArrivalDepartureCacheFactory.getInstance() != null) {
+ logger.debug("Populating StopArrivalDepartureCache cache for period {} to {}", startDate, endDate);
+ StopArrivalDepartureCacheFactory.getInstance().populateCacheFromDb(session, startDate, endDate);
+ }
+
+ endDate = startDate;
+ }
+ endDate = Calendar.getInstance().getTime();
+
+ for (int i = 0; i < CoreConfig.getDaysPopulateHistoricalCache(); i++) {
+ Date startDate = DateUtils.addDays(endDate, -1);
+
+ if (ScheduleBasedHistoricalAverageCache.getInstance() != null) {
+ logger.debug(
+ "Populating ScheduleBasedHistoricalAverageCache cache for period {} to" + " {}",
+ startDate,
+ endDate);
+ ScheduleBasedHistoricalAverageCache.getInstance().populateCacheFromDb(session, startDate, endDate);
+ }
+
+ endDate = startDate;
+ }
+ }
+ }
+
+ /**
+ * Setup JSP Support for ServletContextHandlers.
+ *
+ * NOTE: This is not required or appropriate if using a WebAppContext.
+ *
+ *
+ * @param servletContextHandler the ServletContextHandler to configure
+ * @throws IOException if unable to configure
+ */
+ private void enableEmbeddedJspSupport(ServletContextHandler servletContextHandler) throws IOException {
+ // Establish Scratch directory for the servlet context (used by JSP compilation)
+ File tempDir = new File(System.getProperty("java.io.tmpdir"));
+ File scratchDir = new File(tempDir.toString(), "embedded-jetty-jsp");
+
+ if (!scratchDir.exists() && !scratchDir.mkdirs()) {
+ throw new IOException("Unable to create scratch directory: " + scratchDir);
+ }
+
+ servletContextHandler.setAttribute("javax.servlet.context.tempdir", scratchDir);
+
+ // Set Classloader of Context to be sane (needed for JSTL)
+ // JSP requires a non-System classloader, this simply wraps the
+ // embedded System classloader in a way that makes it suitable
+ // for JSP to use
+ ClassLoader jspClassLoader = new URLClassLoader(new URL[0], this.getClass().getClassLoader());
+ servletContextHandler.setClassLoader(jspClassLoader);
+
+ // Manually call JettyJasperInitializer on context startup
+ servletContextHandler.addBean(new EmbeddedJspStarter(servletContextHandler));
+
+ // Create / Register JSP Servlet (must be named "jsp" per spec)
+ ServletHolder holderJsp = new ServletHolder("jsp", JettyJspServlet.class);
+ holderJsp.setInitOrder(0);
+ holderJsp.setInitParameter("scratchdir", scratchDir.toString());
+ holderJsp.setInitParameter("logVerbosityLevel", "DEBUG");
+ holderJsp.setInitParameter("fork", "false");
+ holderJsp.setInitParameter("xpoweredBy", "false");
+ holderJsp.setInitParameter("compilerTargetVM", "17");
+ holderJsp.setInitParameter("compilerSourceVM", "17");
+ holderJsp.setInitParameter("keepgenerated", "true");
+ servletContextHandler
+ .addServlet(holderJsp, "*.jsp");
+
+ servletContextHandler.setAttribute(InstanceManager.class.getName(), new SimpleInstanceManager());
+ }
+
+ private Server createWebserver() throws IOException, URISyntaxException {
+ QueuedThreadPool jettyThreadpool = new QueuedThreadPool(10, 3, 30000);
+ jettyThreadpool.setName("jetty-threadpool");
+ Server server = new Server(jettyThreadpool);
+ ServerConnector connector = new ServerConnector(server);
+ connector.setPort(cli.port);
+ server.addConnector(connector);
+ server.setStopAtShutdown(true);
+
+ // Base URI for servlet context
+ URI baseUri = getWebRootResourceUri();
+
+ ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
+ servletContextHandler.setErrorHandler(new ErrorHandler());
+ servletContextHandler.setContextPath("/");
+ servletContextHandler.addFilter(new XSSFilter(), "/*", EnumSet.allOf(DispatcherType.class));
+ servletContextHandler.addFilter(new ApiLoggingFilter(), "/api/*", EnumSet.allOf(DispatcherType.class));
+ servletContextHandler.addFilter(new ApiOriginFilter(), "/api/*", EnumSet.allOf(DispatcherType.class));
+ servletContextHandler.addFilter(new WebLoggingFilter(), "/*", EnumSet.allOf(DispatcherType.class));
+ servletContextHandler.setBaseResourceAsString(baseUri.toASCIIString());
+
+ enableEmbeddedJspSupport(servletContextHandler);
+
+ // Default Servlet (always last, always named "default")
+ ServletHolder holderDefault = new ServletHolder("default", DefaultServlet.class);
+ holderDefault.setInitParameter("resourceBase", baseUri.toASCIIString());
+ holderDefault.setInitParameter("dirAllowed", "true");
+ holderDefault.setInitOrder(0);
+ servletContextHandler.addServlet(holderDefault, "/");
+
+ ServletHolder docServlet = new ServletHolder("doc", ServletContainer.class);
+ docServlet.setInitParameter("jersey.config.server.provider.packages", "io.swagger.v3.jaxrs2.integration.resources,org.transitclock.api.resources,org.transitclock.api.utils");
+ docServlet.setInitOrder(0);
+ servletContextHandler.addServlet(docServlet, "/doc/*");
+
+ ServletHolder apiServlet = new ServletHolder("api", ServletContainer.class);
+ apiServlet.setInitParameter("jersey.config.server.provider.packages", "org.transitclock.api.resources");
+ apiServlet.setInitOrder(0);
+ servletContextHandler.addServlet(apiServlet, "/api/v1/*");
+
+ server.setHandler(servletContextHandler);
+
+ return server;
+ }
+
+ private URI getWebRootResourceUri() throws FileNotFoundException, URISyntaxException {
+ URL indexUri = this.getClass().getResource(WEBROOT_INDEX);
+ if (indexUri == null) {
+ throw new FileNotFoundException("Unable to find resource " + WEBROOT_INDEX);
+ }
+ // Points to wherever /webroot/ (the resource) is
+ return indexUri.toURI();
+ }
+
+ private static CommandLineParameters parseAndValidateCmdLine(String[] args) {
+ CommandLineParameters params = new CommandLineParameters();
+ try {
+ // It is tempting to use JCommander's command syntax: http://jcommander.org/#_more_complex_syntaxes_commands
+ // But this seems to lead to confusing switch ordering and more difficult subsequent use of the
+ // parsed commands, since there will be three separate objects.
+ JCommander jc = JCommander.newBuilder()
+ .addObject(params)
+ .args(args)
+ .build();
+
+ if (params.version) {
+ System.exit(0);
+ }
+
+ if (params.help) {
+ jc.setProgramName("java -Xmx4G -jar transitclock.jar");
+ jc.usage();
+ System.exit(0);
+ }
+ params.inferAndValidate();
+ } catch (ParameterException pex) {
+ logger.error("Parameter error: {}", pex.getMessage());
+ System.exit(1);
+ }
+ return params;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/EmbeddedJspStarter.java b/app/src/main/java/org/transitclock/api/EmbeddedJspStarter.java
new file mode 100644
index 000000000..153626936
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/EmbeddedJspStarter.java
@@ -0,0 +1,63 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.transitclock.api;
+
+import jakarta.servlet.jsp.JspFactory;
+import org.apache.jasper.runtime.JspFactoryImpl;
+import org.apache.tomcat.util.scan.StandardJarScanFilter;
+import org.apache.tomcat.util.scan.StandardJarScanner;
+import org.eclipse.jetty.ee10.apache.jsp.JettyJasperInitializer;
+import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+
+/**
+ * JspStarter for embedded ServletContextHandlers
+ *
+ * This is added as a bean that is a jetty LifeCycle on the ServletContextHandler.
+ * This bean's doStart method will be called as the ServletContextHandler starts,
+ * and will call the ServletContainerInitializer for the jsp engine.
+ */
+public class EmbeddedJspStarter extends AbstractLifeCycle {
+ private final JettyJasperInitializer sci;
+ private final ServletContextHandler context;
+
+ public EmbeddedJspStarter(ServletContextHandler context) {
+ StandardJarScanner jarScanner = new StandardJarScanner();
+ StandardJarScanFilter jarScanFilter = new StandardJarScanFilter();
+ jarScanFilter.setTldScan("taglibs-standard-impl-*");
+ jarScanFilter.setTldSkip("apache-*,ecj-*,jetty-*,asm-*,javax.servlet-*,hk2-*,javax.annotation-*,taglibs-standard-spec-*");
+ jarScanner.setJarScanFilter(jarScanFilter);
+
+ this.sci = new JettyJasperInitializer();
+ this.context = context;
+ this.context.setAttribute("org.apache.tomcat.JarScanner", jarScanner);
+ }
+
+ @Override
+ protected void doStart() throws Exception {
+ ClassLoader old = Thread.currentThread().getContextClassLoader();
+ Thread.currentThread().setContextClassLoader(context.getClassLoader());
+ try {
+ sci.onStartup(null, context.getServletContext());
+ super.doStart();
+ } finally {
+ Thread.currentThread().setContextClassLoader(old);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/transitclock/api/data/ApiActiveBlock.java b/app/src/main/java/org/transitclock/api/data/ApiActiveBlock.java
new file mode 100644
index 000000000..37c71a636
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiActiveBlock.java
@@ -0,0 +1,75 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import org.transitclock.domain.webstructs.WebAgency;
+import org.transitclock.service.dto.IpcActiveBlock;
+import org.transitclock.service.dto.IpcTrip;
+import org.transitclock.service.dto.IpcVehicle;
+import org.transitclock.utils.Time;
+
+/**
+ * @author SkiBu Smith
+ */
+public class ApiActiveBlock {
+
+ @XmlAttribute
+ private String id;
+
+ @XmlAttribute
+ private String serviceId;
+
+ @XmlAttribute
+ private String startTime;
+
+ @XmlAttribute
+ private String endTime;
+
+ @XmlElement(name = "trip")
+ private ApiTripTerse apiTripSummary;
+
+ @XmlElement(name = "vehicle")
+ private Collection vehicles;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiActiveBlock() {}
+
+ public ApiActiveBlock(IpcActiveBlock ipcActiveBlock, String agencyId)
+ throws IllegalAccessException, InvocationTargetException {
+ id = ipcActiveBlock.getBlock().getId();
+ serviceId = ipcActiveBlock.getBlock().getServiceId();
+ startTime = Time.timeOfDayStr(ipcActiveBlock.getBlock().getStartTime());
+ endTime = Time.timeOfDayStr(ipcActiveBlock.getBlock().getEndTime());
+
+ List trips = ipcActiveBlock.getBlock().getTrips();
+ IpcTrip ipcTrip = trips.get(ipcActiveBlock.getActiveTripIndex());
+ apiTripSummary = new ApiTripTerse(ipcTrip);
+
+ // Get Time object based on timezone for agency
+ WebAgency webAgency = WebAgency.getCachedWebAgency(agencyId);
+ Time timeForAgency = webAgency.getAgency().getTime();
+
+ vehicles = new ArrayList();
+ for (IpcVehicle ipcVehicles : ipcActiveBlock.getVehicles()) {
+ vehicles.add(new ApiVehicleDetails(ipcVehicles, timeForAgency));
+ }
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public ApiTripTerse getApiTripSummary() {
+ return apiTripSummary;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiActiveBlocks.java b/app/src/main/java/org/transitclock/api/data/ApiActiveBlocks.java
new file mode 100644
index 000000000..3dd61171c
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiActiveBlocks.java
@@ -0,0 +1,59 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcActiveBlock;
+
+/**
+ * Collection of ActiveBlocks
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "blocks")
+public class ApiActiveBlocks {
+
+ @XmlElement(name = "blocks")
+ private List activeBlocks;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiActiveBlocks() {}
+
+ public ApiActiveBlocks(Collection ipcActiveBlocks, String agencyId)
+ throws IllegalAccessException, InvocationTargetException {
+ activeBlocks = new ArrayList();
+ for (IpcActiveBlock ipcActiveBlock : ipcActiveBlocks) {
+ activeBlocks.add(new ApiActiveBlock(ipcActiveBlock, agencyId));
+ }
+
+ // Sort the active blocks by routeId so that can more easily display
+ // the results in order that is clear to user
+ Collections.sort(activeBlocks, comparator);
+ }
+
+ /** For sorting the active blocks by route and then block ID */
+ private static final Comparator comparator = new Comparator() {
+ @Override
+ public int compare(ApiActiveBlock o1, ApiActiveBlock o2) {
+ // Compare route IDs
+ int result = o1.getApiTripSummary()
+ .getRouteId()
+ .compareTo(o2.getApiTripSummary().getRouteId());
+ if (result != 0) return result;
+
+ // Route IDs the same so compare block IDs
+ return o1.getId().compareTo(o2.getId());
+ }
+ };
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiActiveBlocksRoute.java b/app/src/main/java/org/transitclock/api/data/ApiActiveBlocksRoute.java
new file mode 100644
index 000000000..3389750e7
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiActiveBlocksRoute.java
@@ -0,0 +1,58 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import org.transitclock.service.dto.IpcActiveBlock;
+
+/**
+ * A route for when outputting active blocks
+ *
+ * @author SkiBu Smith
+ */
+public class ApiActiveBlocksRoute {
+
+ // ID of route
+ @XmlAttribute
+ private String id;
+
+ // Route short name
+ @XmlAttribute
+ private String shortName;
+
+ // Name of route
+ @XmlAttribute
+ private String name;
+
+ // The active blocks for the route
+ @XmlElement(name = "block")
+ private List activeBlocks;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiActiveBlocksRoute() {}
+
+ public ApiActiveBlocksRoute(String id, String shortName, String name) {
+ this.id = id;
+ this.shortName = shortName;
+ this.name = name;
+
+ activeBlocks = new ArrayList();
+ }
+
+ public void add(IpcActiveBlock ipcActiveBlock, String agencyId)
+ throws IllegalAccessException, InvocationTargetException {
+ activeBlocks.add(new ApiActiveBlock(ipcActiveBlock, agencyId));
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiActiveBlocksRoutes.java b/app/src/main/java/org/transitclock/api/data/ApiActiveBlocksRoutes.java
new file mode 100644
index 000000000..5d7b02f8d
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiActiveBlocksRoutes.java
@@ -0,0 +1,60 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcActiveBlock;
+import org.transitclock.service.dto.IpcTrip;
+
+/**
+ * A list of routes for when outputting active blocks
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "routes")
+public class ApiActiveBlocksRoutes {
+
+ @XmlElement(name = "routes")
+ private List routeData;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiActiveBlocksRoutes() {}
+
+ /**
+ * Constructs an ApiRouteSummaries using a collection of IpcActiveBlock objects.
+ *
+ * @param activeBlocks Already ordered list of active blocks
+ * @param agencyId
+ * @throws InvocationTargetException
+ * @throws IllegalAccessException
+ */
+ public ApiActiveBlocksRoutes(Collection activeBlocks, String agencyId)
+ throws IllegalAccessException, InvocationTargetException {
+ routeData = new ArrayList();
+
+ ApiActiveBlocksRoute apiRoute = null;
+ for (IpcActiveBlock activeBlock : activeBlocks) {
+ IpcTrip trip = activeBlock.getBlock().getTrips().get(activeBlock.getActiveTripIndex());
+
+ // If first block for the current route then create a new
+ // ApiActiveBlocksRoute object to hold the info
+ if (apiRoute == null || !apiRoute.getName().equals(trip.getRouteName())) {
+ apiRoute = new ApiActiveBlocksRoute(trip.getRouteId(), trip.getRouteShortName(), trip.getRouteName());
+
+ routeData.add(apiRoute);
+ }
+
+ // Add the block info to the ApiActiveBlocksRoute object
+ apiRoute.add(activeBlock, agencyId);
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiAdherenceSummary.java b/app/src/main/java/org/transitclock/api/data/ApiAdherenceSummary.java
new file mode 100644
index 000000000..8d7254d9e
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiAdherenceSummary.java
@@ -0,0 +1,30 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@XmlRootElement(name = "adherenceSummary")
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ApiAdherenceSummary {
+
+ @XmlAttribute
+ private Integer late;
+
+ @XmlAttribute
+ private Integer ontime;
+
+ @XmlAttribute
+ private Integer early;
+
+ @XmlAttribute
+ private Integer nodata;
+
+ @XmlAttribute
+ private Integer blocks;
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiAgencies.java b/app/src/main/java/org/transitclock/api/data/ApiAgencies.java
new file mode 100644
index 000000000..fed23a104
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiAgencies.java
@@ -0,0 +1,30 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+/**
+ * A list of Agencies
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "agencies")
+public class ApiAgencies {
+
+ @XmlElement(name = "agency")
+ private List agenciesData;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiAgencies() {}
+
+ public ApiAgencies(List apiAgencies) {
+ agenciesData = apiAgencies;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiAgency.java b/app/src/main/java/org/transitclock/api/data/ApiAgency.java
new file mode 100644
index 000000000..c7b78e71c
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiAgency.java
@@ -0,0 +1,76 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.TimeZone;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import org.transitclock.domain.structs.Agency;
+
+/**
+ * Contains API info for an agency.
+ *
+ * @author SkiBu Smith
+ */
+public class ApiAgency {
+
+ @XmlAttribute
+ private String agencyId;
+
+ // Note that this is the GTFS agency_id, which is often different
+ // from the Transitime agencyId.
+ @XmlAttribute
+ private String id;
+
+ @XmlAttribute
+ private String name;
+
+ @XmlAttribute
+ private String url;
+
+ @XmlAttribute
+ private String timezone;
+
+ @XmlAttribute
+ private int timezoneOffsetMinutes;
+
+ @XmlAttribute
+ private String lang;
+
+ @XmlAttribute
+ private String phone;
+
+ @XmlAttribute
+ private String fareUrl;
+
+ @XmlElement
+ private ApiExtent extent;
+
+ @XmlAttribute
+ private int configRev;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiAgency() {}
+
+ public ApiAgency(String agencyId, Agency agency) {
+ this.agencyId = agencyId;
+ this.id = agency.getId();
+ this.name = agency.getName();
+ this.url = agency.getUrl();
+ this.timezone = agency.getTimeZoneStr();
+ this.lang = agency.getLang();
+ this.phone = agency.getPhone();
+ this.fareUrl = agency.getFareUrl();
+ this.extent = new ApiExtent(agency.getExtent());
+ this.configRev = agency.getConfigRev();
+
+ // Return timezone offset in minutes since that is what Javascript uses.
+ // Need to negate so it works with Javascript Date().getTimezoneOffset().
+ TimeZone timezone = TimeZone.getTimeZone(this.timezone);
+ this.timezoneOffsetMinutes = -timezone.getOffset(System.currentTimeMillis()) / (60 * 1000);
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiArrivalDeparture.java b/app/src/main/java/org/transitclock/api/data/ApiArrivalDeparture.java
new file mode 100755
index 000000000..8cfa5293b
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiArrivalDeparture.java
@@ -0,0 +1,55 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.Date;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcArrivalDeparture;
+
+@XmlRootElement(name = "arrivaldeparture")
+public class ApiArrivalDeparture {
+ @XmlAttribute
+ private String stopId;
+
+ @XmlAttribute
+ private String vehicleId;
+
+ @XmlAttribute
+ private Date time;
+
+ @XmlAttribute
+ private Date scheduledTime;
+
+ @XmlAttribute
+ private boolean isArrival;
+
+ @XmlAttribute
+ private String tripId;
+
+ @XmlAttribute
+ private String routeId;
+
+ @XmlAttribute
+ private Integer stopPathIndex;
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiArrivalDeparture() {}
+
+ public ApiArrivalDeparture(IpcArrivalDeparture ipcArrivalDeparture)
+ throws IllegalAccessException, InvocationTargetException {
+ this.vehicleId = ipcArrivalDeparture.getVehicleId();
+ this.time = ipcArrivalDeparture.getTime();
+ this.routeId = ipcArrivalDeparture.getRouteId();
+ this.tripId = ipcArrivalDeparture.getTripId();
+ this.isArrival = ipcArrivalDeparture.isArrival();
+ this.stopId = ipcArrivalDeparture.getStopId();
+ this.stopPathIndex = ipcArrivalDeparture.getStopPathIndex();
+
+ // TODO
+ // this.scheduledTime=ipcArrivalDeparture.getScheduledTime();
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiArrivalDepartures.java b/app/src/main/java/org/transitclock/api/data/ApiArrivalDepartures.java
new file mode 100755
index 000000000..1acbf9940
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiArrivalDepartures.java
@@ -0,0 +1,46 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcArrivalDeparture;
+
+/**
+ * An ordered list of routes.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "arrivalDepartures")
+public class ApiArrivalDepartures {
+
+ @XmlElement(name = "arrivalDeparture")
+ private List arrivalDeparturesData;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiArrivalDepartures() {}
+
+ /**
+ * Constructs an ApiRouteSummaries using a collection of IpcRouteSummary objects.
+ *
+ * @param routes
+ * @throws InvocationTargetException
+ * @throws IllegalAccessException
+ */
+ public ApiArrivalDepartures(Collection arrivalDepartures)
+ throws IllegalAccessException, InvocationTargetException {
+ arrivalDeparturesData = new ArrayList();
+ for (IpcArrivalDeparture arrivalDeparture : arrivalDepartures) {
+ ApiArrivalDeparture apiArrivalDeparture = new ApiArrivalDeparture(arrivalDeparture);
+ arrivalDeparturesData.add(apiArrivalDeparture);
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiBlock.java b/app/src/main/java/org/transitclock/api/data/ApiBlock.java
new file mode 100644
index 000000000..61333f8ec
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiBlock.java
@@ -0,0 +1,70 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcBlock;
+import org.transitclock.service.dto.IpcRouteSummary;
+import org.transitclock.service.dto.IpcTrip;
+import org.transitclock.utils.Time;
+
+/**
+ * Describes a block
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "block")
+public class ApiBlock {
+
+ @XmlAttribute
+ private int configRev;
+
+ @XmlAttribute
+ private String id;
+
+ @XmlAttribute
+ private String serviceId;
+
+ @XmlAttribute
+ private String startTime;
+
+ @XmlAttribute
+ private String endTime;
+
+ @XmlElement
+ private List trips;
+
+ @XmlElement(name = "routes")
+ private List routeSummaries;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiBlock() {}
+
+ public ApiBlock(IpcBlock ipcBlock) {
+ configRev = ipcBlock.getConfigRev();
+ id = ipcBlock.getId();
+ serviceId = ipcBlock.getServiceId();
+ startTime = Time.timeOfDayStr(ipcBlock.getStartTime());
+ endTime = Time.timeOfDayStr(ipcBlock.getEndTime());
+
+ trips = new ArrayList();
+ for (IpcTrip ipcTrip : ipcBlock.getTrips()) {
+ // Note: not including stop paths in trip pattern output
+ // because that can be really voluminous.
+ trips.add(new ApiTrip(ipcTrip, false));
+ }
+
+ routeSummaries = new ArrayList();
+ for (IpcRouteSummary ipcRouteSummary : ipcBlock.getRouteSummaries()) {
+ routeSummaries.add(new ApiRoute(ipcRouteSummary));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiBlockTerse.java b/app/src/main/java/org/transitclock/api/data/ApiBlockTerse.java
new file mode 100644
index 000000000..921ef863e
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiBlockTerse.java
@@ -0,0 +1,68 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcBlock;
+import org.transitclock.service.dto.IpcRouteSummary;
+import org.transitclock.service.dto.IpcTrip;
+import org.transitclock.utils.Time;
+
+/**
+ * Describes a block in terse form, without schedule and trip pattern info
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "block")
+public class ApiBlockTerse {
+
+ @XmlAttribute
+ private int configRev;
+
+ @XmlAttribute
+ private String id;
+
+ @XmlAttribute
+ private String serviceId;
+
+ @XmlAttribute
+ private String startTime;
+
+ @XmlAttribute
+ private String endTime;
+
+ @XmlElement
+ private List trips;
+
+ @XmlElement(name = "routes")
+ private List routeSummaries;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiBlockTerse() {}
+
+ public ApiBlockTerse(IpcBlock ipcBlock) {
+ configRev = ipcBlock.getConfigRev();
+ id = ipcBlock.getId();
+ serviceId = ipcBlock.getServiceId();
+ startTime = Time.timeOfDayStr(ipcBlock.getStartTime());
+ endTime = Time.timeOfDayStr(ipcBlock.getEndTime());
+
+ trips = new ArrayList();
+ for (IpcTrip ipcTrip : ipcBlock.getTrips()) {
+ trips.add(new ApiTripTerse(ipcTrip));
+ }
+
+ routeSummaries = new ArrayList();
+ for (IpcRouteSummary ipcRouteSummary : ipcBlock.getRouteSummaries()) {
+ routeSummaries.add(new ApiRoute(ipcRouteSummary));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiBlocks.java b/app/src/main/java/org/transitclock/api/data/ApiBlocks.java
new file mode 100644
index 000000000..1c7fb96eb
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiBlocks.java
@@ -0,0 +1,36 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcBlock;
+
+/**
+ * A list of blocks
+ *
+ * @author Michael Smith
+ */
+@XmlRootElement(name = "blocks")
+public class ApiBlocks {
+
+ @XmlElement(name = "block")
+ private List blocksData;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiBlocks() {}
+
+ public ApiBlocks(Collection blocks) {
+ blocksData = new ArrayList(blocks.size());
+ for (IpcBlock block : blocks) {
+ blocksData.add(new ApiBlock(block));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiBlocksTerse.java b/app/src/main/java/org/transitclock/api/data/ApiBlocksTerse.java
new file mode 100644
index 000000000..0102e7dc2
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiBlocksTerse.java
@@ -0,0 +1,35 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcBlock;
+
+/**
+ * A list of terse blocks, without trip pattern or schedule info
+ *
+ * @author Michael Smith
+ */
+@XmlRootElement(name = "blocks")
+public class ApiBlocksTerse {
+ @XmlElement(name = "block")
+ private List blocksData;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiBlocksTerse() {}
+
+ public ApiBlocksTerse(Collection blocks) {
+ blocksData = new ArrayList(blocks.size());
+ for (IpcBlock block : blocks) {
+ blocksData.add(new ApiBlockTerse(block));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiCacheDetails.java b/app/src/main/java/org/transitclock/api/data/ApiCacheDetails.java
new file mode 100755
index 000000000..0fee72dd2
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiCacheDetails.java
@@ -0,0 +1,35 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+/**
+ * For when have list of VehicleDetails. By using this class can control the element name when data
+ * is output.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement
+public class ApiCacheDetails {
+
+ public ApiCacheDetails(String name, Integer size) {
+ super();
+ this.size = size;
+ this.name = name;
+ }
+
+ @XmlElement(name = "name")
+ private String name;
+
+ @XmlElement(name = "size")
+ private Integer size;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiCacheDetails() {}
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiCalendar.java b/app/src/main/java/org/transitclock/api/data/ApiCalendar.java
new file mode 100644
index 000000000..8b12f2a4c
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiCalendar.java
@@ -0,0 +1,62 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import org.transitclock.service.dto.IpcCalendar;
+
+/**
+ * A GTFS calendar
+ *
+ * @author SkiBu Smith
+ */
+public class ApiCalendar {
+
+ @XmlAttribute
+ private String serviceId;
+
+ @XmlAttribute
+ private boolean monday;
+
+ @XmlAttribute
+ private boolean tuesday;
+
+ @XmlAttribute
+ private boolean wednesday;
+
+ @XmlAttribute
+ private boolean thursday;
+
+ @XmlAttribute
+ private boolean friday;
+
+ @XmlAttribute
+ private boolean saturday;
+
+ @XmlAttribute
+ private boolean sunday;
+
+ @XmlAttribute
+ private String startDate;
+
+ @XmlAttribute
+ private String endDate;
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiCalendar() {}
+
+ public ApiCalendar(IpcCalendar ipcCalendar) {
+ this.serviceId = ipcCalendar.getServiceId();
+ this.monday = ipcCalendar.isMonday();
+ this.tuesday = ipcCalendar.isTuesday();
+ this.wednesday = ipcCalendar.isWednesday();
+ this.thursday = ipcCalendar.isThursday();
+ this.friday = ipcCalendar.isFriday();
+ this.saturday = ipcCalendar.isSaturday();
+ this.sunday = ipcCalendar.isSunday();
+ this.startDate = ipcCalendar.getStartDate();
+ this.endDate = ipcCalendar.getEndDate();
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiCalendars.java b/app/src/main/java/org/transitclock/api/data/ApiCalendars.java
new file mode 100644
index 000000000..00b3441ed
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiCalendars.java
@@ -0,0 +1,33 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcCalendar;
+
+/**
+ * List of GTFS calendars
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "calendars")
+public class ApiCalendars {
+
+ @XmlElement
+ private List calendars;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiCalendars() {}
+
+ public ApiCalendars(List ipcCalendars) {
+ calendars = new ArrayList();
+ for (IpcCalendar ipcCalendar : ipcCalendars) calendars.add(new ApiCalendar(ipcCalendar));
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiCommandAck.java b/app/src/main/java/org/transitclock/api/data/ApiCommandAck.java
new file mode 100644
index 000000000..3e26ae617
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiCommandAck.java
@@ -0,0 +1,27 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+@XmlRootElement(name = "acknowledgment")
+public class ApiCommandAck {
+ @XmlAttribute
+ private boolean success;
+
+ @XmlAttribute
+ private String message;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ public ApiCommandAck() {}
+
+ public ApiCommandAck(boolean success, String message) {
+ this.success = success;
+ this.message = message;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiCurrentServerDate.java b/app/src/main/java/org/transitclock/api/data/ApiCurrentServerDate.java
new file mode 100644
index 000000000..50fcaf069
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiCurrentServerDate.java
@@ -0,0 +1,22 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.Date;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+@XmlRootElement(name = "servertime")
+public class ApiCurrentServerDate {
+ @XmlAttribute
+ private Date currentTime;
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiCurrentServerDate() {}
+
+ public ApiCurrentServerDate(Date currentTime) {
+ this.currentTime = currentTime;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiDirection.java b/app/src/main/java/org/transitclock/api/data/ApiDirection.java
new file mode 100644
index 000000000..a1ed6fd22
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiDirection.java
@@ -0,0 +1,49 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import org.transitclock.service.dto.IpcDirection;
+import org.transitclock.service.dto.IpcStop;
+
+/**
+ * A single direction, containing stops
+ *
+ * @author SkiBu Smith
+ */
+public class ApiDirection {
+
+ @XmlAttribute
+ private String id;
+
+ @XmlAttribute
+ private String title;
+
+ @XmlElement(name = "stop")
+ private List stops;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiDirection() {}
+
+ /**
+ * Constructs a ApiDirection using an IpcDirection
+ *
+ * @param direction
+ */
+ public ApiDirection(IpcDirection direction) {
+ this.id = direction.getDirectionId();
+ this.title = direction.getDirectionTitle();
+
+ this.stops = new ArrayList(direction.getStops().size());
+ for (IpcStop stop : direction.getStops()) {
+ this.stops.add(new ApiStop(stop));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiDirections.java b/app/src/main/java/org/transitclock/api/data/ApiDirections.java
new file mode 100644
index 000000000..1ac3fe3db
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiDirections.java
@@ -0,0 +1,38 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcDirection;
+import org.transitclock.service.dto.IpcDirectionsForRoute;
+
+/**
+ * A list of directions.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "directions")
+public class ApiDirections {
+
+ @XmlElement(name = "direction")
+ private List directionsData;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiDirections() {}
+
+ public ApiDirections(IpcDirectionsForRoute stopsForRoute) {
+ Collection directions = stopsForRoute.getDirections();
+ directionsData = new ArrayList(directions.size());
+ for (IpcDirection direction : directions) {
+ directionsData.add(new ApiDirection(direction));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiExportData.java b/app/src/main/java/org/transitclock/api/data/ApiExportData.java
new file mode 100644
index 000000000..77e861ebf
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiExportData.java
@@ -0,0 +1,55 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.Date;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.domain.structs.ExportTable;
+
+/**
+ * For storing static configuration for vehicle in block.
+ *
+ * @author Hubert GoEuropa
+ */
+@XmlRootElement
+public class ApiExportData {
+
+ @XmlAttribute
+ private long id;
+
+ @XmlAttribute
+ private Date dataDate;
+
+ @XmlAttribute
+ private Date exportDate;
+
+ @XmlAttribute
+ private int exportType;
+
+ @XmlAttribute
+ private int exportStatus;
+
+ @XmlAttribute
+ private String fileName;
+
+ public ApiExportData() {}
+
+ public ApiExportData(ExportTable exportTable) {
+ this.id = exportTable.getId();
+ this.exportType = exportTable.getExportType();
+ this.exportStatus = exportTable.getExportStatus();
+ this.fileName = exportTable.getFileName();
+ this.dataDate = exportTable.getDataDate();
+ this.exportDate = exportTable.getExportDate();
+ }
+
+ public ApiExportData(
+ Long id, Date dataDate, Date exportDate, Integer exportType, Integer exportStatus, String fileName) {
+ this.id = id;
+ this.exportType = exportType;
+ this.fileName = fileName;
+ this.dataDate = dataDate;
+ this.exportDate = exportDate;
+ this.exportStatus = exportStatus;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiExportsData.java b/app/src/main/java/org/transitclock/api/data/ApiExportsData.java
new file mode 100644
index 000000000..490ec82f9
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiExportsData.java
@@ -0,0 +1,54 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.domain.structs.ExportTable;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * For when have list of exports. By using this class can control the element name when data is
+ * output.
+ *
+ * @author Hubert goEuropa
+ */
+@XmlRootElement
+public class ApiExportsData implements Serializable {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = -9106451522038837974L;
+
+ @XmlElement(name = "exports")
+ private List apiExportData;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ public ApiExportsData() {
+ }
+
+ /**
+ *
+ */
+ public ApiExportsData(List exportData) {
+ apiExportData = exportData.stream()
+ .map(ApiExportData::new)
+ .collect(Collectors.toList());
+ }
+
+ public List getExportsData() {
+ return apiExportData;
+ }
+
+ public void setExportsData(List exportsData) {
+ this.apiExportData = exportsData;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiExtent.java b/app/src/main/java/org/transitclock/api/data/ApiExtent.java
new file mode 100644
index 000000000..84af3d070
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiExtent.java
@@ -0,0 +1,41 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import org.transitclock.domain.structs.Extent;
+import org.transitclock.utils.MathUtils;
+
+/**
+ * Describes the extent of a route or agency via a min & max lat & lon.
+ *
+ * @author SkiBu Smith
+ */
+public class ApiExtent {
+
+ @XmlAttribute
+ private double minLat;
+
+ @XmlAttribute
+ private double minLon;
+
+ @XmlAttribute
+ private double maxLat;
+
+ @XmlAttribute
+ private double maxLon;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiExtent() {}
+
+ public ApiExtent(Extent extent) {
+ this.minLat = MathUtils.round(extent.getMinLat(), 5);
+ this.minLon = MathUtils.round(extent.getMinLon(), 5);
+ this.maxLat = MathUtils.round(extent.getMaxLat(), 5);
+ this.maxLon = MathUtils.round(extent.getMaxLon(), 5);
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiGpsLocation.java b/app/src/main/java/org/transitclock/api/data/ApiGpsLocation.java
new file mode 100644
index 000000000..d4e3c30cb
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiGpsLocation.java
@@ -0,0 +1,51 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlType;
+import org.transitclock.service.dto.IpcVehicle;
+import org.transitclock.utils.MathUtils;
+import org.transitclock.utils.Time;
+
+/**
+ * Extends a location by including GPS information including time, speed, heading, and pathHeading.
+ *
+ * @author SkiBu Smith
+ */
+@XmlType(propOrder = {"lat", "lon", "time", "speed", "heading"})
+public class ApiGpsLocation extends ApiTransientLocation {
+
+ // Epoch time in seconds (not msec, so that shorter)
+ @XmlAttribute
+ private long time;
+
+ // Speed in m/s. A Double so if null then won't show up in output
+ @XmlAttribute
+ private Double speed;
+
+ // A Double so if null then won't show up in output
+ @XmlAttribute
+ private Double heading;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiGpsLocation() {}
+
+ /**
+ * @param lat
+ * @param lon
+ */
+ public ApiGpsLocation(IpcVehicle vehicle) {
+ super(vehicle.getLatitude(), vehicle.getLongitude());
+
+ this.time = vehicle.getGpsTime() / Time.MS_PER_SEC;
+ // Output only 1 digit past decimal point
+ this.speed = Float.isNaN(vehicle.getSpeed()) ? null : MathUtils.round(vehicle.getSpeed(), 1);
+ // Output only 1 digit past decimal point
+ this.heading = Float.isNaN(vehicle.getHeading()) ? null : MathUtils.round(vehicle.getHeading(), 1);
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiHistoricalAverage.java b/app/src/main/java/org/transitclock/api/data/ApiHistoricalAverage.java
new file mode 100755
index 000000000..02f8c5209
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiHistoricalAverage.java
@@ -0,0 +1,34 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcHistoricalAverage;
+
+/**
+ * Describes an historical average
+ *
+ * @author Sean Og Crudden
+ */
+@XmlRootElement(name = "HistoricalAverage")
+public class ApiHistoricalAverage {
+
+ @XmlAttribute
+ private Integer count;
+
+ @XmlAttribute
+ private Double average;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiHistoricalAverage() {}
+
+ public ApiHistoricalAverage(IpcHistoricalAverage ipcHistoricalAverage) {
+ this.count = ipcHistoricalAverage.getCount();
+ this.average = ipcHistoricalAverage.getAverage();
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiHistoricalAverageCacheKey.java b/app/src/main/java/org/transitclock/api/data/ApiHistoricalAverageCacheKey.java
new file mode 100755
index 000000000..ad9e483b4
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiHistoricalAverageCacheKey.java
@@ -0,0 +1,29 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcHistoricalAverageCacheKey;
+
+/**
+ * Describes an historical average key which is used to refer to data elements in the cache
+ *
+ * @author Sean Og Crudden
+ */
+@XmlRootElement(name = "HistoricalAverageCacheKey")
+public class ApiHistoricalAverageCacheKey {
+
+ @XmlAttribute
+ private String tripId;
+
+ @XmlAttribute
+ private Integer stopPathIndex;
+
+ public ApiHistoricalAverageCacheKey() {}
+
+ public ApiHistoricalAverageCacheKey(IpcHistoricalAverageCacheKey key) {
+
+ this.tripId = key.getTripId();
+ this.stopPathIndex = key.getStopPathIndex();
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiHistoricalAverageCacheKeys.java b/app/src/main/java/org/transitclock/api/data/ApiHistoricalAverageCacheKeys.java
new file mode 100755
index 000000000..b95f950ea
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiHistoricalAverageCacheKeys.java
@@ -0,0 +1,32 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcHistoricalAverageCacheKey;
+
+/**
+ * @author Sean Og Crudden
+ */
+@XmlRootElement(name = "HistoricalAverageCacheKeys")
+public class ApiHistoricalAverageCacheKeys {
+
+ @XmlElement(name = "HistoricalAverageCacheKey")
+ private List apiHistoricalAverageCacheKeys;
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiHistoricalAverageCacheKeys() {}
+
+ public ApiHistoricalAverageCacheKeys(Collection cacheKeys) {
+ apiHistoricalAverageCacheKeys = new ArrayList();
+ for (IpcHistoricalAverageCacheKey key : cacheKeys) {
+ apiHistoricalAverageCacheKeys.add(new ApiHistoricalAverageCacheKey(key));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiHoldingTime.java b/app/src/main/java/org/transitclock/api/data/ApiHoldingTime.java
new file mode 100755
index 000000000..3f3e6e5e0
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiHoldingTime.java
@@ -0,0 +1,71 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.Date;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcHoldingTime;
+
+@XmlRootElement(name = "holdingtime")
+public class ApiHoldingTime {
+
+ @XmlAttribute
+ private Date holdingTime;
+
+ @XmlAttribute
+ private Date creationTime;
+
+ @XmlAttribute
+ private Date currentTime;
+
+ @XmlAttribute
+ private String vehicleId;
+
+ @XmlAttribute
+ private String stopId;
+
+ @XmlAttribute
+ private String tripId;
+
+ @XmlAttribute
+ private String routeId;
+
+ @XmlAttribute
+ private boolean arrivalPredictionUsed;
+
+ @XmlAttribute
+ private boolean arrivalUsed;
+
+ @XmlAttribute
+ private Date arrivalTime;
+
+ @XmlAttribute
+ private boolean hasN1;
+
+ @XmlAttribute
+ private boolean hasN2;
+
+ @XmlAttribute
+ private boolean hasD1;
+
+ @XmlAttribute
+ private int numberPredictionsUsed;
+
+ protected ApiHoldingTime() {}
+
+ public ApiHoldingTime(IpcHoldingTime ipcHoldingTime) throws IllegalAccessException, InvocationTargetException {
+ this.holdingTime = ipcHoldingTime.getHoldingTime();
+ this.creationTime = ipcHoldingTime.getCreationTime();
+ this.vehicleId = ipcHoldingTime.getVehicleId();
+ this.tripId = ipcHoldingTime.getTripId();
+ this.routeId = ipcHoldingTime.getRouteId();
+ this.stopId = ipcHoldingTime.getStopId();
+ this.arrivalPredictionUsed = ipcHoldingTime.isArrivalPredictionUsed();
+ this.arrivalUsed = ipcHoldingTime.isArrivalUsed();
+ this.currentTime = ipcHoldingTime.getCurrentTime();
+ this.arrivalTime = ipcHoldingTime.getArrivalTime();
+ this.hasD1 = ipcHoldingTime.isHasD1();
+ this.numberPredictionsUsed = ipcHoldingTime.getNumberPredictionsUsed();
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiHoldingTimeCacheKey.java b/app/src/main/java/org/transitclock/api/data/ApiHoldingTimeCacheKey.java
new file mode 100755
index 000000000..f27ab337a
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiHoldingTimeCacheKey.java
@@ -0,0 +1,28 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcHoldingTimeCacheKey;
+
+@XmlRootElement(name = "HoldingTimeCacheKey")
+public class ApiHoldingTimeCacheKey {
+
+ @XmlAttribute
+ private String stopid;
+
+ @XmlAttribute
+ private String vehicleId;
+
+ @XmlAttribute
+ private String tripId;
+
+ public ApiHoldingTimeCacheKey(IpcHoldingTimeCacheKey key) {
+
+ this.stopid = key.getStopid();
+ this.vehicleId = key.getVehicleId();
+ this.tripId = key.getTripId();
+ }
+
+ protected ApiHoldingTimeCacheKey() {}
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiHoldingTimeCacheKeys.java b/app/src/main/java/org/transitclock/api/data/ApiHoldingTimeCacheKeys.java
new file mode 100755
index 000000000..cea60fa34
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiHoldingTimeCacheKeys.java
@@ -0,0 +1,32 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcHoldingTimeCacheKey;
+
+/**
+ * @author Sean Óg Crudden
+ */
+@XmlRootElement(name = "HoldingTimeCacheKeys")
+public class ApiHoldingTimeCacheKeys {
+
+ @XmlElement(name = "HoldingTimeCacheKey")
+ private List apiHoldingTimeCacheKeys;
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiHoldingTimeCacheKeys() {}
+
+ public ApiHoldingTimeCacheKeys(Collection cacheKeys) {
+ apiHoldingTimeCacheKeys = new ArrayList();
+ for (IpcHoldingTimeCacheKey key : cacheKeys) {
+ apiHoldingTimeCacheKeys.add(new ApiHoldingTimeCacheKey(key));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiIds.java b/app/src/main/java/org/transitclock/api/data/ApiIds.java
new file mode 100644
index 000000000..a419546fb
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiIds.java
@@ -0,0 +1,37 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.utils.StringUtils;
+
+/**
+ * For outputting simple list of sorted alpha-number IDs
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement
+public class ApiIds {
+
+ @XmlElement
+ private List ids;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiIds() {}
+
+ /**
+ * Creates the API sorted version of list of IDs.
+ *
+ * @param ids
+ */
+ public ApiIds(List ids) {
+ StringUtils.sortIdsNumerically(ids);
+ this.ids = ids;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiKalmanErrorCacheKey.java b/app/src/main/java/org/transitclock/api/data/ApiKalmanErrorCacheKey.java
new file mode 100755
index 000000000..4750e42a5
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiKalmanErrorCacheKey.java
@@ -0,0 +1,29 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcKalmanErrorCacheKey;
+
+/**
+ * Describes an kalman error key which is used to refer to data elements in the kalman error cache
+ *
+ * @author Sean Og Crudden
+ */
+@XmlRootElement(name = "KalmanErrorCacheKey")
+public class ApiKalmanErrorCacheKey {
+
+ @XmlAttribute
+ private String tripId;
+
+ @XmlAttribute
+ private Integer stopPathIndex;
+
+ public ApiKalmanErrorCacheKey() {}
+
+ public ApiKalmanErrorCacheKey(IpcKalmanErrorCacheKey key) {
+
+ this.tripId = key.getTripId();
+ this.stopPathIndex = key.getStopPathIndex();
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiKalmanErrorCacheKeys.java b/app/src/main/java/org/transitclock/api/data/ApiKalmanErrorCacheKeys.java
new file mode 100755
index 000000000..e946211f2
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiKalmanErrorCacheKeys.java
@@ -0,0 +1,32 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcKalmanErrorCacheKey;
+
+/**
+ * @author Sean Og Crudden
+ */
+@XmlRootElement(name = "KalmanErrorCacheKeys")
+public class ApiKalmanErrorCacheKeys {
+
+ @XmlElement(name = "KalmanErrorCacheKey")
+ private List apiKalmanErrorCacheKeys;
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiKalmanErrorCacheKeys() {}
+
+ public ApiKalmanErrorCacheKeys(Collection cacheKeys) {
+ apiKalmanErrorCacheKeys = new ArrayList();
+ for (IpcKalmanErrorCacheKey key : cacheKeys) {
+ apiKalmanErrorCacheKeys.add(new ApiKalmanErrorCacheKey(key));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiLocation.java b/app/src/main/java/org/transitclock/api/data/ApiLocation.java
new file mode 100644
index 000000000..11f8fc87f
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiLocation.java
@@ -0,0 +1,32 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import org.transitclock.domain.structs.Location;
+
+/**
+ * A simple latitude/longitude.
+ *
+ *
This is a non-transient implementation of ApiTransientLocation. By not being transient this
+ * class can be used to output a location as an element (as opposed to an attribute). By inheriting
+ * from ApiTransientLocation don't need to duplicate any code.
+ *
+ * @author SkiBu Smith
+ */
+public class ApiLocation extends ApiTransientLocation {
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiLocation() {}
+
+ public ApiLocation(double lat, double lon) {
+ super(lat, lon);
+ }
+
+ public ApiLocation(Location loc) {
+ super(loc.getLat(), loc.getLon());
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiNearbyPredictionsForAgencies.java b/app/src/main/java/org/transitclock/api/data/ApiNearbyPredictionsForAgencies.java
new file mode 100644
index 000000000..7ccd426c8
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiNearbyPredictionsForAgencies.java
@@ -0,0 +1,34 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+/**
+ * Contains predictions for multiple stops. Has information for agency as well since intended to be
+ * used when providing predictions by location for multiple agencies.
+ *
+ * @author Michael
+ */
+@XmlRootElement(name = "preds")
+public class ApiNearbyPredictionsForAgencies {
+
+ @XmlElement(name = "agencies")
+ private List predictionsForAgency;
+
+ /** Constructor. Method addPredictionsForAgency() called to actually add data. */
+ public ApiNearbyPredictionsForAgencies() {
+ predictionsForAgency = new ArrayList();
+ }
+
+ /**
+ * Adds predictions for an agency.
+ *
+ * @param apiPreds
+ */
+ public void addPredictionsForAgency(ApiPredictions apiPreds) {
+ predictionsForAgency.add(apiPreds);
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiPrediction.java b/app/src/main/java/org/transitclock/api/data/ApiPrediction.java
new file mode 100644
index 000000000..f86a7f68f
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiPrediction.java
@@ -0,0 +1,125 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcPrediction;
+import org.transitclock.utils.Time;
+
+/**
+ * Contains data for a single prediction.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement
+public class ApiPrediction {
+
+ // Epoch time in seconds (not msec, so shorter)
+ @XmlAttribute(name = "time")
+ private long time;
+
+ @XmlAttribute(name = "sec")
+ private int seconds;
+
+ @XmlAttribute(name = "min")
+ private int minutes;
+
+ @XmlAttribute(name = "scheduleBased")
+ protected Boolean schedBasedPreds;
+
+ // Most of the time will be predicting arrival predictions. Therefore
+ // isDeparture will only be displayed for the more rare times that
+ // departure prediction is being provided.
+ @XmlAttribute(name = "departure")
+ private Boolean isDeparture;
+
+ @XmlAttribute(name = "trip")
+ private String tripId;
+
+ @XmlAttribute(name = "blockId")
+ private String blockId;
+
+ @XmlAttribute(name = "tripPattern")
+ private String tripPatternId;
+
+ @XmlAttribute(name = "vehicle")
+ private String vehicleId;
+
+ // Only output if true
+ @XmlAttribute(name = "atEndOfTrip")
+ private Boolean isAtEndOfTrip;
+
+ // Only output if true
+ @XmlAttribute(name = "delayed")
+ private Boolean isDelayed;
+
+ // Only output if true
+ @XmlAttribute(name = "lateAndSubsequentTripSoMarkAsUncertain")
+ private Boolean isLateAndSubsequentTripSoMarkAsUncertain;
+
+ // Only output if true
+ @XmlAttribute(name = "notYetDeparted")
+ private Boolean basedOnScheduledDeparture;
+
+ // Only output if passenger count is valid
+ @XmlAttribute(name = "passengerCount")
+ private String passengerCount;
+
+ @XmlAttribute(name = "isDeparture")
+ private String isDepartureDuplicate; // same field different name
+
+ @XmlAttribute(name = "affectedByLayover")
+ private String affectedByLayover;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ public ApiPrediction() {}
+
+ public ApiPrediction(IpcPrediction prediction) {
+ time = prediction.getPredictionTime() / Time.MS_PER_SEC;
+ seconds = (int) (prediction.getPredictionTime() - System.currentTimeMillis()) / Time.MS_PER_SEC;
+ // Always round down minutes to be conservative and so that user
+ // doesn't miss bus.
+ minutes = seconds / 60;
+
+ // Only set schedBasedPreds if it is schedule based so that the
+ // attribute is not output for the majority of the times that it
+ // is not schedule based.
+ schedBasedPreds = prediction.isSchedBasedPred() ? true : null;
+
+ if (!prediction.isArrival()) isDeparture = true;
+
+ tripId = prediction.getTripId();
+ tripPatternId = prediction.getTripPatternId();
+
+ vehicleId = prediction.getVehicleId();
+
+ if (prediction.isAtEndOfTrip()) isAtEndOfTrip = true;
+
+ // Only set basedOnScheduledDeparture if true so that it is not output
+ // if false since it will then be null
+ if (prediction.isAffectedByWaitStop()) basedOnScheduledDeparture = true;
+
+ // Only set passengerCount if it is valid so that it is not output if it
+ // is not valid since will then be null
+ if (prediction.isPassengerCountValid()) passengerCount = String.valueOf(prediction.getPassengerCount());
+
+ // Only set if true so only output for rare case
+ if (prediction.isDelayed()) isDelayed = true;
+
+ // Only set if true so only output for rare case
+ if (prediction.isLateAndSubsequentTripSoMarkAsUncertain())
+ isLateAndSubsequentTripSoMarkAsUncertain = Boolean.TRUE;
+
+ affectedByLayover = Boolean.toString(prediction.isAffectedByWaitStop());
+
+ isDepartureDuplicate = Boolean.toString(!prediction.isArrival());
+
+ blockId = prediction.getBlockId();
+ isLateAndSubsequentTripSoMarkAsUncertain = true;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiPredictionDestination.java b/app/src/main/java/org/transitclock/api/data/ApiPredictionDestination.java
new file mode 100644
index 000000000..5824b4aa1
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiPredictionDestination.java
@@ -0,0 +1,46 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcPrediction;
+import org.transitclock.service.dto.IpcPredictionsForRouteStopDest;
+
+/**
+ * Contains list of predictions for a particular headsign.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement
+public class ApiPredictionDestination {
+
+ @XmlAttribute(name = "dir")
+ private String directionId;
+
+ @XmlAttribute
+ private String headsign;
+
+ @XmlElement(name = "pred")
+ private List predictions;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiPredictionDestination() {}
+
+ public ApiPredictionDestination(IpcPredictionsForRouteStopDest predictionsForRouteStop) {
+ directionId = predictionsForRouteStop.getDirectionId();
+ headsign = predictionsForRouteStop.getHeadsign();
+
+ predictions = new ArrayList();
+ for (IpcPrediction prediction : predictionsForRouteStop.getPredictionsForRouteStop()) {
+ predictions.add(new ApiPrediction(prediction));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiPredictionForStopPath.java b/app/src/main/java/org/transitclock/api/data/ApiPredictionForStopPath.java
new file mode 100755
index 000000000..1fd7a14bc
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiPredictionForStopPath.java
@@ -0,0 +1,41 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.Date;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcPredictionForStopPath;
+
+@XmlRootElement(name = "traveltimepredictionforstoppath")
+public class ApiPredictionForStopPath {
+
+ @XmlAttribute
+ private String tripId;
+
+ @XmlAttribute
+ private Integer stopPathIndex;
+
+ @XmlAttribute
+ private Date creationTime;
+
+ @XmlAttribute
+ private Double predictionTime;
+
+ @XmlAttribute
+ private String algorithm;
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiPredictionForStopPath() {}
+
+ public ApiPredictionForStopPath(IpcPredictionForStopPath prediction) {
+ super();
+ this.tripId = prediction.getTripId();
+ this.stopPathIndex = prediction.getStopPathIndex();
+ this.creationTime = prediction.getCreationTime();
+ this.predictionTime = prediction.getPredictionTime();
+ this.algorithm = prediction.getAlgorithm();
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiPredictionRouteStop.java b/app/src/main/java/org/transitclock/api/data/ApiPredictionRouteStop.java
new file mode 100644
index 000000000..d63bbec73
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiPredictionRouteStop.java
@@ -0,0 +1,75 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcPredictionsForRouteStopDest;
+import org.transitclock.utils.MathUtils;
+
+/**
+ * List of ApiPredictionDestination objects along with supporting information. Used to output
+ * predictions for a particular stop where the predictions are grouped by headsign.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement
+public class ApiPredictionRouteStop {
+
+ @XmlAttribute
+ private String routeShortName;
+
+ @XmlAttribute
+ private String routeName;
+
+ @XmlAttribute
+ private String routeId;
+
+ @XmlAttribute
+ private String stopId;
+
+ @XmlAttribute
+ private String stopName;
+
+ @XmlAttribute
+ private Integer stopCode;
+
+ // Using String so that it will not be output if not showing predictions
+ // by location because then this value will be null. Also, can this way
+ // format it to desired number of digits of precision.
+ @XmlAttribute
+ private Double distanceToStop;
+
+ @XmlElement(name = "dest")
+ private List destinations;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiPredictionRouteStop() {}
+
+ public ApiPredictionRouteStop(List predictionsForRouteStop) {
+ if (predictionsForRouteStop == null || predictionsForRouteStop.isEmpty()) return;
+
+ IpcPredictionsForRouteStopDest routeStopInfo = predictionsForRouteStop.get(0);
+ routeShortName = routeStopInfo.getRouteShortName();
+ routeName = routeStopInfo.getRouteName();
+ routeId = routeStopInfo.getRouteId();
+ stopId = routeStopInfo.getStopId();
+ stopName = routeStopInfo.getStopName();
+ stopCode = routeStopInfo.getStopCode();
+ distanceToStop = Double.isNaN(routeStopInfo.getDistanceToStop())
+ ? null
+ : MathUtils.round(routeStopInfo.getDistanceToStop(), 1);
+
+ destinations = new ArrayList();
+ for (IpcPredictionsForRouteStopDest destinationInfo : predictionsForRouteStop) {
+ destinations.add(new ApiPredictionDestination(destinationInfo));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiPredictions.java b/app/src/main/java/org/transitclock/api/data/ApiPredictions.java
new file mode 100644
index 000000000..cea6d2590
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiPredictions.java
@@ -0,0 +1,88 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcPredictionsForRouteStopDest;
+
+/**
+ * Contains predictions for multiple routes/stops. Can also contain info for the agency.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "preds")
+public class ApiPredictions {
+
+ // Optional additional info. Needed for when providing predictions for
+ // multiple agencies, such as when getting predictions by location
+ @XmlAttribute
+ private String agencyId = null;
+
+ // Optional additional info. Needed for when providing predictions for
+ // multiple agencies, such as when getting predictions by location
+ @XmlAttribute
+ private String agencyName = null;
+
+ // The actual predictions, by route & stop
+ @XmlElement(name = "predictions")
+ private List predictionsForRouteStop;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ public ApiPredictions() {}
+
+ /**
+ * For constructing a ApiPredictions object from a List of IpcPredictionsForRouteStopDest
+ * objects.
+ *
+ * @param vehicles
+ */
+ public ApiPredictions(List predsForRouteStopDestinations) {
+ predictionsForRouteStop = new ArrayList();
+
+ // Get all the PredictionsForRouteStopDest that are for the same
+ // route/stop and create a PredictionsRouteStopData object for each
+ // route/stop.
+ List predsForRouteStop = null;
+ String previousRouteStopStr = "";
+ for (IpcPredictionsForRouteStopDest predsForRouteStopDest : predsForRouteStopDestinations) {
+ // If this is a new route/stop...
+ String currentRouteStopStr = predsForRouteStopDest.getRouteId() + predsForRouteStopDest.getStopId();
+ if (!currentRouteStopStr.equals(previousRouteStopStr)) {
+ // This is a new route/stop
+ if (predsForRouteStop != null && !predsForRouteStop.isEmpty()) {
+ // create PredictionsRouteStopData object for this
+ // route/stop
+ ApiPredictionRouteStop predictionsForRouteStopData = new ApiPredictionRouteStop(predsForRouteStop);
+ predictionsForRouteStop.add(predictionsForRouteStopData);
+ }
+ predsForRouteStop = new ArrayList();
+ previousRouteStopStr = currentRouteStopStr;
+ }
+ predsForRouteStop.add(predsForRouteStopDest);
+ }
+
+ // Add the last set of route/stop data
+ ApiPredictionRouteStop predictionsForRouteStopData = new ApiPredictionRouteStop(predsForRouteStop);
+ predictionsForRouteStop.add(predictionsForRouteStopData);
+ }
+
+ /**
+ * For setting info about the agency. Needed for when providing predictions for multiple
+ * agencies, such as when getting predictions by location
+ *
+ * @param agencyId
+ * @param agencyName
+ */
+ public void set(String agencyId, String agencyName) {
+ this.agencyId = agencyId;
+ this.agencyName = agencyName;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiPredictionsForStopPath.java b/app/src/main/java/org/transitclock/api/data/ApiPredictionsForStopPath.java
new file mode 100755
index 000000000..0b5b8bfad
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiPredictionsForStopPath.java
@@ -0,0 +1,46 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcPredictionForStopPath;
+
+/**
+ * An ordered list of routes.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "predictions")
+public class ApiPredictionsForStopPath {
+
+ @XmlElement(name = "prediction")
+ private List predictionsForStopPath;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiPredictionsForStopPath() {}
+
+ /**
+ * Constructs an ApiRouteSummaries using a collection of IpcRouteSummary objects.
+ *
+ * @param routes
+ * @throws InvocationTargetException
+ * @throws IllegalAccessException
+ */
+ public ApiPredictionsForStopPath(Collection predictions)
+ throws IllegalAccessException, InvocationTargetException {
+ predictionsForStopPath = new ArrayList();
+ for (IpcPredictionForStopPath prediction : predictions) {
+ ApiPredictionForStopPath apiPredictionForStopPath = new ApiPredictionForStopPath(prediction);
+ predictionsForStopPath.add(apiPredictionForStopPath);
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiRmiServerStatus.java b/app/src/main/java/org/transitclock/api/data/ApiRmiServerStatus.java
new file mode 100644
index 000000000..cb3ee657a
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiRmiServerStatus.java
@@ -0,0 +1,33 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+/**
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "rmiServerStatus")
+public class ApiRmiServerStatus {
+
+ @XmlElement(name = "agency")
+ private List agenciesData;
+
+ /** Sub API class that actually contains all data for each agency */
+ private static class ApiAgencyRmiServerStatus {
+ }
+
+ /**
+ * Constructors a ApiRmiServerStatus object
+ *
+ * @param agencyId
+ * @param ipcServerStatus
+ */
+ public ApiRmiServerStatus() {
+ agenciesData = new ArrayList<>();
+
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiRoute.java b/app/src/main/java/org/transitclock/api/data/ApiRoute.java
new file mode 100644
index 000000000..7963f78c2
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiRoute.java
@@ -0,0 +1,44 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import org.transitclock.service.dto.IpcRouteSummary;
+
+/**
+ * A short description of a route. For when outputting list of routes for agency.
+ *
+ * @author SkiBu Smith
+ */
+public class ApiRoute {
+
+ @XmlAttribute
+ private String id;
+
+ @XmlAttribute
+ private String name;
+
+ @XmlAttribute
+ private String shortName;
+
+ @XmlAttribute
+ private String longName;
+
+ @XmlAttribute
+ private String type;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiRoute() {}
+
+ public ApiRoute(IpcRouteSummary route) {
+ this.id = route.getId();
+ this.name = route.getName();
+ this.shortName = route.getShortName();
+ this.longName = route.getLongName();
+ this.type = route.getType();
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiRouteDetails.java b/app/src/main/java/org/transitclock/api/data/ApiRouteDetails.java
new file mode 100644
index 000000000..bbdf0071c
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiRouteDetails.java
@@ -0,0 +1,90 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.domain.structs.Location;
+import org.transitclock.service.dto.IpcDirection;
+import org.transitclock.service.dto.IpcDirectionsForRoute;
+import org.transitclock.service.dto.IpcRoute;
+import org.transitclock.service.dto.IpcShape;
+
+/**
+ * Provides detailed information for a route include stops and shape info.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "route")
+public class ApiRouteDetails {
+
+ @XmlAttribute
+ private String id;
+
+ @XmlAttribute
+ private String name;
+
+ @XmlAttribute
+ private String shortName;
+
+ @XmlAttribute
+ private String longName;
+
+ @XmlAttribute
+ private String color;
+
+ @XmlAttribute
+ private String textColor;
+
+ @XmlAttribute
+ private String type;
+
+ @XmlElement(name = "direction")
+ private List directions;
+
+ @XmlElement(name = "shape")
+ private List shapes;
+
+ @XmlElement
+ private ApiExtent extent;
+
+ @XmlElement
+ private ApiLocation locationOfNextPredictedVehicle;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiRouteDetails() {}
+
+ public ApiRouteDetails(IpcRoute ipcRoute) {
+ this.id = ipcRoute.getId();
+ this.name = ipcRoute.getName();
+ this.shortName = ipcRoute.getShortName();
+ this.longName = ipcRoute.getLongName();
+ this.color = ipcRoute.getColor();
+ this.textColor = ipcRoute.getTextColor();
+ this.type = ipcRoute.getType();
+
+ IpcDirectionsForRoute stops = ipcRoute.getStops();
+ this.directions = new ArrayList();
+ for (IpcDirection ipcDirection : stops.getDirections()) {
+ this.directions.add(new ApiDirection(ipcDirection));
+ }
+
+ this.shapes = new ArrayList();
+ for (IpcShape shape : ipcRoute.getShapes()) {
+ this.shapes.add(new ApiShape(shape));
+ }
+
+ this.extent = new ApiExtent(ipcRoute.getExtent());
+
+ Location vehicleLoc = ipcRoute.getLocationOfNextPredictedVehicle();
+ if (vehicleLoc == null) this.locationOfNextPredictedVehicle = null;
+ else this.locationOfNextPredictedVehicle = new ApiLocation(vehicleLoc);
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiRoutes.java b/app/src/main/java/org/transitclock/api/data/ApiRoutes.java
new file mode 100644
index 000000000..60c18a3f3
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiRoutes.java
@@ -0,0 +1,70 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.domain.structs.Agency;
+import org.transitclock.service.dto.IpcRoute;
+import org.transitclock.service.dto.IpcRouteSummary;
+
+/**
+ * An ordered list of routes.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement
+public class ApiRoutes {
+ // So can easily get agency name when getting routes. Useful for db reports
+ // and such.
+ @XmlElement(name = "agency")
+ private String agencyName;
+
+ // List of route info
+ @XmlElement(name = "routes")
+ private List routesData;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiRoutes() {}
+
+ /**
+ * Constructs an ApiRouteSummaries using a collection of IpcRouteSummary objects.
+ *
+ * @param routes
+ * @param agency so can get agency name
+ */
+ public ApiRoutes(Collection routes, Agency agency) {
+ routesData = new ArrayList();
+ for (IpcRouteSummary route : routes) {
+ ApiRoute routeSummary = new ApiRoute(route);
+ routesData.add(routeSummary);
+ }
+
+ // Also set agency name
+ agencyName = agency.getName();
+ }
+
+ /**
+ * Constructs an ApiRouteSummaries using a collection of IpcRoute objects.
+ *
+ * @param routes
+ * @param agency so can get agency name
+ */
+ public ApiRoutes(List routes, Agency agency) {
+ routesData = new ArrayList();
+ for (IpcRouteSummary route : routes) {
+ ApiRoute routeSummary = new ApiRoute(route);
+ routesData.add(routeSummary);
+ }
+
+ // Also set agency name
+ agencyName = agency.getName();
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiRoutesDetails.java b/app/src/main/java/org/transitclock/api/data/ApiRoutesDetails.java
new file mode 100644
index 000000000..8a2b0dd1d
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiRoutesDetails.java
@@ -0,0 +1,51 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.domain.structs.Agency;
+import org.transitclock.service.dto.IpcRoute;
+
+/**
+ * When have a list of routes.
+ *
+ * @author Michael
+ */
+@XmlRootElement
+public class ApiRoutesDetails {
+ // So can easily get agency name when getting routes. Useful for db reports
+ // and such.
+ @XmlElement(name = "agency")
+ private String agencyName;
+
+ // List of route info
+ @XmlElement(name = "routes")
+ private List routesData;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ public ApiRoutesDetails() {}
+
+ /**
+ * For constructing a ApiRoutes object from a Collection of IpcRoute objects.
+ *
+ * @param routes
+ * @param agency so can get agency name
+ */
+ public ApiRoutesDetails(Collection routes, Agency agency) {
+ routesData = new ArrayList();
+ for (IpcRoute route : routes) {
+ routesData.add(new ApiRouteDetails(route));
+ }
+
+ // Also set agency name
+ agencyName = agency.getName();
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiScheduleArrDepTime.java b/app/src/main/java/org/transitclock/api/data/ApiScheduleArrDepTime.java
new file mode 100644
index 000000000..8e966a875
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiScheduleArrDepTime.java
@@ -0,0 +1,46 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import org.transitclock.service.dto.IpcSchedTimes;
+import org.transitclock.utils.Time;
+
+/**
+ * Represents a schedule time for a stop. Contains both arrival and departure time and is intended
+ * to be used for displaying the details of a trip.
+ *
+ * @author SkiBu Smith
+ */
+public class ApiScheduleArrDepTime {
+
+ @XmlAttribute
+ private String arrivalTime;
+
+ @XmlAttribute
+ private String departureTime;
+
+ @XmlAttribute
+ private String stopId;
+
+ @XmlAttribute
+ private String stopName;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiScheduleArrDepTime() {}
+
+ public ApiScheduleArrDepTime(IpcSchedTimes ipcScheduleTimes) {
+ Integer arrivalInt = ipcScheduleTimes.getArrivalTime();
+ arrivalTime = arrivalInt == null ? null : Time.timeOfDayStr(arrivalInt);
+
+ Integer departureInt = ipcScheduleTimes.getDepartureTime();
+ departureTime = departureInt == null ? null : Time.timeOfDayStr(departureInt);
+
+ stopId = ipcScheduleTimes.getStopId();
+ stopName = ipcScheduleTimes.getStopName();
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiScheduleHorizStops.java b/app/src/main/java/org/transitclock/api/data/ApiScheduleHorizStops.java
new file mode 100644
index 000000000..951a7c555
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiScheduleHorizStops.java
@@ -0,0 +1,69 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import org.transitclock.service.dto.IpcSchedTime;
+import org.transitclock.service.dto.IpcSchedTrip;
+import org.transitclock.service.dto.IpcSchedule;
+
+/**
+ * Represents a schedule for a route for a specific direction and service class. Stops are listed
+ * horizontally in the matrix.
+ *
+ * @author SkiBu Smith
+ */
+public class ApiScheduleHorizStops {
+
+ @XmlAttribute
+ private String serviceId;
+
+ @XmlAttribute
+ private String serviceName;
+
+ @XmlAttribute
+ private String directionId;
+
+ @XmlAttribute
+ private String routeId;
+
+ @XmlAttribute
+ private String routeName;
+
+ @XmlElement(name = "stop")
+ private List stops;
+
+ @XmlElement
+ private List timesForTrip;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiScheduleHorizStops() {}
+
+ public ApiScheduleHorizStops(IpcSchedule ipcSched) {
+ serviceId = ipcSched.getServiceId();
+ serviceName = ipcSched.getServiceName();
+ directionId = ipcSched.getDirectionId();
+ routeId = ipcSched.getRouteId();
+ routeName = ipcSched.getRouteName();
+
+ // Create the list of stops to be first row of output
+ stops = new ArrayList();
+ IpcSchedTrip firstIpcSchedTrip = ipcSched.getIpcSchedTrips().get(0);
+ for (IpcSchedTime ipcSchedTime : firstIpcSchedTrip.getSchedTimes()) {
+ stops.add(new ApiScheduleStop(ipcSchedTime.getStopId(), ipcSchedTime.getStopName()));
+ }
+
+ // Create the schedule row for each trip
+ timesForTrip = new ArrayList();
+ for (IpcSchedTrip ipcSchedTrip : ipcSched.getIpcSchedTrips()) {
+ timesForTrip.add(new ApiScheduleTimesForTrip(ipcSchedTrip));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiScheduleStop.java b/app/src/main/java/org/transitclock/api/data/ApiScheduleStop.java
new file mode 100644
index 000000000..9ac0d186c
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiScheduleStop.java
@@ -0,0 +1,28 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+
+/**
+ * Contains minimal for a stop for a schedule
+ *
+ * @author Michael
+ */
+public class ApiScheduleStop {
+ @XmlAttribute
+ private String stopId;
+
+ @XmlAttribute
+ private String stopName;
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiScheduleStop() {}
+
+ public ApiScheduleStop(String stopId, String stopName) {
+ this.stopId = stopId;
+ this.stopName = stopName;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiScheduleTime.java b/app/src/main/java/org/transitclock/api/data/ApiScheduleTime.java
new file mode 100644
index 000000000..26fe9ee71
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiScheduleTime.java
@@ -0,0 +1,32 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import org.transitclock.utils.Time;
+
+/**
+ * Represents a schedule time for a stop. Intended to be used for displaying a schedule for a route.
+ *
+ * @author SkiBu Smith
+ */
+public class ApiScheduleTime {
+
+ @XmlAttribute
+ private String timeStr;
+
+ @XmlAttribute
+ private Integer timeSecs;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiScheduleTime() {}
+
+ public ApiScheduleTime(Integer time) {
+ this.timeStr = time == null ? null : Time.timeOfDayShortStr(time);
+ this.timeSecs = time;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiScheduleTimesForStop.java b/app/src/main/java/org/transitclock/api/data/ApiScheduleTimesForStop.java
new file mode 100644
index 000000000..73d15e983
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiScheduleTimesForStop.java
@@ -0,0 +1,43 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+
+/**
+ * Contains the schedule times for a stop for each trip for the route/direction/service. For when
+ * outputting stops vertically.
+ *
+ * @author SkiBu Smith
+ */
+public class ApiScheduleTimesForStop {
+
+ @XmlAttribute
+ private String stopId;
+
+ @XmlAttribute
+ private String stopName;
+
+ @XmlElement(name = "time")
+ private List times;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiScheduleTimesForStop() {}
+
+ public ApiScheduleTimesForStop(String stopId, String stopName) {
+ this.stopId = stopId;
+ this.stopName = stopName;
+ this.times = new ArrayList();
+ }
+
+ public void add(Integer time) {
+ this.times.add(new ApiScheduleTime(time));
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiScheduleTimesForTrip.java b/app/src/main/java/org/transitclock/api/data/ApiScheduleTimesForTrip.java
new file mode 100644
index 000000000..66b19b986
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiScheduleTimesForTrip.java
@@ -0,0 +1,49 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import org.transitclock.service.dto.IpcSchedTime;
+import org.transitclock.service.dto.IpcSchedTrip;
+
+/**
+ * Contains the schedule times for a trip. For when outputting stops horizontally.
+ *
+ * @author Michael
+ */
+public class ApiScheduleTimesForTrip {
+ @XmlAttribute
+ private String tripShortName;
+
+ @XmlAttribute
+ private String tripId;
+
+ @XmlAttribute
+ private String tripHeadsign;
+
+ @XmlAttribute
+ private String blockId;
+
+ @XmlElement(name = "time")
+ private List times;
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiScheduleTimesForTrip() {}
+
+ public ApiScheduleTimesForTrip(IpcSchedTrip ipcSchedTrip) {
+ this.tripShortName = ipcSchedTrip.getTripShortName();
+ this.tripId = ipcSchedTrip.getTripId();
+ this.tripHeadsign = ipcSchedTrip.getTripHeadsign();
+ this.blockId = ipcSchedTrip.getBlockId();
+
+ times = new ArrayList();
+ for (IpcSchedTime ipcSchedTime : ipcSchedTrip.getSchedTimes()) {
+ times.add(new ApiScheduleTime(ipcSchedTime.getTimeOfDay()));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiScheduleTrip.java b/app/src/main/java/org/transitclock/api/data/ApiScheduleTrip.java
new file mode 100644
index 000000000..f34705634
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiScheduleTrip.java
@@ -0,0 +1,40 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import org.transitclock.service.dto.IpcSchedTrip;
+
+/**
+ * Represents a single trip for an ApiSchedule
+ *
+ * @author SkiBu Smith
+ */
+public class ApiScheduleTrip {
+
+ @XmlAttribute
+ private String tripShortName;
+
+ @XmlAttribute
+ private String tripId;
+
+ @XmlAttribute
+ private String tripHeadsign;
+
+ @XmlAttribute
+ private String blockId;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiScheduleTrip() {}
+
+ public ApiScheduleTrip(IpcSchedTrip ipcScheduleTrip) {
+ this.tripShortName = ipcScheduleTrip.getTripShortName();
+ this.tripId = ipcScheduleTrip.getTripId();
+ this.tripHeadsign = ipcScheduleTrip.getTripHeadsign();
+ this.blockId = ipcScheduleTrip.getBlockId();
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiScheduleVertStops.java b/app/src/main/java/org/transitclock/api/data/ApiScheduleVertStops.java
new file mode 100644
index 000000000..328302938
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiScheduleVertStops.java
@@ -0,0 +1,86 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import org.transitclock.service.dto.IpcSchedTime;
+import org.transitclock.service.dto.IpcSchedTrip;
+import org.transitclock.service.dto.IpcSchedule;
+
+/**
+ * Represents a schedule for a route for a specific direction and service class. Stops are listed
+ * vertically in the matrix. For when there are a good number of stops but not as many trips, such
+ * as for commuter rail.
+ *
+ * @author SkiBu Smith
+ */
+public class ApiScheduleVertStops {
+
+ @XmlAttribute
+ private String serviceId;
+
+ @XmlAttribute
+ private String serviceName;
+
+ @XmlAttribute
+ private String directionId;
+
+ @XmlAttribute
+ private String routeId;
+
+ @XmlAttribute
+ private String routeName;
+
+ @XmlElement(name = "trip")
+ private List trips;
+
+ @XmlElement
+ private List timesForStop;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiScheduleVertStops() {}
+
+ public ApiScheduleVertStops(IpcSchedule ipcSched) {
+ serviceId = ipcSched.getServiceId();
+ serviceName = ipcSched.getServiceName();
+ directionId = ipcSched.getDirectionId();
+ routeId = ipcSched.getRouteId();
+ routeName = ipcSched.getRouteName();
+
+ // Create the trips element which contains list of all the trips
+ // for the schedule for the route/direction/service
+ trips = new ArrayList();
+ for (IpcSchedTrip ipcSchedTrip : ipcSched.getIpcSchedTrips()) {
+ trips.add(new ApiScheduleTrip(ipcSchedTrip));
+ }
+
+ // Determine all the times for each stop
+ timesForStop = new ArrayList();
+ // Use first trip to determine which stops are covered
+ List schedTimesForFirstTrip =
+ ipcSched.getIpcSchedTrips().get(0).getSchedTimes();
+ // For each stop. Uses schedule times for first trip to determine
+ // the list of stops since each trip has same number of stops
+ // in IpcSchedule (not every stop is actually visited though).
+ for (int stopIndexInTrip = 0; stopIndexInTrip < schedTimesForFirstTrip.size(); ++stopIndexInTrip) {
+ IpcSchedTime firstTripSchedTime = schedTimesForFirstTrip.get(stopIndexInTrip);
+ String stopId = firstTripSchedTime.getStopId();
+ String stopName = firstTripSchedTime.getStopName();
+ ApiScheduleTimesForStop apiSchedTimesForStop = new ApiScheduleTimesForStop(stopId, stopName);
+ timesForStop.add(apiSchedTimesForStop);
+ // For each trip find the time for the current stop...
+ for (IpcSchedTrip ipcSchedTrip : ipcSched.getIpcSchedTrips()) {
+ // For the current trip find the time for the current stop...
+ IpcSchedTime ipcSchedTime = ipcSchedTrip.getSchedTimes().get(stopIndexInTrip);
+ apiSchedTimesForStop.add(ipcSchedTime.getTimeOfDay());
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiSchedulesHorizStops.java b/app/src/main/java/org/transitclock/api/data/ApiSchedulesHorizStops.java
new file mode 100644
index 000000000..7fa336dd1
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiSchedulesHorizStops.java
@@ -0,0 +1,40 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcSchedule;
+
+@XmlRootElement(name = "schedules")
+public class ApiSchedulesHorizStops {
+
+ @XmlAttribute
+ private String routeId;
+
+ @XmlAttribute
+ private String routeName;
+
+ @XmlElement(name = "schedule")
+ private List schedules;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiSchedulesHorizStops() {}
+
+ public ApiSchedulesHorizStops(List schedules) {
+ this.routeId = schedules.get(0).getRouteId();
+ this.routeName = schedules.get(0).getRouteName();
+
+ this.schedules = new ArrayList(schedules.size());
+ for (IpcSchedule ipcSchedule : schedules) {
+ this.schedules.add(new ApiScheduleHorizStops(ipcSchedule));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiSchedulesVertStops.java b/app/src/main/java/org/transitclock/api/data/ApiSchedulesVertStops.java
new file mode 100644
index 000000000..20f0a4d22
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiSchedulesVertStops.java
@@ -0,0 +1,48 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcSchedule;
+
+/**
+ * Represents a collection of ApiScheduleVertStops objects for a route. There is one
+ * ApiScheduleVertStops for each direction/service for a route. The stops are listed vertically in
+ * the matrix. For when there are a good number of stops but not as many trips, such as for commuter
+ * rail.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "schedules")
+public class ApiSchedulesVertStops {
+
+ @XmlAttribute
+ private String routeId;
+
+ @XmlAttribute
+ private String routeName;
+
+ @XmlElement(name = "schedule")
+ private List schedules;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiSchedulesVertStops() {}
+
+ public ApiSchedulesVertStops(List schedules) {
+ this.routeId = schedules.get(0).getRouteId();
+ this.routeName = schedules.get(0).getRouteName();
+
+ this.schedules = new ArrayList(schedules.size());
+ for (IpcSchedule ipcSchedule : schedules) {
+ this.schedules.add(new ApiScheduleVertStops(ipcSchedule));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiServerMonitor.java b/app/src/main/java/org/transitclock/api/data/ApiServerMonitor.java
new file mode 100644
index 000000000..734a3cd56
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiServerMonitor.java
@@ -0,0 +1,31 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlValue;
+import org.transitclock.monitoring.MonitorResult;
+
+/**
+ * @author SkiBu Smith
+ */
+public class ApiServerMonitor {
+
+ @XmlAttribute
+ private String type;
+
+ @XmlValue
+ private String message;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiServerMonitor() {}
+
+ public ApiServerMonitor(MonitorResult monitorResult) {
+ this.type = monitorResult.getType();
+ this.message = monitorResult.getMessage();
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiServerStatus.java b/app/src/main/java/org/transitclock/api/data/ApiServerStatus.java
new file mode 100644
index 000000000..ac5059c90
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiServerStatus.java
@@ -0,0 +1,48 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcServerStatus;
+import org.transitclock.monitoring.MonitorResult;
+
+/**
+ * Server status for an agency server
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "serverStatus")
+public class ApiServerStatus {
+
+ @XmlAttribute
+ private String agencyId;
+
+ @XmlElement(name = "serverMonitor")
+ private List serverMonitors;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiServerStatus() {}
+
+ /**
+ * Constructors a ApiServerStatus object from agencyId and IpcServerStatus objects.
+ *
+ * @param agencyId
+ * @param ipcServerStatus
+ */
+ public ApiServerStatus(String agencyId, IpcServerStatus ipcServerStatus) {
+ this.agencyId = agencyId;
+
+ serverMonitors = new ArrayList();
+ for (MonitorResult monitorResult : ipcServerStatus.getMonitorResults()) {
+ this.serverMonitors.add(new ApiServerMonitor(monitorResult));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiServiceId.java b/app/src/main/java/org/transitclock/api/data/ApiServiceId.java
new file mode 100644
index 000000000..b31a3c93e
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiServiceId.java
@@ -0,0 +1,35 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A short description of a serviceId. For when outputting list of block IDs for service.
+ *
+ * @author SkiBu Smith
+ */
+public class ApiServiceId {
+
+ @XmlAttribute
+ private String id;
+
+ @XmlAttribute
+ private List blockIds;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiServiceId() {
+ }
+
+ public ApiServiceId(String serviceId, List blockIds) {
+ this.id = serviceId;
+ this.blockIds = blockIds;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiServiceIds.java b/app/src/main/java/org/transitclock/api/data/ApiServiceIds.java
new file mode 100644
index 000000000..72912b8b3
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiServiceIds.java
@@ -0,0 +1,41 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * For outputting simple list of unsorted service IDs with lists of sorted block IDs
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement
+public class ApiServiceIds {
+
+ @XmlElement(name= "serviceIds")
+ private List apiServiceIds;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiServiceIds() {
+ }
+
+ /**
+ * Creates the API unsorted version of list of IDs.
+ *
+ * @param serviceIds
+ */
+ public ApiServiceIds(Map> serviceIds) {
+ apiServiceIds = new ArrayList<>();
+ serviceIds.forEach((key, list) -> apiServiceIds
+ .add(new ApiServiceId(key,list)));
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiShape.java b/app/src/main/java/org/transitclock/api/data/ApiShape.java
new file mode 100644
index 000000000..94894504e
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiShape.java
@@ -0,0 +1,76 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import org.transitclock.domain.structs.Location;
+import org.transitclock.service.dto.IpcShape;
+import org.transitclock.utils.Geo;
+
+/**
+ * A portion of a shape that defines a trip pattern. A List of ApiLocation objects.
+ *
+ * @author SkiBu Smith
+ */
+public class ApiShape {
+
+ @XmlAttribute(name = "tripPattern")
+ private String tripPatternId;
+
+ @XmlAttribute
+ private String headsign;
+
+ // For indicating that in UI should deemphasize this shape because it
+ // is not on a main trip pattern.
+ @XmlAttribute(name = "minor")
+ private Boolean minor;
+
+ @XmlElement(name = "loc")
+ private List locations;
+
+ @XmlAttribute
+ private double length;
+
+ @XmlAttribute
+ private String directionId;
+
+ // To define what kind of pattern is: circular (loop, one ending), linear (normal line with two
+ // different endings)
+ @XmlAttribute
+ private String patternType = "linear";
+
+ private static final int LOOP_ENDING_MAX_DISTANCE = 200;
+ private static final String LOOP_PATTERN = "circular";
+ private static final String LINAR_PATTER = "linear";
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiShape() {}
+
+ public ApiShape(IpcShape shape) {
+ this.tripPatternId = shape.getTripPatternId();
+ this.headsign = shape.getHeadsign();
+ this.length = shape.getLength();
+ this.directionId = shape.getDirectionId();
+ // If true then set to null so that this attribute won't then be
+ // output as XML/JSON, therefore making output a bit more compact.
+ this.minor = shape.isUiShape() ? null : true;
+ this.locations = new ArrayList();
+ for (Location loc : shape.getLocations()) {
+ this.locations.add(new ApiLocation(loc.getLat(), loc.getLon()));
+ }
+ int size = shape.getLocations().size();
+ if (size > 0
+ && Geo.distance(
+ shape.getLocations().get(0),
+ shape.getLocations().get(size - 1))
+ < LOOP_ENDING_MAX_DISTANCE) patternType = LOOP_PATTERN;
+ else patternType = LINAR_PATTER;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiStop.java b/app/src/main/java/org/transitclock/api/data/ApiStop.java
new file mode 100644
index 000000000..bd81c01a0
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiStop.java
@@ -0,0 +1,54 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlType;
+import org.transitclock.service.dto.IpcStop;
+
+/**
+ * Full description of a stop.
+ *
+ *
Note: extending from ApiLocation since have a lat & lon. Would be nice to have ApiLocation as
+ * a member but when try this get a internal server 500 error.
+ *
+ * @author SkiBu Smith
+ */
+@XmlType(propOrder = {"id", "lat", "lon", "name", "code", "minor", "pathLength"})
+public class ApiStop extends ApiTransientLocation {
+
+ @XmlAttribute
+ private String id;
+
+ @XmlAttribute
+ private String name;
+
+ @XmlAttribute
+ private Integer code;
+
+ // For indicating that in UI should deemphasize this stop because it
+ // is not on a main trip pattern.
+ @XmlAttribute(name = "minor")
+ private Boolean minor;
+
+ @XmlAttribute
+ private Double pathLength;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiStop() {}
+
+ public ApiStop(IpcStop stop) {
+ super(stop.getLoc().getLat(), stop.getLoc().getLon());
+ this.id = stop.getId();
+ this.name = stop.getName();
+ this.code = stop.getCode();
+ this.pathLength = stop.getStopPathLength() == null ? 0.0 : stop.getStopPathLength();
+ // If true then set to null so that this attribute won't then be
+ // output as XML/JSON, therefore making output a bit more compact.
+ this.minor = stop.isUiStop() ? null : true;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiStopPath.java b/app/src/main/java/org/transitclock/api/data/ApiStopPath.java
new file mode 100644
index 000000000..1fc6141f2
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiStopPath.java
@@ -0,0 +1,78 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import org.transitclock.domain.structs.Location;
+import org.transitclock.service.dto.IpcStopPath;
+import org.transitclock.utils.MathUtils;
+
+/**
+ * Represents a path from one stop to another.
+ *
+ * @author SkiBu Smith
+ */
+public class ApiStopPath {
+
+ @XmlAttribute
+ private int configRev;
+
+ @XmlAttribute
+ private String stopPathId;
+
+ @XmlAttribute
+ private String stopId;
+
+ @XmlAttribute
+ private String stopName;
+
+ @XmlAttribute
+ private int gtfsStopSeq;
+
+ @XmlAttribute
+ private Boolean layoverStop;
+
+ @XmlAttribute
+ private Boolean waitStop;
+
+ @XmlAttribute
+ private Boolean scheduleAdherenceStop;
+
+ @XmlAttribute
+ private Integer breakTime;
+
+ @XmlElement
+ private List locations;
+
+ @XmlAttribute
+ private Double pathLength;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiStopPath() {}
+
+ public ApiStopPath(IpcStopPath ipcStopPath) {
+ configRev = ipcStopPath.getConfigRev();
+ stopPathId = ipcStopPath.getStopPathId();
+ stopId = ipcStopPath.getStopId();
+ stopName = ipcStopPath.getStopName();
+ gtfsStopSeq = ipcStopPath.getGtfsStopSeq();
+ layoverStop = ipcStopPath.isLayoverStop() ? true : null;
+ waitStop = ipcStopPath.isWaitStop() ? true : null;
+ scheduleAdherenceStop = ipcStopPath.isScheduleAdherenceStop() ? true : null;
+ breakTime = ipcStopPath.getBreakTime() != 0 ? ipcStopPath.getBreakTime() : null;
+
+ locations = new ArrayList();
+ for (Location loc : ipcStopPath.getLocations()) {
+ locations.add(new ApiLocation(loc.getLat(), loc.getLon()));
+ }
+
+ pathLength = MathUtils.round(ipcStopPath.getPathLength(), 1);
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiTransientLocation.java b/app/src/main/java/org/transitclock/api/data/ApiTransientLocation.java
new file mode 100644
index 000000000..678567845
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiTransientLocation.java
@@ -0,0 +1,44 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlTransient;
+import org.transitclock.utils.ChinaGpsOffset;
+import org.transitclock.utils.MathUtils;
+
+/**
+ * A simple latitude/longitude:
+ *
+ *
Note: this class marked as @XmlTransient so that ApiLocations are not part of the domain
+ * model. This means that can't instantiate as an element. The reason doing this is so that the
+ * subclass can set propOrder for all elements in the subclass, including lat & lon. Explanation of
+ * this is in http://blog.bdoughan.com/2011/06/ignoring-inheritance-with-xmltransient.html
+ *
+ * @author SkiBu Smith
+ */
+@XmlTransient
+public class ApiTransientLocation {
+
+ @XmlAttribute
+ private double lat;
+
+ @XmlAttribute
+ private double lon;
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiTransientLocation() {}
+
+ public ApiTransientLocation(double lat, double lon) {
+ // If location is in China (approximately) then adjust lat & lon so
+ // that will be displayed properly on map.
+ ChinaGpsOffset.LatLon latLon = ChinaGpsOffset.transform(lat, lon);
+
+ // Output only 5 digits past decimal point
+ this.lat = MathUtils.round(latLon.getLat(), 5);
+ // Output only 5 digits past decimal point
+ this.lon = MathUtils.round(latLon.getLon(), 5);
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiTravelTimeForSegment.java b/app/src/main/java/org/transitclock/api/data/ApiTravelTimeForSegment.java
new file mode 100644
index 000000000..45d1e93ad
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiTravelTimeForSegment.java
@@ -0,0 +1,60 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import org.transitclock.utils.Geo;
+import org.transitclock.utils.MathUtils;
+import org.transitclock.utils.Time;
+
+/**
+ * For representing travel time for a single segment.
+ *
+ * @author SkiBu Smith
+ */
+public class ApiTravelTimeForSegment {
+
+ @XmlAttribute
+ private int segmentIndex;
+
+ @XmlAttribute
+ private int segmentTimeMsec;
+
+ @XmlAttribute
+ private Double speedInMph;
+
+ @XmlAttribute
+ private Double speedInKph;
+
+ @XmlAttribute
+ private Double speedInMetersPerSec;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiTravelTimeForSegment() {}
+
+ /**
+ * Constructor
+ *
+ * @param segmentIndex
+ * @param segmentTimeMsec
+ * @param segmentLength
+ */
+ public ApiTravelTimeForSegment(int segmentIndex, int segmentTimeMsec, double segmentLength) {
+ this.segmentIndex = segmentIndex;
+ this.segmentTimeMsec = segmentTimeMsec;
+
+ // If segment time is 0 then speeds will default to null and will
+ // not be output. Better than trying to divide by zero since
+ // can't output NaN with JSON.
+ if (segmentTimeMsec != 0) {
+ double speedInMetersPerSec = segmentLength * Time.MS_PER_SEC / segmentTimeMsec;
+ this.speedInMph = MathUtils.round(speedInMetersPerSec / Geo.MPH_TO_MPS, 1);
+ this.speedInKph = MathUtils.round(speedInMetersPerSec / Geo.KPH_TO_MPS, 1);
+ this.speedInMetersPerSec = MathUtils.round(speedInMetersPerSec, 1);
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiTravelTimes.java b/app/src/main/java/org/transitclock/api/data/ApiTravelTimes.java
new file mode 100644
index 000000000..ec3ba3284
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiTravelTimes.java
@@ -0,0 +1,59 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import org.transitclock.domain.structs.TravelTimesForStopPath;
+import org.transitclock.domain.structs.TravelTimesForTrip;
+
+/**
+ * @author SkiBu Smith
+ */
+public class ApiTravelTimes {
+
+ @XmlAttribute
+ private int configRev;
+
+ @XmlAttribute
+ private int travelTimeRev;
+
+ @XmlAttribute
+ private String tripPatternId;
+
+ @XmlAttribute
+ private String tripCreatedForId;
+
+ @XmlElement(name = "travelTimesForStopPath")
+ private List travelTimesForStopPaths;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiTravelTimes() {}
+
+ /**
+ * Constructor
+ *
+ * @param travelTimes
+ */
+ public ApiTravelTimes(TravelTimesForTrip travelTimes) {
+ this.configRev = travelTimes.getConfigRev();
+ this.travelTimeRev = travelTimes.getTravelTimesRev();
+ this.tripPatternId = travelTimes.getTripPatternId();
+ this.tripCreatedForId = travelTimes.getTripCreatedForId();
+
+ this.travelTimesForStopPaths = new ArrayList();
+
+ for (int stopPathIndex = 0;
+ stopPathIndex < travelTimes.getTravelTimesForStopPaths().size();
+ ++stopPathIndex) {
+ TravelTimesForStopPath travelTimesForStopPath = travelTimes.getTravelTimesForStopPath(stopPathIndex);
+ this.travelTimesForStopPaths.add(new ApiTravelTimesForStopPath(stopPathIndex, travelTimesForStopPath));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiTravelTimesForStopPath.java b/app/src/main/java/org/transitclock/api/data/ApiTravelTimesForStopPath.java
new file mode 100644
index 000000000..a302e5b2b
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiTravelTimesForStopPath.java
@@ -0,0 +1,72 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import org.transitclock.domain.structs.TravelTimesForStopPath;
+import org.transitclock.domain.structs.TravelTimesForStopPath.HowSet;
+import org.transitclock.utils.MathUtils;
+
+/**
+ * Represents travel times for a stop path
+ *
+ * @author SkiBu Smith
+ */
+public class ApiTravelTimesForStopPath {
+
+ @XmlAttribute
+ private int stopPathIndex;
+
+ @XmlAttribute
+ private String stopPathId;
+
+ @XmlAttribute
+ private Double travelTimeSegmentLength;
+
+ @XmlAttribute
+ private int stopTimeMsec;
+
+ @XmlAttribute
+ private int totalTravelTimeMsec;
+
+ @XmlAttribute
+ private HowSet howSet;
+
+ @XmlElement(name = "travelTimesForSegment")
+ private List travelTimesForSegments;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiTravelTimesForStopPath() {}
+
+ /**
+ * Constructor
+ *
+ * @param travelTimesForStopPath
+ */
+ public ApiTravelTimesForStopPath(int stopPathIndex, TravelTimesForStopPath travelTimesForStopPath) {
+ this.stopPathIndex = stopPathIndex;
+ this.stopPathId = travelTimesForStopPath.getStopPathId();
+ double travelTimeSegLengthValue = MathUtils.round(travelTimesForStopPath.getTravelTimeSegmentLength(), 1);
+ this.travelTimeSegmentLength = Double.isNaN(travelTimeSegLengthValue) ? null : travelTimeSegLengthValue;
+ this.stopTimeMsec = travelTimesForStopPath.getStopTimeMsec();
+ this.totalTravelTimeMsec = travelTimesForStopPath.getStopPathTravelTimeMsec();
+ this.howSet = travelTimesForStopPath.getHowSet();
+
+ this.travelTimesForSegments = new ArrayList();
+ for (int segmentIndex = 0;
+ segmentIndex < travelTimesForStopPath.getNumberTravelTimeSegments();
+ ++segmentIndex) {
+ int travelTimeForSegment = travelTimesForStopPath.getTravelTimeSegmentMsec(segmentIndex);
+ ApiTravelTimeForSegment travelTime = new ApiTravelTimeForSegment(
+ segmentIndex, travelTimeForSegment, travelTimesForStopPath.getTravelTimeSegmentLength());
+ this.travelTimesForSegments.add(travelTime);
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiTrip.java b/app/src/main/java/org/transitclock/api/data/ApiTrip.java
new file mode 100644
index 000000000..7b2a0a89d
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiTrip.java
@@ -0,0 +1,101 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcSchedTimes;
+import org.transitclock.service.dto.IpcTrip;
+import org.transitclock.utils.Time;
+
+/**
+ * Specifies how trip data is formatted for the API.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "trip")
+public class ApiTrip {
+
+ @XmlAttribute
+ private int configRev;
+
+ @XmlAttribute
+ private String id;
+
+ @XmlAttribute
+ private String shortName;
+
+ @XmlAttribute
+ private String startTime;
+
+ @XmlAttribute
+ private String endTime;
+
+ @XmlAttribute
+ private String directionId;
+
+ @XmlAttribute
+ private String routeId;
+
+ @XmlAttribute
+ private String routeShortName;
+
+ @XmlElement
+ private ApiTripPattern tripPattern;
+
+ @XmlAttribute
+ private String serviceId;
+
+ @XmlAttribute
+ private String headsign;
+
+ @XmlAttribute
+ private String blockId;
+
+ @XmlAttribute
+ private String shapeId;
+
+ // Using a Boolean so that will be output only if true
+ @XmlAttribute
+ private Boolean noSchedule;
+
+ @XmlElement(name = "schedule")
+ private List scheduleTimes;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiTrip() {}
+
+ /**
+ * @param ipcTrip
+ * @param includeStopPaths Stop paths are only included in output if this param set to true.
+ */
+ public ApiTrip(IpcTrip ipcTrip, boolean includeStopPaths) {
+ configRev = ipcTrip.getConfigRev();
+ id = ipcTrip.getId();
+ shortName = ipcTrip.getShortName();
+ startTime = Time.timeOfDayStr(ipcTrip.getStartTime());
+ endTime = Time.timeOfDayStr(ipcTrip.getEndTime());
+ directionId = ipcTrip.getDirectionId();
+ routeId = ipcTrip.getRouteId();
+ routeShortName = ipcTrip.getRouteShortName();
+ tripPattern = new ApiTripPattern(ipcTrip.getTripPattern(), includeStopPaths);
+ serviceId = ipcTrip.getServiceId();
+ headsign = ipcTrip.getHeadsign();
+ blockId = ipcTrip.getBlockId();
+ shapeId = ipcTrip.getShapeId();
+
+ noSchedule = ipcTrip.isNoSchedule() ? true : null;
+
+ scheduleTimes = new ArrayList();
+ for (IpcSchedTimes ipcScheduleTimes : ipcTrip.getScheduleTimes()) {
+ scheduleTimes.add(new ApiScheduleArrDepTime(ipcScheduleTimes));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiTripPattern.java b/app/src/main/java/org/transitclock/api/data/ApiTripPattern.java
new file mode 100644
index 000000000..6fc7377aa
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiTripPattern.java
@@ -0,0 +1,78 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import org.transitclock.service.dto.IpcStopPath;
+import org.transitclock.service.dto.IpcTripPattern;
+
+/**
+ * A single trip pattern
+ *
+ * @author SkiBu Smith
+ */
+public class ApiTripPattern {
+
+ @XmlAttribute
+ private int configRev;
+
+ @XmlAttribute
+ private String id;
+
+ @XmlAttribute
+ private String headsign;
+
+ @XmlAttribute
+ private String directionId;
+
+ @XmlAttribute
+ private String routeId;
+
+ @XmlAttribute
+ private String routeShortName;
+
+ @XmlElement
+ private ApiExtent extent;
+
+ @XmlAttribute
+ private String shapeId;
+
+ @XmlElement
+ private List stopPaths;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiTripPattern() {}
+
+ /**
+ * @param ipcTripPattern
+ * @param includeStopPaths Stop paths are only included in output if this param set to true.
+ */
+ public ApiTripPattern(IpcTripPattern ipcTripPattern, boolean includeStopPaths) {
+ configRev = ipcTripPattern.getConfigRev();
+ id = ipcTripPattern.getId();
+ headsign = ipcTripPattern.getHeadsign();
+ directionId = ipcTripPattern.getDirectionId();
+ routeId = ipcTripPattern.getRouteId();
+ routeShortName = ipcTripPattern.getRouteShortName();
+ extent = new ApiExtent(ipcTripPattern.getExtent());
+ shapeId = ipcTripPattern.getShapeId();
+
+ // Only include stop paths if actually want them. This
+ // can greatly reduce volume of the output.
+ if (includeStopPaths) {
+ stopPaths = new ArrayList();
+ for (IpcStopPath ipcStopPath : ipcTripPattern.getStopPaths()) {
+ stopPaths.add(new ApiStopPath(ipcStopPath));
+ }
+ } else {
+ stopPaths = null;
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiTripPatterns.java b/app/src/main/java/org/transitclock/api/data/ApiTripPatterns.java
new file mode 100644
index 000000000..470971276
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiTripPatterns.java
@@ -0,0 +1,39 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcTripPattern;
+
+/**
+ * A list of ApiTripPattern objects
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement
+public class ApiTripPatterns {
+
+ @XmlElement(name = "tripPatterns")
+ private List tripPatterns;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiTripPatterns() {}
+
+ public ApiTripPatterns(Collection ipcTripPatterns) {
+ tripPatterns = new ArrayList();
+ for (IpcTripPattern ipcTripPattern : ipcTripPatterns) {
+ // Including stop paths in output since that is likely
+ // what user wants since they are trying to understand
+ // the trip patterns for a route.
+ tripPatterns.add(new ApiTripPattern(ipcTripPattern, true));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiTripTerse.java b/app/src/main/java/org/transitclock/api/data/ApiTripTerse.java
new file mode 100644
index 000000000..1af9375b1
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiTripTerse.java
@@ -0,0 +1,63 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcTrip;
+import org.transitclock.utils.Time;
+
+/**
+ * A shorter version of ApiTrip for when all the detailed info is not needed.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "trip")
+public class ApiTripTerse {
+
+ @XmlAttribute
+ private String id;
+
+ @XmlAttribute
+ private String shortName;
+
+ @XmlAttribute
+ private String startTime;
+
+ @XmlAttribute
+ private String endTime;
+
+ @XmlAttribute
+ private String directionId;
+
+ @XmlAttribute
+ private String headsign;
+
+ @XmlAttribute
+ private String routeId;
+
+ @XmlAttribute
+ private String routeShortName;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiTripTerse() {}
+
+ public ApiTripTerse(IpcTrip ipcTrip) {
+ id = ipcTrip.getId();
+ shortName = ipcTrip.getShortName();
+ startTime = Time.timeOfDayStr(ipcTrip.getStartTime());
+ endTime = Time.timeOfDayStr(ipcTrip.getEndTime());
+ directionId = ipcTrip.getDirectionId();
+ headsign = ipcTrip.getHeadsign();
+ routeId = ipcTrip.getRouteId();
+ routeShortName = ipcTrip.getRouteShortName();
+ }
+
+ public String getRouteId() {
+ return routeId;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiTripWithTravelTimes.java b/app/src/main/java/org/transitclock/api/data/ApiTripWithTravelTimes.java
new file mode 100644
index 000000000..99b414391
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiTripWithTravelTimes.java
@@ -0,0 +1,35 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcTrip;
+
+/**
+ * Specifies how trip data along with travel times is formatted for the API.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "trip")
+public class ApiTripWithTravelTimes extends ApiTrip {
+
+ @XmlElement
+ private ApiTravelTimes travelTimes;
+
+ /********************** Member Functions **************************/
+
+ /** No args constructor needed for Jersey since this class is a @XmlRootElement */
+ protected ApiTripWithTravelTimes() {}
+
+ /**
+ * Constructor
+ *
+ * @param ipcTrip
+ * @param includeStopPaths
+ */
+ public ApiTripWithTravelTimes(IpcTrip ipcTrip, boolean includeStopPaths) {
+ super(ipcTrip, includeStopPaths);
+
+ travelTimes = new ApiTravelTimes(ipcTrip.getTravelTimes());
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiVehicle.java b/app/src/main/java/org/transitclock/api/data/ApiVehicle.java
new file mode 100644
index 000000000..948164271
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiVehicle.java
@@ -0,0 +1,58 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlRootElement;
+import jakarta.xml.bind.annotation.XmlType;
+import org.transitclock.api.resources.TransitimeApi.UiMode;
+import org.transitclock.service.dto.IpcVehicle;
+
+/**
+ * Contains the data for a single vehicle.
+ *
+ *
Note: @XmlType(propOrder=""...) is used to get the elements to be output in desired order
+ * instead of the default of alphabetical. This makes the resulting JSON/XML more readable.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement
+@XmlType(
+ propOrder = {
+ "id",
+ "routeId",
+ "routeShortName",
+ "headsign",
+ "directionId",
+ "vehicleType",
+ "uiType",
+ "schedBasedPreds",
+ "loc"
+ })
+public class ApiVehicle extends ApiVehicleAbstract {
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiVehicle() {}
+
+ /**
+ * Takes a Vehicle object for client/server communication and constructs a ApiVehicle object for
+ * the API.
+ *
+ * @param vehicle
+ * @param uiType If should be labeled as "minor" in output for UI.
+ */
+ public ApiVehicle(IpcVehicle vehicle, UiMode uiType) {
+ super(vehicle, uiType);
+ }
+
+ /**
+ * Takes a Vehicle object for client/server communication and constructs a ApiVehicle object for
+ * the API. Sets UiMode to UiMode.NORMAL.
+ *
+ * @param vehicle
+ */
+ public ApiVehicle(IpcVehicle vehicle) {
+ super(vehicle, UiMode.NORMAL);
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiVehicleAbstract.java b/app/src/main/java/org/transitclock/api/data/ApiVehicleAbstract.java
new file mode 100644
index 000000000..39d6db6a0
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiVehicleAbstract.java
@@ -0,0 +1,91 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlTransient;
+import org.transitclock.api.resources.TransitimeApi.UiMode;
+import org.transitclock.service.dto.IpcVehicle;
+
+/**
+ * This class exists so that can have multiple subclasses that inherent from each other while still
+ * being able to set the propOrder for each class. Specifically, ApiVehicleDetails is supposed to be
+ * a subclass of ApiVehicle. But want the vehicle id to be output as the first attribute. But the
+ * attributes for the subclass are output first and one can't normally set the propOrder of parent
+ * class attributes. One gets an internal error if one tries to do so.
+ *
+ *
The solution is to use the abstract class ApiVehicleAbstract. Then can implement ApiVehicle
+ * and ApiVehicleDetails to inherit from ApiVehicleAbstract and those classes can each set propOrder
+ * as desired. Yes, this is rather complicated, but it works.
+ *
+ * @author SkiBu Smith
+ */
+@XmlTransient
+public abstract class ApiVehicleAbstract {
+
+ @XmlAttribute
+ protected String id;
+
+ @XmlElement
+ protected ApiGpsLocation loc;
+
+ @XmlAttribute
+ protected String routeId;
+
+ @XmlAttribute
+ protected String routeShortName;
+
+ @XmlAttribute
+ protected String headsign;
+
+ @XmlAttribute(name = "direction")
+ protected String directionId;
+
+ @XmlAttribute
+ protected String vehicleType;
+
+ // Whether NORMAL, SECONDARY, or MINOR. Specifies how vehicle should
+ // be drawn in the UI
+ @XmlAttribute
+ protected String uiType;
+
+ @XmlAttribute(name = "scheduleBased")
+ protected Boolean schedBasedPreds;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiVehicleAbstract() {}
+
+ /**
+ * Takes a Vehicle object for client/server communication and constructs a ApiVehicle object for
+ * the API.
+ *
+ * @param vehicle
+ * @param uiType If should be labeled as "minor" in output for UI.
+ */
+ public ApiVehicleAbstract(IpcVehicle vehicle, UiMode uiType) {
+ id = vehicle.getId();
+ loc = new ApiGpsLocation(vehicle);
+ routeId = vehicle.getRouteId();
+ routeShortName = vehicle.getRouteShortName();
+ headsign = vehicle.getHeadsign();
+ directionId = vehicle.getDirectionId();
+
+ // Set GTFS vehicle type. If it was not set in the config then use
+ // default value of "3" which is for buses.
+ vehicleType = vehicle.getVehicleType();
+ if (vehicleType == null) vehicleType = "3";
+
+ // Determine UI type. Usually will be displaying vehicles
+ // as NORMAL. To simplify API use null for this case.
+ this.uiType = null;
+ if (uiType == UiMode.SECONDARY) this.uiType = "secondary";
+ else if (uiType == UiMode.MINOR) this.uiType = "minor";
+
+ this.schedBasedPreds = vehicle.isForSchedBasedPred() ? true : null;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiVehicleConfig.java b/app/src/main/java/org/transitclock/api/data/ApiVehicleConfig.java
new file mode 100644
index 000000000..f78ef752f
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiVehicleConfig.java
@@ -0,0 +1,56 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import jakarta.xml.bind.annotation.XmlType;
+import org.transitclock.service.dto.IpcVehicleConfig;
+
+/**
+ * Contains config data for single vehicle.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "vehicleConfig")
+@XmlType(propOrder = {"id", "type", "description", "capacity", "crushCapacity", "nonPassengerVehicle", "name"})
+public class ApiVehicleConfig {
+
+ @XmlAttribute
+ private String id;
+
+ @XmlAttribute
+ private Integer type;
+
+ @XmlAttribute
+ private String description;
+
+ @XmlAttribute
+ private Integer capacity;
+
+ @XmlAttribute
+ private Integer crushCapacity;
+
+ @XmlAttribute
+ private Boolean nonPassengerVehicle;
+
+ @XmlAttribute
+ private String name;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiVehicleConfig() {}
+
+ public ApiVehicleConfig(IpcVehicleConfig vehicle) {
+ this.id = vehicle.getId();
+ this.type = vehicle.getType();
+ this.description = vehicle.getDescription();
+ this.capacity = vehicle.getCapacity();
+ this.crushCapacity = vehicle.getCrushCapacity();
+ this.nonPassengerVehicle = vehicle.isNonPassengerVehicle();
+ this.name = vehicle.getName();
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiVehicleConfigs.java b/app/src/main/java/org/transitclock/api/data/ApiVehicleConfigs.java
new file mode 100644
index 000000000..ad1f60ca6
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiVehicleConfigs.java
@@ -0,0 +1,55 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlElementRef;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcVehicleConfig;
+
+/**
+ * For when have collection of ApiVehicleConfig
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "vehicleConfigs")
+public class ApiVehicleConfigs {
+
+ // Need to use @XmlElementRef so that the element name used for each
+ // ApiVehicle object will be what is specified in the ApiVehicle class.
+ @XmlElementRef
+ private List vehicleConfigs;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiVehicleConfigs() {}
+
+ /**
+ * Constructs a ApiVehicleConfigs object using IpcVehicleConfig data obtained via IPC.
+ *
+ * @param vehicles
+ */
+ public ApiVehicleConfigs(Collection vehicles) {
+ // Sort the vehicles by vehicle ID
+ List vehiclesList = new ArrayList(vehicles);
+ Collections.sort(vehiclesList, new Comparator() {
+ @Override
+ public int compare(final IpcVehicleConfig o1, final IpcVehicleConfig o2) {
+ return o1.getId().compareTo(o2.getId());
+ }
+ });
+
+ // Create list of Api objects from the Ipc objects
+ vehicleConfigs = new ArrayList();
+ for (IpcVehicleConfig vehicle : vehiclesList) {
+ vehicleConfigs.add(new ApiVehicleConfig(vehicle));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiVehicleDetails.java b/app/src/main/java/org/transitclock/api/data/ApiVehicleDetails.java
new file mode 100644
index 000000000..88d9518e8
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiVehicleDetails.java
@@ -0,0 +1,186 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.lang.reflect.InvocationTargetException;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import jakarta.xml.bind.annotation.XmlType;
+import org.transitclock.api.resources.TransitimeApi.UiMode;
+import org.transitclock.core.BlockAssignmentMethod;
+import org.transitclock.service.dto.IpcVehicle;
+import org.transitclock.service.dto.IpcVehicleComplete;
+import org.transitclock.utils.Time;
+
+/**
+ * Contains data for a single vehicle with additional info that is meant more for management than
+ * for passengers.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement
+@XmlType(
+ propOrder = {
+ "id",
+ "vehicleName",
+ "routeId",
+ "routeShortName",
+ "routeName",
+ "headsign",
+ "directionId",
+ "vehicleType",
+ "uiType",
+ "schedBasedPreds",
+ "loc",
+ "scheduleAdherence",
+ "scheduleAdherenceStr",
+ "blockId",
+ "blockAssignmentMethod",
+ "tripId",
+ "tripPatternId",
+ "isDelayed",
+ "isLayover",
+ "layoverDepTime",
+ "layoverDepTimeStr",
+ "nextStopId",
+ "nextStopName",
+ "driverId",
+ "holdingTime"
+ })
+public class ApiVehicleDetails extends ApiVehicleAbstract {
+
+ @XmlAttribute
+ private String routeName;
+
+ @XmlAttribute
+ private String vehicleName;
+
+ // Note: needs to be Integer instead of an int because it can be null
+ // (for vehicles that are not predictable)
+ @XmlAttribute(name = "schAdh")
+ private Integer scheduleAdherence;
+
+ @XmlAttribute(name = "schAdhStr")
+ private String scheduleAdherenceStr;
+
+ @XmlAttribute(name = "block")
+ private String blockId;
+
+ @XmlAttribute(name = "blockMthd")
+ private BlockAssignmentMethod blockAssignmentMethod;
+
+ @XmlAttribute(name = "trip")
+ private String tripId;
+
+ @XmlAttribute(name = "tripPattern")
+ private String tripPatternId;
+
+ @XmlAttribute(name = "delayed")
+ private Boolean isDelayed;
+
+ @XmlAttribute(name = "layover")
+ private Boolean isLayover;
+
+ @XmlAttribute
+ private Long layoverDepTime;
+
+ @XmlAttribute
+ private String layoverDepTimeStr;
+
+ @XmlAttribute
+ private String nextStopId;
+
+ @XmlAttribute
+ private String nextStopName;
+
+ @XmlElement(name = "driver")
+ private String driverId;
+
+ @XmlAttribute
+ private Boolean isScheduledService;
+
+ @XmlAttribute
+ private Long freqStartTime;
+
+ @XmlAttribute
+ private Boolean isAtStop;
+
+ @XmlElement
+ private ApiHoldingTime holdingTime;
+
+ @XmlAttribute
+ private double distanceAlongTrip;
+
+ @XmlAttribute
+ private String licensePlate;
+
+ @XmlAttribute
+ private boolean isCanceled;
+
+ @XmlAttribute
+ private double headway;
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiVehicleDetails() {}
+
+ /**
+ * Takes a Vehicle object for client/server communication and constructs a ApiVehicle object for
+ * the API.
+ *
+ * @param vehicle
+ * @param timeForAgency So can output times in proper timezone
+ * @param uiType Optional parameter. If should be labeled as "minor" in output for UI. Default
+ * is UiMode.NORMAL.
+ * @throws InvocationTargetException
+ * @throws IllegalAccessException
+ */
+ public ApiVehicleDetails(IpcVehicle vehicle, Time timeForAgency, UiMode... uiType)
+ throws IllegalAccessException, InvocationTargetException {
+ super(vehicle, uiType.length > 0 ? uiType[0] : UiMode.NORMAL);
+
+ routeName = vehicle.getRouteName();
+ vehicleName = vehicle.getVehicleName();
+ scheduleAdherence = vehicle.getRealTimeSchedAdh() != null
+ ? vehicle.getRealTimeSchedAdh().getTemporalDifference()
+ : null;
+ scheduleAdherenceStr = vehicle.getRealTimeSchedAdh() != null
+ ? vehicle.getRealTimeSchedAdh().toString()
+ : null;
+ blockId = vehicle.getBlockId();
+ blockAssignmentMethod = vehicle.getBlockAssignmentMethod();
+ tripId = vehicle.getTripId();
+ tripPatternId = vehicle.getTripPatternId();
+ isDelayed = vehicle.isDelayed() ? true : null;
+ isLayover = vehicle.isLayover() ? true : null;
+ layoverDepTime = vehicle.isLayover() ? vehicle.getLayoverDepartureTime() / Time.MS_PER_SEC : null;
+
+ layoverDepTimeStr =
+ vehicle.isLayover() ? timeForAgency.timeStrForTimezone(vehicle.getLayoverDepartureTime()) : null;
+
+ nextStopId = vehicle.getNextStopId() != null ? vehicle.getNextStopId() : null;
+ nextStopName = vehicle.getNextStopName() != null ? vehicle.getNextStopName() : null;
+ driverId = vehicle.getAvl().getDriverId();
+ licensePlate = vehicle.getLicensePlate();
+ isCanceled = false;
+ headway = -1;
+ if (vehicle instanceof IpcVehicleComplete && tripId != null) {
+ distanceAlongTrip = ((IpcVehicleComplete) vehicle).getDistanceAlongTrip();
+ isCanceled = ((IpcVehicleComplete) vehicle).isCanceled();
+ headway = ((IpcVehicleComplete) vehicle).getHeadway();
+ }
+ isScheduledService = vehicle.getFreqStartTime() > 0 ? false : true;
+ if (!isScheduledService) freqStartTime = vehicle.getFreqStartTime();
+ else freqStartTime = null;
+
+ this.isAtStop = vehicle.isAtStop();
+
+ if (vehicle.getHoldingTime() != null) {
+ this.holdingTime = new ApiHoldingTime(vehicle.getHoldingTime());
+ } else {
+ this.holdingTime = null;
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiVehicleToBlockConfig.java b/app/src/main/java/org/transitclock/api/data/ApiVehicleToBlockConfig.java
new file mode 100644
index 000000000..9da737b26
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiVehicleToBlockConfig.java
@@ -0,0 +1,35 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlRootElement;
+import jakarta.xml.bind.annotation.XmlType;
+import org.transitclock.service.dto.IpcVehicleToBlockConfig;
+
+/**
+ * Contains the data for a single vehicle.
+ *
+ *
Note: @XmlType(propOrder=""...) is used to get the elements to be output in desired order
+ * instead of the default of alphabetical. This makes the resulting JSON/XML more readable.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement
+@XmlType(propOrder = {"id", "vehicleId", "blockId", "tripId", "validFrom", "validTo", "assignmentDate"})
+public class ApiVehicleToBlockConfig extends ApiVehicleToBlockConfigAbstract {
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiVehicleToBlockConfig() {}
+
+ /**
+ * Takes a Vehicle object for client/server communication and constructs a ApiVehicle object for
+ * the API. Sets UiMode to UiMode.NORMAL.
+ *
+ * @param vehicleToBlockConfig
+ */
+ public ApiVehicleToBlockConfig(IpcVehicleToBlockConfig vehicleToBlockConfig) {
+ super(vehicleToBlockConfig);
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiVehicleToBlockConfigAbstract.java b/app/src/main/java/org/transitclock/api/data/ApiVehicleToBlockConfigAbstract.java
new file mode 100644
index 000000000..77d5b07e3
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiVehicleToBlockConfigAbstract.java
@@ -0,0 +1,67 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.Date;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlTransient;
+import org.transitclock.service.dto.IpcVehicleToBlockConfig;
+
+/**
+ * This class exists so that can have multiple subclasses that inherent from each other while still
+ * being able to set the propOrder for each class. Specifically, ApiVehicleDetails is supposed to be
+ * a subclass of ApiVehicle. But want the vehicle id to be output as the first attribute. But the
+ * attributes for the subclass are output first and one can't normally set the propOrder of parent
+ * class attributes. One gets an internal error if one tries to do so.
+ *
+ *
The solution is to use the abstract class ApiVehicleAbstract. Then can implement ApiVehicle
+ * and ApiVehicleDetails to inherit from ApiVehicleAbstract and those classes can each set propOrder
+ * as desired. Yes, this is rather complicated, but it works.
+ *
+ * @author SkiBu Smith
+ */
+@XmlTransient
+public abstract class ApiVehicleToBlockConfigAbstract {
+
+ @XmlAttribute
+ protected long id;
+
+ @XmlAttribute
+ protected String vehicleId;
+
+ @XmlAttribute
+ protected Date validFrom;
+
+ @XmlAttribute
+ protected Date validTo;
+
+ @XmlAttribute
+ protected Date assignmentDate;
+
+ @XmlAttribute
+ protected String tripId;
+
+ @XmlAttribute
+ protected String blockId;
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiVehicleToBlockConfigAbstract() {}
+
+ /**
+ * Takes a VehicleToBlockConfig object for client/server communication and constructs a ApiVehicleToBlockConfig object for
+ * the API.
+ *
+ * @param vehicleToBlockConfig
+ */
+ public ApiVehicleToBlockConfigAbstract(IpcVehicleToBlockConfig vehicleToBlockConfig) {
+ id = vehicleToBlockConfig.getId();
+ vehicleId = vehicleToBlockConfig.getVehicleId();
+ tripId = vehicleToBlockConfig.getTripId();
+ blockId = vehicleToBlockConfig.getBlockId();
+ validFrom = vehicleToBlockConfig.getValidFrom();
+ validTo = vehicleToBlockConfig.getValidTo();
+ assignmentDate = vehicleToBlockConfig.getAssignmentDate();
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiVehicleToBlockConfigs.java b/app/src/main/java/org/transitclock/api/data/ApiVehicleToBlockConfigs.java
new file mode 100644
index 000000000..2ed3e7ab3
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiVehicleToBlockConfigs.java
@@ -0,0 +1,43 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.service.dto.IpcVehicleToBlockConfig;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * For when have list of VehicleDetails. By using this class can control the element name when data
+ * is output.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement
+public class ApiVehicleToBlockConfigs {
+
+ @XmlElement(name = "vehicleToBlock")
+ private List vehiclesData;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiVehicleToBlockConfigs() {
+ }
+
+ /**
+ * For constructing a ApiVehicleToBlockConfigs object from a Collection of VehicleToBlockConfig objects.
+ *
+ * @param vehicles
+ */
+ public ApiVehicleToBlockConfigs(List vehicles) {
+ vehiclesData = new ArrayList();
+ for (IpcVehicleToBlockConfig vehicleToBlock : vehicles) {
+ vehiclesData.add(new ApiVehicleToBlockConfig(vehicleToBlock));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiVehicles.java b/app/src/main/java/org/transitclock/api/data/ApiVehicles.java
new file mode 100644
index 000000000..1e010ce7d
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiVehicles.java
@@ -0,0 +1,64 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.api.resources.TransitimeApi.UiMode;
+import org.transitclock.service.dto.IpcVehicle;
+
+/**
+ * For when have list of Vehicles. By using this class can control the element name when data is
+ * output.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement
+public class ApiVehicles {
+
+ @XmlElement(name = "vehicles")
+ private List vehiclesData;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ public ApiVehicles() {}
+
+ /**
+ * For constructing a ApiVehicles object from a Collection of Vehicle objects.
+ *
+ * @param vehicles
+ * @param uiTypesForVehicles Specifies how vehicles should be drawn in UI. Can be NORMAL,
+ * SECONDARY, or MINOR
+ */
+ public ApiVehicles(Collection vehicles, Map uiTypesForVehicles) {
+ vehiclesData = new ArrayList();
+ for (IpcVehicle vehicle : vehicles) {
+ // Determine UI type for vehicle
+ UiMode uiType = uiTypesForVehicles.get(vehicle.getId());
+
+ // Add this vehicle to the ApiVehicle list
+ vehiclesData.add(new ApiVehicle(vehicle, uiType));
+ }
+ }
+
+ /**
+ * For constructing a ApiVehicles object from a Collection of Vehicle objects. Sets UiMode to
+ * UiMode.NORMAL.
+ *
+ * @param vehicles
+ */
+ public ApiVehicles(Collection vehicles) {
+ vehiclesData = new ArrayList();
+ for (IpcVehicle vehicle : vehicles) {
+ // Add this vehicle to the ApiVehicle list
+ vehiclesData.add(new ApiVehicle(vehicle, UiMode.NORMAL));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/ApiVehiclesDetails.java b/app/src/main/java/org/transitclock/api/data/ApiVehiclesDetails.java
new file mode 100644
index 000000000..7feb783d0
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/ApiVehiclesDetails.java
@@ -0,0 +1,65 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import org.transitclock.api.resources.TransitimeApi.UiMode;
+import org.transitclock.domain.structs.Agency;
+import org.transitclock.domain.webstructs.WebAgency;
+import org.transitclock.service.dto.IpcVehicle;
+import org.transitclock.utils.Time;
+
+/**
+ * For when have list of VehicleDetails. By using this class can control the element name when data
+ * is output.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement
+public class ApiVehiclesDetails {
+
+ @XmlElement(name = "vehicles")
+ private List vehiclesData;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey. Otherwise get really obtuse "MessageBodyWriter not
+ * found for media type=application/json" exception.
+ */
+ protected ApiVehiclesDetails() {}
+
+ /**
+ * For constructing a ApiVehiclesDetails object from a Collection of Vehicle objects.
+ *
+ * @param vehicles
+ * @param agencyId
+ * @param uiTypesForVehicles Specifies how vehicles should be drawn in UI. Can be NORMAL,
+ * SECONDARY, or MINOR
+ * @param assigned
+ * @throws InvocationTargetException
+ * @throws IllegalAccessException
+ */
+ public ApiVehiclesDetails(
+ Collection vehicles, String agencyId, Map uiTypesForVehicles, boolean assigned)
+ throws IllegalAccessException, InvocationTargetException {
+ // Get Time object based on timezone for agency
+ WebAgency webAgency = WebAgency.getCachedWebAgency(agencyId);
+ Agency agency = webAgency.getAgency();
+ Time timeForAgency = agency != null ? agency.getTime() : new Time((String) null);
+
+ // Process each vehicle
+ vehiclesData = new ArrayList<>();
+ for (IpcVehicle vehicle : vehicles) {
+ // Determine UI type for vehicle
+ UiMode uiType = uiTypesForVehicles.get(vehicle.getId());
+ if ((assigned && vehicle.getTripId() != null) || !assigned)
+ vehiclesData.add(new ApiVehicleDetails(vehicle, timeForAgency, uiType));
+ }
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/IpcPredictionComparator.java b/app/src/main/java/org/transitclock/api/data/IpcPredictionComparator.java
new file mode 100644
index 000000000..8a8ee993c
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/IpcPredictionComparator.java
@@ -0,0 +1,21 @@
+/* (C)2023 */
+package org.transitclock.api.data;
+
+import java.util.Comparator;
+import org.transitclock.service.dto.IpcPrediction;
+
+/**
+ * A simple comparator for ordering IpcPrediction. In this way, the main class IcpPrediction is not
+ * modified.
+ *
+ * @author vperez
+ */
+public class IpcPredictionComparator implements Comparator {
+
+ /** In this moment, only sequence is needed by GtfsRtTripFeed */
+ @Override
+ public int compare(IpcPrediction o1, IpcPrediction o2) {
+ // TODO Auto-generated method stub
+ return o1.getGtfsStopSeq() - o2.getGtfsStopSeq();
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/gtfs/DataCache.java b/app/src/main/java/org/transitclock/api/data/gtfs/DataCache.java
new file mode 100644
index 000000000..de7f42b9f
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/gtfs/DataCache.java
@@ -0,0 +1,31 @@
+/* (C)2023 */
+package org.transitclock.api.data.gtfs;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.transit.realtime.GtfsRealtime.FeedMessage;
+import org.transitclock.config.data.ApiConfig;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * For caching GTFS-realtime messages. Useful because the messages are huge and take a lot of
+ * resources so if get multiple requests not too far apart then it makes sense to return a cached
+ * version.
+ *
+ * @author SkiBu Smith
+ */
+public class DataCache {
+ private final Cache cacheMap = CacheBuilder.newBuilder()
+ .expireAfterWrite(ApiConfig.gtfsRtCacheSeconds.getValue(), TimeUnit.SECONDS)
+ .build();
+
+
+ public FeedMessage get(String agencyId) {
+ return cacheMap.getIfPresent(agencyId);
+ }
+
+ public void put(String agencyId, FeedMessage feedMessage) {
+ cacheMap.put(agencyId, feedMessage);
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/gtfs/GtfsRtTripFeed.java b/app/src/main/java/org/transitclock/api/data/gtfs/GtfsRtTripFeed.java
new file mode 100644
index 000000000..5a792e6b0
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/gtfs/GtfsRtTripFeed.java
@@ -0,0 +1,343 @@
+/* (C)2023 */
+package org.transitclock.api.data.gtfs;
+
+import com.google.transit.realtime.GtfsRealtime.*;
+import com.google.transit.realtime.GtfsRealtime.FeedHeader.Incrementality;
+import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeEvent;
+import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate;
+import com.google.transit.realtime.GtfsRealtime.TripUpdate.StopTimeUpdate.ScheduleRelationship;
+import lombok.extern.slf4j.Slf4j;
+import org.transitclock.api.utils.AgencyTimezoneCache;
+import org.transitclock.config.data.ApiConfig;
+import org.transitclock.core.holdingmethod.PredictionTimeComparator;
+import org.transitclock.service.dto.IpcPrediction;
+import org.transitclock.service.dto.IpcPredictionsForRouteStopDest;
+import org.transitclock.service.dto.IpcVehicleConfig;
+import org.transitclock.service.contract.VehiclesInterface;
+import org.transitclock.service.PredictionsServiceImpl;
+import org.transitclock.service.VehiclesServiceImpl;
+import org.transitclock.utils.IntervalTimer;
+import org.transitclock.utils.Time;
+
+import java.rmi.RemoteException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * For creating GTFS-realtime trip feed. The data is obtained from the server via RMI.
+ *
+ *
Note: for the trip feed predictions that are schedule based instead of GPS based the
+ * StopTimeEvent uncertainty is set to SCHED_BASED_PRED_UNCERTAINTY_VALUE so that the client can
+ * treat the prediction differently. If a vehicle is delayed and not moving then uncertainty is set
+ * to DELAYED_UNCERTAINTY_VALUE. And if a vehicle is late and the prediction is for a subsequent
+ * trip then uncertainty is set to LATE_AND_SUBSEQUENT_TRIP_UNCERTAINTY_VALUE.
+ *
+ * @author SkiBu Smith
+ */
+@Slf4j
+public class GtfsRtTripFeed {
+ private static final int PREDICTION_MAX_FUTURE_SECS = ApiConfig.predictionMaxFutureSecs.getValue();
+
+ private static final boolean INCLUDE_TRIP_UPDATE_DELAY = ApiConfig.includeTripUpdateDelay.getValue();
+
+ // For when creating StopTimeEvent for schedule based prediction
+ // 5 minutes (300 seconds)
+ private static final int SCHED_BASED_PRED_UNCERTAINTY_VALUE = 5 * 60;
+
+ // For when creating StopTimeEvent and the vehicle is delayed
+ private static final int DELAYED_UNCERTAINTY_VALUE = SCHED_BASED_PRED_UNCERTAINTY_VALUE + 1;
+
+ // If vehicle is late and prediction is for a subsequent trip then
+ // the predictions are not as certain because it is reasonably likely
+ // that another vehicle will take over the subsequent trip. Takes
+ // precedence over SCHED_BASED_PRED_UNCERTAINTY_VALUE.
+ private static final int LATE_AND_SUBSEQUENT_TRIP_UNCERTAINTY_VALUE = DELAYED_UNCERTAINTY_VALUE + 1;
+
+
+ private final String agencyId;
+
+ // For outputting date in GTFS-realtime format
+ private final SimpleDateFormat gtfsRealtimeDateFormatter = new SimpleDateFormat("yyyyMMdd");
+
+ private final SimpleDateFormat gtfsRealtimeTimeFormatter = new SimpleDateFormat("HH:mm:ss");
+
+ public GtfsRtTripFeed(String agencyId) {
+ this.agencyId = agencyId;
+ this.gtfsRealtimeDateFormatter.setTimeZone(AgencyTimezoneCache.get(agencyId));
+ }
+
+ /**
+ * Create TripUpdate for the trip.
+ *
+ * @param predsForTrip
+ * @return
+ */
+ private TripUpdate createTripUpdate(List predsForTrip) {
+ // Create the parent TripUpdate object that is returned.
+ TripUpdate.Builder tripUpdate = TripUpdate.newBuilder();
+
+ VehiclesInterface vehiclesInterface = VehiclesServiceImpl.instance();
+
+ // Add the trip descriptor information
+ IpcPrediction firstPred = predsForTrip.get(0);
+ TripDescriptor.Builder tripDescriptor = TripDescriptor.newBuilder();
+
+ if (firstPred.getRouteId() != null)
+ tripDescriptor.setRouteId(firstPred.getRouteId());
+
+ if (firstPred.getTripId() != null) {
+ tripDescriptor.setTripId(firstPred.getTripId());
+
+ try {
+ if (firstPred.getFreqStartTime() > 0) {
+ String tripStartTimeStr = gtfsRealtimeTimeFormatter.format(new Date(firstPred.getFreqStartTime()));
+ tripDescriptor.setStartTime(tripStartTimeStr);
+ }
+ } catch (Exception ignored) {
+
+ }
+
+ long tripStartEpochTime = firstPred.getTripStartEpochTime();
+ String tripStartDateStr = gtfsRealtimeDateFormatter.format(new Date(tripStartEpochTime));
+ tripDescriptor.setStartDate(tripStartDateStr);
+
+ // Set the relation between this trip and the static schedule. ADDED and CANCELED not
+ // supported.
+ if (firstPred.isTripUnscheduled()) {
+ // A trip that is running with no schedule associated to it -
+ // this value is used to identify trips defined in GTFS frequencies.txt with
+ // exact_times = 0
+ tripDescriptor.setScheduleRelationship(TripDescriptor.ScheduleRelationship.UNSCHEDULED);
+ } else {
+ // Trip that is running in accordance with its GTFS schedule,
+ // or is close enough to the scheduled trip to be associated with it.
+ tripDescriptor.setScheduleRelationship(TripDescriptor.ScheduleRelationship.SCHEDULED);
+ }
+ }
+
+ // Set trip as canceled if it is mark as that from schedBasePreds
+ if (firstPred.isCanceled())
+ tripDescriptor.setScheduleRelationship(TripDescriptor.ScheduleRelationship.CANCELED);
+
+ tripUpdate.setTrip(tripDescriptor);
+ if (firstPred.getDelay() != null && INCLUDE_TRIP_UPDATE_DELAY)
+ tripUpdate.setDelay(firstPred.getDelay()); // set schedule deviation
+
+ // Add the VehicleDescriptor information
+ VehicleDescriptor.Builder vehicleDescriptor = null;
+
+ Collection vehConfigs = vehiclesInterface.getVehicleConfigs();
+ for (IpcVehicleConfig ipcVehicleConfig : vehConfigs) {
+ if (ipcVehicleConfig.getId().equals(firstPred.getVehicleId())) {
+ vehicleDescriptor = VehicleDescriptor
+ .newBuilder()
+ .setId(ipcVehicleConfig.getId());
+ break;
+ }
+ }
+
+ if (vehicleDescriptor == null)
+ vehicleDescriptor = VehicleDescriptor.newBuilder().setId(firstPred.getVehicleId());
+
+ tripUpdate.setVehicle(vehicleDescriptor);
+
+ // Add the StopTimeUpdate information for each prediction
+ if (!firstPred.isCanceled()) {
+ for (IpcPrediction pred : predsForTrip) {
+ StopTimeUpdate.Builder stopTimeUpdate = StopTimeUpdate.newBuilder()
+ .setStopSequence(pred.getGtfsStopSeq())
+ .setStopId(pred.getStopId());
+
+ StopTimeEvent.Builder stopTimeEvent = StopTimeEvent.newBuilder()
+ .setTime(pred.getPredictionTime() / Time.MS_PER_SEC);
+
+ // If schedule based prediction then set the uncertainty to special
+ // value so that client can tell
+ if (pred.isSchedBasedPred())
+ stopTimeEvent.setUncertainty(SCHED_BASED_PRED_UNCERTAINTY_VALUE);
+
+ // If vehicle is late and prediction is for a subsequent trip then
+ // the predictions are not as certain because it is reasonably likely
+ // that another vehicle will take over the subsequent trip. Takes
+ // precedence over SCHED_BASED_PRED_UNCERTAINTY_VALUE.
+ if (pred.isLateAndSubsequentTripSoMarkAsUncertain())
+ stopTimeEvent.setUncertainty(LATE_AND_SUBSEQUENT_TRIP_UNCERTAINTY_VALUE);
+
+ // If vehicle not making forward progress then set uncertainty to
+ // special value so that client can tell. Takes precedence over
+ // LATE_AND_SUBSEQUENT_TRIP_UNCERTAINTY_VALUE.
+ if (pred.isDelayed())
+ stopTimeEvent.setUncertainty(DELAYED_UNCERTAINTY_VALUE);
+
+ if (pred.isArrival())
+ stopTimeUpdate.setArrival(stopTimeEvent);
+ else
+ stopTimeUpdate.setDeparture(stopTimeEvent);
+
+ // The relationship should always be SCHEDULED if departure or arrival time is
+ // given.
+ stopTimeUpdate.setScheduleRelationship(ScheduleRelationship.SCHEDULED);
+
+ tripUpdate.addStopTimeUpdate(stopTimeUpdate);
+ }
+ }
+ // Add timestamp
+ tripUpdate.setTimestamp(firstPred.getAvlTime() / Time.MS_PER_SEC);
+
+ // Return the results
+ return tripUpdate.build();
+ }
+
+ /**
+ * Creates a GTFS-realtime message for the predictions by trip passed in.
+ *
+ * @param predsByTripMap the data to be put into the GTFS-realtime message
+ * @return the GTFS-realtime FeedMessage
+ */
+ private FeedMessage createMessage(Map> predsByTripMap) {
+ FeedMessage.Builder message = FeedMessage.newBuilder();
+
+ FeedHeader.Builder feedheader = FeedHeader.newBuilder()
+ .setGtfsRealtimeVersion("2.0")
+ .setIncrementality(Incrementality.FULL_DATASET)
+ .setTimestamp(System.currentTimeMillis() / Time.MS_PER_SEC);
+ message.setHeader(feedheader);
+
+ // For each trip...
+ for (List predsForTrip : predsByTripMap.values()) {
+
+ // Need to check if predictions for frequency based trip and group by start time if
+ // they are.
+ if (isFrequencyBasedTrip(predsForTrip)) {
+ try {
+ Map> map = createFreqStartTimePredictionMap(predsForTrip);
+
+ for (Long key : map.keySet()) {
+ if (!map.get(key).isEmpty()) {
+ FeedEntity.Builder feedEntity = FeedEntity.newBuilder()
+ .setId(map.get(key).get(0).getVehicleId());
+ TripUpdate tripUpdate = createTripUpdate(map.get(key));
+ feedEntity.setTripUpdate(tripUpdate);
+ message.addEntity(feedEntity);
+ }
+ }
+ } catch (Exception e) {
+ logger.warn("{}", e.getMessage(), e);
+ }
+ } else {
+ // Create feed entity for each schedule trip
+ FeedEntity.Builder feedEntity = FeedEntity.newBuilder()
+ .setId(predsForTrip.get(0).getTripId());
+ try {
+ TripUpdate tripUpdate = createTripUpdate(predsForTrip);
+ feedEntity.setTripUpdate(tripUpdate);
+
+ message.addEntity(feedEntity);
+ } catch (Exception e) {
+ logger.error("Error parsing trip update data. {}", predsForTrip, e);
+ }
+ }
+ }
+
+ return message.build();
+ }
+
+ private boolean isFrequencyBasedTrip(List predsForTrip) {
+ for (IpcPrediction prediction : predsForTrip) {
+ if (prediction.getFreqStartTime() > 0) return true;
+ }
+ return false;
+ }
+
+ private Map> createFreqStartTimePredictionMap(List predsForTrip) {
+ Map> map = new HashMap<>();
+ for (IpcPrediction prediction : predsForTrip) {
+ if (map.get(prediction.getFreqStartTime()) == null) {
+ List list = new ArrayList<>();
+ map.put(prediction.getFreqStartTime(), list);
+ }
+ map.get(prediction.getFreqStartTime())
+ .add(prediction);
+
+ map.get(prediction.getFreqStartTime())
+ .sort(new PredictionTimeComparator());
+ }
+ return map;
+ }
+
+ /**
+ * Returns map of all predictions for the project. Returns null if there was a problem getting
+ * the data via RMI. There is a separate list of predictions for each trip. The map is keyed by
+ * tripId.
+ *
+ * @return Map keyed on tripId of List of Predictions for the trip, or null if could not get
+ * data from server.
+ */
+ private Map> getPredictionsPerTrip() {
+ // Get all the predictions, grouped by vehicle, from the server
+ List allPredictionsByStop = PredictionsServiceImpl.instance()
+ .getAllPredictions(PREDICTION_MAX_FUTURE_SECS);
+
+ // Group the predictions by trip instead of by vehicle
+ Map> predictionsByTrip = new HashMap<>();
+ for (IpcPredictionsForRouteStopDest predictionsForStop : allPredictionsByStop) {
+ for (IpcPrediction prediction : predictionsForStop.getPredictionsForRouteStop()) {
+ predictionsByTrip
+ .computeIfAbsent(prediction.getTripId(), k -> new ArrayList<>())
+ .add(prediction);
+ }
+ }
+
+ var comparator = Comparator.comparingInt(IpcPrediction::getGtfsStopSeq);
+ predictionsByTrip.forEach((s, ipcPredictions) -> {
+ // Sort trip data according to sequence
+ ipcPredictions.sort(comparator);
+ });
+ // Return results
+ return predictionsByTrip;
+ }
+
+ /**
+ * Gets the Vehicle data from RMI and creates corresponding GTFS-RT vehicle feed.
+ *
+ * @return GTFS-RT FeedMessage for vehicle positions
+ */
+ public FeedMessage createMessage() {
+ // Get prediction data from server
+ IntervalTimer timer = new IntervalTimer();
+ Map> predsByTrip = getPredictionsPerTrip();
+ logger.debug(
+ "Getting predictions via RMI for GtfsRtTripFeed.createMessage() took {} msec",
+ timer.elapsedMsec());
+
+ // Use prediction data to create GTFS-RT message and return it.
+ return createMessage(predsByTrip);
+ }
+
+ // For getPossiblyCachedMessage()
+ private static final DataCache tripFeedDataCache = new DataCache();
+
+ /**
+ * For caching Vehicle Positions feed messages.
+ *
+ * @param agencyId
+ * @return
+ */
+ public static FeedMessage getPossiblyCachedMessage(String agencyId) {
+ FeedMessage feedMessage = tripFeedDataCache.get(agencyId);
+ if (feedMessage != null) return feedMessage;
+
+ synchronized (tripFeedDataCache) {
+
+ // Cache may have been filled while waiting.
+ feedMessage = tripFeedDataCache.get(agencyId);
+ if (feedMessage != null) return feedMessage;
+
+ GtfsRtTripFeed feed = new GtfsRtTripFeed(agencyId);
+ feedMessage = feed.createMessage();
+ tripFeedDataCache.put(agencyId, feedMessage);
+ }
+
+ return feedMessage;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/gtfs/GtfsRtVehicleFeed.java b/app/src/main/java/org/transitclock/api/data/gtfs/GtfsRtVehicleFeed.java
new file mode 100644
index 000000000..f64d55982
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/gtfs/GtfsRtVehicleFeed.java
@@ -0,0 +1,266 @@
+/* (C)2023 */
+package org.transitclock.api.data.gtfs;
+
+import com.google.transit.realtime.GtfsRealtime.*;
+import com.google.transit.realtime.GtfsRealtime.FeedHeader.Incrementality;
+import com.google.transit.realtime.GtfsRealtime.TripDescriptor.ScheduleRelationship;
+import com.google.transit.realtime.GtfsRealtime.VehiclePosition.VehicleStopStatus;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.transitclock.api.utils.AgencyTimezoneCache;
+import org.transitclock.service.VehiclesServiceImpl;
+import org.transitclock.service.contract.VehiclesInterface;
+import org.transitclock.service.dto.IpcAvl;
+import org.transitclock.service.dto.IpcVehicleConfig;
+import org.transitclock.service.dto.IpcVehicleGtfsRealtime;
+import org.transitclock.utils.Time;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Date;
+
+/**
+ * For creating GTFS-realtime Vehicle feed. The data is obtained via RMI.
+ *
+ * @author SkiBu Smith
+ */
+public class GtfsRtVehicleFeed {
+
+ private final String agencyId;
+
+ // For outputting date in GTFS-realtime format
+ private SimpleDateFormat gtfsRealtimeDateFormatter = new SimpleDateFormat("yyyyMMdd");
+
+ private SimpleDateFormat gtfsRealtimeTimeFormatter = new SimpleDateFormat("HH:mm:ss");
+
+ private static final Logger logger = LoggerFactory.getLogger(GtfsRtVehicleFeed.class);
+
+ public GtfsRtVehicleFeed(String agencyId) {
+ this.agencyId = agencyId;
+
+ this.gtfsRealtimeDateFormatter.setTimeZone(AgencyTimezoneCache.get(agencyId));
+ }
+
+ /**
+ * Takes in IpcGtfsRealtimeVehicle and puts it into a GTFS-realtime VehiclePosition object.
+ *
+ * @param vehicleData
+ * @return the resulting VehiclePosition
+ * @throws ParseException
+ */
+ private VehiclePosition createVehiclePosition(IpcVehicleGtfsRealtime vehicleData) throws ParseException {
+ // Create the parent VehiclePosition object that is returned.
+ VehiclePosition.Builder vehiclePosition = VehiclePosition.newBuilder();
+
+ // If there is route information then add it via the TripDescriptor
+ if (vehicleData.getRouteId() != null && !vehicleData.getRouteId().isEmpty()) {
+ String tripStartDateStr = gtfsRealtimeDateFormatter.format(new Date(vehicleData.getTripStartEpochTime()));
+ TripDescriptor.Builder tripDescriptor = TripDescriptor.newBuilder()
+ .setRouteId(vehicleData.getRouteId())
+ .setTripId(vehicleData.getTripId())
+ .setStartDate(tripStartDateStr);
+ if (vehicleData.isCanceled()) tripDescriptor.setScheduleRelationship(ScheduleRelationship.CANCELED);
+ if (vehicleData.getFreqStartTime() > 0) {
+ String tripStartTimeStr = gtfsRealtimeTimeFormatter.format(new Date(vehicleData.getFreqStartTime()));
+ tripDescriptor.setStartTime(tripStartTimeStr);
+ }
+
+ // Set the relation between this trip and the static schedule. ADDED and CANCELED not
+ // supported.
+ if (vehicleData.isTripUnscheduled()) {
+ // A trip that is running with no schedule associated to it -
+ // this value is used to identify trips defined in GTFS frequencies.txt with
+ // exact_times = 0
+ tripDescriptor.setScheduleRelationship(TripDescriptor.ScheduleRelationship.UNSCHEDULED);
+ } else {
+ // Trip that is running in accordance with its GTFS schedule,
+ // or is close enough to the scheduled trip to be associated with it.
+ tripDescriptor.setScheduleRelationship(TripDescriptor.ScheduleRelationship.SCHEDULED);
+ }
+
+ vehiclePosition.setTrip(tripDescriptor);
+ }
+
+ // Add the VehicleDescriptor information
+ VehicleDescriptor.Builder vehicleDescriptor =
+ VehicleDescriptor.newBuilder().setId(vehicleData.getId());
+ // License plate information is optional so only add it if not null
+ if (vehicleData.getLicensePlate() != null) vehicleDescriptor.setLicensePlate(vehicleData.getLicensePlate());
+ vehiclePosition.setVehicle(vehicleDescriptor);
+
+ // Add the Position information
+ Position.Builder position =
+ Position.newBuilder().setLatitude(vehicleData.getLatitude()).setLongitude(vehicleData.getLongitude());
+ // Heading and speed are optional so only add them if actually a
+ // valid number.
+ if (!Float.isNaN(vehicleData.getHeading())) {
+ position.setBearing(vehicleData.getHeading());
+ }
+ if (!Float.isNaN(vehicleData.getSpeed())) {
+ position.setSpeed(vehicleData.getSpeed());
+ }
+ vehiclePosition.setPosition(position);
+
+ // Convert the GPS timestamp information to an epoch time as
+ // number of milliseconds since 1970.
+ long gpsTime = vehicleData.getGpsTime();
+ vehiclePosition.setTimestamp(gpsTime / Time.MS_PER_SEC);
+
+ // Set the stop_id if at a stop or going to a stop
+ String stopId = vehicleData.getAtOrNextStopId();
+ if (stopId != null) vehiclePosition.setStopId(stopId);
+
+ // Set current_status part of vehiclePosition if vehicle is actually
+ // predictable. If not predictable then the vehicle stop status will
+ // not be included in feed since it is not stopped nor in transit to.
+ if (vehicleData.isPredictable()) {
+ VehicleStopStatus currentStatus =
+ vehicleData.isAtStop() ? VehicleStopStatus.STOPPED_AT : VehicleStopStatus.IN_TRANSIT_TO;
+ vehiclePosition.setCurrentStatus(currentStatus);
+
+ if (vehicleData.getAtOrNextGtfsStopSeq() != null)
+ vehiclePosition.setCurrentStopSequence(vehicleData.getAtOrNextGtfsStopSeq());
+ }
+
+ // Return the results
+ return vehiclePosition.build();
+ }
+
+ /**
+ * Creates a GTFS-realtime message for the list of ApiVehicle passed in.
+ *
+ * @param vehicles the data to be put into the GTFS-realtime message
+ * @return the GTFS-realtime FeedMessage
+ */
+ private FeedMessage createMessage(Collection vehicles) {
+ FeedMessage.Builder message = FeedMessage.newBuilder();
+
+ FeedHeader.Builder feedheader = FeedHeader.newBuilder()
+ .setGtfsRealtimeVersion("1.0")
+ .setIncrementality(Incrementality.FULL_DATASET)
+ .setTimestamp(System.currentTimeMillis() / Time.MS_PER_SEC);
+ message.setHeader(feedheader);
+
+ for (IpcVehicleGtfsRealtime vehicle : vehicles) {
+
+ IpcAvl newAvl = new IpcAvl(
+ vehicle.getId(),
+ vehicle.getAvl().getTime(),
+ vehicle.getAvl().getLatitude(),
+ vehicle.getAvl().getLongitude(),
+ vehicle.getAvl().getSpeed(),
+ vehicle.getAvl().getHeading(),
+ vehicle.getAvl().getSource(),
+ vehicle.getAvl().getAssignmentId(),
+ vehicle.getAvl().getAssignmentType(),
+ vehicle.getAvl().getDriverId(),
+ vehicle.getAvl().getLicensePlate(),
+ vehicle.getAvl().getPassengerCount());
+
+ IpcVehicleGtfsRealtime newVehicle = new IpcVehicleGtfsRealtime(
+ vehicle.getBlockId(),
+ vehicle.getBlockAssignmentMethod(),
+ newAvl,
+ vehicle.getHeading(),
+ vehicle.getRouteId(),
+ vehicle.getRouteShortName(),
+ vehicle.getRouteName(),
+ vehicle.getTripId(),
+ vehicle.getTripPatternId(),
+ vehicle.isTripUnscheduled(),
+ vehicle.getDirectionId(),
+ vehicle.getHeadsign(),
+ vehicle.isPredictable(),
+ vehicle.isForSchedBasedPred(),
+ vehicle.getRealTimeSchedAdh(),
+ vehicle.isDelayed(),
+ vehicle.isLayover(),
+ vehicle.getLayoverDepartureTime(),
+ vehicle.getNextStopId(),
+ vehicle.getNextStopName(),
+ vehicle.getVehicleType(),
+ vehicle.getTripStartEpochTime(),
+ vehicle.isAtStop(),
+ vehicle.getAtOrNextStopId(),
+ vehicle.getAtOrNextGtfsStopSeq(),
+ vehicle.getFreqStartTime(),
+ vehicle.getHoldingTime(),
+ vehicle.getPredictedLatitude(),
+ vehicle.getPredictedLongitude(),
+ vehicle.isCanceled());
+ FeedEntity.Builder vehiclePositionEntity = FeedEntity.newBuilder()
+ .setId(vehicle.getVehicleName() == null ? vehicle.getId() : vehicle.getVehicleName());
+
+ try {
+ // vehicle
+ VehiclePosition vehiclePosition = createVehiclePosition(newVehicle);
+ vehiclePositionEntity.setVehicle(vehiclePosition);
+ message.addEntity(vehiclePositionEntity);
+ } catch (Exception e) {
+ logger.error("Error parsing vehicle data for vehicle={}", vehicle, e);
+ }
+ }
+
+ return message.build();
+ }
+
+ /**
+ * Returns collection of all vehicles for the project obtained via RMI. Returns null if there
+ * was a problem getting the data via RMI
+ *
+ * @return Collection of Vehicle objects, or null if not available.
+ */
+ private Collection getVehicles() {
+ VehiclesInterface vehiclesInterface = VehiclesServiceImpl.instance();
+ Collection vehicles = vehiclesInterface.getGtfsRealtime();
+
+ for (IpcVehicleGtfsRealtime ipc : vehicles) {
+ Collection vehConfigs = vehiclesInterface.getVehicleConfigs();
+ for (IpcVehicleConfig ipcVehicleConfig : vehConfigs) {
+ if (ipcVehicleConfig.getId().equals(ipc.getId())) {
+ ipc.setVehicleName(ipcVehicleConfig.getName());
+ break;
+ }
+ }
+ }
+ return vehicles;
+ }
+
+ /**
+ * Gets the Vehicle data from RMI and creates corresponding GTFS-RT vehicle feed.
+ *
+ * @return GTFS-RT FeedMessage for vehicle positions
+ */
+ public FeedMessage createMessage() {
+ Collection vehicles = getVehicles();
+ return createMessage(vehicles);
+ }
+
+ // For getPossiblyCachedMessage()
+ private static final DataCache vehicleFeedDataCache = new DataCache();
+
+ /**
+ * For caching Vehicle Positions feed messages.
+ *
+ * @param agencyId
+ * @return
+ */
+ public static FeedMessage getPossiblyCachedMessage(String agencyId) {
+ FeedMessage feedMessage = vehicleFeedDataCache.get(agencyId);
+ if (feedMessage != null) return feedMessage;
+
+ synchronized (vehicleFeedDataCache) {
+
+ // Cache may have been filled while waiting.
+ feedMessage = vehicleFeedDataCache.get(agencyId);
+ if (feedMessage != null) return feedMessage;
+
+ GtfsRtVehicleFeed feed = new GtfsRtVehicleFeed(agencyId);
+ feedMessage = feed.createMessage();
+ vehicleFeedDataCache.put(agencyId, feedMessage);
+ }
+
+ return feedMessage;
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/siri/SiriFramedVehicleJourneyRef.java b/app/src/main/java/org/transitclock/api/data/siri/SiriFramedVehicleJourneyRef.java
new file mode 100644
index 000000000..e3fe914a7
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/siri/SiriFramedVehicleJourneyRef.java
@@ -0,0 +1,47 @@
+/* (C)2023 */
+package org.transitclock.api.data.siri;
+
+import java.text.DateFormat;
+import java.util.Date;
+import jakarta.xml.bind.annotation.XmlElement;
+import org.transitclock.service.dto.IpcVehicle;
+
+/**
+ * Describes the trip
+ *
+ * @author SkiBu Smith
+ */
+public class SiriFramedVehicleJourneyRef {
+
+ // The GTFS service date for the trip the vehicle is serving
+ @XmlElement(name = "DataFrameRef")
+ private String dataFrameRef;
+
+ // Trip ID from GTFS
+ @XmlElement(name = "DatedVehicleJourneyRef")
+ private String datedVehicleJourneyRef;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey for JSON. Otherwise get really obtuse "MessageBodyWriter
+ * not found for media type=application/json" exception.
+ */
+ protected SiriFramedVehicleJourneyRef() {}
+
+ /**
+ * Constructor
+ *
+ * @param vehicle
+ * @param dateFormatter
+ */
+ public SiriFramedVehicleJourneyRef(IpcVehicle vehicle, DateFormat dateFormatter) {
+ // FIXME Note: dataFrameRef is not correct. It should use
+ // the service date, not the GPS time. When assignment spans
+ // midnight this will be wrong. But of course this isn't too
+ // important because if a client would actually want such info
+ // they would want service ID, not the date. Sheesh.
+ dataFrameRef = dateFormatter.format(new Date(vehicle.getGpsTime()));
+ datedVehicleJourneyRef = vehicle.getTripId();
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/siri/SiriLocation.java b/app/src/main/java/org/transitclock/api/data/siri/SiriLocation.java
new file mode 100644
index 000000000..b5bcd062a
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/siri/SiriLocation.java
@@ -0,0 +1,32 @@
+/* (C)2023 */
+package org.transitclock.api.data.siri;
+
+import jakarta.xml.bind.annotation.XmlElement;
+import org.transitclock.utils.Geo;
+
+/**
+ * Location object for SIRI
+ *
+ * @author SkiBu Smith
+ */
+public class SiriLocation {
+
+ @XmlElement(name = "Longitude")
+ private String longitude;
+
+ @XmlElement(name = "Latitude")
+ private String latitude;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey for JSON. Otherwise get really obtuse "MessageBodyWriter
+ * not found for media type=application/json" exception.
+ */
+ protected SiriLocation() {}
+
+ public SiriLocation(double latitude, double longitude) {
+ this.longitude = Geo.format(longitude);
+ this.latitude = Geo.format(latitude);
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/siri/SiriMonitoredCall.java b/app/src/main/java/org/transitclock/api/data/siri/SiriMonitoredCall.java
new file mode 100644
index 000000000..d7abf1770
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/siri/SiriMonitoredCall.java
@@ -0,0 +1,121 @@
+/* (C)2023 */
+package org.transitclock.api.data.siri;
+
+import java.text.DateFormat;
+import java.util.Date;
+import jakarta.xml.bind.annotation.XmlElement;
+import org.transitclock.service.dto.IpcPrediction;
+import org.transitclock.service.dto.IpcVehicleComplete;
+import org.transitclock.utils.StringUtils;
+
+/**
+ * For SIRI MonitorCall element.
+ *
+ * @author SkiBu Smith
+ */
+public class SiriMonitoredCall {
+
+ @XmlElement(name = "StopPointRef")
+ private String stopPointRef;
+
+ @XmlElement(name = "VisitNumber")
+ private int visitNumber;
+
+ // The arrival/departure time elements were found in
+ // http://user47094.vs.easily.co.uk/siri/schema/1.4/examples/exs_stopMonitoring_response.xml
+ // Scheduled time not currently available via IPC so not available here.
+ @XmlElement(name = "AimedArrivalTime")
+ String aimedArrivalTime;
+
+ // Predicted arrival time
+ @XmlElement(name = "ExpectedArrivalTime")
+ String expectedArrivalTime;
+
+ // Scheduled time not currently available via IPC so not available here.
+ @XmlElement(name = "AimedDepartureTime")
+ String aimedDepartureTime;
+
+ // Predicted departure time
+ @XmlElement(name = "ExpectedDepartureTime")
+ String expectedDepartureTime;
+
+ // NYC MTA extensions
+ @XmlElement(name = "Extensions")
+ private Extensions extensions;
+
+ /** The MTA Bus Time extensions to show distance of the vehicle from the stop */
+ public static class Extensions {
+ @XmlElement(name = "Distances")
+ private Distances distances;
+
+ /**
+ * Need a no-arg constructor for Jersey for JSON. Otherwise get really obtuse
+ * "MessageBodyWriter not found for media type=application/json" exception.
+ */
+ protected Extensions() {}
+
+ public Extensions(IpcVehicleComplete ipcCompleteVehicle) {
+ distances = new Distances(ipcCompleteVehicle);
+ }
+ }
+
+ /** The MTA Bus Time extensions to show distance of the vehicle from the stop */
+ public static class Distances {
+ // The distance of the stop from the beginning of the trip/route
+ @XmlElement(name = "CallDistanceAlongRoute")
+ private String callDistanceAlongRoute;
+
+ // The distance from the vehicle to the stop along the route, in meters
+ @XmlElement(name = "DistanceFromCall")
+ private String distanceFromCall;
+
+ /**
+ * Need a no-arg constructor for Jersey for JSON. Otherwise get really obtuse
+ * "MessageBodyWriter not found for media type=application/json" exception.
+ */
+ protected Distances() {}
+
+ public Distances(IpcVehicleComplete ipcCompleteVehicle) {
+ callDistanceAlongRoute =
+ StringUtils.oneDigitFormat(ipcCompleteVehicle.getDistanceOfNextStopFromTripStart());
+
+ distanceFromCall = StringUtils.oneDigitFormat(ipcCompleteVehicle.getDistanceToNextStop());
+ }
+ }
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey for JSON. Otherwise get really obtuse "MessageBodyWriter
+ * not found for media type=application/json" exception.
+ */
+ protected SiriMonitoredCall() {}
+
+ /**
+ * Constructs a MonitoredCall element.
+ *
+ * @param ipcCompleteVehicle
+ * @param prediction The prediction for when doing stop monitoring. When doing vehicle
+ * monitoring should be set to null.
+ * @param timeFormatter For converting epoch time into a Siri time string
+ */
+ public SiriMonitoredCall(
+ IpcVehicleComplete ipcCompleteVehicle, IpcPrediction prediction, DateFormat timeFormatter) {
+ stopPointRef = ipcCompleteVehicle.getNextStopId();
+ // Always using value of 1 for now
+ visitNumber = 1;
+
+ // Deal with the predictions if StopMonitoring query.
+ // Don't have schedule time available so can't provide it.
+ if (prediction != null) {
+ if (prediction.isArrival()) {
+ expectedArrivalTime = timeFormatter.format(new Date(prediction.getPredictionTime()));
+ } else {
+ expectedDepartureTime = timeFormatter.format(new Date(prediction.getPredictionTime()));
+ }
+ }
+
+ // Deal with NYC MTA extensions
+ extensions = new Extensions(ipcCompleteVehicle);
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/siri/SiriMonitoredVehicleJourney.java b/app/src/main/java/org/transitclock/api/data/siri/SiriMonitoredVehicleJourney.java
new file mode 100644
index 000000000..5a620a115
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/siri/SiriMonitoredVehicleJourney.java
@@ -0,0 +1,134 @@
+/* (C)2023 */
+package org.transitclock.api.data.siri;
+
+import java.text.DateFormat;
+import java.util.Date;
+import jakarta.xml.bind.annotation.XmlElement;
+import org.transitclock.service.dto.IpcPrediction;
+import org.transitclock.service.dto.IpcVehicleComplete;
+
+/**
+ * For SIRI MonitoredVehicleJourney element
+ *
+ * @author SkiBu Smith
+ */
+public class SiriMonitoredVehicleJourney {
+
+ // Vehicle Id
+ @XmlElement(name = "VehicleRef")
+ private String vehicleRef;
+
+ // Location of vehicle
+ @XmlElement(name = "VehicleLocation")
+ private SiriLocation vehicleLocation;
+
+ // Vehicle bearing: 0 is East, increments counter-clockwise.
+ // This of course is different from heading, where 0 is north
+ // and it goes clockwise.
+ @XmlElement(name = "Bearing")
+ private String bearingStr;
+
+ // Block ID
+ @XmlElement(name = "BlockRef")
+ private String blockRef;
+
+ // The route name
+ @XmlElement(name = "LineRef")
+ private String lineRef;
+
+ // The GTFS direction
+ @XmlElement(name = "DirectionRef")
+ private String directionRef;
+
+ // Describes the trip
+ @XmlElement(name = "FramedVehicleJourneyRef")
+ private SiriFramedVehicleJourneyRef framedVehicleJourneyRef;
+
+ // Name of route. Using short name since that is available and is
+ // more relevant.
+ @XmlElement(name = "PublishedLineName")
+ private String publishedLineName;
+
+ // Name of agency
+ @XmlElement(name = "OperatorRef")
+ private String operatorRef;
+
+ @XmlElement(name = "OriginRef")
+ private String originRef;
+
+ @XmlElement(name = "DestinationRef")
+ private String destinationRef;
+
+ @XmlElement(name = "DestinationName")
+ private String destinationName;
+
+ @XmlElement(name = "OriginAimedDepartureTime")
+ private String originAimedDepartureTime;
+
+ // Whether vehicle tracked
+ @XmlElement(name = "Monitored")
+ private String monitored;
+
+ // Indicator of whether the bus is making progress (i.e. moving, generally)
+ // or not (with value noProgress).
+ @XmlElement(name = "ProgressRate")
+ private String progressRate;
+
+ @XmlElement(name = "ProgressStatus")
+ private String progressStatus;
+
+ @XmlElement(name = "MonitoredCall")
+ private SiriMonitoredCall monitoredCall;
+
+ @XmlElement(name = "OnwardCalls")
+ private String onwardCalls;
+
+ /********************** Member Functions **************************/
+
+ /**
+ * Need a no-arg constructor for Jersey for JSON. Otherwise get really obtuse "MessageBodyWriter
+ * not found for media type=application/json" exception.
+ */
+ protected SiriMonitoredVehicleJourney() {}
+
+ /**
+ * Constructs that massive MonitoredVehicleJourney element.
+ *
+ * @param ipcCompleteVehicle
+ * @param prediction For when doing stop monitoring. If doing vehicle monitoring then should be
+ * set to null.
+ * @param agencyId
+ * @param timeFormatter For converting epoch time into a Siri time string
+ * @param dateFormatter For converting epoch time into a Siri date string
+ */
+ public SiriMonitoredVehicleJourney(
+ IpcVehicleComplete ipcCompleteVehicle,
+ IpcPrediction prediction,
+ String agencyId,
+ DateFormat timeFormatter,
+ DateFormat dateFormatter) {
+ vehicleRef = ipcCompleteVehicle.getId();
+ vehicleLocation = new SiriLocation(ipcCompleteVehicle.getLatitude(), ipcCompleteVehicle.getLongitude());
+ double bearing = 90 - ipcCompleteVehicle.getHeading();
+ if (bearing < 0) bearing += 360.0;
+ bearingStr = Double.toString(bearing);
+ blockRef = ipcCompleteVehicle.getBlockId();
+ lineRef = ipcCompleteVehicle.getRouteShortName();
+ directionRef = ipcCompleteVehicle.getDirectionId();
+ framedVehicleJourneyRef = new SiriFramedVehicleJourneyRef(ipcCompleteVehicle, dateFormatter);
+ publishedLineName = ipcCompleteVehicle.getRouteName();
+ operatorRef = agencyId;
+ originRef = ipcCompleteVehicle.getOriginStopId();
+ destinationRef = ipcCompleteVehicle.getDestinationId();
+ destinationName = ipcCompleteVehicle.getHeadsign();
+ originAimedDepartureTime = timeFormatter.format(new Date(ipcCompleteVehicle.getTripStartEpochTime()));
+ monitored = "true";
+ progressRate = "normalProgress";
+ progressStatus = ipcCompleteVehicle.isLayover() ? "true" : null;
+
+ monitoredCall = new SiriMonitoredCall(ipcCompleteVehicle, prediction, timeFormatter);
+
+ // Not currently implemented but outputting it for completeness
+ onwardCalls = "";
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/siri/SiriStopMonitoring.java b/app/src/main/java/org/transitclock/api/data/siri/SiriStopMonitoring.java
new file mode 100644
index 000000000..3d42b62d1
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/siri/SiriStopMonitoring.java
@@ -0,0 +1,179 @@
+/* (C)2023 */
+package org.transitclock.api.data.siri;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import jakarta.xml.bind.annotation.XmlTransient;
+import jakarta.xml.bind.annotation.XmlType;
+import org.transitclock.api.utils.AgencyTimezoneCache;
+import org.transitclock.service.dto.IpcPrediction;
+import org.transitclock.service.dto.IpcPredictionsForRouteStopDest;
+import org.transitclock.service.dto.IpcVehicleComplete;
+import org.transitclock.utils.Time;
+
+/**
+ * Top level XML element for SIRI StopMonitoring command.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "Siri")
+@XmlType(propOrder = {"version", "xmlns", "delivery"})
+public class SiriStopMonitoring {
+ @XmlAttribute
+ private String version = "1.3";
+
+ @XmlAttribute
+ private String xmlns = "http://www.siri.org.uk/siri";
+
+ @XmlElement(name = "ServiceDelivery")
+ private SiriServiceDelivery delivery;
+
+ @XmlTransient
+ // Defines how times should be output in Siri
+ private final DateFormat siriDateTimeFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
+
+ // Defines how dates should be output in Siri
+ @XmlTransient
+ private final DateFormat siriDateFormat = new SimpleDateFormat("yyyy-MM-dd");
+
+ /** Simple sub-element so using internal class. */
+ private static class SiriServiceDelivery {
+ @XmlElement(name = "ResponseTimestamp")
+ private String responseTimestamp;
+
+ @XmlElement(name = "StopMonitoringDelivery ")
+ private SiriStopMonitoringDelivery stopMonitoringDelivery;
+
+ /**
+ * Need a no-arg constructor for Jersey for JSON. Otherwise get really obtuse
+ * "MessageBodyWriter not found for media type=application/json" exception.
+ */
+ @SuppressWarnings("unused")
+ protected SiriServiceDelivery() {}
+
+ public SiriServiceDelivery(
+ List preds,
+ Collection vehicles,
+ String agencyId,
+ DateFormat timeFormatter,
+ DateFormat dateFormatter) {
+ responseTimestamp = timeFormatter.format(new Date(System.currentTimeMillis()));
+ stopMonitoringDelivery =
+ new SiriStopMonitoringDelivery(preds, vehicles, agencyId, timeFormatter, dateFormatter);
+ }
+ }
+
+ /** Simple sub-element so using internal class. */
+ private static class SiriStopMonitoringDelivery {
+ // Required by SIRI spec
+ @XmlAttribute
+ private String version = "1.3";
+
+ // Required by SIRI spec
+ @XmlElement(name = "ResponseTimestamp")
+ private String responseTimestamp;
+
+ // Required by SIRI spec
+ @XmlElement(name = "ValidUntil")
+ private String validUntil;
+
+ // Contains prediction and vehicle info. One per prediction
+ @XmlElement(name = "MonitoredStopVisit")
+ private List monitoredStopVisitList;
+
+ /**
+ * Need a no-arg constructor for Jersey for JSON. Otherwise get really obtuse
+ * "MessageBodyWriter not found for media type=application/json" exception.
+ */
+ @SuppressWarnings("unused")
+ protected SiriStopMonitoringDelivery() {}
+
+ public SiriStopMonitoringDelivery(
+ List preds,
+ Collection vehicles,
+ String agencyId,
+ DateFormat timeFormatter,
+ DateFormat dateFormatter) {
+ long currentTime = System.currentTimeMillis();
+ responseTimestamp = timeFormatter.format(new Date(currentTime));
+ validUntil = timeFormatter.format(new Date(currentTime + 2 * Time.MS_PER_MIN));
+
+ // For each prediction create a MonitoredStopVisit
+ monitoredStopVisitList = new ArrayList();
+ for (IpcPredictionsForRouteStopDest predForRouteStopDest : preds) {
+ for (IpcPrediction pred : predForRouteStopDest.getPredictionsForRouteStop()) {
+ // Determine vehicle info associated with prediction
+ IpcVehicleComplete vehicle = getVehicle(pred, vehicles);
+
+ // Created the MonitoredStopVisit for the prediction
+ monitoredStopVisitList.add(
+ new SiriMonitoredStopVisit(vehicle, pred, agencyId, timeFormatter, dateFormatter));
+ }
+ }
+ }
+
+ /**
+ * Determines IpcExtVehicle object for the specified prediction
+ *
+ * @param pred
+ * @param vehicleId
+ * @return
+ */
+ private IpcVehicleComplete getVehicle(IpcPrediction pred, Collection vehicles) {
+ String vehicleId = pred.getVehicleId();
+ for (IpcVehicleComplete vehicle : vehicles) {
+ if (vehicle.getId().equals(vehicleId)) return vehicle;
+ }
+
+ // Didn't find the vehicle so return null
+ return null;
+ }
+ }
+
+ /** Simple sub-element so using internal class. */
+ private static class SiriMonitoredStopVisit {
+ // GPS time for vehicle
+ @XmlElement(name = "RecordedAtTime")
+ private String recordedAtTime;
+
+ @XmlElement(name = "MonitoredVehicleJourney")
+ SiriMonitoredVehicleJourney monitoredVehicleJourney;
+
+ /**
+ * Need a no-arg constructor for Jersey for JSON. Otherwise get really obtuse
+ * "MessageBodyWriter not found for media type=application/json" exception.
+ */
+ @SuppressWarnings("unused")
+ protected SiriMonitoredStopVisit() {}
+
+ public SiriMonitoredStopVisit(
+ IpcVehicleComplete ipcCompleteVehicle,
+ IpcPrediction prediction,
+ String agencyId,
+ DateFormat timeFormatter,
+ DateFormat dateFormatter) {
+ recordedAtTime = timeFormatter.format(new Date(ipcCompleteVehicle.getGpsTime()));
+ monitoredVehicleJourney = new SiriMonitoredVehicleJourney(
+ ipcCompleteVehicle, prediction, agencyId, timeFormatter, dateFormatter);
+ }
+ }
+
+ // No-args needed because this class is an XML root element
+ protected SiriStopMonitoring() {}
+
+ public SiriStopMonitoring(
+ List preds, Collection vehicles, String agencyId) {
+ // Set the time zones for the date formatters
+ siriDateTimeFormat.setTimeZone(AgencyTimezoneCache.get(agencyId));
+ siriDateFormat.setTimeZone(AgencyTimezoneCache.get(agencyId));
+
+ delivery = new SiriServiceDelivery(preds, vehicles, agencyId, siriDateTimeFormat, siriDateFormat);
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/data/siri/SiriVehiclesMonitoring.java b/app/src/main/java/org/transitclock/api/data/siri/SiriVehiclesMonitoring.java
new file mode 100644
index 000000000..77963acc7
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/data/siri/SiriVehiclesMonitoring.java
@@ -0,0 +1,151 @@
+/* (C)2023 */
+package org.transitclock.api.data.siri;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import jakarta.xml.bind.annotation.XmlAttribute;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import jakarta.xml.bind.annotation.XmlTransient;
+import jakarta.xml.bind.annotation.XmlType;
+import org.transitclock.api.utils.AgencyTimezoneCache;
+import org.transitclock.service.dto.IpcVehicleComplete;
+import org.transitclock.utils.Time;
+
+/**
+ * Top level XML element for SIRI VehicleMonitoring command.
+ *
+ * @author SkiBu Smith
+ */
+@XmlRootElement(name = "Siri")
+@XmlType(propOrder = {"version", "xmlns", "delivery"})
+public class SiriVehiclesMonitoring {
+
+ @XmlAttribute
+ private String version = "1.3";
+
+ @XmlAttribute
+ private String xmlns = "http://www.siri.org.uk/siri";
+
+ @XmlElement(name = "ServiceDelivery")
+ private SiriServiceDelivery delivery;
+
+ @XmlTransient
+ // Defines how times should be output in Siri
+ private final DateFormat siriDateTimeFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
+
+ // Defines how dates should be output in Siri
+ @XmlTransient
+ private final DateFormat siriDateFormat = new SimpleDateFormat("yyyy-MM-dd");
+
+ /** Simple sub-element so using internal class. */
+ private static class SiriServiceDelivery {
+ @XmlElement(name = "ResponseTimestamp")
+ private String responseTimestamp;
+
+ @XmlElement(name = "VehicleMonitoringDelivery ")
+ private SiriVehicleMonitoringDelivery vehicleMonitoringDelivery;
+
+ /**
+ * Need a no-arg constructor for Jersey for JSON. Otherwise get really obtuse
+ * "MessageBodyWriter not found for media type=application/json" exception.
+ */
+ @SuppressWarnings("unused")
+ protected SiriServiceDelivery() {}
+
+ public SiriServiceDelivery(
+ Collection vehicles,
+ String agencyId,
+ DateFormat timeFormatter,
+ DateFormat dateFormatter) {
+ responseTimestamp = timeFormatter.format(new Date(System.currentTimeMillis()));
+ vehicleMonitoringDelivery =
+ new SiriVehicleMonitoringDelivery(vehicles, agencyId, timeFormatter, dateFormatter);
+ }
+ }
+
+ /** Simple sub-element so using internal class. */
+ private static class SiriVehicleMonitoringDelivery {
+ // Required by SIRI spec
+ @XmlAttribute
+ private String version = "1.3";
+
+ // Required by SIRI spec
+ @XmlElement(name = "ResponseTimestamp")
+ private String responseTimestamp;
+
+ // Required by SIRI spec
+ @XmlElement(name = "ValidUntil")
+ private String validUntil;
+
+ @XmlElement(name = "VehicleActivity")
+ private List vehicleActivityList;
+
+ /**
+ * Need a no-arg constructor for Jersey for JSON. Otherwise get really obtuse
+ * "MessageBodyWriter not found for media type=application/json" exception.
+ */
+ @SuppressWarnings("unused")
+ protected SiriVehicleMonitoringDelivery() {}
+
+ public SiriVehicleMonitoringDelivery(
+ Collection vehicles,
+ String agencyId,
+ DateFormat timeFormatter,
+ DateFormat dateFormatter) {
+ long currentTime = System.currentTimeMillis();
+ responseTimestamp = timeFormatter.format(new Date(currentTime));
+ validUntil = timeFormatter.format(new Date(currentTime + 2 * Time.MS_PER_MIN));
+
+ vehicleActivityList = new ArrayList();
+ for (IpcVehicleComplete vehicle : vehicles) {
+ vehicleActivityList.add(new SiriVehicleActivity(vehicle, agencyId, timeFormatter, dateFormatter));
+ }
+ }
+ }
+
+ /** Simple sub-element so using internal class. */
+ private static class SiriVehicleActivity {
+ // GPS time for vehicle
+ @XmlElement(name = "RecordedAtTime")
+ private String recordedAtTime;
+
+ // Addition SIRI MonitoredVehicleJourney element
+ @XmlElement(name = "MonitoredVehicleJourney")
+ private SiriMonitoredVehicleJourney monitoredVehicleJourney;
+
+ /**
+ * Need a no-arg constructor for Jersey for JSON. Otherwise get really obtuse
+ * "MessageBodyWriter not found for media type=application/json" exception.
+ */
+ @SuppressWarnings("unused")
+ protected SiriVehicleActivity() {}
+
+ public SiriVehicleActivity(
+ IpcVehicleComplete ipcCompleteVehicle,
+ String agencyId,
+ DateFormat timeFormatter,
+ DateFormat dateFormatter) {
+ recordedAtTime = timeFormatter.format(new Date(ipcCompleteVehicle.getGpsTime()));
+ monitoredVehicleJourney =
+ new SiriMonitoredVehicleJourney(ipcCompleteVehicle, null, agencyId, timeFormatter, dateFormatter);
+ }
+ }
+
+ /********************** Member Functions **************************/
+
+ // No-args needed because this class is an XML root element
+ protected SiriVehiclesMonitoring() {}
+
+ public SiriVehiclesMonitoring(Collection vehicles, String agencyId) {
+ // Set the time zones for the date formatters
+ siriDateTimeFormat.setTimeZone(AgencyTimezoneCache.get(agencyId));
+ siriDateFormat.setTimeZone(AgencyTimezoneCache.get(agencyId));
+
+ delivery = new SiriServiceDelivery(vehicles, agencyId, siriDateTimeFormat, siriDateFormat);
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/reports/AvlJsonQuery.java b/app/src/main/java/org/transitclock/api/reports/AvlJsonQuery.java
new file mode 100644
index 000000000..4eafa4888
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/reports/AvlJsonQuery.java
@@ -0,0 +1,156 @@
+/* (C)2023 */
+package org.transitclock.api.reports;
+
+import org.transitclock.core.reports.GenericJsonQuery;
+import org.transitclock.utils.Time;
+
+import java.text.ParseException;
+
+/**
+ * Does a query of AVL data and returns result in JSON format.
+ *
+ * @author SkiBu Smith
+ */
+public class AvlJsonQuery {
+ // Maximum number of rows that can be retrieved by a query
+ private static final int MAX_ROWS = 50000;
+
+ /**
+ * Queries agency for AVL data and returns result as a JSON string. Limited to returning
+ * MAX_ROWS (50,000) data points.
+ *
+ * @param agencyId
+ * @param vehicleId Which vehicle to get data for. Set to null or empty string to get data for
+ * all vehicles
+ * @param beginDate date to start query
+ * @param numdays of days to collect data for
+ * @param beginTime optional time of day during the date range
+ * @param endTime optional time of day during the date range
+ * @return AVL reports in JSON format. Can be empty JSON array if no data meets criteria.
+ */
+ public static String getAvlJson(
+ String agencyId, String vehicleId, String beginDate, String numdays, String beginTime, String endTime) {
+ // Determine the time portion of the SQL
+ String timeSql = "";
+ // If beginTime or endTime set but not both then use default values
+ if ((beginTime != null && !beginTime.isEmpty()) || (endTime != null && !endTime.isEmpty())) {
+ if (beginTime == null || beginTime.isEmpty()) beginTime = "00:00";
+ if (endTime == null || endTime.isEmpty()) endTime = "24:00";
+ }
+ // cast('2000-01-01 01:12:00'::timestamp as time);
+ if (beginTime != null && !beginTime.isEmpty()) {
+ timeSql = " AND cast(time::timestamp as time) BETWEEN '" + beginTime + "' AND '" + endTime + "' ";
+ }
+
+ String sql = "SELECT vehicle_id, name, time, assignment_id, lat, lon, speed, heading,"
+ + " time_processed, source FROM avl_reports INNER JOIN vehicle_configs ON"
+ + " vehicle_configs.id = avl_reports.vehicle_id WHERE time BETWEEN cast(? as"
+ + " timestamp) AND cast(? as timestamp) + INTERVAL '"
+ + numdays
+ + " day' "
+ + timeSql;
+
+ // If only want data for single vehicle then specify so in SQL
+ if (vehicleId != null && !vehicleId.isEmpty()) {
+ sql += " AND vehicle_id='" + vehicleId + "' ";
+ }
+
+ // Make sure data is ordered by vehicleId so that can draw lines
+ // connecting the AVL reports per vehicle properly. Also then need
+ // to order by time to make sure they are in proper order. And
+ // lastly, limit AVL reports to 5000 so that someone doesn't try
+ // to view too much data at once.
+
+ sql += "ORDER BY vehicle_id, time LIMIT " + MAX_ROWS;
+
+ try {
+ java.util.Date startdate = Time.parseDate(beginDate);
+
+ return GenericJsonQuery.getJsonString(agencyId, sql, startdate, startdate);
+ } catch (ParseException e) {
+ return e.getMessage();
+ }
+ }
+
+ /**
+ * Queries agency for AVL data and corresponding Match and Trip data. By joining in Match and
+ * Trip data can see what the block and trip IDs, the routeShortName, and possibly other
+ * information, for each AVL report. Returns result as a JSON string. Limited to returning
+ * MAX_ROWS (50,000) data points.
+ *
+ * @param agencyId
+ * @param vehicleId Which vehicle to get data for. Set to empty string to get data for all
+ * vehicles. If null then will get data by route.
+ * @param routeId Which route to get data for. Set to empty string to get data for all routes.
+ * If null then will get data by vehicle.
+ * @param beginDate date to start query
+ * @param numdays of days to collect data for
+ * @param beginTime optional time of day during the date range
+ * @param endTime optional time of day during the date range
+ * @return AVL reports in JSON format. Can be empty JSON array if no data meets criteria.
+ */
+ public static String getAvlWithMatchesJson(
+ String agencyId,
+ String vehicleId,
+ String routeId,
+ String beginDate,
+ String numdays,
+ String beginTime,
+ String endTime) {
+ // Determine the time portion of the SQL
+ // If beginTime or endTime set but not both then use default values
+ if ((beginTime != null && !beginTime.isEmpty()) || (endTime != null && !endTime.isEmpty())) {
+ if (beginTime == null || beginTime.isEmpty()) {
+ beginTime = "00:00";
+ }
+ if (endTime == null || endTime.isEmpty()) {
+ endTime = "24:00";
+ }
+ }
+
+ String timeSql = "";
+ if (beginTime != null && !beginTime.isEmpty()) {
+ timeSql = " AND time::time BETWEEN '" + beginTime + "' AND '" + endTime + "' ";
+ }
+
+ String sql = "SELECT a.vehicle_id, a.time, a.assignment_id, a.lat, a.lon, "
+ + " a.speed, a.heading, a.time_processed, "
+ + " vs.block_id, vs.trip_id, vs.trip_short_name, vs.route_id, "
+ + " vs.route_short_name, vs.schedule_adherence_msec, vs.schedule_adherence, "
+ + " vs.is_delayed, vs.is_layover, vs.is_wait_stop "
+ + "FROM avl_reports a "
+ + " LEFT JOIN vehicle_states vs "
+ + " ON vs.vehicle_id = a.vehicle_id AND vs.avl_time = a.time "
+ + "WHERE a.time BETWEEN '"
+ + beginDate
+ + "' "
+ + " AND TIMESTAMP '"
+ + beginDate
+ + "' + INTERVAL '"
+ + numdays
+ + " day' "
+ + timeSql;
+
+ // If only want data for single route then specify so in SQL.
+ // Since some agencies like sfmta don't have consistent route IDs
+ // across schedule changes need to try to match to GTFS route_id or
+ // route_short_name.
+ if (vehicleId == null && routeId != null && !routeId.trim().isEmpty()) {
+ sql += "AND (vs.route_id='" + routeId + "' OR vs.route_short_name='" + routeId + "') ";
+ }
+
+ // If only want data for single vehicle then specify so in SQL
+ if (vehicleId != null && !vehicleId.trim().isEmpty()) {
+ sql += "AND a.vehicle_id='" + vehicleId + "' ";
+ }
+
+ // Make sure data is ordered by vehicleId so that can draw lines
+ // connecting the AVL reports per vehicle properly. Also then need
+ // to order by time to make sure they are in proper order. And
+ // lastly, limit AVL reports to 5000 so that someone doesn't try
+ // to view too much data at once.
+ sql += "ORDER BY a.vehicle_id, time LIMIT " + MAX_ROWS;
+
+ return GenericJsonQuery.getJsonString(agencyId, sql);
+ }
+}
diff --git a/app/src/main/java/org/transitclock/api/reports/ChartGenericJsonQuery.java b/app/src/main/java/org/transitclock/api/reports/ChartGenericJsonQuery.java
new file mode 100644
index 000000000..f06f9e3ff
--- /dev/null
+++ b/app/src/main/java/org/transitclock/api/reports/ChartGenericJsonQuery.java
@@ -0,0 +1,84 @@
+/* (C)2023 */
+package org.transitclock.api.reports;
+
+import lombok.extern.slf4j.Slf4j;
+import org.transitclock.api.reports.ChartJsonBuilder.RowBuilder;
+import org.transitclock.domain.GenericQuery;
+
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.List;
+
+/**
+ * For providing data to a Google scatter chart when need to specify specific SQL for retrieving
+ * data from the database. Since any SQL statement can be used this feed can be used for generating
+ * a variety of scatter charts.
+ *
+ * @author SkiBu Smith
+ */
+@Slf4j
+public class ChartGenericJsonQuery extends GenericQuery {
+ private final ChartJsonBuilder jsonBuilder;
+
+
+ /**
+ * @param agencyId
+ * @throws SQLException
+ */
+ public ChartGenericJsonQuery(String agencyId) throws SQLException {
+ super(agencyId);
+ jsonBuilder = new ChartJsonBuilder();
+ }
+
+ /* (non-Javadoc)
+ * @see org.transitclock.db.GenericQuery#addColumn(java.lang.String, int)
+ */
+ @Override
+ protected void addColumn(String columnName, int type) {
+ if (columnName.equals("tooltip")) jsonBuilder.addTooltipColumn();
+ else if (type == Types.NUMERIC
+ || type == Types.INTEGER
+ || type == Types.SMALLINT
+ || type == Types.BIGINT
+ || type == Types.DECIMAL
+ || type == Types.FLOAT
+ || type == Types.DOUBLE) jsonBuilder.addNumberColumn(columnName);
+ else if (type == Types.VARCHAR) jsonBuilder.addStringColumn(columnName);
+ else logger.error("Unknown type={} for columnName={}", type, columnName);
+ }
+
+ /* (non-Javadoc)
+ * @see org.transitclock.db.GenericQuery#addRow(java.util.List)
+ */
+ @Override
+ protected void addRow(List