geop_ops_model/parameters/
configurations.rs1use geop_core_editor::{
23 editor::{Changed, EditorContext},
24 registry::Panel,
25};
26use geop_core_math::geop_error::{GeopError, GeopResult};
27use geop_core_program::{
28 numeric::{evaluate, read_parameter},
29 program::{Program, ProgramStep},
30 reader::Reader,
31};
32use serde::{Deserialize, Serialize};
33
34use crate::parameters::{
35 definition::{CONFIGURATION, Row, validate_name},
36 steps::{LOOKUP, LookupTableArgs},
37};
38
39pub struct ConfigurationsPanel;
41
42#[derive(Clone, Debug, PartialEq, Serialize)]
44pub struct TableChoice {
45 pub step: String,
47 pub input: String,
50 pub rows: Vec<String>,
52 pub selected: String,
54}
55
56#[derive(Clone, Debug, PartialEq, Serialize)]
58pub struct ConfigurationsState {
59 pub tables: Vec<TableChoice>,
62}
63
64#[derive(Clone, Debug, Serialize, Deserialize)]
66#[serde(tag = "command", rename_all = "snake_case")]
67pub enum ConfigurationsCommand {
68 Select { input: String, row: String },
70 Add {
74 #[serde(default)]
75 name: Option<String>,
76 },
77 Suppress { step: String },
81}
82
83impl Panel for ConfigurationsPanel {
84 const KEY: &'static str = "configurations";
85 type State = ConfigurationsState;
86 type Command = ConfigurationsCommand;
87
88 fn state(&self, program: &Program, _: &Reader) -> ConfigurationsState {
89 let mut tables: Vec<TableChoice> = (program.steps.iter())
90 .filter(|step| step.operation == LOOKUP)
91 .filter_map(|step| {
92 let table = serde_json::from_value::<LookupTableArgs>(step.args.clone()).ok()?;
93 Some(TableChoice {
94 step: step.id.clone(),
95 input: table.input,
96 rows: table.rows.into_iter().map(|row| row.name).collect(),
97 selected: table.selected,
98 })
99 })
100 .collect();
101 tables.sort_by_key(|table| table.input != CONFIGURATION);
102 ConfigurationsState { tables }
103 }
104
105 fn command(
106 &self,
107 ctx: &mut EditorContext,
108 command: ConfigurationsCommand,
109 ) -> GeopResult<Changed> {
110 match command {
111 ConfigurationsCommand::Select { input, row } => {
112 ctx.edit_program(|program| select(program, &input, &row))?;
113 }
114 ConfigurationsCommand::Add { name } => {
115 ctx.idle()?;
116 ctx.edit_program(|program| add(program, name.as_deref()))?;
117 }
118 ConfigurationsCommand::Suppress { step } => {
119 let runs = runs(ctx.program(), &ctx.part(), &step)?;
120 ctx.edit_program(|program| suppress(program, &step, runs))?;
121 }
122 }
123 Ok(Changed::Program)
124 }
125}
126
127fn table(program: &Program, input: &str) -> Option<(usize, LookupTableArgs)> {
130 (program.steps.iter().enumerate()).find_map(|(at, step)| {
131 let table = serde_json::from_value::<LookupTableArgs>(step.args.clone()).ok()?;
132 (step.operation == LOOKUP && table.input == input).then_some((at, table))
133 })
134}
135
136fn write_table(program: &mut Program, at: usize, table: &LookupTableArgs) -> GeopResult<()> {
138 let step = (program.steps.get_mut(at))
139 .ok_or_else(|| GeopError::new(format!("the program has no step at {at}")))?;
140 step.args = serde_json::to_value(table)
141 .map_err(|e| GeopError::new(format!("the table {:?}: {e}", table.input)))?;
142 Ok(())
143}
144
145fn select(program: &mut Program, input: &str, row: &str) -> GeopResult<()> {
147 let (at, mut table) = table(program, input)
148 .ok_or_else(|| GeopError::new(format!("the program looks up no table by {input:?}")))?;
149 if !table.rows.iter().any(|r| r.name == row) {
150 let rows: Vec<&str> = table.rows.iter().map(|r| r.name.as_str()).collect();
151 return Err(GeopError::new(format!(
152 "the table {input:?} has no row {row:?}: its rows are {}",
153 rows.join(", ")
154 )));
155 }
156 table.selected = row.to_string();
157 write_table(program, at, &table)
158}
159
160fn add(program: &mut Program, name: Option<&str>) -> GeopResult<()> {
165 let (at, mut table) = match table(program, CONFIGURATION) {
166 Some(found) => found,
167 None => {
168 let table = LookupTableArgs {
169 input: CONFIGURATION.into(),
170 columns: Vec::new(),
171 rows: vec![Row {
172 name: "Default".into(),
173 values: Vec::new(),
174 }],
175 selected: "Default".into(),
176 };
177 let id = (std::iter::once("configurations".to_string()))
178 .chain((2..).map(|n| format!("configurations{n}")))
179 .find(|id| program.step(id).is_err())
180 .unwrap_or_default();
181 program.steps.insert(0, ProgramStep::of(id, &table)?);
182 (0, table)
183 }
184 };
185 let taken = |name: &str| table.rows.iter().any(|r| r.name == name);
186 let name = match name.map(str::trim) {
187 Some("") => return Err(GeopError::new("a configuration needs a name")),
188 Some(name) if taken(name) => {
189 return Err(GeopError::new(format!(
190 "there is a configuration {name:?} already"
191 )));
192 }
193 Some(name) => name.to_string(),
194 None => (2..)
195 .map(|n| format!("Configuration {n}"))
196 .find(|name| !taken(name))
197 .unwrap_or_default(),
198 };
199 let values = (table.rows.iter())
200 .find(|r| r.name == table.selected)
201 .map(|r| r.values.clone())
202 .unwrap_or_else(|| vec![0.0; table.columns.len()]);
203 table.rows.push(Row {
204 name: name.clone(),
205 values,
206 });
207 table.selected = name;
208 write_table(program, at, &table)
209}
210
211fn runs(program: &Program, part: &Reader, id: &str) -> GeopResult<bool> {
215 let step = program
216 .step(id)
217 .map_err(|e| e.with_context("suppressing a step: commit it first"))?;
218 Ok(match &step.active {
219 None => true,
220 Some(formula) => {
221 !matches!(evaluate(formula, |name| read_parameter(part, name)), Ok(value) if value == 0.0)
222 }
223 })
224}
225
226fn column_of(id: &str) -> String {
229 let name: String = (id.chars())
230 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
231 .collect();
232 match name.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') {
233 true => name,
234 false => format!("_{name}"),
235 }
236}
237
238fn suppress(program: &mut Program, id: &str, runs: bool) -> GeopResult<()> {
245 let at = program.index_of(id)?;
246 let configurations = table(program, CONFIGURATION).filter(|(_, table)| table.rows.len() > 1);
247 let Some((table_at, mut table)) = configurations else {
248 program.step_mut(id)?.active = (runs).then(|| "0".to_string());
249 return Ok(());
250 };
251 if table_at > at {
252 return Err(GeopError::new(format!(
253 "the configurations are looked up after the step `{id}`, by the step `{}`: move \
254 that step before it to suppress `{id}` in one configuration",
255 program
256 .steps
257 .get(table_at)
258 .map_or("", |step| step.id.as_str())
259 )));
260 }
261 let read = |column: &str| format!("{CONFIGURATION}.{column}");
264 let active = program.step(id)?.active.clone();
265 let column = match (table.columns.iter()).find(|c| active.as_deref() == Some(read(c).as_str()))
266 {
267 Some(column) => column.clone(),
268 None => {
269 let base = column_of(id);
270 validate_name(&base)?;
271 let column = (std::iter::once(base.clone()))
272 .chain((2..).map(|n| format!("{base}{n}")))
273 .find(|c| !table.columns.contains(c))
274 .unwrap_or(base);
275 table.columns.push(column.clone());
276 for row in &mut table.rows {
277 row.values.push(f64::from(u8::from(runs)));
278 }
279 column
280 }
281 };
282 let k = (table.columns.iter().position(|c| *c == column))
283 .ok_or_else(|| GeopError::new(format!("the table has no column {column:?}")))?;
284 let selected = table.selected.clone();
285 if let Some(row) = table.rows.iter_mut().find(|r| r.name == selected)
286 && let Some(value) = row.values.get_mut(k)
287 {
288 *value = f64::from(u8::from(!runs));
289 }
290 let everywhere = (table.rows.iter()).all(|r| r.values.get(k).is_some_and(|v| *v != 0.0));
292 let active = match everywhere {
293 true => {
294 table.columns.retain(|c| *c != column);
295 for row in &mut table.rows {
296 row.values = (row.values.iter().enumerate())
297 .filter(|(j, _)| *j != k)
298 .map(|(_, v)| *v)
299 .collect();
300 }
301 None
302 }
303 false => Some(read(&column)),
304 };
305 write_table(program, table_at, &table)?;
306 program.step_mut(id)?.active = active;
307 Ok(())
308}