merge w/ gtfs
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled

This commit is contained in:
Nicholas Orlowsky 2025-11-08 13:14:36 -05:00
commit 786f32e7e3
No known key found for this signature in database
GPG key ID: A9F3BA4C0AA7A70B
39 changed files with 1220 additions and 1515 deletions

View file

@ -19,6 +19,7 @@ WORKDIR /app
EXPOSE 8080 EXPOSE 8080
COPY --from=build /api/target/release/septastic_api /app/septastic_api COPY --from=build /api/target/release/septastic_api /app/septastic_api
COPY --from=build /api/config.yaml /app/config.yaml
COPY api/assets /app/assets COPY api/assets /app/assets
COPY api/templates /app/templates COPY api/templates /app/templates

489
api/Cargo.lock generated
View file

@ -103,7 +103,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
dependencies = [ dependencies = [
"quote", "quote",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -237,7 +237,7 @@ dependencies = [
"actix-router", "actix-router",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -391,6 +391,15 @@ version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]] [[package]]
name = "askama" name = "askama"
version = "0.14.0" version = "0.14.0"
@ -418,7 +427,7 @@ dependencies = [
"rustc-hash", "rustc-hash",
"serde", "serde",
"serde_derive", "serde_derive",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -717,6 +726,12 @@ version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "bytemuck"
version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
@ -738,6 +753,34 @@ dependencies = [
"bytes", "bytes",
] ]
[[package]]
name = "bzip2"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
dependencies = [
"bzip2-sys",
]
[[package]]
name = "bzip2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bea8dcd42434048e4f7a304411d9273a411f647446c1234a65ce0554923f4cff"
dependencies = [
"libbz2-rs-sys",
]
[[package]]
name = "bzip2-sys"
version = "0.1.13+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
dependencies = [
"cc",
"pkg-config",
]
[[package]] [[package]]
name = "camino" name = "camino"
version = "1.1.12" version = "1.1.12"
@ -862,7 +905,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -905,6 +948,12 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@ -1004,6 +1053,27 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "csv"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf"
dependencies = [
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "ctr" name = "ctr"
version = "0.9.2" version = "0.9.2"
@ -1013,6 +1083,12 @@ dependencies = [
"cipher", "cipher",
] ]
[[package]]
name = "deflate64"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204"
[[package]] [[package]]
name = "der" name = "der"
version = "0.7.10" version = "0.7.10"
@ -1033,6 +1109,28 @@ dependencies = [
"powerfmt", "powerfmt",
] ]
[[package]]
name = "derivative"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.20" version = "0.99.20"
@ -1043,7 +1141,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustc_version", "rustc_version",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -1063,7 +1161,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
"unicode-xid", "unicode-xid",
] ]
@ -1098,7 +1196,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -1241,6 +1339,12 @@ dependencies = [
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.2" version = "1.1.2"
@ -1248,6 +1352,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"libz-rs-sys",
"miniz_oxide", "miniz_oxide",
] ]
@ -1393,7 +1498,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -1454,9 +1559,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"r-efi", "r-efi",
"wasi 0.14.2+wasi-0.2.4", "wasi 0.14.2+wasi-0.2.4",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -1493,6 +1600,39 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "gtfs-realtime"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "421e7aa1a3a540a6f2e046cb349b184be285e16be332aff043e188b2b563c2ff"
dependencies = [
"prost",
"prost-build",
"prost-derive",
"serde",
]
[[package]]
name = "gtfs-structures"
version = "0.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3211c82a20a529763951e9072e4d85c06b6c8d4efa75d08859ccb58e702334e3"
dependencies = [
"bytes",
"chrono",
"csv",
"derivative",
"futures",
"itertools",
"reqwest",
"rgb",
"serde",
"serde_derive",
"sha2",
"thiserror 1.0.69",
"zip 2.4.2",
]
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.26" version = "0.3.26"
@ -1952,6 +2092,15 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.15" version = "1.0.15"
@ -1979,7 +2128,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -2026,6 +2175,12 @@ dependencies = [
"spin", "spin",
] ]
[[package]]
name = "libbz2-rs-sys"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.174" version = "0.2.174"
@ -2069,6 +2224,15 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "libz-rs-sys"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd"
dependencies = [
"zlib-rs",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.3.8" version = "0.3.8"
@ -2129,6 +2293,37 @@ dependencies = [
"value-bag", "value-bag",
] ]
[[package]]
name = "lzma-rs"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
dependencies = [
"byteorder",
"crc",
]
[[package]]
name = "lzma-rust2"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a"
dependencies = [
"crc",
"sha2",
]
[[package]]
name = "lzma-sys"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]] [[package]]
name = "md-5" name = "md-5"
version = "0.10.6" version = "0.10.6"
@ -2182,6 +2377,12 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "multimap"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
[[package]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.14" version = "0.2.14"
@ -2302,7 +2503,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -2352,6 +2553,16 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
]
[[package]] [[package]]
name = "pem-rfc7468" name = "pem-rfc7468"
version = "0.7.0" version = "0.7.0"
@ -2367,6 +2578,16 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "petgraph"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
dependencies = [
"fixedbitset",
"indexmap",
]
[[package]] [[package]]
name = "phf" name = "phf"
version = "0.12.1" version = "0.12.1"
@ -2508,6 +2729,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppmd-rust"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.21" version = "0.2.21"
@ -2517,6 +2744,16 @@ dependencies = [
"zerocopy", "zerocopy",
] ]
[[package]]
name = "prettyplease"
version = "0.2.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2"
dependencies = [
"proc-macro2",
"syn 2.0.104",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.95" version = "1.0.95"
@ -2526,6 +2763,58 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "prost"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-build"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1"
dependencies = [
"heck",
"itertools",
"log",
"multimap",
"once_cell",
"petgraph",
"prettyplease",
"prost",
"prost-types",
"regex",
"syn 2.0.104",
"tempfile",
]
[[package]]
name = "prost-derive"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "prost-types"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72"
dependencies = [
"prost",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.40" version = "1.0.40"
@ -2686,6 +2975,15 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "rgb"
version = "0.8.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
dependencies = [
"bytemuck",
]
[[package]] [[package]]
name = "ring" name = "ring"
version = "0.17.14" version = "0.17.14"
@ -2888,13 +3186,18 @@ dependencies = [
"dotenv", "dotenv",
"env_logger", "env_logger",
"futures-util", "futures-util",
"gtfs-realtime",
"gtfs-structures",
"libseptastic", "libseptastic",
"log", "log",
"prost",
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml",
"sqlx", "sqlx",
"sqlx-cli", "sqlx-cli",
"zip 5.1.1",
] ]
[[package]] [[package]]
@ -2914,7 +3217,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -2941,6 +3244,19 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.6" version = "0.10.6"
@ -2994,6 +3310,12 @@ dependencies = [
"rand_core 0.6.4", "rand_core 0.6.4",
] ]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "1.0.1" version = "1.0.1"
@ -3137,7 +3459,7 @@ dependencies = [
"quote", "quote",
"sqlx-core", "sqlx-core",
"sqlx-macros-core", "sqlx-macros-core",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -3161,7 +3483,7 @@ dependencies = [
"sqlx-mysql", "sqlx-mysql",
"sqlx-postgres", "sqlx-postgres",
"sqlx-sqlite", "sqlx-sqlite",
"syn", "syn 2.0.104",
"tokio", "tokio",
"url", "url",
] ]
@ -3298,6 +3620,17 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.104" version = "2.0.104"
@ -3326,7 +3659,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -3399,7 +3732,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -3410,7 +3743,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -3497,7 +3830,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -3609,7 +3942,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -3688,6 +4021,12 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
@ -3799,7 +4138,7 @@ dependencies = [
"log", "log",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3834,7 +4173,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3911,7 +4250,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -3922,7 +4261,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -4206,6 +4545,15 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "xz2"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
dependencies = [
"lzma-sys",
]
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.0" version = "0.8.0"
@ -4226,7 +4574,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
"synstructure", "synstructure",
] ]
@ -4247,7 +4595,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
] ]
[[package]] [[package]]
@ -4267,7 +4615,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
"synstructure", "synstructure",
] ]
@ -4276,6 +4624,20 @@ name = "zeroize"
version = "1.8.1" version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]] [[package]]
name = "zerotrie" name = "zerotrie"
@ -4307,7 +4669,82 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.104",
]
[[package]]
name = "zip"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
dependencies = [
"aes",
"arbitrary",
"bzip2 0.5.2",
"constant_time_eq",
"crc32fast",
"crossbeam-utils",
"deflate64",
"displaydoc",
"flate2",
"getrandom 0.3.3",
"hmac",
"indexmap",
"lzma-rs",
"memchr",
"pbkdf2",
"sha1",
"thiserror 2.0.12",
"time",
"xz2",
"zeroize",
"zopfli",
"zstd",
]
[[package]]
name = "zip"
version = "5.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f852905151ac8d4d06fdca66520a661c09730a74c6d4e2b0f27b436b382e532"
dependencies = [
"aes",
"arbitrary",
"bzip2 0.6.0",
"constant_time_eq",
"crc32fast",
"deflate64",
"flate2",
"getrandom 0.3.3",
"hmac",
"indexmap",
"lzma-rust2",
"memchr",
"pbkdf2",
"ppmd-rust",
"sha1",
"time",
"zeroize",
"zopfli",
"zstd",
]
[[package]]
name = "zlib-rs"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2"
[[package]]
name = "zopfli"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7"
dependencies = [
"bumpalo",
"crc32fast",
"log",
"simd-adler32",
] ]
[[package]] [[package]]

