commit b9de50bb2b2787f7a5f510a53716678e4e9037b8 Author: Stefan Bühler Date: Sat Sep 28 12:16:55 2019 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53eaa21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..be781e1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,607 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "anymap" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "autocfg" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "backtrace" +version = "0.3.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace-sys 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "backtrace-sys" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "base-x" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bincode" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "boolinator" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bumpalo" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "byteorder" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bytes" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cc" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "chrono" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "failure" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace 0.3.38 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure_derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", + "synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fnv" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "galmon-web" +version = "0.1.0" +dependencies = [ + "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_repr 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "stdweb 0.4.18 (registry+https://github.com/rust-lang/crates.io-index)", + "yew 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "http" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "indexmap" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "iovec" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "itoa" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-integer" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "proc-macro-hack" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro-nested" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro2" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "redox_syscall" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rustc-demangle" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ryu" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_derive" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_json" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_repr" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "stdweb" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "discard 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", + "stdweb-derive 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "stdweb-internal-macros 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "stdweb-internal-runtime 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base-x 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", + "sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "syn" +version = "0.15.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "syn" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "synstructure" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "time" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "wasm-bindgen" +version = "0.2.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "wasm-bindgen-macro 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-shared 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-macro-support 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-backend 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-shared 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.42" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "yew" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "anymap 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", + "bincode 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", + "indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-hack 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-nested 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", + "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "stdweb 0.4.18 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)", + "yew-macro 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "yew-macro" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "boolinator 2.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-hack 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[metadata] +"checksum anymap 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "33954243bd79057c2de7338850b85983a44588021f8a5fee574a8888c6de4344" +"checksum autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b671c8fb71b457dd4ae18c4ba1e59aa81793daacc361d82fcd410cef0d491875" +"checksum backtrace 0.3.38 (registry+https://github.com/rust-lang/crates.io-index)" = "690a62be8920ccf773ee00ef0968649b0e724cda8bd5b12286302b4ae955fdf5" +"checksum backtrace-sys 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "82a830b4ef2d1124a711c71d263c5abdc710ef8e907bd508c88be475cebc422b" +"checksum base-x 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "76f4eae81729e69bb1819a26c6caac956cc429238388091f98cb6cd858f16443" +"checksum bincode 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9f2fb9e29e72fd6bc12071533d5dc7664cb01480c59406f656d7ac25c7bd8ff7" +"checksum boolinator 2.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9" +"checksum bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ad807f2fc2bf185eeb98ff3a901bd46dc5ad58163d0fa4577ba0d25674d71708" +"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" +"checksum bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" +"checksum cc 1.0.45 (registry+https://github.com/rust-lang/crates.io-index)" = "4fc9a35e1f4290eb9e5fc54ba6cf40671ed2a2514c3eeb2b2a908dda2ea5a1be" +"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +"checksum chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e8493056968583b0193c1bb04d6f7684586f3726992d6c573261941a895dbd68" +"checksum discard 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +"checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" +"checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" +"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" +"checksum http 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "372bcb56f939e449117fb0869c2e8fd8753a8223d92a172c6e808cf123a5b6e4" +"checksum indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a61202fbe46c4a951e9404a720a0180bcf3212c750d735cb5c4ba4dc551299f3" +"checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" +"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" +"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +"checksum libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "34fcd2c08d2f832f376f4173a231990fa5aef4e99fb569867318a227ef4c06ba" +"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +"checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09" +"checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" +"checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +"checksum proc-macro-hack 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e688f31d92ffd7c1ddc57a1b4e6d773c0f2a14ee437a4b0a4f5a69c80eb221c8" +"checksum proc-macro-nested 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "369a6ed065f249a159e06c45752c780bda2fb53c995718f9e484d08daa9eb42e" +"checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +"checksum proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "afdc77cc74ec70ed262262942ebb7dac3d479e9e5cfa2da1841c0806f6cdabcc" +"checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" +"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" +"checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" +"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +"checksum ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997" +"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +"checksum serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)" = "9796c9b7ba2ffe7a9ce53c2287dfc48080f4b2b362fcc245a259b3a7201119dd" +"checksum serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)" = "4b133a43a1ecd55d4086bd5b4dc6c1751c68b1bfbeba7a5040442022c7e7c02e" +"checksum serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)" = "051c49229f282f7c6f3813f8286cc1e3323e8051823fce42c7ea80fe13521704" +"checksum serde_repr 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "cd02c7587ec314570041b2754829f84d873ced14a96d1fd1823531e11db40573" +"checksum sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" +"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +"checksum stdweb 0.4.18 (registry+https://github.com/rust-lang/crates.io-index)" = "a68c0ce28cf7400ed022e18da3c4591e14e1df02c70e93573cc59921b3923aeb" +"checksum stdweb-derive 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0e21ebd9179de08f2300a65454268a17ea3de204627458588c84319c4def3930" +"checksum stdweb-internal-macros 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "e68f7d08b76979a43e93fe043b66d2626e35d41d68b0b85519202c6dd8ac59fa" +"checksum stdweb-internal-runtime 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d52317523542cc0af5b7e31017ad0f7d1e78da50455e38d5657cd17754f617da" +"checksum syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)" = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +"checksum syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "66850e97125af79138385e9b88339cbcd037e3f28ceab8c5ad98e64f0f1f80bf" +"checksum synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "02353edf96d6e4dc81aea2d8490a7e9db177bf8acb0e951c24940bf866cb313f" +"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" +"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +"checksum wasm-bindgen 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "ffde3534e5fa6fd936e3260cd62cd644b8656320e369388f9303c955895e35d4" +"checksum wasm-bindgen-backend 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "40c0543374a7ae881cdc5d32d19de28d1d1929e92263ffa7e31712cc2d53f9f1" +"checksum wasm-bindgen-macro 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "f914c94c2c5f4c9364510ca2429e59c92157ec89429243bcc245e983db990a71" +"checksum wasm-bindgen-macro-support 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "9168c413491e4233db7b6884f09a43beb00c14d11d947ffd165242daa48a2385" +"checksum wasm-bindgen-shared 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" = "326c32126e1a157b6ced7400061a84ac5b11182b2cda6edad7314eb3ae9ac9fe" +"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +"checksum yew 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "479f32df0b06ef05893cbb3c195a0ae92b10587e2eff1969f1a7741d3fb96666" +"checksum yew-macro 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "92d6380df5f9c5d4cfd1191bd31ed44299f302e6040126d602ee8b262252cb7e" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6aadd05 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "galmon-web" +version = "0.1.0" +authors = ["Stefan Bühler "] +edition = "2018" + +[dependencies] +chrono = { version = "0.4.9", features = ["serde"] } +failure = "0.1.5" +percent-encoding = "2.1.0" +serde = { version = "1.0.99", features = ["rc"] } +serde_repr = "0.1.5" +stdweb = "0.4.18" +yew = "0.8.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..75183b9 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +Experimental, alternative frontend (written in rust) for https://galmon.eu + +Code and algorithms for map rendering heavily inspired (read "copied") from [d3-geo](https://github.com/d3/d3-geo). diff --git a/src/api/adapter_macro.rs b/src/api/adapter_macro.rs new file mode 100644 index 0000000..4aea719 --- /dev/null +++ b/src/api/adapter_macro.rs @@ -0,0 +1,106 @@ +// build adapter with custom serialization names / grouping of (flattened) optional entries +macro_rules! adapter { + ( + $adapter:ident => Option<$base:ident> { + $( + $(#[$field_meta:meta])* + $field:ident: $field_ty:ty, + )* + } + ) => { + #[derive(Debug, serde::Serialize, serde::Deserialize)] + pub struct $adapter { + $( + $(#[$field_meta])* + #[serde(default, skip_serializing_if = "Option::is_none")] + $field: Option<$field_ty>, + )* + } + + impl $adapter { + pub fn serialize(value: &Option<$base>, serializer: S) -> Result + where + S: serde::Serializer, + { + Self::from(value.clone()).serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + <$adapter as serde::Deserialize>::deserialize(deserializer).map(Option::<$base>::from) + } + } + + impl From> for $adapter { + fn from(base: Option<$base>) -> Self { + if let Some(base) = base { + Self { + $($field: Some(base.$field),)* + } + } else { + Self { + $($field: None,)* + } + } + } + } + + impl From<$adapter> for Option<$base> { + fn from(adapter: $adapter) -> Self { + Option::Some($base { + $($field: adapter.$field?,)* + }) + } + } + }; + ( + $adapter:ident => $base:ident { + $( + $(#[$field_meta:meta])* + $field:ident: $field_ty:ty, + )* + } + ) => { + #[derive(Debug, serde::Serialize, serde::Deserialize)] + pub struct $adapter { + $( + $(#[$field_meta])* + $field: $field_ty, + )* + } + + impl $adapter { + pub fn serialize(value: &$base, serializer: S) -> Result + where + S: serde::Serializer, + { + Self::from(value.clone()).serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result<$base, D::Error> + where + D: serde::Deserializer<'de>, + { + <$adapter as serde::Deserialize>::deserialize(deserializer).map($base::from) + } + } + + impl From<$base> for $adapter { + fn from(base: $base) -> Self { + Self { + $($field: base.$field,)* + } + } + } + + impl From<$adapter> for $base { + fn from(adapter: $adapter) -> Self { + Self { + $($field: adapter.$field,)* + } + } + } + }; +} diff --git a/src/api/apiservice.rs b/src/api/apiservice.rs new file mode 100644 index 0000000..51961e8 --- /dev/null +++ b/src/api/apiservice.rs @@ -0,0 +1,111 @@ +use failure::{format_err, Error, ResultExt}; +use serde::Deserialize; +use std::cell::{Cell, RefCell}; +use std::fmt; +use yew::prelude::*; +use yew::{ + format::{Json, Nothing, Text}, + services::fetch::{Request, Response, FetchService, FetchTask as YewFetchTask}, +}; +use crate::config::Config; + +pub struct FetchTask { + _task: YewFetchTask, +} + +impl FetchTask { + fn new(task: YewFetchTask) -> Self { + Self { _task: task } + } +} + +impl fmt::Debug for FetchTask { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("FetchTask") + } +} + +pub struct APIService { + send_message: Callback, + fetch_service: RefCell, +} + +impl APIService<()> { + pub fn new_no_message() -> Self { + Self::new(Callback::from(|()| ())) + } +} + +impl APIService { + pub fn new(send_message: Callback) -> Self + { + Self { + send_message, + fetch_service: RefCell::new(FetchService::new()), + } + } + + pub fn fetch(&self, req: Request, callback: F) -> Option + where + T: for<'de> Deserialize<'de>, + F: FnOnce(Result) -> Message + 'static, + IN: Into, + { + let method = req.method().clone(); + let url = req.uri().clone(); + let callback = Cell::new(Some(callback)); + let send_message = self.send_message.clone(); + let decode_response = move |response: Response| { + // only works once + let callback = callback.replace(None).unwrap(); + + let (parts, body) = response.into_parts(); + if !parts.status.is_success() { + if parts.headers.is_empty() { + // CORS failure + jslog!("{} {:?} failed due to CORS, can't see real error (status: {})", method, url, parts.status); + return send_message.emit(callback(Err(format_err!("{} {:?} failed due to CORS", method, url)))); + } + let e = format!("{} {:?} failed with {}", method, url, parts.status); + crate::log(&e); + return send_message.emit(callback(Err(failure::err_msg(e)))); + } + + let body = match body { + Err(e) => { + let e = format!("{} {:?} failed request body although status {} is fine: {}", method, url, parts.status, e); + crate::log(&e); + return send_message.emit(callback(Err(failure::err_msg(e)))); + }, + Ok(v) => v + }; + send_message.emit(callback(Json::>::from(Ok(body)).0.with_context(|e| format!("parsing response failed: {}", e)).map_err(Error::from))); + }; + let mut service = self.fetch_service.borrow_mut(); + Some(FetchTask::new(service.fetch(req, Callback::>::from(decode_response)))) + } + + pub fn get(&self, url: &str, callback: F) -> Option + where + T: for<'de> Deserialize<'de>, + F: FnOnce(Result) -> Message + 'static, + { + self.fetch(Request::get(url).body(Nothing).unwrap(), callback) + } + + pub fn api_get(&self, config: &Config, path: fmt::Arguments<'_>, callback: F) -> Option + where + T: for<'de> Deserialize<'de>, + F: FnOnce(Result) -> Message + 'static, + { + let url = format!("{}{}", config.base_url, path); + let req = Request::get(url).body(Nothing).unwrap(); + self.fetch(req, callback) + } +} + +impl fmt::Debug for APIService { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("APIService") + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..5b31d39 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,375 @@ +use failure::Error; +use serde::{Serialize, Deserialize}; +use std::collections::HashMap; +use std::time::SystemTime; + +#[macro_use] +mod adapter_macro; +mod apiservice; +pub mod world_geo; + +pub use self::apiservice::{APIService, FetchTask}; + +use crate::config::Config; + +impl APIService { + pub fn api_world_geo(&self, config: &Config, callback: F) -> Option + where + F: FnOnce(Result) -> Message + 'static, + { + self.api_get(config, format_args!("/geo/world.geojson"), callback) + } +} + +pub type DateTime = chrono::DateTime; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Global { + #[serde(rename = "gps-offset-ns")] + pub gps_offset_ns: f64, + #[serde(rename = "gps-utc-offset-ns")] + pub gps_utc_offset_ns: f64, + #[serde(rename = "last-seen", with = "chrono::serde::ts_seconds")] + pub last_seen: DateTime, + #[serde(rename = "leap-second-planned")] + pub leap_second_planned: bool, + #[serde(rename = "leap-seconds")] + pub leap_seconds: f64, + #[serde(rename = "utc-offset-ns")] + pub utc_offset_ns: f64, +} + +impl APIService { + pub fn api_global(&self, config: &Config, callback: F) -> Option + where + F: FnOnce(Result) -> Message + 'static, + { + self.api_get(config, format_args!("/global.json"), callback) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)] +#[repr(u8)] +pub enum GNS { + GPS = 0, // US + Galileo = 2, // EU + BeiDou = 3, // CN + Glonass = 6, // RU +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AlmanacEntry { + // NOT in Glonass + #[serde(rename = "eph-ecefX")] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub eph_ecef_x: Option, + #[serde(rename = "eph-ecefY")] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub eph_ecef_y: Option, + #[serde(rename = "eph-ecefZ")] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub eph_ecef_z: Option, + + // optional in Glonass + #[serde(rename = "eph-latitude")] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub eph_latitude: Option, + #[serde(rename = "eph-longitude")] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub eph_longitude: Option, + + // NOT in Glonass + pub t: Option, + pub t0e: Option, + + // all + pub gnssid: GNS, + pub name: String, + pub observed: bool, + pub inclination: f64, +} + +pub type Almanac = HashMap; + +impl APIService { + pub fn api_almanac(&self, config: &Config, callback: F) -> Option + where + F: FnOnce(Result) -> Message + 'static, + { + self.api_get(config, format_args!("/almanac.json"), callback) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Observer { + pub id: u32, + #[serde(rename = "last-seen", with = "chrono::serde::ts_seconds")] + pub last_seen: DateTime, + pub latitude: f64, + pub longitude: f64, + #[serde(rename = "svs")] + pub satellite_vehicles: HashMap, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ObservedSatelliteVehicle { + #[serde(rename = "age-s")] + pub age_s: f32, // u32? + pub azi: f64, + pub db: u32, + pub elev: f64, + #[serde(rename = "fullName")] + pub full_name: String, + pub gnss: GNS, + #[serde(rename = "last-seen")] + pub last_seen: i64, + pub name: String, + pub prres: f64, + pub sigid: u32, + pub sv: u32, +} + +pub type ObserverList = Vec; + +impl APIService { + pub fn api_observers(&self, config: &Config, callback: F) -> Option + where + F: FnOnce(Result) -> Message + 'static, + { + self.api_get(config, format_args!("/observers.json"), callback) + } +} + +#[derive(Clone, Copy, Debug)] +/// A certain point in time in some reference system, in seconds +pub struct Instant { + pub week_number: u16, // depending on source only might have rolled on 8-bit + pub time_of_week: u32, +} + +impl Instant { + pub fn system_time(self) -> SystemTime { + SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(self.epoch()) + } + + pub fn unroll_weeknumber(self) -> u32 { + let current_epoch = SystemTime::now().duration_since(SystemTime::now()).expect("before 1970-01-01").as_secs(); + let current_wn = (current_epoch / (86400*7)) as u32; + let wn = self.week_number as u32; + let wn_bits = (16 - self.week_number.leading_zeros()).min(8); // assume at least 8-bit precision in weeknumber + let wn_mask = !0u32 << wn_bits; + let round_up = 1 << (wn_bits - 1); // if we already reached "halftime" round up + wn + ((current_wn + round_up) & wn_mask) // add bits from current_wn + } + + pub fn epoch(self) -> u64 { + self.unroll_weeknumber() as u64 * (86400*7) + self.time_of_week as u64 + } +} + +#[derive(Clone, Copy, Debug)] +pub struct Vec3 { + pub x: f64, + pub y: f64, + pub z: f64, +} + +#[derive(Clone, Debug)] +pub struct ReferenceTimeOffset { + /// offset in 2**(-30) seconds at base Instant (close to nanoseconds) + pub base_offset: i32, + /// correction in 2**(-50) seconds per second since base Instant + pub correction: i32, + /// time at which constant offset was measured + pub base: Instant, + /// text describing delta for some "current" (last_seen) Instant in nanoseconds offset and change in nanoseconds per day. + pub delta: String, +} + +mod sv { + use super::*; + + adapter!{ + LastSeen => Instant { + #[serde(rename = "wn")] + week_number: u16, + #[serde(rename = "tow")] + time_of_week: u32, + } + } + + adapter!{ + Position => Option { + x: f64, + y: f64, + z: f64, + } + } + + adapter!{ + UtcOffsetInstant => Option { + #[serde(rename = "wn0t")] + week_number: u16, + #[serde(rename = "t0t")] + time_of_week: u32, + } + } + + adapter!{ + // bug https://github.com/ahupowerdns/galmon/issues/8: Glonass sends a0 and a1, but not the other values + UtcOffset => Option { + #[serde(rename = "a0")] + base_offset: i32, + #[serde(rename = "a1")] + correction: i32, + #[serde(flatten, with = "UtcOffsetInstant")] + base: Instant, + #[serde(rename = "delta-utc")] + delta: String, + } + } + + adapter!{ + GpsOffsetInstant => Option { + #[serde(rename = "wn0g")] + week_number: u16, + #[serde(rename = "t0g")] + time_of_week: u32, + } + } + + adapter!{ + GpsOffset => Option { + #[serde(rename = "a0g")] + base_offset: i32, + #[serde(rename = "a1g")] + correction: i32, + #[serde(flatten, with = "GpsOffsetInstant")] + base: Instant, + #[serde(rename = "delta-gps")] + delta: String, + } + } +} + +// TODO: "undo" flatten #[serde(default, skip_serializing_if = "Option::is_none")] + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SatelliteVehicle { + /* Identification: */ + gnssid: GNS, + svid: u32, /* vehicle id; can be moved between satellites */ + sigid: u32, /* "signal", also appended as "@{sigid}" to the full name */ + /* Data: */ + /// Signal In Space Accuracy + #[serde(default, skip_serializing_if = "Option::is_none")] + sisa: Option, + #[serde(rename = "eph-age-m", default, skip_serializing_if = "Option::is_none")] + /// Age of ephemeris in minutes + eph_age_m: Option, + + #[serde(rename = "best-tle", default, skip_serializing_if = "Option::is_none")] + best_tle: Option, + #[serde(rename = "best-tle-dist", default, skip_serializing_if = "Option::is_none")] + best_tle_dist: Option, + #[serde(rename = "best-tle-int-desig", default, skip_serializing_if = "Option::is_none")] + best_tle_int_desig: Option, + #[serde(rename = "best-tle-norad", default, skip_serializing_if = "Option::is_none")] + best_tle_norad: Option, + + #[serde(rename = "alma-dist", default, skip_serializing_if = "Option::is_none")] + alma_dist: Option, // distance from almanac position in kilometers + + #[serde(default, skip_serializing_if = "Option::is_none")] + aode: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + aodc: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + iod: Option, + // IOD data: + #[serde(default, skip_serializing_if = "Option::is_none")] + af0: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + af1: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + af2: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + t0c: Option, // clock epoch + + #[serde(flatten, with = "sv::Position")] + position: Option, + + // utc offset (all but Glonass): combined data + #[serde(flatten, with = "sv::UtcOffset")] + utc_offset: Option, + + // GPS offset (only Galileo and BeiDou) + #[serde(flatten, with = "sv::GpsOffset")] + gpc_offset: Option, + + #[serde(rename = "dtLS")] + dt_ls: i8, + + #[serde(default, skip_serializing_if = "Option::is_none")] + health: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + healthissue: Option, // some codes? + + // Galileo only: Health flags for E1 (common) and E5 (uncommon) frequencies. + #[serde(default, skip_serializing_if = "Option::is_none")] + e1bhs: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + e1bdvs: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + e5bhs: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + e5bdvs: Option, + + #[serde(rename = "latest-disco", default, skip_serializing_if = "Option::is_none")] + latest_disco: Option, + #[serde(rename = "latest-disco-age", default, skip_serializing_if = "Option::is_none")] + latest_disco_age: Option, + + #[serde(rename = "time-disco", default, skip_serializing_if = "Option::is_none")] + time_disco: Option, + + #[serde(flatten, with = "sv::LastSeen")] + last_seen: Instant, + + /// Number of seconds since we've last received from this SV. A satellite can be out of sight for a long time + #[serde(rename = "last-seen-s")] + last_seen_s: i64, + + #[serde(rename = "fullName")] + full_name: String, // format!("{}@{}", self.name, self.sigid) + name: String, + + perrecv: HashMap, // keys: Observer `id` +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SatelliteVehiclePerReceiver { + #[serde(rename = "elev")] + elevation: f64, + #[serde(rename = "azi", default, skip_serializing_if = "Option::is_none")] + azimuth: Option, + db: i32, + #[serde(rename = "last-seen-s")] + last_seen_s: i64, + prres: f64, + delta_hz: Option, + delta_hz_corr: Option, +} + +pub type SatelliteVehicles = HashMap; + +impl APIService { + pub fn api_satellite_vehicles(&self, config: &Config, callback: F) -> Option + where + F: FnOnce(Result) -> Message + 'static, + { + self.api_get(config, format_args!("/svs.json"), callback) + } +} diff --git a/src/api/world_geo.rs b/src/api/world_geo.rs new file mode 100644 index 0000000..b1fcc44 --- /dev/null +++ b/src/api/world_geo.rs @@ -0,0 +1,108 @@ +use serde::{Serialize, Deserialize}; +use std::fmt; + +macro_rules! tagtype { + ($name:ident: $tag:literal) => { + #[derive(Clone, Copy, Default, Debug)] + pub struct $name; + + impl Serialize for $name { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str($tag) + } + } + + impl<'de> Deserialize<'de> for $name { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + impl serde::de::Visitor<'_> for Visitor { + type Value = $name; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "tag type {:?}", $tag) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if v != $tag { + return Err(E::invalid_value(serde::de::Unexpected::Str(v), &$tag)); + } + Ok($name) + } + + } + deserializer.deserialize_str(Visitor) + } + } + }; +} + +tagtype!(TagFeatureCollection: "FeatureCollection"); + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FeatureCollection { + pub r#type: TagFeatureCollection, + pub features: Vec, +} + +tagtype!(TagFeature: "Feature"); + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Feature { + pub r#type: TagFeature, + pub id: String, + pub properties: FeatureProperties, + pub geometry: Geometry, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FeatureProperties { + pub name: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum Geometry { + Polygon { + coordinates: PolygonData, + }, + MultiPolygon { + coordinates: Vec, + }, +} + +// first list of points is outer "hull" (should be clockwise), remaining list of points are holes (counterclockwise) +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PolygonData(pub Vec>); + +#[derive(Clone, Copy, Debug)] +pub struct Position { + pub longitude: f32, + pub latitude: f32, +} + +impl Serialize for Position { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + (self.longitude, self.latitude).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Position { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + <(f32, f32)>::deserialize(deserializer).map(|(longitude, latitude)| Position { longitude, latitude }) + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..08702e0 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,9 @@ +use serde::{Serialize, Deserialize}; +use std::rc::Rc; + +#[derive(Default, Clone, Debug, Serialize, Deserialize)] +pub struct ConfigData { + pub base_url: String, +} + +pub type Config = Rc; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d8e696a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,42 @@ +#![recursion_limit="512"] + +#[macro_use] +extern crate stdweb; + +#[allow(unused_macros)] +macro_rules! jslog { + ($($tt:tt)*) => { + $crate::log(&format!($($tt)*)); + }; +} + +#[allow(unused_macros)] +macro_rules! jserror { + ($($tt:tt)*) => { + $crate::error(&format!($($tt)*)); + }; +} + +pub mod api; +pub mod config; +pub mod models; +pub mod ui; +pub mod uitools; +pub mod utils; + +#[inline(never)] +fn log(message: &str) { + yew::services::ConsoleService::new().log(message); +} + +#[inline(never)] +#[allow(unused)] +fn error(message: &str) { + yew::services::ConsoleService::new().error(message); +} + +fn main() { + yew::start_app::(); +} + +// https://galmon.eu/observers.json diff --git a/src/models/almanac.rs b/src/models/almanac.rs new file mode 100644 index 0000000..428fd49 --- /dev/null +++ b/src/models/almanac.rs @@ -0,0 +1,48 @@ +use crate::api; +use crate::config::Config; +use crate::models::helper::*; + +#[derive(Debug)] +pub struct AlmanacT { + config: Config, +} + +impl Model for AlmanacT { + type FetchData = api::Almanac; + const NAME: &'static str = "Almanac"; + + fn refresh(sr: &ModelHandle) -> Option { + let sref = sr.clone(); + let sd = sr.data(); + sr.service().api_almanac(&sd.config, move |result| { + sref.set_received(result); + }) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Almanac(pub(super) ModelHandle); + +impl std::ops::Deref for Almanac { + type Target = ModelHandle; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Almanac { + pub fn new(config: Config) -> Self { + Self(ModelHandle::new( + AlmanacT { config }, + (), + )) + } + + pub(super) fn new_shared(config: Config, base: ModelBase) -> Self { + Self(ModelHandle::new( + AlmanacT { config }, + base, + )) + } +} diff --git a/src/models/base.rs b/src/models/base.rs new file mode 100644 index 0000000..f2d7c9c --- /dev/null +++ b/src/models/base.rs @@ -0,0 +1,122 @@ +use crate::config::Config; +use crate::models::almanac::Almanac; +use crate::models::global::Global; +use crate::models::helper::{Callbacks, CallbackRegistration, ModelBase}; +use crate::models::observers::Observers; +use std::cell::Cell; +use std::rc::Rc; + +#[derive(Debug)] +struct Watch { + almanac: Almanac, + almanac_waiting: Cell, + global: Global, + global_waiting: Cell, + observers: Observers, + observers_waiting: Cell, + callbacks: Callbacks, +} + +impl Watch { + fn on_almanac_update(&self) { + self.almanac_waiting.set(false); + self.on_generic_update(); + } + + fn on_global_update(&self) { + self.global_waiting.set(false); + self.on_generic_update(); + } + + fn on_observers_update(&self) { + self.observers_waiting.set(false); + self.on_generic_update(); + } + + fn on_generic_update(&self) { + if !self.almanac_waiting.get() && !self.global_waiting.get() && !self.observers_waiting.get() { + self.callbacks.emit(); + } + } +} + +#[derive(Debug)] +pub struct Inner { + watch: Rc, + _almanac_cbr: CallbackRegistration, + _global_cbr: CallbackRegistration, + _observers_cbr: CallbackRegistration, +} + +/// All basic data +#[derive(Clone, Debug)] +pub struct Base(Rc); + +impl Base { + pub fn new(config: Config) -> Self { + let base = ModelBase::from(()); + let watch = Rc::new(Watch { + almanac: Almanac::new_shared(config.clone(), base.clone()), + almanac_waiting: Cell::new(true), + global: Global::new_shared(config.clone(), base.clone()), + global_waiting: Cell::new(true), + observers: Observers::new_shared(config, base), + observers_waiting: Cell::new(true), + callbacks: Callbacks::new(), + }); + + let _almanac_cbr = watch.almanac.register(yew::Callback::from({ + let watch = watch.clone(); + move |()| watch.on_almanac_update() + })); + let _global_cbr = watch.global.register(yew::Callback::from({ + let watch = watch.clone(); + move |()| watch.on_global_update() + })); + let _observers_cbr = watch.observers.register(yew::Callback::from({ + let watch = watch.clone(); + move |()| watch.on_observers_update() + })); + + let inner = Rc::new(Inner { + watch, + _almanac_cbr, + _global_cbr, + _observers_cbr, + }); + + Self(inner) + } + + pub fn register(&self, callback: yew::Callback<()>) -> CallbackRegistration { + self.0.watch.callbacks.register(callback) + } + + pub fn refresh(&self) { + self.0.watch.almanac_waiting.set(true); + self.0.watch.global_waiting.set(true); + self.0.watch.observers_waiting.set(true); + + self.0.watch.almanac.refresh(); + self.0.watch.global.refresh(); + self.0.watch.observers.refresh(); + } + + pub fn is_pending(&self) -> bool { + self.0.watch.almanac.is_pending() + || self.0.watch.global.is_pending() + || self.0.watch.observers.is_pending() + } + + pub fn almanac(&self) -> &Almanac { + &self.0.watch.almanac + } + + pub fn global(&self) -> &Global { + &self.0.watch.global + } + + pub fn observers(&self) -> &Observers { + &self.0.watch.observers + } +} diff --git a/src/models/global.rs b/src/models/global.rs new file mode 100644 index 0000000..472ebbe --- /dev/null +++ b/src/models/global.rs @@ -0,0 +1,48 @@ +use crate::api; +use crate::config::Config; +use crate::models::helper::*; + +#[derive(Debug)] +pub struct GlobalT { + config: Config, +} + +impl Model for GlobalT { + type FetchData = api::Global; + const NAME: &'static str = "Global"; + + fn refresh(sr: &ModelHandle) -> Option { + let sref = sr.clone(); + let sd = sr.data(); + sr.service().api_global(&sd.config, move |result| { + sref.set_received(result); + }) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Global(pub(super) ModelHandle); + +impl std::ops::Deref for Global { + type Target = ModelHandle; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Global { + pub fn new(config: Config) -> Self { + Self(ModelHandle::new( + GlobalT { config }, + (), + )) + } + + pub(super) fn new_shared(config: Config, base: ModelBase) -> Self { + Self(ModelHandle::new( + GlobalT { config }, + base, + )) + } +} diff --git a/src/models/helper.rs b/src/models/helper.rs new file mode 100644 index 0000000..6f71798 --- /dev/null +++ b/src/models/helper.rs @@ -0,0 +1,273 @@ +use failure::Error; +use std::cell::RefCell; +use std::fmt; +use std::rc::{Rc, Weak}; + +use crate::api; +use crate::utils::callbacks; + +// -------------------- callbacks -------------------- + +/// Callback registration. +/// +/// Callback will be unregistered when this is dropped. +#[derive(Default, Debug)] +pub struct CallbackRegistration(callbacks::CallbackRegistration<()>); + +#[derive(Debug)] +pub(super) struct Callbacks(callbacks::Callbacks<()>); + +impl Callbacks { + pub(super) fn new() -> Self { + Self(callbacks::Callbacks::new()) + } + + pub(super) fn register(&self, callback: yew::Callback<()>) -> CallbackRegistration { + CallbackRegistration(self.0.register(callback)) + } + + pub(super) fn emit(&self) { + self.0.emit(()); + } +} + +impl Drop for Callbacks { + fn drop(&mut self) { + // final emit to let all know we're gone + self.emit(); + } +} + +// -------------------- model data wrapper -------------------- + +#[derive(Debug)] +pub(super) struct ModelBase { + callbacks: Callbacks, + service: Rc>, +} + +impl Clone for ModelBase { + fn clone(&self) -> Self { + Self { + callbacks: Callbacks::new(), // always fresh callbacks + service: self.service.clone(), + } + } +} + +impl From<()> for ModelBase { + fn from(_: ()) -> Self { + Self { + callbacks: Callbacks::new(), + service: Rc::new(api::APIService::new_no_message()), + } + } +} + +impl From<&ModelHandle> for ModelBase { + fn from(model: &ModelHandle) -> Self { + model.inner.base.clone() + } +} + +#[derive(Debug)] +struct Inner { + base: ModelBase, + result: RefCell>>>, + fetch: RefCell>, + custom: T, +} + +mod hidden { + pub trait Model: Sized + std::fmt::Debug { + type FetchData: std::fmt::Debug; + const NAME: &'static str; + + fn refresh(sr: &super::ModelHandle) -> Option; + } +} +pub(super) use self::hidden::Model; + +pub struct ModelHandle { + inner: Rc>, +} + +impl ModelHandle { + pub(super) fn new(s: T, b: B) -> Self + where + B: Into, + { + let inner = Rc::new(Inner { + base: b.into(), + result: Default::default(), + fetch: Default::default(), + custom: s, + }); + Self { + inner, + } + } + + pub(super) fn service(&self) -> &api::APIService<()> { + &self.inner.base.service + } + + pub(super) fn data(&self) -> &T { + &self.inner.custom + } + + #[allow(unused)] + pub(super) fn merge_received(&self, result: Result, load: L, merge: M) + where + T::FetchData: Clone, + M: FnOnce(&mut T::FetchData, I), + L: FnOnce(I) -> T::FetchData, + { + let mut data = self.inner.result.borrow_mut(); + self.inner.fetch.replace(None); + + // result: RefCell>>>, + match result { + Ok(v) => { + if let Some(data) = &mut *data { + let data = Rc::make_mut(data); + if let Ok(data) = data { + // merge with existing data + merge(data, v); + } else { + // overwrite error + *data = Ok(load(v)); + } + } else { + // first reply + *data = Some(Rc::new(Ok(load(v)))); + } + }, + Err(e) => { + if data.is_none() { + // first reply, remember error + *data = Some(Rc::new(Err(format!("{}", e)))); + } + }, + } + + drop(data); // release lock before notify + self.inner.base.callbacks.emit(); + } + + pub(super) fn set_received(&self, result: Result) { + let mut data = self.inner.result.borrow_mut(); + self.inner.fetch.replace(None); + + // result: RefCell>>>, + match result { + Ok(v) => { + *data = Some(Rc::new(Ok(v))); + }, + Err(e) => { + if data.is_none() { + // first reply, remember error + *data = Some(Rc::new(Err(format!("{}", e)))); + } + }, + } + + drop(data); // release lock before notify + self.inner.base.callbacks.emit(); + } + + #[allow(unused)] + pub(super) fn downgrade(&self) -> ModelWeakHandle { + ModelWeakHandle { + inner: Rc::downgrade(&self.inner), + } + } + + /// Refresh data + pub fn refresh(&self) { + let mut fetch = self.inner.fetch.borrow_mut(); + if fetch.is_some() { return; } // pending request + *fetch = T::refresh(self); + } + + /// Whether a request is pending (doesn't mean no previous data is available) + pub fn is_pending(&self) -> bool { + let fetch = self.inner.fetch.borrow(); + fetch.is_some() + } + + /// Get data. + /// + /// If all fetches so far returned in an error returns the first error; + /// if no data was received yet (and no error was returned) an initial + /// refresh will be triggered. + pub fn get(&self) -> Option>> { + let result = self.inner.result.borrow(); + if result.is_none() { + self.refresh(); + } + result.clone() + } + + /// Register a callback to be called when data is refreshed. + pub fn register(&self, callback: yew::Callback<()>) -> CallbackRegistration { + self.inner.base.callbacks.register(callback) + } +} + +impl Clone for ModelHandle { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl From<&'_ ModelHandle> for ModelHandle { + fn from(r: &'_ ModelHandle) -> Self { + Self { + inner: r.inner.clone(), + } + } +} + +impl PartialEq for ModelHandle { + fn eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.inner, &other.inner) + } +} + +impl Eq for ModelHandle { } + +impl fmt::Debug for ModelHandle { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("ModelHandle<")?; + f.write_str(T::NAME)?; + f.write_str("(")?; + self.inner.fmt(f)?; + f.write_str(")")?; + Ok(()) + } +} + +#[allow(unused)] +pub struct ModelWeakHandle { + inner: Weak>, +} + +impl ModelWeakHandle { + #[allow(unused)] + pub(super) fn upgrade(&self) -> Option> { + Some(ModelHandle { + inner: self.inner.upgrade()?, + }) + } +} + +impl fmt::Debug for ModelWeakHandle { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("ModelWeakHandle<")?; + f.write_str(T::NAME)?; + f.write_str(">") + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..29b7524 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,13 @@ +mod almanac; +mod base; +mod global; +mod helper; +mod observers; +mod world_geo; + +pub use self::almanac::Almanac; +pub use self::base::Base; +pub use self::global::Global; +pub use self::helper::CallbackRegistration; +pub use self::observers::Observers; +pub use self::world_geo::WorldGeo; diff --git a/src/models/observers.rs b/src/models/observers.rs new file mode 100644 index 0000000..afc9adb --- /dev/null +++ b/src/models/observers.rs @@ -0,0 +1,48 @@ +use crate::api; +use crate::config::Config; +use crate::models::helper::*; + +#[derive(Debug)] +pub struct ObserversT { + config: Config, +} + +impl Model for ObserversT { + type FetchData = api::ObserverList; + const NAME: &'static str = "Observers"; + + fn refresh(sr: &ModelHandle) -> Option { + let sref = sr.clone(); + let sd = sr.data(); + sr.service().api_observers(&sd.config, move |result| { + sref.set_received(result); + }) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Observers(pub(super) ModelHandle); + +impl std::ops::Deref for Observers { + type Target = ModelHandle; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Observers { + pub fn new(config: Config) -> Self { + Self(ModelHandle::new( + ObserversT { config }, + (), + )) + } + + pub(super) fn new_shared(config: Config, base: ModelBase) -> Self { + Self(ModelHandle::new( + ObserversT { config }, + base, + )) + } +} diff --git a/src/models/world_geo.rs b/src/models/world_geo.rs new file mode 100644 index 0000000..0265675 --- /dev/null +++ b/src/models/world_geo.rs @@ -0,0 +1,41 @@ +use crate::api; +use crate::config::Config; +use crate::models::helper::*; + +#[derive(Debug)] +pub struct WorldGeoT { + config: Config, +} + +impl Model for WorldGeoT { + type FetchData = api::world_geo::FeatureCollection; + const NAME: &'static str = "WorldGeo"; + + fn refresh(sr: &ModelHandle) -> Option { + let sref = sr.clone(); + let sd = sr.data(); + sr.service().api_world_geo(&sd.config, move |result| { + sref.set_received(result); + }) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct WorldGeo(pub(super) ModelHandle); + +impl std::ops::Deref for WorldGeo { + type Target = ModelHandle; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl WorldGeo { + pub fn new(config: Config) -> Self { + Self(ModelHandle::new( + WorldGeoT { config }, + (), + )) + } +} diff --git a/src/ui/app.rs b/src/ui/app.rs new file mode 100644 index 0000000..de71077 --- /dev/null +++ b/src/ui/app.rs @@ -0,0 +1,207 @@ +use yew::prelude::*; + +use crate::api; +use crate::models; +use crate::ui::main::{Main, MainPath}; +use crate::uitools::routing::{self, Path as _}; +use std::rc::Rc; + +#[derive(Clone, Debug)] +pub struct BaseData { + almanac_data: Rc>, + global_data: Rc>, + observers_data: Rc>, +} + +impl BaseData { + pub fn new(base: &models::Base) -> Result> { + use crate::uitools::loading::{loading, load_error}; + + let almanac = base.almanac().get(); + let global = base.global().get(); + let observers = base.observers().get(); + + let almanac_data = if let Some(almanac_data) = almanac { + if let Err(e) = &*almanac_data { + return Err(load_error("almanac", || Msg::Refresh, e.as_str())); + } + almanac_data + } else { + return Err(loading("almanac")); + }; + let global_data = if let Some(global_data) = global { + if let Err(e) = &*global_data { + return Err(load_error("global", || Msg::Refresh, e.as_str())); + } + global_data + } else { + return Err(loading("global")); + }; + let observers_data = if let Some(observers_data) = observers { + if let Err(e) = &*observers_data { + return Err(load_error("observers", || Msg::Refresh, e.as_str())); + } + observers_data + } else { + return Err(loading("observers")); + }; + Ok(Self { + almanac_data, + global_data, + observers_data, + }) + } + + pub fn almanac(&self) -> &api::Almanac { + (*self.almanac_data).as_ref().unwrap() + } + + pub fn global(&self) -> &api::Global { + (*self.global_data).as_ref().unwrap() + } + + pub fn observers(&self) -> &api::ObserverList { + (*self.observers_data).as_ref().unwrap() + } +} + +pub enum Msg { + Goto(MainPath), + OnPopState(String), + Refresh, + Notify(()), +} + +pub struct MainApp { + base: models::Base, + _base_cbr: models::CallbackRegistration, + world_geo: models::WorldGeo, + _world_geo_cbr: models::CallbackRegistration, + _redraw_1sec: yew::services::interval::IntervalTask, + path: MainPath, + crumbs: Vec, +} + +impl Component for MainApp { + type Message = Msg; + type Properties = (); + + fn create(_properties: Self::Properties, mut link: ComponentLink) -> Self { + let mut interval_service = yew::services::IntervalService::new(); + + let onpopstate = |v: stdweb::Value| -> Msg { + Msg::OnPopState(v.into_string().unwrap()) + }; + let onpopstate = link.send_back(onpopstate); + let onpopstate = move |v| { onpopstate.emit(v) }; + + let pathstr = stdweb::js!{ + window.onpopstate = function(event) { + @{onpopstate}(window.location.hash.substr(1)); + }; + + return window.location.hash.substr(1); + }.into_string().unwrap(); + + let path = MainPath::parse(&pathstr); + let crumbs = routing::crumbs(&path); + + crate::log("starting app"); + + let config = std::rc::Rc::new(crate::config::ConfigData { + base_url: String::from("https://galmon.eu"), + }); + let base = models::Base::new(config.clone()); + + let world_geo = models::WorldGeo::new(config.clone()); + + Self { + _base_cbr: base.register(link.send_back(Msg::Notify)), + _world_geo_cbr: world_geo.register(link.send_back(Msg::Notify)), + _redraw_1sec: interval_service.spawn(std::time::Duration::from_secs(/* TODO */ 100), link.send_back(|_| Msg::Notify(()))), + base, + world_geo, + path, + crumbs, + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::Goto(path) => { + if self.path == path { + false + } else { + self.path = path; + self.crumbs = routing::crumbs(&self.path); + let pathstr = routing::make_path(&self.path).unwrap(); + stdweb::js!{ @(no_return) + window.location = "#" + @{pathstr}; + }; + true + } + }, + Msg::OnPopState(path) => { + let path = MainPath::parse(&path); + self.update(Msg::Goto(path)) + }, + Msg::Refresh => { + self.base.refresh(); + false + }, + Msg::Notify(()) => true, + } + } +} + +impl MainApp { + fn view_crumb(&self, crumb: &MainPath) -> Html { + let crumb = crumb.clone(); + let title = format!("{}", crumb); + if self.path == crumb { + html!{ + + } + } else { + html!{ + + } + } + } + + fn view_wait(&self) -> Html { + match BaseData::new(&self.base) { + Err(html) => html, + Ok(base) => html! { +
+ } + } + } +} + +impl Renderable for MainApp { + fn view(&self) -> Html { + html!{ +
+ + { self.view_wait() } +
+ } + } +} diff --git a/src/ui/main.rs b/src/ui/main.rs new file mode 100644 index 0000000..4972ede --- /dev/null +++ b/src/ui/main.rs @@ -0,0 +1,144 @@ +mod observer; +mod world_geo; + +use std::fmt; +use yew::prelude::*; + +use crate::models; +use crate::ui::app::BaseData; +use crate::uitools::routing; + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum MainPath { + Index, + Observers, + Map, +} + +impl Default for MainPath { + fn default() -> Self { + Self::Index + } +} + +impl fmt::Display for MainPath { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Index => write!(f, "Home"), + Self::Observers => write!(f, "Observer list"), + Self::Map => write!(f, "Map"), +// Self::Server(name, ServerPath::Zones(ZonesPath::Index)) => name.fmt(f), +// Self::Server(_, sp) => sp.fmt(f), + } + } +} + +impl routing::Path for MainPath { + fn fmt(&self, f: &mut dyn routing::PathFormatter) -> fmt::Result { + match self { + Self::Index => Ok(()), + Self::Observers => f.append("observers"), + Self::Map => f.append("map"), +// Self::Server(name, sp) => { +// f.append(&name)?; +// sp.fmt(f) +// }, + } + } + + fn parse(path: &str) -> Self { + let (next, rem) = routing::next_component(path); + match (&*next, rem) { + ("", _) => Self::Index, + ("observers", _) => Self::Observers, + ("map", _) => Self::Map, + // (name, rem) => Self::Server(Rc::new(name.into()), rem.map(ServerPath::parse).unwrap_or_default()), + _ => Self::Index, + } + } + + fn crumbs(&self, mut add: F) { + add(Self::Index); + match self { + Self::Index => { + add(Self::Observers); + add(Self::Map); + }, + Self::Observers => add(Self::Observers), + Self::Map => add(Self::Map), + // Self::Server(name, sp) => sp.crumbs(move |sp| add(Self::Server(name.clone(), sp))), + } + } +} + +#[derive(Properties)] +pub struct ModelProperties { + #[props(required)] + pub ongoto: Callback, + pub path: MainPath, + #[props(required)] + pub base: BaseData, + #[props(required)] + pub world_geo: models::WorldGeo, +} + +pub struct Main { + props: ModelProperties, +} + +pub enum Msg { + RefreshGeo, + Goto(MainPath), +} + +impl Main { + fn goto(&mut self, path: MainPath) -> bool { + if path == self.props.path { + false + } else { + self.props.path = path; + self.props.ongoto.emit(self.props.path.clone()); + true + } + } +} + +impl Component for Main { + type Message = Msg; + type Properties = ModelProperties; + + fn create(props: Self::Properties, _link: ComponentLink) -> Self { + Self { + props, + } + } + + fn change(&mut self, props: Self::Properties) -> ShouldRender { + self.props = props; + true + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::Goto(path) => { + self.goto(path) + }, + Msg::RefreshGeo => { + self.props.world_geo.refresh(); + false + } + } + } +} + +impl Renderable
for Main { + fn view(&self) -> Html { + match self.props.path { + MainPath::Index => html!{"Home"}, + MainPath::Observers => self.view_observer_list(), + MainPath::Map => crate::uitools::loading::show("map", self.props.world_geo.get(), || Msg::RefreshGeo, |world_geo| { + self.view_map(world_geo) + }), + } + } +} diff --git a/src/ui/main/observer.rs b/src/ui/main/observer.rs new file mode 100644 index 0000000..26f34df --- /dev/null +++ b/src/ui/main/observer.rs @@ -0,0 +1,33 @@ +use super::*; + +use crate::api::Observer; + +impl Main { + fn observer_list_row(&self, observer: &Observer) -> Html { + html!{ + { observer.id } + { crate::uitools::ago(observer.last_seen) } + { observer.longitude } + { observer.latitude } + } + } + + pub fn view_observer_list(&self) -> Html { + let observers = self.props.base.observers(); + html!{ + + + + + + + + + { for observers.iter().map(|observer| { + self.observer_list_row(observer) + }) } + +
{ "ID" }{ "Last seen" }{ "Longitude" }{ "Latitude" }
+ } + } +} diff --git a/src/ui/main/world_geo.rs b/src/ui/main/world_geo.rs new file mode 100644 index 0000000..a8d26a2 --- /dev/null +++ b/src/ui/main/world_geo.rs @@ -0,0 +1,345 @@ +mod cartesian; +mod contains; +mod clip; +mod clip_antimeridian; +mod path_sink; +mod resample; + +pub use self::cartesian::Cartesian; +pub use self::contains::polygon_contains_south; +pub use self::clip::{Clip, ClipControl, Clipper, PathClipper}; +pub use self::clip_antimeridian::ClipAntimeridian; +pub use self::path_sink::PathSink; +pub use self::resample::ResamplePath; + +use super::*; + +const F32_PRECISION: f32 = 1e-6; + +use crate::api::world_geo::{Feature, FeatureCollection, Geometry, PolygonData, Position}; + +#[derive(Clone, Copy, Debug)] +pub struct RadianPoint { + pub lambda: f32, // "longitude" + pub phi: f32, // "latitude" +} + +impl RadianPoint { + pub fn wrap(self) -> Self { + use std::f32::consts::{PI, FRAC_PI_2}; + + let lambda = if self.lambda.abs() <= PI { + self.lambda + } else { + self.lambda.signum() * ((self.lambda.abs() + PI) % (2.0 * PI) - PI) + }; + let phi = if self.phi.abs() <= FRAC_PI_2 { + self.phi + } else { + self.phi.signum() * ((self.phi.abs() + FRAC_PI_2) % PI - FRAC_PI_2) + }; + Self { lambda, phi } + } +} + +impl PartialEq for RadianPoint { + fn eq(&self, other: &RadianPoint) -> bool { + (self.lambda - other.lambda).abs() < F32_PRECISION + && (self.phi - other.phi).abs() < F32_PRECISION + } +} +impl Eq for RadianPoint {} + +impl From for RadianPoint { + fn from(pos: Position) -> Self { + Self { + lambda: pos.longitude.to_radians(), // + std::f32::consts::FRAC_PI_4, // some test rotation + phi: pos.latitude.to_radians(), + }.wrap() + } +} + +#[derive(Clone, Copy, Debug)] +pub struct ProjectedPoint { + pub x: f32, + pub y: f32, +} + +pub trait Projection { + fn project(&self, p: RadianPoint) -> ProjectedPoint; +} + +impl Projection for &P { + fn project(&self, p: RadianPoint) -> ProjectedPoint { + (**self).project(p) + } +} + +pub trait DrawPath { + type Point; + + // only valid during ring or path are "open" + fn line(&mut self, to: Self::Point, stroke: bool); + + // closed polygon + fn ring_start(&mut self) {} + fn ring_end(&mut self) {} + + // open path, not filled + fn path_start(&mut self) {} + fn path_end(&mut self) {} +} + +impl PathTransformer for &mut DP { + type Point = DP::Point; + type Sink = DP; + + fn sink(&mut self) -> &mut Self::Sink { + &mut **self + } + + fn transform_line(&mut self, to: Self::Point, stroke: bool) { + self.line(to, stroke); + } +} + +pub trait PathTransformer { + type Point; + type Sink: DrawPath; + + fn sink(&mut self) -> &mut Self::Sink; + fn transform_line(&mut self, to: Self::Point, stroke: bool); + + fn transform_ring_start(&mut self) { + self.sink().ring_start(); + } + + fn transform_ring_end(&mut self) { + self.sink().ring_end(); + } + + fn transform_path_start(&mut self) { + self.sink().path_start(); + } + + fn transform_path_end(&mut self) { + self.sink().path_end(); + } +} + +impl DrawPath for PT { + type Point = ::Point; + + fn line(&mut self, to: Self::Point, stroke: bool) { + self.transform_line(to, stroke); + } + + fn ring_start(&mut self) { + self.transform_ring_start(); + } + + fn ring_end(&mut self) { + self.transform_ring_end(); + } + + fn path_start(&mut self) { + self.transform_path_start(); + } + + fn path_end(&mut self) { + self.transform_path_end(); + } +} + +pub struct ApplyProjection { + pub projection: P, + pub sink: S, +} + +impl> ApplyProjection { + #[allow(unused)] + pub fn new(projection: P, sink: S) -> Self { + Self { + projection, + sink, + } + } +} + +impl> PathTransformer for ApplyProjection { + type Sink = S; + type Point = RadianPoint; + + fn sink(&mut self) -> &mut Self::Sink { + &mut self.sink + } + + fn transform_line(&mut self, to: Self::Point, stroke: bool) { + self.sink.line(self.projection.project(to), stroke); + } +} + +#[allow(unused)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum MapProjection { + EquiRectangular, + EqualEarth, + Fahey, +} + +impl Projection for MapProjection { + fn project(&self, p: RadianPoint) -> ProjectedPoint { + use std::f32::consts::{PI, FRAC_PI_2}; + + match self { + MapProjection::EquiRectangular => { + ProjectedPoint { + x: p.lambda.min(PI).max(-PI) * (180.0 / PI), + y: -p.phi.min(FRAC_PI_2).max(-FRAC_PI_2) * (180.0 / PI), + } + }, + MapProjection::EqualEarth => { + const A1: f32 = 1.340264; + const A2: f32 = -0.081106; + const A3: f32 = 0.000893; + const A4: f32 = 0.003796; + let m = (3.0f32).sqrt() / 2.0; + + let l = (m * p.phi.sin()).asin(); + let l2 = l * l; + let l6 = l2 * l2 * l2; + ProjectedPoint { + x: 70.0 * p.lambda * l.cos() / (m * (A1 + 3.0 * A2 * l2 + l6 * (7.0 * A3 + 9.0 * A4 * l2))), + y: -70.0 * l * (A1 + A2 * l2 + l6 * (A3 + A4 * l2)), + } + }, + MapProjection::Fahey => { + let fahey_k = 35.0f32.to_radians().cos(); + let t = (p.phi / 2.0).tan(); + ProjectedPoint { + x: 50.0 * p.lambda * fahey_k * (1.0 - t*t).sqrt(), + y: -50.0 * (1.0 + fahey_k) * t, + } + } + } + } +} + +pub trait DrawOnMap { + fn draw_on_map(&self, projection: MapProjection) -> Html; +} + +impl DrawOnMap for PolygonData { +/* + // wrapping output stream: + transformRadians(transformRotate(rotate)(preclip(projectResample(postclip(stream))))); + + -> first map input points to radian (*pi/180) + -> apply rotation (zero by default) + -> clip (pre) (clipAntimeridian by default) + -> resample and project + -> clip (post) (identity by default) +*/ + + fn draw_on_map(&self, projection: MapProjection) -> Html { + let mut sink = PathSink::new(); + + let mut resample_sink = ClipAntimeridian::new(ResamplePath::new(projection, sink.draw_path(), 0.5)); + // let mut resample_sink = ClipAntimeridian::new(ApplyProjection::new(projection, sink.draw_path())); + + for line in &self.0 { + let line = line.iter().cloned().map(RadianPoint::from); + + resample_sink.ring_start(); + for point in line { + resample_sink.line(point, true); + } + resample_sink.ring_end(); + } + drop(resample_sink); + + if let Some(stroke_path) = sink.stroke_path { + html!{ + + + + + } + } else { + html!{ + + } + } + } +} + +impl DrawOnMap for Geometry { + fn draw_on_map(&self, projection: MapProjection) -> Html { + match self { + Geometry::Polygon { coordinates } => coordinates.draw_on_map(projection), + Geometry::MultiPolygon { coordinates } => html!{ + { for coordinates.iter().map(|p| p.draw_on_map(projection)) } + }, + } + } +} + +impl DrawOnMap for Feature { + fn draw_on_map(&self, projection: MapProjection) -> Html { + html!{ + + { self.geometry.draw_on_map(projection) } + + } + } +} + +impl DrawOnMap for FeatureCollection { + fn draw_on_map(&self, projection: MapProjection) -> Html { + html!{ + + { for self.features.iter().map(|c| c.draw_on_map(projection)) } + + } + } +} + +fn build_grid(projection: MapProjection) -> Html
{ + let mut sink = PathSink::new(); + let mut resample_sink = ResamplePath::new(projection, sink.draw_path(), 0.5); + + for longitude in (-180..=180).step_by(10) { + let height = if longitude % 90 == 0 { 90 } else { 80 }; + let lambda = (longitude as f32).to_radians(); + let phi = (height as f32).to_radians(); + resample_sink.path_start(); + resample_sink.line(RadianPoint { lambda, phi: -phi }, true); + resample_sink.line(RadianPoint { lambda, phi: 0.0 }, true); + resample_sink.line(RadianPoint { lambda, phi: phi }, true); + resample_sink.path_end(); + } + for latitude in (-90..=90).step_by(10) { + let phi = (latitude as f32).to_radians(); + resample_sink.path_start(); + for longitude in (-180..=180).step_by(5) { + resample_sink.line(RadianPoint { lambda: (longitude as f32).to_radians(), phi }, true); + } + resample_sink.path_end(); + } + drop(resample_sink); + + html!{ + + } +} + +impl Main { + pub fn view_map(&self, world_geo: &FeatureCollection) -> Html { + html!{ + + { world_geo.draw_on_map(MapProjection::Fahey) } + { build_grid(MapProjection::Fahey) } + + } + } +} diff --git a/src/ui/main/world_geo/cartesian.rs b/src/ui/main/world_geo/cartesian.rs new file mode 100644 index 0000000..e0aa924 --- /dev/null +++ b/src/ui/main/world_geo/cartesian.rs @@ -0,0 +1,72 @@ +use super::RadianPoint; + +#[derive(Clone, Copy, Debug)] +pub struct Cartesian { + pub a: f32, + pub b: f32, + pub c: f32, +} + +impl Cartesian { + #[allow(unused)] + pub fn normalize(self) -> Self { + let norm = (self * self).sqrt(); + self / norm + } +} + +impl From for Cartesian { + fn from(p: RadianPoint) -> Self { + let cos_phi = p.phi.cos(); + Self { + a: cos_phi * p.lambda.cos(), + b: cos_phi * p.lambda.sin(), + c: p.phi.sin(), + } + } +} + +impl std::ops::Add for Cartesian { + type Output = Cartesian; + + fn add(self, rhs: Cartesian) -> Self::Output { + Cartesian { + a: self.a + rhs.a, + b: self.b + rhs.b, + c: self.c + rhs.c, + } + } +} + +// cross product +impl std::ops::BitXor for Cartesian { + type Output = Self; + + fn bitxor(self, rhs: Self) -> Self::Output { + Self { + a: self.b * rhs.c - self.c * rhs.b, + b: self.c * rhs.a - self.a * rhs.c, + c: self.a * rhs.b - self.b * rhs.a, + } + } +} + +impl std::ops::Mul for Cartesian { + type Output = f32; + + fn mul(self, rhs: Cartesian) -> Self::Output { + self.a * rhs.a + self.b * rhs.b + self.c * rhs.c + } +} + +impl std::ops::Div for Cartesian { + type Output = Cartesian; + + fn div(self, rhs: f32) -> Self::Output { + Cartesian { + a: self.a / rhs, + b: self.b / rhs, + c: self.c / rhs, + } + } +} diff --git a/src/ui/main/world_geo/clip.rs b/src/ui/main/world_geo/clip.rs new file mode 100644 index 0000000..60e8cc2 --- /dev/null +++ b/src/ui/main/world_geo/clip.rs @@ -0,0 +1,361 @@ +use super::{ + DrawPath, + RadianPoint, + PathTransformer, + polygon_contains_south, +}; + +struct RingState { + start_new_segment: bool, + segments: Vec>, +} + +pub struct ClipControl { + sink: S, + ring: Option, +} + +impl ClipControl +where + S: DrawPath, +{ + pub fn line(&mut self, to: RadianPoint, stroke: bool) { + if let Some(ring) = &mut self.ring { + if ring.start_new_segment { + ring.start_new_segment = false; + if ring.segments.last().map(|l| l.len() <= 1).unwrap_or(false) { + // last segment empty or only a single point; remove it + ring.segments.pop(); + } + ring.segments.push(vec![(to, stroke)]); + } else { + ring.segments.last_mut().unwrap().push((to, stroke)); + } + } else { + self.sink.line(to, stroke); + } + } + + pub fn split_path(&mut self) { + if let Some(ring) = &mut self.ring { + ring.start_new_segment = true; + } else { + self.sink.path_end(); + self.sink.path_start(); + } + } + + fn send_ring_segment(&mut self, segment: Vec<(RadianPoint, bool)>) { + self.sink.ring_start(); + for (point, stroke) in segment { + self.sink.line(point, stroke); + } + self.sink.ring_end(); + } +} + +pub trait PathClipper { + fn line(&mut self, control: &mut ClipControl, to: RadianPoint, stroke: bool) + where + S: DrawPath, + ; +} + +pub trait Clipper { + type PathClipper: PathClipper; + + fn create_clipper(&self) -> Self::PathClipper; + + fn interpolate(&self, control: &mut ClipControl, from_to: Option<(RadianPoint, RadianPoint)>, forward: bool) + where + S: DrawPath, + ; +} + +pub struct Clip +where + S: DrawPath, + C: Clipper, +{ + control: ClipControl, + polygon: Vec>, + polygon_first_stroke: bool, + segments_with_splits: Vec>, + clipper: C, + path_clipper: Option, +} + +impl Clip +where + S: DrawPath, + C: Clipper, +{ + pub fn new(sink: S, clipper: C) -> Self { + Self { + control: ClipControl { + sink, + ring: None, + }, + polygon: Vec::new(), + polygon_first_stroke: false, + segments_with_splits: Vec::new(), + clipper, + path_clipper: None, + } + } + + fn clip_rejoin(&mut self, contains_south: bool) { + let segments = std::mem::replace(&mut self.segments_with_splits, Vec::new()); + + struct Intersection { + point: RadianPoint, + clip_next: usize, + clip_prev: usize, + entry: bool, + visited: bool, + } + + impl Intersection { + fn new(point: RadianPoint) -> Self { + Intersection { + point, + clip_next: 0, + clip_prev: 0, + entry: false, + visited: false, + } + } + + fn compare(a: &&mut Self, b: &&mut Self) -> std::cmp::Ordering { + let x = &a.point; + let y = &b.point; + // start with crossings on the west (left) side (going north), + // then continue on the east side (going south) + let x_is_west = x.lambda < 0.0; + let y_is_west = y.lambda < 0.0; + if x_is_west != y_is_west { + return std::cmp::Ord::cmp(&x_is_west, &y_is_west); + } + // now both on the same side + if x_is_west { + // up is the natural "phi" ordering + std::cmp::PartialOrd::partial_cmp(&x.phi, &y.phi).unwrap() + } else { + // down needs reverse + std::cmp::PartialOrd::partial_cmp(&y.phi, &x.phi).unwrap() + } + } + } + + let mut rem_segments = Vec::new(); + let mut intersections = Vec::new(); + + for segment in segments { + if segment.len() <= 1 { continue; } + let first = segment[0].0; + let last = segment[segment.len()-1].0; + + if segment.len() < 3 || first == last { + self.control.send_ring_segment(segment); + continue; + } + rem_segments.push(segment); + + // always two intersections entries correspond to the same segment + intersections.push(Intersection::new(first)); + intersections.push(Intersection::new(last)); + } + + if rem_segments.is_empty() { + // TODO: check winding order of exterior ring to add sphere? + return; + } + + { + let ix_base = intersections.as_ptr(); + let mut ix_sort: Vec<_> = intersections.iter_mut().collect(); + ix_sort.sort_by(Intersection::compare); + let last_ndx = ix_sort.len() - 1; + let index_of = |ix: &Intersection| -> usize { + let ix: *const Intersection = ix as _; + (ix as usize - ix_base as usize) / std::mem::size_of::() + }; + ix_sort[0].clip_prev = index_of(ix_sort[last_ndx]); + ix_sort[last_ndx].clip_next = index_of(ix_sort[0]); + for ndx in 0..last_ndx { + ix_sort[ndx].clip_next = index_of(ix_sort[ndx+1]); + ix_sort[ndx+1].clip_prev = index_of(ix_sort[ndx]); + } + // if we don't contain south pole the first entry on the "bottom left" (south east) is an entry + let mut entry = contains_south; + for ix in ix_sort { + entry = !entry; + ix.entry = entry; + } + } + + let n = intersections.len(); + loop { + let mut current_ndx = match intersections.iter().position(|ix| !ix.visited) { + Some(v) => v, + None => break, // done + }; + + self.control.sink.ring_start(); + loop { + // start with subject + intersections[current_ndx].visited = true; + if 0 == (current_ndx & 1) { + let segment = &rem_segments[current_ndx / 2]; + for (point, stroke) in segment.iter() { + self.control.sink.line(*point, *stroke); + } + current_ndx = (current_ndx + 1) % n; + } else { + // this should be very unlikely unless the input is broken. + let segment = &rem_segments[current_ndx / 2]; + let mut next_stroke = segment[0].1; + for (point, stroke) in segment.iter().rev() { + self.control.sink.line(*point, next_stroke); + next_stroke = *stroke; + } + current_ndx = (current_ndx + (n - 1)) % n; + } + if intersections[current_ndx].visited { break; } + // now alternate to clip + intersections[current_ndx].visited = true; + if intersections[current_ndx].entry { + let next_ndx = intersections[current_ndx].clip_next; + let from = intersections[current_ndx].point; + let to = intersections[next_ndx].point; + self.clipper.interpolate(&mut self.control, Some((from, to)), true); + current_ndx = next_ndx; + } else { + let next_ndx = intersections[current_ndx].clip_prev; + let from = intersections[current_ndx].point; + let to = intersections[next_ndx].point; + self.clipper.interpolate(&mut self.control, Some((from, to)), false); + current_ndx = next_ndx; + } + if intersections[current_ndx].visited { break; } + } + self.control.sink.ring_end(); + } + } + + // pub? trait? + fn sphere(&mut self) { + self.control.sink.ring_start(); + self.clipper.interpolate(&mut self.control, None, true); + self.control.sink.ring_end(); + } +} + +impl PathTransformer for Clip +where + S: DrawPath, + C: Clipper, +{ + type Point = RadianPoint; + type Sink = S; + + fn sink(&mut self) -> &mut Self::Sink { + &mut self.control.sink + } + + fn transform_line(&mut self, to: Self::Point, stroke: bool) { + let path_clipper = self.path_clipper.as_mut().expect("missing geometry state"); + if self.control.ring.is_some() { + let current_poly = self.polygon.last_mut().unwrap(); + if current_poly.is_empty() { + self.polygon_first_stroke = stroke; + } + // remember original polygon rings for contains check + current_poly.push(to); + } + path_clipper.line(&mut self.control, to, stroke); + } + + fn transform_ring_start(&mut self) { + assert!(self.path_clipper.is_none()); + assert!(self.control.ring.is_none()); + self.path_clipper = Some(self.clipper.create_clipper()); + self.control.ring = Some(RingState { + start_new_segment: true, + segments: Vec::new(), + }); + self.polygon.push(Vec::new()); + } + + fn transform_ring_end(&mut self) { + assert!(self.path_clipper.is_some()); + assert!(self.control.ring.is_some()); + + let current_poly = self.polygon.last().unwrap(); + if current_poly.len() < 1 { + // no data or single point, skip + self.control.ring = None; + self.path_clipper = None; + self.polygon.pop(); + return; + } + + // close ring in clip and reset clipper + self.path_clipper.take().unwrap().line(&mut self.control, current_poly[0], self.polygon_first_stroke); + + if current_poly.len() < 2 { + // doesn't describe an are, don't remember for contains check + self.polygon.pop(); + } + + let mut segments = self.control.ring.take().unwrap().segments; + if segments.is_empty() { return; } + if segments.len() == 1 { + // no splits, just forward to sink + let segment = segments.pop().unwrap(); + self.control.send_ring_segment(segment); + return; + } + + // rejoin last and first segment, as they should touch + { + let mut first = segments.swap_remove(0); // now [0] contains previous last + segments[0].append(&mut first); + } + + // delay handling until we have all rings of polygon + self.segments_with_splits.append(&mut segments); + } + + fn transform_path_start(&mut self) { + assert!(self.path_clipper.is_none()); + assert!(self.control.ring.is_none()); + self.path_clipper = Some(self.clipper.create_clipper()); + self.control.sink.path_start(); + } + + fn transform_path_end(&mut self) { + assert!(self.path_clipper.take().is_some()); // reset path_clipper + assert!(self.control.ring.is_none()); + self.control.sink.path_end(); + } +} + +impl Drop for Clip +where + S: DrawPath, + C: Clipper, +{ + fn drop(&mut self) { + let contains_south = polygon_contains_south(&self.polygon); + + if self.segments_with_splits.is_empty() { + if contains_south { + self.sphere(); + } + } else { + jslog!("contains_south: {}", contains_south); + self.clip_rejoin(contains_south); + } + } +} diff --git a/src/ui/main/world_geo/clip_antimeridian.rs b/src/ui/main/world_geo/clip_antimeridian.rs new file mode 100644 index 0000000..dbc4042 --- /dev/null +++ b/src/ui/main/world_geo/clip_antimeridian.rs @@ -0,0 +1,146 @@ +use super::{ + Cartesian, + Clip, + ClipControl, + Clipper, + PathClipper, + RadianPoint, + DrawPath, + F32_PRECISION, +}; + +fn _clip_longitude_degeneracies(p: RadianPoint) -> RadianPoint { + use std::f32::consts::PI; + + let border = PI.copysign(p.lambda); + if (p.lambda - border).abs() < F32_PRECISION { + RadianPoint { + lambda: p.lambda - (border * F32_PRECISION), + phi: p.phi, + } + } else { + p + } +} + +// line from a to b crosses antimeridian: calculate at which latitude ("phi") it intersects. +fn _clip_antimeridian_intersect(a: RadianPoint, b: RadianPoint) -> f32 { + let a = _clip_longitude_degeneracies(a); + let b = _clip_longitude_degeneracies(b); + + let x = Cartesian::from(a); + let y = Cartesian::from(b); + + let result_adjacent = x.b * y.a - y.b * x.a; + + if result_adjacent.abs() > F32_PRECISION { + let result_opposite = x.c * y.b - y.c * x.b; + (result_opposite / result_adjacent).atan() + } else { + (a.phi + b.phi) * 0.5 + } +} + +pub struct ClipAntimeridian; + +impl ClipAntimeridian { + #[allow(unused)] + pub fn new(sink: S) -> Clip + where + S: DrawPath, + { + Clip::new(sink, ClipAntimeridian) + } +} + +impl Clipper for ClipAntimeridian { + type PathClipper = ClipAntimeridianPathClipper; + + fn create_clipper(&self) -> Self::PathClipper { + ClipAntimeridianPathClipper { + first_prev: None + } + } + + fn interpolate(&self, control: &mut ClipControl, from_to: Option<(RadianPoint, RadianPoint)>, forward: bool) + where + S: DrawPath, + { + use std::f32::consts::{PI, FRAC_PI_2}; + let forward_sign = if forward { 1.0 } else { -1.0 }; + + if let Some((from, to)) = from_to { + if (to.lambda - from.lambda).abs() > PI { + // crossing antimeridian + let lambda = PI.copysign(to.lambda - from.lambda); + let phi = forward_sign * lambda / 2.0; + control.line(RadianPoint { lambda: -lambda, phi }, false); + control.line(RadianPoint { lambda: 0.0, phi }, false); + control.line(RadianPoint { lambda: lambda, phi }, false); + } else { + control.line(to, false); + } + } else { + let phi = forward_sign * FRAC_PI_2; + control.line(RadianPoint { lambda: -PI, phi: phi }, false); + control.line(RadianPoint { lambda: 0.0, phi: phi }, false); + control.line(RadianPoint { lambda: PI, phi: phi }, false); + control.line(RadianPoint { lambda: PI, phi: 0.0 }, false); + control.line(RadianPoint { lambda: PI, phi: -phi }, false); + control.line(RadianPoint { lambda: 0.0, phi: -phi }, false); + control.line(RadianPoint { lambda: -PI, phi: -phi }, false); + control.line(RadianPoint { lambda: -PI, phi: 0.0 }, false); + control.line(RadianPoint { lambda: -PI, phi: phi }, false); + } + } +} + +pub struct ClipAntimeridianPathClipper { + first_prev: Option<((RadianPoint, bool), bool, RadianPoint)>, +} + +impl PathClipper for ClipAntimeridianPathClipper { + fn line(&mut self, control: &mut ClipControl, next: RadianPoint, stroke: bool) + where + S: DrawPath, + { + use std::f32::consts::{PI, FRAC_PI_2}; + + let next_positive = next.lambda > 0.0; + + if let Some(((first, first_stroke), prev_positive, prev)) = &mut self.first_prev { + let delta = (next.lambda - prev.lambda).abs(); + if (delta - PI).abs() < F32_PRECISION { // line crosses a pole + // north or south? + let phi_side = if (prev.phi + next.phi) > 0.0 { FRAC_PI_2 } else { -FRAC_PI_2 }; + // go to pole on same latitude as prev point + control.line(RadianPoint { lambda: prev.lambda, phi: phi_side }, stroke); + // then to the "nearer" side (east/west), still at the pole ("same point", but projection might differ) + control.line(RadianPoint { lambda: PI.copysign(prev.lambda), phi: phi_side }, false); + control.split_path(); + // start at side (east/west), but at pole + *first = RadianPoint { lambda: PI.copysign(next.lambda), phi: phi_side }; + *first_stroke = stroke; + control.line(*first, false); + // move to pole on same latitude as next point + control.line(RadianPoint { lambda: next.lambda, phi: phi_side }, false); + } else if *prev_positive != next_positive && delta >= PI { // line crosses antimeridian + // get latitude for the intersection + let phi_side = _clip_antimeridian_intersect(*prev, next); + // move to intersection on prev side + control.line(RadianPoint { lambda: PI.copysign(prev.lambda), phi: phi_side }, stroke); + control.split_path(); + // start at intersection on next side + *first = RadianPoint { lambda: PI.copysign(next.lambda), phi: phi_side }; + *first_stroke = stroke; + control.line(*first, false); + } + + *prev_positive = next_positive; + *prev = next; + } else { + self.first_prev = Some(((next, stroke), next_positive, next)); + } + control.line(next, stroke); + } +} diff --git a/src/ui/main/world_geo/contains.rs b/src/ui/main/world_geo/contains.rs new file mode 100644 index 0000000..aae9729 --- /dev/null +++ b/src/ui/main/world_geo/contains.rs @@ -0,0 +1,25 @@ +use super::RadianPoint; + +fn wrap_longitude(lambda: f32) -> f32 { + use std::f32::consts::PI; + + if lambda.abs() <= PI { + lambda + } else { + lambda.signum() * ((lambda.abs() + PI) % (2.0 * PI) - PI) + } +} + +pub fn polygon_contains_south(polygon: &[Vec]) -> bool { + let mut angle_sum = 0.0; + for ring in polygon { + if let Some(mut prev) = ring.last() { + for point in ring { + angle_sum += wrap_longitude(point.lambda - prev.lambda); + prev = point; + } + } + } + + angle_sum < -std::f32::consts::PI +} diff --git a/src/ui/main/world_geo/path_sink.rs b/src/ui/main/world_geo/path_sink.rs new file mode 100644 index 0000000..4c7762c --- /dev/null +++ b/src/ui/main/world_geo/path_sink.rs @@ -0,0 +1,108 @@ +use super::{ + ProjectedPoint, + DrawPath, +}; + +pub struct PathSink { + pub path: String, + pub stroke_path: Option, +} + +impl PathSink { + pub fn new() -> Self { + PathSink { + path: String::new(), + stroke_path: None, + } + } +} + +impl PathSink { + fn _write(&mut self, path: std::fmt::Arguments<'_>) { + use std::fmt::Write; + self.path.write_fmt(path).unwrap(); + if let Some(stroke_path) = &mut self.stroke_path { + stroke_path.write_fmt(path).unwrap(); + } + } + + fn _write2(&mut self, path_stroke: std::fmt::Arguments<'_>, path_no_stroke: std::fmt::Arguments<'_>) { + use std::fmt::Write; + let stroke_path = { + let path = &self.path; + self.stroke_path.get_or_insert_with(|| path.clone()) + }; + self.path.write_fmt(path_no_stroke).unwrap(); + stroke_path.write_fmt(path_stroke).unwrap(); + } + + pub fn draw_path(&mut self) -> impl DrawPath + '_ { + #[derive(Clone, Copy, PartialEq, Eq, Debug)] + enum State { + Ring, + Path, + } + + struct Draw<'a> { + sink: &'a mut PathSink, + empty: bool, + state: Option, + }; + + impl DrawPath for Draw<'_> { + type Point = ProjectedPoint; + + fn line(&mut self, to: ProjectedPoint, stroke: bool) { + let state = self.state.expect("need to be in some active geometry (path, ring)"); + if self.empty { + self.sink._write(format_args!("M{:.3},{:.3}", to.x, to.y)); + self.empty = false; + } else if stroke { + self.sink._write(format_args!("L{:.3},{:.3}", to.x, to.y)); + } else if state == State::Path { + // "no stroke" in path is simple move + self.sink._write(format_args!("M{:.3},{:.3}", to.x, to.y)); + } else { + self.sink._write2( + format_args!("M{:.3},{:.3}", to.x, to.y), + format_args!("L{:.3},{:.3}", to.x, to.y), + ); + } + } + + fn ring_start(&mut self) { + assert_eq!(self.state, None); + assert!(self.empty); + self.state = Some(State::Ring); + } + + fn ring_end(&mut self) { + assert_eq!(self.state, Some(State::Ring)); + if !self.empty { + self.sink._write(format_args!("Z")); + self.empty = true; + } + self.state = None; + } + + // open path, not filled + fn path_start(&mut self) { + assert_eq!(self.state, None); + assert!(self.empty); + self.state = Some(State::Path); + } + + fn path_end(&mut self) { + assert_eq!(self.state, Some(State::Path)); + self.empty = true; + self.state = None; + } + } + + Draw { + sink: self, + empty: true, + state: None, + } + } +} diff --git a/src/ui/main/world_geo/resample.rs b/src/ui/main/world_geo/resample.rs new file mode 100644 index 0000000..162bbe6 --- /dev/null +++ b/src/ui/main/world_geo/resample.rs @@ -0,0 +1,130 @@ +pub use super::*; + +#[derive(Clone, Copy, Debug)] +struct SegmentPoint { + input: RadianPoint, + cartesian: Cartesian, + projected: ProjectedPoint, +} + +impl SegmentPoint { + fn new

(projection: P, input: RadianPoint) -> Self + where + P: Projection, + { + SegmentPoint { + input, + cartesian: input.into(), + projected: projection.project(input), + } + } +} + +fn resample_segment(sink: &mut S, projection: &P, from: SegmentPoint, to: SegmentPoint, stroke: bool, recursion_limit: u32, resolution: f32) +where + P: Projection, + S: DrawPath, +{ + let cos_min_distance = 30.0f32.to_radians().cos(); + + let dx = to.projected.x - from.projected.x; + let dy = to.projected.y - from.projected.y; + let dist_square = dx*dx + dy*dy; + if dist_square > (4.0 * resolution) && recursion_limit > 1 { + let mid_cartesian = { + // normalize mid point between from and to (onto sphere) + let s = from.cartesian + to.cartesian; // normalizing anyway, drop `*0.5` + s / (s * s).sqrt() + }; + let mid_lambda = if (mid_cartesian.c.abs() - 1.0).abs() < F32_PRECISION + || (from.input.lambda - to.input.lambda).abs() < F32_PRECISION + { + // close to poles (a and b will be near zero, atan2 would fail) + // or from/to lambdas close together (atan2 should work though in this case, but it would convert -pi to pi) + (from.input.lambda + to.input.lambda) / 2.0 + } else { + f32::atan2(mid_cartesian.b, mid_cartesian.a) + }; + let mid_input = RadianPoint { + phi: mid_cartesian.c.asin(), + lambda: mid_lambda, + }; + let mid = SegmentPoint { + input: mid_input, + cartesian: mid_cartesian, + projected: projection.project(mid_input), + }; + + let dx2 = mid.projected.x - from.projected.x; + let dy2 = mid.projected.y - from.projected.y; + // (dy, -dx): orthogonal vector to (dx, dy) + // norm(dy, -dx) * (dx2, dy2): (projected) distance of mid from line between from and to + let mid_line_dist_square = { + let d = dy*dx2 - dx*dy2; + (d * d) / dist_square + }; + // norm(dx, dy) * (dx2, dy2): (projected) "progress" of mid *on* the line between from and to + // let mid_progress = (dx*dx2 + dy*dy2) / dist_square; + if mid_line_dist_square > resolution // perpendicular projected distance + // /* this is broken and probably not needed */ || (mid_progress - 0.5).abs() > 0.3 // midpoint close to an end + || (to.cartesian * from.cartesian) < cos_min_distance // angular distance + { + resample_segment(sink, projection, from, mid, stroke, recursion_limit - 1, resolution); + sink.line(mid.projected, stroke); + resample_segment(sink, projection, mid, to, stroke, recursion_limit - 1, resolution); + } + } +} + +pub struct ResamplePath { + projection: P, + sink: S, + resolution: f32, + recursion_limit: u32, + start_prev: Option<(bool, SegmentPoint, SegmentPoint)>, +} + +impl> ResamplePath { + pub fn new(projection: P, sink: S, resolution: f32) -> Self { + Self { + projection, + sink, + resolution, + recursion_limit: 16, + start_prev: None, + } + } +} + +impl> PathTransformer for ResamplePath { + type Sink = S; + type Point = RadianPoint; + + fn sink(&mut self) -> &mut Self::Sink { + &mut self.sink + } + + fn transform_line(&mut self, to: Self::Point, stroke: bool) { + let to = SegmentPoint::new(&self.projection, to); + if let Some((_init_stroke, _start, prev)) = &mut self.start_prev { + resample_segment(&mut self.sink, &self.projection, *prev, to, stroke, self.recursion_limit, self.resolution); + self.sink.line(to.projected, stroke); + *prev = to; + } else { + self.sink.line(to.projected, stroke); + self.start_prev = Some((stroke, to, to)); + } + } + + fn transform_ring_end(&mut self) { + if let Some((init_stroke, start, prev)) = self.start_prev.take() { + resample_segment(&mut self.sink, &self.projection, prev, start, init_stroke, self.recursion_limit, self.resolution); + } + self.sink().ring_end(); + } + + fn transform_path_end(&mut self) { + self.start_prev = None; + self.sink().path_end(); + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..97cf99b --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,2 @@ +pub mod app; +pub mod main; diff --git a/src/uitools/chrono.rs b/src/uitools/chrono.rs new file mode 100644 index 0000000..8a01f06 --- /dev/null +++ b/src/uitools/chrono.rs @@ -0,0 +1,60 @@ +use chrono::{DateTime, Utc}; +use std::fmt; +use yew::prelude::*; + +pub fn now() -> DateTime { + let now = stdweb::web::Date::now() / 1000.0; + let now_floor = now.floor(); + let secs = now_floor as i64; + let nanosecs = ((now - now_floor) * 1e9).round() as u32; + DateTime::from_utc(chrono::NaiveDateTime::from_timestamp(secs, nanosecs), Utc) +} + +struct PrettyFormatDuration(chrono::Duration); + +impl fmt::Display for PrettyFormatDuration { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.0 == chrono::Duration::zero() { + return write!(f, "now"); + } + + let (abs_duration, future) = if self.0 < chrono::Duration::zero() { + (-self.0, true) + } else { + (self.0, false) + }; + if future { + write!(f, "in ")?; + } + + if abs_duration.num_seconds() < 2 { + // write!(f, "{}ms", abs_duration.num_milliseconds())?; + write!(f, "now")?; + } else if abs_duration.num_seconds() < 120 { + write!(f, "{}s", abs_duration.num_seconds())?; + } else if abs_duration.num_minutes() < 120 { + write!(f, "{}m", abs_duration.num_minutes())?; + } else if abs_duration.num_hours() < 48 { + write!(f, "{}h", abs_duration.num_hours())?; + } else { + write!(f, "{}d", abs_duration.num_days())?; + } + + if !future { + write!(f, " ago")?; + } + + Ok(()) + } +} + +pub fn ago(dt: DateTime) -> Html +where + COMP: Component, +{ + let duration = now().signed_duration_since(dt); + let dt_s = format!("{}", dt); + html! { + + } +} diff --git a/src/uitools/icons.rs b/src/uitools/icons.rs new file mode 100644 index 0000000..cfd4124 --- /dev/null +++ b/src/uitools/icons.rs @@ -0,0 +1,17 @@ +use yew::prelude::*; + +pub fn icon_unlock() -> Html { + html!{ } +} + +pub fn icon_lock() -> Html { + html!{ } +} + +pub fn icon_refresh() -> Html { + html!{ } +} + +pub fn icon_question_circle() -> Html { + html!{ } +} diff --git a/src/uitools/loading.rs b/src/uitools/loading.rs new file mode 100644 index 0000000..281ce9e --- /dev/null +++ b/src/uitools/loading.rs @@ -0,0 +1,62 @@ +use std::rc::Rc; +use yew::prelude::*; + +pub fn loading(title: &str) -> Html +where + COMP: Component, +{ + html!{ +

+ + { "Loading " } { title } { "..." } +
+ } +} + +pub fn load_error(title: &str, refresh: M, error: &str) -> Html +where + COMP: Component + Renderable, + M: Fn() -> COMP::Message + 'static, +{ + html!{ +
+ + { crate::uitools::icons::icon_refresh() } + { "Loading " } { title } { " failed: " }{ error } + +
+ } +} + +pub fn show(title: &str, data: Option>>, refresh: M, display: D) -> Html +where + COMP: Component + Renderable, + M: Fn() -> COMP::Message + 'static, + D: FnOnce(&T) -> Html, +{ + if let Some(data) = data { + match &*data { + Ok(v) => display(v), + Err(e) => load_error(title, refresh, e.as_str()), + } + } else { + loading(title) + } +} + +pub fn show_with(title: &str, data: Option>>, refresh: M, display: D, display_fail: F) -> Html +where + COMP: Component + Renderable, + M: Fn() -> COMP::Message + 'static, + D: FnOnce(&T) -> Html, + F: FnOnce(Html) -> Html, +{ + if let Some(data) = data { + match &*data { + Ok(v) => display(v), + Err(e) => display_fail(load_error(title, refresh, e.as_str())), + } + } else { + display_fail(loading(title)) + } +} diff --git a/src/uitools/mod.rs b/src/uitools/mod.rs new file mode 100644 index 0000000..7afd703 --- /dev/null +++ b/src/uitools/mod.rs @@ -0,0 +1,6 @@ +mod chrono; +pub mod icons; +pub mod loading; +pub mod routing; + +pub use self::chrono::ago; diff --git a/src/uitools/routing.rs b/src/uitools/routing.rs new file mode 100644 index 0000000..abe64b7 --- /dev/null +++ b/src/uitools/routing.rs @@ -0,0 +1,48 @@ +use std::borrow::Cow; +use std::fmt; + +pub trait PathFormatter { + fn append(&mut self, component: &str) -> fmt::Result; +} + +pub trait Path: Default + fmt::Display + Clone { + fn fmt(&self, f: &mut dyn PathFormatter) -> fmt::Result; + fn parse(path: &str) -> Self; + fn crumbs(&self, mut add: F) { + add(self.clone()); + } +} + +pub fn next_component(path: &str) -> (Cow<'_, str>, Option<&str>) { + let (next, rem) = match path.find('/') { + None => (path, None), + Some(sep) => (&path[..sep], Some(&path[sep+1..])), + }; + (percent_encoding::percent_decode_str(next).decode_utf8_lossy(), rem) +} + +// fragment specials plus directory separator +const FRAGMENT_DIR: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`').add(b'/').add(b'%'); +struct PathString(String); +impl PathFormatter for PathString { + fn append(&mut self, component: &str) -> fmt::Result { + use std::fmt::Write; + if self.0.is_empty() { + write!(&mut self.0, "{}", percent_encoding::utf8_percent_encode(component, FRAGMENT_DIR)) + } else { + write!(&mut self.0, "/{}", percent_encoding::utf8_percent_encode(component, FRAGMENT_DIR)) + } + } +} + +pub fn make_path(path: &P) -> Result { + let mut p = PathString(String::new()); + Path::fmt(path, &mut p)?; + Ok(p.0) +} + +pub fn crumbs(path: &P) -> Vec

