initial commit

This commit is contained in:
Stefan Bühler 2019-09-28 12:16:55 +02:00
commit b9de50bb2b
40 changed files with 4203 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
**/*.rs.bk

607
Cargo.lock generated Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>
}
}
}

View 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,
}
}
}

View 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);
}
}
}

View 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);
}
}

View 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
}

View 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,
}
}
}

View 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
View File

@ -0,0 +1,2 @@
pub mod app;
pub mod main;

60
src/uitools/chrono.rs Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
#[macro_use]
mod local_dl_list;
pub mod callbacks;

1
static/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
config.json

1
static/bootstrap-debian Symbolic link
View File

@ -0,0 +1 @@
/usr/share/javascript/bootstrap4/

1
static/fontawesome-debian Symbolic link
View File

@ -0,0 +1 @@
/usr/share/fonts-font-awesome

64
static/index.html Normal file
View 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>