View file

@ -22,3 +22,8 @@ reqwest = { version = "0.12.22", features = [ "json", "blocking" ] }
sqlx-cli = "0.8.6" sqlx-cli = "0.8.6"
futures-util = "0.3.31" futures-util = "0.3.31"
actix-session = { version = "0.11.0", features = ["cookie-session"] } actix-session = { version = "0.11.0", features = ["cookie-session"] }
serde_yaml = "0.9.34"
gtfs-structures = "0.45.1"
zip = "5.1.1"
gtfs-realtime = "0.2.0"
prost = "0.14.1"

10
api/config.yaml Normal file
View file

@ -0,0 +1,10 @@
gtfs_zips:
- uri: "https://www3.septa.org/developer/gtfs_public.zip"
subzip: "google_rail.zip"
- uri: "https://www3.septa.org/developer/gtfs_public.zip"
subzip: "google_bus.zip"
# - uri: "https://www.njtransit.com/rail_data.zip"
# - uri: "https://www.njtransit.com/bus_data.zip"
annotations:
synthetic_routes:
- id: 'NYC'

View file

@ -1,12 +1,10 @@
use actix_web::{get, web::{self, Data}, HttpRequest, HttpResponse, Responder}; use actix_web::{get, web::{self, Data}, HttpRequest, HttpResponse, Responder};
use anyhow::anyhow; use anyhow::anyhow;
use std::{time::Instant, sync::Arc}; use std::{collections::HashSet, sync::Arc, time::Instant};
use libseptastic::{route::RouteType, stop_schedule::Trip}; use libseptastic::{direction, route::RouteType, stop_schedule::Trip};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use askama::Template;
use crate::AppState; use crate::AppState;
use crate::database;
#[get("/routes")] #[get("/routes")]
async fn get_routes_html(req: HttpRequest, state: Data<Arc<AppState>>) -> impl Responder { async fn get_routes_html(req: HttpRequest, state: Data<Arc<AppState>>) -> impl Responder {
@ -15,8 +13,7 @@ async fn get_routes_html(req: HttpRequest, state: Data<Arc<AppState>>) -> impl R
async move { async move {
let start_time = Instant::now(); let start_time = Instant::now();
let all_routes = database::get_all_routes(&mut statex.database.begin().await?).await?; let all_routes: Vec<libseptastic::route::Route> = statex.gtfs_service.get_routes();
let rr_routes = all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::RegionalRail).collect(); let rr_routes = all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::RegionalRail).collect();
let subway_routes = all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::SubwayElevated).collect(); let subway_routes = all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::SubwayElevated).collect();
let trolley_routes = all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::Trolley).collect(); let trolley_routes = all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::Trolley).collect();
@ -40,7 +37,7 @@ async fn get_routes_html(req: HttpRequest, state: Data<Arc<AppState>>) -> impl R
#[get("/routes.json")] #[get("/routes.json")]
async fn get_routes_json(state: Data<Arc<AppState>>) -> impl Responder { async fn get_routes_json(state: Data<Arc<AppState>>) -> impl Responder {
let all_routes = database::get_all_routes(&mut state.database.begin().await.unwrap()).await.unwrap(); let all_routes: Vec<libseptastic::route::Route> = state.gtfs_service.get_routes();
HttpResponse::Ok().json(all_routes) HttpResponse::Ok().json(all_routes)
} }
@ -58,11 +55,15 @@ pub struct RouteResponse {
} }
async fn get_route_info(route_id: String, state: Data<Arc<AppState>>) -> ::anyhow::Result<RouteResponse> { async fn get_route_info(route_id: String, state: Data<Arc<AppState>>) -> ::anyhow::Result<RouteResponse> {
let mut tx = state.database.begin().await?; let route = state.gtfs_service.get_route(route_id.clone())?;
let mut trips = state.gtfs_service.get_schedule(route_id)?;
let route = database::get_route_by_id(route_id.clone(), &mut tx).await?; let mut seen = HashSet::new();
let directions = database::get_direction_by_route_id(route_id.clone(), &mut tx).await?; let directions: Vec<_> = trips
let mut trips = database::get_schedule_by_route_id(route_id.clone(), &mut tx).await?; .iter()
.map(|x| x.direction.clone())
.filter(|dir| seen.insert(dir.direction.to_string()))
.collect();
state.trip_tracking_service.annotate_trips(&mut trips); state.trip_tracking_service.annotate_trips(&mut trips);
@ -127,7 +128,7 @@ async fn api_get_route(state: Data<Arc<AppState>>, path: web::Path<String>) -> i
#[get("/api/route/{route_id}/schedule")] #[get("/api/route/{route_id}/schedule")]
async fn api_get_schedule(state: Data<Arc<AppState>>, path: web::Path<String>) -> impl Responder { async fn api_get_schedule(state: Data<Arc<AppState>>, path: web::Path<String>) -> impl Responder {
let route_id = path.into_inner(); let route_id = path.into_inner();
let route_r = database::get_schedule_by_route_id(route_id, &mut state.database.begin().await.unwrap()).await; let route_r: anyhow::Result<i32> = Ok(5);
if let Ok(route) = route_r { if let Ok(route) = route_r {
HttpResponse::Ok().json(route) HttpResponse::Ok().json(route)

View file

@ -3,18 +3,17 @@ use env_logger::Env;
use log::*; use log::*;
use dotenv::dotenv; use dotenv::dotenv;
use serde::Deserialize; use serde::Deserialize;
use services::trip_tracking::{self}; use services::{gtfs_pull, trip_tracking::{self}};
use templates::ContentTemplate; use templates::ContentTemplate;
use std::{sync::Arc, time::Instant}; use std::{fs::File, io::Read, sync::Arc, time::Instant};
use askama::Template; use askama::Template;
mod database;
mod services; mod services;
mod controllers; mod controllers;
mod templates; mod templates;
pub struct AppState { pub struct AppState {
database: ::sqlx::postgres::PgPool, gtfs_service: services::gtfs_pull::GtfsPullService,
trip_tracking_service: services::trip_tracking::TripTrackingService trip_tracking_service: services::trip_tracking::TripTrackingService
} }
@ -79,19 +78,22 @@ async fn main() -> ::anyhow::Result<()> {
let version: &str = option_env!("CARGO_PKG_VERSION").expect("Expected package version"); let version: &str = option_env!("CARGO_PKG_VERSION").expect("Expected package version");
info!("Starting the SEPTASTIC Server v{} (commit: {})", version, "NONE"); info!("Starting the SEPTASTIC Server v{} (commit: {})", version, "NONE");
let connection_string = let mut file = File::open("./config.yaml")?;
std::env::var("DB_CONNSTR").expect("Expected database connection string"); let mut file_contents = String::new();
file.read_to_string(&mut file_contents);
let pool = ::sqlx::postgres::PgPoolOptions::new() let config_file = serde_yaml::from_str::<gtfs_pull::Config>(file_contents.as_str())?;
.max_connections(5)
.connect(&connection_string)
.await?;
let tt_service = trip_tracking::TripTrackingService::new(); let tt_service = services::trip_tracking::TripTrackingService::new();
tt_service.start(); tt_service.start();
let svc = gtfs_pull::GtfsPullService::new(config_file);
svc.start();
svc.wait_for_ready();
let state = Arc::new(AppState { let state = Arc::new(AppState {
database: pool, gtfs_service: svc,
trip_tracking_service: tt_service trip_tracking_service: tt_service
}); });

View file

@ -1 +1,3 @@
pub mod trip_tracking; pub mod trip_tracking;
pub mod gtfs_pull;
pub mod gtfs_rt;

View file

@ -103,7 +103,10 @@ impl TripTrackingService {
None => None None => None
}, },
timestamp: live_track.timestamp, timestamp: live_track.timestamp,
vehicle_id: live_track.vehicle_id vehicle_ids: match live_track.vehicle_id {
Some(x) => x.split(",").map(|f| String::from(f)).collect(),
None => vec![]
}
} }
) )
} }

View file

