From 9297006ab3f95121e79aee6e0953d7a8a1b62757 Mon Sep 17 00:00:00 2001 From: Nicholas Orlowsky Date: Mon, 16 Feb 2026 21:36:36 -0500 Subject: [PATCH] cleanup --- .direnv/nix-profile-25.11-sizirny50f893gx0 | 2 +- .direnv/nix-profile-25.11-sizirny50f893gx0.rc | 57 +- Dockerfile | 18 +- ...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 | 253 -------- api/src/main.rs | 122 ---- api/src/services/gtfs_pull.rs | 425 ------------- 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 | 143 ----- api/templates/route_symbol.html | 19 - api/templates/routes.html | 60 -- api/templates/stop.html | 100 --- api/templates/stop_table.html | 73 --- 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 | 9 +- libseptastic/src/route_stop.rs | 2 +- libseptastic/src/schedule_day.rs | 2 +- libseptastic/src/stop.rs | 13 +- libseptastic/src/stop_schedule.rs | 73 ++- shell.nix | 9 +- {api => web}/.gitignore | 1 + {api => web}/Cargo.lock | 2 +- {api => web}/Cargo.toml | 2 +- {api => web}/assets/style.css | 0 {api => web}/config.yaml | 4 - web/src/controllers/index.rs | 10 + {api => web}/src/controllers/mod.rs | 1 + web/src/controllers/route.rs | 137 ++++ web/src/controllers/stop.rs | 281 ++++++++ {api => web}/src/database.rs | 0 web/src/main.rs | 66 ++ {api => web}/src/routing.rs | 0 web/src/services/gtfs_pull.rs | 601 ++++++++++++++++++ web/src/services/mod.rs | 2 + {api => web}/src/services/trip_tracking.rs | 170 ++--- web/src/session_middleware.rs | 69 ++ {api => web}/src/templates.rs | 102 ++- web/templates/index.html | 13 + web/templates/layout.html | 103 +++ web/templates/route.html | 138 ++++ web/templates/route_symbol.html | 13 + web/templates/routes.html | 93 +++ web/templates/stop.html | 139 ++++ web/templates/stop_table.html | 69 ++ {api => web}/templates/stop_table_impl.html | 1 - {api => web}/templates/stops.html | 16 +- 58 files changed, 2032 insertions(+), 2074 deletions(-) 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 example.env rename {api => web}/.gitignore (65%) rename {api => web}/Cargo.lock (99%) rename {api => web}/Cargo.toml (97%) rename {api => web}/assets/style.css (100%) rename {api => web}/config.yaml (87%) 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 (59%) create mode 100644 web/src/session_middleware.rs rename {api => web}/src/templates.rs (75%) 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_table.html rename {api => web}/templates/stop_table_impl.html (99%) rename {api => web}/templates/stops.html (64%) 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/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 116e1d3..0000000 --- a/api/src/controllers/stop.rs +++ /dev/null @@ -1,253 +0,0 @@ -use actix_web::{HttpRequest, HttpResponse, Responder, get, guard::Header, http::header::Header, web::{self, Data}}; -use anyhow::anyhow; -use askama::Template; -use chrono::{NaiveTime, TimeDelta, Timelike}; -use chrono_tz::America::New_York; -use libseptastic::stop_schedule::{self, LiveTrip, SeatAvailability, Trip, TripTracking}; -use log::info; -use serde::{Deserialize, Serialize}; -use serde_qs::actix::QsQuery; -use std::{collections::{BTreeSet, 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, 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 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 && filter.trip_matches(trip) { - 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 -} -#[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("/stop/{stop_id}/table")] -async fn get_stop_table_html(req: HttpRequest, 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, - filters: Some(query.0.clone()), - query_str, - stop_id: stop_id.to_string() - }.render().unwrap()) - } - else { - HttpResponse::InternalServerError().body("Error") - } -} - -#[get("/stop/{stop_id}")] -async fn get_stop_html(req: HttpRequest, state: Data>, path: web::Path, query: QsQuery) -> impl Responder { - crate::perform_action(req, move || { - let statex = state.clone(); - let pathx = path.clone(); - let queryx = query.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, &queryx).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: BTreeSet::from_iter(routes.into_iter()), - trips: filtered_trips, - current_time: cur_time, - filters: Some(queryx.0.clone()), - query_str: serde_qs::Config::new().array_format(serde_qs::ArrayFormat::Unindexed).serialize_string(&queryx.0).unwrap(), - }, - 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 dcb5574..0000000 --- a/api/src/services/gtfs_pull.rs +++ /dev/null @@ -1,425 +0,0 @@ -use std::{cmp::Ordering, collections::{HashMap, HashSet, hash_map::Entry}, 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::{error, info, warn}; -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>, - pub directions: 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() ,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(), 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); - info!("{}", global_rt_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/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 85cfba4..0000000 --- a/api/templates/route.html +++ /dev/null @@ -1,143 +0,0 @@ -{%- 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/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 472ccd3..0000000 --- a/api/templates/stop.html +++ /dev/null @@ -1,100 +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) %} - {% 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/api/templates/stop_table.html b/api/templates/stop_table.html deleted file mode 100644 index 6118618..0000000 --- a/api/templates/stop_table.html +++ /dev/null @@ -1,73 +0,0 @@ -{%- 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/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 4264f9f..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, PartialOrd, Ord)] +#[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 c94ea9a..62d04e4 100644 --- a/libseptastic/src/route.rs +++ b/libseptastic/src/route.rs @@ -9,7 +9,7 @@ pub enum RouteType { SubwayElevated, RegionalRail, Bus, - TracklessTrolley + TracklessTrolley, } #[derive(::sqlx::FromRow, Serialize, Deserialize, Debug, Clone)] @@ -19,12 +19,12 @@ pub struct Route { pub color_hex: String, pub route_type: RouteType, pub id: String, - pub directions: Vec + pub directions: Vec, } impl PartialEq for Route { fn eq(&self, other: &Self) -> bool { - self.id == other.id + self.id == other.id } } @@ -42,10 +42,9 @@ impl PartialOrd for Route { } } - #[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 f9a1474..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, Serializer, de::Error}; +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 { @@ -99,17 +110,18 @@ pub enum SeatAvailability { CrushedStandingRoomOnly = 3, FewSeats = 2, ManySeats = 1, - Empty = 0 + Empty = 0, } impl<'de> Deserialize<'de> for SeatAvailability { fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de> { + 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("")) + None => Err(serde::de::Error::custom("")), }; } } @@ -123,10 +135,15 @@ impl Serialize for SeatAvailability { } } - impl SeatAvailability { pub fn iter() -> Vec { - vec![Self::Empty, Self::ManySeats, Self::FewSeats, Self::CrushedStandingRoomOnly, Self::Full] + vec![ + Self::Empty, + Self::ManySeats, + Self::FewSeats, + Self::CrushedStandingRoomOnly, + Self::Full, + ] } pub fn to_string(&self) -> String { @@ -138,14 +155,14 @@ impl SeatAvailability { 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" + Self::Empty => "Empty", }) } @@ -156,10 +173,10 @@ impl SeatAvailability { "FEW_SEATS_AVAILABLE" => Some(Self::FewSeats), "MANY_SEATS_AVAILABLE" => Some(Self::ManySeats), "EMPTY" => Some(Self::Empty), - _ => None + _ => None, } } - + pub fn from_opt_string(opt_str: &Option) -> Option { if let Some(str) = &opt_str { Self::from_string(str) 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 74f75f3..31daf09 100644 --- a/api/Cargo.lock +++ b/web/Cargo.lock @@ -2779,7 +2779,7 @@ dependencies = [ ] [[package]] -name = "septastic_api" +name = "septastic_web" version = "0.1.0" dependencies = [ "actix-cors", diff --git a/api/Cargo.toml b/web/Cargo.toml similarity index 97% rename from api/Cargo.toml rename to web/Cargo.toml index c8b3382..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" diff --git a/api/assets/style.css b/web/assets/style.css similarity index 100% rename from api/assets/style.css rename to web/assets/style.css diff --git a/api/config.yaml b/web/config.yaml similarity index 87% rename from api/config.yaml rename to web/config.yaml index 84f020c..93b58ea 100644 --- a/api/config.yaml +++ b/web/config.yaml @@ -5,8 +5,6 @@ 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: multiplatform_stops: - id: 'WTC' @@ -35,5 +33,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..1cd8012 --- /dev/null +++ b/web/src/controllers/stop.rs @@ -0,0 +1,281 @@ +use crate::{ + AppState, + session_middleware::{SessionResponder, SessionResponse}, + templates::TripPerspective, +}; +use actix_web::{ + HttpResponse, Responder, get, + web::{self, Data}, +}; +use askama::Template; +use chrono::{TimeDelta, Timelike}; +use chrono_tz::America::New_York; +use libseptastic::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 }, + ) +} + +#[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..e929c0c --- /dev/null +++ b/web/src/main.rs @@ -0,0 +1,66 @@ +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_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::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::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..4ea5e8f --- /dev/null +++ b/web/src/services/gtfs_pull.rs @@ -0,0 +1,601 @@ +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 Annotations { + pub multiplatform_stops: 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<()> { + 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); + info!("{}", global_rt_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 59% rename from api/src/services/trip_tracking.rs rename to web/src/services/trip_tracking.rs index 9bccb70..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 log::{error, info}; -use serde::{Serialize, Deserialize, Deserializer}; use libseptastic::stop_schedule::{LiveTrip, SeatAvailability, TripTracking}; +use log::{error, info}; +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("("); @@ -89,7 +88,7 @@ impl TripTrackingService { separated.push_bind(live_data.heading); separated.push_bind(match &live_data.seat_availability { Some(s) => Some(s.to_string()), - None => None + None => None, }); separated.push_bind(live_data.vehicle_ids.clone()); separated.push_bind(live_data.trip_id.clone()); @@ -111,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; @@ -134,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" { @@ -158,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: 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![] + 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; @@ -217,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 75% rename from api/src/templates.rs rename to web/src/templates.rs index 7ac8dc7..3d1caae 100644 --- a/api/src/templates.rs +++ b/web/src/templates.rs @@ -1,9 +1,15 @@ -use chrono_tz::America::New_York; -use libseptastic::{direction::Direction, stop_schedule::{Trip, TripTracking, SeatAvailability}}; -use std::{cmp::Ordering, collections::{BTreeMap, BTreeSet}}; -use serde::{Serialize}; -use libseptastic::stop_schedule::TripTracking::Tracked; use chrono::Timelike; +use chrono_tz::America::New_York; +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; @@ -14,7 +20,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)] @@ -22,7 +28,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)] @@ -31,7 +37,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)] @@ -42,32 +48,28 @@ 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)] @@ -78,7 +80,7 @@ pub struct StopTemplate { pub trips: Vec, pub current_time: i64, pub filters: Option, - pub query_str: String + pub query_str: String, } #[derive(askama::Template)] @@ -86,20 +88,16 @@ pub struct StopTemplate { pub struct StopTableTemplate { pub trips: Vec, pub current_time: i64, - pub filters: Option, pub query_str: String, - pub stop_id: String + pub stop_id: String, } -pub fn build_timetables( - directions: Vec, - trips: Vec, -) -> 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(); @@ -126,24 +124,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); @@ -154,15 +150,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 { @@ -171,11 +169,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, }); } @@ -186,16 +184,14 @@ mod filters { use askama::filter_fn; #[filter_fn] - pub fn format_load_time( - nanos: &u128, - _: &dyn askama::Values, - ) -> askama::Result { + 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)); } @@ -222,7 +218,7 @@ 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, diff --git a/web/templates/index.html b/web/templates/index.html new file mode 100644 index 0000000..5c40bdf --- /dev/null +++ b/web/templates/index.html @@ -0,0 +1,13 @@ +

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/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..3785f32 --- /dev/null +++ b/web/templates/routes.html @@ -0,0 +1,93 @@ +

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_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/api/templates/stop_table_impl.html b/web/templates/stop_table_impl.html similarity index 99% rename from api/templates/stop_table_impl.html rename to web/templates/stop_table_impl.html index a3a6bf9..3219ed3 100644 --- a/api/templates/stop_table_impl.html +++ b/web/templates/stop_table_impl.html @@ -1,4 +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/api/templates/stops.html b/web/templates/stops.html similarity index 64% rename from api/templates/stops.html rename to web/templates/stops.html index e1bf30a..b64ba08 100644 --- a/api/templates/stops.html +++ b/web/templates/stops.html @@ -1,17 +1,19 @@

Stops

- -

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

- +

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

-

Transit Centers

+ +

Transit Centers

+

Hubs to connect between different modes of transit

{% for stop in tc_stops %} - -

]

+
+ +

]

{% endfor %}
-