+
+
+
+
+
+
+
+
+
Admin 0 – Countries
+
+
+
+
+

+
About
+
Countries distinguish between metropolitan (homeland) and independent and semi-independent portions of sovereign states. If you want to see the dependent overseas regions broken out (like in ISO codes, see France for example), use map units instead.
+
Each country is coded with a world region that roughly follows the United Nations setup.
+
Includes some thematic data from the United Nations, U.S. Central Intelligence Agency, and elsewhere.
+
Disclaimer
+
Natural Earth Vector draws boundaries of countries according to defacto status. We show who actually controls the situation on the ground. Please feel free to mashup our disputed areas (link) theme to match your particular political outlook.
+
Known Problems
+
None.
+
Version History
+
+
+
The master changelog is available on Github »
+
+
+
+
+
+ This entry was posted
+ on Monday, September 21st, 2009 at 10:21 am and is filed under 110m-cultural-vectors.
+ You can follow any responses to this entry through the RSS 2.0 feed.
+
+ Both comments and pings are currently closed.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/shp/ne_110m_admin_0_countries.VERSION.txt b/shp/ne_110m_admin_0_countries.VERSION.txt
new file mode 100644
index 0000000..d7445e2
--- /dev/null
+++ b/shp/ne_110m_admin_0_countries.VERSION.txt
@@ -0,0 +1 @@
+5.1.1
diff --git a/shp/ne_110m_admin_0_countries.cpg b/shp/ne_110m_admin_0_countries.cpg
new file mode 100644
index 0000000..3ad133c
--- /dev/null
+++ b/shp/ne_110m_admin_0_countries.cpg
@@ -0,0 +1 @@
+UTF-8
\ No newline at end of file
diff --git a/shp/ne_110m_admin_0_countries.dbf b/shp/ne_110m_admin_0_countries.dbf
new file mode 100755
index 0000000..e0acd06
Binary files /dev/null and b/shp/ne_110m_admin_0_countries.dbf differ
diff --git a/shp/ne_110m_admin_0_countries.prj b/shp/ne_110m_admin_0_countries.prj
new file mode 100755
index 0000000..b13a717
--- /dev/null
+++ b/shp/ne_110m_admin_0_countries.prj
@@ -0,0 +1 @@
+GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.017453292519943295]]
\ No newline at end of file
diff --git a/shp/ne_110m_admin_0_countries.shp b/shp/ne_110m_admin_0_countries.shp
new file mode 100755
index 0000000..9318e45
Binary files /dev/null and b/shp/ne_110m_admin_0_countries.shp differ
diff --git a/shp/ne_110m_admin_0_countries.shx b/shp/ne_110m_admin_0_countries.shx
new file mode 100755
index 0000000..c3728e0
Binary files /dev/null and b/shp/ne_110m_admin_0_countries.shx differ
diff --git a/src/assets/Roboto-Bold.ttf b/src/assets/Roboto-Bold.ttf
new file mode 100644
index 0000000..aaf374d
Binary files /dev/null and b/src/assets/Roboto-Bold.ttf differ
diff --git a/src/assets/Roboto-Light.ttf b/src/assets/Roboto-Light.ttf
new file mode 100644
index 0000000..664e1b2
Binary files /dev/null and b/src/assets/Roboto-Light.ttf differ
diff --git a/src/assets/Roboto-Regular.ttf b/src/assets/Roboto-Regular.ttf
new file mode 100644
index 0000000..3e6e2e7
Binary files /dev/null and b/src/assets/Roboto-Regular.ttf differ
diff --git a/src/assets/amiri-regular.ttf b/src/assets/amiri-regular.ttf
new file mode 100644
index 0000000..0fa885b
Binary files /dev/null and b/src/assets/amiri-regular.ttf differ
diff --git a/src/assets/entypo.ttf b/src/assets/entypo.ttf
new file mode 100644
index 0000000..fc305d2
Binary files /dev/null and b/src/assets/entypo.ttf differ
diff --git a/src/backend.rs b/src/chart/backend.rs
similarity index 96%
rename from src/backend.rs
rename to src/chart/backend.rs
index f813440..f99f768 100644
--- a/src/backend.rs
+++ b/src/chart/backend.rs
@@ -1,6 +1,11 @@
-use cairo::{Context as CairoContext, FontSlant, FontWeight};
+use gtk::cairo::{Context as CairoContext, FontSlant, FontWeight};
+use plotters_backend::text_anchor::{HPos, VPos};
#[allow(unused_imports)]
+use plotters_backend::{
+ BackendColor, BackendCoord, BackendStyle, BackendTextStyle, DrawingBackend, DrawingErrorKind,
+ FontStyle, FontTransform,
+};
/// The drawing backend that is backed with a Cairo context
pub struct CairoBackend<'a> {
@@ -72,7 +77,7 @@ impl<'a> CairoBackend<'a> {
}
impl<'a> DrawingBackend for CairoBackend<'a> {
- type ErrorType = cairo::Error;
+ type ErrorType = gtk::cairo::Error;
fn get_size(&self) -> (u32, u32) {
(self.width, self.height)
diff --git a/src/chart/imp.rs b/src/chart/imp.rs
new file mode 100644
index 0000000..caf42bb
--- /dev/null
+++ b/src/chart/imp.rs
@@ -0,0 +1,19 @@
+use gtk::subclass::prelude::*;
+use gtk::{glib, prelude::WidgetExtManual};
+use std::cell::{Cell, RefCell};
+
+#[derive(Default)]
+pub struct Chart {}
+
+#[glib::object_subclass]
+impl ObjectSubclass for Chart {
+ const NAME: &'static str = "Chart";
+ type Type = super::Chart;
+ type ParentType = gtk::DrawingArea;
+}
+
+impl ObjectImpl for Chart {}
+
+impl WidgetImpl for Chart {}
+
+impl DrawingAreaImpl for Chart {}
diff --git a/src/chart/mod.rs b/src/chart/mod.rs
new file mode 100644
index 0000000..f1a21fc
--- /dev/null
+++ b/src/chart/mod.rs
@@ -0,0 +1,109 @@
+mod backend;
+mod imp;
+use self::backend::CairoBackend;
+use crate::widgets::render::{Render, RenderConfig, RenderMotion};
+use gtk::prelude::*;
+use gtk::{glib, AspectFrame};
+use plotters::prelude::*;
+
+glib::wrapper! {
+ pub struct Chart(ObjectSubclass
)
+ @extends gtk::DrawingArea, gtk::Widget;
+}
+
+impl Chart {
+ pub fn new() -> Self {
+ let this: Self = glib::Object::new();
+ this.set_hexpand(true);
+ this.set_vexpand(true);
+
+ this.set_draw_func(|_s, context, w, h| {
+ let root_area = CairoBackend::new(context, (w as u32, h as u32))
+ .expect("Can't create backend")
+ .into_drawing_area();
+ root_area.fill(&BLACK).unwrap();
+
+ let root_area = root_area.titled("Image Title", ("sans-serif", 60)).unwrap();
+
+ let (upper, lower) = root_area.split_vertically(512);
+
+ let x_axis = (-3.4f32..3.4).step(0.1);
+
+ let mut cc = ChartBuilder::on(&upper)
+ .margin(5)
+ .set_all_label_area_size(50)
+ .caption("Sine and Cosine", ("sans-serif", 40))
+ .build_cartesian_2d(-3.4f32..3.4, -1.2f32..1.2f32).unwrap();
+
+ cc.configure_mesh()
+ .x_labels(20)
+ .y_labels(10)
+ .disable_mesh()
+ .x_label_formatter(&|v| format!("{:.1}", v))
+ .y_label_formatter(&|v| format!("{:.1}", v))
+ .draw().unwrap();
+
+ cc.draw_series(LineSeries::new(x_axis.values().map(|x| (x, x.sin())), &RED)).unwrap()
+ .label("Sine")
+ .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED));
+
+ cc.draw_series(LineSeries::new(
+ x_axis.values().map(|x| (x, x.cos())),
+ &BLUE,
+ )).unwrap()
+ .label("Cosine")
+ .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], BLUE));
+
+ cc.configure_series_labels().border_style(WHITE).draw().unwrap();
+
+ /*
+ // It's possible to use a existing pointing element
+ cc.draw_series(PointSeries::<_, _, Circle<_>>::new(
+ (-3.0f32..2.1f32).step(1.0).values().map(|x| (x, x.sin())),
+ 5,
+ Into::::into(&RGBColor(255,0,0)).filled(),
+ )).unwrap();*/
+
+ // Otherwise you can use a function to construct your pointing element yourself
+ cc.draw_series(PointSeries::of_element(
+ (-3.0f32..2.1f32).step(1.0).values().map(|x| (x, x.sin())),
+ 5,
+ ShapeStyle::from(&RED).filled(),
+ &|coord, size, style| {
+ EmptyElement::at(coord)
+ + Circle::new((0, 0), size, style)
+ + Text::new(format!("{:.?}", coord), (0, 15), ("sans-serif", 15))
+ },
+ )).unwrap();
+
+ let drawing_areas = lower.split_evenly((1, 2));
+
+ for (drawing_area, idx) in drawing_areas.iter().zip(1..) {
+ let mut cc = ChartBuilder::on(drawing_area)
+ .x_label_area_size(30)
+ .y_label_area_size(30)
+ .margin_right(20)
+ .caption(format!("y = x^{}", 1 + 2 * idx), ("sans-serif", 40))
+ .build_cartesian_2d(-1f32..1f32, -1f32..1f32).unwrap();
+ cc.configure_mesh()
+ .x_labels(5)
+ .y_labels(3)
+ .max_light_lines(4)
+ .draw().unwrap();
+
+ cc.draw_series(LineSeries::new(
+ (-1f32..1f32)
+ .step(0.01)
+ .values()
+ .map(|x| (x, x.powf(idx as f32 * 2.0 + 1.0))),
+ &BLUE,
+ )).unwrap();
+ }
+
+ // To avoid the IO failure being ignored silently, we manually call the present function
+ root_area.present().expect("Unable to write result to file, please make sure 'plotters-doc-data' dir exists under current dir");
+ });
+
+ this
+ }
+}
diff --git a/src/components/app.rs b/src/components/app.rs
new file mode 100644
index 0000000..94b76cb
--- /dev/null
+++ b/src/components/app.rs
@@ -0,0 +1,357 @@
+use super::{
+ control_panel::{ControlPanelInputMsg, ControlPanelModel},
+ messages::{MonitorInputMsg, MonitorOutputMsg},
+ monitor::MonitorModel,
+ setting::SettingModel,
+ ControlPanelOutputMsg, TimelineMsg,
+};
+use crate::data_utils::plugin_result_impl;
+use crate::pipeline::element::{
+ Element, InstantElement, InstantElementDrawerType, TimeSeriesElement,
+};
+use crate::pipeline::{GridElementImpl, OffscreenRenderer};
+use crate::widgets::AssoElement;
+use crate::{
+ coords::{
+ cms::CMS,
+ proj::{Mercator, ProjectionS},
+ Mapper,
+ },
+ data::MetaInfo,
+ errors::RenderError,
+ pipeline::{utils::data_to_element, Dispatcher, Pipeline, RenderResult},
+ plugin_system::init_plugin,
+ widgets::render::Layer,
+ CONFIG, PLUGIN_MANAGER,
+};
+use abi_stable::std_types::RStr;
+use adw::prelude::*;
+use chrono::{prelude::*, Duration};
+use futures::future::BoxFuture;
+use gtk::glib::clone;
+use gtk::prelude::*;
+use once_cell::sync::Lazy;
+use radarg_plugin_interface::PluginResult;
+use relm4::actions::{AccelsPlus, RelmAction, RelmActionGroup};
+use relm4::*;
+use relm4::{gtk, Component, ComponentParts, ComponentSender, RelmWidgetExt, SimpleComponent};
+use relm4_components::open_dialog::{
+ OpenDialog, OpenDialogMsg, OpenDialogResponse, OpenDialogSettings,
+};
+use smallvec::SmallVec;
+use std::marker::PhantomData;
+use std::{
+ any::Any,
+ borrow::{Borrow, BorrowMut},
+ cell::RefCell,
+ collections::{BTreeMap, HashMap},
+ path::PathBuf,
+ rc::Rc,
+ sync::{Arc, Mutex},
+};
+use tokio::{sync::oneshot, task};
+use tracing::{debug, error, info, warn};
+use tracing_subscriber::fmt::layer;
+
+relm4::new_action_group!(FileActionGroup, "file");
+relm4::new_stateless_action!(OpenAction, FileActionGroup, "open");
+pub static FILE_PATH_ROOT: Lazy> = Lazy::new(|| Mutex::new(PathBuf::new()));
+
+pub type ElementKey = String;
+
+#[derive(Debug)]
+pub enum AppMsg {
+ CloseRequest,
+ Close,
+ OpenDialog,
+ SwitchToTime(usize),
+ NewElement(Element),
+ DeleteElement(ElementKey),
+ NewLayer(Layer),
+}
+pub type Buffer = Rc, Option>>>>;
+type RcDispatcher = Rc;
+#[tracker::track]
+pub struct AppModel {
+ #[do_not_track]
+ dispatcher: RcDispatcher,
+ #[do_not_track]
+ cms: CMS,
+ waiting_for: Option>,
+ #[do_not_track]
+ open_dialog: Controller,
+ #[do_not_track]
+ control: Controller,
+ #[do_not_track]
+ render: Controller,
+ #[do_not_track]
+ layers: Rc>>,
+ #[do_not_track]
+ elements: Vec>>,
+ #[do_not_track]
+ setting: Controller,
+}
+
+#[derive(Debug)]
+pub enum AppCommand {
+ PrepareFinished(Vec>),
+ TestBuffer((String, RenderResult)),
+ Test,
+}
+
+#[relm4::component(pub)]
+impl Component for AppModel {
+ type CommandOutput = AppCommand;
+ type Init = ();
+ type Input = AppMsg;
+ type Output = ();
+
+ view! {
+ #[root]
+ main_window=adw::ApplicationWindow {
+ set_default_width: 1200,
+ set_default_height: 900,
+ set_focus_on_click:true,
+ connect_close_request[sender] => move |_| {
+ sender.input(AppMsg::CloseRequest);
+ gtk::glib::Propagation::Proceed
+ },
+ gtk::Box{
+ set_orientation: gtk::Orientation::Vertical,
+ set_valign:gtk::Align::Fill,
+ set_spacing:2,
+ gtk::HeaderBar{
+ pack_start=&adw::ViewSwitcher{
+ set_stack: Some(&view_stack),
+ }
+ },
+ #[name="view_stack"]
+ adw::ViewStack{
+ set_hexpand:true,
+ set_vexpand:true,
+ },
+ },
+ connect_close_request[sender] => move |_| {
+ sender.input(AppMsg::CloseRequest);
+ gtk::glib::Propagation::Proceed
+ }
+ },
+ popover_child = gtk::Spinner {
+ set_spinning: true,
+ },
+ home_page = gtk::Box{
+ set_orientation: gtk::Orientation::Vertical,
+ set_hexpand:true,
+ set_vexpand:true,
+ gtk::Box{
+ set_orientation: gtk::Orientation::Horizontal,
+ set_margin_top: 2,
+ set_margin_start: 10,
+ set_margin_end: 10,
+ #[name="popover_menu_bar"]
+ gtk::PopoverMenuBar::from_model(Some(&main_menu)){
+ set_hexpand: true,
+ },
+ },
+ model.control.widget(),
+ #[name="monitor_toast"]
+ adw::ToastOverlay{
+ set_hexpand: true,
+ set_vexpand: true,
+ model.render.widget(),
+ },
+ },
+ home_stack_page = view_stack.add_titled(&home_page, Some("home"), "Home") -> adw::ViewStackPage{
+ set_icon_name:Some("home-filled"),
+ },
+ setting_stack_page = view_stack.add_titled(model.setting.widget(), Some("setting"), "Setting") -> adw::ViewStackPage{
+ set_icon_name:Some("settings-filled")
+ },
+ }
+
+ menu! {
+ main_menu: {
+ "File" {
+ "Open" => OpenAction,
+ "Open Folder" => OpenAction,
+ },
+ "Edit" {
+ "New Layer" => OpenAction,
+ "Undo" => OpenAction,
+ "Redo" => OpenAction,
+ },
+ "Plugins" {
+ "Plugin1" => OpenAction,
+ "Plugin2" => OpenAction,
+ },
+ }
+ }
+
+ fn init(
+ params: Self::Init,
+ root: Self::Root,
+ sender: ComponentSender,
+ ) -> ComponentParts {
+ let layers = Rc::new(RefCell::new(Vec::with_capacity(20)));
+ let control = ControlPanelModel::builder().launch(layers.clone()).forward(
+ sender.input_sender(),
+ |msg| match msg {
+ ControlPanelOutputMsg::OpenFile((key, time)) => AppMsg::Close,
+ },
+ );
+ let render =
+ MonitorModel::builder()
+ .launch(layers.clone())
+ .forward(sender.input_sender(), |a| match a {
+ MonitorOutputMsg::LayerRenderFinished => AppMsg::Close,
+ MonitorOutputMsg::LayerSwitchToTime(idx) => AppMsg::SwitchToTime(idx),
+ _ => AppMsg::Close,
+ });
+ let setting = SettingModel::builder()
+ .launch(())
+ .forward(sender.input_sender(), |a| AppMsg::Close);
+ let mut dispatcher = Rc::new(Dispatcher::new(5, 5, chrono::Duration::minutes(1)));
+ let cms = CMS::new(Mercator::default().into(), (3000.0, 3000.0));
+ let dialog_dispatcher = dispatcher.clone();
+ let dialog_render_sender = render.sender().clone();
+ let dialog = OpenDialog::builder()
+ .transient_for_native(&root)
+ .launch(OpenDialogSettings::default())
+ .forward(sender.input_sender(), move |response| match response {
+ OpenDialogResponse::Accept(path) => {
+ *FILE_PATH_ROOT.lock().unwrap() = path.clone();
+ let data = Self::open_file_only(path);
+ let meta: MetaInfo = (&data.meta).clone().into();
+ let (lat_start, lat_end) = meta.lat_range.unwrap();
+ let (lon_start, lon_end) = meta.lon_range.unwrap();
+ let element_impl = plugin_result_impl(&data);
+ let mut renderer = OffscreenRenderer::new(3000, 3000).unwrap();
+ let mut canvas = renderer.create_canvas();
+ let mut dialog_cms = CMS::new(Mercator::default().into(), (3000.0, 3000.0));
+ let mut data_target = element_impl.render(&data, &mut canvas, &mut dialog_cms);
+ data_target.data = Some(Arc::new(data) as Arc);
+
+ let element = Element::create_instant(
+ InstantElementDrawerType::Prepared((data_target, Arc::new(element_impl))),
+ dialog_dispatcher.clone(),
+ "ET".to_string(),
+ )
+ .get_instance();
+ let layer =
+ Layer::new(true, "New Layer".to_string(), AssoElement::Instant(element));
+ dialog_render_sender.emit(MonitorInputMsg::AddMetaItem(meta.to_map()));
+ dialog_render_sender.emit(MonitorInputMsg::SetRenderRange(
+ lon_start, lon_end, lat_start, lat_end,
+ ));
+ AppMsg::NewLayer(layer)
+ }
+ _ => AppMsg::Close,
+ });
+
+ let buffer: Buffer = Rc::new(RefCell::new(HashMap::new()));
+ let model = AppModel {
+ cms,
+ dispatcher,
+ waiting_for: None,
+ elements: Vec::with_capacity(20),
+ open_dialog: dialog,
+ control,
+ render,
+ layers,
+ setting,
+ tracker: 0,
+ };
+ let widgets = view_output!();
+ let mut group = RelmActionGroup::::new();
+ relm4::main_application().set_accelerators_for_action::(&["O"]);
+ let action: RelmAction = {
+ RelmAction::new_stateless(move |_| {
+ sender.input(AppMsg::OpenDialog);
+ })
+ };
+ group.add_action(action);
+ group.register_for_widget(&widgets.main_window);
+
+ ComponentParts { model, widgets }
+ }
+
+ fn update_with_view(
+ &mut self,
+ widgets: &mut Self::Widgets,
+ msg: Self::Input,
+ _sender: ComponentSender,
+ root: &Self::Root,
+ ) {
+ self.reset();
+ match msg {
+ AppMsg::NewLayer(layer) => {
+ (*self.layers).borrow_mut().push(layer);
+ self.render.sender().send(MonitorInputMsg::RefreshLayerList);
+ }
+ AppMsg::CloseRequest => {
+ relm4::main_application().quit();
+ }
+ AppMsg::Close => {}
+ AppMsg::SwitchToTime(idx) => {
+ let mut layer = (*self.layers).borrow_mut();
+ let switched_layer = layer.get_mut(idx).unwrap();
+ let asso_element = switched_layer.pop_associated_element();
+
+ if let AssoElement::Instant(e) = asso_element {
+ let dispatcher = self.dispatcher.clone();
+ let cms = self.cms.clone();
+ let (mut series, start_time) = e.to_time_series(dispatcher, cms);
+ switched_layer.set_time(start_time);
+ series.register(start_time).unwrap();
+ let element = Arc::new(Mutex::new(series));
+ switched_layer.set_associated_element(AssoElement::TimeSeries(element.clone()));
+ self.elements.push(element);
+ }
+ self.render.sender().send(MonitorInputMsg::RefreshLayerList);
+ }
+ AppMsg::OpenDialog => {
+ self.open_dialog.emit(OpenDialogMsg::Open);
+ }
+ _ => {}
+ }
+ self.update_view(widgets, _sender);
+ }
+
+ fn update_cmd(
+ &mut self,
+ message: Self::CommandOutput,
+ sender: ComponentSender,
+ root: &Self::Root,
+ ) {
+ match message {
+ AppCommand::PrepareFinished(mut v) => {}
+ _ => {
+ println!("test");
+ }
+ }
+ }
+}
+
+impl AppModel {
+ fn open_file(
+ path: impl AsRef,
+ dispatcher: Rc,
+ cms: CMS,
+ ) -> Option<(Option>, Element)> {
+ let plugin = PLUGIN_MANAGER.get_plugin_by_name("etws_loader").unwrap();
+ let mut result = plugin
+ .load(RStr::from_str(path.as_ref().to_str().unwrap()))
+ .unwrap();
+ let block = result.blocks.first().unwrap();
+ data_to_element(block, dispatcher, cms)
+ .map(|v| (Some(Box::new(result) as Box), v))
+ }
+
+ fn open_file_only(path: impl AsRef) -> PluginResult {
+ let plugin = PLUGIN_MANAGER.get_plugin_by_name("etws_loader").unwrap();
+ let mut result = plugin
+ .load(RStr::from_str(path.as_ref().to_str().unwrap()))
+ .unwrap();
+ return result;
+ }
+}
diff --git a/src/components/control_panel/control_panel.rs b/src/components/control_panel/control_panel.rs
new file mode 100644
index 0000000..b53c124
--- /dev/null
+++ b/src/components/control_panel/control_panel.rs
@@ -0,0 +1,303 @@
+use super::messages::*;
+use super::thumbnail::{ImgItem, TypedListView};
+use crate::data::{CoordType, Radar2d, RadarData2d};
+use crate::plugin_system::init_plugin;
+use crate::widgets::render::predefined::color_mapper::BoundaryNorm;
+use crate::widgets::render::Layer;
+use crate::widgets::timeline::{Selection, TimeLine};
+use abi_stable::std_types::RStr;
+use adw::prelude::*;
+use chrono::{prelude::*, DateTime, Duration, TimeZone, Utc};
+use gtk::glib::clone;
+use gtk::prelude::{BoxExt, ButtonExt, GtkWindowExt, OrientableExt, ToggleButtonExt};
+use ndarray::{Array1, Array2, Array3};
+use radarg_plugin_interface::VecResult;
+use relm4::prelude::*;
+use relm4_components::open_button::{OpenButton, OpenButtonSettings};
+use relm4_components::open_dialog::OpenDialogSettings;
+use relm4_icons::icon_names;
+use std::cell::{Ref, RefCell};
+use std::path::PathBuf;
+use std::rc::Rc;
+
+#[tracker::track]
+#[derive(Debug)]
+pub struct ControlPanelModel {
+ timeline_enabled: bool,
+ enabled: bool,
+ timeline_start: DateTime,
+ selection: Option>,
+ key: Option,
+ #[do_not_track]
+ layers: Rc>>,
+ #[tracker::no_eq]
+ list_img_wrapper: TypedListView,
+}
+
+#[relm4::component(pub)]
+impl SimpleComponent for ControlPanelModel {
+ type Init = Rc>>;
+ type Output = ControlPanelOutputMsg;
+ type Input = ControlPanelInputMsg;
+
+ view! {
+ #[root]
+ gtk::Box {
+ set_orientation: gtk::Orientation::Horizontal,
+ set_spacing: 10,
+ set_size_request: (100, 150),
+ set_margin_horizontal:5,
+ set_margin_top: 5,
+ gtk::Frame{
+ set_width_request: 100,
+ gtk::Box{
+ set_orientation: gtk::Orientation::Vertical,
+ set_margin_all:10,
+ set_spacing: 10,
+ gtk::Box{
+ set_orientation: gtk::Orientation::Vertical,
+ gtk::Label{
+ set_label: "Header",
+ add_css_class:"h2",
+ set_halign: gtk::Align::Start,
+ },
+ #[local]
+ layer_controller_selection -> gtk::DropDown{
+ set_expression:None::<>k::Expression>,
+ connect_selected_notify[sender] => move |step_selector| {
+ println!("Selected: {}", step_selector.selected());
+ },
+ }
+ },
+ #[local]
+ step_selector -> gtk::DropDown{
+ set_expression:None::<>k::Expression>,
+ connect_selected_notify[sender] => move |step_selector| {
+ println!("Selected: {}", step_selector.selected());
+ },
+ },
+ gtk::Box{
+ set_orientation: gtk::Orientation::Horizontal,
+ set_spacing:10,
+ gtk::Button{
+ // set_icon_name: "rewind-filled",
+ set_icon_name: icon_names::REWIND_FILLED,
+ #[track = "model.changed(ControlPanelModel::enabled()) || model.changed(ControlPanelModel::key())"]
+ set_sensitive: model.enabled && model.key.is_some(),
+ connect_clicked[sender] => move |_| {
+ sender.input(ControlPanelInputMsg::SelectionRewind);
+ },
+ },
+ gtk::Button{
+ set_icon_name: "play-filled"
+ },
+ gtk::Button{
+ set_icon_name: "fast-forward-filled",
+ #[track = "model.changed(ControlPanelModel::enabled()) || model.changed(ControlPanelModel::key())"]
+ set_sensitive: model.enabled && model.key.is_some(),
+ connect_clicked[sender] => move |_| {
+ sender.input(ControlPanelInputMsg::SelectionFastForward);
+ },
+ }
+ }
+
+ }
+ },
+ gtk::Frame{
+ gtk::Box{
+ set_orientation:gtk::Orientation::Vertical,
+ set_margin_horizontal:10,
+ set_margin_vertical:10,
+ set_spacing: 4,
+ gtk::Label{
+ set_label: "TimeLine",
+ add_css_class:"h2",
+ set_halign: gtk::Align::Start,
+ },
+ gtk::Box{
+ set_orientation: gtk::Orientation::Horizontal,
+ set_spacing:5,
+
+ #[name="pop"]
+ gtk::Popover{
+ set_position: gtk::PositionType::Bottom,
+ set_pointing_to: Some(>k::gdk::Rectangle::new(
+ 0,
+ 0,
+ 80,
+ 40,
+ )),
+ gtk::Calendar{
+ connect_day_selected[sender,pop] => move |cal| {
+ let date = cal.date().ymd();
+ let date = Utc.with_ymd_and_hms(date.0, date.1 as u32, date.2 as u32, 0,0,0).unwrap();
+ sender.input(ControlPanelInputMsg::TimeLine(
+ TimelineMsg::SetStart(date)
+ ));
+ pop.popdown();
+ }
+ }
+ },
+ gtk::Button{
+ #[track = "model.changed(ControlPanelModel::timeline_start())"]
+ set_label:&model.timeline_start.format("%Y-%m-%d").to_string(),
+ connect_clicked[sender,pop] => move |_| {
+ pop.popup();
+ },
+ },
+ gtk::Button{
+ set_icon_name: "rewind-filled",
+ // add_controller: fastforward_long_press_detector,
+ connect_clicked[sender] => move |_| {
+ sender.input(ControlPanelInputMsg::TimeLine(
+ TimelineMsg::Rewind(-Duration::minutes(12))
+ ));
+ },
+ },
+ TimeLine{
+ set_height_request: 40,
+ set_width_request: 400,
+ #[track = "model.changed(ControlPanelModel::timeline_start())"]
+ set_time_start: model.timeline_start,
+ #[track = "model.changed(ControlPanelModel::selection())"]
+ set_selection: model.selection.map(|p| Selection::Point(p)),
+ connect_start_time_notify[sender] => move |time| {
+ let time = time.start_time();
+ sender.input(
+ ControlPanelInputMsg::Selection(
+ if time > i64::MIN {
+ Some(Utc.timestamp_opt(time, 0).unwrap())
+ } else{
+ None
+ }));
+
+ },
+ },
+ gtk::Button{
+ set_icon_name: "fast-forward-filled",
+ connect_clicked[sender] => move |_| {
+ sender.input(ControlPanelInputMsg::TimeLine(
+ TimelineMsg::FastForward(Duration::minutes(12))
+ ));
+ },
+ }
+ },
+
+ gtk::ScrolledWindow{
+ set_height_request: 75,
+ set_max_content_height: 75,
+ #[local_ref]
+ my_view -> gtk::ListView{
+ add_css_class: "lv",
+ set_orientation: gtk::Orientation::Horizontal,
+ }
+ },
+ }
+ },
+ gtk::Separator{
+ set_orientation: gtk::Orientation::Vertical,
+ },
+ gtk::ScrolledWindow{
+ set_hexpand: true,
+ gtk::Box{
+ set_height_request: 140,
+ set_width_request: 300,
+ set_orientation:gtk::Orientation::Horizontal,
+
+ }
+ }
+ },
+
+ }
+
+ fn init(
+ init: Self::Init,
+ root: Self::Root,
+ sender: relm4::ComponentSender,
+ ) -> relm4::ComponentParts {
+ let select_model =
+ gtk::StringList::new(&["Option 1", "Option 2", "Option 3", "Editable..."]);
+ let step_selector = gtk::DropDown::from_strings(&["Option 1", "Option 2", "Option 3"]);
+
+ let mut list_img_wrapper: TypedListView =
+ TypedListView::new();
+
+ let timeline_start = Utc::now();
+ let model = ControlPanelModel {
+ timeline_enabled: true,
+ key: None,
+ enabled: true,
+ selection: None,
+ layers: init,
+ timeline_start,
+ list_img_wrapper,
+ tracker: 0,
+ };
+
+ let layer_controller_selection = gtk::DropDown::from_strings(&[]);
+ let my_view = &model.list_img_wrapper.view;
+ let widgets = view_output!();
+
+ ComponentParts { model, widgets }
+ }
+ fn update(&mut self, msg: Self::Input, _sender: ComponentSender) {
+ self.reset();
+ match msg {
+ ControlPanelInputMsg::TimeLine(line_msg) => match line_msg {
+ TimelineMsg::Rewind(c) | TimelineMsg::FastForward(c) => {
+ let current = self.get_timeline_start().clone();
+ self.set_timeline_start(current + c);
+ }
+ TimelineMsg::SetStart(time) => {
+ self.set_timeline_start(time);
+ }
+ TimelineMsg::Disable => {}
+ TimelineMsg::Enable => {}
+ },
+ ControlPanelInputMsg::Selection(selection) => {
+ self.set_selection(selection);
+ }
+ ControlPanelInputMsg::SelectionRewind => {
+ let current = self.get_selection().clone();
+ if let Some(current) = current {
+ self.set_selection(Some(current - Duration::minutes(1)));
+ _sender.output(ControlPanelOutputMsg::OpenFile((
+ self.key.clone().unwrap(),
+ current - Duration::minutes(1),
+ )));
+ }
+ }
+ ControlPanelInputMsg::SelectionFastForward => {
+ let current = self.get_selection().clone();
+ if let Some(current) = current {
+ self.set_selection(Some(current + Duration::minutes(1)));
+ _sender.output(ControlPanelOutputMsg::OpenFile((
+ self.key.clone().unwrap(),
+ current + Duration::minutes(1),
+ )));
+ }
+ }
+
+ ControlPanelInputMsg::SetThumb(v) => {
+ for (texture, rx, t) in v {
+ self.list_img_wrapper
+ .append(ImgItem::new(t.to_string(), texture, true, rx));
+ }
+ }
+
+ ControlPanelInputMsg::SetThumbByDate((time, thumb)) => {}
+
+ ControlPanelInputMsg::Disable => {
+ self.set_enabled(false);
+ }
+
+ ControlPanelInputMsg::Enable => {
+ self.set_enabled(true);
+ }
+
+ ControlPanelInputMsg::SetKey(key) => {
+ self.set_key(Some(key));
+ }
+ }
+ }
+}
diff --git a/src/components/control_panel/messages.rs b/src/components/control_panel/messages.rs
new file mode 100644
index 0000000..c93c4fe
--- /dev/null
+++ b/src/components/control_panel/messages.rs
@@ -0,0 +1,35 @@
+use chrono::{DateTime, Duration, Utc};
+use tokio::sync::oneshot;
+
+#[derive(Debug)]
+pub enum TimelineMsg {
+ Rewind(Duration),
+ FastForward(Duration),
+ SetStart(DateTime),
+ Disable,
+ Enable,
+}
+
+#[derive(Debug)]
+pub enum ControlPanelInputMsg {
+ TimeLine(TimelineMsg),
+ Selection(Option>),
+ SetThumb(
+ Vec<(
+ Option,
+ Option>,
+ DateTime,
+ )>,
+ ),
+ SetThumbByDate((DateTime, Option)),
+ SelectionRewind,
+ SelectionFastForward,
+ SetKey(String),
+ Disable,
+ Enable,
+}
+
+#[derive(Debug)]
+pub enum ControlPanelOutputMsg {
+ OpenFile((String, DateTime)),
+}
diff --git a/src/components/control_panel/mod.rs b/src/components/control_panel/mod.rs
new file mode 100644
index 0000000..792c76f
--- /dev/null
+++ b/src/components/control_panel/mod.rs
@@ -0,0 +1,5 @@
+mod control_panel;
+mod messages;
+mod thumbnail;
+pub use control_panel::*;
+pub use messages::*;
diff --git a/src/components/control_panel/thumbnail.rs b/src/components/control_panel/thumbnail.rs
new file mode 100644
index 0000000..1c94107
--- /dev/null
+++ b/src/components/control_panel/thumbnail.rs
@@ -0,0 +1,78 @@
+use gtk::prelude::*;
+pub use relm4::typed_view::list::{RelmListItem, TypedListView};
+use relm4::RelmWidgetExt;
+use std::sync::{Arc, Mutex};
+use tokio::sync::oneshot;
+
+#[derive(Debug)]
+pub(super) struct ImgItem {
+ pub(super) time: String,
+ pub(super) img: Arc>>,
+ pub(super) visiable: bool,
+ pub(super) rx: Option>,
+ handle: Option>,
+}
+
+impl ImgItem {
+ pub(super) fn new(
+ time: String,
+ img: Option,
+ visiable: bool,
+ rx: Option>,
+ ) -> Self {
+ assert!(rx.is_none() && img.is_some() || rx.is_some() && img.is_none());
+ Self {
+ time,
+ img: Arc::new(Mutex::new(img)),
+ visiable,
+ rx,
+ handle: None,
+ }
+ }
+}
+pub(super) struct Widgets {
+ img: gtk::Image,
+}
+
+impl RelmListItem for ImgItem {
+ type Root = gtk::Frame;
+ type Widgets = Widgets;
+
+ fn setup(_item: >k::ListItem) -> (gtk::Frame, Widgets) {
+ relm4::view! {
+ my_box = gtk::Frame {
+ set_height_request: 70,
+ set_width_request: 100,
+ gtk::Box{
+ set_margin_all:2,
+ set_hexpand:true,
+ set_vexpand:true,
+ #[name = "img"]
+ gtk::Image{}
+ }
+ }
+ }
+ let widgets = Widgets { img };
+ (my_box, widgets)
+ }
+
+ fn bind(&mut self, widgets: &mut Self::Widgets, _root: &mut Self::Root) {
+ let Widgets { img } = widgets;
+ if self.rx.is_some() {
+ let new_rx = self.rx.take().unwrap();
+ let ltex = self.img.clone();
+ let new_img = img.clone();
+ self.handle = Some(relm4::spawn_local(async move {
+ let tex = new_rx.await;
+ if let Ok(tex) = tex {
+ ltex.lock().unwrap().replace(tex);
+ new_img.set_paintable(ltex.lock().unwrap().as_ref());
+ } else {
+ eprintln!("Failed to get texture");
+ }
+ }));
+ }
+ let ltex = self.img.lock().unwrap();
+ img.set_paintable(ltex.as_ref());
+ }
+}
diff --git a/src/components/mod.rs b/src/components/mod.rs
new file mode 100644
index 0000000..b210ffb
--- /dev/null
+++ b/src/components/mod.rs
@@ -0,0 +1,8 @@
+pub mod app;
+mod control_panel;
+mod monitor;
+mod setting;
+
+
+pub use control_panel::*;
+pub use monitor::*;
diff --git a/src/components/monitor/messages.rs b/src/components/monitor/messages.rs
new file mode 100644
index 0000000..7d7c0a1
--- /dev/null
+++ b/src/components/monitor/messages.rs
@@ -0,0 +1,55 @@
+use std::{collections::HashMap, fmt::Debug};
+
+use crate::{
+ components::app::ElementKey,
+ pipeline::element::ElementID,
+ widgets::{render::Layer, widget::Widget},
+};
+
+pub enum MonitorInputMsg {
+ RefreshRender,
+ AddWidget(Box),
+ RemoveWidget,
+ AddMetaItem(HashMap),
+ ClearMetaItems,
+ UpdateMetaItem(HashMap),
+ RefreshTiles,
+ RefreshLayerList,
+ SetRenderRange(f64, f64, f64, f64),
+ ChangeZoom(f64),
+ None,
+}
+
+impl Debug for MonitorInputMsg {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ MonitorInputMsg::RefreshRender => write!(f, "MonitorInputMsg::RefreshRender"),
+ MonitorInputMsg::RefreshTiles => write!(f, "MonitorInputMsg::RefreshTiles"),
+ MonitorInputMsg::ChangeZoom(_) => write!(f, "MonitorInputMsg::ChangeZoom"),
+ MonitorInputMsg::SetRenderRange(_, _, _, _) => {
+ write!(f, "MonitorInputMsg::SetRenderRange")
+ }
+ MonitorInputMsg::RefreshLayerList => write!(f, "MonitorInputMsg::RefreshLayerList"),
+ MonitorInputMsg::None => write!(f, "MonitorInputMsg::None"),
+ MonitorInputMsg::AddWidget(_) => write!(f, "MonitorInputMsg::AddWidget"),
+ MonitorInputMsg::RemoveWidget => write!(f, "MonitorInputMsg::RemoveWidget"),
+ MonitorInputMsg::AddMetaItem(_) => write!(f, "MonitorInputMsg::RemoveWidget"),
+ MonitorInputMsg::ClearMetaItems => write!(f, "MonitorInputMsg::ClearMetaItems"),
+ MonitorInputMsg::UpdateMetaItem(_) => write!(f, "MonitorInputMsg::UpdateMetaItem"),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum MonitorOutputMsg {
+ LayerAdded(usize),
+ LayerRemoved(usize),
+ LayerUpdated(usize),
+ LayerSwitchToTime(usize),
+ LayerRenderFinished,
+}
+
+#[derive(Debug)]
+pub enum RenderInputMsg {
+ Monitor(MonitorInputMsg),
+}
diff --git a/src/components/monitor/mod.rs b/src/components/monitor/mod.rs
new file mode 100644
index 0000000..ee7e130
--- /dev/null
+++ b/src/components/monitor/mod.rs
@@ -0,0 +1,4 @@
+pub mod messages;
+pub mod monitor;
+pub mod sidebar;
+pub use monitor::*;
diff --git a/src/components/monitor/monitor.rs b/src/components/monitor/monitor.rs
new file mode 100644
index 0000000..f5feab5
--- /dev/null
+++ b/src/components/monitor/monitor.rs
@@ -0,0 +1,281 @@
+use super::messages::{MonitorInputMsg, MonitorOutputMsg};
+use crate::coords::cms::CMS;
+use crate::pipeline::offscreen_renderer::OffscreenRenderer;
+use crate::widgets::predefined::color_mapper::BoundaryNorm;
+use crate::widgets::predefined::widgets::ColorBar;
+use crate::widgets::render::RenderConfig;
+use crate::widgets::widget::{Widget, WidgetType};
+use crate::widgets::WidgetFrame;
+use crate::{
+ coords::{proj::Mercator, Mapper},
+ widgets::dynamic_col::DynamicCol,
+ widgets::render::{Layer, Render},
+};
+use geo::k_nearest_concave_hull;
+use gtk::glib::clone;
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::rc::Rc;
+use std::sync::{Arc, Mutex};
+use tracing::*;
+
+use super::sidebar::{sidebar::SideBarModel, SideBarInputMsg, SideBarOutputMsg};
+use crate::coords::Range;
+use crate::map_tile::MapTile;
+use crate::map_tile_utils::lat_lon_to_zoom;
+use crate::pipeline::element::Target;
+use crate::utils::estimate_zoom_level;
+use adw::prelude::*;
+use femtovg::ImageId;
+use fns::debounce;
+use relm4::{component::Component, *};
+use slippy_map_tiles::Tile;
+use tokio::task;
+use tracing::instrument::WithSubscriber;
+
+#[derive(Debug)]
+pub enum MonitorCommand {
+ LoadedTile,
+ None,
+}
+#[tracker::track]
+pub struct MonitorModel {
+ render_cfg: RenderConfig,
+ render_range: (f64, f64, f64, f64),
+ sidebar_open: bool,
+ sidebar_width: i32,
+ zoom: u8,
+ #[do_not_track]
+ last_call: Rc>,
+ #[do_not_track]
+ map_tile_getter: Rc,
+ new_layer: i8,
+ #[no_eq]
+ widgets: Vec,
+ #[no_eq]
+ layers: Rc>>,
+ #[no_eq]
+ sidebar: Controller,
+}
+
+pub struct MonitorWidgets {
+ paned: gtk::Paned,
+}
+
+#[relm4::component(pub)]
+impl Component for MonitorModel {
+ type CommandOutput = MonitorCommand;
+ type Input = MonitorInputMsg;
+ type Init = Rc>>;
+ type Output = MonitorOutputMsg;
+
+ view! {
+ #[root]
+ adw::BreakpointBin {
+ set_hexpand: true,
+ set_vexpand: true,
+ set_height_request: 500,
+ set_width_request: 700,
+ #[wrap(Some)]
+ #[name="test"]
+ set_child = &DynamicCol{
+ set_end_width: 300,
+ set_hexpand: true,
+ set_vexpand: true,
+ #[wrap(Some)]
+ #[name="paned"]
+ set_child_paned=>k::Paned{
+ #[wrap(Some)]
+ #[name="render"]
+ set_start_child=>k::Frame{
+ add_css_class: "rb",
+ set_margin_all: 5,
+ #[name="widget_layer"]
+ gtk::Overlay{
+ #[name = "renderer"]
+ #[wrap(Some)]
+ set_child = &Render{
+ #[track = "model.changed(MonitorModel::render_cfg())"]
+ set_cfg: model.render_cfg,
+ #[track = "model.changed(MonitorModel::render_range())"]
+ set_view: model.render_range,
+ set_tiles: model.map_tile_getter.clone(),
+ connect_render_status_notify[sender] => move |r| {
+ sender.output(MonitorOutputMsg::LayerRenderFinished);
+ },
+ connect_range_changing_notify[sender] => move |r| {
+ sender.input(MonitorInputMsg::RefreshTiles);
+ },
+ connect_scale_notify[sender] => move |r| {
+ let scale = r.scale();
+ {
+ let initial = r.scale_rate();
+ let mut rate_start = initial_rate.lock().unwrap();
+ if rate_start.is_none() {
+ *rate_start = Some(scale);
+ }
+ }
+ debouncer.call(scale);
+ },
+ set_interior_layers: model.layers.clone(),
+ },
+ add_overlay=>k::Button{
+ set_label:"Add",
+ set_margin_all:10,
+ set_valign: gtk::Align::Start,
+ set_halign: gtk::Align::End,
+ },
+ #[track = "model.changed(MonitorModel::new_layer())"]
+ #[iterate]
+ add_overlay: &model.widgets
+ },
+
+ },
+ #[wrap(Some)]
+ set_end_child=model.sidebar.widget(),
+ }
+ }
+ }
+ }
+
+ fn update_with_view(
+ &mut self,
+ widgets: &mut Self::Widgets,
+ message: Self::Input,
+ sender: ComponentSender,
+ root: &Self::Root,
+ ) {
+ self.reset();
+ match message {
+ MonitorInputMsg::RefreshRender => {
+ widgets.renderer.queue_render();
+ }
+ MonitorInputMsg::RefreshLayerList => {
+ self.sidebar.sender().send(SideBarInputMsg::RefreshList);
+ sender.input(MonitorInputMsg::RefreshRender);
+ }
+ MonitorInputMsg::AddMetaItem(map) => {
+ self.sidebar.emit(SideBarInputMsg::AddMetaItems(map))
+ }
+ MonitorInputMsg::SetRenderRange(lon_start, lon_end, lat_start, lat_end) => {
+ let current_rate = widgets.renderer.scale_rate();
+ self.set_render_range((lat_start, lat_end, lon_start, lon_end));
+ let new_rate = widgets.renderer.scale_rate();
+ let zoom: f64 = (current_rate / new_rate).log2();
+ sender.input(MonitorInputMsg::ChangeZoom(zoom));
+ }
+ MonitorInputMsg::ClearMetaItems => self.sidebar.emit(SideBarInputMsg::ClearMetaItems),
+ MonitorInputMsg::UpdateMetaItem(map) => {
+ self.sidebar.emit(SideBarInputMsg::ClearMetaItems);
+ self.sidebar.emit(SideBarInputMsg::AddMetaItems(map))
+ }
+ MonitorInputMsg::RefreshTiles => {
+ let ((x1, x2), (y1, y2)) = widgets.renderer.render_range();
+ self.load_tile(&sender, ((y1 as f32, y2 as f32), (x1 as f32, x2 as f32)));
+ }
+ MonitorInputMsg::AddWidget(widget) => match widget.widget_type() {
+ WidgetType::Cairo => {
+ let frame = WidgetFrame::new();
+ frame.set_widget(widget);
+ self.widgets.push(frame);
+ }
+ WidgetType::OpenGl => {}
+ _ => {}
+ },
+ MonitorInputMsg::ChangeZoom(zoom) => {
+ let new_zoom = (self.zoom as f64 + zoom).clamp(0.0, 19.0).round() as u8;
+ if self.zoom != new_zoom {
+ self.zoom = new_zoom;
+ self.map_tile_getter.set_zoom(new_zoom);
+ sender.input(MonitorInputMsg::RefreshTiles);
+ }
+ }
+ MonitorInputMsg::RemoveWidget => {}
+ MonitorInputMsg::None => {}
+ _ => {}
+ }
+
+ self.update_view(widgets, sender);
+ }
+
+ fn init(
+ init: Self::Init,
+ root: Self::Root,
+ sender: ComponentSender,
+ ) -> ComponentParts {
+ let sidebar_sender = sender.clone();
+ let sidebar: Controller = SideBarModel::builder()
+ .launch(init.clone())
+ .forward(sender.input_sender(), move |msg| match msg {
+ SideBarOutputMsg::SwitchToTimeSeries(layer) => {
+ sidebar_sender.output(MonitorOutputMsg::LayerSwitchToTime(layer));
+ MonitorInputMsg::None
+ }
+ _ => MonitorInputMsg::None,
+ });
+
+ let render_cfg = RenderConfig {
+ padding: [0.0, 0.0, 0.0, 0.0],
+ };
+
+ let new_sender = sender.clone();
+
+ let mut model = MonitorModel {
+ render_range: (4.0, 53.3, 73.3, 135.0),
+ new_layer: 0,
+ widgets: vec![],
+ render_cfg,
+ sidebar_open: true,
+ sidebar_width: 400,
+ last_call: Rc::new(RefCell::new(std::time::Instant::now())),
+ layers: init,
+ zoom: 4,
+ map_tile_getter: Rc::new(MapTile::default()),
+ sidebar,
+ tracker: 0,
+ };
+
+ let initial_rate = Arc::new(Mutex::new(None));
+ let debouncer_rate = initial_rate.clone();
+ let debouncer_sender = sender.clone();
+ let debouncer = fns::debounce(
+ move |zoom: f64| {
+ let rate: f64 = debouncer_rate.lock().unwrap().take().unwrap();
+ let zoom: f64 = (rate / zoom).log2();
+ debouncer_sender.input(MonitorInputMsg::ChangeZoom(zoom));
+ debouncer_sender.input(MonitorInputMsg::RefreshRender);
+ },
+ std::time::Duration::from_millis(500),
+ );
+
+ let widgets = view_output! {};
+ ComponentParts { model, widgets }
+ }
+
+ fn update_cmd_with_view(
+ &mut self,
+ widgets: &mut Self::Widgets,
+ message: Self::CommandOutput,
+ sender: ComponentSender,
+ root: &Self::Root,
+ ) {
+ self.reset();
+ match message {
+ MonitorCommand::LoadedTile => {
+ sender.input(MonitorInputMsg::RefreshRender);
+ }
+ _ => {}
+ }
+ self.update_view(widgets, sender);
+ }
+}
+
+impl MonitorModel {
+ fn load_tile(&self, sender: &ComponentSender, range: ((f32, f32), (f32, f32))) {
+ let task = self.map_tile_getter.load_tiles(range, sender.clone());
+ sender.oneshot_command(async move {
+ task.await;
+ MonitorCommand::None
+ });
+ }
+}
diff --git a/src/components/monitor/sidebar/bottom_bar.rs b/src/components/monitor/sidebar/bottom_bar.rs
new file mode 100644
index 0000000..f13c7bd
--- /dev/null
+++ b/src/components/monitor/sidebar/bottom_bar.rs
@@ -0,0 +1,56 @@
+use gtk::prelude::*;
+use relm4::{
+ factory::FactoryView,
+ gtk,
+ prelude::{DynamicIndex, FactoryComponent},
+ FactorySender,
+};
+
+use super::SideBarInputMsg;
+
+#[derive(Debug)]
+pub enum TestMsg {
+ Delete,
+ MoveUp,
+ MoveDown,
+ Add,
+}
+
+pub struct BottomBarModel {
+ icon: String,
+ msg: TestMsg,
+}
+
+impl BottomBarModel {
+ pub fn new(icon: String) -> Self {
+ Self {
+ icon,
+ msg: TestMsg::Add,
+ }
+ }
+}
+
+#[relm4::factory(pub)]
+impl FactoryComponent for BottomBarModel {
+ type ParentWidget = gtk::Box;
+ type Input = ();
+ type Output = TestMsg;
+ type Init = BottomBarModel;
+ type CommandOutput = ();
+
+ view! {
+ #[root]
+ gtk::Box{
+ gtk::Button{
+ set_icon_name=self.icon.as_str(),
+ }
+ }
+
+ }
+
+ fn init_model(init: Self::Init, index: &DynamicIndex, sender: FactorySender) -> Self {
+ init
+ }
+
+ fn update(&mut self, message: Self::Input, sender: FactorySender) {}
+}
diff --git a/src/components/monitor/sidebar/meta_data_list.rs b/src/components/monitor/sidebar/meta_data_list.rs
new file mode 100644
index 0000000..8fb2ae1
--- /dev/null
+++ b/src/components/monitor/sidebar/meta_data_list.rs
@@ -0,0 +1,58 @@
+use gtk::prelude::*;
+use relm4::{
+ binding::{Binding, U8Binding},
+ factory::FactoryView,
+ gtk,
+ prelude::{DynamicIndex, FactoryComponent},
+ typed_view::column::{LabelColumn, RelmColumn, TypedColumnView},
+ view, FactorySender, RelmObjectExt,
+};
+
+#[derive(Debug, PartialEq, Eq)]
+pub(super) struct MyListItem {
+ tag: String,
+ info: String,
+}
+
+impl MyListItem {
+ pub fn new(tag: String, info: String) -> Self {
+ Self { tag, info }
+ }
+}
+
+pub(super) struct TagColumn;
+
+impl LabelColumn for TagColumn {
+ type Item = MyListItem;
+ type Value = String;
+ const COLUMN_NAME: &'static str = "tag";
+ const ENABLE_SORT: bool = true;
+
+ fn get_cell_value(item: &Self::Item) -> Self::Value {
+ // item.value
+ item.tag.clone()
+ }
+
+ fn format_cell_value(value: &Self::Value) -> String {
+ format!("{}", value)
+ }
+}
+
+pub(super) struct InfoColumn;
+
+impl RelmColumn for InfoColumn {
+ type Root = gtk::Label;
+ type Widgets = ();
+ type Item = MyListItem;
+
+ const COLUMN_NAME: &'static str = "info";
+
+ fn setup(_item: >k::ListItem) -> (Self::Root, Self::Widgets) {
+ let a = gtk::Label::new(None);
+ (a, ())
+ }
+
+ fn bind(item: &mut Self::Item, _: &mut Self::Widgets, label: &mut Self::Root) {
+ label.set_text(item.info.as_str());
+ }
+}
diff --git a/src/components/monitor/sidebar/mod.rs b/src/components/monitor/sidebar/mod.rs
new file mode 100644
index 0000000..a2c7b4b
--- /dev/null
+++ b/src/components/monitor/sidebar/mod.rs
@@ -0,0 +1,4 @@
+pub mod sidebar;
+pub use sidebar::*;
+pub mod bottom_bar;
+pub mod meta_data_list;
diff --git a/src/components/monitor/sidebar/sidebar.rs b/src/components/monitor/sidebar/sidebar.rs
new file mode 100644
index 0000000..954ca19
--- /dev/null
+++ b/src/components/monitor/sidebar/sidebar.rs
@@ -0,0 +1,365 @@
+use crate::widgets::AssoElement;
+use abi_stable::type_level::trait_marker::Hash;
+use chrono::{DateTime, Utc};
+use glib_macros::clone;
+use gtk::glib;
+use gtk::prelude::WidgetExt;
+use gtk::prelude::*;
+use relm4::actions::{AccelsPlus, RelmAction};
+use relm4::{
+ binding::{Binding, U8Binding},
+ factory::{DynamicIndex, FactoryComponent, FactorySender, FactoryVecDeque},
+ gtk::gio,
+ prelude::*,
+ typed_view::{
+ column::TypedColumnView,
+ list::{RelmListItem, TypedListView},
+ },
+ RelmObjectExt,
+};
+use std::{cell::RefCell, collections::HashMap, rc::Rc};
+
+use crate::components::app::AppMsg;
+use crate::{
+ chart::Chart,
+ data::Npz,
+ widgets::render::{predefined::color_mapper::BoundaryNorm, Layer},
+};
+
+use super::{
+ bottom_bar::BottomBarModel,
+ meta_data_list::{InfoColumn, MyListItem, TagColumn},
+};
+
+pub struct SideBarModel {
+ layers: Rc>>,
+ selected_layer_idx: usize,
+ counter: u8,
+ list_view_wrapper: TypedListView,
+ bottom_bar_vec: FactoryVecDeque,
+ meta_list_view: TypedColumnView,
+}
+
+#[derive(Debug)]
+pub enum SideBarInputMsg {
+ AddMetaItems(HashMap),
+ ClearMetaItems,
+ RefreshList,
+ None,
+}
+
+#[derive(Debug)]
+pub enum SideBarOutputMsg {
+ NewLayer(Layer),
+ SwitchToTimeSeries(usize),
+}
+
+#[relm4::component(pub)]
+impl SimpleComponent for SideBarModel {
+ type Init = Rc>>;
+ type Output = SideBarOutputMsg;
+ type Input = SideBarInputMsg;
+
+ view! {
+ #[root]
+ gtk::Box {
+ set_orientation: gtk::Orientation::Vertical,
+ set_spacing: 5,
+ set_margin_all: 5,
+
+ gtk::Paned{
+ set_orientation: gtk::Orientation::Vertical,
+ set_position: 200,
+ #[wrap(Some)]
+ set_start_child = >k::Box{
+ set_orientation: gtk::Orientation::Vertical,
+ set_spacing: 5,
+ gtk::Frame{
+ add_css_class: "rb",
+ #[name="meta_panel"]
+ gtk::Notebook::builder().vexpand(true).hexpand(true).build() -> gtk::Notebook{}
+ },
+ },
+
+ #[wrap(Some)]
+ set_end_child=>k::Box{
+ set_orientation: gtk::Orientation::Vertical,
+ set_vexpand: true,
+ set_hexpand: true,
+ #[name="bottom_panel"]
+ gtk::Notebook::builder().vexpand(true).build() -> gtk::Notebook{
+ set_margin_top: 10,
+ set_margin_bottom: 5
+ },
+ #[local_ref]
+ counter_box -> gtk::Box{
+ set_spacing: 5,
+ }
+ }
+ }
+ },
+ layer_page = gtk::ScrolledWindow::builder()
+ .vexpand(true)
+ .hexpand(true)
+ .build() -> gtk::ScrolledWindow{
+ #[wrap(Some)]
+ #[local_ref]
+ set_child=my_view -> gtk::ListView{},
+ set_margin_horizontal:5,
+ set_margin_vertical:3
+ },
+ #[local_ref]
+ meta_view -> gtk::ColumnView{
+ set_hexpand:true,
+ set_vexpand:true,
+ set_show_column_separators: true,
+ set_show_row_separators: true,
+ set_enable_rubberband:true,
+ set_reorderable:false,
+ },
+ bottom_panel.append_page(&layer_page, Some(>k::Label::new(Some("Layers")))),
+ meta_panel.append_page(meta_view, Some(>k::Label::new(Some("Meta")))),
+ meta_panel.append_page(&Chart::new(), Some(>k::Label::new(Some("Chart")))),
+ #[local_ref]
+ info_c -> gtk::ColumnViewColumn{
+ set_expand: true
+ }
+ }
+
+ fn init(
+ init: Self::Init,
+ root: Self::Root,
+ sender: ComponentSender,
+ ) -> ComponentParts {
+ // Initialize the ListView wrapper
+ let mut list_view_wrapper: TypedListView =
+ TypedListView::with_sorting();
+ // let mut bottom_bar_vec = FactoryVecDeque::new(gtk::Box::default(), sender.input_sender());
+
+ let mut bottom_bar_vec =
+ FactoryVecDeque::builder()
+ .launch_default()
+ .forward(sender.input_sender(), |msg| match msg {
+ _ => SideBarInputMsg::None,
+ });
+
+ let app = relm4::main_application();
+
+ {
+ let mut bottom_bar_vec_guard = bottom_bar_vec.guard();
+ bottom_bar_vec_guard.push_back(BottomBarModel::new("add-filled".to_string()));
+ bottom_bar_vec_guard.push_back(BottomBarModel::new("delete-filled".to_string()));
+ bottom_bar_vec_guard.push_back(BottomBarModel::new("chevron-up-filled".to_string()));
+ bottom_bar_vec_guard.push_back(BottomBarModel::new("chevron-down-filled".to_string()));
+ }
+ let mut meta_list_view = TypedColumnView::new();
+ meta_list_view.append_column::();
+ meta_list_view.append_column::();
+
+ let mut model = SideBarModel {
+ meta_list_view,
+ layers: init,
+ selected_layer_idx: 0,
+ counter: 0,
+ list_view_wrapper,
+ bottom_bar_vec,
+ };
+ let my_view = &model.list_view_wrapper.view;
+ let counter_box = model.bottom_bar_vec.widget();
+ let meta_view = &model.meta_list_view.view;
+ let columns = model.meta_list_view.get_columns();
+ let info_c = columns.get("info").unwrap();
+ let widgets = view_output!();
+
+ {
+ let mut list = model
+ .layers
+ .borrow()
+ .iter()
+ .enumerate()
+ .map(|(idx, v)| {
+ LayerItem::new(
+ idx as u32,
+ v.name.clone(),
+ v.visiable,
+ v.get_thumbnail(),
+ match v.get_associated_element() {
+ AssoElement::TimeSeries(_) => LayerStatus::BindToTime(Utc::now()),
+ AssoElement::Instant(_) => LayerStatus::Instance,
+ _ => LayerStatus::Instance,
+ },
+ )
+ })
+ .collect::>();
+ model.list_view_wrapper.extend_from_iter(list);
+ }
+
+ ComponentParts { model, widgets }
+ }
+
+ fn update(&mut self, message: Self::Input, sender: ComponentSender) {
+ match message {
+ SideBarInputMsg::RefreshList => {
+ let mut list = self
+ .layers
+ .borrow()
+ .iter()
+ .enumerate()
+ .map(|(idx, v)| {
+ LayerItem::new(
+ idx as u32,
+ v.name.clone(),
+ v.visiable,
+ v.get_thumbnail(),
+ match v.get_associated_element() {
+ AssoElement::TimeSeries(_) => LayerStatus::BindToTime(Utc::now()),
+ AssoElement::Instant(_) => LayerStatus::Instance,
+ _ => LayerStatus::Instance,
+ },
+ )
+ })
+ .collect::>();
+ self.list_view_wrapper.clear();
+ self.list_view_wrapper.extend_from_iter(list);
+ }
+ SideBarInputMsg::AddMetaItems(hs) => {
+ for (k, v) in hs {
+ self.meta_list_view.append(MyListItem::new(k, v));
+ }
+ }
+ SideBarInputMsg::ClearMetaItems => {
+ self.meta_list_view.clear();
+ }
+ _ => {}
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
+enum LayerStatus {
+ BindToTime(DateTime),
+ Instance,
+ BindToOtherLayer(usize),
+}
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
+struct LayerItem {
+ key: u32,
+ layer_name: String,
+ visiable: bool,
+ status: LayerStatus,
+ img: Option,
+}
+
+impl LayerItem {
+ fn new(
+ key: u32,
+ name: String,
+ visiable: bool,
+ img: Option,
+ status: LayerStatus,
+ ) -> Self {
+ Self {
+ key,
+ layer_name: name,
+ visiable,
+ status,
+ img,
+ }
+ }
+}
+
+struct Widgets {
+ label: gtk::Label,
+ screen_shot: gtk::Image,
+ status: gtk::Label,
+ button: gtk::CheckButton,
+ menu: gtk::PopoverMenu,
+}
+
+impl RelmListItem for LayerItem {
+ type Root = gtk::Box;
+ type Widgets = Widgets;
+
+ fn setup(_item: >k::ListItem) -> (gtk::Box, Widgets) {
+ relm4::menu! {
+ main_menu: {
+ }
+ }
+
+ relm4::view! {
+ my_box = gtk::Box {
+ gtk::Frame{
+ set_margin_end: 10,
+ #[name = "screen_shot"]
+ gtk::Image{
+ set_size_request: (65, 40),
+ }
+ },
+ #[name = "label"]
+ gtk::Label{
+ set_halign: gtk::Align::Start,
+ },
+ #[name = "status"]
+ gtk::Label{
+ set_halign: gtk::Align::Start
+ },
+ gtk::Label{
+ set_hexpand: true,
+ },
+ #[name = "button"]
+ gtk::CheckButton{
+ set_halign: gtk::Align::End,
+ },
+ #[name = "menu"]
+ gtk::PopoverMenu::from_model(Some(&main_menu)){}
+ }
+ }
+
+ let widgets = Widgets {
+ screen_shot,
+ label,
+ status,
+ button,
+ menu,
+ };
+
+ (my_box, widgets)
+ }
+
+ fn bind(&mut self, widgets: &mut Self::Widgets, _root: &mut Self::Root) {
+ let Widgets {
+ label,
+ button,
+ screen_shot,
+ status,
+ menu,
+ } = widgets;
+
+ relm4::menu! {
+ main_menu: {
+ }
+ }
+ menu.set_menu_model(Some(&main_menu));
+
+ status.set_label(&match self.status {
+ LayerStatus::BindToTime(t) => format!("Bind To Time: {}", t),
+ LayerStatus::Instance => "Instance".to_string(),
+ LayerStatus::BindToOtherLayer(idx) => format!("Bind To Layer: {}", idx),
+ });
+
+ let gesture_click = gtk::GestureClick::new();
+ gesture_click.set_button(gtk::gdk::BUTTON_SECONDARY);
+ screen_shot.set_paintable(self.img.as_ref());
+
+ let menu = menu.clone();
+ gesture_click.connect_released(clone!(@weak menu => move |gesture_click, _, x, y| {
+ menu.set_pointing_to(Some(>k::gdk::Rectangle::new(x as i32, y as i32, 1, 1)));
+ menu.popup();
+ }));
+
+ _root.add_controller(gesture_click);
+ label.set_label(&self.layer_name);
+ button.set_active(self.visiable);
+ }
+}
diff --git a/src/components/setting/dispatcher_list.rs b/src/components/setting/dispatcher_list.rs
new file mode 100644
index 0000000..c3efabe
--- /dev/null
+++ b/src/components/setting/dispatcher_list.rs
@@ -0,0 +1,57 @@
+use abi_stable::traits::IntoReprC;
+use adw::prelude::*;
+use gtk::prelude::*;
+use relm4::{factory::FactoryView, gtk, prelude::*, FactorySender};
+
+#[derive(Debug)]
+pub enum Msg {}
+
+#[derive(Debug)]
+pub enum OutputMsg {
+ Update((String, String)),
+}
+
+pub struct PathItem {
+ title: String,
+ path: String,
+}
+
+impl PathItem {
+ pub fn new(title: String, path: String) -> Self {
+ Self { title, path }
+ }
+}
+
+#[relm4::factory(pub)]
+impl FactoryComponent for PathItem {
+ type ParentWidget = adw::PreferencesGroup;
+ // type ParentInput = super::SettingMsg;
+ type Input = ();
+ type Output = OutputMsg;
+ type Init = PathItem;
+ type CommandOutput = ();
+
+ view! {
+ #[root]
+ adw::EntryRow{
+ set_title: &self.title,
+ set_text: &self.path,
+ set_max_width_chars:100,
+ connect_changed[sender] => move |s|{
+ let key = s.title().clone().into();
+ sender.output(OutputMsg::Update((key,s.text().to_string())));
+ },
+ add_suffix=>k::Switch{
+ set_height_request: 10,
+ set_margin_vertical:15,
+ set_hexpand: false,
+ set_vexpand: false
+ }
+ }
+
+ }
+ fn init_model(init: Self::Init, index: &DynamicIndex, sender: FactorySender) -> Self {
+ init
+ }
+ fn update(&mut self, message: Self::Input, sender: FactorySender) {}
+}
diff --git a/src/components/setting/messages.rs b/src/components/setting/messages.rs
new file mode 100644
index 0000000..e5baaf0
--- /dev/null
+++ b/src/components/setting/messages.rs
@@ -0,0 +1,5 @@
+#[derive(Debug)]
+pub enum SettingMsg {
+ PathFormats((String, (String, String))),
+ SaveConfig,
+}
diff --git a/src/components/setting/mod.rs b/src/components/setting/mod.rs
new file mode 100644
index 0000000..4a14321
--- /dev/null
+++ b/src/components/setting/mod.rs
@@ -0,0 +1,5 @@
+pub mod dispatcher_list;
+pub mod messages;
+pub mod setting;
+pub use messages::*;
+pub use setting::*;
diff --git a/src/components/setting/setting.rs b/src/components/setting/setting.rs
new file mode 100644
index 0000000..47f5963
--- /dev/null
+++ b/src/components/setting/setting.rs
@@ -0,0 +1,138 @@
+use super::dispatcher_list::PathItem;
+use crate::{
+ config::PATH_FORMATS,
+ data::{self, CoordType, Radar2d},
+ widgets::render::{predefined::color_mapper::BoundaryNorm, Layer},
+ CONFIG, PLUGIN_MANAGER,
+};
+use adw::prelude::*;
+use gtk::prelude::*;
+use relm4::factory::{DynamicIndex, FactoryComponent, FactorySender, FactoryVecDeque};
+use relm4::typed_view::list::TypedListView;
+use relm4::{gtk, ComponentParts, ComponentSender, RelmWidgetExt, SimpleComponent};
+use relm4_components::open_dialog::{
+ OpenDialog, OpenDialogMsg, OpenDialogResponse, OpenDialogSettings,
+};
+use std::{borrow::BorrowMut, collections::HashMap, path};
+
+use super::SettingMsg;
+
+pub struct SettingModel {
+ path_list: FactoryVecDeque,
+}
+
+#[relm4::component(pub)]
+impl SimpleComponent for SettingModel {
+ type Init = ();
+ type Input = SettingMsg;
+ type Output = ();
+
+ view! {
+ #[root]
+ adw::NavigationSplitView{
+ set_hexpand:true,
+ set_vexpand:true,
+ #[wrap(Some)]
+ set_sidebar=&adw::NavigationPage{
+ #[wrap(Some)]
+ set_child=>k::StackSidebar{
+ set_stack:&stack,
+ }
+ },
+ #[wrap(Some)]
+ set_content=&adw::NavigationPage{
+ #[wrap(Some)]
+ #[name="stack"]
+ set_child=>k::Stack{
+ set_transition_type:gtk::StackTransitionType::SlideLeftRight,
+ set_transition_duration:100,
+ set_hexpand:true,
+ set_vexpand:true,
+ set_margin_all:10,
+ set_margin_top:10,
+ set_margin_bottom:10,
+ set_margin_start:10,
+ set_margin_end:10,
+ }}
+ },
+ path_page = gtk::Box{
+ set_orientation:gtk::Orientation::Vertical,
+ gtk::Box{
+ set_hexpand:true,
+ gtk::Label::new(Some("Paths")){
+ add_css_class: "h1",
+ set_halign: gtk::Align::Start,
+ },
+ gtk::Button{
+ set_halign: gtk::Align::End,
+ connect_clicked[sender] => move |_| {
+ sender.input(SettingMsg::SaveConfig);
+ },
+ #[wrap(Some)]
+ set_child=&adw::ButtonContent{
+ set_label: "Save",
+ set_icon_name: "save-filled",
+ }
+ }
+
+ },
+ adw::PreferencesPage{
+ set_title:"Paths",
+ #[local_ref]
+ add=my_view -> adw::PreferencesGroup{
+ set_title:"Add",
+ }
+ }
+ },
+ stack.add_titled(&path_page, None, "Paths"),
+
+ }
+
+ fn init(
+ params: Self::Init,
+ root: Self::Root,
+ sender: ComponentSender,
+ ) -> ComponentParts {
+ let mut path_list =
+ FactoryVecDeque::builder()
+ .launch_default()
+ .forward(sender.input_sender(), |msg| match msg {
+ _ => SettingMsg::SaveConfig,
+ });
+ {
+ let config = CONFIG.lock().unwrap();
+ let etws_config = config.plugins.get("etws_loader").unwrap();
+ let paths = etws_config.path_formats.as_ref().unwrap_or(&PATH_FORMATS);
+ let mut list_guard = path_list.guard();
+ for p in paths {
+ list_guard.push_back(PathItem::new(p.0.clone(), p.1.clone()));
+ }
+ }
+ let model = SettingModel { path_list };
+
+ let my_view = model.path_list.widget();
+ let general_page = gtk::Box::new(gtk::Orientation::Vertical, 10);
+ let widgets = view_output!();
+
+ ComponentParts { model, widgets }
+ }
+
+ fn update(&mut self, msg: Self::Input, _sender: ComponentSender) {
+ match msg {
+ SettingMsg::PathFormats((plugin, (k, v))) => {
+ let mut config = CONFIG.lock().unwrap();
+ let mut pluging_cfg = config.plugins.get_mut(&plugin).unwrap();
+ if pluging_cfg.path_formats.is_none() {
+ pluging_cfg.path_formats = Some(HashMap::new());
+ }
+ pluging_cfg.path_formats.as_mut().unwrap().insert(k, v);
+ let mut list_guard = self.path_list.guard();
+ // list_guard.push_back(PathItem::new(k, v));
+ }
+ SettingMsg::SaveConfig => {
+ let mut config = CONFIG.lock().unwrap();
+ config.save();
+ }
+ }
+ }
+}
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..e99457b
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,86 @@
+use crate::errors::ConfigError;
+use dirs;
+use serde::{Deserialize, Serialize};
+use std::{collections::HashMap, env, io::Write, path::PathBuf};
+use toml;
+
+lazy_static! {
+ pub static ref PATH_FORMATS: HashMap = {
+ let mut map = HashMap::new();
+ map.insert("R".to_string(), String::new());
+ map.insert("V".to_string(), String::new());
+ map.insert("SW".to_string(), String::new());
+ map.insert("CC".to_string(), String::new());
+ map.insert("ZDR".to_string(), String::new());
+ map.insert("PHIDP".to_string(), String::new());
+ map.insert("KDP".to_string(), String::new());
+ map.insert("HCA".to_string(), String::new());
+ map.insert("DBZ".to_string(), String::new());
+ map.insert("QPE".to_string(), String::new());
+ map.insert("QPF".to_string(), String::new());
+ map.insert("VIL".to_string(), String::new());
+ map.insert("OHP".to_string(), String::new());
+ map.insert("THP".to_string(), String::new());
+ map.insert("ET".to_string(), String::new());
+ map.insert("EB".to_string(), String::new());
+ map
+ };
+}
+
+#[derive(Debug, Serialize, Deserialize, Default)]
+pub struct Config {
+ pub plugins: HashMap,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct PluginConfig {
+ pub version: String,
+ pub path_formats: Option>,
+}
+
+impl Config {
+ pub fn from_file(path: impl AsRef) -> Result {
+ let file = std::fs::read_to_string(path)?;
+ let config = toml::from_str(&file)?;
+ Ok(config)
+ }
+
+ pub fn from_env() -> Result {
+ if let Some(dir_path) = env::var("RADARG_CONFIG")
+ .ok()
+ .map(|x| PathBuf::from(x))
+ .or(dirs::config_dir())
+ {
+ let path = dir_path.join("radarg.toml");
+ println!("{:?}", path);
+ if path.exists() {
+ return Ok(Self::from_file(path)?);
+ } else {
+ let default_config = Config::default();
+ let mut file = std::fs::File::create(path)?;
+ let ser_config = toml::to_string_pretty(&default_config).unwrap();
+ file.write_all(ser_config.as_bytes());
+ return Ok(default_config);
+ }
+ }
+
+ Err(ConfigError::DefaultConfigError)
+ }
+
+ pub fn save(&self) -> Result<(), ConfigError> {
+ if let Some(dir_path) = env::var("RADARG_CONFIG")
+ .ok()
+ .map(|x| PathBuf::from(x))
+ .or(dirs::config_dir())
+ {
+ let path = dir_path.join("radarg.toml");
+ let mut file = std::fs::File::create(path)?;
+ let ser_config = toml::to_string_pretty(&self).unwrap();
+ file.write_all(ser_config.as_bytes());
+
+ Ok(())
+ } else {
+ Err(ConfigError::DefaultConfigError)
+ }
+ }
+}
diff --git a/src/coords/cms.rs b/src/coords/cms.rs
new file mode 100644
index 0000000..772b077
--- /dev/null
+++ b/src/coords/cms.rs
@@ -0,0 +1,60 @@
+use std::ops::Range;
+use geo_types::LineString;
+use crate::coords::Mapper;
+
+#[derive(Debug, Clone)]
+pub struct CMS {
+ mapper: Mapper,
+ window_size: (f32, f32),
+ bounds: (f64, f64, f64, f64),
+}
+
+unsafe impl Send for CMS {}
+unsafe impl Sync for CMS {}
+
+impl CMS {
+ pub fn new(mapper: Mapper, window_size: (f32, f32)) -> Self {
+ let bounds = mapper.get_bounds();
+ Self {
+ mapper,
+ window_size,
+ bounds,
+ }
+ }
+
+ pub fn set_lat_range(&mut self, lat_range: Range) {
+ self.mapper.set_lat_range(lat_range);
+ self.bounds = self.mapper.get_bounds()
+ }
+
+ pub fn set_lon_range(&mut self, lon_range: Range) {
+ self.mapper.set_lon_range(lon_range);
+ self.bounds = self.mapper.get_bounds();
+ }
+
+ pub fn map(&self, loc: (f64, f64)) -> Option<(f32, f32)> {
+ self.mapper.map(loc).ok().map(|(x, y)| {
+ // println!("x: {}, y: {}", x, y);
+ let (w, h) = self.window_size;
+ let (w, h) = (w as f64, h as f64);
+ let (x, y) = (x - self.bounds.0, y - self.bounds.2);
+ // TODO: check if the following line is correct : 1.0 - y / (self.bounds.3 - self.bounds.2)
+ let (x, y) = (
+ x / (self.bounds.1 - self.bounds.0),
+ y / (self.bounds.3 - self.bounds.2),
+ );
+ let (x, y) = (x * w, y * h);
+ (x as f32, y as f32)
+ })
+ }
+
+ pub fn ring_map(&self, line: &LineString) -> Option> {
+ Some(
+ line.points()
+ .into_iter()
+ .map(|p| self.map((p.x(), p.y())).unwrap())
+ .collect::>()
+ .into(),
+ )
+ }
+}
diff --git a/src/coords/mapper.rs b/src/coords/mapper.rs
new file mode 100644
index 0000000..d6f31e9
--- /dev/null
+++ b/src/coords/mapper.rs
@@ -0,0 +1,252 @@
+use crate::widgets::render::WindowCoord;
+use std::borrow::ToOwned;
+
+use super::{proj::ProjectionS, Range};
+use geo_types::{coord, Coord as GCoord, LineString};
+use proj::{Proj, ProjError};
+
+pub struct Mapper {
+ proj: Proj,
+ pub range: (Range, Range),
+ bounds: (f64, f64, f64, f64),
+}
+
+unsafe impl Send for Mapper {}
+unsafe impl Sync for Mapper {}
+
+impl std::fmt::Debug for Mapper {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Mapper")
+ .field("proj", &self.proj)
+ .field("range", &self.range)
+ .field("bounds", &self.bounds)
+ .finish()
+ }
+}
+
+impl Clone for Mapper {
+ fn clone(&self) -> Self {
+ let c = self.proj.proj_info();
+ let new_proj = Proj::new(c.definition.unwrap().as_str()).unwrap();
+ Self {
+ proj: new_proj,
+ range: self.range,
+ bounds: self.bounds,
+ }
+ }
+}
+
+impl From for Mapper {
+ fn from(proj: Proj) -> Self {
+ let default_range: (Range, Range) = ((-180.0..180.0).into(), (-81.0..81.0).into());
+ let bounds = Self::bound(&proj, default_range.clone()).unwrap();
+ Self {
+ proj,
+ range: (default_range.0.into(), default_range.1.into()),
+ bounds,
+ }
+ }
+}
+
+impl From for Mapper {
+ fn from(proj: C) -> Self {
+ let p = Proj::new(proj.build().as_str()).unwrap();
+ p.into()
+ }
+}
+
+impl Mapper {
+ pub fn new(
+ proj: Proj,
+ lon_range: std::ops::Range,
+ lat_range: std::ops::Range,
+ ) -> 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,
+ range,
+ bounds,
+ }
+ }
+
+ pub fn inverse_map(&self, coord: (f64, f64)) -> Result<(f64, f64), ProjError> {
+ let (x, y) = coord;
+ // let c = (x as f64) * (self.bounds.1 - self.bounds.0) + self.bounds.0;
+ // let d = ((1.0 - y) as f64) * (self.bounds.3 - self.bounds.2) + self.bounds.2;
+ let (lon, lat) = self.proj.project((x, y), true)?;
+ Ok((lon.to_degrees(), lat.to_degrees()))
+ }
+
+ pub fn point_in_bound(&self, point: (f64, f64)) -> bool {
+ let (x, y) = point;
+ let (lon_range, lat_range) = self.range;
+ return (x <= lon_range.1 && x >= lon_range.0) && (y <= lat_range.1 && y >= lat_range.0);
+ }
+
+ pub fn get_bounds(&self) -> (f64, f64, f64, f64) {
+ self.bounds
+ }
+
+ pub fn map(&self, point: (f64, f64)) -> Result<(f64, f64), ProjError> {
+ let mut point = point;
+ // if !self.point_in_bound(point) {
+ // point = (
+ // point.0.clamp(self.range.0 .0, self.range.0 .1),
+ // point.1.clamp(self.range.1 .0, self.range.1 .1),
+ // );
+ // }
+ let (p1, p2) = self
+ .proj
+ .convert((point.0.to_radians(), point.1.to_radians()))?;
+
+ // let x = (p1 - self.bounds.0) / (self.bounds.1 - self.bounds.0);
+ // let y = (p2 - self.bounds.2) / (self.bounds.3 - self.bounds.2);
+
+ // Ok((x, (1.0 - y)))
+ //
+ Ok((p1, p2))
+ }
+
+ pub fn set_lon_range(&mut self, range: std::ops::Range) -> &mut Self {
+ self.range.0 = range.into();
+ self.bounds = Self::bound(&self.proj, self.range.clone()).unwrap();
+ self
+ }
+
+ pub fn set_lat_range(&mut self, range: std::ops::Range) -> &mut Self {
+ self.range.1 = range.into();
+ self.bounds = Self::bound(&self.proj, self.range.clone()).unwrap();
+ self
+ }
+
+ 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))
+ }
+
+ pub fn ring_map(&self, ring: &LineString) -> Result {
+ let mut result = Vec::new();
+ let projed = self.map_line(ring)?;
+ for (l, p) in ring.lines().zip(projed.lines()) {
+ let start_projected: (f64, f64) = p.start.into();
+ let end_projected: (f64, f64) = p.end.into();
+ let cartesian_start = self.cartesian(l.start.x, l.start.y);
+ let cartesian_end = self.cartesian(l.end.x, l.end.y);
+
+ let delta2 = 0.5;
+ let depth = 16;
+ let mut res: Vec = Vec::new();
+ res.push(p.start);
+ self.resample_line_to(
+ start_projected,
+ end_projected,
+ l.start.x,
+ l.end.x,
+ cartesian_start,
+ cartesian_end,
+ &|x| self.map((x.x, x.y)),
+ delta2,
+ depth,
+ &mut res,
+ )?;
+ res.push(p.end);
+ result.extend(res);
+ }
+
+ Ok(LineString::new(result))
+ }
+
+ pub fn map_line(&self, line: &LineString) -> Result {
+ let result: Result =
+ line.points().map(|p| self.map((p.x(), p.y()))).collect();
+
+ Ok(result?)
+ }
+
+ #[inline]
+ fn cartesian(&self, lambda: f64, phi: f64) -> (f64, f64, f64) {
+ let cos_phi = phi.cosh();
+ return (cos_phi * lambda.cosh(), cos_phi * lambda.sinh(), phi.sinh());
+ }
+
+ fn resample_line_to(
+ &self,
+ start_projected: (f64, f64),
+ end_projected: (f64, f64),
+ lambda0: f64,
+ lambda1: f64,
+ cartesian_start: (f64, f64, f64),
+ cartesian_end: (f64, f64, f64),
+ project: &F,
+ delta2: f64,
+ depth: u8,
+ result: &mut Vec
[…] earlier. It’s the result of a conversion of a polygon shapefile of country boundaries (from Natural Earth, a fantastic, public domain, physical/cultural spatial data source) to a raster data […]
+ + +[…] Le mappe sono scaricate da https://www.naturalearthdata.com […]
+ + +[…] Le mappe sono scaricate da https://www.naturalearthdata.com […]
+ + +