Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

Pico Transit is a friendly little companion for getting around on public transit. Real schedules, real-time arrivals, live connections at any stop, and a live map that shows exactly where your ride actually is — no ads, no clutter, no infinite scroll. Just "where's my bus," answered nicely. 🚏✨

Right now Pico Transit knows its way around **MBTA** and **RIPTA**, with more agencies hopefully hopping aboard down the road. It's built on the [Light SDK](../) for the Light Phone III, so it stays just as calm and un-distracting as the rest of your Light experience.
Right now Pico Transit knows its way around **MBTA**, **RIPTA**, and **RTD Denver**, with more agencies hopefully hopping aboard down the road. It's built on the [Light SDK](../) for the Light Phone III, so it stays just as calm and un-distracting as the rest of your Light experience.

Pico Transit can be used alongside the light phone's directions tool for more context on your commutes, or standalone, covering buses, commuter rail.subway systems, and transit stations.

## 🗺️ What can it do?

- 🏠 **Pick your agency** — MBTA or RIPTA — and Pico Transit downloads their schedule right onto your phone.
- 🏠 **Pick your agency** — MBTA, RIPTA, or RTD Denver — and Pico Transit downloads their schedule right onto your phone.

![alt text](docs/screenshots/Screenshot_20260801_202930.png)

Expand Down
4 changes: 2 additions & 2 deletions tool/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

Pico Transit is a friendly little companion for getting around on public transit. Real schedules, real-time arrivals, live connections at any stop, and a live map that shows exactly where your ride actually is — no ads, no clutter, no infinite scroll. Just "when's my bus," answered nicely. 🚏✨

Right now Pico Transit knows its way around **MBTA** and **RIPTA**, with more agencies hopefully hopping aboard down the road. It's built on the [Light SDK](../) for the Light Phone III, so it stays just as calm and un-distracting as the rest of your Light experience.
Right now Pico Transit knows its way around **MBTA**, **RIPTA**, and **RTD Denver**, with more agencies hopefully hopping aboard down the road. It's built on the [Light SDK](../) for the Light Phone III, so it stays just as calm and un-distracting as the rest of your Light experience.

## 🗺️ What can it do?

- 🏠 **Pick your agency** — MBTA or RIPTA — and Pico Transit downloads their schedule right onto your phone.
- 🏠 **Pick your agency** — MBTA, RIPTA, or RTD Denver — and Pico Transit downloads their schedule right onto your phone.
- ⚙️ **Settings** — a default agency to skip the picker, light/dark map tiles, and on/off toggles (tap-and-hold a stop to jump to its arrivals — the same gesture also jumps from a Station map's own name to the main map centered on it; double-tap a station to zoom into its platforms; track tapped-open stops' own vehicles on the map; the home screen's trip progress bar; the home screen's daily message; and "See Everything," a map mode covered below).
- 📅 **Explore Schedules** — browse by Subway 🚇, Commuter Rail 🚆, or Bus 🚌, pick a route, a direction, and a stop, and see every departure today.
- 🔗 **Connections** — tap any stop along a trip to see what else comes through there next, across every platform of a station, not just the one your trip happened to use. Great for planning a transfer on the fly.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ enum class GtfsAgency(
"https://cdn.mbta.com/realtime/VehiclePositions.pb",
components = listOf(MbtaV3VehicleSource),
),
RTD(
"rtd",
"RTD Denver",
"https://www.rtd-denver.com/files/gtfs/google_transit.zip",
"https://open-data.rtd-denver.com/files/gtfs-rt/rtd/TripUpdate.pb",
"https://open-data.rtd-denver.com/files/gtfs-rt/rtd/VehiclePosition.pb",
),
// REMOVABLE: these two URLs only work because of the :netconfig cleartext exception (see
// class doc above). Set both back to null to restore HTTPS-only enforcement for RIPTA.
RIPTA(
Expand Down
23 changes: 13 additions & 10 deletions tool/src/main/kotlin/com/thelightphone/transit/gtfs/GtfsIngestor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.ktor.client.request.head
import io.ktor.http.HttpHeaders
import java.io.BufferedReader
import java.io.File
import java.net.URI
import java.util.zip.ZipInputStream

enum class GtfsIngestStatus {
Expand Down Expand Up @@ -100,11 +101,7 @@ class GtfsIngestor(private val filesDir: File) {
}
status in 300..399 -> {
val location = response.headers[HttpHeaders.Location] ?: return null
currentUrl = if (location.startsWith("http://")) {
"https://" + location.removePrefix("http://")
} else {
location
}
currentUrl = secureRedirectUrl(currentUrl, location)
}
else -> return null
}
Expand Down Expand Up @@ -151,11 +148,7 @@ class GtfsIngestor(private val filesDir: File) {
status in 300..399 -> {
val location = response.headers[HttpHeaders.Location]
?: throw GtfsIngestException("GTFS download redirected without a Location header")
currentUrl = if (location.startsWith("http://")) {
"https://" + location.removePrefix("http://")
} else {
location
}
currentUrl = secureRedirectUrl(currentUrl, location)
}
else -> throw GtfsIngestException("GTFS download failed: HTTP $status")
}
Expand Down Expand Up @@ -199,6 +192,16 @@ class GtfsIngestor(private val filesDir: File) {
}
}