@ -72,7 +72,7 @@ pub fn build_timetables(
let mut direction_trips: Vec<&Trip> = trips let mut direction_trips: Vec<&Trip> = trips
.iter() .iter()
.filter(|trip| trip.direction_id == direction.direction_id) .filter(|trip| trip.direction.direction == direction.direction)
.collect(); .collect();
direction_trips.sort_by_key(|trip| { direction_trips.sort_by_key(|trip| {
@ -86,7 +86,7 @@ pub fn build_timetables(
for trip in direction_trips.clone() { for trip in direction_trips.clone() {
if let Some(last) = trip.schedule.iter().max_by_key(|x| x.arrival_time) { if let Some(last) = trip.schedule.iter().max_by_key(|x| x.arrival_time) {
if next_id == None && i64::from(seconds_since_midnight) < last.arrival_time { if next_id == None && i64::from(seconds_since_midnight) < last.arrival_time {
next_id = Some(last.trip_id.clone()); next_id = Some(last.stop.id.to_string());
} }
} }
} }
@ -107,12 +107,12 @@ pub fn build_timetables(
for (trip_index, trip) in direction_trips.iter().enumerate() { for (trip_index, trip) in direction_trips.iter().enumerate() {
for stop in &trip.schedule { for stop in &trip.schedule {
let entry = stop_map let entry = stop_map
.entry(stop.stop_id) .entry(stop.stop.id)
.or_insert((stop.stop_sequence, stop.stop_name.clone(), vec![None; direction_trips.len()])); .or_insert((stop.stop_sequence, stop.stop.name.clone(), vec![None; direction_trips.len()]));
// If this stop_id appears in multiple trips with different sequences, keep the lowest // If this stop_id appears in multiple trips with different sequences, keep the lowest
entry.0 = entry.0.max(stop.stop_sequence); entry.0 = entry.0.max(stop.stop_sequence);
entry.1 = stop.stop_name.clone(); entry.1 = stop.stop.name.clone();
entry.2[trip_index] = Some(stop.arrival_time); entry.2[trip_index] = Some(stop.arrival_time);
} }
} }

View file

@ -105,7 +105,7 @@ document.addEventListener("DOMContentLoaded", () => {
</div> </div>
{% for timetable in timetables %} {% for timetable in timetables %}
<details style="margin-top: 15px;" data-direction-id="{{ timetable.direction.direction_id }}"> <details style="margin-top: 15px;" data-direction-id="{{ timetable.direction.direction }}">
<summary> <summary>
<div style="display: inline-block;"> <div style="display: inline-block;">
<h3>{{ timetable.direction.direction | capitalize }} to</h3> <h3>{{ timetable.direction.direction | capitalize }} to</h3>
@ -120,7 +120,7 @@ document.addEventListener("DOMContentLoaded", () => {
{% for trip_id in timetable.trip_ids %} {% for trip_id in timetable.trip_ids %}
{% if let Some(next_id_v) = timetable.next_id %} {% if let Some(next_id_v) = timetable.next_id %}
{% if next_id_v == trip_id %} {% if next_id_v == trip_id %}
<th class="next-col" id="next-col-{{ timetable.direction.direction_id }}"> <th class="next-col" id="next-col-{{ timetable.direction.direction }}">
{% else %} {% else %}
<th> <th>
{% endif %} {% endif %}

View file

@ -1,18 +1,18 @@
{% macro route_symbol(route) %} {% macro route_symbol(route) %}
{% match route.route_type %} {% match route.route_type %}
{% when libseptastic::route::RouteType::Trolley | libseptastic::route::RouteType::SubwayElevated %} {% when libseptastic::route::RouteType::Trolley | libseptastic::route::RouteType::SubwayElevated %}
<div class="metro-container bg-{{ route.id }}"> <div class="metro-container bg-{{ route.short_name }}">
{{ route.id }} {{ route.short_name }}
</div> </div>
{% endwhen %} {% endwhen %}
{% when libseptastic::route::RouteType::RegionalRail %} {% when libseptastic::route::RouteType::RegionalRail %}
<div class="rr-container"> <div class="rr-container">
{{ route.id }} {{ route.short_name }}
</div> </div>
{% endwhen %} {% endwhen %}
{% when libseptastic::route::RouteType::Bus | libseptastic::route::RouteType::TracklessTrolley %} {% when libseptastic::route::RouteType::Bus | libseptastic::route::RouteType::TracklessTrolley %}
<div class="bus-container"> <div class="bus-container">
{{ route.id }} {{ route.short_name }}
</div> </div>
{% endwhen %} {% endwhen %}
{% endmatch %} {% endmatch %}

View file

@ -7,7 +7,7 @@
<p style="margin-top: 10px; margin-bottom: 10px;">For infrequent rail service to suburban locations</p> <p style="margin-top: 10px; margin-bottom: 10px;">For infrequent rail service to suburban locations</p>
{% for route in rr_routes %} {% for route in rr_routes %}
<a href="/route/{{ route.id }}" style="display: flex; justify-content: space-between;"> <a href="/route/{{ route.id }}" style="display: flex; justify-content: space-between;">
<p class="line-link">[ <b>{{ format!("{:7}", route.id) }}:</b> {{ route.name }} </p><p>]</p> <p class="line-link">[ <b>{{ format!("{:7}", route.short_name) }}:</b> {{ route.name }} </p><p>]</p>
</a> </a>
{% endfor %} {% endfor %}
</fieldset> </fieldset>
@ -20,7 +20,7 @@
</div> </div>
{% for route in subway_routes %} {% for route in subway_routes %}
<a href="/route/{{ route.id }}" style="display: flex; justify-content: space-between;"> <a href="/route/{{ route.id }}" style="display: flex; justify-content: space-between;">
<p class="line-link">[ <b>{{ format!("{:7}", route.id) }}:</b> {{ route.name }} </p><p>]</p> <p class="line-link">[ <b>{{ format!("{:7}", route.short_name) }}:</b> {{ route.name }} </p><p>]</p>
</a> </a>
{% endfor %} {% endfor %}
@ -30,7 +30,7 @@
{% for route in trolley_routes %} {% for route in trolley_routes %}
<a href="/route/{{ route.id }}" style="display: flex; justify-content: space-between;"> <a href="/route/{{ route.id }}" style="display: flex; justify-content: space-between;">
<p class="line-link">[ <b>{{ format!("{:7}", route.id) }}:</b> {{ route.name }} </p><p>]</p> <p class="line-link">[ <b>{{ format!("{:7}", route.short_name) }}:</b> {{ route.name }} </p><p>]</p>
</a> </a>
{% endfor %} {% endfor %}
</fieldset> </fieldset>
@ -40,7 +40,7 @@
<p style="margin-top: 10px; margin-bottom: 10px;">For service of varying frequency within SEPTA's entire service area</p> <p style="margin-top: 10px; margin-bottom: 10px;">For service of varying frequency within SEPTA's entire service area</p>
{% for route in bus_routes %} {% for route in bus_routes %}
<a href="/route/{{ route.id }}" style="display: flex; justify-content: space-between;"> <a href="/route/{{ route.id }}" style="display: flex; justify-content: space-between;">
<p class="line-link">[ <b>{{ format!("{:7}", route.id) }}:</b> {{ route.name }} </p><p>]</p> <p class="line-link">[ <b>{{ format!("{:7}", route.short_name) }}:</b> {{ route.name }} </p><p>]</p>
</a> </a>
{% endfor %} {% endfor %}
</fieldset> </fieldset>

688
data_loader/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -17,3 +17,6 @@ libseptastic = { path = "../libseptastic/" }
env_logger = "0.11.8" env_logger = "0.11.8"
log = "0.4.27" log = "0.4.27"
reqwest = { version = "0.12.22", features = [ "json", "blocking" ] } reqwest = { version = "0.12.22", features = [ "json", "blocking" ] }
serde_yaml = "0.9.34"
gtfs-structures = "0.45.1"
zip = "5.1.1"

10
data_loader/config.yaml Normal file
View file

@ -0,0 +1,10 @@
gtfs_zips:
- uri: "https://www3.septa.org/developer/gtfs_public.zip"
subzip: "google_rail.zip"
- uri: "https://www3.septa.org/developer/gtfs_public.zip"
subzip: "google_bus.zip"
# - uri: "https://www.njtransit.com/rail_data.zip"
# - uri: "https://www.njtransit.com/bus_data.zip"
annotations:
synthetic_routes:
- id: 'NYC'

View file

@ -1,12 +0,0 @@
use libseptastic::{direction::Direction, route::Route, route_stop::RouteStop, schedule_day::ScheduleDay, stop::Stop, stop_schedule::StopSchedule};
pub mod septa;
pub struct AuthorityInfo {
pub routes: Vec<Route>,
pub stops: Vec<Stop>,
pub route_stops: Vec<RouteStop>,
pub stop_schedules: Vec<StopSchedule>,
pub schedule_days: Vec<ScheduleDay>,
pub directions: Vec<Direction>
}

View file

@ -1,258 +0,0 @@
use dotenv::dotenv;
use libseptastic::stop::Stop;
use crate::septa::route_stop;
use crate::septa_json::direction::Direction;
use crate::septa_json::route::Route;
use crate::septa_json::route_stop::RouteStop;
use crate::septa_json::schedule_day::{Calendar, ScheduleDay};
use crate::septa_json::stop_schedule::StopSchedule;
use sqlx::postgres::PgPoolOptions;
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::{Arc, Mutex};
use std::{fs, thread};
use std::path::Path;
use std::time::SystemTime;
use env_logger::{Builder, Env};
use log::{error, info, warn};
use crate::traits::*;
use super::AuthorityInfo;
async fn convert_routes(json_data: Vec<crate::septa_json::route::Route>) -> ::anyhow::Result<Vec<libseptastic::route::Route>> {
let mut parsed_data: Vec<libseptastic::route::Route> = Vec::new();
for json_single in json_data {
parsed_data.push(*libseptastic::route::Route::from_septa_json(json_single)?);
}
Ok(parsed_data)
}
async fn convert_stops(json_data: Vec<crate::septa_json::route_stop::RouteStop>) -> ::anyhow::Result<Vec<libseptastic::stop::Stop>> {
let mut parsed_data: Vec<libseptastic::stop::Stop> = Vec::new();
for json_single in json_data {
parsed_data.push(*libseptastic::stop::Stop::from_septa_json(json_single)?);
}
Ok(parsed_data)
}
async fn convert_route_stops(json_data: Vec<crate::septa_json::route_stop::RouteStop>) -> ::anyhow::Result<Vec<libseptastic::route_stop::RouteStop>> {
let mut parsed_data: Vec<libseptastic::route_stop::RouteStop> = Vec::new();
for json_single in json_data {
parsed_data.push(*libseptastic::route_stop::RouteStop::from_septa_json(json_single)?);
}
Ok(parsed_data)
}
async fn convert_stop_schedules(json_data:Vec<crate::septa_json::stop_schedule::StopSchedule>) -> ::anyhow::Result<Vec<libseptastic::stop_schedule::StopSchedule>> {
let mut parsed_data: Vec<libseptastic::stop_schedule::StopSchedule> = Vec::new();
for json_single in json_data {
parsed_data.push(*libseptastic::stop_schedule::StopSchedule::from_septa_json(json_single)?);
}
Ok(parsed_data)
}
async fn convert_schedule_days(json_data: crate::septa_json::schedule_day::Calendar) -> ::anyhow::Result<Vec<libseptastic::schedule_day::ScheduleDay>> {
let mut parsed_data: Vec<libseptastic::schedule_day::ScheduleDay> = Vec::new();
parsed_data.append(&mut *Vec::<libseptastic::schedule_day::ScheduleDay>::from_septa_json(json_data)?);
Ok(parsed_data)
}
async fn convert_directions(json_data: Vec<crate::septa_json::direction::Direction>) -> ::anyhow::Result<Vec<libseptastic::direction::Direction>> {
let mut parsed_data: Vec<libseptastic::direction::Direction> = Vec::new();
for json_single in json_data {
parsed_data.push(*libseptastic::direction::Direction::from_septa_json(json_single)?);
}
Ok(parsed_data)
}
pub struct SeptaFetcher{
}
impl SeptaFetcher {
const HOST: &str = "https://flat-api.septa.org";
async fn fetch_version() -> ::anyhow::Result<String> {
let unix_seconds = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?.as_secs();
let mut version = reqwest::get(format!("{}/version.txt?t={}", Self::HOST, unix_seconds)).await?.text().await?;
version = version.trim().to_string();
Ok(version)
}
async fn fetch_routes(version: &String) -> ::anyhow::Result<Vec<Route>> {
Ok(reqwest::get(format!("{}/routes.json?v={}", Self::HOST, version)).await?.json::<Vec<Route>>().await?)
}
async fn fetch_route_stops(routes: &mut Vec<Route>, version: &String) -> ::anyhow::Result<Vec<RouteStop>> {
let mut all_route_stops: Vec<RouteStop> = Vec::new();
let mut i = 0;
while i < routes.len() {
let route = &routes[i];
if let Ok(route_stops_json) = reqwest::get(format!("{}/stops/{}/stops.json?v={}", Self::HOST, route.route_id, version)).await {
if let Ok(mut route_stops) = route_stops_json.json::<Vec<RouteStop>>().await {
all_route_stops.append(&mut route_stops);
i += 1;
continue;
}
}
warn!("Encountered failure downloading route stops for Route {}, removing", route.route_id);
routes.swap_remove(i);
}
if routes.len() == 0 {
error!("Unable to download SEPTA routes");
return Err(anyhow::anyhow!("Unable to successfully download any SEPTA routes"));
}
Ok(all_route_stops)
}
async fn fetch_directions(version: &String) -> ::anyhow::Result<Vec<Direction>> {
Ok(reqwest::get(format!("{}/directions.json?v={}", Self::HOST, version)).await?.json::<Vec<Direction>>().await?)
}
async fn fetch_calendar(version: &String) -> ::anyhow::Result<Calendar> {
Ok(reqwest::get(format!("{}/calendar.json?v={}", Self::HOST, version)).await?.json::<Calendar>().await?)
}
async fn fetch_schedules(stops: &Vec<RouteStop>, version: &String) -> ::anyhow::Result<Vec<StopSchedule>> {
const NUM_THREADS: usize = 10;
let mut threads = Vec::with_capacity(NUM_THREADS);
let mut stop_ids: HashSet<i64> = HashSet::new();
let results = Arc::new(Mutex::new(Vec::new()));
let mut worker_queues: Vec<VecDeque<(i64, i64)>> = Vec::with_capacity(NUM_THREADS);
for _ in 0..NUM_THREADS {
worker_queues.push(VecDeque::new());
}
for stop in stops {
stop_ids.insert(stop.stop_id);
}
let mut cur_thread_no = 0;
for stop_id in stop_ids {
worker_queues[cur_thread_no].push_back((stop_id, 0));
cur_thread_no = (cur_thread_no + 1) % NUM_THREADS;
}
for thread_no in 0..NUM_THREADS {
let mut worker_queue = worker_queues[thread_no].clone();
let local_result = results.clone();
let local_version = version.clone();
threads.push(thread::spawn(move || {
let mut stop_schedules: Vec<StopSchedule> = Vec::new();
while let Some(stop_id) = worker_queue.pop_front() {
let uri = format!("{}/schedules/stop_schedule/{}.json?v={}", Self::HOST, stop_id.0, local_version);
match (||{reqwest::blocking::get(uri)?.json::<Vec<StopSchedule>>()})() {
Ok(mut element) => (stop_schedules.append(&mut element)),
Err(_) => {
if stop_id.1 < 5 {
error!("Error downloading stop schedule for {} (try {}). Retrying.", stop_id.0, stop_id.1);
worker_queue.push_back((stop_id.0, stop_id.1 + 1));
} else {
error!("Error downloading stop schedule for {} (try {}). Giving up.", stop_id.0, stop_id.1);
}
}
}
}
if let Ok(mut res) = local_result.lock() {
res.append(&mut stop_schedules);
}
}));
}
for thread in threads {
thread.join();
}
Ok(results.lock().unwrap().clone())
}
pub async fn fetch_septa_data() -> ::anyhow::Result<AuthorityInfo> {
let version = Self::fetch_version().await?;
info!("Got SEPTA schedule version {}", version);
let mut routes = Self::fetch_routes(&version).await?;
info!("Discovered {} SEPTA routes", routes.len());
routes = routes.into_iter().filter(|x| x.release_name == "20250928").collect();
let route_stops = Self::fetch_route_stops(&mut routes, &version).await?;
info!("Stop data for {} stops on {} routes successfully downloaded", route_stops.len(), routes.len());
let directions = Self::fetch_directions(&version).await?;
info!("Directions data successfully downloaded for {} route directions", directions.len());
let mut calendar = Self::fetch_calendar(&version).await?;
info!("Calendar data successfully downloaded for {} days", calendar.len());
for entry in calendar.clone() {
if entry.1.release_name != "20250928" {
calendar.remove(&entry.0.clone());
}
}
let mut schedule_stops = Self::fetch_schedules(&route_stops, &version).await?;
info!("Schedule data downloaded for {} scheduled stops", schedule_stops.len());
schedule_stops = schedule_stops.into_iter().filter(|x| x.release_name == "20250928").collect();
let mut stop_map: HashMap<String, i64> = HashMap::new();
let mut stop_set: HashSet<i64> = HashSet::new();
for stop in route_stops.iter() {
stop_map.insert(stop.stop_name.clone(), stop.stop_id);
stop_set.insert(stop.stop_id);
}
let mut route_map: HashSet<String> = HashSet::new();
for route in routes.iter() {
if route_map.contains(&route.route_id) {
error!("Duplicate route found for {}", route.route_id.clone());
}
route_map.insert(route.route_id.clone());
}
let filtered_directions: Vec<_> = directions.iter()
.filter(|direction| {
let keep = route_map.contains(&direction.route);
if !keep {
warn!(
"Removing route '{}' from direction data (This data has old and new wayfinding values)",
direction.route
);
}
keep
})
.cloned()
.collect();
info!("Data is valid");
Ok(AuthorityInfo { routes: convert_routes(routes).await?, stops: convert_stops(route_stops.clone()).await?, route_stops: convert_route_stops(route_stops).await?, stop_schedules: convert_stop_schedules(schedule_stops).await?, schedule_days: convert_schedule_days(calendar).await?, directions: convert_directions(filtered_directions).await? })
}
}

View file

@ -1,26 +1,11 @@
use std::{clone, env, fs::{self, File}, io::{Cursor, Read, Write}, path::{Path, PathBuf}, sync::{Arc, Mutex}, thread, time::Duration};
use dotenv::dotenv; use dotenv::dotenv;
use libseptastic::stop::Stop; use serde::{Deserialize, Serialize};
use septa::route_stop;
use septa_json::direction::Direction;
use septa_json::route::Route;
use septa_json::route_stop::RouteStop;
use septa_json::schedule_day::{Calendar, ScheduleDay};
use septa_json::stop_schedule::StopSchedule;
use sqlx::postgres::PgPoolOptions; use sqlx::postgres::PgPoolOptions;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use std::{fs, thread};
use std::path::Path;
use std::time::SystemTime;
use env_logger::{Builder, Env}; use env_logger::{Builder, Env};
use log::{error, info, warn}; use log::{error, info, warn};
use zip::ZipArchive;
pub mod traits;
use traits::*;
pub mod septa_json;
pub mod septa;
pub mod fetchers;
#[tokio::main] #[tokio::main]
@ -29,44 +14,20 @@ async fn main() -> ::anyhow::Result<()> {
let env = Env::new().filter_or("RUST_LOG", "data_loader=info"); let env = Env::new().filter_or("RUST_LOG", "data_loader=info");
Builder::from_env(env).init(); Builder::from_env(env).init();
loop{
thread::sleep(Duration::from_secs(120));
}
let database_url = std::env::var("DATABASE_URL").expect("Database URL"); let database_url = std::env::var("DATABASE_URL").expect("Database URL");
let pool = PgPoolOptions::new() let pool = PgPoolOptions::new()
.max_connections(5) .max_connections(5)
.connect(&database_url) .connect(&database_url)
.await?; .await?;
let septa_data = fetchers::septa::SeptaFetcher::fetch_septa_data().await?;
let mut tx = pool.begin().await?; let mut tx = pool.begin().await?;
libseptastic::route::Route::create_table(&mut tx).await?;
libseptastic::direction::Direction::create_table(&mut tx).await?;
libseptastic::stop::Stop::create_table(&mut tx).await?;
libseptastic::route_stop::RouteStop::create_table(&mut tx).await?;
libseptastic::stop_schedule::StopSchedule::create_table(&mut tx).await?;
libseptastic::schedule_day::ScheduleDay::create_table(&mut tx).await?;
info!("Inserting Route Data");
libseptastic::route::Route::insert_many(septa_data.routes, &mut tx).await?;
info!("Inserting Direction Data");
libseptastic::direction::Direction::insert_many(septa_data.directions, &mut tx).await?;
info!("Inserting Stop Data");
libseptastic::stop::Stop::insert_many(septa_data.stops, &mut tx).await?;
info!("Inserting Route-Stop Data");
libseptastic::route_stop::RouteStop::insert_many(septa_data.route_stops, &mut tx).await?;
info!("Inserting Stop Schedule Data");
libseptastic::stop_schedule::StopSchedule::insert_many(septa_data.stop_schedules, &mut tx).await?;
info!("Inserting Schedule Day Data");
libseptastic::schedule_day::ScheduleDay::insert_many(septa_data.schedule_days, &mut tx).await?;
tx.commit().await?; tx.commit().await?;
pool.close().await; pool.close().await;

View file

@ -1,85 +0,0 @@
use libseptastic::direction::{CardinalDirection, Direction};
use sqlx::{Postgres, Transaction};
use crate::traits::{DbObj, FromSeptaJson};
use crate::septa_json;
impl DbObj<Direction> for Direction {
async fn create_table(tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
sqlx::query("
CREATE TABLE IF NOT EXISTS septa_directions (
route_id TEXT NOT NULL,
direction_id BIGINT NOT NULL,
direction septa_direction_type NOT NULL,
direction_destination TEXT NOT NULL,
FOREIGN KEY (route_id) REFERENCES septa_routes(id) ON DELETE CASCADE,
PRIMARY KEY (route_id, direction_id)
);
")
.execute(&mut **tx)
.await?;
Ok(())
}
async fn insert_many(dirs: Vec<Direction>, tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
let mut route_ids: Vec<String> = Vec::new();
let mut direction_ids: Vec<i64> = Vec::new();
let mut directions: Vec<CardinalDirection> = Vec::new();
let mut direction_destinations: Vec<String> = Vec::new();
for dir in dirs {
route_ids.push(dir.route_id);
direction_ids.push(dir.direction_id);
directions.push(dir.direction);
direction_destinations.push(dir.direction_destination);
}
sqlx::query("
INSERT INTO septa_directions (
route_id,
direction_id,
direction,
direction_destination
)
SELECT * FROM UNNEST(
$1::text[],
$2::bigint[],
$3::septa_direction_type[],
$4::text[]
);
")
.bind(&route_ids[..])
.bind(&direction_ids[..])
.bind(&directions[..])
.bind(&direction_destinations[..])
.execute(&mut **tx)
.await?;
Ok(())
}
async fn insert(&self, tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
Self::insert_many(vec![self.clone()], tx).await?;
Ok(())
}
}
impl FromSeptaJson<septa_json::direction::Direction> for Direction {
fn from_septa_json(json_dir: septa_json::direction::Direction) -> ::anyhow::Result<Box<Direction>> {
Ok(Box::new(Direction { route_id: json_dir.route, direction_id: json_dir.direction.parse::<i64>()?,
direction: match json_dir.true_direction.as_str().clone() {
"Eastbound" => Ok(libseptastic::direction::CardinalDirection::Eastbound),
"Westbound" => Ok(libseptastic::direction::CardinalDirection::Westbound),
"Outbound" => Ok(libseptastic::direction::CardinalDirection::Outbound),
"Inbound" => Ok(libseptastic::direction::CardinalDirection::Inbound),
"Southbound" => Ok(libseptastic::direction::CardinalDirection::Southbound),
"Northbound" => Ok(libseptastic::direction::CardinalDirection::Northbound),
"LOOP" => Ok(libseptastic::direction::CardinalDirection::Loop),
"Loop" => Ok(libseptastic::direction::CardinalDirection::Loop),
_ => Err(anyhow::anyhow!(format!("Unable to find right direction {}", json_dir.true_direction)))
}?
, direction_destination: json_dir.direction_destination }))
}
}

View file

@ -1,7 +0,0 @@
pub mod route;
pub mod direction;
pub mod stop;
pub mod route_stop;
pub mod stop_schedule;
pub mod schedule_day;
pub mod ridership;

View file

@ -1,216 +0,0 @@
use std::collections::HashMap;
use libseptastic::direction::CardinalDirection;
use libseptastic::ridership::Ridership;
use sqlx::{Postgres, Transaction};
use crate::traits::{DbObj, FromSeptaJson, FromSeptaJsonAndStations};
use crate::septa_json;
impl DbObj<Ridership> for Ridership {
async fn create_table(tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
sqlx::query("
CREATE TABLE IF NOT EXISTS septa_ridership (
route_id TEXT NOT NULL,
stop_id bigint NOT NULL,
direction septa_direction_type,
exp_ons bigint,
exp_offs bigint,
ons bigint,
offs bigint,
year bigint,
PRIMARY KEY (route_id, stop_id, direction),
FOREIGN KEY (route_id) REFERENCES septa_routes(id) ON DELETE CASCADE,
FOREIGN KEY (stop_id) REFERENCES septa_stops(id) ON DELETE CASCADE
);
")
.execute(&mut **tx)
.await?;
Ok(())
}
async fn insert_many(dirs: Vec<Ridership>, tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
let mut route_ids: Vec<String> = Vec::new();
let mut stop_ids: Vec<i64> = Vec::new();
let mut directions: Vec<CardinalDirection> = Vec::new();
let mut exp_onss: Vec<i64> = Vec::new();
let mut exp_offss: Vec<i64> = Vec::new();
let mut onss: Vec<i64> = Vec::new();
let mut offss: Vec<i64> = Vec::new();
let mut years: Vec<i64> = Vec::new();
for dir in dirs {
route_ids.push(dir.route_id);
stop_ids.push(dir.stop_id);
directions.push(dir.direction);
exp_onss.push(dir.exp_ons);
exp_offss.push(dir.exp_offs);
onss.push(dir.ons);
offss.push(dir.offs);
years.push(dir.year);
}
sqlx::query("
INSERT INTO
septa_ridership
(
route_id,
stop_id,
direction,
exp_ons,
exp_offs,
ons,
offs,
year
)
SELECT * FROM UNNEST(
$1::text[],
$2::bigint[],
$3::septa_direction_type[],
$4::bigint[],
$5::bigint[],
$6::bigint[],
$7::bigint[],
$8::bigint[]
)
ON CONFLICT DO NOTHING
;
")
.bind(&route_ids[..])
.bind(&stop_ids[..])
.bind(&directions[..])
.bind(&exp_onss[..])
.bind(&exp_offss[..])
.bind(&onss[..])
.bind(&offss[..])
.bind(&years[..])
.execute(&mut **tx)
.await?;
Ok(())
}
async fn insert(&self, tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
Self::insert_many(vec![self.clone()], tx).await?;
Ok(())
}
}
impl FromSeptaJsonAndStations<septa_json::ridership::Ridership> for Ridership {
fn from_septa_json(json_rider: septa_json::ridership::Ridership, stop_map: &HashMap<String, i64>) -> ::anyhow::Result<Box<Ridership>> {
Ok(Box::new(Ridership {
route_id: match json_rider.route_id.as_str() {
// SEPTA Metro
"MFL" => "L1",
"BSL" => "B1",
"MFO" => "L1 OWL",
"BSO" => "B1 OWL",
"J" => "41",
"L" => "51",
"G" => "63",
"H" => "71",
"XH" => "81",
"R" => "82",
"101" => "D1",
"102" => "D2",
"15" => "G1",
"NHSL" => "M1",
"10" => "T1",
"34" => "T2",
"13" => "T3",
"11" => "T4",
"36" => "T5",
// Regional Rail Remap
"Airport" => "AIR",
"Chestnut Hill West" => "CHW",
"Chestnut Hill East" => "CHE",
"Cynwyd" => "CYN",
"Fox Chase" => "FOX",
"Lansdale/Doylestown" => "LAN",
"Media/Wawa" => "MED",
"Manyunk/Norristown" => "NOR",
"Paoli/Thorndale" => "PAO",
"Trenton" => "TRE",
"Warminster" => "WAR",
"Wilmington/Newark" => "WIL",
"West Trenton" => "WTR",
any => any
}.to_string(),
stop_id: {
if json_rider.stop_code != "" {
Ok(json_rider.stop_code.parse::<i64>()?)
} else {
if let Some(sid) = stop_map.get(&(match json_rider.stop_name.as_str() {
"Churchmans Crossing" => "Churchman's Crossing",
"Prospect Park" => "Prospect Park - Moore",
"Highland Ave." => "Highland Avenue",
"Chester TC" => "Chester Transit Center",
"University City" => "Penn Medicine Station",
"Wynnefield" => "Wynnefield Avenue",
"Temple" => "Temple University",
"Fern Rock" => "Fern Rock T C",
"Jenkintown" => "Jenkintown Wyncote",
"Cornwells Height" => "Cornwells Heights",
"Levittown" => "Levittown Station",
"9th Street" => "9th Street Lansdale",
"Del Val College" => "Delaware Valley College",
"Sedwick" => "Sedgwick",
"Allen Lane" => "Richard Allen Lane",
"Elm Street" => "Norristown - Elm Street",
"Norristown" => "Norristown Transit Center",
"Neshaminy Falls" => "Neshaminy",
"Moylan Rose Valley" => "Moylan-Rose Valley",
"Morton" => "Morton-Rutledge",
"Easwick" => "Eastwick",
"30th Street Station" => "Gray 30th Street",
"Holmesburg Jct" => "Holmesburg Junction",
any => any
}.to_string())) {
Ok(sid.clone())
} else {
Err(::anyhow::anyhow!("Station {} not found", json_rider.stop_name))
}
}
}?,
exp_ons: json_rider.exp_ons.parse()?,
exp_offs: json_rider.exp_offs.parse()?,
ons: json_rider.ons.parse()?,
offs: json_rider.offs.parse()?,
year: 2024, //FIXME FIXME! Actually parse
direction: match json_rider.direction.as_str() {
"Eastbound" => Ok(CardinalDirection::Eastbound),
"Westbound" => Ok(CardinalDirection::Westbound),
"Northbound" => Ok(CardinalDirection::Northbound),
"Southbound" => Ok(CardinalDirection::Southbound),
"Inbound" => Ok(CardinalDirection::Inbound),
"Outbound" => Ok(CardinalDirection::Outbound),
"Loop" => Ok(CardinalDirection::Loop),
"" => match json_rider.route_id.as_str() {
"AIR" => Ok(CardinalDirection::Inbound),
"CHE" => Ok(CardinalDirection::Outbound),
"CHW" => Ok(CardinalDirection::Inbound),
"CYN" => Ok(CardinalDirection::Inbound),
"FOX" => Ok(CardinalDirection::Outbound),
"LAN" => Ok(CardinalDirection::Outbound),
"NOR" => Ok(CardinalDirection::Outbound),
"MED" => Ok(CardinalDirection::Inbound),
"PAO" => Ok(CardinalDirection::Inbound),
"TRE" => Ok(CardinalDirection::Inbound),
"WAR" => Ok(CardinalDirection::Outbound),
"WTR" => Ok(CardinalDirection::Outbound),
"WIL" => Ok(CardinalDirection::Inbound),
_ => Err(anyhow::anyhow!("Bad dir value RR route id {}", json_rider.route_id))
},
_ => Err(
::anyhow::anyhow!(
"Unknown true direction {} for Route {}",
json_rider.direction,
json_rider.route_id
)
)
}?}))
}
}

View file

@ -1,187 +0,0 @@
use sqlx::{Postgres, Transaction};
use crate::traits::{DbObj, FromSeptaJson};
use crate::septa_json;
use libseptastic::route::{Route, RouteType, InterlinedRoute};
impl DbObj<Route> for Route {
async fn create_table(tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
sqlx::query("
CREATE TYPE septa_route_type AS ENUM (
'trolley',
'subway_elevated',
'regional_rail',
'bus',
'trackless_trolley'
);
")
.execute(&mut **tx)
.await?;
sqlx::query("
CREATE TYPE septa_direction_type AS ENUM (
'northbound',
'southbound',
'eastbound',
'westbound',
'inbound',
'outbound',
'loop'
);
")
.execute(&mut **tx)
.await?;
sqlx::query("
CREATE TABLE IF NOT EXISTS septa_routes (
id VARCHAR(8) PRIMARY KEY,
name VARCHAR(64) NOT NULL,
short_name VARCHAR(32) NOT NULL,
color_hex VARCHAR(6) NOT NULL,
route_type septa_route_type NOT NULL
);
")
.execute(&mut **tx)
.await?;
sqlx::query("
CREATE TABLE IF NOT EXISTS interlined_routes (
id VARCHAR(8) PRIMARY KEY,
name VARCHAR(64) NOT NULL
);
")
.execute(&mut **tx)
.await?;
sqlx::query("
CREATE TABLE IF NOT EXISTS interlined_routes_subroutes (
interline_id VARCHAR(8),
route_id VARCHAR(8),
FOREIGN KEY (interline_id) REFERENCES interlined_routes(id),
FOREIGN KEY (route_id) REFERENCES septa_routes(id)
);
")
.execute(&mut **tx)
.await?;
Ok(())
}
async fn insert_many(routes: Vec<Route>, tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
let mut names = Vec::new();
let mut short_names = Vec::new();
let mut color_hexes = Vec::new();
let mut route_types: Vec<RouteType> = Vec::new();
let mut ids = Vec::new();
for route in routes {
ids.push(route.id);
names.push(route.name);
short_names.push(route.short_name);
color_hexes.push(route.color_hex);
route_types.push(route.route_type);
}
sqlx::query("
INSERT INTO
septa_routes
(
id,
name,
short_name,
color_hex,
route_type
)
SELECT * FROM UNNEST(
$1::text[],
$2::text[],
$3::text[],
$4::text[],
$5::septa_route_type[]
);
")
.bind(&ids)
.bind(&names)
.bind(&short_names)
.bind(&color_hexes)
.bind(&route_types)
.execute(&mut **tx)
.await?;
let ir: InterlinedRoute = InterlinedRoute {
interline_id: String::from("B"),
interline_name: String::from("Broad Street Line"),
interlined_routes: vec![String::from("B1"), String::from("B2"), String::from("B3")]
};
sqlx::query("
INSERT INTO
interlined_routes
(
id,
name
)
VALUES
(
$1,
$2
);
")
.bind(&ir.interline_id)
.bind(&ir.interline_name)
.execute(&mut **tx)
.await?;
for route in ir.interlined_routes {
sqlx::query("
INSERT INTO
interlined_routes_subroutes
(
interline_id,
route_id
)
VALUES
(
$1,
$2
);
")
.bind(&ir.interline_id)
.bind(&route)
.execute(&mut **tx)
.await?;
}
Ok(())
}
async fn insert(&self, tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
Self::insert_many(vec![self.clone()], tx).await?;
Ok(())
}
}
impl FromSeptaJson<septa_json::route::RouteType> for RouteType {
fn from_septa_json(json_rt: septa_json::route::RouteType) -> ::anyhow::Result<Box<RouteType>> {
Ok(Box::new(match json_rt {
septa_json::route::RouteType::Trolley => RouteType::Trolley,
septa_json::route::RouteType::SubwayElevated => RouteType::SubwayElevated,
septa_json::route::RouteType::RegionalRail => RouteType::RegionalRail,
septa_json::route::RouteType::Bus => RouteType::Bus,
septa_json::route::RouteType::TracklessTrolley => RouteType::TracklessTrolley,
}))
}
}
impl FromSeptaJson<septa_json::route::Route> for Route {
fn from_septa_json(json_route: septa_json::route::Route) -> ::anyhow::Result<Box<Route>> {
Ok(Box::new(Route {
name: json_route.route_long_name,
short_name: json_route.route_short_name,
color_hex: json_route.route_color,
route_type: *RouteType::from_septa_json(json_route.route_type)?,
id: json_route.route_id,
// FIXME: Actually get direction
}))
}
}

View file

@ -1,78 +0,0 @@
use sqlx::{Postgres, Transaction};
use crate::traits::{DbObj, FromSeptaJson};
use crate::septa_json;
use libseptastic::route_stop::RouteStop;
impl DbObj<RouteStop> for RouteStop {
async fn create_table(tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
sqlx::query("
CREATE TABLE IF NOT EXISTS septa_route_stops (
route_id TEXT NOT NULL,
stop_id BIGINT NOT NULL,
direction_id BIGINT NOT NULL,
stop_sequence BIGINT NOT NULL,
PRIMARY KEY (route_id, stop_id, direction_id),
FOREIGN KEY (route_id) REFERENCES septa_routes(id) ON DELETE CASCADE,
FOREIGN KEY (stop_id) REFERENCES septa_stops(id) ON DELETE CASCADE
);
")
.execute(&mut **tx)
.await?;
Ok(())
}
async fn insert_many(rses: Vec<RouteStop>, tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
let mut route_ids: Vec<String> = Vec::new();
let mut stop_ids: Vec<i64> = Vec::new();
let mut direction_ids: Vec<i64> = Vec::new();
let mut stop_sequences: Vec<i64> = Vec::new();
for rs in rses {
route_ids.push(rs.route_id.clone());
stop_ids.push(rs.stop_id.clone());
direction_ids.push(rs.direction_id.clone());
stop_sequences.push(rs.stop_sequence.clone());
}
sqlx::query("
INSERT INTO
septa_route_stops
(
route_id,
stop_id,
direction_id,
stop_sequence
)
SELECT * FROM UNNEST($1::text[], $2::bigint[], $3::bigint[], $4::bigint[])
ON CONFLICT DO NOTHING
;
")
.bind(&route_ids[..])
.bind(&stop_ids[..])
.bind(&direction_ids[..])
.bind(&stop_sequences[..])
.execute(&mut **tx)
.await?;
Ok(())
}
async fn insert(&self, tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
Self::insert_many(vec![self.clone()], tx).await?;
Ok(())
}
}
impl FromSeptaJson<septa_json::route_stop::RouteStop> for RouteStop {
fn from_septa_json(json_stops: septa_json::route_stop::RouteStop) -> ::anyhow::Result<Box<RouteStop>> {
Ok(Box::new(RouteStop{
route_id: json_stops.route_id,
stop_id: json_stops.stop_id,
direction_id: json_stops.direction_id,
stop_sequence: json_stops.stop_sequence
}))
}
}

View file

@ -1,70 +0,0 @@
use sqlx::{Postgres, Transaction};
use crate::traits::{DbObj, FromSeptaJson};
use crate::septa_json;
use libseptastic::schedule_day::ScheduleDay;
impl DbObj<ScheduleDay> for ScheduleDay {
async fn create_table(tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
sqlx::query("
CREATE TABLE IF NOT EXISTS septa_schedule_days (
date TEXT NOT NULL,
service_id TEXT NOT NULL,
PRIMARY KEY (date, service_id)
);
")
.execute(&mut **tx)
.await?;
Ok(())
}
async fn insert_many(scheds: Vec<ScheduleDay>, tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
let mut dates: Vec<String> = Vec::new();
let mut service_ids: Vec<String> = Vec::new();
for sched in scheds {
dates.push(sched.date);
service_ids.push(sched.service_id);
}
sqlx::query("
INSERT INTO septa_schedule_days (
date,
service_id
)
SELECT * FROM UNNEST(
$1::text[],
$2::text[]
)
ON CONFLICT DO NOTHING
;
")
.bind(&dates[..])
.bind(&service_ids[..])
.execute(&mut **tx)
.await?;
Ok(())
}
async fn insert(&self, tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
Self::insert_many(vec![self.clone()], tx).await?;
Ok(())
}
}
impl FromSeptaJson<septa_json::schedule_day::Calendar> for Vec<ScheduleDay> {
fn from_septa_json(json_sched: septa_json::schedule_day::Calendar) -> ::anyhow::Result<Box<Vec<ScheduleDay>>> {
let mut res: Vec<ScheduleDay> = Vec::new();
for (day, sched) in json_sched {
for svc in sched.service_id {
res.push(ScheduleDay { date: day.clone(), service_id: svc });
}
}
Ok(Box::new(res))
}
}

View file

@ -1,108 +0,0 @@
use sqlx::{Postgres, Transaction};
use crate::traits::{DbObj, FromSeptaJson};
use crate::septa_json;
use libseptastic::stop::{Stop, StopType};
impl DbObj<Stop> for Stop {
async fn create_table(tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
sqlx::query("
CREATE TYPE septa_stop_type AS ENUM (
'far_side',
'middle_block_near_side',
'normal'
);
")
.execute(&mut **tx)
.await?;
sqlx::query("
CREATE TABLE IF NOT EXISTS septa_stops (
id BIGINT PRIMARY KEY,
name VARCHAR(128) NOT NULL,
lat DOUBLE PRECISION NOT NULL,
lng DOUBLE PRECISION NOT NULL,
stop_type septa_stop_type NOT NULL
);
")
.execute(&mut **tx)
.await?;
Ok(())
}
async fn insert_many(stations: Vec<Stop>, tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
let mut ids: Vec<i64> = Vec::new();
let mut names: Vec<String> = Vec::new();
let mut lats: Vec<f64> = Vec::new();
let mut lngs: Vec<f64> = Vec::new();
let mut stop_types: Vec<StopType> = Vec::new();
for station in stations {
ids.push(station.id);
names.push(station.name);
lats.push(station.lat);
lngs.push(station.lng);
stop_types.push(station.stop_type);
}
sqlx::query("
INSERT INTO
septa_stops
(
id,
name,
lat,
lng,
stop_type
)
SELECT * FROM UNNEST(
$1::bigint[],
$2::text[],
$3::double precision[],
$4::double precision[],
$5::septa_stop_type[]
)
ON CONFLICT DO NOTHING
;
")
.bind(&ids[..])
.bind(&names[..])
.bind(&lats[..])
.bind(&lngs[..])
.bind(&stop_types[..])
.execute(&mut **tx)
.await?;
Ok(())
}
async fn insert(&self, tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
Self::insert_many(vec![self.clone()], tx).await?;
Ok(())
}
}
impl FromSeptaJson<septa_json::route_stop::RouteStop> for Stop {
fn from_septa_json(json_station: septa_json::route_stop::RouteStop) -> ::anyhow::Result<Box<Stop>> {
let mut name = json_station.stop_name;
let mut stop_type = StopType::Normal;
if let Some(new_name) = name.strip_suffix("- MNBS") {
stop_type = StopType::MiddleBlockNearSide;
name = new_name.to_string();
}
if let Some(new_name) = name.strip_suffix("- FS") {
stop_type = StopType::FarSide;
name = new_name.to_string();
}
Ok(Box::new(Stop {
name,
id: json_station.stop_id,
lat: json_station.stop_lat,
lng: json_station.stop_lon,
stop_type
}))
}
}

View file

@ -1,127 +0,0 @@
use sqlx::{Postgres, Transaction};
use crate::traits::{DbObj, FromSeptaJson};
use crate::septa_json;
use libseptastic::stop_schedule::StopSchedule;
impl DbObj<StopSchedule> for StopSchedule {
async fn create_table(tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
sqlx::query("
CREATE TABLE IF NOT EXISTS septa_stop_schedules (
route_id TEXT NOT NULL,
trip_id TEXT NOT NULL,
service_id TEXT NOT NULL,
direction_id BIGINT NOT NULL,
arrival_time BIGINT NOT NULL,
stop_id BIGINT NOT NULL,
stop_sequence BIGINT NOT NULL,
PRIMARY KEY (trip_id, stop_id),
FOREIGN KEY (route_id) REFERENCES septa_routes(id) ON DELETE CASCADE,
FOREIGN KEY (stop_id) REFERENCES septa_stops(id) ON DELETE CASCADE
);
")
//FOREIGN KEY (route_id, direction_id) REFERENCES septa_directions(route_id, direction_id) ON DELETE CASCADE,
.execute(&mut **tx)
.await?;
sqlx::query("
CREATE INDEX septa_stop_schedule_trip_id_idx ON septa_stop_schedules (trip_id);
").execute(&mut **tx).await?;
sqlx::query("
CREATE INDEX septa_stop_schedule_service_id_idx ON septa_stop_schedules (service_id);
").execute(&mut **tx).await?;
Ok(())
}
async fn insert_many(scheds: Vec<StopSchedule>, tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
let mut route_ids: Vec<String> = Vec::new();
let mut trip_ids: Vec<String> = Vec::new();
let mut service_ids: Vec<String> = Vec::new();
let mut direction_ids: Vec<i64> = Vec::new();
let mut arrival_times: Vec<i64> = Vec::new();
let mut stop_ids: Vec<i64> = Vec::new();
let mut stop_sequences: Vec<i64> = Vec::new();
for sched in scheds {
route_ids.push(sched.route_id);
trip_ids.push(sched.trip_id);
service_ids.push(sched.service_id);
direction_ids.push(sched.direction_id);
arrival_times.push(sched.arrival_time);
stop_ids.push(sched.stop_id);
stop_sequences.push(sched.stop_sequence);
}
sqlx::query("
INSERT INTO septa_stop_schedules (
route_id,
trip_id,
service_id,
direction_id,
arrival_time,
stop_id,
stop_sequence
)
SELECT * FROM UNNEST(
$1::text[],
$2::text[],
$3::text[],
$4::bigint[],
$5::bigint[],
$6::bigint[],
$7::bigint[]
)
ON CONFLICT DO NOTHING
;
")
.bind(&route_ids[..])
.bind(&trip_ids[..])
.bind(&service_ids[..])
.bind(&direction_ids[..])
.bind(&arrival_times[..])
.bind(&stop_ids[..])
.bind(&stop_sequences[..])
.execute(&mut **tx)
.await?;
Ok(())
}
async fn insert(&self, tx: &mut Transaction<'_, Postgres>) -> anyhow::Result<()> {
Self::insert_many(vec![self.clone()], tx).await?;
Ok(())
}
}
impl FromSeptaJson<septa_json::stop_schedule::StopSchedule> for StopSchedule {
fn from_septa_json(json_sched: septa_json::stop_schedule::StopSchedule) -> ::anyhow::Result<Box<StopSchedule>> {
let time_parts: Vec<&str> = json_sched.arrival_time.split(":").collect();
let arrival_time: i64 = {
let hour: i64 = time_parts[0].parse::<i64>()?;
let minute: i64 = time_parts[1].parse::<i64>()?;
let second: i64 = time_parts[2].parse::<i64>()?;
(hour*3600) + (minute * 60) + second
};
Ok(Box::new(StopSchedule {
route_id: json_sched.route_id,
trip_id: match json_sched.trip_id{
septa_json::stop_schedule::TripId::RegionalRail(x) => x,
septa_json::stop_schedule::TripId::Other(y) => y.to_string()
},
stop_name: String::from(""),
service_id: json_sched.service_id,
// FIXME: Actually get direction
direction_id: json_sched.direction_id,
arrival_time,
stop_id: json_sched.stop_id,
stop_sequence: json_sched.stop_sequence
}))
}
}

View file

@ -1,19 +0,0 @@
use crate::traits::*;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct Direction {
#[serde(rename="Route")]
pub route: String,
#[serde(rename="DirectionDescription")]
pub direction_destination: String,
#[serde(rename="Direction")]
pub direction: String,
#[serde(rename="TrueDirection")]
pub true_direction: String,
#[serde(rename="Mode")]
pub mode: String
}
impl SeptaJson for Direction {}

View file

@ -1,6 +0,0 @@
pub mod route;
pub mod route_stop;
pub mod stop_schedule;
pub mod schedule_day;
pub mod direction;
pub mod ridership;

View file

@ -1,37 +0,0 @@
use crate::traits::*;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct Ridership {
#[serde(rename = "Stop ID")]
pub stop_id: Option<String>,
#[serde(rename = "Stop Code")]
pub stop_code: String,
#[serde(rename = "Stop")]
pub stop_name: String,
#[serde(rename = "Route")]
pub route_id: String,
#[serde(rename = "Direction")]
pub direction: String,
#[serde(rename = "Ons")]
pub ons: String,
#[serde(rename = "Offs")]
pub offs: String,
#[serde(rename = "Exp_Ons")]
pub exp_ons: String,
#[serde(rename = "Exp_Offs")]
pub exp_offs: String,
#[serde(rename = "Year")]
pub year: String
}
impl SeptaJson for Ridership {}

View file

@ -1,42 +0,0 @@
use crate::traits::*;
use serde_repr::*;
use serde::Deserialize;
#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug)]
#[repr(u8)]
pub enum RouteType {
Trolley= 0,
SubwayElevated = 1,
RegionalRail = 2,
Bus = 3,
TracklessTrolley = 11
}
#[derive(Debug, Deserialize)]
pub struct Document {
#[serde(rename = "type")]
pub doc_type: String,
pub title: String,
pub url: String,
pub link_label: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct Route {
pub release_name: String,
pub route_id: String,
pub route_short_name: String,
pub route_long_name: String,
pub route_type: RouteType,
pub route_color: String,
pub route_text_color: String,
pub route_frequency_text: String,
pub is_frequent_bus: bool,
pub route_color_dark: String,
pub route_color_text_dark: String,
pub documents: Vec<Document>
}
impl SeptaJson for Route {}
impl SeptaJson for RouteType {}

View file

@ -1,31 +0,0 @@
use crate::traits::*;
use serde::{self, Deserialize, Deserializer};
pub fn de_string_or_int<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
use serde_json::Value;
match Value::deserialize(deserializer)? {
Value::Number(n) => n.as_i64().ok_or_else(|| Error::custom("Invalid number")),
Value::String(s) => s.parse::<i64>().map_err(|_| Error::custom("Invalid string number")),
_ => Err(Error::custom("Expected a string or number")),
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct RouteStop {
pub route_id: String,
pub direction_id: i64,
#[serde(deserialize_with = "de_string_or_int")]
pub stop_id: i64,
pub stop_name: String,
pub stop_sequence: i64,
pub stop_lat: f64,
pub stop_lon: f64,
}
impl SeptaJson for RouteStop {}
impl SeptaJson for Vec<RouteStop> {}

View file

@ -1,18 +0,0 @@
use std::collections::{BTreeMap, HashMap, HashSet};
use crate::traits::*;
use serde::{Deserialize, Serialize};
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
pub struct ScheduleDay {
pub release_name: String,
pub special: bool,
pub service_id: Vec<String>,
pub service_added: Vec<String>,
pub service_removed: Vec<String>
}
pub type Calendar = BTreeMap<String, ScheduleDay>;
impl SeptaJson for Calendar {}

View file

@ -1,27 +0,0 @@
use crate::traits::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum TripId {
RegionalRail(String),
Other(i64)
}
#[derive(Debug, Clone, Deserialize)]
pub struct StopSchedule {
pub route_id: String,
pub block_id: i64,
pub release_name: String,
pub trip_id: TripId,
pub service_id: String,
pub trip_headsign: Option<String>,
pub direction_id: i64,
pub arrival_time: String,
pub stop_id: i64,
pub stop_name: String,
pub stop_sequence: i64,
pub last_stop_id: i64
}
impl SeptaJson for StopSchedule {}

View file

@ -1,23 +0,0 @@
use std::collections::HashMap;
use sqlx::{Postgres, Transaction};
pub trait SeptaJson {}
pub trait FromSeptaJson<T: SeptaJson> {
fn from_septa_json(septa_json: T) -> ::anyhow::Result<Box<Self>>;
}
pub trait FromSeptaJsonAndStations<T: SeptaJson> {
fn from_septa_json(septa_json: T, stop_map: &HashMap<String, i64>) -> ::anyhow::Result<Box<Self>>;
}
pub trait DbObj<T> {
fn create_table(tx: &mut Transaction<'_, Postgres>) -> impl std::future::Future< Output = ::anyhow::Result<()>> + Send;
fn insert(&self, tx: &mut Transaction<'_, Postgres>) -> impl std::future::Future< Output = ::anyhow::Result<()>> + Send;
fn insert_many(s: Vec<T>, tx: &mut Transaction<'_, Postgres>) -> impl std::future::Future< Output = ::anyhow::Result<()>> + Send;
}
pub trait Fetchable<T: SeptaJson> {}

View file

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(sqlx::Type, Serialize, Deserialize, PartialEq, Debug, Clone)] #[derive(sqlx::Type, Serialize, Deserialize, PartialEq, Debug, Clone, Copy, Eq)]
#[sqlx(type_name = "septa_direction_type", rename_all = "snake_case")] #[sqlx(type_name = "septa_direction_type", rename_all = "snake_case")]
pub enum CardinalDirection { pub enum CardinalDirection {
Northbound, Northbound,
@ -15,8 +15,6 @@ pub enum CardinalDirection {
#[derive(::sqlx::Decode, Serialize, Deserialize, Debug, Clone)] #[derive(::sqlx::Decode, Serialize, Deserialize, Debug, Clone)]
pub struct Direction { pub struct Direction {
pub route_id: String,
pub direction_id: i64,
pub direction: CardinalDirection, pub direction: CardinalDirection,
pub direction_destination: String pub direction_destination: String
} }

View file

@ -1,4 +1,5 @@
pub mod route; pub mod route;
pub mod agency;
pub mod stop; pub mod stop;
pub mod route_stop; pub mod route_stop;
pub mod stop_schedule; pub mod stop_schedule;

View file

@ -1,4 +1,6 @@
#[derive(sqlx::Type, PartialEq, Debug, Clone)] use serde::{Deserialize, Serialize};
#[derive(sqlx::Type, PartialEq, Debug, Clone, Serialize, Deserialize)]
#[sqlx(type_name = "septa_stop_type", rename_all = "snake_case")] #[sqlx(type_name = "septa_stop_type", rename_all = "snake_case")]
pub enum StopType { pub enum StopType {
FarSide, FarSide,
@ -6,7 +8,7 @@ pub enum StopType {
Normal Normal
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Stop { pub struct Stop {
pub id: i64, pub id: i64,
pub name: String, pub name: String,

View file

@ -1,22 +1,19 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::direction::Direction;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StopSchedule { pub struct StopSchedule {
pub route_id: String,
pub stop_name: String,
pub trip_id: String,
pub service_id: String,
pub direction_id: i64,
pub arrival_time: i64, pub arrival_time: i64,
pub stop_id: i64, pub stop_sequence: i64,
pub stop_sequence: i64 pub stop: crate::stop::Stop
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Trip { pub struct Trip {
pub route_id: String, pub service_id: String,
pub trip_id: String, pub trip_id: String,
pub direction_id: i64, pub direction: Direction,
pub tracking_data: TripTracking, pub tracking_data: TripTracking,
pub schedule: Vec<StopSchedule> pub schedule: Vec<StopSchedule>
} }
@ -33,5 +30,5 @@ pub struct LiveTrip {
pub delay: f64, pub delay: f64,
pub next_stop_id: Option<i64>, pub next_stop_id: Option<i64>,
pub timestamp: i64, pub timestamp: i64,
pub vehicle_id: Option<String> pub vehicle_ids: Vec<String>
} }

View file

@ -4,5 +4,6 @@ stdenv.mkDerivation {
nativeBuildInputs = [ pkg-config postgresql_14 ]; nativeBuildInputs = [ pkg-config postgresql_14 ];
buildInputs = [ buildInputs = [
cryptsetup cryptsetup
protobuf
]; ];
} }