From 3f68335eb4be7358674c675e7196dfd3300bad36 Mon Sep 17 00:00:00 2001 From: Nicholas Orlowsky Date: Sat, 21 Feb 2026 15:26:48 -0500 Subject: [PATCH] cleanup and filter support --- .direnv/nix-profile-25.11-sizirny50f893gx0 | 2 +- .direnv/nix-profile-25.11-sizirny50f893gx0.rc | 57 +- Dockerfile | 18 +- README.md | 62 ++ ...6137b576c37e76410d6845fcf9d3e91e98733.json | 65 -- ...f10857eb34c6a023ed2f17f1d2702348b006d.json | 59 -- ...5f25d1ff7486fcb266e7306de8c624090bc89.json | 57 -- ...4b6043cbda87c0b5ebb01ba74a6ebb430425c.json | 55 -- api/assets/test.html | 65 -- api/src/controllers/route.rs | 173 ----- api/src/controllers/stop.rs | 193 ------ api/src/main.rs | 122 ---- api/src/services/gtfs_pull.rs | 384 ----------- api/src/services/gtfs_rt.rs | 76 --- api/src/services/mod.rs | 3 - api/templates/index.html | 13 - api/templates/layout.html | 98 --- api/templates/route.html | 142 ---- api/templates/route_symbol.html | 19 - api/templates/routes.html | 60 -- api/templates/stop.html | 28 - api/templates/stop_table.html | 51 -- api/templates/stop_table_impl.html | 3 - api/templates/stops.html | 27 - example.env | 1 - libseptastic/src/agency.rs | 3 +- libseptastic/src/direction.rs | 13 +- libseptastic/src/lib.rs | 10 +- libseptastic/src/ridership.rs | 2 +- libseptastic/src/route.rs | 28 +- libseptastic/src/route_stop.rs | 2 +- libseptastic/src/schedule_day.rs | 2 +- libseptastic/src/stop.rs | 13 +- libseptastic/src/stop_schedule.rs | 131 +++- shell.nix | 9 +- {api => web}/.gitignore | 1 + {api => web}/Cargo.lock | 45 +- {api => web}/Cargo.toml | 5 +- {api => web}/assets/style.css | 8 +- {api => web}/config.yaml | 11 +- web/src/controllers/index.rs | 10 + {api => web}/src/controllers/mod.rs | 1 + web/src/controllers/route.rs | 137 ++++ web/src/controllers/stop.rs | 313 +++++++++ {api => web}/src/database.rs | 0 web/src/main.rs | 67 ++ {api => web}/src/routing.rs | 0 web/src/services/gtfs_pull.rs | 629 ++++++++++++++++++ web/src/services/mod.rs | 2 + {api => web}/src/services/trip_tracking.rs | 173 +++-- web/src/session_middleware.rs | 69 ++ {api => web}/src/templates.rs | 121 ++-- web/templates/index.html | 17 + web/templates/layout.html | 103 +++ web/templates/route.html | 138 ++++ web/templates/route_symbol.html | 13 + web/templates/routes.html | 92 +++ web/templates/stop.html | 139 ++++ web/templates/stop_search_results.html | 12 + web/templates/stop_table.html | 69 ++ web/templates/stop_table_impl.html | 3 + web/templates/stops.html | 41 ++ 62 files changed, 2364 insertions(+), 1901 deletions(-) create mode 100644 README.md delete mode 100644 api/.sqlx/query-1e1738a3256da7058ddaff950616137b576c37e76410d6845fcf9d3e91e98733.json delete mode 100644 api/.sqlx/query-4c9b19351cb8e0f6d0b3a45b699f10857eb34c6a023ed2f17f1d2702348b006d.json delete mode 100644 api/.sqlx/query-8479e45bc7bc3f4170102ffc78d5f25d1ff7486fcb266e7306de8c624090bc89.json delete mode 100644 api/.sqlx/query-cf4e35691a709fd362b53e3d1f64b6043cbda87c0b5ebb01ba74a6ebb430425c.json delete mode 100644 api/assets/test.html delete mode 100644 api/src/controllers/route.rs delete mode 100644 api/src/controllers/stop.rs delete mode 100644 api/src/main.rs delete mode 100644 api/src/services/gtfs_pull.rs delete mode 100644 api/src/services/gtfs_rt.rs delete mode 100644 api/src/services/mod.rs delete mode 100644 api/templates/index.html delete mode 100644 api/templates/layout.html delete mode 100644 api/templates/route.html delete mode 100644 api/templates/route_symbol.html delete mode 100644 api/templates/routes.html delete mode 100644 api/templates/stop.html delete mode 100644 api/templates/stop_table.html delete mode 100644 api/templates/stop_table_impl.html delete mode 100644 api/templates/stops.html delete mode 100644 example.env rename {api => web}/.gitignore (65%) rename {api => web}/Cargo.lock (99%) rename {api => web}/Cargo.toml (87%) rename {api => web}/assets/style.css (97%) rename {api => web}/config.yaml (82%) create mode 100644 web/src/controllers/index.rs rename {api => web}/src/controllers/mod.rs (65%) create mode 100644 web/src/controllers/route.rs create mode 100644 web/src/controllers/stop.rs rename {api => web}/src/database.rs (100%) create mode 100644 web/src/main.rs rename {api => web}/src/routing.rs (100%) create mode 100644 web/src/services/gtfs_pull.rs create mode 100644 web/src/services/mod.rs rename {api => web}/src/services/trip_tracking.rs (57%) create mode 100644 web/src/session_middleware.rs rename {api => web}/src/templates.rs (70%) create mode 100644 web/templates/index.html create mode 100644 web/templates/layout.html create mode 100644 web/templates/route.html create mode 100644 web/templates/route_symbol.html create mode 100644 web/templates/routes.html create mode 100644 web/templates/stop.html create mode 100644 web/templates/stop_search_results.html create mode 100644 web/templates/stop_table.html create mode 100644 web/templates/stop_table_impl.html create mode 100644 web/templates/stops.html diff --git a/.direnv/nix-profile-25.11-sizirny50f893gx0 b/.direnv/nix-profile-25.11-sizirny50f893gx0 index 458ba8c..c2e19a9 120000 --- a/.direnv/nix-profile-25.11-sizirny50f893gx0 +++ b/.direnv/nix-profile-25.11-sizirny50f893gx0 @@ -1 +1 @@ -/nix/store/jyg5pzxlxkbvzy1wb808kc5idmbij4r6-env-env \ No newline at end of file +/nix/store/vvk9w0m05l0yr8ahyib95sg4dmzm354c-env-env \ No newline at end of file diff --git a/.direnv/nix-profile-25.11-sizirny50f893gx0.rc b/.direnv/nix-profile-25.11-sizirny50f893gx0.rc index 99f4d9f..43a8ef2 100644 --- a/.direnv/nix-profile-25.11-sizirny50f893gx0.rc +++ b/.direnv/nix-profile-25.11-sizirny50f893gx0.rc @@ -14,6 +14,8 @@ CONFIG_SHELL='/nix/store/rlq03x4cwf8zn73hxaxnx0zn5q9kifls-bash-5.3p3/bin/bash' export CONFIG_SHELL CXX='g++' export CXX +DETERMINISTIC_BUILD='1' +export DETERMINISTIC_BUILD HOSTTYPE='x86_64' HOST_PATH='/nix/store/y2ngdv5xdfy5m6jrdgjn1r81rkqy41rh-lvm2-2.03.35-bin/bin:/nix/store/k0gl1zc7f5hk87lylxwbipb0b870bcmk-openssl-3.6.0-bin/bin:/nix/store/rf0crwiz4z45li0n31pqwczi49jg2kwj-util-linux-minimal-2.41.2-bin/bin:/nix/store/f03spphbp2fyq35pcbwxkivxr3av6874-libargon2-20190702/bin:/nix/store/1h477y97ws0m7qdyqdc6s2gj6flba3ha-cryptsetup-2.8.1-bin/bin:/nix/store/vpjk1z71ga010lyzn7f5abbjm4pnr7qn-protobuf-32.1/bin:/nix/store/imad8dvhp77h0pjbckp6wvmnyhp8dpgg-coreutils-9.8/bin:/nix/store/av4xw9f56xlx5pgv862wabfif6m1yc0a-findutils-4.10.0/bin:/nix/store/20axvl7mgj15m23jgmnq97hx37fgz7bk-diffutils-3.12/bin:/nix/store/drc7kang929jaza6cy9zdx10s4gw1z5p-gnused-4.9/bin:/nix/store/x3zjxxz8m4ki88axp0gn8q8m6bldybba-gnugrep-3.12/bin:/nix/store/y2wdhdcrffp9hnkzk06d178hq3g98jay-gawk-5.3.2/bin:/nix/store/yi3c5karhx764ham5rfwk7iynr8mjf6q-gnutar-1.35/bin:/nix/store/d471xb7sfbah076s8rx02i68zpxc2r5n-gzip-1.14/bin:/nix/store/qm9rxn2sc1vrz91i443rr6f0vxm0zd82-bzip2-1.0.8-bin/bin:/nix/store/3fmzbq9y4m9nk235il7scmvwn8j9zy3p-gnumake-4.4.1/bin:/nix/store/rlq03x4cwf8zn73hxaxnx0zn5q9kifls-bash-5.3p3/bin:/nix/store/qrwznp1ikdf0qw05wia2haiwi32ik5n0-patch-2.8/bin:/nix/store/v0rfdwhg6w6i0yb6dbry4srk6pnj3xp0-xz-5.8.1-bin/bin:/nix/store/paj6a1lpzp57hz1djm5bs86b7ci221r0-file-5.45/bin' export HOST_PATH @@ -35,13 +37,13 @@ NIX_CC='/nix/store/vr15iyyykg9zai6fpgvhcgyw7gckl78w-gcc-wrapper-14.3.0' export NIX_CC NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu='1' export NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu -NIX_CFLAGS_COMPILE=' -frandom-seed=jyg5pzxlxk -isystem /nix/store/f9qvdl02ckaiawxaqmb3w738g2j395p4-postgresql-14.20-dev/include -isystem /nix/store/9071ak7icm08rqjgw7raybxmhhp086wc-cryptsetup-2.8.1-dev/include -isystem /nix/store/fxxaan0lgr5yqs299sjfw0klwwd53313-lvm2-2.03.35-dev/include -isystem /nix/store/pzxlqc84603x27hibv6zq6giyar0rz0m-json-c-0.18-dev/include -isystem /nix/store/ydrckgnllgg8nmhdwni81h7xhcpnrlhd-openssl-3.6.0-dev/include -isystem /nix/store/57nxr75jbgqnxgbjsp4f6r9shyqzylm2-util-linux-minimal-2.41.2-dev/include -isystem /nix/store/g3prh2g2vsnfwrqp19cf1wnpchn5p00b-popt-1.19/include -isystem /nix/store/f03spphbp2fyq35pcbwxkivxr3av6874-libargon2-20190702/include -isystem /nix/store/vpjk1z71ga010lyzn7f5abbjm4pnr7qn-protobuf-32.1/include -isystem /nix/store/7qfvcajvjs89fxqk4379zhbdmlmxjaxb-abseil-cpp-20250814.1/include -isystem /nix/store/f9qvdl02ckaiawxaqmb3w738g2j395p4-postgresql-14.20-dev/include -isystem /nix/store/9071ak7icm08rqjgw7raybxmhhp086wc-cryptsetup-2.8.1-dev/include -isystem /nix/store/fxxaan0lgr5yqs299sjfw0klwwd53313-lvm2-2.03.35-dev/include -isystem /nix/store/pzxlqc84603x27hibv6zq6giyar0rz0m-json-c-0.18-dev/include -isystem /nix/store/ydrckgnllgg8nmhdwni81h7xhcpnrlhd-openssl-3.6.0-dev/include -isystem /nix/store/57nxr75jbgqnxgbjsp4f6r9shyqzylm2-util-linux-minimal-2.41.2-dev/include -isystem /nix/store/g3prh2g2vsnfwrqp19cf1wnpchn5p00b-popt-1.19/include -isystem /nix/store/f03spphbp2fyq35pcbwxkivxr3av6874-libargon2-20190702/include -isystem /nix/store/vpjk1z71ga010lyzn7f5abbjm4pnr7qn-protobuf-32.1/include -isystem /nix/store/7qfvcajvjs89fxqk4379zhbdmlmxjaxb-abseil-cpp-20250814.1/include' +NIX_CFLAGS_COMPILE=' -frandom-seed=vvk9w0m05l -isystem /nix/store/f9qvdl02ckaiawxaqmb3w738g2j395p4-postgresql-14.20-dev/include -isystem /nix/store/jj6jldlw37r8yy9kc1smrax9dhnjm2x4-python3-3.13.9/include -isystem /nix/store/9071ak7icm08rqjgw7raybxmhhp086wc-cryptsetup-2.8.1-dev/include -isystem /nix/store/fxxaan0lgr5yqs299sjfw0klwwd53313-lvm2-2.03.35-dev/include -isystem /nix/store/pzxlqc84603x27hibv6zq6giyar0rz0m-json-c-0.18-dev/include -isystem /nix/store/ydrckgnllgg8nmhdwni81h7xhcpnrlhd-openssl-3.6.0-dev/include -isystem /nix/store/57nxr75jbgqnxgbjsp4f6r9shyqzylm2-util-linux-minimal-2.41.2-dev/include -isystem /nix/store/g3prh2g2vsnfwrqp19cf1wnpchn5p00b-popt-1.19/include -isystem /nix/store/f03spphbp2fyq35pcbwxkivxr3av6874-libargon2-20190702/include -isystem /nix/store/vpjk1z71ga010lyzn7f5abbjm4pnr7qn-protobuf-32.1/include -isystem /nix/store/7qfvcajvjs89fxqk4379zhbdmlmxjaxb-abseil-cpp-20250814.1/include -isystem /nix/store/f9qvdl02ckaiawxaqmb3w738g2j395p4-postgresql-14.20-dev/include -isystem /nix/store/jj6jldlw37r8yy9kc1smrax9dhnjm2x4-python3-3.13.9/include -isystem /nix/store/9071ak7icm08rqjgw7raybxmhhp086wc-cryptsetup-2.8.1-dev/include -isystem /nix/store/fxxaan0lgr5yqs299sjfw0klwwd53313-lvm2-2.03.35-dev/include -isystem /nix/store/pzxlqc84603x27hibv6zq6giyar0rz0m-json-c-0.18-dev/include -isystem /nix/store/ydrckgnllgg8nmhdwni81h7xhcpnrlhd-openssl-3.6.0-dev/include -isystem /nix/store/57nxr75jbgqnxgbjsp4f6r9shyqzylm2-util-linux-minimal-2.41.2-dev/include -isystem /nix/store/g3prh2g2vsnfwrqp19cf1wnpchn5p00b-popt-1.19/include -isystem /nix/store/f03spphbp2fyq35pcbwxkivxr3av6874-libargon2-20190702/include -isystem /nix/store/vpjk1z71ga010lyzn7f5abbjm4pnr7qn-protobuf-32.1/include -isystem /nix/store/7qfvcajvjs89fxqk4379zhbdmlmxjaxb-abseil-cpp-20250814.1/include' export NIX_CFLAGS_COMPILE NIX_ENFORCE_NO_NATIVE='1' export NIX_ENFORCE_NO_NATIVE NIX_HARDENING_ENABLE='bindnow format fortify fortify3 libcxxhardeningextensive libcxxhardeningfast pic relro stackclashprotection stackprotector strictoverflow zerocallusedregs' export NIX_HARDENING_ENABLE -NIX_LDFLAGS='-rpath /home/nickorlow/programming/septastic/api/outputs/out/lib -L/nix/store/f9qvdl02ckaiawxaqmb3w738g2j395p4-postgresql-14.20-dev/lib -L/nix/store/9l82bx0yp6q4rk2sdkjr3r8zvyghz0mh-postgresql-14.20-lib/lib -L/nix/store/p48k53gf4mskvxrfahajaczk6m3isfrm-postgresql-14.20/lib -L/nix/store/0s06zawwhmqq3vdf39bi94lxwdyvg705-lvm2-2.03.35-lib/lib -L/nix/store/64q3424klqaq5bq409nrmjmiyrs04k2a-json-c-0.18/lib -L/nix/store/61i74yjkj9p1qphfl7018ja4sdwkipx0-openssl-3.6.0/lib -L/nix/store/z4wx1a8n24fxfl4rjpf0jg8cmp5b76b5-util-linux-minimal-2.41.2-lib/lib -L/nix/store/g3prh2g2vsnfwrqp19cf1wnpchn5p00b-popt-1.19/lib -L/nix/store/f03spphbp2fyq35pcbwxkivxr3av6874-libargon2-20190702/lib -L/nix/store/c7rsnyvdb0zwhj8fb9hnxpfw7nfshfxb-cryptsetup-2.8.1/lib -L/nix/store/vpjk1z71ga010lyzn7f5abbjm4pnr7qn-protobuf-32.1/lib -L/nix/store/7qfvcajvjs89fxqk4379zhbdmlmxjaxb-abseil-cpp-20250814.1/lib -L/nix/store/f9qvdl02ckaiawxaqmb3w738g2j395p4-postgresql-14.20-dev/lib -L/nix/store/9l82bx0yp6q4rk2sdkjr3r8zvyghz0mh-postgresql-14.20-lib/lib -L/nix/store/p48k53gf4mskvxrfahajaczk6m3isfrm-postgresql-14.20/lib -L/nix/store/0s06zawwhmqq3vdf39bi94lxwdyvg705-lvm2-2.03.35-lib/lib -L/nix/store/64q3424klqaq5bq409nrmjmiyrs04k2a-json-c-0.18/lib -L/nix/store/61i74yjkj9p1qphfl7018ja4sdwkipx0-openssl-3.6.0/lib -L/nix/store/z4wx1a8n24fxfl4rjpf0jg8cmp5b76b5-util-linux-minimal-2.41.2-lib/lib -L/nix/store/g3prh2g2vsnfwrqp19cf1wnpchn5p00b-popt-1.19/lib -L/nix/store/f03spphbp2fyq35pcbwxkivxr3av6874-libargon2-20190702/lib -L/nix/store/c7rsnyvdb0zwhj8fb9hnxpfw7nfshfxb-cryptsetup-2.8.1/lib -L/nix/store/vpjk1z71ga010lyzn7f5abbjm4pnr7qn-protobuf-32.1/lib -L/nix/store/7qfvcajvjs89fxqk4379zhbdmlmxjaxb-abseil-cpp-20250814.1/lib' +NIX_LDFLAGS='-rpath /home/nickorlow/programming/septastic/api/outputs/out/lib -L/nix/store/f9qvdl02ckaiawxaqmb3w738g2j395p4-postgresql-14.20-dev/lib -L/nix/store/9l82bx0yp6q4rk2sdkjr3r8zvyghz0mh-postgresql-14.20-lib/lib -L/nix/store/p48k53gf4mskvxrfahajaczk6m3isfrm-postgresql-14.20/lib -L/nix/store/jj6jldlw37r8yy9kc1smrax9dhnjm2x4-python3-3.13.9/lib -L/nix/store/0s06zawwhmqq3vdf39bi94lxwdyvg705-lvm2-2.03.35-lib/lib -L/nix/store/64q3424klqaq5bq409nrmjmiyrs04k2a-json-c-0.18/lib -L/nix/store/61i74yjkj9p1qphfl7018ja4sdwkipx0-openssl-3.6.0/lib -L/nix/store/z4wx1a8n24fxfl4rjpf0jg8cmp5b76b5-util-linux-minimal-2.41.2-lib/lib -L/nix/store/g3prh2g2vsnfwrqp19cf1wnpchn5p00b-popt-1.19/lib -L/nix/store/f03spphbp2fyq35pcbwxkivxr3av6874-libargon2-20190702/lib -L/nix/store/c7rsnyvdb0zwhj8fb9hnxpfw7nfshfxb-cryptsetup-2.8.1/lib -L/nix/store/vpjk1z71ga010lyzn7f5abbjm4pnr7qn-protobuf-32.1/lib -L/nix/store/7qfvcajvjs89fxqk4379zhbdmlmxjaxb-abseil-cpp-20250814.1/lib -L/nix/store/f9qvdl02ckaiawxaqmb3w738g2j395p4-postgresql-14.20-dev/lib -L/nix/store/9l82bx0yp6q4rk2sdkjr3r8zvyghz0mh-postgresql-14.20-lib/lib -L/nix/store/p48k53gf4mskvxrfahajaczk6m3isfrm-postgresql-14.20/lib -L/nix/store/jj6jldlw37r8yy9kc1smrax9dhnjm2x4-python3-3.13.9/lib -L/nix/store/0s06zawwhmqq3vdf39bi94lxwdyvg705-lvm2-2.03.35-lib/lib -L/nix/store/64q3424klqaq5bq409nrmjmiyrs04k2a-json-c-0.18/lib -L/nix/store/61i74yjkj9p1qphfl7018ja4sdwkipx0-openssl-3.6.0/lib -L/nix/store/z4wx1a8n24fxfl4rjpf0jg8cmp5b76b5-util-linux-minimal-2.41.2-lib/lib -L/nix/store/g3prh2g2vsnfwrqp19cf1wnpchn5p00b-popt-1.19/lib -L/nix/store/f03spphbp2fyq35pcbwxkivxr3av6874-libargon2-20190702/lib -L/nix/store/c7rsnyvdb0zwhj8fb9hnxpfw7nfshfxb-cryptsetup-2.8.1/lib -L/nix/store/vpjk1z71ga010lyzn7f5abbjm4pnr7qn-protobuf-32.1/lib -L/nix/store/7qfvcajvjs89fxqk4379zhbdmlmxjaxb-abseil-cpp-20250814.1/lib' export NIX_LDFLAGS NIX_NO_SELF_RPATH='1' NIX_PKG_CONFIG_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu='1' @@ -58,13 +60,19 @@ OLDPWD='' export OLDPWD OPTERR='1' OSTYPE='linux-gnu' -PATH='/nix/store/8vdiwpbh0g4avsd6x5v4s0di32vcl3dp-pkg-config-wrapper-0.29.2/bin:/nix/store/f9qvdl02ckaiawxaqmb3w738g2j395p4-postgresql-14.20-dev/bin:/nix/store/p48k53gf4mskvxrfahajaczk6m3isfrm-postgresql-14.20/bin:/nix/store/8q2582rd22xp8jlcg1xn1w219q5lx5xa-patchelf-0.15.2/bin:/nix/store/vr15iyyykg9zai6fpgvhcgyw7gckl78w-gcc-wrapper-14.3.0/bin:/nix/store/kzq78n13l8w24jn8bx4djj79k5j717f1-gcc-14.3.0/bin:/nix/store/q6wgv06q39bfhx2xl8ysc05wi6m2zdss-glibc-2.40-66-bin/bin:/nix/store/imad8dvhp77h0pjbckp6wvmnyhp8dpgg-coreutils-9.8/bin:/nix/store/xwydcyvlsa3cvssk0y5llgdhlhjvmqdm-binutils-wrapper-2.44/bin:/nix/store/dc9vaz50jg7mibk9xvqw5dqv89cxzla3-binutils-2.44/bin:/nix/store/y2ngdv5xdfy5m6jrdgjn1r81rkqy41rh-lvm2-2.03.35-bin/bin:/nix/store/k0gl1zc7f5hk87lylxwbipb0b870bcmk-openssl-3.6.0-bin/bin:/nix/store/rf0crwiz4z45li0n31pqwczi49jg2kwj-util-linux-minimal-2.41.2-bin/bin:/nix/store/f03spphbp2fyq35pcbwxkivxr3av6874-libargon2-20190702/bin:/nix/store/1h477y97ws0m7qdyqdc6s2gj6flba3ha-cryptsetup-2.8.1-bin/bin:/nix/store/vpjk1z71ga010lyzn7f5abbjm4pnr7qn-protobuf-32.1/bin:/nix/store/imad8dvhp77h0pjbckp6wvmnyhp8dpgg-coreutils-9.8/bin:/nix/store/av4xw9f56xlx5pgv862wabfif6m1yc0a-findutils-4.10.0/bin:/nix/store/20axvl7mgj15m23jgmnq97hx37fgz7bk-diffutils-3.12/bin:/nix/store/drc7kang929jaza6cy9zdx10s4gw1z5p-gnused-4.9/bin:/nix/store/x3zjxxz8m4ki88axp0gn8q8m6bldybba-gnugrep-3.12/bin:/nix/store/y2wdhdcrffp9hnkzk06d178hq3g98jay-gawk-5.3.2/bin:/nix/store/yi3c5karhx764ham5rfwk7iynr8mjf6q-gnutar-1.35/bin:/nix/store/d471xb7sfbah076s8rx02i68zpxc2r5n-gzip-1.14/bin:/nix/store/qm9rxn2sc1vrz91i443rr6f0vxm0zd82-bzip2-1.0.8-bin/bin:/nix/store/3fmzbq9y4m9nk235il7scmvwn8j9zy3p-gnumake-4.4.1/bin:/nix/store/rlq03x4cwf8zn73hxaxnx0zn5q9kifls-bash-5.3p3/bin:/nix/store/qrwznp1ikdf0qw05wia2haiwi32ik5n0-patch-2.8/bin:/nix/store/v0rfdwhg6w6i0yb6dbry4srk6pnj3xp0-xz-5.8.1-bin/bin:/nix/store/paj6a1lpzp57hz1djm5bs86b7ci221r0-file-5.45/bin' +PATH='/nix/store/8vdiwpbh0g4avsd6x5v4s0di32vcl3dp-pkg-config-wrapper-0.29.2/bin:/nix/store/f9qvdl02ckaiawxaqmb3w738g2j395p4-postgresql-14.20-dev/bin:/nix/store/p48k53gf4mskvxrfahajaczk6m3isfrm-postgresql-14.20/bin:/nix/store/ah9sslqrl111k74wxd5mdjswza9zvwv6-rustfmt-1.91.1/bin:/nix/store/l914ycms28s9if296ygwj7aaflpa1psy-cargo-1.91.1/bin:/nix/store/kqmpsqbrf6r0g9kq1sqa0pd5r8lr12m1-djlint-1.36.4/bin:/nix/store/jj6jldlw37r8yy9kc1smrax9dhnjm2x4-python3-3.13.9/bin:/nix/store/zdg5imhib6a1kll2mdzpxsnjk6ifv3r2-python3.13-cssbeautifier-1.15.4/bin:/nix/store/b9ldwlzwk6jmsmgvh0rfrwg1rxmirzrn-python3.13-editorconfig-0.17.1/bin:/nix/store/czax2jpa46yr9dkbj5cng270c89sqzz4-python3.13-jsbeautifier-1.15.4/bin:/nix/store/6xvl7wswdiq57lsq26y8dal6hpvmsbsj-python3.13-json5-0.12.0/bin:/nix/store/in46mljcw89s7jp8wycfap7zgzsybjya-python3.13-tqdm-4.67.1/bin:/nix/store/8q2582rd22xp8jlcg1xn1w219q5lx5xa-patchelf-0.15.2/bin:/nix/store/vr15iyyykg9zai6fpgvhcgyw7gckl78w-gcc-wrapper-14.3.0/bin:/nix/store/kzq78n13l8w24jn8bx4djj79k5j717f1-gcc-14.3.0/bin:/nix/store/q6wgv06q39bfhx2xl8ysc05wi6m2zdss-glibc-2.40-66-bin/bin:/nix/store/imad8dvhp77h0pjbckp6wvmnyhp8dpgg-coreutils-9.8/bin:/nix/store/xwydcyvlsa3cvssk0y5llgdhlhjvmqdm-binutils-wrapper-2.44/bin:/nix/store/dc9vaz50jg7mibk9xvqw5dqv89cxzla3-binutils-2.44/bin:/nix/store/y2ngdv5xdfy5m6jrdgjn1r81rkqy41rh-lvm2-2.03.35-bin/bin:/nix/store/k0gl1zc7f5hk87lylxwbipb0b870bcmk-openssl-3.6.0-bin/bin:/nix/store/rf0crwiz4z45li0n31pqwczi49jg2kwj-util-linux-minimal-2.41.2-bin/bin:/nix/store/f03spphbp2fyq35pcbwxkivxr3av6874-libargon2-20190702/bin:/nix/store/1h477y97ws0m7qdyqdc6s2gj6flba3ha-cryptsetup-2.8.1-bin/bin:/nix/store/vpjk1z71ga010lyzn7f5abbjm4pnr7qn-protobuf-32.1/bin:/nix/store/imad8dvhp77h0pjbckp6wvmnyhp8dpgg-coreutils-9.8/bin:/nix/store/av4xw9f56xlx5pgv862wabfif6m1yc0a-findutils-4.10.0/bin:/nix/store/20axvl7mgj15m23jgmnq97hx37fgz7bk-diffutils-3.12/bin:/nix/store/drc7kang929jaza6cy9zdx10s4gw1z5p-gnused-4.9/bin:/nix/store/x3zjxxz8m4ki88axp0gn8q8m6bldybba-gnugrep-3.12/bin:/nix/store/y2wdhdcrffp9hnkzk06d178hq3g98jay-gawk-5.3.2/bin:/nix/store/yi3c5karhx764ham5rfwk7iynr8mjf6q-gnutar-1.35/bin:/nix/store/d471xb7sfbah076s8rx02i68zpxc2r5n-gzip-1.14/bin:/nix/store/qm9rxn2sc1vrz91i443rr6f0vxm0zd82-bzip2-1.0.8-bin/bin:/nix/store/3fmzbq9y4m9nk235il7scmvwn8j9zy3p-gnumake-4.4.1/bin:/nix/store/rlq03x4cwf8zn73hxaxnx0zn5q9kifls-bash-5.3p3/bin:/nix/store/qrwznp1ikdf0qw05wia2haiwi32ik5n0-patch-2.8/bin:/nix/store/v0rfdwhg6w6i0yb6dbry4srk6pnj3xp0-xz-5.8.1-bin/bin:/nix/store/paj6a1lpzp57hz1djm5bs86b7ci221r0-file-5.45/bin' export PATH PKG_CONFIG='pkg-config' export PKG_CONFIG -PKG_CONFIG_PATH='/nix/store/f9qvdl02ckaiawxaqmb3w738g2j395p4-postgresql-14.20-dev/lib/pkgconfig:/nix/store/9071ak7icm08rqjgw7raybxmhhp086wc-cryptsetup-2.8.1-dev/lib/pkgconfig:/nix/store/fxxaan0lgr5yqs299sjfw0klwwd53313-lvm2-2.03.35-dev/lib/pkgconfig:/nix/store/pzxlqc84603x27hibv6zq6giyar0rz0m-json-c-0.18-dev/lib/pkgconfig:/nix/store/ydrckgnllgg8nmhdwni81h7xhcpnrlhd-openssl-3.6.0-dev/lib/pkgconfig:/nix/store/57nxr75jbgqnxgbjsp4f6r9shyqzylm2-util-linux-minimal-2.41.2-dev/lib/pkgconfig:/nix/store/g3prh2g2vsnfwrqp19cf1wnpchn5p00b-popt-1.19/lib/pkgconfig:/nix/store/f03spphbp2fyq35pcbwxkivxr3av6874-libargon2-20190702/lib/pkgconfig:/nix/store/vpjk1z71ga010lyzn7f5abbjm4pnr7qn-protobuf-32.1/lib/pkgconfig:/nix/store/7qfvcajvjs89fxqk4379zhbdmlmxjaxb-abseil-cpp-20250814.1/lib/pkgconfig' +PKG_CONFIG_PATH='/nix/store/f9qvdl02ckaiawxaqmb3w738g2j395p4-postgresql-14.20-dev/lib/pkgconfig:/nix/store/jj6jldlw37r8yy9kc1smrax9dhnjm2x4-python3-3.13.9/lib/pkgconfig:/nix/store/9071ak7icm08rqjgw7raybxmhhp086wc-cryptsetup-2.8.1-dev/lib/pkgconfig:/nix/store/fxxaan0lgr5yqs299sjfw0klwwd53313-lvm2-2.03.35-dev/lib/pkgconfig:/nix/store/pzxlqc84603x27hibv6zq6giyar0rz0m-json-c-0.18-dev/lib/pkgconfig:/nix/store/ydrckgnllgg8nmhdwni81h7xhcpnrlhd-openssl-3.6.0-dev/lib/pkgconfig:/nix/store/57nxr75jbgqnxgbjsp4f6r9shyqzylm2-util-linux-minimal-2.41.2-dev/lib/pkgconfig:/nix/store/g3prh2g2vsnfwrqp19cf1wnpchn5p00b-popt-1.19/lib/pkgconfig:/nix/store/f03spphbp2fyq35pcbwxkivxr3av6874-libargon2-20190702/lib/pkgconfig:/nix/store/vpjk1z71ga010lyzn7f5abbjm4pnr7qn-protobuf-32.1/lib/pkgconfig:/nix/store/7qfvcajvjs89fxqk4379zhbdmlmxjaxb-abseil-cpp-20250814.1/lib/pkgconfig' export PKG_CONFIG_PATH PS4='+ ' +PYTHONHASHSEED='0' +export PYTHONHASHSEED +PYTHONNOUSERSITE='1' +export PYTHONNOUSERSITE +PYTHONPATH='/nix/store/kqmpsqbrf6r0g9kq1sqa0pd5r8lr12m1-djlint-1.36.4/lib/python3.13/site-packages:/nix/store/p352iawg0q3q18ajklyrih6k46nhfz11-python3.13-click-8.2.1/lib/python3.13/site-packages:/nix/store/jj6jldlw37r8yy9kc1smrax9dhnjm2x4-python3-3.13.9/lib/python3.13/site-packages:/nix/store/3bws72fgyh893p03y7s4gyg1wyyfj2gn-python3.13-colorama-0.4.6/lib/python3.13/site-packages:/nix/store/zdg5imhib6a1kll2mdzpxsnjk6ifv3r2-python3.13-cssbeautifier-1.15.4/lib/python3.13/site-packages:/nix/store/b9ldwlzwk6jmsmgvh0rfrwg1rxmirzrn-python3.13-editorconfig-0.17.1/lib/python3.13/site-packages:/nix/store/czax2jpa46yr9dkbj5cng270c89sqzz4-python3.13-jsbeautifier-1.15.4/lib/python3.13/site-packages:/nix/store/32fdhi7k6nvrbj2blzhkq8ga5zq22wch-python3.13-six-1.17.0/lib/python3.13/site-packages:/nix/store/6xvl7wswdiq57lsq26y8dal6hpvmsbsj-python3.13-json5-0.12.0/lib/python3.13/site-packages:/nix/store/sxbna9s12fb3cds258n95bs8zdjlp0iv-python3.13-pathspec-0.12.1/lib/python3.13/site-packages:/nix/store/h7a9751dg6hj9zczx3zf7xb3wa522pzl-python3.13-pyyaml-6.0.3/lib/python3.13/site-packages:/nix/store/206sd795rz2b42p2vhkszv4zziy77087-python3.13-regex-2025.9.18/lib/python3.13/site-packages:/nix/store/31fval0xlhyd16jz8j6svb1wb8vvq4di-python3.13-tomli-2.2.1/lib/python3.13/site-packages:/nix/store/in46mljcw89s7jp8wycfap7zgzsybjya-python3.13-tqdm-4.67.1/lib/python3.13/site-packages' +export PYTHONPATH RANLIB='ranlib' export RANLIB READELF='readelf' @@ -79,8 +87,12 @@ STRINGS='strings' export STRINGS STRIP='strip' export STRIP -XDG_DATA_DIRS='/nix/store/8vdiwpbh0g4avsd6x5v4s0di32vcl3dp-pkg-config-wrapper-0.29.2/share:/nix/store/p48k53gf4mskvxrfahajaczk6m3isfrm-postgresql-14.20/share:/nix/store/8q2582rd22xp8jlcg1xn1w219q5lx5xa-patchelf-0.15.2/share' +XDG_DATA_DIRS='/nix/store/8vdiwpbh0g4avsd6x5v4s0di32vcl3dp-pkg-config-wrapper-0.29.2/share:/nix/store/p48k53gf4mskvxrfahajaczk6m3isfrm-postgresql-14.20/share:/nix/store/l914ycms28s9if296ygwj7aaflpa1psy-cargo-1.91.1/share:/nix/store/jj6jldlw37r8yy9kc1smrax9dhnjm2x4-python3-3.13.9/share:/nix/store/8q2582rd22xp8jlcg1xn1w219q5lx5xa-patchelf-0.15.2/share' export XDG_DATA_DIRS +_PYTHON_HOST_PLATFORM='linux-x86_64' +export _PYTHON_HOST_PLATFORM +_PYTHON_SYSCONFIGDATA_NAME='_sysconfigdata__linux_x86_64-linux-gnu' +export _PYTHON_SYSCONFIGDATA_NAME __structuredAttrs='' export __structuredAttrs _substituteStream_has_warned_replace_deprecation='false' @@ -116,9 +128,9 @@ doInstallCheck='' export doInstallCheck dontAddDisableDepTrack='1' export dontAddDisableDepTrack -declare -a envBuildBuildHooks=() -declare -a envBuildHostHooks=() -declare -a envBuildTargetHooks=() +declare -a envBuildBuildHooks=('addPythonPath' 'sysconfigdataHook' ) +declare -a envBuildHostHooks=('addPythonPath' 'sysconfigdataHook' ) +declare -a envBuildTargetHooks=('addPythonPath' 'sysconfigdataHook' ) declare -a envHostHostHooks=('pkgConfigWrapper_addPkgConfigPath' 'ccWrapper_addCVars' 'bintoolsWrapper_addLDVars' ) declare -a envHostTargetHooks=('pkgConfigWrapper_addPkgConfigPath' 'ccWrapper_addCVars' 'bintoolsWrapper_addLDVars' ) declare -a envTargetTargetHooks=() @@ -128,7 +140,7 @@ mesonFlags='' export mesonFlags name='env-env' export name -nativeBuildInputs='/nix/store/8vdiwpbh0g4avsd6x5v4s0di32vcl3dp-pkg-config-wrapper-0.29.2 /nix/store/f9qvdl02ckaiawxaqmb3w738g2j395p4-postgresql-14.20-dev' +nativeBuildInputs='/nix/store/8vdiwpbh0g4avsd6x5v4s0di32vcl3dp-pkg-config-wrapper-0.29.2 /nix/store/f9qvdl02ckaiawxaqmb3w738g2j395p4-postgresql-14.20-dev /nix/store/ah9sslqrl111k74wxd5mdjswza9zvwv6-rustfmt-1.91.1 /nix/store/l914ycms28s9if296ygwj7aaflpa1psy-cargo-1.91.1 /nix/store/kqmpsqbrf6r0g9kq1sqa0pd5r8lr12m1-djlint-1.36.4' export nativeBuildInputs out='/home/nickorlow/programming/septastic/api/outputs/out' export out @@ -147,7 +159,7 @@ patches='' export patches pkg='/nix/store/vr15iyyykg9zai6fpgvhcgyw7gckl78w-gcc-wrapper-14.3.0' declare -a pkgsBuildBuild=() -declare -a pkgsBuildHost=('/nix/store/8vdiwpbh0g4avsd6x5v4s0di32vcl3dp-pkg-config-wrapper-0.29.2' '/nix/store/f9qvdl02ckaiawxaqmb3w738g2j395p4-postgresql-14.20-dev' '/nix/store/9l82bx0yp6q4rk2sdkjr3r8zvyghz0mh-postgresql-14.20-lib' '/nix/store/p48k53gf4mskvxrfahajaczk6m3isfrm-postgresql-14.20' '/nix/store/8q2582rd22xp8jlcg1xn1w219q5lx5xa-patchelf-0.15.2' '/nix/store/l2xk4ac1wx9c95kpp8vymv9r9yn57fvh-update-autotools-gnu-config-scripts-hook' '/nix/store/0y5xmdb7qfvimjwbq7ibg1xdgkgjwqng-no-broken-symlinks.sh' '/nix/store/cv1d7p48379km6a85h4zp6kr86brh32q-audit-tmpdir.sh' '/nix/store/85clx3b0xkdf58jn161iy80y5223ilbi-compress-man-pages.sh' '/nix/store/wgrbkkaldkrlrni33ccvm3b6vbxzb656-make-symlinks-relative.sh' '/nix/store/5yzw0vhkyszf2d179m0qfkgxmp5wjjx4-move-docs.sh' '/nix/store/fyaryjvghbkpfnsyw97hb3lyb37s1pd6-move-lib64.sh' '/nix/store/kd4xwxjpjxi71jkm6ka0np72if9rm3y0-move-sbin.sh' '/nix/store/pag6l61paj1dc9sv15l7bm5c17xn5kyk-move-systemd-user-units.sh' '/nix/store/cmzya9irvxzlkh7lfy6i82gbp0saxqj3-multiple-outputs.sh' '/nix/store/x8c40nfigps493a07sdr2pm5s9j1cdc0-patch-shebangs.sh' '/nix/store/cickvswrvann041nqxb0rxilc46svw1n-prune-libtool-files.sh' '/nix/store/xyff06pkhki3qy1ls77w10s0v79c9il0-reproducible-builds.sh' '/nix/store/z7k98578dfzi6l3hsvbivzm7hfqlk0zc-set-source-date-epoch-to-latest.sh' '/nix/store/pilsssjjdxvdphlg2h19p0bfx5q0jzkn-strip.sh' '/nix/store/vr15iyyykg9zai6fpgvhcgyw7gckl78w-gcc-wrapper-14.3.0' '/nix/store/xwydcyvlsa3cvssk0y5llgdhlhjvmqdm-binutils-wrapper-2.44' ) +declare -a pkgsBuildHost=('/nix/store/8vdiwpbh0g4avsd6x5v4s0di32vcl3dp-pkg-config-wrapper-0.29.2' '/nix/store/f9qvdl02ckaiawxaqmb3w738g2j395p4-postgresql-14.20-dev' '/nix/store/9l82bx0yp6q4rk2sdkjr3r8zvyghz0mh-postgresql-14.20-lib' '/nix/store/p48k53gf4mskvxrfahajaczk6m3isfrm-postgresql-14.20' '/nix/store/ah9sslqrl111k74wxd5mdjswza9zvwv6-rustfmt-1.91.1' '/nix/store/l914ycms28s9if296ygwj7aaflpa1psy-cargo-1.91.1' '/nix/store/kqmpsqbrf6r0g9kq1sqa0pd5r8lr12m1-djlint-1.36.4' '/nix/store/p352iawg0q3q18ajklyrih6k46nhfz11-python3.13-click-8.2.1' '/nix/store/jj6jldlw37r8yy9kc1smrax9dhnjm2x4-python3-3.13.9' '/nix/store/3bws72fgyh893p03y7s4gyg1wyyfj2gn-python3.13-colorama-0.4.6' '/nix/store/zdg5imhib6a1kll2mdzpxsnjk6ifv3r2-python3.13-cssbeautifier-1.15.4' '/nix/store/b9ldwlzwk6jmsmgvh0rfrwg1rxmirzrn-python3.13-editorconfig-0.17.1' '/nix/store/czax2jpa46yr9dkbj5cng270c89sqzz4-python3.13-jsbeautifier-1.15.4' '/nix/store/32fdhi7k6nvrbj2blzhkq8ga5zq22wch-python3.13-six-1.17.0' '/nix/store/6xvl7wswdiq57lsq26y8dal6hpvmsbsj-python3.13-json5-0.12.0' '/nix/store/sxbna9s12fb3cds258n95bs8zdjlp0iv-python3.13-pathspec-0.12.1' '/nix/store/h7a9751dg6hj9zczx3zf7xb3wa522pzl-python3.13-pyyaml-6.0.3' '/nix/store/206sd795rz2b42p2vhkszv4zziy77087-python3.13-regex-2025.9.18' '/nix/store/31fval0xlhyd16jz8j6svb1wb8vvq4di-python3.13-tomli-2.2.1' '/nix/store/in46mljcw89s7jp8wycfap7zgzsybjya-python3.13-tqdm-4.67.1' '/nix/store/8q2582rd22xp8jlcg1xn1w219q5lx5xa-patchelf-0.15.2' '/nix/store/l2xk4ac1wx9c95kpp8vymv9r9yn57fvh-update-autotools-gnu-config-scripts-hook' '/nix/store/0y5xmdb7qfvimjwbq7ibg1xdgkgjwqng-no-broken-symlinks.sh' '/nix/store/cv1d7p48379km6a85h4zp6kr86brh32q-audit-tmpdir.sh' '/nix/store/85clx3b0xkdf58jn161iy80y5223ilbi-compress-man-pages.sh' '/nix/store/wgrbkkaldkrlrni33ccvm3b6vbxzb656-make-symlinks-relative.sh' '/nix/store/5yzw0vhkyszf2d179m0qfkgxmp5wjjx4-move-docs.sh' '/nix/store/fyaryjvghbkpfnsyw97hb3lyb37s1pd6-move-lib64.sh' '/nix/store/kd4xwxjpjxi71jkm6ka0np72if9rm3y0-move-sbin.sh' '/nix/store/pag6l61paj1dc9sv15l7bm5c17xn5kyk-move-systemd-user-units.sh' '/nix/store/cmzya9irvxzlkh7lfy6i82gbp0saxqj3-multiple-outputs.sh' '/nix/store/x8c40nfigps493a07sdr2pm5s9j1cdc0-patch-shebangs.sh' '/nix/store/cickvswrvann041nqxb0rxilc46svw1n-prune-libtool-files.sh' '/nix/store/xyff06pkhki3qy1ls77w10s0v79c9il0-reproducible-builds.sh' '/nix/store/z7k98578dfzi6l3hsvbivzm7hfqlk0zc-set-source-date-epoch-to-latest.sh' '/nix/store/pilsssjjdxvdphlg2h19p0bfx5q0jzkn-strip.sh' '/nix/store/vr15iyyykg9zai6fpgvhcgyw7gckl78w-gcc-wrapper-14.3.0' '/nix/store/xwydcyvlsa3cvssk0y5llgdhlhjvmqdm-binutils-wrapper-2.44' ) declare -a pkgsBuildTarget=() declare -a pkgsHostHost=() declare -a pkgsHostTarget=('/nix/store/9071ak7icm08rqjgw7raybxmhhp086wc-cryptsetup-2.8.1-dev' '/nix/store/fxxaan0lgr5yqs299sjfw0klwwd53313-lvm2-2.03.35-dev' '/nix/store/y2ngdv5xdfy5m6jrdgjn1r81rkqy41rh-lvm2-2.03.35-bin' '/nix/store/0s06zawwhmqq3vdf39bi94lxwdyvg705-lvm2-2.03.35-lib' '/nix/store/pzxlqc84603x27hibv6zq6giyar0rz0m-json-c-0.18-dev' '/nix/store/64q3424klqaq5bq409nrmjmiyrs04k2a-json-c-0.18' '/nix/store/ydrckgnllgg8nmhdwni81h7xhcpnrlhd-openssl-3.6.0-dev' '/nix/store/k0gl1zc7f5hk87lylxwbipb0b870bcmk-openssl-3.6.0-bin' '/nix/store/61i74yjkj9p1qphfl7018ja4sdwkipx0-openssl-3.6.0' '/nix/store/57nxr75jbgqnxgbjsp4f6r9shyqzylm2-util-linux-minimal-2.41.2-dev' '/nix/store/rf0crwiz4z45li0n31pqwczi49jg2kwj-util-linux-minimal-2.41.2-bin' '/nix/store/z4wx1a8n24fxfl4rjpf0jg8cmp5b76b5-util-linux-minimal-2.41.2-lib' '/nix/store/g3prh2g2vsnfwrqp19cf1wnpchn5p00b-popt-1.19' '/nix/store/f03spphbp2fyq35pcbwxkivxr3av6874-libargon2-20190702' '/nix/store/1h477y97ws0m7qdyqdc6s2gj6flba3ha-cryptsetup-2.8.1-bin' '/nix/store/c7rsnyvdb0zwhj8fb9hnxpfw7nfshfxb-cryptsetup-2.8.1' '/nix/store/vpjk1z71ga010lyzn7f5abbjm4pnr7qn-protobuf-32.1' '/nix/store/7qfvcajvjs89fxqk4379zhbdmlmxjaxb-abseil-cpp-20250814.1' ) @@ -691,6 +703,11 @@ addEnvHooks () eval "${pkgHookVar}s"'+=("$@")'; done } +addPythonPath () +{ + + addToSearchPathWithCustomDelimiter : PYTHONPATH $1/lib/python3.13/site-packages +} addToSearchPath () { @@ -2017,6 +2034,26 @@ substituteStream () done; printf "%s" "${!var}" } +sysconfigdataHook () +{ + + if [ "$1" = '/nix/store/jj6jldlw37r8yy9kc1smrax9dhnjm2x4-python3-3.13.9' ]; then + export _PYTHON_HOST_PLATFORM='linux-x86_64'; + export _PYTHON_SYSCONFIGDATA_NAME='_sysconfigdata__linux_x86_64-linux-gnu'; + fi +} +toPythonPath () +{ + + local paths="$1"; + local result=; + for i in $paths; + do + p="$i/lib/python3.13/site-packages"; + result="${result}${result:+:}$p"; + done; + echo $result +} unpackFile () { diff --git a/Dockerfile b/Dockerfile index 2e37db5..9cc14be 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,27 +5,27 @@ ENV SCCACHE_DIR=/build-cache ENV RUSTC_WRAPPER=sccache WORKDIR . -COPY ./api ./api +COPY ./web ./web COPY ./libseptastic/ ./libseptastic/ -COPY ./api/assets ./assets -COPY ./api/templates ./templates +COPY ./web/assets ./assets +COPY ./web/templates ./templates RUN apt -y update && apt install -y libssl-dev protobuf-compiler libc-dev sccache build-essential pkg-config -RUN cd api && cargo build --release +RUN cd web && cargo build --release FROM debian:trixie-slim WORKDIR /app EXPOSE 8080 -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/templates /app/templates +COPY --from=build /web/target/release/septastic_web /app/septastic_web +COPY --from=build /web/config.yaml /app/config.yaml +COPY web/assets /app/assets +COPY web/templates /app/templates RUN apt -y update && apt install -y curl ENV RUST_LOG=info ENV EXPOSE_PORT=8080 -ENTRYPOINT ["./septastic_api"] +ENTRYPOINT ["./septastic_web"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..6787977 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# SEPTASTIC + +A fantastic way to ride SEPTA. + +This is a (hastily written) rust program that provides schedule information and +live information for gtfs data feeds (though is specifically tailored to SEPTA). + +## Roadmap + +- [] implement gtfs-rt instead of SEPTA's proprietary API +- [] implement routing +- [] implement a map to show location of vehicles +- [] implement support for connecting legs of a Regional Rail trip +- [] work on providing a better experience with non-SEPTA gtfs files +- [] simple account system to save info such as work commute/fav routes +- [] parse gtfs data ourselves instead of using library +- [] support more of the gtfs specification + +## Building + +The build system for this isn't extremely straightforward. I have a shell.nix that +contains some shell utils specific to this project, but don't have a flake setup +that would allow you to easily build. If you have a newer rust toolchain installed, +you should be able to run this with Cargo. + +I also provide a Dockerfile you can use as the base for a dev container. + +## Running + +There's a config file in this repository that has a format for specifying GTFS +file locations as well as 'annotations' to those files to enrich them. Currently, +the following annotations exist: + +- **multiplatform_stops:** used to combine multiple stops into one stop as though +each 'sub-stop' is it's own platform. This is useful for when you want to group +stops together from multiple gtfs sources (i.e. NJT/SEPTA data for Trenton Transit +Center) or wish to combine multiple stops that are nearby into one location for +easier tracking. + + +```yaml +- id: 'WTC' + name: 'Wissahickon Transit Center' + platform_station_ids: + - 'SEPTABUS_2' + - 'SEPTABUS_31032' + - 'SEPTABUS_32980' + - 'SEPTABUS_32988' + - 'SEPTABUS_32989' + - 'SEPTABUS_32990' + - 'SEPTABUS_32992' + - 'SEPTABUS_32993' + - 'SEPTARAIL_90220' +``` + +This project uses postgres (via sqlx) to store an archive of realtime data, though +it is not required to run SEPTASTIC (all transit data is stored in-memory). + +--- + +I have an instance of this running at [septastic.net](https://septastic.net), +feel free to check it out. diff --git a/api/.sqlx/query-1e1738a3256da7058ddaff950616137b576c37e76410d6845fcf9d3e91e98733.json b/api/.sqlx/query-1e1738a3256da7058ddaff950616137b576c37e76410d6845fcf9d3e91e98733.json deleted file mode 100644 index 7d3aacf..0000000 --- a/api/.sqlx/query-1e1738a3256da7058ddaff950616137b576c37e76410d6845fcf9d3e91e98733.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT \n septa_stop_schedules.route_id,\n septa_stops.name as stop_name,\n trip_id, \n septa_stop_schedules.service_id, \n septa_stop_schedules.direction_id,\n arrival_time,\n stop_id,\n stop_sequence\n FROM \n septa_stop_schedules\n INNER JOIN septa_stops \n ON septa_stops.id = septa_stop_schedules.stop_id\n INNER JOIN septa_schedule_days\n ON septa_schedule_days.date = $2 \n AND\n septa_schedule_days.service_id = septa_stop_schedules.service_id\n WHERE \n septa_stop_schedules.route_id = $1\n ;", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "route_id", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "stop_name", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "trip_id", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "service_id", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "direction_id", - "type_info": "Int8" - }, - { - "ordinal": 5, - "name": "arrival_time", - "type_info": "Int8" - }, - { - "ordinal": 6, - "name": "stop_id", - "type_info": "Int8" - }, - { - "ordinal": 7, - "name": "stop_sequence", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false - ] - }, - "hash": "1e1738a3256da7058ddaff950616137b576c37e76410d6845fcf9d3e91e98733" -} diff --git a/api/.sqlx/query-4c9b19351cb8e0f6d0b3a45b699f10857eb34c6a023ed2f17f1d2702348b006d.json b/api/.sqlx/query-4c9b19351cb8e0f6d0b3a45b699f10857eb34c6a023ed2f17f1d2702348b006d.json deleted file mode 100644 index 2402d9c..0000000 --- a/api/.sqlx/query-4c9b19351cb8e0f6d0b3a45b699f10857eb34c6a023ed2f17f1d2702348b006d.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT \n id, \n name, \n short_name, \n color_hex, \n route_type as \"route_type: libseptastic::route::RouteType\"\n FROM \n septa_routes\n WHERE \n id = $1\n ;", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "name", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "short_name", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "color_hex", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "route_type: libseptastic::route::RouteType", - "type_info": { - "Custom": { - "name": "septa_route_type", - "kind": { - "Enum": [ - "trolley", - "subway_elevated", - "regional_rail", - "bus", - "trackless_trolley" - ] - } - } - } - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false - ] - }, - "hash": "4c9b19351cb8e0f6d0b3a45b699f10857eb34c6a023ed2f17f1d2702348b006d" -} diff --git a/api/.sqlx/query-8479e45bc7bc3f4170102ffc78d5f25d1ff7486fcb266e7306de8c624090bc89.json b/api/.sqlx/query-8479e45bc7bc3f4170102ffc78d5f25d1ff7486fcb266e7306de8c624090bc89.json deleted file mode 100644 index 9971141..0000000 --- a/api/.sqlx/query-8479e45bc7bc3f4170102ffc78d5f25d1ff7486fcb266e7306de8c624090bc89.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT \n id, \n name, \n short_name, \n color_hex, \n route_type as \"route_type: libseptastic::route::RouteType\"\n FROM \n septa_routes\n ORDER BY\n CASE\n WHEN id ~ '^[0-9]+$' THEN CAST(id AS INT)\n ELSE NULL\n END ASC,\n id ASC;\n ;", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "name", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "short_name", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "color_hex", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "route_type: libseptastic::route::RouteType", - "type_info": { - "Custom": { - "name": "septa_route_type", - "kind": { - "Enum": [ - "trolley", - "subway_elevated", - "regional_rail", - "bus", - "trackless_trolley" - ] - } - } - } - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - false, - false - ] - }, - "hash": "8479e45bc7bc3f4170102ffc78d5f25d1ff7486fcb266e7306de8c624090bc89" -} diff --git a/api/.sqlx/query-cf4e35691a709fd362b53e3d1f64b6043cbda87c0b5ebb01ba74a6ebb430425c.json b/api/.sqlx/query-cf4e35691a709fd362b53e3d1f64b6043cbda87c0b5ebb01ba74a6ebb430425c.json deleted file mode 100644 index 2ff1747..0000000 --- a/api/.sqlx/query-cf4e35691a709fd362b53e3d1f64b6043cbda87c0b5ebb01ba74a6ebb430425c.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT \n route_id,\n direction_id,\n direction as \"direction: libseptastic::direction::CardinalDirection\",\n direction_destination\n FROM \n septa_directions\n WHERE \n route_id = $1\n ;", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "route_id", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "direction_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "direction: libseptastic::direction::CardinalDirection", - "type_info": { - "Custom": { - "name": "septa_direction_type", - "kind": { - "Enum": [ - "northbound", - "southbound", - "eastbound", - "westbound", - "inbound", - "outbound", - "loop" - ] - } - } - } - }, - { - "ordinal": 3, - "name": "direction_destination", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false - ] - }, - "hash": "cf4e35691a709fd362b53e3d1f64b6043cbda87c0b5ebb01ba74a6ebb430425c" -} diff --git a/api/assets/test.html b/api/assets/test.html deleted file mode 100644 index 618a6c3..0000000 --- a/api/assets/test.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - Canvas Curve Full Width - - - - - - - - diff --git a/api/src/controllers/route.rs b/api/src/controllers/route.rs deleted file mode 100644 index 64537ab..0000000 --- a/api/src/controllers/route.rs +++ /dev/null @@ -1,173 +0,0 @@ -use actix_web::{get, web::{self, Data}, HttpRequest, HttpResponse, Responder}; -use anyhow::anyhow; -use std::{collections::{HashMap, HashSet}, sync::Arc, time::Instant}; -use libseptastic::{direction, route::RouteType, stop_schedule::Trip}; -use serde::{Serialize, Deserialize}; - -use crate::AppState; -//use crate::{routing::{bfs_rts, construct_graph, get_stops_near}, AppState}; -//use crate::routing; - -#[get("/routes")] -async fn get_routes_html(req: HttpRequest, state: Data>) -> impl Responder { - crate::perform_action(req, move || { - let statex = state.clone(); - async move { - let start_time = Instant::now(); - - let all_routes: Vec = statex.gtfs_service.get_routes(); - 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 trolley_routes = all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::Trolley).collect(); - let bus_routes = all_routes.into_iter().filter(|x| x.route_type == RouteType::TracklessTrolley || x.route_type == RouteType::Bus).collect(); - - Ok(crate::templates::ContentTemplate { - page_title: Some(String::from("Routes")), - page_desc: Some(String::from("All SEPTA routes.")), - widescreen: false, - content: crate::templates::RoutesTemplate { - rr_routes, - subway_routes, - trolley_routes, - bus_routes, - }, - load_time_ms: Some(start_time.elapsed().as_millis()) - }) - } - }).await -} - - -#[get("/routes.json")] -async fn get_routes_json(state: Data>) -> impl Responder { - let all_routes: Vec = state.gtfs_service.get_routes(); - HttpResponse::Ok().json(all_routes) -} - -#[derive(Debug, Deserialize, Clone)] -pub struct RouteQueryParams { - #[serde(default)] // Optional: handle missing parameters with a default value - stops: Option, -} - -#[derive(Serialize, Deserialize)] -pub struct RouteResponse { - pub route: libseptastic::route::Route, - pub directions: Vec, - pub schedule: Vec -} - -async fn get_route_info(route_id: String, state: Data>) -> ::anyhow::Result { - let route = state.gtfs_service.get_route(route_id.clone())?; - let mut trips = state.gtfs_service.get_schedule(route_id)?; - - let mut seen = HashSet::new(); - let directions: Vec<_> = trips - .iter() - .map(|x| x.direction.clone()) - .filter(|dir| seen.insert(dir.direction.to_string())) - .collect(); - - state.trip_tracking_service.annotate_trips(&mut trips).await; - - Ok(RouteResponse{ - route, - directions, - schedule: trips - }) -} - -//#[get("/directions")] -//async fn get_directions(state: Data>) -> impl Responder { -// let near_thresh = 0.45; -// -// let sig_cds = routing::Coordinates { -// lat: 40.008420, -// lng: -75.213439 -// }; -// -// let home_cds = routing::Coordinates { -// lat: 39.957210, -// lng: -75.166214 -// }; -// -// let all_stops = state.gtfs_service.get_all_stops(); -// -// -// -// let origin_stops: HashSet = get_stops_near(home_cds, &all_stops); -// let dest_stops: HashSet = get_stops_near(sig_cds.clone(), &all_stops); -// -// let mut graph = construct_graph(sig_cds, &all_stops, &state.gtfs_service); -// -// let mut response = String::new(); -// for stop in &origin_stops { -// response += bfs_rts(&stop, &mut graph, &dest_stops).as_str(); -// } -// -// return response; -//} - -#[get("/route/{route_id}")] -async fn get_route(state: Data>, req: HttpRequest, info: web::Query, path: web::Path) -> impl Responder { - crate::perform_action(req, move || { - let pathx = path.clone(); - let infox = info.clone(); - let statex = state.clone(); - async move { - let mut filters: Option> = None; - if let Some (stops_v) = infox.stops.clone() { - let mut items = Vec::new(); - - for sid in stops_v.split(",") { - items.push(String::from(sid)); - } - filters = Some(items); - } - - let route_id = pathx; - let route_info_r = get_route_info(route_id.clone(), statex.clone()).await; - - if let Ok(route_info) = route_info_r { - let timetables = crate::templates::build_timetables(route_info.directions, route_info.schedule); - - Ok(crate::templates::ContentTemplate { - widescreen: false, - page_title: Some(format!("Schedules for {}", route_id.clone())), - page_desc: Some(format!("Schedule information for {}", route_id.clone())), - content: crate::templates::RouteTemplate { - route: route_info.route, - timetables, - filter_stops: filters.clone() - }, - load_time_ms: None - }) - } else { - Err(anyhow!("test")) - } - }}).await -} - -#[get("/route/{route_id}.json")] -async fn api_get_route(state: Data>, path: web::Path) -> impl Responder { - let route_id = path.into_inner(); - let route_info_r = get_route_info(route_id, state).await; - if let Ok(route_info) = route_info_r { - HttpResponse::Ok().json(route_info) - } else { - HttpResponse::InternalServerError().body("Error") - } -} - -#[get("/api/route/{route_id}/schedule")] -async fn api_get_schedule(state: Data>, path: web::Path) -> impl Responder { - let route_id = path.into_inner(); - let route_r: anyhow::Result = Ok(5); - - if let Ok(route) = route_r { - HttpResponse::Ok().json(route) - } else { - HttpResponse::InternalServerError().body("Error") - } -} - diff --git a/api/src/controllers/stop.rs b/api/src/controllers/stop.rs deleted file mode 100644 index 4592098..0000000 --- a/api/src/controllers/stop.rs +++ /dev/null @@ -1,193 +0,0 @@ -use actix_web::{HttpRequest, HttpResponse, Responder, get, web::{self, Data}}; -use anyhow::anyhow; -use askama::Template; -use chrono::{NaiveTime, Timelike}; -use chrono_tz::America::New_York; -use libseptastic::stop_schedule::{self, LiveTrip, Trip, TripTracking}; -use log::info; -use std::{collections::HashSet, sync::Arc, time::Instant}; - -use crate::{AppState, templates::TripPerspective}; - -#[get("/stops")] -async fn get_stops_html(req: HttpRequest, state: Data>) -> impl Responder { - crate::perform_action(req, move || { - let statex = state.clone(); - async move { - let start_time = Instant::now(); - let stops = statex.gtfs_service.get_all_stops().iter().filter_map(|f| { - if f.1.id.contains("ANNOTATED") { - Some(libseptastic::stop::Stop::clone(f.1)) - } else { - None - } - }).collect(); - - Ok(crate::ContentTemplate { - page_title: Some(String::from("Stops")), - page_desc: Some(String::from("Stops")), - widescreen: false, - content: crate::templates::StopsTemplate { - tc_stops: stops - }, - load_time_ms: Some(start_time.elapsed().as_millis()) - }) - } - }).await -} - -async fn get_trip_perspective_for_stop(state: &Data>,stop: &libseptastic::stop::Stop) -> Vec { - let routes: Vec = state.gtfs_service.get_routes_at_stop(&stop.id).iter().filter_map(|route| { - match state.gtfs_service.get_route(route.clone()) { - Ok(route) => Some(route), - Err(_) => None - } - }).collect(); - - let route_ids: HashSet = routes.iter().map(|route| route.id.clone()).collect(); - - let mut trips = state.gtfs_service.get_all_trips().iter().filter_map(|trip| { - if route_ids.contains(trip.0) { - Some(trip.1.clone()) - } else { - None - } - }).flatten().collect(); - - - state.trip_tracking_service.annotate_trips(&mut trips).await; - - let now_utc = chrono::Utc::now(); - let now = now_utc.with_timezone(&New_York); - let naive_time = now.time(); - let cur_time = i64::from(naive_time.num_seconds_from_midnight()); - - let mut filtered_trips: Vec = trips.iter().filter_map(|trip| { - //if !trip.is_active_on(&now.naive_local()) { - // return None; - //} - - // poor midnight handling? - if !trip.calendar_day.is_calendar_active_for_date(&now.naive_local().date()) { - return None - } - let mut est_arrival_time = 0; - let mut is_tracked = false; - let stop_sched : Vec<_> = trip.schedule.iter().filter(|stop_schedule| { - if stop_schedule.stop.id != stop.id { - return false; - } - - match &trip.tracking_data { - libseptastic::stop_schedule::TripTracking::Tracked(live) => { - let actual_arrival_time = stop_schedule.get_arrival_time(&live); - est_arrival_time = stop_schedule.arrival_time; - is_tracked = true; - return - (actual_arrival_time - cur_time) > -(1 * 60) - && - (actual_arrival_time - cur_time) < (60 * 60) - ; - }, - libseptastic::stop_schedule::TripTracking::Untracked => { - est_arrival_time = stop_schedule.arrival_time; - return - (stop_schedule.arrival_time - cur_time) > -(3 * 60) - && - (stop_schedule.arrival_time - cur_time) < (60 * 60) - ; - }, - libseptastic::stop_schedule::TripTracking::Cancelled => { - return false; - } - } - }).filter_map(|ss| Some(ss.clone())).collect(); - - if stop_sched.len() > 0 { - Some(TripPerspective { - est_arrival_time, - is_tracked, - perspective_stop: stop_sched.first().unwrap().clone(), - trip: trip.clone() - }) - } else { - None - } - }).collect(); - - filtered_trips.sort_by_key(|f| - match &f.trip.tracking_data { - TripTracking::Tracked(live) => f.perspective_stop.get_arrival_time(&live), - _ => f.perspective_stop.arrival_time - }); - - filtered_trips -} - -#[get("/stop/{stop_id}/table")] -async fn get_stop_table_html(req: HttpRequest, state: Data>, path: web::Path) -> impl Responder { - let stop_id = path; - - if let Some(stop) = state.gtfs_service.get_stop_by_id(&stop_id) { - let filtered_trips = get_trip_perspective_for_stop(&state, &stop).await; - - let now_utc = chrono::Utc::now(); - let now = now_utc.with_timezone(&New_York); - let naive_time = now.time(); - let cur_time = i64::from(naive_time.num_seconds_from_midnight()); - - - HttpResponse::Ok().body(crate::templates::StopTableTemplate { - trips: filtered_trips, - current_time: cur_time - }.render().unwrap()) - } - else { - HttpResponse::InternalServerError().body("Error") - } -} - -#[get("/stop/{stop_id}")] -async fn get_stop_html(req: HttpRequest, state: Data>, path: web::Path) -> impl Responder { - crate::perform_action(req, move || { - let statex = state.clone(); - let pathx = path.clone(); - async move { - let stop_id = pathx; - let start_time = Instant::now(); - - if let Some(stop) = statex.gtfs_service.get_stop_by_id(&stop_id) { - - let routes: Vec = statex.gtfs_service.get_routes_at_stop(&stop.id).iter().filter_map(|route| { - match statex.gtfs_service.get_route(route.clone()) { - Ok(route) => Some(route), - Err(_) => None - } - }).collect(); - - let filtered_trips = get_trip_perspective_for_stop(&statex, &stop).await; - - let now_utc = chrono::Utc::now(); - let now = now_utc.with_timezone(&New_York); - let naive_time = now.time(); - let cur_time = i64::from(naive_time.num_seconds_from_midnight()); - - Ok(crate::templates::ContentTemplate { - page_title: Some(stop.name.clone()), - page_desc: Some(String::from("Stop information")), - widescreen: false, - content: crate::templates::StopTemplate { - stop: stop.clone(), - routes, - trips: filtered_trips, - current_time: cur_time - }, - load_time_ms: Some(start_time.elapsed().as_millis()) - }) - } - else { - Err(anyhow!("Stop not found!")) - } - } - }).await -} diff --git a/api/src/main.rs b/api/src/main.rs deleted file mode 100644 index bc7d958..0000000 --- a/api/src/main.rs +++ /dev/null @@ -1,122 +0,0 @@ -use actix_web::{cookie::Cookie, get, web::Data, App, HttpRequest, HttpResponse, HttpServer, Responder}; -use env_logger::Env; -use log::*; -use dotenv::dotenv; -use serde::Deserialize; -use services::{gtfs_pull, trip_tracking::{self}}; -use templates::ContentTemplate; -use std::{fs::File, io::Read, sync::Arc, time::Instant}; -use askama::Template; - -mod services; -mod controllers; -mod templates; -//mod routing; - -pub struct AppState { - gtfs_service: services::gtfs_pull::GtfsPullService, - trip_tracking_service: services::trip_tracking::TripTrackingService -} - -#[derive(Deserialize)] -struct LocalStateQuery { - pub widescreen: Option -} - -pub async fn perform_action(req: HttpRequest, func: F) -> impl Responder -where T: Template, - F: Fn() -> Fut, - Fut: Future>> + 'static { - - let start_time = Instant::now(); - let mut enable_widescreen = true; - if let Some(widescreen_set) = req.cookie("widescreen") { - enable_widescreen = widescreen_set.value() == "true"; - } - - let query_params = actix_web::web::Query::::from_query(req.query_string()).unwrap(); - - if let Some(set_widescreen) = query_params.widescreen { - enable_widescreen = set_widescreen; - } - - let x = func().await; - - match x { - Ok(mut y) => { - y.widescreen = enable_widescreen; - y.load_time_ms = Some(start_time.elapsed().as_nanos()); - let mut cookie = Cookie::new("widescreen", y.widescreen.to_string()); - cookie.set_path("/"); - HttpResponse::Ok() - .cookie(cookie) - .body(y.render().unwrap()) - }, - Err(err) => { - error!("Returned error b/c {:?}", err); - HttpResponse::InternalServerError().body("Error") - } - } -} - -#[get("/")] -async fn get_index(req: HttpRequest) -> impl Responder { - perform_action(req, move || async { - Ok(templates::ContentTemplate { - page_title: None, - page_desc: None, - content: templates::IndexTemplate {}, - load_time_ms: None, - widescreen: false - }) - }).await -} - -#[tokio::main] -async fn main() -> ::anyhow::Result<()> { - env_logger::init_from_env(Env::default().default_filter_or("septastic_api=info")); - dotenv().ok(); - - let version: &str = option_env!("CARGO_PKG_VERSION").expect("Expected package version"); - info!("Starting the SEPTASTIC Server v{} (commit: {})", version, "NONE"); - - let mut file = File::open("./config.yaml")?; - let mut file_contents = String::new(); - file.read_to_string(&mut file_contents); - - let config_file = serde_yaml::from_str::(file_contents.as_str())?; - - let tt_service = services::trip_tracking::TripTrackingService::new().await; - tt_service.start(); - - let svc = gtfs_pull::GtfsPullService::new(config_file); - svc.start(); - svc.wait_for_ready(); - - let state = Arc::new(AppState { - gtfs_service: svc, - trip_tracking_service: tt_service - }); - - HttpServer::new(move || { - App::new() - .wrap(actix_cors::Cors::permissive()) - .app_data(Data::new(state.clone())) - .service(controllers::route::api_get_route) - .service(controllers::route::api_get_schedule) - .service(controllers::route::get_route) - .service(controllers::route::get_routes_json) - .service(controllers::route::get_routes_html) -// .service(controllers::route::get_directions) - .service(controllers::stop::get_stops_html) - .service(controllers::stop::get_stop_html) - .service(controllers::stop::get_stop_table_html) - .service(get_index) - .service(actix_files::Files::new("/assets", "./assets")) - }) - .bind(("0.0.0.0", 8080))? - .run() - .await?; - - Ok(()) -} diff --git a/api/src/services/gtfs_pull.rs b/api/src/services/gtfs_pull.rs deleted file mode 100644 index ce47f21..0000000 --- a/api/src/services/gtfs_pull.rs +++ /dev/null @@ -1,384 +0,0 @@ -use std::{collections::{HashMap, HashSet}, env, hash::Hash, io::Cursor, path::PathBuf, sync::{Arc, Mutex, MutexGuard}, thread, time::Duration}; - -use anyhow::anyhow; -use libseptastic::{stop::Platform, stop_schedule::CalendarDay}; -use log::{info, error}; -use serde::{Deserialize, Serialize}; -use zip::ZipArchive; - - -macro_rules! make_global_id { - ($prefix: expr, $id: expr) => (format!("{}_{}", $prefix, $id)) -} - -#[derive(Serialize, Deserialize, PartialEq, Debug,Clone)] -struct GtfsSource { - pub uri: String, - pub subzip: Option, - pub prefix: String -} - -#[derive(Serialize, Deserialize, PartialEq, Debug,Clone)] -struct MultiplatformStopConfig { - pub id: String, - pub name: String, - pub platform_station_ids: Vec -} - -#[derive(Serialize, Deserialize, PartialEq, Debug,Clone)] -struct Annotations { - pub multiplatform_stops: Vec -} - -#[derive(Serialize, Deserialize, PartialEq, Debug)] -pub struct Config { - pub gtfs_zips: Vec, - pub annotations: Annotations -} - -#[derive(Clone)] -struct GtfsFile { - pub source: GtfsSource, - pub hash: Option -} - -struct TransitData { - pub routes: HashMap>, - pub agencies: HashMap, - pub trips: HashMap>, - pub stops: HashMap>, - pub platforms: HashMap>, - pub calendar_days: HashMap>, - - // extended lookup methods - pub route_id_by_stops: HashMap>, - pub stops_by_route_id: HashMap>, - pub stops_by_platform_id: HashMap> -} - -struct GtfsPullServiceState { - pub gtfs_files: Vec, - pub tmp_dir: PathBuf, - pub ready: bool, - pub annotations: Annotations, - pub transit_data: TransitData -} - -pub struct GtfsPullService { - state: Arc> -} - -impl TransitData { - pub fn new() -> Self { - return TransitData { routes: HashMap::new(), agencies: HashMap::new(), trips: HashMap::new(), stops: HashMap::new(), platforms: HashMap::new(), route_id_by_stops: HashMap::new(), stops_by_route_id: HashMap::new(), stops_by_platform_id: HashMap::new(), calendar_days: HashMap::new() } - } -} - -impl GtfsPullService { - const UPDATE_SECONDS: u64 = 3600*24; - const READYSTATE_CHECK_MILLISECONDS: u64 = 500; - - pub fn new(config: Config) -> Self { - Self { - state: Arc::new(Mutex::new( - GtfsPullServiceState { - gtfs_files: config.gtfs_zips.iter().map(|f| { GtfsFile { source: f.clone(), hash: None} }).collect(), - tmp_dir: env::temp_dir(), - annotations: config.annotations.clone(), - ready: false, - transit_data: TransitData::new() - } - )) - } - } - - pub fn wait_for_ready(&self) { - while !(self.state.lock().unwrap()).ready { - thread::sleep( - Duration::from_millis(Self::READYSTATE_CHECK_MILLISECONDS) - ); - } - } - - pub fn start(&self) { - let cloned_state = Arc::clone(&self.state); - thread::spawn(move || { - loop { - let recloned_state = Arc::clone(&cloned_state); - let res = Self::update_gtfs_data(recloned_state); - - match res { - Err(err) => { - error!("{}", err); - } - _ => {} - } - - thread::sleep(Duration::from_secs(Self::UPDATE_SECONDS)); - } - }); - } - - pub fn get_routes(&self) -> Vec { - let l_state = self.state.lock().unwrap(); - l_state.transit_data.routes.iter().map(|r| libseptastic::route::Route::clone(r.1)).collect() - } - - pub fn get_route(&self, route_id: String) -> anyhow::Result { - let l_state = self.state.lock().unwrap(); - if let Some(route) = l_state.transit_data.routes.get(&route_id) { - Ok(libseptastic::route::Route::clone(route)) - } else { - Err(anyhow!("")) - } - } - - pub fn get_all_routes(&self) -> HashMap { - let l_state = self.state.lock().unwrap(); - l_state.transit_data.routes.iter().map(|r| (r.0.clone(), libseptastic::route::Route::clone(r.1))).collect() - } - - pub fn get_all_stops(&self) -> HashMap> { - let l_state = self.state.lock().unwrap(); - l_state.transit_data.stops.clone() - } - - pub fn get_all_trips(&self) -> HashMap> { - let l_state = self.state.lock().unwrap(); - l_state.transit_data.trips.clone() - } - - pub fn get_routes_at_stop(&self, id: &String) -> HashSet { - let l_state = self.state.lock().unwrap(); - l_state.transit_data.route_id_by_stops.get(id).unwrap_or(&HashSet::new()).clone() - } - - pub fn get_stops_by_route(&self, id: &String) -> HashSet { - let l_state = self.state.lock().unwrap(); - l_state.transit_data.stops_by_route_id.get(id).unwrap_or(&HashSet::new()).clone() - } - - pub fn get_stop_by_id(&self, id: &String) -> Option { - let l_state = self.state.lock().unwrap(); - match l_state.transit_data.stops.get(id) { - Some(stop) => Some(libseptastic::stop::Stop::clone(stop)), - None => None - } - } - - pub fn get_schedule(&self, route_id: String) -> anyhow::Result> { - let l_state = self.state.lock().unwrap(); - if let Some(trips) = l_state.transit_data.trips.get(&route_id) { - Ok(trips.clone()) - } else { - Err(anyhow!("")) - } - } - - fn postprocess_stops(state: &mut MutexGuard<'_, GtfsPullServiceState>) -> anyhow::Result<()> { - for annotated_stop in state.annotations.multiplatform_stops.clone() { - let global_id = make_global_id!("ANNOTATED", annotated_stop.id.clone()); - let stop = Arc::new(libseptastic::stop::Stop { - id: global_id.clone(), - name: annotated_stop.name.clone(), - platforms: libseptastic::stop::StopType::MultiPlatform(annotated_stop.platform_station_ids.iter().map(|platform_id| { - info!("Folding {} stop into stop {} as platform", platform_id.clone(), annotated_stop.id.clone()); - let platform = match state.transit_data.stops.remove(platform_id).unwrap().platforms.clone() { - libseptastic::stop::StopType::SinglePlatform(plat) => Ok(plat), - _ => Err(anyhow!("")) - }.unwrap(); - - state.transit_data.stops_by_platform_id.remove(&platform.id).unwrap(); - - platform - }).collect()) - }); - - state.transit_data.stops.insert(global_id.clone(), stop.clone()); - match &stop.platforms { - libseptastic::stop::StopType::MultiPlatform(platforms) => { - for platform in platforms { - state.transit_data.stops_by_platform_id.insert(platform.id.clone(), stop.clone()); - } - Ok(()) - }, - _ => Err(anyhow!("")) - }? - } - - Ok(()) - } - - fn populate_stops(state: &mut MutexGuard<'_, GtfsPullServiceState>, prefix: &String, gtfs: >fs_structures::Gtfs) -> anyhow::Result<()> { - for stop in >fs.stops { - let global_id = make_global_id!(prefix, stop.1.id.clone()); - let platform = Arc::new(Platform { - id : global_id.clone(), - name: stop.1.name.clone().unwrap(), - lat: stop.1.latitude.unwrap(), - lng: stop.1.longitude.unwrap(), - platform_location: libseptastic::stop::PlatformLocationType::Normal - }); - - let stop = Arc::new(libseptastic::stop::Stop { - id: global_id.clone(), - name: stop.1.name.clone().unwrap(), - platforms: libseptastic::stop::StopType::SinglePlatform(platform.clone()) - }); - - state.transit_data.stops.insert(global_id.clone(), stop.clone()); - state.transit_data.platforms.insert(global_id.clone(), platform.clone()); - state.transit_data.stops_by_platform_id.insert(global_id.clone(), stop.clone()); - } - - Ok(()) - } - - fn populate_routes(state: &mut MutexGuard<'_, GtfsPullServiceState>, prefix: &String, gtfs: >fs_structures::Gtfs) -> anyhow::Result<()> { - for route in >fs.routes { - let global_rt_id = make_global_id!(prefix, route.1.id); - - let rt_name = match route.1.long_name.clone() { - Some(x) => x, - _ => String::from("Unknown") - }; - - state.transit_data.routes.insert(global_rt_id.clone(), Arc::new(libseptastic::route::Route{ - name: rt_name, - short_name: match route.1.short_name.clone() { - Some(x) => x, - _ => String::from("unknown") - }, - color_hex: match route.1.color{ - Some(x) => x.to_string(), - _ => String::from("unknown") - }, - id: global_rt_id, - route_type: match route.1.route_type { - gtfs_structures::RouteType::Bus => libseptastic::route::RouteType::Bus, - gtfs_structures::RouteType::Rail => libseptastic::route::RouteType::RegionalRail, - gtfs_structures::RouteType::Subway => libseptastic::route::RouteType::SubwayElevated, - gtfs_structures::RouteType::Tramway => libseptastic::route::RouteType::Trolley, - _ => libseptastic::route::RouteType::TracklessTrolley - } - })); - } - - Ok(()) - } - - fn populate_trips(state: &mut MutexGuard<'_, GtfsPullServiceState>, prefix: &String, gtfs: >fs_structures::Gtfs) -> anyhow::Result<()> { - for trip in >fs.trips { - let global_rt_id = make_global_id!(prefix, trip.1.route_id); - let sched = trip.1.stop_times.iter().map(|s| { - let global_stop_id = make_global_id!(prefix, s.stop.id); - - let stop = state.transit_data.stops_by_platform_id.get(&global_stop_id).unwrap().clone(); - let platform = state.transit_data.platforms.get(&global_stop_id).unwrap().clone(); - - state.transit_data.route_id_by_stops.entry(stop.id.clone()).or_insert(HashSet::new()).insert(global_rt_id.clone()); - state.transit_data.stops_by_route_id.entry(global_rt_id.clone()).or_insert(HashSet::new()).insert(stop.id.clone()); - - state.transit_data.route_id_by_stops.entry(platform.id.clone()).or_insert(HashSet::new()).insert(global_rt_id.clone()); - state.transit_data.stops_by_route_id.entry(global_rt_id.clone()).or_insert(HashSet::new()).insert(platform.id.clone()); - - libseptastic::stop_schedule::StopSchedule{ - arrival_time: i64::from(s.arrival_time.unwrap()), - stop_sequence: i64::from(s.stop_sequence), - stop, - platform - } - }).collect(); - - if let Some(calendar_day) = state.transit_data.calendar_days.get(&trip.1.service_id.clone()) { - let trip = libseptastic::stop_schedule::Trip{ - trip_id: trip.1.id.clone(), - route: state.transit_data.routes.get(&make_global_id!(prefix, trip.1.route_id)).unwrap().clone(), - direction: libseptastic::direction::Direction { - direction: match trip.1.direction_id.unwrap() { - gtfs_structures::DirectionType::Outbound => libseptastic::direction::CardinalDirection::Outbound, - gtfs_structures::DirectionType::Inbound => libseptastic::direction::CardinalDirection::Inbound - }, - direction_destination: trip.1.trip_headsign.clone().unwrap() - }, - tracking_data: libseptastic::stop_schedule::TripTracking::Untracked, - schedule: sched, - service_id: trip.1.service_id.clone(), - calendar_day: calendar_day.clone() - }; - - if let Some(trip_arr) = state.transit_data.trips.get_mut(&global_rt_id) { - trip_arr.push(trip); - } else { - state.transit_data.trips.insert(global_rt_id, vec![trip]); - } - } - } - - Ok(()) - } - - pub fn update_gtfs_data(state: Arc>) -> anyhow::Result<()> { - let mut l_state = state.lock().unwrap(); - let files = l_state.gtfs_files.clone(); - l_state.transit_data = TransitData::new(); - - let mut gtfses = Vec::new(); - - for gtfs_file in files.iter() { - let gtfs = if let Some(subzip) = gtfs_file.source.subzip.clone() { - info!("Reading GTFS file at {} (subzip {})", gtfs_file.source.uri, subzip); - let res = reqwest::blocking::get(gtfs_file.source.uri.clone())?; - let outer_archive = res.bytes()?; - let mut archive = ZipArchive::new(Cursor::new(outer_archive))?; - archive.extract(l_state.tmp_dir.clone())?; - - let mut file_path = l_state.tmp_dir.clone(); - file_path.push(subzip.clone()); - - info!("Downloaded, parsing"); - - gtfs_structures::Gtfs::new(file_path.to_str().unwrap())? - } else { - info!("Reading GTFS file at {}", gtfs_file.source.uri); - gtfs_structures::Gtfs::new(gtfs_file.source.uri.as_str())? - }; - - gtfses.push((gtfs, gtfs_file.source.prefix.clone())); - } - - - info!("Data loaded, processing..."); - - for (gtfs, prefix) in >fses { - GtfsPullService::populate_routes(&mut l_state, &prefix, >fs)?; - GtfsPullService::populate_stops(&mut l_state, &prefix, >fs)?; - for calendar in >fs.calendar { - l_state.transit_data.calendar_days.insert(calendar.1.id.clone(), Arc::new(CalendarDay{ - id: calendar.1.id.clone(), - monday: calendar.1.monday, - tuesday: calendar.1.tuesday, - wednesday: calendar.1.wednesday, - thursday: calendar.1.thursday, - friday: calendar.1.friday, - saturday: calendar.1.saturday, - sunday: calendar.1.sunday, - start_date: calendar.1.start_date, - end_date: calendar.1.end_date - })); - } - } - - GtfsPullService::postprocess_stops(&mut l_state)?; - - for (gtfs, prefix) in >fses { - GtfsPullService::populate_trips(&mut l_state, &prefix, >fs)?; - } - - l_state.ready = true; - info!("Finished initial sync, ready state is true"); - - - Ok(()) - } -} diff --git a/api/src/services/gtfs_rt.rs b/api/src/services/gtfs_rt.rs deleted file mode 100644 index 2a9add9..0000000 --- a/api/src/services/gtfs_rt.rs +++ /dev/null @@ -1,76 +0,0 @@ -use serde_json::Value; -use serde::de; -use std::sync::{Arc, Mutex}; -use std::thread; -use std::collections::HashMap; -use std::time::Duration; -use log::error; -use serde::{Serialize, Deserialize, Deserializer}; -use libseptastic::stop_schedule::{LiveTrip, TripTracking}; -use prost::Message; - -struct TripTrackingServiceState { - pub tracking_data: HashMap:: -} - -#[derive(Clone)] -struct GtfsRtFile { - pub uri: String, - pub hash: Option -} - -pub struct TripTrackingService { - state: Arc>, - //pub gtfs_files: Vec -} - -impl TripTrackingService { - const UPDATE_SECONDS: u64 = 30; - - pub fn new() -> Self { - Self { - state: Arc::new(Mutex::new(TripTrackingServiceState{ tracking_data: HashMap::new()})) - } - } - - pub fn start(&self) { - let cloned_state = Arc::clone(&self.state); - thread::spawn( move || { - loop { - let clonedx_state = Arc::clone(&cloned_state); - let res = Self::update_live_trips(clonedx_state); - - match res { - Err(err) => { - error!("{}", err); - } - _ => {} - } - - thread::sleep(Duration::from_secs(Self::UPDATE_SECONDS)); - } - }); - } - - pub fn annotate_trips(&self, trips: &mut Vec) { - for trip in trips { - trip.tracking_data = match self.state.lock().unwrap().tracking_data.get(&trip.trip_id.clone()){ - Some(x) => x.clone(), - None => TripTracking::Untracked - }; - } - } - - fn update_live_trips(service: Arc>) -> anyhow::Result<()> { - - let url = "https://api-endpoint.mta.info/Dataservice/mtagtfsfeeds/nyct%2Fgtfs-l"; - let response = reqwest::blocking::get(url).unwrap(); - let bytes = response.bytes().unwrap(); - let data: Result = prost::Message::decode(bytes.as_ref()); - let data = data.unwrap(); - - println!("{:#?}", data); - - Ok(()) - } -} diff --git a/api/src/services/mod.rs b/api/src/services/mod.rs deleted file mode 100644 index 013f9ce..0000000 --- a/api/src/services/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod trip_tracking; -pub mod gtfs_pull; -pub mod gtfs_rt; diff --git a/api/templates/index.html b/api/templates/index.html deleted file mode 100644 index 6bf0564..0000000 --- a/api/templates/index.html +++ /dev/null @@ -1,13 +0,0 @@ -

SEPTASTIC!

-

A fantastic way to ride SEPTA

- -

- SEPTASTIC is a website and (a soon to be) mobile app. Its purpose is to provide - information about how to ride SEPTA (and connecting transit authorities) in a - quick and information-rich manner. -

- -

- Currently, all this website has is timetables for every - SEPTA route. More to come soon! -

diff --git a/api/templates/layout.html b/api/templates/layout.html deleted file mode 100644 index 8d35551..0000000 --- a/api/templates/layout.html +++ /dev/null @@ -1,98 +0,0 @@ - - - - - {% if let Some(title) = page_title %} - {{ title }} | SEPTASTIC - {% else %} - SEPTASTIC - {% endif %} - - {% if let Some(desc) = page_desc %} - - {% else %} - - {% endif %} - - - - - - - - - - - {% if widescreen %} -
- {% else %} -
- {% endif %} -
- This website is not run by SEPTA. Data may be inaccurate. -
- - -
- - {{ content|safe }} - -
-
-
-
-

SEPTASTIC!

-

- Copyright © Nicholas Orlowsky 2025 -

- {% if let Some(load_time) = load_time_ms %} -

Data loaded in {{ *load_time | format_load_time }}

- {% endif %} -

Total load time

-
-
- {% if widescreen %} - [ disable widescreen ] - {% else %} - [ enable widescreen ] - {% endif %} -
-
- - -
-
- - diff --git a/api/templates/route.html b/api/templates/route.html deleted file mode 100644 index 1684adf..0000000 --- a/api/templates/route.html +++ /dev/null @@ -1,142 +0,0 @@ -{%- import "route_symbol.html" as scope -%} - - - - -
- {% call scope::route_symbol(route) %} -

{{ route.name }}

-
- -{% for timetable in timetables %} -
- -
-

{{ timetable.direction.direction | capitalize }} to

-

{{ timetable.direction.direction_destination }}

-
-
-
- - - - - {% for trip_id in timetable.trip_ids %} - {% if let Some(next_id_v) = timetable.next_id %} - {% if next_id_v == trip_id %} - - {% endfor %} - - - - {% for row in timetable.rows %} - {% if let Some(filter_stop_v) = filter_stops %} - {% if !filter_stop_v.contains(&row.stop_id) %} - {% continue %} - {% endif %} - {% endif %} - - - {% for time in row.times %} - - - {% if let Some(t) = time %} - {% let live_o = timetable.tracking_data[loop.index0] %} - {% if let Tracked(live) = live_o %} - {% let time = (t + (live.delay * 60.0) as i64) %} - - {% elif let TripTracking::Cancelled = live_o %} - - {% else %} - - {% endif %} - {% else %} - - {% endif %} - {% endfor %} - - {% endfor %} - -
Stop - {% else %} - - {% endif %} - {% else %} - - {% endif %} - {{ trip_id }} -
{{ row.stop_name }} - {{ time | format_time }} - {{ t | format_time }}{{ t | format_time }} -
-
-
-{% endfor %} diff --git a/api/templates/route_symbol.html b/api/templates/route_symbol.html deleted file mode 100644 index aca4ed4..0000000 --- a/api/templates/route_symbol.html +++ /dev/null @@ -1,19 +0,0 @@ -{% macro route_symbol(route) %} - {% match route.route_type %} - {% when libseptastic::route::RouteType::Trolley | libseptastic::route::RouteType::SubwayElevated %} -
- {{ route.short_name }} -
- {% endwhen %} - {% when libseptastic::route::RouteType::RegionalRail %} -
- {{ route.short_name }} -
- {% endwhen %} - {% when libseptastic::route::RouteType::Bus | libseptastic::route::RouteType::TracklessTrolley %} -
- {{ route.short_name }} -
- {% endwhen %} - {% endmatch %} -{% endmacro %} diff --git a/api/templates/routes.html b/api/templates/routes.html deleted file mode 100644 index 2c8d711..0000000 --- a/api/templates/routes.html +++ /dev/null @@ -1,60 +0,0 @@ -

Routes

- -

Click on a route to see details and a schedule. Schedules in prevailing local time.

- -
-

Regional Rail

-

For infrequent rail service to suburban locations

- {% for route in rr_routes %} - -

]

-
- {% endfor %} -
- -
-

Metro

-

For frequent rail service within Philadelphia and suburban locations

-
-

[ Subway/Elevated

]

-
- {% for route in subway_routes %} - -

]

