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
|
export CONFIG_SHELL
|
||||||
CXX='g++'
|
CXX='g++'
|
||||||
export CXX
|
export CXX
|
||||||
|
DETERMINISTIC_BUILD='1'
|
||||||
|
export DETERMINISTIC_BUILD
|
||||||
HOSTTYPE='x86_64'
|
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'
|
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
|
export HOST_PATH
|
||||||
|
|
@ -35,13 +37,13 @@ NIX_CC='/nix/store/vr15iyyykg9zai6fpgvhcgyw7gckl78w-gcc-wrapper-14.3.0'
|
||||||
export NIX_CC
|
export NIX_CC
|
||||||
NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu='1'
|
NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu='1'
|
||||||
export NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu
|
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
|
export NIX_CFLAGS_COMPILE
|
||||||
NIX_ENFORCE_NO_NATIVE='1'
|
NIX_ENFORCE_NO_NATIVE='1'
|
||||||
export NIX_ENFORCE_NO_NATIVE
|
export NIX_ENFORCE_NO_NATIVE
|
||||||
NIX_HARDENING_ENABLE='bindnow format fortify fortify3 libcxxhardeningextensive libcxxhardeningfast pic relro stackclashprotection stackprotector strictoverflow zerocallusedregs'
|
NIX_HARDENING_ENABLE='bindnow format fortify fortify3 libcxxhardeningextensive libcxxhardeningfast pic relro stackclashprotection stackprotector strictoverflow zerocallusedregs'
|
||||||
export NIX_HARDENING_ENABLE
|
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
|
export NIX_LDFLAGS
|
||||||
NIX_NO_SELF_RPATH='1'
|
NIX_NO_SELF_RPATH='1'
|
||||||
NIX_PKG_CONFIG_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu='1'
|
NIX_PKG_CONFIG_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu='1'
|
||||||
|
|
@ -58,13 +60,19 @@ OLDPWD=''
|
||||||
export OLDPWD
|
export OLDPWD
|
||||||
OPTERR='1'
|
OPTERR='1'
|
||||||
OSTYPE='linux-gnu'
|
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
|
export PATH
|
||||||
PKG_CONFIG='pkg-config'
|
PKG_CONFIG='pkg-config'
|
||||||
export 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
|
export PKG_CONFIG_PATH
|
||||||
PS4='+ '
|
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'
|
RANLIB='ranlib'
|
||||||
export RANLIB
|
export RANLIB
|
||||||
READELF='readelf'
|
READELF='readelf'
|
||||||
|
|
@ -79,8 +87,12 @@ STRINGS='strings'
|
||||||
export STRINGS
|
export STRINGS
|
||||||
STRIP='strip'
|
STRIP='strip'
|
||||||
export 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
|
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=''
|
__structuredAttrs=''
|
||||||
export __structuredAttrs
|
export __structuredAttrs
|
||||||
_substituteStream_has_warned_replace_deprecation='false'
|
_substituteStream_has_warned_replace_deprecation='false'
|
||||||
|
|
@ -116,9 +128,9 @@ doInstallCheck=''
|
||||||
export doInstallCheck
|
export doInstallCheck
|
||||||
dontAddDisableDepTrack='1'
|
dontAddDisableDepTrack='1'
|
||||||
export dontAddDisableDepTrack
|
export dontAddDisableDepTrack
|
||||||
declare -a envBuildBuildHooks=()
|
declare -a envBuildBuildHooks=('addPythonPath' 'sysconfigdataHook' )
|
||||||
declare -a envBuildHostHooks=()
|
declare -a envBuildHostHooks=('addPythonPath' 'sysconfigdataHook' )
|
||||||
declare -a envBuildTargetHooks=()
|
declare -a envBuildTargetHooks=('addPythonPath' 'sysconfigdataHook' )
|
||||||
declare -a envHostHostHooks=('pkgConfigWrapper_addPkgConfigPath' 'ccWrapper_addCVars' 'bintoolsWrapper_addLDVars' )
|
declare -a envHostHostHooks=('pkgConfigWrapper_addPkgConfigPath' 'ccWrapper_addCVars' 'bintoolsWrapper_addLDVars' )
|
||||||
declare -a envHostTargetHooks=('pkgConfigWrapper_addPkgConfigPath' 'ccWrapper_addCVars' 'bintoolsWrapper_addLDVars' )
|
declare -a envHostTargetHooks=('pkgConfigWrapper_addPkgConfigPath' 'ccWrapper_addCVars' 'bintoolsWrapper_addLDVars' )
|
||||||
declare -a envTargetTargetHooks=()
|
declare -a envTargetTargetHooks=()
|
||||||
|
|
@ -128,7 +140,7 @@ mesonFlags=''
|
||||||
export mesonFlags
|
export mesonFlags
|
||||||
name='env-env'
|
name='env-env'
|
||||||
export name
|
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
|
export nativeBuildInputs
|
||||||
out='/home/nickorlow/programming/septastic/api/outputs/out'
|
out='/home/nickorlow/programming/septastic/api/outputs/out'
|
||||||
export out
|
export out
|
||||||
|
|
@ -147,7 +159,7 @@ patches=''
|
||||||
export patches
|
export patches
|
||||||
pkg='/nix/store/vr15iyyykg9zai6fpgvhcgyw7gckl78w-gcc-wrapper-14.3.0'
|
pkg='/nix/store/vr15iyyykg9zai6fpgvhcgyw7gckl78w-gcc-wrapper-14.3.0'
|
||||||
declare -a pkgsBuildBuild=()
|
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 pkgsBuildTarget=()
|
||||||
declare -a pkgsHostHost=()
|
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' )
|
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"'+=("$@")';
|
eval "${pkgHookVar}s"'+=("$@")';
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
addPythonPath ()
|
||||||
|
{
|
||||||
|
|
||||||
|
addToSearchPathWithCustomDelimiter : PYTHONPATH $1/lib/python3.13/site-packages
|
||||||
|
}
|
||||||
addToSearchPath ()
|
addToSearchPath ()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
@ -2017,6 +2034,26 @@ substituteStream ()
|
||||||
done;
|
done;
|
||||||
printf "%s" "${!var}"
|
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 ()
|
unpackFile ()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
|
||||||
18
Dockerfile
18
Dockerfile
|
|
@ -5,27 +5,27 @@ ENV SCCACHE_DIR=/build-cache
|
||||||
ENV RUSTC_WRAPPER=sccache
|
ENV RUSTC_WRAPPER=sccache
|
||||||
|
|
||||||
WORKDIR .
|
WORKDIR .
|
||||||
COPY ./api ./api
|
COPY ./web ./web
|
||||||
COPY ./libseptastic/ ./libseptastic/
|
COPY ./libseptastic/ ./libseptastic/
|
||||||
COPY ./api/assets ./assets
|
COPY ./web/assets ./assets
|
||||||
COPY ./api/templates ./templates
|
COPY ./web/templates ./templates
|
||||||
|
|
||||||
|
|
||||||
RUN apt -y update && apt install -y libssl-dev protobuf-compiler libc-dev sccache build-essential pkg-config
|
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
|
FROM debian:trixie-slim
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
COPY --from=build /api/target/release/septastic_api /app/septastic_api
|
COPY --from=build /web/target/release/septastic_web /app/septastic_web
|
||||||
COPY --from=build /api/config.yaml /app/config.yaml
|
COPY --from=build /web/config.yaml /app/config.yaml
|
||||||
COPY api/assets /app/assets
|
COPY web/assets /app/assets
|
||||||
COPY api/templates /app/templates
|
COPY web/templates /app/templates
|
||||||
|
|
||||||
RUN apt -y update && apt install -y curl
|
RUN apt -y update && apt install -y curl
|
||||||
|
|
||||||
ENV RUST_LOG=info
|
ENV RUST_LOG=info
|
||||||
ENV EXPOSE_PORT=8080
|
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};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Agency {
|
pub struct Agency {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
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")]
|
#[sqlx(type_name = "septa_direction_type", rename_all = "snake_case")]
|
||||||
pub enum CardinalDirection {
|
pub enum CardinalDirection {
|
||||||
Northbound,
|
Northbound,
|
||||||
|
|
@ -10,17 +11,17 @@ pub enum CardinalDirection {
|
||||||
Westbound,
|
Westbound,
|
||||||
Inbound,
|
Inbound,
|
||||||
Outbound,
|
Outbound,
|
||||||
Loop
|
Loop,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(::sqlx::Decode, Serialize, Deserialize, Debug, Clone)]
|
#[derive(::sqlx::Decode, Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct Direction {
|
pub struct Direction {
|
||||||
pub direction: CardinalDirection,
|
pub direction: CardinalDirection,
|
||||||
pub direction_destination: String
|
pub direction_destination: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for CardinalDirection {
|
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 {
|
let output = match self {
|
||||||
CardinalDirection::Northbound => "Northbound",
|
CardinalDirection::Northbound => "Northbound",
|
||||||
CardinalDirection::Southbound => "Southbound",
|
CardinalDirection::Southbound => "Southbound",
|
||||||
|
|
@ -28,7 +29,7 @@ impl std::fmt::Display for CardinalDirection {
|
||||||
CardinalDirection::Westbound => "Westbound",
|
CardinalDirection::Westbound => "Westbound",
|
||||||
CardinalDirection::Inbound => "Inbound",
|
CardinalDirection::Inbound => "Inbound",
|
||||||
CardinalDirection::Outbound => "Outbound",
|
CardinalDirection::Outbound => "Outbound",
|
||||||
CardinalDirection::Loop => "Loop"
|
CardinalDirection::Loop => "Loop",
|
||||||
};
|
};
|
||||||
std::write!(f, "{}", output)
|
std::write!(f, "{}", output)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
pub mod route;
|
|
||||||
pub mod agency;
|
pub mod agency;
|
||||||
pub mod stop;
|
|
||||||
pub mod route_stop;
|
|
||||||
pub mod stop_schedule;
|
|
||||||
pub mod schedule_day;
|
|
||||||
pub mod direction;
|
pub mod direction;
|
||||||
pub mod ridership;
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LineRidership {
|
pub struct LineRidership {
|
||||||
pub route_id: String,
|
pub route_id: String,
|
||||||
pub unlinked_trips: i64
|
pub unlinked_trips: i64,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ pub enum RouteType {
|
||||||
SubwayElevated,
|
SubwayElevated,
|
||||||
RegionalRail,
|
RegionalRail,
|
||||||
Bus,
|
Bus,
|
||||||
TracklessTrolley
|
TracklessTrolley,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(::sqlx::FromRow, Serialize, Deserialize, Debug, Clone)]
|
#[derive(::sqlx::FromRow, Serialize, Deserialize, Debug, Clone)]
|
||||||
|
|
@ -19,7 +19,7 @@ pub struct Route {
|
||||||
pub color_hex: String,
|
pub color_hex: String,
|
||||||
pub route_type: RouteType,
|
pub route_type: RouteType,
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub directions: Vec<crate::direction::Direction>
|
pub directions: Vec<crate::direction::Direction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Route {
|
impl PartialEq for Route {
|
||||||
|
|
@ -42,10 +42,9 @@ impl PartialOrd for Route {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(::sqlx::FromRow, Serialize, Deserialize, Debug, Clone)]
|
#[derive(::sqlx::FromRow, Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct InterlinedRoute {
|
pub struct InterlinedRoute {
|
||||||
pub interline_id: String,
|
pub interline_id: String,
|
||||||
pub interline_name: 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 route_id: String,
|
||||||
pub stop_id: i64,
|
pub stop_id: i64,
|
||||||
pub direction_id: i64,
|
pub direction_id: i64,
|
||||||
pub stop_sequence: i64
|
pub stop_sequence: i64,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ScheduleDay {
|
pub struct ScheduleDay {
|
||||||
pub date: String,
|
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};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
@ -7,13 +10,13 @@ use serde::{Deserialize, Serialize};
|
||||||
pub enum PlatformLocationType {
|
pub enum PlatformLocationType {
|
||||||
FarSide,
|
FarSide,
|
||||||
MiddleBlockNearSide,
|
MiddleBlockNearSide,
|
||||||
Normal
|
Normal,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum StopType {
|
pub enum StopType {
|
||||||
SinglePlatform(Arc<Platform>),
|
SinglePlatform(Arc<Platform>),
|
||||||
MultiPlatform(Vec<Arc<Platform>>)
|
MultiPlatform(Vec<Arc<Platform>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
|
@ -22,7 +25,7 @@ pub struct Platform {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub lat: f64,
|
pub lat: f64,
|
||||||
pub lng: f64,
|
pub lng: f64,
|
||||||
pub platform_location: PlatformLocationType
|
pub platform_location: PlatformLocationType,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use chrono::{Datelike, Days, TimeZone, Weekday};
|
use chrono::{Datelike, Days, Weekday};
|
||||||
use serde::{Deserialize, Serialize, Serializer, de::Error};
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
|
|
||||||
use crate::{direction::Direction, route::Route, stop::Platform};
|
use crate::{direction::Direction, route::Route, stop::Platform};
|
||||||
|
|
||||||
|
|
@ -10,11 +10,11 @@ pub struct StopSchedule {
|
||||||
pub arrival_time: i64,
|
pub arrival_time: i64,
|
||||||
pub stop_sequence: i64,
|
pub stop_sequence: i64,
|
||||||
pub stop: Arc<crate::stop::Stop>,
|
pub stop: Arc<crate::stop::Stop>,
|
||||||
pub platform: Arc<Platform>
|
pub platform: Arc<Platform>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StopSchedule {
|
impl StopSchedule {
|
||||||
pub fn get_arrival_time(&self, live_info: &LiveTrip) -> i64 {
|
pub fn get_arrival_time(&self, live_info: &LiveTrip) -> i64 {
|
||||||
return self.arrival_time + (live_info.delay * 60.0 as f64) as i64;
|
return self.arrival_time + (live_info.delay * 60.0 as f64) as i64;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -22,31 +22,42 @@ impl StopSchedule {
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Trip {
|
pub struct Trip {
|
||||||
pub service_id: String,
|
pub service_id: String,
|
||||||
pub route: Arc<Route>,
|
pub route: Arc<Route>,
|
||||||
pub trip_id: String,
|
pub trip_id: String,
|
||||||
pub direction: Direction,
|
pub direction: Direction,
|
||||||
pub tracking_data: TripTracking,
|
pub tracking_data: TripTracking,
|
||||||
pub schedule: Vec<StopSchedule>,
|
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 {
|
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;
|
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);
|
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();
|
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);
|
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();
|
dt_trip_end = dt_trip_end.checked_add_days(Days::new(1)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,7 +69,7 @@ impl Trip {
|
||||||
pub enum TripTracking {
|
pub enum TripTracking {
|
||||||
Tracked(LiveTrip),
|
Tracked(LiveTrip),
|
||||||
Untracked,
|
Untracked,
|
||||||
Cancelled
|
Cancelled,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -72,7 +83,7 @@ pub struct CalendarDay {
|
||||||
pub saturday: bool,
|
pub saturday: bool,
|
||||||
pub sunday: bool,
|
pub sunday: bool,
|
||||||
pub start_date: chrono::NaiveDate,
|
pub start_date: chrono::NaiveDate,
|
||||||
pub end_date: chrono::NaiveDate
|
pub end_date: chrono::NaiveDate,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CalendarDay {
|
impl CalendarDay {
|
||||||
|
|
@ -99,17 +110,18 @@ pub enum SeatAvailability {
|
||||||
CrushedStandingRoomOnly = 3,
|
CrushedStandingRoomOnly = 3,
|
||||||
FewSeats = 2,
|
FewSeats = 2,
|
||||||
ManySeats = 1,
|
ManySeats = 1,
|
||||||
Empty = 0
|
Empty = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for SeatAvailability {
|
impl<'de> Deserialize<'de> for SeatAvailability {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
where
|
where
|
||||||
D: serde::Deserializer<'de> {
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
let string = String::deserialize(deserializer)?;
|
let string = String::deserialize(deserializer)?;
|
||||||
return match SeatAvailability::from_string(&string) {
|
return match SeatAvailability::from_string(&string) {
|
||||||
Some(x) => Ok(x),
|
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 {
|
impl SeatAvailability {
|
||||||
pub fn iter() -> Vec<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 {
|
pub fn to_string(&self) -> String {
|
||||||
|
|
@ -145,7 +162,7 @@ impl SeatAvailability {
|
||||||
Self::CrushedStandingRoomOnly => "Sardines",
|
Self::CrushedStandingRoomOnly => "Sardines",
|
||||||
Self::FewSeats => "Few seats",
|
Self::FewSeats => "Few seats",
|
||||||
Self::ManySeats => "Many seats",
|
Self::ManySeats => "Many seats",
|
||||||
Self::Empty => "Empty"
|
Self::Empty => "Empty",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,7 +173,7 @@ impl SeatAvailability {
|
||||||
"FEW_SEATS_AVAILABLE" => Some(Self::FewSeats),
|
"FEW_SEATS_AVAILABLE" => Some(Self::FewSeats),
|
||||||
"MANY_SEATS_AVAILABLE" => Some(Self::ManySeats),
|
"MANY_SEATS_AVAILABLE" => Some(Self::ManySeats),
|
||||||
"EMPTY" => Some(Self::Empty),
|
"EMPTY" => Some(Self::Empty),
|
||||||
_ => None
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
with import <nixpkgs> {};
|
with import <nixpkgs> {};
|
||||||
stdenv.mkDerivation {
|
stdenv.mkDerivation {
|
||||||
name = "env";
|
name = "env";
|
||||||
nativeBuildInputs = [ pkg-config postgresql_14 ];
|
nativeBuildInputs = [
|
||||||
|
pkg-config
|
||||||
|
postgresql_14
|
||||||
|
rustfmt
|
||||||
|
cargo
|
||||||
|
djlint
|
||||||
|
];
|
||||||
|
|
||||||
buildInputs = [
|
buildInputs = [
|
||||||
cryptsetup
|
cryptsetup
|
||||||
protobuf
|
protobuf
|
||||||
|
|
|
||||||
1
api/.gitignore → web/.gitignore
vendored
1
api/.gitignore → web/.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
||||||
target/
|
target/
|
||||||
.env
|
.env
|
||||||
|
.sqlx/
|
||||||
2
api/Cargo.lock → web/Cargo.lock
generated
2
api/Cargo.lock → web/Cargo.lock
generated
|
|
@ -2779,7 +2779,7 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "septastic_api"
|
name = "septastic_web"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-cors",
|
"actix-cors",
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "septastic_api"
|
name = "septastic_web"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
|
@ -5,8 +5,6 @@ gtfs_zips:
|
||||||
- uri: "https://www3.septa.org/developer/gtfs_public.zip"
|
- uri: "https://www3.septa.org/developer/gtfs_public.zip"
|
||||||
prefix: "SEPTABUS"
|
prefix: "SEPTABUS"
|
||||||
subzip: "google_bus.zip"
|
subzip: "google_bus.zip"
|
||||||
# - uri: "https://www.njtransit.com/rail_data.zip"
|
|
||||||
# - uri: "https://www.njtransit.com/bus_data.zip"
|
|
||||||
annotations:
|
annotations:
|
||||||
multiplatform_stops:
|
multiplatform_stops:
|
||||||
- id: 'WTC'
|
- id: 'WTC'
|
||||||
|
|
@ -35,5 +33,3 @@ annotations:
|
||||||
- 'SEPTABUS_2687'
|
- 'SEPTABUS_2687'
|
||||||
- 'SEPTABUS_18451'
|
- 'SEPTABUS_18451'
|
||||||
- 'SEPTABUS_17170'
|
- '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 route;
|
||||||
pub mod stop;
|
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 chrono::Utc;
|
||||||
use serde_json::Value;
|
|
||||||
use serde::de;
|
|
||||||
use sqlx::{Postgres, QueryBuilder, Transaction};
|
|
||||||
use std::sync::{Arc};
|
|
||||||
use futures::lock::Mutex;
|
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 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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LiveTripJson {
|
pub struct LiveTripJson {
|
||||||
|
|
@ -34,18 +34,18 @@ pub struct LiveTripJson {
|
||||||
pub next_stop_sequence: Option<i64>,
|
pub next_stop_sequence: Option<i64>,
|
||||||
pub seat_availability: Option<String>,
|
pub seat_availability: Option<String>,
|
||||||
pub vehicle_id: Option<String>,
|
pub vehicle_id: Option<String>,
|
||||||
pub timestamp: i64
|
pub timestamp: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
const HOST: &str = "https://www3.septa.org";
|
const HOST: &str = "https://www3.septa.org";
|
||||||
|
|
||||||
struct TripTrackingServiceState {
|
struct TripTrackingServiceState {
|
||||||
pub tracking_data: HashMap::<String, TripTracking>,
|
pub tracking_data: HashMap<String, TripTracking>,
|
||||||
pub database: ::sqlx::postgres::PgPool
|
pub database: ::sqlx::postgres::PgPool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TripTrackingService {
|
pub struct TripTrackingService {
|
||||||
state: Arc<Mutex<TripTrackingServiceState>>
|
state: Arc<Mutex<TripTrackingServiceState>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TripTrackingService {
|
impl TripTrackingService {
|
||||||
|
|
@ -53,10 +53,9 @@ impl TripTrackingService {
|
||||||
|
|
||||||
pub async fn log_delay(
|
pub async fn log_delay(
|
||||||
transaction: &mut Transaction<'_, Postgres>,
|
transaction: &mut Transaction<'_, Postgres>,
|
||||||
tracking_data: &HashMap::<String, TripTracking>,
|
tracking_data: &HashMap<String, TripTracking>,
|
||||||
timestamp: i64
|
timestamp: i64,
|
||||||
) -> ::anyhow::Result<()> {
|
) -> ::anyhow::Result<()> {
|
||||||
|
|
||||||
let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(
|
let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(
|
||||||
"INSERT INTO
|
"INSERT INTO
|
||||||
live_tracking
|
live_tracking
|
||||||
|
|
@ -73,7 +72,7 @@ impl TripTrackingService {
|
||||||
trip_id,
|
trip_id,
|
||||||
route_id
|
route_id
|
||||||
)
|
)
|
||||||
VALUES"
|
VALUES",
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut separated = query_builder.separated(", ");
|
let mut separated = query_builder.separated(", ");
|
||||||
|
|
@ -89,7 +88,7 @@ impl TripTrackingService {
|
||||||
separated.push_bind(live_data.heading);
|
separated.push_bind(live_data.heading);
|
||||||
separated.push_bind(match &live_data.seat_availability {
|
separated.push_bind(match &live_data.seat_availability {
|
||||||
Some(s) => Some(s.to_string()),
|
Some(s) => Some(s.to_string()),
|
||||||
None => None
|
None => None,
|
||||||
});
|
});
|
||||||
separated.push_bind(live_data.vehicle_ids.clone());
|
separated.push_bind(live_data.vehicle_ids.clone());
|
||||||
separated.push_bind(live_data.trip_id.clone());
|
separated.push_bind(live_data.trip_id.clone());
|
||||||
|
|
@ -111,17 +110,21 @@ impl TripTrackingService {
|
||||||
let pool = ::sqlx::postgres::PgPoolOptions::new()
|
let pool = ::sqlx::postgres::PgPoolOptions::new()
|
||||||
.max_connections(5)
|
.max_connections(5)
|
||||||
.connect(&connection_string)
|
.connect(&connection_string)
|
||||||
.await.unwrap();
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
Self {
|
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) {
|
pub fn start(&self) {
|
||||||
info!("Starting live tracking service");
|
info!("Starting live tracking service");
|
||||||
let cloned_state = Arc::clone(&self.state);
|
let cloned_state = Arc::clone(&self.state);
|
||||||
tokio::spawn( async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
let clonedx_state = Arc::clone(&cloned_state);
|
let clonedx_state = Arc::clone(&cloned_state);
|
||||||
let res = Self::update_live_trips(clonedx_state).await;
|
let res = Self::update_live_trips(clonedx_state).await;
|
||||||
|
|
@ -140,16 +143,27 @@ impl TripTrackingService {
|
||||||
|
|
||||||
pub async fn annotate_trips(&self, trips: &mut Vec<libseptastic::stop_schedule::Trip>) {
|
pub async fn annotate_trips(&self, trips: &mut Vec<libseptastic::stop_schedule::Trip>) {
|
||||||
for trip in trips {
|
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(),
|
Some(x) => x.clone(),
|
||||||
None => TripTracking::Untracked
|
None => TripTracking::Untracked,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_live_trips(service: Arc<Mutex<TripTrackingServiceState>>) -> anyhow::Result<()> {
|
async fn update_live_trips(
|
||||||
let mut new_map: HashMap<String, TripTracking> = HashMap::new();
|
service: Arc<Mutex<TripTrackingServiceState>>,
|
||||||
let live_tracks = reqwest::get(format!("{}/api/v2/trips/", HOST)).await?.json::<Vec<LiveTripJson>>().await?;
|
) -> 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 {
|
for live_track in live_tracks {
|
||||||
let track: TripTracking = {
|
let track: TripTracking = {
|
||||||
|
|
@ -158,48 +172,50 @@ impl TripTrackingService {
|
||||||
} else if live_track.status == "CANCELED" {
|
} else if live_track.status == "CANCELED" {
|
||||||
TripTracking::Cancelled
|
TripTracking::Cancelled
|
||||||
} else {
|
} else {
|
||||||
TripTracking::Tracked(
|
TripTracking::Tracked(LiveTrip {
|
||||||
LiveTrip {
|
trip_id: live_track.trip_id.clone(),
|
||||||
trip_id: live_track.trip_id.clone(),
|
route_id: live_track.route_id,
|
||||||
route_id: live_track.route_id,
|
delay: live_track.delay,
|
||||||
delay: live_track.delay,
|
seat_availability: SeatAvailability::from_opt_string(
|
||||||
seat_availability: SeatAvailability::from_opt_string(&live_track.seat_availability),
|
&live_track.seat_availability,
|
||||||
heading: match live_track.heading {
|
),
|
||||||
Some(hdg) => if hdg != "" { Some(hdg.parse::<f64>()?)} else {None},
|
heading: match live_track.heading {
|
||||||
None => None
|
Some(hdg) => {
|
||||||
},
|
if hdg != "" {
|
||||||
latitude: match live_track.lat {
|
Some(hdg.parse::<f64>()?)
|
||||||
Some(lat) => Some(lat.parse::<f64>()?),
|
} else {
|
||||||
None => 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![]
|
|
||||||
}
|
}
|
||||||
}
|
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(
|
new_map.insert(live_track.trip_id.clone(), track);
|
||||||
live_track.trip_id.clone(),
|
|
||||||
track
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut svc = service.lock().await;
|
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> {
|
fn de_numstr<'de, D: Deserializer<'de>>(deserializer: D) -> Result<String, D::Error> {
|
||||||
Ok(match Value::deserialize(deserializer)? {
|
Ok(match Value::deserialize(deserializer)? {
|
||||||
Value::String(s) => s,
|
Value::String(s) => s,
|
||||||
Value::Number(num) => num.as_i64().ok_or(de::Error::custom("Invalid number"))?.to_string(),
|
Value::Number(num) => num
|
||||||
_ => return Err(de::Error::custom("wrong type"))
|
.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> {
|
fn de_numstro<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<String>, D::Error> {
|
||||||
Ok(match Value::deserialize(deserializer)? {
|
Ok(match Value::deserialize(deserializer)? {
|
||||||
Value::String(s) => Some(s),
|
Value::String(s) => Some(s),
|
||||||
Value::Number(num) => Some(num.as_i64().ok_or(de::Error::custom("Invalid number"))?.to_string()),
|
Value::Number(num) => Some(
|
||||||
_ => None
|
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> {
|
fn de_numstrflo<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<String>, D::Error> {
|
||||||
Ok(match Value::deserialize(deserializer)? {
|
Ok(match Value::deserialize(deserializer)? {
|
||||||
Value::String(s) => Some(s),
|
Value::String(s) => Some(s),
|
||||||
Value::Number(num) => Some(num.as_f64().ok_or(de::Error::custom("Invalid number"))?.to_string()),
|
Value::Number(num) => Some(
|
||||||
_ => None
|
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::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;
|
use crate::controllers::stop::StopFilter;
|
||||||
|
|
||||||
|
|
@ -14,7 +20,7 @@ pub struct ContentTemplate<T: askama::Template> {
|
||||||
pub page_title: Option<String>,
|
pub page_title: Option<String>,
|
||||||
pub page_desc: Option<String>,
|
pub page_desc: Option<String>,
|
||||||
pub load_time_ms: Option<u128>,
|
pub load_time_ms: Option<u128>,
|
||||||
pub widescreen: bool
|
pub widescreen: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(askama::Template)]
|
#[derive(askama::Template)]
|
||||||
|
|
@ -22,7 +28,7 @@ pub struct ContentTemplate<T: askama::Template> {
|
||||||
pub struct RouteTemplate {
|
pub struct RouteTemplate {
|
||||||
pub route: libseptastic::route::Route,
|
pub route: libseptastic::route::Route,
|
||||||
pub timetables: Vec<TimetableDirection>,
|
pub timetables: Vec<TimetableDirection>,
|
||||||
pub filter_stops: Option<Vec<String>>
|
pub filter_stops: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(askama::Template)]
|
#[derive(askama::Template)]
|
||||||
|
|
@ -31,7 +37,7 @@ pub struct RoutesTemplate {
|
||||||
pub rr_routes: Vec<libseptastic::route::Route>,
|
pub rr_routes: Vec<libseptastic::route::Route>,
|
||||||
pub subway_routes: Vec<libseptastic::route::Route>,
|
pub subway_routes: Vec<libseptastic::route::Route>,
|
||||||
pub trolley_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)]
|
#[derive(askama::Template)]
|
||||||
|
|
@ -42,32 +48,28 @@ pub struct StopsTemplate {
|
||||||
|
|
||||||
#[derive(askama::Template)]
|
#[derive(askama::Template)]
|
||||||
#[template(path = "index.html")]
|
#[template(path = "index.html")]
|
||||||
pub struct IndexTemplate {
|
pub struct IndexTemplate {}
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct TimetableStopRow {
|
pub struct TimetableStopRow {
|
||||||
pub stop_id: String,
|
pub stop_id: String,
|
||||||
pub stop_name: String,
|
pub stop_name: String,
|
||||||
pub stop_sequence: i64,
|
pub stop_sequence: i64,
|
||||||
pub times: Vec<Option<i64>>
|
pub times: Vec<Option<i64>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct TimetableDirection {
|
pub struct TimetableDirection {
|
||||||
pub direction: Direction,
|
pub direction: Direction,
|
||||||
pub trip_ids: Vec<String>,
|
pub trip_ids: Vec<String>,
|
||||||
pub tracking_data: Vec<TripTracking>,
|
pub tracking_data: Vec<TripTracking>,
|
||||||
pub rows: Vec<TimetableStopRow>,
|
pub rows: Vec<TimetableStopRow>,
|
||||||
pub next_id: Option<String>
|
pub next_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TripPerspective {
|
pub struct TripPerspective {
|
||||||
pub trip:libseptastic::stop_schedule::Trip,
|
pub trip: libseptastic::stop_schedule::Trip,
|
||||||
pub perspective_stop: libseptastic::stop_schedule::StopSchedule,
|
pub perspective_stop: libseptastic::stop_schedule::StopSchedule,
|
||||||
pub est_arrival_time: i64,
|
|
||||||
pub is_tracked: bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(askama::Template)]
|
#[derive(askama::Template)]
|
||||||
|
|
@ -78,7 +80,7 @@ pub struct StopTemplate {
|
||||||
pub trips: Vec<TripPerspective>,
|
pub trips: Vec<TripPerspective>,
|
||||||
pub current_time: i64,
|
pub current_time: i64,
|
||||||
pub filters: Option<StopFilter>,
|
pub filters: Option<StopFilter>,
|
||||||
pub query_str: String
|
pub query_str: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(askama::Template)]
|
#[derive(askama::Template)]
|
||||||
|
|
@ -86,20 +88,16 @@ pub struct StopTemplate {
|
||||||
pub struct StopTableTemplate {
|
pub struct StopTableTemplate {
|
||||||
pub trips: Vec<TripPerspective>,
|
pub trips: Vec<TripPerspective>,
|
||||||
pub current_time: i64,
|
pub current_time: i64,
|
||||||
pub filters: Option<StopFilter>,
|
|
||||||
pub query_str: String,
|
pub query_str: String,
|
||||||
pub stop_id: String
|
pub stop_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_timetables(
|
pub fn build_timetables(directions: Vec<Direction>, trips: Vec<Trip>) -> Vec<TimetableDirection> {
|
||||||
directions: Vec<Direction>,
|
|
||||||
trips: Vec<Trip>,
|
|
||||||
) -> Vec<TimetableDirection> {
|
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
|
|
||||||
for direction in directions {
|
for direction in directions {
|
||||||
let now_utc = chrono::Utc::now();
|
let now_utc = chrono::Utc::now();
|
||||||
let now = now_utc.with_timezone(&New_York);
|
let now = now_utc.with_timezone(&New_York);
|
||||||
let naive_time = now.time();
|
let naive_time = now.time();
|
||||||
let seconds_since_midnight = naive_time.num_seconds_from_midnight();
|
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
|
let trip_ids: Vec<String> = direction_trips.iter().map(|t| t.trip_id.clone()).collect();
|
||||||
.iter()
|
|
||||||
.map(|t| t.trip_id.clone())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let live_trips: Vec<TripTracking> = direction_trips
|
let live_trips: Vec<TripTracking> = direction_trips
|
||||||
.iter()
|
.iter()
|
||||||
.map(|t| t.tracking_data.clone())
|
.map(|t| t.tracking_data.clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
||||||
let mut stop_map: BTreeMap<String, (i64, String, Vec<Option<i64>>)> = BTreeMap::new();
|
let mut stop_map: BTreeMap<String, (i64, String, Vec<Option<i64>>)> = BTreeMap::new();
|
||||||
|
|
||||||
for (trip_index, trip) in direction_trips.iter().enumerate() {
|
for (trip_index, trip) in direction_trips.iter().enumerate() {
|
||||||
for stop in &trip.schedule {
|
for stop in &trip.schedule {
|
||||||
let entry = stop_map
|
let entry = stop_map.entry(stop.stop.id.clone()).or_insert((
|
||||||
.entry(stop.stop.id.clone())
|
stop.stop_sequence,
|
||||||
.or_insert((stop.stop_sequence, stop.stop.name.clone(), vec![None; direction_trips.len()]));
|
stop.stop.name.clone(),
|
||||||
|
vec![None; direction_trips.len()],
|
||||||
|
));
|
||||||
|
|
||||||
// If this stop_id appears in multiple trips with different sequences, keep the lowest
|
// If this stop_id appears in multiple trips with different sequences, keep the lowest
|
||||||
entry.0 = entry.0.max(stop.stop_sequence);
|
entry.0 = entry.0.max(stop.stop_sequence);
|
||||||
|
|
@ -154,15 +150,17 @@ pub fn build_timetables(
|
||||||
|
|
||||||
let mut rows: Vec<TimetableStopRow> = stop_map
|
let mut rows: Vec<TimetableStopRow> = stop_map
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(stop_id, (stop_sequence, stop_name, times))| TimetableStopRow {
|
.map(
|
||||||
stop_id,
|
|(stop_id, (stop_sequence, stop_name, times))| TimetableStopRow {
|
||||||
stop_sequence,
|
stop_id,
|
||||||
stop_name,
|
stop_sequence,
|
||||||
times,
|
stop_name,
|
||||||
})
|
times,
|
||||||
|
},
|
||||||
|
)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
rows.sort_by(| a, b| {
|
rows.sort_by(|a, b| {
|
||||||
if a.stop_sequence < b.stop_sequence {
|
if a.stop_sequence < b.stop_sequence {
|
||||||
Ordering::Less
|
Ordering::Less
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -174,8 +172,8 @@ pub fn build_timetables(
|
||||||
direction: direction.clone(),
|
direction: direction.clone(),
|
||||||
trip_ids,
|
trip_ids,
|
||||||
rows,
|
rows,
|
||||||
tracking_data: live_trips ,
|
tracking_data: live_trips,
|
||||||
next_id
|
next_id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,16 +184,14 @@ mod filters {
|
||||||
use askama::filter_fn;
|
use askama::filter_fn;
|
||||||
|
|
||||||
#[filter_fn]
|
#[filter_fn]
|
||||||
pub fn format_load_time(
|
pub fn format_load_time(nanos: &u128, _: &dyn askama::Values) -> askama::Result<String> {
|
||||||
nanos: &u128,
|
|
||||||
_: &dyn askama::Values,
|
|
||||||
) -> askama::Result<String> {
|
|
||||||
if *nanos >= 1000000000 {
|
if *nanos >= 1000000000 {
|
||||||
return Ok(format!("{}s", (nanos/1000000000)));
|
return Ok(format!("{}s", (nanos / 1000000000)));
|
||||||
} else if *nanos >= 1000000 {
|
} else if *nanos >= 1000000 {
|
||||||
return Ok(format!("{}ms", nanos/1000000));
|
return Ok(format!("{}ms", nanos / 1000000));
|
||||||
} if *nanos >= 1000 {
|
}
|
||||||
return Ok(format!("{}us", nanos/1000));
|
if *nanos >= 1000 {
|
||||||
|
return Ok(format!("{}us", nanos / 1000));
|
||||||
} else {
|
} else {
|
||||||
return Ok(format!("{}ns", nanos));
|
return Ok(format!("{}ns", nanos));
|
||||||
}
|
}
|
||||||
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 -%}
|
{%- import "stop_table.html" as stop_table -%}
|
||||||
|
|
||||||
{% call stop_table::stop_table(trips, current_time, stop_id, query_str) %}
|
{% call stop_table::stop_table(trips, current_time, stop_id, query_str) %}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
<h1>Stops</h1>
|
<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>
|
<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>
|
<p style="margin-top: 10px; margin-bottom: 10px;">Hubs to connect between different modes of transit</p>
|
||||||
{% for stop in tc_stops %}
|
{% for stop in tc_stops %}
|
||||||
<a href="/stop/{{ stop.id }}" style="display: flex; justify-content: space-between;">
|
<a href="/stop/{{ stop.id }}"
|
||||||
<p class="line-link">[ {{ stop.name }} </p><p>]</p>
|
style="display: flex;
|
||||||
|
justify-content: space-between">
|
||||||
|
<p class="line-link">[ {{ stop.name }}</p>
|
||||||
|
<p>]</p>
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.line-link, .lines-label {
|
.line-link, .lines-label {
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue