diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..665ecfdf9
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,11 @@
+**Summary:**
+
+Summarize the changes in the pull request including how it relates to any issues (include the issue number, or link them).
+
+**Expected behavior:**
+
+Explain how you expect the pull request to work in your testing (in case other platforms/versions exhibit different behavior).
+
+Please make sure these boxes are checked before submitting your pull request - thanks!
+
+- [ ] Linked all relevant issues
diff --git a/.github/release.yml b/.github/release.yml
new file mode 100644
index 000000000..a03049b4e
--- /dev/null
+++ b/.github/release.yml
@@ -0,0 +1,14 @@
+changelog:
+ categories:
+ - title: Breaking changes
+ labels:
+ - "Breaking change"
+ - title: Bugfixes
+ labels:
+ - bug
+ - title: Non-breaking changes
+ labels:
+ - "*"
+ - title: Dependency upgrades
+ labels:
+ - dependencies
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f516e09e6..2c9d90c5d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,36 +5,83 @@ on:
pull_request:
branches: [ master ]
+env:
+ MAVEN_ARGS: "--no-transfer-progress -Dstyle.color=always"
+
jobs:
- build:
+ build:
runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ locale: [ "en_US.utf8", "fr_FR.utf8" ]
+
+ env:
+ LANG: ${{ matrix.locale }}
+
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- - name: Set up JDK
- uses: actions/setup-java@v1
- with:
- java-version: 11
+ - name: Set locale to ${{ matrix.locale }}
+ run: |
+ lang=`echo "${{ matrix.locale }}" | head -c 2`
+ sudo apt-get -qq install -y language-pack-${lang}
+
+ echo ""
+
+ # list installed locales
+ echo "Available locales"
+ locale -a
+ sudo locale-gen ${{ matrix.locale }}
+ sudo update-locale LANG=${{ matrix.locale }}
+
+ - run: date
- - name: Cache Maven dependencies
- uses: actions/cache@v3
+ - uses: actions/setup-java@v4
with:
- path: ~/.m2/repository
- key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
- restore-keys: |
- ${{ runner.os }}-maven-
+ distribution: 'temurin'
+ java-version: '17'
+ cache: 'maven'
- name: Test project with Maven
- run: mvn --no-transfer-progress test install
+ run: |
+ mvn $MAVEN_ARGS test package
- - name: Build documentation
- run: mvn --no-transfer-progress site
+ cli-integration-tests:
+ runs-on: ubuntu-latest
- - name: Deploy documentation to Github Pages
- # only deploy after merging to master
- if: github.repository_owner == 'OneBusAway' && github.event_name == 'push' && github.ref == 'refs/heads/master'
- uses: JamesIves/github-pages-deploy-action@v4
- with:
- folder: target/site/
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+ cache: 'maven'
+
+ - name: Run CLI integration tests
+ run: ./cli-tests/cli-tests.sh
+
+ container-image:
+ if: github.repository_owner == 'onebusaway' && github.event_name == 'push' && github.ref == 'refs/heads/master'
+ runs-on: ubuntu-latest
+
+ env:
+ CONTAINER_REGISTRY_NAMESPACE: docker.io/opentransitsoftwarefoundation
+ CONTAINER_REGISTRY_USER: onebusawaybot
+ CONTAINER_REGISTRY_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+ cache: 'maven'
+
+ - name: Build container image tarball
+ run: |
+ MAVEN_SKIP_ARGS="-Dmaven.test.skip=true -Dmaven.source.skip=true"
+ mvn $MAVEN_ARGS $MAVEN_SKIP_ARGS package jib:build
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 000000000..39281602d
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,20 @@
+name: Create Github release
+
+on:
+ push:
+ tags:
+ - "v*.*.*"
+
+permissions:
+ contents: write
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Release
+ uses: softprops/action-gh-release@v2
+ with:
+ generate_release_notes: true
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 77f573429..a99882886 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,5 @@ junkyard
/onebusaway-gtfs-modules.iml
/pom.xml.versionsBackup
*.iml
+*.zip
+*.jar
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 7060531e5..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-language: java
-jdk:
- - openjdk8
-cache:
- directories:
- - $HOME/.m2
diff --git a/Makefile b/Makefile
new file mode 100644
index 000000000..7cbf3f563
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,4 @@
+release:
+ git checkout master
+ git pull
+ mvn release:clean release:prepare release:perform -Dgoals=deploy release:clean
diff --git a/README.md b/README.md
index 3bfe716cf..b1e9a4044 100644
--- a/README.md
+++ b/README.md
@@ -1,30 +1,29 @@
-# onebusaway-gtfs-modules [![CI](https://github.com/OneBusAway/onebusaway-gtfs-modules/actions/workflows/ci.yml/badge.svg)](https://github.com/OneBusAway/onebusaway-gtfs-modules/actions/workflows/ci.yml)
+# onebusaway-gtfs-modules
+
+[![CI](https://github.com/OneBusAway/onebusaway-gtfs-modules/actions/workflows/ci.yml/badge.svg)](https://github.com/OneBusAway/onebusaway-gtfs-modules/actions/workflows/ci.yml)
+[![Maven Central](https://img.shields.io/maven-central/v/org.onebusaway/onebusaway-gtfs-modules.svg)](https://mvnrepository.com/artifact/org.onebusaway/onebusaway-gtfs-modules)
A Java library for reading and writing [GTFS](https://developers.google.com/transit/gtfs) feeds, including database support.
-See more documentation [on the wiki](https://github.com/OneBusAway/onebusaway-gtfs-modules/wiki).
+See more documentation in the [`docs folder`](./docs).
## Maven usage
In your `pom.xml`, include:
-~~~
-
-
- public.onebusaway.org
- https://repo.camsys-apps.com/releases/
-
-
-~~~
-
-... and inside ``:
+```
-~~~
- org.onebusaway
- onebusaway-gtfs
- 1.3.88
+ org.onebusaway
+ onebusaway-gtfs
+ ${VERSION}
-~~~
+```
...where `` contains the latest version number.
+
+## Update on camsys-apps.com repo
+
+In August 2024 @leonardehrenfried took over maintainership and subsequently all artifacts are
+now again published to Maven Central. Adding camsys-apps.com to your Maven repo configuration is no
+longer necessary when you use version 1.4.18 or newer.
\ No newline at end of file
diff --git a/cli-tests/cli-tests.sh b/cli-tests/cli-tests.sh
new file mode 100755
index 000000000..b3d56200d
--- /dev/null
+++ b/cli-tests/cli-tests.sh
@@ -0,0 +1,28 @@
+#!/bin/bash +x
+
+mvn clean package --no-transfer-progress -DskipTests -Dmaven.source.skip=true -Dmaven.javadoc.skip=true
+
+EXAMPLE_1="example.gtfs.zip"
+EXAMPLE_2="deathvalley.gtfs.zip"
+
+# transformer-cli
+
+TRANSFORMER_JAR="transformer-cli.jar"
+
+cp onebusaway-gtfs-transformer-cli/target/onebusaway-gtfs-transformer-cli.jar ./${TRANSFORMER_JAR}
+wget https://github.com/google/transit/blob/master/gtfs/spec/en/examples/sample-feed-1.zip?raw=true -O ${EXAMPLE_1}
+
+java -jar ${TRANSFORMER_JAR} --help
+
+java -jar ${TRANSFORMER_JAR} --transform="{'op':'remove','match':{'file':'stops.txt','stop_id':'BEATTY_AIRPORT'}}" ${EXAMPLE_1} gtfs.transformed.zip
+
+# merge-cli
+
+MERGE_JAR="merge-cli.jar"
+
+cp onebusaway-gtfs-merge-cli/target/onebusaway-gtfs-merge-cli-*.jar ./${MERGE_JAR}
+wget "http://data.trilliumtransit.com/gtfs/deathvalley-demo-ca-us/deathvalley-demo-ca-us.zip" -O ${EXAMPLE_2}
+
+java -jar ${MERGE_JAR} --help
+
+java -jar ${MERGE_JAR} ${EXAMPLE_1} ${EXAMPLE_2} gtfs.merged.zip
diff --git a/src/site/apt/index.apt.vm b/docs/index.md
similarity index 65%
rename from src/site/apt/index.apt.vm
rename to docs/index.md
index f8445a862..07dac4987 100644
--- a/src/site/apt/index.apt.vm
+++ b/docs/index.md
@@ -1,36 +1,28 @@
-onebusaway-gtfs-modules
+# onebusaway-gtfs-modules
- We provide a Java library for reading and writing {{{https://developers.google.com/transit/gtfs} GTFS}} feeds, including database support.
-
- <> ${currentVersion}
-
- Details on all releases can be found in the {{{./release-notes.html}Release Notes}}.
+ We provide a Java library for reading and writing [ GTFS](https://developers.google.com/transit/gtfs) feeds, including database support.
The library is broken up into a few key modules:
- * <<>> - The core library for reading and writing GTFS
+ * `onebusaway-gtfs` - The core library for reading and writing GTFS
- * <<>> - Support for {{{http://www.hibernate.org/}Hibernate}} database persistence of GTFS data
+ * `onebusaway-gtfs-hibernate` - Support for [Hibernate](http://www.hibernate.org/) database persistence of GTFS data
- * <<>> - Command-line utilty for loading GTFS feeds into a database - see {{{./onebusaway-gtfs-hibernate-cli.html}the full documentation}}.
+ * `onebusaway-gtfs-hibernate-cli` - Command-line utilty for loading GTFS feeds into a database - see [the full documentation](./onebusaway-gtfs-hibernate-cli.md).
- * <<>> - Tools for transforming GTFS data
+ * `onebusaway-gtfs-transformer` - Tools for transforming GTFS data
- * <<>> - Command-line utility for transforming GTFS - see {{{./onebusaway-gtfs-transformer-cli.html}the full documentation}}.
+ * `onebusaway-gtfs-transformer-cli` - Command-line utility for transforming GTFS - see [the full documentation](./onebusaway-gtfs-transformer-cli.md).
- * <<>> - Tools for merging GTFS data
+ * `onebusaway-gtfs-merge` - Tools for merging GTFS data
- * <<>> - Command-line utility for merging GTFS feeds - see {{{./onebusaway-gtfs-merge-cli.html}the full documentation}}.
-
-Documentation
-
- You can access the {{{./apidocs/index.html}latest Javadoc for the library}}. Also, see example source code below.
+ * `onebusaway-gtfs-merge-cli` - Command-line utility for merging GTFS feeds - see [the full documentation](./onebusaway-gtfs-merge-cli.md).
-Using in Maven
+## Using in Maven
The library is available as a Maven module. Simply add the following dependencies:
-+---+
+```
@@ -51,19 +43,30 @@ Using in Maven
${currentVersion}
-+---+
+```
+
+## Docker images
+
+There are automatically generated docker images available at https://registry.hub.docker.com/u/opentransitsoftwarefoundation.
+Contributions to image-specific documentation are welcome.
+
+### `onebusaway-gtfs-transformer-cli`
+
+See [the full documentation](./onebusaway-gtfs-transformer-cli.md) for more configuration options.
-#if( $currentVersion.contains('SNAPSHOT') )
- To use a SNAPSHOT version of the library, you'll need to {{{https://github.com/OneBusAway/onebusaway/wiki/Maven-Repository}add a reference to the OneBusAway Maven repository}}.
-#end
+For example, assuming that all the following files are in the `/path/to/local/data/directory` directory, to run the `remove-matching-route.rule` rule against `gtfs-data.zip` to generate `gtfs-data-out.zip` you can use:
+```bash
+docker run -v /path/to/local/data/directory:/data --rm opentransitsoftwarefoundation/onebusaway-gtfs-transformer-cli:4.4.0 --transform=/data/remove-matching-route.rule /data/gtfs-data.zip /data/gtfs-data-out.zip
+```
+The `gtfs-data-out.zip` file will be in the `/path/to/local/data/directory` directory.
-Example Code
+## Example Code
* Basic Reading
Let's introduce basic code for reading a GTFS feed and handling the resulting entities:
-+---+
+```
public class GtfsReaderExampleMain {
public static void main(String[] args) throws IOException {
@@ -107,18 +110,18 @@ public class GtfsReaderExampleMain {
}
}
}
-+---+
+```
- Notice that the {{{./apidocs/org/onebusaway/gtfs/serialization/GtfsReader.html}GtfsReader}} does the bulk of the work of reading the GTFS feed. The general pattern is to create the reader, set the input file, and call `run()` to start the reading process. You can manage the resulting GTFS entities in a couple of ways:
+ Notice that the [GtfsReader](./apidocs/org/onebusaway/gtfs/serialization/GtfsReader.html) does the bulk of the work of reading the GTFS feed. The general pattern is to create the reader, set the input file, and call `run()` to start the reading process. You can manage the resulting GTFS entities in a couple of ways:
- * Register an {{{../../onebusaway-csv-entities/${onebusaway_csv_entities_version}/apidocs/org/onebusaway/csv_entities/EntityHandler.html}EntityHandler}} to handle entities as they are read
+ * Register an [EntityHandler](../../onebusaway-csv-entities/${onebusaway_csv_entities_version}/apidocs/org/onebusaway/csv_entities/EntityHandler.html) to handle entities as they are read
- * Use an instance of {{{./apidocs/org/onebusaway/gtfs/services/GenericMutableDao.html}GenericMutableDao}} to examine the loaded entities after reading is complete
+ * Use an instance of [GenericMutableDao](./apidocs/org/onebusaway/gtfs/services/GenericMutableDao.html) to examine the loaded entities after reading is complete
* Basic Writing
-+---+
+```
public class GtfsWriterExampleMain {
public static void main(String[] args) throws IOException {
@@ -146,17 +149,17 @@ public class GtfsWriterExampleMain {
writer.close();
}
}
-+---+
+```
* Basic Database Reading
- The class <<>> in the
-<<>> directory includes basic code for reading
+ The class `org.onebusaway.gtfs.examples.GtfsHibernateReaderExampleMain` in the
+`onebusaway-gtfs-hibernate/src/test/java` directory includes basic code for reading
a GTFS feed into a database and querying the resulting entities.
The sample code has been summarized for length and clarity:
-+---+
+```
public class GtfsHibernateReaderExampleMain {
public static void main(String[] args) throws IOException {
@@ -200,10 +203,10 @@ public class GtfsHibernateReaderExampleMain {
return new HibernateGtfsFactory(sessionFactory);
}
}
-+---+
+```
This code is roughly similar to the example reader code for
-<<>>, with the main difference being the use of <<>>, which is a convenience
+`onebusaway-gtfs`, with the main difference being the use of `HibernateGtfsFactory`, which is a convenience
factory for creating database-aware DAOs.
@@ -214,13 +217,13 @@ it to use a different database and you totally can. See {{http://hibernate.org/
Hibernate, but also check out the default hibernate config file used in the example above. It's located in the following
directory:
-+---+
+```
onebusaway-gtfs-hibernate/src/test/resources/org/onebusaway/gtfs/examples/hibernate-configuration-examples.xml
-+---+
+```
The contents look like:
-+---+
+```
@@ -236,25 +239,25 @@ onebusaway-gtfs-hibernate/src/test/resources/org/onebusaway/gtfs/examples/hibern
-+---+
+```
Here you can configure the data source used for the database connection along with the Hibernate dialect.
* Reading Custom Fields
- Does your GTFS feed have custom fields not defined by the core <<>> library? It's possible
+ Does your GTFS feed have custom fields not defined by the core `onebusaway-gtfs` library? It's possible
to read and write this data without modify OBA source code using the "extensions" mechanism. Consider a
-<<>> file with a custom <<>> field:
+`stops.txt` file with a custom `extra_stop_info` field:
-+---+
+```
stop_id,stop_name,stop_lat,stop_lon,extra_stop_info
123,Some Station,47.0,-122.0,This is a cool transit station
-+---+
+```
- The <<>> field isn't included in the the {{{./apidocs/org/onebusaway/gtfs/model/Stop.html}Stop}} data
-model by default. So instead, we define a special <<>> Java bean type with the new field:
+ The `extra_stop_info` field isn't included in the the [Stop](./apidocs/org/onebusaway/gtfs/model/Stop.html) data
+model by default. So instead, we define a special `StopExtension` Java bean type with the new field:
-+---+
+```
public static class StopExtension {
@CsvField(optional = true)
private String extraStopInfo;
@@ -262,28 +265,28 @@ public static class StopExtension {
public String getExtraStopInfo() { ... }
public void setExtraStopInfo(String info) { ... }
}
-+---+
+```
We can now register our class as an extension of the default stop data model:
-+---+
+```
DefaultEntitySchemaFactory factory = GtfsEntitySchemaFactory.createEntitySchemaFactory();
factory.addExtension(Stop.class, StopExtension.class);
GtfsReader reader = new GtfsReader();
reader.setEntitySchemaFactory(factory);
-+---+
+```
- Now when you read your GTFS feed with the <<>> instance, <<>> objects
+ Now when you read your GTFS feed with the `GtfsReader` instance, `StopExtension` objects
will automatically be created, populated, and attached to stops as they are read:
-+---+
+```
Stop stop = dao.getStopForId(...);
StopExtension extension = stop.getExtension(StopExtension.class);
System.out.println(extension.getExtraStopInfo());
-+---+
+```
For more information on defining the mapping from GTFS fields to Java beans, see documentation for
-the {{{https://github.com/OneBusAway/onebusaway-csv-entities}onebusaway-csv-entities}} project,
-including the {{{../../onebusaway-csv-entities/${onebusaway_csv_entities_version}/apidocs/org/onebusaway/csv_entities/schema/annotations/CsvField.html}@CsvField}}
+the [onebusaway-csv-entities](https://github.com/OneBusAway/onebusaway-csv-entities) project,
+including the [@CsvField](../../onebusaway-csv-entities/${onebusaway_csv_entities_version}/apidocs/org/onebusaway/csv_entities/schema/annotations/CsvField.html)
annotation documentation.
\ No newline at end of file
diff --git a/src/site/apt/onebusaway-gtfs-hibernate-cli.apt.vm b/docs/onebusaway-gtfs-hibernate-cli.md
similarity index 68%
rename from src/site/apt/onebusaway-gtfs-hibernate-cli.apt.vm
rename to docs/onebusaway-gtfs-hibernate-cli.md
index 9e7af5bcc..4cf866f4f 100644
--- a/src/site/apt/onebusaway-gtfs-hibernate-cli.apt.vm
+++ b/docs/onebusaway-gtfs-hibernate-cli.md
@@ -6,8 +6,8 @@ GTFS Hibernate Command-Line Utility
Introduction
- The <<>> command-line utility is a simple command-line tool for loading a
-{{{https://developers.google.com/transit/gtfs}GTFS}} feed into a database.
+ The `onebusaway-gtfs-hibernate-cli` command-line utility is a simple command-line tool for loading a
+[GTFS](https://developers.google.com/transit/gtfs) feed into a database.
Getting the Application
@@ -21,13 +21,13 @@ Getting the Application
#set( $url = 'https://repo.camsys-apps.com/' + $repository + '/org/onebusaway/onebusaway-gtfs-hibernate-cli/' + ${currentVersion} + '/onebusaway-gtfs-hibernate-cli-' + ${currentVersion} + '.jar' )
- {{{${url}}onebusaway-gtfs-hibernate-cli-${currentVersion}.jar}}
+ [.jar](${url}}onebusaway-gtfs-hibernate-cli-${currentVersion)
Using the Application
You'll need a Java 11 runtime installed to run the client. To run the application:
-+---+
+```
java -classpath onebusaway-gtfs-hiberante-cli.jar:your-database-jdbc.jar \
org.onebusaway.gtfs.GtfsDatabaseLoaderMain \
--driverClass=... \
@@ -35,7 +35,7 @@ java -classpath onebusaway-gtfs-hiberante-cli.jar:your-database-jdbc.jar \
--username=... \
--password=... \
gtfs_path
-+---+
+```
Note that the utility doesn't include any JDBC client jars for any databases by default. You will need
to download an appropriate JDBC client for your database and include it on the classpath when running
@@ -44,13 +44,13 @@ using the command-line arguments specified below.
* Arguments
- * <<<--driverClass=...>>> : JDBC driver class for your JDBC provider (eg. "org.hsqldb.jdbcDriver")
+ * `--driverClass=...` : JDBC driver class for your JDBC provider (eg. "org.hsqldb.jdbcDriver")
- * <<<--url=...>>> : JDBC connection url for your database (eg. "jdbc:hsqldb:mem:temp_db")
+ * `--url=...` : JDBC connection url for your database (eg. "jdbc:hsqldb:mem:temp_db")
- * <<<--username=...>>> : JDBC connection username
+ * `--username=...` : JDBC connection username
- * <<<--password=...>>> : JDBC connection password
+ * `--password=...` : JDBC connection password
[]
diff --git a/docs/onebusaway-gtfs-merge-cli.md b/docs/onebusaway-gtfs-merge-cli.md
new file mode 100644
index 000000000..5c8fbb3c2
--- /dev/null
+++ b/docs/onebusaway-gtfs-merge-cli.md
@@ -0,0 +1,112 @@
+# GTFS Merge Command-Line Application
+
+## Introduction
+
+The `onebusaway-gtfs-merge-cli` command-line application is a simple command-line tool for merging
+[GTFS](https://developers.google.com/transit/gtfs) feeds.
+
+## Getting the Application
+
+You can download the application from Maven Central.
+
+Go to https://repo1.maven.org/maven2/org/onebusaway/onebusaway-gtfs-merge-cli/, select the version
+you want and get the URL for the largest jar file. An example would be
+https://repo1.maven.org/maven2/org/onebusaway/onebusaway-gtfs-merge-cli/3.2.2/onebusaway-gtfs-merge-cli-3.2.2.jar
+
+## Using the Application
+
+You'll need a Java 17 runtime installed to run the client.
+
+To run the application:
+
+```
+java -jar onebusaway-gtfs-merge-cli.jar [--args] input_gtfs_path_a input_gtfs_path_b ... output_gtfs_path
+```
+
+**Note**: Merging large GTFS feeds is often processor and memory intensive. You'll likely need to increase the
+max amount of memory allocated to Java with an option like `-Xmx1G` (adjust the limit as needed). I also recommend
+adding the `-server` argument if you are running the Oracle or OpenJDK, as it can really increase performance.
+
+## Configuring the Application
+
+The merge application supports a number of options and arguments for configuring the application's behavior. The
+general pattern is to specify options for each type of file in a GTFS feed using the `--file` option, specifying
+specific options for each file type after the `--file` option. Here's a quick example:
+
+```
+--file=routes.txt --duplicateDetection=identity --file=calendar.txt --logDroppedDuplicates ...
+```
+
+ The merge application supports merging the following files:
+
+ - `agency.txt`
+ - `stops.txt`
+ - `routes.txt`
+ - `trips.txt` and `stop_times.txt`
+ - `calendar.txt` and `calendar_dates.txt`
+ - `shapes.txt`
+ - `fare_attributes.txt`
+ - `fare_rules.txt`
+ - `frequencies.txt`
+ - `transfers.txt`
+
+You can specify merge options for each of these files using the `--file=gtfs_file.txt` option. File types listed
+together (eg. `trips.txt>> and `stop_times.txt`) are handled by the same merge strategy, so specifying options for
+either will have the same effect. For details on options you might specify, read on.
+
+## Handling Duplicates
+
+The main issue to considering when merging GTFS feeds is the handling of duplicate entries between the two feeds,
+including how to identify duplicates and what to do with duplicates when they are found.
+
+### Identifying Duplicates
+
+We support a couple of methods for determining when entries from two different feeds are actually duplicates. By default,
+the merge tool will attempt to automatically determine the best merge strategy to use. You can also control the specific
+strategy used on a per-file basis using the `--duplicateDetection` argument. You can specify any of the following
+strategies for duplicate detection.
+
+ - `--duplicateDetection=identity` - If two entries have the same id (eg. stop id, route id, trip id), then they are
+ considered the same. This is the more strict matching policy.
+
+ - `--duplicateDetection=fuzzy` - If two entries have common elements (eg. stop name or location, route short name,
+ trip stop sequence), then they are considered the same. This is the more lenient matching policy, and is highly
+ dependent on the type of GTFS entry being matched.
+
+ - `--duplicateDetection=none` - Entries between two feeds are never considered to be duplicates, even if they have
+ the same id or similar properties.
+
+### Logging Duplicates
+
+Sometimes your feed might have unexpected duplicates. You can tell the merge tool to log duplicates it finds or even
+immediately exit with the following arguments:
+
+ - `--logDroppedDuplicates` - log a message when a duplicate is found
+
+ - `--errorOnDroppedDuplicates` - throw an exception when a duplicate is found, stopping the program
+
+## Examples
+
+### Handling a Service Change
+
+Agencies often schedule major changes to their system around a particular date, with one GTFS feed for before the
+service change and a different GTFS feed for after. We'd like to be able to merge these disjoint feeds into one
+feed with continuous coverage.
+
+In our example, an agency produces two feeds where the entries in `agency.txt` and `stops.txt` are exactly
+the same, so the default policy of identifying and dropping duplicates will work fine there. The `routes.txt` file
+is a bit trickier, since the route ids are different between the two feeds but the entries are largely the same. We
+will use fuzzy duplicate detection to match the routes between the two feeds.
+
+The next issue is the `calendar.txt` file. The agency uses the same `service_id` values in both feeds
+(eg. `WEEK`, `SAT`, `SUN`) with different start and end dates in the two feeds. If the default policy of
+dropping duplicate entries was used, we'd lose the dates in one of the service periods. Instead, we rename duplicates
+such that the service ids from the second feed will be renamed to `b-WEEK`, `b-SAT`, etc. and all
+`trips.txt` entries in the second feed will be updated appropriately. The result is that trips from the first
+and second feed will both have the proper calendar entries in the merged feed.
+
+Putting it all together, here is what the command-line options for the application would look like:
+
+```
+--file=routes.txt --fuzzyDuplicates --file=calendar.txt --renameDuplicates
+```
\ No newline at end of file
diff --git a/docs/onebusaway-gtfs-transformer-cli-sample1.png b/docs/onebusaway-gtfs-transformer-cli-sample1.png
new file mode 100644
index 000000000..eec780758
Binary files /dev/null and b/docs/onebusaway-gtfs-transformer-cli-sample1.png differ
diff --git a/docs/onebusaway-gtfs-transformer-cli.md b/docs/onebusaway-gtfs-transformer-cli.md
new file mode 100644
index 000000000..7b357e7b7
--- /dev/null
+++ b/docs/onebusaway-gtfs-transformer-cli.md
@@ -0,0 +1,639 @@
+# GTFS Transformation Command-Line Application
+
+
+* [GTFS Transformation Command-Line Application](#gtfs-transformation-command-line-application)
+ * [Introduction](#introduction)
+ * [Requirements](#requirements)
+ * [Getting the Application](#getting-the-application)
+ * [Using the Application](#using-the-application)
+ * [Arguments](#arguments)
+ * [Transform Syntax](#transform-syntax)
+ * [Matching](#matching-)
+ * [Regular Expressions](#regular-expressions)
+ * [Compound Property Expressions](#compound-property-expressions)
+ * [Multi-Value Matches](#multi-value-matches)
+ * [Collection-Like Entities](#collection-like-entities)
+ * [Types of Transforms](#types-of-transforms)
+ * [Add an Entity](#add-an-entity)
+ * [Update an Entity](#update-an-entity)
+ * [Find/Replace](#findreplace)
+ * [Path Expressions](#path-expressions-)
+ * [Retain an Entity](#retain-an-entity)
+ * [Remove an Entity](#remove-an-entity)
+ * [Retain Up From Polygon](#retain-up-from-polygon)
+ * [Trim Trip From Polygon](#trim-trip-from-polygon)
+ * [Trim a Trip](#trim-a-trip)
+ * [Generate Stop Times](#generate-stop-times)
+ * [Extend Service Calendars](#extend-service-calendars)
+ * [Remove Old Calendar Statements](#remove-old-calendar-statements)
+ * [Deduplicate Calendar Entries](#deduplicate-calendar-entries)
+ * [Truncate New Calendar Statements](#truncate-new-calendar-statements)
+ * [Merge Trips and Simplify Calendar Entries](#merge-trips-and-simplify-calendar-entries)
+ * [Shift Negative Stop Times](#shift-negative-stop-times)
+ * [Arbitrary Transform](#arbitrary-transform)
+ * [How to Reduce your GTFS](#how-to-reduce-your-gtfs)
+ * [Clip National GTFS for Regional Integration and Consistency](#clip-national-gtfs-for-regional-integration-and-consistency)
+
+
+## Introduction
+
+The `onebusaway-gtfs-transformer-cli` command-line application is a simple command-line tool for transforming
+[GTFS](https://developers.google.com/transit/gtfs) feeds.
+
+### Requirements
+
+ * Java 17 or greater
+
+### Getting the Application
+
+You can download the application from Maven Central: https://repo1.maven.org/maven2/org/onebusaway/onebusaway-gtfs-transformer-cli/
+
+Select the largest jar file from the version you would like to use, for example https://repo1.maven.org/maven2/org/onebusaway/onebusaway-gtfs-transformer-cli/2.0.0/onebusaway-gtfs-transformer-cli-2.0.0.jar
+
+### Using the Application
+
+To run the application:
+
+```
+java -jar onebusaway-gtfs-transformer-cli.jar [-args] input_gtfs_path ... output_gtfs_path
+```
+
+`input_gtfs_path` and `output_gtfs_path` can be either a directory containing a GTFS feed or a .zip file.
+
+_Note_: Transforming large GTFS feeds is processor and memory intensive. You'll likely need to increase the
+max amount of memory allocated to Java with an option like `-Xmx1G` or greater. Adding the `-server` argument
+if you are running the Oracle or OpenJDK can also increase performance.
+
+### Arguments
+
+ * `--transform=...` : specify a transformation to apply to the input GTFS feed (see syntax below)
+ * `--agencyId=id` : specify a default agency id for the input GTFS feed
+ * `--overwriteDuplicates` : specify that duplicate GTFS entities should overwrite each other when read
+
+
+### Transform Syntax
+
+Transforms are specified as snippets of example. A simple example to remove a stop might look like:
+
+```
+{"op":"remove","match":{"file":"stops.txt","stop_name":"Stop NameP"}}
+```
+
+You can pass those snippets to the application in a couple of ways. The simplest is directly on the command line.
+
+```
+--transform='{...}'
+```
+
+You can have multiple `--transform` arguments to specify multiple transformations. However, if you have a LOT of
+transformations that you wish to apply, it can be easier to put them in a file, with a JSON snippet per line. Then
+specify the file on the command-line:
+
+```
+ --transform=path/to/local-file
+```
+
+You can even specify a URL where the transformations will be read:
+
+```
+--transform=http://server/path
+```
+
+### Matching
+
+We provide a number of configurable transformations out-of-the-box that can do simple operations like adding,
+updating, retaining, and removing GTFS entities. Many of the transforms accept a "`match`" term that controls how the
+rule matches against entities:
+
+```
+{"op":..., "match":{"file":"routes.txt", "route_short_name":"574"}}
+```
+
+Here, the match snippet at minimum requires a `file` property that specifies the type of GTFS entity to match.
+Any file name defined in the [GTFS specification](https://developers.google.com/transit/gtfs/reference#FeedFiles)
+can be used.
+
+You can specify additional properties and values to match against as needed. Again, use the field names defined for
+each file name in the GTFS specification. For example, the snippet above will match any entry in `routes.txt` with a
+`route_short_name` value of `574`.
+
+### Regular Expressions
+
+ Property matching also supports regular expressions that allow you to match property values conforming to a regexp pattern. For example, the snippet below will match any entry in `stops.txt` with a `stop_id` starting with `de:08`.
+
+```
+{"op":..., "match":{"file":"stops.txt", "stop_id":"m/^de:08.*/"}}
+```
+
+### Compound Property Expressions
+
+ Property matching also supports compound property expressions that allow you to match across GTFS relational
+references. Let's look at a simple example:
+
+```
+{"op":..., "match":{"file":"trips.txt", "route.route_short_name":"10"}}
+```
+
+Here the special `routes` property references the route entry associated with each trip, allowing you to match
+the properties of the route. You can even chain references, like `route.agency` to match against the agency
+associated with the trip. Here is the full list of supported compound property references:
+
+```
+{"op":..., "match":{"file":"routes.txt", "agency.name":"Metro"}}
+{"op":..., "match":{"file":"trips.txt", "route.route_short_name":"10"}}
+{"op":..., "match":{"file":"stop_times.txt", "stop.stop_id":"153"}}
+{"op":..., "match":{"file":"stop_times.txt", "trip.route.route_type":3}}
+{"op":..., "match":{"file":"frequencies.txt", "trip.service_id":"WEEKDAY"}}
+{"op":..., "match":{"file":"transfers.txt", "fromStop.stop_id":"173"}}
+{"op":..., "match":{"file":"transfers.txt", "toStop.stop_id":"173"}}
+{"op":..., "match":{"file":"fare_rules.txt", "fare.currencyType":"USD"}}
+{"op":..., "match":{"file":"fare_rules.txt", "route.route_short_name":"10"}}
+```
+
+### Multi-Value Matches
+
+ The compound property expressions shown above are all for 1-to-1 relations, but matching also supports a limited
+form of multi-value matching for 1-to-N relations. Let's look at a simple example:
+
+```
+{"op":..., "match":{"file":"routes.txt", "any(trips.trip_headsign)":"Downtown"}}
+```
+
+Notice the addition of `any(...)` around the property name. Here we are using a special `trips` property that
+expands to include all trips associated with each route. Now, if *any* trip belonging to the route has the specified
+`trip_headsign` value, then the route matches. Here is the full list of supported multi-value property matches:
+
+```
+{"op":..., "match":{"file":"agency.txt", "any(routes.X)":"Y"}}
+{"op":..., "match":{"file":"routes.txt", "any(trips.X)":"Y"}}
+{"op":..., "match":{"file":"trips.txt", "any(stop_times.X)":"Y"}}
+```
+
+### Collection-Like Entities
+
+There are a number of GTFS entites that are more effectively collections identified by a common key. For example,
+shape points in `shapes.txt` linked by a common `shape_id` value or `calendar.txt` and `calendar_dates.txt` entries
+linked by a common `service_id` value. You can use a special `collection `match clause to match against the entire
+collection.
+
+```
+{"op":..., "match":{"collection":"shape", "shape_id":"XYZ"}}
+{"op":..., "match":{"collection":"calendar", "service_id":"XYZ"}}
+```
+
+You can use the calendar collection matches, for example, to retain a calendar, including all `calendar.txt`,
+`calendar_dates.txt`, and `trip.txt` entries that reference the specified `service_id` value. This convenient
+short-hand is easier than writing the equivalent expression using references to the three file types separately.
+
+### Types of Transforms
+
+#### Add an Entity
+
+Create and add a new entity to the feed.
+
+```
+{"op":"add","obj":{"file":"agency.txt", "agency_id":"ST", "agency_name":"Sound Transit",
+"agency_url":"http://www.soundtransit.org", "agency_timezone":"America/Los_Angeles"}}
+```
+
+#### Update an Entity
+
+You can update arbitrary fields of a GTFS entity.
+
+```
+{"op":"update", "match":{"file":"routes.txt", "route_short_name":"574"}, "update":{"agency_id":"ST"}}
+```
+
+Normally, update values are used as-is. However, we support a number of
+special update operations:
+
+#### Find/Replace
+
+```
+{"op":"update", "match":{"file":"trips.txt"}, "update":{"trip_short_name":"s/North/N/"}}
+```
+
+By using `s/.../.../` syntax in the update value, the upda```te will perform
+a find-replace operation on the specified property value. Consider the
+following example:
+
+```
+{"op":"update", "match":{"file":"trips.txt"}, "update":{"trip_short_name":"s/North/N/"}}
+```
+
+Here, a trip with a headsign of `North Seattle` will be updated to `N Seattle`.
+
+#### Path Expressions
+
+By using `path(...)` syntax in the update value, the expression will be
+treated as a compound Java bean properties path expression. This path
+expression will be evaluated against the target entity to produce the update
+value. Consider the following example:
+
+```
+{"op":"update", "match":{"file":"trips.txt"}, "update":{"trip_short_name":"path(route.longName)"}}
+```
+
+Here, the `trip_short_name` field is updated for each trip in the feed.
+The value will be copied from the `route_long_name` field of each trip's
+associated route.
+
+#### Retain an Entity
+
+We also provide a powerful mechanism for selecting just a sub-set of a feed.
+You can apply retain operations to entities you wish to keep and all the supporting entities referenced
+by the retained entity will be retained as well. Unreferenced entities will be pruned.
+
+In the following example, only route B15 will be retained, along with all the stops, trips, stop times, shapes, and agencies linked to directly by that route.
+
+```
+{"op":"retain", "match":{"file":"routes.txt", "route_short_name":"B15"}}
+```
+
+By default, we retain across [block_id](https://developers.google.com/transit/gtfs/reference#trips_block_id_field) values
+specified in trips.txt. That means if a particular trip is retained (perhaps because its parent route is retained),
+and the trip specifies a block_id, then all the trips referencing that block_id will be retained as well, along with
+their own routes, stop times, and shapes. This can potentially lead to unexpected results if you retain one route and
+suddenly see other routes included because they are linked by block_id.
+
+You can disable this feature by specifying `retainBlocks: false` in the JSON transformer snippet. Here is an
+example:
+
+```
+{"op":"retain","match":{"file":"routes.txt", "route_short_name":"B15"}, "retainBlocks":false}
+```
+
+#### Remove an Entity
+
+You can remove a specific entity from a feed.
+
+```
+{"op":"remove", "match":{"file":"stops.txt", "stop_name":"Stop Name"}}
+```
+
+Note that removing an entity has a cascading effect. If you remove a trip, all the stop times that depend on that
+trip will also be removed. If you remove a route, all the trips and stop times for that route will be removed.
+
+#### Retain Up From Polygon
+
+Retain Up From Polygon is an operation that filters GTFS input data based on a specified geographic area, using a polygon defined in WKT (Well-Known Text) format, which is configurable in the JSON transformer snippet.
+
+This strategy applies two main functions:
+
+ * **Retain Function**: retains **up** all stops, trips, and routes that are located inside the defined polygon.
+
+ The algorithm starts by applying retain up to each entity, traversing the entity dependency tree. Starting from the stop, retain up is applied to the stop_times referencing this stop, then to the trips, and so on.
+
+ Once the base of the entity tree is reached, it automatically applies retain **down** to all the traversed entities. Therefore, all the trips of the route and then all the stop_times of each trip will be tagged as **retain**.
+
+ * **Remove Function**: any entities not retained within the polygon are removed.
+
+This strategy ensures that the GTFS output retains only the entities directly or indirectly linked to the geographical area concerned.
+
+**Parameters**:
+
+ * **polygon**: a required argument, which accepts the polygon in WKT format using the WGS84 coordinate system (SRID: 4326). This polygon defines the area of interest for filtering.
+
+```
+{"op":"transform","class":"org.onebusaway.gtfs_transformer.impl.RetainUpFromPolygon","polygon":"POLYGON ((-123.0 37.0, -123.0 38.0, -122.0 38.0, -122.0 37.0, -123.0 37.0))"}
+```
+
+#### Trim Trip From Polygon
+
+The Trim Trip From Polygon strategy refines GTFS data by removing all stop_times associated with stops located outside a specified geographical area. The area is defined using a configurable WKT Polygon or Multipolygon in the JSON transformer snippet.
+
+This removal of stop_times is achieved by invoking the **TrimTrip operation**, ensuring that only stops within the defined polygon are retained.
+
+Only valid stop_times within the polygon are retained, maintaining the integrity of the trips.
+
+**Parameters**:
+
+ * **polygon**: a required argument, which accepts the polygon in WKT format using the WGS84 coordinate system (SRID: 4326). This polygon defines the area of interest for filtering.
+
+```
+{"op":"transform","class":"org.onebusaway.gtfs_transformer.impl.TrimTripFromPolygon","polygon":"POLYGON ((-123.0 37.0, -123.0 38.0, -122.0 38.0, -122.0 37.0, -123.0 37.0))"}
+```
+
+#### Trim a Trip
+
+You can remove stop times from the beginning or end of a trip using the "trim_trip" operation. Example:
+
+```
+{"op":"trim_trip", "match":{"file":"trips.txt", "route_id":"R10"}, "from_stop_id":"138S"}
+```
+
+For any trip belonging to the specified route and passing through the specified stop, all stop times from the specified
+stop onward will be removed from the trip. You can also remove stop times from the beginning of the trip as well:
+
+```
+{"op":"trim_trip", "match":{"file":"trips.txt", "route_id":"R10"}, "to_stop_id":"138S"}
+```
+
+Or both:
+
+```
+{"op":"trim_trip", "match":{"file":"trips.txt", "route_id":"R10"}, "to_stop_id":"125S", "from_stop_id":"138S"}
+```
+
+#### Generate Stop Times
+
+You can generate stop time entries for a trip. Example:
+
+```
+{"op":"stop_times_factory", "trip_id":"TRIP01", "start_time":"06:00:00", "end_time":"06:20:00", "stop_ids":["S01", "S02", "S03"]}
+```
+
+A series of entries in `stop_times.txt` will be generated for the specified trip, traveling along the specified sequence of
+stops. The departure time for the first stop will be set from the `start_time` field, the arrival time for the last stop will
+be set from the `end_time` field, and the times for intermediate stops will be interpolated based on their distance along the
+trip.
+
+#### Extend Service Calendars
+
+Sometimes you need to extend the service dates in a GTFS feed, perhaps in order to temporarily extend an expired feed. Extending
+the feed by hand can be a tedious task, especially when the feed uses a complex combination of `calendar.txt` and `calendar_dates.txt`
+entries. Fortunately, the GTFS tranformer tool supports a `calendar_extension` operation that can help simplify the work. Example:
+
+```
+{"op":"calendar_extension", "end_date":"20130331"}
+```
+
+The operation requires just one argument by default: `end_date` to specify the new end-date for the feed. The operation does
+its best to intelligently extend each service calendar, as identified by a `service_id` in `calendar.txt` or `calendar_dates.txt`.
+There are a few wrinkles to be aware of, however.
+
+Extending a `calendar.txt` entry is usually just a matter of setting a new `end_date` value in the feed. Extending a service
+calendar represented only through `calendar_dates.txt` entries is a bit more complex. For such a service calendar, we attempt to
+determine which days of the week are typically active for the calendar and extend only those. For example, is the calendar is
+always active on Saturday but has one or two Sunday entries, we will only add entries for Saturday when extending the calendar.
+
+Also note that we will not extend "inactive" service calendars. A service calendar is considered inactive if its last active
+service date is already in the past. By default, any calendar that's been expired for more than two weeks is considered inactive.
+This helps handle feeds that have merged two service periods in one feed. For example, one calendar active from June 1 - July 31
+and a second calendar active from August 1 to September 31. If it's the last week of September and you are extending the feed,
+you typically only mean to extend the second service calendar. You can control this inactive calendar cutoff with an optional
+argument:
+
+```
+{"op":"calendar_extension", "end_date":"20130331", "inactive_calendar_cutoff":"20121031"}
+```
+
+Calendars that have expired before the specified date will be considered inactive and won't be extended.
+
+_Note_: We don't make any effort to extend canceled service dates, as specified in `calendar_dates.txt` for holidays and
+other special events. It's too tricky to automatically determine how they should be handled. You may need to still handle
+those manually.
+
+#### Remove Old Calendar Statements
+
+RemoveOldCalendarStatements is an operation designed to remove calendar and calendar dates entries that are no longer valid on today's date.
+
+By default, it deletes entries from both the calendar.txt and calendar_dates.txt files, where the end_date in calendar.txt or the date field in calendar_dates.txt has passed.
+
+With the remove_today attribute added to the JSON transformer snippet, users can control whether entries in calendar or calendar_dates that are valid for today are included or excluded in the GTFS output.
+
+ * If remove_today is set to true, the transformer will remove entries for the current date.
+
+```
+ {"op":"transform", "class":"org.onebusaway.gtfs_transformer.impl.RemoveOldCalendarStatements", "remove_today":true}
+```
+
+ * If remove_today is set to false or not specified, the transformer will retain the calendar entries for the current date.
+
+```
+{"op":"transform", "class":"org.onebusaway.gtfs_transformer.impl.RemoveOldCalendarStatements", "remove_today":false}
+```
+
+Additionally, after truncating the calendar entries, it is recommended to use a **retain operation** to ensure that only trips with valid calendar dates are retained.
+
+Without this retain operation, the `trips.txt` file will contain trips with non-existent calendar dates, leading to invalid data.
+
+```
+{"op":"transform", "class":"org.onebusaway.gtfs_transformer.impl.RemoveOldCalendarStatements", "remove_today":false}
+{"op":"retain", "match":{"file":"calendar_dates.txt"}, "retainBlocks":false}
+```
+
+#### Deduplicate Calendar Entries
+
+Finds GTFS service_ids that have the exact same set of active days and consolidates each set of duplicated
+ids to a single service_id entry.
+
+```
+{"op":"deduplicate_service_ids"}
+```
+
+#### Truncate New Calendar Statements
+
+This operation truncates calendar and calendar date entries based on the configuration attributes in the JSON transformer snippet:
+
+ * calendar_field: Specifies the unit of time for truncation. It can have one of the following values:
+ - `Calendar.YEAR` = 1
+ - `Calendar.MONTH` = 2 (default)
+ - `Calendar.DAY_OF_MONTH` = 5
+ - `Calendar.DAY_OF_YEAR` = 6
+
+ * calendar_amount: Specifies the number of units to truncate entries.
+ The value is an integer representing the amount (default = 1).
+
+Both `calendar_field` and `calendar_amount` must be provided as integers in the JSON transformer.
+
+If these parameters are not specified, the default behavior is truncation by 1 month.
+
+Example :
+
+Truncate calendar and calendar dates to the next 21 days:
+
+```
+{"op":"transform", "class":"org.onebusaway.gtfs_transformer.impl.TruncateNewCalendarStatements","calendar_field":6,"calendar_amount":21}
+```
+
+Truncate entries to the next 3 months:
+
+```
+{"op":"transform", "class":"org.onebusaway.gtfs_transformer.impl.TruncateNewCalendarStatements","calendar_field":2,"calendar_amount":3}
+```
+
+Additionally, after truncating the calendar entries, it is recommended to use a **retain operation** to ensure that only trips with valid calendar dates are retained.
+
+Without this retain operation, the `trips.txt` file will contain trips with non-existent calendar dates, leading to invalid data.
+
+```
+{"op":"transform", "class":"org.onebusaway.gtfs_transformer.impl.TruncateNewCalendarStatements","calendar_field":6,"calendar_amount":21}
+{"op":"retain", "match":{"file":"calendar_dates.txt"}, "retainBlocks":false}
+```
+
+#### Merge Trips and Simplify Calendar Entries
+
+Some agencies model their transit schedule favoring multiple entries in calendar_dates.txt as opposed to a more concise
+entry in calendar.txt. A smaller number of agencies take this scheme even further, creating trips.txt entries for each
+service date, even when the underlying trips are exactly the same. This can cause the size of the GTFS to grow dramatically
+as trips and stop times are duplicated.
+
+We provide a simple transformer that can attempt to detect these duplicate trips, remove them, and simplify the underlying
+calendar entries to match. To run it, apply the following transform:
+
+```
+{"op":"calendar_simplification"}
+```
+
+The transform takes additional optional arguments to control its behavior:
+
+ * min_number_of_weeks_for_calendar_entry - how many weeks does a service id need to
+ span before it gets its own entry in calendar.txt (default=3)
+
+ * day_of_the_week_inclusion_ratio - if a service id is being modeled with a
+ calendar.txt entry, how frequent does a day of the week need to before it's
+ modeled positively in calendar.txt with any negative exceptions noted in
+ calendar_dates.txt, vs making no entry for that day of the week in
+ calendar.txt and instead noting any positive exceptions in
+ calendar_dates.txt. This is useful for filtering out a calendar that is
+ always active on Sunday, but has one or two Mondays for a holiday.
+ Frequency is defined as how often the target day of the week occurs vs the
+ count for day of the week appearing MOST frequently for the service id
+ (default=0.5)
+
+ * undo_google_transit_data_feed_merge_tool - set to true to indicate that merged trip ids,
+ as produced by the [GoogleTransitDataFeedMergeTool](http://code.google.com/p/googletransitdatafeed/wiki/Merge),
+ should be un-mangled where possible. Merged trip ids will often have the form
+ `OriginalTripId_merged_1234567`. We attempt to set the trip id back to `OrginalTripId`
+ where appropriate.
+
+
+#### Shift Negative Stop Times
+
+Some agencies have trips that they model as starting BEFORE midnight on a given service date. For these agencies, it
+would be convenient to represent these trips with negative arrival and departure times in stop_times.txt. The GTFS spec and
+many GTFS consumers do not support negative stop times, however.
+
+To help these agencies, we provide a transform to "fix" GTFS feeds with negative stop times by identifying such trips,
+shifting the arrival and departure times to make them positive, and updating the service calendar entries for these trips
+such that the resulting schedule is semantically the same.
+
+To run it, apply the following transform:
+
+```
+{"op":"shift_negative_stop_times"}
+```
+
+_A note on negative stop times:_ When writing negative stop times, the negative value ONLY applies to the hour portion
+ of the time. Here are a few examples:
+
+ * "-01:45:00" => "23:45:00" on the previous day
+
+ * "-05:13:32" => "19:13:32" on the previous day
+
+* Remove non-revenue stops
+
+ Stop_times which do not allow pick up or drop off are also known as non-revenue stops. Some GTFS consumers display
+ these stops as if they were stops that passengers can use, at which point it is helpful to remove them.
+
+ To remove them, apply the following transform:
+
+```
+{"op":"remove_non_revenue_stops"}
+```
+
+ Terminals (the first and last stop_time of a trip) can be excluded from removal with the following transform:
+
+```
+{"op":"remove_non_revenue_stops_excluding_terminals"}}
+```
+
+* Replacing trip_headsign with the last stop
+
+ Certain feeds contain unhelpful or incorrect trip_headsign. They can be replaced with the last stop's stop_name.
+
+```
+{"op":"last_stop_to_headsign"}
+```
+
+#### Arbitrary Transform
+
+We also allow you to specify arbitrary transformations as well. Here, you specify your transformation class and we will
+automatically instantiate it for use in the transform pipeline.
+
+```
+{"op":"transform", "class":"some.class.implementing.GtfsTransformStrategy"}
+```
+
+We additionally provide a mechanism for setting additional properties of the transform. For all additional properties
+specified in the JSON snippet, we will attempt to set that Java bean property value on the instantiated transformation object.
+See for example:
+
+```
+{"op":"transform", "class":"org.onebusaway.gtfs_transformer.updates.ShapeTransformStrategy", "shape_id":"6010031", \
+"shape":"wjb~G|abmVpAz]v_@@?wNE_GDaFs@?@dFX`GGjN__@A"}
+```
+
+Here, we set additional properties on the `ShapeTransformStrategy`, making it possible to reuse and configure a generic
+transformer to your needs.
+
+Additional Examples
+
+### How to Reduce your GTFS
+
+We can apply a modification that retains certain GTFS entities and all other entities required directly or indirectly by
+those entities. For example, create a file with the following contents (call it modifications.txt, as an example):
+
+```
+{"op":"retain", "match":{"file":"routes.txt", "route_short_name":"B15"}}
+{"op":"retain", "match":{"file":"routes.txt", "route_short_name":"B62"}}
+{"op":"retain", "match":{"file":"routes.txt", "route_short_name":"B63"}}
+{"op":"retain", "match":{"file":"routes.txt", "route_short_name":"BX19"}}
+{"op":"retain", "match":{"file":"routes.txt", "route_short_name":"Q54"}}
+{"op":"retain", "match":{"file":"routes.txt", "route_short_name":"S53"}}
+```
+
+Then run:
+
+```
+java -jar onebusaway-gtfs-transformer-cli.jar --transform=modifications.txt source-gtfs.zip target-gtfs.zip
+```
+
+The resulting GTFS will have the retained only the routes with the matching short names and all other entities required
+to support those routes.
+
+* Add a Full Schedule to an Existing Feed
+
+Consider an existing feed with a number of routes and stops. We can add an entirely new route, with trips and stop-times
+and frequency-based service, using the transform. This can be handy to add temporary service to an existing feed.
+
+```
+{"op":"add", "obj":{"file":"routes.txt", "route_id":"r0", "route_long_name":"Temporary Shuttle", "route_type":3}}
+
+{"op":"add", "obj":{"file":"calendar.txt", "service_id":"WEEKDAY", "start_date":"20120601", "end_date":"20130630", "monday":1, "tuesday":1, "wednesday":1, "thursday":1, "friday":1}}
+
+{"op":"add", "obj":{"file":"trips.txt", "trip_id":"t0", "route_id":"r0", "service_id":"WEEKDAY", "trip_headsign":"Inbound"}}
+{"op":"add", "obj":{"file":"trips.txt", "trip_id":"t1", "route_id":"r0", "service_id":"WEEKDAY", "trip_headsign":"Outbound"}}
+
+{"op":"add","obj":{"file":"frequencies.txt","trip_id":"t0","start_time":"06:00:00","end_time":"22:00:00","headway_secs":900}}
+{"op":"add","obj":{"file":"frequencies.txt","trip_id":"t1","start_time":"06:00:00","end_time":"22:00:00","headway_secs":900}}
+
+{"op":"stop_times_factory", "trip_id":"t0", "start_time":"06:00:00", "end_time":"06:20:00", "stop_ids":["s0", "s1", "s2", "s3"]}
+{"op":"stop_times_factory", "trip_id":"t1", "start_time":"06:00:00", "end_time":"06:20:00", "stop_ids":["s3", "s2", "s1", "s0"]}
+```
+
+### Clip National GTFS for Regional Integration and Consistency
+
+This section of the document describes how to reduce a large GTFS to a smaller area. Several transformations can be applied to a national GTFS to clean it up and adjust the data to a regional area in order to get ready for the integration with another regional GTFS. Below is an overview of the operations carried out:
+
+ * Removing Inactive Calendars and Dates.
+ * Truncating Calendars and Dates to 21 days.
+ * Retaining Data Within a Specific Geographic Area: a small geographic area is used for retaining only the entities within our area of interest. All routes and trips that do not pass through this area will therefore be eliminated.
+ * Trimming Stop Times Outside a Specific Geographic Area: a larger polygon is used to ensure that only the relevant stops_times within a wider region are retained. That means that all trips that go outside the area are truncated.
+ * Clean up entities that are no longer referenced by any trips.
+
+RetainUpFromPolygon and TrimTripFromPolygon together will clip the GTFS data to a small area and allow some Origin/Destination transit to nearby cities.
+
+```
+{"op":"transform", "class":"org.onebusaway.gtfs_transformer.impl.RemoveOldCalendarStatements"}
+{"op":"transform", "class":"org.onebusaway.gtfs_transformer.impl.TruncateNewCalendarStatements","calendar_field":6,"calendar_amount":21}
+{"op":"retain", "match":{"file":"calendar_dates.txt"}, "retainBlocks":false}
+
+{"op":"transform","class":"org.onebusaway.gtfs_transformer.impl.RetainUpFromPolygon","polygon":"MULTIPOLYGON (((1.2 43.7, 1.55 43.7, 1.55 43.4, 1.2 43.4, 1.2 43.7)))"}
+
+{"op":"transform","class":"org.onebusaway.gtfs_transformer.impl.TrimTripFromPolygon","polygon":"MULTIPOLYGON (((1.0 44.2, 2.2 44.2, 2.2 43.3, 1.0 43.3, 1.0 44.2)))"}
+{"op":"retain", "match":{"file":"trips.txt"}, "retainBlocks":false}
+```
+
+![RetainUpFromPolygon and TrimTripFromPolygon](onebusaway-gtfs-transformer-cli-sample1.png "RetainUpFromPolygon and TrimTripFromPolygon")
\ No newline at end of file
diff --git a/onebusaway-collections/pom.xml b/onebusaway-collections/pom.xml
new file mode 100644
index 000000000..e2b825966
--- /dev/null
+++ b/onebusaway-collections/pom.xml
@@ -0,0 +1,58 @@
+
+ 4.0.0
+
+ onebusaway-collections
+ jar
+
+ onebusaway-collections
+ A library with a number of convenient methods for working with collections
+
+
+ org.onebusaway
+ onebusaway-gtfs-modules
+ 5.0.1-openmove-1
+ ../pom.xml
+
+
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+ io.github.classgraph
+ classgraph
+ 4.8.179
+
+
+
+
+
+
+ com.google.cloud.tools
+ jib-maven-plugin
+
+
+ true
+
+
+
+
+ io.github.zlika
+ reproducible-build-maven-plugin
+ 0.17
+
+
+ run-when-packaged
+
+ strip-jar
+
+ package
+
+
+
+
+
+
diff --git a/onebusaway-gtfs/src/main/java/META-INF/MANIFEST.MF b/onebusaway-collections/src/main/java/META-INF/MANIFEST.MF
similarity index 100%
rename from onebusaway-gtfs/src/main/java/META-INF/MANIFEST.MF
rename to onebusaway-collections/src/main/java/META-INF/MANIFEST.MF
diff --git a/onebusaway-collections/src/main/java/org/onebusaway/collections/CollectionsLibrary.java b/onebusaway-collections/src/main/java/org/onebusaway/collections/CollectionsLibrary.java
new file mode 100644
index 000000000..868b587ff
--- /dev/null
+++ b/onebusaway-collections/src/main/java/org/onebusaway/collections/CollectionsLibrary.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (C) 2011 Brian Ferris
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.collections;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+public class CollectionsLibrary {
+
+ public static Set set(T... values) {
+ Set set = new HashSet();
+ for (T value : values)
+ set.add(value);
+ return set;
+ }
+
+ public static final boolean isEmpty(Collection> c) {
+ return c == null || c.isEmpty();
+ }
+}
diff --git a/onebusaway-collections/src/main/java/org/onebusaway/collections/ConcurrentCollectionsLibrary.java b/onebusaway-collections/src/main/java/org/onebusaway/collections/ConcurrentCollectionsLibrary.java
new file mode 100644
index 000000000..141dbd949
--- /dev/null
+++ b/onebusaway-collections/src/main/java/org/onebusaway/collections/ConcurrentCollectionsLibrary.java
@@ -0,0 +1,167 @@
+/**
+ * Copyright (C) 2011 Brian Ferris
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.collections;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+
+public class ConcurrentCollectionsLibrary {
+
+ private static final ListFactory _listFactory = new ListFactory();
+
+ private static final SetFactory _setFactory = new SetFactory();
+
+ public static void addToMapValueList(
+ ConcurrentMap> map, KEY key, VALUE value) {
+ CollectionFactory> factory = listFactory();
+ addToMapValueCollection(map, key, value, factory);
+ }
+
+ public static void removeFromMapValueList(
+ ConcurrentMap> map, KEY key, VALUE value) {
+ CollectionFactory> factory = listFactory();
+ removeFromMapValueCollection(map, key, value, factory);
+ }
+
+ public static void addToMapValueSet(
+ ConcurrentMap> map, KEY key, VALUE value) {
+ CollectionFactory> factory = setFactory();
+ addToMapValueCollection(map, key, value, factory);
+ }
+
+ public static void removeFromMapValueSet(
+ ConcurrentMap> map, KEY key, VALUE value) {
+ CollectionFactory> factory = setFactory();
+ removeFromMapValueCollection(map, key, value, factory);
+ }
+
+ /****
+ *
+ ****/
+
+ private static > void addToMapValueCollection(
+ ConcurrentMap map, KEY key, VALUE value,
+ CollectionFactory factory) {
+
+ while (true) {
+
+ C values = map.get(key);
+
+ if (values == null) {
+ C newKeys = factory.create(value);
+ values = map.putIfAbsent(key, newKeys);
+ if (values == null)
+ return;
+ }
+
+ C origCopy = factory.copy(values);
+
+ if (origCopy.contains(value))
+ return;
+
+ C extendedCopy = factory.copy(origCopy);
+ extendedCopy.add(value);
+
+ if (map.replace(key, origCopy, extendedCopy))
+ return;
+ }
+ }
+
+ private static > void removeFromMapValueCollection(
+ ConcurrentMap map, KEY key, VALUE value,
+ CollectionFactory factory) {
+
+ while (true) {
+
+ C values = map.get(key);
+
+ if (values == null)
+ return;
+
+ C origCopy = factory.copy(values);
+
+ if (!origCopy.contains(value))
+ return;
+
+ C reducedCopy = factory.copy(origCopy);
+ reducedCopy.remove(value);
+
+ if (reducedCopy.isEmpty()) {
+ if (map.remove(key, origCopy))
+ return;
+ } else {
+ if (map.replace(key, origCopy, reducedCopy))
+ return;
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static CollectionFactory> listFactory() {
+ return _listFactory;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static CollectionFactory> setFactory() {
+ return _setFactory;
+ }
+
+ private interface CollectionFactory> {
+ public C create(VALUE value);
+
+ public C copy(C existingValues);
+ }
+
+ @SuppressWarnings("rawtypes")
+ private static class ListFactory implements CollectionFactory {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Collection create(Object value) {
+ List values = new ArrayList(1);
+ values.add(value);
+ return values;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Collection copy(Collection existingValues) {
+ return new ArrayList(existingValues);
+ }
+ }
+
+ @SuppressWarnings("rawtypes")
+ private static class SetFactory implements CollectionFactory {
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Collection create(Object value) {
+ Set values = new HashSet();
+ values.add(value);
+ return values;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Collection copy(Collection existingValues) {
+ return new HashSet(existingValues);
+ }
+ }
+}
diff --git a/onebusaway-collections/src/main/java/org/onebusaway/collections/Counter.java b/onebusaway-collections/src/main/java/org/onebusaway/collections/Counter.java
new file mode 100644
index 000000000..82d98bccb
--- /dev/null
+++ b/onebusaway-collections/src/main/java/org/onebusaway/collections/Counter.java
@@ -0,0 +1,122 @@
+/**
+ * Copyright (C) 2011 Brian Ferris
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.collections;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class Counter implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private Map _counts = new HashMap();
+
+ private int _total = 0;
+
+ public int size() {
+ return _counts.size();
+ }
+
+ public void increment(T key, int offset) {
+ int count = getCount(key) + offset;
+ _counts.put(key, count);
+ _total += offset;
+ }
+
+ public void increment(T key) {
+ increment(key, 1);
+ }
+
+ public void decrement(T key) {
+ increment(key, -1);
+ }
+
+ public int getCount(T key) {
+ Integer count = _counts.get(key);
+ if (count == null)
+ count = 0;
+ return count;
+ }
+
+ public Set getKeys() {
+ return _counts.keySet();
+ }
+
+ public Set> getEntrySet() {
+ return _counts.entrySet();
+ }
+
+ public int getTotal() {
+ return _total;
+ }
+
+ public T getMax() {
+ int maxCount = 0;
+ T maxValue = null;
+ for (Map.Entry entry : _counts.entrySet()) {
+ if (maxValue == null || maxCount < entry.getValue()) {
+ maxValue = entry.getKey();
+ maxCount = entry.getValue();
+ }
+ }
+ return maxValue;
+ }
+
+ /**
+ * @return sorted from min to max
+ */
+ public List getSortedKeys() {
+ List values = new ArrayList(_counts.keySet());
+ Collections.sort(values, new Comparator() {
+ public int compare(T o1, T o2) {
+ int a = getCount(o1);
+ int b = getCount(o2);
+ if (a == b)
+ return 0;
+ return a < b ? -1 : 1;
+ }
+ });
+ return values;
+ }
+
+ /*******************************************************************************************************************
+ * {@link Object} Interface
+ ******************************************************************************************************************/
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || !(obj instanceof Counter))
+ return false;
+ Counter> c = (Counter>) obj;
+ return _counts.equals(c._counts);
+ }
+
+ @Override
+ public int hashCode() {
+ return _counts.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return _counts.toString();
+ }
+}
diff --git a/onebusaway-collections/src/main/java/org/onebusaway/collections/DirectedGraph.java b/onebusaway-collections/src/main/java/org/onebusaway/collections/DirectedGraph.java
new file mode 100644
index 000000000..15cae7d19
--- /dev/null
+++ b/onebusaway-collections/src/main/java/org/onebusaway/collections/DirectedGraph.java
@@ -0,0 +1,164 @@
+/**
+ * Copyright (C) 2011 Brian Ferris
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.collections;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.onebusaway.collections.tuple.Pair;
+import org.onebusaway.collections.tuple.Tuples;
+
+public class DirectedGraph {
+
+ private Map> _outboundEdges = new HashMap>();
+
+ private Map> _inboundEdges = new HashMap>();
+
+ public DirectedGraph() {
+
+ }
+
+ public DirectedGraph(DirectedGraph graph) {
+ for (T node : graph.getNodes())
+ addNode(node);
+ for (Pair edge : graph.getEdges())
+ addEdge(edge.getFirst(), edge.getSecond());
+ }
+
+ public Set getNodes() {
+ Set nodes = new HashSet();
+ nodes.addAll(_outboundEdges.keySet());
+ nodes.addAll(_inboundEdges.keySet());
+ return nodes;
+ }
+
+ public Set> getEdges() {
+ Set> edges = new HashSet>();
+ for (T from : _outboundEdges.keySet()) {
+ for (T to : _outboundEdges.get(from))
+ edges.add(Tuples.pair(from, to));
+ }
+ return edges;
+ }
+
+ public Set getInboundNodes(T node) {
+ return get(_inboundEdges, node, false);
+ }
+
+ public Set getOutboundNodes(T node) {
+ return get(_outboundEdges, node, false);
+ }
+
+ public boolean isConnected(T from, T to) {
+
+ if (from.equals(to))
+ return true;
+
+ return isConnected(from, to, new HashSet());
+ }
+
+ public void addNode(T node) {
+ get(_outboundEdges, node, true);
+ get(_inboundEdges, node, true);
+ }
+
+ public void addEdge(T from, T to) {
+ get(_outboundEdges, from, true).add(to);
+ get(_inboundEdges, to, true).add(from);
+ }
+
+ public void removeEdge(T from, T to) {
+ get(_outboundEdges, from, false).remove(to);
+ get(_inboundEdges, to, false).remove(from);
+ }
+
+ private void removeNode(T node) {
+
+ for (T from : get(_inboundEdges, node, false))
+ get(_outboundEdges, from, false).remove(node);
+ _inboundEdges.remove(node);
+
+ for (T to : get(_outboundEdges, node, false))
+ get(_inboundEdges, to, false).remove(node);
+ _outboundEdges.remove(node);
+ }
+
+ public List getTopologicalSort(Comparator tieBreaker) {
+
+ List order = new ArrayList();
+ DirectedGraph g = new DirectedGraph(this);
+
+ while (true) {
+
+ Set nodes = g.getNodes();
+
+ if (nodes.isEmpty())
+ return order;
+
+ List noInbound = new ArrayList();
+
+ for (T node : nodes) {
+ if (g.getInboundNodes(node).isEmpty())
+ noInbound.add(node);
+ }
+
+ if (noInbound.isEmpty())
+ throw new IllegalStateException("cycle");
+
+ if (tieBreaker != null)
+ Collections.sort(noInbound, tieBreaker);
+
+ T node = noInbound.get(0);
+ order.add(node);
+ g.removeNode(node);
+ }
+ }
+
+ /****
+ * Private Methods
+ ****/
+
+ private boolean isConnected(T from, T to, Set visited) {
+
+ if (from.equals(to))
+ return true;
+
+ for (T next : get(_outboundEdges, from, false)) {
+ if (visited.add(next)) {
+ if (isConnected(next, to, visited))
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private Set get(Map> edges, T key, boolean create) {
+ Set set = edges.get(key);
+ if (set == null) {
+ set = new HashSet();
+ if (create)
+ edges.put(key, set);
+ }
+ return set;
+ }
+}
diff --git a/onebusaway-collections/src/main/java/org/onebusaway/collections/FactoryMap.java b/onebusaway-collections/src/main/java/org/onebusaway/collections/FactoryMap.java
new file mode 100644
index 000000000..5581388f1
--- /dev/null
+++ b/onebusaway-collections/src/main/java/org/onebusaway/collections/FactoryMap.java
@@ -0,0 +1,291 @@
+/**
+ * Copyright (C) 2011 Brian Ferris
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.collections;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+
+/**
+ * A extension of {@link HashMap} that will automatically create a {@link Map}
+ * key-value entry if a call to {@link #get(Object)} is made where the key is
+ * not already present in the map.
+ *
+ * Default map entries can be created by passing in an instance of
+ * {@link IValueFactory} as an object factory (see
+ * {@link #FactoryMap(IValueFactory)}).
+ *
+ * Maps can also be created by passing a plain-old Java object. The class of the
+ * Java object will be used to create new value instances on demand, as long the
+ * class has a no-arg constructor (see {@link #FactoryMap(Object)}).
+ *
+ * @author bdferris
+ */
+public class FactoryMap extends HashMap {
+
+ private static final long serialVersionUID = 1L;
+
+ private IValueFactory _valueFactory;
+
+ /**
+ * Object factory interface for creating a new value for a specified key for
+ * use in {@link FactoryMap}
+ *
+ * @author bdferris
+ */
+ public interface IValueFactory {
+ public VF create(KF key);
+ }
+
+ /**
+ * A convenience method for creating an instance of {@link FactoryMap} that
+ * wraps an existing {@link Map} and has a specific default value. The default
+ * value's class will be used to create new value instances as long as it has
+ * a no-arg constructor.
+ *
+ * @param map an existing map to wrap
+ * @param defaultValue see {@link #FactoryMap(Object)} for discussion
+ * @return a {@link Map} with factory-map behavior
+ */
+ public static Map create(Map map, V defaultValue) {
+ return new MapImpl(map, new ClassInstanceFactory(
+ defaultValue.getClass()));
+ }
+
+ /**
+ * A convenience method for creating an instance of {@link FactoryMap} that
+ * wraps an existing {@link Map} and has a specific default value factory.
+ *
+ * @param map an existing map to wrap
+ * @param factory see {@link #FactoryMap(IValueFactory)} for discussion
+ * @return a {@link Map} with factory-map behavior
+ */
+ public static Map create(Map map,
+ IValueFactory factory) {
+ return new MapImpl(map, factory);
+ }
+
+ /**
+ * A convenience method for creating an instance of {@link FactoryMap} that
+ * wraps an existing {@link SortedMap} and has a specific default value. The
+ * default value's class will be used to create new value instances as long as
+ * it has a no-arg constructor.
+ *
+ * @param map an existing sorted map to wrap
+ * @param defaultValue see {@link #FactoryMap(Object)} for discussion
+ * @return a {@link SortedMap} with factory-map behavior
+ */
+ public static SortedMap createSorted(SortedMap map,
+ V defaultValue) {
+ return new SortedMapImpl(map, new ClassInstanceFactory(
+ defaultValue.getClass()));
+ }
+
+ /**
+ * A convenience method for creating an instance of {@link FactoryMap} that
+ * wraps an existing {@link SortedMap} and has a specific default value
+ * factory.
+ *
+ * @param map an existing sorted map to wrap
+ * @param factory see {@link #FactoryMap(IValueFactory)} for discussion
+ * @return a {@link SortedMap} with factory-map behavior
+ */
+ public static SortedMap createSorted(SortedMap map,
+ IValueFactory factory) {
+ return new SortedMapImpl(map, factory);
+ }
+
+ /**
+ * A factory map constructor that accepts a default value instance. The
+ * {@link Class} of the default value instance will be used to create new
+ * default value instances as needed assuming the class has no-arg
+ * constructor. New values will be created when calls are made to
+ * {@link #get(Object)} and the specified key is not already present in the
+ * map. Why do we accept an object instance instead of a class instance? It
+ * makes it easier to handle cases where V is itself a parameterized type.
+ *
+ * @param factoryInstance the {@link Class} of the instance will be used to
+ * create new values as needed
+ */
+ public FactoryMap(V factoryInstance) {
+ this(new ClassInstanceFactory(factoryInstance.getClass()));
+ }
+
+ /**
+ * A factory map constructor that accepts a {@link IValueFactory} default
+ * value factory. The value factory will be called when calls are made to
+ * {@link #get(Object)} and the specified key is not already present in the
+ * map.
+ *
+ * @param valueFactory the default value factory
+ */
+ public FactoryMap(IValueFactory valueFactory) {
+ _valueFactory = valueFactory;
+ }
+
+ /**
+ * Returns the value to which the specified key is mapped, or a default value
+ * instance if the specified key is not present in the map. Subsequent clals
+ * to {@link #get(Object)} with the same key will return the same value
+ * instance.
+ *
+ * @see Map#get(Object)
+ * @see #put(Object, Object)
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public V get(Object key) {
+ if (!containsKey(key))
+ put((K) key, createValue((K) key));
+ return super.get(key);
+ }
+
+ private V createValue(K key) {
+ return _valueFactory.create(key);
+ }
+
+ private static class ClassInstanceFactory implements
+ IValueFactory, Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private Class extends V> _valueClass;
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public ClassInstanceFactory(Class valueClass) {
+ _valueClass = valueClass;
+ }
+
+ public V create(K key) {
+ try {
+ return _valueClass.newInstance();
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+
+ private static class MapImpl implements Map, Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private Map _source;
+
+ private IValueFactory _valueFactory;
+
+ public MapImpl(Map source, IValueFactory valueFactory) {
+ _source = source;
+ _valueFactory = valueFactory;
+ }
+
+ public void clear() {
+ _source.clear();
+ }
+
+ public boolean containsKey(Object key) {
+ return _source.containsKey(key);
+ }
+
+ public boolean containsValue(Object value) {
+ return _source.containsValue(value);
+ }
+
+ public Set> entrySet() {
+ return _source.entrySet();
+ }
+
+ @SuppressWarnings("unchecked")
+ public V get(Object key) {
+ if (!containsKey(key))
+ _source.put((K) key, createValue((K) key));
+ return _source.get(key);
+ }
+
+ public boolean isEmpty() {
+ return _source.isEmpty();
+ }
+
+ public Set keySet() {
+ return _source.keySet();
+ }
+
+ public V put(K key, V value) {
+ return _source.put(key, value);
+ }
+
+ public void putAll(Map extends K, ? extends V> t) {
+ _source.putAll(t);
+ }
+
+ public V remove(Object key) {
+ return _source.remove(key);
+ }
+
+ public int size() {
+ return _source.size();
+ }
+
+ public Collection values() {
+ return _source.values();
+ }
+
+ private V createValue(K key) {
+ return _valueFactory.create(key);
+ }
+ }
+
+ private static class SortedMapImpl extends MapImpl implements
+ SortedMap {
+
+ private static final long serialVersionUID = 1L;
+
+ private SortedMap _source;
+
+ public SortedMapImpl(SortedMap source,
+ IValueFactory valueFactory) {
+ super(source, valueFactory);
+ _source = source;
+ }
+
+ public Comparator super K> comparator() {
+ return _source.comparator();
+ }
+
+ public K firstKey() {
+ return _source.firstKey();
+ }
+
+ public SortedMap headMap(K toKey) {
+ return _source.headMap(toKey);
+ }
+
+ public K lastKey() {
+ return _source.lastKey();
+ }
+
+ public SortedMap subMap(K fromKey, K toKey) {
+ return _source.subMap(fromKey, toKey);
+ }
+
+ public SortedMap tailMap(K fromKey) {
+ return _source.tailMap(fromKey);
+ }
+ }
+}
diff --git a/onebusaway-collections/src/main/java/org/onebusaway/collections/FunctionalLibrary.java b/onebusaway-collections/src/main/java/org/onebusaway/collections/FunctionalLibrary.java
new file mode 100644
index 000000000..82a847753
--- /dev/null
+++ b/onebusaway-collections/src/main/java/org/onebusaway/collections/FunctionalLibrary.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (C) 2011 Brian Ferris
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.collections;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.onebusaway.collections.beans.PropertyPathExpression;
+
+public final class FunctionalLibrary {
+ private FunctionalLibrary() {
+
+ }
+
+ public static List filter(Iterable elements,
+ String propertyPathExpression, Object value) {
+ List matches = new ArrayList();
+ PropertyPathExpression query = new PropertyPathExpression(
+ propertyPathExpression);
+ for (T element : elements) {
+ Object result = query.invoke(element);
+ if ((value == null && result == null)
+ || (value != null && value.equals(result)))
+ matches.add(element);
+ }
+ return matches;
+ }
+
+ public static T filterFirst(Iterable elements,
+ String propertyPathExpression, Object value) {
+ List matches = filter(elements, propertyPathExpression, value);
+ return matches.isEmpty() ? null : matches.get(0);
+ }
+}
diff --git a/onebusaway-collections/src/main/java/org/onebusaway/collections/MappingLibrary.java b/onebusaway-collections/src/main/java/org/onebusaway/collections/MappingLibrary.java
new file mode 100644
index 000000000..746d471ba
--- /dev/null
+++ b/onebusaway-collections/src/main/java/org/onebusaway/collections/MappingLibrary.java
@@ -0,0 +1,193 @@
+/**
+ * Copyright (C) 2011 Brian Ferris
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.collections;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.onebusaway.collections.beans.PropertyPathExpression;
+
+/**
+ * A number of functional-programming-inspired convenience methods for mapping
+ * one set of values to another.
+ *
+ * @author bdferris
+ * @see PropertyPathExpression
+ */
+public class MappingLibrary {
+
+ /**
+ * Iterate over a collection of values, evaluating a
+ * {@link PropertyPathExpression} on each value, and constructing a
+ * {@link List} from the expression results.
+ *
+ * @param values an iterable collection of values to iterate over
+ * @param propertyPathExpression a property path expression to evaluate
+ * against each collection value
+ * @return a List composed of the property path expression evaluation results
+ */
+ @SuppressWarnings("unchecked")
+ public static List map(Iterable values,
+ String propertyPathExpression) {
+ List mappedValues = new ArrayList();
+ PropertyPathExpression query = new PropertyPathExpression(
+ propertyPathExpression);
+ for (T1 value : values)
+ mappedValues.add((T2) query.invoke(value));
+ return mappedValues;
+ }
+
+ /**
+ * This method is kept for backwards compatibility, and a more concise version
+ * can be found in {@link #map(Iterable, String)}
+ */
+ public static List map(Iterable values,
+ String propertyPathExpression, Class resultType) {
+ return map(values, propertyPathExpression);
+ }
+
+ /**
+ * Construct a {@link Map} from a set of values where the key for each value
+ * is the result from the evaluation of a {@link PropertyPathExpression} on
+ * each value. If two values in the iterable collection have the same key,
+ * subsequent values will overwrite previous values.
+ *
+ * @param values an iterable collection of values to iterate over
+ * @param propertyPathExpression a property path expression to evaluate
+ * against each collection value
+ * @return a map with values from the specified collection and keys from the
+ * property path expression
+ */
+ @SuppressWarnings("unchecked")
+ public static Map mapToValue(Iterable values,
+ String propertyPathExpression) {
+
+ Map byKey = new HashMap();
+ PropertyPathExpression query = new PropertyPathExpression(
+ propertyPathExpression);
+
+ for (V value : values) {
+ K key = (K) query.invoke(value);
+ byKey.put(key, value);
+ }
+
+ return byKey;
+ }
+
+ /**
+ * This method is kept for backwards compatibility, and a more concise version
+ * can be found in {@link #mapToValue(Iterable, String)}
+ */
+ public static Map mapToValue(Iterable values,
+ String property, Class keyType) {
+ return mapToValue(values, property);
+ }
+
+ /**
+ * Construct a {@link Map} from a set of values where the key for each value
+ * is the result of the evaluation of a {@link PropertyPathExpression} on each
+ * value. Each key maps to a {@link List} of values that all mapped to that
+ * same key.
+ *
+ * @param values an iterable collection of values to iterate over
+ * @param propertyPathExpression a property path expression to evaluate
+ * against each collection value
+ * @return a map with values from the specified collection and keys from the
+ * property path expression
+ */
+ @SuppressWarnings("unchecked")
+ public static Map> mapToValueList(Iterable values,
+ String property) {
+ return mapToValueCollection(values, property, new ArrayList().getClass());
+ }
+
+ /**
+ * This method is kept for backwards compatibility, and a more concise version
+ * can be found in {@link #mapToValueList(Iterable, String)}
+ */
+ @SuppressWarnings("unchecked")
+ public static Map> mapToValueList(Iterable values,
+ String property, Class keyType) {
+ return mapToValueCollection(values, property, new ArrayList().getClass());
+ }
+
+ /**
+ * Construct a {@link Map} from a set of values where the key for each value
+ * is the result of the evaluation of a {@link PropertyPathExpression} on each
+ * value. Each key maps to a {@link Set} of values that all mapped to that
+ * same key.
+ *
+ * @param values an iterable collection of values to iterate over
+ * @param propertyPathExpression a property path expression to evaluate
+ * against each collection value
+ * @return a map with values from the specified collection and keys from the
+ * property path expression
+ */
+
+ @SuppressWarnings("unchecked")
+ public static Map> mapToValueSet(Iterable values,
+ String property) {
+ return mapToValueCollection(values, property, new HashSet().getClass());
+ }
+
+ /**
+ * Construct a {@link Map} from a set of values where the key for each value
+ * is the result of the evaluation of a {@link PropertyPathExpression} on each
+ * value. Each key maps to a collection of values that all mapped to that same
+ * key. The collection type must have a no-arg constructor that can be used to
+ * create new collection instances as necessary.
+ *
+ * @param values an iterable collection of values to iterate over
+ * @param propertyPathExpression a property path expression to evaluate
+ * against each collection value
+ * @param collectionType the collection type used to contain mutiple values
+ * that map to the same key
+ * @return a map with values from the specified collection and keys from the
+ * property path expression
+ */
+ @SuppressWarnings("unchecked")
+ public static , CIMPL extends C> Map mapToValueCollection(
+ Iterable values, String property, Class collectionType) {
+
+ Map byKey = new HashMap();
+ PropertyPathExpression query = new PropertyPathExpression(property);
+
+ for (V value : values) {
+
+ K key = (K) query.invoke(value);
+ C valuesForKey = byKey.get(key);
+ if (valuesForKey == null) {
+
+ try {
+ valuesForKey = collectionType.newInstance();
+ } catch (Exception ex) {
+ throw new IllegalStateException(
+ "error instantiating collection type: " + collectionType, ex);
+ }
+
+ byKey.put(key, valuesForKey);
+ }
+ valuesForKey.add(value);
+ }
+
+ return byKey;
+ }
+}
diff --git a/onebusaway-collections/src/main/java/org/onebusaway/collections/Max.java b/onebusaway-collections/src/main/java/org/onebusaway/collections/Max.java
new file mode 100644
index 000000000..22443582c
--- /dev/null
+++ b/onebusaway-collections/src/main/java/org/onebusaway/collections/Max.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (C) 2011 Brian Ferris
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.collections;
+
+public class Max {
+
+ private Min _min = new Min();
+
+ public void add(double value, T element) {
+ _min.add(-value, element);
+ }
+
+ public T getMaxElement() {
+ return _min.getMinElement();
+ }
+
+ public double getMaxValue() {
+ return -_min.getMinValue();
+ }
+}
diff --git a/onebusaway-collections/src/main/java/org/onebusaway/collections/Min.java b/onebusaway-collections/src/main/java/org/onebusaway/collections/Min.java
new file mode 100644
index 000000000..88d41369a
--- /dev/null
+++ b/onebusaway-collections/src/main/java/org/onebusaway/collections/Min.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (C) 2011 Brian Ferris
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.collections;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Min {
+
+ private double _minValue = Double.POSITIVE_INFINITY;
+
+ private List _minElements = new ArrayList();
+
+ public void add(double value, T element) {
+ if (value == _minValue) {
+ _minElements.add(element);
+ } else if (value < _minValue) {
+ _minElements.clear();
+ _minElements.add(element);
+ _minValue = value;
+ }
+ }
+
+ public boolean isEmpty() {
+ return _minElements.isEmpty();
+ }
+
+ public double getMinValue() {
+ return _minValue;
+ }
+
+ public T getMinElement() {
+ if (_minElements.isEmpty())
+ return null;
+ return _minElements.get(0);
+ }
+
+ public List getMinElements() {
+ return _minElements;
+ }
+
+}
diff --git a/onebusaway-collections/src/main/java/org/onebusaway/collections/Range.java b/onebusaway-collections/src/main/java/org/onebusaway/collections/Range.java
new file mode 100644
index 000000000..f53dc3925
--- /dev/null
+++ b/onebusaway-collections/src/main/java/org/onebusaway/collections/Range.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright (C) 2011 Brian Ferris
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.collections;
+
+/**
+ * A simple, mutable double-precision range class for tracking a min and max
+ * value.
+ *
+ * @author bdferris
+ */
+public class Range {
+
+ private double _min = Double.POSITIVE_INFINITY;
+
+ private double _max = Double.NEGATIVE_INFINITY;
+
+ public Range() {
+
+ }
+
+ public Range(double v) {
+ addValue(v);
+ }
+
+ public Range(double from, double to) {
+ addValue(from);
+ addValue(to);
+ }
+
+ public void addValue(double value) {
+ _min = Math.min(_min, value);
+ _max = Math.max(_max, value);
+ }
+
+ public void setMin(double value) {
+ _min = value;
+ _max = Math.max(_max, value);
+ }
+
+ public void setMax(double value) {
+ _min = Math.min(_min, value);
+ _max = value;
+ }
+
+ public double getMin() {
+ return _min;
+ }
+
+ public double getMax() {
+ return _max;
+ }
+
+ public double getRange() {
+ return _max - _min;
+ }
+
+ public boolean isEmpty() {
+ return _min > _max;
+ }
+
+ @Override
+ public String toString() {
+ return _min + " " + _max;
+ }
+}
diff --git a/onebusaway-collections/src/main/java/org/onebusaway/collections/adapter/AdaptableCollection.java b/onebusaway-collections/src/main/java/org/onebusaway/collections/adapter/AdaptableCollection.java
new file mode 100644
index 000000000..2fd9c9c7a
--- /dev/null
+++ b/onebusaway-collections/src/main/java/org/onebusaway/collections/adapter/AdaptableCollection.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (C) 2011 Brian Ferris
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.collections.adapter;
+
+import java.util.AbstractCollection;
+import java.util.Collection;
+import java.util.Iterator;
+
+class AdaptableCollection extends AbstractCollection {
+
+ private final Collection _source;
+
+ private final IAdapter _adapater;
+
+ public AdaptableCollection(Collection source,
+ IAdapter adapater) {
+ _source = source;
+ _adapater = adapater;
+ }
+
+ @Override
+ public Iterator iterator() {
+ return AdapterLibrary.adaptIterator(_source.iterator(), _adapater);
+ }
+
+ @Override
+ public int size() {
+ return _source.size();
+ }
+}
diff --git a/onebusaway-collections/src/main/java/org/onebusaway/collections/adapter/AdaptableSet.java b/onebusaway-collections/src/main/java/org/onebusaway/collections/adapter/AdaptableSet.java
new file mode 100644
index 000000000..0c0344647
--- /dev/null
+++ b/onebusaway-collections/src/main/java/org/onebusaway/collections/adapter/AdaptableSet.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (C) 2011 Brian Ferris
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.collections.adapter;
+
+import java.util.AbstractSet;
+import java.util.Iterator;
+import java.util.Set;
+
+class AdaptableSet extends AbstractSet {
+
+ private final Set _source;
+
+ private final IAdapter _adapter;
+
+ public AdaptableSet(Set source, IAdapter adapter) {
+ _source = source;
+ _adapter = adapter;
+ }
+
+ @Override
+ public Iterator iterator() {
+ return AdapterLibrary.adaptIterator(_source.iterator(), _adapter);
+ }
+
+ @Override
+ public int size() {
+ return _source.size();
+ }
+
+}
diff --git a/onebusaway-collections/src/main/java/org/onebusaway/collections/adapter/AdaptableValueMapEntry.java b/onebusaway-collections/src/main/java/org/onebusaway/collections/adapter/AdaptableValueMapEntry.java
new file mode 100644
index 000000000..737f71efa
--- /dev/null
+++ b/onebusaway-collections/src/main/java/org/onebusaway/collections/adapter/AdaptableValueMapEntry.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright (C) 2011 Brian Ferris
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.collections.adapter;
+
+import java.util.Map;
+import java.util.Map.Entry;
+
+class AdaptableValueMapEntry implements
+ Map.Entry {
+
+ private final Entry _source;
+
+ private final IAdapter _adapter;
+
+ public AdaptableValueMapEntry(Map.Entry source,
+ IAdapter adapter) {
+ _source = source;
+ _adapter = adapter;
+ }
+
+ @Override
+ public KEY getKey() {
+ return _source.getKey();
+ }
+
+ @Override
+ public TO_VALUE getValue() {
+ return AdapterLibrary.apply(_adapter, _source.getValue());
+ }
+
+ @Override
+ public TO_VALUE setValue(TO_VALUE value) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/onebusaway-collections/src/main/java/org/onebusaway/collections/adapter/AdaptableValueSortedMap.java b/onebusaway-collections/src/main/java/org/onebusaway/collections/adapter/AdaptableValueSortedMap.java
new file mode 100644
index 000000000..b9634cd29
--- /dev/null
+++ b/onebusaway-collections/src/main/java/org/onebusaway/collections/adapter/AdaptableValueSortedMap.java
@@ -0,0 +1,144 @@
+/**
+ * Copyright (C) 2011 Brian Ferris
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.collections.adapter;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+
+class AdaptableValueSortedMap implements
+ SortedMap {
+
+ private final SortedMap _source;
+
+ private final IAdapter _adapter;
+
+ public AdaptableValueSortedMap(SortedMap source,
+ IAdapter adapter) {
+ _source = source;
+ _adapter = adapter;
+ }
+
+ @Override
+ public int size() {
+ return _source.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return _source.isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return _source.containsKey(key);
+ }
+
+ @Override
+ public TO_VALUE get(Object key) {
+ return adapt(_source.get(key));
+ }
+
+ @Override
+ public TO_VALUE remove(Object key) {
+ return adapt(_source.remove(key));
+ }
+
+ @Override
+ public void clear() {
+ _source.clear();
+ }
+
+ @Override
+ public Comparator super KEY> comparator() {
+ return _source.comparator();
+ }
+
+ @Override
+ public SortedMap subMap(KEY fromKey, KEY toKey) {
+ return new AdaptableValueSortedMap(
+ _source.subMap(fromKey, toKey), _adapter);
+ }
+
+ @Override
+ public SortedMap headMap(KEY toKey) {
+ return new AdaptableValueSortedMap(
+ _source.headMap(toKey), _adapter);
+ }
+
+ @Override
+ public SortedMap tailMap(KEY fromKey) {
+ return new AdaptableValueSortedMap(
+ _source.tailMap(fromKey), _adapter);
+ }
+
+ @Override
+ public KEY firstKey() {
+ return _source.firstKey();
+ }
+
+ @Override
+ public KEY lastKey() {
+ return _source.lastKey();
+ }
+
+ @Override
+ public Set keySet() {
+ return _source.keySet();
+ }
+
+ @Override
+ public Collection values() {
+ return AdapterLibrary.adaptCollection(_source.values(), _adapter);
+ }
+
+ @Override
+ public Set> entrySet() {
+ return AdapterLibrary.adaptSet(_source.entrySet(),
+ new MapEntryValueAdapter(_adapter));
+ }
+
+ /****
+ * Any value methods that include modification are unsupported
+ ****/
+
+ @Override
+ public TO_VALUE put(KEY key, TO_VALUE value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void putAll(Map extends KEY, ? extends TO_VALUE> m) {
+ throw new UnsupportedOperationException();
+ }
+
+ /****
+ * Private Methods
+ ****/
+
+ private TO_VALUE adapt(FROM_VALUE value) {
+ if (value == null)
+ return null;
+ return _adapter.adapt(value);
+ }
+}
diff --git a/onebusaway-collections/src/main/java/org/onebusaway/collections/adapter/AdapterLibrary.java b/onebusaway-collections/src/main/java/org/onebusaway/collections/adapter/AdapterLibrary.java
new file mode 100644
index 000000000..ee2e18e95
--- /dev/null
+++ b/onebusaway-collections/src/main/java/org/onebusaway/collections/adapter/AdapterLibrary.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright (C) 2011 Brian Ferris
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onebusaway.collections.adapter;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+
+public class AdapterLibrary {
+
+ public static final TO apply(IAdapter adapter, FROM value) {
+ if (value == null)
+ return null;
+ return adapter.adapt(value);
+ }
+
+ public static IAdapter getIdentityAdapter(Class type) {
+ return new IdentityAdapter();
+ }
+
+ public static Iterable adapt(Iterable source,
+ IAdapter adapter) {
+ return new IterableAdapter(source, adapter);
+ }
+
+ public static Iterator adaptIterator(Iterator source,
+ IAdapter adapter) {
+ return new IteratorAdapter(source, adapter);
+ }
+
+ public static