-
- {% endfor %} - -
-

[ Trolleys

]

-
- - {% for route in trolley_routes %} - -

]

-
- {% endfor %} -
- -
-

Bus

-

For service of varying frequency within SEPTA's entire service area

- {% for route in bus_routes %} - -

]

-
- {% endfor %} -
- - diff --git a/api/templates/stop.html b/api/templates/stop.html deleted file mode 100644 index 2f6c132..0000000 --- a/api/templates/stop.html +++ /dev/null @@ -1,28 +0,0 @@ -{%- import "route_symbol.html" as scope -%} -{%- import "stop_table.html" as stop_table -%} - -
-

{{ stop.name }}

-
- -

With service available on:

-
- {% for route in routes %} -
- {% call scope::route_symbol(route) %} -
- {% endfor %} -
- -{#{% if let libseptastic::stop::StopType::MultiPlatform(platforms) = stop.platforms %} -
-

Platforms at this station:

- {% for platform in platforms %} -

{{ platform.name }}

- {% endfor %} -
-{% endif %}#} - -
- {% call stop_table::stop_table(trips, current_time) %} -
diff --git a/api/templates/stop_table.html b/api/templates/stop_table.html deleted file mode 100644 index 71ff6b1..0000000 --- a/api/templates/stop_table.html +++ /dev/null @@ -1,51 +0,0 @@ -{%- import "route_symbol.html" as scope -%} - -{% macro stop_table(trips, current_time) %} - - - - - - - - -{% for trip in trips %} - - - - - {% if let Tracked(tracked_trip) = trip.trip.tracking_data %} - - {% else %} - - {% endif %} - {% if let Tracked(tracked_trip) = trip.trip.tracking_data %} - - {% else %} - - {% endif %} - -{% endfor %} - - - -
ROUTEDESTINATIONBOARDING AREATIMEVEHICLE
- {% call scope::route_symbol(trip.trip.route) %} - -

{{ trip.trip.direction.direction_destination }}

-
-

{{ trip.perspective_stop.platform.name }}

-
-

{{ &trip.perspective_stop.get_arrival_time(&tracked_trip) | format_time }}

-

{{ ( trip.perspective_stop.get_arrival_time(&tracked_trip) - current_time) / 60 }} mins

-
-

{{ trip.perspective_stop.arrival_time | format_time }}

-

{{ (trip.perspective_stop.arrival_time - current_time) / 60 }} mins

-
- {{ tracked_trip.vehicle_ids.join(", ") }} - - - -
-

Updated at: {{ current_time | format_time_with_seconds }}

-
-{% endmacro %} diff --git a/api/templates/stop_table_impl.html b/api/templates/stop_table_impl.html deleted file mode 100644 index e230ec6..0000000 --- a/api/templates/stop_table_impl.html +++ /dev/null @@ -1,3 +0,0 @@ -{%- import "stop_table.html" as stop_table -%} - -{% call stop_table::stop_table(trips, current_time) %} diff --git a/api/templates/stops.html b/api/templates/stops.html deleted file mode 100644 index e1bf30a..0000000 --- a/api/templates/stops.html +++ /dev/null @@ -1,27 +0,0 @@ -

Stops

- -

Click on a route to see details and a schedule. Schedules in prevailing local time.

- -
-

Transit Centers

-

Hubs to connect between different modes of transit

- {% for stop in tc_stops %} - -

]

