├── dummy.rs ├── .gitignore ├── db └── db.png ├── font1.png ├── backend ├── src │ ├── lib.rs │ ├── io │ │ ├── mod.rs │ │ ├── image_io.rs │ │ ├── in_memory_image.rs │ │ ├── bitmap_font.rs │ │ └── bmp_on_disk.rs │ ├── model │ │ ├── datatypes │ │ │ ├── reference.rs │ │ │ ├── color.rs │ │ │ ├── flood.rs │ │ │ ├── boolean.rs │ │ │ ├── counter.rs │ │ │ ├── pie.rs │ │ │ ├── image.rs │ │ │ ├── abc.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── colors.rs │ │ ├── model.rs │ │ └── async_model_reader.rs │ ├── image.rs │ └── db.rs ├── Cargo.lock └── Cargo.toml ├── .dockerignore ├── Cargo.toml ├── command-line ├── Cargo.lock ├── Cargo.toml └── src │ └── main.rs ├── LICENSE ├── web-server ├── Cargo.toml └── src │ ├── json.rs │ ├── main.rs │ └── handlers.rs ├── web-client ├── Cargo.toml └── src │ ├── record_view.rs │ ├── api.rs │ ├── lib.rs │ └── field_view.rs ├── static ├── index.html └── tracker.html ├── README.md ├── badbee.iml ├── Dockerfile └── Cargo.lock /dummy.rs: -------------------------------------------------------------------------------- 1 | fn main() {} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | -------------------------------------------------------------------------------- /db/db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexeyGrishin/badbee/HEAD/db/db.png -------------------------------------------------------------------------------- /font1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexeyGrishin/badbee/HEAD/font1.png -------------------------------------------------------------------------------- /backend/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod model; 2 | pub mod io; 3 | pub mod image; 4 | pub mod db; 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | old 2 | command-line 3 | *.iml 4 | target 5 | .idea 6 | Dockerfile 7 | .dockerignore 8 | *.exe 9 | *.md -------------------------------------------------------------------------------- /backend/src/io/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bitmap_font; 2 | pub mod image_io; 3 | mod in_memory_image; 4 | mod bmp_on_disk; 5 | 6 | 7 | -------------------------------------------------------------------------------- /backend/src/model/datatypes/reference.rs: -------------------------------------------------------------------------------- 1 | use crate::model::model::FieldType; 2 | 3 | pub const REFERENCE_TYPE: FieldType = FieldType(0b010_111_010); -------------------------------------------------------------------------------- /backend/src/model/mod.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | pub mod model; 3 | pub(crate) mod datatypes; 4 | pub mod async_model_reader; 5 | pub(crate) mod colors; -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | #[package] 2 | #name = "badbee" 3 | #version = "0.1.0" 4 | #edition = "2018" 5 | 6 | [workspace] 7 | members = ["backend", "web-server"] 8 | 9 | -------------------------------------------------------------------------------- /backend/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "backend" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /command-line/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "command-line" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /command-line/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "badbee-command-line" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | badbee-backend = { path = "../backend"} 10 | byteorder = "1.4.3" 11 | tokio = { version = "1", features = ["full"] } 12 | log = "0.4" 13 | stderrlog = "0.5.1" -------------------------------------------------------------------------------- /backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "badbee-backend" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [lib] 7 | path = "src/lib.rs" 8 | name = "badbee_backend" 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | image = "0.23.14" 13 | imageproc = "0.22.0" 14 | base64 = "0.13.0" 15 | tokio = { version = "1", features = ["full"] } 16 | byteorder = "1.4.3" 17 | log = "0.4" 18 | stderrlog = "0.5.1" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /web-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "web-server" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [[bin]] 7 | path = "src/main.rs" 8 | name = "web-server" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | tokio = { version = "1", features = ["full"] } 14 | warp = "0.3" 15 | badbee-backend = { path = "../backend"} 16 | maplit = "1.0.2" 17 | serde_json = "1.0.68" 18 | serde_derive = "1.0" 19 | serde = "1.0" 20 | log = "0.4" 21 | stderrlog = "0.5.1" -------------------------------------------------------------------------------- /backend/src/io/image_io.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{OpenOptions}; 2 | use std::path::{Path}; 3 | use crate::image::{BoxedStorableImage}; 4 | use crate::io::bmp_on_disk::BMPOnDiskImage; 5 | use crate::io::in_memory_image::InMemoryImage; 6 | 7 | pub fn load_image(path: &str) -> BoxedStorableImage { 8 | let path = Path::new(path); 9 | let file = OpenOptions::new().read(true).write(true).open(path).unwrap(); 10 | let extension = path.extension().unwrap().to_str().unwrap(); 11 | match extension { 12 | "bmp" => Box::new(BMPOnDiskImage::new(file)), 13 | _ => Box::new(InMemoryImage::new(&path)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /web-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "web-client" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [workspace] 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | path = "src/lib.rs" 11 | 12 | [dev-dependencies.web-sys] 13 | version = "0.3" 14 | features = ["HtmlCanvasElement", "CanvasRenderingContext2d", "HtmlImageElement", "MouseEvent"] 15 | 16 | [dependencies] 17 | sauron = { version = "0.43" } 18 | serde_json = "1.0.68" 19 | serde_derive = "1.0" 20 | serde = "1.0" 21 | log = "0.4" 22 | console_log = {version="0.2", features = ["color"]} 23 | console_error_panic_hook = { version = "0.1" } 24 | gloo-events = "0.1.1" 25 | gloo-timers = "0.2.1" 26 | 27 | [profile.release] 28 | lto = true -------------------------------------------------------------------------------- /backend/src/model/datatypes/color.rs: -------------------------------------------------------------------------------- 1 | use crate::model::model::{Field, FieldType, DataType, DataValue, DataError, IncompatibleError}; 2 | use crate::image::ImageView; 3 | 4 | pub const COLOR_TYPE: FieldType = FieldType(0b_111_100_111); 5 | 6 | pub(crate) struct ColorDataType; 7 | 8 | impl DataType for ColorDataType { 9 | 10 | fn read(&self, image: &ImageView, _: &Field) -> Result { 11 | Ok(DataValue::Color { value: image.get_pixel(0, 0) }) 12 | } 13 | 14 | fn write(&self, image: &mut ImageView, _: &Field, value: DataValue) -> Result<(), DataError> { 15 | let color = get_matched!(value, DataValue::Color { value })?; 16 | image.fill(&color); 17 | Ok(()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /web-client/src/record_view.rs: -------------------------------------------------------------------------------- 1 | use sauron::Node; 2 | use crate::api::Record; 3 | use crate::Msg; 4 | use sauron::node; 5 | use sauron::html::text; 6 | use crate::field_view::field_view; 7 | use crate::field_view::color_label; 8 | 9 | pub(crate) fn record_view(record: Record) -> Node { 10 | 11 | node! [ 12 |
13 |
14 | { text("{\n") }
15 | { text(format!("   id: \"{}\",\n", record.id)) }
16 | { text("   column: ") } { color_label(&record.column) }
17 |         {
18 |             for idx in 0..record.fields.len() {
19 |                 field_view(record.fields[idx].clone(), &record.id, idx)
20 |             }
21 |         }
22 | 
23 | { text("\n") } { text("}") }
24 |             
25 | 26 | 27 |
28 | 29 | ] 30 | } -------------------------------------------------------------------------------- /backend/src/model/datatypes/flood.rs: -------------------------------------------------------------------------------- 1 | use crate::model::model::{Field, FieldType, DataType, DataValue, DataError}; 2 | use crate::image::ImageView; 3 | 4 | pub const FLOOD_TYPE: FieldType = FieldType(0b_111_110_100); 5 | 6 | pub(crate) struct FloodDataType; 7 | 8 | impl DataType for FloodDataType { 9 | 10 | fn read(&self, image: &ImageView, _: &Field) -> Result { 11 | let mut pixels = 0; 12 | let mut flood_pixels = 0; 13 | for x in 0..image.width { 14 | for y in 0..image.height { 15 | pixels += 1; 16 | if image.get_pixel(x, y).is_data() { 17 | flood_pixels += 1; 18 | } 19 | } 20 | } 21 | Ok(DataValue::Float { value: (flood_pixels as f32) / (pixels as f32)}) 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /backend/src/model/datatypes/boolean.rs: -------------------------------------------------------------------------------- 1 | use crate::model::model::{Field, FieldType, DataType, DataValue, DataError, IncompatibleError}; 2 | use crate::image::{ImageView}; 3 | use crate::model::colors::RGB; 4 | 5 | pub const BOOL_TYPE: FieldType = FieldType(0b_100_110_110); 6 | 7 | pub(crate) struct BooleanDataType; 8 | 9 | impl DataType for BooleanDataType { 10 | 11 | fn read(&self, image: &ImageView, _: &Field) -> Result { 12 | Ok(DataValue::Boolean { value: !image.get_pixel(0, 0).is_blank() }) 13 | } 14 | 15 | fn write(&self, image: &mut ImageView, _: &Field, value: DataValue) -> Result<(), DataError> { 16 | let boolean = get_matched!(value, DataValue::Boolean { value })?; 17 | if boolean { 18 | image.fill(&RGB::new(0, 255, 0)) 19 | } else { 20 | image.clear() 21 | } 22 | Ok(()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | #BADBEE viewer 4 | 36 | 37 | 38 | 39 | 40 | 41 | 44 | 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WTFPL 4 | 5 | # How to build 6 | 7 | ## Preconditions 8 | 1. Install rust & cargo 9 | 2. Install wasm-pack from `https://rustwasm.github.io/wasm-pack/installer/` 10 | 3. `cargo install cargo-watch` 11 | 12 | ## Build server 13 | `cargo build`. 14 | 15 | ## Build client side 16 | 17 | * `wasm-pack build --out-dir ../static --target web` 18 | * for release `wasm-pack build --out-dir ../static --target web --release` 19 | * watching changes: `cargo watch -s "wasm-pack build --out-dir ../static --target web"` 20 | 21 | ## Run 22 | `cargo run --package web-server` 23 | 24 | # Docker 25 | 26 | Build with `docker build -t badbee .` 27 | 28 | Run with specific base `docker run -d --rm --name badbee -e DB_FILE=db.png -p 3030:3030 -v "$pwd/db:/usr/badbee/db" badbee` 29 | 30 | Run with specific base and bmp settings `docker run -d --rm --name badbee -e DB_FILE=db.png -e BMP_SLICE_STEP=1024 -e KEEP_IN_MEMORY_INV=4 -p 3030:3030 -v "$pwd/db:/usr/badbee/db" badbee` 31 | 32 | ## For debug 33 | 34 | Run bash to check pathes and other: `docker run --rm -it --entrypoint bash badbee` 35 | 36 | Run cadvisor to monitor resources: `docker run -d --rm --name cadvisor -p 8080:8080 --volume=/:/rootfs:ro --volume=/var/run:/var/run:rw --volume=/sys:/sys:ro --volume=/var/lib/docker/:/var/lib/docker:ro google/cadvisor` 37 | 38 | -------------------------------------------------------------------------------- /badbee.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /backend/src/model/datatypes/counter.rs: -------------------------------------------------------------------------------- 1 | use crate::model::model::{Field, FieldType, DataType, DataValue, DataError}; 2 | use std::collections::HashSet; 3 | use crate::image::ImageView; 4 | 5 | pub const COUNTER_TYPE: FieldType = FieldType(0b_111_010_111); 6 | 7 | pub(crate) struct CounterDataType; 8 | 9 | impl DataType for CounterDataType { 10 | 11 | fn read(&self, image: &ImageView, _: &Field) -> Result { 12 | let mut used_pixels: HashSet<(u32, u32)> = HashSet::new(); 13 | let mut domains_found = 0; 14 | 15 | for y in 0..image.height { 16 | for x in 0..image.width { 17 | if image.get_pixel(x, y).is_data() && !used_pixels.contains(&(x, y)) { 18 | let mut pixels_to_check: Vec<(u32, u32)> = vec![(x,y)]; 19 | 20 | while !pixels_to_check.is_empty() { 21 | let (x,y) = pixels_to_check.pop().unwrap(); 22 | if used_pixels.contains(&(x, y)) { continue } 23 | used_pixels.insert((x,y)); 24 | if !image.get_pixel(x, y).is_data() { continue } 25 | pixels_to_check.push((x+1, y)); 26 | pixels_to_check.push((x-1, y)); 27 | pixels_to_check.push((x, y+1)); 28 | pixels_to_check.push((x, y-1)); 29 | } 30 | domains_found += 1; 31 | } 32 | } 33 | } 34 | Ok(DataValue::Int {value: domains_found}) 35 | } 36 | 37 | } 38 | 39 | -------------------------------------------------------------------------------- /command-line/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::thread::sleep; 2 | use std::time::Duration; 3 | use badbee_backend::io::bitmap_font::BitmapFont; 4 | use badbee_backend::io::image_io::load_image; 5 | use badbee_backend::image::{ImageView, StorableImage}; 6 | use badbee_backend::model::async_model_reader::load_model_into; 7 | use std::io::{Seek, SeekFrom, Read}; 8 | use std::os::windows::fs::{FileExt, MetadataExt}; 9 | use byteorder::{LittleEndian, ReadBytesExt}; 10 | use std::ops::Div; 11 | use badbee_backend::model::model::{Model, DataValue}; 12 | use badbee_backend::db::{DBHandle, DBQuery}; 13 | 14 | #[tokio::main] 15 | async fn main() { 16 | let handle = DBHandle::run_in_background("db.png"); 17 | 18 | let mut query = DBQuery::new();//.limit(1).build();//.limit(2); 19 | let model = handle.get_model().await.unwrap(); 20 | for rec in &model.records { 21 | println!("RECORD {:?}", rec.position); 22 | for f in &rec.fields { 23 | println!("FIELD {:?} {:?} {:?}", f.field_type, f.data_start, f.ref_to_record) 24 | } 25 | } 26 | println!(); 27 | let records = handle.get_records(query.clone()).await.unwrap(); 28 | //handle.set_field(records[0].id.x, records[0].id.y, 1, DataValue::Boolean { value: true}).await; 29 | //handle.sync().await; 30 | //let records = handle.get_records(query.clone()).await; 31 | for rec in &records { 32 | println!("RECORD {:?}", rec.id); 33 | for field in &rec.fields { 34 | println!(" FIELD {:?} ref={:?}", field.value, field.reference) 35 | } 36 | } 37 | 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.55.0 as builder 2 | 3 | FROM builder as server-build 4 | WORKDIR /usr/src/badbee 5 | # advice from https://stackoverflow.com/questions/58473606/cache-rust-dependencies-with-docker-build 6 | COPY dummy.rs . 7 | COPY ./Cargo.* . 8 | COPY ./backend/Cargo.toml ./backend/Cargo.toml 9 | COPY ./web-server/Cargo.toml ./web-server/Cargo.toml 10 | 11 | RUN sed -i 's#src/lib.rs#../dummy.rs#' backend/Cargo.toml 12 | RUN cargo build --release -p badbee-backend 13 | 14 | RUN sed -i 's#src/main.rs#../dummy.rs#' web-server/Cargo.toml 15 | RUN cargo build --release -p web-server 16 | 17 | WORKDIR /usr/src/badbee 18 | COPY ./web-server ./web-server 19 | COPY ./backend ./backend 20 | 21 | RUN cargo build --release 22 | 23 | FROM builder as client-build 24 | RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 25 | 26 | WORKDIR /usr/src/badbee 27 | COPY dummy.rs . 28 | COPY ./web-client/Cargo.toml ./web-client/Cargo.toml 29 | RUN sed -i 's#src/lib.rs#../dummy.rs#' web-client/Cargo.toml 30 | WORKDIR /usr/src/badbee/web-client 31 | RUN wasm-pack build --out-dir ../static --target web --release 32 | 33 | 34 | WORKDIR /usr/src/badbee 35 | COPY ./web-client ./web-client 36 | COPY ./static ./static 37 | WORKDIR /usr/src/badbee/web-client 38 | RUN wasm-pack build --out-dir ../static --target web --release 39 | 40 | FROM debian:buster-slim 41 | RUN apt-get update && rm -rf /var/lib/apt/lists/* 42 | WORKDIR /usr/badbee 43 | COPY --from=server-build /usr/src/badbee/target/release/web-server . 44 | COPY --from=client-build /usr/src/badbee/static ./static 45 | COPY font1.png . 46 | CMD ["./web-server"] -------------------------------------------------------------------------------- /backend/src/model/colors.rs: -------------------------------------------------------------------------------- 1 | use image::{Rgba, Rgb, Pixel}; 2 | 3 | #[derive(Eq, PartialEq, Debug, Copy, Clone, Hash)] 4 | pub struct RGB { 5 | pub r: u8, 6 | pub g: u8, 7 | pub b: u8, 8 | } 9 | 10 | impl RGB { 11 | pub fn new(r: u8, g: u8, b: u8) -> Self { 12 | Self { r, g, b } 13 | } 14 | 15 | pub(crate) fn is_blank(&self) -> bool { 16 | self.r == 255 && self.g == 255 && self.b == 255 17 | } 18 | 19 | pub(crate) fn is_meta(&self) -> bool { 20 | self.r == 0xBA && self.g == 0xDB && self.b == 0xEE 21 | } 22 | 23 | pub(crate) fn is_data(&self) -> bool { 24 | !self.is_blank() && !self.is_meta() 25 | } 26 | 27 | pub fn to_hex_color(&self) -> String { 28 | format!("#{:02X?}{:02X?}{:02X?}", self.r, self.g, self.b) 29 | } 30 | } 31 | 32 | pub const BLANK: RGB = RGB { r: 255, g: 255, b: 255 }; 33 | 34 | impl From<&String> for RGB { 35 | fn from(s: &String) -> Self { 36 | Self::new( 37 | u8::from_str_radix(&s[1..3], 16).unwrap(), 38 | u8::from_str_radix(&s[3..5], 16).unwrap(), 39 | u8::from_str_radix(&s[5..7], 16).unwrap() 40 | ) 41 | } 42 | } 43 | 44 | impl From> for RGB { 45 | fn from(rgba: Rgba) -> Self { 46 | RGB::new(rgba[0], rgba[1], rgba[2]) 47 | } 48 | } 49 | 50 | impl From<&RGB> for Rgba { 51 | fn from(rgb: &RGB) -> Self { 52 | Rgba::from_channels(rgb.r, rgb.g, rgb.b, 255) 53 | } 54 | } 55 | 56 | 57 | impl From for Rgb { 58 | fn from(rgb: RGB) -> Self { 59 | Self::from_channels(rgb.r, rgb.g, rgb.b, 255) 60 | } 61 | } -------------------------------------------------------------------------------- /backend/src/model/datatypes/pie.rs: -------------------------------------------------------------------------------- 1 | use crate::model::model::{Field, FieldType, DataType, DataValue, DataError, IncompatibleError}; 2 | use std::collections::HashMap; 3 | use crate::image::{ImageView}; 4 | use crate::model::colors::RGB; 5 | 6 | pub const PIE_TYPE: FieldType = FieldType(0b_111_101_110); 7 | 8 | pub(crate) struct PieDataType; 9 | 10 | impl DataType for PieDataType { 11 | 12 | fn read(&self, image: &ImageView, _: &Field) -> Result { 13 | let mut counters_per_color: HashMap = HashMap::new(); 14 | let mut pixels = 0; 15 | for x in 0..image.width { 16 | for y in 0..image.height { 17 | pixels += 1; 18 | let key = image.get_pixel(x, y); 19 | match counters_per_color.get_mut(&key) { 20 | None => { counters_per_color.insert(key, 1); } 21 | Some(value) => (*value) += 1 22 | }; 23 | } 24 | } 25 | Ok(DataValue::Histogram { 26 | value: counters_per_color.drain().map(|(key, value)| (key, (value as f32) / (pixels as f32))).collect() 27 | }) 28 | } 29 | 30 | fn write(&self, image: &mut ImageView, _: &Field, value: DataValue) -> Result<(), DataError> { 31 | let pie_value = get_matched!(value, DataValue::Histogram { value })?; 32 | image.clear(); 33 | let total_pixels = image.width * image.height; 34 | let mut xx = 0; 35 | let mut yy = 0; 36 | for (color,value) in pie_value { 37 | let mut pixels = (value * total_pixels as f32).floor() as u32; 38 | if pixels == 0 { continue; } 39 | 'out: while xx < image.width { 40 | while yy < image.height { 41 | image.set_pixel(xx, yy, color).unwrap(); 42 | yy += 1; 43 | pixels -= 1; 44 | if pixels == 0 { 45 | break 'out; 46 | } 47 | } 48 | yy = 0; 49 | xx += 1; 50 | } 51 | } 52 | Ok(()) 53 | } 54 | } -------------------------------------------------------------------------------- /backend/src/model/datatypes/image.rs: -------------------------------------------------------------------------------- 1 | use crate::model::model::{FieldType, Field, DataType, DataValue, DataError, IncompatibleError}; 2 | use crate::image::ImageView; 3 | use image::{ImageFormat, ImageError}; 4 | use imageproc::drawing::Canvas; 5 | use base64::DecodeError; 6 | 7 | pub const IMAGE_TYPE: FieldType = FieldType(0b_000_000_000); 8 | 9 | pub(crate) struct ImageDataType; 10 | 11 | impl DataType for ImageDataType { 12 | 13 | fn read(&self, image: &ImageView, _: &Field) -> Result { 14 | Ok(DataValue::Image { 15 | width: image.width, 16 | height: image.height, 17 | data_url: format!("data:image/png;base64,{}", image.get_base64()) 18 | }) 19 | } 20 | 21 | fn write(&self, image: &mut ImageView, _field: &Field, value: DataValue) -> Result<(), DataError> { 22 | match value { 23 | DataValue::Image { width, height, data_url } => { 24 | if width != image.width || height != image.height { 25 | log::error!("target size is {}x{}, but value size is {}x{}", image.width, image.height, width, height); 26 | Result::Err(DataError::Incompatible(IncompatibleError::InvalidSize)) 27 | } else { 28 | let bytes = base64::decode(data_url.strip_prefix("data:image/png;base64,").unwrap())?; 29 | let temp_image = image::load_from_memory_with_format(&bytes, ImageFormat::Png)?; 30 | for x in 0..width { 31 | for y in 0..height { 32 | image.set_pixel(x, y, temp_image.get_pixel(x, y))?; 33 | } 34 | } 35 | Result::Ok(()) 36 | } 37 | 38 | } 39 | _ => Result::Err(DataError::Incompatible(IncompatibleError::InvalidDataType)) 40 | } 41 | } 42 | } 43 | 44 | impl From<()> for DataError { 45 | fn from(_: ()) -> Self { 46 | DataError::Incompatible(IncompatibleError::InvalidSize) 47 | } 48 | } 49 | 50 | impl From for DataError { 51 | fn from(e: DecodeError) -> Self { 52 | DataError::Incompatible(IncompatibleError::CannotParseValue(e.to_string())) 53 | } 54 | } 55 | 56 | impl From for DataError { 57 | fn from(ie: ImageError) -> Self { 58 | log::error!("Error {}", ie); 59 | DataError::Incompatible(IncompatibleError::CannotParseValue(ie.to_string())) 60 | } 61 | } -------------------------------------------------------------------------------- /backend/src/image.rs: -------------------------------------------------------------------------------- 1 | use crate::model::model::Vector2D; 2 | use crate::model::colors::{RGB, BLANK}; 3 | use std::fmt::Debug; 4 | 5 | pub struct ImageView<'a> { 6 | image: &'a mut BoxedStorableImage, 7 | x: u32, 8 | y: u32, 9 | pub(crate) width: u32, 10 | pub(crate) height: u32, 11 | } 12 | 13 | impl<'a> ImageView<'a> { 14 | pub fn new(image: &'a mut BoxedStorableImage, from: Vector2D, to_inclusive: Vector2D) -> Self { 15 | ImageView { image: image, x: from.x, y: from.y, width: to_inclusive.x - from.x + 1, height: to_inclusive.y - from.y + 1 } 16 | } 17 | 18 | pub fn from(img: &mut BoxedStorableImage) -> ImageView { 19 | let size = Vector2D::new(img.width() - 1, img.height() - 1); 20 | ImageView::new(img, Vector2D::new(0, 0), size) 21 | } 22 | 23 | pub(crate) fn get_pixel(&self, x: u32, y: u32) -> RGB { 24 | if x >= self.width || y >= self.height { 25 | BLANK 26 | } else { 27 | self.image.get_pixel(self.x + x, self.y + y) 28 | } 29 | } 30 | 31 | pub fn set_pixel(&mut self, x: u32, y: u32, rgb: T) -> Result<(), ()> where T: Into { 32 | if x < self.width && y < self.height { 33 | self.image.as_mut().set_pixel(self.x + x, self.y + y, &rgb.into()); 34 | Result::Ok(()) 35 | } else { 36 | Result::Err(()) 37 | } 38 | } 39 | 40 | pub fn fill(&mut self, rgb: &RGB) { 41 | for x in 0..self.width { 42 | for y in 0..self.height { 43 | self.set_pixel(x, y, *rgb).unwrap(); 44 | } 45 | } 46 | } 47 | pub fn clear(&mut self) { 48 | self.fill(&RGB::new(255, 255, 255)) 49 | } 50 | 51 | pub fn optimize(&self) { 52 | self.image.optimize(); 53 | } 54 | 55 | pub fn get_base64(&self) -> String { 56 | self.image.get_base64(self.x, self.y, self.width, self.height) 57 | } 58 | } 59 | 60 | 61 | #[derive(Debug)] 62 | pub enum SyncResponse { 63 | Ok, 64 | Reloaded, 65 | } 66 | 67 | 68 | pub trait StorableImage: Debug { 69 | fn get_pixel(&self, x: u32, y: u32) -> RGB; 70 | 71 | fn set_pixel(&mut self, x: u32, y: u32, rgb: &RGB); 72 | 73 | fn width(&self) -> u32; 74 | 75 | fn height(&self) -> u32; 76 | 77 | fn sync(&mut self) -> Result; 78 | 79 | fn optimize(&self); 80 | 81 | fn get_base64(&self, x: u32, y: u32, width: u32, height: u32) -> String; 82 | } 83 | 84 | pub type BoxedStorableImage = Box; -------------------------------------------------------------------------------- /backend/src/model/datatypes/abc.rs: -------------------------------------------------------------------------------- 1 | use crate::model::model::{FieldType, Field, DataType, DataValue, DataError, IncompatibleError}; 2 | use crate::io::bitmap_font::BitmapFont; 3 | use crate::image::ImageView; 4 | 5 | pub const ABC_TYPE: FieldType = FieldType(0b_010_101_101); 6 | 7 | pub(crate) struct ABCDataType { 8 | bitmap_font: BitmapFont, 9 | } 10 | 11 | impl ABCDataType { 12 | pub(crate) fn new() -> ABCDataType { 13 | return ABCDataType { 14 | bitmap_font: BitmapFont::open3x5("font1.png", "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_+-*") 15 | } 16 | } 17 | } 18 | 19 | impl DataType for ABCDataType { 20 | 21 | fn read(&self, image: &ImageView, _: &Field) -> Result { 22 | let mut chars: Vec = vec![]; 23 | 24 | let mut start_x = 0; 25 | let mut start_y = 1; 26 | 27 | while start_x < image.width && start_y < image.height { 28 | let mut char = ' '; 29 | 'out: for dx in 0..=2 { 30 | if start_x + dx >= image.width { continue } 31 | for dy in -1..=2 as i32 { 32 | if start_y == 0 && dy < 0 { continue } 33 | if (start_y as i32 + dy) as u32 >= image.height { continue } 34 | match self.bitmap_font.get_char(image, start_x + dx, (start_y as i32 + dy) as u32) { 35 | Some(c) => { 36 | char = c; 37 | start_x += dx; 38 | start_x += self.bitmap_font.char_dimensions.0; 39 | start_y = (start_y as i32 + dy) as u32; 40 | //start_y += self.bitmap_font.char_dimensions.1; 41 | break 'out; 42 | } 43 | None => continue 44 | } 45 | } 46 | } 47 | if char == ' ' { 48 | start_x += self.bitmap_font.char_dimensions.0; 49 | //start_y += self.bitmap_font.char_dimensions.1; 50 | } 51 | chars.push(char); 52 | } 53 | 54 | let string: String = chars.iter().collect(); 55 | let string = String::from(string.trim()); 56 | Ok(DataValue::String { value: string }) 57 | } 58 | 59 | fn write(&self, image: &mut ImageView, _: &Field, value: DataValue) -> Result<(), DataError> { 60 | let string = get_matched!(value, DataValue::String { value })?; 61 | image.clear(); 62 | self.bitmap_font.put_string(image, 1, 1, string.as_str()); 63 | Ok(()) 64 | } 65 | } -------------------------------------------------------------------------------- /web-server/src/json.rs: -------------------------------------------------------------------------------- 1 | use serde_json::{Value, json}; 2 | use badbee_backend::model::model::{DataValue}; 3 | use crate::handlers::vec2id; 4 | use std::collections::HashMap; 5 | use badbee_backend::db::DataFieldValue; 6 | 7 | pub(crate) fn to_json(val: &DataFieldValue, embed_refs: bool) -> Value { 8 | let mut out = match &val.value { 9 | DataValue::Null => json!({"value": Value::Null}), 10 | DataValue::Boolean { value } => json!({"value": value, "type": "boolean"}), 11 | DataValue::Int { value } => json!({"value": value, "type": "int"}), 12 | DataValue::Float { value } => json!({"value": value, "type": "float"}), 13 | DataValue::String { value } => json!({"value": value, "type": "string"}), 14 | DataValue::Color { value } => json!({"value": value.to_hex_color(), "type": "color"}), 15 | DataValue::Image { width, height, data_url } => json!({ 16 | "type": "image", "value": { 17 | "width": width, "height": height, "data_url": data_url 18 | } 19 | }), 20 | DataValue::Histogram { value } => json!({ 21 | "type": "pie", 22 | "value": value.iter().map(|(k,v)| (k.to_hex_color(), *v)).collect::>() 23 | }), 24 | DataValue::Custom { subtype, value } => json!({"type": subtype, "value": value}) 25 | }; 26 | if let Some(id) = val.reference { 27 | out["reference"] = json!(vec2id(id)); 28 | if !embed_refs { 29 | out["value"] = Value::Null; 30 | } 31 | } 32 | json!(out) 33 | } 34 | 35 | pub(crate) fn from_json(val: &Value) -> Option { 36 | //todo: check "type" as well 37 | let _vtype = val.get("type").and_then(|v| v.as_str()); 38 | let val = val.get("value")?; 39 | match val { 40 | Value::Null => None, 41 | Value::Bool(value) => Some(DataValue::Boolean { value: *value }), 42 | Value::Number(value) if value.is_f64() => Some(DataValue::Float { value: value.as_f64().unwrap() as f32 }), 43 | Value::Number(value) => Some(DataValue::Int { value: value.as_i64().unwrap() as i32 }), 44 | Value::String(value) if value.starts_with("#") => Some(DataValue::Color { value: value.into() }), 45 | Value::String(value) => Some(DataValue::String { value: value.clone() }), 46 | Value::Array(_) => None, 47 | Value::Object(obj) if obj.contains_key("data_url") => 48 | //todo: support other objects 49 | Some(DataValue::Image { 50 | width: obj["width"].as_i64().unwrap() as u32, 51 | height: obj["height"].as_i64().unwrap() as u32, 52 | data_url: obj["data_url"].as_str().unwrap().to_string(), 53 | }), 54 | Value::Object(obj) => 55 | Some(DataValue::Histogram { 56 | value: obj.iter() 57 | .map(|(k, v)| (k.into(), v.as_f64().unwrap() as f32)) 58 | .collect() 59 | }) 60 | } 61 | } 62 | 63 | 64 | -------------------------------------------------------------------------------- /backend/src/io/in_memory_image.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Error, ErrorKind}; 2 | use std::path::{Path, PathBuf}; 3 | use std::time::SystemTime; 4 | 5 | use image::{ColorType, DynamicImage, GenericImage, GenericImageView, ImageResult}; 6 | use image::codecs::png::PngEncoder; 7 | 8 | use crate::image::{StorableImage, SyncResponse}; 9 | use crate::model::colors::RGB; 10 | use std::fmt::{Debug, Formatter}; 11 | 12 | pub struct InMemoryImage { 13 | image: DynamicImage, 14 | path: PathBuf, 15 | dirty: bool, 16 | last_modified_time: SystemTime, 17 | } 18 | 19 | impl InMemoryImage { 20 | pub(crate) fn new(path: &Path) -> Self { 21 | Self { 22 | image: image::open(path).unwrap(), 23 | path: path.to_path_buf(), 24 | dirty: false, 25 | last_modified_time: std::fs::metadata(path).unwrap().modified().unwrap(), 26 | } 27 | } 28 | } 29 | 30 | impl StorableImage for InMemoryImage { 31 | fn get_pixel(&self, x: u32, y: u32) -> RGB { 32 | self.image.get_pixel(x, y).into() 33 | } 34 | 35 | 36 | fn set_pixel(&mut self, x: u32, y: u32, rgb: &RGB) { 37 | self.image.put_pixel(x, y, rgb.into()); 38 | self.dirty = true; 39 | } 40 | 41 | fn width(&self) -> u32 { 42 | self.image.width() 43 | } 44 | 45 | fn height(&self) -> u32 { 46 | self.image.height() 47 | } 48 | 49 | fn sync(&mut self) -> Result { 50 | let path = self.path.as_path(); 51 | let modified = std::fs::metadata(path)?.modified().unwrap(); 52 | if self.dirty { 53 | let copied = self.image.clone(); 54 | let copied_path = path.to_path_buf(); 55 | tokio::spawn( async move { 56 | if let ImageResult::Err(e) = copied.save(copied_path) { 57 | log::error!("Error during saving {}", e) 58 | } 59 | }); 60 | self.dirty = false; 61 | Ok(SyncResponse::Ok) 62 | } else if modified > self.last_modified_time { 63 | self.last_modified_time = modified; 64 | self.image = image::open(path).map_err(|_| Error::new(ErrorKind::Other, "Cannot load"))?; 65 | Ok(SyncResponse::Reloaded) 66 | } else { 67 | Ok(SyncResponse::Ok) 68 | } 69 | } 70 | 71 | fn optimize(&self) { 72 | 73 | } 74 | 75 | fn get_base64(&self, x: u32, y: u32, width: u32, height: u32) -> String { 76 | let sub_image = self.image.view(x, y, width, height); 77 | let mut buf: Vec = vec![]; 78 | PngEncoder::new(&mut buf) 79 | .encode(sub_image.to_image().as_raw(), sub_image.dimensions().0, sub_image.dimensions().1, ColorType::Rgba8).unwrap(); 80 | base64::encode(&buf) 81 | } 82 | } 83 | 84 | 85 | impl Debug for InMemoryImage { 86 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 87 | f.debug_struct("InMemoryImage") 88 | .field("path", &self.path) 89 | .finish() 90 | } 91 | } -------------------------------------------------------------------------------- /backend/src/io/bitmap_font.rs: -------------------------------------------------------------------------------- 1 | use image::{DynamicImage, GenericImageView}; 2 | use std::collections::HashMap; 3 | use crate::image::{ImageView}; 4 | use crate::model::colors::RGB; 5 | 6 | 7 | pub struct BitmapFont { 8 | image: DynamicImage, 9 | spacing: u32, 10 | pub(crate) char_dimensions: (u32, u32), 11 | alphabet: Vec, 12 | 13 | mapping: HashMap, 14 | chars_to_idx: HashMap, 15 | } 16 | 17 | impl BitmapFont { 18 | pub fn open3x5(path: &str, alphabet: &str) -> BitmapFont { 19 | let mut bf = BitmapFont { 20 | image: image::open(path).unwrap(), 21 | spacing: 1, 22 | char_dimensions: (3, 5), 23 | alphabet: alphabet.chars().collect(), 24 | mapping: HashMap::new(), 25 | chars_to_idx: HashMap::new(), 26 | }; 27 | 28 | bf.init_mapping(); 29 | bf 30 | } 31 | 32 | fn init_mapping(&mut self) { 33 | let mut char_idx = 0; 34 | for x in (0..self.image.width()).step_by((self.char_dimensions.0 + self.spacing) as usize) { 35 | for y in (0..self.image.height()).step_by((self.char_dimensions.1 + self.spacing) as usize) { 36 | let mut key: u32 = 0; 37 | for xx in x..x + self.char_dimensions.0 { 38 | for yy in y..y + self.char_dimensions.1 { 39 | key = key << 1; 40 | let pix: RGB = self.image.get_pixel(xx, yy).into(); 41 | if !pix.is_blank() { 42 | key = key | 1; 43 | } 44 | } 45 | } 46 | if key != 0 { 47 | self.mapping.insert(key, self.alphabet[char_idx]); 48 | self.chars_to_idx.insert(self.alphabet[char_idx], char_idx); 49 | } 50 | } 51 | char_idx += 1 52 | } 53 | } 54 | 55 | pub fn get_char(&self, image: &ImageView, x: u32, y: u32) -> Option { 56 | let mut key: u32 = 0; 57 | for xx in x..x + self.char_dimensions.0 { 58 | for yy in y..y + self.char_dimensions.1 { 59 | key = key << 1; 60 | if !image.get_pixel(xx, yy).is_blank() { 61 | key = key | 1; 62 | } 63 | } 64 | } 65 | self.mapping.get(&key).map(|c| *c) 66 | } 67 | 68 | pub fn put_string(&self, image: &mut ImageView, x: u32, y: u32, str: &str) { 69 | let mut cx = x; 70 | for chr in str.chars() { 71 | if let Some(idc) = self.chars_to_idx.get(&chr) { 72 | let char_idx = *idc as u32; 73 | let font_x = char_idx * self.char_dimensions.0 + (char_idx * self.spacing); 74 | for dx in 0..self.char_dimensions.0 { 75 | for dy in 0..self.char_dimensions.1 { 76 | if let Result::Err(_) = image.set_pixel(cx + dx, y + dy, self.image.get_pixel(font_x + dx, dy)) { 77 | log::error!("Cannot put string {} at {},{}: too long", str, x, y); 78 | break //todo: return error 79 | } 80 | } 81 | } 82 | } 83 | cx += self.char_dimensions.0 + self.spacing; 84 | } 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /backend/src/model/datatypes/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::model::model::{DataType, Field, DataError, DataValue, FieldType, IncompatibleError}; 2 | use crate::image::ImageView; 3 | 4 | pub(crate) mod boolean; 5 | pub(crate) mod flood; 6 | pub(crate) mod image; 7 | pub(crate) mod abc; 8 | pub(crate) mod color; 9 | pub(crate) mod counter; 10 | pub(crate) mod reference; 11 | pub(crate) mod pie; 12 | 13 | pub const DEFAULT_TYPE: FieldType = FieldType(0b_000_000_000); 14 | 15 | const BOOLEAN_DT: boolean::BooleanDataType = boolean::BooleanDataType {}; 16 | const IMAGE_DT: image::ImageDataType = image::ImageDataType {}; 17 | const FLOOD_DT: flood::FloodDataType = flood::FloodDataType {}; 18 | const COLOR_DT: color::ColorDataType = color::ColorDataType {}; 19 | const COUNTER_DT: counter::CounterDataType = counter::CounterDataType {}; 20 | const PIE_DT: pie::PieDataType = pie::PieDataType {}; 21 | 22 | pub struct DataTypes { 23 | abc_data_type: abc::ABCDataType 24 | } 25 | 26 | impl DataTypes { 27 | 28 | pub fn new() -> Self { 29 | Self { 30 | abc_data_type: abc::ABCDataType::new() 31 | } 32 | } 33 | 34 | pub fn read_casted(&self, image: &ImageView, field: &Field, ftype: FieldType) -> Result { 35 | match ftype { 36 | boolean::BOOL_TYPE => BOOLEAN_DT.read(image, field), 37 | image::IMAGE_TYPE => IMAGE_DT.read(image, field), 38 | flood::FLOOD_TYPE => FLOOD_DT.read(image, field), 39 | abc::ABC_TYPE => self.abc_data_type.read(image, field), 40 | color::COLOR_TYPE => COLOR_DT.read(image, field), 41 | counter::COUNTER_TYPE => COUNTER_DT.read(image, field), 42 | pie::PIE_TYPE => PIE_DT.read(image, field), 43 | reference::REFERENCE_TYPE => Ok(DataValue::Null), 44 | _ => Err(DataError::UnknownType(ftype)) 45 | } 46 | } 47 | 48 | pub fn read(&self, image: &ImageView, field: &Field) -> Result { 49 | self.read_casted(image, field, field.field_type) 50 | } 51 | 52 | pub fn write(&self, image: &mut ImageView, field: &Field, value: DataValue) -> Result<(), DataError> { 53 | self.write_casted(image, field, field.field_type, value) 54 | } 55 | 56 | pub fn write_casted(&self, image: &mut ImageView, field: &Field, ftype: FieldType, value: DataValue) -> Result<(), DataError> { 57 | match ftype { 58 | boolean::BOOL_TYPE => BOOLEAN_DT.write(image, field, value), 59 | image::IMAGE_TYPE => IMAGE_DT.write(image, field, value), 60 | flood::FLOOD_TYPE => FLOOD_DT.write(image, field, value), 61 | abc::ABC_TYPE => self.abc_data_type.write(image, field, value), 62 | color::COLOR_TYPE => COLOR_DT.write(image, field, value), 63 | counter::COUNTER_TYPE => COUNTER_DT.write(image, field, value), 64 | pie::PIE_TYPE => PIE_DT.write(image, field, value), 65 | reference::REFERENCE_TYPE => Err(DataError::Incompatible(IncompatibleError::InvalidDataType)), 66 | _ => Err(DataError::UnknownType(ftype)) 67 | } 68 | } 69 | 70 | #[allow(dead_code)] 71 | pub fn get_preferred_type(&self, val: DataValue) -> Option { 72 | match val { 73 | DataValue::Boolean { .. } => Some(boolean::BOOL_TYPE), 74 | DataValue::Int { .. } => Some(counter::COUNTER_TYPE), 75 | DataValue::Float { .. } => Some(flood::FLOOD_TYPE), 76 | DataValue::String { .. } => Some(abc::ABC_TYPE), 77 | DataValue::Color { .. } => Some(color::COLOR_TYPE), 78 | DataValue::Image { .. } => Some(image::IMAGE_TYPE), 79 | DataValue::Histogram { .. } => Some(pie::PIE_TYPE), 80 | DataValue::Custom { .. } => None, 81 | DataValue::Null => None 82 | } 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /backend/src/model/model.rs: -------------------------------------------------------------------------------- 1 | use crate::image::{ImageView}; 2 | use std::collections::HashMap; 3 | use crate::model::colors::RGB; 4 | use std::time::Duration; 5 | 6 | #[derive(Eq, PartialEq, Debug, Copy, Clone)] 7 | pub struct FieldType(pub u16); 8 | 9 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 10 | pub struct Vector2D { 11 | pub x: u32, 12 | pub y: u32, 13 | } 14 | 15 | impl Vector2D { 16 | pub fn new(x: u32, y: u32) -> Self { 17 | Self { x, y } 18 | } 19 | } 20 | 21 | #[derive(Debug, Clone)] 22 | pub struct Field { 23 | pub field_type: FieldType, 24 | pub data_start: Vector2D, 25 | pub(crate) data_end: Vector2D, 26 | pub(crate) type_start: Vector2D, 27 | pub ref_to_record: Option, 28 | } 29 | 30 | #[derive(Debug, Clone)] 31 | pub struct Record { 32 | pub position: Vector2D, 33 | pub fields: Vec, 34 | pub column: String, 35 | pub(crate) rb_position: Vector2D, 36 | } 37 | 38 | #[derive(Debug, Clone)] 39 | pub struct Model { 40 | pub records: Vec, 41 | pub loading_time: Duration, 42 | 43 | by_id: HashMap, 44 | } 45 | 46 | impl Model { 47 | pub fn new() -> Self { 48 | Self { records: vec![], by_id: HashMap::new(), loading_time: Duration::from_secs(0) } 49 | } 50 | 51 | pub fn add_record(&mut self, rec: &Record) { 52 | self.records.push(rec.clone()); 53 | self.by_id.insert(rec.position, self.records.len() - 1); 54 | } 55 | 56 | pub fn insert_record(&mut self, idx: usize, rec: &Record) { 57 | self.records.insert(idx, rec.clone()); 58 | //let's recalculate all... 59 | for idx in 0..self.records.len() { 60 | self.by_id.insert(self.records[idx].position, idx); 61 | } 62 | } 63 | 64 | pub fn get_by_id(&self, x: u32, y: u32) -> Option<&Record> { 65 | self.by_id.get(&Vector2D::new(x, y)).map(|i| &self.records[*i]) 66 | } 67 | } 68 | 69 | #[derive(Debug)] 70 | pub enum IncompatibleError { 71 | InvalidDataType, 72 | InvalidSize, 73 | CannotParseValue(String), 74 | } 75 | 76 | #[derive(Debug)] 77 | pub enum DataError { 78 | Incompatible(IncompatibleError), 79 | UnknownType(FieldType), 80 | NotImplemented, 81 | NotFound, 82 | } 83 | 84 | #[derive(Debug)] 85 | pub enum DataValue { 86 | Null, 87 | Boolean { value: bool }, 88 | Int { value: i32 }, 89 | Float { value: f32 }, 90 | String { value: String }, 91 | Color { value: RGB }, 92 | Image { width: u32, height: u32, data_url: String }, 93 | Histogram { value: HashMap }, 94 | 95 | Custom { subtype: String, value: String }, 96 | } 97 | 98 | macro_rules! get_matched { 99 | ($val: expr, $subtype: pat) => { 100 | match $val { 101 | $subtype => Ok($val), 102 | _ => Err(DataError::Incompatible(IncompatibleError::InvalidDataType)) 103 | } 104 | } 105 | } 106 | 107 | pub trait DataType { 108 | fn read(&self, image: &ImageView, field: &Field) -> Result; 109 | 110 | fn write(&self, _image: &mut ImageView, _field: &Field, _value: DataValue) -> Result<(), DataError> { 111 | Err(DataError::NotImplemented) 112 | } 113 | } 114 | 115 | impl From for String { 116 | fn from(de: DataError) -> Self { 117 | match de { 118 | DataError::Incompatible(er) => format!("Incompatible: {:?}", er), 119 | DataError::UnknownType(dt) => format!("Unknown data type {:?}", dt), 120 | DataError::NotImplemented => String::from("Not implemented!"), 121 | DataError::NotFound => String::from("Not found"), 122 | } 123 | } 124 | } 125 | 126 | impl std::ops::AddAssign for Vector2D { 127 | fn add_assign(&mut self, rhs: Vector2D) { 128 | self.x += rhs.x; 129 | self.y += rhs.y; 130 | } 131 | } -------------------------------------------------------------------------------- /web-client/src/api.rs: -------------------------------------------------------------------------------- 1 | use sauron::Cmd; 2 | use sauron::prelude::*; 3 | use sauron::prelude::web_sys::RequestInit; 4 | use serde_derive::Deserialize; 5 | use crate::{App, Msg}; 6 | 7 | 8 | pub type DBList = Vec; 9 | 10 | #[derive(Deserialize, Debug, Clone)] 11 | pub struct RecordField { 12 | #[serde(rename = "type")] 13 | pub(crate) ftype: Option, 14 | pub(crate) reference: Option, 15 | pub(crate) value: serde_json::Value 16 | } 17 | 18 | #[derive(Deserialize, Debug, Clone)] 19 | pub struct Record { 20 | pub(crate) id: String, 21 | pub(crate) fields: Vec, 22 | pub(crate) column: String, 23 | } 24 | 25 | #[derive(Deserialize, Debug, Clone)] 26 | pub struct Model { 27 | pub(crate) records: u32, 28 | pub(crate) loading_time: u32 29 | } 30 | 31 | pub type RecordsList = Vec; 32 | 33 | pub struct RecordsQuery { 34 | offset: Option, limit: Option, column: Option, ids: Option> 35 | } 36 | 37 | impl RecordsQuery { 38 | pub fn all() -> Self { Self { offset: None, limit: None, column: None, ids: None }} 39 | 40 | pub fn column(name: String) -> Self { Self { offset: None, limit: None, column: Some(name), ids: None }} 41 | 42 | pub fn subset(offset: u32, limit: u32) -> Self { Self { offset: Some(offset), limit: Some(limit), column: None, ids: None}} 43 | 44 | pub fn column_subset(name: String, offset: u32, limit: u32) -> Self { Self { offset: Some(offset), limit: Some(limit), column: Some(name), ids: None}} 45 | 46 | pub fn by_ids(ids: Vec) -> Self { Self { offset: None, limit: None, column: None, ids: Some(ids)}} 47 | } 48 | 49 | impl App { 50 | 51 | pub fn fetch_db_list(&self) -> Cmd { 52 | Http::fetch_with_text_response_decoder( 53 | "/dbs.json", 54 | |s| Msg::DBListLoaded(Result::Ok(serde_json::from_str(&s).unwrap())), 55 | |err| Msg::DBListLoaded(Result::Err(err)) 56 | ) 57 | } 58 | 59 | pub fn fetch_db_info(&self, name: String) -> Cmd { 60 | Http::fetch_with_text_response_decoder( 61 | format!("{}/model.json", name).as_str(), 62 | |s| Msg::DBInfoLoaded(Result::Ok(serde_json::from_str(&s).unwrap())), 63 | |err| Msg::DBInfoLoaded(Result::Err(err)) 64 | ) 65 | } 66 | 67 | pub fn patch_record(&self, name: &String, rid: String, fid: u32, body: serde_json::Value) -> Cmd { 68 | Http::fetch_with_request_and_response_decoder( 69 | format!("{}/records/{}/{}", name, rid, fid).as_str(), 70 | Some(RequestInit::new() 71 | .method("PUT") 72 | .headers(&JsValue::from_serde(&serde_json::json!({ "Content-Type": "application/json"})).unwrap()) 73 | .body(Some(&JsValue::from_str(&body.to_string()))) 74 | .clone() 75 | ), 76 | |(_, p)| p.dispatch(Msg::DBRecordPatched(Result::Ok(()))), 77 | |err| Msg::DBRecordPatched(Result::Err(err)) 78 | ) 79 | } 80 | 81 | pub fn fetch_all_records(&self, name: &String) -> Cmd { 82 | self.fetch_records(name, RecordsQuery::all()) 83 | } 84 | 85 | pub fn fetch_records(&self, name: &String, query: RecordsQuery) -> Cmd { 86 | let mut url = format!("{}/records.json?", name); 87 | if let Some(offset) = query.offset { 88 | url += format!("offset={}&", offset).as_str(); 89 | } 90 | if let Some(limit) = query.limit { 91 | url += format!("limit={}&", limit).as_str(); 92 | } 93 | if let Some(column) = query.column { 94 | url += format!("column={}&", column).as_str(); 95 | } 96 | if let Some(ids) = query.ids { 97 | url += "ids="; 98 | for id in ids { 99 | url += format!("{},", id).as_str(); 100 | } 101 | url += "&"; 102 | } 103 | Http::fetch_with_text_response_decoder( 104 | url.as_str(), 105 | |s| Msg::RecordsLoaded(Result::Ok(serde_json::from_str(&s).unwrap())), 106 | |err| Msg::RecordsLoaded(Result::Err(err)) 107 | ) 108 | } 109 | } -------------------------------------------------------------------------------- /web-server/src/main.rs: -------------------------------------------------------------------------------- 1 | mod json; 2 | mod handlers; 3 | 4 | use warp::{Filter}; 5 | use std::collections::HashMap; 6 | use serde_derive::Deserialize; 7 | use std::time::Duration; 8 | use badbee_backend::db::DBHandle; 9 | use crate::handlers::{get_records_handler, put_field_handler, get_model_handler, clone_record_handler, get_dbs_handler}; 10 | use std::sync::Arc; 11 | use tokio::sync::Mutex; 12 | use tokio::signal; 13 | 14 | pub type DBMAP = Arc>>; 15 | 16 | #[derive(Deserialize)] 17 | pub struct RecordsQuery { 18 | offset: Option, 19 | limit: Option, 20 | column: Option, 21 | ids: Option, 22 | //comma-separated 23 | embed_refs: Option, 24 | } 25 | 26 | #[tokio::main] 27 | async fn main() { 28 | stderrlog::new().verbosity(2).init().unwrap(); 29 | 30 | let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel(); 31 | 32 | let dbs: DBMAP = Arc::new(Mutex::new(HashMap::new())); 33 | 34 | match std::env::var("DB_FILE") { 35 | Ok(value) => { 36 | log::info!("Load db specified in DB_NAME env var ({})", value.clone()); 37 | dbs.lock().await.insert(value.clone(), DBHandle::run_in_background(format!("db/{}", value).as_str())); 38 | } 39 | Err(_) => { 40 | log::info!("Load default db"); 41 | dbs.lock().await.insert("db".to_string(), DBHandle::run_in_background("db/db.png")); 42 | } 43 | } 44 | 45 | let sync_dbs = dbs.clone(); 46 | let shutdown_dbs = dbs.clone(); 47 | 48 | let periodical_sync = tokio::spawn(async move { 49 | loop { 50 | for (_, v) in sync_dbs.lock().await.iter() { 51 | v.sync().await; 52 | } 53 | tokio::time::sleep(Duration::from_secs(1)).await; 54 | } 55 | }); 56 | 57 | 58 | fn with_dbs(dbs: DBMAP) -> impl Filter + Clone { 59 | warp::any().map(move || dbs.clone()) 60 | } 61 | 62 | let with_dbs_filter = with_dbs(dbs); 63 | 64 | let get_dbs = warp::path!("dbs.json") 65 | .and(with_dbs_filter.clone()) 66 | .and_then(get_dbs_handler); 67 | 68 | let get_records = warp::path!(String / "records.json") 69 | .and(warp::query()) 70 | .and(with_dbs_filter.clone()) 71 | .and_then(get_records_handler); 72 | 73 | let get_model = warp::path!(String / "model.json") 74 | .and(with_dbs_filter.clone()) 75 | .and_then(get_model_handler); 76 | 77 | let put_field = warp::put() 78 | .and(warp::path!(String / "records" / u32 / u32 / u32)) 79 | .and(with_dbs_filter.clone()) 80 | .and(warp::body::json()) 81 | .and_then(put_field_handler) 82 | ; 83 | 84 | let clone_record = warp::post() 85 | .and(warp::path!(String / "records" / u32 / u32 / "clone")) 86 | .and(with_dbs_filter.clone()) 87 | .and_then(clone_record_handler); 88 | 89 | let cors = warp::cors() 90 | .allow_any_origin() 91 | .allow_headers(vec!["User-Agent", "Sec-Fetch-Mode", "Referer", "Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", "Content-Type"]) 92 | .allow_methods(vec!["POST", "GET", "PUT", "PATCH", "DELETE"]) 93 | .build(); 94 | 95 | let routes = get_dbs 96 | .or(get_records) 97 | .or(put_field) 98 | .or(get_model) 99 | .or(clone_record); 100 | let static_files = warp::get().and(warp::fs::dir("static")); 101 | 102 | let (_, server) = warp::serve(routes.or(static_files).with(cors)) 103 | .bind_with_graceful_shutdown(([0, 0, 0, 0], 3030), async { shutdown_rx.await.ok(); }); 104 | 105 | tokio::task::spawn(server); 106 | log::info!("Listen to 0.0.0.0:3030. Waiting for ctrl-c"); 107 | signal::ctrl_c().await.expect("failed to listen for event"); 108 | log::info!("received ctrl-c event"); 109 | 110 | shutdown_tx.send(()).unwrap(); 111 | log::info!("stopped web"); 112 | periodical_sync.abort(); 113 | log::info!("stopped periodical"); 114 | for db_handle in shutdown_dbs.lock().await.values() { 115 | db_handle.sync().await; 116 | db_handle.shutdown(); 117 | } 118 | log::info!("stopped dbs"); 119 | 120 | 121 | } -------------------------------------------------------------------------------- /web-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod api; 2 | mod record_view; 3 | mod field_view; 4 | 5 | use sauron::html::text; 6 | use sauron::prelude::*; 7 | use sauron::js_sys::TypeError; 8 | use sauron::{node, Cmd, Application, Node, Program}; 9 | use crate::api::{Record, RecordsQuery}; 10 | use record_view::record_view; 11 | use serde_json::json; 12 | 13 | 14 | #[derive(Debug)] 15 | pub enum Msg { 16 | DBListLoaded(Result), 17 | DBInfoLoaded(Result), 18 | DBSelected(String), 19 | RecordsLoaded(Result), 20 | DBRecordPatched(Result<(), TypeError>), 21 | 22 | PrevPage, NextPage, 23 | 24 | PatchRequested { id: String, fi: usize, new_value: serde_json::Value }, 25 | 26 | Noop 27 | } 28 | 29 | pub struct App { 30 | db_name: Option, 31 | offset: u32, 32 | limit: u32, 33 | total: u32, 34 | loading_time: u32, 35 | 36 | records: Vec, 37 | db_names: Vec 38 | } 39 | 40 | 41 | impl App { 42 | pub fn new() -> Self { 43 | App { 44 | db_name: None, 45 | offset: 0, 46 | limit: 0, 47 | total: 0, 48 | loading_time: 0, 49 | records: vec![], 50 | db_names: vec![] 51 | } 52 | } 53 | 54 | fn load_records(&mut self) -> Cmd { 55 | self.fetch_records(&self.db_name.as_ref().unwrap(), RecordsQuery::subset( self.offset, self.limit)) 56 | } 57 | } 58 | 59 | impl Application for App { 60 | fn init(&mut self) -> Cmd { 61 | console_log::init_with_level(log::Level::Trace).unwrap(); 62 | self.fetch_db_list() 63 | } 64 | 65 | fn update(&mut self, msg: Msg) -> Cmd { 66 | match msg { 67 | Msg::Noop => {}, 68 | Msg::DBListLoaded(list) => { 69 | //log::info!("{:?}", list); 70 | self.db_names = list.unwrap(); 71 | return Cmd::from(Effects::with_local(vec![Msg::DBSelected(self.db_names.last().unwrap().clone())])); 72 | } 73 | Msg::DBInfoLoaded(info) => { 74 | let model = info.unwrap(); 75 | self.total = model.records; 76 | self.offset = 0; 77 | self.limit = 30; 78 | log::info!("Model {} loaded in {} ms", self.db_name.as_ref().unwrap(), model.loading_time); 79 | self.loading_time = model.loading_time; 80 | return self.load_records(); 81 | } 82 | Msg::DBSelected(db_name) => { 83 | self.db_name = Some(db_name.clone()); 84 | return self.fetch_db_info(db_name); 85 | } 86 | Msg::RecordsLoaded(list) => { 87 | //log::info!("{:?}", list); 88 | self.records = list.unwrap() 89 | } 90 | Msg::DBRecordPatched(_result) => { 91 | //log::info!("{:?}", result); 92 | }, 93 | Msg::NextPage => { 94 | self.offset += 30; 95 | return self.load_records(); 96 | }, 97 | Msg::PrevPage => { 98 | self.offset -= 30; 99 | return self.load_records(); 100 | }, 101 | Msg::PatchRequested { id, fi, new_value } => { 102 | return self.patch_record(self.db_name.as_ref().unwrap(), id, fi as u32, json!({"value": new_value})) 103 | } 104 | } 105 | Cmd::none().should_update_view(true) 106 | } 107 | 108 | fn view(&self) -> Node { 109 | node!( 110 |
111 |
112 | 124 | { view_if(self.db_name.is_some(), node! { 125 | { text(format!("DB loaded in {} ms", self.loading_time)) } 126 | } ) } 127 |
128 | 129 | 130 |
131 |
132 |
133 | { for record in &self.records { 134 | record_view(record.clone()) 135 | }} 136 |
137 |
138 | ) 139 | } 140 | } 141 | 142 | #[wasm_bindgen(start)] 143 | pub fn main() { 144 | console_error_panic_hook::set_once(); 145 | Program::mount_to_body(App::new()); 146 | } -------------------------------------------------------------------------------- /static/tracker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 37 |
38 | 39 |

actiPLANS

40 | 41 | 42 | 43 | 44 |
45 | 46 | 47 |
48 | 49 | -------------------------------------------------------------------------------- /web-server/src/handlers.rs: -------------------------------------------------------------------------------- 1 | use crate::{DBMAP, RecordsQuery}; 2 | use warp::reply::{Json, with_status}; 3 | use badbee_backend::db::{DBQuery, DataRecord, DBResult}; 4 | use crate::json::{to_json, from_json}; 5 | use serde_json::{json, Value}; 6 | use warp::{Reply, Rejection}; 7 | use badbee_backend::model::model::Vector2D; 8 | use warp::http::StatusCode; 9 | use log::error; 10 | 11 | pub async fn get_dbs_handler(dbs: DBMAP) -> Result { 12 | let db_names: Vec = dbs.lock().await.keys().map(|k| k.clone()).collect(); 13 | Ok(warp::reply::json(&db_names)) 14 | } 15 | 16 | pub async fn get_model_handler(dbname: String, dbs: DBMAP) -> Result, Rejection> { 17 | if !dbs.lock().await.contains_key(dbname.as_str()) { 18 | return Ok(Box::new(with_status("Unknown db", StatusCode::NOT_FOUND))); 19 | } 20 | let db = &dbs.lock().await[dbname.as_str()]; 21 | match db.get_model().await { 22 | DBResult::Ok(model) => Ok(Box::new(warp::reply::json(&json!({ 23 | "loading_time": model.loading_time.as_millis() as u32, 24 | "records": model.records.len(), 25 | "fields_max": model.records.iter().map(|r| r.fields.len()).max(), 26 | "fields_min": model.records.iter().map(|r| r.fields.len()).min(), 27 | })))), 28 | DBResult::StillLoading(progress) => { 29 | Ok( 30 | Box::new(with_status(format!("Still loading model ({}%)", (progress*100.0) as u32), StatusCode::PARTIAL_CONTENT)) 31 | ) 32 | 33 | }, 34 | DBResult::Err(error) => { 35 | error!("ERROR {}", error); 36 | Ok(Box::new(with_status(error, StatusCode::INTERNAL_SERVER_ERROR))) 37 | } 38 | } 39 | } 40 | 41 | pub async fn clone_record_handler(dbname: String, x: u32, y: u32, dbs: DBMAP) -> Result, Rejection> { 42 | if !dbs.lock().await.contains_key(dbname.as_str()) { 43 | return Ok(Box::new(with_status("Unknown db", StatusCode::NOT_FOUND))); 44 | } 45 | let db = &dbs.lock().await[dbname.as_str()]; 46 | match db.clone_record(x, y).await { 47 | DBResult::Ok(record) => Ok(Box::new(get_records_json(vec![record], false))), 48 | DBResult::StillLoading(progress) => { 49 | Ok( 50 | Box::new(with_status(format!("Still loading model ({}%)", (progress*100.0) as u32), StatusCode::PARTIAL_CONTENT)) 51 | ) 52 | 53 | }, 54 | DBResult::Err(error) => { 55 | error!("ERROR {}", error); 56 | Ok(Box::new(with_status(error, StatusCode::INTERNAL_SERVER_ERROR))) 57 | } 58 | } 59 | } 60 | 61 | pub async fn get_records_handler(dbname: String, q: RecordsQuery, dbs: DBMAP) -> Result, Rejection> { 62 | if !dbs.lock().await.contains_key(dbname.as_str()) { 63 | return Ok(Box::new(with_status("Unknown db", StatusCode::NOT_FOUND))); 64 | } 65 | let db = &dbs.lock().await[dbname.as_str()]; 66 | let mut query = DBQuery::new(); 67 | if let Some(ids) = q.ids { 68 | let ids: Vec = ids.split(",") 69 | .map(|it| it.to_string()) 70 | .filter(|it| it.contains("/")) 71 | .collect(); 72 | if ids.len() != 0 { 73 | query.ids(ids.iter().map(|xy| { 74 | let mut split = xy.split("/"); 75 | Vector2D::new(split.next().unwrap().parse().unwrap(), split.next().unwrap().parse().unwrap()) 76 | }).collect()); 77 | } 78 | } 79 | if let Some(offset) = q.offset { 80 | query.offset(offset); 81 | } 82 | if let Some(limit) = q.limit { 83 | query.limit(limit); 84 | } 85 | if let Some(column) = q.column { 86 | query.column(column); 87 | } 88 | match db.get_records(query).await { 89 | DBResult::Ok(records) => Ok(Box::new(get_records_json(records, q.embed_refs.unwrap_or(false)))), 90 | DBResult::StillLoading(progress) => { 91 | Ok( 92 | Box::new(with_status(format!("Still loading model ({}%)", (progress*100.0) as u32), StatusCode::PARTIAL_CONTENT)) 93 | ) 94 | 95 | }, 96 | DBResult::Err(error) => { 97 | error!("ERROR {}", error); 98 | Ok(Box::new(with_status(error, StatusCode::INTERNAL_SERVER_ERROR))) 99 | } 100 | } 101 | } 102 | 103 | 104 | 105 | pub async fn put_field_handler(dbname: String, x: u32, y: u32, fi: u32, dbs: DBMAP, json: Value) -> Result { 106 | if !dbs.lock().await.contains_key(dbname.as_str()) { 107 | return Ok(with_status("Unknown db".to_string(), StatusCode::NOT_FOUND)); 108 | } 109 | if let Value::Object(ref _obj) = json { 110 | let db = &dbs.lock().await[dbname.as_str()]; 111 | let value = from_json(&json).unwrap(); //todo: err 112 | match db.set_field(x, y, fi, value).await { 113 | DBResult::Ok(_) => Ok(with_status("Ok".to_string(), StatusCode::OK)), 114 | DBResult::StillLoading(progress) => { 115 | Ok( 116 | with_status(format!("Still loading model ({}%)", (progress*100.0) as u32), StatusCode::PARTIAL_CONTENT) 117 | ) 118 | 119 | }, 120 | DBResult::Err(error) => { 121 | error!("ERROR {}", error); 122 | Ok(with_status(error, StatusCode::INTERNAL_SERVER_ERROR)) 123 | } 124 | } 125 | } else { 126 | Ok(with_status("Invalid json".to_string(), StatusCode::INTERNAL_SERVER_ERROR)) 127 | } 128 | 129 | } 130 | 131 | fn get_records_json(records: Vec, embed_refs: bool) -> Json { 132 | let mut jsons = vec![]; 133 | for rec in records.iter() { 134 | let mut field_jsons = vec![]; 135 | for field in &rec.fields { 136 | field_jsons.push(to_json(&field, embed_refs)) 137 | } 138 | let rec_json = json![{ 139 | "id": vec2id(rec.id), 140 | "column": rec.column, 141 | "fields": field_jsons 142 | }]; 143 | jsons.push(rec_json); 144 | } 145 | warp::reply::json(&jsons) 146 | } 147 | 148 | 149 | pub(crate) fn vec2id(vector: Vector2D) -> String { 150 | format!("{}/{}", vector.x, vector.y) 151 | } 152 | -------------------------------------------------------------------------------- /web-client/src/field_view.rs: -------------------------------------------------------------------------------- 1 | use sauron::Node; 2 | use sauron::node; 3 | use sauron::html::text; 4 | use crate::api::RecordField; 5 | use crate::Msg; 6 | use serde_json::json; 7 | use sauron::wasm_bindgen::JsCast; 8 | use gloo_timers::callback::Timeout; 9 | 10 | pub(crate) fn field_view(field: RecordField, rec_id: &str, field_idx: usize) -> Node { 11 | let ftype = field.ftype.unwrap_or("none".to_string()); 12 | node! [ 13 | 14 | { text(",\n ") } { text(format!("field_{}: ", field_idx)) } 15 | 16 | { 17 | match &field.reference { 18 | Some(ref_id) => reference_view(ref_id, rec_id, field_idx), 19 | None => match ftype.as_str() { 20 | "image" => image_view(field.value, rec_id, field_idx), 21 | "color" => color_view(field.value, rec_id, field_idx), 22 | "string" => string_view(field.value, rec_id, field_idx), 23 | "int" => int_view(field.value, rec_id, field_idx), 24 | "boolean" => boolean_view(field.value, rec_id, field_idx), 25 | "pie" => pie_view(field.value, rec_id, field_idx), 26 | "float" => float_view(field.value, rec_id, field_idx), 27 | _ => unknown_view(field.value, rec_id, field_idx) 28 | } 29 | } 30 | } 31 | 32 | 33 | ] 34 | } 35 | 36 | fn color_view(value: serde_json::Value, rec_id: &str, field_idx: usize) -> Node { 37 | let rec_id = rec_id.to_string(); 38 | let value = value.as_str().unwrap().to_string(); 39 | node! [ 40 | 45 | ] 46 | } 47 | 48 | fn event2ctx(e: &sauron::web_sys::MouseEvent) -> sauron::web_sys::CanvasRenderingContext2d { 49 | let canvas = e.target().unwrap().dyn_into::().unwrap(); 50 | canvas.get_context("2d").expect("no 2d ctx").expect("no 2d ctx 2").dyn_into::().expect("no 2d ctx 3") 51 | } 52 | 53 | fn image_view(value: serde_json::Value, rec_id: &str, field_idx: usize) -> Node { 54 | let url = value["data_url"].as_str().unwrap().to_string(); 55 | let width = value["width"].as_u64().unwrap(); 56 | let height = value["height"].as_u64().unwrap(); 57 | let len = rec_id.len(); 58 | let rec_id = rec_id.to_string(); 59 | 60 | let img = sauron::html::img( 61 | vec![ 62 | sauron::html::attributes::src(url), 63 | sauron::html::attributes::style("display", "none"), 64 | sauron::events::on("load", move |e| { 65 | let img = e.as_web().unwrap().target().unwrap().dyn_into::().unwrap(); 66 | let canvas = img.next_sibling().unwrap().dyn_into::().unwrap(); 67 | let ctx = canvas.get_context("2d").expect("no 2d ctx").expect("no 2d ctx 2").dyn_into::().expect("no 2d ctx 3"); 68 | Timeout::new(len as u32, move || { 69 | ctx.draw_image_with_html_image_element(&img, 0.0, 0.0).unwrap(); 70 | }).forget(); 71 | Msg::Noop 72 | }) 73 | ], vec![] 74 | ); 75 | 76 | node! [ 77 | { img } 78 | 119 | 120 | ] 121 | } 122 | 123 | fn int_view(value: serde_json::Value, _rec_id: &str, _field_idx: usize) -> Node { 124 | node! [ {text(value.to_string())} ] 125 | } 126 | 127 | fn pie_view(value: serde_json::Value, _rec_id: &str, _field_idx: usize) -> Node { 128 | 129 | node! [ 130 |
131 | { 132 | for (key, value) in value.as_object().unwrap() { 133 | node! [ { color_label(key) } { text(":") } { percentage(value.as_f64().unwrap()) }
] 134 | } 135 | } 136 |
137 | ] 138 | } 139 | 140 | fn string_view(value: serde_json::Value, rec_id: &str, field_idx: usize) -> Node { 141 | let rec_id = rec_id.to_string(); 142 | let value = value.as_str().unwrap().to_string(); 143 | node! [ 144 | 149 | ] 150 | } 151 | 152 | fn boolean_view(value: serde_json::Value, rec_id: &str, field_idx: usize) -> Node { 153 | let rec_id = rec_id.to_string(); 154 | let value = value.as_bool().unwrap(); 155 | node! [ 156 | 161 | ] 162 | } 163 | 164 | fn reference_view(ref_id: &str, _rec_id: &str, _field_idx: usize) -> Node { 165 | node! [ 166 | 167 | {text(ref_id)} 168 | 169 | ] 170 | } 171 | 172 | fn unknown_view(value: serde_json::Value, _rec_id: &str, _field_idx: usize) -> Node { 173 | node! [ {text(value.to_string())} ] 174 | } 175 | 176 | pub(crate) fn color_label(color: &str) -> Node { 177 | node![ 178 | 179 | { text(color) } 180 | { text("◼") } 181 | ] 182 | } 183 | 184 | pub(crate) fn percentage(value: f64) -> Node { 185 | node![ 186 | { text(format!("{:.2} %", value*100.0)) } 187 | ] 188 | } 189 | 190 | fn float_view(value: serde_json::Value, _rec_id: &str, _field_idx: usize) -> Node { 191 | percentage(value.as_f64().unwrap()) 192 | } -------------------------------------------------------------------------------- /backend/src/io/bmp_on_disk.rs: -------------------------------------------------------------------------------- 1 | use std::cell::{RefCell, RefMut}; 2 | use std::fs::File; 3 | use std::io::{Error, Read, Seek, SeekFrom, Write}; 4 | use std::ops::Div; 5 | use log::info; 6 | use std::env; 7 | 8 | use byteorder::{LittleEndian, ReadBytesExt}; 9 | use image::{ColorType, RgbImage}; 10 | use image::codecs::png::PngEncoder; 11 | 12 | use crate::image::{StorableImage, SyncResponse}; 13 | use crate::model::colors::RGB; 14 | use std::fmt::{Debug, Formatter}; 15 | 16 | #[derive(Debug, Clone)] 17 | struct BMPParams { 18 | width: u32, 19 | height: u32, 20 | data_offset: u32, 21 | data_padding: u32, //actually u8 22 | 23 | slice_step: usize, 24 | keep_in_memory_inv: usize 25 | } 26 | 27 | struct BMPSlice { 28 | y_from: u32, 29 | y_to_exclusive: u32, 30 | data: Option>, 31 | dirty: bool, 32 | 33 | bmp_params: BMPParams, 34 | loaded_nr: u32 35 | } 36 | 37 | impl BMPSlice { 38 | fn new(y_from: u32, y_to_exclusive: u32, bmp_params: BMPParams) -> Self { 39 | Self { 40 | y_from, 41 | y_to_exclusive, 42 | data: None, 43 | dirty: false, 44 | bmp_params: bmp_params.clone(), 45 | loaded_nr: 0 46 | } 47 | } 48 | 49 | fn load(&mut self, file: &mut File) { 50 | if self.is_loaded() { return; } 51 | 52 | let mut buffer = vec![0; self.capacity()]; 53 | file.seek(self.seek()).expect("Cannot seek"); 54 | file.read_exact(buffer.as_mut_slice()).expect("Cannot read"); 55 | self.data = Some(buffer.into_boxed_slice()); 56 | info!("Loaded slice {}-{} [{} bytes from {:?} {:?}]", self.y_from, self.y_to_exclusive, self.capacity(), self.seek(), self.bmp_params) 57 | } 58 | 59 | fn capacity(&self) -> usize { 60 | ((self.y_to_exclusive - self.y_from) * (self.bmp_params.width * 3 + self.bmp_params.data_padding)) as usize 61 | } 62 | 63 | fn seek(&self) -> SeekFrom { 64 | SeekFrom::Start((self.bmp_params.data_offset + self.y_from * (self.bmp_params.width * 3 + self.bmp_params.data_padding)) as u64) 65 | } 66 | 67 | fn unload(&mut self) { 68 | assert_eq!(self.dirty, false); 69 | self.data = None; 70 | info!("Unload slice {}-{}", self.y_from, self.y_to_exclusive) 71 | } 72 | 73 | fn save_if_loaded_and_dirty(&mut self, file: &mut File) { 74 | if self.data.is_some() && self.dirty { 75 | file.seek(self.seek()).unwrap(); 76 | file.write(self.data.as_ref().unwrap().as_ref()).expect("Cannot write"); 77 | file.sync_all().expect("Cannot sync"); //todo: here? 78 | self.dirty = false; 79 | info!("Saved slice {}-{}", self.y_from, self.y_to_exclusive) 80 | } 81 | } 82 | 83 | 84 | fn is_loaded(&self) -> bool { 85 | self.data.is_some() 86 | } 87 | 88 | fn get_pixel(&self, x: u32, y: u32) -> RGB { 89 | assert!(self.is_loaded()); 90 | let idx = (y - self.y_from) * (3 * self.bmp_params.width + self.bmp_params.data_padding) + x * 3; 91 | let idx = idx as usize; 92 | 93 | let data = self.data.as_ref().unwrap(); 94 | RGB::new(data[idx + 2], data[idx + 1], data[idx]) 95 | } 96 | 97 | fn set_pixel(&mut self, x: u32, y: u32, rgb: &RGB) { 98 | assert!(self.is_loaded()); 99 | let idx = (y - self.y_from) * (3 * self.bmp_params.width + self.bmp_params.data_padding) + x * 3; 100 | let idx = idx as usize; 101 | let data = self.data.as_mut().unwrap().as_mut(); 102 | //bmp is BGR 103 | data[idx] = rgb.b; 104 | data[idx + 1] = rgb.g; 105 | data[idx + 2] = rgb.r; 106 | self.dirty = true; 107 | } 108 | } 109 | 110 | pub struct BMPOnDiskImage { 111 | file: RefCell, 112 | bmp_params: BMPParams, 113 | slices: Vec>, 114 | next_loaded_nr: RefCell 115 | } 116 | 117 | impl BMPOnDiskImage { 118 | pub(crate) fn new(mut file: File) -> Self { 119 | //read header, create bmp params 120 | file.seek(SeekFrom::Start(10)).unwrap(); 121 | let data_offset = file.read_u32::().unwrap(); 122 | 123 | file.seek(SeekFrom::Current(4)).unwrap(); 124 | let width = file.read_u32::().unwrap(); 125 | let height = file.read_u32::().unwrap(); 126 | let data_padding = (((width * 3) as f32).div(4.0).ceil() * 4.0) as u32 - (width * 3); 127 | let bmp_params = BMPParams { 128 | width, 129 | height, 130 | data_offset, 131 | data_padding, 132 | slice_step: env::var("BMP_SLICE_STEP").unwrap_or("1024".to_string()).parse::().unwrap(), 133 | keep_in_memory_inv: env::var("BMP_KEEP_IN_MEM_INV").unwrap_or("2".to_string()).parse::().unwrap() 134 | 135 | }; 136 | let mut slices = vec![]; 137 | for y in (0..height).step_by(bmp_params.slice_step) { 138 | slices.push(RefCell::new(BMPSlice::new(y, (y + bmp_params.slice_step as u32).min(height), bmp_params.clone()))) 139 | } 140 | Self { 141 | file: RefCell::new(file), 142 | bmp_params: bmp_params.clone(), 143 | slices, 144 | next_loaded_nr: RefCell::new(1) 145 | } 146 | } 147 | 148 | fn load_slice_if_needed(&self, slice: &mut RefMut) { 149 | if !slice.is_loaded() { 150 | slice.load(&mut self.file.borrow_mut()); 151 | slice.loaded_nr = *self.next_loaded_nr.borrow(); 152 | *self.next_loaded_nr.borrow_mut() += 1; 153 | } 154 | } 155 | } 156 | 157 | impl StorableImage for BMPOnDiskImage { 158 | fn get_pixel(&self, x: u32, y: u32) -> RGB { 159 | let y = self.bmp_params.height - y - 1; 160 | let idx = y as usize / self.bmp_params.slice_step; 161 | let mut slice = self.slices[idx].borrow_mut(); 162 | self.load_slice_if_needed(&mut slice); 163 | return slice.get_pixel(x, y); 164 | } 165 | 166 | fn set_pixel(&mut self, x: u32, y: u32, rgb: &RGB) { 167 | let y = self.bmp_params.height - y - 1; 168 | let idx = y as usize / self.bmp_params.slice_step; 169 | let mut slice = self.slices[idx].borrow_mut(); 170 | self.load_slice_if_needed(&mut slice); 171 | slice.set_pixel(x, y, rgb); 172 | } 173 | 174 | fn width(&self) -> u32 { 175 | self.bmp_params.width 176 | } 177 | 178 | fn height(&self) -> u32 { 179 | self.bmp_params.height 180 | } 181 | 182 | fn sync(&mut self) -> Result { 183 | //todo: last modified - also check 184 | let mut file_ref = self.file.borrow_mut(); 185 | let mut loaded = 0; 186 | let count = self.slices.len(); 187 | for slice in &self.slices { 188 | let mut slice = slice.borrow_mut(); 189 | slice.save_if_loaded_and_dirty(&mut file_ref); 190 | if slice.is_loaded() { 191 | loaded += 1; 192 | } 193 | } 194 | while count > 1 && loaded >= count / self.bmp_params.keep_in_memory_inv { 195 | self.slices.iter() 196 | .filter(|s| (*s).borrow().is_loaded()) 197 | .min_by(|s1, s2| (*s1).borrow().loaded_nr.cmp(&(*s2).borrow().loaded_nr)) 198 | .map(|s| s.borrow_mut().unload()); 199 | loaded -= 1; 200 | } 201 | Ok(SyncResponse::Ok) 202 | } 203 | 204 | fn optimize(&self) { 205 | let mut loaded = 0; 206 | let count = self.slices.len(); 207 | if count <= 1 { return } 208 | for slice in &self.slices { 209 | let slice = slice.borrow(); 210 | if slice.is_loaded() { 211 | loaded += 1; 212 | } 213 | } 214 | while loaded >= count / self.bmp_params.keep_in_memory_inv { 215 | self.slices.iter() 216 | .filter(|s| (*s).borrow().is_loaded()) 217 | .min_by(|s1, s2| (*s1).borrow().loaded_nr.cmp(&(*s2).borrow().loaded_nr)) 218 | .map(|s| s.borrow_mut().unload()); 219 | loaded -= 1; 220 | } 221 | } 222 | 223 | fn get_base64(&self, x: u32, y: u32, width: u32, height: u32) -> String { 224 | //not sure how to do it better... 225 | let mut temp_image: RgbImage = image::ImageBuffer::new(width, height); 226 | for xx in x..(x+width) { 227 | for yy in y..(y+height) { 228 | temp_image.put_pixel(xx - x, yy - y, self.get_pixel(xx, yy).into()); 229 | } 230 | } 231 | let mut buf: Vec = vec![]; 232 | PngEncoder::new(&mut buf) 233 | .encode(temp_image.as_raw(), temp_image.dimensions().0, temp_image.dimensions().1, ColorType::Rgb8).unwrap(); 234 | base64::encode(&buf) 235 | } 236 | } 237 | 238 | impl Debug for BMPOnDiskImage { 239 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 240 | f.debug_struct("BMPOnDiskImage") 241 | .field("info", &self.bmp_params) 242 | .finish() 243 | } 244 | } -------------------------------------------------------------------------------- /backend/src/model/async_model_reader.rs: -------------------------------------------------------------------------------- 1 | use crate::model::model::{Model, Vector2D, Field, FieldType, Record}; 2 | use std::collections::{HashMap, HashSet}; 3 | use crate::model::datatypes::reference::REFERENCE_TYPE; 4 | use crate::image::ImageView; 5 | use std::collections::hash_map::Entry; 6 | use crate::model::datatypes::DEFAULT_TYPE; 7 | use tokio::sync::mpsc::{UnboundedSender}; 8 | use crate::io::image_io::load_image; 9 | use crate::db::DBMessage; 10 | use std::sync::{Arc, Mutex}; 11 | use log::info; 12 | use std::time::SystemTime; 13 | 14 | #[derive(Clone, Copy, Debug)] 15 | struct Block { 16 | x1: u32, 17 | y1: u32, 18 | x2: u32, 19 | y2: u32, 20 | block_id: usize, 21 | } 22 | 23 | impl Block { 24 | fn contains(&self, x: u32, y: u32) -> bool { 25 | x >= self.x1 && y >= self.y1 && x <= self.x2 && y <= self.y2 26 | } 27 | } 28 | 29 | // 1024x1024 pixels blocks 30 | struct BlocksMap { 31 | map: HashMap<(u32, u32), Vec>, 32 | } 33 | 34 | impl BlocksMap { 35 | fn new() -> Self { 36 | Self { map: HashMap::new() } 37 | } 38 | 39 | fn add(&mut self, block: Block) { 40 | let x1 = block.x1 / 1024; 41 | let x2 = block.x2 / 1024; 42 | let y1 = block.y1 / 1024; 43 | let y2 = block.y2 / 1024; 44 | for x in x1..=x2 { 45 | for y in y1..=y2 { 46 | match self.map.entry((x, y)) { 47 | Entry::Occupied(mut vector) => { 48 | vector.get_mut().push(block.clone()); 49 | } 50 | Entry::Vacant(entry) => { 51 | entry.insert(vec![block.clone()]); 52 | } 53 | }; 54 | } 55 | } 56 | } 57 | 58 | fn get_block(&self, x: u32, y: u32) -> Option<&Block> { 59 | match self.map.get(&(x / 1024, y / 1024)) { 60 | Some(vec) => vec.iter().find(|b| b.contains(x, y)), 61 | None => None 62 | } 63 | } 64 | 65 | fn get_blocks(&self) -> Vec<&Block> { 66 | let mut v: Vec<&Block> = self.map.values() 67 | .flatten() 68 | .collect(); 69 | 70 | v.sort_by(|b1, b2| b1.block_id.cmp(&b2.block_id)); 71 | v 72 | } 73 | } 74 | 75 | 76 | pub fn do_load_async(path: &str, tx: UnboundedSender, progress: Arc>) { 77 | let path = path.to_string(); 78 | tokio::spawn(async move { 79 | let mut image = load_image(path.as_str()); 80 | let mut model = Model::new(); 81 | let image_view = ImageView::from(&mut image); 82 | load_model_into(&mut model, image_view, |p| { 83 | let mut float = progress.lock().unwrap(); 84 | *float = p; 85 | }); 86 | tx.send(DBMessage::SetModel { model, image }).unwrap(); 87 | }); 88 | } 89 | 90 | pub fn load_model_into(model: &mut Model, image: ImageView<'_>, on_progress: impl Fn(f32)) { 91 | 92 | let start_time = SystemTime::now(); 93 | let mut next_block_id = 0; 94 | let mut connection_map: HashMap = HashMap::new(); 95 | let mut fields: Vec = vec![]; 96 | let mut blocks_map = BlocksMap::new(); 97 | 98 | let is_blank = |x, y| { 99 | (x >= image.width || y >= image.height) || image.get_pixel(x, y).is_blank() 100 | }; 101 | 102 | let is_meta = |x, y| { 103 | (x < image.width && y < image.height) && image.get_pixel(x, y).is_meta() 104 | }; 105 | 106 | let if_not_blank = |x, y, value| { 107 | if is_blank(x, y) { 0 } else { value } 108 | }; 109 | 110 | let default_type = 111 | if_not_blank(0, 0, 0b100_000_000) 112 | | if_not_blank(1, 0, 0b010_000_000) 113 | | if_not_blank(2, 0, 0b001_000_000) 114 | | if_not_blank(0, 1, 0b000_100_000) 115 | | if_not_blank(1, 1, 0b000_010_000) 116 | | if_not_blank(2, 1, 0b000_001_000) 117 | | if_not_blank(0, 2, 0b000_000_100) 118 | | if_not_blank(1, 2, 0b000_000_010) 119 | | if_not_blank(2, 2, 0b000_000_001); 120 | 121 | // find blocks 122 | for y in 1..image.height { 123 | for x in 1..image.width { 124 | if is_meta(x, y) && is_blank(x, y - 1) && is_blank(x - 1, y - 1) && 125 | is_meta(x + 1, y) && is_meta(x, y + 1) && is_blank(x + 1, y - 1) && 126 | is_blank(x - 1, y) && is_blank(x - 1, y + 1) && 127 | blocks_map.get_block(x, y).is_none() { 128 | let top_left = Vector2D { x, y }; 129 | 130 | let mut right_bottom = top_left.clone(); 131 | while is_meta(right_bottom.x + 1, top_left.y) { 132 | right_bottom.x += 1 133 | } 134 | while is_meta(top_left.x, right_bottom.y + 1) { 135 | right_bottom.y += 1 136 | } 137 | 138 | let mut data_top_left = top_left.clone(); 139 | while is_meta(data_top_left.x, data_top_left.y) && data_top_left.x < right_bottom.x && data_top_left.y < right_bottom.y { 140 | data_top_left.x += 1; 141 | data_top_left.y += 1; 142 | } 143 | let mut data_right_bottom = right_bottom.clone(); 144 | while is_meta(data_right_bottom.x, data_right_bottom.y) && data_right_bottom.x > data_top_left.x && data_right_bottom.y > data_top_left.y { 145 | data_right_bottom.x -= 1; 146 | data_right_bottom.y -= 1; 147 | } 148 | assert!(data_right_bottom.x >= data_top_left.x, "failed: {} > {}", data_right_bottom.x, data_top_left.x); 149 | assert!(data_right_bottom.y >= data_top_left.y); 150 | 151 | let type_start_point = Vector2D { 152 | x: right_bottom.x - 2, 153 | y: top_left.y - 3, 154 | }; 155 | let mut ftype = 156 | if_not_blank(type_start_point.x + 0, type_start_point.y + 0, 0b100_000_000) 157 | | if_not_blank(type_start_point.x + 1, type_start_point.y + 0, 0b010_000_000) 158 | | if_not_blank(type_start_point.x + 2, type_start_point.y + 0, 0b001_000_000) 159 | | if_not_blank(type_start_point.x + 0, type_start_point.y + 1, 0b000_100_000) 160 | | if_not_blank(type_start_point.x + 1, type_start_point.y + 1, 0b000_010_000) 161 | | if_not_blank(type_start_point.x + 2, type_start_point.y + 1, 0b000_001_000) 162 | | if_not_blank(type_start_point.x + 0, type_start_point.y + 2, 0b000_000_100) 163 | | if_not_blank(type_start_point.x + 1, type_start_point.y + 2, 0b000_000_010) 164 | | if_not_blank(type_start_point.x + 2, type_start_point.y + 2, 0b000_000_001); 165 | 166 | if ftype == DEFAULT_TYPE.0 { 167 | ftype = default_type 168 | } 169 | 170 | let column_pix = image.get_pixel(top_left.x, 0); 171 | fields.push(Record { 172 | position: top_left, 173 | column: column_pix.to_hex_color(), 174 | rb_position: right_bottom, 175 | fields: vec![Field { 176 | field_type: FieldType(ftype), 177 | data_start: data_top_left, 178 | data_end: data_right_bottom, 179 | type_start: type_start_point, 180 | ref_to_record: None, 181 | }], 182 | }); 183 | blocks_map.add(Block { 184 | block_id: next_block_id, 185 | x1: top_left.x, 186 | y1: top_left.y, 187 | x2: right_bottom.x, 188 | y2: right_bottom.y, 189 | }); 190 | 191 | next_block_id += 1; 192 | //println!("found block {:?}", top_left); 193 | } 194 | } 195 | image.optimize(); 196 | on_progress(0.33 * (y as f32) / (image.height as f32)); 197 | } 198 | 199 | // find connections 200 | //debug!("find connections"); 201 | let mut points_investigated: HashSet = HashSet::new(); 202 | let blocks_count = blocks_map.get_blocks().len(); 203 | let mut block_idx = 0; 204 | for block in blocks_map.get_blocks() { 205 | //ok, here we go. start flood fill 206 | let mut points_to_investigate: Vec = vec![]; 207 | 208 | const NOT_CONNECTED: usize = usize::max_value(); 209 | let connect_from_id = block.block_id; 210 | let mut connect_to_id = NOT_CONNECTED; 211 | for x in block.x1 - 1..=block.x2 + 1 { 212 | points_to_investigate.push(Vector2D::new(x, block.y1 - 1)); 213 | points_to_investigate.push(Vector2D::new(x, block.y2 + 1)); 214 | } 215 | for y in block.y1..=block.y2 { 216 | points_to_investigate.push(Vector2D::new(block.x1 - 1, y)); 217 | points_to_investigate.push(Vector2D::new(block.x2 + 1, y)); 218 | } 219 | //println!("Check for {} {} {}", block.block_id, block.x1, block.y1); 220 | 221 | 'loop1: while !points_to_investigate.is_empty() { 222 | let p = points_to_investigate.pop().unwrap(); 223 | if points_investigated.contains(&p) { continue; } 224 | points_investigated.insert(p); 225 | if !is_meta(p.x, p.y) { continue; } 226 | if block.contains(p.x, p.y) { continue; } 227 | //println!("check {:?}", p); 228 | match blocks_map.get_block(p.x, p.y) { 229 | Some(block) => { 230 | if block.block_id < connect_from_id { 231 | continue 'loop1; 232 | } else if connect_from_id != block.block_id { 233 | connect_to_id = block.block_id; 234 | break 'loop1; 235 | } else if connect_from_id != NOT_CONNECTED && connect_to_id != NOT_CONNECTED && block.block_id != connect_to_id && block.block_id != connect_from_id { 236 | info!("WARNING: we have {} -> {}, but also found {}", connect_from_id, connect_to_id, block.block_id); 237 | } 238 | } 239 | None => { 240 | points_to_investigate.push(Vector2D { x: p.x + 1, y: p.y }); 241 | points_to_investigate.push(Vector2D { x: p.x - 1, y: p.y }); 242 | points_to_investigate.push(Vector2D { x: p.x, y: p.y + 1 }); 243 | points_to_investigate.push(Vector2D { x: p.x, y: p.y - 1 }); 244 | } 245 | } 246 | } 247 | if connect_to_id != NOT_CONNECTED && connect_from_id != NOT_CONNECTED { 248 | assert!(connect_to_id > connect_from_id); 249 | connection_map.insert(connect_to_id, connect_from_id); 250 | } 251 | image.optimize(); 252 | block_idx = block_idx + 1; 253 | on_progress(0.33 + 0.33 * (block_idx as f32) / (blocks_count as f32)); 254 | } 255 | 256 | //println!("references"); 257 | // process references 258 | for idx in 0..fields.len() { 259 | let rec = &fields[idx]; 260 | let field = &rec.fields[0]; 261 | if field.field_type != REFERENCE_TYPE { continue; } 262 | let start_point = Vector2D { x: field.type_start.x + 1, y: field.type_start.y + 1 }; 263 | let color = image.get_pixel(start_point.x, start_point.y); 264 | // println!(" reference from {:?} color={:?} ts={:?}", rec.position, color, field.type_start); 265 | let mut points_to_process = vec![start_point]; 266 | let mut points_investigated = HashSet::new(); 267 | 268 | let from_field = rec.position; 269 | 270 | while !points_to_process.is_empty() { 271 | let p = points_to_process.pop().unwrap(); 272 | if points_investigated.contains(&p) { continue; } 273 | points_investigated.insert(p); 274 | match blocks_map.get_block(p.x, p.y) { 275 | Some(block) => { 276 | // println!("p {:?} {:?}", p, block); 277 | let found_rec = fields[block.block_id].clone(); 278 | if found_rec.position != from_field { 279 | let field = &mut fields[idx].fields[0]; 280 | let found_field = found_rec.fields[0].clone(); 281 | // println!("found reference from {:?} to {:?}", from_field, found_field.data_start); 282 | field.field_type = found_field.field_type; 283 | field.data_start = found_field.data_start; 284 | field.data_end = found_field.data_end; 285 | field.ref_to_record = Some(found_rec.position); 286 | break; 287 | } 288 | } 289 | None => { 290 | if image.get_pixel(p.x, p.y) == color { 291 | for dx in -4..4 as i32 { 292 | for dy in -4..4 as i32 { 293 | points_to_process.push(Vector2D { x: (p.x as i32 + dx) as u32, y: (p.y as i32 + dy) as u32 }); 294 | } 295 | } 296 | } 297 | } 298 | } 299 | } 300 | image.optimize(); 301 | on_progress(0.67 + 0.16 * (idx as f32) / (fields.len() as f32)); 302 | } 303 | 304 | 305 | // and now all together 306 | 307 | //println!("together"); 308 | let fields_count = fields.len(); 309 | for idx in (0..fields_count).rev() { 310 | let mut record = fields.remove(idx as usize); 311 | //println!(" idx = {} fields = {} conn_from = {:?}", idx, record.fields.len(), connection_map.get(&idx) ); 312 | match connection_map.get(&idx) { 313 | None => model.insert_record(0, &record), 314 | Some(from_idx) => { 315 | let mut target_record = &mut fields[*from_idx]; 316 | target_record.rb_position.x = target_record.rb_position.x.max(record.rb_position.x); 317 | target_record.rb_position.y = target_record.rb_position.y.max(record.rb_position.y); 318 | target_record.fields.append(&mut record.fields) 319 | } 320 | } 321 | on_progress(0.84 + 0.16 * ((fields_count - idx) as f32) / (fields_count as f32)); 322 | } 323 | 324 | model.loading_time = start_time.elapsed().unwrap(); 325 | } -------------------------------------------------------------------------------- /backend/src/db.rs: -------------------------------------------------------------------------------- 1 | use crate::image::{ImageView, BoxedStorableImage, SyncResponse}; 2 | use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; 3 | use tokio::sync::oneshot; 4 | use crate::model::model::{DataValue, Model, Record, DataError, Vector2D}; 5 | use crate::model::async_model_reader::{do_load_async, load_model_into}; 6 | use crate::model::datatypes::DataTypes; 7 | use std::fmt::{Debug, Formatter}; 8 | use std::sync::{Mutex, Arc}; 9 | use log::*; 10 | 11 | 12 | #[derive(Debug)] 13 | pub struct DataRecord { 14 | pub id: Vector2D, 15 | pub column: String, 16 | pub fields: Vec, 17 | } 18 | 19 | #[derive(Debug)] 20 | pub struct DataFieldValue { 21 | pub value: DataValue, 22 | pub reference: Option, 23 | } 24 | 25 | 26 | #[derive(Debug, Clone)] 27 | pub struct DBQuery { 28 | offset: Option, 29 | limit: Option, 30 | column: Option, 31 | ids: Option> 32 | } 33 | 34 | impl DBQuery { 35 | pub fn new() -> Self { 36 | Self { offset: None, limit: None, column: None, ids: None } 37 | } 38 | 39 | pub fn offset(&mut self, offset: u32) -> &mut Self { 40 | self.offset = Some(offset); 41 | self 42 | } 43 | 44 | pub fn limit(&mut self, limit: u32) -> &mut Self { 45 | self.limit = Some(limit); 46 | self 47 | } 48 | 49 | pub fn column(&mut self, column: String) -> &mut Self { 50 | self.column = Some(column); 51 | self 52 | } 53 | 54 | pub fn ids(&mut self, ids: Vec) -> &mut Self { 55 | self.ids = Some(ids); 56 | self 57 | } 58 | 59 | 60 | pub fn build(&self) -> Self { 61 | Self { 62 | offset: self.offset, 63 | limit: self.limit, 64 | column: self.column.clone(), 65 | ids: self.ids.clone() 66 | } 67 | } 68 | } 69 | 70 | pub enum DBMessage { 71 | GetModel { tx: oneshot::Sender> }, 72 | GetRecords { query: DBQuery, tx: oneshot::Sender>> }, 73 | SetField { x: u32, y: u32, fi: u32, value: DataValue, tx: oneshot::Sender> }, 74 | CloneRecord { x: u32, y: u32, tx: oneshot::Sender> }, 75 | Sync, 76 | 77 | // like "private" ? 78 | SetModel { model: Model, image: BoxedStorableImage }, 79 | 80 | Shutdown 81 | } 82 | 83 | impl Debug for DBMessage { 84 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 85 | match self { 86 | DBMessage::GetModel { .. } => f.debug_struct("DBMessage::GetModel").finish(), 87 | DBMessage::GetRecords { query, .. } => f.debug_struct("DBMessage::GetRecords").field("query", query).finish(), 88 | DBMessage::CloneRecord { x, y, .. } => f.debug_struct("DBMessage::CloneRecord").field("x", x).field("y", y).finish(), 89 | DBMessage::SetField { x, y, fi, value, .. } => f.debug_struct("DBMessage::SetField").field("x", x).field("y", y).field("field_index", fi).field("value", value).finish(), 90 | DBMessage::Sync => f.debug_struct("DBMessage::Sync").finish(), 91 | DBMessage::SetModel { .. } => f.debug_struct("DBMessage::SetModel").finish(), 92 | DBMessage::Shutdown => f.debug_struct("DBMessage::Shutdown").finish(), 93 | } 94 | } 95 | } 96 | 97 | #[derive(Debug)] 98 | pub enum DBResult { 99 | Ok(T), 100 | StillLoading(f32), 101 | Err(String), 102 | } 103 | 104 | impl DBResult where T: Debug { 105 | pub fn unwrap(self) -> T { 106 | match self { 107 | DBResult::Ok(value) => value, 108 | _ => panic!("Unexpected {:?} instead of value", self) 109 | } 110 | } 111 | } 112 | 113 | impl> From> for DBResult { 114 | fn from(r: Result) -> Self { 115 | match r { 116 | Ok(value) => DBResult::Ok(value), 117 | Err(error) => DBResult::Err(error.into()) 118 | } 119 | } 120 | } 121 | 122 | 123 | struct DB { 124 | path: String, 125 | image: Option, 126 | model: Option, 127 | data_types: DataTypes, 128 | 129 | model_loading_progress: Arc>, 130 | } 131 | 132 | impl DB { 133 | fn new(path: S) -> Self where S: Into { 134 | Self { 135 | path: path.into(), 136 | image: None, 137 | model: None, 138 | data_types: DataTypes::new(), 139 | model_loading_progress: Arc::new(Mutex::new(0.0)), 140 | } 141 | } 142 | 143 | async fn handle(&mut self, message: DBMessage) -> () { 144 | //let message_str = format!("{:?}", message); 145 | //println!("DB[{}]: start processing {}", self.path, message_str); 146 | match message { 147 | DBMessage::Shutdown => {}, 148 | DBMessage::SetModel { model, image } => { 149 | self.model = Some(model); 150 | self.image = Some(image); 151 | *self.model_loading_progress.lock().unwrap() = 1.0; 152 | } 153 | DBMessage::CloneRecord { x, y, tx } => { 154 | let data_types = &self.data_types; 155 | match &mut self.model { 156 | Some(model) => { 157 | let mut image = self.image.as_mut().unwrap(); 158 | let result: Result = model.get_by_id(x, y) 159 | .map_or(Result::Err(DataError::NotFound), |r| Result::Ok(r.clone())) 160 | .and_then(|rec| { 161 | 162 | let mut x = rec.position.x; 163 | 164 | let mut y = model.records.iter() 165 | .filter(|r| r.column == rec.column && r.position.x == x) 166 | .map(|r| r.rb_position.y) 167 | .max().unwrap() + 10; 168 | 169 | while y + rec.rb_position.y - rec.position.y > image.height() { 170 | x = x + rec.rb_position.x - rec.position.x + 10; 171 | y = model.records.iter() 172 | .filter(|r| r.column == rec.column && r.position.x == x) 173 | .map(|r| r.rb_position.y) 174 | .max().unwrap_or(rec.position.y) + 10; 175 | } 176 | 177 | let x_shift = x - rec.position.x; 178 | let y_shift = y - rec.position.y; 179 | 180 | for xx in rec.position.x..=rec.rb_position.x { 181 | for yy in rec.position.y..=rec.rb_position.y { 182 | //todo: not optimal 183 | image.set_pixel( 184 | xx + x_shift, 185 | yy + y_shift, 186 | &image.get_pixel(xx, yy), 187 | ) 188 | } 189 | } 190 | 191 | let mut new_record = rec.clone(); 192 | let shift_vector = Vector2D::new(x_shift, y_shift); 193 | new_record.position += shift_vector; 194 | new_record.rb_position += shift_vector; 195 | for field in &mut new_record.fields { 196 | field.type_start += shift_vector; 197 | field.data_start += shift_vector; 198 | field.data_end += shift_vector; 199 | } 200 | model.add_record(&new_record); 201 | 202 | to_data_record(data_types, &new_record, &mut image) 203 | }); 204 | tx.send(result.into()).unwrap(); 205 | } 206 | None => { 207 | tx.send(DBResult::StillLoading(*self.model_loading_progress.lock().unwrap())).unwrap(); 208 | } 209 | } 210 | } 211 | DBMessage::GetRecords { query, tx } => { 212 | match &self.model { 213 | Some(model) => { 214 | let mut image = self.image.as_mut().unwrap(); 215 | let data_types = &self.data_types; 216 | let records_to_return: Vec<&Record> = match query.ids { 217 | Some(ids) => ids.iter() 218 | .map(|id| model.get_by_id(id.x, id.y)) 219 | .filter_map(|o| o) 220 | .collect(), 221 | None => model.records 222 | .iter() 223 | .filter(|r| match &query.column { 224 | Some(c) => r.column.eq(c), 225 | None => true 226 | }) 227 | .skip(query.offset.unwrap_or(0) as usize) 228 | .take(query.limit.unwrap_or(model.records.len() as u32) as usize) 229 | .collect() 230 | 231 | }; 232 | 233 | let records_to_return: Result, DataError> = records_to_return.iter() 234 | .map(|r| to_data_record(data_types, r, &mut image)) 235 | .collect(); 236 | match records_to_return { 237 | Ok(records) => tx.send(DBResult::Ok(records)).unwrap(), 238 | Err(error) => tx.send(DBResult::Err(error.into())).unwrap() 239 | } 240 | } 241 | None => { 242 | tx.send(DBResult::StillLoading(*self.model_loading_progress.lock().unwrap())).unwrap(); 243 | } 244 | } 245 | } 246 | DBMessage::GetModel { tx } => { 247 | match &self.model { 248 | Some(model) => { 249 | tx.send(DBResult::Ok(model.clone())).unwrap(); 250 | } 251 | None => { 252 | tx.send(DBResult::StillLoading(*self.model_loading_progress.lock().unwrap())).unwrap(); 253 | } 254 | } 255 | } 256 | DBMessage::SetField { x, y, fi, value, tx } => { 257 | let image = self.image.as_mut().unwrap(); 258 | let model = self.model.as_ref(); 259 | match model 260 | .and_then(|model| model.get_by_id(x, y)) 261 | .and_then(|rec| rec.fields.get(fi as usize)) { 262 | Some(field) => { 263 | let mut view = ImageView::new(image, field.data_start, field.data_end); 264 | tx.send(self.data_types.write(&mut view, &field, value).into()).unwrap(); 265 | } 266 | None => { 267 | tx.send(DBResult::Err("Not found".to_string())).unwrap() 268 | } 269 | } 270 | } 271 | DBMessage::Sync => { 272 | match self.image.as_mut() { 273 | None => {} 274 | Some(image) => { 275 | if let SyncResponse::Reloaded = image.sync().unwrap() { 276 | info!("[{}] Reload model", self.path); 277 | let mut model = Model::new(); 278 | load_model_into(&mut model, ImageView::from(image), |_f| { } ); 279 | self.model = Some(model); 280 | info!("[{}] Reloaded.", self.path); 281 | 282 | } 283 | } 284 | } 285 | //info!("Sync completed: {:?}", result) 286 | } 287 | }; 288 | //println!("DB[{}]: processed {}", self.path, message_str); 289 | } 290 | } 291 | 292 | fn to_data_record(data_types: &DataTypes, rec: &Record, image: &mut BoxedStorableImage) -> Result { 293 | let mut fields = vec![]; 294 | 295 | for field in &rec.fields { 296 | let view = ImageView::new(image, field.data_start, field.data_end); 297 | fields.push(DataFieldValue { 298 | value: data_types.read(&view, field)?, 299 | reference: field.ref_to_record, 300 | }) 301 | } 302 | 303 | Ok(DataRecord { 304 | id: rec.position, 305 | column: rec.column.clone(), 306 | fields, 307 | }) 308 | } 309 | 310 | 311 | #[derive(Clone)] 312 | pub struct DBHandle { 313 | tx: UnboundedSender, 314 | } 315 | 316 | impl DBHandle { 317 | pub fn run_in_background(path: &str) -> DBHandle { 318 | let mut db = DB::new(path); 319 | let (tx, mut rx) = unbounded_channel(); 320 | let path = path.to_string(); 321 | let async_tx = tx.clone(); 322 | tokio::spawn(async move { 323 | do_load_async(path.as_str(), async_tx, db.model_loading_progress.clone()); 324 | while let Some(message) = rx.recv().await { 325 | if let DBMessage::Shutdown = message { 326 | break; 327 | } 328 | db.handle(message).await 329 | } 330 | }); 331 | DBHandle { tx } 332 | } 333 | 334 | pub async fn get_records(&self, query: DBQuery) -> DBResult> { 335 | let (tx, rx) = oneshot::channel(); 336 | self.tx.send(DBMessage::GetRecords { query, tx }).unwrap(); 337 | rx.await.unwrap() 338 | } 339 | 340 | pub async fn get_model(&self) -> DBResult { 341 | let (tx, rx) = oneshot::channel(); 342 | self.tx.send(DBMessage::GetModel { tx }).unwrap(); 343 | rx.await.unwrap() 344 | } 345 | 346 | pub async fn set_field(&self, x: u32, y: u32, fi: u32, value: DataValue) -> DBResult<()> { 347 | let (tx, rx) = oneshot::channel(); 348 | self.tx.send(DBMessage::SetField { x, y, fi, value, tx}).unwrap(); 349 | rx.await.unwrap() 350 | } 351 | 352 | pub async fn clone_record(&self, x: u32, y: u32) -> DBResult { 353 | let (tx, rx) = oneshot::channel(); 354 | self.tx.send(DBMessage::CloneRecord { x, y, tx }).unwrap(); 355 | rx.await.unwrap() 356 | } 357 | 358 | pub async fn sync(&self) { 359 | self.tx.send(DBMessage::Sync).unwrap(); 360 | } 361 | 362 | pub fn shutdown(&self) { 363 | self.tx.send(DBMessage::Shutdown).unwrap(); 364 | } 365 | 366 | } 367 | 368 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ab_glyph_rasterizer" 7 | version = "0.1.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d9fe5e32de01730eb1f6b7f5b51c17e03e2325bf40a74f754f04f130043affff" 10 | 11 | [[package]] 12 | name = "adler" 13 | version = "1.0.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 16 | 17 | [[package]] 18 | name = "adler32" 19 | version = "1.2.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" 22 | 23 | [[package]] 24 | name = "atty" 25 | version = "0.2.14" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 28 | dependencies = [ 29 | "hermit-abi", 30 | "libc", 31 | "winapi", 32 | ] 33 | 34 | [[package]] 35 | name = "autocfg" 36 | version = "1.0.1" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 39 | 40 | [[package]] 41 | name = "badbee-backend" 42 | version = "0.1.0" 43 | dependencies = [ 44 | "base64", 45 | "byteorder", 46 | "image", 47 | "imageproc", 48 | "log", 49 | "stderrlog", 50 | "tokio", 51 | ] 52 | 53 | [[package]] 54 | name = "base64" 55 | version = "0.13.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 58 | 59 | [[package]] 60 | name = "bitflags" 61 | version = "1.3.2" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 64 | 65 | [[package]] 66 | name = "block-buffer" 67 | version = "0.9.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" 70 | dependencies = [ 71 | "generic-array", 72 | ] 73 | 74 | [[package]] 75 | name = "buf_redux" 76 | version = "0.8.4" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" 79 | dependencies = [ 80 | "memchr", 81 | "safemem", 82 | ] 83 | 84 | [[package]] 85 | name = "bytemuck" 86 | version = "1.7.2" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b" 89 | 90 | [[package]] 91 | name = "byteorder" 92 | version = "1.4.3" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 95 | 96 | [[package]] 97 | name = "bytes" 98 | version = "1.1.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" 101 | 102 | [[package]] 103 | name = "cfg-if" 104 | version = "1.0.0" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 107 | 108 | [[package]] 109 | name = "chrono" 110 | version = "0.4.19" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 113 | dependencies = [ 114 | "libc", 115 | "num-integer", 116 | "num-traits", 117 | "time", 118 | "winapi", 119 | ] 120 | 121 | [[package]] 122 | name = "color_quant" 123 | version = "1.1.0" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 126 | 127 | [[package]] 128 | name = "conv" 129 | version = "0.3.3" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299" 132 | dependencies = [ 133 | "custom_derive", 134 | ] 135 | 136 | [[package]] 137 | name = "cpufeatures" 138 | version = "0.2.1" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" 141 | dependencies = [ 142 | "libc", 143 | ] 144 | 145 | [[package]] 146 | name = "crc32fast" 147 | version = "1.2.1" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" 150 | dependencies = [ 151 | "cfg-if", 152 | ] 153 | 154 | [[package]] 155 | name = "crossbeam-channel" 156 | version = "0.5.1" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" 159 | dependencies = [ 160 | "cfg-if", 161 | "crossbeam-utils", 162 | ] 163 | 164 | [[package]] 165 | name = "crossbeam-deque" 166 | version = "0.8.1" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" 169 | dependencies = [ 170 | "cfg-if", 171 | "crossbeam-epoch", 172 | "crossbeam-utils", 173 | ] 174 | 175 | [[package]] 176 | name = "crossbeam-epoch" 177 | version = "0.9.5" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" 180 | dependencies = [ 181 | "cfg-if", 182 | "crossbeam-utils", 183 | "lazy_static", 184 | "memoffset", 185 | "scopeguard", 186 | ] 187 | 188 | [[package]] 189 | name = "crossbeam-utils" 190 | version = "0.8.5" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" 193 | dependencies = [ 194 | "cfg-if", 195 | "lazy_static", 196 | ] 197 | 198 | [[package]] 199 | name = "custom_derive" 200 | version = "0.1.7" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" 203 | 204 | [[package]] 205 | name = "deflate" 206 | version = "0.8.6" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" 209 | dependencies = [ 210 | "adler32", 211 | "byteorder", 212 | ] 213 | 214 | [[package]] 215 | name = "digest" 216 | version = "0.9.0" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" 219 | dependencies = [ 220 | "generic-array", 221 | ] 222 | 223 | [[package]] 224 | name = "either" 225 | version = "1.6.1" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 228 | 229 | [[package]] 230 | name = "fnv" 231 | version = "1.0.7" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 234 | 235 | [[package]] 236 | name = "form_urlencoded" 237 | version = "1.0.1" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 240 | dependencies = [ 241 | "matches", 242 | "percent-encoding", 243 | ] 244 | 245 | [[package]] 246 | name = "futures" 247 | version = "0.3.17" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" 250 | dependencies = [ 251 | "futures-channel", 252 | "futures-core", 253 | "futures-io", 254 | "futures-sink", 255 | "futures-task", 256 | "futures-util", 257 | ] 258 | 259 | [[package]] 260 | name = "futures-channel" 261 | version = "0.3.17" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" 264 | dependencies = [ 265 | "futures-core", 266 | "futures-sink", 267 | ] 268 | 269 | [[package]] 270 | name = "futures-core" 271 | version = "0.3.17" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" 274 | 275 | [[package]] 276 | name = "futures-io" 277 | version = "0.3.17" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" 280 | 281 | [[package]] 282 | name = "futures-sink" 283 | version = "0.3.17" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" 286 | 287 | [[package]] 288 | name = "futures-task" 289 | version = "0.3.17" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" 292 | 293 | [[package]] 294 | name = "futures-util" 295 | version = "0.3.17" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" 298 | dependencies = [ 299 | "autocfg", 300 | "futures-core", 301 | "futures-sink", 302 | "futures-task", 303 | "pin-project-lite", 304 | "pin-utils", 305 | "slab", 306 | ] 307 | 308 | [[package]] 309 | name = "generic-array" 310 | version = "0.14.4" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" 313 | dependencies = [ 314 | "typenum", 315 | "version_check", 316 | ] 317 | 318 | [[package]] 319 | name = "getrandom" 320 | version = "0.1.16" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 323 | dependencies = [ 324 | "cfg-if", 325 | "libc", 326 | "wasi 0.9.0+wasi-snapshot-preview1", 327 | ] 328 | 329 | [[package]] 330 | name = "getrandom" 331 | version = "0.2.3" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" 334 | dependencies = [ 335 | "cfg-if", 336 | "libc", 337 | "wasi 0.10.2+wasi-snapshot-preview1", 338 | ] 339 | 340 | [[package]] 341 | name = "gif" 342 | version = "0.11.2" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "5a668f699973d0f573d15749b7002a9ac9e1f9c6b220e7b165601334c173d8de" 345 | dependencies = [ 346 | "color_quant", 347 | "weezl", 348 | ] 349 | 350 | [[package]] 351 | name = "h2" 352 | version = "0.3.4" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "d7f3675cfef6a30c8031cf9e6493ebdc3bb3272a3fea3923c4210d1830e6a472" 355 | dependencies = [ 356 | "bytes", 357 | "fnv", 358 | "futures-core", 359 | "futures-sink", 360 | "futures-util", 361 | "http", 362 | "indexmap", 363 | "slab", 364 | "tokio", 365 | "tokio-util", 366 | "tracing", 367 | ] 368 | 369 | [[package]] 370 | name = "hashbrown" 371 | version = "0.11.2" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 374 | 375 | [[package]] 376 | name = "headers" 377 | version = "0.3.4" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "f0b7591fb62902706ae8e7aaff416b1b0fa2c0fd0878b46dc13baa3712d8a855" 380 | dependencies = [ 381 | "base64", 382 | "bitflags", 383 | "bytes", 384 | "headers-core", 385 | "http", 386 | "mime", 387 | "sha-1", 388 | "time", 389 | ] 390 | 391 | [[package]] 392 | name = "headers-core" 393 | version = "0.2.0" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" 396 | dependencies = [ 397 | "http", 398 | ] 399 | 400 | [[package]] 401 | name = "hermit-abi" 402 | version = "0.1.19" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 405 | dependencies = [ 406 | "libc", 407 | ] 408 | 409 | [[package]] 410 | name = "http" 411 | version = "0.2.4" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" 414 | dependencies = [ 415 | "bytes", 416 | "fnv", 417 | "itoa", 418 | ] 419 | 420 | [[package]] 421 | name = "http-body" 422 | version = "0.4.3" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5" 425 | dependencies = [ 426 | "bytes", 427 | "http", 428 | "pin-project-lite", 429 | ] 430 | 431 | [[package]] 432 | name = "httparse" 433 | version = "1.5.1" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" 436 | 437 | [[package]] 438 | name = "httpdate" 439 | version = "1.0.1" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" 442 | 443 | [[package]] 444 | name = "hyper" 445 | version = "0.14.13" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "15d1cfb9e4f68655fa04c01f59edb405b6074a0f7118ea881e5026e4a1cd8593" 448 | dependencies = [ 449 | "bytes", 450 | "futures-channel", 451 | "futures-core", 452 | "futures-util", 453 | "h2", 454 | "http", 455 | "http-body", 456 | "httparse", 457 | "httpdate", 458 | "itoa", 459 | "pin-project-lite", 460 | "socket2", 461 | "tokio", 462 | "tower-service", 463 | "tracing", 464 | "want", 465 | ] 466 | 467 | [[package]] 468 | name = "idna" 469 | version = "0.2.3" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 472 | dependencies = [ 473 | "matches", 474 | "unicode-bidi", 475 | "unicode-normalization", 476 | ] 477 | 478 | [[package]] 479 | name = "image" 480 | version = "0.23.14" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" 483 | dependencies = [ 484 | "bytemuck", 485 | "byteorder", 486 | "color_quant", 487 | "gif", 488 | "jpeg-decoder", 489 | "num-iter", 490 | "num-rational", 491 | "num-traits", 492 | "png", 493 | "scoped_threadpool", 494 | "tiff", 495 | ] 496 | 497 | [[package]] 498 | name = "imageproc" 499 | version = "0.22.0" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "7923654f3ce7cb6849d5dc9e544aaeab49c508a90b56c721b046e7234c74ab53" 502 | dependencies = [ 503 | "conv", 504 | "image", 505 | "itertools", 506 | "num 0.3.1", 507 | "rand 0.7.3", 508 | "rand_distr", 509 | "rayon", 510 | "rulinalg", 511 | "rusttype", 512 | ] 513 | 514 | [[package]] 515 | name = "indexmap" 516 | version = "1.7.0" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" 519 | dependencies = [ 520 | "autocfg", 521 | "hashbrown", 522 | ] 523 | 524 | [[package]] 525 | name = "input_buffer" 526 | version = "0.4.0" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "f97967975f448f1a7ddb12b0bc41069d09ed6a1c161a92687e057325db35d413" 529 | dependencies = [ 530 | "bytes", 531 | ] 532 | 533 | [[package]] 534 | name = "instant" 535 | version = "0.1.10" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" 538 | dependencies = [ 539 | "cfg-if", 540 | ] 541 | 542 | [[package]] 543 | name = "itertools" 544 | version = "0.9.0" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" 547 | dependencies = [ 548 | "either", 549 | ] 550 | 551 | [[package]] 552 | name = "itoa" 553 | version = "0.4.8" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" 556 | 557 | [[package]] 558 | name = "jpeg-decoder" 559 | version = "0.1.22" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" 562 | dependencies = [ 563 | "rayon", 564 | ] 565 | 566 | [[package]] 567 | name = "lazy_static" 568 | version = "1.4.0" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 571 | 572 | [[package]] 573 | name = "libc" 574 | version = "0.2.102" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103" 577 | 578 | [[package]] 579 | name = "lock_api" 580 | version = "0.4.5" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" 583 | dependencies = [ 584 | "scopeguard", 585 | ] 586 | 587 | [[package]] 588 | name = "log" 589 | version = "0.4.14" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 592 | dependencies = [ 593 | "cfg-if", 594 | ] 595 | 596 | [[package]] 597 | name = "maplit" 598 | version = "1.0.2" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" 601 | 602 | [[package]] 603 | name = "matches" 604 | version = "0.1.9" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" 607 | 608 | [[package]] 609 | name = "matrixmultiply" 610 | version = "0.1.15" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "dcad67dcec2d58ff56f6292582377e6921afdf3bfbd533e26fb8900ae575e002" 613 | dependencies = [ 614 | "rawpointer", 615 | ] 616 | 617 | [[package]] 618 | name = "memchr" 619 | version = "2.4.1" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 622 | 623 | [[package]] 624 | name = "memoffset" 625 | version = "0.6.4" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" 628 | dependencies = [ 629 | "autocfg", 630 | ] 631 | 632 | [[package]] 633 | name = "mime" 634 | version = "0.3.16" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 637 | 638 | [[package]] 639 | name = "mime_guess" 640 | version = "2.0.3" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" 643 | dependencies = [ 644 | "mime", 645 | "unicase", 646 | ] 647 | 648 | [[package]] 649 | name = "miniz_oxide" 650 | version = "0.3.7" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" 653 | dependencies = [ 654 | "adler32", 655 | ] 656 | 657 | [[package]] 658 | name = "miniz_oxide" 659 | version = "0.4.4" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" 662 | dependencies = [ 663 | "adler", 664 | "autocfg", 665 | ] 666 | 667 | [[package]] 668 | name = "mio" 669 | version = "0.7.13" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" 672 | dependencies = [ 673 | "libc", 674 | "log", 675 | "miow", 676 | "ntapi", 677 | "winapi", 678 | ] 679 | 680 | [[package]] 681 | name = "miow" 682 | version = "0.3.7" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" 685 | dependencies = [ 686 | "winapi", 687 | ] 688 | 689 | [[package]] 690 | name = "multipart" 691 | version = "0.17.1" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "d050aeedc89243f5347c3e237e3e13dc76fbe4ae3742a57b94dc14f69acf76d4" 694 | dependencies = [ 695 | "buf_redux", 696 | "httparse", 697 | "log", 698 | "mime", 699 | "mime_guess", 700 | "quick-error", 701 | "rand 0.7.3", 702 | "safemem", 703 | "tempfile", 704 | "twoway", 705 | ] 706 | 707 | [[package]] 708 | name = "ntapi" 709 | version = "0.3.6" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" 712 | dependencies = [ 713 | "winapi", 714 | ] 715 | 716 | [[package]] 717 | name = "num" 718 | version = "0.1.42" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" 721 | dependencies = [ 722 | "num-integer", 723 | "num-iter", 724 | "num-traits", 725 | ] 726 | 727 | [[package]] 728 | name = "num" 729 | version = "0.3.1" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "8b7a8e9be5e039e2ff869df49155f1c06bd01ade2117ec783e56ab0932b67a8f" 732 | dependencies = [ 733 | "num-bigint", 734 | "num-complex", 735 | "num-integer", 736 | "num-iter", 737 | "num-rational", 738 | "num-traits", 739 | ] 740 | 741 | [[package]] 742 | name = "num-bigint" 743 | version = "0.3.3" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" 746 | dependencies = [ 747 | "autocfg", 748 | "num-integer", 749 | "num-traits", 750 | ] 751 | 752 | [[package]] 753 | name = "num-complex" 754 | version = "0.3.1" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "747d632c0c558b87dbabbe6a82f3b4ae03720d0646ac5b7b4dae89394be5f2c5" 757 | dependencies = [ 758 | "num-traits", 759 | ] 760 | 761 | [[package]] 762 | name = "num-integer" 763 | version = "0.1.44" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" 766 | dependencies = [ 767 | "autocfg", 768 | "num-traits", 769 | ] 770 | 771 | [[package]] 772 | name = "num-iter" 773 | version = "0.1.42" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" 776 | dependencies = [ 777 | "autocfg", 778 | "num-integer", 779 | "num-traits", 780 | ] 781 | 782 | [[package]] 783 | name = "num-rational" 784 | version = "0.3.2" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" 787 | dependencies = [ 788 | "autocfg", 789 | "num-bigint", 790 | "num-integer", 791 | "num-traits", 792 | ] 793 | 794 | [[package]] 795 | name = "num-traits" 796 | version = "0.2.14" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 799 | dependencies = [ 800 | "autocfg", 801 | ] 802 | 803 | [[package]] 804 | name = "num_cpus" 805 | version = "1.13.0" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 808 | dependencies = [ 809 | "hermit-abi", 810 | "libc", 811 | ] 812 | 813 | [[package]] 814 | name = "once_cell" 815 | version = "1.8.0" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" 818 | 819 | [[package]] 820 | name = "opaque-debug" 821 | version = "0.3.0" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 824 | 825 | [[package]] 826 | name = "owned_ttf_parser" 827 | version = "0.6.0" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "9f923fb806c46266c02ab4a5b239735c144bdeda724a50ed058e5226f594cde3" 830 | dependencies = [ 831 | "ttf-parser", 832 | ] 833 | 834 | [[package]] 835 | name = "parking_lot" 836 | version = "0.11.2" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" 839 | dependencies = [ 840 | "instant", 841 | "lock_api", 842 | "parking_lot_core", 843 | ] 844 | 845 | [[package]] 846 | name = "parking_lot_core" 847 | version = "0.8.5" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" 850 | dependencies = [ 851 | "cfg-if", 852 | "instant", 853 | "libc", 854 | "redox_syscall", 855 | "smallvec", 856 | "winapi", 857 | ] 858 | 859 | [[package]] 860 | name = "percent-encoding" 861 | version = "2.1.0" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 864 | 865 | [[package]] 866 | name = "pin-project" 867 | version = "1.0.8" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08" 870 | dependencies = [ 871 | "pin-project-internal", 872 | ] 873 | 874 | [[package]] 875 | name = "pin-project-internal" 876 | version = "1.0.8" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389" 879 | dependencies = [ 880 | "proc-macro2", 881 | "quote", 882 | "syn", 883 | ] 884 | 885 | [[package]] 886 | name = "pin-project-lite" 887 | version = "0.2.7" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" 890 | 891 | [[package]] 892 | name = "pin-utils" 893 | version = "0.1.0" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 896 | 897 | [[package]] 898 | name = "png" 899 | version = "0.16.8" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" 902 | dependencies = [ 903 | "bitflags", 904 | "crc32fast", 905 | "deflate", 906 | "miniz_oxide 0.3.7", 907 | ] 908 | 909 | [[package]] 910 | name = "ppv-lite86" 911 | version = "0.2.10" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 914 | 915 | [[package]] 916 | name = "proc-macro2" 917 | version = "1.0.29" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" 920 | dependencies = [ 921 | "unicode-xid", 922 | ] 923 | 924 | [[package]] 925 | name = "quick-error" 926 | version = "1.2.3" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 929 | 930 | [[package]] 931 | name = "quote" 932 | version = "1.0.9" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 935 | dependencies = [ 936 | "proc-macro2", 937 | ] 938 | 939 | [[package]] 940 | name = "rand" 941 | version = "0.7.3" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 944 | dependencies = [ 945 | "getrandom 0.1.16", 946 | "libc", 947 | "rand_chacha 0.2.2", 948 | "rand_core 0.5.1", 949 | "rand_hc 0.2.0", 950 | ] 951 | 952 | [[package]] 953 | name = "rand" 954 | version = "0.8.4" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" 957 | dependencies = [ 958 | "libc", 959 | "rand_chacha 0.3.1", 960 | "rand_core 0.6.3", 961 | "rand_hc 0.3.1", 962 | ] 963 | 964 | [[package]] 965 | name = "rand_chacha" 966 | version = "0.2.2" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 969 | dependencies = [ 970 | "ppv-lite86", 971 | "rand_core 0.5.1", 972 | ] 973 | 974 | [[package]] 975 | name = "rand_chacha" 976 | version = "0.3.1" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 979 | dependencies = [ 980 | "ppv-lite86", 981 | "rand_core 0.6.3", 982 | ] 983 | 984 | [[package]] 985 | name = "rand_core" 986 | version = "0.5.1" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 989 | dependencies = [ 990 | "getrandom 0.1.16", 991 | ] 992 | 993 | [[package]] 994 | name = "rand_core" 995 | version = "0.6.3" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 998 | dependencies = [ 999 | "getrandom 0.2.3", 1000 | ] 1001 | 1002 | [[package]] 1003 | name = "rand_distr" 1004 | version = "0.2.2" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "96977acbdd3a6576fb1d27391900035bf3863d4a16422973a409b488cf29ffb2" 1007 | dependencies = [ 1008 | "rand 0.7.3", 1009 | ] 1010 | 1011 | [[package]] 1012 | name = "rand_hc" 1013 | version = "0.2.0" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 1016 | dependencies = [ 1017 | "rand_core 0.5.1", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "rand_hc" 1022 | version = "0.3.1" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" 1025 | dependencies = [ 1026 | "rand_core 0.6.3", 1027 | ] 1028 | 1029 | [[package]] 1030 | name = "rawpointer" 1031 | version = "0.1.0" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "ebac11a9d2e11f2af219b8b8d833b76b1ea0e054aa0e8d8e9e4cbde353bdf019" 1034 | 1035 | [[package]] 1036 | name = "rayon" 1037 | version = "1.5.1" 1038 | source = "registry+https://github.com/rust-lang/crates.io-index" 1039 | checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" 1040 | dependencies = [ 1041 | "autocfg", 1042 | "crossbeam-deque", 1043 | "either", 1044 | "rayon-core", 1045 | ] 1046 | 1047 | [[package]] 1048 | name = "rayon-core" 1049 | version = "1.9.1" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" 1052 | dependencies = [ 1053 | "crossbeam-channel", 1054 | "crossbeam-deque", 1055 | "crossbeam-utils", 1056 | "lazy_static", 1057 | "num_cpus", 1058 | ] 1059 | 1060 | [[package]] 1061 | name = "redox_syscall" 1062 | version = "0.2.10" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" 1065 | dependencies = [ 1066 | "bitflags", 1067 | ] 1068 | 1069 | [[package]] 1070 | name = "remove_dir_all" 1071 | version = "0.5.3" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 1074 | dependencies = [ 1075 | "winapi", 1076 | ] 1077 | 1078 | [[package]] 1079 | name = "rulinalg" 1080 | version = "0.4.2" 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" 1082 | checksum = "04ada202c9685e1d72a7420c578e92b358dbf807d3dfabb676a3dab9cc3bb12f" 1083 | dependencies = [ 1084 | "matrixmultiply", 1085 | "num 0.1.42", 1086 | ] 1087 | 1088 | [[package]] 1089 | name = "rusttype" 1090 | version = "0.9.2" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "dc7c727aded0be18c5b80c1640eae0ac8e396abf6fa8477d96cb37d18ee5ec59" 1093 | dependencies = [ 1094 | "ab_glyph_rasterizer", 1095 | "owned_ttf_parser", 1096 | ] 1097 | 1098 | [[package]] 1099 | name = "ryu" 1100 | version = "1.0.5" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 1103 | 1104 | [[package]] 1105 | name = "safemem" 1106 | version = "0.3.3" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" 1109 | 1110 | [[package]] 1111 | name = "scoped-tls" 1112 | version = "1.0.0" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" 1115 | 1116 | [[package]] 1117 | name = "scoped_threadpool" 1118 | version = "0.1.9" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" 1121 | 1122 | [[package]] 1123 | name = "scopeguard" 1124 | version = "1.1.0" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 1127 | 1128 | [[package]] 1129 | name = "serde" 1130 | version = "1.0.130" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" 1133 | 1134 | [[package]] 1135 | name = "serde_derive" 1136 | version = "1.0.130" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" 1139 | dependencies = [ 1140 | "proc-macro2", 1141 | "quote", 1142 | "syn", 1143 | ] 1144 | 1145 | [[package]] 1146 | name = "serde_json" 1147 | version = "1.0.68" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" 1150 | dependencies = [ 1151 | "itoa", 1152 | "ryu", 1153 | "serde", 1154 | ] 1155 | 1156 | [[package]] 1157 | name = "serde_urlencoded" 1158 | version = "0.7.0" 1159 | source = "registry+https://github.com/rust-lang/crates.io-index" 1160 | checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" 1161 | dependencies = [ 1162 | "form_urlencoded", 1163 | "itoa", 1164 | "ryu", 1165 | "serde", 1166 | ] 1167 | 1168 | [[package]] 1169 | name = "sha-1" 1170 | version = "0.9.8" 1171 | source = "registry+https://github.com/rust-lang/crates.io-index" 1172 | checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" 1173 | dependencies = [ 1174 | "block-buffer", 1175 | "cfg-if", 1176 | "cpufeatures", 1177 | "digest", 1178 | "opaque-debug", 1179 | ] 1180 | 1181 | [[package]] 1182 | name = "signal-hook-registry" 1183 | version = "1.4.0" 1184 | source = "registry+https://github.com/rust-lang/crates.io-index" 1185 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 1186 | dependencies = [ 1187 | "libc", 1188 | ] 1189 | 1190 | [[package]] 1191 | name = "slab" 1192 | version = "0.4.4" 1193 | source = "registry+https://github.com/rust-lang/crates.io-index" 1194 | checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" 1195 | 1196 | [[package]] 1197 | name = "smallvec" 1198 | version = "1.6.1" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" 1201 | 1202 | [[package]] 1203 | name = "socket2" 1204 | version = "0.4.2" 1205 | source = "registry+https://github.com/rust-lang/crates.io-index" 1206 | checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" 1207 | dependencies = [ 1208 | "libc", 1209 | "winapi", 1210 | ] 1211 | 1212 | [[package]] 1213 | name = "stderrlog" 1214 | version = "0.5.1" 1215 | source = "registry+https://github.com/rust-lang/crates.io-index" 1216 | checksum = "45a53e2eff3e94a019afa6265e8ee04cb05b9d33fe9f5078b14e4e391d155a38" 1217 | dependencies = [ 1218 | "atty", 1219 | "chrono", 1220 | "log", 1221 | "termcolor", 1222 | "thread_local", 1223 | ] 1224 | 1225 | [[package]] 1226 | name = "syn" 1227 | version = "1.0.76" 1228 | source = "registry+https://github.com/rust-lang/crates.io-index" 1229 | checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84" 1230 | dependencies = [ 1231 | "proc-macro2", 1232 | "quote", 1233 | "unicode-xid", 1234 | ] 1235 | 1236 | [[package]] 1237 | name = "tempfile" 1238 | version = "3.2.0" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" 1241 | dependencies = [ 1242 | "cfg-if", 1243 | "libc", 1244 | "rand 0.8.4", 1245 | "redox_syscall", 1246 | "remove_dir_all", 1247 | "winapi", 1248 | ] 1249 | 1250 | [[package]] 1251 | name = "termcolor" 1252 | version = "1.1.2" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 1255 | dependencies = [ 1256 | "winapi-util", 1257 | ] 1258 | 1259 | [[package]] 1260 | name = "thread_local" 1261 | version = "1.0.1" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 1264 | dependencies = [ 1265 | "lazy_static", 1266 | ] 1267 | 1268 | [[package]] 1269 | name = "tiff" 1270 | version = "0.6.1" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" 1273 | dependencies = [ 1274 | "jpeg-decoder", 1275 | "miniz_oxide 0.4.4", 1276 | "weezl", 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "time" 1281 | version = "0.1.43" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" 1284 | dependencies = [ 1285 | "libc", 1286 | "winapi", 1287 | ] 1288 | 1289 | [[package]] 1290 | name = "tinyvec" 1291 | version = "1.4.0" 1292 | source = "registry+https://github.com/rust-lang/crates.io-index" 1293 | checksum = "5241dd6f21443a3606b432718b166d3cedc962fd4b8bea54a8bc7f514ebda986" 1294 | dependencies = [ 1295 | "tinyvec_macros", 1296 | ] 1297 | 1298 | [[package]] 1299 | name = "tinyvec_macros" 1300 | version = "0.1.0" 1301 | source = "registry+https://github.com/rust-lang/crates.io-index" 1302 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 1303 | 1304 | [[package]] 1305 | name = "tokio" 1306 | version = "1.12.0" 1307 | source = "registry+https://github.com/rust-lang/crates.io-index" 1308 | checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc" 1309 | dependencies = [ 1310 | "autocfg", 1311 | "bytes", 1312 | "libc", 1313 | "memchr", 1314 | "mio", 1315 | "num_cpus", 1316 | "once_cell", 1317 | "parking_lot", 1318 | "pin-project-lite", 1319 | "signal-hook-registry", 1320 | "tokio-macros", 1321 | "winapi", 1322 | ] 1323 | 1324 | [[package]] 1325 | name = "tokio-macros" 1326 | version = "1.3.0" 1327 | source = "registry+https://github.com/rust-lang/crates.io-index" 1328 | checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" 1329 | dependencies = [ 1330 | "proc-macro2", 1331 | "quote", 1332 | "syn", 1333 | ] 1334 | 1335 | [[package]] 1336 | name = "tokio-stream" 1337 | version = "0.1.7" 1338 | source = "registry+https://github.com/rust-lang/crates.io-index" 1339 | checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f" 1340 | dependencies = [ 1341 | "futures-core", 1342 | "pin-project-lite", 1343 | "tokio", 1344 | ] 1345 | 1346 | [[package]] 1347 | name = "tokio-tungstenite" 1348 | version = "0.13.0" 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" 1350 | checksum = "e1a5f475f1b9d077ea1017ecbc60890fda8e54942d680ca0b1d2b47cfa2d861b" 1351 | dependencies = [ 1352 | "futures-util", 1353 | "log", 1354 | "pin-project", 1355 | "tokio", 1356 | "tungstenite", 1357 | ] 1358 | 1359 | [[package]] 1360 | name = "tokio-util" 1361 | version = "0.6.8" 1362 | source = "registry+https://github.com/rust-lang/crates.io-index" 1363 | checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd" 1364 | dependencies = [ 1365 | "bytes", 1366 | "futures-core", 1367 | "futures-sink", 1368 | "log", 1369 | "pin-project-lite", 1370 | "tokio", 1371 | ] 1372 | 1373 | [[package]] 1374 | name = "tower-service" 1375 | version = "0.3.1" 1376 | source = "registry+https://github.com/rust-lang/crates.io-index" 1377 | checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" 1378 | 1379 | [[package]] 1380 | name = "tracing" 1381 | version = "0.1.28" 1382 | source = "registry+https://github.com/rust-lang/crates.io-index" 1383 | checksum = "84f96e095c0c82419687c20ddf5cb3eadb61f4e1405923c9dc8e53a1adacbda8" 1384 | dependencies = [ 1385 | "cfg-if", 1386 | "log", 1387 | "pin-project-lite", 1388 | "tracing-core", 1389 | ] 1390 | 1391 | [[package]] 1392 | name = "tracing-core" 1393 | version = "0.1.20" 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" 1395 | checksum = "46125608c26121c81b0c6d693eab5a420e416da7e43c426d2e8f7df8da8a3acf" 1396 | dependencies = [ 1397 | "lazy_static", 1398 | ] 1399 | 1400 | [[package]] 1401 | name = "try-lock" 1402 | version = "0.2.3" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" 1405 | 1406 | [[package]] 1407 | name = "ttf-parser" 1408 | version = "0.6.2" 1409 | source = "registry+https://github.com/rust-lang/crates.io-index" 1410 | checksum = "3e5d7cd7ab3e47dda6e56542f4bbf3824c15234958c6e1bd6aaa347e93499fdc" 1411 | 1412 | [[package]] 1413 | name = "tungstenite" 1414 | version = "0.12.0" 1415 | source = "registry+https://github.com/rust-lang/crates.io-index" 1416 | checksum = "8ada8297e8d70872fa9a551d93250a9f407beb9f37ef86494eb20012a2ff7c24" 1417 | dependencies = [ 1418 | "base64", 1419 | "byteorder", 1420 | "bytes", 1421 | "http", 1422 | "httparse", 1423 | "input_buffer", 1424 | "log", 1425 | "rand 0.8.4", 1426 | "sha-1", 1427 | "url", 1428 | "utf-8", 1429 | ] 1430 | 1431 | [[package]] 1432 | name = "twoway" 1433 | version = "0.1.8" 1434 | source = "registry+https://github.com/rust-lang/crates.io-index" 1435 | checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" 1436 | dependencies = [ 1437 | "memchr", 1438 | ] 1439 | 1440 | [[package]] 1441 | name = "typenum" 1442 | version = "1.14.0" 1443 | source = "registry+https://github.com/rust-lang/crates.io-index" 1444 | checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" 1445 | 1446 | [[package]] 1447 | name = "unicase" 1448 | version = "2.6.0" 1449 | source = "registry+https://github.com/rust-lang/crates.io-index" 1450 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" 1451 | dependencies = [ 1452 | "version_check", 1453 | ] 1454 | 1455 | [[package]] 1456 | name = "unicode-bidi" 1457 | version = "0.3.6" 1458 | source = "registry+https://github.com/rust-lang/crates.io-index" 1459 | checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" 1460 | 1461 | [[package]] 1462 | name = "unicode-normalization" 1463 | version = "0.1.19" 1464 | source = "registry+https://github.com/rust-lang/crates.io-index" 1465 | checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" 1466 | dependencies = [ 1467 | "tinyvec", 1468 | ] 1469 | 1470 | [[package]] 1471 | name = "unicode-xid" 1472 | version = "0.2.2" 1473 | source = "registry+https://github.com/rust-lang/crates.io-index" 1474 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 1475 | 1476 | [[package]] 1477 | name = "url" 1478 | version = "2.2.2" 1479 | source = "registry+https://github.com/rust-lang/crates.io-index" 1480 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 1481 | dependencies = [ 1482 | "form_urlencoded", 1483 | "idna", 1484 | "matches", 1485 | "percent-encoding", 1486 | ] 1487 | 1488 | [[package]] 1489 | name = "utf-8" 1490 | version = "0.7.6" 1491 | source = "registry+https://github.com/rust-lang/crates.io-index" 1492 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 1493 | 1494 | [[package]] 1495 | name = "version_check" 1496 | version = "0.9.3" 1497 | source = "registry+https://github.com/rust-lang/crates.io-index" 1498 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 1499 | 1500 | [[package]] 1501 | name = "want" 1502 | version = "0.3.0" 1503 | source = "registry+https://github.com/rust-lang/crates.io-index" 1504 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1505 | dependencies = [ 1506 | "log", 1507 | "try-lock", 1508 | ] 1509 | 1510 | [[package]] 1511 | name = "warp" 1512 | version = "0.3.1" 1513 | source = "registry+https://github.com/rust-lang/crates.io-index" 1514 | checksum = "332d47745e9a0c38636dbd454729b147d16bd1ed08ae67b3ab281c4506771054" 1515 | dependencies = [ 1516 | "bytes", 1517 | "futures", 1518 | "headers", 1519 | "http", 1520 | "hyper", 1521 | "log", 1522 | "mime", 1523 | "mime_guess", 1524 | "multipart", 1525 | "percent-encoding", 1526 | "pin-project", 1527 | "scoped-tls", 1528 | "serde", 1529 | "serde_json", 1530 | "serde_urlencoded", 1531 | "tokio", 1532 | "tokio-stream", 1533 | "tokio-tungstenite", 1534 | "tokio-util", 1535 | "tower-service", 1536 | "tracing", 1537 | ] 1538 | 1539 | [[package]] 1540 | name = "wasi" 1541 | version = "0.9.0+wasi-snapshot-preview1" 1542 | source = "registry+https://github.com/rust-lang/crates.io-index" 1543 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 1544 | 1545 | [[package]] 1546 | name = "wasi" 1547 | version = "0.10.2+wasi-snapshot-preview1" 1548 | source = "registry+https://github.com/rust-lang/crates.io-index" 1549 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 1550 | 1551 | [[package]] 1552 | name = "web-server" 1553 | version = "0.1.0" 1554 | dependencies = [ 1555 | "badbee-backend", 1556 | "log", 1557 | "maplit", 1558 | "serde", 1559 | "serde_derive", 1560 | "serde_json", 1561 | "stderrlog", 1562 | "tokio", 1563 | "warp", 1564 | ] 1565 | 1566 | [[package]] 1567 | name = "weezl" 1568 | version = "0.1.5" 1569 | source = "registry+https://github.com/rust-lang/crates.io-index" 1570 | checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e" 1571 | 1572 | [[package]] 1573 | name = "winapi" 1574 | version = "0.3.9" 1575 | source = "registry+https://github.com/rust-lang/crates.io-index" 1576 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1577 | dependencies = [ 1578 | "winapi-i686-pc-windows-gnu", 1579 | "winapi-x86_64-pc-windows-gnu", 1580 | ] 1581 | 1582 | [[package]] 1583 | name = "winapi-i686-pc-windows-gnu" 1584 | version = "0.4.0" 1585 | source = "registry+https://github.com/rust-lang/crates.io-index" 1586 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1587 | 1588 | [[package]] 1589 | name = "winapi-util" 1590 | version = "0.1.5" 1591 | source = "registry+https://github.com/rust-lang/crates.io-index" 1592 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1593 | dependencies = [ 1594 | "winapi", 1595 | ] 1596 | 1597 | [[package]] 1598 | name = "winapi-x86_64-pc-windows-gnu" 1599 | version = "0.4.0" 1600 | source = "registry+https://github.com/rust-lang/crates.io-index" 1601 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1602 | --------------------------------------------------------------------------------