{ + let mut crumbs = Vec::new(); + path.crumbs(|crumb| crumbs.push(crumb)); + crumbs +} diff --git a/src/utils/callbacks.rs b/src/utils/callbacks.rs new file mode 100644 index 0000000..e11dbd1 --- /dev/null +++ b/src/utils/callbacks.rs @@ -0,0 +1,140 @@ +use std::fmt; +use std::rc::{Rc, Weak}; +use std::mem::ManuallyDrop; + +local_dl_list! { + mod cblist { + link CallbackLink; + head CallbackHead; + member link of CallbackEntry; + } +} +local_dl_list! { + mod cbexeclist { + link CallbackExecLink; + head CallbackExecHead; + member exec_link of CallbackEntry; + } +} + +struct CallbackEntry { + link: CallbackLink, + exec_link: CallbackExecLink, + callback: yew::Callback, +} + +struct InnerCallbacks { + cbhead: CallbackHead, +} + +impl Drop for InnerCallbacks { + fn drop(&mut self) { + unsafe { + while let Some(nodeptr) = self.cbhead.pop_front() { + // free entry + drop(Box::from_raw(nodeptr as *mut CallbackEntry)); + } + } + } +} + +pub struct Callbacks { + inner: Rc>, +} + +impl Callbacks { + pub fn new() -> Self { + Self { + inner: Rc::new(InnerCallbacks { + cbhead: CallbackHead::new(), + }), + } + } + + pub fn register(&self, callback: yew::Callback) -> CallbackRegistration { + let entry = ManuallyDrop::new(Box::new(CallbackEntry { + link: CallbackLink::new(), + exec_link: CallbackExecLink::new(), + callback, + })); + unsafe { self.inner.cbhead.append(&entry); } + + CallbackRegistration { + inner: Rc::downgrade(&self.inner), + entry: &entry as &CallbackEntry as *const _, + } + } + + pub fn emit(&self, data: T) + where + T: Clone, + { + unsafe { + // copy list first so we don't run into problems when callbacks modify it + let cbexec = CallbackExecHead::new(); + + if let Some(mut nodeptr) = self.inner.cbhead.front() { + loop { + let entry: &CallbackEntry = &*nodeptr; + entry.exec_link.unlink(); + cbexec.append(&entry); + match self.inner.cbhead.next(entry) { + Some(nextptr) => nodeptr = nextptr, + None => break, + } + } + } + + while let Some(nodeptr) = cbexec.pop_front() { + let entry: &CallbackEntry = &*nodeptr; + entry.callback.emit(data.clone()); + if entry.link.is_unlinked() { + // free entry + drop(Box::from_raw(nodeptr as *mut CallbackEntry)); + } + } + } + } +} + +impl fmt::Debug for Callbacks { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("Callbacks") + } +} + +pub struct CallbackRegistration { + inner: Weak>, + entry: *const CallbackEntry, +} + +impl Default for CallbackRegistration { + fn default() -> Self { + Self { + inner: Weak::default(), + entry: std::ptr::null(), + } + } +} + +impl Drop for CallbackRegistration { + fn drop(&mut self) { + if let Some(_inner) = self.inner.upgrade() { + // if inner isn't alive anymore the callback has already been freed. + unsafe { + let entry = &*self.entry; + entry.link.unlink(); + if entry.exec_link.is_unlinked() { + // not in exec list, free now + drop(Box::from_raw(self.entry as *mut CallbackEntry)); + } + } + } + } +} + +impl fmt::Debug for CallbackRegistration { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("CallbackRegistration") + } +} diff --git a/src/utils/local_dl_list.rs b/src/utils/local_dl_list.rs new file mode 100644 index 0000000..020b4b0 --- /dev/null +++ b/src/utils/local_dl_list.rs @@ -0,0 +1,257 @@ +use std::cell::Cell; +use std::ptr; + +#[doc(hidden)] +#[derive(Debug)] +pub struct LocalDLHead { + prev: Cell<*const LocalDLHead>, + next: Cell<*const LocalDLHead>, +} + +impl LocalDLHead { + // only internal use, no need for Default trait + pub const fn new() -> Self { + Self { + prev: Cell::new(ptr::null()), + next: Cell::new(ptr::null()), + } + } + + pub fn init(&self) { + if self.next.get().is_null() { + self.next.set(self); + self.prev.set(self); + } + } + + pub fn is_unlinked(&self) -> bool { + self.next.get().is_null() || self.next.get() == (self as _) + } + + pub unsafe fn unlink(&self) { + if !self.is_unlinked() { + /* unsafe */ { &*self.prev.get() }.next.set(self.next.get()); + /* unsafe */ { &*self.next.get() }.prev.set(self.prev.get()); + } + self.next.set(ptr::null()); + self.prev.set(ptr::null()); + } + + pub unsafe fn insert_after(&self, node: &Self) { + debug_assert!(node.is_unlinked()); + assert_ne!(self as *const _, node as *const _); + self.init(); + node.next.set(self.next.get()); + node.prev.set(self); + /* unsafe */ { &*node.next.get() }.prev.set(node); + self.next.set(node); + } + + pub unsafe fn insert_before(&self, node: &Self) { + debug_assert!(node.is_unlinked()); + assert_ne!(self as *const _, node as *const _); + self.init(); + node.next.set(self); + node.prev.set(self.prev.get()); + /* unsafe */ { &*node.prev.get() }.next.set(node); + self.prev.set(node); + } + + pub unsafe fn next(&self) -> Option<*const Self> { + if self.is_unlinked() { + return None; + } + let node = self.next.get(); + Some(node) + } + + pub unsafe fn pop_front(&self) -> Option<*const Self> { + if self.is_unlinked() { + return None; + } + let node = self.next.get(); + /* unsafe */ { &*node }.unlink(); + Some(node) + } + + pub unsafe fn prev(&self) -> Option<*const Self> { + if self.is_unlinked() { + return None; + } + let node = self.prev.get(); + Some(node) + } + + pub unsafe fn pop_back(&self) -> Option<*const Self> { + if self.is_unlinked() { + return None; + } + let node = self.prev.get(); + /* unsafe */ { &*node }.unlink(); + Some(node) + } + + pub unsafe fn take_from(&mut self, other: &Self) { + debug_assert!(self.is_unlinked()); + if !other.is_unlinked() { + other.insert_after(self); + other.unlink(); + } + } +} + +impl Default for LocalDLHead { + fn default() -> Self { + Self::new() + } +} + +impl Drop for LocalDLHead { + fn drop(&mut self) { + assert!(self.is_unlinked()); + } +} + +macro_rules! local_dl_list { + (mod $($tt:tt)*) => { + _local_dl_list! { + [pub(self)] [pub(super)] mod $($tt)* + } + }; + (pub mod $($tt:tt)*) => { + _local_dl_list! { + [pub] [pub] mod $($tt)* + } + }; + (pub(crate) mod $($tt:tt)*) => { + _local_dl_list! { + [pub(crate)] [pub(crate)] mod $($tt)* + } + }; +} + +#[doc(hidden)] +macro_rules! _local_dl_list { + ([$vis:vis] [$innervis:vis] mod $modname:ident { + link $link_name:ident; + head $head_name:ident; + member $member:ident of $parent:ident; + }) => { + mod $modname { + use super::$parent; + use $crate::utils::local_dl_list::LocalDLHead; + use core::marker::PhantomData; + + #[derive(Debug)] + $innervis struct $link_name { + head: LocalDLHead, + _mark: PhantomData, + } + + #[allow(dead_code)] + impl $link_name { + fn __base_from_node(ptr: *const LocalDLHead) -> *const $parent { + let member_offset: usize = { + let p = core::ptr::NonNull::<$parent>::dangling(); + let $parent:: { $member: member, .. } = unsafe { &*p.as_ptr() }; + let head: &LocalDLHead = &member.head; + (head as *const _ as usize) - (p.as_ptr() as *const _ as usize) + }; + unsafe { (ptr as *const u8).sub(member_offset) as *const $parent } + } + + $innervis const fn new() -> Self { + Self { + head: LocalDLHead::new(), + _mark: PhantomData, + } + } + + $innervis fn is_unlinked(&self) -> bool { + self.head.is_unlinked() + } + + $innervis unsafe fn unlink(&self) { + self.head.unlink(); + } + + $innervis unsafe fn insert_after(&self, node: &$parent) { + let node_link: &Self = &node.$member; + self.head.insert_after(&node_link.head); + } + + $innervis unsafe fn insert_before(&self, node: &$parent) { + let node_link: &Self = &node.$member; + self.head.insert_before(&node_link.head); + } + } + + #[derive(Debug)] + $innervis struct $head_name { + head: LocalDLHead, + _mark: PhantomData, + } + + #[allow(dead_code)] + impl $head_name { + $innervis const fn new() -> Self { + Self { + head: LocalDLHead::new(), + _mark: PhantomData, + } + } + + $innervis fn is_empty(&self) -> bool { + self.head.is_unlinked() + } + + $innervis unsafe fn prepend(&self, node: &$parent) { + let node_link: &$link_name = &node.$member; + self.head.insert_after(&node_link.head); + } + + $innervis unsafe fn append(&self, node: &$parent) { + let node_link: &$link_name = &node.$member; + self.head.insert_before(&node_link.head); + } + + $innervis unsafe fn front(&self) -> Option<*const $parent> { + let node_link = self.head.next()?; + Some($link_name::::__base_from_node(node_link)) + } + + $innervis unsafe fn back(&self) -> Option<*const $parent> { + let node_link = self.head.prev()?; + Some($link_name::::__base_from_node(node_link)) + } + + $innervis unsafe fn next(&self, node: &$parent) -> Option<*const $parent> { + let node_link = node.$member.head.next()?; + if node_link as *const LocalDLHead == &self.head as *const LocalDLHead { return None; } + Some($link_name::::__base_from_node(node_link)) + } + + $innervis unsafe fn prev(&self, node: &$parent) -> Option<*const $parent> { + let node_link = node.$member.head.prev()?; + if node_link as *const LocalDLHead == &self.head as *const LocalDLHead { return None; } + Some($link_name::::__base_from_node(node_link)) + } + + $innervis unsafe fn pop_front(&self) -> Option<*const $parent> { + let node_link = self.head.pop_front()?; + Some($link_name::::__base_from_node(node_link)) + } + + $innervis unsafe fn pop_back(&self) -> Option<*const $parent> { + let node_link = self.head.pop_back()?; + Some($link_name::::__base_from_node(node_link)) + } + + $innervis unsafe fn take_from(&mut self, other: &Self) { + self.head.take_from(&other.head); + } + } + } + $vis use self::$modname::{$link_name, $head_name}; + }; +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..9f9d6a1 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,3 @@ +#[macro_use] +mod local_dl_list; +pub mod callbacks; diff --git a/static/.gitignore b/static/.gitignore new file mode 100644 index 0000000..d344ba6 --- /dev/null +++ b/static/.gitignore @@ -0,0 +1 @@ +config.json diff --git a/static/bootstrap-debian b/static/bootstrap-debian new file mode 120000 index 0000000..9cbd468 --- /dev/null +++ b/static/bootstrap-debian @@ -0,0 +1 @@ +/usr/share/javascript/bootstrap4/ \ No newline at end of file diff --git a/static/fontawesome-debian b/static/fontawesome-debian new file mode 120000 index 0000000..3437336 --- /dev/null +++ b/static/fontawesome-debian @@ -0,0 +1 @@ +/usr/share/fonts-font-awesome \ No newline at end of file diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..f895038 --- /dev/null +++ b/static/index.html @@ -0,0 +1,64 @@ + + + + galmon + + + + + + + + + + + + + + +