-
- {% endfor %} -
- - diff --git a/example.env b/example.env deleted file mode 100644 index 93d1f56..0000000 --- a/example.env +++ /dev/null @@ -1 +0,0 @@ -DB_CONNSTR= diff --git a/libseptastic/src/agency.rs b/libseptastic/src/agency.rs index 1d1697e..ffb19be 100644 --- a/libseptastic/src/agency.rs +++ b/libseptastic/src/agency.rs @@ -1,8 +1,7 @@ use serde::{Deserialize, Serialize}; - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Agency { pub id: String, - pub name: String + pub name: String, } diff --git a/libseptastic/src/direction.rs b/libseptastic/src/direction.rs index 88bbc54..ff3f9f1 100644 --- a/libseptastic/src/direction.rs +++ b/libseptastic/src/direction.rs @@ -1,7 +1,8 @@ - use serde::{Deserialize, Serialize}; -#[derive(sqlx::Type, Serialize, Deserialize, PartialEq, Debug, Clone, Copy, Eq)] +#[derive( + sqlx::Type, Serialize, Deserialize, PartialEq, Debug, Clone, Copy, Eq, PartialOrd, Ord, +)] #[sqlx(type_name = "septa_direction_type", rename_all = "snake_case")] pub enum CardinalDirection { Northbound, @@ -10,17 +11,17 @@ pub enum CardinalDirection { Westbound, Inbound, Outbound, - Loop + Loop, } #[derive(::sqlx::Decode, Serialize, Deserialize, Debug, Clone)] pub struct Direction { pub direction: CardinalDirection, - pub direction_destination: String + pub direction_destination: String, } impl std::fmt::Display for CardinalDirection { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let output = match self { CardinalDirection::Northbound => "Northbound", CardinalDirection::Southbound => "Southbound", @@ -28,7 +29,7 @@ impl std::fmt::Display for CardinalDirection { CardinalDirection::Westbound => "Westbound", CardinalDirection::Inbound => "Inbound", CardinalDirection::Outbound => "Outbound", - CardinalDirection::Loop => "Loop" + CardinalDirection::Loop => "Loop", }; std::write!(f, "{}", output) } diff --git a/libseptastic/src/lib.rs b/libseptastic/src/lib.rs index 29bdfe8..921fa32 100644 --- a/libseptastic/src/lib.rs +++ b/libseptastic/src/lib.rs @@ -1,8 +1,8 @@ -pub mod route; pub mod agency; -pub mod stop; -pub mod route_stop; -pub mod stop_schedule; -pub mod schedule_day; pub mod direction; pub mod ridership; +pub mod route; +pub mod route_stop; +pub mod schedule_day; +pub mod stop; +pub mod stop_schedule; diff --git a/libseptastic/src/ridership.rs b/libseptastic/src/ridership.rs index 84bc2cd..a5af70f 100644 --- a/libseptastic/src/ridership.rs +++ b/libseptastic/src/ridership.rs @@ -17,5 +17,5 @@ pub struct Ridership { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LineRidership { pub route_id: String, - pub unlinked_trips: i64 + pub unlinked_trips: i64, } diff --git a/libseptastic/src/route.rs b/libseptastic/src/route.rs index 4189f71..62d04e4 100644 --- a/libseptastic/src/route.rs +++ b/libseptastic/src/route.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + use serde::{Deserialize, Serialize}; #[derive(sqlx::Type, Serialize, Deserialize, PartialEq, Debug, Clone)] @@ -7,7 +9,7 @@ pub enum RouteType { SubwayElevated, RegionalRail, Bus, - TracklessTrolley + TracklessTrolley, } #[derive(::sqlx::FromRow, Serialize, Deserialize, Debug, Clone)] @@ -16,13 +18,33 @@ pub struct Route { pub short_name: String, pub color_hex: String, pub route_type: RouteType, - pub id: String + pub id: String, + pub directions: Vec, } +impl PartialEq for Route { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for Route {} + +impl Ord for Route { + fn cmp(&self, other: &Self) -> Ordering { + self.id.cmp(&other.id) + } +} + +impl PartialOrd for Route { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.id.cmp(&other.id)) + } +} #[derive(::sqlx::FromRow, Serialize, Deserialize, Debug, Clone)] pub struct InterlinedRoute { pub interline_id: String, pub interline_name: String, - pub interlined_routes: Vec + pub interlined_routes: Vec, } diff --git a/libseptastic/src/route_stop.rs b/libseptastic/src/route_stop.rs index 8d9d4b4..6ad1700 100644 --- a/libseptastic/src/route_stop.rs +++ b/libseptastic/src/route_stop.rs @@ -3,5 +3,5 @@ pub struct RouteStop { pub route_id: String, pub stop_id: i64, pub direction_id: i64, - pub stop_sequence: i64 + pub stop_sequence: i64, } diff --git a/libseptastic/src/schedule_day.rs b/libseptastic/src/schedule_day.rs index b91409a..9d2f988 100644 --- a/libseptastic/src/schedule_day.rs +++ b/libseptastic/src/schedule_day.rs @@ -1,5 +1,5 @@ #[derive(Debug, Clone)] pub struct ScheduleDay { pub date: String, - pub service_id: String + pub service_id: String, } diff --git a/libseptastic/src/stop.rs b/libseptastic/src/stop.rs index 4520120..e5099e1 100644 --- a/libseptastic/src/stop.rs +++ b/libseptastic/src/stop.rs @@ -1,4 +1,7 @@ -use std::{hash::{Hash, Hasher}, sync::Arc}; +use std::{ + hash::{Hash, Hasher}, + sync::Arc, +}; use serde::{Deserialize, Serialize}; @@ -7,13 +10,13 @@ use serde::{Deserialize, Serialize}; pub enum PlatformLocationType { FarSide, MiddleBlockNearSide, - Normal + Normal, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum StopType { SinglePlatform(Arc), - MultiPlatform(Vec>) + MultiPlatform(Vec>), } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -22,12 +25,12 @@ pub struct Platform { pub name: String, pub lat: f64, pub lng: f64, - pub platform_location: PlatformLocationType + pub platform_location: PlatformLocationType, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Stop { - pub id: String, + pub id: String, pub name: String, pub platforms: StopType, } diff --git a/libseptastic/src/stop_schedule.rs b/libseptastic/src/stop_schedule.rs index 44c11ab..7ff3e7e 100644 --- a/libseptastic/src/stop_schedule.rs +++ b/libseptastic/src/stop_schedule.rs @@ -1,7 +1,7 @@ use std::sync::Arc; -use chrono::{Datelike, Days, TimeZone, Weekday}; -use serde::{Deserialize, Serialize}; +use chrono::{Datelike, Days, Weekday}; +use serde::{Deserialize, Serialize, Serializer}; use crate::{direction::Direction, route::Route, stop::Platform}; @@ -10,11 +10,11 @@ pub struct StopSchedule { pub arrival_time: i64, pub stop_sequence: i64, pub stop: Arc, - pub platform: Arc + pub platform: Arc, } -impl StopSchedule { - pub fn get_arrival_time(&self, live_info: &LiveTrip) -> i64 { +impl StopSchedule { + pub fn get_arrival_time(&self, live_info: &LiveTrip) -> i64 { return self.arrival_time + (live_info.delay * 60.0 as f64) as i64; } } @@ -22,34 +22,45 @@ impl StopSchedule { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Trip { pub service_id: String, - pub route: Arc, + pub route: Arc, pub trip_id: String, pub direction: Direction, pub tracking_data: TripTracking, pub schedule: Vec, - pub calendar_day: Arc + pub calendar_day: Arc, } -impl Trip { +impl Trip { pub fn is_active_on(&self, datetime: &chrono::NaiveDateTime) -> bool { - if !self.calendar_day.is_calendar_active_for_date(&datetime.date()) { + if !self + .calendar_day + .is_calendar_active_for_date(&datetime.date()) + { return false; } - let time_trip_start = chrono::NaiveTime::from_num_seconds_from_midnight_opt(self.schedule.first().unwrap().arrival_time as u32 % (60*60*24), 0).unwrap(); + let time_trip_start = chrono::NaiveTime::from_num_seconds_from_midnight_opt( + self.schedule.first().unwrap().arrival_time as u32 % (60 * 60 * 24), + 0, + ) + .unwrap(); let mut dt_trip_start = chrono::NaiveDateTime::new(datetime.date(), time_trip_start); - - if self.schedule.first().unwrap().arrival_time > (60*60*24) { + + if self.schedule.first().unwrap().arrival_time > (60 * 60 * 24) { dt_trip_start = dt_trip_start.checked_add_days(Days::new(1)).unwrap(); } - - let time_trip_end = chrono::NaiveTime::from_num_seconds_from_midnight_opt(self.schedule.last().unwrap().arrival_time as u32 % (60*60*24), 0).unwrap(); + + let time_trip_end = chrono::NaiveTime::from_num_seconds_from_midnight_opt( + self.schedule.last().unwrap().arrival_time as u32 % (60 * 60 * 24), + 0, + ) + .unwrap(); let mut dt_trip_end = chrono::NaiveDateTime::new(datetime.date(), time_trip_end); - if self.schedule.last().unwrap().arrival_time > (60*60*24) { + if self.schedule.last().unwrap().arrival_time > (60 * 60 * 24) { dt_trip_end = dt_trip_end.checked_add_days(Days::new(1)).unwrap(); } - + return *datetime >= dt_trip_start && *datetime <= dt_trip_end; } } @@ -58,7 +69,7 @@ impl Trip { pub enum TripTracking { Tracked(LiveTrip), Untracked, - Cancelled + Cancelled, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -72,7 +83,7 @@ pub struct CalendarDay { pub saturday: bool, pub sunday: bool, pub start_date: chrono::NaiveDate, - pub end_date: chrono::NaiveDate + pub end_date: chrono::NaiveDate, } impl CalendarDay { @@ -93,6 +104,88 @@ impl CalendarDay { } } +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum SeatAvailability { + Full = 4, + CrushedStandingRoomOnly = 3, + FewSeats = 2, + ManySeats = 1, + Empty = 0, +} + +impl<'de> Deserialize<'de> for SeatAvailability { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let string = String::deserialize(deserializer)?; + return match SeatAvailability::from_string(&string) { + Some(x) => Ok(x), + None => Err(serde::de::Error::custom("")), + }; + } +} + +impl Serialize for SeatAvailability { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_str()) + } +} + +impl SeatAvailability { + pub fn iter() -> Vec { + vec![ + Self::Empty, + Self::ManySeats, + Self::FewSeats, + Self::CrushedStandingRoomOnly, + Self::Full, + ] + } + + pub fn to_string(&self) -> String { + String::from(match &self { + Self::Full => "FULL", + Self::CrushedStandingRoomOnly => "CRUSHED_STANDING_ROOM_ONLY", + Self::FewSeats => "FEW_SEATS_AVAILABLE", + Self::ManySeats => "MANY_SEATS_AVAILABLE", + Self::Empty => "EMPTY", + }) + } + + pub fn to_human_string(&self) -> String { + String::from(match &self { + Self::Full => "Full", + Self::CrushedStandingRoomOnly => "Sardines", + Self::FewSeats => "Few seats", + Self::ManySeats => "Many seats", + Self::Empty => "Empty", + }) + } + + pub fn from_string(str: &String) -> Option { + match str.as_str() { + "FULL" => Some(Self::Full), + "CRUSHED_STANDING_ROOM_ONLY" => Some(Self::CrushedStandingRoomOnly), + "FEW_SEATS_AVAILABLE" => Some(Self::FewSeats), + "MANY_SEATS_AVAILABLE" => Some(Self::ManySeats), + "EMPTY" => Some(Self::Empty), + _ => None, + } + } + + pub fn from_opt_string(opt_str: &Option) -> Option { + if let Some(str) = &opt_str { + Self::from_string(str) + } else { + None + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LiveTrip { pub delay: f64, @@ -102,7 +195,7 @@ pub struct LiveTrip { pub latitude: Option, pub longitude: Option, pub heading: Option, - pub seat_availability: Option, + pub seat_availability: Option, pub trip_id: String, pub route_id: String, } diff --git a/shell.nix b/shell.nix index ea8ae70..1bdf27b 100644 --- a/shell.nix +++ b/shell.nix @@ -1,7 +1,14 @@ with import {}; stdenv.mkDerivation { name = "env"; - nativeBuildInputs = [ pkg-config postgresql_14 ]; + nativeBuildInputs = [ + pkg-config + postgresql_14 + rustfmt + cargo + djlint + ]; + buildInputs = [ cryptsetup protobuf diff --git a/api/.gitignore b/web/.gitignore similarity index 65% rename from api/.gitignore rename to web/.gitignore index 14ee500..08b70ca 100644 --- a/api/.gitignore +++ b/web/.gitignore @@ -1,2 +1,3 @@ target/ .env +.sqlx/ diff --git a/api/Cargo.lock b/web/Cargo.lock similarity index 99% rename from api/Cargo.lock rename to web/Cargo.lock index 3b088e5..31daf09 100644 --- a/api/Cargo.lock +++ b/web/Cargo.lock @@ -387,11 +387,11 @@ dependencies = [ [[package]] name = "askama" -version = "0.14.0" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" +checksum = "08e1676b346cadfec169374f949d7490fd80a24193d37d2afce0c047cf695e57" dependencies = [ - "askama_derive", + "askama_macros", "itoa", "percent-encoding", "serde", @@ -400,9 +400,9 @@ dependencies = [ [[package]] name = "askama_derive" -version = "0.14.0" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" +checksum = "7661ff56517787343f376f75db037426facd7c8d3049cef8911f1e75016f3a37" dependencies = [ "askama_parser", "basic-toml", @@ -416,14 +416,24 @@ dependencies = [ ] [[package]] -name = "askama_parser" -version = "0.14.0" +name = "askama_macros" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" +checksum = "713ee4dbfd1eb719c2dab859465b01fa1d21cb566684614a713a6b7a99a4e47b" dependencies = [ - "memchr", + "askama_derive", +] + +[[package]] +name = "askama_parser" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d62d674238a526418b30c0def480d5beadb9d8964e7f38d635b03bf639c704c" +dependencies = [ + "rustc-hash", "serde", "serde_derive", + "unicode-ident", "winnow", ] @@ -2769,7 +2779,7 @@ dependencies = [ ] [[package]] -name = "septastic_api" +name = "septastic_web" version = "0.1.0" dependencies = [ "actix-cors", @@ -2792,6 +2802,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "serde_qs", "serde_yaml", "sqlx", "sqlx-cli", @@ -2831,6 +2842,20 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac22439301a0b6f45a037681518e3169e8db1db76080e2e9600a08d1027df037" +dependencies = [ + "actix-web", + "futures", + "itoa", + "percent-encoding", + "ryu", + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" diff --git a/api/Cargo.toml b/web/Cargo.toml similarity index 87% rename from api/Cargo.toml rename to web/Cargo.toml index 141b61b..3d6363d 100644 --- a/api/Cargo.toml +++ b/web/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "septastic_api" +name = "septastic_web" version = "0.1.0" edition = "2024" @@ -12,7 +12,7 @@ log = "0.4.27" serde_json = "1.0.140" sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres"] } libseptastic = { path = "../libseptastic/" } -askama = "0.14.0" +askama = "0.15.4" actix-files = "0.6.6" serde = "1.0.219" chrono = "0.4.41" @@ -29,3 +29,4 @@ gtfs-realtime = "0.2.0" prost = "0.14.1" futures = "0.3.31" tokio = "1.48.0" +serde_qs = { version = "1.0.0", features = ["actix4"] } diff --git a/api/assets/style.css b/web/assets/style.css similarity index 97% rename from api/assets/style.css rename to web/assets/style.css index cb86bf6..3210184 100644 --- a/api/assets/style.css +++ b/web/assets/style.css @@ -151,8 +151,6 @@ img { color: #000000; width: max-content; height: max-content; - aspect-ratio: 3/2; - line-height: 1; } .tscroll { width: 100%; @@ -174,3 +172,9 @@ img { details summary > * { display: inline; } + +details { + margin-top: 5px; + margin-bottom: 5px; + padding: 5px; +} diff --git a/api/config.yaml b/web/config.yaml similarity index 82% rename from api/config.yaml rename to web/config.yaml index e24345b..3f82198 100644 --- a/api/config.yaml +++ b/web/config.yaml @@ -5,9 +5,13 @@ gtfs_zips: - uri: "https://www3.septa.org/developer/gtfs_public.zip" prefix: "SEPTABUS" subzip: "google_bus.zip" -# - uri: "https://www.njtransit.com/rail_data.zip" -# - uri: "https://www.njtransit.com/bus_data.zip" annotations: + parent_stop_blacklist: + - 'SEPTABUS_32993' + - 'SEPTABUS_31032' + stop_rename_rules: + - pattern: '(.*) Transportation Center' + replace: '\1 Transit Center' multiplatform_stops: - id: 'WTC' name: 'Wissahickon Transit Center' @@ -20,6 +24,7 @@ annotations: - 'SEPTABUS_32990' - 'SEPTABUS_32992' - 'SEPTABUS_32993' + - 'SEPTARAIL_90220' - id: 'STC' name: "Susquehanna Transit Center" platform_station_ids: @@ -34,5 +39,3 @@ annotations: - 'SEPTABUS_2687' - 'SEPTABUS_18451' - 'SEPTABUS_17170' - synthetic_routes: - - id: 'NYC' diff --git a/web/src/controllers/index.rs b/web/src/controllers/index.rs new file mode 100644 index 0000000..6a2cd49 --- /dev/null +++ b/web/src/controllers/index.rs @@ -0,0 +1,10 @@ +use crate::{ + session_middleware::{SessionResponder, SessionResponse}, + templates::IndexTemplate, +}; +use actix_web::{Responder, get}; + +#[get("/")] +async fn get_index_html(resp: SessionResponse) -> impl Responder { + resp.respond("Home", "SEPTASTIC Home Page", IndexTemplate {}) +} diff --git a/api/src/controllers/mod.rs b/web/src/controllers/mod.rs similarity index 65% rename from api/src/controllers/mod.rs rename to web/src/controllers/mod.rs index a83d8c0..5b25864 100644 --- a/api/src/controllers/mod.rs +++ b/web/src/controllers/mod.rs @@ -1,2 +1,3 @@ +pub mod index; pub mod route; pub mod stop; diff --git a/web/src/controllers/route.rs b/web/src/controllers/route.rs new file mode 100644 index 0000000..3e0f188 --- /dev/null +++ b/web/src/controllers/route.rs @@ -0,0 +1,137 @@ +use crate::{ + AppState, + session_middleware::{SessionResponder, SessionResponse}, +}; +use actix_web::{ + HttpResponse, Responder, get, + web::{self, Data}, +}; +use libseptastic::{route::RouteType, stop_schedule::Trip}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashSet, sync::Arc}; + +#[derive(Debug, Deserialize, Clone)] +struct RouteQueryParams { + #[serde(default)] // Optional: handle missing parameters with a default value + stops: Option, +} + +#[derive(Serialize, Deserialize)] +struct RouteResponse { + pub route: libseptastic::route::Route, + pub directions: Vec, + pub schedule: Vec, +} + +async fn get_route_info( + route_id: String, + state: Data>, +) -> ::anyhow::Result { + let route = state.gtfs_service.get_route(route_id.clone())?; + let mut trips = state.gtfs_service.get_schedule(route_id)?; + + let mut seen = HashSet::new(); + let directions: Vec<_> = trips + .iter() + .map(|x| x.direction.clone()) + .filter(|dir| seen.insert(dir.direction.to_string())) + .collect(); + + state.trip_tracking_service.annotate_trips(&mut trips).await; + + Ok(RouteResponse { + route, + directions, + schedule: trips, + }) +} + +#[get("/routes")] +async fn get_routes_html(state: Data>, resp: SessionResponse) -> impl Responder { + let all_routes: Vec = state.gtfs_service.get_routes(); + 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 trolley_routes = all_routes + .clone() + .into_iter() + .filter(|x| x.route_type == RouteType::Trolley) + .collect(); + let bus_routes = all_routes + .into_iter() + .filter(|x| x.route_type == RouteType::TracklessTrolley || x.route_type == RouteType::Bus) + .collect(); + + resp.respond( + "Routes", + "All routes", + crate::templates::RoutesTemplate { + rr_routes, + subway_routes, + trolley_routes, + bus_routes, + }, + ) +} + +#[get("/routes.json")] +async fn get_routes_json(state: Data>) -> impl Responder { + let all_routes: Vec = state.gtfs_service.get_routes(); + HttpResponse::Ok().json(all_routes) +} + +#[get("/route/{route_id}")] +async fn get_route_html( + state: Data>, + info: web::Query, + path: web::Path, + resp: SessionResponse, +) -> impl Responder { + let mut filters: Option> = None; + if let Some(stops_v) = info.stops.clone() { + let mut items = Vec::new(); + + for sid in stops_v.split(",") { + items.push(String::from(sid)); + } + filters = Some(items); + } + + let route_id = path; + let route_info_r = get_route_info(route_id.clone(), state.clone()).await; + + if let Ok(route_info) = route_info_r { + let timetables = + crate::templates::build_timetables(route_info.directions, route_info.schedule); + + resp.respond( + format!("Schedules for {}", route_id.clone()).as_str(), + format!("Schedule information for {}", route_id.clone()).as_str(), + crate::templates::RouteTemplate { + route: route_info.route, + timetables, + filter_stops: filters.clone(), + }, + ) + } else { + HttpResponse::InternalServerError().body("") + } +} + +#[get("/route/{route_id}.json")] +async fn get_route_json(state: Data>, path: web::Path) -> impl Responder { + let route_id = path.into_inner(); + let route_info_r = get_route_info(route_id, state).await; + if let Ok(route_info) = route_info_r { + HttpResponse::Ok().json(route_info) + } else { + HttpResponse::InternalServerError().body("Error") + } +} diff --git a/web/src/controllers/stop.rs b/web/src/controllers/stop.rs new file mode 100644 index 0000000..5321509 --- /dev/null +++ b/web/src/controllers/stop.rs @@ -0,0 +1,313 @@ +use crate::{ + AppState, + session_middleware::{SessionResponder, SessionResponse}, + templates::TripPerspective, +}; +use actix_web::{ + HttpResponse, Responder, get, post, web::{self, Data} +}; +use askama::Template; +use chrono::{TimeDelta, Timelike}; +use chrono_tz::America::New_York; +use libseptastic::{stop::Stop, stop_schedule::{SeatAvailability, Trip, TripTracking}}; +use serde::{Deserialize, Serialize}; +use serde_qs::actix::QsQuery; +use std::{ + collections::{BTreeSet, HashSet}, + sync::Arc, +}; + +async fn get_trip_perspective_for_stop( + state: &Data>, + stop: &libseptastic::stop::Stop, + filter: &StopFilter, +) -> Vec { + let routes: Vec = state + .gtfs_service + .get_routes_at_stop(&stop.id) + .iter() + .filter_map(|route| match state.gtfs_service.get_route(route.clone()) { + Ok(route) => Some(route), + Err(_) => None, + }) + .collect(); + + let route_ids: HashSet = routes.iter().map(|route| route.id.clone()).collect(); + + let mut trips = state + .gtfs_service + .get_all_trips() + .iter() + .filter_map(|trip| { + if route_ids.contains(trip.0) { + Some(trip.1.clone()) + } else { + None + } + }) + .flatten() + .collect(); + + state.trip_tracking_service.annotate_trips(&mut trips).await; + + let now_utc = chrono::Utc::now(); + let now = now_utc.with_timezone(&New_York); + let naive_time = now.time(); + let cur_time = i64::from(naive_time.num_seconds_from_midnight()); + + let mut filtered_trips: Vec = trips + .iter() + .filter_map(|trip| { + // poor midnight handling? -- going to offset by 4 hours, assume next 'schedule day' + // starts at 4a. Still may miss some trips. Oh well! + if !trip.calendar_day.is_calendar_active_for_date( + &now.naive_local() + .checked_add_signed(TimeDelta::hours(-4))? + .date(), + ) { + return None; + } + + let stop_sched: Vec<_> = trip + .schedule + .iter() + .filter(|stop_schedule| { + if stop_schedule.stop.id != stop.id { + return false; + } + + match &trip.tracking_data { + libseptastic::stop_schedule::TripTracking::Tracked(live) => { + let actual_arrival_time = stop_schedule.get_arrival_time(&live); + return (actual_arrival_time - cur_time) > -(1 * 60) + && (actual_arrival_time - cur_time) < (60 * 60); + } + libseptastic::stop_schedule::TripTracking::Untracked => { + return (stop_schedule.arrival_time - cur_time) > -(3 * 60) + && (stop_schedule.arrival_time - cur_time) < (60 * 60); + } + libseptastic::stop_schedule::TripTracking::Cancelled => { + return false; + } + } + }) + .filter_map(|ss| Some(ss.clone())) + .collect(); + + if stop_sched.len() > 0 && filter.trip_matches(trip) { + Some(TripPerspective { + perspective_stop: stop_sched.first().unwrap().clone(), + trip: trip.clone(), + }) + } else { + None + } + }) + .collect(); + + filtered_trips.sort_by_key(|f| match &f.trip.tracking_data { + TripTracking::Tracked(live) => f.perspective_stop.get_arrival_time(&live), + _ => f.perspective_stop.arrival_time, + }); + + filtered_trips +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct StopFilter { + pub routes: Option>, + pub live_tracked: Option, + pub scheduled: Option, + pub crowding: Option>, + pub unknown_crowding: Option, +} + +impl StopFilter { + pub fn trip_matches(&self, trip: &Trip) -> bool { + let unspecified = self.live_tracked == None && self.scheduled == None; + let unknown_crowding = self.unknown_crowding.unwrap_or(true); + + if (Some(false) == self.scheduled || (!unspecified && self.scheduled == None)) + && match trip.tracking_data { + TripTracking::Untracked => true, + _ => false, + } + { + return false; + } + + if (Some(false) == self.live_tracked || (!unspecified && self.live_tracked == None)) + && match trip.tracking_data { + TripTracking::Tracked(_) => true, + _ => false, + } + { + return false; + } + + if let Some(routes) = &self.routes { + let route_str = format!("{},{}", trip.route.id, trip.direction.direction); + if !routes.contains(&route_str) { + return false; + } + } + + if let Some(crowding) = &self.crowding { + if let TripTracking::Tracked(live_trip) = &trip.tracking_data { + if let Some(seat_availability) = &live_trip.seat_availability { + if !crowding.contains(seat_availability) { + return false; + } + } else { + return unknown_crowding; + } + } else { + return unknown_crowding; + } + } + + return true; + } +} + +#[get("/stops")] +async fn get_stops_html(state: Data>, resp: SessionResponse) -> impl Responder { + let stops = state + .gtfs_service + .get_all_stops() + .iter() + .filter_map(|f| { + if f.1.id.contains("ANNOTATED") { + Some(libseptastic::stop::Stop::clone(f.1)) + } else { + None + } + }) + .collect(); + + resp.respond( + "Stops", + "Stops", + crate::templates::StopsTemplate { tc_stops: stops }, + ) +} + +#[derive(Deserialize)] +struct StringSearch { + search: String +} + +#[post("/stops/search")] +async fn search_stops_html(state: Data>, params: web::Form) -> impl Responder { + let results_limit = 25; + let search_str = params.search.to_lowercase(); + let stops: Vec = state + .gtfs_service + .get_all_stops() + .iter() + .filter_map(|f| { + // Non-ideal + if f.1.name.to_lowercase().contains(&search_str) || + f.1.id.to_lowercase().contains(&search_str) { + Some(libseptastic::stop::Stop::clone(f.1)) + } else { + None + } + }) + .collect(); + + HttpResponse::Ok().body(crate::templates::StopSearchResults { + results: if stops.len() > results_limit { + stops[..results_limit].to_vec() + } else { + stops + } + }.render().unwrap()) +} + +#[get("/stop/{stop_id}/table")] +async fn get_stop_table_html( + state: Data>, + path: web::Path, + query: QsQuery, +) -> impl Responder { + let stop_id = path; + + if let Some(stop) = state.gtfs_service.get_stop_by_id(&stop_id) { + let filtered_trips = get_trip_perspective_for_stop(&state, &stop, &query).await; + + let now_utc = chrono::Utc::now(); + let now = now_utc.with_timezone(&New_York); + let naive_time = now.time(); + let cur_time = i64::from(naive_time.num_seconds_from_midnight()); + let query_str = serde_qs::Config::new() + .array_format(serde_qs::ArrayFormat::Unindexed) + .serialize_string(&query.0.clone()) + .unwrap(); + + HttpResponse::Ok() + .append_header(( + "HX-Replace-Url", + format!("/stop/{}?{}", stop_id, &query_str).as_str(), + )) + .body( + crate::templates::StopTableTemplate { + trips: filtered_trips, + current_time: cur_time, + query_str, + stop_id: stop_id.to_string(), + } + .render() + .unwrap(), + ) + } else { + HttpResponse::InternalServerError().body("Error") + } +} + +#[get("/stop/{stop_id}")] +async fn get_stop_html( + state: Data>, + path: web::Path, + query: QsQuery, + resp: SessionResponse, +) -> impl Responder { + let stop_id = path; + + if let Some(stop) = state.gtfs_service.get_stop_by_id(&stop_id) { + let routes: Vec = state + .gtfs_service + .get_routes_at_stop(&stop.id) + .iter() + .filter_map(|route| match state.gtfs_service.get_route(route.clone()) { + Ok(route) => Some(route), + Err(_) => None, + }) + .collect(); + + let filtered_trips = get_trip_perspective_for_stop(&state, &stop, &query).await; + + let now_utc = chrono::Utc::now(); + let now = now_utc.with_timezone(&New_York); + let naive_time = now.time(); + let cur_time = i64::from(naive_time.num_seconds_from_midnight()); + + resp.respond( + stop.name.as_str(), + "Stop information", + crate::templates::StopTemplate { + stop: stop.clone(), + routes: BTreeSet::from_iter(routes.into_iter()), + trips: filtered_trips, + current_time: cur_time, + filters: Some(query.0.clone()), + query_str: serde_qs::Config::new() + .array_format(serde_qs::ArrayFormat::Unindexed) + .serialize_string(&query.0) + .unwrap(), + }, + ) + } else { + HttpResponse::InternalServerError().body("Error") + } +} diff --git a/api/src/database.rs b/web/src/database.rs similarity index 100% rename from api/src/database.rs rename to web/src/database.rs diff --git a/web/src/main.rs b/web/src/main.rs new file mode 100644 index 0000000..fe13eb5 --- /dev/null +++ b/web/src/main.rs @@ -0,0 +1,67 @@ +use actix_web::{App, HttpServer, web::Data}; +use dotenv::dotenv; +use env_logger::Env; +use log::*; +use services::gtfs_pull; +use std::{fs::File, io::Read, sync::Arc}; + +mod controllers; +mod services; +mod session_middleware; +mod templates; + +pub struct AppState { + gtfs_service: services::gtfs_pull::GtfsPullService, + trip_tracking_service: services::trip_tracking::TripTrackingService, +} + +#[tokio::main] +async fn main() -> ::anyhow::Result<()> { + env_logger::init_from_env(Env::default().default_filter_or("septastic_web=info")); + dotenv().ok(); + + let version: &str = option_env!("CARGO_PKG_VERSION").expect("Expected package version"); + info!( + "Starting the SEPTASTIC Server v{} (commit: {})", + version, "NONE" + ); + + let mut file = File::open("./config.yaml")?; + let mut file_contents = String::new(); + file.read_to_string(&mut file_contents)?; + + let config_file = serde_yaml::from_str::(file_contents.as_str())?; + + let tt_service = services::trip_tracking::TripTrackingService::new().await; + tt_service.start(); + + let svc = gtfs_pull::GtfsPullService::new(config_file); + svc.start(); + svc.wait_for_ready(); + + let state = Arc::new(AppState { + gtfs_service: svc, + trip_tracking_service: tt_service, + }); + + HttpServer::new(move || { + App::new() + .wrap(actix_cors::Cors::permissive()) + .app_data(Data::new(state.clone())) + .service(controllers::route::get_route_html) + .service(controllers::route::get_route_json) + .service(controllers::route::get_routes_html) + .service(controllers::route::get_routes_json) + .service(controllers::stop::get_stops_html) + .service(controllers::stop::get_stop_html) + .service(controllers::stop::search_stops_html) + .service(controllers::stop::get_stop_table_html) + .service(controllers::index::get_index_html) + .service(actix_files::Files::new("/assets", "./assets")) + }) + .bind(("0.0.0.0", 8080))? + .run() + .await?; + + Ok(()) +} diff --git a/api/src/routing.rs b/web/src/routing.rs similarity index 100% rename from api/src/routing.rs rename to web/src/routing.rs diff --git a/web/src/services/gtfs_pull.rs b/web/src/services/gtfs_pull.rs new file mode 100644 index 0000000..b6fe525 --- /dev/null +++ b/web/src/services/gtfs_pull.rs @@ -0,0 +1,629 @@ +use anyhow::anyhow; +use libseptastic::{stop::Platform, stop_schedule::CalendarDay}; +use log::{error, info, warn}; +use serde::{Deserialize, Serialize}; +use std::{ + cmp::Ordering, + collections::{HashMap, HashSet, hash_map::Entry}, + env, + io::Cursor, + path::PathBuf, + sync::{Arc, Mutex, MutexGuard}, + thread, + time::Duration, +}; +use zip::ZipArchive; + +macro_rules! make_global_id { + ($prefix: expr, $id: expr) => { + format!("{}_{}", $prefix, $id) + }; +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct GtfsSource { + pub uri: String, + pub subzip: Option, + pub prefix: String, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct MultiplatformStopConfig { + pub id: String, + pub name: String, + pub platform_station_ids: Vec, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct StopRenameRule { + pub pattern: String, + pub replace: String, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +pub struct Annotations { + pub multiplatform_stops: Vec, + pub parent_stop_blacklist: Vec, + pub stop_rename_rules: Vec + +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub struct Config { + pub gtfs_zips: Vec, + pub annotations: Annotations, +} + +#[derive(Clone)] +pub struct GtfsFile { + pub source: GtfsSource, +} + +pub struct TransitData { + pub routes: HashMap>, + pub agencies: HashMap, + pub trips: HashMap>, + pub stops: HashMap>, + pub platforms: HashMap>, + pub calendar_days: HashMap>, + pub directions: HashMap>>, + + // extended lookup methods + pub route_id_by_stops: HashMap>, + pub stops_by_route_id: HashMap>, + pub stops_by_platform_id: HashMap>, +} + +pub struct GtfsPullServiceState { + pub gtfs_files: Vec, + pub tmp_dir: PathBuf, + pub ready: bool, + pub annotations: Annotations, + pub transit_data: TransitData, +} + +pub struct GtfsPullService { + state: Arc>, +} + +impl TransitData { + pub fn new() -> Self { + return TransitData { + routes: HashMap::new(), + agencies: HashMap::new(), + trips: HashMap::new(), + stops: HashMap::new(), + platforms: HashMap::new(), + route_id_by_stops: HashMap::new(), + stops_by_route_id: HashMap::new(), + stops_by_platform_id: HashMap::new(), + calendar_days: HashMap::new(), + directions: HashMap::new(), + }; + } +} + +impl GtfsPullService { + const UPDATE_SECONDS: u64 = 3600 * 24; + const READYSTATE_CHECK_MILLISECONDS: u64 = 500; + + pub fn new(config: Config) -> Self { + Self { + state: Arc::new(Mutex::new(GtfsPullServiceState { + gtfs_files: config + .gtfs_zips + .iter() + .map(|f| GtfsFile { source: f.clone() }) + .collect(), + tmp_dir: env::temp_dir(), + annotations: config.annotations.clone(), + ready: false, + transit_data: TransitData::new(), + })), + } + } + + pub fn wait_for_ready(&self) { + while !(self.state.lock().unwrap()).ready { + thread::sleep(Duration::from_millis(Self::READYSTATE_CHECK_MILLISECONDS)); + } + } + + pub fn start(&self) { + let cloned_state = Arc::clone(&self.state); + thread::spawn(move || { + loop { + let recloned_state = Arc::clone(&cloned_state); + let res = Self::update_gtfs_data(recloned_state); + + match res { + Err(err) => { + error!("{}", err); + } + _ => {} + } + + thread::sleep(Duration::from_secs(Self::UPDATE_SECONDS)); + } + }); + } + + pub fn get_routes(&self) -> Vec { + let l_state = self.state.lock().unwrap(); + l_state + .transit_data + .routes + .iter() + .map(|r| libseptastic::route::Route::clone(r.1)) + .collect() + } + + pub fn get_route(&self, route_id: String) -> anyhow::Result { + let l_state = self.state.lock().unwrap(); + if let Some(route) = l_state.transit_data.routes.get(&route_id) { + Ok(libseptastic::route::Route::clone(route)) + } else { + Err(anyhow!("")) + } + } + + pub fn get_all_routes(&self) -> HashMap { + let l_state = self.state.lock().unwrap(); + l_state + .transit_data + .routes + .iter() + .map(|r| (r.0.clone(), libseptastic::route::Route::clone(r.1))) + .collect() + } + + pub fn get_all_stops(&self) -> HashMap> { + let l_state = self.state.lock().unwrap(); + l_state.transit_data.stops.clone() + } + + pub fn get_all_trips(&self) -> HashMap> { + let l_state = self.state.lock().unwrap(); + l_state.transit_data.trips.clone() + } + + pub fn get_routes_at_stop(&self, id: &String) -> HashSet { + let l_state = self.state.lock().unwrap(); + l_state + .transit_data + .route_id_by_stops + .get(id) + .unwrap_or(&HashSet::new()) + .clone() + } + + pub fn get_stops_by_route(&self, id: &String) -> HashSet { + let l_state = self.state.lock().unwrap(); + l_state + .transit_data + .stops_by_route_id + .get(id) + .unwrap_or(&HashSet::new()) + .clone() + } + + pub fn get_stop_by_id(&self, id: &String) -> Option { + let l_state = self.state.lock().unwrap(); + match l_state.transit_data.stops.get(id) { + Some(stop) => Some(libseptastic::stop::Stop::clone(stop)), + None => None, + } + } + + pub fn get_schedule( + &self, + route_id: String, + ) -> anyhow::Result> { + let l_state = self.state.lock().unwrap(); + if let Some(trips) = l_state.transit_data.trips.get(&route_id) { + Ok(trips.clone()) + } else { + Err(anyhow!("")) + } + } + + fn postprocess_stops(state: &mut MutexGuard<'_, GtfsPullServiceState>) -> anyhow::Result<()> { + for annotated_stop in state.annotations.multiplatform_stops.clone() { + let global_id = make_global_id!("ANNOTATED", annotated_stop.id.clone()); + let stop = Arc::new(libseptastic::stop::Stop { + id: global_id.clone(), + name: annotated_stop.name.clone(), + platforms: libseptastic::stop::StopType::MultiPlatform( + annotated_stop + .platform_station_ids + .iter() + .map(|platform_id| { + info!( + "Folding {} stop into stop {} as platform", + platform_id.clone(), + annotated_stop.id.clone() + ); + let platform = match state + .transit_data + .stops + .remove(platform_id) + .unwrap() + .platforms + .clone() + { + libseptastic::stop::StopType::SinglePlatform(plat) => Ok(plat), + _ => Err(anyhow!("")), + } + .unwrap(); + + state + .transit_data + .stops_by_platform_id + .remove(&platform.id) + .unwrap(); + + platform + }) + .collect(), + ), + }); + + state + .transit_data + .stops + .insert(global_id.clone(), stop.clone()); + match &stop.platforms { + libseptastic::stop::StopType::MultiPlatform(platforms) => { + for platform in platforms { + state + .transit_data + .stops_by_platform_id + .insert(platform.id.clone(), stop.clone()); + } + Ok(()) + } + _ => Err(anyhow!("")), + }? + } + + Ok(()) + } + + fn populate_stops( + state: &mut MutexGuard<'_, GtfsPullServiceState>, + prefix: &String, + gtfs: >fs_structures::Gtfs, + ) -> anyhow::Result<()> { + let mut map: HashMap>= HashMap::new(); + for stop in >fs.stops { + let global_id = make_global_id!(prefix, stop.1.id.clone()); + let platform = Arc::new(Platform { + id: global_id.clone(), + name: stop.1.name.clone().unwrap(), + lat: stop.1.latitude.unwrap(), + lng: stop.1.longitude.unwrap(), + platform_location: libseptastic::stop::PlatformLocationType::Normal, + }); + + if let Some(parent) = &stop.1.parent_station { + let parent_global_id = make_global_id!(prefix, parent); + if !state.annotations.parent_stop_blacklist.contains(&parent_global_id) { + map.entry(parent_global_id) + .or_insert(vec![]).push(global_id.clone()); + } + } + + let stop = Arc::new(libseptastic::stop::Stop { + id: global_id.clone(), + name: stop.1.name.clone().unwrap(), + platforms: libseptastic::stop::StopType::SinglePlatform(platform.clone()), + }); + + + state + .transit_data + .stops + .insert(global_id.clone(), stop.clone()); + state + .transit_data + .platforms + .insert(global_id.clone(), platform.clone()); + state + .transit_data + .stops_by_platform_id + .insert(global_id.clone(), stop.clone()); + } + + for pair in &map { + let parent_stop = state.transit_data.stops.get(pair.0).unwrap().clone(); + //let child_stop: Vec = pair.1.iter().map(|stop_id| { + // state.transit_data.stops.get(stop_id).unwrap().clone() + //}).collect(); + state.annotations.multiplatform_stops.push( + MultiplatformStopConfig { id: parent_stop.id.clone(), name: parent_stop.name.clone(), platform_station_ids: pair.1.clone() } + ); + } + + Ok(()) + } + + fn populate_routes( + state: &mut MutexGuard<'_, GtfsPullServiceState>, + prefix: &String, + gtfs: >fs_structures::Gtfs, + ) -> anyhow::Result<()> { + for route in >fs.routes { + let global_rt_id = make_global_id!(prefix, route.1.id); + + let rt_name = match route.1.long_name.clone() { + Some(x) => x, + _ => String::from("Unknown"), + }; + + let dirs = match state.transit_data.directions.get(&global_rt_id) { + Some(x) => x + .iter() + .map(|f| libseptastic::direction::Direction::clone(f)) + .collect(), + None => { + warn!("Excluding {} because it has no directions", global_rt_id); + continue; + } + }; + + state.transit_data.routes.insert( + global_rt_id.clone(), + Arc::new(libseptastic::route::Route { + name: rt_name, + directions: dirs, + short_name: match route.1.short_name.clone() { + Some(x) => x, + _ => String::from("unknown"), + }, + color_hex: match route.1.color { + Some(x) => x.to_string(), + _ => String::from("unknown"), + }, + id: global_rt_id, + route_type: match route.1.route_type { + gtfs_structures::RouteType::Bus => libseptastic::route::RouteType::Bus, + gtfs_structures::RouteType::Rail => { + libseptastic::route::RouteType::RegionalRail + } + gtfs_structures::RouteType::Subway => { + libseptastic::route::RouteType::SubwayElevated + } + gtfs_structures::RouteType::Tramway => { + libseptastic::route::RouteType::Trolley + } + _ => libseptastic::route::RouteType::TracklessTrolley, + }, + }), + ); + } + + Ok(()) + } + + fn populate_directions( + state: &mut MutexGuard<'_, GtfsPullServiceState>, + prefix: &String, + gtfs: >fs_structures::Gtfs, + ) -> anyhow::Result<()> { + for trip in >fs.trips { + let global_rt_id = make_global_id!(prefix, trip.1.route_id); + + let dir = libseptastic::direction::Direction { + direction: match trip.1.direction_id.unwrap() { + gtfs_structures::DirectionType::Outbound => { + libseptastic::direction::CardinalDirection::Outbound + } + gtfs_structures::DirectionType::Inbound => { + libseptastic::direction::CardinalDirection::Inbound + } + }, + direction_destination: trip.1.trip_headsign.clone().unwrap(), + }; + + match state.transit_data.directions.entry(global_rt_id) { + Entry::Vacant(e) => { + e.insert(vec![Arc::new(dir)]); + } + Entry::Occupied(mut e) => { + if e.get() + .iter() + .filter(|x| x.direction == dir.direction) + .count() + == 0 + { + e.get_mut().push(Arc::new(dir)); + } + } + } + } + + for dir in &mut state.transit_data.directions { + dir.1.sort_by(|x, y| { + if x.direction > y.direction { + Ordering::Greater + } else { + Ordering::Less + } + }); + } + + Ok(()) + } + + fn populate_trips( + state: &mut MutexGuard<'_, GtfsPullServiceState>, + prefix: &String, + gtfs: >fs_structures::Gtfs, + ) -> anyhow::Result<()> { + for trip in >fs.trips { + let global_rt_id = make_global_id!(prefix, trip.1.route_id); + let sched = trip + .1 + .stop_times + .iter() + .map(|s| { + let global_stop_id = make_global_id!(prefix, s.stop.id); + + let stop = state + .transit_data + .stops_by_platform_id + .get(&global_stop_id) + .unwrap() + .clone(); + let platform = state + .transit_data + .platforms + .get(&global_stop_id) + .unwrap() + .clone(); + + state + .transit_data + .route_id_by_stops + .entry(stop.id.clone()) + .or_insert(HashSet::new()) + .insert(global_rt_id.clone()); + state + .transit_data + .stops_by_route_id + .entry(global_rt_id.clone()) + .or_insert(HashSet::new()) + .insert(stop.id.clone()); + + state + .transit_data + .route_id_by_stops + .entry(platform.id.clone()) + .or_insert(HashSet::new()) + .insert(global_rt_id.clone()); + state + .transit_data + .stops_by_route_id + .entry(global_rt_id.clone()) + .or_insert(HashSet::new()) + .insert(platform.id.clone()); + + libseptastic::stop_schedule::StopSchedule { + arrival_time: i64::from(s.arrival_time.unwrap()), + stop_sequence: i64::from(s.stop_sequence), + stop, + platform, + } + }) + .collect(); + + if let Some(calendar_day) = state + .transit_data + .calendar_days + .get(&trip.1.service_id.clone()) + { + let trip = libseptastic::stop_schedule::Trip { + trip_id: trip.1.id.clone(), + route: state + .transit_data + .routes + .get(&make_global_id!(prefix, trip.1.route_id)) + .unwrap() + .clone(), + direction: libseptastic::direction::Direction { + direction: match trip.1.direction_id.unwrap() { + gtfs_structures::DirectionType::Outbound => { + libseptastic::direction::CardinalDirection::Outbound + } + gtfs_structures::DirectionType::Inbound => { + libseptastic::direction::CardinalDirection::Inbound + } + }, + direction_destination: trip.1.trip_headsign.clone().unwrap(), + }, + tracking_data: libseptastic::stop_schedule::TripTracking::Untracked, + schedule: sched, + service_id: trip.1.service_id.clone(), + calendar_day: calendar_day.clone(), + }; + + if let Some(trip_arr) = state.transit_data.trips.get_mut(&global_rt_id) { + trip_arr.push(trip); + } else { + state.transit_data.trips.insert(global_rt_id, vec![trip]); + } + } + } + + Ok(()) + } + + pub fn update_gtfs_data(state: Arc>) -> anyhow::Result<()> { + let mut l_state = state.lock().unwrap(); + let files = l_state.gtfs_files.clone(); + l_state.transit_data = TransitData::new(); + + let mut gtfses = Vec::new(); + + for gtfs_file in files.iter() { + let gtfs = if let Some(subzip) = gtfs_file.source.subzip.clone() { + info!( + "Reading GTFS file at {} (subzip {})", + gtfs_file.source.uri, subzip + ); + let res = reqwest::blocking::get(gtfs_file.source.uri.clone())?; + let outer_archive = res.bytes()?; + let mut archive = ZipArchive::new(Cursor::new(outer_archive))?; + archive.extract(l_state.tmp_dir.clone())?; + + let mut file_path = l_state.tmp_dir.clone(); + file_path.push(subzip.clone()); + + info!("Downloaded, parsing"); + + gtfs_structures::Gtfs::new(file_path.to_str().unwrap())? + } else { + info!("Reading GTFS file at {}", gtfs_file.source.uri); + gtfs_structures::Gtfs::new(gtfs_file.source.uri.as_str())? + }; + + gtfses.push((gtfs, gtfs_file.source.prefix.clone())); + } + + info!("Data loaded, processing..."); + + for (gtfs, prefix) in >fses { + GtfsPullService::populate_directions(&mut l_state, &prefix, >fs)?; + GtfsPullService::populate_routes(&mut l_state, &prefix, >fs)?; + GtfsPullService::populate_stops(&mut l_state, &prefix, >fs)?; + for calendar in >fs.calendar { + l_state.transit_data.calendar_days.insert( + calendar.1.id.clone(), + Arc::new(CalendarDay { + id: calendar.1.id.clone(), + monday: calendar.1.monday, + tuesday: calendar.1.tuesday, + wednesday: calendar.1.wednesday, + thursday: calendar.1.thursday, + friday: calendar.1.friday, + saturday: calendar.1.saturday, + sunday: calendar.1.sunday, + start_date: calendar.1.start_date, + end_date: calendar.1.end_date, + }), + ); + } + } + + GtfsPullService::postprocess_stops(&mut l_state)?; + + for (gtfs, prefix) in >fses { + GtfsPullService::populate_trips(&mut l_state, &prefix, >fs)?; + } + + l_state.ready = true; + info!("Finished initial sync, ready state is true"); + + Ok(()) + } +} diff --git a/web/src/services/mod.rs b/web/src/services/mod.rs new file mode 100644 index 0000000..57730cb --- /dev/null +++ b/web/src/services/mod.rs @@ -0,0 +1,2 @@ +pub mod gtfs_pull; +pub mod trip_tracking; diff --git a/api/src/services/trip_tracking.rs b/web/src/services/trip_tracking.rs similarity index 57% rename from api/src/services/trip_tracking.rs rename to web/src/services/trip_tracking.rs index 6189160..dc54e8d 100644 --- a/api/src/services/trip_tracking.rs +++ b/web/src/services/trip_tracking.rs @@ -1,14 +1,14 @@ use chrono::Utc; -use serde_json::Value; -use serde::de; -use sqlx::{Postgres, QueryBuilder, Transaction}; -use std::sync::{Arc}; use futures::lock::Mutex; -use std::collections::HashMap; -use std::time::Duration; +use libseptastic::stop_schedule::{LiveTrip, SeatAvailability, TripTracking}; use log::{error, info}; -use serde::{Serialize, Deserialize, Deserializer}; -use libseptastic::stop_schedule::{LiveTrip, TripTracking}; +use serde::de; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::Value; +use sqlx::{Postgres, QueryBuilder, Transaction}; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LiveTripJson { @@ -34,18 +34,18 @@ pub struct LiveTripJson { pub next_stop_sequence: Option, pub seat_availability: Option, pub vehicle_id: Option, - pub timestamp: i64 + pub timestamp: i64, } const HOST: &str = "https://www3.septa.org"; struct TripTrackingServiceState { - pub tracking_data: HashMap::, - pub database: ::sqlx::postgres::PgPool + pub tracking_data: HashMap, + pub database: ::sqlx::postgres::PgPool, } pub struct TripTrackingService { - state: Arc> + state: Arc>, } impl TripTrackingService { @@ -53,10 +53,9 @@ impl TripTrackingService { pub async fn log_delay( transaction: &mut Transaction<'_, Postgres>, - tracking_data: &HashMap::, - timestamp: i64 + tracking_data: &HashMap, + timestamp: i64, ) -> ::anyhow::Result<()> { - let mut query_builder: QueryBuilder = QueryBuilder::new( "INSERT INTO live_tracking @@ -73,10 +72,10 @@ impl TripTrackingService { trip_id, route_id ) - VALUES" + VALUES", ); - let mut separated = query_builder.separated(", "); + let mut separated = query_builder.separated(", "); for trip in tracking_data { if let TripTracking::Tracked(live_data) = trip.1 { separated.push("("); @@ -87,7 +86,10 @@ impl TripTrackingService { separated.push_bind(live_data.latitude); separated.push_bind(live_data.longitude); separated.push_bind(live_data.heading); - separated.push_bind(live_data.seat_availability.clone()); + separated.push_bind(match &live_data.seat_availability { + Some(s) => Some(s.to_string()), + None => None, + }); separated.push_bind(live_data.vehicle_ids.clone()); separated.push_bind(live_data.trip_id.clone()); separated.push_bind(live_data.route_id.clone()); @@ -108,17 +110,21 @@ impl TripTrackingService { let pool = ::sqlx::postgres::PgPoolOptions::new() .max_connections(5) .connect(&connection_string) - .await.unwrap(); + .await + .unwrap(); Self { - state: Arc::new(Mutex::new(TripTrackingServiceState{ tracking_data: HashMap::new(), database: pool})) + state: Arc::new(Mutex::new(TripTrackingServiceState { + tracking_data: HashMap::new(), + database: pool, + })), } } pub fn start(&self) { info!("Starting live tracking service"); let cloned_state = Arc::clone(&self.state); - tokio::spawn( async move { + tokio::spawn(async move { loop { let clonedx_state = Arc::clone(&cloned_state); let res = Self::update_live_trips(clonedx_state).await; @@ -131,23 +137,34 @@ impl TripTrackingService { } tokio::time::sleep(Duration::from_secs(Self::UPDATE_SECONDS)).await; - } + } }); } pub async fn annotate_trips(&self, trips: &mut Vec) { for trip in trips { - trip.tracking_data = match self.state.lock().await.tracking_data.get(&trip.trip_id.clone()){ + trip.tracking_data = match self + .state + .lock() + .await + .tracking_data + .get(&trip.trip_id.clone()) + { Some(x) => x.clone(), - None => TripTracking::Untracked + None => TripTracking::Untracked, }; } } - async fn update_live_trips(service: Arc>) -> anyhow::Result<()> { - let mut new_map: HashMap = HashMap::new(); - let live_tracks = reqwest::get(format!("{}/api/v2/trips/", HOST)).await?.json::>().await?; - + async fn update_live_trips( + service: Arc>, + ) -> anyhow::Result<()> { + let mut new_map: HashMap = HashMap::new(); + let live_tracks = reqwest::get(format!("{}/api/v2/trips/", HOST)) + .await? + .json::>() + .await?; + for live_track in live_tracks { let track: TripTracking = { if live_track.status == "NO GPS" { @@ -155,48 +172,50 @@ impl TripTrackingService { } else if live_track.status == "CANCELED" { TripTracking::Cancelled } else { - TripTracking::Tracked( - LiveTrip { - trip_id: live_track.trip_id.clone(), - route_id: live_track.route_id, - delay: live_track.delay, - seat_availability: live_track.seat_availability, - heading: match live_track.heading { - Some(hdg) => if hdg != "" { Some(hdg.parse::()?)} else {None}, - None => None - }, - latitude: match live_track.lat { - Some(lat) => Some(lat.parse::()?), - None => None - }, - longitude: match live_track.lon { - Some(lon) => Some(lon.parse::()?), - None => None - }, - next_stop_id: match live_track.next_stop_id { - Some(x) => match x.parse() { - Ok(y) => Some(y), - Err(_) => None - }, - None => None - }, - timestamp: live_track.timestamp, - vehicle_ids: match live_track.vehicle_id { - Some(x) => x.split(",").map(|f| String::from(f)).collect(), - None => vec![] + TripTracking::Tracked(LiveTrip { + trip_id: live_track.trip_id.clone(), + route_id: live_track.route_id, + delay: live_track.delay, + seat_availability: SeatAvailability::from_opt_string( + &live_track.seat_availability, + ), + heading: match live_track.heading { + Some(hdg) => { + if hdg != "" { + Some(hdg.parse::()?) + } else { + None + } } - } - ) + None => None, + }, + latitude: match live_track.lat { + Some(lat) => Some(lat.parse::()?), + None => None, + }, + longitude: match live_track.lon { + Some(lon) => Some(lon.parse::()?), + None => None, + }, + next_stop_id: match live_track.next_stop_id { + Some(x) => match x.parse() { + Ok(y) => Some(y), + Err(_) => None, + }, + None => None, + }, + timestamp: live_track.timestamp, + vehicle_ids: match live_track.vehicle_id { + Some(x) => x.split(",").map(|f| String::from(f)).collect(), + None => vec![], + }, + }) } }; - if let TripTracking::Cancelled = track { - } + if let TripTracking::Cancelled = track {} - new_map.insert( - live_track.trip_id.clone(), - track - ); + new_map.insert(live_track.trip_id.clone(), track); } let mut svc = service.lock().await; @@ -214,24 +233,34 @@ impl TripTrackingService { fn de_numstr<'de, D: Deserializer<'de>>(deserializer: D) -> Result { Ok(match Value::deserialize(deserializer)? { Value::String(s) => s, - Value::Number(num) => num.as_i64().ok_or(de::Error::custom("Invalid number"))?.to_string(), - _ => return Err(de::Error::custom("wrong type")) + Value::Number(num) => num + .as_i64() + .ok_or(de::Error::custom("Invalid number"))? + .to_string(), + _ => return Err(de::Error::custom("wrong type")), }) } fn de_numstro<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { Ok(match Value::deserialize(deserializer)? { Value::String(s) => Some(s), - Value::Number(num) => Some(num.as_i64().ok_or(de::Error::custom("Invalid number"))?.to_string()), - _ => None + Value::Number(num) => Some( + num.as_i64() + .ok_or(de::Error::custom("Invalid number"))? + .to_string(), + ), + _ => None, }) } fn de_numstrflo<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { Ok(match Value::deserialize(deserializer)? { Value::String(s) => Some(s), - Value::Number(num) => Some(num.as_f64().ok_or(de::Error::custom("Invalid number"))?.to_string()), - _ => None + Value::Number(num) => Some( + num.as_f64() + .ok_or(de::Error::custom("Invalid number"))? + .to_string(), + ), + _ => None, }) } - diff --git a/web/src/session_middleware.rs b/web/src/session_middleware.rs new file mode 100644 index 0000000..57d2db1 --- /dev/null +++ b/web/src/session_middleware.rs @@ -0,0 +1,69 @@ +use actix_web::{FromRequest, HttpRequest, HttpResponse, cookie::Cookie, dev::Payload}; +use askama::Template; +use serde::Deserialize; +use std::{pin::Pin, time::Instant}; + +#[derive(Deserialize)] +struct LocalStateQuery { + pub widescreen: Option, +} + +pub trait SessionResponder { + fn respond(&self, page_title: &str, page_desc: &str, content: T) -> HttpResponse; +} + +pub struct SessionResponse { + start_time: Instant, + widescreen: bool, +} + +impl SessionResponder for SessionResponse +where + T: Template, +{ + fn respond(&self, page_title: &str, page_desc: &str, content: T) -> HttpResponse { + let end_time = Instant::now(); + let mut cookie = Cookie::new("widescreen", self.widescreen.to_string()); + cookie.set_path("/"); + + HttpResponse::Ok().cookie(cookie).body( + crate::templates::ContentTemplate { + page_title: Some(page_title.to_string()), + page_desc: Some(page_desc.to_string()), + content, + load_time_ms: Some((end_time - self.start_time).as_nanos()), + widescreen: self.widescreen, + } + .render() + .unwrap(), + ) + } +} + +impl FromRequest for SessionResponse { + type Error = actix_web::Error; + type Future = Pin>>>; + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + let start_time = Instant::now(); + + let mut enable_widescreen = true; + if let Some(widescreen_set) = req.cookie("widescreen") { + enable_widescreen = widescreen_set.value() == "true"; + } + + let query_params = + actix_web::web::Query::::from_query(req.query_string()).unwrap(); + + if let Some(set_widescreen) = query_params.widescreen { + enable_widescreen = set_widescreen; + } + + Box::pin(async move { + Ok(SessionResponse { + start_time, + widescreen: enable_widescreen, + }) + }) + } +} diff --git a/api/src/templates.rs b/web/src/templates.rs similarity index 70% rename from api/src/templates.rs rename to web/src/templates.rs index 806ac16..a0d73a9 100644 --- a/api/src/templates.rs +++ b/web/src/templates.rs @@ -1,9 +1,18 @@ -use chrono_tz::America::New_York; -use libseptastic::{direction::Direction, stop_schedule::{Trip, TripTracking}}; -use std::{cmp::Ordering, collections::BTreeMap}; -use serde::{Serialize}; -use libseptastic::stop_schedule::TripTracking::Tracked; use chrono::Timelike; +use chrono_tz::America::New_York; +use libseptastic::stop::Stop; +use libseptastic::stop_schedule::TripTracking::Tracked; +use libseptastic::{ + direction::Direction, + stop_schedule::{SeatAvailability, Trip, TripTracking}, +}; +use serde::Serialize; +use std::{ + cmp::Ordering, + collections::{BTreeMap, BTreeSet}, +}; + +use crate::controllers::stop::StopFilter; #[derive(askama::Template)] #[template(path = "layout.html")] @@ -12,7 +21,7 @@ pub struct ContentTemplate { pub page_title: Option, pub page_desc: Option, pub load_time_ms: Option, - pub widescreen: bool + pub widescreen: bool, } #[derive(askama::Template)] @@ -20,7 +29,7 @@ pub struct ContentTemplate { pub struct RouteTemplate { pub route: libseptastic::route::Route, pub timetables: Vec, - pub filter_stops: Option> + pub filter_stops: Option>, } #[derive(askama::Template)] @@ -29,7 +38,7 @@ pub struct RoutesTemplate { pub rr_routes: Vec, pub subway_routes: Vec, pub trolley_routes: Vec, - pub bus_routes: Vec + pub bus_routes: Vec, } #[derive(askama::Template)] @@ -40,59 +49,62 @@ pub struct StopsTemplate { #[derive(askama::Template)] #[template(path = "index.html")] -pub struct IndexTemplate { -} +pub struct IndexTemplate {} #[derive(Debug, Serialize)] pub struct TimetableStopRow { - pub stop_id: String, + pub stop_id: String, pub stop_name: String, pub stop_sequence: i64, - pub times: Vec> + pub times: Vec>, } - #[derive(Debug, Serialize)] pub struct TimetableDirection { pub direction: Direction, pub trip_ids: Vec, pub tracking_data: Vec, pub rows: Vec, - pub next_id: Option + pub next_id: Option, } pub struct TripPerspective { - pub trip:libseptastic::stop_schedule::Trip, + pub trip: libseptastic::stop_schedule::Trip, pub perspective_stop: libseptastic::stop_schedule::StopSchedule, - pub est_arrival_time: i64, - pub is_tracked: bool } #[derive(askama::Template)] #[template(path = "stop.html")] pub struct StopTemplate { pub stop: libseptastic::stop::Stop, - pub routes: Vec, + pub routes: BTreeSet, pub trips: Vec, - pub current_time: i64 + pub current_time: i64, + pub filters: Option, + pub query_str: String, } #[derive(askama::Template)] #[template(path = "stop_table_impl.html")] pub struct StopTableTemplate { pub trips: Vec, - pub current_time: i64 + pub current_time: i64, + pub query_str: String, + pub stop_id: String, } -pub fn build_timetables( - directions: Vec, - trips: Vec, -) -> Vec { +#[derive(askama::Template)] +#[template(path = "stop_search_results.html")] +pub struct StopSearchResults { + pub results: Vec +} + +pub fn build_timetables(directions: Vec, trips: Vec) -> Vec { let mut results = Vec::new(); for direction in directions { - let now_utc = chrono::Utc::now(); - let now = now_utc.with_timezone(&New_York); + let now_utc = chrono::Utc::now(); + let now = now_utc.with_timezone(&New_York); let naive_time = now.time(); let seconds_since_midnight = naive_time.num_seconds_from_midnight(); @@ -119,24 +131,22 @@ pub fn build_timetables( } } - let trip_ids: Vec = direction_trips - .iter() - .map(|t| t.trip_id.clone()) - .collect(); + let trip_ids: Vec = direction_trips.iter().map(|t| t.trip_id.clone()).collect(); let live_trips: Vec = direction_trips .iter() .map(|t| t.tracking_data.clone()) .collect(); - let mut stop_map: BTreeMap>)> = BTreeMap::new(); for (trip_index, trip) in direction_trips.iter().enumerate() { for stop in &trip.schedule { - let entry = stop_map - .entry(stop.stop.id.clone()) - .or_insert((stop.stop_sequence, stop.stop.name.clone(), vec![None; direction_trips.len()])); + let entry = stop_map.entry(stop.stop.id.clone()).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 entry.0 = entry.0.max(stop.stop_sequence); @@ -147,15 +157,17 @@ pub fn build_timetables( let mut rows: Vec = stop_map .into_iter() - .map(|(stop_id, (stop_sequence, stop_name, times))| TimetableStopRow { - stop_id, - stop_sequence, - stop_name, - times, - }) + .map( + |(stop_id, (stop_sequence, stop_name, times))| TimetableStopRow { + stop_id, + stop_sequence, + stop_name, + times, + }, + ) .collect(); - rows.sort_by(| a, b| { + rows.sort_by(|a, b| { if a.stop_sequence < b.stop_sequence { Ordering::Less } else { @@ -164,11 +176,11 @@ pub fn build_timetables( }); results.push(TimetableDirection { - direction: direction.clone(), + direction: direction.clone(), trip_ids, rows, - tracking_data: live_trips , - next_id + tracking_data: live_trips, + next_id, }); } @@ -176,21 +188,23 @@ pub fn build_timetables( } mod filters { - pub fn format_load_time( - nanos: &u128, - _: &dyn askama::Values, - ) -> askama::Result { + use askama::filter_fn; + + #[filter_fn] + pub fn format_load_time(nanos: &u128, _: &dyn askama::Values) -> askama::Result { if *nanos >= 1000000000 { - return Ok(format!("{}s", (nanos/1000000000))); + return Ok(format!("{}s", (nanos / 1000000000))); } else if *nanos >= 1000000 { - return Ok(format!("{}ms", nanos/1000000)); - } if *nanos >= 1000 { - return Ok(format!("{}us", nanos/1000)); + return Ok(format!("{}ms", nanos / 1000000)); + } + if *nanos >= 1000 { + return Ok(format!("{}us", nanos / 1000)); } else { return Ok(format!("{}ns", nanos)); } } + #[filter_fn] pub fn format_time( seconds_since_midnight: &i64, _: &dyn askama::Values, @@ -211,7 +225,8 @@ mod filters { let minutes = total_minutes % 60; Ok(format!("{}:{:02} {}", hours, minutes, ampm)) } - + + #[filter_fn] pub fn format_time_with_seconds( seconds_since_midnight: &i64, _: &dyn askama::Values, diff --git a/web/templates/index.html b/web/templates/index.html new file mode 100644 index 0000000..96351ee --- /dev/null +++ b/web/templates/index.html @@ -0,0 +1,17 @@ +

SEPTASTIC!

+

+ A fantastic way to ride SEPTA +

+

+ SEPTASTIC is a website which provides information about riding SEPTA. It's + source code is available at + git.nickorlow.com +

+ +

+ This website is mostly for personal use, and thus the interface and data + is tailored to my SEPTA riding experience. This manifests in a couple of + weird things, such as the non-existent 'Susquehanna Transit Center'. +

+ + diff --git a/web/templates/layout.html b/web/templates/layout.html new file mode 100644 index 0000000..bd97b76 --- /dev/null +++ b/web/templates/layout.html @@ -0,0 +1,103 @@ + + + + {% if let Some(title) = page_title %} + {{ title }} | SEPTASTIC + {% else %} + SEPTASTIC + {% endif %} + {% if let Some(desc) = page_desc %} + + {% else %} + + {% endif %} + + + + + + + + + + {% if widescreen %} +
+ {% else %} +
+ {% endif %} +
This website is not run by SEPTA. Data may be inaccurate.
+ +
+ {{ content|safe }} +
+
+
+
+

+ SEPTASTIC! +

+

+ Copyright © Nicholas Orlowsky 2025 +

+ {% if let Some(load_time) = load_time_ms %} +

+ Data loaded in {{ *load_time | format_load_time }} +

+ {% endif %} +

