initial commit
This commit is contained in:
commit
b9de50bb2b
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
**/*.rs.bk
|
607
Cargo.lock
generated
Normal file
607
Cargo.lock
generated
Normal file
@ -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"
|
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "galmon-web"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Stefan Bühler <stbuehler@web.de>"]
|
||||||
|
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"
|
3
README.md
Normal file
3
README.md
Normal file
@ -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).
|
106
src/api/adapter_macro.rs
Normal file
106
src/api/adapter_macro.rs
Normal file
@ -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<S>(value: &Option<$base>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
Self::from(value.clone()).serialize(serializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<$base>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
<$adapter as serde::Deserialize>::deserialize(deserializer).map(Option::<$base>::from)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<$base>> 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<S>(value: &$base, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
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,)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
111
src/api/apiservice.rs
Normal file
111
src/api/apiservice.rs
Normal file
@ -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<Message: 'static> {
|
||||||
|
send_message: Callback<Message>,
|
||||||
|
fetch_service: RefCell<FetchService>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl APIService<()> {
|
||||||
|
pub fn new_no_message() -> Self {
|
||||||
|
Self::new(Callback::from(|()| ()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message: 'static> APIService<Message> {
|
||||||
|
pub fn new(send_message: Callback<Message>) -> Self
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
send_message,
|
||||||
|
fetch_service: RefCell::new(FetchService::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch<T, F, IN>(&self, req: Request<IN>, callback: F) -> Option<FetchTask>
|
||||||
|
where
|
||||||
|
T: for<'de> Deserialize<'de>,
|
||||||
|
F: FnOnce(Result<T, Error>) -> Message + 'static,
|
||||||
|
IN: Into<Text>,
|
||||||
|
{
|
||||||
|
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<Text>| {
|
||||||
|
// 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::<Result<T, Error>>::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::<Response<Text>>::from(decode_response))))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get<T, F>(&self, url: &str, callback: F) -> Option<FetchTask>
|
||||||
|
where
|
||||||
|
T: for<'de> Deserialize<'de>,
|
||||||
|
F: FnOnce(Result<T, Error>) -> Message + 'static,
|
||||||
|
{
|
||||||
|
self.fetch(Request::get(url).body(Nothing).unwrap(), callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn api_get<T, F>(&self, config: &Config, path: fmt::Arguments<'_>, callback: F) -> Option<FetchTask>
|
||||||
|
where
|
||||||
|
T: for<'de> Deserialize<'de>,
|
||||||
|
F: FnOnce(Result<T, Error>) -> Message + 'static,
|
||||||
|
{
|
||||||
|
let url = format!("{}{}", config.base_url, path);
|
||||||
|
let req = Request::get(url).body(Nothing).unwrap();
|
||||||
|
self.fetch(req, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message> fmt::Debug for APIService<Message> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.write_str("APIService")
|
||||||
|
}
|
||||||
|
}
|
375
src/api/mod.rs
Normal file
375
src/api/mod.rs
Normal file
@ -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<Message: 'static> APIService<Message> {
|
||||||
|
pub fn api_world_geo<F>(&self, config: &Config, callback: F) -> Option<FetchTask>
|
||||||
|
where
|
||||||
|
F: FnOnce(Result<world_geo::FeatureCollection, Error>) -> Message + 'static,
|
||||||
|
{
|
||||||
|
self.api_get(config, format_args!("/geo/world.geojson"), callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type DateTime = chrono::DateTime<chrono::Utc>;
|
||||||
|
|
||||||
|
#[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<Message: 'static> APIService<Message> {
|
||||||
|
pub fn api_global<F>(&self, config: &Config, callback: F) -> Option<FetchTask>
|
||||||
|
where
|
||||||
|
F: FnOnce(Result<Global, Error>) -> 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<f64>,
|
||||||
|
#[serde(rename = "eph-ecefY")]
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub eph_ecef_y: Option<f64>,
|
||||||
|
#[serde(rename = "eph-ecefZ")]
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub eph_ecef_z: Option<f64>,
|
||||||
|
|
||||||
|
// optional in Glonass
|
||||||
|
#[serde(rename = "eph-latitude")]
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub eph_latitude: Option<f64>,
|
||||||
|
#[serde(rename = "eph-longitude")]
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub eph_longitude: Option<f64>,
|
||||||
|
|
||||||
|
// NOT in Glonass
|
||||||
|
pub t: Option<f64>,
|
||||||
|
pub t0e: Option<f64>,
|
||||||
|
|
||||||
|
// all
|
||||||
|
pub gnssid: GNS,
|
||||||
|
pub name: String,
|
||||||
|
pub observed: bool,
|
||||||
|
pub inclination: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Almanac = HashMap<String, AlmanacEntry>;
|
||||||
|
|
||||||
|
impl<Message: 'static> APIService<Message> {
|
||||||
|
pub fn api_almanac<F>(&self, config: &Config, callback: F) -> Option<FetchTask>
|
||||||
|
where
|
||||||
|
F: FnOnce(Result<Almanac, Error>) -> 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<String, ObservedSatelliteVehicle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Observer>;
|
||||||
|
|
||||||
|
impl<Message: 'static> APIService<Message> {
|
||||||
|
pub fn api_observers<F>(&self, config: &Config, callback: F) -> Option<FetchTask>
|
||||||
|
where
|
||||||
|
F: FnOnce(Result<ObserverList, Error>) -> 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<Vec3> {
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
z: f64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter!{
|
||||||
|
UtcOffsetInstant => Option<Instant> {
|
||||||
|
#[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<ReferenceTimeOffset> {
|
||||||
|
#[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<Instant> {
|
||||||
|
#[serde(rename = "wn0g")]
|
||||||
|
week_number: u16,
|
||||||
|
#[serde(rename = "t0g")]
|
||||||
|
time_of_week: u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter!{
|
||||||
|
GpsOffset => Option<ReferenceTimeOffset> {
|
||||||
|
#[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<String>,
|
||||||
|
#[serde(rename = "eph-age-m", default, skip_serializing_if = "Option::is_none")]
|
||||||
|
/// Age of ephemeris in minutes
|
||||||
|
eph_age_m: Option<f32>,
|
||||||
|
|
||||||
|
#[serde(rename = "best-tle", default, skip_serializing_if = "Option::is_none")]
|
||||||
|
best_tle: Option<String>,
|
||||||
|
#[serde(rename = "best-tle-dist", default, skip_serializing_if = "Option::is_none")]
|
||||||
|
best_tle_dist: Option<f64>,
|
||||||
|
#[serde(rename = "best-tle-int-desig", default, skip_serializing_if = "Option::is_none")]
|
||||||
|
best_tle_int_desig: Option<String>,
|
||||||
|
#[serde(rename = "best-tle-norad", default, skip_serializing_if = "Option::is_none")]
|
||||||
|
best_tle_norad: Option<i32>,
|
||||||
|
|
||||||
|
#[serde(rename = "alma-dist", default, skip_serializing_if = "Option::is_none")]
|
||||||
|
alma_dist: Option<f64>, // distance from almanac position in kilometers
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
aode: Option<u32>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
aodc: Option<u32>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
iod: Option<u16>,
|
||||||
|
// IOD data:
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
af0: Option<i32>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
af1: Option<i32>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
af2: Option<u8>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
t0c: Option<u16>, // clock epoch
|
||||||
|
|
||||||
|
#[serde(flatten, with = "sv::Position")]
|
||||||
|
position: Option<Vec3>,
|
||||||
|
|
||||||
|
// utc offset (all but Glonass): combined data
|
||||||
|
#[serde(flatten, with = "sv::UtcOffset")]
|
||||||
|
utc_offset: Option<ReferenceTimeOffset>,
|
||||||
|
|
||||||
|
// GPS offset (only Galileo and BeiDou)
|
||||||
|
#[serde(flatten, with = "sv::GpsOffset")]
|
||||||
|
gpc_offset: Option<ReferenceTimeOffset>,
|
||||||
|
|
||||||
|
#[serde(rename = "dtLS")]
|
||||||
|
dt_ls: i8,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
health: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
healthissue: Option<u32>, // some codes?
|
||||||
|
|
||||||
|
// Galileo only: Health flags for E1 (common) and E5 (uncommon) frequencies.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
e1bhs: Option<u8>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
e1bdvs: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
e5bhs: Option<u8>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
e5bdvs: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(rename = "latest-disco", default, skip_serializing_if = "Option::is_none")]
|
||||||
|
latest_disco: Option<f64>,
|
||||||
|
#[serde(rename = "latest-disco-age", default, skip_serializing_if = "Option::is_none")]
|
||||||
|
latest_disco_age: Option<f64>,
|
||||||
|
|
||||||
|
#[serde(rename = "time-disco", default, skip_serializing_if = "Option::is_none")]
|
||||||
|
time_disco: Option<f64>,
|
||||||
|
|
||||||
|
#[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<u32, SatelliteVehiclePerReceiver>, // 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<f64>,
|
||||||
|
db: i32,
|
||||||
|
#[serde(rename = "last-seen-s")]
|
||||||
|
last_seen_s: i64,
|
||||||
|
prres: f64,
|
||||||
|
delta_hz: Option<f64>,
|
||||||
|
delta_hz_corr: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SatelliteVehicles = HashMap<String, SatelliteVehicle>;
|
||||||
|
|
||||||
|
impl<Message: 'static> APIService<Message> {
|
||||||
|
pub fn api_satellite_vehicles<F>(&self, config: &Config, callback: F) -> Option<FetchTask>
|
||||||
|
where
|
||||||
|
F: FnOnce(Result<SatelliteVehicles, Error>) -> Message + 'static,
|
||||||
|
{
|
||||||
|
self.api_get(config, format_args!("/svs.json"), callback)
|
||||||
|
}
|
||||||
|
}
|
108
src/api/world_geo.rs
Normal file
108
src/api/world_geo.rs
Normal file
@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str($tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for $name {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
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<E>(self, v: &str) -> Result<Self::Value, E>
|
||||||
|
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<Feature>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<PolygonData>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<Vec<Position>>);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Position {
|
||||||
|
pub longitude: f32,
|
||||||
|
pub latitude: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Position {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
(self.longitude, self.latitude).serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Position {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
<(f32, f32)>::deserialize(deserializer).map(|(longitude, latitude)| Position { longitude, latitude })
|
||||||
|
}
|
||||||
|
}
|
9
src/config.rs
Normal file
9
src/config.rs
Normal file
@ -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<ConfigData>;
|
42
src/main.rs
Normal file
42
src/main.rs
Normal file
@ -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::<ui::app::MainApp>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://galmon.eu/observers.json
|
48
src/models/almanac.rs
Normal file
48
src/models/almanac.rs
Normal file
@ -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<Self>) -> Option<api::FetchTask> {
|
||||||
|
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<AlmanacT>);
|
||||||
|
|
||||||
|
impl std::ops::Deref for Almanac {
|
||||||
|
type Target = ModelHandle<AlmanacT>;
|
||||||
|
|
||||||
|
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,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
122
src/models/base.rs
Normal file
122
src/models/base.rs
Normal file
@ -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<bool>,
|
||||||
|
global: Global,
|
||||||
|
global_waiting: Cell<bool>,
|
||||||
|
observers: Observers,
|
||||||
|
observers_waiting: Cell<bool>,
|
||||||
|
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<Watch>,
|
||||||
|
_almanac_cbr: CallbackRegistration,
|
||||||
|
_global_cbr: CallbackRegistration,
|
||||||
|
_observers_cbr: CallbackRegistration,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All basic data
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Base(Rc<Inner>);
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
48
src/models/global.rs
Normal file
48
src/models/global.rs
Normal file
@ -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<Self>) -> Option<api::FetchTask> {
|
||||||
|
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<GlobalT>);
|
||||||
|
|
||||||
|
impl std::ops::Deref for Global {
|
||||||
|
type Target = ModelHandle<GlobalT>;
|
||||||
|
|
||||||
|
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,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
273
src/models/helper.rs
Normal file
273
src/models/helper.rs
Normal file
@ -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<api::APIService<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<T: Model> From<&ModelHandle<T>> for ModelBase {
|
||||||
|
fn from(model: &ModelHandle<T>) -> Self {
|
||||||
|
model.inner.base.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Inner<F, T> {
|
||||||
|
base: ModelBase,
|
||||||
|
result: RefCell<Option<Rc<Result<F, String>>>>,
|
||||||
|
fetch: RefCell<Option<api::FetchTask>>,
|
||||||
|
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<Self>) -> Option<super::api::FetchTask>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(super) use self::hidden::Model;
|
||||||
|
|
||||||
|
pub struct ModelHandle<T: Model> {
|
||||||
|
inner: Rc<Inner<T::FetchData, T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Model> ModelHandle<T> {
|
||||||
|
pub(super) fn new<B>(s: T, b: B) -> Self
|
||||||
|
where
|
||||||
|
B: Into<ModelBase>,
|
||||||
|
{
|
||||||
|
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<M, L, I>(&self, result: Result<I, Error>, 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<Option<Rc<Result<F, String>>>>,
|
||||||
|
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<T::FetchData, Error>) {
|
||||||
|
let mut data = self.inner.result.borrow_mut();
|
||||||
|
self.inner.fetch.replace(None);
|
||||||
|
|
||||||
|
// result: RefCell<Option<Rc<Result<F, String>>>>,
|
||||||
|
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<T> {
|
||||||
|
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<Rc<Result<T::FetchData, String>>> {
|
||||||
|
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<T: Model> Clone for ModelHandle<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: self.inner.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Model> From<&'_ ModelHandle<T>> for ModelHandle<T> {
|
||||||
|
fn from(r: &'_ ModelHandle<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: r.inner.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Model> PartialEq for ModelHandle<T> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
Rc::ptr_eq(&self.inner, &other.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Model> Eq for ModelHandle<T> { }
|
||||||
|
|
||||||
|
impl<T: Model> fmt::Debug for ModelHandle<T> {
|
||||||
|
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<T: Model> {
|
||||||
|
inner: Weak<Inner<T::FetchData, T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Model> ModelWeakHandle<T> {
|
||||||
|
#[allow(unused)]
|
||||||
|
pub(super) fn upgrade(&self) -> Option<ModelHandle<T>> {
|
||||||
|
Some(ModelHandle {
|
||||||
|
inner: self.inner.upgrade()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Model> fmt::Debug for ModelWeakHandle<T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.write_str("ModelWeakHandle<")?;
|
||||||
|
f.write_str(T::NAME)?;
|
||||||
|
f.write_str(">")
|
||||||
|
}
|
||||||
|
}
|
13
src/models/mod.rs
Normal file
13
src/models/mod.rs
Normal file
@ -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;
|
48
src/models/observers.rs
Normal file
48
src/models/observers.rs
Normal file
@ -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<Self>) -> Option<api::FetchTask> {
|
||||||
|
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<ObserversT>);
|
||||||
|
|
||||||
|
impl std::ops::Deref for Observers {
|
||||||
|
type Target = ModelHandle<ObserversT>;
|
||||||
|
|
||||||
|
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,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
41
src/models/world_geo.rs
Normal file
41
src/models/world_geo.rs
Normal file
@ -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<Self>) -> Option<api::FetchTask> {
|
||||||
|
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<WorldGeoT>);
|
||||||
|
|
||||||
|
impl std::ops::Deref for WorldGeo {
|
||||||
|
type Target = ModelHandle<WorldGeoT>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorldGeo {
|
||||||
|
pub fn new(config: Config) -> Self {
|
||||||
|
Self(ModelHandle::new(
|
||||||
|
WorldGeoT { config },
|
||||||
|
(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
207
src/ui/app.rs
Normal file
207
src/ui/app.rs
Normal file
@ -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<Result<api::Almanac, String>>,
|
||||||
|
global_data: Rc<Result<api::Global, String>>,
|
||||||
|
observers_data: Rc<Result<api::ObserverList, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BaseData {
|
||||||
|
pub fn new(base: &models::Base) -> Result<Self, Html<MainApp>> {
|
||||||
|
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<MainPath>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for MainApp {
|
||||||
|
type Message = Msg;
|
||||||
|
type Properties = ();
|
||||||
|
|
||||||
|
fn create(_properties: Self::Properties, mut link: ComponentLink<Self>) -> 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<Self> {
|
||||||
|
let crumb = crumb.clone();
|
||||||
|
let title = format!("{}", crumb);
|
||||||
|
if self.path == crumb {
|
||||||
|
html!{
|
||||||
|
<li class="nav-item active">
|
||||||
|
<button class="nav-link btn btn-light" onclick=move |_| Msg::Goto(crumb.clone())>{ title }</button>
|
||||||
|
// <a class="nav-link" onclick=move |_| Msg::Goto(crumb.clone())>{ title }</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html!{
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link btn btn-light" onclick=move |_| Msg::Goto(crumb.clone())>{ title }</button>
|
||||||
|
// <a class="nav-link" onclick=move |_| Msg::Goto(crumb.clone())>{ title }</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_wait(&self) -> Html<Self> {
|
||||||
|
match BaseData::new(&self.base) {
|
||||||
|
Err(html) => html,
|
||||||
|
Ok(base) => html! {
|
||||||
|
<Main ongoto=Msg::Goto path=self.path.clone() base=base world_geo=self.world_geo.clone()/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Renderable<MainApp> for MainApp {
|
||||||
|
fn view(&self) -> Html<Self> {
|
||||||
|
html!{
|
||||||
|
<div class="container-fluid">
|
||||||
|
<nav class="mb-3 navbar navbar-expand-lg navbar-light bg-light select-none">
|
||||||
|
<a class="navbar-brand" href="">{"galmon"}</a>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link btn btn-light" onclick=|_| Msg::Refresh>
|
||||||
|
{ crate::uitools::icons::icon_refresh() }
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{ for self.crumbs.iter().map(|c| self.view_crumb(c)) }
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{ self.view_wait() }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
144
src/ui/main.rs
Normal file
144
src/ui/main.rs
Normal file
@ -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<F: FnMut(Self)>(&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<MainPath>,
|
||||||
|
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 {
|
||||||
|
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<Main> for Main {
|
||||||
|
fn view(&self) -> Html<Self> {
|
||||||
|
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)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
src/ui/main/observer.rs
Normal file
33
src/ui/main/observer.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
use crate::api::Observer;
|
||||||
|
|
||||||
|
impl Main {
|
||||||
|
fn observer_list_row(&self, observer: &Observer) -> Html<Self> {
|
||||||
|
html!{ <tr>
|
||||||
|
<th scope="row">{ observer.id }</th>
|
||||||
|
<td class="text-right">{ crate::uitools::ago(observer.last_seen) }</td>
|
||||||
|
<td>{ observer.longitude }</td>
|
||||||
|
<td>{ observer.latitude }</td>
|
||||||
|
</tr> }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_observer_list(&self) -> Html<Self> {
|
||||||
|
let observers = self.props.base.observers();
|
||||||
|
html!{
|
||||||
|
<table class="table table-striped table-sm text-monospace d-inline technical">
|
||||||
|
<thead><tr>
|
||||||
|
<th scope="col">{ "ID" }</th>
|
||||||
|
<th scope="col">{ "Last seen" }</th>
|
||||||
|
<th scope="col">{ "Longitude" }</th>
|
||||||
|
<th scope="col">{ "Latitude" }</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody class="text-nowrap">
|
||||||
|
{ for observers.iter().map(|observer| {
|
||||||
|
self.observer_list_row(observer)
|
||||||
|
}) }
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
345
src/ui/main/world_geo.rs
Normal file
345
src/ui/main/world_geo.rs
Normal file
@ -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<Position> 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<P: Projection> 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<DP: DrawPath> 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<PT: PathTransformer> DrawPath for PT {
|
||||||
|
type Point = <PT as PathTransformer>::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<P: Projection, S> {
|
||||||
|
pub projection: P,
|
||||||
|
pub sink: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: Projection, S: DrawPath<Point=ProjectedPoint>> ApplyProjection<P, S> {
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn new(projection: P, sink: S) -> Self {
|
||||||
|
Self {
|
||||||
|
projection,
|
||||||
|
sink,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: Projection, S: DrawPath<Point=ProjectedPoint>> PathTransformer for ApplyProjection<P, S> {
|
||||||
|
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<COMP: Component>(&self, projection: MapProjection) -> Html<COMP>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<COMP: Component>(&self, projection: MapProjection) -> Html<COMP> {
|
||||||
|
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!{
|
||||||
|
<g>
|
||||||
|
<path style="stroke: none;" fill-rule="evenodd" d=sink.path />
|
||||||
|
<path style="fill: none;" d=stroke_path />
|
||||||
|
</g>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html!{
|
||||||
|
<path fill-rule="evenodd" d=sink.path />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrawOnMap for Geometry {
|
||||||
|
fn draw_on_map<COMP: Component>(&self, projection: MapProjection) -> Html<COMP> {
|
||||||
|
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<COMP: Component>(&self, projection: MapProjection) -> Html<COMP> {
|
||||||
|
html!{
|
||||||
|
<g title=&self.properties.name>
|
||||||
|
{ self.geometry.draw_on_map(projection) }
|
||||||
|
</g>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrawOnMap for FeatureCollection {
|
||||||
|
fn draw_on_map<COMP: Component>(&self, projection: MapProjection) -> Html<COMP> {
|
||||||
|
html!{
|
||||||
|
<g style="fill: #5EAFC6; stroke: #75739F;">
|
||||||
|
{ for self.features.iter().map(|c| c.draw_on_map(projection)) }
|
||||||
|
</g>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_grid(projection: MapProjection) -> Html<Main> {
|
||||||
|
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!{
|
||||||
|
<path style="fill:none; stroke: #AAAAAA80;" d=sink.path />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Main {
|
||||||
|
pub fn view_map(&self, world_geo: &FeatureCollection) -> Html<Self> {
|
||||||
|
html!{
|
||||||
|
<svg viewBox="-200 -100 400 200" style="stroke-width: 0.2; max-height: 80%; max-width: 80%;">
|
||||||
|
{ world_geo.draw_on_map(MapProjection::Fahey) }
|
||||||
|
{ build_grid(MapProjection::Fahey) }
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
src/ui/main/world_geo/cartesian.rs
Normal file
72
src/ui/main/world_geo/cartesian.rs
Normal file
@ -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<RadianPoint> 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<f32> 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
361
src/ui/main/world_geo/clip.rs
Normal file
361
src/ui/main/world_geo/clip.rs
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
use super::{
|
||||||
|
DrawPath,
|
||||||
|
RadianPoint,
|
||||||
|
PathTransformer,
|
||||||
|
polygon_contains_south,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RingState {
|
||||||
|
start_new_segment: bool,
|
||||||
|
segments: Vec<Vec<(RadianPoint, bool)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ClipControl<S> {
|
||||||
|
sink: S,
|
||||||
|
ring: Option<RingState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> ClipControl<S>
|
||||||
|
where
|
||||||
|
S: DrawPath<Point = RadianPoint>,
|
||||||
|
{
|
||||||
|
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<S>(&mut self, control: &mut ClipControl<S>, to: RadianPoint, stroke: bool)
|
||||||
|
where
|
||||||
|
S: DrawPath<Point = RadianPoint>,
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Clipper {
|
||||||
|
type PathClipper: PathClipper;
|
||||||
|
|
||||||
|
fn create_clipper(&self) -> Self::PathClipper;
|
||||||
|
|
||||||
|
fn interpolate<S>(&self, control: &mut ClipControl<S>, from_to: Option<(RadianPoint, RadianPoint)>, forward: bool)
|
||||||
|
where
|
||||||
|
S: DrawPath<Point = RadianPoint>,
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Clip<S, C>
|
||||||
|
where
|
||||||
|
S: DrawPath<Point = RadianPoint>,
|
||||||
|
C: Clipper,
|
||||||
|
{
|
||||||
|
control: ClipControl<S>,
|
||||||
|
polygon: Vec<Vec<RadianPoint>>,
|
||||||
|
polygon_first_stroke: bool,
|
||||||
|
segments_with_splits: Vec<Vec<(RadianPoint, bool)>>,
|
||||||
|
clipper: C,
|
||||||
|
path_clipper: Option<C::PathClipper>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, C> Clip<S, C>
|
||||||
|
where
|
||||||
|
S: DrawPath<Point = RadianPoint>,
|
||||||
|
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::<Intersection>()
|
||||||
|
};
|
||||||
|
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<S, C> PathTransformer for Clip<S, C>
|
||||||
|
where
|
||||||
|
S: DrawPath<Point = RadianPoint>,
|
||||||
|
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<S, C> Drop for Clip<S, C>
|
||||||
|
where
|
||||||
|
S: DrawPath<Point = RadianPoint>,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
146
src/ui/main/world_geo/clip_antimeridian.rs
Normal file
146
src/ui/main/world_geo/clip_antimeridian.rs
Normal file
@ -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<S>(sink: S) -> Clip<S, ClipAntimeridian>
|
||||||
|
where
|
||||||
|
S: DrawPath<Point = RadianPoint>,
|
||||||
|
{
|
||||||
|
Clip::new(sink, ClipAntimeridian)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clipper for ClipAntimeridian {
|
||||||
|
type PathClipper = ClipAntimeridianPathClipper;
|
||||||
|
|
||||||
|
fn create_clipper(&self) -> Self::PathClipper {
|
||||||
|
ClipAntimeridianPathClipper {
|
||||||
|
first_prev: None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interpolate<S>(&self, control: &mut ClipControl<S>, from_to: Option<(RadianPoint, RadianPoint)>, forward: bool)
|
||||||
|
where
|
||||||
|
S: DrawPath<Point = RadianPoint>,
|
||||||
|
{
|
||||||
|
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<S>(&mut self, control: &mut ClipControl<S>, next: RadianPoint, stroke: bool)
|
||||||
|
where
|
||||||
|
S: DrawPath<Point = RadianPoint>,
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
25
src/ui/main/world_geo/contains.rs
Normal file
25
src/ui/main/world_geo/contains.rs
Normal file
@ -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<RadianPoint>]) -> 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
|
||||||
|
}
|
108
src/ui/main/world_geo/path_sink.rs
Normal file
108
src/ui/main/world_geo/path_sink.rs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
use super::{
|
||||||
|
ProjectedPoint,
|
||||||
|
DrawPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct PathSink {
|
||||||
|
pub path: String,
|
||||||
|
pub stroke_path: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Point = ProjectedPoint> + '_ {
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
enum State {
|
||||||
|
Ring,
|
||||||
|
Path,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Draw<'a> {
|
||||||
|
sink: &'a mut PathSink,
|
||||||
|
empty: bool,
|
||||||
|
state: Option<State>,
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
130
src/ui/main/world_geo/resample.rs
Normal file
130
src/ui/main/world_geo/resample.rs
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
pub use super::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct SegmentPoint {
|
||||||
|
input: RadianPoint,
|
||||||
|
cartesian: Cartesian,
|
||||||
|
projected: ProjectedPoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SegmentPoint {
|
||||||
|
fn new<P>(projection: P, input: RadianPoint) -> Self
|
||||||
|
where
|
||||||
|
P: Projection,
|
||||||
|
{
|
||||||
|
SegmentPoint {
|
||||||
|
input,
|
||||||
|
cartesian: input.into(),
|
||||||
|
projected: projection.project(input),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resample_segment<P, S>(sink: &mut S, projection: &P, from: SegmentPoint, to: SegmentPoint, stroke: bool, recursion_limit: u32, resolution: f32)
|
||||||
|
where
|
||||||
|
P: Projection,
|
||||||
|
S: DrawPath<Point = ProjectedPoint>,
|
||||||
|
{
|
||||||
|
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<P: Projection, S> {
|
||||||
|
projection: P,
|
||||||
|
sink: S,
|
||||||
|
resolution: f32,
|
||||||
|
recursion_limit: u32,
|
||||||
|
start_prev: Option<(bool, SegmentPoint, SegmentPoint)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: Projection, S: DrawPath<Point=ProjectedPoint>> ResamplePath<P, S> {
|
||||||
|
pub fn new(projection: P, sink: S, resolution: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
projection,
|
||||||
|
sink,
|
||||||
|
resolution,
|
||||||
|
recursion_limit: 16,
|
||||||
|
start_prev: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: Projection, S: DrawPath<Point=ProjectedPoint>> PathTransformer for ResamplePath<P, S> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
2
src/ui/mod.rs
Normal file
2
src/ui/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod app;
|
||||||
|
pub mod main;
|
60
src/uitools/chrono.rs
Normal file
60
src/uitools/chrono.rs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use std::fmt;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
pub fn now() -> DateTime<Utc> {
|
||||||
|
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<COMP>(dt: DateTime<Utc>) -> Html<COMP>
|
||||||
|
where
|
||||||
|
COMP: Component,
|
||||||
|
{
|
||||||
|
let duration = now().signed_duration_since(dt);
|
||||||
|
let dt_s = format!("{}", dt);
|
||||||
|
html! {
|
||||||
|
<time datetime=&dt_s title=&dt_s>{ PrettyFormatDuration(duration) }</time>
|
||||||
|
}
|
||||||
|
}
|
17
src/uitools/icons.rs
Normal file
17
src/uitools/icons.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
pub fn icon_unlock<COMP: Component>() -> Html<COMP> {
|
||||||
|
html!{ <i class="fa fa-unlock-alt"/> }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon_lock<COMP: Component>() -> Html<COMP> {
|
||||||
|
html!{ <i class="fa fa-lock"/> }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon_refresh<COMP: Component>() -> Html<COMP> {
|
||||||
|
html!{ <i class="fa fa-refresh"/> }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon_question_circle<COMP: Component>() -> Html<COMP> {
|
||||||
|
html!{ <i class="fa fa-question-circle-o"/> }
|
||||||
|
}
|
62
src/uitools/loading.rs
Normal file
62
src/uitools/loading.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
pub fn loading<COMP>(title: &str) -> Html<COMP>
|
||||||
|
where
|
||||||
|
COMP: Component,
|
||||||
|
{
|
||||||
|
html!{
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span class="spinner-border spinner-border-sm text-info mr-2" role="status"/>
|
||||||
|
<span class="text-info"> { "Loading " } { title } { "..." }</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_error<COMP, M>(title: &str, refresh: M, error: &str) -> Html<COMP>
|
||||||
|
where
|
||||||
|
COMP: Component + Renderable<COMP>,
|
||||||
|
M: Fn() -> COMP::Message + 'static,
|
||||||
|
{
|
||||||
|
html!{
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span class="text-danger">
|
||||||
|
<span class="clickable mr-2" onclick=move |_| refresh()> { crate::uitools::icons::icon_refresh() } </span>
|
||||||
|
{ "Loading " } { title } { " failed: " }{ error }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show<COMP, M, D, T>(title: &str, data: Option<Rc<Result<T, String>>>, refresh: M, display: D) -> Html<COMP>
|
||||||
|
where
|
||||||
|
COMP: Component + Renderable<COMP>,
|
||||||
|
M: Fn() -> COMP::Message + 'static,
|
||||||
|
D: FnOnce(&T) -> Html<COMP>,
|
||||||
|
{
|
||||||
|
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<COMP, M, D, F, T>(title: &str, data: Option<Rc<Result<T, String>>>, refresh: M, display: D, display_fail: F) -> Html<COMP>
|
||||||
|
where
|
||||||
|
COMP: Component + Renderable<COMP>,
|
||||||
|
M: Fn() -> COMP::Message + 'static,
|
||||||
|
D: FnOnce(&T) -> Html<COMP>,
|
||||||
|
F: FnOnce(Html<COMP>) -> Html<COMP>,
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
6
src/uitools/mod.rs
Normal file
6
src/uitools/mod.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
mod chrono;
|
||||||
|
pub mod icons;
|
||||||
|
pub mod loading;
|
||||||
|
pub mod routing;
|
||||||
|
|
||||||
|
pub use self::chrono::ago;
|
48
src/uitools/routing.rs
Normal file
48
src/uitools/routing.rs
Normal file
@ -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<F: FnMut(Self)>(&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<P: Path>(path: &P) -> Result<String, fmt::Error> {
|
||||||
|
let mut p = PathString(String::new());
|
||||||
|
Path::fmt(path, &mut p)?;
|
||||||
|
Ok(p.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn crumbs<P: Path>(path: &P) -> Vec<P> {
|
||||||
|
let mut crumbs = Vec::new();
|
||||||
|
path.crumbs(|crumb| crumbs.push(crumb));
|
||||||
|
crumbs
|
||||||
|
}
|
140
src/utils/callbacks.rs
Normal file
140
src/utils/callbacks.rs
Normal file
@ -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<T> {
|
||||||
|
link: CallbackLink<T>,
|
||||||
|
exec_link: CallbackExecLink<T>,
|
||||||
|
callback: yew::Callback<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InnerCallbacks<T> {
|
||||||
|
cbhead: CallbackHead<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Drop for InnerCallbacks<T> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
while let Some(nodeptr) = self.cbhead.pop_front() {
|
||||||
|
// free entry
|
||||||
|
drop(Box::from_raw(nodeptr as *mut CallbackEntry<T>));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Callbacks<T> {
|
||||||
|
inner: Rc<InnerCallbacks<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static> Callbacks<T> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Rc::new(InnerCallbacks {
|
||||||
|
cbhead: CallbackHead::new(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(&self, callback: yew::Callback<T>) -> CallbackRegistration<T> {
|
||||||
|
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<T> 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<T> = &*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<T> = &*nodeptr;
|
||||||
|
entry.callback.emit(data.clone());
|
||||||
|
if entry.link.is_unlinked() {
|
||||||
|
// free entry
|
||||||
|
drop(Box::from_raw(nodeptr as *mut CallbackEntry<T>));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> fmt::Debug for Callbacks<T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.write_str("Callbacks")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CallbackRegistration<T> {
|
||||||
|
inner: Weak<InnerCallbacks<T>>,
|
||||||
|
entry: *const CallbackEntry<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for CallbackRegistration<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Weak::default(),
|
||||||
|
entry: std::ptr::null(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Drop for CallbackRegistration<T> {
|
||||||
|
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<T>));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> fmt::Debug for CallbackRegistration<T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
f.write_str("CallbackRegistration")
|
||||||
|
}
|
||||||
|
}
|
257
src/utils/local_dl_list.rs
Normal file
257
src/utils/local_dl_list.rs
Normal file
@ -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<T> {
|
||||||
|
head: LocalDLHead,
|
||||||
|
_mark: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl<T> $link_name<T> {
|
||||||
|
fn __base_from_node(ptr: *const LocalDLHead) -> *const $parent<T> {
|
||||||
|
let member_offset: usize = {
|
||||||
|
let p = core::ptr::NonNull::<$parent<T>>::dangling();
|
||||||
|
let $parent::<T> { $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<T> }
|
||||||
|
}
|
||||||
|
|
||||||
|
$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<T>) {
|
||||||
|
let node_link: &Self = &node.$member;
|
||||||
|
self.head.insert_after(&node_link.head);
|
||||||
|
}
|
||||||
|
|
||||||
|
$innervis unsafe fn insert_before(&self, node: &$parent<T>) {
|
||||||
|
let node_link: &Self = &node.$member;
|
||||||
|
self.head.insert_before(&node_link.head);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
$innervis struct $head_name<T> {
|
||||||
|
head: LocalDLHead,
|
||||||
|
_mark: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl<T> $head_name<T> {
|
||||||
|
$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<T>) {
|
||||||
|
let node_link: &$link_name<T> = &node.$member;
|
||||||
|
self.head.insert_after(&node_link.head);
|
||||||
|
}
|
||||||
|
|
||||||
|
$innervis unsafe fn append(&self, node: &$parent<T>) {
|
||||||
|
let node_link: &$link_name<T> = &node.$member;
|
||||||
|
self.head.insert_before(&node_link.head);
|
||||||
|
}
|
||||||
|
|
||||||
|
$innervis unsafe fn front(&self) -> Option<*const $parent<T>> {
|
||||||
|
let node_link = self.head.next()?;
|
||||||
|
Some($link_name::<T>::__base_from_node(node_link))
|
||||||
|
}
|
||||||
|
|
||||||
|
$innervis unsafe fn back(&self) -> Option<*const $parent<T>> {
|
||||||
|
let node_link = self.head.prev()?;
|
||||||
|
Some($link_name::<T>::__base_from_node(node_link))
|
||||||
|
}
|
||||||
|
|
||||||
|
$innervis unsafe fn next(&self, node: &$parent<T>) -> Option<*const $parent<T>> {
|
||||||
|
let node_link = node.$member.head.next()?;
|
||||||
|
if node_link as *const LocalDLHead == &self.head as *const LocalDLHead { return None; }
|
||||||
|
Some($link_name::<T>::__base_from_node(node_link))
|
||||||
|
}
|
||||||
|
|
||||||
|
$innervis unsafe fn prev(&self, node: &$parent<T>) -> Option<*const $parent<T>> {
|
||||||
|
let node_link = node.$member.head.prev()?;
|
||||||
|
if node_link as *const LocalDLHead == &self.head as *const LocalDLHead { return None; }
|
||||||
|
Some($link_name::<T>::__base_from_node(node_link))
|
||||||
|
}
|
||||||
|
|
||||||
|
$innervis unsafe fn pop_front(&self) -> Option<*const $parent<T>> {
|
||||||
|
let node_link = self.head.pop_front()?;
|
||||||
|
Some($link_name::<T>::__base_from_node(node_link))
|
||||||
|
}
|
||||||
|
|
||||||
|
$innervis unsafe fn pop_back(&self) -> Option<*const $parent<T>> {
|
||||||
|
let node_link = self.head.pop_back()?;
|
||||||
|
Some($link_name::<T>::__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};
|
||||||
|
};
|
||||||
|
}
|
3
src/utils/mod.rs
Normal file
3
src/utils/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#[macro_use]
|
||||||
|
mod local_dl_list;
|
||||||
|
pub mod callbacks;
|
1
static/.gitignore
vendored
Normal file
1
static/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
config.json
|
1
static/bootstrap-debian
Symbolic link
1
static/bootstrap-debian
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
/usr/share/javascript/bootstrap4/
|
1
static/fontawesome-debian
Symbolic link
1
static/fontawesome-debian
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
/usr/share/fonts-font-awesome
|
64
static/index.html
Normal file
64
static/index.html
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>galmon</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=1" name="viewport" />
|
||||||
|
<script>
|
||||||
|
var Module = {};
|
||||||
|
var __cargo_web = {};
|
||||||
|
Object.defineProperty( Module, 'canvas', {
|
||||||
|
get: function() {
|
||||||
|
if( __cargo_web.canvas ) {
|
||||||
|
return __cargo_web.canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
var canvas = document.createElement( 'canvas' );
|
||||||
|
document.querySelector( 'body' ).appendChild( canvas );
|
||||||
|
__cargo_web.canvas = canvas;
|
||||||
|
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<!-- you could download bootstrap + fontawesome manually, but there might be some differences -->
|
||||||
|
<!--
|
||||||
|
<link rel="stylesheet" href="bootstrap/css/bootstrap.min.css" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="fontawesome/css/all.min.css" crossorigin="anonymous">
|
||||||
|
-->
|
||||||
|
<!-- try using debian ones through symlinks, tested with:
|
||||||
|
libjs-bootstrap4 4.3.1+dfsg2-1
|
||||||
|
fonts-font-awesome 5.0.10+really4.7.0~dfsg-1
|
||||||
|
-->
|
||||||
|
<link rel="stylesheet" href="bootstrap-debian/css/bootstrap.min.css" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="fontawesome-debian/css/font-awesome.min.css" crossorigin="anonymous">
|
||||||
|
<style>
|
||||||
|
table.technical {
|
||||||
|
font-size: 80%;
|
||||||
|
line-height: 100%;
|
||||||
|
}
|
||||||
|
input[type="checkbox"] {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.select-all {
|
||||||
|
-moz-user-select: all;
|
||||||
|
-webkit-user-select: all;
|
||||||
|
-ms-user-select: all;
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
.select-none {
|
||||||
|
-moz-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="galmon-web.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user