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; } 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