+ Total load time +

+
+
+ {% if widescreen %} + [ disable widescreen ] + {% else %} + [ enable widescreen ] + {% endif %} +
+
+ +
+
+ + diff --git a/web/templates/route.html b/web/templates/route.html new file mode 100644 index 0000000..3d66b02 --- /dev/null +++ b/web/templates/route.html @@ -0,0 +1,138 @@ +{%- import "route_symbol.html" as scope -%} + +
+ {% call scope::route_symbol(route) %} + {% endcall %} +

{{ route.name }}

+
+{% for timetable in timetables %} +
+ +
+

{{ timetable.direction.direction | capitalize }} to

+

{{ timetable.direction.direction_destination }}

+
+
+
+ + + + + {% for trip_id in timetable.trip_ids %} + {% if let Some(next_id_v) = timetable.next_id %} + {% if next_id_v == trip_id %} + + {% endfor %} + + + + {% for row in timetable.rows %} + {% if let Some(filter_stop_v) = filter_stops %} + {% if !filter_stop_v.contains(&row.stop_id) %} + {% continue %} + {% endif %} + {% endif %} + + + {% for time in row.times %} + {% if let Some(t) = time %} + {% let live_o = timetable.tracking_data[loop.index0] %} + {% if let Tracked(live) = live_o %} + {% let time = (t + (live.delay * 60.0) as i64) %} + + {% elif let TripTracking::Cancelled = live_o %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + {% endfor %} + + {% endfor %} + +
Stop + {% else %} + + {% endif %} + {% else %} + + {% endif %} + {{ trip_id }} +
{{ row.stop_name }} + {{ time | format_time }} + + {{ t | format_time }} + {{ t | format_time }}
+
+
+ {% endfor %} diff --git a/web/templates/route_symbol.html b/web/templates/route_symbol.html new file mode 100644 index 0000000..89f9d1f --- /dev/null +++ b/web/templates/route_symbol.html @@ -0,0 +1,13 @@ +{% macro route_symbol(route) %} + {% match route.route_type %} + {% when libseptastic::route::RouteType::Trolley | libseptastic::route::RouteType::SubwayElevated %} +
{{ route.short_name }}
+{% endwhen %} +{% when libseptastic::route::RouteType::RegionalRail %} +
{{ route.short_name }}
+{% endwhen %} +{% when libseptastic::route::RouteType::Bus | libseptastic::route::RouteType::TracklessTrolley %} +
{{ route.short_name }}
+{% endwhen %} +{% endmatch %} +{% endmacro %} diff --git a/web/templates/routes.html b/web/templates/routes.html new file mode 100644 index 0000000..d4c88e9 --- /dev/null +++ b/web/templates/routes.html @@ -0,0 +1,92 @@ +

Routes

+

Click on a route to see details and a schedule. Schedules in prevailing local time.

+
+ +

Regional Rail

+
+

For infrequent rail service to suburban locations

+ {% for route in rr_routes %} + + +

]

