1 /*
2 * Copyright 2012, 2013 Thomas Schöps
3 * Copyright 2012-2019 Kai Pastor
4 *
5 * This file is part of OpenOrienteering.
6 *
7 * OpenOrienteering is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * OpenOrienteering is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with OpenOrienteering. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21 #include "file_format_t.h"
22
23 #include <array>
24 #include <initializer_list>
25 #include <limits>
26 #include <memory>
27 // IWYU pragma: no_include <type_traits>
28 #include <utility>
29
30 #include <Qt>
31 #include <QtGlobal>
32 #include <QtTest>
33 #include <QBuffer>
34 #include <QByteArray>
35 #include <QCoreApplication>
36 #include <QDir>
37 #include <QFile>
38 #include <QFileInfo>
39 #include <QIODevice>
40 #include <QLatin1String>
41 #include <QPageSize>
42 #include <QPoint>
43 #include <QPointF>
44 #include <QRectF>
45 #include <QSize>
46 #include <QSizeF>
47 #include <QString>
48 #include <QTemporaryDir>
49 #include <QVariant>
50
51 #include "global.h"
52 #include "test_config.h"
53 #include "core/georeferencing.h"
54 #include "core/latlon.h"
55 #include "core/map.h"
56 #include "core/map_color.h"
57 #include "core/map_coord.h"
58 #include "core/map_grid.h"
59 #include "core/map_part.h"
60 #include "core/map_printer.h"
61 #include "core/objects/object.h"
62 #include "core/objects/text_object.h"
63 #include "core/symbols/symbol.h"
64 #include "fileformats/file_format.h"
65 #include "fileformats/file_format_registry.h"
66 #include "fileformats/file_import_export.h"
67 #include "fileformats/ocd_file_export.h"
68 #include "fileformats/ocd_file_format.h"
69 #include "fileformats/xml_file_format.h"
70 #include "templates/template.h"
71 #include "undo/undo.h"
72 #include "undo/undo_manager.h"
73 #include "util/backports.h" // IWYU pragma: keep
74
75 using namespace OpenOrienteering;
76
77
78 #ifdef QT_PRINTSUPPORT_LIB
79
80 namespace QTest
81 {
82 /*
83 * This debug helper must use the parameter name 't' in order to avoid
84 * a warning about a difference between declaration and definition.
85 */
86 template<>
toString(const MapPrinterPageFormat & t)87 char* toString(const MapPrinterPageFormat& t)
88 {
89 const auto& page_format = t;
90 QByteArray ba = "";
91 ba += qPrintable(QPageSize::key(page_format.page_size));
92 ba += (page_format.orientation == MapPrinterPageFormat::Landscape) ? " landscape (" : " portrait (";
93 ba += QByteArray::number(page_format.paper_dimensions.width(), 'f', 2) + "x";
94 ba += QByteArray::number(page_format.paper_dimensions.height(), 'f', 2) + "), ";
95 ba += QByteArray::number(page_format.page_rect.left(), 'f', 2) + ",";
96 ba += QByteArray::number(page_format.page_rect.top(), 'f', 2) + "+";
97 ba += QByteArray::number(page_format.page_rect.width(), 'f', 2) + "x";
98 ba += QByteArray::number(page_format.page_rect.height(), 'f', 2) + ", overlap ";
99 ba += QByteArray::number(page_format.h_overlap, 'f', 2) + ",";
100 ba += QByteArray::number(page_format.v_overlap, 'f', 2);
101 return qstrdup(ba.data());
102 }
103
104 /*
105 * This debug helper must use the parameter name 't' in order to avoid
106 * a warning about a difference between declaration and definition.
107 */
108 template<>
toString(const MapPrinterOptions & t)109 char* toString(const MapPrinterOptions& t)
110 {
111 const auto& options = t;
112 QByteArray ba = "";
113 ba += "1:" + QByteArray::number(options.scale) + ", ";
114 ba += QByteArray::number(options.resolution) + " dpi, ";
115 if (options.show_templates)
116 ba += ", templates";
117 if (options.show_grid)
118 ba += ", grid";
119 if (options.simulate_overprinting)
120 ba += ", overprinting";
121 return qstrdup(ba.data());
122 }
123 }
124
125 #endif
126
127
128
129 namespace
130 {
comparePrinterConfig(const MapPrinterConfig & copy,const MapPrinterConfig & orig)131 void comparePrinterConfig(const MapPrinterConfig& copy, const MapPrinterConfig& orig)
132 {
133 QCOMPARE(copy.center_print_area, orig.center_print_area);
134 QCOMPARE(copy.options, orig.options);
135 QCOMPARE(copy.page_format, orig.page_format);
136 // Compare print area with reasonable precision, here: 0.1 mm
137 QCOMPARE((copy.print_area.topLeft()*10.0).toPoint(), (orig.print_area.topLeft()*10.0).toPoint());
138 QCOMPARE((copy.print_area.size()*10.0).toSize(), (orig.print_area.size()*10.0).toSize());
139 QCOMPARE(copy.printer_name, orig.printer_name);
140 QCOMPARE(copy.single_page_print_area, orig.single_page_print_area);
141 }
142
compareMaps(const Map & actual,const Map & expected)143 void compareMaps(const Map& actual, const Map& expected)
144 {
145 // TODO: This does not compare everything - yet ...
146
147 // Miscellaneous
148 QCOMPARE(actual.getScaleDenominator(), expected.getScaleDenominator());
149 QCOMPARE(actual.getMapNotes(), expected.getMapNotes());
150
151 const auto& actual_georef = actual.getGeoreferencing();
152 const auto& expected_georef = expected.getGeoreferencing();
153 QCOMPARE(actual_georef.getScaleDenominator(), expected_georef.getScaleDenominator());
154 QCOMPARE(actual_georef.isLocal(), expected_georef.isLocal());
155 QCOMPARE(actual_georef.getCombinedScaleFactor(), expected_georef.getCombinedScaleFactor());
156 QCOMPARE(actual_georef.getAuxiliaryScaleFactor(), expected_georef.getAuxiliaryScaleFactor());
157 QCOMPARE(actual_georef.getDeclination(), expected_georef.getDeclination());
158 QCOMPARE(actual_georef.getGrivation(), expected_georef.getGrivation());
159 QCOMPARE(actual_georef.getMapRefPoint(), expected_georef.getMapRefPoint());
160 QCOMPARE(actual_georef.getProjectedRefPoint(), expected_georef.getProjectedRefPoint());
161 QCOMPARE(actual_georef.getProjectedCRSId(), expected_georef.getProjectedCRSId());
162 QCOMPARE(actual_georef.getProjectedCRSName(), expected_georef.getProjectedCRSName());
163 QCOMPARE(actual_georef.getProjectedCoordinatesName(), expected_georef.getProjectedCoordinatesName());
164 QCOMPARE(actual_georef.getProjectedCRSSpec(), expected_georef.getProjectedCRSSpec());
165 QVERIFY(qAbs(actual_georef.getGeographicRefPoint().latitude() - expected_georef.getGeographicRefPoint().latitude()) < 0.5e-8);
166 QVERIFY(qAbs(actual_georef.getGeographicRefPoint().longitude() - expected_georef.getGeographicRefPoint().longitude()) < 0.5e-8);
167
168 QCOMPARE(actual.getGrid(), expected.getGrid());
169 QCOMPARE(actual.isAreaHatchingEnabled(), expected.isAreaHatchingEnabled());
170 QCOMPARE(actual.isBaselineViewEnabled(), expected.isBaselineViewEnabled());
171
172 // Colors
173 if (actual.getNumColors() != expected.getNumColors())
174 {
175 QCOMPARE(actual.getNumColors(), expected.getNumColors());
176 }
177 else for (int i = 0; i < actual.getNumColors(); ++i)
178 {
179 QCOMPARE(*actual.getColor(i), *expected.getColor(i));
180 }
181
182 // Symbols
183 if (actual.getNumSymbols() != expected.getNumSymbols())
184 {
185 QCOMPARE(actual.getNumSymbols(), expected.getNumSymbols());
186 }
187 else for (int i = 0; i < actual.getNumSymbols(); ++i)
188 {
189 if (!actual.getSymbol(i)->equals(expected.getSymbol(i), Qt::CaseSensitive))
190 qDebug("%s vs %s", qPrintable(actual.getSymbol(i)->getNumberAsString()),
191 qPrintable(expected.getSymbol(i)->getNumberAsString()));
192 QVERIFY(actual.getSymbol(i)->equals(expected.getSymbol(i), Qt::CaseSensitive));
193 QVERIFY(actual.getSymbol(i)->stateEquals(expected.getSymbol(i)));
194 }
195
196 // Parts and objects
197 QCOMPARE(actual.getCurrentPartIndex(), expected.getCurrentPartIndex());
198 if (actual.getNumParts() != expected.getNumParts())
199 {
200 QCOMPARE(actual.getNumParts(), expected.getNumParts());
201 }
202 else for (auto p = 0u; p < static_cast<decltype(p)>(actual.getNumParts()); ++p)
203 {
204 const auto& actual_part = *actual.getPart(p);
205 const auto& expected_part = *expected.getPart(p);
206 QVERIFY(actual_part.getName().compare(expected_part.getName(), Qt::CaseSensitive) == 0);
207 if (actual_part.getNumObjects() != expected_part.getNumObjects())
208 {
209 QCOMPARE(actual_part.getNumObjects(), expected_part.getNumObjects());
210 }
211 else for (int i = 0; i < actual_part.getNumObjects(); ++i)
212 {
213 QVERIFY(actual_part.getObject(i)->equals(expected_part.getObject(i), true));
214 }
215 }
216
217 // Object selection
218 QCOMPARE(actual.getNumSelectedObjects(), expected.getNumSelectedObjects());
219 if (actual.getFirstSelectedObject() == nullptr)
220 {
221 QCOMPARE(actual.getFirstSelectedObject(), expected.getFirstSelectedObject());
222 }
223 else
224 {
225 QCOMPARE(actual.getCurrentPart()->findObjectIndex(actual.getFirstSelectedObject()),
226 expected.getCurrentPart()->findObjectIndex(expected.getFirstSelectedObject()));
227 if (actual.getCurrentPart()->getNumObjects() != expected.getCurrentPart()->getNumObjects())
228 {
229 for (auto object_index : qAsConst(actual.selectedObjects()))
230 {
231 QVERIFY(actual.isObjectSelected(actual.getCurrentPart()->getObject(expected.getCurrentPart()->findObjectIndex(object_index))));
232 }
233 }
234 }
235
236 // Undo steps
237 // TODO: Currently only the number of steps is compared here.
238 QCOMPARE(actual.undoManager().undoStepCount(), expected.undoManager().undoStepCount());
239 QCOMPARE(actual.undoManager().redoStepCount(), expected.undoManager().redoStepCount());
240 if (actual.undoManager().canUndo())
241 QCOMPARE(actual.undoManager().nextUndoStep()->getType(), expected.undoManager().nextUndoStep()->getType());
242 if (actual.undoManager().canRedo())
243 QCOMPARE(actual.undoManager().nextRedoStep()->getType(), expected.undoManager().nextRedoStep()->getType());
244
245 // Templates
246 QCOMPARE(actual.getFirstFrontTemplate(), expected.getFirstFrontTemplate());
247 if (actual.getNumTemplates() != expected.getNumTemplates())
248 {
249 QCOMPARE(actual.getNumTemplates(), expected.getNumTemplates());
250 }
251 else for (int i = 0; i < actual.getNumTemplates(); ++i)
252 {
253 // TODO: only template filenames are compared
254 QCOMPARE(actual.getTemplate(i)->getTemplateFilename(), expected.getTemplate(i)->getTemplateFilename());
255 }
256 }
257
258
259 /**
260 * Compares map features in a way that works for lossy exporters and importers.
261 *
262 * A lossy exporter may create extra colors, symbols and objects in order to
263 * maintain the map's appearance. It may also merge all map parts into a
264 * single part.
265 *
266 * A lossy importer might skip some elements, but it is fair to require that
267 * it imports everything which is exported by the corresponding exporter.
268 */
fuzzyCompareMaps(const Map & actual,const Map & expected)269 void fuzzyCompareMaps(const Map& actual, const Map& expected)
270 {
271 // Miscellaneous
272 QCOMPARE(actual.getScaleDenominator(), expected.getScaleDenominator());
273 QCOMPARE(actual.getMapNotes(), expected.getMapNotes());
274
275 // Georeferencing
276 const auto& actual_georef = actual.getGeoreferencing();
277 const auto& expected_georef = expected.getGeoreferencing();
278 QCOMPARE(actual_georef.getScaleDenominator(), expected_georef.getScaleDenominator());
279 QCOMPARE(actual_georef.getGrivation(), expected_georef.getGrivation());
280
281 auto test_label = QString::fromLatin1("actual: %1, expected: %2");
282 auto ref_point = actual_georef.getMapRefPoint();
283 auto actual_point = actual_georef.toProjectedCoords(ref_point);
284 auto expected_point = expected_georef.toProjectedCoords(ref_point);
285 QVERIFY2(qAbs(actual_point.x() - expected_point.x()) < 1.0, qPrintable(test_label.arg(actual_point.x()).arg(expected_point.x())));
286 QVERIFY2(qAbs(actual_point.y() - expected_point.y()) < 1.0, qPrintable(test_label.arg(actual_point.y()).arg(expected_point.y())));
287 ref_point += MapCoord{500, 500};
288 actual_point = actual_georef.toProjectedCoords(expected_georef.getMapRefPoint());
289 expected_point = expected_georef.toProjectedCoords(expected_georef.getMapRefPoint());
290 QVERIFY2(qAbs(actual_point.x() - expected_point.x()) < 1.0, qPrintable(test_label.arg(actual_point.x()).arg(expected_point.x())));
291 QVERIFY2(qAbs(actual_point.y() - expected_point.y()) < 1.0, qPrintable(test_label.arg(actual_point.y()).arg(expected_point.y())));
292
293 if (!actual_georef.isLocal())
294 {
295 QCOMPARE(actual_georef.isLocal(), expected_georef.isLocal());
296 QCOMPARE(actual_georef.toGeographicCoords(actual_point), expected_georef.toGeographicCoords(actual_point));
297 }
298
299 // Colors
300 QVERIFY2(actual.getNumColors() >= expected.getNumColors(), qPrintable(test_label.arg(actual.getNumColors()).arg(expected.getNumColors())));
301 QVERIFY2(actual.getNumColors() <= 2 * expected.getNumColors(), qPrintable(test_label.arg(actual.getNumColors()).arg(expected.getNumColors())));
302
303 // Symbols
304 // Combined symbols may be dropped on export
305 QVERIFY2(2 * actual.getNumSymbols() >= expected.getNumSymbols(), qPrintable(test_label.arg(actual.getNumSymbols()).arg(expected.getNumSymbols())));
306 QVERIFY2(actual.getNumSymbols() <= 2 * expected.getNumSymbols(), qPrintable(test_label.arg(actual.getNumSymbols()).arg(expected.getNumSymbols())));
307
308 // Objects
309 QVERIFY2(actual.getNumObjects() >= expected.getNumObjects(), qPrintable(test_label.arg(actual.getNumObjects()).arg(expected.getNumObjects())));
310 QVERIFY2(actual.getNumObjects() <= 2 * expected.getNumObjects(), qPrintable(test_label.arg(actual.getNumObjects()).arg(expected.getNumObjects())));
311
312 // Parts
313 if (actual.getNumParts() > 1)
314 {
315 QCOMPARE(actual.getNumParts(), expected.getNumParts());
316 }
317 }
318
319
saveAndLoadMap(const Map & input,const FileFormat * format)320 std::unique_ptr<Map> saveAndLoadMap(const Map& input, const FileFormat* format)
321 {
322 auto out = std::make_unique<Map>();
323 auto exporter = format->makeExporter({}, &input, nullptr);
324 auto importer = format->makeImporter({}, out.get(), nullptr);
325 if (exporter && importer)
326 {
327 QBuffer buffer;
328 exporter->setDevice(&buffer);
329 importer->setDevice(&buffer);
330 if (buffer.open(QIODevice::ReadWrite)
331 && exporter->doExport()
332 && buffer.seek(0)
333 && importer->doImport())
334 {
335 return out; // success
336 }
337 }
338 out.reset(); // failure
339 return out;
340 }
341
342 auto const issue_513_files = {
343 "testdata:issue-513-coords-outside-printable.xmap",
344 "testdata:issue-513-coords-outside-printable.omap",
345 "testdata:issue-513-coords-outside-qint32.omap",
346 };
347
348 auto const example_files = {
349 "data:/examples/complete map.omap",
350 "data:/examples/forest sample.omap",
351 "data:/examples/overprinting.omap",
352 "testdata:templates/world-file.xmap",
353 };
354
355 auto const xml_test_files = {
356 "data:barrier.omap",
357 "data:rotated.omap",
358 "data:tags.omap",
359 "data:text-object.omap",
360 "data:undo.omap",
361 };
362
363 } // namespace
364
365
366
initTestCase()367 void FileFormatTest::initTestCase()
368 {
369 QCoreApplication::setOrganizationName(QString::fromLatin1("OpenOrienteering.org"));
370 QCoreApplication::setApplicationName(QString::fromLatin1("FileFormatTest"));
371
372 doStaticInitializations();
373
374 const auto prefix = QString::fromLatin1("data");
375 QDir::addSearchPath(prefix, QDir(QString::fromUtf8(MAPPER_TEST_SOURCE_DIR)).absoluteFilePath(prefix));
376 QDir::addSearchPath(prefix, QDir(QString::fromUtf8(MAPPER_TEST_SOURCE_DIR)).absoluteFilePath(QStringLiteral("..")));
377 QDir::addSearchPath(QStringLiteral("testdata"), QDir(QString::fromUtf8(MAPPER_TEST_SOURCE_DIR)).absoluteFilePath(QStringLiteral("data")));
378 }
379
380
381
mapCoordtoString_data()382 void FileFormatTest::mapCoordtoString_data()
383 {
384 using native_int = decltype(MapCoord().nativeX());
385 QTest::addColumn<native_int>("x");
386 QTest::addColumn<native_int>("y");
387 QTest::addColumn<int>("flags");
388 QTest::addColumn<QByteArray>("expected");
389
390 // Verify toString() especially for native coordinates at the numeric limits.
391 using bounds = std::numeric_limits<native_int>;
392 QTest::newRow("max,-1/0") << bounds::max() << -1 << 0 << QByteArray("2147483647 -1;");
393 QTest::newRow("-2,max/1") << -2 << bounds::max() << 1 << QByteArray("-2 2147483647 1;");
394 QTest::newRow("min,min/255") << bounds::min() << bounds::min() << 255 << QByteArray("-2147483648 -2147483648 255;");
395 }
396
mapCoordtoString()397 void FileFormatTest::mapCoordtoString()
398 {
399 using native_int = decltype(MapCoord().nativeX());
400 QFETCH(native_int, x);
401 QFETCH(native_int, y);
402 QFETCH(int, flags);
403 QFETCH(QByteArray, expected);
404
405 auto const coord = MapCoord::fromNative(x, y, MapCoord::Flags(flags));
406 QCOMPARE(coord.toString(), QString::fromLatin1(expected));
407
408 MapCoord::StringBuffer<char> buffer;
409 QCOMPARE(coord.toUtf8(buffer), expected);
410 }
411
412
413
understandsTest_data()414 void FileFormatTest::understandsTest_data()
415 {
416 quint8 ocd_start_raw[2] = { 0xAD, 0x0C };
417 auto ocd_start = QByteArray::fromRawData(reinterpret_cast<const char*>(ocd_start_raw), 2).append("random data");
418 auto omap_start = QByteArray("OMAP plus random data");
419 auto xml_start = QByteArray("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
420 auto xml_legacy = QByteArray(xml_start + "\r\n<map xmlns=\"http://oorienteering.sourceforge.net/mapper/xml/v2\">");
421 auto xml_regular = QByteArray(xml_start + "\n<map xmlns=\"http://openorienteering.org/apps/mapper/xml/v2\" version=\"7\">");
422 auto xml_gpx = QByteArray(xml_start + "\n<gpx>");
423
424 // Add all file formats which support import and export
425 QTest::addColumn<QByteArray>("format_id");
426 QTest::addColumn<QByteArray>("data");
427 QTest::addColumn<int>("support");
428
429 QTest::newRow("XML < xml legacy") << QByteArray("XML") << xml_legacy << int(FileFormat::FullySupported);
430 QTest::newRow("XML < xml regular") << QByteArray("XML") << xml_regular << int(FileFormat::FullySupported);
431 QTest::newRow("XML < xml start") << QByteArray("XML") << xml_start << int(FileFormat::Unknown);
432 QTest::newRow("XML < xml incomplete 1") << QByteArray("XML") << xml_regular.left(xml_start.length() - 10) << int(FileFormat::Unknown);
433 QTest::newRow("XML < xml incomplete 2") << QByteArray("XML") << xml_regular.left(xml_start.length() + 10) << int(FileFormat::Unknown);
434 QTest::newRow("XML < ''") << QByteArray("XML") << QByteArray() << int(FileFormat::Unknown);
435 QTest::newRow("XML < xml other") << QByteArray("XML") << xml_gpx << int(FileFormat::NotSupported);
436 QTest::newRow("XML < 'OMAPxxx'") << QByteArray("XML") << omap_start << int(FileFormat::FullySupported);
437 QTest::newRow("XML < 0x0CADxxx") << QByteArray("XML") << ocd_start << int(FileFormat::NotSupported);
438
439 QTest::newRow("OCD < 0x0CADxxx") << QByteArray("OCD") << ocd_start << int(FileFormat::FullySupported);
440 QTest::newRow("OCD < 0x0CAD") << QByteArray("OCD") << ocd_start.left(2) << int(FileFormat::FullySupported);
441 QTest::newRow("OCD < 0x0c") << QByteArray("OCD") << ocd_start.left(1) << int(FileFormat::Unknown);
442 QTest::newRow("OCD < ''") << QByteArray("OCD") << QByteArray() << int(FileFormat::Unknown);
443 QTest::newRow("OCD < 'OMAPxxx'") << QByteArray("OCD") << omap_start << int(FileFormat::NotSupported);
444 QTest::newRow("OCD < xml start") << QByteArray("OCD") << xml_start << int(FileFormat::NotSupported);
445
446 /// \todo Test OgrFileFormat (ID "OGR")
447 }
448
understandsTest()449 void FileFormatTest::understandsTest()
450 {
451 QFETCH(QByteArray, format_id);
452 QFETCH(QByteArray, data);
453 QFETCH(int, support);
454
455 auto format = FileFormats.findFormat(format_id);
456 #ifdef MAPPER_BIG_ENDIAN
457 if (format_id.startsWith("OCD"))
458 QEXPECT_FAIL("", "OCD format not support on big endian systems", Abort);
459 #endif
460 QVERIFY(format);
461 QVERIFY(format->supportsReading());
462 QCOMPARE(int(format->understands(data.constData(), data.length())), support);
463 }
464
465
466
formatForDataTest_data()467 void FileFormatTest::formatForDataTest_data()
468 {
469 understandsTest_data();
470 }
471
formatForDataTest()472 void FileFormatTest::formatForDataTest()
473 {
474 QFETCH(QByteArray, format_id); Q_UNUSED(format_id)
475 QFETCH(QByteArray, data);
476 QFETCH(int, support);
477
478 #ifdef MAPPER_BIG_ENDIAN
479 if (format_id.startsWith("OCD"))
480 return;
481 #endif
482
483 QTemporaryDir dir;
484 QVERIFY(dir.isValid());
485
486 auto path = QDir(dir.path()).absoluteFilePath(QStringLiteral("testfile"));
487 QFile out_file(path);
488 QVERIFY(out_file.open(QIODevice::WriteOnly));
489 QVERIFY(out_file.write(data) == data.size());
490 out_file.close();
491 QVERIFY(!out_file.error());
492
493 auto result = FileFormats.findFormatForData(path, FileFormat::AllFiles);
494 switch (support)
495 {
496 case FileFormat::NotSupported:
497 QVERIFY(!result || result->understands(data.constData(), data.length()) != FileFormat::NotSupported);
498 break;
499 case FileFormat::Unknown:
500 QVERIFY(result);
501 QVERIFY(result->understands(data.constData(), data.length()) != FileFormat::NotSupported);
502 break;
503 case FileFormat::FullySupported:
504 QVERIFY(result);
505 QVERIFY(result->understands(data.constData(), data.length()) == FileFormat::FullySupported);
506 break;
507 }
508 }
509
510
511
issue_513_high_coordinates_data()512 void FileFormatTest::issue_513_high_coordinates_data()
513 {
514 QTest::addColumn<QString>("filepath");
515
516 for (auto const* raw_path : issue_513_files)
517 {
518 QTest::newRow(raw_path) << QString::fromUtf8(raw_path);
519 }
520 }
521
issue_513_high_coordinates()522 void FileFormatTest::issue_513_high_coordinates()
523 {
524 QFETCH(QString, filepath);
525
526 QVERIFY(QFileInfo::exists(filepath));
527
528 // Load the test map
529 Map map {};
530 QVERIFY(map.loadFrom(filepath));
531
532 // The map's two objects must exist. Otherwise one may have been deleted
533 // for being irregular, indicating failure to handle high coordinates.
534 QCOMPARE(map.getNumObjects(), 2);
535
536 // The map's two undo steps must exist. Otherwise one may have been deleted
537 // for being irregular, indicating failure to handle high coordinates.
538 QCOMPARE(map.undoManager().undoStepCount(), 2);
539
540 QCOMPARE(map.getNumParts(), 1);
541 {
542 auto const* part = map.getPart(0);
543 auto extent = part->calculateExtent(true);
544 QVERIFY2(extent.top() < 1000000.0, "extent.top() outside printable range");
545 QVERIFY2(extent.left() > -1000000.0, "extent.left() outside printable range");
546 QVERIFY2(extent.bottom() > -1000000.0, "extent.bottom() outside printable range");
547 QVERIFY2(extent.right() < 1000000.0, "extent.right() outside printable range");
548
549 QCOMPARE(part->getNumObjects(), 2);
550 auto const* object = part->getObject(1);
551 QCOMPARE(object->getType(), Object::Text);
552 auto const* text = static_cast<TextObject const*>(object);
553 QVERIFY(!text->hasSingleAnchor());
554 QCOMPARE(text->getBoxSize(), MapCoord::fromNative(16000, 10000));
555 }
556
557 auto print_area = map.printerConfig().print_area;
558 QVERIFY2(print_area.top() < 1000000.0, "extent.top() outside printable range");
559 QVERIFY2(print_area.left() > -1000000.0, "extent.left() outside printable range");
560 QVERIFY2(print_area.bottom() > -1000000.0, "extent.bottom() outside printable range");
561 QVERIFY2(print_area.right() < 1000000.0, "extent.right() outside printable range");
562 }
563
564
565
saveAndLoad_data()566 void FileFormatTest::saveAndLoad_data()
567 {
568 QTest::addColumn<QByteArray>("id"); // memory management for test data tag
569 QTest::addColumn<QByteArray>("format_id");
570 QTest::addColumn<int>("format_version");
571 QTest::addColumn<QString>("filepath");
572
573 // Tests for particular feature of the XML file format.
574 for (auto const* raw_path : xml_test_files)
575 {
576 auto const* format_id = "XML";
577 auto id = QByteArray{};
578 id.reserve(int(qstrlen(raw_path) + qstrlen(format_id) + 5u));
579 id.append(raw_path).append(" <> ").append(format_id);
580
581 QTest::newRow(id) << id << QByteArray{format_id} << 0 << QString::fromLatin1(raw_path);
582 }
583
584 // Add all file formats which support import and export
585 static const auto format_ids = {
586 "XML",
587 #ifndef MAPPER_BIG_ENDIAN
588 "OCD",
589 #endif
590 };
591
592 for (auto format_id : format_ids)
593 {
594 auto format = FileFormats.findFormat(format_id);
595 QVERIFY(format);
596 QCOMPARE(format->fileType(), FileFormat::MapFile);
597 QVERIFY(format->supportsReading());
598 QVERIFY(format->supportsWriting());
599
600 for (auto raw_path : example_files)
601 {
602 auto id = QByteArray{};
603 id.reserve(int(qstrlen(raw_path) + qstrlen(format_id) + 5u));
604 id.append(raw_path).append(" <> ").append(format_id);
605 auto path = QString::fromUtf8(raw_path);
606
607 if (qstrcmp(format_id, "OCD") == 0)
608 {
609 for (auto i : { 8, 9, 10, 11, 12 })
610 {
611 auto ocd_id = id;
612 ocd_id.append(QByteArray::number(i));
613 QTest::newRow(ocd_id) << ocd_id << QByteArray{format_id}+QByteArray::number(i) << i << path;
614 }
615 auto ocd_id = id;
616 ocd_id.append(" (default)");
617 QTest::newRow(ocd_id) << ocd_id << QByteArray{format_id} << int(OcdFileExport::default_version) << path;
618 ocd_id = id;
619 ocd_id.append("-legacy");
620 QTest::newRow(ocd_id) << ocd_id << QByteArray{format_id}+"-legacy" << 8 << path;
621 }
622 else
623 {
624 QTest::newRow(id) << id << QByteArray{format_id} << 0 << path;
625 }
626 }
627 }
628 }
629
saveAndLoad()630 void FileFormatTest::saveAndLoad()
631 {
632 QFETCH(QByteArray, format_id);
633 QFETCH(int, format_version);
634 QFETCH(QString, filepath);
635
636 QVERIFY(QFileInfo::exists(filepath));
637
638 // Find the file format and verify that it exists
639 const FileFormat* format = FileFormats.findFormat(format_id);
640 QVERIFY(format);
641
642 // Load the test map
643 auto original = std::make_unique<Map>();
644 QVERIFY(original->loadFrom(filepath));
645
646 // Fix precision of grid rotation
647 MapGrid grid = original->getGrid();
648 grid.setAdditionalRotation(Georeferencing::roundDeclination(grid.getAdditionalRotation()));
649 original->setGrid(grid);
650
651 // Manipulate some data
652 auto printer_config = original->printerConfig();
653 printer_config.page_format.h_overlap += 2.0;
654 printer_config.page_format.v_overlap += 4.0;
655 original->setPrinterConfig(printer_config);
656
657 // Save and load the map
658 auto new_map = saveAndLoadMap(*original, format);
659 QVERIFY2(new_map, "Exception while importing / exporting.");
660
661 if (format_version)
662 {
663 if (qstrncmp(format_id, "OCD", 3) == 0)
664 {
665 QCOMPARE(new_map->property(OcdFileFormat::versionProperty()).toInt(), format_version);
666 }
667 }
668
669 // If the export is lossy, do an extra export / import cycle in order to
670 // be independent of information which cannot be exported into this format
671 if (new_map && format->isWritingLossy())
672 {
673 fuzzyCompareMaps(*new_map, *original);
674
675 original = std::move(new_map);
676 new_map = saveAndLoadMap(*original, format);
677 QVERIFY2(new_map, "Exception while importing / exporting.");
678 }
679
680 compareMaps(*new_map, *original);
681 comparePrinterConfig(new_map->printerConfig(), original->printerConfig());
682
683 if (filepath.endsWith(QStringLiteral("text-object.omap")))
684 {
685 QCOMPARE(new_map->getNumParts(), 1);
686 auto const* part = new_map->getPart(0);
687 QCOMPARE(part->getNumObjects(), 4);
688 for (int i = 0; i < 3; ++i)
689 {
690 auto const* object = part->getObject(i);
691 QCOMPARE(object->getType(), Object::Text);
692 QCOMPARE(static_cast<TextObject const*>(object)->getBoxSize(), MapCoord::fromNative(16000, 10000));
693 }
694 }
695 }
696
697
698
pristineMapTest()699 void FileFormatTest::pristineMapTest()
700 {
701 auto spot_color = std::make_unique<MapColor>(QString::fromLatin1("spot color"), 0);
702 spot_color->setSpotColorName(QString::fromLatin1("SPOTCOLOR"));
703 spot_color->setCmyk({0.1f, 0.2f, 0.3f, 0.4f});
704 spot_color->setRgbFromCmyk();
705
706 auto mixed_color = std::make_unique<MapColor>(QString::fromLatin1("mixed color"), 1);
707 mixed_color->setSpotColorComposition({ {&*spot_color, 0.5f} });
708 mixed_color->setCmykFromSpotColors();
709 mixed_color->setRgbFromCmyk();
710
711 auto custom_color = std::make_unique<MapColor>(QString::fromLatin1("custom color"), 2);
712 custom_color->setSpotColorComposition({ {&*spot_color, 0.5f} });
713 custom_color->setCmyk({0.1f, 0.2f, 0.3f, 0.4f});
714 custom_color->setRgb({0.5f, 0.6f, 0.7f});
715
716 Map map {};
717 map.addColor(spot_color.release(), 0);
718 map.addColor(mixed_color.release(), 1);
719 map.addColor(custom_color.release(), 2);
720
721 XMLFileFormat format;
722 auto reloaded_map = saveAndLoadMap(map, &format);
723 QVERIFY(bool(reloaded_map));
724 compareMaps(*reloaded_map, map);
725 }
726
727
728
ogrExportTest_data()729 void FileFormatTest::ogrExportTest_data()
730 {
731 QTest::addColumn<QString>("map_filepath");
732 QTest::addColumn<QString>("ogr_extension");
733 QTest::addColumn<int>("latitude");
734 QTest::addColumn<int>("longitude");
735
736 QTest::newRow("complete map") << QString::fromLatin1("data:/examples/complete map.omap")
737 << QString::fromLatin1("gpx")
738 << 48 << 12;
739 }
740
ogrExportTest()741 void FileFormatTest::ogrExportTest()
742 {
743 #ifdef MAPPER_USE_GDAL
744 QFETCH(QString, map_filepath);
745 QFETCH(QString, ogr_extension);
746 QFETCH(int, latitude);
747 QFETCH(int, longitude);
748
749 QTemporaryDir dir;
750 QVERIFY(dir.isValid());
751 auto const ogr_filepath = QString {dir.path() + QLatin1String("/ogrexport.") + ogr_extension};
752
753 {
754 Map map;
755 QVERIFY(map.loadFrom(map_filepath));
756 QVERIFY(map.getGeoreferencing().isValid());
757
758 auto const exported_latlon = map.getGeoreferencing().getGeographicRefPoint();
759 QCOMPARE(qRound(exported_latlon.latitude()), latitude);
760 QCOMPARE(qRound(exported_latlon.longitude()), longitude);
761
762 auto const* format = FileFormats.findFormat("OGR-export");
763 QVERIFY(format);
764
765 auto exporter = format->makeExporter(ogr_filepath, &map, nullptr);
766 QVERIFY(bool(exporter));
767 QVERIFY(exporter->doExport());
768 }
769
770 {
771 Map map;
772
773 auto const* format = FileFormats.findFormat("OGR");
774 QVERIFY(format);
775
776 auto importer = format->makeImporter(ogr_filepath, &map, nullptr);
777 QVERIFY(bool(importer));
778 QVERIFY(importer->doImport());
779 QVERIFY(map.getGeoreferencing().isValid());
780
781 auto const imported_latlon = map.getGeoreferencing().getGeographicRefPoint();
782 QCOMPARE(qRound(imported_latlon.latitude()), latitude);
783 QCOMPARE(qRound(imported_latlon.longitude()), longitude);
784 }
785 #endif
786 }
787
788
789
790 /*
791 * We don't need a real GUI window.
792 *
793 * But we discovered QTBUG-58768 macOS: Crash when using QPrinter
794 * while running with "minimal" platform plugin.
795 */
796 #ifndef Q_OS_MACOS
797 namespace {
798 auto Q_DECL_UNUSED qpa_selected = qputenv("QT_QPA_PLATFORM", "minimal"); // clazy:exclude=non-pod-global-static
799 }
800 #endif
801
802
803 QTEST_MAIN(FileFormatTest)
804