From 9201079c05a91635675ef22277902b9224216ae7 Mon Sep 17 00:00:00 2001 From: Matthias Kuehlewein Date: Sat, 1 Mar 2025 14:05:40 +0100 Subject: [PATCH 01/16] Find objects based on additional object properties Extend the 'Find' mechanism to operate also on object properties like minimum area and length constraint violations. --- src/core/objects/object.cpp | 69 +++++++++++++++++++++++- src/core/objects/object.h | 29 ++++++++++ src/core/objects/object_query.cpp | 89 +++++++++++++++++++++++++++++-- src/core/objects/object_query.h | 11 ++-- 4 files changed, 191 insertions(+), 7 deletions(-) diff --git a/src/core/objects/object.cpp b/src/core/objects/object.cpp index 7343886f7..8c96b03c1 100644 --- a/src/core/objects/object.cpp +++ b/src/core/objects/object.cpp @@ -65,6 +65,7 @@ class QRectF; namespace literal { + // map file static const QLatin1String object("object"); static const QLatin1String symbol("symbol"); static const QLatin1String type("type"); @@ -76,10 +77,18 @@ namespace literal static const QLatin1String rotation("rotation"); static const QLatin1String size("size"); static const QLatin1String tags("tags"); + + // object properties + static const QLatin1String UndefinedSymbol("UndefinedSymbol"); + static const QLatin1String AreaTooSmall("AreaTooSmall"); + static const QLatin1String LineTooShort("LineTooShort"); + static const QLatin1String PaperArea("PaperArea"); + static const QLatin1String RealArea("RealArea"); + static const QLatin1String PaperLength("PaperLength"); + static const QLatin1String RealLength("RealLength"); } - namespace OpenOrienteering { // ### Object implementation ### @@ -755,6 +764,24 @@ void Object::includeControlPointsRect(QRectF& rect) const } +QVariant Object::getObjectProperty(const QString& property) const +{ + if (property == literal::UndefinedSymbol) + { + if (map && symbol) + return QVariant(map->findSymbolIndex(symbol) < 0); + } + + return QVariant(); +} + +bool Object::isObjectProperty(const QString& property) const +{ + if (property == literal::UndefinedSymbol) + return true; + + return false; +} // ### PathPart ### @@ -3220,6 +3247,46 @@ bool PathObject::isLineTooShort() const } +// override +QVariant PathObject::getObjectProperty(const QString& property) const +{ + if (property == literal::AreaTooSmall) + return QVariant(isAreaTooSmall()); + + if (property == literal::LineTooShort) + return QVariant(isLineTooShort()); + + if (property == literal::PaperArea) + return QVariant(calculatePaperArea()); + + if (property == literal::RealArea) + return QVariant(calculateRealArea()); + + if (property == literal::PaperLength) + return QVariant(getPaperLength()); + + if (property == literal::RealLength) + return QVariant(getRealLength()); + + return Object::getObjectProperty(property); // pass to base class function +} + +// override +bool PathObject::isObjectProperty(const QString& property) const +{ + if (property == literal::AreaTooSmall + || property == literal::LineTooShort + || property == literal::PaperArea + || property == literal::RealArea + || property == literal::PaperLength + || property == literal::RealLength + ) + return true; + + return Object::isObjectProperty(property); // pass to base class function +} + + // ### PointObject ### PointObject::PointObject(const Symbol* symbol) diff --git a/src/core/objects/object.h b/src/core/objects/object.h index 816ad8aa8..18f3fd76b 100644 --- a/src/core/objects/object.h +++ b/src/core/objects/object.h @@ -317,6 +317,21 @@ friend class XMLImportExport; */ void includeControlPointsRect(QRectF& rect) const; + + /** + * Returns dynamic object properties that are common for all objects. + * Derived object classes (i.e., PathObject) override this function to return class specific object properties. + * If derived object classes don't provide a requested property, they invoke the base class function. + */ + virtual QVariant getObjectProperty(const QString& property) const; + + /** + * Returns true if property is a dynamic object property that is provided for all objects. + * Derived object classes (i.e., PathObject) override this function to test for class specific object properties. + * If derived object classes don't provide a requested property, they invoke the base class function. + */ + virtual bool isObjectProperty(const QString& property) const; + protected: virtual void updateEvent() const; @@ -925,6 +940,20 @@ class PathObject : public Object // clazy:exclude=copyable-polymorphic bool isLineTooShort() const; + /** + * Returns dynamic object properties that are specific for the PathObject class. + * Note: Overrides the base class function that returns dynamic object properties that are common for all objects. + * If a requested property is not provided by PathObject, the base class function is invoked. + */ + QVariant getObjectProperty(const QString& property) const override; + + /** + * Returns true if property is a dynamic object property that is provided by the PathObject class. + * Note: Overrides the base class function that returns true for dynamic object properties that are common for all objects. + * If a requested property is not provided by PathObject, the base class function is invoked. + */ + bool isObjectProperty(const QString& property) const override; + protected: /** * Adjusts the end index of the given part and the start/end indexes of the following parts. diff --git a/src/core/objects/object_query.cpp b/src/core/objects/object_query.cpp index c5f182e9c..826742b35 100644 --- a/src/core/objects/object_query.cpp +++ b/src/core/objects/object_query.cpp @@ -1,6 +1,7 @@ /* * Copyright 2016 Mitchell Krome - * Copyright 2017-2024 Kai Pastor + * Copyright 2017-2025 Kai Pastor + * Copyright 2025 Matthias Kühlewein * * This file is part of OpenOrienteering. * @@ -32,7 +33,6 @@ #include #include #include -#include #include #include "core/map.h" @@ -393,6 +393,72 @@ QString ObjectQuery::labelFor(ObjectQuery::Operator op) Q_UNREACHABLE(); } +/* +QVariant ObjectQuery::getObjectProperty(const Object* object, const StringOperands& tags) const +{ + auto property = QVariant(); + + // check if tag refers to object properties + if (tags.key.startsWith(QLatin1Char('.'))) + { + const auto internal_tag = tags.key.mid(1); + if (object->getType() == Object::Path) + { + const auto& path_object = static_cast(object); + property = path_object->getObjectProperty(internal_tag); + } + else + { + property = object->getObjectProperty(internal_tag); + } + } + return property; +}*/ + +bool ObjectQuery::getBooleanObjectProperty(const Object* object, const StringOperands& tags, bool& value) const +{ + auto property = QVariant(); + + // check if tag refers to object properties + if (tags.value.startsWith(QLatin1Char('.'))) + { + const auto internal_tag = tags.value.mid(1); + if (object->getType() == Object::Path) + { + const auto& path_object = static_cast(object); + property = path_object->getObjectProperty(internal_tag); + } + else + { + property = object->getObjectProperty(internal_tag); + } + } + if (property.isValid() && static_cast(property.type()) == QMetaType::Bool) + { + value = property.toBool(); + return true; + } + return false; +} + +bool ObjectQuery::isObjectProperty(const Object* object, const QString& tag_value) const +{ + // check if tags_value refers to object properties + if (tag_value.startsWith(QLatin1Char('.'))) + { + const auto internal_tag = tag_value.mid(1); + if (object->getType() == Object::Path) + { + const auto& path_object = static_cast(object); + return path_object->isObjectProperty(internal_tag); + } + else + { + return object->isObjectProperty(internal_tag); + } + } + return false; +} bool ObjectQuery::operator()(const Object* object) const @@ -400,11 +466,21 @@ bool ObjectQuery::operator()(const Object* object) const switch(op) { case OperatorIs: + /*{ + auto property = getObjectProperty(object, tags); + if (property.isValid()) + return static_cast(property.type()) == QMetaType::Bool && property.toBool(); + }*/ return [](auto const& container, auto const& tags) { auto const it = container.find(tags.key); return it != container.end() && it->value == tags.value; } (object->tags(), tags); case OperatorIsNot: + /*{ + auto property = getObjectProperty(object, tags); + if (property.isValid()) + return static_cast(property.type()) != QMetaType::Bool || !property.toBool(); + }*/ // If the object does have the tag, not is true return [](auto const& container, auto const& tags) { auto const it = container.find(tags.key); @@ -416,6 +492,10 @@ bool ObjectQuery::operator()(const Object* object) const return it != container.end() && it->value.contains(tags.value); } (object->tags(), tags); case OperatorSearch: + qDebug("\nOperatorSearch"); + bool value; + if (getBooleanObjectProperty(object, tags, value)) + return value; if (object->getSymbol() && object->getSymbol()->getName().contains(tags.value, Qt::CaseInsensitive)) return true; for (auto const& current : object->tags()) @@ -426,8 +506,11 @@ bool ObjectQuery::operator()(const Object* object) const } return false; case OperatorObjectText: + if (isObjectProperty(object, tags.value)) // don't search for object properties keywords + return false; + qDebug("\nOperatorObjectText"); if (object->getType() == Object::Text) - return static_cast(object)->getText().contains(tags.value, Qt::CaseInsensitive); + return static_cast(object)->getText().contains(tags.value, Qt::CaseInsensitive); return false; case OperatorAnd: diff --git a/src/core/objects/object_query.h b/src/core/objects/object_query.h index 611a5061f..b2dcb68a1 100644 --- a/src/core/objects/object_query.h +++ b/src/core/objects/object_query.h @@ -1,6 +1,6 @@ /* * Copyright 2016 Mitchell Krome - * Copyright 2017-2024 Kai Pastor + * Copyright 2017-2025 Kai Pastor * * This file is part of OpenOrienteering. * @@ -27,6 +27,7 @@ #include #include #include +#include namespace OpenOrienteering { @@ -201,6 +202,10 @@ class ObjectQuery */ void consume(ObjectQuery&& other); + //QVariant getObjectProperty(const Object* object, const StringOperands& tags) const; + bool getBooleanObjectProperty(const Object* object, const StringOperands& tags, bool& value) const; + bool isObjectProperty(const Object* object, const QString& tag_value) const; + using SymbolOperand = const Symbol*; Operator op; @@ -208,7 +213,7 @@ class ObjectQuery union { LogicalOperands subqueries; - StringOperands tags; + StringOperands tags; SymbolOperand symbol; }; @@ -317,4 +322,4 @@ bool operator!=(const ObjectQuery::StringOperands& lhs, const ObjectQuery::Strin Q_DECLARE_METATYPE(OpenOrienteering::ObjectQuery::Operator) -#endif +#endif // OPENORIENTEERING_OBJECT_QUERY_H From d9dc956b2dfaf00cd85625236cb0a070cb380d61 Mon Sep 17 00:00:00 2001 From: Matthias Kuehlewein Date: Sun, 2 Mar 2025 17:40:12 +0100 Subject: [PATCH 02/16] Refactoring ObjectQuery class --- src/core/objects/object_query.cpp | 44 ++++++++++++------------------- src/core/objects/object_query.h | 7 +++++ 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/core/objects/object_query.cpp b/src/core/objects/object_query.cpp index 826742b35..69ff564e1 100644 --- a/src/core/objects/object_query.cpp +++ b/src/core/objects/object_query.cpp @@ -210,7 +210,7 @@ ObjectQuery::ObjectQuery(const ObjectQuery& query) { ; // nothing } - else if (op < 16) + else if (IsLogicalOperator(op)) { new (&subqueries) LogicalOperands(query.subqueries); } @@ -266,10 +266,8 @@ ObjectQuery::ObjectQuery(const QString& key, ObjectQuery::Operator op, const QSt { // Can't have an empty key (but can have empty value) // Must be a key/value operator - Q_ASSERT(op >= 16); - Q_ASSERT(op <= 18); - if (op < 16 || op > 18 - || key.length() == 0) + Q_ASSERT(IsTagOperator(op)); // 16..18 + if (!IsTagOperator(op) || key.length() == 0) { reset(); } @@ -280,12 +278,10 @@ ObjectQuery::ObjectQuery(ObjectQuery::Operator op, const QString& value) : op { op } , tags { {}, value } { - // Can't have an empty key (but can have empty value) - // Must be a key/value operator - Q_ASSERT(op >= 19); - Q_ASSERT(op <= 20); - if (op < 19 || op > 20 - || value.length() == 0) + // Can't have an empty value (but can have empty key) + // Must be a value operator + Q_ASSERT(IsValueOperator(op)); // 19..20 + if (!IsValueOperator(op) || value.length() == 0) { reset(); } @@ -298,10 +294,8 @@ ObjectQuery::ObjectQuery(const ObjectQuery& first, ObjectQuery::Operator op, con { // Both sub-queries must be valid. // Must be a logical operator - Q_ASSERT(op >= 1); - Q_ASSERT(op <= 3); - if (op < 1 || op > 3 - || !first || !second) + Q_ASSERT(IsLogicalOperator(op)); // 1..3 + if (!IsLogicalOperator(op) || !first || !second) { reset(); } @@ -319,10 +313,8 @@ ObjectQuery::ObjectQuery(ObjectQuery&& first, ObjectQuery::Operator op, ObjectQu { // Both sub-queries must be valid. // Must be a logical operator - Q_ASSERT(op >= 1); - Q_ASSERT(op <= 3); - if (op < 1 || op > 3 - || !first || !second) + Q_ASSERT(IsLogicalOperator(op)); // 1..3 + if (!IsLogicalOperator(op) || !first || !second) { reset(); } @@ -535,14 +527,13 @@ bool ObjectQuery::operator()(const Object* object) const const ObjectQuery::LogicalOperands* ObjectQuery::logicalOperands() const { const LogicalOperands* result = nullptr; - if (op >= 1 && op <= 3) + if (IsLogicalOperator(op)) { result = &subqueries; } else { - Q_ASSERT(op >= 1); - Q_ASSERT(op <= 3); + Q_ASSERT(IsLogicalOperator(op)); } return result; } @@ -551,14 +542,13 @@ const ObjectQuery::LogicalOperands* ObjectQuery::logicalOperands() const const ObjectQuery::StringOperands* ObjectQuery::tagOperands() const { const StringOperands* result = nullptr; - if (op >= 16 && op <= 20) + if (IsStringOperator(op)) // 16..20 { result = &tags; } else { - Q_ASSERT(op >= 16); - Q_ASSERT(op <= 20); + Q_ASSERT(IsStringOperator(op)); } return result; } @@ -637,7 +627,7 @@ void ObjectQuery::reset() { ; // nothing } - else if (op < 16) + else if (IsLogicalOperator(op)) { subqueries.~LogicalOperands(); op = ObjectQuery::OperatorInvalid; @@ -662,7 +652,7 @@ void ObjectQuery::consume(ObjectQuery&& other) { ; // nothing else } - else if (op < 16) + else if (IsLogicalOperator(op)) { new (&subqueries) ObjectQuery::LogicalOperands(std::move(other.subqueries)); other.subqueries.~LogicalOperands(); diff --git a/src/core/objects/object_query.h b/src/core/objects/object_query.h index b2dcb68a1..bc37c94eb 100644 --- a/src/core/objects/object_query.h +++ b/src/core/objects/object_query.h @@ -53,6 +53,7 @@ class ObjectQuery Q_DECLARE_TR_FUNCTIONS(OpenOrienteering::ObjectQuery) public: + // Important: When adding operators update the Is... functions below enum Operator { // Operators 1 .. 15 operate on other queries OperatorAnd = 1, ///< And-chains two object queries @@ -63,6 +64,7 @@ class ObjectQuery OperatorIs = 16, ///< Tests an existing tag for equality with the given value (case-sensitive) OperatorIsNot = 17, ///< Tests an existing tag for inequality with the given value (case-sensitive) OperatorContains = 18, ///< Tests an existing tag for containing the given value (case-sensitive) + OperatorSearch = 19, ///< Tests if the symbol name, a tag key or a tag value contains the given value (case-insensitive) OperatorObjectText = 20, ///< Text object content (case-insensitive) @@ -206,6 +208,11 @@ class ObjectQuery bool getBooleanObjectProperty(const Object* object, const StringOperands& tags, bool& value) const; bool isObjectProperty(const Object* object, const QString& tag_value) const; + bool IsLogicalOperator(Operator op) const { return op >= 1 && op <= 3; } + bool IsTagOperator(Operator op) const { return op >= 16 && op <= 18; } + bool IsValueOperator(Operator op) const { return op >= 19 && op <= 20; } + bool IsStringOperator(Operator op) const { return op >= 16 && op <= 20; } + using SymbolOperand = const Symbol*; Operator op; From 1e4903f24c786add420016b58c6251b7d23042a8 Mon Sep 17 00:00:00 2001 From: Matthias Kuehlewein Date: Mon, 3 Mar 2025 00:02:47 +0100 Subject: [PATCH 03/16] MapFindFeature: Delete object and find next object Add a button to delete a single selected object and to find the next matching one. --- src/gui/map/map_find_feature.cpp | 50 ++++++++++++++++++++++++++------ src/gui/map/map_find_feature.h | 29 ++++++++++++------ 2 files changed, 61 insertions(+), 18 deletions(-) diff --git a/src/gui/map/map_find_feature.cpp b/src/gui/map/map_find_feature.cpp index 023bb10a9..affb9a7f7 100644 --- a/src/gui/map/map_find_feature.cpp +++ b/src/gui/map/map_find_feature.cpp @@ -115,11 +115,16 @@ void MapFindFeature::showDialog() tag_selector = new TagSelectWidget; + auto find_all = new QPushButton(tr("Find &all")); + connect(find_all, &QPushButton::clicked, this, &MapFindFeature::findAll); + auto find_next = new QPushButton(tr("&Find next")); connect(find_next, &QPushButton::clicked, this, &MapFindFeature::findNext); - auto find_all = new QPushButton(tr("Find &all")); - connect(find_all, &QPushButton::clicked, this, &MapFindFeature::findAll); + delete_find_next = new QPushButton(tr("Delete && Find next")); + connect(delete_find_next, &QPushButton::clicked, this, &MapFindFeature::deleteAndFindNext); + connect(controller.getMap(), &Map::objectSelectionChanged, this, &MapFindFeature::objectSelectionChanged); + objectSelectionChanged(); auto tags_button = new QPushButton(tr("Query editor")); tags_button->setCheckable(true); @@ -138,12 +143,13 @@ void MapFindFeature::showDialog() connect(tags_button, &QAbstractButton::toggled, this, &MapFindFeature::tagSelectorToggled); auto layout = new QGridLayout; - layout->addLayout(editor_stack, 0, 0, 6, 1); - layout->addWidget(find_next, 0, 1, 1, 1); - layout->addWidget(find_all, 1, 1, 1, 1); - layout->addWidget(tags_button, 3, 1, 1, 1); - layout->addWidget(tag_selector_buttons, 5, 1, 1, 1); - layout->addWidget(button_box, 6, 0, 1, 2); + layout->addLayout(editor_stack, 0, 0, 7, 1); + layout->addWidget(find_all, 0, 1, 1, 1); + layout->addWidget(find_next, 1, 1, 1, 1); + layout->addWidget(delete_find_next, 2, 1, 1, 1); + layout->addWidget(tags_button, 4, 1, 1, 1); + layout->addWidget(tag_selector_buttons, 6, 1, 1, 1); + layout->addWidget(button_box, 7, 0, 1, 2); find_dialog->setLayout(layout); } @@ -154,7 +160,6 @@ void MapFindFeature::showDialog() } - ObjectQuery MapFindFeature::makeQuery() const { auto query = ObjectQuery{}; @@ -187,6 +192,7 @@ ObjectQuery MapFindFeature::makeQuery() const } +// slot void MapFindFeature::findNext() { if (auto query = makeQuery()) @@ -201,6 +207,7 @@ void MapFindFeature::findNextMatchingObject(MapEditorController& controller, con Object* first_match = nullptr; // the first match in all objects Object* pivot_object = map->getFirstSelectedObject(); Object* next_match = nullptr; // the next match after pivot_object + map->clearObjectSelection(false); auto search = [&](Object* object) { @@ -234,6 +241,21 @@ void MapFindFeature::findNextMatchingObject(MapEditorController& controller, con } +// slot +void MapFindFeature::deleteAndFindNext() +{ + auto map = controller.getMap(); + map->deleteSelectedObjects(); + // restore start point for search in findNext() but only if the object still exists. + if (previous_object && map->getCurrentPart()->contains(previous_object)) + { + map->addObjectToSelection(previous_object, false); + } + findNext(); +} + + +// slot void MapFindFeature::findAll() { if (auto query = makeQuery()) @@ -260,12 +282,22 @@ void MapFindFeature::findAllMatchingObjects(MapEditorController& controller, con } +// slot +void MapFindFeature::objectSelectionChanged() +{ + auto map = controller.getMap(); + delete_find_next->setEnabled(map->getNumSelectedObjects() == 1); +} + + +// slot void MapFindFeature::showHelp() const { Util::showHelp(controller.getWindow(), "find_objects.html"); } +// slot void MapFindFeature::tagSelectorToggled(bool active) { editor_stack->setCurrentIndex(active ? 1 : 0); diff --git a/src/gui/map/map_find_feature.h b/src/gui/map/map_find_feature.h index f4af33672..aea3e2df8 100644 --- a/src/gui/map/map_find_feature.h +++ b/src/gui/map/map_find_feature.h @@ -27,6 +27,7 @@ class QAction; class QDialog; +class QPushButton; class QStackedLayout; class QTextEdit; class QWidget; @@ -34,6 +35,7 @@ class QWidget; namespace OpenOrienteering { class MapEditorController; +class Object; class ObjectQuery; class TagSelectWidget; @@ -54,9 +56,22 @@ class MapFindFeature : public QObject void setEnabled(bool enabled); - QAction* showDialogAction() { return show_action; } + QAction* showDialogAction() const { return show_action; } - QAction* findNextAction() { return find_next_action; } + QAction* findNextAction() const { return find_next_action; } + +private slots: + void findNext(); + + void deleteAndFindNext(); + + void findAll(); + + void objectSelectionChanged(); + + void showHelp() const; + + void tagSelectorToggled(bool active); static void findNextMatchingObject(MapEditorController& controller, const ObjectQuery& query); @@ -67,13 +82,6 @@ class MapFindFeature : public QObject ObjectQuery makeQuery() const; - void findNext(); - - void findAll(); - - void showHelp() const; - - void tagSelectorToggled(bool active); MapEditorController& controller; @@ -82,9 +90,12 @@ class MapFindFeature : public QObject QTextEdit* text_edit = nullptr; // child of find_dialog TagSelectWidget* tag_selector = nullptr; // child of find_dialog QWidget* tag_selector_buttons = nullptr; // child of find_dialog + QPushButton* delete_find_next = nullptr; // child of find_dialog QAction* show_action = nullptr; // child of this QAction* find_next_action = nullptr; // child of this + Object* previous_object = nullptr; + Q_DISABLE_COPY(MapFindFeature) }; From a4c94e939ca59bb345f2bc6159119de2cbe5e142 Mon Sep 17 00:00:00 2001 From: Matthias Kuehlewein Date: Tue, 22 Apr 2025 21:21:00 +0200 Subject: [PATCH 04/16] Add comparison of length and area --- src/core/objects/object_query.cpp | 170 ++++++++++++++++++++++++++++-- src/core/objects/object_query.h | 11 ++ 2 files changed, 175 insertions(+), 6 deletions(-) diff --git a/src/core/objects/object_query.cpp b/src/core/objects/object_query.cpp index 69ff564e1..ec358eeb6 100644 --- a/src/core/objects/object_query.cpp +++ b/src/core/objects/object_query.cpp @@ -46,7 +46,7 @@ namespace { -static QChar special_chars[9] = { +static QChar special_chars[11] = { QLatin1Char('"'), QLatin1Char(' '), QLatin1Char('\t'), @@ -55,7 +55,9 @@ static QChar special_chars[9] = { QLatin1Char('='), QLatin1Char('!'), QLatin1Char('~'), - QLatin1Char('\\') + QLatin1Char('\\'), + QLatin1Char('<'), + QLatin1Char('>') }; QString toEscaped(QString string) @@ -264,10 +266,10 @@ ObjectQuery::ObjectQuery(const QString& key, ObjectQuery::Operator op, const QSt : op { op } , tags { key, value } { - // Can't have an empty key (but can have empty value) + // Can't have an empty key (but can have empty value (if not a numerical operator)) // Must be a key/value operator - Q_ASSERT(IsTagOperator(op)); // 16..18 - if (!IsTagOperator(op) || key.length() == 0) + Q_ASSERT(IsTagOperator(op) || IsNumericalOperator(op)); // 16..18 and 24..27 + if ((!IsTagOperator(op) && !IsNumericalOperator(op)) || key.length() == 0 || (value.length() == 0 && IsNumericalOperator(op))) { reset(); } @@ -363,6 +365,15 @@ QString ObjectQuery::labelFor(ObjectQuery::Operator op) //: Very short label return tr("Text"); + case OperatorLess: + return tr("less than"); + case OperatorLessOrEqual: + return tr("less or equal than"); + case OperatorGreater: + return tr("greater than"); + case OperatorGreaterOrEqual: + return tr("greater or equal than"); + case OperatorAnd: //: Very short label return tr("and"); @@ -433,6 +444,32 @@ bool ObjectQuery::getBooleanObjectProperty(const Object* object, const StringOpe return false; } +/*bool ObjectQuery::getDoubleObjectProperty(const Object* object, const StringOperands& tags, double& value) const +{ + auto property = QVariant(); + + // check if tag refers to object properties + if (tags.key.startsWith(QLatin1Char('.'))) + { + const auto internal_tag = tags.key.mid(1); + if (object->getType() == Object::Path) + { + const auto& path_object = static_cast(object); + property = path_object->getObjectProperty(internal_tag); + } + else + { + property = object->getObjectProperty(internal_tag); + } + } + if (property.isValid() && static_cast(property.type()) == QMetaType::Double) + { + value = property.toDouble(); + return true; + } + return false; +}*/ + bool ObjectQuery::isObjectProperty(const Object* object, const QString& tag_value) const { // check if tags_value refers to object properties @@ -452,6 +489,52 @@ bool ObjectQuery::isObjectProperty(const Object* object, const QString& tag_valu return false; } +bool ObjectQuery::compareObjectProperty(const Object* object, const StringOperands& tags, Operator op) const +{ + auto property = QVariant(); + + // check if tag refers to object properties + if (tags.key.startsWith(QLatin1Char('.'))) + { + const auto internal_tag = tags.key.mid(1); + if (object->getType() == Object::Path) + { + const auto& path_object = static_cast(object); + property = path_object->getObjectProperty(internal_tag); + } + else + { + property = object->getObjectProperty(internal_tag); + } + } + if (property.isValid() && static_cast(property.type()) == QMetaType::Double) + { + bool ok; + const auto comp_value = tags.value.toDouble(&ok); + if (ok) + { + const auto value = property.toDouble(&ok); + if (ok) + { + switch(op) + { + case OperatorLess: + return value < comp_value; + case OperatorLessOrEqual: + return value <= comp_value; + case OperatorGreater: + return value > comp_value; + case OperatorGreaterOrEqual: + return value >= comp_value; + default: + return false; // unreachable + } + Q_UNREACHABLE(); + } + } + } + return false; +} bool ObjectQuery::operator()(const Object* object) const { @@ -505,6 +588,12 @@ bool ObjectQuery::operator()(const Object* object) const return static_cast(object)->getText().contains(tags.value, Qt::CaseInsensitive); return false; + case OperatorLess: + case OperatorLessOrEqual: + case OperatorGreater: + case OperatorGreaterOrEqual: + return compareObjectProperty(object, tags, op); + case OperatorAnd: return (*subqueries.first)(object) && (*subqueries.second)(object); case OperatorOr: @@ -589,6 +678,19 @@ QString ObjectQuery::toString() const ret = QLatin1Char('"') + toEscaped(tags.value) + QLatin1Char('"'); break; + case OperatorLess: + ret = keyToString(tags.key) + QLatin1String(" < ") + toEscaped(tags.value); + break; + case OperatorLessOrEqual: + ret = keyToString(tags.key) + QLatin1String(" <= ") + toEscaped(tags.value); + break; + case OperatorGreater: + ret = keyToString(tags.key) + QLatin1String(" > ") + toEscaped(tags.value); + break; + case OperatorGreaterOrEqual: + ret = keyToString(tags.key) + QLatin1String(" >= ") + toEscaped(tags.value); + break; + case OperatorAnd: if (subqueries.first->getOperator() == OperatorOr) ret = QLatin1Char('(') + subqueries.first->toString() + QLatin1Char(')'); @@ -688,6 +790,10 @@ bool operator==(const ObjectQuery& lhs, const ObjectQuery& rhs) case ObjectQuery::OperatorContains: case ObjectQuery::OperatorSearch: case ObjectQuery::OperatorObjectText: + case ObjectQuery::OperatorLess: + case ObjectQuery::OperatorLessOrEqual: + case ObjectQuery::OperatorGreater: + case ObjectQuery::OperatorGreaterOrEqual: return lhs.tags == rhs.tags; case ObjectQuery::OperatorAnd: @@ -768,6 +874,42 @@ ObjectQuery ObjectQueryParser::parse(const QString& text) break; } } + else if (token == TokenNumericalOperator) + { + auto op = token_text; + auto num_op = token_text.toString(); + getToken(); + if (token == TokenWord) + { + auto value = tokenAsString(); + if (num_op == QLatin1String("<")) + { + *current = { key, ObjectQuery::OperatorLess , value }; + } + else if (num_op == QLatin1String("<=")) + { + *current = { key, ObjectQuery::OperatorLessOrEqual , value }; + } + else if (num_op == QLatin1String(">")) + { + *current = { key, ObjectQuery::OperatorGreater, value }; + } + else if (num_op == QLatin1String(">=")) + { + *current = { key, ObjectQuery::OperatorGreaterOrEqual, value }; + } + else // can not happen + { + qWarning("Undefined operation %s", qUtf8Printable(num_op)); + } + getToken(); + } + else + { + op = {}; + break; + } + } else { *current = { ObjectQuery::OperatorSearch, key }; @@ -941,6 +1083,20 @@ void ObjectQueryParser::getToken() token_text = input.mid(token_start, 2); pos += 2; } + else if (current == QLatin1Char('<') || current == QLatin1Char('>')) + { + token = TokenNumericalOperator; + if (pos+1 < input.length() && input.at(pos+1) == QLatin1Char('=')) + { + token_text = input.mid(token_start, 2); + pos += 2; + } + else + { + token_text = input.mid(token_start, 1); + ++pos; + } + } else { for (++pos; pos < input.length(); ++pos) @@ -951,7 +1107,9 @@ void ObjectQueryParser::getToken() || current == QLatin1Char('\t') || current == QLatin1Char('(') || current == QLatin1Char(')') - || current == QLatin1Char('=')) + || current == QLatin1Char('=') + || current == QLatin1Char('<') + || current == QLatin1Char('>')) { break; } diff --git a/src/core/objects/object_query.h b/src/core/objects/object_query.h index bc37c94eb..ab31c734f 100644 --- a/src/core/objects/object_query.h +++ b/src/core/objects/object_query.h @@ -1,6 +1,7 @@ /* * Copyright 2016 Mitchell Krome * Copyright 2017-2025 Kai Pastor + * Copyright 2025 Matthias Kühlewein * * This file is part of OpenOrienteering. * @@ -68,6 +69,12 @@ class ObjectQuery OperatorSearch = 19, ///< Tests if the symbol name, a tag key or a tag value contains the given value (case-insensitive) OperatorObjectText = 20, ///< Text object content (case-insensitive) + // Operators 24 .. 27 operate on object properties + OperatorLess = 24, + OperatorLessOrEqual = 25, + OperatorGreater = 26, + OperatorGreaterOrEqual = 27, + // More operators, 32 .. OperatorSymbol = 32, ///< Test the symbol for equality. @@ -206,12 +213,15 @@ class ObjectQuery //QVariant getObjectProperty(const Object* object, const StringOperands& tags) const; bool getBooleanObjectProperty(const Object* object, const StringOperands& tags, bool& value) const; + //bool getDoubleObjectProperty(const Object* object, const StringOperands& tags, double& value) const; bool isObjectProperty(const Object* object, const QString& tag_value) const; + bool compareObjectProperty(const Object* object, const StringOperands& tags, Operator op) const; bool IsLogicalOperator(Operator op) const { return op >= 1 && op <= 3; } bool IsTagOperator(Operator op) const { return op >= 16 && op <= 18; } bool IsValueOperator(Operator op) const { return op >= 19 && op <= 20; } bool IsStringOperator(Operator op) const { return op >= 16 && op <= 20; } + bool IsNumericalOperator(Operator op) const { return op >= 24 && op <= 27; } using SymbolOperand = const Symbol*; @@ -292,6 +302,7 @@ class ObjectQueryParser TokenNot, TokenLeftParen, TokenRightParen, + TokenNumericalOperator, }; private: From 103307f287c24045d0e1745f8869b54e70422eaa Mon Sep 17 00:00:00 2001 From: Matthias Kuehlewein Date: Tue, 29 Apr 2025 22:32:37 +0200 Subject: [PATCH 05/16] Refactoring --- src/core/objects/object.cpp | 57 ++++++------ src/core/objects/object.h | 15 +-- src/core/objects/object_query.cpp | 150 +++++++----------------------- src/core/objects/object_query.h | 3 - 4 files changed, 69 insertions(+), 156 deletions(-) diff --git a/src/core/objects/object.cpp b/src/core/objects/object.cpp index 8c96b03c1..d5e8e0f44 100644 --- a/src/core/objects/object.cpp +++ b/src/core/objects/object.cpp @@ -22,6 +22,7 @@ #include "object.h" #include +#include #include #include #include @@ -79,18 +80,39 @@ namespace literal static const QLatin1String tags("tags"); // object properties - static const QLatin1String UndefinedSymbol("UndefinedSymbol"); - static const QLatin1String AreaTooSmall("AreaTooSmall"); - static const QLatin1String LineTooShort("LineTooShort"); - static const QLatin1String PaperArea("PaperArea"); - static const QLatin1String RealArea("RealArea"); - static const QLatin1String PaperLength("PaperLength"); - static const QLatin1String RealLength("RealLength"); + // boolean operations + static const QLatin1String UndefinedSymbol(".UndefinedSymbol"); + static const QLatin1String AreaTooSmall(".AreaTooSmall"); + static const QLatin1String LineTooShort(".LineTooShort"); + // comparisons + static const QLatin1String PaperArea(".PaperArea"); + static const QLatin1String RealArea(".RealArea"); + static const QLatin1String PaperLength(".PaperLength"); + static const QLatin1String RealLength(".RealLength"); } namespace OpenOrienteering { +static const std::array object_properties = {literal::UndefinedSymbol, literal::AreaTooSmall, literal::LineTooShort, + literal::PaperArea, literal::RealArea, literal::PaperLength, literal::RealLength}; + +bool Object::isObjectProperty(const QString& property) +{ + return std::find(object_properties.begin(), object_properties.end(), property) != object_properties.end(); +} + +bool Object::isBooleanObjectProperty(const QString& property) +{ + return std::find(object_properties.begin(), object_properties.begin() + 2, property) != object_properties.begin() + 2; +} + +bool Object::isComparisonObjectProperty(const QString& property) +{ + return std::find(object_properties.begin() + 3, object_properties.end(), property) != object_properties.end(); +} + + // ### Object implementation ### Object::Object(Object::Type type, const Symbol* symbol) @@ -775,13 +797,6 @@ QVariant Object::getObjectProperty(const QString& property) const return QVariant(); } -bool Object::isObjectProperty(const QString& property) const -{ - if (property == literal::UndefinedSymbol) - return true; - - return false; -} // ### PathPart ### @@ -3271,20 +3286,6 @@ QVariant PathObject::getObjectProperty(const QString& property) const return Object::getObjectProperty(property); // pass to base class function } -// override -bool PathObject::isObjectProperty(const QString& property) const -{ - if (property == literal::AreaTooSmall - || property == literal::LineTooShort - || property == literal::PaperArea - || property == literal::RealArea - || property == literal::PaperLength - || property == literal::RealLength - ) - return true; - - return Object::isObjectProperty(property); // pass to base class function -} // ### PointObject ### diff --git a/src/core/objects/object.h b/src/core/objects/object.h index 18f3fd76b..a3c17917f 100644 --- a/src/core/objects/object.h +++ b/src/core/objects/object.h @@ -96,6 +96,8 @@ friend class XMLImportExport; /** Creates an empty object with the given type, symbol, coords and (optional) map. */ explicit Object(Type type, const Symbol* symbol, MapCoordVector coords, Map* map = nullptr); + //static constexpr QLatin1String ObjectProperties[] = {QLatin1String{"aaa"}}; + protected: /** * Constructs an Object, initialized from the given prototype. @@ -327,10 +329,10 @@ friend class XMLImportExport; /** * Returns true if property is a dynamic object property that is provided for all objects. - * Derived object classes (i.e., PathObject) override this function to test for class specific object properties. - * If derived object classes don't provide a requested property, they invoke the base class function. */ - virtual bool isObjectProperty(const QString& property) const; + static bool isObjectProperty(const QString& property); + static bool isBooleanObjectProperty(const QString& property); + static bool isComparisonObjectProperty(const QString& property); protected: virtual void updateEvent() const; @@ -947,12 +949,6 @@ class PathObject : public Object // clazy:exclude=copyable-polymorphic */ QVariant getObjectProperty(const QString& property) const override; - /** - * Returns true if property is a dynamic object property that is provided by the PathObject class. - * Note: Overrides the base class function that returns true for dynamic object properties that are common for all objects. - * If a requested property is not provided by PathObject, the base class function is invoked. - */ - bool isObjectProperty(const QString& property) const override; protected: /** @@ -1164,7 +1160,6 @@ struct ObjectPathCoord : public PathCoord }; - //### Object inline code ### inline diff --git a/src/core/objects/object_query.cpp b/src/core/objects/object_query.cpp index ec358eeb6..9807ee565 100644 --- a/src/core/objects/object_query.cpp +++ b/src/core/objects/object_query.cpp @@ -396,94 +396,25 @@ QString ObjectQuery::labelFor(ObjectQuery::Operator op) Q_UNREACHABLE(); } -/* -QVariant ObjectQuery::getObjectProperty(const Object* object, const StringOperands& tags) const -{ - auto property = QVariant(); - - // check if tag refers to object properties - if (tags.key.startsWith(QLatin1Char('.'))) - { - const auto internal_tag = tags.key.mid(1); - if (object->getType() == Object::Path) - { - const auto& path_object = static_cast(object); - property = path_object->getObjectProperty(internal_tag); - } - else - { - property = object->getObjectProperty(internal_tag); - } - } - return property; -}*/ - bool ObjectQuery::getBooleanObjectProperty(const Object* object, const StringOperands& tags, bool& value) const { - auto property = QVariant(); - - // check if tag refers to object properties - if (tags.value.startsWith(QLatin1Char('.'))) - { - const auto internal_tag = tags.value.mid(1); - if (object->getType() == Object::Path) - { - const auto& path_object = static_cast(object); - property = path_object->getObjectProperty(internal_tag); - } - else - { - property = object->getObjectProperty(internal_tag); - } - } - if (property.isValid() && static_cast(property.type()) == QMetaType::Bool) - { - value = property.toBool(); - return true; - } - return false; -} - -/*bool ObjectQuery::getDoubleObjectProperty(const Object* object, const StringOperands& tags, double& value) const -{ - auto property = QVariant(); - - // check if tag refers to object properties - if (tags.key.startsWith(QLatin1Char('.'))) + // check if tag refers to boolean object properties + if (Object::isBooleanObjectProperty(tags.value)) { - const auto internal_tag = tags.key.mid(1); + auto property = QVariant(); if (object->getType() == Object::Path) { const auto& path_object = static_cast(object); - property = path_object->getObjectProperty(internal_tag); + property = path_object->getObjectProperty(tags.value); } else { - property = object->getObjectProperty(internal_tag); + property = object->getObjectProperty(tags.value); } - } - if (property.isValid() && static_cast(property.type()) == QMetaType::Double) - { - value = property.toDouble(); - return true; - } - return false; -}*/ - -bool ObjectQuery::isObjectProperty(const Object* object, const QString& tag_value) const -{ - // check if tags_value refers to object properties - if (tag_value.startsWith(QLatin1Char('.'))) - { - const auto internal_tag = tag_value.mid(1); - if (object->getType() == Object::Path) + if (property.isValid() && static_cast(property.type()) == QMetaType::Bool) { - const auto& path_object = static_cast(object); - return path_object->isObjectProperty(internal_tag); - } - else - { - return object->isObjectProperty(internal_tag); + value = property.toBool(); + return true; } } return false; @@ -491,45 +422,44 @@ bool ObjectQuery::isObjectProperty(const Object* object, const QString& tag_valu bool ObjectQuery::compareObjectProperty(const Object* object, const StringOperands& tags, Operator op) const { - auto property = QVariant(); - - // check if tag refers to object properties - if (tags.key.startsWith(QLatin1Char('.'))) + // check if tag refers to comparison object properties + if (Object::isComparisonObjectProperty(tags.key)) { - const auto internal_tag = tags.key.mid(1); + auto property = QVariant(); if (object->getType() == Object::Path) { const auto& path_object = static_cast(object); - property = path_object->getObjectProperty(internal_tag); + property = path_object->getObjectProperty(tags.key); } else { - property = object->getObjectProperty(internal_tag); + property = object->getObjectProperty(tags.key); } - } - if (property.isValid() && static_cast(property.type()) == QMetaType::Double) - { - bool ok; - const auto comp_value = tags.value.toDouble(&ok); - if (ok) + + if (property.isValid() && static_cast(property.type()) == QMetaType::Double) { - const auto value = property.toDouble(&ok); + bool ok; + const auto comp_value = tags.value.toDouble(&ok); if (ok) { - switch(op) + const auto value = property.toDouble(&ok); + if (ok) { - case OperatorLess: - return value < comp_value; - case OperatorLessOrEqual: - return value <= comp_value; - case OperatorGreater: - return value > comp_value; - case OperatorGreaterOrEqual: - return value >= comp_value; - default: - return false; // unreachable + switch(op) + { + case OperatorLess: + return value < comp_value; + case OperatorLessOrEqual: + return value <= comp_value; + case OperatorGreater: + return value > comp_value; + case OperatorGreaterOrEqual: + return value >= comp_value; + default: + return false; // unreachable + } + Q_UNREACHABLE(); } - Q_UNREACHABLE(); } } } @@ -541,21 +471,11 @@ bool ObjectQuery::operator()(const Object* object) const switch(op) { case OperatorIs: - /*{ - auto property = getObjectProperty(object, tags); - if (property.isValid()) - return static_cast(property.type()) == QMetaType::Bool && property.toBool(); - }*/ return [](auto const& container, auto const& tags) { auto const it = container.find(tags.key); return it != container.end() && it->value == tags.value; } (object->tags(), tags); case OperatorIsNot: - /*{ - auto property = getObjectProperty(object, tags); - if (property.isValid()) - return static_cast(property.type()) != QMetaType::Bool || !property.toBool(); - }*/ // If the object does have the tag, not is true return [](auto const& container, auto const& tags) { auto const it = container.find(tags.key); @@ -581,7 +501,7 @@ bool ObjectQuery::operator()(const Object* object) const } return false; case OperatorObjectText: - if (isObjectProperty(object, tags.value)) // don't search for object properties keywords + if (Object::isObjectProperty(tags.value)) // don't search for object properties keywords return false; qDebug("\nOperatorObjectText"); if (object->getType() == Object::Text) @@ -879,7 +799,7 @@ ObjectQuery ObjectQueryParser::parse(const QString& text) auto op = token_text; auto num_op = token_text.toString(); getToken(); - if (token == TokenWord) + if ((token == TokenWord || token == TokenString) && Object::isComparisonObjectProperty(key)) { auto value = tokenAsString(); if (num_op == QLatin1String("<")) diff --git a/src/core/objects/object_query.h b/src/core/objects/object_query.h index ab31c734f..e002eaa77 100644 --- a/src/core/objects/object_query.h +++ b/src/core/objects/object_query.h @@ -211,10 +211,7 @@ class ObjectQuery */ void consume(ObjectQuery&& other); - //QVariant getObjectProperty(const Object* object, const StringOperands& tags) const; bool getBooleanObjectProperty(const Object* object, const StringOperands& tags, bool& value) const; - //bool getDoubleObjectProperty(const Object* object, const StringOperands& tags, double& value) const; - bool isObjectProperty(const Object* object, const QString& tag_value) const; bool compareObjectProperty(const Object* object, const StringOperands& tags, Operator op) const; bool IsLogicalOperator(Operator op) const { return op >= 1 && op <= 3; } From 2325d1d647c2823e6d0f827b0fb70734ccf71327 Mon Sep 17 00:00:00 2001 From: Matthias Kuehlewein Date: Wed, 30 Apr 2025 21:22:11 +0200 Subject: [PATCH 06/16] MapFindFeature: Insert keyword from list of keywords On right click in the text editor show extended context menu that allows to insert one of the keywords (e.g., SYMBOL). --- src/core/objects/object.cpp | 11 +++++---- src/core/objects/object.h | 12 ++++++---- src/core/objects/object_query.cpp | 2 -- src/gui/map/map_find_feature.cpp | 40 +++++++++++++++++++++++++++++-- src/gui/map/map_find_feature.h | 22 ++++++++++++++--- 5 files changed, 72 insertions(+), 15 deletions(-) diff --git a/src/core/objects/object.cpp b/src/core/objects/object.cpp index d5e8e0f44..9e8e9b52a 100644 --- a/src/core/objects/object.cpp +++ b/src/core/objects/object.cpp @@ -22,7 +22,6 @@ #include "object.h" #include -#include #include #include #include @@ -94,8 +93,8 @@ namespace literal namespace OpenOrienteering { -static const std::array object_properties = {literal::UndefinedSymbol, literal::AreaTooSmall, literal::LineTooShort, - literal::PaperArea, literal::RealArea, literal::PaperLength, literal::RealLength}; +static const std::vector object_properties = {literal::UndefinedSymbol, literal::AreaTooSmall, literal::LineTooShort, + literal::PaperArea, literal::RealArea, literal::PaperLength, literal::RealLength}; bool Object::isObjectProperty(const QString& property) { @@ -104,7 +103,7 @@ bool Object::isObjectProperty(const QString& property) bool Object::isBooleanObjectProperty(const QString& property) { - return std::find(object_properties.begin(), object_properties.begin() + 2, property) != object_properties.begin() + 2; + return std::find(object_properties.begin(), object_properties.begin() + 3, property) != object_properties.begin() + 3; } bool Object::isComparisonObjectProperty(const QString& property) @@ -112,6 +111,10 @@ bool Object::isComparisonObjectProperty(const QString& property) return std::find(object_properties.begin() + 3, object_properties.end(), property) != object_properties.end(); } +const std::vector& Object::getObjectProperties() +{ + return object_properties; +} // ### Object implementation ### diff --git a/src/core/objects/object.h b/src/core/objects/object.h index a3c17917f..1bbeea989 100644 --- a/src/core/objects/object.h +++ b/src/core/objects/object.h @@ -96,7 +96,6 @@ friend class XMLImportExport; /** Creates an empty object with the given type, symbol, coords and (optional) map. */ explicit Object(Type type, const Symbol* symbol, MapCoordVector coords, Map* map = nullptr); - //static constexpr QLatin1String ObjectProperties[] = {QLatin1String{"aaa"}}; protected: /** @@ -327,13 +326,18 @@ friend class XMLImportExport; */ virtual QVariant getObjectProperty(const QString& property) const; - /** - * Returns true if property is a dynamic object property that is provided for all objects. - */ + /** Returns true if property is a dynamic object property. */ static bool isObjectProperty(const QString& property); + + /** Returns true if property is a dynamic object property that returns a boolean value. */ static bool isBooleanObjectProperty(const QString& property); + + /** Returns true if property is a dynamic object property that returns a value for comparison. */ static bool isComparisonObjectProperty(const QString& property); + /** Returns keywords of the available dynamic object properties. */ + static const std::vector& getObjectProperties(); + protected: virtual void updateEvent() const; diff --git a/src/core/objects/object_query.cpp b/src/core/objects/object_query.cpp index 9807ee565..258f8f2a2 100644 --- a/src/core/objects/object_query.cpp +++ b/src/core/objects/object_query.cpp @@ -487,7 +487,6 @@ bool ObjectQuery::operator()(const Object* object) const return it != container.end() && it->value.contains(tags.value); } (object->tags(), tags); case OperatorSearch: - qDebug("\nOperatorSearch"); bool value; if (getBooleanObjectProperty(object, tags, value)) return value; @@ -503,7 +502,6 @@ bool ObjectQuery::operator()(const Object* object) const case OperatorObjectText: if (Object::isObjectProperty(tags.value)) // don't search for object properties keywords return false; - qDebug("\nOperatorObjectText"); if (object->getType() == Object::Text) return static_cast(object)->getText().contains(tags.value, Qt::CaseInsensitive); return false; diff --git a/src/gui/map/map_find_feature.cpp b/src/gui/map/map_find_feature.cpp index affb9a7f7..3d42fcf8b 100644 --- a/src/gui/map/map_find_feature.cpp +++ b/src/gui/map/map_find_feature.cpp @@ -1,5 +1,6 @@ /* * Copyright 2017-2020, 2024, 2025 Kai Pastor + * Copyright 2025 Matthias Kühlewein * * This file is part of OpenOrienteering. * @@ -24,13 +25,16 @@ #include #include +#include #include #include #include #include // IWYU pragma: keep +#include +#include #include #include -#include +#include #include #include "core/map.h" @@ -110,7 +114,7 @@ void MapFindFeature::showDialog() find_dialog = new QDialog(window); find_dialog->setWindowTitle(tr("Find objects")); - text_edit = new QTextEdit; + text_edit = new MapFindTextEdit; text_edit->setLineWrapMode(QTextEdit::WidgetWidth); tag_selector = new TagSelectWidget; @@ -311,4 +315,36 @@ void MapFindFeature::tagSelectorToggled(bool active) } +// slot +void MapFindTextEdit::insertKeyword(QAction* action) +{ + const auto keyword = action->data().toString(); + insertPlainText(keyword); +} + +// override +void MapFindTextEdit::contextMenuEvent(QContextMenuEvent* event) +{ + QMenu* menu = createStandardContextMenu(event->globalPos()); + menu->addSeparator(); + auto* insert_menu = new QMenu(tr("Insert keyword..."), menu); + insert_menu->menuAction()->setMenuRole(QAction::NoRole); + auto* keyword_actions_group = new QActionGroup(this); + + auto keywords = Object::getObjectProperties(); + keywords.insert(keywords.end(), {QLatin1String("SYMBOL"), QLatin1String("AND"), QLatin1String("OR"), QLatin1String("NOT")}); + for (auto& keyword : keywords) + { + auto* action = new QAction(keyword, this); + action->setData(QVariant(keyword)); + keyword_actions_group->addAction(action); + insert_menu->addAction(action); + } + menu->addMenu(insert_menu); + connect(keyword_actions_group, &QActionGroup::triggered, this, &MapFindTextEdit::insertKeyword); + + menu->exec(event->globalPos()); + delete menu; +} + } // namespace OpenOrienteering diff --git a/src/gui/map/map_find_feature.h b/src/gui/map/map_find_feature.h index aea3e2df8..589ca9d94 100644 --- a/src/gui/map/map_find_feature.h +++ b/src/gui/map/map_find_feature.h @@ -24,12 +24,13 @@ #include #include #include +#include class QAction; +class QContextMenuEvent; class QDialog; class QPushButton; class QStackedLayout; -class QTextEdit; class QWidget; namespace OpenOrienteering { @@ -39,6 +40,22 @@ class Object; class ObjectQuery; class TagSelectWidget; +/** + * The context menu (right click) is extended by the possibility to insert + * one of the keywords (e.g., SYMBOL, AND...) + */ +class MapFindTextEdit : public QTextEdit +{ + Q_OBJECT + +private: + void contextMenuEvent(QContextMenuEvent* event) override; + +private slots: + void insertKeyword(QAction* action); +}; + + /** * Provides an interactive feature for finding objects in the map. * @@ -87,7 +104,7 @@ private slots: MapEditorController& controller; QPointer find_dialog; // child of controller's window QStackedLayout* editor_stack = nullptr; // child of find_dialog - QTextEdit* text_edit = nullptr; // child of find_dialog + MapFindTextEdit* text_edit = nullptr; // child of find_dialog TagSelectWidget* tag_selector = nullptr; // child of find_dialog QWidget* tag_selector_buttons = nullptr; // child of find_dialog QPushButton* delete_find_next = nullptr; // child of find_dialog @@ -99,7 +116,6 @@ private slots: Q_DISABLE_COPY(MapFindFeature) }; - } // namespace OpenOrienteering #endif // OPENORIENTEERING_MAP_FIND_FEATURE_H From 1b140453f93def34d0b97eaf1b1e58780d2cc682 Mon Sep 17 00:00:00 2001 From: Matthias Kuehlewein Date: Thu, 1 May 2025 19:49:52 +0200 Subject: [PATCH 07/16] MapFindFeature: Center view on found objects Add checkmark to center the view on the found object(s). --- src/core/map.cpp | 4 ++++ src/core/map.h | 1 + src/gui/map/map_find_feature.cpp | 34 +++++++++++++++++++++++++++----- src/gui/map/map_find_feature.h | 9 +++++++-- src/gui/map/map_widget.cpp | 15 +++++++++++--- src/gui/map/map_widget.h | 6 +++--- 6 files changed, 56 insertions(+), 13 deletions(-) diff --git a/src/core/map.cpp b/src/core/map.cpp index 4ad563ece..664ea79f9 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -1114,6 +1114,10 @@ void Map::ensureVisibilityOfSelectedObjects(SelectionVisibility visibility) widget->ensureVisibilityOfRect(rect, MapWidget::DiscreteZoom); break; + case CenterFullVisibility: + widget->ensureVisibilityOfRect(rect, MapWidget::DiscreteZoom, true); + break; + case IgnoreVisibilty: break; // Do nothing diff --git a/src/core/map.h b/src/core/map.h index 0807dcdd9..bfec1cf82 100644 --- a/src/core/map.h +++ b/src/core/map.h @@ -121,6 +121,7 @@ friend class XMLFileExporter; { FullVisibility, PartialVisibility, + CenterFullVisibility, IgnoreVisibilty }; diff --git a/src/gui/map/map_find_feature.cpp b/src/gui/map/map_find_feature.cpp index 3d42fcf8b..382836f4b 100644 --- a/src/gui/map/map_find_feature.cpp +++ b/src/gui/map/map_find_feature.cpp @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -130,6 +131,10 @@ void MapFindFeature::showDialog() connect(controller.getMap(), &Map::objectSelectionChanged, this, &MapFindFeature::objectSelectionChanged); objectSelectionChanged(); + center_view = new QCheckBox(tr("Center view")); + center_view->setChecked(true); + connect(center_view, &QCheckBox::stateChanged, this, &MapFindFeature::centerView); + auto tags_button = new QPushButton(tr("Query editor")); tags_button->setCheckable(true); @@ -151,9 +156,10 @@ void MapFindFeature::showDialog() layout->addWidget(find_all, 0, 1, 1, 1); layout->addWidget(find_next, 1, 1, 1, 1); layout->addWidget(delete_find_next, 2, 1, 1, 1); - layout->addWidget(tags_button, 4, 1, 1, 1); - layout->addWidget(tag_selector_buttons, 6, 1, 1, 1); - layout->addWidget(button_box, 7, 0, 1, 2); + layout->addWidget(center_view, 3, 1, 1, 1); + layout->addWidget(tags_button, 5, 1, 1, 1); + layout->addWidget(tag_selector_buttons, 7, 1, 1, 1); + layout->addWidget(button_box, 8, 0, 1, 2); find_dialog->setLayout(layout); } @@ -238,7 +244,10 @@ void MapFindFeature::findNextMatchingObject(MapEditorController& controller, con map->addObjectToSelection(next_match, false); map->emitSelectionChanged(); - map->ensureVisibilityOfSelectedObjects(Map::FullVisibility); + if (center_view->isChecked()) + map->ensureVisibilityOfSelectedObjects(Map::CenterFullVisibility); + else + map->ensureVisibilityOfSelectedObjects(Map::FullVisibility); if (!map->selectedObjects().empty()) controller.setEditTool(); @@ -278,7 +287,10 @@ void MapFindFeature::findAllMatchingObjects(MapEditorController& controller, con }, std::cref(query)); map->emitSelectionChanged(); - map->ensureVisibilityOfSelectedObjects(Map::FullVisibility); + if (center_view->isChecked()) + map->ensureVisibilityOfSelectedObjects(Map::CenterFullVisibility); + else + map->ensureVisibilityOfSelectedObjects(Map::FullVisibility); controller.getWindow()->showStatusBarMessage(OpenOrienteering::TagSelectWidget::tr("%n object(s) selected", nullptr, map->getNumSelectedObjects()), 2000); if (!map->selectedObjects().empty()) @@ -294,6 +306,18 @@ void MapFindFeature::objectSelectionChanged() } +// slot +void MapFindFeature::centerView() +{ + if (center_view->isChecked()) + { + auto map = controller.getMap(); + if (map->getNumSelectedObjects()) + map->ensureVisibilityOfSelectedObjects(Map::CenterFullVisibility); + } +} + + // slot void MapFindFeature::showHelp() const { diff --git a/src/gui/map/map_find_feature.h b/src/gui/map/map_find_feature.h index 589ca9d94..75f43158a 100644 --- a/src/gui/map/map_find_feature.h +++ b/src/gui/map/map_find_feature.h @@ -1,5 +1,6 @@ /* * Copyright 2017-2019, 2025 Kai Pastor + * Copyright 2025 Matthias Kühlewein * * This file is part of OpenOrienteering. * @@ -27,6 +28,7 @@ #include class QAction; +class QCheckBox; class QContextMenuEvent; class QDialog; class QPushButton; @@ -41,8 +43,8 @@ class ObjectQuery; class TagSelectWidget; /** - * The context menu (right click) is extended by the possibility to insert - * one of the keywords (e.g., SYMBOL, AND...) + * The context menu (right click) is extended by the possibility + * to select and insert one of the keywords (e.g., SYMBOL, AND...) */ class MapFindTextEdit : public QTextEdit { @@ -86,6 +88,8 @@ private slots: void objectSelectionChanged(); + void centerView(); + void showHelp() const; void tagSelectorToggled(bool active); @@ -108,6 +112,7 @@ private slots: TagSelectWidget* tag_selector = nullptr; // child of find_dialog QWidget* tag_selector_buttons = nullptr; // child of find_dialog QPushButton* delete_find_next = nullptr; // child of find_dialog + QCheckBox* center_view = nullptr; // child of find_dialog QAction* show_action = nullptr; // child of this QAction* find_next_action = nullptr; // child of this diff --git a/src/gui/map/map_widget.cpp b/src/gui/map/map_widget.cpp index 69bd9acd4..e568470c2 100644 --- a/src/gui/map/map_widget.cpp +++ b/src/gui/map/map_widget.cpp @@ -1,6 +1,6 @@ /* * Copyright 2012-2014 Thomas Schöps - * Copyright 2013-2020 Kai Pastor + * Copyright 2013-2020, 2025 Kai Pastor * * This file is part of OpenOrienteering. * @@ -426,7 +426,7 @@ void MapWidget::moveMap(int steps_x, int steps_y) } } -void MapWidget::ensureVisibilityOfRect(QRectF map_rect, ZoomOption zoom_option) +void MapWidget::ensureVisibilityOfRect(QRectF map_rect, ZoomOption zoom_option, bool center_view) { // Amount in pixels that is scrolled "too much" if the rect is not completely visible // TODO: change to absolute size using dpi value @@ -436,7 +436,11 @@ void MapWidget::ensureVisibilityOfRect(QRectF map_rect, ZoomOption zoom_option) // TODO: this method assumes that the viewport is not rotated. if (rect().contains(viewport_rect.topLeft()) && rect().contains(viewport_rect.bottomRight())) + { + if (center_view) + view->setCenter(MapCoord{ map_rect.center() }); return; + } auto offset = MapCoordF{ 0, 0 }; @@ -451,7 +455,12 @@ void MapWidget::ensureVisibilityOfRect(QRectF map_rect, ZoomOption zoom_option) offset.ry() = view->pixelToLength(viewport_rect.bottom() - height() + pixel_border) / 1000.0; if (!qIsNull(offset.lengthSquared())) - view->setCenter(view->center() + offset); + { + if (center_view) + view->setCenter(MapCoord{ map_rect.center() }); + else + view->setCenter(view->center() + offset); + } // If the rect is still not completely in view, we have to zoom out viewport_rect = mapToViewport(map_rect).toAlignedRect(); diff --git a/src/gui/map/map_widget.h b/src/gui/map/map_widget.h index 0b19eb34a..d1fb56059 100644 --- a/src/gui/map/map_widget.h +++ b/src/gui/map/map_widget.h @@ -1,6 +1,6 @@ /* * Copyright 2012-2014 Thomas Schöps - * Copyright 2013-2020 Kai Pastor + * Copyright 2013-2020, 2025 Kai Pastor * * This file is part of OpenOrienteering. * @@ -214,7 +214,7 @@ friend class MapView; /** * Adjusts the viewport so the given rect is inside the view. */ - void ensureVisibilityOfRect(QRectF map_rect, ZoomOption zoom_option); // clazy:exclude=function-args-by-ref + void ensureVisibilityOfRect(QRectF map_rect, ZoomOption zoom_option, bool center_view = false); // clazy:exclude=function-args-by-ref /** * Sets the view so the rect is centered and zooomed to fill the widget. @@ -584,4 +584,4 @@ MapWidget::CoordsType MapWidget::getCoordsDisplay() const } // namespace OpenOrienteering -#endif +#endif // OPENORIENTEERING_MAP_WIDGET_H From 6a2cc802cec52fb660409acf7ede8db34c45dff4 Mon Sep 17 00:00:00 2001 From: Matthias Kuehlewein Date: Mon, 5 May 2025 16:56:38 +0200 Subject: [PATCH 08/16] MapFindFeature: Show number of selected objects in dialog --- src/gui/map/map_find_feature.cpp | 5 +++++ src/gui/map/map_find_feature.h | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/gui/map/map_find_feature.cpp b/src/gui/map/map_find_feature.cpp index 382836f4b..3c51201be 100644 --- a/src/gui/map/map_find_feature.cpp +++ b/src/gui/map/map_find_feature.cpp @@ -31,6 +31,7 @@ #include #include #include // IWYU pragma: keep +#include #include #include #include @@ -120,6 +121,8 @@ void MapFindFeature::showDialog() tag_selector = new TagSelectWidget; + selected_objects = new QLabel(); // initialization by objectSelectionChanged() below + auto find_all = new QPushButton(tr("Find &all")); connect(find_all, &QPushButton::clicked, this, &MapFindFeature::findAll); @@ -158,6 +161,7 @@ void MapFindFeature::showDialog() layout->addWidget(delete_find_next, 2, 1, 1, 1); layout->addWidget(center_view, 3, 1, 1, 1); layout->addWidget(tags_button, 5, 1, 1, 1); + layout->addWidget(selected_objects, 7, 0, 1, 1); layout->addWidget(tag_selector_buttons, 7, 1, 1, 1); layout->addWidget(button_box, 8, 0, 1, 2); @@ -303,6 +307,7 @@ void MapFindFeature::objectSelectionChanged() { auto map = controller.getMap(); delete_find_next->setEnabled(map->getNumSelectedObjects() == 1); + selected_objects->setText(tr("Number of selected objects: %1").arg(map->getNumSelectedObjects())); } diff --git a/src/gui/map/map_find_feature.h b/src/gui/map/map_find_feature.h index 75f43158a..5b900f0b7 100644 --- a/src/gui/map/map_find_feature.h +++ b/src/gui/map/map_find_feature.h @@ -31,6 +31,7 @@ class QAction; class QCheckBox; class QContextMenuEvent; class QDialog; +class QLabel; class QPushButton; class QStackedLayout; class QWidget; @@ -113,6 +114,7 @@ private slots: QWidget* tag_selector_buttons = nullptr; // child of find_dialog QPushButton* delete_find_next = nullptr; // child of find_dialog QCheckBox* center_view = nullptr; // child of find_dialog + QLabel* selected_objects = nullptr; // child of find_dialog QAction* show_action = nullptr; // child of this QAction* find_next_action = nullptr; // child of this From eb71a273b65c2dfec647ce9b2606c16cb5ca2aa2 Mon Sep 17 00:00:00 2001 From: Matthias Kuehlewein Date: Sun, 18 May 2025 01:40:57 +0200 Subject: [PATCH 09/16] Adjustments after rebasing onto master due to GH-2371 --- src/gui/map/map_find_feature.cpp | 31 +++++++++++-------------------- src/gui/map/map_find_feature.h | 24 +++++++++++------------- 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/src/gui/map/map_find_feature.cpp b/src/gui/map/map_find_feature.cpp index 3c51201be..18af89657 100644 --- a/src/gui/map/map_find_feature.cpp +++ b/src/gui/map/map_find_feature.cpp @@ -206,15 +206,18 @@ ObjectQuery MapFindFeature::makeQuery() const } -// slot void MapFindFeature::findNext() { if (auto query = makeQuery()) - findNextMatchingObject(controller, query); + { + // remember current selected object as start point in case next object found is deleted by 'Delete & Find Next' + previous_object = controller.getMap()->getFirstSelectedObject(); + findNextMatchingObject(controller, query, center_view->isChecked()); + } } // static -void MapFindFeature::findNextMatchingObject(MapEditorController& controller, const ObjectQuery& query) +void MapFindFeature::findNextMatchingObject(MapEditorController& controller, const ObjectQuery& query, bool center_selection_visibility) { auto* map = controller.getMap(); @@ -248,22 +251,18 @@ void MapFindFeature::findNextMatchingObject(MapEditorController& controller, con map->addObjectToSelection(next_match, false); map->emitSelectionChanged(); - if (center_view->isChecked()) - map->ensureVisibilityOfSelectedObjects(Map::CenterFullVisibility); - else - map->ensureVisibilityOfSelectedObjects(Map::FullVisibility); + map->ensureVisibilityOfSelectedObjects(center_selection_visibility ? Map::CenterFullVisibility : Map::FullVisibility); if (!map->selectedObjects().empty()) controller.setEditTool(); } -// slot void MapFindFeature::deleteAndFindNext() { auto map = controller.getMap(); map->deleteSelectedObjects(); - // restore start point for search in findNext() but only if the object still exists. + // restore start point for search in findNextMatchingObject() but only if the object still exists. if (previous_object && map->getCurrentPart()->contains(previous_object)) { map->addObjectToSelection(previous_object, false); @@ -272,15 +271,14 @@ void MapFindFeature::deleteAndFindNext() } -// slot void MapFindFeature::findAll() { if (auto query = makeQuery()) - findAllMatchingObjects(controller, query); + findAllMatchingObjects(controller, query, center_view->isChecked()); } // static -void MapFindFeature::findAllMatchingObjects(MapEditorController& controller, const ObjectQuery& query) +void MapFindFeature::findAllMatchingObjects(MapEditorController& controller, const ObjectQuery& query, bool center_selection_visibility) { auto map = controller.getMap(); map->clearObjectSelection(false); @@ -291,10 +289,7 @@ void MapFindFeature::findAllMatchingObjects(MapEditorController& controller, con }, std::cref(query)); map->emitSelectionChanged(); - if (center_view->isChecked()) - map->ensureVisibilityOfSelectedObjects(Map::CenterFullVisibility); - else - map->ensureVisibilityOfSelectedObjects(Map::FullVisibility); + map->ensureVisibilityOfSelectedObjects(center_selection_visibility ? Map::CenterFullVisibility : Map::FullVisibility); controller.getWindow()->showStatusBarMessage(OpenOrienteering::TagSelectWidget::tr("%n object(s) selected", nullptr, map->getNumSelectedObjects()), 2000); if (!map->selectedObjects().empty()) @@ -302,7 +297,6 @@ void MapFindFeature::findAllMatchingObjects(MapEditorController& controller, con } -// slot void MapFindFeature::objectSelectionChanged() { auto map = controller.getMap(); @@ -311,7 +305,6 @@ void MapFindFeature::objectSelectionChanged() } -// slot void MapFindFeature::centerView() { if (center_view->isChecked()) @@ -323,14 +316,12 @@ void MapFindFeature::centerView() } -// slot void MapFindFeature::showHelp() const { Util::showHelp(controller.getWindow(), "find_objects.html"); } -// slot void MapFindFeature::tagSelectorToggled(bool active) { editor_stack->setCurrentIndex(active ? 1 : 0); diff --git a/src/gui/map/map_find_feature.h b/src/gui/map/map_find_feature.h index 5b900f0b7..3d52088c9 100644 --- a/src/gui/map/map_find_feature.h +++ b/src/gui/map/map_find_feature.h @@ -76,11 +76,19 @@ class MapFindFeature : public QObject void setEnabled(bool enabled); - QAction* showDialogAction() const { return show_action; } + QAction* showDialogAction() { return show_action; } - QAction* findNextAction() const { return find_next_action; } + QAction* findNextAction() { return find_next_action; } + + static void findNextMatchingObject(MapEditorController& controller, const ObjectQuery& query, bool center_selection_visibility = false); + + static void findAllMatchingObjects(MapEditorController& controller, const ObjectQuery& query, bool center_selection_visibility = false); + +private: + void showDialog(); + + ObjectQuery makeQuery() const; -private slots: void findNext(); void deleteAndFindNext(); @@ -95,16 +103,6 @@ private slots: void tagSelectorToggled(bool active); - static void findNextMatchingObject(MapEditorController& controller, const ObjectQuery& query); - - static void findAllMatchingObjects(MapEditorController& controller, const ObjectQuery& query); - -private: - void showDialog(); - - ObjectQuery makeQuery() const; - - MapEditorController& controller; QPointer find_dialog; // child of controller's window From 507f56b956858f512bb6011ad082d7584dbcc3d0 Mon Sep 17 00:00:00 2001 From: Matthias Kuehlewein Date: Thu, 25 Dec 2025 17:41:39 +0100 Subject: [PATCH 10/16] Complete rework Move main functionality in new classes. Change pattern of dynamic queries, encapsulate comparision operations in dynamic query pattern. Enhance keyword pull-down menu to be context sensitive. --- code-check-wrapper.sh | 1 + src/CMakeLists.txt | 1 + src/core/objects/dynamic_object_query.cpp | 519 ++++++++++++++++++++++ src/core/objects/dynamic_object_query.h | 120 +++++ src/core/objects/object_query.cpp | 67 ++- src/core/objects/object_query.h | 12 + src/gui/map/map_find_feature.cpp | 20 +- src/gui/map/map_find_feature.h | 2 +- 8 files changed, 735 insertions(+), 7 deletions(-) create mode 100644 src/core/objects/dynamic_object_query.cpp create mode 100644 src/core/objects/dynamic_object_query.h diff --git a/code-check-wrapper.sh b/code-check-wrapper.sh index b77d903de..2fb781a37 100755 --- a/code-check-wrapper.sh +++ b/code-check-wrapper.sh @@ -55,6 +55,7 @@ for I in \ course_file_format.cpp \ crs_ \ duplicate_equals_t.cpp \ + dynamic_object_query.cpp \ file_dialog.cpp \ /file_format.cpp \ file_format_t.cpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 10c595c63..aeed1a2b4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -110,6 +110,7 @@ set(Mapper_Common_SRCS core/virtual_path.cpp core/objects/boolean_tool.cpp + core/objects/dynamic_object_query.cpp core/objects/object.cpp core/objects/object_mover.cpp core/objects/object_query.cpp diff --git a/src/core/objects/dynamic_object_query.cpp b/src/core/objects/dynamic_object_query.cpp new file mode 100644 index 000000000..36ca10ee7 --- /dev/null +++ b/src/core/objects/dynamic_object_query.cpp @@ -0,0 +1,519 @@ +/* + * Copyright 2026 Matthias Kühlewein + * + * This file is part of OpenOrienteering. + * + * OpenOrienteering is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenOrienteering is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenOrienteering. If not, see . + */ + +#include "dynamic_object_query.h" + +#include +#include + +#include +#include +#include + +#include "core/map.h" +#include "core/map_part.h" +#include "core/objects/object.h" +#include "core/symbols/symbol.h" + +namespace { + +struct CompOpStruct { + const QString op; + const std::function fn; +}; + +static const CompOpStruct numerical_compare_operations[6] = { + { QLatin1String("<="), [](double op1, double op2) { return op1 <= op2; } }, + { QLatin1String(">="), [](double op1, double op2) { return op1 >= op2; } }, + { QLatin1String("=="), [](double op1, double op2) { return op1 == op2; } }, + { QLatin1String("!="), [](double op1, double op2) { return op1 != op2; } }, + { QLatin1String("<"), [](double op1, double op2) { return op1 < op2; } }, + { QLatin1String(">"), [](double op1, double op2) { return op1 > op2; } }, +}; + +bool NumericalComparison(bool compare, QString element, double op1 = 0.0) +{ + for (const auto& comp : numerical_compare_operations) + { + if (element.startsWith(comp.op)) + { + const auto valuestr = element.remove(comp.op).trimmed(); + bool ok; + const double op2 = valuestr.toDouble(&ok); + if (!compare) + return ok; + return (comp.fn)(op1, op2); + } + } + return false; +} + +bool EvaluateAndOrOperation(const QString& element, int& and_or_operation) +{ + if (element == QLatin1String("AND")) + and_or_operation = 0; + else if (element == QLatin1String("OR")) + and_or_operation = 1; + else + return false; + return true; +} + +bool EvaluatePaperReal(const QString& element, int& type) +{ + if (element == QLatin1String("PAPER")) + type = 0; + else if (element == QLatin1String("REAL")) + type = 1; + else + return false; + return true; +} + +bool EvaluateSymbolType(QString element, int symbol_type, bool& result) +{ + const QStringList symbol_types = {QLatin1String("Point"), QLatin1String("Line"), QLatin1String("Area"), QLatin1String("Text"), QLatin1String("Combined")}; + int operation; + + if (element.startsWith(QLatin1String("=="))) + operation = 1; + else if (element.startsWith(QLatin1String("!="))) + operation = 0; + else + return false; + + const auto valuestr = element.remove(0, 2).trimmed(); + //auto index = symbol_types.indexOf(valuestr, 0, Qt::CaseInsensitive); // Qt 6.7 + auto match = std::find_if(symbol_types.begin(), symbol_types.end(), [&valuestr](const auto& item) { + return !item.compare(valuestr, Qt::CaseInsensitive); + }); + if (match != symbol_types.end()) + { + auto index = match - symbol_types.begin(); + result = operation ? (1<getType()) + { + case DynamicObjectQuery::AreaObjectQuery: + if (object->getType() == Object::Path) + { + const auto& path_object = static_cast(object); + const auto symbol = path_object->getSymbol(); + if (symbol && symbol->getContainedTypes() & Symbol::Area) + return static_cast(dynamic_query)->performQuery(path_object); + } + return false; + case DynamicObjectQuery::LineObjectQuery: + if (object->getType() == Object::Path) + { + const auto& path_object = static_cast(object); + return static_cast(dynamic_query)->performQuery(path_object); + } + return false; + case DynamicObjectQuery::SymbolQuery: + { + const auto symbol = object->getSymbol(); + const auto map = object->getMap(); + return symbol && map ? static_cast(dynamic_query)->performQuery(map, symbol) : false; + } + case DynamicObjectQuery::GeneralObjectQuery: + { + const auto map = object->getMap(); + return map ? static_cast(dynamic_query)->performQuery(map, object) : false; + } + default: + return false; // we should not get here + } + + Q_UNREACHABLE(); +} + +const QStringList DynamicObjectQueryManager::getContextKeywords(const QString& text, int position, bool& append) +{ + int keyword_found = -1; + int i = position - 1; + for ( ; i > 3; --i) // consider shortest keyword + { + if (text.at(i) == QLatin1Char(')')) + { + break; + } + else if (text.at(i) == QLatin1Char('(')) + { + for (auto j = 0; j < keywords.size(); ++j) + { + auto match = text.lastIndexOf(keywords[j], i-1); + if (match != -1 && match == i - keywords[j].length()) + { + keyword_found = j; + break; + } + } + } + } + + if (keyword_found == -1) + { + QStringList context_keywords; + for (auto keyword : keywords) + context_keywords.append(keyword.append(QLatin1String("()"))); + append = true; + return context_keywords; + } + + auto parameters = text.mid(i, position - i).trimmed(); + + switch (keyword_found) + { + case DynamicObjectQuery::LineObjectQuery: + case DynamicObjectQuery::AreaObjectQuery: + for (const auto& comp : numerical_compare_operations) + { + if (parameters.endsWith(comp.op)) + return QStringList(); + } + { + QStringList keywords = {keyword_found == DynamicObjectQuery::LineObjectQuery ? QLatin1String("ISTOOSHORT;") : QLatin1String("ISTOOSMALL;")}; + keywords += QString(QLatin1String("PAPER; REAL; AND; OR;")).split(QLatin1Char(' ')); + for (const auto& comp : numerical_compare_operations) + keywords += comp.op; + return keywords; + } + + case DynamicObjectQuery::SymbolQuery: + if (parameters.endsWith(QLatin1String("==")) || parameters.endsWith(QLatin1String("!="))) + { + const auto find_type_keyword = parameters.lastIndexOf(QLatin1String("TYPE")); + const auto find_id_keyword = parameters.lastIndexOf(QLatin1String("ID")); + if (find_type_keyword >= find_id_keyword) // if both keywords are not found the '=' part is needed for the -1 values + { + return QString(QLatin1String("Point; Line; Area; Text; Combined;")).split(QLatin1Char(' ')); + } + return QStringList(); + } + return QString(QLatin1String("ISUNDEFINED; TYPE; ID; == != AND; OR;")).split(QLatin1Char(' ')); + + case DynamicObjectQuery::GeneralObjectQuery: + return QStringList{QLatin1String("IGNORESYMBOL;"), QLatin1String("ISDUPLICATE;")}; + + default: + return QStringList(); // we should not get here + } + + Q_UNREACHABLE(); +} + + +AreaObjectQuery::AreaObjectQuery(QStringRef token_attributes_text) +: DynamicObjectQuery(DynamicObjectQuery::AreaObjectQuery) +{ + parseTokenAttributes(token_attributes_text); + valid = performQuery(nullptr); // dry run +} + +bool AreaObjectQuery::performQuery(const PathObject* path_object) const +{ + int area_type = 0; // 0 = PAPER, 1 = REAL + int and_or_operation = 1; // 0 = AND, 1 = OR + double object_value; + int object_value_status = -1; // -1 = undefined, 0 = PAPER value, 1 = REAL value + + bool result = false; + for (auto& element : attributes) + { + if (EvaluateAndOrOperation(element, and_or_operation)) + continue; + else if (EvaluatePaperReal(element, area_type)) + continue; + else if (element == QLatin1String("ISTOOSMALL")) + { + const bool is_too_small = path_object ? path_object->isAreaTooSmall() : false; + result = and_or_operation ? (result || is_too_small) : (result && is_too_small); + and_or_operation = 0; // default after first operation is AND operation + } + else if (NumericalComparison(false, element)) + { + if (object_value_status != area_type) + { + object_value = path_object ? (area_type ? path_object->calculateRealArea() : path_object->calculatePaperArea()) : 0.0; + object_value_status = area_type; + } + const bool comparison_result = NumericalComparison(true, element, object_value); + result = and_or_operation ? (result || comparison_result) : (result && comparison_result); + and_or_operation = 0; // default after first operation is AND operation + } + else // unknown element or failure in NumericalComparison() + { + if (!path_object) + return false; // dry run failed + break; + } + } + if (path_object) + return result; + return true; // dry run was successful +} + + +LineObjectQuery::LineObjectQuery(QStringRef token_attributes_text) +: DynamicObjectQuery(DynamicObjectQuery::LineObjectQuery) +{ + parseTokenAttributes(token_attributes_text); + valid = performQuery(nullptr); // dry run +} + +bool LineObjectQuery::performQuery(const PathObject* path_object) const +{ + int line_type = 0; // 0 = PAPER, 1 = REAL + int and_or_operation = 1; // 0 = AND, 1 = OR + double object_value; + int object_value_status = -1; // -1 = undefined, 0 = PAPER value, 1 = REAL value + + bool result = false; + for (auto& element : attributes) + { + if (EvaluateAndOrOperation(element, and_or_operation)) + continue; + else if (EvaluatePaperReal(element, line_type)) + continue; + else if (element == QLatin1String("ISTOOSHORT")) + { + const bool is_too_short = path_object ? path_object->isLineTooShort() : false; + result = and_or_operation ? (result || is_too_short) : (result && is_too_short); + and_or_operation = 0; // default after first operation is AND operation + } + else if (NumericalComparison(false, element)) + { + if (object_value_status != line_type) + { + object_value = path_object ? (line_type ? path_object->getRealLength() : path_object->getPaperLength()) : 0.0; + object_value_status = line_type; + } + const bool comparison_result = NumericalComparison(true, element, object_value); + result = and_or_operation ? (result || comparison_result) : (result && comparison_result); + and_or_operation = 0; // default after first operation is AND operation + } + else // unknown element or failure in NumericalComparison() + { + if (!path_object) + return false; // dry run failed + break; + } + } + if (path_object) + return result; + return true; // dry run was successful +} + + +SymbolQuery::SymbolQuery(QStringRef token_attributes_text) +: DynamicObjectQuery(DynamicObjectQuery::SymbolQuery) +{ + parseTokenAttributes(token_attributes_text); + valid = performQuery(nullptr, nullptr); // dry run +} + + +bool SymbolQuery::performQuery(const Map* map, const Symbol* symbol) const +{ + int and_or_operation = 1; // 0 = AND, 1 = OR + int symbol_property = 0; // 0 = TYPE, 1 = ID + + bool result = false; + bool eval_symbol_result; + for (auto& element : attributes) + { + if (EvaluateAndOrOperation(element, and_or_operation)) + continue; + else if (element == QLatin1String("TYPE")) + symbol_property = 0; + else if (element == QLatin1String("ID")) + symbol_property = 1; + else if (element == QLatin1String("ISUNDEFINED")) + { + const bool is_undefined = map && symbol ? (map->findSymbolIndex(symbol) < 0) : false; + result = and_or_operation ? (result || is_undefined) : (result && is_undefined); + and_or_operation = 0; // default after first operation is AND operation + } + else if (symbol_property == 0 && EvaluateSymbolType(element, (symbol ? symbol->getType() : 0), eval_symbol_result)) + { + result = and_or_operation ? (result || eval_symbol_result) : (result && eval_symbol_result); + and_or_operation = 0; // default after first operation is AND operation + } + else if (symbol_property == 1 && EvaluateSymbolId(element, (symbol ? symbol->getNumberAsString() : QLatin1String("1")), eval_symbol_result)) + { + result = and_or_operation ? (result || eval_symbol_result) : (result && eval_symbol_result); + and_or_operation = 0; // default after first operation is AND operation + } + else // unknown element or failure in EvaluateSymbolType() or EvaluateSymbolId() + { + if (!symbol) + return false; // dry run failed + break; + } + } + if (symbol) + return result; + return true; // dry run was successful +} + + +GeneralObjectQuery::GeneralObjectQuery(QStringRef token_attributes_text) +: DynamicObjectQuery(DynamicObjectQuery::GeneralObjectQuery) +{ + parseTokenAttributes(token_attributes_text); + valid = performQuery(nullptr, nullptr); // dry run +} + +bool GeneralObjectQuery::performQuery(const Map* map, const Object* object) const +{ + int and_or_operation = 1; // 0 = AND, 1 = OR + bool ignore_symbols = false; + + bool result = false; + for (auto& element : attributes) + { + if (EvaluateAndOrOperation(element, and_or_operation)) + continue; + else if (element == QLatin1String("IGNORESYMBOL")) + ignore_symbols = true; + else if (element == QLatin1String("ISDUPLICATE")) + { + if (!map || !object) + return true; + const bool isduplicate_result = map->getCurrentPart()->existsObject([object, ignore_symbols](auto const* o) + { return object != o && object->equals(o, !ignore_symbols); } + ); + result = and_or_operation ? (result || isduplicate_result) : (result && isduplicate_result); + and_or_operation = 0; // default after first operation is AND operation + } + else // unknown element + { + if (!object) + return false; // dry run failed + break; + } + } + if (object) + return result; + return true; // dry run was successful +} + + +DynamicObjectQuery::DynamicObjectQuery(Type type) noexcept +: type { type } +{ + // nothing else +} + +void DynamicObjectQuery::parseTokenAttributes(QStringRef token_attributes_text) +{ +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + attributes = token_attributes_text.toString().split(QLatin1Char(';'), Qt::SkipEmptyParts); +#else + attributes = token_attributes_text.toString().split(QLatin1Char(';'), QString::SkipEmptyParts); +#endif + for (auto& item : attributes) + item = item.trimmed(); +} + + +} // namespace OpenOrienteering diff --git a/src/core/objects/dynamic_object_query.h b/src/core/objects/dynamic_object_query.h new file mode 100644 index 000000000..f70b04b25 --- /dev/null +++ b/src/core/objects/dynamic_object_query.h @@ -0,0 +1,120 @@ +/* + * Copyright 2026 Matthias Kühlewein + * + * This file is part of OpenOrienteering. + * + * OpenOrienteering is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenOrienteering is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenOrienteering. If not, see . + */ + +#ifndef DYNAMIC_OBJECT_QUERY_H +#define DYNAMIC_OBJECT_QUERY_H + +#include + +#include +#include +#include + +namespace OpenOrienteering { + +class Map; +class Object; +class PathObject; +class Symbol; + +class DynamicObjectQuery +{ +public: + /** + * Enumeration of all possible dynamic query types. + */ + enum Type + { + LineObjectQuery = 0, + AreaObjectQuery = 1, + SymbolQuery = 2, + GeneralObjectQuery = 3 + }; + + DynamicObjectQuery(Type type) noexcept; + ~DynamicObjectQuery(); + + Type getType() const { return type; } + bool IsValid() const { return valid; } + +protected: + void parseTokenAttributes(QStringRef token_attributes_text); + + QStringList attributes; + bool valid; + +private: + Type type; +}; + + +class AreaObjectQuery : public DynamicObjectQuery +{ +public: + AreaObjectQuery(QStringRef token_attributes_text); + + bool performQuery(const PathObject* path_object) const; +}; + + +class LineObjectQuery : public DynamicObjectQuery +{ +public: + LineObjectQuery(QStringRef token_attributes_text); + + bool performQuery(const PathObject* path_object) const; +}; + + +class SymbolQuery : public DynamicObjectQuery +{ +public: + SymbolQuery(QStringRef token_attributes_text); + + bool performQuery(const Map* map, const Symbol* symbol) const; +}; + + +class GeneralObjectQuery : public DynamicObjectQuery +{ +public: + GeneralObjectQuery(QStringRef token_attributes_text); + + bool performQuery(const Map* map, const Object* object) const; +}; + + +class DynamicObjectQueryManager +{ +public: + DynamicObjectQueryManager(); + + static bool performDynamicQuery(const Object* object, const DynamicObjectQuery* dynamic_query); + static const QStringList getContextKeywords(const QString& text, int position, bool& append); + + DynamicObjectQuery* parse(QStringRef token_text, QStringRef token_attributes_text); + +private: + static const QStringList keywords; +}; + + +} // namespace OpenOrienteering + +#endif // DYNAMIC_OBJECT_QUERY_H diff --git a/src/core/objects/object_query.cpp b/src/core/objects/object_query.cpp index 258f8f2a2..bcfbfed9c 100644 --- a/src/core/objects/object_query.cpp +++ b/src/core/objects/object_query.cpp @@ -148,7 +148,7 @@ class ObjectQueryNesting * Create a cheap placeholder ObjectQuery. * * During object query parsing, operands right of operators are unknown - * at construction time. However, ObjectQuery does not allowed to construct + * at construction time. However, ObjectQuery does not allow to construct * logical query instances with invalid operands. This function creates valid * placeholders which can be replaced with an invalid operand in a second step. */ @@ -224,6 +224,10 @@ ObjectQuery::ObjectQuery(const ObjectQuery& query) { symbol = query.symbol; } + else if (op == ObjectQuery::OperatorDynamic) + { + dynamic_query = query.dynamic_query; + } } @@ -251,7 +255,7 @@ ObjectQuery& ObjectQuery::operator=(ObjectQuery&& proto) noexcept return *this; reset(); - consume(std::move(proto)); + consume(std::move(proto)); return *this; } @@ -336,6 +340,14 @@ ObjectQuery::ObjectQuery(const Symbol* symbol) noexcept } +ObjectQuery::ObjectQuery(const DynamicObjectQuery* dynamic_query) noexcept +: op { ObjectQuery::OperatorDynamic } +, dynamic_query { dynamic_query } +{ + // nothing else +} + + // static ObjectQuery ObjectQuery::negation(ObjectQuery query) noexcept { @@ -388,6 +400,10 @@ QString ObjectQuery::labelFor(ObjectQuery::Operator op) //: Very short label return tr("Symbol"); + case OperatorDynamic: + //: Very short label + return tr("Dynamic"); + case OperatorInvalid: //: Very short label return tr("invalid"); @@ -522,6 +538,10 @@ bool ObjectQuery::operator()(const Object* object) const case OperatorSymbol: return object->getSymbol() == symbol; + case OperatorDynamic: + Q_ASSERT(dynamic_query); + return DynamicObjectQueryManager::performDynamicQuery(object, dynamic_query); + case OperatorInvalid: return false; } @@ -631,6 +651,7 @@ QString ObjectQuery::toString() const ret = QLatin1String("SYMBOL \"") + (symbol ? symbol->getNumberAsString() : QString{}) + QLatin1Char('\"'); break; + case OperatorDynamic: //TODO case OperatorInvalid: // Default empty string is sufficient break; @@ -661,6 +682,13 @@ void ObjectQuery::reset() { op = ObjectQuery::OperatorInvalid; } + else if (op == ObjectQuery::OperatorDynamic) + { + Q_ASSERT(dynamic_query); + if (dynamic_query) + delete dynamic_query; + op = ObjectQuery::OperatorInvalid; + } } @@ -686,6 +714,10 @@ void ObjectQuery::consume(ObjectQuery&& other) { symbol = other.symbol; } + else if (op == ObjectQuery::OperatorDynamic) + { + dynamic_query = other.dynamic_query; + } other.op = ObjectQuery::OperatorInvalid; } @@ -724,6 +756,9 @@ bool operator==(const ObjectQuery& lhs, const ObjectQuery& rhs) case ObjectQuery::OperatorSymbol: return lhs.symbol == rhs.symbol; + case ObjectQuery::OperatorDynamic: + return lhs.dynamic_query == rhs.dynamic_query; + case ObjectQuery::OperatorInvalid: return false; } @@ -918,6 +953,11 @@ ObjectQuery ObjectQueryParser::parse(const QString& text) } getToken(); } + else if (token == TokenDynamicQuery && !*current) + { + *current = ObjectQuery{dynamic_token}; + getToken(); + } else { // Invalid input @@ -1045,6 +1085,29 @@ void ObjectQueryParser::getToken() token = TokenAnd; else if (token_text == QLatin1String("NOT")) token = TokenNot; + else if (current == QLatin1Char('(')) + { + int token_attributes_start = ++pos; + for ( ; pos < input.length(); ++pos) + { + if (input.at(pos) == QLatin1Char(')')) + { + token_attributes_text = input.mid(token_attributes_start, pos - token_attributes_start); // n == 0 ? + //qDebug("Token: %s\n",qUtf8Printable(token_text.toString())); + //qDebug("Tokenattr.: %s\n",qUtf8Printable(token_attributes_text.toString())); + dynamic_token = dynamic_object_manager.parse(token_text, token_attributes_text); //TODO: make smartptr? + if (dynamic_token) + { + if (dynamic_token->IsValid()) + token = TokenDynamicQuery; + else + delete dynamic_token; // TODO: show information about failure + } + ++pos; + break; + } + } + } else if (token_text == QLatin1String("SYMBOL")) token = TokenSymbol; else diff --git a/src/core/objects/object_query.h b/src/core/objects/object_query.h index e002eaa77..9caa37a78 100644 --- a/src/core/objects/object_query.h +++ b/src/core/objects/object_query.h @@ -24,6 +24,8 @@ #include +#include "core/objects/dynamic_object_query.h" + #include #include #include @@ -77,6 +79,7 @@ class ObjectQuery // More operators, 32 .. OperatorSymbol = 32, ///< Test the symbol for equality. + OperatorDynamic = 33, OperatorInvalid = 0 ///< Marks an invalid query }; @@ -146,6 +149,10 @@ class ObjectQuery */ ObjectQuery(const Symbol* symbol) noexcept; + /** + * Constructs a query for . + */ + ObjectQuery(const DynamicObjectQuery* dynamic_query) noexcept; /** * Returns a query which is the negation of the sub-query. @@ -229,6 +236,7 @@ class ObjectQuery LogicalOperands subqueries; StringOperands tags; SymbolOperand symbol; + const DynamicObjectQuery* dynamic_query; }; }; @@ -300,6 +308,7 @@ class ObjectQueryParser TokenLeftParen, TokenRightParen, TokenNumericalOperator, + TokenDynamicQuery, }; private: @@ -309,10 +318,13 @@ class ObjectQueryParser const Symbol* findSymbol(const QString& key) const; + DynamicObjectQueryManager dynamic_object_manager; const Map* map = nullptr; QStringRef input; QStringRef token_text; + QStringRef token_attributes_text; TokenType token; + DynamicObjectQuery* dynamic_token; int token_start = -1; int pos; }; diff --git a/src/gui/map/map_find_feature.cpp b/src/gui/map/map_find_feature.cpp index 18af89657..300b831b4 100644 --- a/src/gui/map/map_find_feature.cpp +++ b/src/gui/map/map_find_feature.cpp @@ -1,6 +1,6 @@ /* * Copyright 2017-2020, 2024, 2025 Kai Pastor - * Copyright 2025 Matthias Kühlewein + * Copyright 2026 Matthias Kühlewein * * This file is part of OpenOrienteering. * @@ -36,11 +36,13 @@ #include #include #include +#include #include #include #include "core/map.h" #include "core/map_part.h" +#include "core/objects/dynamic_object_query.h" #include "core/objects/object.h" #include "core/objects/object_query.h" #include "core/symbols/symbol.h" @@ -189,7 +191,6 @@ ObjectQuery MapFindFeature::makeQuery() const query = ObjectQuery{ ObjectQuery(ObjectQuery::OperatorSearch, text), ObjectQuery::OperatorOr, ObjectQuery(ObjectQuery::OperatorObjectText, text) }; - } } else @@ -340,6 +341,12 @@ void MapFindTextEdit::insertKeyword(QAction* action) { const auto keyword = action->data().toString(); insertPlainText(keyword); + if (keyword.endsWith(QLatin1Char(')'))) + { + auto position = textCursor(); + if (position.movePosition(QTextCursor::Left)) + setTextCursor(position); + } } // override @@ -351,8 +358,13 @@ void MapFindTextEdit::contextMenuEvent(QContextMenuEvent* event) insert_menu->menuAction()->setMenuRole(QAction::NoRole); auto* keyword_actions_group = new QActionGroup(this); - auto keywords = Object::getObjectProperties(); - keywords.insert(keywords.end(), {QLatin1String("SYMBOL"), QLatin1String("AND"), QLatin1String("OR"), QLatin1String("NOT")}); + //auto keywords = Object::getObjectProperties(); TBR + bool append = false;; + auto keywords = DynamicObjectQueryManager::getContextKeywords(toPlainText(), textCursor().position(), append); + if (append) + { + keywords.append({QLatin1String(" SYMBOL "), QLatin1String(" AND " ), QLatin1String(" OR "), QLatin1String(" NOT ")}); + } for (auto& keyword : keywords) { auto* action = new QAction(keyword, this); diff --git a/src/gui/map/map_find_feature.h b/src/gui/map/map_find_feature.h index 3d52088c9..c568ccc47 100644 --- a/src/gui/map/map_find_feature.h +++ b/src/gui/map/map_find_feature.h @@ -1,6 +1,6 @@ /* * Copyright 2017-2019, 2025 Kai Pastor - * Copyright 2025 Matthias Kühlewein + * Copyright 2026 Matthias Kühlewein * * This file is part of OpenOrienteering. * From 90f14f2c298c4528c31156c4e7e31bf113aff3b9 Mon Sep 17 00:00:00 2001 From: Matthias Kuehlewein Date: Sat, 3 Jan 2026 17:35:24 +0100 Subject: [PATCH 11/16] Cleanup previous approach --- src/core/objects/dynamic_object_query.cpp | 2 +- src/core/objects/object.cpp | 73 +-------- src/core/objects/object.h | 30 +--- src/core/objects/object_query.cpp | 184 ++-------------------- src/core/objects/object_query.h | 20 +-- src/gui/map/map_find_feature.cpp | 3 +- src/gui/map/map_widget.cpp | 2 +- src/gui/map/map_widget.h | 2 +- 8 files changed, 21 insertions(+), 295 deletions(-) diff --git a/src/core/objects/dynamic_object_query.cpp b/src/core/objects/dynamic_object_query.cpp index 36ca10ee7..5fdfc8dea 100644 --- a/src/core/objects/dynamic_object_query.cpp +++ b/src/core/objects/dynamic_object_query.cpp @@ -289,7 +289,7 @@ const QStringList DynamicObjectQueryManager::getContextKeywords(const QString& t } return QStringList(); } - return QString(QLatin1String("ISUNDEFINED; TYPE; ID; == != AND; OR;")).split(QLatin1Char(' ')); + return QString(QLatin1String("ISUNDEFINED; TYPE; ID; AND; OR; == !=")).split(QLatin1Char(' ')); case DynamicObjectQuery::GeneralObjectQuery: return QStringList{QLatin1String("IGNORESYMBOL;"), QLatin1String("ISDUPLICATE;")}; diff --git a/src/core/objects/object.cpp b/src/core/objects/object.cpp index 9e8e9b52a..7343886f7 100644 --- a/src/core/objects/object.cpp +++ b/src/core/objects/object.cpp @@ -65,7 +65,6 @@ class QRectF; namespace literal { - // map file static const QLatin1String object("object"); static const QLatin1String symbol("symbol"); static const QLatin1String type("type"); @@ -77,44 +76,11 @@ namespace literal static const QLatin1String rotation("rotation"); static const QLatin1String size("size"); static const QLatin1String tags("tags"); - - // object properties - // boolean operations - static const QLatin1String UndefinedSymbol(".UndefinedSymbol"); - static const QLatin1String AreaTooSmall(".AreaTooSmall"); - static const QLatin1String LineTooShort(".LineTooShort"); - // comparisons - static const QLatin1String PaperArea(".PaperArea"); - static const QLatin1String RealArea(".RealArea"); - static const QLatin1String PaperLength(".PaperLength"); - static const QLatin1String RealLength(".RealLength"); } -namespace OpenOrienteering { - -static const std::vector object_properties = {literal::UndefinedSymbol, literal::AreaTooSmall, literal::LineTooShort, - literal::PaperArea, literal::RealArea, literal::PaperLength, literal::RealLength}; - -bool Object::isObjectProperty(const QString& property) -{ - return std::find(object_properties.begin(), object_properties.end(), property) != object_properties.end(); -} - -bool Object::isBooleanObjectProperty(const QString& property) -{ - return std::find(object_properties.begin(), object_properties.begin() + 3, property) != object_properties.begin() + 3; -} -bool Object::isComparisonObjectProperty(const QString& property) -{ - return std::find(object_properties.begin() + 3, object_properties.end(), property) != object_properties.end(); -} - -const std::vector& Object::getObjectProperties() -{ - return object_properties; -} +namespace OpenOrienteering { // ### Object implementation ### @@ -789,17 +755,6 @@ void Object::includeControlPointsRect(QRectF& rect) const } -QVariant Object::getObjectProperty(const QString& property) const -{ - if (property == literal::UndefinedSymbol) - { - if (map && symbol) - return QVariant(map->findSymbolIndex(symbol) < 0); - } - - return QVariant(); -} - // ### PathPart ### @@ -3265,32 +3220,6 @@ bool PathObject::isLineTooShort() const } -// override -QVariant PathObject::getObjectProperty(const QString& property) const -{ - if (property == literal::AreaTooSmall) - return QVariant(isAreaTooSmall()); - - if (property == literal::LineTooShort) - return QVariant(isLineTooShort()); - - if (property == literal::PaperArea) - return QVariant(calculatePaperArea()); - - if (property == literal::RealArea) - return QVariant(calculateRealArea()); - - if (property == literal::PaperLength) - return QVariant(getPaperLength()); - - if (property == literal::RealLength) - return QVariant(getRealLength()); - - return Object::getObjectProperty(property); // pass to base class function -} - - - // ### PointObject ### PointObject::PointObject(const Symbol* symbol) diff --git a/src/core/objects/object.h b/src/core/objects/object.h index 1bbeea989..816ad8aa8 100644 --- a/src/core/objects/object.h +++ b/src/core/objects/object.h @@ -96,7 +96,6 @@ friend class XMLImportExport; /** Creates an empty object with the given type, symbol, coords and (optional) map. */ explicit Object(Type type, const Symbol* symbol, MapCoordVector coords, Map* map = nullptr); - protected: /** * Constructs an Object, initialized from the given prototype. @@ -318,26 +317,6 @@ friend class XMLImportExport; */ void includeControlPointsRect(QRectF& rect) const; - - /** - * Returns dynamic object properties that are common for all objects. - * Derived object classes (i.e., PathObject) override this function to return class specific object properties. - * If derived object classes don't provide a requested property, they invoke the base class function. - */ - virtual QVariant getObjectProperty(const QString& property) const; - - /** Returns true if property is a dynamic object property. */ - static bool isObjectProperty(const QString& property); - - /** Returns true if property is a dynamic object property that returns a boolean value. */ - static bool isBooleanObjectProperty(const QString& property); - - /** Returns true if property is a dynamic object property that returns a value for comparison. */ - static bool isComparisonObjectProperty(const QString& property); - - /** Returns keywords of the available dynamic object properties. */ - static const std::vector& getObjectProperties(); - protected: virtual void updateEvent() const; @@ -946,14 +925,6 @@ class PathObject : public Object // clazy:exclude=copyable-polymorphic bool isLineTooShort() const; - /** - * Returns dynamic object properties that are specific for the PathObject class. - * Note: Overrides the base class function that returns dynamic object properties that are common for all objects. - * If a requested property is not provided by PathObject, the base class function is invoked. - */ - QVariant getObjectProperty(const QString& property) const override; - - protected: /** * Adjusts the end index of the given part and the start/end indexes of the following parts. @@ -1164,6 +1135,7 @@ struct ObjectPathCoord : public PathCoord }; + //### Object inline code ### inline diff --git a/src/core/objects/object_query.cpp b/src/core/objects/object_query.cpp index bcfbfed9c..bc72050b9 100644 --- a/src/core/objects/object_query.cpp +++ b/src/core/objects/object_query.cpp @@ -1,7 +1,7 @@ /* * Copyright 2016 Mitchell Krome - * Copyright 2017-2025 Kai Pastor - * Copyright 2025 Matthias Kühlewein + * Copyright 2017-2024 Kai Pastor + * Copyright 2026 Matthias Kühlewein * * This file is part of OpenOrienteering. * @@ -46,7 +46,7 @@ namespace { -static QChar special_chars[11] = { +static QChar special_chars[9] = { QLatin1Char('"'), QLatin1Char(' '), QLatin1Char('\t'), @@ -55,9 +55,7 @@ static QChar special_chars[11] = { QLatin1Char('='), QLatin1Char('!'), QLatin1Char('~'), - QLatin1Char('\\'), - QLatin1Char('<'), - QLatin1Char('>') + QLatin1Char('\\') }; QString toEscaped(QString string) @@ -270,10 +268,10 @@ ObjectQuery::ObjectQuery(const QString& key, ObjectQuery::Operator op, const QSt : op { op } , tags { key, value } { - // Can't have an empty key (but can have empty value (if not a numerical operator)) + // Can't have an empty key (but can have empty value) // Must be a key/value operator - Q_ASSERT(IsTagOperator(op) || IsNumericalOperator(op)); // 16..18 and 24..27 - if ((!IsTagOperator(op) && !IsNumericalOperator(op)) || key.length() == 0 || (value.length() == 0 && IsNumericalOperator(op))) + Q_ASSERT(IsTagOperator(op)); // 16..18 + if (!IsTagOperator(op) || key.length() == 0) { reset(); } @@ -377,15 +375,6 @@ QString ObjectQuery::labelFor(ObjectQuery::Operator op) //: Very short label return tr("Text"); - case OperatorLess: - return tr("less than"); - case OperatorLessOrEqual: - return tr("less or equal than"); - case OperatorGreater: - return tr("greater than"); - case OperatorGreaterOrEqual: - return tr("greater or equal than"); - case OperatorAnd: //: Very short label return tr("and"); @@ -412,75 +401,6 @@ QString ObjectQuery::labelFor(ObjectQuery::Operator op) Q_UNREACHABLE(); } -bool ObjectQuery::getBooleanObjectProperty(const Object* object, const StringOperands& tags, bool& value) const -{ - // check if tag refers to boolean object properties - if (Object::isBooleanObjectProperty(tags.value)) - { - auto property = QVariant(); - if (object->getType() == Object::Path) - { - const auto& path_object = static_cast(object); - property = path_object->getObjectProperty(tags.value); - } - else - { - property = object->getObjectProperty(tags.value); - } - if (property.isValid() && static_cast(property.type()) == QMetaType::Bool) - { - value = property.toBool(); - return true; - } - } - return false; -} - -bool ObjectQuery::compareObjectProperty(const Object* object, const StringOperands& tags, Operator op) const -{ - // check if tag refers to comparison object properties - if (Object::isComparisonObjectProperty(tags.key)) - { - auto property = QVariant(); - if (object->getType() == Object::Path) - { - const auto& path_object = static_cast(object); - property = path_object->getObjectProperty(tags.key); - } - else - { - property = object->getObjectProperty(tags.key); - } - - if (property.isValid() && static_cast(property.type()) == QMetaType::Double) - { - bool ok; - const auto comp_value = tags.value.toDouble(&ok); - if (ok) - { - const auto value = property.toDouble(&ok); - if (ok) - { - switch(op) - { - case OperatorLess: - return value < comp_value; - case OperatorLessOrEqual: - return value <= comp_value; - case OperatorGreater: - return value > comp_value; - case OperatorGreaterOrEqual: - return value >= comp_value; - default: - return false; // unreachable - } - Q_UNREACHABLE(); - } - } - } - } - return false; -} bool ObjectQuery::operator()(const Object* object) const { @@ -503,9 +423,6 @@ bool ObjectQuery::operator()(const Object* object) const return it != container.end() && it->value.contains(tags.value); } (object->tags(), tags); case OperatorSearch: - bool value; - if (getBooleanObjectProperty(object, tags, value)) - return value; if (object->getSymbol() && object->getSymbol()->getName().contains(tags.value, Qt::CaseInsensitive)) return true; for (auto const& current : object->tags()) @@ -516,18 +433,10 @@ bool ObjectQuery::operator()(const Object* object) const } return false; case OperatorObjectText: - if (Object::isObjectProperty(tags.value)) // don't search for object properties keywords - return false; if (object->getType() == Object::Text) return static_cast(object)->getText().contains(tags.value, Qt::CaseInsensitive); return false; - case OperatorLess: - case OperatorLessOrEqual: - case OperatorGreater: - case OperatorGreaterOrEqual: - return compareObjectProperty(object, tags, op); - case OperatorAnd: return (*subqueries.first)(object) && (*subqueries.second)(object); case OperatorOr: @@ -616,19 +525,6 @@ QString ObjectQuery::toString() const ret = QLatin1Char('"') + toEscaped(tags.value) + QLatin1Char('"'); break; - case OperatorLess: - ret = keyToString(tags.key) + QLatin1String(" < ") + toEscaped(tags.value); - break; - case OperatorLessOrEqual: - ret = keyToString(tags.key) + QLatin1String(" <= ") + toEscaped(tags.value); - break; - case OperatorGreater: - ret = keyToString(tags.key) + QLatin1String(" > ") + toEscaped(tags.value); - break; - case OperatorGreaterOrEqual: - ret = keyToString(tags.key) + QLatin1String(" >= ") + toEscaped(tags.value); - break; - case OperatorAnd: if (subqueries.first->getOperator() == OperatorOr) ret = QLatin1Char('(') + subqueries.first->toString() + QLatin1Char(')'); @@ -651,7 +547,7 @@ QString ObjectQuery::toString() const ret = QLatin1String("SYMBOL \"") + (symbol ? symbol->getNumberAsString() : QString{}) + QLatin1Char('\"'); break; - case OperatorDynamic: //TODO + case OperatorDynamic: //TODO? case OperatorInvalid: // Default empty string is sufficient break; @@ -740,10 +636,6 @@ bool operator==(const ObjectQuery& lhs, const ObjectQuery& rhs) case ObjectQuery::OperatorContains: case ObjectQuery::OperatorSearch: case ObjectQuery::OperatorObjectText: - case ObjectQuery::OperatorLess: - case ObjectQuery::OperatorLessOrEqual: - case ObjectQuery::OperatorGreater: - case ObjectQuery::OperatorGreaterOrEqual: return lhs.tags == rhs.tags; case ObjectQuery::OperatorAnd: @@ -827,42 +719,6 @@ ObjectQuery ObjectQueryParser::parse(const QString& text) break; } } - else if (token == TokenNumericalOperator) - { - auto op = token_text; - auto num_op = token_text.toString(); - getToken(); - if ((token == TokenWord || token == TokenString) && Object::isComparisonObjectProperty(key)) - { - auto value = tokenAsString(); - if (num_op == QLatin1String("<")) - { - *current = { key, ObjectQuery::OperatorLess , value }; - } - else if (num_op == QLatin1String("<=")) - { - *current = { key, ObjectQuery::OperatorLessOrEqual , value }; - } - else if (num_op == QLatin1String(">")) - { - *current = { key, ObjectQuery::OperatorGreater, value }; - } - else if (num_op == QLatin1String(">=")) - { - *current = { key, ObjectQuery::OperatorGreaterOrEqual, value }; - } - else // can not happen - { - qWarning("Undefined operation %s", qUtf8Printable(num_op)); - } - getToken(); - } - else - { - op = {}; - break; - } - } else { *current = { ObjectQuery::OperatorSearch, key }; @@ -1041,20 +897,6 @@ void ObjectQueryParser::getToken() token_text = input.mid(token_start, 2); pos += 2; } - else if (current == QLatin1Char('<') || current == QLatin1Char('>')) - { - token = TokenNumericalOperator; - if (pos+1 < input.length() && input.at(pos+1) == QLatin1Char('=')) - { - token_text = input.mid(token_start, 2); - pos += 2; - } - else - { - token_text = input.mid(token_start, 1); - ++pos; - } - } else { for (++pos; pos < input.length(); ++pos) @@ -1065,9 +907,7 @@ void ObjectQueryParser::getToken() || current == QLatin1Char('\t') || current == QLatin1Char('(') || current == QLatin1Char(')') - || current == QLatin1Char('=') - || current == QLatin1Char('<') - || current == QLatin1Char('>')) + || current == QLatin1Char('=')) { break; } @@ -1092,16 +932,14 @@ void ObjectQueryParser::getToken() { if (input.at(pos) == QLatin1Char(')')) { - token_attributes_text = input.mid(token_attributes_start, pos - token_attributes_start); // n == 0 ? - //qDebug("Token: %s\n",qUtf8Printable(token_text.toString())); - //qDebug("Tokenattr.: %s\n",qUtf8Printable(token_attributes_text.toString())); + token_attributes_text = input.mid(token_attributes_start, pos - token_attributes_start); dynamic_token = dynamic_object_manager.parse(token_text, token_attributes_text); //TODO: make smartptr? if (dynamic_token) { if (dynamic_token->IsValid()) token = TokenDynamicQuery; else - delete dynamic_token; // TODO: show information about failure + delete dynamic_token; // TODO: show information about failure? } ++pos; break; diff --git a/src/core/objects/object_query.h b/src/core/objects/object_query.h index 9caa37a78..d263476dd 100644 --- a/src/core/objects/object_query.h +++ b/src/core/objects/object_query.h @@ -1,7 +1,7 @@ /* * Copyright 2016 Mitchell Krome - * Copyright 2017-2025 Kai Pastor - * Copyright 2025 Matthias Kühlewein + * Copyright 2017-2024 Kai Pastor + * Copyright 2026 Matthias Kühlewein * * This file is part of OpenOrienteering. * @@ -30,7 +30,6 @@ #include #include #include -#include namespace OpenOrienteering { @@ -71,15 +70,9 @@ class ObjectQuery OperatorSearch = 19, ///< Tests if the symbol name, a tag key or a tag value contains the given value (case-insensitive) OperatorObjectText = 20, ///< Text object content (case-insensitive) - // Operators 24 .. 27 operate on object properties - OperatorLess = 24, - OperatorLessOrEqual = 25, - OperatorGreater = 26, - OperatorGreaterOrEqual = 27, - // More operators, 32 .. OperatorSymbol = 32, ///< Test the symbol for equality. - OperatorDynamic = 33, + OperatorDynamic = 33, ///< Processing dynamic object properties OperatorInvalid = 0 ///< Marks an invalid query }; @@ -150,7 +143,7 @@ class ObjectQuery ObjectQuery(const Symbol* symbol) noexcept; /** - * Constructs a query for . + * Constructs a query for a dynamic object property. */ ObjectQuery(const DynamicObjectQuery* dynamic_query) noexcept; @@ -218,14 +211,10 @@ class ObjectQuery */ void consume(ObjectQuery&& other); - bool getBooleanObjectProperty(const Object* object, const StringOperands& tags, bool& value) const; - bool compareObjectProperty(const Object* object, const StringOperands& tags, Operator op) const; - bool IsLogicalOperator(Operator op) const { return op >= 1 && op <= 3; } bool IsTagOperator(Operator op) const { return op >= 16 && op <= 18; } bool IsValueOperator(Operator op) const { return op >= 19 && op <= 20; } bool IsStringOperator(Operator op) const { return op >= 16 && op <= 20; } - bool IsNumericalOperator(Operator op) const { return op >= 24 && op <= 27; } using SymbolOperand = const Symbol*; @@ -307,7 +296,6 @@ class ObjectQueryParser TokenNot, TokenLeftParen, TokenRightParen, - TokenNumericalOperator, TokenDynamicQuery, }; diff --git a/src/gui/map/map_find_feature.cpp b/src/gui/map/map_find_feature.cpp index 300b831b4..d68ad1962 100644 --- a/src/gui/map/map_find_feature.cpp +++ b/src/gui/map/map_find_feature.cpp @@ -358,8 +358,7 @@ void MapFindTextEdit::contextMenuEvent(QContextMenuEvent* event) insert_menu->menuAction()->setMenuRole(QAction::NoRole); auto* keyword_actions_group = new QActionGroup(this); - //auto keywords = Object::getObjectProperties(); TBR - bool append = false;; + bool append = false; auto keywords = DynamicObjectQueryManager::getContextKeywords(toPlainText(), textCursor().position(), append); if (append) { diff --git a/src/gui/map/map_widget.cpp b/src/gui/map/map_widget.cpp index e568470c2..8439b2bed 100644 --- a/src/gui/map/map_widget.cpp +++ b/src/gui/map/map_widget.cpp @@ -1,6 +1,6 @@ /* * Copyright 2012-2014 Thomas Schöps - * Copyright 2013-2020, 2025 Kai Pastor + * Copyright 2013-2020, 2026 Kai Pastor * * This file is part of OpenOrienteering. * diff --git a/src/gui/map/map_widget.h b/src/gui/map/map_widget.h index d1fb56059..fc9cff9fe 100644 --- a/src/gui/map/map_widget.h +++ b/src/gui/map/map_widget.h @@ -1,6 +1,6 @@ /* * Copyright 2012-2014 Thomas Schöps - * Copyright 2013-2020, 2025 Kai Pastor + * Copyright 2013-2020, 2026 Kai Pastor * * This file is part of OpenOrienteering. * From b0c4a86d4207a6bb6643256698a031d94380f6bc Mon Sep 17 00:00:00 2001 From: Matthias Kuehlewein Date: Sat, 3 Jan 2026 21:05:43 +0100 Subject: [PATCH 12/16] Add shortcut for keyword insertion --- src/gui/map/map_find_feature.cpp | 32 +++++++++++++++++++++++++++++--- src/gui/map/map_find_feature.h | 7 +++++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/gui/map/map_find_feature.cpp b/src/gui/map/map_find_feature.cpp index d68ad1962..8babb4480 100644 --- a/src/gui/map/map_find_feature.cpp +++ b/src/gui/map/map_find_feature.cpp @@ -23,19 +23,27 @@ #include +#include #include #include +#include #include #include #include #include #include +#include #include // IWYU pragma: keep #include +#include +#include #include #include #include +#include #include +#include +#include #include #include #include @@ -352,9 +360,27 @@ void MapFindTextEdit::insertKeyword(QAction* action) // override void MapFindTextEdit::contextMenuEvent(QContextMenuEvent* event) { - QMenu* menu = createStandardContextMenu(event->globalPos()); + showCustomContextMenu(event->globalPos()); +} + +// override +void MapFindTextEdit::keyPressEvent(QKeyEvent* event) +{ + if (event->key() == tr("K") && (event->modifiers() & Qt::ControlModifier)) + { + showCustomContextMenu(viewport()->mapToGlobal(cursorRect().center())); + } + else + { + QTextEdit::keyPressEvent(event); + } +} + +void MapFindTextEdit::showCustomContextMenu(const QPoint& globalPos) +{ + QMenu* menu = createStandardContextMenu(globalPos); menu->addSeparator(); - auto* insert_menu = new QMenu(tr("Insert keyword..."), menu); + auto* insert_menu = new QMenu(tr("Insert keyword...\tCtrl+K"), menu); insert_menu->menuAction()->setMenuRole(QAction::NoRole); auto* keyword_actions_group = new QActionGroup(this); @@ -374,7 +400,7 @@ void MapFindTextEdit::contextMenuEvent(QContextMenuEvent* event) menu->addMenu(insert_menu); connect(keyword_actions_group, &QActionGroup::triggered, this, &MapFindTextEdit::insertKeyword); - menu->exec(event->globalPos()); + menu->exec(globalPos); delete menu; } diff --git a/src/gui/map/map_find_feature.h b/src/gui/map/map_find_feature.h index c568ccc47..2d9761410 100644 --- a/src/gui/map/map_find_feature.h +++ b/src/gui/map/map_find_feature.h @@ -24,14 +24,15 @@ #include #include #include -#include #include class QAction; class QCheckBox; class QContextMenuEvent; class QDialog; +class QKeyEvent; class QLabel; +class QPoint; class QPushButton; class QStackedLayout; class QWidget; @@ -44,7 +45,7 @@ class ObjectQuery; class TagSelectWidget; /** - * The context menu (right click) is extended by the possibility + * The context menu (right click or Ctrl+K) is extended by the possibility * to select and insert one of the keywords (e.g., SYMBOL, AND...) */ class MapFindTextEdit : public QTextEdit @@ -53,6 +54,8 @@ class MapFindTextEdit : public QTextEdit private: void contextMenuEvent(QContextMenuEvent* event) override; + void keyPressEvent(QKeyEvent* event) override; + void showCustomContextMenu(const QPoint& globalPos); private slots: void insertKeyword(QAction* action); From cab660d9bbeac11c8ace1783f12de2aae962f814 Mon Sep 17 00:00:00 2001 From: Matthias Kuehlewein Date: Wed, 7 Jan 2026 15:51:26 +0100 Subject: [PATCH 13/16] Search for duplicated objects can ignore object tags --- src/core/objects/dynamic_object_query.cpp | 9 ++++++--- src/core/objects/object.cpp | 14 +++++++++----- src/core/objects/object.h | 3 ++- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/core/objects/dynamic_object_query.cpp b/src/core/objects/dynamic_object_query.cpp index 5fdfc8dea..6a577a1fe 100644 --- a/src/core/objects/dynamic_object_query.cpp +++ b/src/core/objects/dynamic_object_query.cpp @@ -292,7 +292,7 @@ const QStringList DynamicObjectQueryManager::getContextKeywords(const QString& t return QString(QLatin1String("ISUNDEFINED; TYPE; ID; AND; OR; == !=")).split(QLatin1Char(' ')); case DynamicObjectQuery::GeneralObjectQuery: - return QStringList{QLatin1String("IGNORESYMBOL;"), QLatin1String("ISDUPLICATE;")}; + return QString(QLatin1String("IGNORESYMBOL; IGNORETAGS; ISDUPLICATE;")).split(QLatin1Char(' ')); default: return QStringList(); // we should not get here @@ -467,6 +467,7 @@ bool GeneralObjectQuery::performQuery(const Map* map, const Object* object) cons { int and_or_operation = 1; // 0 = AND, 1 = OR bool ignore_symbols = false; + bool ignore_tags = false; bool result = false; for (auto& element : attributes) @@ -475,12 +476,14 @@ bool GeneralObjectQuery::performQuery(const Map* map, const Object* object) cons continue; else if (element == QLatin1String("IGNORESYMBOL")) ignore_symbols = true; + else if (element == QLatin1String("IGNORETAGS")) + ignore_tags = true; else if (element == QLatin1String("ISDUPLICATE")) { if (!map || !object) return true; - const bool isduplicate_result = map->getCurrentPart()->existsObject([object, ignore_symbols](auto const* o) - { return object != o && object->equals(o, !ignore_symbols); } + const bool isduplicate_result = map->getCurrentPart()->existsObject([object, ignore_symbols, ignore_tags](auto const* o) + { return object != o && object->equals(o, !ignore_symbols, !ignore_tags); } ); result = and_or_operation ? (result || isduplicate_result) : (result && isduplicate_result); and_or_operation = 0; // default after first operation is AND operation diff --git a/src/core/objects/object.cpp b/src/core/objects/object.cpp index 7343886f7..e01eb12e5 100644 --- a/src/core/objects/object.cpp +++ b/src/core/objects/object.cpp @@ -133,7 +133,7 @@ void Object::copyFrom(const Object& other) extent = other.extent; } -bool Object::equals(const Object* other, bool compare_symbol) const +bool Object::equals(const Object* other, bool compare_symbol, bool compare_tags) const { if (type != other->type) return false; @@ -186,11 +186,15 @@ bool Object::equals(const Object* other, bool compare_symbol) const return false; } - if (object_tags.empty()) - return other->object_tags.empty(); + if (compare_tags) + { + if (object_tags.empty()) + return other->object_tags.empty(); - using std::begin; using std::end; - return std::is_permutation(object_tags.begin(), object_tags.end(), other->object_tags.begin(), other->object_tags.end()); + using std::begin; using std::end; + return std::is_permutation(object_tags.begin(), object_tags.end(), other->object_tags.begin(), other->object_tags.end()); + } + return true; } diff --git a/src/core/objects/object.h b/src/core/objects/object.h index 816ad8aa8..9cfb5f9ee 100644 --- a/src/core/objects/object.h +++ b/src/core/objects/object.h @@ -124,9 +124,10 @@ friend class XMLImportExport; * Checks for equality with another object. * * If compare_symbol is set, also the symbols are compared for having the same properties. + * If compare_tags is set, also the tags are compared for being equal. * Note that the map property is not compared. */ - bool equals(const Object* other, bool compare_symbol) const; + bool equals(const Object* other, bool compare_symbol, bool compare_tags = true) const; virtual bool validate() const; From 7f0eefa920fea7a4f33365a3821f0173e5dcf9a8 Mon Sep 17 00:00:00 2001 From: Matthias Kuehlewein Date: Thu, 8 Jan 2026 14:21:41 +0100 Subject: [PATCH 14/16] MapFindFeature: Set shortcut for Find action Use Ctrl+Shift+F as shortcut since Ctrl+F is used by 'Fill / Create border' tool. --- src/gui/map/map_find_feature.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gui/map/map_find_feature.cpp b/src/gui/map/map_find_feature.cpp index 8babb4480..a95d89368 100644 --- a/src/gui/map/map_find_feature.cpp +++ b/src/gui/map/map_find_feature.cpp @@ -82,6 +82,7 @@ MapFindFeature::MapFindFeature(MapEditorController& controller) show_action->setMenuRole(QAction::NoRole); // QKeySequence::Find may be Ctrl+F, which conflicts with "Fill / Create Border" //show_action->setShortcut(QKeySequence::Find); + show_action->setShortcut(QKeySequence(tr("Ctrl+Shift+F"))); //action->setStatusTip(tr_tip); show_action->setWhatsThis(Util::makeWhatThis("edit_menu.html")); connect(show_action, &QAction::triggered, this, &MapFindFeature::showDialog); From dd87fc198673b8a21e145a24590c3116527799c1 Mon Sep 17 00:00:00 2001 From: Matthias Kuehlewein Date: Sun, 11 Jan 2026 12:41:08 +0100 Subject: [PATCH 15/16] Differentiate between open and closed lines For lines (i.e. LINE()), the new keywords ISOPEN and ISCLOSED test if a line is closed (i.e., is a polygon) or not. --- src/core/objects/dynamic_object_query.cpp | 24 ++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/core/objects/dynamic_object_query.cpp b/src/core/objects/dynamic_object_query.cpp index 6a577a1fe..2b0d10c31 100644 --- a/src/core/objects/dynamic_object_query.cpp +++ b/src/core/objects/dynamic_object_query.cpp @@ -28,6 +28,7 @@ #include "core/map.h" #include "core/map_part.h" +#include "core/virtual_path.h" #include "core/objects/object.h" #include "core/symbols/symbol.h" @@ -271,7 +272,16 @@ const QStringList DynamicObjectQueryManager::getContextKeywords(const QString& t return QStringList(); } { - QStringList keywords = {keyword_found == DynamicObjectQuery::LineObjectQuery ? QLatin1String("ISTOOSHORT;") : QLatin1String("ISTOOSMALL;")}; + QStringList keywords; + if (keyword_found == DynamicObjectQuery::LineObjectQuery) + { + keywords = QString(QLatin1String("ISTOOSHORT; ISCLOSED; ISOPEN;")).split(QLatin1Char(' ')); + } + else + { + keywords.append(QLatin1String("ISTOOSMALL;")); + } + keywords += QString(QLatin1String("PAPER; REAL; AND; OR;")).split(QLatin1Char(' ')); for (const auto& comp : numerical_compare_operations) keywords += comp.op; @@ -391,6 +401,18 @@ bool LineObjectQuery::performQuery(const PathObject* path_object) const result = and_or_operation ? (result || comparison_result) : (result && comparison_result); and_or_operation = 0; // default after first operation is AND operation } + else if (element == QLatin1String("ISCLOSED")) + { + const bool is_closed = path_object ? path_object->parts().front().isClosed() : false; + result = and_or_operation ? (result || is_closed) : (result && is_closed); + and_or_operation = 0; // default after first operation is AND operation + } + else if (element == QLatin1String("ISOPEN")) + { + const bool is_open = path_object ? !path_object->parts().front().isClosed() : false; + result = and_or_operation ? (result || is_open) : (result && is_open); + and_or_operation = 0; // default after first operation is AND operation + } else // unknown element or failure in NumericalComparison() { if (!path_object) From 60426595155a1bc10e20c70d71edd5222dfce91b Mon Sep 17 00:00:00 2001 From: Matthias Kuehlewein Date: Sun, 11 Jan 2026 17:45:42 +0100 Subject: [PATCH 16/16] MapFindFeature: Load and apply queries from file Allow to open either an .xml or .txt file which contains a list of queries (consisting of a name, the query itself and an optional description (being shown as a tooltip)). Selecting a query from the list will add it to the text input. --- src/gui/map/map_find_feature.cpp | 191 ++++++++++++++++++++++++++++++- src/gui/map/map_find_feature.h | 23 +++- 2 files changed, 210 insertions(+), 4 deletions(-) diff --git a/src/gui/map/map_find_feature.cpp b/src/gui/map/map_find_feature.cpp index a95d89368..1e02d2f0c 100644 --- a/src/gui/map/map_find_feature.cpp +++ b/src/gui/map/map_find_feature.cpp @@ -28,25 +28,33 @@ #include #include #include +#include #include #include #include +#include #include +#include +#include +#include #include #include // IWYU pragma: keep #include #include #include #include +#include #include #include #include +#include #include -#include #include #include +#include #include #include +#include #include "core/map.h" #include "core/map_part.h" @@ -54,6 +62,7 @@ #include "core/objects/object.h" #include "core/objects/object_query.h" #include "core/symbols/symbol.h" +#include "gui/file_dialog.h" #include "gui/main_window.h" #include "gui/util_gui.h" #include "gui/map/map_editor.h" @@ -134,6 +143,17 @@ void MapFindFeature::showDialog() selected_objects = new QLabel(); // initialization by objectSelectionChanged() below + auto* query_collection_box = new QHBoxLayout(); + query_collection = new QComboBox(); + query_collection->setEnabled(false); + connect(query_collection, QOverload::of(&QComboBox::currentIndexChanged), this, &MapFindFeature::querySelected); + query_collection_box->addWidget(new QLabel(tr("Query collection"))); + query_collection_box->addWidget(query_collection, 1); + + auto* load_query_collection = new QPushButton(QIcon(QString::fromLatin1(":/images/open.png")), QLatin1String{}); + query_collection_box->addWidget(load_query_collection); + connect(load_query_collection, &QPushButton::clicked, this, &MapFindFeature::loadQueryCollection); + auto find_all = new QPushButton(tr("Find &all")); connect(find_all, &QPushButton::clicked, this, &MapFindFeature::findAll); @@ -174,7 +194,8 @@ void MapFindFeature::showDialog() layout->addWidget(tags_button, 5, 1, 1, 1); layout->addWidget(selected_objects, 7, 0, 1, 1); layout->addWidget(tag_selector_buttons, 7, 1, 1, 1); - layout->addWidget(button_box, 8, 0, 1, 2); + layout->addLayout(query_collection_box, 8, 0, 1, 1); + layout->addWidget(button_box, 9, 0, 1, 2); find_dialog->setLayout(layout); } @@ -344,6 +365,172 @@ void MapFindFeature::tagSelectorToggled(bool active) } } +void MapFindFeature::querySelected() +{ + const auto index = query_collection->currentIndex(); + if (index > 0) + { + text_edit->insertPlainText(query_collection_list.at(index - 1).query); + } +} + +void MapFindFeature::showUnsupportedElementWarning(QXmlStreamReader& xml) const +{ + QMessageBox::warning(find_dialog, + tr("Warning"), + tr("Unsupported element: %1 (line %2 column %3)") + .arg(xml.name().toString()) + .arg(xml.lineNumber()) + .arg(xml.columnNumber()) + ); +} + +void MapFindFeature::loadQueryCollection() +{ + auto const filter = QString{QLatin1String{"(*txt *.xml)"}}; + auto const filepath = FileDialog::getOpenFileName(find_dialog, + tr("Open Query collection file..."), + QLatin1String{}, + filter); + if (filepath.isEmpty()) + return; + + QFile query_collection_file(filepath); + if (!query_collection_file.open(QFile::ReadOnly)) + { + QMessageBox::warning(find_dialog, + tr("Error"), + ::OpenOrienteering::MainWindow::tr("Cannot open file:\n%1\n\n%2") + .arg(filepath, query_collection_file.errorString()) + ); + return; + } + + query_collection_list.clear(); + + if (filepath.endsWith(QLatin1String(".xml"), Qt::CaseInsensitive)) + { + QXmlStreamReader xml(&query_collection_file); + if (xml.readNextStartElement()) + { + while (xml.readNextStartElement()) + { + if (xml.name() == QLatin1String("object_query")) + { + QueryCollectionItem query_collection_item; + + while (xml.readNextStartElement()) + { + auto value = xml.readElementText(); + if (xml.name() == QLatin1String("name")) + { + query_collection_item.name = value; + } + else if (xml.name() == QLatin1String("query")) + { + query_collection_item.query = value; + } + else if (xml.name() == QLatin1String("hint")) + { + query_collection_item.hint = value; + } + else + { + showUnsupportedElementWarning(xml); + } + } + if (!query_collection_item.name.isEmpty() && !query_collection_item.query.isEmpty()) + { + query_collection_list.push_back(query_collection_item); + } + } + else + { + xml.skipCurrentElement(); + showUnsupportedElementWarning(xml); + } + } + } + } + else // .txt + { + QTextStream stream(&query_collection_file); + QString line; + QueryCollectionItem query_collection_item; + + while (stream.readLineInto(&line, 1000)) // arbitrary limit + { + if (line.startsWith(QLatin1String("name="))) + { + line.remove(0, 5); + if (!query_collection_item.name.isEmpty()) + { + if (query_collection_item.query.isEmpty()) + { + QMessageBox::warning(find_dialog, + tr("Warning"), + tr("Missing query for: %1") + .arg(query_collection_item.name) + ); + } + else + { + query_collection_list.push_back(query_collection_item); + query_collection_item.name.clear(); + query_collection_item.query.clear(); + query_collection_item.hint.clear(); + } + } + query_collection_item.name = line; + } + else if (line.startsWith(QLatin1String("query="))) + { + line.remove(0, 6); + query_collection_item.query += line; + } + else if (line.startsWith(QLatin1String("hint="))) + { + line.remove(0, 5); + query_collection_item.hint += line; + } + else + { + // skip empty lines, lines with comments and anything else (silently) + } + } + if (!query_collection_item.name.isEmpty()) + { + if (query_collection_item.query.isEmpty()) + { + QMessageBox::warning(find_dialog, + tr("Warning"), + tr("Missing query for: %1") + .arg(query_collection_item.name) + ); + } + else + query_collection_list.push_back(query_collection_item); + } + } + + QSignalBlocker block(query_collection); + + query_collection->clear(); + if (!query_collection_list.size()) + { + query_collection->setEnabled(false); + return; + } + query_collection->addItem(QLatin1String("---")); + for (auto i = 0; i < (int)query_collection_list.size(); ++i) + { + query_collection->addItem(query_collection_list.at(i).name); + if (!query_collection_list.at(i).hint.isEmpty()) + query_collection->setItemData(i + 1, query_collection_list.at(i).hint, Qt::ToolTipRole); + } + query_collection->setEnabled(true); +} + // slot void MapFindTextEdit::insertKeyword(QAction* action) diff --git a/src/gui/map/map_find_feature.h b/src/gui/map/map_find_feature.h index 2d9761410..4fa94dd3f 100644 --- a/src/gui/map/map_find_feature.h +++ b/src/gui/map/map_find_feature.h @@ -21,13 +21,17 @@ #ifndef OPENORIENTEERING_MAP_FIND_FEATURE_H #define OPENORIENTEERING_MAP_FIND_FEATURE_H +#include + #include #include #include +#include #include class QAction; class QCheckBox; +class QComboBox; class QContextMenuEvent; class QDialog; class QKeyEvent; @@ -36,6 +40,7 @@ class QPoint; class QPushButton; class QStackedLayout; class QWidget; +class QXmlStreamReader; namespace OpenOrienteering { @@ -79,9 +84,9 @@ class MapFindFeature : public QObject void setEnabled(bool enabled); - QAction* showDialogAction() { return show_action; } + QAction* showDialogAction() const { return show_action; } - QAction* findNextAction() { return find_next_action; } + QAction* findNextAction() const { return find_next_action; } static void findNextMatchingObject(MapEditorController& controller, const ObjectQuery& query, bool center_selection_visibility = false); @@ -106,6 +111,11 @@ class MapFindFeature : public QObject void tagSelectorToggled(bool active); + void querySelected(); + + void loadQueryCollection(); + + void showUnsupportedElementWarning(QXmlStreamReader& xml) const; MapEditorController& controller; QPointer find_dialog; // child of controller's window @@ -116,11 +126,20 @@ class MapFindFeature : public QObject QPushButton* delete_find_next = nullptr; // child of find_dialog QCheckBox* center_view = nullptr; // child of find_dialog QLabel* selected_objects = nullptr; // child of find_dialog + QComboBox* query_collection = nullptr; // child of find_dialog QAction* show_action = nullptr; // child of this QAction* find_next_action = nullptr; // child of this Object* previous_object = nullptr; + struct QueryCollectionItem { + QString name; + QString query; + QString hint = {}; + }; + + std::vector query_collection_list; + Q_DISABLE_COPY(MapFindFeature) };