From 2e897110ef957678fb3972c4553d95258223bab1 Mon Sep 17 00:00:00 2001 From: Matthias Kuehlewein Date: Sat, 18 Mar 2023 17:24:00 +0100 Subject: [PATCH 1/2] Auxiliary symbol properties (GH-2136) When importing files, Mapper may encounter symbol properties that are not part of its native symbol properties but need to be applied when importing objects. For example, horizontal and vertical text alignment are symbol properties in OCAD but object properties in Mapper. Extend the Symbol base class to support auxiliary symbol properties that are neither saved to a map file nor loaded from it. Use auxiliary symbol properties for the GDAL importer and for importing .ocd files. --- src/core/map.cpp | 9 +++++ src/core/map.h | 4 ++- src/core/objects/text_object.h | 4 +++ src/core/symbols/symbol.cpp | 4 +-- src/core/symbols/symbol.h | 22 +++++++++++++ src/fileformats/ocd_file_import.cpp | 29 +++++++++++----- src/fileformats/ocd_file_import.h | 12 ++----- src/gdal/ogr_file_format.cpp | 51 +++++++++++++++-------------- 8 files changed, 91 insertions(+), 44 deletions(-) diff --git a/src/core/map.cpp b/src/core/map.cpp index 4ad563ece..a1821cd92 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -2508,6 +2508,15 @@ void Map::setOtherDirty() setHasUnsavedChanges(true); } + +void Map::clearAuxiliarySymbolProperties() +{ + for (auto* symbol : symbols) + { + symbol->clearAuxiliaryProperties(); + } +} + // slot void Map::undoCleanChanged(bool is_clean) { diff --git a/src/core/map.h b/src/core/map.h index 0807dcdd9..956fb6a42 100644 --- a/src/core/map.h +++ b/src/core/map.h @@ -1,6 +1,6 @@ /* * Copyright 2012-2014 Thomas Schöps - * Copyright 2013-2020, 2024 Kai Pastor + * Copyright 2013-2020, 2024, 2025 Kai Pastor * * This file is part of OpenOrienteering. * @@ -1375,6 +1375,8 @@ public slots: */ void setOtherDirty(); + /** Removes all auxiliary symbol properties from all symbols. */ + void clearAuxiliarySymbolProperties(); // Static diff --git a/src/core/objects/text_object.h b/src/core/objects/text_object.h index cdb244089..1a11f1ca2 100644 --- a/src/core/objects/text_object.h +++ b/src/core/objects/text_object.h @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -393,4 +394,7 @@ const TextObjectLineInfo*TextObject::getLineInfo(int i) const } // namespace OpenOrienteering +Q_DECLARE_METATYPE(OpenOrienteering::TextObject::HorizontalAlignment) +Q_DECLARE_METATYPE(OpenOrienteering::TextObject::VerticalAlignment) + #endif // OPENORIENTEERING_TEXT_OBJECT_H diff --git a/src/core/symbols/symbol.cpp b/src/core/symbols/symbol.cpp index 8312395e6..6c433c314 100644 --- a/src/core/symbols/symbol.cpp +++ b/src/core/symbols/symbol.cpp @@ -1,6 +1,6 @@ /* * Copyright 2012, 2013 Thomas Schöps - * Copyright 2012-2020, 2022, 2024 Kai Pastor + * Copyright 2012-2020, 2022, 2024, 2025 Kai Pastor * * This file is part of OpenOrienteering. * @@ -40,7 +40,6 @@ #include #include #include -#include #include #include @@ -89,6 +88,7 @@ Symbol::Symbol(const Symbol& proto) , is_hidden { proto.is_hidden } , is_protected { proto.is_protected } , is_rotatable { proto.is_rotatable } +, auxiliary_properties ( proto.auxiliary_properties ) { // nothing else } diff --git a/src/core/symbols/symbol.h b/src/core/symbols/symbol.h index c3886e9e1..316f513ad 100644 --- a/src/core/symbols/symbol.h +++ b/src/core/symbols/symbol.h @@ -36,6 +36,7 @@ #include #include #include +#include class QXmlStreamReader; class QXmlStreamWriter; @@ -528,6 +529,26 @@ class Symbol */ virtual int getMinimumLength() const; + /** + * Sets an auxiliary property of this symbol. + */ + void setAuxiliaryProperty(int key, QVariant value) { auxiliary_properties.insert(key, value); } + + /** + * Returns an auxiliary property of this symbol. + */ + QVariant getAuxiliaryProperty(int key) const { return auxiliary_properties.value(key); } + + /** + * Returns an auxiliary property of this symbol or a default value. + */ + QVariant getAuxiliaryProperty(int key, QVariant default_value) const { return auxiliary_properties.value(key, default_value); } + + /** + * Removes all auxiliary properties of this symbol. + */ + void clearAuxiliaryProperties() { auxiliary_properties.clear(); } + protected: /** * Sets the rotatability state of the symbol. @@ -650,6 +671,7 @@ class Symbol bool is_hidden; /// \see isHidden() bool is_protected; /// \see isProtected() bool is_rotatable = false; + QHash auxiliary_properties; /// For temporary use during import. Not to be saved on export. }; diff --git a/src/fileformats/ocd_file_import.cpp b/src/fileformats/ocd_file_import.cpp index d597c5fa4..992b3fe28 100644 --- a/src/fileformats/ocd_file_import.cpp +++ b/src/fileformats/ocd_file_import.cpp @@ -1,5 +1,6 @@ /* * Copyright 2013-2022, 2024, 2025 Kai Pastor + * Copyright 2022, 2024-2026 Matthias Kühlewein * * Some parts taken from file_format_oc*d8{.h,_p.h,cpp} which are * Copyright 2012 Pete Curtis @@ -97,7 +98,18 @@ static QTextCodec* codecFromSettings() } // namespace +namespace { + /** + * For using the auxiliary properties + */ + enum OcdFileImportProperties + { + HAlign = 0, + VAlign = 1 + }; + +} // namespace OcdFileImport::OcdImportedPathObject::~OcdImportedPathObject() = default; @@ -330,6 +342,7 @@ void OcdFileImport::importImplementation() importView(file); } } + map->clearAuxiliarySymbolProperties(); // No deep copy during import FILEFORMAT_ASSERT(file.byteArray().constData() == buffer.constData()); @@ -1923,7 +1936,7 @@ Object* OcdFileImport::importObject(const O& ocd_object, MapPart* part) auto t = new TextObject(symbol); t->setText(getObjectText(ocd_object)); t->setRotation(convertAngle(ocd_object.angle)); - t->setHorizontalAlignment(text_halign_map.value(symbol)); + t->setHorizontalAlignment((symbol->getAuxiliaryProperty(OcdFileImportProperties::HAlign).value())); // Vertical alignment is set in fillTextPathCoords(). // Text objects need special path translation @@ -2251,7 +2264,7 @@ bool OcdFileImport::fillTextPathCoords(TextObject *object, TextSymbol *symbol, q // anchor point MapCoord coord = convertOcdPoint(ocd_points[0]); object->setAnchorPosition(coord.nativeX(), coord.nativeY()); - object->setVerticalAlignment(text_valign_map.value(symbol)); + object->setVerticalAlignment((symbol->getAuxiliaryProperty(OcdFileImportProperties::VAlign).value())); } return true; @@ -2277,32 +2290,32 @@ void OcdFileImport::setBasicAttributes(OcdFileImport::OcdImportedTextSymbol* sym switch (attributes.alignment & Ocd::HAlignMask) { case Ocd::HAlignLeft: - text_halign_map[symbol] = TextObject::AlignLeft; + symbol->setAuxiliaryProperty(OcdFileImportProperties::HAlign, QVariant(TextObject::AlignLeft)); break; case Ocd::HAlignRight: - text_halign_map[symbol] = TextObject::AlignRight; + symbol->setAuxiliaryProperty(OcdFileImportProperties::HAlign, QVariant(TextObject::AlignRight)); break; case Ocd::HAlignJustified: /// \todo Implement justified alignment addSymbolWarning(symbol, tr("Justified alignment is not supported.")); Q_FALLTHROUGH(); default: - text_halign_map[symbol] = TextObject::AlignHCenter; + symbol->setAuxiliaryProperty(OcdFileImportProperties::HAlign, QVariant(TextObject::AlignHCenter)); } switch (attributes.alignment & Ocd::VAlignMask) { case Ocd::VAlignTop: - text_valign_map[symbol] = TextObject::AlignTop; + symbol->setAuxiliaryProperty(OcdFileImportProperties::VAlign, QVariant(TextObject::AlignTop)); break; case Ocd::VAlignMiddle: - text_valign_map[symbol] = TextObject::AlignVCenter; + symbol->setAuxiliaryProperty(OcdFileImportProperties::VAlign, QVariant(TextObject::AlignVCenter)); break; default: addSymbolWarning(symbol, tr("Vertical alignment '%1' is not supported.").arg(attributes.alignment & Ocd::VAlignMask)); Q_FALLTHROUGH(); case Ocd::VAlignBottom: - text_valign_map[symbol] = TextObject::AlignBaseline; + symbol->setAuxiliaryProperty(OcdFileImportProperties::VAlign, QVariant(TextObject::AlignBaseline)); } if (attributes.char_spacing != 0) diff --git a/src/fileformats/ocd_file_import.h b/src/fileformats/ocd_file_import.h index 861a19aa3..b3c77bfb6 100644 --- a/src/fileformats/ocd_file_import.h +++ b/src/fileformats/ocd_file_import.h @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 Kai Pastor + * Copyright 2013-2022, 2025 Kai Pastor * * Some parts taken from file_format_oc*d8{.h,_p.h,cpp} which are * Copyright 2012 Pete Curtis @@ -84,7 +84,7 @@ class OcdFileImport : public Importer QString unnumbered_text; }; - // Helper classes that provide to core classes' protected members + // Helper classes that provide access to core classes' protected members class OcdImportedAreaSymbol : public AreaSymbol { @@ -292,7 +292,7 @@ class OcdFileImport : public Importer * Mapper normally generates symbol icons in the required size, but OCD * format carries user-defined rastern icons. These imported low resolution * icons needs to be kept and used only if they are really different from - * the default icons generatored by Mapper. + * the default icons generated by Mapper. */ template< class OcdBaseSymbol > void dropRedundantIcon(Symbol* symbol, const OcdBaseSymbol& ocd_base_symbol); @@ -349,12 +349,6 @@ class OcdFileImport : public Importer /// maps OCD symbol number to oo-mapper symbol object QHash symbol_index; - /// maps OO Mapper text symbol pointer to OCD defined horizontal alignment (stored in objects instead of symbols in OO Mapper) - QHash text_halign_map; - - /// maps OO Mapper text symbol pointer to OCD defined vertical alignment (stored in objects instead of symbols in OO Mapper) - QHash text_valign_map; - /// maps OCD symbol number to rectangle information struct QHash rectangle_info; diff --git a/src/gdal/ogr_file_format.cpp b/src/gdal/ogr_file_format.cpp index e50740224..45906c192 100644 --- a/src/gdal/ogr_file_format.cpp +++ b/src/gdal/ogr_file_format.cpp @@ -26,7 +26,6 @@ #include #include #include -#include #include #include @@ -608,6 +607,19 @@ namespace { } // namespace +namespace { + + /** + * For using the auxiliary properties + */ + enum OgrFileImportProperties + { + label = 0, + angle = 1, + anchor = 2 + }; + +} // namespace // ### OgrFileImportFormat ### @@ -923,6 +935,9 @@ bool OgrFileImport::importImplementation() .arg(tr("Not enough coordinates."))); } + if (map) + map->clearAuxiliarySymbolProperties(); + return true; } @@ -1177,15 +1192,9 @@ Object* OgrFileImport::importPointGeometry(OGRFeatureH feature, OGRGeometryH geo return object; } - if (symbol->getType() == Symbol::Text) + if (symbol->getType() == Symbol::Text && !symbol->getAuxiliaryProperty(OgrFileImportProperties::label).isNull()) { - const auto& description = symbol->getDescription(); - auto length = description.length(); - auto split = description.indexOf(QLatin1Char(' ')); - FILEFORMAT_ASSERT(split > 0); - FILEFORMAT_ASSERT(split < length); - - auto label = description.right(length - split - 1); + auto label = symbol->getAuxiliaryProperty(OgrFileImportProperties::label).toString(); if (label.startsWith(QLatin1Char{'{'}) && label.endsWith(QLatin1Char{'}'})) { label.remove(0, 1); @@ -1206,13 +1215,13 @@ Object* OgrFileImport::importPointGeometry(OGRFeatureH feature, OGRGeometryH geo object->setText(label); bool ok; - auto anchor = QStringRef(&description, 1, 2).toInt(&ok); + auto anchor = symbol->getAuxiliaryProperty(OgrFileImportProperties::anchor, 1).toInt(&ok); if (ok) { applyLabelAnchor(anchor, object); } - - auto angle = QStringRef(&description, 3, split-3).toDouble(&ok); + + auto angle = symbol->getAuxiliaryProperty(OgrFileImportProperties::angle, 0.0).toDouble(&ok); if (ok) { object->setRotation(qDegreesToRadians(angle)); @@ -1678,21 +1687,15 @@ TextSymbol* OgrFileImport::getSymbolForLabel(OGRStyleToolH tool, const QByteArra map->addSymbol(copy.release(), map->getNumSymbols()); } + text_symbol->setAuxiliaryProperty(OgrFileImportProperties::label, QVariant(QString::fromUtf8(label_string))); + auto anchor = qBound(1, OGR_ST_GetParamNum(tool, OGRSTLabelAnchor, &is_null), 12); - if (is_null) - anchor = 1; + if (!is_null) + text_symbol->setAuxiliaryProperty(OgrFileImportProperties::anchor, QVariant(anchor)); auto angle = OGR_ST_GetParamDbl(tool, OGRSTLabelAngle, &is_null); - if (is_null) - angle = 0.0; - - QString description; - description.reserve(int(qstrlen(label_string) + 100)); - description.append(QString::number(100 + anchor)); - description.append(QString::number(angle, 'g', 1)); - description.append(QLatin1Char(' ')); - description.append(QString::fromUtf8(label_string)); - text_symbol->setDescription(description); + if (!is_null) + text_symbol->setAuxiliaryProperty(OgrFileImportProperties::angle, QVariant(angle)); return text_symbol; } From 0562af4d6aece52e3b9bd3653bd1930c801d7295 Mon Sep 17 00:00:00 2001 From: Matthias Kuehlewein Date: Tue, 23 Dec 2025 10:37:08 +0100 Subject: [PATCH 2/2] OgrFileImport: Add test for DXF import --- test/CMakeLists.txt | 3 +- test/data/import/DXFTextImport.dxf | 272 +++++++++++++++++++++++++++++ test/dxf_t.cpp | 171 ++++++++++++++++++ 3 files changed, 445 insertions(+), 1 deletion(-) create mode 100755 test/data/import/DXFTextImport.dxf create mode 100644 test/dxf_t.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 145450a59..3f206f38f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,5 +1,5 @@ # -# Copyright 2012-2020 Kai Pastor +# Copyright 2012-2020, 2025 Kai Pastor # # This file is part of OpenOrienteering. # @@ -190,6 +190,7 @@ add_unit_test(util_t ../src/util/util add_system_test(coord_xml_t MANUAL) # System tests +add_system_test(dxf_t) add_system_test(file_format_t) add_system_test(duplicate_equals_t) add_system_test(map_t) diff --git a/test/data/import/DXFTextImport.dxf b/test/data/import/DXFTextImport.dxf new file mode 100755 index 000000000..d800ec835 --- /dev/null +++ b/test/data/import/DXFTextImport.dxf @@ -0,0 +1,272 @@ + 0 +SECTION + 2 +HEADER + 9 +$EXTNAMES +290 + 1 + 9 +$EXTMIN + 10 +0.000 + 20 +0.000 + 9 +$EXTMAX + 10 +256.000 + 20 +0.000 + 0 +ENDSEC + 0 +SECTION + 2 +TABLES + 0 +TABLE + 2 +LAYER + 2 +2 +100 +AcDbSymbolTable + 70 + 2 + 0 +LAYER + 5 +25 +100 +AcDbSymbolTableRecord +100 +AcDbLayerTableRecord + 2 +901000 + 70 + 0 + 62 + 7 + 6 +CONTINUOUS + 0 +LAYER + 5 +25 +100 +AcDbSymbolTableRecord +100 +AcDbLayerTableRecord + 2 +902000 + 70 + 0 + 62 + 7 + 6 +CONTINUOUS + 0 +LAYER + 5 +25 +100 +AcDbSymbolTableRecord +100 +AcDbLayerTableRecord + 2 +902001 + 70 + 0 + 62 + 7 + 6 +CONTINUOUS + 0 +ENDTAB + 0 +ENDSEC + 0 +SECTION + 2 +ENTITIES + 0 +MTEXT + 5 +27 +100 +AcDbEntity + 8 +902000 +100 +AcDbMText + 10 +-8.110 + 20 +-0.760 + 40 +2.822 + 1 +Test1 + 50 +-17.2 + 71 +7 + 0 +MTEXT + 5 +28 +100 +AcDbEntity + 8 +902000 +100 +AcDbMText + 10 +8.000 + 20 +-4.780 + 40 +2.822 + 1 +Test4 + 50 +-348.4 + 71 +7 + 0 +MTEXT + 5 +29 +100 +AcDbEntity + 8 +902001 +100 +AcDbMText + 10 +-0.130 + 20 +-11.840 + 40 +3.528 + 1 +Test2 + 50 +53.2 + 71 +7 + 0 +MTEXT + 5 +2A +100 +AcDbEntity + 8 +902000 +100 +AcDbMText + 10 +7.400 + 20 +-10.940 + 40 +2.822 + 1 +Test4 + 50 +-348.4 + 71 +7 + 0 +MTEXT + 5 +2B +100 +AcDbEntity + 8 +902000 +100 +AcDbMText + 10 +-9.680 + 20 +-5.750 + 40 +2.822 + 1 +Test1 + 50 +-17.2 + 71 +7 + 0 +MTEXT + 5 +2C +100 +AcDbEntity + 8 +902000 +100 +AcDbMText + 10 +-11.160 + 20 +-10.750 + 40 +2.822 + 1 +Test1 + 50 +-17.2 + 71 +7 + 0 +MTEXT + 5 +2D +100 +AcDbEntity + 8 +901000 +100 +AcDbMText + 10 +-0.760 + 20 +1.350 + 40 +6.350 + 1 +Test3 + 50 +0.0 + 71 +7 + 0 +MTEXT + 5 +2E +100 +AcDbEntity + 8 +901000 +100 +AcDbMText + 10 +16.920 + 20 +0.150 + 40 +6.350 + 1 +Test3 + 50 +0.0 + 71 +7 + 0 +ENDSEC + 0 +EOF diff --git a/test/dxf_t.cpp b/test/dxf_t.cpp new file mode 100644 index 000000000..c5a38a8a2 --- /dev/null +++ b/test/dxf_t.cpp @@ -0,0 +1,171 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test_config.h" + +#include "global.h" +#include "core/map.h" +#include "core/map_part.h" +#include "core/map_view.h" +#include "core/objects/object.h" +#include "core/objects/text_object.h" +#include "core/symbols/symbol.h" + +#ifdef MAPPER_USE_GDAL +#include "gdal/ogr_template.h" +#endif + +class QWidget; + +using namespace OpenOrienteering; + +/** + * @test Tests DXF import. + */ +class DxfTest : public QObject +{ +Q_OBJECT +private: + void handleCascadedDialogs() + { + QTimer::singleShot(500, this, [this] { + QWidget* positioning_dialog = QApplication::activeModalWidget(); + if (positioning_dialog) + { + QTimer::singleShot(500, this, [] { + QWidget* symbol_assignment_dialog = QApplication::activeModalWidget(); + if (symbol_assignment_dialog) + { + QTest::keyPress(symbol_assignment_dialog, Qt::Key_Enter); // triggers the Ok button + } + }); + QTest::keyPress(positioning_dialog, Qt::Key_Enter); // triggers the Ok button + } + }); + } + +private slots: + void initTestCase() + { + Q_INIT_RESOURCE(resources); + doStaticInitializations(); + QDir::addSearchPath(QStringLiteral("testdata"), QDir(QString::fromUtf8(MAPPER_TEST_SOURCE_DIR)).absoluteFilePath(QStringLiteral("data"))); + } + +#ifdef MAPPER_USE_GDAL + void dxfTextImport() + { + Map map; + MapView view{ &map }; + + const QString filename = QStringLiteral("testdata:import/DXFTextImport.dxf"); + OgrTemplate ogr_template {filename, &map}; + + handleCascadedDialogs(); + + QVERIFY(ogr_template.setupAndLoad(nullptr, &view)); + auto template_map = ogr_template.takeTemplateMap(); + QVERIFY(template_map); + + map.importMap(*template_map, Map::MinimalObjectImport); + + const auto number_symbols = map.getNumSymbols(); + QCOMPARE(number_symbols, 3); + + const auto number_objects = map.getNumObjects(); + QCOMPARE(number_objects, 8); + + for (auto i = 0; i < number_objects; ++i) + { + const auto* object = map.getCurrentPart()->getObject(i); + QVERIFY(object->getType() == Object::Type::Text); + + const auto* text_object = object->asText(); + const auto text = text_object->getText(); + QVERIFY(text.startsWith(QLatin1String("Test"))); + + const auto rotation = text_object->getRotation(); + if (text == QLatin1String("Test1")) + { + QCOMPARE(rotation, qDegreesToRadians(-17.2)); + } + else if (text == QLatin1String("Test2")) + { + QCOMPARE(rotation, qDegreesToRadians(53.2)); + } + else if (text == QLatin1String("Test3")) + { + QCOMPARE(rotation, qDegreesToRadians(0.0)); + } + else if (text == QLatin1String("Test4")) + { + QCOMPARE(rotation, qDegreesToRadians(-348.0)); // instead of -348.4 due to GDAL issue + } + } + + for (auto i = 0; i < number_symbols; ++i) + { + const auto* symbol = map.getSymbol(i); + QVERIFY(symbol->getDescription().isEmpty()); + } + } + + void dxfTextImportTwice() + { + Map map; + MapView view{ &map }; + + const QString filename = QStringLiteral("testdata:import/DXFTextImport.dxf"); + OgrTemplate ogr_template {filename, &map}; + + handleCascadedDialogs(); + + QVERIFY(ogr_template.setupAndLoad(nullptr, &view)); + auto template_map = ogr_template.takeTemplateMap(); + QVERIFY(template_map); + + map.importMap(*template_map, Map::MinimalObjectImport); + map.importMap(*template_map, Map::MinimalObjectImport); + + QCOMPARE(map.getNumSymbols(), 3); + QCOMPARE(map.getNumObjects(), 16); + } +#endif +}; // class DxfTest + +/* + * We don't need a real GUI window. + */ +namespace { + auto Q_DECL_UNUSED qpa_selected = qputenv("QT_QPA_PLATFORM", "minimal"); // clazy:exclude=non-pod-global-static +} + +QTEST_MAIN(DxfTest) +#include "dxf_t.moc" // IWYU pragma: keep