use crate::{
DiagramItem, LibraryModel, MainWindow,
bridge::library::source::{self as library_source, LibrarySource},
settings::WorkerSettings,
};
use anyhow::Result;
use indicatrix_net::library::{
DesignSummary, LibraryRequest, LibraryResponse, PerformanceAggregateWire, PerformanceBoundWire,
PerformanceFilterWire, PerformanceMetricWire, RangeFilterWire,
};
use indicatrix_vault::{
db::sqlite::{Database, DisplayFilters, SortOrder},
model::{
entry::DiagramListItem,
filter::RangeFilter,
performance::{
PerformanceAggregate, PerformanceBound, PerformanceFilter, PerformanceMetric,
},
},
};
use slint::{ComponentHandle, Model, ModelRc, VecModel};
use std::sync::{Arc, Mutex};
use tracing::error;
fn active_bound(value: f32, bound_edge: f32) -> Option<f64> {
let moved_off_edge = (value - bound_edge).abs() >= 1e-6;
moved_off_edge.then_some(f64::from(value))
}
#[must_use]
pub fn read_range_filter(ui: &MainWindow) -> RangeFilter {
RangeFilter {
ri_min: active_bound(
ui.global::<LibraryModel>().get_ri_filter_min(),
ui.global::<LibraryModel>().get_ri_bounds_min(),
),
ri_max: active_bound(
ui.global::<LibraryModel>().get_ri_filter_max(),
ui.global::<LibraryModel>().get_ri_bounds_max(),
),
lw_min: active_bound(
ui.global::<LibraryModel>().get_lw_filter_min(),
ui.global::<LibraryModel>().get_lw_bounds_min(),
),
lw_max: active_bound(
ui.global::<LibraryModel>().get_lw_filter_max(),
ui.global::<LibraryModel>().get_lw_bounds_max(),
),
volume_min: active_bound(
ui.global::<LibraryModel>().get_volume_filter_min(),
ui.global::<LibraryModel>().get_volume_bounds_min(),
),
volume_max: active_bound(
ui.global::<LibraryModel>().get_volume_filter_max(),
ui.global::<LibraryModel>().get_volume_bounds_max(),
),
facets_min: active_bound(
ui.global::<LibraryModel>().get_facets_filter_min(),
ui.global::<LibraryModel>().get_facets_bounds_min(),
)
.map(|v| v.round() as i64),
facets_max: active_bound(
ui.global::<LibraryModel>().get_facets_filter_max(),
ui.global::<LibraryModel>().get_facets_bounds_max(),
)
.map(|v| v.round() as i64),
ri_tolerance: ui
.global::<LibraryModel>()
.get_ri_tolerance_enabled()
.then(|| {
(
f64::from(ui.global::<LibraryModel>().get_ri_tolerance_center()),
f64::from(ui.global::<LibraryModel>().get_ri_tolerance_value()),
)
}),
performance: read_performance_filters(ui),
include_ignored: ui.global::<LibraryModel>().get_show_ignored(),
}
}
#[must_use]
pub fn read_sort_order(ui: &MainWindow) -> SortOrder {
match ui.global::<LibraryModel>().get_sort_order_index() {
1 => SortOrder::Title,
2 => SortOrder::Newest,
3 => SortOrder::RecentlyEdited,
_ => SortOrder::CatalogueOrder,
}
}
#[must_use]
pub fn read_local_only(ui: &MainWindow) -> bool {
ui.global::<LibraryModel>().get_local_only_filter()
}
#[must_use]
pub fn read_tag_filter(ui: &MainWindow) -> Option<String> {
let name = ui.global::<LibraryModel>().get_active_tag_filter_name();
(!name.is_empty()).then(|| name.to_string())
}
#[must_use]
pub fn read_id_filter(ui: &MainWindow) -> Option<Vec<i64>> {
let model = ui.global::<LibraryModel>().get_recent_import_filter();
if model.row_count() == 0 {
return None;
}
Some(model.iter().map(i64::from).collect())
}
fn read_performance_filters(ui: &MainWindow) -> Vec<PerformanceFilter> {
let rows = ui.global::<LibraryModel>().get_performance_filters();
let mut filters = Vec::with_capacity(rows.row_count());
let mut rejected = 0usize;
for row in rows.iter() {
let metric = match row.metric_index {
0 => PerformanceMetric::Brilliance,
1 => PerformanceMetric::Extinction,
_ => PerformanceMetric::Windowing,
};
let bound = if row.bound_index == 0 {
PerformanceBound::AtMost(row.threshold_pct)
} else {
PerformanceBound::AtLeast(row.threshold_pct)
};
let aggregate = if row.aggregate_index == 0 {
PerformanceAggregate::Worst
} else {
PerformanceAggregate::Mean
};
match PerformanceFilter::new(metric, bound, row.tilt_radius_deg, aggregate) {
Ok(filter) => filters.push(filter),
Err(_) => rejected += 1,
}
}
if rejected > 0 {
ui.global::<LibraryModel>().set_status_message(
format!(
"{rejected} tilt-performance filter(s) had an invalid tilt radius and were \
ignored."
)
.into(),
);
}
filters
}
pub fn refresh_diagram_list_via_source(
ui: &MainWindow,
db_mutex: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
search: &str,
shape_filter: &str,
gear_filter: &str,
) {
let current = source
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
match current {
LibrarySource::Local => {
refresh_diagram_list(ui, db_mutex, search, shape_filter, gear_filter);
}
LibrarySource::Remote(worker) => {
refresh_diagram_list_remote(ui, worker, search, shape_filter, gear_filter);
}
}
}
pub(crate) fn refresh_diagram_list_remote(
ui: &MainWindow,
worker: WorkerSettings,
search: &str,
shape_filter: &str,
gear_filter: &str,
) {
let clean_shape = if shape_filter == "All Shapes" {
"All".to_string()
} else {
shape_filter.to_string()
};
let clean_gear = if gear_filter == "All Gears" {
"All".to_string()
} else {
gear_filter.to_string()
};
let range = to_range_wire(&read_range_filter(ui));
let request = LibraryRequest::Search {
query: search.to_string(),
shape_filter: clean_shape,
gear_filter: clean_gear,
range,
};
library_source::spawn_library_request(
ui.as_weak(),
worker,
request,
|ui, result| match result {
Ok(LibraryResponse::SearchResults {
items,
excluded_for_missing_curves,
}) => {
let total = items.len();
let slint_items: Vec<DiagramItem> = items.iter().map(to_diagram_item).collect();
ui.global::<LibraryModel>()
.set_diagram_list(ModelRc::new(VecModel::from(slint_items)));
ui.global::<LibraryModel>().set_total_count(total as i32);
ui.global::<LibraryModel>().set_matched_count(total as i32);
ui.global::<LibraryModel>()
.set_performance_excluded_count(excluded_for_missing_curves as i32);
}
Ok(_) => {
ui.global::<LibraryModel>()
.set_status_message("Unexpected reply searching the remote library.".into());
}
Err(e) => {
error!("Remote search failed: {e}");
ui.global::<LibraryModel>()
.set_status_message(format!("Remote search failed: {e}").into());
}
},
);
}
fn to_range_wire(range: &RangeFilter) -> RangeFilterWire {
RangeFilterWire {
ri_min: range.ri_min,
ri_max: range.ri_max,
lw_min: range.lw_min,
lw_max: range.lw_max,
volume_min: range.volume_min,
volume_max: range.volume_max,
facets_min: range.facets_min,
facets_max: range.facets_max,
ri_tolerance: range.ri_tolerance,
include_ignored: range.include_ignored,
performance: range
.performance
.iter()
.map(to_performance_filter_wire)
.collect(),
}
}
const fn to_performance_filter_wire(filter: &PerformanceFilter) -> PerformanceFilterWire {
let metric = match filter.metric {
PerformanceMetric::Brilliance => PerformanceMetricWire::Brilliance,
PerformanceMetric::Extinction => PerformanceMetricWire::Extinction,
PerformanceMetric::Windowing => PerformanceMetricWire::Windowing,
};
let bound = match filter.bound {
PerformanceBound::AtMost(t) => PerformanceBoundWire::AtMost(t),
PerformanceBound::AtLeast(t) => PerformanceBoundWire::AtLeast(t),
};
let aggregate = match filter.aggregate {
PerformanceAggregate::Worst => PerformanceAggregateWire::Worst,
PerformanceAggregate::Mean => PerformanceAggregateWire::Mean,
};
PerformanceFilterWire {
metric,
bound,
tilt_radius_deg: filter.tilt_radius_deg,
aggregate,
}
}
fn is_local_url(url: &str) -> bool {
url.starts_with("local://")
}
pub(crate) fn to_diagram_item(item: &DesignSummary) -> DiagramItem {
DiagramItem {
id: item.entry_id as i32,
title: item.title.clone().into(),
shape: item.shape.clone().unwrap_or_default().into(),
gear: item.index_gear.clone().unwrap_or_default().into(),
facets: item.facets_count.clone().unwrap_or_default().into(),
designer: item.designer_info.clone().unwrap_or_default().into(),
lw_ratio: item.lw_ratio.clone().unwrap_or_default().into(),
ri: item.refractive_index.clone().unwrap_or_default().into(),
ignored: item.ignored,
is_local: is_local_url(&item.url),
tags: ModelRc::new(VecModel::from(Vec::<slint::SharedString>::new())),
}
}
pub struct DiagramListRow {
pub item: DiagramListItem,
pub ignored: bool,
pub tags: Vec<String>,
}
pub struct FetchedDiagramList {
pub rows: Vec<DiagramListRow>,
pub total: usize,
pub matched_count: usize,
pub excluded_for_missing_curves: usize,
}
pub fn fetch_diagram_list(
db_mutex: &Arc<Mutex<Database>>,
search: &str,
shape_filter: &str,
gear_filter: &str,
range: &RangeFilter,
) -> Result<FetchedDiagramList> {
fetch_diagram_list_with_options(
db_mutex,
search,
shape_filter,
gear_filter,
range,
DisplaySearchOptions::default(),
)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DisplaySearchOptions<'a> {
pub order: SortOrder,
pub local_only: bool,
pub tag_filter: Option<&'a str>,
pub id_filter: Option<&'a [i64]>,
}
pub fn fetch_diagram_list_with_options(
db_mutex: &Arc<Mutex<Database>>,
search: &str,
shape_filter: &str,
gear_filter: &str,
range: &RangeFilter,
options: DisplaySearchOptions<'_>,
) -> Result<FetchedDiagramList> {
let clean_shape = if shape_filter == "All Shapes" {
"All"
} else {
shape_filter
};
let clean_gear = if gear_filter == "All Gears" {
"All"
} else {
gear_filter
};
let (rows, total, matched_count, excluded_for_missing_curves) = {
let db = db_mutex
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let tag_filter_id = options
.tag_filter
.and_then(|name| db.tag_id_by_name(name).ok().flatten());
let filters = DisplayFilters {
order: options.order,
local_only: options.local_only,
tag_filter: tag_filter_id,
id_filter: options.id_filter,
};
let result = db.search_diagrams_display(search, clean_shape, clean_gear, range, filters)?;
let total = db.get_total_count().unwrap_or(result.items.len());
let matched_count = db
.count_matching_diagrams(search, clean_shape, clean_gear, range, filters)
.unwrap_or(result.items.len());
let mut tags_by_entry = db.tags_by_entry().unwrap_or_default();
let rows: Vec<DiagramListRow> = result
.items
.into_iter()
.map(|item| {
let ignored = item.ignored;
let tags = tags_by_entry.remove(&item.id).unwrap_or_default();
DiagramListRow {
item,
ignored,
tags,
}
})
.collect();
drop(db);
(
rows,
total,
matched_count,
result.excluded_for_missing_curves,
)
};
Ok(FetchedDiagramList {
rows,
total,
matched_count,
excluded_for_missing_curves,
})
}
pub fn apply_diagram_list_to_ui(ui: &MainWindow, fetched: FetchedDiagramList) {
let slint_items: Vec<DiagramItem> = fetched
.rows
.into_iter()
.map(|row| DiagramItem {
id: row.item.id as i32,
title: row.item.title.into(),
shape: row.item.shape.unwrap_or_default().into(),
gear: row.item.index_gear.unwrap_or_default().into(),
facets: row.item.facets_count.unwrap_or_default().into(),
designer: row.item.designer_info.unwrap_or_default().into(),
lw_ratio: row.item.lw_ratio.unwrap_or_default().into(),
ri: row.item.refractive_index.unwrap_or_default().into(),
ignored: row.ignored,
is_local: is_local_url(&row.item.url),
tags: ModelRc::new(VecModel::from(
row.tags
.into_iter()
.map(Into::into)
.collect::<Vec<slint::SharedString>>(),
)),
})
.collect();
ui.global::<LibraryModel>()
.set_diagram_list(ModelRc::new(VecModel::from(slint_items)));
ui.global::<LibraryModel>()
.set_total_count(fetched.total as i32);
ui.global::<LibraryModel>()
.set_matched_count(fetched.matched_count as i32);
ui.global::<LibraryModel>()
.set_performance_excluded_count(fetched.excluded_for_missing_curves as i32);
}
pub fn refresh_diagram_list(
ui: &MainWindow,
db_mutex: &Arc<Mutex<Database>>,
search: &str,
shape_filter: &str,
gear_filter: &str,
) {
let range = read_range_filter(ui);
let order = read_sort_order(ui);
let local_only = read_local_only(ui);
let tag_filter = read_tag_filter(ui);
let id_filter = read_id_filter(ui);
match fetch_diagram_list_with_options(
db_mutex,
search,
shape_filter,
gear_filter,
&range,
DisplaySearchOptions {
order,
local_only,
tag_filter: tag_filter.as_deref(),
id_filter: id_filter.as_deref(),
},
) {
Ok(fetched) => apply_diagram_list_to_ui(ui, fetched),
Err(e) => {
error!("Failed to search diagrams: {:?}", e);
ui.global::<LibraryModel>()
.set_status_message(format!("Error searching database: {e}").into());
}
}
}