├── data └── debug │ ├── .gitignore │ ├── package.json │ ├── source.json │ └── script.js ├── src └── main │ └── scala │ ├── algorithm │ ├── Query.scala │ ├── ParetoSet.scala │ ├── Connection.scala │ ├── package.scala │ ├── Csa.scala │ ├── BackwardsCSA.scala │ ├── AdjustedCsa.scala │ └── McCsa.scala │ ├── gtfs │ ├── types.scala │ ├── readers.scala │ └── GTFSData.scala │ └── rest │ └── Endpoint.scala ├── .gitignore ├── README.md └── LICENSE /data/debug/.gitignore: -------------------------------------------------------------------------------- 1 | *.txt 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /src/main/scala/algorithm/Query.scala: -------------------------------------------------------------------------------- 1 | package algorithm 2 | 3 | /** 4 | * Created by hendrikniemann on 17.08.2016. 5 | */ 6 | case class Query(depStation: Int, arrStation: Int, depTime: Int) 7 | -------------------------------------------------------------------------------- /data/debug/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "debugdata", 3 | "version": "1.0.0", 4 | "description": "Script to generate debug gtfs data", 5 | "main": "script.js", 6 | "dependencies": { 7 | "ramda": "^0.22.1" 8 | }, 9 | "devDependencies": {}, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "author": "hendrikniemann", 14 | "license": "MIT" 15 | } 16 | -------------------------------------------------------------------------------- /src/main/scala/algorithm/ParetoSet.scala: -------------------------------------------------------------------------------- 1 | package algorithm 2 | 3 | class ParetoSet[T](dominator: (T, T) => Boolean, items: Set[T] = Set[T]()) extends Iterable[T] { 4 | def +(item: T): ParetoSet[T] = { 5 | if (items exists { dominator(_, item) }) this 6 | else new ParetoSet(dominator, items -- (items filter { dominator(item, _) }) + item) 7 | } 8 | 9 | def ++(iterable: Iterable[T]): ParetoSet[T] = iterable.foldLeft[ParetoSet[T]](this)(_ + _) 10 | 11 | override def iterator: Iterator[T] = items.iterator 12 | } 13 | -------------------------------------------------------------------------------- /src/main/scala/gtfs/types.scala: -------------------------------------------------------------------------------- 1 | package gtfs 2 | 3 | case class CalendarDate(serviceId: Int, date: Int, exceptionType: Int) 4 | case class Route(id: Int, shortName: String, longName: String) 5 | case class Stop(id: Int, name: String) 6 | case class StopTime(tripId: Int, arrivalTime: Long, departureTime: Long, stopId: Int, stopSequence: Int) 7 | case class Trip(routeId: Int, serviceId: Int, id: Int, tripHeadsign: String) 8 | trait Transfer 9 | case class MinimumTransferTime(stopId: Int, minutes: Int) extends Transfer 10 | case class Footpath(fromStopId: Int, toStopId: Int, minutes: Int) extends Transfer 11 | case class TripTransfer(fromTrip: Int, toTrip: Int, minutes: Int) extends Transfer 12 | object UnknownTransfer extends Transfer 13 | -------------------------------------------------------------------------------- /src/main/scala/algorithm/Connection.scala: -------------------------------------------------------------------------------- 1 | package algorithm 2 | 3 | import gtfs.Trip 4 | 5 | /** 6 | * Created by hendrikniemann on 17.08.2016. 7 | */ 8 | trait Connection { 9 | val depStation: Int 10 | val arrStation: Int 11 | val depTime: Long 12 | val arrTime: Long 13 | } 14 | 15 | case class TripConnection( 16 | depStation: Int, 17 | arrStation: Int, 18 | depTime: Long, 19 | arrTime: Long, 20 | trip: Int 21 | ) extends Connection 22 | 23 | case class FootConnection( 24 | depStation: Int, 25 | arrStation: Int, 26 | depTime: Long, 27 | arrTime: Long 28 | ) extends Connection 29 | -------------------------------------------------------------------------------- /src/main/scala/algorithm/package.scala: -------------------------------------------------------------------------------- 1 | import scala.annotation.tailrec 2 | 3 | /** 4 | * Created by hendrikniemann on 31.08.2016. 5 | */ 6 | package object algorithm { 7 | /** 8 | * Simple binary lower bound search looking for lowest value that predicate holds true 9 | * 10 | * @param predicate function that maps values to true or false 11 | * @param values by predicate sorted array of values 12 | * @return lowest index that the predicate holds true for 13 | */ 14 | def findLowerBound[T](predicate: T => Boolean, values: Array[T]): Int = { 15 | @tailrec 16 | def binarySearch(lower: Int, upper: Int): Int = { 17 | if (lower >= upper) 18 | lower 19 | else { 20 | val between = (upper - lower) / 2 + lower 21 | if (predicate(values(between))) binarySearch(lower, between) else binarySearch(between + 1, upper) 22 | } 23 | } 24 | binarySearch(0, values.length - 1) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /data/debug/source.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { "id": 1, "stops": [1, 2, 3, 4] }, 4 | { "id": 2, "stops": [1, 3, 5, 6] }, 5 | { "id": 3, "stops": [1, 6, 7, 4, 1] } 6 | ], 7 | "stops": [ 8 | { "id": 1, "changeTime": 1 }, 9 | { "id": 2, "changeTime": 1 }, 10 | { "id": 3, "changeTime": 1 }, 11 | { "id": 4, "changeTime": 1 }, 12 | { "id": 5, "changeTime": 1 }, 13 | { "id": 6, "changeTime": 1 }, 14 | { "id": 7, "changeTime": 1 } 15 | ], 16 | "distances": [ 17 | { "from": 1, "to": 2, "minutes": 4, "route": 1 }, 18 | { "from": 2, "to": 3, "minutes": 6, "route": 1 }, 19 | { "from": 3, "to": 4, "minutes": 7, "route": 1 }, 20 | { "from": 1, "to": 3, "minutes": 4, "route": 2 }, 21 | { "from": 3, "to": 5, "minutes": 4, "route": 2 }, 22 | { "from": 5, "to": 6, "minutes": 4, "route": 2 }, 23 | { "from": 1, "to": 6, "minutes": 9, "route": 3 }, 24 | { "from": 6, "to": 7, "minutes": 3, "route": 3 }, 25 | { "from": 7, "to": 4, "minutes": 12, "route": 3 }, 26 | { "from": 4, "to": 1, "minutes": 16, "route": 3 } 27 | ], 28 | "footpaths": [ 29 | { "from": 5, "to": 7, "minutes": 5 } 30 | ], 31 | "intervals": [ 32 | { "route": 1, "interval": 30, "departure": 1, "reverse": false }, 33 | { "route": 1, "interval": 30, "departure": 1, "reverse": true }, 34 | { "route": 2, "interval": 30, "departure": 8, "reverse": false }, 35 | { "route": 2, "interval": 30, "departure": 8, "reverse": true }, 36 | { "route": 3, "interval": 60, "departure": 5, "reverse": false } 37 | ], 38 | "maxTime": 1440 39 | } 40 | -------------------------------------------------------------------------------- /src/main/scala/algorithm/Csa.scala: -------------------------------------------------------------------------------- 1 | package algorithm 2 | 3 | case class BasicConnection(depStation: Int, arrStation: Int, depTime: Long, arrTime: Long) extends Connection 4 | 5 | object Csa { 6 | private def makeConnection[C <: Connection](shortest: Map[Int, C], start: Int, end: Int): Option[List[C]] = { 7 | if (end == start) Some(List()) else shortest.get(end) map { connection => 8 | connection :: makeConnection(shortest, start, connection.depStation).get 9 | } 10 | } 11 | 12 | def find[C <: Connection](connections: Array[C], query: Query): Option[List[C]] = { 13 | val infinity = BasicConnection(0, 0, 0, Int.MaxValue) 14 | 15 | var shortest: Map[Int, C] = Map[Int, C]() 16 | 17 | // we look up the earliest relevant connection with binary search 18 | var i = findLowerBound((c: C) => c.depTime >= query.depTime, connections) 19 | // since this calculates one to one queries we can break when the connection departs later then EAT at target stop 20 | while (i < connections.length && shortest.getOrElse(query.arrStation, infinity).arrTime > connections(i).depTime) { 21 | val conn = connections(i) 22 | if ( 23 | conn.depStation == query.depStation && query.depTime <= conn.depTime || 24 | shortest.getOrElse(conn.depStation, infinity).arrTime < conn.depTime 25 | ) { 26 | shortest.get(conn.arrStation) match { 27 | case Some(current) => 28 | if (current.arrTime > conn.arrTime) shortest += (conn.arrStation -> conn) 29 | case None => 30 | shortest += (conn.arrStation -> conn) 31 | } 32 | } 33 | i += 1 34 | } 35 | makeConnection(shortest, query.depStation, query.arrStation) map { _.reverse } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/scala,intellij,sbt 2 | 3 | ### Scala ### 4 | *.class 5 | *.log 6 | 7 | # sbt specific 8 | .cache 9 | .history 10 | .lib/ 11 | dist/* 12 | target/ 13 | lib_managed/ 14 | src_managed/ 15 | project/boot/ 16 | project/plugins/project/ 17 | 18 | # Scala-IDE specific 19 | .scala_dependencies 20 | .worksheet 21 | 22 | 23 | ### Intellij ### 24 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 25 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 26 | 27 | # User-specific stuff: 28 | .idea/workspace.xml 29 | .idea/tasks.xml 30 | .idea/dictionaries 31 | .idea/vcs.xml 32 | .idea/jsLibraryMappings.xml 33 | 34 | # Sensitive or high-churn files: 35 | .idea/dataSources.ids 36 | .idea/dataSources.xml 37 | .idea/dataSources.local.xml 38 | .idea/sqlDataSources.xml 39 | .idea/dynamic.xml 40 | .idea/uiDesigner.xml 41 | 42 | # Gradle: 43 | .idea/gradle.xml 44 | .idea/libraries 45 | 46 | # Mongo Explorer plugin: 47 | .idea/mongoSettings.xml 48 | 49 | ## File-based project format: 50 | *.iws 51 | 52 | ## Plugin-specific files: 53 | 54 | # IntelliJ 55 | /out/ 56 | .idea/* 57 | 58 | # mpeltonen/sbt-idea plugin 59 | .idea_modules/ 60 | 61 | # JIRA plugin 62 | atlassian-ide-plugin.xml 63 | 64 | # Crashlytics plugin (for Android Studio and IntelliJ) 65 | com_crashlytics_export_strings.xml 66 | crashlytics.properties 67 | crashlytics-build.properties 68 | fabric.properties 69 | 70 | ### Intellij Patch ### 71 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 72 | 73 | # *.iml 74 | # modules.xml 75 | # .idea/misc.xml 76 | # *.ipr 77 | 78 | 79 | ### SBT ### 80 | # Simple Build Tool 81 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control 82 | 83 | target/ 84 | lib_managed/ 85 | src_managed/ 86 | project/boot/ 87 | .history 88 | .cache 89 | -------------------------------------------------------------------------------- /src/main/scala/gtfs/readers.scala: -------------------------------------------------------------------------------- 1 | package gtfs 2 | 3 | import java.io.{File, FileNotFoundException} 4 | 5 | import kantan.codecs.ResourceIterator 6 | import kantan.codecs.Result.Success 7 | import kantan.csv.RowDecoder 8 | import kantan.csv.ops._ 9 | 10 | abstract class AbstractReader[T] { 11 | protected def findFile(path: String): File = { 12 | val file = new File(path) 13 | if (!file.exists()) throw new FileNotFoundException(path) else file 14 | } 15 | 16 | protected implicit val decoder: RowDecoder[T] 17 | 18 | def read(file: String): ResourceIterator[T] = findFile(file) asCsvReader[T](',', true) collect { 19 | case Success(a) => a 20 | } 21 | } 22 | 23 | object StopReader extends AbstractReader[Stop] { 24 | implicit val decoder: RowDecoder[Stop] = RowDecoder.decoder(0, 1)(Stop.apply) 25 | } 26 | 27 | object StopTimeReader extends AbstractReader[StopTime] { 28 | private def timeStringToInt(str: String) = str.split(':') map { _.toInt } match { 29 | case Array(h, m, s) => h * 60 + m 30 | case _ => throw new IllegalArgumentException("Provided time string must have format hh:mm:ss!") 31 | } 32 | 33 | implicit val decoder: RowDecoder[StopTime] = RowDecoder.decoder(0, 1, 2, 3, 4)( 34 | (id: Int, arr: String, dep: String, sid: Int, seq: Int) => 35 | StopTime(id, timeStringToInt(arr), timeStringToInt(dep), sid, seq) 36 | ) 37 | } 38 | 39 | object CalendarDateReader extends AbstractReader[CalendarDate] { 40 | implicit val decoder: RowDecoder[CalendarDate] = RowDecoder.decoder(0, 1, 2)(CalendarDate.apply) 41 | } 42 | 43 | object TripReader extends AbstractReader[Trip] { 44 | implicit val decoder: RowDecoder[Trip] = RowDecoder.decoder(0, 1, 2, 3)(Trip.apply) 45 | } 46 | 47 | object RouteReader extends AbstractReader[Route] { 48 | implicit val decoder: RowDecoder[Route] = RowDecoder.decoder(0, 1, 2)(Route.apply) 49 | } 50 | 51 | object TransferReader extends AbstractReader[Transfer] { 52 | implicit val decoder: RowDecoder[Transfer] = RowDecoder.decoder(0, 1, 2, 3, 4, 5)( 53 | (fromStop: Int, toStop: Int, transferType: Int, minTime: Int, fromTrip: String, toTrip: String) => { 54 | if (!fromTrip.isEmpty && !toTrip.isEmpty) TripTransfer(fromTrip.toInt, toTrip.toInt, minTime / 60) 55 | else if (fromTrip.isEmpty && toTrip.isEmpty && fromStop == toStop) MinimumTransferTime(fromStop, minTime / 60) 56 | else if (fromTrip.isEmpty && toTrip.isEmpty) Footpath(fromStop, toStop, minTime / 60) 57 | else UnknownTransfer 58 | } 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/main/scala/algorithm/BackwardsCSA.scala: -------------------------------------------------------------------------------- 1 | package algorithm 2 | 3 | class BackwardsCSA(connections: Array[TripConnection]) { 4 | private val byDeparture = connections 5 | private val byArrival = connections sortWith { _.arrTime < _.arrTime } 6 | 7 | private def makeConnection[C <: Connection](shortest: Map[Int, C], start: Int, end: Int): Option[List[C]] = { 8 | if (end == start) Some(List()) else shortest.get(end) map { connection => 9 | connection :: makeConnection(shortest, start, connection.arrStation).get 10 | } 11 | } 12 | 13 | def find(depStation: Int, arrStation: Int, depTime: Long): Option[List[Connection]] = { 14 | val infinity = BasicConnection(0, 0, 0, Int.MaxValue) 15 | 16 | var shortest: Map[Int, Connection] = Map[Int, Connection]() 17 | 18 | // we look up the earliest relevant connection with binary search 19 | var i = findLowerBound((c: Connection) => c.depTime >= depTime, byDeparture) 20 | // since this calculates one to one queries we can break when the connection departs later then EAT at target stop 21 | while (i < byDeparture.length && shortest.getOrElse(arrStation, infinity).arrTime > byDeparture(i).depTime) { 22 | val conn = byDeparture(i) 23 | if ( 24 | conn.depStation == depStation && depTime <= conn.depTime || 25 | shortest.getOrElse(conn.depStation, infinity).arrTime < conn.depTime 26 | ) { 27 | shortest.get(conn.arrStation) match { 28 | case Some(current) => 29 | if (current.arrTime > conn.arrTime) shortest += (conn.arrStation -> conn) 30 | case None => 31 | shortest += (conn.arrStation -> conn) 32 | } 33 | } 34 | i += 1 35 | } 36 | 37 | // If we could not find an EAT for the target station we can exit here 38 | if (!shortest.isDefinedAt(arrStation)) return None 39 | 40 | val eat = shortest(arrStation).arrTime 41 | 42 | // now we scan backwards through the array to find the latest departing connection arriving at eat 43 | i = findLowerBound((c: Connection) => c.arrTime > eat, byArrival) - 1 44 | 45 | shortest = Map() 46 | 47 | while (i > 0 && byArrival(i).arrTime >= depTime) { 48 | val conn = byArrival(i) 49 | if ( 50 | conn.arrStation == arrStation && eat >= conn.arrTime || 51 | shortest.getOrElse(conn.arrStation, infinity).depTime > conn.arrTime 52 | ) { 53 | shortest.get(conn.depStation) match { 54 | case Some(current) => 55 | if (current.depTime < conn.depTime) shortest += (conn.depStation -> conn) 56 | case None => 57 | shortest += (conn.depStation -> conn) 58 | } 59 | } 60 | i -= 1 61 | } 62 | 63 | makeConnection(shortest, arrStation, depStation) 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Practical Connection Scan Algorithm Implementation 2 | 3 | > 💡 **This repository is no longer actively maintained by DB Systel GmbH. It was created as part of a bachelor thesis. If you are interested in continuing the effort, please feel free to contact us.** 4 | 5 | This is a high level implementation of the Connection Scan Algorithm. 6 | Modifications are made to fit special requirements for timetable information systems at [Deutsche Bahn](http://deutschebahn.com). 7 | Goal of this implementation is to find out about difficulties when applying the algorithm to real world problems and requirements and to provide a publicly available working example. 8 | 9 | ## About the algorithm 10 | 11 | The algorithm was introduced in 2013 by [Julian Dibbelt, Thomas Pajor, Ben Strasser, and Dorothea Wagner](http://i11www.iti.uni-karlsruhe.de/extra/publications/dpsw-isftr-13.pdf). 12 | 13 | ## About the project 14 | 15 | My bachelor thesis studies the relevance and applicability of the algorithm for [Deutsche Bahn](http://deutschebahn.com). 16 | This repository contains code that was created for the thesis. 17 | 18 | ### Features of the multi criteria implementation 19 | 20 | Pareto optimal result set with criteria _depature time_, _arrival time_ and _number of changes_. Apart from that the implementation supports footpaths, minimum change times and trip specific change times which are all read in from the _transfers.txt_ 21 | 22 | ## Getting started 23 | 24 | To run this project you will need: 25 | 26 | | Programm | Version | 27 | | :---------------------------------- | :------ | 28 | | [Scala](http://www.scala-lang.org/) | > 2.11 | 29 | | [JDK](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) | > 8 | 30 | | [SBT](http://www.scala-sbt.org/) | > 0.13 | 31 | 32 | Clone or download the files from Github. 33 | 34 | Get the [inofficial Fernverkehr GTFS Feed](https://github.com/fredlockheed/db-fv-gtfs/). For now this is hardcoded to the fields provided in this feed. If you want to use your own GTFS feed take a look into [gtfs.readers](https://github.com/dbsystel/practical-csa/blob/master/src/main/scala/gtfs/readers.scala) and make sure the parameters are matched correctly to the case class fields of the type. 35 | 36 | You can also use the debug data from `data/debug`. You will need _node.js_ to execute the script and generate the GTFS data: 37 | ``` 38 | cd data/debug/ 39 | npm install 40 | node script.js source.js 41 | ``` 42 | 43 | Use you command line to execute the following commands in the project's folder: 44 | 45 | ``` 46 | sbt compile 47 | sbt run 48 | ``` 49 | 50 | Enter the location of the extracted GTFS feed on you file system. This will load the data and start a HTTP REST Endpoint on `http://localhost:8080`. 51 | 52 | Query connections with: 53 | 54 | ``` 55 | # basic csa 56 | get http://localhost:8080/query/{from}/{to} 57 | 58 | # multi criteria 59 | get http://localhost:8080/mc/{from}/{to} 60 | ``` 61 | 62 | With `from` and `to` being the integer stop IDs of your GTFS feed. 63 | 64 | ## License 65 | 66 | Licensed under Apache 2.0 67 | Copyright 2016-2016 DB Systel GmbH 68 | -------------------------------------------------------------------------------- /src/main/scala/algorithm/AdjustedCsa.scala: -------------------------------------------------------------------------------- 1 | package algorithm 2 | 3 | import gtfs.Footpath 4 | 5 | import scala.annotation.tailrec 6 | 7 | /** 8 | * A CSA implementation that respects transfer times for stations, footpaths, and train specific change times 9 | * 10 | * @param connections sorted array of connections by departure time 11 | * @param transferTimes map of transfer times by stops 12 | * @param footpaths map of footpaths from station 13 | */ 14 | class AdjustedCsa( 15 | val connections: Array[TripConnection], 16 | val transferTimes: Map[Int, Int], 17 | val footpaths: Map[Int, Iterable[Footpath]] 18 | ) { 19 | 20 | /** 21 | * Helper function to find the connection from an array of shortest connections 22 | * The function travels back to start 23 | * 24 | * @param shortest a map of shortest connections to stops 25 | * @param start the start stop of the connection 26 | * @param end the stop traveled to 27 | * @return Some list of connection to take from last to first or None if connection not found 28 | */ 29 | private def makeConnection[C <: Connection](shortest: Map[Int, C], start: Int, end: Int): Option[List[C]] = { 30 | if (end == start) Some(List()) 31 | else shortest.get(end) map { connection => 32 | connection :: makeConnection(shortest, start, connection.depStation).get 33 | } 34 | } 35 | 36 | /** 37 | * Helper function to check if two connections connect 38 | * This function assumes the connections meet at a.arrStation / b.depStation without checking it! 39 | */ 40 | private def connects(a: Connection, b: TripConnection): Boolean = a match { 41 | case x: TripConnection => x.trip == b.trip || x.arrTime <= b.depTime - transferTimes(b.depStation) 42 | case x: FootConnection => x.arrTime <= b.depTime 43 | } 44 | 45 | def find(query: Query): Option[List[Connection]] = { 46 | val infinity = TripConnection(0, 0, 0, Int.MaxValue, 0) 47 | 48 | var shortest: Map[Int, Connection] = Map() 49 | 50 | def insert(e: TripConnection) = { 51 | val paths = footpaths(e.arrStation) 52 | 53 | paths foreach { case Footpath(fromStopId, toStopId, minutes) => 54 | if (e.arrTime + minutes < shortest.getOrElse(toStopId, infinity).arrTime) { 55 | shortest += (toStopId -> FootConnection( 56 | fromStopId, toStopId, e.arrTime, e.arrTime + minutes 57 | )) 58 | } 59 | } 60 | 61 | shortest += (e.arrStation -> e) 62 | } 63 | 64 | // we look up the earliest relevant connection with binary search 65 | var i = findLowerBound((c: Connection) => c.depTime >= query.depTime, connections) 66 | // since this calculates one to one queries we can break when the connection departs later then EAT at target stop 67 | while (i < connections.length && shortest.getOrElse(query.arrStation, infinity).arrTime > connections(i).depTime) { 68 | val conn = connections(i) 69 | if ( 70 | conn.depStation == query.depStation && query.depTime <= conn.depTime || 71 | connects(shortest.getOrElse(conn.depStation, infinity), conn) 72 | ) { 73 | shortest.get(conn.arrStation) match { 74 | case Some(current) => if (current.arrTime > conn.arrTime) insert(conn) 75 | case None => insert(conn) 76 | } 77 | } 78 | i += 1 79 | } 80 | makeConnection(shortest, query.depStation, query.arrStation) map { 81 | _.reverse 82 | } 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /src/main/scala/gtfs/GTFSData.scala: -------------------------------------------------------------------------------- 1 | package gtfs 2 | 3 | import java.time.LocalDateTime 4 | import java.time.temporal.ChronoUnit 5 | 6 | import algorithm._ 7 | 8 | class GTFSData( 9 | val stops: Map[Int, Stop], 10 | val routes: Map[Int, Route], 11 | val trips: Map[Int, Trip], 12 | val stopTimes: Map[Int, Iterable[StopTime]], 13 | val connections: Array[TripConnection], 14 | val transferTimes: Map[Int, Int], 15 | val footpaths: Map[Int, Iterable[Footpath]], 16 | val tripTransfers: Map[Int, Iterable[TripTransfer]] 17 | ) { 18 | def findStopByName(name: String): Option[Stop] = stops find { _._2.name == name } map { _._2 } 19 | } 20 | 21 | object GTFSData { 22 | val epoch = LocalDateTime.of(2000, 1, 1, 0, 0) 23 | 24 | private def sinceEpoch(date: LocalDateTime, time: Long): Long = { 25 | epoch.until(date, ChronoUnit.MINUTES) + time 26 | } 27 | 28 | private def makeConnectionsFromStops(trip: Trip, date: LocalDateTime)(timedStops: List[StopTime]): List[TripConnection] = timedStops match { 29 | case from :: to :: rest => 30 | TripConnection( 31 | from.stopId, 32 | to.stopId, 33 | sinceEpoch(date, from.departureTime), 34 | sinceEpoch(date, to.arrivalTime), trip.id 35 | ) :: makeConnectionsFromStops(trip, date)(to :: rest) 36 | case _ => Nil 37 | } 38 | 39 | def fromDirPath(path: String): GTFSData = { 40 | val stopData = StopReader.read(path + "stops.txt") 41 | val calendarData = CalendarDateReader.read(path + "calendar_dates.txt") 42 | val stopTimeData = StopTimeReader.read(path + "stop_times.txt") 43 | val routeData = RouteReader.read(path + "routes.txt") 44 | val tripData = TripReader.read(path + "trips.txt") 45 | val transferData = TransferReader.read(path + "transfers.txt") 46 | 47 | // We would like to have the data in some different data structures for easier use (lookups) 48 | // Maps with id as Key 49 | val stops = stopData.foldLeft(Map[Int, Stop]())((p: Map[Int, Stop], n: Stop) => p + (n.id -> n)) 50 | val trips = tripData.foldLeft(Map[Int, Trip]())((p: Map[Int, Trip], n: Trip) => p + (n.id -> n)) 51 | val routes = routeData.foldLeft(Map[Int, Route]())((p: Map[Int, Route], n: Route) => p + (n.id -> n)) 52 | var transferTimes = Map[Int, Int]() withDefaultValue 0 53 | 54 | 55 | var allFootpaths = Set[Footpath]() 56 | var allTripTransfers = Set[TripTransfer]() 57 | transferData foreach { 58 | case MinimumTransferTime(stopId, minutes) => transferTimes += (stopId -> minutes) 59 | case f: Footpath => allFootpaths += f 60 | case t: TripTransfer => allTripTransfers += t 61 | } 62 | val footpaths = allFootpaths groupBy { _.fromStopId } withDefaultValue Nil 63 | 64 | 65 | // In Map[List] with foreign key 66 | val tripsByRoute = trips.values.toList groupBy { _.serviceId } 67 | val stopTimes = stopTimeData.toList groupBy { _.tripId } 68 | val tripTransfers = allTripTransfers groupBy { _.fromTrip } 69 | 70 | val connections: Array[TripConnection] = calendarData.toArray flatMap { 71 | (trafficDay: CalendarDate) => { 72 | // If a service operates on a date all trips of the service operate on this date 73 | // we then have to go through all of these trips and add their connections 74 | val associatedTrips: List[Trip] = tripsByRoute.getOrElse(trafficDay.serviceId, Nil) 75 | 76 | // java.time.LocalDateTime of traffic day 77 | val timestamp = LocalDateTime.of(trafficDay.date / 10000, (trafficDay.date % 10000) / 100, trafficDay.date % 100, 0, 0) 78 | 79 | associatedTrips flatMap { 80 | trip => stopTimes.get(trip.id) map makeConnectionsFromStops(trip, timestamp) getOrElse Nil 81 | } 82 | } 83 | } 84 | 85 | new GTFSData(stops, routes, trips, stopTimes, connections.sortBy(c => c.depTime), transferTimes, footpaths, tripTransfers) 86 | } 87 | 88 | def empty = new GTFSData(Map(), Map(), Map(), Map(), Array(), Map(), Map(), Map()) 89 | } 90 | -------------------------------------------------------------------------------- /src/main/scala/algorithm/McCsa.scala: -------------------------------------------------------------------------------- 1 | package algorithm 2 | 3 | import gtfs.{Footpath, TripTransfer} 4 | 5 | class Domination[T](criteria: ((T, T) => Boolean)*) extends ((T, T) => Boolean) { 6 | def apply(x: T, y: T): Boolean = (criteria exists { _(x, y) }) && (criteria forall { !_(y, x) }) 7 | } 8 | 9 | class McCsa( 10 | connections: Array[TripConnection], 11 | transferTimes: Map[Int, Int], 12 | footpaths: Map[Int, Iterable[Footpath]], 13 | tripTransfers: Map[Int, Iterable[TripTransfer]] 14 | ) { 15 | def find(startStation: Int, targetStation: Int, startTime: Long): ParetoSet[List[Connection]] = { 16 | def changes(l: List[Connection]) = l.collect({ 17 | case x: TripConnection => x 18 | }).foldLeft(Set[Int]())(_ + _.trip).size - 1 19 | 20 | def earliestConnection(c: List[Connection]): Long = c.head match { 21 | case FootConnection(_, _, _, arrTime) => arrTime 22 | case TripConnection(_, arrStation, _, arrTime, _) => transferTimes(arrStation) + arrTime 23 | } 24 | 25 | def minTrip(tid: Int): Int = tripTransfers.get(tid). 26 | map(_.map(_.minutes).min). 27 | getOrElse(Int.MaxValue) 28 | 29 | def nextTrip(c: List[Connection]): Long = c.head match { 30 | case FootConnection(_, _, _, arrTime) => arrTime 31 | case TripConnection(_, arrStation, _, arrTime, tripid) => 32 | arrTime + (transferTimes(arrStation) min minTrip(tripid)) 33 | } 34 | 35 | def connects(a: Connection, b: TripConnection): Boolean = a match { 36 | case x: TripConnection => 37 | val tripTransfer = tripTransfers.get(x.trip) flatMap { _ find { _.toTrip == b.trip } } 38 | (tripTransfer.isDefined && x.arrTime <= b.depTime - tripTransfer.get.minutes) || 39 | x.trip == b.trip || x.arrTime <= b.depTime - transferTimes(b.depStation) 40 | case x: FootConnection => x.arrTime <= b.depTime 41 | } 42 | 43 | val dom = new Domination( 44 | (a: List[Connection], b: List[Connection]) => a.last.depTime > b.last.depTime, 45 | (a: List[Connection], b: List[Connection]) => earliestConnection(a) < earliestConnection(b), 46 | (a: List[Connection], b: List[Connection]) => changes(a) < changes(b), 47 | (a: List[Connection], b: List[Connection]) => !(earliestConnection(b) < nextTrip(a)) 48 | ) 49 | 50 | val strictDom = new Domination( 51 | (a: List[Connection], b: List[Connection]) => a.last.depTime > b.last.depTime, 52 | (a: List[Connection], b: List[Connection]) => a.head.arrTime < b.head.arrTime, 53 | (a: List[Connection], b: List[Connection]) => changes(a) < changes(b) 54 | ) 55 | 56 | val emptySet = new ParetoSet[List[Connection]](dom) 57 | val strictSet = new ParetoSet[List[Connection]](strictDom) 58 | var shortest: Map[Int, ParetoSet[List[Connection]]] = Map(targetStation -> strictSet) withDefaultValue emptySet 59 | 60 | def insert(c: List[Connection]): Unit = c match { 61 | case (x: TripConnection) :: xs => 62 | val newParetoSet = shortest(x.arrStation) + c 63 | if (shortest(x.arrStation) != newParetoSet) { 64 | for { 65 | l <- footpaths.get(x.arrStation) 66 | p <- l 67 | betterFootpath = FootConnection(p.fromStopId, p.toStopId, x.arrTime, x.arrTime + p.minutes) :: c 68 | } insert(betterFootpath) 69 | } 70 | shortest += x.arrStation -> newParetoSet 71 | case x :: xs => shortest += x.arrStation -> (shortest(x.arrStation) + c) 72 | } 73 | 74 | var i = findLowerBound((c: TripConnection) => c.depTime >= startTime, connections) 75 | var breakTime = Long.MaxValue 76 | while (i < connections.length && connections(i).depTime < breakTime) { 77 | val conn = connections(i) 78 | if (conn.depStation == startStation && startTime <= conn.depTime) 79 | insert(List(conn)) 80 | else { 81 | shortest(conn.depStation) filter { l => connects(l.head, conn) } map { conn :: _ } foreach insert 82 | 83 | footpaths(startStation) find { p => 84 | p.toStopId == conn.depStation && startTime + p.minutes < conn.depTime 85 | } foreach { p => 86 | insert(List(conn, FootConnection(p.fromStopId, p.toStopId, conn.depTime - p.minutes, conn.depTime))) 87 | } 88 | } 89 | i += 1 90 | breakTime = shortest(targetStation).map(_.head.arrTime + 300).foldLeft(Long.MaxValue)(_ min _) 91 | } 92 | 93 | shortest(targetStation) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /data/debug/script.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const R = require('ramda'); 4 | 5 | // logError :: Error => Undefined 6 | const logError = err => { if(err) console.error(err); }; 7 | 8 | if (!process.argv[2] || !fs.existsSync(process.argv[2])) { 9 | throw 'No path of source JSON or source doesn\'t exist'; 10 | } 11 | 12 | const BASE_PATH = path.dirname(process.argv[2]); 13 | const data = JSON.parse(fs.readFileSync(process.argv[2]).toString()); 14 | 15 | // getNextId :: String -> Number 16 | const getNextId = (function() { 17 | const ids = {}; 18 | return name => { 19 | if (typeof ids[name] === 'undefined') { 20 | ids[name] = 0; 21 | } 22 | return ++ids[name]; 23 | } 24 | })(); 25 | 26 | // ensureDouble :: Number -> String 27 | const ensureDouble = R.when(R.gt(10), num => `0${num}`); 28 | 29 | // toTime :: Number -> String 30 | const toTime = time => `${ensureDouble(Math.floor(time / 60))}:${ensureDouble(time % 60)}:00`; 31 | 32 | // getTime :: { route: Number, from: Number, to: Number } -> Number 33 | const getTime = ({ route, from, to }) => 34 | R.find(t => 35 | t.route === route && (t.from === from && t.to == to || t.from === to && t.to == from) 36 | , data.distances); 37 | 38 | // routeToCsv :: Route -> String 39 | const routeToCsv = ({ id }) => `${id},R${id},Route ${id},X\n`; 40 | 41 | // tripToCsv :: Trip -> String 42 | const tripToCsv = ({ id, route }) => `${route},1,${id},T${id}\n`; 43 | 44 | // stopToCsv :: Stop -> String 45 | const stopToCsv = ({ id }) => `${id},Stop ${id},${id}.1,${id}.2,Europe/Berlin\n`; 46 | 47 | // stopTimeToCsv :: StopTime -> String 48 | const stopTimeToCsv = ({ trip, arrivalTime, departureTime, stop, seq }) => 49 | `${trip},${toTime(arrivalTime)},${toTime(departureTime)},${stop},${seq}\n`; 50 | 51 | // stopTransferToCsv :: Stop -> String 52 | const stopTransferToCsv = ({ id, changeTime }) => `${id},${id},2,${changeTime * 60},,\n`; 53 | 54 | // footpathToCsv :: Footpath -> String 55 | const footpathToCsv = ({ from, to, minutes }) => `${from},${to},2,${minutes * 60},,\n`; 56 | 57 | // Trip :: Interval -> Number -> Trip 58 | const Trip = ({ route, departure, reverse }, time) => { 59 | const transition = ([from, to]) => ({ route, from, to }); 60 | const routeObj = R.find(R.propEq('id', route), data.routes); 61 | const stopList = reverse ? R.reverse(routeObj.stops) : routeObj.stops; 62 | const transitions = R.map(transition, R.aperture(2, stopList)); 63 | const id = getNextId('TRIP'); 64 | 65 | let currTime = time + departure; 66 | let seq = 0; 67 | let stops = [{ 68 | trip: id, 69 | stop: transitions[0].from, 70 | arrivalTime: 0, 71 | departureTime: currTime, 72 | seq: seq++, 73 | }]; 74 | R.forEach(t => { 75 | currTime += getTime(t).minutes; 76 | stops.push({ 77 | trip: id, 78 | stop: t.to, 79 | arrivalTime: currTime, 80 | departureTime: ++currTime, 81 | seq: seq++, 82 | }); 83 | }, transitions); 84 | return { id, route, stops }; 85 | }; 86 | 87 | // trips :: [Trip] 88 | const trips = R.chain(route => { 89 | const intervals = R.filter(R.propEq('route', route.id), data.intervals); 90 | 91 | return R.chain(interval => { 92 | const t = []; 93 | for (let time = 0; time < data.maxTime; time += interval.interval) { 94 | t.push(Trip(interval, time)); 95 | } 96 | return t; 97 | }, intervals); 98 | }, data.routes); 99 | 100 | const calendarDatesCsv = 'service_id,date,exception_type\n1,20000101,1\n'; 101 | fs.writeFile(BASE_PATH + '/calendar_dates.txt', calendarDatesCsv, logError); 102 | 103 | const agencyCsv = 'agency_id,agency_name,agency_url,agency_timezone,agency_lang\nX,Agency X,www.example.com,Europe/Berlin,de1,20000101,1\n'; 104 | fs.writeFile(BASE_PATH + '/agency.txt', agencyCsv, logError); 105 | 106 | const routesCsv = 'route_id,route_short_name,route_long_name,route_type,agency_id\n' + 107 | data.routes.map(routeToCsv).join(''); 108 | fs.writeFile(BASE_PATH + '/routes.txt', routesCsv, logError); 109 | 110 | const stopsCsv = 'stop_id,stop_name,stop_lat,stop_lon,stop_timezone\n' + 111 | data.stops.map(stopToCsv).join(''); 112 | fs.writeFile(BASE_PATH + '/stops.txt', stopsCsv, logError); 113 | 114 | const tripsCsv = 'route_id,service_id,trip_id,trip_headsign\n' + 115 | trips.map(tripToCsv).join(''); 116 | fs.writeFile(BASE_PATH + '/trips.txt', tripsCsv, logError); 117 | 118 | const stopTimesCsv = 'trip_id,arrival_time,departure_time,stop_id,stop_sequence\n' + 119 | R.chain(R.prop('stops'), trips).map(stopTimeToCsv).join(''); 120 | fs.writeFile(BASE_PATH + '/stop_times.txt', stopTimesCsv, logError); 121 | 122 | const transfersCsv = 'from_stop_id,to_stop_id,transfer_type,min_transfer_time\n' + 123 | data.stops.map(stopTransferToCsv).join('') + 124 | data.footpaths.map(footpathToCsv).join(''); 125 | fs.writeFile(BASE_PATH + '/transfers.txt', transfersCsv, logError); 126 | -------------------------------------------------------------------------------- /src/main/scala/rest/Endpoint.scala: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import java.time.LocalDateTime 4 | import java.time.temporal.ChronoUnit 5 | 6 | import algorithm._ 7 | import gtfs._ 8 | import org.analogweb.core.Servers 9 | import org.analogweb.scala.Analogweb 10 | 11 | object Endpoint extends Analogweb { 12 | var data = GTFSData.empty 13 | 14 | /** 15 | * Concats following connections that belong to the same trip by omitting the stops in between: 16 | * A -> B :: B -> C => A -> C 17 | * This creates a pretty connection for the user 18 | * @param l A connection that should be taken by the user 19 | * @return List of real (not elementary) connections 20 | */ 21 | private def denseConnection(l: List[Connection]): List[Connection] = l match { 22 | case Nil => Nil 23 | case (x: TripConnection) :: xs => denseConnection(xs) match { 24 | case (y: TripConnection) :: ys if x.trip == y.trip => 25 | TripConnection(x.depStation, y.arrStation, x.depTime, y.arrTime, x.trip) :: ys 26 | case ys => x :: ys 27 | } 28 | case x :: xs => x :: denseConnection(xs) 29 | } 30 | 31 | private def makeDescriptionFromConnection: Connection => String = { 32 | case FootConnection(_, _, depTime, arrTime) => s"Footpath ${arrTime - depTime} minutes" 33 | case TripConnection(_, _, _, _, trip) => 34 | val Trip(routeId, _, _, tripHeadsign) = data.trips(trip) 35 | s"$tripHeadsign to ${data.routes(routeId).shortName}" 36 | case _ => "Unknown Trip" 37 | } 38 | 39 | /** 40 | * Transforms trip connections to a JSON printable connection that can be delived by the API 41 | * @param connection a TripConnection 42 | * @return a NiceConnection derived from the input parameter 43 | */ 44 | private def makeNiceConnection(connection: Connection): NiceConnection = NiceConnection( 45 | data.stops(connection.depStation), 46 | data.stops(connection.arrStation), 47 | GTFSData.epoch.plusMinutes(connection.depTime).toString, 48 | GTFSData.epoch.plusMinutes(connection.arrTime).toString, 49 | makeDescriptionFromConnection(connection) 50 | ) 51 | 52 | /** 53 | * Calculates the minutes since epoch 54 | * @return Time since epoch in minutes 55 | */ 56 | private def now = GTFSData.epoch.until(LocalDateTime.now(), ChronoUnit.MINUTES).toInt 57 | 58 | def main(args: Array[String]): Unit = { 59 | val dir = if (args.length > 0) args(0) else scala.io.StdIn.readLine("GTFS location: ") 60 | println("Initializing GTFS feed, this might take a while...") 61 | data = GTFSData.fromDirPath(dir) 62 | 63 | println(s"Found ${data.stops.size} stops and ${data.connections.length} connections!") 64 | Servers.create(8080).run() 65 | } 66 | 67 | /** 68 | * Gets the time from optional query parameter or returns current time if parameter is None 69 | * @param par optional Query parameter 70 | * @return Time in minutes since epoch from now or given time string 71 | */ 72 | def getTime(par: Option[String]): Int = { 73 | par map { s => GTFSData.epoch.until(LocalDateTime.parse(s), ChronoUnit.MINUTES).toInt } getOrElse now 74 | } 75 | 76 | get("/query/{from}/{to}") { implicit r => 77 | val time = getTime(r.queryOption("time")) 78 | 79 | val res = List(param("from"), param("to")) map { s => data.stops.get(s.toInt) } match { 80 | case Some(start) :: Some(destination) :: Nil => 81 | Csa.find(data.connections, Query(start.id, destination.id, time)) 82 | case _ => None 83 | } 84 | 85 | res map { 86 | denseConnection(_) map makeNiceConnection 87 | } map asJson getOrElse BadRequest(asText("Could not find a connecting journey!")) 88 | } 89 | 90 | get("/backwards/{from}/{to}") { implicit r => 91 | val time = getTime(r.queryOption("time")) 92 | 93 | val res = List(param("from"), param("to")) map { s => data.stops.get(s.toInt) } match { 94 | case Some(start) :: Some(destination) :: Nil => 95 | new BackwardsCSA(data.connections).find(start.id, destination.id, time) 96 | case _ => None 97 | } 98 | 99 | res map { 100 | denseConnection(_) map makeNiceConnection 101 | } map asJson getOrElse BadRequest(asText("Could not find a connecting journey!")) 102 | } 103 | 104 | get("/stop/{id}") { implicit r => 105 | data.stops.get(param("id").toInt) map asJson getOrElse BadRequest(asText("Could not find a stop with that ID.")) 106 | } 107 | 108 | get("/adjusted/{from}/{to}") { implicit r => 109 | val time = getTime(r.queryOption("time")) 110 | 111 | val res = List(param("from"), param("to")) map { s => data.stops.get(s.toInt) } match { 112 | case Some(start) :: Some(destination) :: Nil => 113 | new AdjustedCsa(data.connections, data.transferTimes, data.footpaths).find(Query(start.id, destination.id, time)) 114 | case _ => None 115 | } 116 | 117 | res map { 118 | denseConnection(_) map makeNiceConnection 119 | } map asJson getOrElse BadRequest(asText("Could not find a connecting journey!")) 120 | } 121 | 122 | get("/mc/{from}/{to}") { implicit r => 123 | val time = getTime(r.queryOption("time")) 124 | 125 | val res = List(param("from"), param("to")) map { s => data.stops.get(s.toInt) } match { 126 | case Some(start) :: Some(destination) :: Nil => 127 | val mcCsa = new McCsa(data.connections, data.transferTimes, data.footpaths, data.tripTransfers) 128 | Some(mcCsa.find(start.id, destination.id, time)) 129 | case _ => None 130 | } 131 | 132 | res map { 133 | _ map { _.reverse } map { denseConnection(_) map makeNiceConnection } 134 | } map { 135 | _.toList.sortBy(_.head.departureTime) 136 | } map asJson getOrElse BadRequest(asText("Could not find a connecting journey!")) 137 | } 138 | } 139 | 140 | case class NiceConnection(from: Stop, to: Stop, departureTime: String, arrivalTime: String, name: String) 141 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | --------------------------------------------------------------------------------