+
+ {% endfor %} +
+
+ +

Metro

+
+

+ For frequent rail service within Philadelphia and suburban locations +

+
+

[ Subway/Elevated

+

]

+
+ {% for route in subway_routes %} + + +

]

+
+ {% endfor %} +
+

[ Trolleys

+

]

+
+ {% for route in trolley_routes %} + + +

]

+
+ {% endfor %} +
+
+ +

Bus

+
+

+ For service of varying frequency within SEPTA's entire service area +

+ {% for route in bus_routes %} + + +

]

+
+ {% endfor %} +
+ diff --git a/web/templates/stop.html b/web/templates/stop.html new file mode 100644 index 0000000..637470f --- /dev/null +++ b/web/templates/stop.html @@ -0,0 +1,139 @@ +{%- import "route_symbol.html" as scope -%} +{%- import "stop_table.html" as stop_table -%} +
+

{{ stop.name }}

+
+

With service available on:

+
+ {% for route in routes %} +
+ {% call scope::route_symbol(route) %} + {% endcall %} +
+ {% endfor %} +
+
+ +

Filters

+
+
+
+
+
+ Route + {% for route in routes %} + {% for dir in route.directions %} + {% if let Some(fil) = filters && let Some(rts) = fil.routes %} + {% let route_filter_id = format!("{},{}", route.id, dir.direction) %} + + {% else %} + + {% endif %} + +
+ {% endfor %} + {% endfor %} + + +
+
+
+ Ride Options + {% if let Some(fil) = filters && let Some(lt) = fil.live_tracked %} + + {% else %} + + {% endif %} + +
+ {% if let Some(fil) = filters && let Some(sc) = fil.scheduled %} + + {% else %} + + {% endif %} + +
+
+
+ Crowding + {% for avail in SeatAvailability::iter() %} + {% if let Some(fil) = filters && let Some(crd) = fil.crowding %} + + {% else %} + + {% endif %} + +
+ {% endfor %} + {% if let Some(fil) = filters && let Some(uc) = fil.unknown_crowding %} + + {% else %} + + {% endif %} + +
+
+
+ +
+
+
+
+ {% call stop_table::stop_table(trips, current_time, stop.id, query_str) %} + {% endcall %} +
diff --git a/web/templates/stop_search_results.html b/web/templates/stop_search_results.html new file mode 100644 index 0000000..9c16d43 --- /dev/null +++ b/web/templates/stop_search_results.html @@ -0,0 +1,12 @@ +{% for stop in results %} + + +

]

