ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
cvQueryWidgets.cpp
Go to the documentation of this file.
1 // ----------------------------------------------------------------------------
2 // - CloudViewer: www.cloudViewer.org -
3 // ----------------------------------------------------------------------------
4 // Copyright (c) 2018-2024 www.cloudViewer.org
5 // SPDX-License-Identifier: MIT
6 // ----------------------------------------------------------------------------
7 
8 #include "cvQueryWidgets.h"
9 
10 #include <CVLog.h>
11 
12 //=============================================================================
13 // cvQueryValueWidget Implementation
14 //=============================================================================
15 
17  : QWidget(parent), m_type(NO_VALUE) {
18  rebuildUI();
19 }
20 
22  if (m_type != type) {
23  m_type = type;
24  rebuildUI();
25  }
26 }
27 
28 QMap<QString, QString> cvQueryValueWidget::values() const {
29  QMap<QString, QString> result;
30  for (auto it = m_lineEdits.constBegin(); it != m_lineEdits.constEnd();
31  ++it) {
32  result[it.key()] = it.value()->text();
33  }
34  return result;
35 }
36 
37 void cvQueryValueWidget::setValues(const QMap<QString, QString>& values) {
38  for (auto it = values.constBegin(); it != values.constEnd(); ++it) {
39  if (m_lineEdits.contains(it.key())) {
40  m_lineEdits[it.key()]->setText(it.value());
41  }
42  }
43 }
44 
46  for (auto* edit : m_lineEdits) {
47  edit->clear();
48  }
49 }
50 
51 void cvQueryValueWidget::rebuildUI() {
52  // Delete existing widgets
53  qDeleteAll(findChildren<QWidget*>(QString(), Qt::FindDirectChildrenOnly));
54  m_lineEdits.clear();
55  delete layout();
56 
57  switch (m_type) {
58  case NO_VALUE:
59  // No UI needed
60  break;
61 
62  case SINGLE_VALUE:
64  auto* vbox = new QVBoxLayout(this);
65  vbox->setContentsMargins(0, 0, 0, 0);
66  vbox->setSpacing(3);
67 
68  auto* edit = new QLineEdit(this);
69  edit->setObjectName("value");
70  if (m_type == SINGLE_VALUE) {
71  edit->setPlaceholderText(tr("value"));
72  } else {
73  edit->setPlaceholderText(tr("comma separated values"));
74  }
75  vbox->addWidget(edit);
76  m_lineEdits["value"] = edit;
77 
78  connect(edit, &QLineEdit::textChanged, this,
80  break;
81  }
82 
83  case RANGE_PAIR: {
84  auto* hbox = new QHBoxLayout(this);
85  hbox->setContentsMargins(0, 0, 0, 0);
86  hbox->setSpacing(3);
87 
88  auto* editMin = new QLineEdit(this);
89  editMin->setObjectName("value_min");
90  editMin->setPlaceholderText(tr("minimum"));
91  m_lineEdits["value_min"] = editMin;
92 
93  auto* label = new QLabel(tr("and"), this);
94  label->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
95 
96  auto* editMax = new QLineEdit(this);
97  editMax->setObjectName("value_max");
98  editMax->setPlaceholderText(tr("maximum"));
99  m_lineEdits["value_max"] = editMax;
100 
101  hbox->addWidget(editMin, 1);
102  hbox->addWidget(label, 0);
103  hbox->addWidget(editMax, 1);
104 
105  connect(editMin, &QLineEdit::textChanged, this,
107  connect(editMax, &QLineEdit::textChanged, this,
109  break;
110  }
111 
112  case LOCATION:
114  auto* grid = new QGridLayout(this);
115  grid->setContentsMargins(0, 0, 0, 0);
116  grid->setVerticalSpacing(3);
117  grid->setHorizontalSpacing(3);
118 
119  auto* editX = new QLineEdit(this);
120  editX->setObjectName("value_x");
121  editX->setPlaceholderText(tr("X"));
122  m_lineEdits["value_x"] = editX;
123 
124  auto* editY = new QLineEdit(this);
125  editY->setObjectName("value_y");
126  editY->setPlaceholderText(tr("Y"));
127  m_lineEdits["value_y"] = editY;
128 
129  auto* editZ = new QLineEdit(this);
130  editZ->setObjectName("value_z");
131  editZ->setPlaceholderText(tr("Z"));
132  m_lineEdits["value_z"] = editZ;
133 
134  grid->addWidget(editX, 0, 0);
135  grid->addWidget(editY, 0, 1);
136  grid->addWidget(editZ, 0, 2);
137 
138  connect(editX, &QLineEdit::textChanged, this,
140  connect(editY, &QLineEdit::textChanged, this,
142  connect(editZ, &QLineEdit::textChanged, this,
144 
145  if (m_type == LOCATION_WITH_TOLERANCE) {
146  auto* editTolerance = new QLineEdit(this);
147  editTolerance->setObjectName("value_tolerance");
148  editTolerance->setPlaceholderText(tr("within epsilon"));
149  m_lineEdits["value_tolerance"] = editTolerance;
150  grid->addWidget(editTolerance, 1, 0, 1, 3);
151 
152  connect(editTolerance, &QLineEdit::textChanged, this,
154  }
155  break;
156  }
157  }
158 }
159 
160 //=============================================================================
161 // cvQueryConditionWidget Implementation
162 //=============================================================================
163 
165  : QWidget(parent),
166  m_termCombo(new QComboBox(this)),
167  m_operatorCombo(new QComboBox(this)),
168  m_valueWidget(new cvQueryValueWidget(this)) {
169  m_termCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
170  m_operatorCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
171 
172  auto* hbox = new QHBoxLayout(this);
173  hbox->setContentsMargins(0, 0, 0, 0);
174  hbox->setSpacing(3);
175  hbox->addWidget(m_termCombo, 0, Qt::AlignTop);
176  hbox->addWidget(m_operatorCombo, 0, Qt::AlignTop);
177  hbox->addWidget(m_valueWidget, 1, Qt::AlignTop);
178 
179  connect(m_termCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
180  this, &cvQueryConditionWidget::onTermChanged);
181  connect(m_operatorCombo,
182  QOverload<int>::of(&QComboBox::currentIndexChanged), this,
183  &cvQueryConditionWidget::onOperatorChanged);
184  connect(m_valueWidget, &cvQueryValueWidget::valueChanged, this,
186 }
187 
189  const QStringList& arrayNames,
190  const QMap<QString, int>& arrayComponents,
191  bool isPointData) {
192  const QSignalBlocker blocker(m_termCombo);
193  m_termCombo->clear();
194 
195  // Add ID first (ParaView style)
196  addTerm(tr("ID"), ARRAY, "id");
197 
198  // Add arrays
199  for (const QString& arrayName : arrayNames) {
200  int numComponents = arrayComponents.value(arrayName, 1);
201 
202  if (numComponents == 1) {
203  // Single component array
204  addTerm(arrayName, ARRAY, arrayName);
205  } else if (numComponents > 1) {
206  // Multi-component: add magnitude first, then components (lowercase
207  // 'magnitude' to match ParaView)
208  addTerm(QString("%1 (magnitude)").arg(arrayName), ARRAY,
209  QString("mag(%1)").arg(arrayName));
210 
211  // Component names
212  QStringList compNames;
213  if (numComponents == 3) {
214  compNames << "X" << "Y" << "Z";
215  } else if (numComponents == 4) {
216  compNames << "X" << "Y" << "Z" << "W";
217  } else if (numComponents == 2) {
218  compNames << "X" << "Y";
219  } else {
220  for (int i = 0; i < numComponents; ++i) {
221  compNames << QString::number(i);
222  }
223  }
224 
225  for (int i = 0; i < numComponents && i < compNames.size(); ++i) {
226  addTerm(QString("%1 (%2)").arg(arrayName, compNames[i]), ARRAY,
227  QString("%1[:,%2]").arg(arrayName).arg(i));
228  }
229  }
230  }
231 
232  // Add Point/Cell location query
233  if (isPointData) {
234  addTerm(tr("Point"), POINT_NEAREST_TO, "inputs");
235  } else {
236  addTerm(tr("Cell"), CELL_CONTAINING_POINT, "inputs");
237  }
238 
239  // Initialize operators for first term
240  if (m_termCombo->count() > 0) {
241  m_termCombo->setCurrentIndex(0);
242  onTermChanged(0);
243  }
244 }
245 
247  QString expr = m_operatorCombo->currentData(ExprTemplateRole).toString();
248  if (expr.isEmpty()) {
249  return QString();
250  }
251 
252  QMap<QString, QString> values = m_valueWidget->values();
253  values["term"] = currentTerm();
254 
255  return QueryExpressionUtils::formatExpression(expr, values);
256 }
257 
258 void cvQueryConditionWidget::setExpression(const QString& expr) {
259  // Try to match against all operators
260  for (int termType = CELL_CONTAINING_POINT; termType >= 0; --termType) {
261  populateOperators(static_cast<TermType>(termType));
262 
263  for (int i = 0; i < m_operatorCombo->count(); ++i) {
264  QRegularExpression regex =
265  m_operatorCombo->itemData(i, ExprRegExRole)
266  .toRegularExpression();
267  QRegularExpressionMatch match = regex.match(expr);
268 
269  if (match.hasMatch()) {
270  // Found a match!
271  setCurrentTerm(match.captured("term"));
272  m_operatorCombo->setCurrentIndex(i);
273 
274  // Extract values
275  QMap<QString, QString> values;
276  for (const QString& key :
277  {"value", "value_min", "value_max", "value_x", "value_y",
278  "value_z", "value_tolerance"}) {
279  QString capturedValue = match.captured(key);
280  if (!capturedValue.isNull() && !capturedValue.isEmpty()) {
281  values[key] = capturedValue;
282  }
283  }
284  m_valueWidget->setValues(values);
285  return;
286  }
287  }
288  }
289 
290  // Couldn't parse - reset to defaults
291  if (m_termCombo->count() > 0) {
292  m_termCombo->setCurrentIndex(0);
293  }
294  if (m_operatorCombo->count() > 0) {
295  m_operatorCombo->setCurrentIndex(0);
296  }
297  m_valueWidget->clear();
298 }
299 
301  if (m_termCombo->count() > 0) {
302  m_termCombo->setCurrentIndex(0);
303  }
304  if (m_operatorCombo->count() > 0) {
305  m_operatorCombo->setCurrentIndex(0);
306  }
307  m_valueWidget->clear();
308 }
309 
310 void cvQueryConditionWidget::onTermChanged(int index) {
311  Q_UNUSED(index);
312  populateOperators(currentTermType());
313  updateValueWidget();
314  emit conditionChanged();
315 }
316 
317 void cvQueryConditionWidget::onOperatorChanged(int index) {
318  Q_UNUSED(index);
319  updateValueWidget();
320  emit conditionChanged();
321 }
322 
323 void cvQueryConditionWidget::populateOperators(TermType termType) {
324  const QSignalBlocker blocker(m_operatorCombo);
325  m_operatorCombo->clear();
326 
328 
329  switch (termType) {
330  case ARRAY:
331  addOperator("is", VT::SINGLE_VALUE, "{term} == {value}");
332  addOperator("is in range", VT::RANGE_PAIR,
333  "({term} > {value_min}) & ({term} < {value_max})");
334  addOperator("is one of", VT::COMMA_SEPARATED_VALUES,
335  "isin({term}, [{value}])");
336  addOperator("is >=", VT::SINGLE_VALUE, "{term} >= {value}");
337  addOperator("is <=", VT::SINGLE_VALUE, "{term} <= {value}");
338  addOperator("is min", VT::NO_VALUE, "{term} == min({term})");
339  addOperator("is max", VT::NO_VALUE, "{term} == max({term})");
340  addOperator("is min per block", VT::NO_VALUE,
341  "{term} == min_per_block({term})");
342  addOperator("is max per block", VT::NO_VALUE,
343  "{term} == max_per_block({term})");
344  addOperator("is NaN", VT::NO_VALUE, "isnan({term})");
345  addOperator("is <= mean", VT::NO_VALUE, "{term} <= mean({term})");
346  addOperator("is >= mean", VT::NO_VALUE, "{term} >= mean({term})");
347  addOperator("is mean", VT::SINGLE_VALUE,
348  "abs({term} - mean({term})) <= {value}");
349  break;
350 
351  case POINT_NEAREST_TO:
352  addOperator("nearest to", VT::LOCATION_WITH_TOLERANCE,
353  "pointIsNear([({value_x}, {value_y}, {value_z}),], "
354  "{value_tolerance}, {term})");
355  break;
356 
358  addOperator("containing", VT::LOCATION,
359  "cellContainsPoint({term}, [({value_x}, {value_y}, "
360  "{value_z}),])");
361  break;
362  }
363 }
364 
365 void cvQueryConditionWidget::updateValueWidget() {
366  auto valueType = static_cast<cvQueryValueWidget::ValueType>(
367  m_operatorCombo->currentData(ValueTypeRole).toInt());
368  m_valueWidget->setType(valueType);
369 }
370 
371 QString cvQueryConditionWidget::currentTerm() const {
372  return m_termCombo->currentData(NameRole).toString();
373 }
374 
375 cvQueryConditionWidget::TermType cvQueryConditionWidget::currentTermType()
376  const {
377  return static_cast<TermType>(
378  m_termCombo->currentData(TermTypeRole).toInt());
379 }
380 
381 void cvQueryConditionWidget::setCurrentTerm(const QString& term) {
382  for (int i = 0; i < m_termCombo->count(); ++i) {
383  if (m_termCombo->itemData(i, NameRole).toString() == term) {
384  m_termCombo->setCurrentIndex(i);
385  return;
386  }
387  }
388 
389  // Term not found - add as unknown
390  m_termCombo->insertItem(0, term + "(?)");
391  m_termCombo->setItemData(0, ARRAY, TermTypeRole);
392  m_termCombo->setItemData(0, term, NameRole);
393  m_termCombo->setCurrentIndex(0);
394 }
395 
396 void cvQueryConditionWidget::addOperator(
397  const QString& text,
399  const QString& expressionTemplate) {
400  int index = m_operatorCombo->count();
401  m_operatorCombo->addItem(text);
402  m_operatorCombo->setItemData(index, valueType, ValueTypeRole);
403  m_operatorCombo->setItemData(index, expressionTemplate, ExprTemplateRole);
404 
405  // Create regex for parsing
406  QRegularExpression regex =
407  QueryExpressionUtils::createRegex(expressionTemplate);
408  m_operatorCombo->setItemData(index, regex, ExprRegExRole);
409 }
410 
411 void cvQueryConditionWidget::addTerm(const QString& text,
412  TermType type,
413  const QString& internalName) {
414  int index = m_termCombo->count();
415  m_termCombo->addItem(text);
416  m_termCombo->setItemData(index, type, TermTypeRole);
417  m_termCombo->setItemData(index, internalName, NameRole);
418 }
419 
420 //=============================================================================
421 // QueryExpressionUtils Implementation
422 //=============================================================================
423 
425 
426 QString formatExpression(const QString& templateStr,
427  const QMap<QString, QString>& values) {
428  QString result = templateStr;
429 
430  // Check for empty values (except term which should always exist)
431  for (auto it = values.constBegin(); it != values.constEnd(); ++it) {
432  if (it.key() != "term" && it.value().isEmpty()) {
433  return QString(); // Invalid expression
434  }
435  }
436 
437  // Replace all placeholders
438  for (auto it = values.constBegin(); it != values.constEnd(); ++it) {
439  QString placeholder = QString("{%1}").arg(it.key());
440  result.replace(placeholder, it.value());
441  }
442 
443  return result;
444 }
445 
446 QRegularExpression createRegex(const QString& templateStr) {
447  QString pattern = templateStr;
448 
449  // Escape special regex characters in the template
450  pattern.replace("(", "\\(").replace(")", "\\)");
451  pattern.replace("[", "\\[").replace("]", "\\]");
452  pattern.replace("+", "\\+").replace("*", "\\*");
453  pattern.replace(".", "\\.");
454 
455  // Define capture patterns for different types (matching ParaView exactly)
456  QMap<QString, QString> capturePatterns;
457  // Note: the term can include "accl", "mag(accl)", "accl[:,0]", etc.
458  // Hence the pattern is not simply "\w+".
459  capturePatterns["term"] = R"==(\w+|\w+\‍(\w+\)|\w+\[:,\d+\])==";
460  // Value pattern allows: alphanumeric, dot, underscore, comma, space, hyphen
461  capturePatterns["value"] = R"([a-zA-Z0-9\._,\s\-]+)";
462  // Numeric patterns allow: alphanumeric, underscore, dot, hyphen
463  capturePatterns["value_min"] = R"([a-zA-Z0-9_.\-]+)";
464  capturePatterns["value_max"] = R"([a-zA-Z0-9_.\-]+)";
465  capturePatterns["value_x"] = R"([a-zA-Z0-9_.\-]+)";
466  capturePatterns["value_y"] = R"([a-zA-Z0-9_.\-]+)";
467  capturePatterns["value_z"] = R"([a-zA-Z0-9_.\-]+)";
468  capturePatterns["value_tolerance"] = R"([a-zA-Z0-9_.\-]+)";
469 
470  // Replace placeholders with named capture groups
471  // First occurrence gets the full capture group, subsequent ones are
472  // back-references
473  QMap<QString, bool> usedKeys;
474  for (auto it = capturePatterns.constBegin();
475  it != capturePatterns.constEnd(); ++it) {
476  QString placeholder = QString("{%1}").arg(it.key());
477  int firstOccurrence = pattern.indexOf(placeholder);
478 
479  if (firstOccurrence != -1) {
480  // Replace first occurrence with named capture group
481  pattern.replace(firstOccurrence, placeholder.length(),
482  QString("(?<%1>%2)").arg(it.key(), it.value()));
483  usedKeys[it.key()] = true;
484 
485  // Replace remaining occurrences with back-reference
486  pattern.replace(placeholder, QString("\\g{%1}").arg(it.key()));
487  }
488  }
489 
490  QRegularExpression regex("^" + pattern + "$");
491  if (!regex.isValid()) {
492  CVLog::Warning(QString("[QueryExpressionUtils] Invalid regex: %1")
493  .arg(regex.errorString()));
494  }
495 
496  return regex;
497 }
498 
499 QStringList splitByAnd(const QString& expression) {
500  QStringList result;
501  int parentCount = 0;
502  int start = 0;
503 
504  for (int pos = 0; pos < expression.size(); ++pos) {
505  if (expression[pos] == '(') {
506  ++parentCount;
507  } else if (expression[pos] == ')') {
508  --parentCount;
509  } else if (parentCount == 0 && expression[pos] == '&') {
510  QString term = expression.mid(start, pos - start).trimmed();
511  if (!term.isEmpty()) {
512  // Remove outer parentheses if present
513  if (term.startsWith('(') && term.endsWith(')')) {
514  term = term.mid(1, term.length() - 2).trimmed();
515  }
516  result.append(term);
517  }
518  start = pos + 1;
519  }
520  }
521 
522  // Add last term
523  if (start < expression.size()) {
524  QString term = expression.mid(start).trimmed();
525  if (!term.isEmpty()) {
526  if (term.startsWith('(') && term.endsWith(')')) {
527  term = term.mid(1, term.length() - 2).trimmed();
528  }
529  result.append(term);
530  }
531  }
532 
533  return result;
534 }
535 
536 } // namespace QueryExpressionUtils
char type
core::Tensor result
Definition: VtkUtils.cpp:76
static bool Warning(const char *format,...)
Prints out a formatted warning message in console.
Definition: CVLog.cpp:133
void setExpression(const QString &expr)
cvQueryConditionWidget(QWidget *parent=nullptr)
QString expression() const
void updateTerms(const QStringList &arrayNames, const QMap< QString, int > &arrayComponents, bool isPointData)
cvQueryValueWidget - Dynamic value input widget
void setType(ValueType type)
void setValues(const QMap< QString, QString > &values)
QMap< QString, QString > values() const
cvQueryValueWidget(QWidget *parent=nullptr)
Helper functions for expression formatting and parsing.
QStringList splitByAnd(const QString &expression)
QString formatExpression(const QString &templateStr, const QMap< QString, QString > &values)
QRegularExpression createRegex(const QString &templateStr)