/** Resolves absolute and relative redirects while never following a redirect back to plain HTTP. */
private fun secureRedirectUrl(currentUrl: String, location: String): String {
val resolved = URI(currentUrl).resolve(location).toString()
return if (resolved.startsWith("http://")) {
"https://" + resolved.removePrefix("http://")
} else {
resolved
}
}
Comment on lines +195 to +203

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Jose! Does this change resolve the HTTP:// vs HTTPS:// issues for other agencies aswell? before I merge the change, I'd like to confirm that RIPTA will still work with these changes (HTTP:// Realtime feed)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It did on my end, but you can compile and test on your end to make sure that this is the case for your device as well. Lmk and if it doesn’t work I’ll find a workaround.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It worked but because the netconfig module bypass isn't removed I think RIPTA is still using that. I have a discussion open on it in the light SDK. just tested denver. Loaded up On here. Left a parentheses in the homescreen that stopped the compile... my bad... but It looks like Denver is working but was a little wonky switching agencies earlier. Thoughts on bustang? it looks like they have a separate feed for it. I can add it as a separate feed. the one structural difference I noticed is direction is in the routes, I'd just need add a check to see if routes contain directions, if not schedules should jump to the stops screen for agencies like Bustang... On another note I need to add an issue for a bug with stop tracking for buses, if it falls back on raw geocodes it may jump ahead if a stop is technically closer. happened to me the other day when I was riding in to providence, passed the stop going into the city and finished the trip while the bus needed to hit a few more stops... working on it.


private fun loadRoutes(db: SQLiteDatabase, reader: BufferedReader) {
db.delete("routes", null, null)
val stmt = db.compileStatement(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import io.ktor.client.call.body
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.request.get
import io.ktor.http.HttpHeaders
import java.net.URI
import java.time.LocalDate
import java.time.ZoneId
import kotlinx.serialization.ExperimentalSerializationApi
Expand Down Expand Up @@ -170,11 +171,7 @@ object GtfsRealtimeClient {
status in 300..399 -> {
val location = response.headers[HttpHeaders.Location]
?: throw GtfsRealtimeException("GTFS-RT redirected without a Location header")
currentUrl = if (location.startsWith("http://")) {
"https://" + location.removePrefix("http://")
} else {
location
}
currentUrl = secureRealtimeRedirectUrl(currentUrl, location)
}
else -> throw GtfsRealtimeException("GTFS-RT fetch failed: HTTP $status")
}
Expand All @@ -186,6 +183,16 @@ object GtfsRealtimeClient {
}
}

/** Resolves absolute and relative redirects while never following a redirect back to plain HTTP. */
private fun secureRealtimeRedirectUrl(currentUrl: String, location: String): String {
val resolved = URI(currentUrl).resolve(location).toString()
return if (resolved.startsWith("http://")) {
"https://" + resolved.removePrefix("http://")
} else {
resolved
}
}
Comment on lines +186 to +194

/** Default +/- window (seconds) within which a live prediction still counts as "On time". */
const val ARRIVAL_STATUS_TOLERANCE_SECONDS = 90L

Expand Down