+
+{% endfor %} + +{% if results.len() == 0 %} +

No results found

+{% endif %} diff --git a/web/templates/stop_table.html b/web/templates/stop_table.html new file mode 100644 index 0000000..7eeb6fa --- /dev/null +++ b/web/templates/stop_table.html @@ -0,0 +1,69 @@ +{%- import "route_symbol.html" as scope -%} +{% macro stop_table(trips, current_time, stop_id, query_str) %} +
+ + + + + + + + + + + {% for trip in trips %} + + + + + {% if let Tracked(tracked_trip) = trip.trip.tracking_data %} + + {% else %} + + {% endif %} + {% if let Tracked(tracked_trip) = trip.trip.tracking_data %} + + {% else %} + + {% endif %} + + {% if let Tracked(tracked_trip) = trip.trip.tracking_data %} + {% if let Some(seat_avail) = tracked_trip.seat_availability %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + + {% endfor %} + + + +
ROUTEDESTINATIONBOARDING AREATIMEVEHICLETRIPCROWDING
+ {% call scope::route_symbol(trip.trip.route) %} + {% endcall %} + +

{{ trip.trip.direction.direction_destination }}

+
+

{{ trip.perspective_stop.platform.name }}

+
+

{{ &trip.perspective_stop.get_arrival_time(&tracked_trip) | format_time }}

+

+ {{ ( trip.perspective_stop.get_arrival_time(&tracked_trip) - current_time) / 60 }} mins +

+

{{ tracked_trip.delay.round() }} late

+
+

{{ trip.perspective_stop.arrival_time | format_time }}

+

+ {{ (trip.perspective_stop.arrival_time - current_time) / 60 }} mins +

+
{{ tracked_trip.vehicle_ids.join(", ") }}-{{ trip.trip.trip_id }}{{ seat_avail.to_human_string() }}N/A-
+

Updated at: {{ current_time | format_time_with_seconds }}

+
+
+{% endmacro %} diff --git a/web/templates/stop_table_impl.html b/web/templates/stop_table_impl.html new file mode 100644 index 0000000..3219ed3 --- /dev/null +++ b/web/templates/stop_table_impl.html @@ -0,0 +1,3 @@ +{%- import "stop_table.html" as stop_table -%} +{% call stop_table::stop_table(trips, current_time, stop_id, query_str) %} +{% endcall %} diff --git a/web/templates/stops.html b/web/templates/stops.html new file mode 100644 index 0000000..c505e5d --- /dev/null +++ b/web/templates/stops.html @@ -0,0 +1,41 @@ +

Stops

+

Click on a route to see details and a schedule. Schedules in prevailing local time.

+
+ +

Transit Centers

+
+

Hubs to connect between different modes of transit

+ {% for stop in tc_stops %} + + +

]

+
+ {% endfor %} +
+
+ +

Other Stops

+
+

SEPTA has 13,000+ stops, search for yours here

+ +
+
+
+