cleanup
This commit is contained in:
parent
b7ec6a292f
commit
9297006ab3
58 changed files with 2032 additions and 2074 deletions
|
|
@ -1 +1 @@
|
|||
/nix/store/jyg5pzxlxkbvzy1wb808kc5idmbij4r6-env-env
|
||||
/nix/store/vvk9w0m05l0yr8ahyib95sg4dmzm354c-env-env
|
||||
|
|
@ -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 ()
|
||||
{
|
||||
|
||||
|
|
|
|||
18
Dockerfile
18
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"]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Canvas Curve Full Width</title>
|
||||
<style>
|
||||
body { margin: 0; }
|
||||
canvas { display: block; background: white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="curveCanvas" height="180"></canvas>
|
||||
|
||||
<script>
|
||||
const canvas = document.getElementById('curveCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Set canvas width to window width
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
|
||||
const rightEnd = canvas.width;
|
||||
const flatStart = 0;
|
||||
const flatEnd = 450;
|
||||
const curveStart = flatEnd;
|
||||
const curveControl1 = 475;
|
||||
const curveControl2 = 525;
|
||||
const curvePeak = 550;
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Blue stripe (top)
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(flatStart, 90);
|
||||
ctx.lineTo(flatEnd, 90);
|
||||
ctx.bezierCurveTo(curveControl1, 90, curveControl2, 30, curvePeak, 30);
|
||||
ctx.lineTo(rightEnd, 30);
|
||||
ctx.lineTo(rightEnd, 60);
|
||||
ctx.lineTo(curvePeak, 60);
|
||||
ctx.bezierCurveTo(curveControl2, 60, curveControl1, 120, flatEnd, 120);
|
||||
ctx.lineTo(flatStart, 120);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = '#007ac2';
|
||||
ctx.fill();
|
||||
|
||||
// Orange stripe (bottom)
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(flatStart, 120);
|
||||
ctx.lineTo(flatEnd, 120);
|
||||
ctx.bezierCurveTo(curveControl1, 120, curveControl2, 60, curvePeak, 60);
|
||||
ctx.lineTo(rightEnd, 60);
|
||||
ctx.lineTo(rightEnd, 90);
|
||||
ctx.lineTo(curvePeak, 90);
|
||||
ctx.bezierCurveTo(curveControl2, 90, curveControl1, 150, flatEnd, 150);
|
||||
ctx.lineTo(flatStart, 150);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = '#f15a22';
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
resizeCanvas();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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<Arc<AppState>>) -> impl Responder {
|
||||
crate::perform_action(req, move || {
|
||||
let statex = state.clone();
|
||||
async move {
|
||||
let start_time = Instant::now();
|
||||
|
||||
let all_routes: Vec<libseptastic::route::Route> = statex.gtfs_service.get_routes();
|
||||
let rr_routes = all_routes.clone().into_iter().filter(|x| x.route_type == RouteType::RegionalRail).collect();
|
||||
let 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<Arc<AppState>>) -> impl Responder {
|
||||
let all_routes: Vec<libseptastic::route::Route> = 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<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct RouteResponse {
|
||||
pub route: libseptastic::route::Route,
|
||||
pub directions: Vec<libseptastic::direction::Direction>,
|
||||
pub schedule: Vec<Trip>
|
||||
}
|
||||
|
||||
async fn get_route_info(route_id: String, state: Data<Arc<AppState>>) -> ::anyhow::Result<RouteResponse> {
|
||||
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<Arc<AppState>>) -> 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<String> = get_stops_near(home_cds, &all_stops);
|
||||
// let dest_stops: HashSet<String> = 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<Arc<AppState>>, req: HttpRequest, info: web::Query<RouteQueryParams>, path: web::Path<String>) -> 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<Vec<String>> = 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<Arc<AppState>>, path: web::Path<String>) -> 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<Arc<AppState>>, path: web::Path<String>) -> impl Responder {
|
||||
let route_id = path.into_inner();
|
||||
let route_r: anyhow::Result<i32> = Ok(5);
|
||||
|
||||
if let Ok(route) = route_r {
|
||||
HttpResponse::Ok().json(route)
|
||||
} else {
|
||||
HttpResponse::InternalServerError().body("Error")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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<Arc<AppState>>) -> 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<Arc<AppState>>,stop: &libseptastic::stop::Stop, filter: &StopFilter) -> Vec<TripPerspective> {
|
||||
|
||||
let routes: Vec<libseptastic::route::Route> = 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<String> = 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<TripPerspective> = 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<HashSet<String>>,
|
||||
pub live_tracked: Option<bool>,
|
||||
pub scheduled: Option<bool>,
|
||||
pub crowding: Option<HashSet<SeatAvailability>>,
|
||||
pub unknown_crowding: Option<bool>
|
||||
}
|
||||
|
||||
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<Arc<AppState>>, path: web::Path<String>, query: QsQuery<StopFilter>) -> 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<Arc<AppState>>, path: web::Path<String>, query: QsQuery<StopFilter>) -> 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<libseptastic::route::Route> = 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
|
||||
}
|
||||
122
api/src/main.rs
122
api/src/main.rs
|
|
@ -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<bool>
|
||||
}
|
||||
|
||||
pub async fn perform_action<F, Fut, T >(req: HttpRequest, func: F) -> impl Responder
|
||||
where T: Template,
|
||||
F: Fn() -> Fut,
|
||||
Fut: Future<Output = anyhow::Result<ContentTemplate<T>>> + '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::<LocalStateQuery>::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::<gtfs_pull::Config>(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(())
|
||||
}
|
||||
|
|
@ -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<String>,
|
||||
pub prefix: String
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug,Clone)]
|
||||
struct MultiplatformStopConfig {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub platform_station_ids: Vec<String>
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug,Clone)]
|
||||
struct Annotations {
|
||||
pub multiplatform_stops: Vec<MultiplatformStopConfig>
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
pub struct Config {
|
||||
pub gtfs_zips: Vec<GtfsSource>,
|
||||
pub annotations: Annotations
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct GtfsFile {
|
||||
pub source: GtfsSource,
|
||||
pub hash: Option<String>
|
||||
}
|
||||
|
||||
struct TransitData {
|
||||
pub routes: HashMap<String, Arc<libseptastic::route::Route>>,
|
||||
pub agencies: HashMap<String, libseptastic::agency::Agency>,
|
||||
pub trips: HashMap<String, Vec<libseptastic::stop_schedule::Trip>>,
|
||||
pub stops: HashMap<String, Arc<libseptastic::stop::Stop>>,
|
||||
pub platforms: HashMap<String, Arc<libseptastic::stop::Platform>>,
|
||||
pub calendar_days: HashMap<String, Arc<libseptastic::stop_schedule::CalendarDay>>,
|
||||
pub directions: HashMap<String, Vec<Arc<libseptastic::direction::Direction>>>,
|
||||
|
||||
// extended lookup methods
|
||||
pub route_id_by_stops: HashMap<String, HashSet<String>>,
|
||||
pub stops_by_route_id: HashMap<String, HashSet<String>>,
|
||||
pub stops_by_platform_id: HashMap<String, Arc<libseptastic::stop::Stop>>
|
||||
}
|
||||
|
||||
struct GtfsPullServiceState {
|
||||
pub gtfs_files: Vec<GtfsFile>,
|
||||
pub tmp_dir: PathBuf,
|
||||
pub ready: bool,
|
||||
pub annotations: Annotations,
|
||||
pub transit_data: TransitData
|
||||
}
|
||||
|
||||
pub struct GtfsPullService {
|
||||
state: Arc<Mutex<GtfsPullServiceState>>
|
||||
}
|
||||
|
||||
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<libseptastic::route::Route> {
|
||||
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<libseptastic::route::Route> {
|
||||
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<String, libseptastic::route::Route> {
|
||||
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<String, Arc<libseptastic::stop::Stop>> {
|
||||
let l_state = self.state.lock().unwrap();
|
||||
l_state.transit_data.stops.clone()
|
||||
}
|
||||
|
||||
pub fn get_all_trips(&self) -> HashMap<String, Vec<libseptastic::stop_schedule::Trip>> {
|
||||
let l_state = self.state.lock().unwrap();
|
||||
l_state.transit_data.trips.clone()
|
||||
}
|
||||
|
||||
pub fn get_routes_at_stop(&self, id: &String) -> HashSet<String> {
|
||||
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<String> {
|
||||
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<libseptastic::stop::Stop> {
|
||||
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<Vec<libseptastic::stop_schedule::Trip>> {
|
||||
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<Mutex<GtfsPullServiceState>>) -> 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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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::<String, TripTracking>
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct GtfsRtFile {
|
||||
pub uri: String,
|
||||
pub hash: Option<String>
|
||||
}
|
||||
|
||||
pub struct TripTrackingService {
|
||||
state: Arc<Mutex<TripTrackingServiceState>>,
|
||||
//pub gtfs_files: Vec<GtfsRtFile>
|
||||
}
|
||||
|
||||
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<libseptastic::stop_schedule::Trip>) {
|
||||
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<Mutex<TripTrackingServiceState>>) -> 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<gtfs_realtime::FeedMessage, prost::DecodeError> = prost::Message::decode(bytes.as_ref());
|
||||
let data = data.unwrap();
|
||||
|
||||
println!("{:#?}", data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
pub mod trip_tracking;
|
||||
pub mod gtfs_pull;
|
||||
pub mod gtfs_rt;
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<h1>SEPTASTIC!</h1>
|
||||
<p><i>A fantastic way to ride SEPTA</i></p>
|
||||
|
||||
<p style="margin-top: 25px;">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 25px; margin-bottom: 25px;">
|
||||
Currently, all this website has is <a href="/routes">timetables for every
|
||||
SEPTA route</a>. More to come soon!
|
||||
</p>
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
{% if let Some(title) = page_title %}
|
||||
<title>{{ title }} | SEPTASTIC</title>
|
||||
{% else %}
|
||||
<title>SEPTASTIC</title>
|
||||
{% endif %}
|
||||
|
||||
{% if let Some(desc) = page_desc %}
|
||||
<meta name="{{ desc }}" />
|
||||
{% else %}
|
||||
<meta name="SEPTASTIC" />
|
||||
{% endif %}
|
||||
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
setTimeout(() => {
|
||||
const perfData = window.performance.timing;
|
||||
const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart;
|
||||
|
||||
const loadTimeElement = document.getElementById('js_load_time');
|
||||
loadTimeElement.textContent += ` ${pageLoadTime}ms`;
|
||||
}, 0); // Minimal delay to wait for `loadEventEnd` to be populated
|
||||
};
|
||||
</script>
|
||||
<noscript>
|
||||
<style>
|
||||
.js-only {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
<style>
|
||||
.silverliner-svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 200px; /* Fixed height matching the viewBox */
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
{% if widescreen %}
|
||||
<div class="body">
|
||||
{% else %}
|
||||
<div class="body body-small">
|
||||
{% endif %}
|
||||
<div style="background-color: #ff0000; color: #ffffff; font-size: .7em; padding: 5px; margin-bottom: 10px; margin-top: 10px;">
|
||||
This website is not run by SEPTA. Data may be inaccurate.
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<div>
|
||||
<a class="nav-link" href="/">[ Home ]</a>
|
||||
<a class="nav-link" href="/routes">[ Routes ]</a>
|
||||
<a class="nav-link" href="/stops">[ Stops ]</a>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<hr/>
|
||||
|
||||
{{ content|safe }}
|
||||
|
||||
<footer>
|
||||
<hr />
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<div>
|
||||
<p style="margin-bottom: 0px; margin-top:0px;"><b>SEPTASTIC!</b></p>
|
||||
<p style="margin-bottom: 0px;margin-top: 0px;">
|
||||
<small>Copyright © <a href="https://nickorlow.com">Nicholas Orlowsky</a> 2025</small>
|
||||
</p>
|
||||
{% if let Some(load_time) = load_time_ms %}
|
||||
<p style="marin-top: 5px; color: #555555;"><small><i>Data loaded in {{ *load_time | format_load_time }}</i></small></p>
|
||||
{% endif %}
|
||||
<p class="js-only" style="marin-top: 5px; color: #555555;"><small><i id="js_load_time">Total load time</i></small></p>
|
||||
</div>
|
||||
<div>
|
||||
{% if widescreen %}
|
||||
<a href="?widescreen=false"><small>[ disable widescreen ]</small></a>
|
||||
{% else %}
|
||||
<a href="?widescreen=true"><small>[ enable widescreen ]</small></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<noscript><p style="margin-top: 10px;"><small>[!] You do not have JavaScript enabled. Some features will be missing.</small></p></noscript>
|
||||
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
{%- import "route_symbol.html" as scope -%}
|
||||
<style>
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const scrollToNextColumn = (directionId) => {
|
||||
const target = document.getElementById("next-col-" + directionId);
|
||||
if (target) {
|
||||
const scrollContainer = target.closest(".tscroll");
|
||||
const firstCol = scrollContainer.querySelector("th:first-child");
|
||||
const firstColWidth = firstCol ? firstCol.offsetWidth : 0;
|
||||
|
||||
// Get the target's position relative to the scroll container
|
||||
const targetLeft = target.offsetLeft;
|
||||
|
||||
// Scroll so the target appears right after the sticky column
|
||||
scrollContainer.scrollLeft = targetLeft - firstColWidth;
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelectorAll("details[data-direction-id]").forEach(details => {
|
||||
const directionId = details.getAttribute("data-direction-id");
|
||||
|
||||
// Scroll immediately if details is already open
|
||||
if (details.open) {
|
||||
setTimeout(() => scrollToNextColumn(directionId), 50);
|
||||
}
|
||||
|
||||
// Also scroll when details is opened
|
||||
details.addEventListener("toggle", () => {
|
||||
if (details.open) {
|
||||
setTimeout(() => scrollToNextColumn(directionId), 50);
|
||||
}
|
||||
});
|
||||
});
|
||||
document.querySelectorAll(".train-direction-table").forEach((table) => {
|
||||
table.addEventListener("click", (e) => {
|
||||
const cell = e.target.closest("td, th");
|
||||
if (!cell) return;
|
||||
|
||||
// Clear previous highlights
|
||||
table.querySelectorAll("tr").forEach(row => row.classList.remove("highlight-row"));
|
||||
table.querySelectorAll("td, th").forEach(c => c.classList.remove("highlight-col"));
|
||||
|
||||
const row = cell.parentNode;
|
||||
const colIndex = Array.from(cell.parentNode.children).indexOf(cell);
|
||||
|
||||
// If it's the first column (row header)
|
||||
if (cell.cellIndex === 0 && cell.tagName === "TD") {
|
||||
row.classList.add("highlight-row");
|
||||
}
|
||||
// If it's a column header
|
||||
else if (row.parentNode.tagName === "THEAD") {
|
||||
table.querySelectorAll("tr").forEach(r => {
|
||||
const cell = r.children[colIndex];
|
||||
if (cell) cell.classList.add("highlight-col");
|
||||
});
|
||||
}
|
||||
// If it's a center cell
|
||||
else {
|
||||
row.classList.add("highlight-row");
|
||||
table.querySelectorAll("tr").forEach(r => {
|
||||
const cell = r.children[colIndex];
|
||||
if (cell) cell.classList.add("highlight-col");
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div style="display: flex; align-items: center;">
|
||||
{% call scope::route_symbol(route) %}
|
||||
{% endcall %}
|
||||
<h1 style="margin-left: 15px;">{{ route.name }}</h1>
|
||||
</div>
|
||||
|
||||
{% for timetable in timetables %}
|
||||
<details style="margin-top: 15px;" data-direction-id="{{ timetable.direction.direction }}">
|
||||
<summary>
|
||||
<div style="display: inline-block;">
|
||||
<h3>{{ timetable.direction.direction | capitalize }} to</h3>
|
||||
<h2>{{ timetable.direction.direction_destination }}</h2>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="tscroll">
|
||||
<table class="train-direction-table" style="margin-top: 5px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Stop</th>
|
||||
{% for trip_id in timetable.trip_ids %}
|
||||
{% if let Some(next_id_v) = timetable.next_id %}
|
||||
{% if next_id_v == trip_id %}
|
||||
<th class="next-col" id="next-col-{{ timetable.direction.direction }}">
|
||||
{% else %}
|
||||
<th>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<th>
|
||||
{% endif %}
|
||||
{{ trip_id }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in timetable.rows %}
|
||||
{% if let Some(filter_stop_v) = filter_stops %}
|
||||
{% if !filter_stop_v.contains(&row.stop_id) %}
|
||||
{% continue %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>{{ row.stop_name }}</td>
|
||||
{% 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) %}
|
||||
<td style="background-color: #003300">
|
||||
<span style="color: #22bb22"> {{ time | format_time }} </span>
|
||||
</td>
|
||||
{% elif let TripTracking::Cancelled = live_o %}
|
||||
<td style="color: #ff0000"><s>{{ t | format_time }}</s></td>
|
||||
{% else %}
|
||||
<td>{{ t | format_time }}</td>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<td>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{% macro route_symbol(route) %}
|
||||
{% match route.route_type %}
|
||||
{% when libseptastic::route::RouteType::Trolley | libseptastic::route::RouteType::SubwayElevated %}
|
||||
<div class="metro-container bg-{{ route.short_name }}">
|
||||
{{ route.short_name }}
|
||||
</div>
|
||||
{% endwhen %}
|
||||
{% when libseptastic::route::RouteType::RegionalRail %}
|
||||
<div class="rr-container">
|
||||
{{ route.short_name }}
|
||||
</div>
|
||||
{% endwhen %}
|
||||
{% when libseptastic::route::RouteType::Bus | libseptastic::route::RouteType::TracklessTrolley %}
|
||||
<div class="bus-container">
|
||||
{{ route.short_name }}
|
||||
</div>
|
||||
{% endwhen %}
|
||||
{% endmatch %}
|
||||
{% endmacro %}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
<h1>Routes</h1>
|
||||
|
||||
<p>Click on a route to see details and a schedule. Schedules in prevailing local time.</p>
|
||||
|
||||
<fieldset>
|
||||
<legend><h2>Regional Rail</h2></legend>
|
||||
<p style="margin-top: 10px; margin-bottom: 10px;">For infrequent rail service to suburban locations</p>
|
||||
{% for route in rr_routes %}
|
||||
<a href="/route/{{ route.id }}" style="display: flex; justify-content: space-between;">
|
||||
<p class="line-link">[ <b>{{ format!("{:7}", route.short_name) }}:</b> {{ route.name }} </p><p>]</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend><h2>Metro</h2></legend>
|
||||
<p style="margin-top: 10px; margin-bottom: 10px;">For frequent rail service within Philadelphia and suburban locations</p>
|
||||
<div class="lines-label" style="font-weight: bold; width: 100%; display: flex; justify-content: space-between;">
|
||||
<p>[ Subway/Elevated </p><p>]</p>
|
||||
</div>
|
||||
{% for route in subway_routes %}
|
||||
<a href="/route/{{ route.id }}" style="display: flex; justify-content: space-between;">
|
||||
<p class="line-link">[ <b>{{ format!("{:7}", route.short_name) }}:</b> {{ route.name }} </p><p>]</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
<div class="lines-label" style="font-weight: bold; width: 100%; display: flex; justify-content: space-between;">
|
||||
<p>[ Trolleys </p><p>]</p>
|
||||
</div>
|
||||
|
||||
{% for route in trolley_routes %}
|
||||
<a href="/route/{{ route.id }}" style="display: flex; justify-content: space-between;">
|
||||
<p class="line-link">[ <b>{{ format!("{:7}", route.short_name) }}:</b> {{ route.name }} </p><p>]</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend><h2>Bus</h2></legend>
|
||||
<p style="margin-top: 10px; margin-bottom: 10px;">For service of varying frequency within SEPTA's entire service area</p>
|
||||
{% for route in bus_routes %}
|
||||
<a href="/route/{{ route.id }}" style="display: flex; justify-content: space-between;">
|
||||
<p class="line-link">[ <b>{{ format!("{:7}", route.short_name) }}:</b> {{ route.name }} </p><p>]</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
<style>
|
||||
.line-link, .lines-label {
|
||||
white-space: pre;
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.lines-label {
|
||||
color: #ffffff;
|
||||
background-color: #000000;
|
||||
width: max-content;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
{%- import "route_symbol.html" as scope -%}
|
||||
{%- import "stop_table.html" as stop_table -%}
|
||||
|
||||
<div style="display: flex; align-items: center;">
|
||||
<h1>{{ stop.name }}</h1>
|
||||
</div>
|
||||
|
||||
<p>With service available on:</p>
|
||||
<div style="display: flex; justify-content: start; padding-top: 5px; padding-bottom: 5px; flex-wrap: wrap; gap: 5px;">
|
||||
{% for route in routes %}
|
||||
<div style="margin-right: 5px">
|
||||
{% call scope::route_symbol(route) %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<summary><p style="font-weight: bold; font-size: large;">Filters</p></summary>
|
||||
<form hx-trigger="submit" hx-get="/stop/{{ stop.id }}/table" hx-target="#nta-table" hx-swap="outerHTML" hx-push-url="/stop/{{ stop.id}}">
|
||||
<div style="margin: 5px; padding: 10px; background-color: #eee;">
|
||||
<div style="display: flex; flex-wrap: wrap;">
|
||||
<fieldset style="flex-grow: 1;">
|
||||
<legend>Route</legend>
|
||||
{% 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) %}
|
||||
<input type="checkbox" class="route-checkbox" name="routes" id="{{ route.id }},{{ dir.direction }}" value="{{ route.id }},{{ dir.direction}}" checked="{{ rts.contains(&*route_filter_id) }}">
|
||||
{% else %}
|
||||
<input type="checkbox" class="route-checkbox" name="routes" id="{{ route.id }},{{ dir.direction }}" value="{{ route.id }},{{ dir.direction}}" checked="true">
|
||||
{% endif %}
|
||||
|
||||
<label for="{{ route.id }},{{ dir.direction }}">
|
||||
<b>{{ route.short_name }}</b>: {{ dir.direction_destination }}
|
||||
</label>
|
||||
<br>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<input type="checkbox" id="master"
|
||||
hx-on:click="document.querySelectorAll('.route-checkbox').forEach(c => c.checked = this.checked)">
|
||||
<label for="master">Select/Deselect All</label>
|
||||
</fieldset>
|
||||
<div style="flex-grow: 1;">
|
||||
<fieldset>
|
||||
<legend>Ride Options</legend>
|
||||
{% if let Some(fil) = filters && let Some(lt) = fil.live_tracked %}
|
||||
<input type="checkbox" name="live_tracked" id="live_tracked" value="true" checked="{{ lt }}">
|
||||
{% else %}
|
||||
<input type="checkbox" name="live_tracked" id="live_tracked" value="true" checked="true">
|
||||
{% endif %}
|
||||
<label for="live-tracked">
|
||||
Live Tracked
|
||||
</label>
|
||||
<br>
|
||||
{% if let Some(fil) = filters && let Some(sc) = fil.scheduled %}
|
||||
<input type="checkbox" name="scheduled" id="scheduled" value="true" checked="{{ sc }}">
|
||||
{% else %}
|
||||
<input type="checkbox" name="scheduled" id="scheduled" value="true" checked="true">
|
||||
{% endif %}
|
||||
<label for="scheduled">
|
||||
Scheduled
|
||||
</label>
|
||||
<br>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Crowding</legend>
|
||||
{% for avail in SeatAvailability::iter() %}
|
||||
|
||||
{% if let Some(fil) = filters && let Some(crd) = fil.crowding %}
|
||||
<input type="checkbox" name="crowding" id="{{ avail.to_string() }}" value="{{ avail.to_string() }}" checked="{{ crd.contains(&avail) }}">
|
||||
{% else %}
|
||||
<input type="checkbox" name="crowding" id="{{ avail.to_string() }}" value="{{ avail.to_string() }}" checked="true">
|
||||
{% endif %}
|
||||
<label for="{{ avail.to_string() }}">
|
||||
{{ avail.to_human_string() }}
|
||||
</label>
|
||||
<br>
|
||||
{% endfor %}
|
||||
|
||||
{% if let Some(fil) = filters && let Some(uc) = fil.unknown_crowding %}
|
||||
<input type="checkbox" name="unknown_crowding" id="unknown_crowding" value="true" checked="{{ uc }}">
|
||||
{% else %}
|
||||
<input type="checkbox" name="unknown_crowding" id="unknown_crowding" value="true" checked="true">
|
||||
{% endif %}
|
||||
<label for="scheduled">
|
||||
Unknown
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<input type="submit" value="Apply">
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
<div style="overflow-x: scroll; max-width: 100%;">
|
||||
{% call stop_table::stop_table(trips, current_time, stop.id, query_str) %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
{%- import "route_symbol.html" as scope -%}
|
||||
|
||||
{% macro stop_table(trips, current_time, stop_id, query_str) %}
|
||||
<div id="nta-table" hx-get="/stop/{{ stop_id }}/table?{{ query_str }}" hx-trigger="every 5s" hx-swap="outer-html">
|
||||
<table class="train-direction-table">
|
||||
<tr>
|
||||
<th>ROUTE</th>
|
||||
<th>DESTINATION</th>
|
||||
<th>BOARDING AREA</th>
|
||||
<th>TIME</th>
|
||||
<th>VEHICLE</th>
|
||||
<th>TRIP</th>
|
||||
<th>CROWDING</th>
|
||||
</tr>
|
||||
{% for trip in trips %}
|
||||
<tr>
|
||||
<td>
|
||||
{% call scope::route_symbol(trip.trip.route) %}
|
||||
{% endcall %}
|
||||
</td>
|
||||
<td>
|
||||
<p>{{ trip.trip.direction.direction_destination }}</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>{{ trip.perspective_stop.platform.name }}</p>
|
||||
</td>
|
||||
{% if let Tracked(tracked_trip) = trip.trip.tracking_data %}
|
||||
<td style="color: #008800">
|
||||
<p style="font-size: small;">{{ &trip.perspective_stop.get_arrival_time(&tracked_trip) | format_time }}</p>
|
||||
<p style="font-size: x-small; font-style: italic;">{{ ( trip.perspective_stop.get_arrival_time(&tracked_trip) - current_time) / 60 }} mins</p>
|
||||
<p style="font-size: x-small; font-style: italic;">{{ tracked_trip.delay.round() }} late</p>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>
|
||||
<p style="font-size: small;">{{ trip.perspective_stop.arrival_time | format_time }}</p>
|
||||
<p style="font-size: x-small; font-style: italic;">{{ (trip.perspective_stop.arrival_time - current_time) / 60 }} mins</p>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if let Tracked(tracked_trip) = trip.trip.tracking_data %}
|
||||
<td>
|
||||
{{ tracked_trip.vehicle_ids.join(", ") }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td>
|
||||
-
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>{{ trip.trip.trip_id }}</td>
|
||||
{% if let Tracked(tracked_trip) = trip.trip.tracking_data %}
|
||||
{% if let Some(seat_avail) = tracked_trip.seat_availability %}
|
||||
<td>
|
||||
{{ seat_avail.to_human_string() }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td>
|
||||
N/A
|
||||
</td>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<td>
|
||||
-
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<p>Updated at: {{ current_time | format_time_with_seconds }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
|
@ -1 +0,0 @@
|
|||
DB_CONNSTR=
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<crate::direction::Direction>
|
||||
pub directions: Vec<crate::direction::Direction>,
|
||||
}
|
||||
|
||||
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<String>
|
||||
pub interlined_routes: Vec<String>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct ScheduleDay {
|
||||
pub date: String,
|
||||
pub service_id: String
|
||||
pub service_id: String,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Platform>),
|
||||
MultiPlatform(Vec<Arc<Platform>>)
|
||||
MultiPlatform(Vec<Arc<Platform>>),
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<crate::stop::Stop>,
|
||||
pub platform: Arc<Platform>
|
||||
pub platform: Arc<Platform>,
|
||||
}
|
||||
|
||||
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<Route>,
|
||||
pub route: Arc<Route>,
|
||||
pub trip_id: String,
|
||||
pub direction: Direction,
|
||||
pub tracking_data: TripTracking,
|
||||
pub schedule: Vec<StopSchedule>,
|
||||
pub calendar_day: Arc<CalendarDay>
|
||||
pub calendar_day: Arc<CalendarDay>,
|
||||
}
|
||||
|
||||
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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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<SeatAvailability> {
|
||||
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<String>) -> Option<SeatAvailability> {
|
||||
if let Some(str) = &opt_str {
|
||||
Self::from_string(str)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
with import <nixpkgs> {};
|
||||
stdenv.mkDerivation {
|
||||
name = "env";
|
||||
nativeBuildInputs = [ pkg-config postgresql_14 ];
|
||||
nativeBuildInputs = [
|
||||
pkg-config
|
||||
postgresql_14
|
||||
rustfmt
|
||||
cargo
|
||||
djlint
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
cryptsetup
|
||||
protobuf
|
||||
|
|
|
|||
1
api/.gitignore → web/.gitignore
vendored
1
api/.gitignore → web/.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
target/
|
||||
.env
|
||||
.sqlx/
|
||||
2
api/Cargo.lock → web/Cargo.lock
generated
2
api/Cargo.lock → web/Cargo.lock
generated
|
|
@ -2779,7 +2779,7 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "septastic_api"
|
||||
name = "septastic_web"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"actix-cors",
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "septastic_api"
|
||||
name = "septastic_web"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
|
|
@ -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'
|
||||
10
web/src/controllers/index.rs
Normal file
10
web/src/controllers/index.rs
Normal file
|
|
@ -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 {})
|
||||
}
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
pub mod index;
|
||||
pub mod route;
|
||||
pub mod stop;
|
||||
137
web/src/controllers/route.rs
Normal file
137
web/src/controllers/route.rs
Normal file
|
|
@ -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<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct RouteResponse {
|
||||
pub route: libseptastic::route::Route,
|
||||
pub directions: Vec<libseptastic::direction::Direction>,
|
||||
pub schedule: Vec<Trip>,
|
||||
}
|
||||
|
||||
async fn get_route_info(
|
||||
route_id: String,
|
||||
state: Data<Arc<AppState>>,
|
||||
) -> ::anyhow::Result<RouteResponse> {
|
||||
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<Arc<AppState>>, resp: SessionResponse) -> impl Responder {
|
||||
let all_routes: Vec<libseptastic::route::Route> = 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<Arc<AppState>>) -> impl Responder {
|
||||
let all_routes: Vec<libseptastic::route::Route> = state.gtfs_service.get_routes();
|
||||
HttpResponse::Ok().json(all_routes)
|
||||
}
|
||||
|
||||
#[get("/route/{route_id}")]
|
||||
async fn get_route_html(
|
||||
state: Data<Arc<AppState>>,
|
||||
info: web::Query<RouteQueryParams>,
|
||||
path: web::Path<String>,
|
||||
resp: SessionResponse,
|
||||
) -> impl Responder {
|
||||
let mut filters: Option<Vec<String>> = 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<Arc<AppState>>, path: web::Path<String>) -> 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")
|
||||
}
|
||||
}
|
||||
281
web/src/controllers/stop.rs
Normal file
281
web/src/controllers/stop.rs
Normal file
|
|
@ -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<Arc<AppState>>,
|
||||
stop: &libseptastic::stop::Stop,
|
||||
filter: &StopFilter,
|
||||
) -> Vec<TripPerspective> {
|
||||
let routes: Vec<libseptastic::route::Route> = 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<String> = 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<TripPerspective> = 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<HashSet<String>>,
|
||||
pub live_tracked: Option<bool>,
|
||||
pub scheduled: Option<bool>,
|
||||
pub crowding: Option<HashSet<SeatAvailability>>,
|
||||
pub unknown_crowding: Option<bool>,
|
||||
}
|
||||
|
||||
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<Arc<AppState>>, 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<Arc<AppState>>,
|
||||
path: web::Path<String>,
|
||||
query: QsQuery<StopFilter>,
|
||||
) -> 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<Arc<AppState>>,
|
||||
path: web::Path<String>,
|
||||
query: QsQuery<StopFilter>,
|
||||
resp: SessionResponse,
|
||||
) -> impl Responder {
|
||||
let stop_id = path;
|
||||
|
||||
if let Some(stop) = state.gtfs_service.get_stop_by_id(&stop_id) {
|
||||
let routes: Vec<libseptastic::route::Route> = 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")
|
||||
}
|
||||
}
|
||||
66
web/src/main.rs
Normal file
66
web/src/main.rs
Normal file
|
|
@ -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::<gtfs_pull::Config>(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(())
|
||||
}
|
||||
601
web/src/services/gtfs_pull.rs
Normal file
601
web/src/services/gtfs_pull.rs
Normal file
|
|
@ -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<String>,
|
||||
pub prefix: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||
pub struct MultiplatformStopConfig {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub platform_station_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||
pub struct Annotations {
|
||||
pub multiplatform_stops: Vec<MultiplatformStopConfig>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
pub struct Config {
|
||||
pub gtfs_zips: Vec<GtfsSource>,
|
||||
pub annotations: Annotations,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GtfsFile {
|
||||
pub source: GtfsSource,
|
||||
}
|
||||
|
||||
pub struct TransitData {
|
||||
pub routes: HashMap<String, Arc<libseptastic::route::Route>>,
|
||||
pub agencies: HashMap<String, libseptastic::agency::Agency>,
|
||||
pub trips: HashMap<String, Vec<libseptastic::stop_schedule::Trip>>,
|
||||
pub stops: HashMap<String, Arc<libseptastic::stop::Stop>>,
|
||||
pub platforms: HashMap<String, Arc<libseptastic::stop::Platform>>,
|
||||
pub calendar_days: HashMap<String, Arc<libseptastic::stop_schedule::CalendarDay>>,
|
||||
pub directions: HashMap<String, Vec<Arc<libseptastic::direction::Direction>>>,
|
||||
|
||||
// extended lookup methods
|
||||
pub route_id_by_stops: HashMap<String, HashSet<String>>,
|
||||
pub stops_by_route_id: HashMap<String, HashSet<String>>,
|
||||
pub stops_by_platform_id: HashMap<String, Arc<libseptastic::stop::Stop>>,
|
||||
}
|
||||
|
||||
pub struct GtfsPullServiceState {
|
||||
pub gtfs_files: Vec<GtfsFile>,
|
||||
pub tmp_dir: PathBuf,
|
||||
pub ready: bool,
|
||||
pub annotations: Annotations,
|
||||
pub transit_data: TransitData,
|
||||
}
|
||||
|
||||
pub struct GtfsPullService {
|
||||
state: Arc<Mutex<GtfsPullServiceState>>,
|
||||
}
|
||||
|
||||
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<libseptastic::route::Route> {
|
||||
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<libseptastic::route::Route> {
|
||||
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<String, libseptastic::route::Route> {
|
||||
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<String, Arc<libseptastic::stop::Stop>> {
|
||||
let l_state = self.state.lock().unwrap();
|
||||
l_state.transit_data.stops.clone()
|
||||
}
|
||||
|
||||
pub fn get_all_trips(&self) -> HashMap<String, Vec<libseptastic::stop_schedule::Trip>> {
|
||||
let l_state = self.state.lock().unwrap();
|
||||
l_state.transit_data.trips.clone()
|
||||
}
|
||||
|
||||
pub fn get_routes_at_stop(&self, id: &String) -> HashSet<String> {
|
||||
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<String> {
|
||||
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<libseptastic::stop::Stop> {
|
||||
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<Vec<libseptastic::stop_schedule::Trip>> {
|
||||
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<Mutex<GtfsPullServiceState>>) -> 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(())
|
||||
}
|
||||
}
|
||||
2
web/src/services/mod.rs
Normal file
2
web/src/services/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod gtfs_pull;
|
||||
pub mod trip_tracking;
|
||||
|
|
@ -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<i64>,
|
||||
pub seat_availability: Option<String>,
|
||||
pub vehicle_id: Option<String>,
|
||||
pub timestamp: i64
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
const HOST: &str = "https://www3.septa.org";
|
||||
|
||||
struct TripTrackingServiceState {
|
||||
pub tracking_data: HashMap::<String, TripTracking>,
|
||||
pub database: ::sqlx::postgres::PgPool
|
||||
pub tracking_data: HashMap<String, TripTracking>,
|
||||
pub database: ::sqlx::postgres::PgPool,
|
||||
}
|
||||
|
||||
pub struct TripTrackingService {
|
||||
state: Arc<Mutex<TripTrackingServiceState>>
|
||||
state: Arc<Mutex<TripTrackingServiceState>>,
|
||||
}
|
||||
|
||||
impl TripTrackingService {
|
||||
|
|
@ -53,10 +53,9 @@ impl TripTrackingService {
|
|||
|
||||
pub async fn log_delay(
|
||||
transaction: &mut Transaction<'_, Postgres>,
|
||||
tracking_data: &HashMap::<String, TripTracking>,
|
||||
timestamp: i64
|
||||
tracking_data: &HashMap<String, TripTracking>,
|
||||
timestamp: i64,
|
||||
) -> ::anyhow::Result<()> {
|
||||
|
||||
let mut query_builder: QueryBuilder<Postgres> = 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<libseptastic::stop_schedule::Trip>) {
|
||||
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<Mutex<TripTrackingServiceState>>) -> anyhow::Result<()> {
|
||||
let mut new_map: HashMap<String, TripTracking> = HashMap::new();
|
||||
let live_tracks = reqwest::get(format!("{}/api/v2/trips/", HOST)).await?.json::<Vec<LiveTripJson>>().await?;
|
||||
|
||||
async fn update_live_trips(
|
||||
service: Arc<Mutex<TripTrackingServiceState>>,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut new_map: HashMap<String, TripTracking> = HashMap::new();
|
||||
let live_tracks = reqwest::get(format!("{}/api/v2/trips/", HOST))
|
||||
.await?
|
||||
.json::<Vec<LiveTripJson>>()
|
||||
.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::<f64>()?)} else {None},
|
||||
None => None
|
||||
},
|
||||
latitude: match live_track.lat {
|
||||
Some(lat) => Some(lat.parse::<f64>()?),
|
||||
None => None
|
||||
},
|
||||
longitude: match live_track.lon {
|
||||
Some(lon) => Some(lon.parse::<f64>()?),
|
||||
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::<f64>()?)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
None => None,
|
||||
},
|
||||
latitude: match live_track.lat {
|
||||
Some(lat) => Some(lat.parse::<f64>()?),
|
||||
None => None,
|
||||
},
|
||||
longitude: match live_track.lon {
|
||||
Some(lon) => Some(lon.parse::<f64>()?),
|
||||
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<String, D::Error> {
|
||||
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<Option<String>, 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<Option<String>, 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,
|
||||
})
|
||||
}
|
||||
|
||||
69
web/src/session_middleware.rs
Normal file
69
web/src/session_middleware.rs
Normal file
|
|
@ -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<bool>,
|
||||
}
|
||||
|
||||
pub trait SessionResponder<T: Template> {
|
||||
fn respond(&self, page_title: &str, page_desc: &str, content: T) -> HttpResponse;
|
||||
}
|
||||
|
||||
pub struct SessionResponse {
|
||||
start_time: Instant,
|
||||
widescreen: bool,
|
||||
}
|
||||
|
||||
impl<T> SessionResponder<T> 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<Box<dyn Future<Output = Result<SessionResponse, Self::Error>>>>;
|
||||
|
||||
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::<LocalStateQuery>::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,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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<T: askama::Template> {
|
|||
pub page_title: Option<String>,
|
||||
pub page_desc: Option<String>,
|
||||
pub load_time_ms: Option<u128>,
|
||||
pub widescreen: bool
|
||||
pub widescreen: bool,
|
||||
}
|
||||
|
||||
#[derive(askama::Template)]
|
||||
|
|
@ -22,7 +28,7 @@ pub struct ContentTemplate<T: askama::Template> {
|
|||
pub struct RouteTemplate {
|
||||
pub route: libseptastic::route::Route,
|
||||
pub timetables: Vec<TimetableDirection>,
|
||||
pub filter_stops: Option<Vec<String>>
|
||||
pub filter_stops: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(askama::Template)]
|
||||
|
|
@ -31,7 +37,7 @@ pub struct RoutesTemplate {
|
|||
pub rr_routes: Vec<libseptastic::route::Route>,
|
||||
pub subway_routes: Vec<libseptastic::route::Route>,
|
||||
pub trolley_routes: Vec<libseptastic::route::Route>,
|
||||
pub bus_routes: Vec<libseptastic::route::Route>
|
||||
pub bus_routes: Vec<libseptastic::route::Route>,
|
||||
}
|
||||
|
||||
#[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<Option<i64>>
|
||||
pub times: Vec<Option<i64>>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct TimetableDirection {
|
||||
pub direction: Direction,
|
||||
pub trip_ids: Vec<String>,
|
||||
pub tracking_data: Vec<TripTracking>,
|
||||
pub rows: Vec<TimetableStopRow>,
|
||||
pub next_id: Option<String>
|
||||
pub next_id: Option<String>,
|
||||
}
|
||||
|
||||
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<TripPerspective>,
|
||||
pub current_time: i64,
|
||||
pub filters: Option<StopFilter>,
|
||||
pub query_str: String
|
||||
pub query_str: String,
|
||||
}
|
||||
|
||||
#[derive(askama::Template)]
|
||||
|
|
@ -86,20 +88,16 @@ pub struct StopTemplate {
|
|||
pub struct StopTableTemplate {
|
||||
pub trips: Vec<TripPerspective>,
|
||||
pub current_time: i64,
|
||||
pub filters: Option<StopFilter>,
|
||||
pub query_str: String,
|
||||
pub stop_id: String
|
||||
pub stop_id: String,
|
||||
}
|
||||
|
||||
pub fn build_timetables(
|
||||
directions: Vec<Direction>,
|
||||
trips: Vec<Trip>,
|
||||
) -> Vec<TimetableDirection> {
|
||||
pub fn build_timetables(directions: Vec<Direction>, trips: Vec<Trip>) -> Vec<TimetableDirection> {
|
||||
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<String> = direction_trips
|
||||
.iter()
|
||||
.map(|t| t.trip_id.clone())
|
||||
.collect();
|
||||
let trip_ids: Vec<String> = direction_trips.iter().map(|t| t.trip_id.clone()).collect();
|
||||
|
||||
let live_trips: Vec<TripTracking> = direction_trips
|
||||
.iter()
|
||||
.map(|t| t.tracking_data.clone())
|
||||
.collect();
|
||||
|
||||
|
||||
let mut stop_map: BTreeMap<String, (i64, String, Vec<Option<i64>>)> = 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<TimetableStopRow> = 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<String> {
|
||||
pub fn format_load_time(nanos: &u128, _: &dyn askama::Values) -> askama::Result<String> {
|
||||
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,
|
||||
13
web/templates/index.html
Normal file
13
web/templates/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<h1>SEPTASTIC!</h1>
|
||||
<p>
|
||||
<i>A fantastic way to ride SEPTA</i>
|
||||
</p>
|
||||
<p style="margin-top: 25px;">
|
||||
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.
|
||||
</p>
|
||||
<p style="margin-top: 25px; margin-bottom: 25px;">
|
||||
Currently, all this website has is <a href="/routes">timetables for every
|
||||
SEPTA route</a>. More to come soon!
|
||||
</p>
|
||||
103
web/templates/layout.html
Normal file
103
web/templates/layout.html
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
{% if let Some(title) = page_title %}
|
||||
<title>{{ title }} | SEPTASTIC</title>
|
||||
{% else %}
|
||||
<title>SEPTASTIC</title>
|
||||
{% endif %}
|
||||
{% if let Some(desc) = page_desc %}
|
||||
<meta name="{{ desc }}" />
|
||||
{% else %}
|
||||
<meta name="SEPTASTIC" />
|
||||
{% endif %}
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
setTimeout(() => {
|
||||
const perfData = window.performance.timing;
|
||||
const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart;
|
||||
|
||||
const loadTimeElement = document.getElementById('js_load_time');
|
||||
loadTimeElement.textContent += ` ${pageLoadTime}ms`;
|
||||
}, 0); // Minimal delay to wait for `loadEventEnd` to be populated
|
||||
};
|
||||
</script>
|
||||
<noscript>
|
||||
<style>
|
||||
.js-only {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
<style>
|
||||
.silverliner-svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 200px; /* Fixed height matching the viewBox */
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
{% if widescreen %}
|
||||
<div class="body">
|
||||
{% else %}
|
||||
<div class="body body-small">
|
||||
{% endif %}
|
||||
<div style="background-color: #ff0000;
|
||||
color: #ffffff;
|
||||
font-size: .7em;
|
||||
padding: 5px;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px">This website is not run by SEPTA. Data may be inaccurate.</div>
|
||||
<nav>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<div>
|
||||
<a class="nav-link" href="/">[ Home ]</a>
|
||||
<a class="nav-link" href="/routes">[ Routes ]</a>
|
||||
<a class="nav-link" href="/stops">[ Stops ]</a>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</nav>
|
||||
<hr />
|
||||
{{ content|safe }}
|
||||
<footer>
|
||||
<hr />
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<div>
|
||||
<p style="margin-bottom: 0px; margin-top:0px;">
|
||||
<b>SEPTASTIC!</b>
|
||||
</p>
|
||||
<p style="margin-bottom: 0px;margin-top: 0px;">
|
||||
<small>Copyright © <a href="https://nickorlow.com">Nicholas Orlowsky</a> 2025</small>
|
||||
</p>
|
||||
{% if let Some(load_time) = load_time_ms %}
|
||||
<p style="marin-top: 5px; color: #555555;">
|
||||
<small><i>Data loaded in {{ *load_time | format_load_time }}</i></small>
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="js-only" style="marin-top: 5px; color: #555555;">
|
||||
<small><i id="js_load_time">Total load time</i></small>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
{% if widescreen %}
|
||||
<a href="?widescreen=false"><small>[ disable widescreen ]</small></a>
|
||||
{% else %}
|
||||
<a href="?widescreen=true"><small>[ enable widescreen ]</small></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<noscript>
|
||||
<p style="margin-top: 10px;">
|
||||
<small>[!] You do not have JavaScript enabled. Many features will be missing/broken!</small>
|
||||
</p>
|
||||
</noscript>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
138
web/templates/route.html
Normal file
138
web/templates/route.html
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
{%- import "route_symbol.html" as scope -%}
|
||||
<script>
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const scrollToNextColumn = (directionId) => {
|
||||
const target = document.getElementById("next-col-" + directionId);
|
||||
if (target) {
|
||||
const scrollContainer = target.closest(".tscroll");
|
||||
const firstCol = scrollContainer.querySelector("th:first-child");
|
||||
const firstColWidth = firstCol ? firstCol.offsetWidth : 0;
|
||||
|
||||
// Get the target's position relative to the scroll container
|
||||
const targetLeft = target.offsetLeft;
|
||||
|
||||
// Scroll so the target appears right after the sticky column
|
||||
scrollContainer.scrollLeft = targetLeft - firstColWidth;
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelectorAll("details[data-direction-id]").forEach(details => {
|
||||
const directionId = details.getAttribute("data-direction-id");
|
||||
|
||||
// Scroll immediately if details is already open
|
||||
if (details.open) {
|
||||
setTimeout(() => scrollToNextColumn(directionId), 50);
|
||||
}
|
||||
|
||||
// Also scroll when details is opened
|
||||
details.addEventListener("toggle", () => {
|
||||
if (details.open) {
|
||||
setTimeout(() => scrollToNextColumn(directionId), 50);
|
||||
}
|
||||
});
|
||||
});
|
||||
document.querySelectorAll(".train-direction-table").forEach((table) => {
|
||||
table.addEventListener("click", (e) => {
|
||||
const cell = e.target.closest("td, th");
|
||||
if (!cell) return;
|
||||
|
||||
// Clear previous highlights
|
||||
table.querySelectorAll("tr").forEach(row => row.classList.remove("highlight-row"));
|
||||
table.querySelectorAll("td, th").forEach(c => c.classList.remove("highlight-col"));
|
||||
|
||||
const row = cell.parentNode;
|
||||
const colIndex = Array.from(cell.parentNode.children).indexOf(cell);
|
||||
|
||||
// If it's the first column (row header)
|
||||
if (cell.cellIndex === 0 && cell.tagName === "TD") {
|
||||
row.classList.add("highlight-row");
|
||||
}
|
||||
// If it's a column header
|
||||
else if (row.parentNode.tagName === "THEAD") {
|
||||
table.querySelectorAll("tr").forEach(r => {
|
||||
const cell = r.children[colIndex];
|
||||
if (cell) cell.classList.add("highlight-col");
|
||||
});
|
||||
}
|
||||
// If it's a center cell
|
||||
else {
|
||||
row.classList.add("highlight-row");
|
||||
table.querySelectorAll("tr").forEach(r => {
|
||||
const cell = r.children[colIndex];
|
||||
if (cell) cell.classList.add("highlight-col");
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<div style="display: flex; align-items: center;">
|
||||
{% call scope::route_symbol(route) %}
|
||||
{% endcall %}
|
||||
<h1 style="margin-left: 15px;">{{ route.name }}</h1>
|
||||
</div>
|
||||
{% for timetable in timetables %}
|
||||
<details style="margin-top: 15px"
|
||||
data-direction-id="{{ timetable.direction.direction }}">
|
||||
<summary>
|
||||
<div style="display: inline-block;">
|
||||
<h3>{{ timetable.direction.direction | capitalize }} to</h3>
|
||||
<h2>{{ timetable.direction.direction_destination }}</h2>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="tscroll">
|
||||
<table class="train-direction-table" style="margin-top: 5px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Stop</th>
|
||||
{% for trip_id in timetable.trip_ids %}
|
||||
{% if let Some(next_id_v) = timetable.next_id %}
|
||||
{% if next_id_v == trip_id %}
|
||||
<th class="next-col" id="next-col-{{ timetable.direction.direction }}">
|
||||
{% else %}
|
||||
<th>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<th>
|
||||
{% endif %}
|
||||
{{ trip_id }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in timetable.rows %}
|
||||
{% if let Some(filter_stop_v) = filter_stops %}
|
||||
{% if !filter_stop_v.contains(&row.stop_id) %}
|
||||
{% continue %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>{{ row.stop_name }}</td>
|
||||
{% 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) %}
|
||||
<td style="background-color: #003300">
|
||||
<span style="color: #22bb22">{{ time | format_time }}</span>
|
||||
</td>
|
||||
{% elif let TripTracking::Cancelled = live_o %}
|
||||
<td style="color: #ff0000">
|
||||
<s>{{ t | format_time }}</s>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>{{ t | format_time }}</td>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
13
web/templates/route_symbol.html
Normal file
13
web/templates/route_symbol.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{% macro route_symbol(route) %}
|
||||
{% match route.route_type %}
|
||||
{% when libseptastic::route::RouteType::Trolley | libseptastic::route::RouteType::SubwayElevated %}
|
||||
<div class="metro-container bg-{{ route.short_name }}">{{ route.short_name }}</div>
|
||||
{% endwhen %}
|
||||
{% when libseptastic::route::RouteType::RegionalRail %}
|
||||
<div class="rr-container">{{ route.short_name }}</div>
|
||||
{% endwhen %}
|
||||
{% when libseptastic::route::RouteType::Bus | libseptastic::route::RouteType::TracklessTrolley %}
|
||||
<div class="bus-container">{{ route.short_name }}</div>
|
||||
{% endwhen %}
|
||||
{% endmatch %}
|
||||
{% endmacro %}
|
||||
93
web/templates/routes.html
Normal file
93
web/templates/routes.html
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<h1>Routes</h1>
|
||||
<p>Click on a route to see details and a schedule. Schedules in prevailing local time.</p>
|
||||
<fieldset>
|
||||
<legend>
|
||||
<h2>Regional Rail</h2>
|
||||
</legend>
|
||||
<p style="margin-top: 10px; margin-bottom: 10px;">For infrequent rail service to suburban locations</p>
|
||||
{% for route in rr_routes %}
|
||||
<a href="/route/{{ route.id }}"
|
||||
style="display: flex;
|
||||
justify-content: space-between">
|
||||
<p class="line-link">
|
||||
[ <b>{{ format!("{:7}", route.short_name) }}:</b> {{ route.name }}
|
||||
</p>
|
||||
<p>]</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>
|
||||
<h2>Metro</h2>
|
||||
</legend>
|
||||
<p style="margin-top: 10px; margin-bottom: 10px;">
|
||||
For frequent rail service within Philadelphia and suburban locations
|
||||
</p>
|
||||
<div class="lines-label"
|
||||
style="font-weight: bold;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between">
|
||||
<p>[ Subway/Elevated</p>
|
||||
<p>]</p>
|
||||
</div>
|
||||
{% for route in subway_routes %}
|
||||
<a href="/route/{{ route.id }}"
|
||||
style="display: flex;
|
||||
justify-content: space-between">
|
||||
<p class="line-link">
|
||||
[ <b>{{ format!("{:7}", route.short_name) }}:</b> {{ route.name }}
|
||||
</p>
|
||||
<p>]</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
<div class="lines-label"
|
||||
style="font-weight: bold;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between">
|
||||
<p>[ Trolleys</p>
|
||||
<p>]</p>
|
||||
</div>
|
||||
{% for route in trolley_routes %}
|
||||
<a href="/route/{{ route.id }}"
|
||||
style="display: flex;
|
||||
justify-content: space-between">
|
||||
<p class="line-link">
|
||||
[ <b>{{ format!("{:7}", route.short_name) }}:</b> {{ route.name }}
|
||||
</p>
|
||||
<p>]</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>
|
||||
<h2>Bus</h2>
|
||||
</legend>
|
||||
<p style="margin-top: 10px; margin-bottom: 10px;">
|
||||
For service of varying frequency within SEPTA's entire service area
|
||||
</p>
|
||||
{% for route in bus_routes %}
|
||||
<a href="/route/{{ route.id }}"
|
||||
style="display: flex;
|
||||
justify-content: space-between">
|
||||
<p class="line-link">
|
||||
[ <b>{{ format!("{:7}", route.short_name) }}:</b> {{ route.name }}
|
||||
</p>
|
||||
<p>]</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<style>
|
||||
.line-link, .lines-label {
|
||||
white-space: pre;
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.lines-label {
|
||||
color: #ffffff;
|
||||
background-color: #000000;
|
||||
width: max-content;
|
||||
}
|
||||
</style>
|
||||
139
web/templates/stop.html
Normal file
139
web/templates/stop.html
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
{%- import "route_symbol.html" as scope -%}
|
||||
{%- import "stop_table.html" as stop_table -%}
|
||||
<div style="display: flex; align-items: center;">
|
||||
<h1>{{ stop.name }}</h1>
|
||||
</div>
|
||||
<p>With service available on:</p>
|
||||
<div style="display: flex;
|
||||
justify-content: start;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px">
|
||||
{% for route in routes %}
|
||||
<div style="margin-right: 5px">
|
||||
{% call scope::route_symbol(route) %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<details>
|
||||
<summary>
|
||||
<p style="font-weight: bold; font-size: large;">Filters</p>
|
||||
</summary>
|
||||
<form hx-trigger="submit"
|
||||
hx-get="/stop/{{ stop.id }}/table"
|
||||
hx-target="#nta-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="/stop/{{ stop.id }}">
|
||||
<div style="margin: 5px; padding: 10px; background-color: #eee;">
|
||||
<div style="display: flex; flex-wrap: wrap;">
|
||||
<fieldset style="flex-grow: 1;">
|
||||
<legend>Route</legend>
|
||||
{% 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) %}
|
||||
<input type="checkbox"
|
||||
class="route-checkbox"
|
||||
name="routes"
|
||||
id="{{ route.id }},{{ dir.direction }}"
|
||||
value="{{ route.id }},{{ dir.direction }}"
|
||||
checked="{{ rts.contains(&*route_filter_id) }}">
|
||||
{% else %}
|
||||
<input type="checkbox"
|
||||
class="route-checkbox"
|
||||
name="routes"
|
||||
id="{{ route.id }},{{ dir.direction }}"
|
||||
value="{{ route.id }},{{ dir.direction }}"
|
||||
checked="true">
|
||||
{% endif %}
|
||||
<label for="{{ route.id }},{{ dir.direction }}">
|
||||
<b>{{ route.short_name }}</b>: {{ dir.direction_destination }}
|
||||
</label>
|
||||
<br>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
<input type="checkbox"
|
||||
id="master"
|
||||
hx-on:click="document.querySelectorAll('.route-checkbox').forEach(c => c.checked = this.checked)">
|
||||
<label for="master">Select/Deselect All</label>
|
||||
</fieldset>
|
||||
<div style="flex-grow: 1;">
|
||||
<fieldset>
|
||||
<legend>Ride Options</legend>
|
||||
{% if let Some(fil) = filters && let Some(lt) = fil.live_tracked %}
|
||||
<input type="checkbox"
|
||||
name="live_tracked"
|
||||
id="live_tracked"
|
||||
value="true"
|
||||
checked="{{ lt }}">
|
||||
{% else %}
|
||||
<input type="checkbox"
|
||||
name="live_tracked"
|
||||
id="live_tracked"
|
||||
value="true"
|
||||
checked="true">
|
||||
{% endif %}
|
||||
<label for="live-tracked">Live Tracked</label>
|
||||
<br>
|
||||
{% if let Some(fil) = filters && let Some(sc) = fil.scheduled %}
|
||||
<input type="checkbox"
|
||||
name="scheduled"
|
||||
id="scheduled"
|
||||
value="true"
|
||||
checked="{{ sc }}">
|
||||
{% else %}
|
||||
<input type="checkbox"
|
||||
name="scheduled"
|
||||
id="scheduled"
|
||||
value="true"
|
||||
checked="true">
|
||||
{% endif %}
|
||||
<label for="scheduled">Scheduled</label>
|
||||
<br>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Crowding</legend>
|
||||
{% for avail in SeatAvailability::iter() %}
|
||||
{% if let Some(fil) = filters && let Some(crd) = fil.crowding %}
|
||||
<input type="checkbox"
|
||||
name="crowding"
|
||||
id="{{ avail.to_string() }}"
|
||||
value="{{ avail.to_string() }}"
|
||||
checked="{{ crd.contains(&avail) }}">
|
||||
{% else %}
|
||||
<input type="checkbox"
|
||||
name="crowding"
|
||||
id="{{ avail.to_string() }}"
|
||||
value="{{ avail.to_string() }}"
|
||||
checked="true">
|
||||
{% endif %}
|
||||
<label for="{{ avail.to_string() }}">{{ avail.to_human_string() }}</label>
|
||||
<br>
|
||||
{% endfor %}
|
||||
{% if let Some(fil) = filters && let Some(uc) = fil.unknown_crowding %}
|
||||
<input type="checkbox"
|
||||
name="unknown_crowding"
|
||||
id="unknown_crowding"
|
||||
value="true"
|
||||
checked="{{ uc }}">
|
||||
{% else %}
|
||||
<input type="checkbox"
|
||||
name="unknown_crowding"
|
||||
id="unknown_crowding"
|
||||
value="true"
|
||||
checked="true">
|
||||
{% endif %}
|
||||
<label for="scheduled">Unknown</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<input type="submit" value="Apply">
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
<div style="overflow-x: scroll; max-width: 100%;">
|
||||
{% call stop_table::stop_table(trips, current_time, stop.id, query_str) %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
69
web/templates/stop_table.html
Normal file
69
web/templates/stop_table.html
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
{%- import "route_symbol.html" as scope -%}
|
||||
{% macro stop_table(trips, current_time, stop_id, query_str) %}
|
||||
<div id="nta-table"
|
||||
hx-get="/stop/{{ stop_id }}/table?{{ query_str }}"
|
||||
hx-trigger="every 5s"
|
||||
hx-swap="outer-html">
|
||||
<table class="train-direction-table">
|
||||
<tr>
|
||||
<th>ROUTE</th>
|
||||
<th>DESTINATION</th>
|
||||
<th>BOARDING AREA</th>
|
||||
<th>TIME</th>
|
||||
<th>VEHICLE</th>
|
||||
<th>TRIP</th>
|
||||
<th>CROWDING</th>
|
||||
</tr>
|
||||
{% for trip in trips %}
|
||||
<tr>
|
||||
<td>
|
||||
{% call scope::route_symbol(trip.trip.route) %}
|
||||
{% endcall %}
|
||||
</td>
|
||||
<td>
|
||||
<p>{{ trip.trip.direction.direction_destination }}</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>{{ trip.perspective_stop.platform.name }}</p>
|
||||
</td>
|
||||
{% if let Tracked(tracked_trip) = trip.trip.tracking_data %}
|
||||
<td style="color: #008800">
|
||||
<p style="font-size: small;">{{ &trip.perspective_stop.get_arrival_time(&tracked_trip) | format_time }}</p>
|
||||
<p style="font-size: x-small; font-style: italic;">
|
||||
{{ ( trip.perspective_stop.get_arrival_time(&tracked_trip) - current_time) / 60 }} mins
|
||||
</p>
|
||||
<p style="font-size: x-small; font-style: italic;">{{ tracked_trip.delay.round() }} late</p>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>
|
||||
<p style="font-size: small;">{{ trip.perspective_stop.arrival_time | format_time }}</p>
|
||||
<p style="font-size: x-small; font-style: italic;">
|
||||
{{ (trip.perspective_stop.arrival_time - current_time) / 60 }} mins
|
||||
</p>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if let Tracked(tracked_trip) = trip.trip.tracking_data %}
|
||||
<td>{{ tracked_trip.vehicle_ids.join(", ") }}</td>
|
||||
{% else %}
|
||||
<td>-</td>
|
||||
{% endif %}
|
||||
<td>{{ trip.trip.trip_id }}</td>
|
||||
{% if let Tracked(tracked_trip) = trip.trip.tracking_data %}
|
||||
{% if let Some(seat_avail) = tracked_trip.seat_availability %}
|
||||
<td>{{ seat_avail.to_human_string() }}</td>
|
||||
{% else %}
|
||||
<td>N/A</td>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<td>-</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<p>Updated at: {{ current_time | format_time_with_seconds }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
{%- import "stop_table.html" as stop_table -%}
|
||||
|
||||
{% call stop_table::stop_table(trips, current_time, stop_id, query_str) %}
|
||||
{% endcall %}
|
||||
|
|
@ -1,17 +1,19 @@
|
|||
<h1>Stops</h1>
|
||||
|
||||
<p>Click on a route to see details and a schedule. Schedules in prevailing local time.</p>
|
||||
|
||||
<p>Click on a route to see details and a schedule. Schedules in prevailing local time.</p>
|
||||
<fieldset>
|
||||
<legend><h2>Transit Centers</h2></legend>
|
||||
<legend>
|
||||
<h2>Transit Centers</h2>
|
||||
</legend>
|
||||
<p style="margin-top: 10px; margin-bottom: 10px;">Hubs to connect between different modes of transit</p>
|
||||
{% for stop in tc_stops %}
|
||||
<a href="/stop/{{ stop.id }}" style="display: flex; justify-content: space-between;">
|
||||
<p class="line-link">[ {{ stop.name }} </p><p>]</p>
|
||||
<a href="/stop/{{ stop.id }}"
|
||||
style="display: flex;
|
||||
justify-content: space-between">
|
||||
<p class="line-link">[ {{ stop.name }}</p>
|
||||
<p>]</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
<style>
|
||||
.line-link, .lines-label {
|
||||
white-space: pre;
|
||||
Loading…
Add table
Add a link
Reference in a new issue