render in img
This commit is contained in:
parent
5df83c4398
commit
4ec48fc21c
@ -2,6 +2,6 @@ fn main() {
|
||||
glib_build_tools::compile_resources(
|
||||
&["src/resources"],
|
||||
"src/resources/resources.gresource.xml",
|
||||
"monitor.gresource",
|
||||
"p.gresource",
|
||||
);
|
||||
}
|
||||
BIN
src/assets/Roboto-Bold.ttf
Normal file
BIN
src/assets/Roboto-Bold.ttf
Normal file
Binary file not shown.
BIN
src/assets/Roboto-Light.ttf
Normal file
BIN
src/assets/Roboto-Light.ttf
Normal file
Binary file not shown.
BIN
src/assets/Roboto-Regular.ttf
Normal file
BIN
src/assets/Roboto-Regular.ttf
Normal file
Binary file not shown.
BIN
src/assets/amiri-regular.ttf
Normal file
BIN
src/assets/amiri-regular.ttf
Normal file
Binary file not shown.
BIN
src/assets/entypo.ttf
Normal file
BIN
src/assets/entypo.ttf
Normal file
Binary file not shown.
@ -1,21 +1,20 @@
|
||||
use super::{proj::ProjectionS, Range};
|
||||
use geo_types::{coord, Coord as GCoord, LineString};
|
||||
use proj::{Proj, ProjError};
|
||||
use std::ops::Range;
|
||||
|
||||
use super::proj::ProjectionS;
|
||||
use std::ops;
|
||||
|
||||
pub struct Mapper {
|
||||
proj: Proj,
|
||||
range: (Range<f64>, Range<f64>),
|
||||
pub range: (Range, Range),
|
||||
bounds: (f64, f64, f64, f64),
|
||||
}
|
||||
impl From<Proj> for Mapper {
|
||||
fn from(proj: Proj) -> Self {
|
||||
let default_range: (Range<f64>, Range<f64>) = (-180.0..180.0, -90.0..90.0);
|
||||
let default_range: (Range, Range) = ((-180.0..180.0).into(), (-90.0..90.0).into());
|
||||
let bounds = Self::bound(&proj, default_range.clone()).unwrap();
|
||||
Self {
|
||||
proj: proj,
|
||||
range: default_range,
|
||||
range: (default_range.0.into(), default_range.1.into()),
|
||||
bounds,
|
||||
}
|
||||
}
|
||||
@ -29,9 +28,13 @@ impl<C: ProjectionS> From<C> for Mapper {
|
||||
}
|
||||
|
||||
impl Mapper {
|
||||
pub fn new(proj: Proj, lon_range: Range<f64>, lat_range: Range<f64>) -> Self {
|
||||
let bounds = Self::bound(&proj, (lon_range.clone(), lat_range.clone())).unwrap();
|
||||
let range = (lon_range, lat_range);
|
||||
pub fn new(
|
||||
proj: Proj,
|
||||
lon_range: std::ops::Range<f64>,
|
||||
lat_range: std::ops::Range<f64>,
|
||||
) -> Self {
|
||||
let bounds = Self::bound(&proj, (lon_range.clone().into(), lat_range.clone().into())).unwrap();
|
||||
let range = (lon_range.into(), lat_range.into());
|
||||
Self {
|
||||
proj: proj,
|
||||
range,
|
||||
@ -50,22 +53,19 @@ impl Mapper {
|
||||
Ok((x, y))
|
||||
}
|
||||
|
||||
pub fn set_lon_range(&mut self, range: Range<f64>) {
|
||||
self.range.0 = range;
|
||||
pub fn set_lon_range(&mut self, range: std::ops::Range<f64>) {
|
||||
self.range.0 = range.into();
|
||||
self.bounds = Self::bound(&self.proj, self.range.clone()).unwrap();
|
||||
}
|
||||
|
||||
pub fn set_lat_range(&mut self, range: Range<f64>) {
|
||||
self.range.1 = range;
|
||||
pub fn set_lat_range(&mut self, range: std::ops::Range<f64>) {
|
||||
self.range.1 = range.into();
|
||||
self.bounds = Self::bound(&self.proj, self.range.clone()).unwrap();
|
||||
}
|
||||
|
||||
fn bound(
|
||||
proj: &Proj,
|
||||
range: (Range<f64>, Range<f64>),
|
||||
) -> Result<(f64, f64, f64, f64), ProjError> {
|
||||
let left_bottom = proj.convert((range.0.start.to_radians(), range.1.start.to_radians()))?;
|
||||
let right_top = proj.convert((range.0.end.to_radians(), range.1.end.to_radians()))?;
|
||||
fn bound(proj: &Proj, range: (Range, Range)) -> Result<(f64, f64, f64, f64), ProjError> {
|
||||
let left_bottom = proj.convert((range.0 .0.to_radians(), range.1 .0.to_radians()))?;
|
||||
let right_top = proj.convert((range.0 .1.to_radians(), range.1 .1.to_radians()))?;
|
||||
Ok((left_bottom.0, right_top.0, left_bottom.1, right_top.1))
|
||||
}
|
||||
|
||||
@ -81,7 +81,7 @@ impl Mapper {
|
||||
let delta2 = 0.5;
|
||||
let depth = 16;
|
||||
let mut res: Vec<GCoord> = Vec::new();
|
||||
res.push(l.start);
|
||||
res.push(p.start);
|
||||
self.resample_line_to(
|
||||
start_projected,
|
||||
end_projected,
|
||||
@ -94,7 +94,7 @@ impl Mapper {
|
||||
depth,
|
||||
&mut res,
|
||||
)?;
|
||||
res.push(l.end);
|
||||
res.push(p.end);
|
||||
result.extend(res);
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ pub mod proj;
|
||||
pub mod wgs84;
|
||||
|
||||
pub use mapper::Mapper;
|
||||
pub use wgs84::LatLonCoord;
|
||||
// pub use wgs84::LatLonCoord;
|
||||
|
||||
pub type ScreenCoord = (f64, f64);
|
||||
type Lat = f64;
|
||||
@ -114,6 +114,14 @@ impl<T: AsPrimitive<f64> + Num> From<(T, T)> for Range {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Num + AsPrimitive<f64>> From<std::ops::Range<T>> for Range {
|
||||
fn from(value: std::ops::Range<T>) -> Self {
|
||||
let value = (value.start.as_(), value.end.as_());
|
||||
let (_min, _max) = (value.0.min(value.1), value.0.max(value.1));
|
||||
Self(_min, _max)
|
||||
}
|
||||
}
|
||||
|
||||
// impl<T, Raw> RadarData2d<T, Raw>
|
||||
// where
|
||||
// T: Num + Clone + PartialEq + PartialOrd,
|
||||
|
||||
@ -8,10 +8,6 @@ use geo_macros::Prj;
|
||||
pub struct Mercator {
|
||||
/// The central longitude of the projection.
|
||||
pub central_lon: f64,
|
||||
/// The minimum latitude of the projection.
|
||||
pub min_latitude: f64,
|
||||
/// The maximum latitude of the projection.
|
||||
pub max_latitude: f64,
|
||||
/// The false easting of the projection.
|
||||
pub false_easting: f64,
|
||||
/// The false northing of the projection.
|
||||
@ -32,8 +28,6 @@ impl Mercator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
central_lon: 0.0,
|
||||
min_latitude: -82.0,
|
||||
max_latitude: 82.0,
|
||||
false_easting: 0.0,
|
||||
false_northing: 0.0,
|
||||
latitude_true_scale: 0.0,
|
||||
@ -61,20 +55,4 @@ impl ProjectionS for Mercator {
|
||||
_proj_string
|
||||
}
|
||||
|
||||
fn logic_range(&self, lon_range: Option<Range>, lat_range: Option<Range>) -> (Range, Range) {
|
||||
let lon_range = lon_range.unwrap_or(Range {
|
||||
0: -180f64,
|
||||
1: 180f64,
|
||||
});
|
||||
let lat_range = lat_range.unwrap_or(Range {
|
||||
0: self.min_latitude,
|
||||
1: self.max_latitude,
|
||||
});
|
||||
|
||||
let lat_range = Range {
|
||||
0: lat_range.0.max(self.min_latitude),
|
||||
1: lat_range.1.min(self.max_latitude),
|
||||
};
|
||||
(lon_range, lat_range)
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,18 +18,6 @@ pub enum Projs {
|
||||
pub trait ProjectionS {
|
||||
/// Returns a proj-string of the projection.
|
||||
fn build(&self) -> String;
|
||||
|
||||
/// Returns the logical range of the projection.
|
||||
/// In common, different projections have different logical ranges.
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `lon_range` - An optional longitude range.
|
||||
/// * `lat_range` - An optional latitude range.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A tuple containing the longitude and latitude ranges.
|
||||
fn logic_range(&self, lon_range: Option<Range>, lat_range: Option<Range>) -> (Range, Range);
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@ -38,42 +26,42 @@ pub(super) enum ProjError {
|
||||
ProjError(#[from] proj::ProjError),
|
||||
}
|
||||
|
||||
pub(super) struct PCS<T: ProjectionS> {
|
||||
pub lon_range: Range,
|
||||
pub lat_range: Range,
|
||||
pub proj_param: T,
|
||||
// pub proj_target: proj5::CoordinateBuf,
|
||||
pub transformer: Proj,
|
||||
}
|
||||
// pub(super) struct PCS<T: ProjectionS> {
|
||||
// pub lon_range: Range,
|
||||
// pub lat_range: Range,
|
||||
// pub proj_param: T,
|
||||
// // pub proj_target: proj5::CoordinateBuf,
|
||||
// pub transformer: Proj,
|
||||
// }
|
||||
|
||||
impl<T: ProjectionS> PCS<T> {
|
||||
pub(super) fn new(proj_param: T, lon_range: Option<Range>, lat_range: Option<Range>) -> Self {
|
||||
let (lon_range, lat_range) = proj_param.logic_range(lon_range, lat_range);
|
||||
Self {
|
||||
lon_range,
|
||||
lat_range,
|
||||
transformer: Proj::new(proj_param.build().as_str()).unwrap(),
|
||||
proj_param: proj_param,
|
||||
}
|
||||
}
|
||||
// impl<T: ProjectionS> PCS<T> {
|
||||
// pub(super) fn new(proj_param: T, lon_range: Option<Range>, lat_range: Option<Range>) -> Self {
|
||||
// let (lon_range, lat_range) = proj_param.logic_range(lon_range, lat_range);
|
||||
// Self {
|
||||
// lon_range,
|
||||
// lat_range,
|
||||
// transformer: Proj::new(proj_param.build().as_str()).unwrap(),
|
||||
// proj_param: proj_param,
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn bbox(&self) -> Result<(Range, Range), ProjError> {
|
||||
let _proj_transformer = &self.transformer;
|
||||
let lb = (self.lon_range.0.to_radians(), self.lat_range.0.to_radians());
|
||||
let rt = (self.lon_range.1.to_radians(), self.lat_range.1.to_radians());
|
||||
// pub fn bbox(&self) -> Result<(Range, Range), ProjError> {
|
||||
// let _proj_transformer = &self.transformer;
|
||||
// let lb = (self.lon_range.0.to_radians(), self.lat_range.0.to_radians());
|
||||
// let rt = (self.lon_range.1.to_radians(), self.lat_range.1.to_radians());
|
||||
|
||||
let bl = _proj_transformer.convert(lb)?;
|
||||
let rt = _proj_transformer.convert(rt)?;
|
||||
// let bl = _proj_transformer.convert(lb)?;
|
||||
// let rt = _proj_transformer.convert(rt)?;
|
||||
|
||||
Ok((Range::from((bl.0, rt.0)), Range::from((bl.1, rt.1))))
|
||||
}
|
||||
// Ok((Range::from((bl.0, rt.0)), Range::from((bl.1, rt.1))))
|
||||
// }
|
||||
|
||||
pub fn map(&self, lon_lat: (Lat, Lon)) -> (f64, f64) {
|
||||
let _proj_transformer = &self.transformer;
|
||||
let _lon_lat = _proj_transformer
|
||||
.convert((lon_lat.0.to_radians(), lon_lat.1.to_radians()))
|
||||
.unwrap();
|
||||
// pub fn map(&self, lon_lat: (Lat, Lon)) -> (f64, f64) {
|
||||
// let _proj_transformer = &self.transformer;
|
||||
// let _lon_lat = _proj_transformer
|
||||
// .convert((lon_lat.0.to_radians(), lon_lat.1.to_radians()))
|
||||
// .unwrap();
|
||||
|
||||
_lon_lat
|
||||
}
|
||||
}
|
||||
// _lon_lat
|
||||
// }
|
||||
// }
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use super::proj::{ProjectionS, PCS};
|
||||
// use super::proj::{ProjectionS, PCS};
|
||||
use super::Coord;
|
||||
use super::{Lat, Lon, Range};
|
||||
use proj::ProjError;
|
||||
@ -13,76 +13,76 @@ pub enum CoordError {
|
||||
},
|
||||
}
|
||||
|
||||
pub struct LatLonCoord<T: ProjectionS> {
|
||||
actual: (Range, Range),
|
||||
logical: (Range, Range),
|
||||
pcs: PCS<T>,
|
||||
}
|
||||
|
||||
impl<T: ProjectionS> LatLonCoord<T> {
|
||||
/// Creates a new `LatLonCoord` instance.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `lon` - An optional longitude range.
|
||||
/// * `lat` - An optional latitude range.
|
||||
/// * `actual` - A tuple containing the actual ranges.
|
||||
/// * `project` - A projection.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A new `LatLonCoord` instance.
|
||||
pub fn new(
|
||||
lon: Option<Range>,
|
||||
lat: Option<Range>,
|
||||
actual: ((i32, i32), (i32, i32)),
|
||||
// pub struct LatLonCoord<T: ProjectionS> {
|
||||
// actual: (Range, Range),
|
||||
project: T,
|
||||
) -> Self {
|
||||
let pcs = PCS::new(project, lon, lat);
|
||||
let _box = pcs.bbox().unwrap();
|
||||
Self {
|
||||
actual: (Range::from(actual.0), Range::from(actual.1)),
|
||||
pcs: pcs,
|
||||
logical: _box,
|
||||
}
|
||||
}
|
||||
// logical: (Range, Range),
|
||||
// pcs: PCS<T>,
|
||||
// }
|
||||
|
||||
pub fn set_actual(&mut self, actual: ((i32, i32), (i32, i32))) {
|
||||
self.actual = (Range::from(actual.0), Range::from(actual.1));
|
||||
}
|
||||
// impl<T: ProjectionS> LatLonCoord<T> {
|
||||
// /// Creates a new `LatLonCoord` instance.
|
||||
// ///
|
||||
// /// # Arguments
|
||||
// ///
|
||||
// /// * `lon` - An optional longitude range.
|
||||
// /// * `lat` - An optional latitude range.
|
||||
// /// * `actual` - A tuple containing the actual ranges.
|
||||
// /// * `project` - A projection.
|
||||
// ///
|
||||
// /// # Returns
|
||||
// ///
|
||||
// /// A new `LatLonCoord` instance.
|
||||
// pub fn new(
|
||||
// lon: Option<Range>,
|
||||
// lat: Option<Range>,
|
||||
// actual: ((i32, i32), (i32, i32)),
|
||||
// // actual: (Range, Range),
|
||||
// project: T,
|
||||
// ) -> Self {
|
||||
// let pcs = PCS::new(project, lon, lat);
|
||||
// let _box = pcs.bbox().unwrap();
|
||||
// Self {
|
||||
// actual: (Range::from(actual.0), Range::from(actual.1)),
|
||||
// pcs: pcs,
|
||||
// logical: _box,
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn lon_range(&self) -> Range {
|
||||
self.pcs.lon_range
|
||||
}
|
||||
// pub fn set_actual(&mut self, actual: ((i32, i32), (i32, i32))) {
|
||||
// self.actual = (Range::from(actual.0), Range::from(actual.1));
|
||||
// }
|
||||
|
||||
pub fn lat_range(&self) -> Range {
|
||||
self.pcs.lat_range
|
||||
}
|
||||
}
|
||||
// pub fn lon_range(&self) -> Range {
|
||||
// self.pcs.lon_range
|
||||
// }
|
||||
|
||||
impl<T: ProjectionS> Coord<f64> for LatLonCoord<T> {
|
||||
fn map(&self, axis_1: f64, axis_2: f64) -> super::ScreenCoord {
|
||||
let point = self.pcs.map((axis_1, axis_2));
|
||||
let logical_dim1_span = self.logical.0 .1 - self.logical.0 .0;
|
||||
// pub fn lat_range(&self) -> Range {
|
||||
// self.pcs.lat_range
|
||||
// }
|
||||
// }
|
||||
|
||||
let dim1_rate = (point.0 - self.logical.0 .0) / logical_dim1_span;
|
||||
let logical_dim2_span = self.logical.1 .1 - self.logical.1 .0;
|
||||
let dim2_rate = (point.1 - self.logical.1 .0) / logical_dim2_span;
|
||||
// impl<T: ProjectionS> Coord<f64> for LatLonCoord<T> {
|
||||
// fn map(&self, axis_1: f64, axis_2: f64) -> super::ScreenCoord {
|
||||
// let point = self.pcs.map((axis_1, axis_2));
|
||||
// let logical_dim1_span = self.logical.0 .1 - self.logical.0 .0;
|
||||
|
||||
(
|
||||
(dim1_rate * (self.actual.0 .1 - self.actual.0 .0) as f64) + self.actual.0 .0,
|
||||
(dim2_rate * (self.actual.1 .1 - self.actual.1 .0) as f64) + self.actual.1 .0,
|
||||
)
|
||||
}
|
||||
// let dim1_rate = (point.0 - self.logical.0 .0) / logical_dim1_span;
|
||||
// let logical_dim2_span = self.logical.1 .1 - self.logical.1 .0;
|
||||
// let dim2_rate = (point.1 - self.logical.1 .0) / logical_dim2_span;
|
||||
|
||||
fn dim1_range(&self) -> (f64, f64) {
|
||||
let v = self.lon_range();
|
||||
(v.0, v.1)
|
||||
}
|
||||
// (
|
||||
// (dim1_rate * (self.actual.0 .1 - self.actual.0 .0) as f64) + self.actual.0 .0,
|
||||
// (dim2_rate * (self.actual.1 .1 - self.actual.1 .0) as f64) + self.actual.1 .0,
|
||||
// )
|
||||
// }
|
||||
|
||||
fn dim2_range(&self) -> (f64, f64) {
|
||||
let v = self.lat_range();
|
||||
(v.0, v.1)
|
||||
}
|
||||
}
|
||||
// fn dim1_range(&self) -> (f64, f64) {
|
||||
// let v = self.lon_range();
|
||||
// (v.0, v.1)
|
||||
// }
|
||||
|
||||
// fn dim2_range(&self) -> (f64, f64) {
|
||||
// let v = self.lat_range();
|
||||
// (v.0, v.1)
|
||||
// }
|
||||
// }
|
||||
|
||||
15
src/main.rs
15
src/main.rs
@ -1,6 +1,7 @@
|
||||
use coords::proj::Mercator;
|
||||
use coords::Mapper;
|
||||
use data::{Npz, Radar2d};
|
||||
use femtovg::{Color, Paint};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{gio, glib, Application, ApplicationWindow};
|
||||
use std::ptr;
|
||||
@ -10,7 +11,6 @@ mod errors;
|
||||
mod monitor;
|
||||
mod pipeline;
|
||||
mod render;
|
||||
mod tree;
|
||||
mod window;
|
||||
use monitor::Monitor;
|
||||
use render::{BackgroundConfig, BackgroundWidget, ForegroundConfig, ForegroundWidget, Render};
|
||||
@ -29,8 +29,11 @@ fn main() -> glib::ExitCode {
|
||||
.or_else(|_| libloading::os::windows::Library::open_already_loaded("epoxy-0.dll"))
|
||||
.unwrap();
|
||||
|
||||
gio::resources_register_include!("p.gresource")
|
||||
.expect("Failed to register resources.");
|
||||
|
||||
epoxy::load_with(|name| {
|
||||
unsafe { library.get::<_>(name.as_bytes()) }
|
||||
unsafe { library.get::<>(name.as_bytes()) }
|
||||
.map(|symbol| *symbol)
|
||||
.unwrap_or(ptr::null())
|
||||
});
|
||||
@ -52,16 +55,20 @@ fn build_ui(app: &Application) {
|
||||
.title("My GTK App")
|
||||
.build();
|
||||
|
||||
let mut background_config = BackgroundConfig::new();
|
||||
let mut background_config = BackgroundConfig::new()
|
||||
.change_painter(Paint::color(Color::white()))
|
||||
.change_show_lat_lines(true)
|
||||
.change_show_lon_lines(true);
|
||||
let mut foreground_config = ForegroundConfig::new();
|
||||
|
||||
let background_widget = BackgroundWidget::new(background_config);
|
||||
let foreground_widget = ForegroundWidget::new(foreground_config);
|
||||
let render = Render::new(background_widget, foreground_widget);
|
||||
|
||||
let path = "/Users/ruomu/projects/cinrad_g/test2.npz";
|
||||
let path = "/Users/tsuki/projects/radar-g/test2.npz";
|
||||
let data = Radar2d::<i8>::load(path, Npz).unwrap();
|
||||
let projection = Mercator::new();
|
||||
|
||||
let mut mapper: Mapper = projection.into();
|
||||
mapper.set_lat_range(29.960..30.764);
|
||||
mapper.set_lon_range(120.038..120.965);
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
use anyhow::{Ok, Result};
|
||||
use femtovg;
|
||||
use femtovg::{self};
|
||||
use geo_types::{line_string, LineString};
|
||||
use image::RgbImage;
|
||||
use ndarray::Array2;
|
||||
use num_traits::Num;
|
||||
use ndarray::parallel::prelude::*;
|
||||
use ndarray::{Array2, ArrayView2};
|
||||
use num_traits::{Num, AsPrimitive, FromPrimitive};
|
||||
|
||||
use crate::{
|
||||
coords::Mapper,
|
||||
@ -18,22 +19,34 @@ impl Color {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<femtovg::Color> for Color {
|
||||
fn from(value: femtovg::Color) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Pipeline<T> {
|
||||
type Output;
|
||||
fn run(&self, input: T) -> Result<Self::Output>;
|
||||
}
|
||||
pub struct ProjPipe {
|
||||
pub mapper: Mapper,
|
||||
pub struct ProjPipe<'a> {
|
||||
pub mapper: &'a Mapper,
|
||||
}
|
||||
|
||||
impl<T, Raw> Pipeline<RadarData2d<T, Raw>> for ProjPipe
|
||||
impl<'a> ProjPipe<'a> {
|
||||
pub fn new(mapper: &'a Mapper) -> Self {
|
||||
Self { mapper }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b: 'a, T, Raw> Pipeline<&'b RadarData2d<T, Raw>> for ProjPipe<'a>
|
||||
where
|
||||
T: Num + Clone + PartialEq + PartialOrd,
|
||||
Raw: ndarray::Data<Elem = T> + Clone + ndarray::RawDataClone,
|
||||
{
|
||||
type Output = Array2<LineString>;
|
||||
|
||||
fn run(&self, input: RadarData2d<T, Raw>) -> Result<Self::Output> {
|
||||
fn run(&self, input: &'b RadarData2d<T, Raw>) -> Result<Self::Output> {
|
||||
let dim1 = input.dim1.view();
|
||||
let dim2 = input.dim2.view();
|
||||
|
||||
@ -92,3 +105,26 @@ impl<T: Num + PartialOrd> ShadePipe<T> {
|
||||
&self.colors[left]
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, Raw> Pipeline<&'a RadarData2d<T, Raw>> for ShadePipe<T>
|
||||
where
|
||||
T: Num + PartialEq + PartialOrd + Clone + FromPrimitive,
|
||||
Raw: ndarray::Data<Elem = T> + Clone + ndarray::RawDataClone,
|
||||
{
|
||||
type Output = Array2<Option<femtovg::Color>>;
|
||||
|
||||
fn run(&self, input: &'a RadarData2d<T, Raw>) -> Result<Array2<Option<femtovg::Color>>> {
|
||||
let data = input.data.view();
|
||||
|
||||
let result = data.mapv(|v| {
|
||||
if T::from_i8(-125).unwrap() == v {
|
||||
None
|
||||
} else {
|
||||
let color = self.get_color(v);
|
||||
Some(color.0)
|
||||
}
|
||||
});
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,8 +9,8 @@ use std::cell::RefCell;
|
||||
pub struct BackgroundConfig {
|
||||
pub show_lat_lines: bool,
|
||||
pub show_lon_lines: bool,
|
||||
pub lat_lines: Vec<Vec<WindowCoord>>,
|
||||
pub lon_lines: Vec<Vec<WindowCoord>>,
|
||||
pub lat_lines: Vec<f64>,
|
||||
pub lon_lines: Vec<f64>,
|
||||
pub painter: Paint,
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
mod imp;
|
||||
use crate::render::WindowCoord;
|
||||
use femtovg::{renderer::OpenGl, Canvas, Path};
|
||||
use crate::coords::{Mapper, Range};
|
||||
use femtovg::{renderer::OpenGl, Canvas, Color, Paint, Path};
|
||||
use geo_types::{line_string, LineString, Point};
|
||||
use glib::subclass::types::ObjectSubclassIsExt;
|
||||
use gtk::{ffi::gtk_widget_get_width, glib, graphene::Rect, prelude::SnapshotExtManual};
|
||||
use std::ops::Range;
|
||||
use std::cell::Ref;
|
||||
|
||||
pub use self::imp::BackgroundConfig;
|
||||
|
||||
@ -25,36 +25,122 @@ impl BackgroundWidget {
|
||||
this
|
||||
}
|
||||
|
||||
fn mesh_lines(&self, canvas: &mut Canvas<OpenGl>) {
|
||||
let imp = self.imp();
|
||||
let line_painter = &imp.config.borrow().painter;
|
||||
if imp.config.borrow().show_lat_lines {
|
||||
for lat_line in imp.config.borrow().lat_lines.iter() {
|
||||
fn draw_lines<V: IntoIterator<Item = LineString<f32>>>(
|
||||
&self,
|
||||
canvas: &mut Canvas<OpenGl>,
|
||||
line_painter: &Paint,
|
||||
p: V,
|
||||
) {
|
||||
p.into_iter().for_each(|line| {
|
||||
let mut path = Path::new();
|
||||
lat_line.iter().for_each(|v| {
|
||||
let (x, y) = *v;
|
||||
path.move_to(x, y);
|
||||
let points: Vec<Point<f32>> = line.points().collect();
|
||||
path.move_to(points[0].x() as f32, points[0].y() as f32);
|
||||
points[1..].into_iter().for_each(|p| {
|
||||
path.line_to(p.x() as f32, p.y() as f32);
|
||||
});
|
||||
canvas.stroke_path(&mut path, line_painter);
|
||||
}
|
||||
}
|
||||
if imp.config.borrow().show_lon_lines {}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn draw(&self, canvas: &mut Canvas<OpenGl>, scale: f32, dpi: i32) {
|
||||
let canvas_widht = canvas.width();
|
||||
fn mesh_lines(
|
||||
&self,
|
||||
canvas: &mut Canvas<OpenGl>,
|
||||
canvas_width: f32,
|
||||
canvas_height: f32,
|
||||
mapper: Ref<'_, Mapper>,
|
||||
bound: (Range, Range),
|
||||
) {
|
||||
let imp = self.imp();
|
||||
let line_painter = &imp.config.borrow().painter;
|
||||
let (left, right) = (bound.0 .0, bound.0 .1);
|
||||
let (bottom, top) = (bound.1 .0, bound.1 .1);
|
||||
let config = imp.config.borrow();
|
||||
|
||||
if config.show_lat_lines {
|
||||
let r = config.lat_lines.iter().map(|lat| {
|
||||
let line = LineString::new(vec![(left, *lat).into(), (right, *lat).into()]);
|
||||
let result = mapper.map_line(&line).unwrap();
|
||||
LineString::new(
|
||||
result
|
||||
.points()
|
||||
.map(|p| (p.x() as f32 * canvas_width, p.y() as f32 * canvas_height).into())
|
||||
.collect(),
|
||||
)
|
||||
});
|
||||
|
||||
self.draw_lines(canvas, line_painter, r);
|
||||
}
|
||||
|
||||
if config.show_lat_lines {
|
||||
config.lat_lines.iter().for_each(|lat| {
|
||||
let mut paint = Paint::color(Color::white());
|
||||
paint.set_font_size(35.0);
|
||||
paint.set_line_width(1.0);
|
||||
let text_location = mapper.map((left, *lat)).unwrap();
|
||||
|
||||
let _ = canvas.stroke_text(
|
||||
text_location.0 as f32 * canvas_width,
|
||||
text_location.1 as f32 * canvas_height,
|
||||
format!("{:.2} N", lat),
|
||||
&paint,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if config.show_lon_lines {
|
||||
config.lon_lines.iter().for_each(|lon| {
|
||||
let mut paint = Paint::color(Color::white());
|
||||
paint.set_font_size(35.0);
|
||||
paint.set_line_width(1.0);
|
||||
let text_location = mapper.map((*lon, top + 0.1)).unwrap();
|
||||
|
||||
let _ = canvas.stroke_text(
|
||||
text_location.0 as f32 * canvas_width,
|
||||
text_location.1 as f32 * canvas_height,
|
||||
format!("{:.2}", lon),
|
||||
&paint,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if imp.config.borrow().show_lon_lines {
|
||||
let r = config.lon_lines.iter().map(|lon| {
|
||||
let line = LineString::new(vec![(*lon, bottom).into(), (*lon, top).into()]);
|
||||
let result = mapper.map_line(&line).unwrap();
|
||||
LineString::new(
|
||||
result
|
||||
.points()
|
||||
.map(|p| (p.x() as f32 * canvas_width, p.y() as f32 * canvas_height).into())
|
||||
.collect(),
|
||||
)
|
||||
});
|
||||
|
||||
self.draw_lines(canvas, line_painter, r);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(
|
||||
&self,
|
||||
canvas: &mut Canvas<OpenGl>,
|
||||
scale: f32,
|
||||
dpi: i32,
|
||||
mapper: Ref<'_, Mapper>,
|
||||
bound: (Range, Range),
|
||||
) {
|
||||
let canvas_width = canvas.width();
|
||||
let canvas_height = canvas.height();
|
||||
|
||||
let config = self.imp().config.borrow();
|
||||
|
||||
self.mesh_lines(canvas);
|
||||
self.mesh_lines(canvas, canvas_width, canvas_height, mapper, bound);
|
||||
}
|
||||
|
||||
pub fn set_lat_lines(&self, lat_lines: Vec<Vec<WindowCoord>>) {
|
||||
pub fn set_lat_lines(&self, lat_lines: Vec<f64>) {
|
||||
let imp = self.imp();
|
||||
imp.config.borrow_mut().lat_lines = lat_lines;
|
||||
}
|
||||
|
||||
pub fn set_lon_lines(&self, lon_lines: Vec<Vec<WindowCoord>>) {
|
||||
pub fn set_lon_lines(&self, lon_lines: Vec<f64>) {
|
||||
let imp = self.imp();
|
||||
imp.config.borrow_mut().lon_lines = lon_lines;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use crate::render::{imp, WindowCoord};
|
||||
use femtovg::{ImageId, Paint};
|
||||
use femtovg::{ImageId, Paint, Color};
|
||||
use geo_macros::Prj;
|
||||
use geo_types::LineString;
|
||||
use gtk::glib;
|
||||
@ -29,6 +29,7 @@ pub struct ForegroundWidget {
|
||||
pub(super) dim2: RefCell<Option<Array2<f64>>>,
|
||||
pub(super) image: RefCell<Option<ImageId>>,
|
||||
pub(super) data: RefCell<Option<Array2<LineString>>>,
|
||||
pub(super) color: RefCell<Option<Array2<Option<Color>>>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
mod imp;
|
||||
use crate::coords::Mapper;
|
||||
use crate::tree::get;
|
||||
use femtovg::{renderer::OpenGl, Canvas, Path};
|
||||
use femtovg::{Color, FontId, ImageFlags, Paint};
|
||||
use geo_types::LineString;
|
||||
use femtovg::{Color, FontId, ImageFlags, ImageId, Paint};
|
||||
use geo_types::{LineString, Point};
|
||||
use glib::subclass::types::ObjectSubclassIsExt;
|
||||
use gtk::glib;
|
||||
use ndarray::{s, Array2, Axis, Zip};
|
||||
@ -34,12 +33,12 @@ impl ForegroundWidget {
|
||||
let canvas_height = canvas.height();
|
||||
let config = self.imp().config.borrow();
|
||||
|
||||
println!("Resize: {} {}", canvas.width(), canvas.height());
|
||||
let colors = self.imp().color.borrow();
|
||||
let data = self.imp().data.borrow();
|
||||
|
||||
let mut img = self.imp().image.borrow_mut();
|
||||
|
||||
if img.is_none() {
|
||||
let mut img_id = canvas
|
||||
if self.imp().image.borrow().is_none() {
|
||||
println!("rebuild image");
|
||||
let img_id = canvas
|
||||
.create_image_empty(
|
||||
canvas_width as usize,
|
||||
canvas_height as usize,
|
||||
@ -47,44 +46,75 @@ impl ForegroundWidget {
|
||||
ImageFlags::empty(),
|
||||
)
|
||||
.unwrap();
|
||||
canvas.save();
|
||||
canvas.reset();
|
||||
if let Ok(size) = canvas.image_size(img_id) {
|
||||
// canvas.set_render_target(femtovg::RenderTarget::Image(img_id));
|
||||
canvas.clear_rect(0, 0, size.0 as u32, size.1 as u32, Color::rgb(255, 255, 0));
|
||||
|
||||
img.replace(img_id);
|
||||
if let Ok(v) = canvas.image_size(img_id) {
|
||||
println!("create image: {:?}", v);
|
||||
canvas.set_render_target(femtovg::RenderTarget::Image(img_id));
|
||||
let colors = self.imp().color.borrow();
|
||||
let data = self.imp().data.borrow();
|
||||
if colors.is_some() && data.is_some() {
|
||||
for (i, c) in colors
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.t()
|
||||
.iter()
|
||||
.zip(data.as_ref().unwrap().iter())
|
||||
{
|
||||
if i.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
canvas.reset();
|
||||
}
|
||||
|
||||
let img = canvas
|
||||
.load_image_file("test.png", ImageFlags::NEAREST)
|
||||
.unwrap();
|
||||
|
||||
let pts = c.points().collect::<Vec<Point>>();
|
||||
let first_point = pts[0];
|
||||
let mut path = Path::new();
|
||||
|
||||
path.rect(0.0, 0.0, canvas_width, canvas_height);
|
||||
canvas.fill_path(
|
||||
&path,
|
||||
&Paint::image(img, 0.0, 0.0, canvas_width, canvas_height, 0.0, 1.0),
|
||||
path.move_to(
|
||||
first_point.x() as f32 * v.0 as f32 + 0.8,
|
||||
first_point.y() as f32 * v.1 as f32 + 0.8,
|
||||
);
|
||||
|
||||
canvas.flush();
|
||||
pts[1..].into_iter().for_each(|p| {
|
||||
path.line_to(p.x() as f32 * v.0 as f32, p.y() as f32 * v.1 as f32)
|
||||
});
|
||||
let c = i.unwrap();
|
||||
canvas.fill_path(&path, &Paint::color(c));
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_dims(&mut self, dims: (Array2<f64>, Array2<f64>)) {
|
||||
self.imp().image.replace(Some(img_id));
|
||||
}
|
||||
}
|
||||
|
||||
canvas.set_render_target(femtovg::RenderTarget::Screen);
|
||||
|
||||
let mut path = Path::new();
|
||||
path.rect(0.0, 0.0, canvas_width, canvas_height);
|
||||
|
||||
canvas.fill_path(
|
||||
&path,
|
||||
&Paint::image(
|
||||
self.imp().image.borrow().as_ref().unwrap().to_owned(),
|
||||
0.0,
|
||||
0.0,
|
||||
canvas_width,
|
||||
canvas_height,
|
||||
0.0,
|
||||
1.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn set_dims(&self, dims: (Array2<f64>, Array2<f64>)) {
|
||||
self.imp().dim1.replace(Some(dims.0));
|
||||
self.imp().dim2.replace(Some(dims.1));
|
||||
}
|
||||
|
||||
// pub(super) fn set_image(&mut self, image: Vec<u8>) {
|
||||
// self.imp().image.replace(Some(image));
|
||||
// }
|
||||
pub(super) fn set_image(&self, image: ImageId) {
|
||||
self.imp().image.replace(Some(image));
|
||||
}
|
||||
|
||||
pub(super) fn set_data(&mut self, data: Array2<LineString>) {
|
||||
pub fn set_colors(&self, colors: Array2<Option<Color>>) {
|
||||
self.imp().color.replace(Some(colors));
|
||||
}
|
||||
|
||||
pub fn set_data(&self, data: Array2<LineString>) {
|
||||
self.imp().data.replace(Some(data));
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ use super::foreground::ForegroundWidget;
|
||||
use super::WindowCoord;
|
||||
use crate::coords::proj::Mercator;
|
||||
use crate::coords::Mapper;
|
||||
use femtovg::{Color, Paint, Path};
|
||||
use femtovg::{Color, Paint, Path, FontId};
|
||||
use gtk::glib;
|
||||
use gtk::subclass::prelude::*;
|
||||
use gtk::traits::{GLAreaExt, WidgetExt};
|
||||
@ -20,6 +20,12 @@ pub struct RenderConfig {
|
||||
pub transform: WindowCoord,
|
||||
}
|
||||
|
||||
struct Fonts {
|
||||
sans: FontId,
|
||||
bold: FontId,
|
||||
light: FontId,
|
||||
}
|
||||
|
||||
pub struct Render {
|
||||
pub(super) background: RefCell<BackgroundWidget>,
|
||||
pub(super) foreground: RefCell<ForegroundWidget>,
|
||||
@ -81,24 +87,42 @@ impl GLAreaImpl for Render {
|
||||
let canvas = canvas.as_mut().unwrap();
|
||||
|
||||
let dpi = self.obj().scale_factor();
|
||||
let w = self.obj().width();
|
||||
let h = self.obj().width();
|
||||
let w = canvas.width();
|
||||
let h = canvas.height();
|
||||
let configs = self.config.borrow();
|
||||
|
||||
canvas.clear_rect(
|
||||
0,
|
||||
0,
|
||||
(w * dpi) as u32,
|
||||
(h * dpi) as u32,
|
||||
(w as i32 * dpi) as u32,
|
||||
(h as i32 * dpi) as u32,
|
||||
Color::rgba(0, 0, 0, 255),
|
||||
);
|
||||
|
||||
// self.background.borrow().draw(canvas, configs.scale, dpi);
|
||||
let mapper = self.mapper.borrow();
|
||||
let (lon_range, lat_range) = mapper.range.clone();
|
||||
|
||||
{
|
||||
let background_widget = self.background.borrow_mut();
|
||||
background_widget.set_lat_lines(lat_range.key_points(5));
|
||||
}
|
||||
{
|
||||
let background_widget = self.background.borrow_mut();
|
||||
background_widget.set_lon_lines(lon_range.key_points(10));
|
||||
}
|
||||
|
||||
self.foreground
|
||||
.borrow()
|
||||
.draw(canvas, configs.scale, dpi, self.mapper.borrow());
|
||||
|
||||
self.background.borrow().draw(
|
||||
canvas,
|
||||
configs.scale,
|
||||
dpi,
|
||||
self.mapper.borrow(),
|
||||
(lon_range, lat_range),
|
||||
);
|
||||
|
||||
canvas.flush();
|
||||
|
||||
true
|
||||
@ -113,6 +137,7 @@ impl Render {
|
||||
if self.canvas.borrow().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let widget = self.obj();
|
||||
widget.attach_buffers();
|
||||
|
||||
@ -131,7 +156,8 @@ impl Render {
|
||||
(renderer, glow::NativeFramebuffer(id))
|
||||
};
|
||||
renderer.set_screen_target(Some(fbo));
|
||||
let canvas = Canvas::new(renderer).expect("Cannot create canvas");
|
||||
let mut canvas = Canvas::new(renderer).expect("Cannot create canvas");
|
||||
canvas.add_font_dir("/Users/tsuki/projects/radar-g/src/assets").unwrap();
|
||||
self.canvas.replace(Some(canvas));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
mod background;
|
||||
mod foreground;
|
||||
mod imp;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::coords::Mapper;
|
||||
use crate::data::{MultiDimensionData, RadarData2d};
|
||||
use crate::pipeline::ProjPipe;
|
||||
use crate::pipeline::{Pipeline, ShadePipe};
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub use self::background::{BackgroundConfig, BackgroundWidget};
|
||||
pub use self::foreground::{ForegroundConfig, ForegroundWidget};
|
||||
use self::imp::RenderConfig;
|
||||
use crate::data::DownSampleMeth;
|
||||
use femtovg::Color;
|
||||
pub use glib::subclass::prelude::*;
|
||||
use image::RgbImage;
|
||||
use ndarray::{self, s, Array2, Axis, Dimension, Ix2, Zip};
|
||||
@ -70,45 +72,37 @@ impl Render {
|
||||
{
|
||||
assert!(data.dim1.shape().len() == data.dim2.shape().len());
|
||||
|
||||
let levels: Vec<T> = vec![0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65]
|
||||
.into_iter()
|
||||
.map(|b| T::from_i8(b).unwrap())
|
||||
.collect();
|
||||
let colors = vec![
|
||||
Color::rgb(0, 172, 164),
|
||||
Color::rgb(192, 192, 254),
|
||||
Color::rgb(122, 114, 238),
|
||||
Color::rgb(30, 38, 208),
|
||||
Color::rgb(166, 252, 168),
|
||||
Color::rgb(0, 234, 0),
|
||||
Color::rgb(16, 146, 26),
|
||||
Color::rgb(252, 244, 100),
|
||||
Color::rgb(200, 200, 2),
|
||||
Color::rgb(140, 140, 0),
|
||||
Color::rgb(254, 172, 172),
|
||||
Color::rgb(254, 100, 92),
|
||||
Color::rgb(238, 2, 48),
|
||||
Color::rgb(212, 142, 254),
|
||||
Color::rgb(170, 36, 250),
|
||||
];
|
||||
|
||||
let mapper = self.imp().mapper.borrow();
|
||||
// data.downsample((801 * 2 / 3, 947 * 2 / 3), DownSampleMeth::VAR);
|
||||
|
||||
let pjp = ProjPipe::new(&mapper);
|
||||
let rrp = ShadePipe::new(levels, colors.into_iter().map(|v| v.into()).collect());
|
||||
|
||||
let rainbow = pjp.run(&data).unwrap();
|
||||
self.imp().foreground.borrow_mut().set_data(rainbow);
|
||||
let pbow:Array2<Option<Color>> = rrp.run(&data).unwrap();
|
||||
self.imp().foreground.borrow_mut().set_colors(pbow);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// let levels: Vec<i8> = vec![0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65];
|
||||
// let colors = vec![
|
||||
// Color::rgb(0, 172, 164),
|
||||
// Color::rgb(192, 192, 254),
|
||||
// Color::rgb(122, 114, 238),
|
||||
// Color::rgb(30, 38, 208),
|
||||
// Color::rgb(166, 252, 168),
|
||||
// Color::rgb(0, 234, 0),
|
||||
// Color::rgb(16, 146, 26),
|
||||
// Color::rgb(252, 244, 100),
|
||||
// Color::rgb(200, 200, 2),
|
||||
// Color::rgb(140, 140, 0),
|
||||
// Color::rgb(254, 172, 172),
|
||||
// Color::rgb(254, 100, 92),
|
||||
// Color::rgb(238, 2, 48),
|
||||
// Color::rgb(212, 142, 254),
|
||||
// Color::rgb(170, 36, 250),
|
||||
// ];
|
||||
|
||||
// let c = d.map(|v| {
|
||||
// let c = get(&levels, &colors, *v);
|
||||
// image::Rgb([
|
||||
// (c.r * 255.0) as u8,
|
||||
// (c.g * 255.0) as u8,
|
||||
// (c.b * 255.0) as u8,
|
||||
// ])
|
||||
// });
|
||||
|
||||
// let mut img = RgbImage::from_fn(927, 801, |x, y| c[[y as usize, x as usize]]);
|
||||
|
||||
// img.save("test.png").unwrap();
|
||||
|
||||
// self.imp()
|
||||
// .foreground
|
||||
// .borrow_mut()
|
||||
// .set_dims((meshed.dim1, meshed.dim2));
|
||||
|
||||
BIN
src/resources/Roboto-Bold.ttf
Normal file
BIN
src/resources/Roboto-Bold.ttf
Normal file
Binary file not shown.
BIN
src/resources/Roboto-Light.ttf
Normal file
BIN
src/resources/Roboto-Light.ttf
Normal file
Binary file not shown.
BIN
src/resources/Roboto-Regular.ttf
Normal file
BIN
src/resources/Roboto-Regular.ttf
Normal file
Binary file not shown.
BIN
src/resources/amiri-regular.ttf
Normal file
BIN
src/resources/amiri-regular.ttf
Normal file
Binary file not shown.
BIN
src/resources/entypo.ttf
Normal file
BIN
src/resources/entypo.ttf
Normal file
Binary file not shown.
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/org/cinrad_g/">
|
||||
<file compressed="true" preprocess="xml-stripblanks">monitor.ui</file>
|
||||
<file alias="bold.ttf">Roboto-Bold.ttf</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
|
||||
23
src/tree.rs
23
src/tree.rs
@ -1,23 +0,0 @@
|
||||
use femtovg::Color;
|
||||
use num_traits::Num;
|
||||
|
||||
pub fn get<T, V: Copy>(levels: &Vec<T>, colors: &Vec<V>, v: T) -> V
|
||||
where
|
||||
T: Num + PartialOrd + Copy,
|
||||
{
|
||||
let len = levels.len();
|
||||
|
||||
let mut left = 0;
|
||||
let mut right = len - 1;
|
||||
|
||||
while left < right - 1 {
|
||||
let middle = (right + left) / 2;
|
||||
if v > levels[middle] {
|
||||
left = middle;
|
||||
} else {
|
||||
right = middle;
|
||||
}
|
||||
}
|
||||
|
||||
colors[left]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user