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