1 /*
2  *    Copyright 2012, 2013 Thomas Schöps
3  *    Copyright 2013-2020 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 
22 #include "template.h"
23 
24 #include <algorithm>
25 #include <cmath>
26 #include <iosfwd>
27 #include <iterator>
28 #include <new>
29 #include <type_traits>
30 
31 #include <Qt>
32 #include <QtMath>
33 #include <QByteArray>
34 #include <QChar>
35 #include <QColor>
36 #include <QCoreApplication>
37 #include <QDir>
38 #include <QFileInfo>
39 #include <QLatin1Char>
40 #include <QLatin1String>
41 #include <QMessageBox>
42 #include <QPainter>
43 #include <QRectF>
44 #include <QStringRef>
45 #include <QTransform>
46 #include <QXmlStreamAttributes>
47 #include <QXmlStreamReader>
48 #include <QXmlStreamWriter>
49 
50 #include "core/georeferencing.h"
51 #include "core/map_view.h"
52 #include "core/map.h"
53 #include "core/objects/object.h"
54 #include "fileformats/file_format.h"
55 #include "gdal/gdal_template.h"
56 #include "gdal/ogr_template.h"
57 #include "gui/file_dialog.h"
58 #include "templates/template_image.h"
59 #include "templates/template_map.h"
60 #include "templates/template_track.h"
61 #include "util/backports.h"  // IWYU pragma: keep
62 #include "util/util.h"
63 #include "util/xml_stream_util.h"
64 
65 
66 namespace OpenOrienteering {
67 
68 class Template::ScopedOffsetReversal
69 {
70 public:
ScopedOffsetReversal(Template & temp)71 	explicit ScopedOffsetReversal(Template& temp)
72 	: temp(temp)
73 	, copy(temp.transform)
74 	, needed(temp.accounted_offset != MapCoord{} )
75 	{
76 		if (needed)
77 		{
78 			// Temporarily revert the offset the saved data is expected to
79 			// match the original data before bounds checking / correction
80 			temp.applyTemplatePositionOffset();
81 		}
82 	}
83 
~ScopedOffsetReversal()84 	~ScopedOffsetReversal()
85 	{
86 		if (needed)
87 		{
88 			temp.transform = copy;
89 			temp.updateTransformationMatrices();
90 		}
91 	}
92 
93 	ScopedOffsetReversal(const ScopedOffsetReversal&) = delete;
94 	ScopedOffsetReversal(ScopedOffsetReversal&&) = delete;
95 	ScopedOffsetReversal& operator=(const ScopedOffsetReversal&) = delete;
96 	ScopedOffsetReversal&& operator=(ScopedOffsetReversal&&) = delete;
97 
98 private:
99 	Template& temp;
100 	const TemplateTransform copy;
101 	const bool needed;
102 };
103 
104 
105 
106 // ### TemplateTransform ###
107 
108 Q_STATIC_ASSERT(std::is_standard_layout<TemplateTransform>::value);
109 Q_STATIC_ASSERT(std::is_nothrow_default_constructible<TemplateTransform>::value);
110 Q_STATIC_ASSERT(std::is_nothrow_copy_constructible<TemplateTransform>::value);
111 Q_STATIC_ASSERT(std::is_nothrow_move_constructible<TemplateTransform>::value);
112 Q_STATIC_ASSERT(std::is_nothrow_copy_assignable<TemplateTransform>::value);
113 Q_STATIC_ASSERT(std::is_nothrow_move_assignable<TemplateTransform>::value);
114 
115 // static
fromQTransform(const QTransform & qt)116 TemplateTransform TemplateTransform::fromQTransform(const QTransform& qt) noexcept
117 {
118 	const auto rotation = std::atan2(qt.m21()-qt.m12(), qt.m11()+qt.m22());
119 
120 	// Determine the non-rotating, pure scaling aspect.
121 	// Note TemplateTransform rotation is opposite to QTransform.
122 	// This rotation makes the shear components equal.
123 	QTransform scaling = qt * (qFuzzyIsNull(rotation) ? QTransform() : QTransform().rotateRadians(rotation));
124 	return { qRound(1000 * qt.m31()),
125 	         qRound(1000 * qt.m32()),
126 	         rotation,
127 	         scaling.m11(),
128 	         scaling.m22(),
129 	         (scaling.m12()+scaling.m21())/2 };
130 }
131 
132 
133 struct NonShearingObjectTransform
134 {
135 	TemplateTransform const& transform;
136 
operator ()OpenOrienteering::NonShearingObjectTransform137 	void operator()(Object* object) const
138 	{
139 		object->scale(transform.template_scale_x, transform.template_scale_y);
140 		object->rotate(transform.template_rotation);
141 		object->move(transform.template_x, transform.template_y);
142 	}
143 };
144 
145 struct ShearingObjectTransform
146 {
147 	TemplateTransform const& transform;
148 	QTransform const scaling;
149 
operator ()OpenOrienteering::ShearingObjectTransform150 	void operator()(Object* object) const
151 	{
152 		object->transform(scaling);
153 		object->rotate(transform.template_rotation);
154 		object->move(transform.template_x, transform.template_y);
155 	}
156 };
157 
makeObjectTransform() const158 std::function<void (Object*)> TemplateTransform::makeObjectTransform() const
159 {
160 	if (qFuzzyIsNull(template_shear))
161 		return NonShearingObjectTransform{*this};
162 
163 	return ShearingObjectTransform{*this,
164 	                               {template_scale_x, template_shear,
165 	                                template_shear, template_scale_y,
166 	                                0, 0}};
167 }
168 
169 
save(QXmlStreamWriter & xml,const QString & role) const170 void TemplateTransform::save(QXmlStreamWriter& xml, const QString& role) const
171 {
172 	xml.writeStartElement(QString::fromLatin1("transformation"));
173 	xml.writeAttribute(QString::fromLatin1("role"), role);
174 	xml.writeAttribute(QString::fromLatin1("x"), QString::number(template_x));
175 	xml.writeAttribute(QString::fromLatin1("y"), QString::number(template_y));
176 	xml.writeAttribute(QString::fromLatin1("scale_x"), QString::number(template_scale_x));
177 	xml.writeAttribute(QString::fromLatin1("scale_y"), QString::number(template_scale_y));
178 	if (!qFuzzyIsNull(template_shear))
179 		xml.writeAttribute(QString::fromLatin1("shear"), QString::number(template_shear));
180 	xml.writeAttribute(QString::fromLatin1("rotation"), QString::number(template_rotation));
181 	xml.writeEndElement(/*transformation*/);
182 }
183 
load(QXmlStreamReader & xml)184 void TemplateTransform::load(QXmlStreamReader& xml)
185 {
186 	Q_ASSERT(xml.name() == QLatin1String("transformation"));
187 
188 	XmlElementReader element { xml };
189 	auto x64 = element.attribute<qint64>(QLatin1String("x"));
190 	auto y64 = element.attribute<qint64>(QLatin1String("y"));
191 	auto coord = MapCoord::fromNative64withOffset(x64, y64);
192 	template_x = coord.nativeX();
193 	template_y = coord.nativeY();
194 	template_scale_x = element.attribute<double>(QLatin1String("scale_x"));
195 	template_scale_y = element.attribute<double>(QLatin1String("scale_y"));
196 	template_shear = element.attribute<double>(QLatin1String("shear")); // 0 if absent
197 	template_rotation = element.attribute<double>(QLatin1String("rotation"));
198 }
199 
operator ==(const TemplateTransform & lhs,const TemplateTransform & rhs)200 bool operator==(const TemplateTransform& lhs, const TemplateTransform& rhs) noexcept
201 {
202 	return lhs.template_x == rhs.template_x
203 	        && lhs.template_y == rhs.template_y
204 	        && (qFuzzyCompare(lhs.template_rotation, rhs.template_rotation)
205 	            || (qIsNull(lhs.template_rotation) && qIsNull(rhs.template_rotation)))
206 	        && qFuzzyCompare(lhs.template_scale_x, rhs.template_scale_x)
207 	        && qFuzzyCompare(lhs.template_scale_y, rhs.template_scale_y)
208 	        && (qFuzzyCompare(lhs.template_shear, rhs.template_shear)
209 	            || (qFuzzyIsNull(lhs.template_shear) && qFuzzyIsNull(rhs.template_shear)));
210 }
211 
operator !=(const TemplateTransform & lhs,const TemplateTransform & rhs)212 bool operator!=(const TemplateTransform& lhs, const TemplateTransform& rhs) noexcept
213 {
214 	return !operator==(lhs, rhs);
215 }
216 
217 
218 
219 // ### Template ###
220 
221 bool Template::suppressAbsolutePaths = false;
222 
223 
Template(const QString & path,not_null<Map * > map)224 Template::Template(const QString& path, not_null<Map*> map)
225 : map(map)
226 {
227 	QFileInfo file_info{path};
228 	template_file = file_info.fileName();
229 
230 	auto canonical_path = file_info.canonicalFilePath();
231 	template_path = canonical_path.isEmpty() ? path : canonical_path;
232 
233 	updateTransformationMatrices();
234 }
235 
Template(const Template & proto)236 Template::Template(const Template& proto)
237 : QObject(nullptr)
238 , map(proto.map)
239 , template_file(proto.template_file)
240 , template_path(proto.template_path)
241 , template_relative_path(proto.template_relative_path)
242 , template_state(proto.template_state)
243 , error_string(proto.error_string)
244 , has_unsaved_changes(proto.has_unsaved_changes)
245 , is_georeferenced(proto.is_georeferenced)
246 , accounted_offset(proto.accounted_offset)
247 , transform(proto.transform)
248 , other_transform(proto.other_transform)
249 , adjusted(proto.adjusted)
250 , adjustment_dirty(proto.adjustment_dirty)
251 , passpoints(proto.passpoints)
252 , template_group(proto.template_group)
253 , map_to_template(proto.map_to_template)
254 , template_to_map(proto.template_to_map)
255 , template_to_map_other(proto.template_to_map_other)
256 {
257 	// nothing else
258 }
259 
~Template()260 Template::~Template()
261 {
262 	Q_ASSERT(template_state != Loaded);
263 }
264 
errorString() const265 QString Template::errorString() const
266 {
267 	return error_string;
268 }
269 
setErrorString(const QString & text)270 void Template::setErrorString(const QString &text)
271 {
272 	error_string = text;
273 }
274 
275 
276 
saveTemplateConfiguration(QXmlStreamWriter & xml,bool open,const QDir * map_dir) const277 void Template::saveTemplateConfiguration(QXmlStreamWriter& xml, bool open, const QDir* map_dir) const
278 {
279 	xml.writeStartElement(QString::fromLatin1("template"));
280 	xml.writeAttribute(QString::fromLatin1("open"), QString::fromLatin1(open ? "true" : "false"));
281 	xml.writeAttribute(QString::fromLatin1("name"), getTemplateFilename());
282 	auto primary_path = getTemplatePath();
283 	auto relative_path = getTemplateRelativePath();
284 	if (getTemplateState() != Invalid)
285 	{
286 		if (map_dir)
287 			relative_path = map_dir->relativeFilePath(primary_path);
288 		else if (relative_path.isEmpty())
289 			relative_path = getTemplateFilename();
290 	}
291 	if (suppressAbsolutePaths && QFileInfo(primary_path).isAbsolute())
292 		primary_path = relative_path;
293 	xml.writeAttribute(QString::fromLatin1("path"), primary_path);
294 	xml.writeAttribute(QString::fromLatin1("relpath"), relative_path);
295 
296 	if (template_group)
297 	{
298 		xml.writeAttribute(QString::fromLatin1("group"), QString::number(template_group));
299 	}
300 
301 	if (is_georeferenced)
302 	{
303 		xml.writeAttribute(QString::fromLatin1("georef"), QString::fromLatin1("true"));
304 	}
305 	else
306 	{
307 		ScopedOffsetReversal no_offset{*const_cast<Template*>(this)};
308 
309 		xml.writeStartElement(QString::fromLatin1("transformations"));
310 		if (adjusted)
311 			xml.writeAttribute(QString::fromLatin1("adjusted"), QString::fromLatin1("true"));
312 		if (adjustment_dirty)
313 			xml.writeAttribute(QString::fromLatin1("adjustment_dirty"), QString::fromLatin1("true"));
314 		int num_passpoints = (int)passpoints.size();
315 		xml.writeAttribute(QString::fromLatin1("passpoints"), QString::number(num_passpoints));
316 
317 		transform.save(xml, QString::fromLatin1("active"));
318 		other_transform.save(xml, QString::fromLatin1("other"));
319 
320 		for (int i = 0; i < num_passpoints; ++i)
321 			passpoints[i].save(xml);
322 
323 		map_to_template.save(xml, QString::fromLatin1("map_to_template"));
324 		template_to_map.save(xml, QString::fromLatin1("template_to_map"));
325 		template_to_map_other.save(xml, QString::fromLatin1("template_to_map_other"));
326 
327 		xml.writeEndElement(/*transformations*/);
328 	}
329 
330 	saveTypeSpecificTemplateConfiguration(xml);
331 
332 	if (open && hasUnsavedChanges())
333 	{
334 		// Save the template itself (e.g. image, gpx file, etc.)
335 		saveTemplateFile();
336 		const_cast<Template*>(this)->setHasUnsavedChanges(false); // TODO: Error handling?
337 	}
338 	xml.writeEndElement(/*template*/);
339 }
340 
loadTemplateConfiguration(QXmlStreamReader & xml,Map & map,bool & open)341 std::unique_ptr<Template> Template::loadTemplateConfiguration(QXmlStreamReader& xml, Map& map, bool& open)
342 {
343 	Q_ASSERT(xml.name() == QLatin1String("template"));
344 
345 	QXmlStreamAttributes attributes = xml.attributes();
346 	if (attributes.hasAttribute(QLatin1String("open")))
347 		open = (attributes.value(QLatin1String("open")) == QLatin1String("true"));
348 
349 	QString path = attributes.value(QLatin1String("path")).toString();
350 	auto temp = templateForFile(path, &map);
351 	if (!temp)
352 		temp = std::make_unique<TemplateImage>(path, &map); // fallback
353 
354 	temp->setTemplateRelativePath(attributes.value(QLatin1String("relpath")).toString());
355 	if (attributes.hasAttribute(QLatin1String("name")))
356 		temp->template_file = attributes.value(QLatin1String("name")).toString();
357 	temp->is_georeferenced = (attributes.value(QLatin1String("georef")) == QLatin1String("true"));
358 	if (attributes.hasAttribute(QLatin1String("group")))
359 		temp->template_group = attributes.value(QLatin1String("group")).toInt();
360 
361 	while (xml.readNextStartElement())
362 	{
363 		if (!temp->is_georeferenced && xml.name() == QLatin1String("transformations"))
364 		{
365 			temp->adjusted = (xml.attributes().value(QLatin1String("adjusted")) == QLatin1String("true"));
366 			temp->adjustment_dirty = (xml.attributes().value(QLatin1String("adjustment_dirty")) == QLatin1String("true"));
367 			int num_passpoints = xml.attributes().value(QLatin1String("passpoints")).toInt();
368 Q_ASSERT(temp->passpoints.size() == 0);
369 			temp->passpoints.reserve(qMin(num_passpoints, 10)); // 10 is not a limit
370 
371 			while (xml.readNextStartElement())
372 			{
373 				QStringRef role = xml.attributes().value(QLatin1String("role"));
374 				if (xml.name() == QLatin1String("transformation"))
375 				{
376 					if (role == QLatin1String("active"))
377 						temp->transform.load(xml);
378 					else if (xml.attributes().value(QLatin1String("role")) == QLatin1String("other"))
379 						temp->other_transform.load(xml);
380 					else
381 					{
382 						qDebug("%s", xml.qualifiedName().toLocal8Bit().constData());
383 						xml.skipCurrentElement(); // unsupported
384 					}
385 				}
386 				else if (xml.name() == QLatin1String("passpoint"))
387 				{
388 					temp->passpoints.push_back(PassPoint::load(xml));
389 				}
390 				else if (xml.name() == QLatin1String("matrix"))
391 				{
392 					if (role == QLatin1String("map_to_template"))
393 						temp->map_to_template.load(xml);
394 					else if (role == QLatin1String("template_to_map"))
395 						temp->template_to_map.load(xml);
396 					else if (role == QLatin1String("template_to_map_other"))
397 						temp->template_to_map_other.load(xml);
398 					else
399 					{
400 						qDebug("%s", xml.qualifiedName().toLocal8Bit().constData());
401 						xml.skipCurrentElement(); // unsupported
402 					}
403 				}
404 				else
405 				{
406 					qDebug("%s", xml.qualifiedName().toLocal8Bit().constData());
407 					xml.skipCurrentElement(); // unsupported
408 				}
409 			}
410 		}
411 		else if (!temp->loadTypeSpecificTemplateConfiguration(xml))
412 		{
413 			temp.reset();
414 			break;
415 		}
416 	}
417 
418 	if (temp && !temp->is_georeferenced)
419 	{
420 		// Fix template adjustment after moving objects during import (cf. #513)
421 		const auto offset = MapCoord::boundsOffset();
422 		if (!offset.isZero())
423 		{
424 			if (temp->template_to_map.getCols() == 3 && temp->template_to_map.getRows() == 3)
425 			{
426 				temp->template_to_map.set(0, 2, temp->template_to_map.get(0, 2) - offset.x / 1000.0);
427 				temp->template_to_map.set(1, 2, temp->template_to_map.get(1, 2) - offset.y / 1000.0);
428 				temp->template_to_map.invert(temp->map_to_template);
429 			}
430 
431 			if (temp->template_to_map_other.getCols() == 3 && temp->template_to_map_other.getRows() == 3)
432 			{
433 				temp->template_to_map_other.set(0, 2, temp->template_to_map_other.get(0, 2) - offset.x / 1000.0);
434 				temp->template_to_map_other.set(1, 2, temp->template_to_map_other.get(1, 2) - offset.y / 1000.0);
435 			}
436 		}
437 
438 		// Fix template alignment problems caused by grivation rounding since version 0.6
439 		const double correction = map.getGeoreferencing().getGrivationError();
440 		if (qAbs(correction) != 0.0
441 		    && (qstrcmp(temp->getTemplateType(), "TemplateTrack") == 0
442 		        || qstrcmp(temp->getTemplateType(), "OgrTemplate") == 0) )
443 		{
444 			temp->setTemplateRotation(temp->getTemplateRotation() + Georeferencing::degToRad(correction));
445 		}
446 	}
447 
448 	return temp;
449 }
450 
saveTemplateFile() const451 bool Template::saveTemplateFile() const
452 {
453 	return false;
454 }
455 
switchTemplateFile(const QString & new_path,bool load_file)456 void Template::switchTemplateFile(const QString& new_path, bool load_file)
457 {
458 	if (template_state == Loaded)
459 	{
460 		setTemplateAreaDirty();
461 		unloadTemplateFile();
462 	}
463 
464 	template_path          = new_path;
465 	template_file          = QFileInfo(new_path).fileName();
466 	template_relative_path = QString();
467 	template_state         = Template::Unloaded;
468 
469 	if (load_file)
470 		loadTemplateFile(false);
471 }
472 
execSwitchTemplateFileDialog(QWidget * dialog_parent)473 bool Template::execSwitchTemplateFileDialog(QWidget* dialog_parent)
474 {
475 	QString new_path = FileDialog::getOpenFileName(dialog_parent,
476 	                                               tr("Find the moved template file"),
477 	                                               QString(),
478 	                                               tr("All files (*.*)") );
479 	new_path = QFileInfo(new_path).canonicalFilePath();
480 	if (new_path.isEmpty())
481 		return false;
482 
483 	const State   old_state = getTemplateState();
484 	const QString old_path  = getTemplatePath();
485 
486 	switchTemplateFile(new_path, true);
487 	if (getTemplateState() != Loaded)
488 	{
489 		QString error_template = QCoreApplication::translate("OpenOrienteering::TemplateListWidget", "Cannot open template\n%1:\n%2").arg(new_path);
490 		QString error = errorString();
491 		Q_ASSERT(!error.isEmpty());
492 		QMessageBox::warning(dialog_parent,
493 		                     tr("Error"),
494 		                     error_template.arg(error));
495 
496 		// Revert change
497 		switchTemplateFile(old_path, old_state == Loaded);
498 		if (old_state == Invalid)
499 		{
500 			template_state = Invalid;
501 		}
502 		return false;
503 	}
504 
505 	return true;
506 }
507 
configureAndLoad(QWidget * dialog_parent,MapView * view)508 bool Template::configureAndLoad(QWidget* dialog_parent, MapView* view)
509 {
510 	bool center_in_view = true;
511 
512 	if (!preLoadConfiguration(dialog_parent))
513 		return false;
514 	if (!loadTemplateFile(true))
515 		return false;
516 	if (!postLoadConfiguration(dialog_parent, center_in_view))
517 	{
518 		unloadTemplateFile();
519 		return false;
520 	}
521 
522 	// If the template is not georeferenced, position it at the viewport midpoint
523 	if (!isTemplateGeoreferenced() && center_in_view)
524 	{
525 		auto offset = MapCoord { calculateTemplateBoundingBox().center() };
526 		setTemplatePosition(view->center() - offset);
527 	}
528 
529 	return true;
530 }
531 
532 
533 
tryToFindTemplateFile(const QString & map_path)534 Template::LookupResult Template::tryToFindTemplateFile(const QString& map_path)
535 {
536 	// This function normally sets the state either to Invalid or Unloaded.
537 	// However, the Loaded state must not be changed here because this would
538 	// cause inconsistencies with other data held by templates in this state.
539 	auto const set_state = [this](auto proposed_state) {
540 		if (template_state != Loaded)
541 			template_state = proposed_state;
542 	};
543 
544 	// Determining the directory from a file or directory
545 	auto const dir = [](const QString& path) -> QDir {
546 		auto const path_info = QFileInfo(path);
547 		if (path_info.isFile())
548 			return path_info.dir();
549 		if (path_info.isDir())
550 			return { path };
551 		return {};
552 	};
553 
554 	// 1. The relative path with regard to the map directory, if both are valid
555 	auto const rel_path = getTemplateRelativePath();
556 	if (!rel_path.isEmpty() && !map_path.isEmpty())
557 	{
558 		auto const abs_path_info = QFileInfo(dir(map_path).absoluteFilePath(rel_path));
559 		if (abs_path_info.isFile())
560 		{
561 			setTemplateFileInfo(abs_path_info);
562 			set_state(Unloaded);
563 			return FoundByRelPath;
564 		}
565 	}
566 
567 	// 2. The absolute path of the template
568 	auto const template_path_info = QFileInfo(getTemplatePath());
569 	if (template_path_info.isFile())
570 	{
571 		/* setTemplateFileInfo(template_path_info); */
572 		set_state(Unloaded);
573 		return FoundByAbsPath;
574 	}
575 
576 	// 3. The map directory, if valid, for the filename of the template
577 	auto const filename = getTemplateFilename();
578 	if (!filename.isEmpty() && !map_path.isEmpty())
579 	{
580 		auto const abs_path_info = QFileInfo(dir(map_path).absoluteFilePath(filename));
581 		if (abs_path_info.isFile())
582 		{
583 			setTemplateFileInfo(abs_path_info);
584 			set_state(Unloaded);
585 			return FoundInMapDir;
586 		}
587 	}
588 
589 	set_state(Invalid);
590 	setErrorString(tr("No such file."));
591 	return NotFound;
592 }
593 
594 
tryToFindAndReloadTemplateFile(const QString & map_path)595 bool Template::tryToFindAndReloadTemplateFile(const QString& map_path)
596 {
597 	return tryToFindTemplateFile(map_path) != NotFound
598 	       && loadTemplateFile(false);
599 }
600 
preLoadConfiguration(QWidget *)601 bool Template::preLoadConfiguration(QWidget* /*dialog_parent*/)
602 {
603 	return true;
604 }
605 
loadTemplateFile(bool configuring)606 bool Template::loadTemplateFile(bool configuring)
607 {
608 	Q_ASSERT(template_state != Loaded);
609 
610 	const State old_state = template_state;
611 
612 	setErrorString(QString());
613 	try
614 	{
615 		if (!QFileInfo::exists(template_path))
616 		{
617 			template_state = Invalid;
618 			setErrorString(tr("No such file."));
619 		}
620 		else if (loadTemplateFileImpl(configuring))
621 		{
622 			template_state = Loaded;
623 		}
624 		else
625 		{
626 			template_state = Invalid;
627 			if (errorString().isEmpty())
628 			{
629 				qDebug("%s: Missing error message from failure in %s::loadTemplateFileImpl(bool)", \
630 				         Q_FUNC_INFO, \
631 				         getTemplateType() );
632 				setErrorString(tr("Is the format of the file correct for this template type?"));
633 			}
634 		}
635 	}
636 	catch (std::bad_alloc&)
637 	{
638 		template_state = Invalid;
639 		setErrorString(tr("Not enough free memory."));
640 	}
641 	catch (FileFormatException& e)
642 	{
643 		template_state = Invalid;
644 		setErrorString(e.message());
645 	}
646 
647 	if (old_state != template_state)
648 		emit templateStateChanged();
649 
650 	return template_state == Loaded;
651 }
652 
postLoadConfiguration(QWidget *,bool &)653 bool Template::postLoadConfiguration(QWidget* /*dialog_parent*/, bool& /*out_center_in_view*/)
654 {
655 	return true;
656 }
657 
unloadTemplateFile()658 void Template::unloadTemplateFile()
659 {
660 	Q_ASSERT(template_state == Loaded);
661 	if (hasUnsavedChanges())
662 	{
663 		// The changes are lost
664 		setHasUnsavedChanges(false);
665 	}
666 	unloadTemplateFileImpl();
667 	template_state = Unloaded;
668 	emit templateStateChanged();
669 }
670 
671 
672 // virtual
canChangeTemplateGeoreferenced()673 bool Template::canChangeTemplateGeoreferenced()
674 {
675 	return false;
676 }
677 
678 // virtual
trySetTemplateGeoreferenced(bool value,QWidget *)679 bool Template::trySetTemplateGeoreferenced(bool value, QWidget* /*dialog_parent*/)
680 {
681 	return isTemplateGeoreferenced() == value;
682 }
683 
684 
applyTemplateTransform(QPainter * painter) const685 void Template::applyTemplateTransform(QPainter* painter) const
686 {
687 	painter->translate(transform.template_x / 1000.0, transform.template_y / 1000.0);
688 	// Rotate counter-clockwise.
689 	painter->rotate(-transform.template_rotation * (180 / M_PI));
690 
691 	// Scale
692 	if (qFuzzyIsNull(transform.template_shear))
693 	{
694 		painter->scale(transform.template_scale_x, transform.template_scale_y);
695 	}
696 	else
697 	{
698 		QTransform scaling(transform.template_scale_x, transform.template_shear,
699 		                   transform.template_shear, transform.template_scale_y,
700 		                   0, 0);
701 		painter->setTransform(scaling, true);
702 	}
703 }
704 
getTemplateExtent() const705 QRectF Template::getTemplateExtent() const
706 {
707 	Q_ASSERT(!is_georeferenced);
708 	return infiniteRectF();
709 }
710 
scale(double factor,const MapCoord & center)711 void Template::scale(double factor, const MapCoord& center)
712 {
713 	Q_ASSERT(!is_georeferenced);
714 	setTemplatePosition(center + factor * (templatePosition() - center));
715 	setTemplateScaleX(factor * getTemplateScaleX());
716 	setTemplateScaleY(factor * getTemplateScaleY());
717 
718 	accounted_offset *= factor;
719 }
720 
rotate(double rotation,const MapCoord & center)721 void Template::rotate(double rotation, const MapCoord& center)
722 {
723 	Q_ASSERT(!is_georeferenced);
724 
725 	auto offset = templatePositionOffset();
726 	setTemplatePositionOffset({});
727 
728 	setTemplateRotation(getTemplateRotation() + rotation);
729 
730 	auto position = MapCoordF{templatePosition() - center};
731 	position.rotate(-rotation);
732 	setTemplatePosition(MapCoord{position} + center);
733 
734 	setTemplatePositionOffset(offset);
735 }
736 
setTemplateAreaDirty()737 void Template::setTemplateAreaDirty()
738 {
739 	QRectF template_area = calculateTemplateBoundingBox();
740 	map->setTemplateAreaDirty(this, template_area, getTemplateBoundingBoxPixelBorder());	// TODO: Would be better to do this with the corner points, instead of the bounding box
741 }
742 
canBeDrawnOnto() const743 bool Template::canBeDrawnOnto() const
744 {
745 	return false;
746 }
747 
calculateTemplateBoundingBox() const748 QRectF Template::calculateTemplateBoundingBox() const
749 {
750 	// Create bounding box by calculating the positions of all corners of the transformed extent rect
751 	QRectF extent = getTemplateExtent();
752 	QRectF bbox;
753 	rectIncludeSafe(bbox, templateToMap(extent.topLeft()));
754 	rectInclude(bbox, templateToMap(extent.topRight()));
755 	rectInclude(bbox, templateToMap(extent.bottomRight()));
756 	rectInclude(bbox, templateToMap(extent.bottomLeft()));
757 	return bbox;
758 }
759 
drawOntoTemplate(not_null<MapCoordF * > coords,int num_coords,const QColor & color,qreal width,QRectF map_bbox)760 void Template::drawOntoTemplate(not_null<MapCoordF*> coords, int num_coords, const QColor& color, qreal width, QRectF map_bbox)
761 {
762 	Q_ASSERT(canBeDrawnOnto());
763 	Q_ASSERT(num_coords > 1);
764 
765 	if (!map_bbox.isValid())
766 	{
767 		map_bbox = QRectF(coords[0].x(), coords[0].y(), 0, 0);
768 		for (int i = 1; i < num_coords; ++i)
769 			rectInclude(map_bbox, coords[i]);
770 	}
771 	auto const radius = qMin(getTemplateScaleX(), getTemplateScaleY()) * qMax((width+1) / 2, 1.0);
772 	QRectF radius_bbox = QRectF(map_bbox.left() - radius, map_bbox.top() - radius,
773 								map_bbox.width() + 2*radius, map_bbox.height() + 2*radius);
774 
775 	drawOntoTemplateImpl(coords, num_coords, color, width);
776 	map->setTemplateAreaDirty(this, radius_bbox, 0);
777 
778 	setHasUnsavedChanges(true);
779 }
780 
drawOntoTemplateUndo(bool)781 void Template::drawOntoTemplateUndo(bool /*redo*/)
782 {
783 	// nothing
784 }
785 
addPassPoint(const PassPoint & point,int pos)786 void Template::addPassPoint(const PassPoint& point, int pos)
787 {
788 	Q_ASSERT(!is_georeferenced);
789 	passpoints.insert(passpoints.begin() + pos, point);
790 }
deletePassPoint(int pos)791 void Template::deletePassPoint(int pos)
792 {
793 	passpoints.erase(passpoints.begin() + pos);
794 }
clearPassPoints()795 void Template::clearPassPoints()
796 {
797 	passpoints.clear();
798 	setAdjustmentDirty(true);
799 	adjusted = false;
800 }
801 
switchTransforms()802 void Template::switchTransforms()
803 {
804 	Q_ASSERT(!is_georeferenced);
805 	setTemplateAreaDirty();
806 
807 	TemplateTransform temp = transform;
808 	transform = other_transform;
809 	other_transform = temp;
810 	template_to_map_other = template_to_map;
811 	updateTransformationMatrices();
812 
813 	adjusted = !adjusted;
814 	setTemplateAreaDirty();
815 	map->setTemplatesDirty();
816 }
getTransform(TemplateTransform & out) const817 void Template::getTransform(TemplateTransform& out) const
818 {
819 	Q_ASSERT(!is_georeferenced);
820 	out = transform;
821 }
setTransform(const TemplateTransform & transform)822 void Template::setTransform(const TemplateTransform& transform)
823 {
824 	Q_ASSERT(!is_georeferenced);
825 	setTemplateAreaDirty();
826 
827 	this->transform = transform;
828 	updateTransformationMatrices();
829 
830 	setTemplateAreaDirty();
831 	map->setTemplatesDirty();
832 }
getOtherTransform(TemplateTransform & out) const833 void Template::getOtherTransform(TemplateTransform& out) const
834 {
835 	Q_ASSERT(!is_georeferenced);
836 	out = other_transform;
837 }
setOtherTransform(const TemplateTransform & transform)838 void Template::setOtherTransform(const TemplateTransform& transform)
839 {
840 	Q_ASSERT(!is_georeferenced);
841 	other_transform = transform;
842 }
843 
setTemplateFileInfo(const QFileInfo & file_info)844 void Template::setTemplateFileInfo(const QFileInfo& file_info)
845 {
846 	template_path = file_info.canonicalFilePath();
847 	if (template_path.isEmpty())
848 		template_path = file_info.path();
849 	template_file = file_info.fileName();
850 }
851 
setTemplatePath(const QString & value)852 void Template::setTemplatePath(const QString& value)
853 {
854 	setTemplateFileInfo(QFileInfo(value));
855 }
856 
setHasUnsavedChanges(bool value)857 void Template::setHasUnsavedChanges(bool value)
858 {
859 	has_unsaved_changes = value;
860 	if (value)
861 		map->setTemplatesDirty();
862 }
863 
setAdjustmentDirty(bool value)864 void Template::setAdjustmentDirty(bool value)
865 {
866 	adjustment_dirty = value;
867 	if (value)
868 		map->setTemplatesDirty();
869 }
870 
871 
872 
hasAlpha() const873 bool Template::hasAlpha() const
874 {
875 	return template_state == Template::Loaded;
876 }
877 
878 
879 
supportedExtensions()880 const std::vector<QByteArray>& Template::supportedExtensions()
881 {
882 	static std::vector<QByteArray> extensions;
883 	if (extensions.empty())
884 	{
885 		auto& image_extensions = TemplateImage::supportedExtensions();
886 		auto& map_extensions   = TemplateMap::supportedExtensions();
887 #ifdef MAPPER_USE_GDAL
888 		auto& ogr_extensions   = OgrTemplate::supportedExtensions();
889 #else
890 		auto ogr_extensions    = std::vector<QByteArray>{ };
891 #endif
892 		auto& track_extensions = TemplateTrack::supportedExtensions();
893 		extensions.reserve(image_extensions.size()
894 		                   + map_extensions.size()
895 		                   + ogr_extensions.size()
896 		                   + track_extensions.size());
897 		extensions.insert(end(extensions), begin(image_extensions), end(image_extensions));
898 		extensions.insert(end(extensions), begin(map_extensions), end(map_extensions));
899 		extensions.insert(end(extensions), begin(ogr_extensions), end(ogr_extensions));
900 		extensions.insert(end(extensions), begin(track_extensions), end(track_extensions));
901 	}
902 	return extensions;
903 }
904 
templateForFile(const QString & path,Map * map)905 std::unique_ptr<Template> Template::templateForFile(const QString& path, Map* map)
906 {
907 	auto path_ends_with_any_of = [path](const std::vector<QByteArray>& list) -> bool {
908 		using namespace std;
909 		return any_of(begin(list), end(list), [path](const QByteArray& extension) {
910 			return path.size() > extension.size()
911 			       && path[path.size() - extension.size() - 1] == QLatin1Char('.')
912 			       && path.endsWith(QLatin1String(extension), Qt::CaseInsensitive);
913 		} );
914 	};
915 
916 	std::unique_ptr<Template> t;
917 	// Start with placeholder 'if', for consistency in the following macro/if-else sequence
918 	if (false)  // NOLINT
919 		{} // nothing
920 #ifdef MAPPER_USE_GDAL
921 	else if (path_ends_with_any_of(GdalTemplate::supportedExtensions()))
922 		t = std::make_unique<GdalTemplate>(path, map);
923 #endif
924 	else if (path_ends_with_any_of(TemplateImage::supportedExtensions()))
925 		t = std::make_unique<TemplateImage>(path, map);
926 	else if (path_ends_with_any_of(TemplateMap::supportedExtensions()))
927 		t = std::make_unique<TemplateMap>(path, map);
928 #ifdef MAPPER_USE_GDAL
929 	else if (path_ends_with_any_of(OgrTemplate::supportedExtensions()))
930 		t = std::make_unique<OgrTemplate>(path, map);
931 #endif
932 	else if (path_ends_with_any_of(TemplateTrack::supportedExtensions()))
933 		t = std::make_unique<TemplateTrack>(path, map);
934 #ifdef MAPPER_USE_GDAL
935 	else
936 		t = std::make_unique<OgrTemplate>(path, map);
937 #endif
938 
939 	return t;
940 }
941 
942 
943 
saveTypeSpecificTemplateConfiguration(QXmlStreamWriter &) const944 void Template::saveTypeSpecificTemplateConfiguration(QXmlStreamWriter& /*xml*/) const
945 {
946 	// nothing
947 }
948 
loadTypeSpecificTemplateConfiguration(QXmlStreamReader & xml)949 bool Template::loadTypeSpecificTemplateConfiguration(QXmlStreamReader& xml)
950 {
951 	xml.skipCurrentElement();
952 	return true;
953 }
954 
drawOntoTemplateImpl(MapCoordF *,int,const QColor &,qreal)955 void Template::drawOntoTemplateImpl(MapCoordF* /*coords*/, int /*num_coords*/, const QColor& /*color*/, qreal /*width*/)
956 {
957 	// nothing
958 }
959 
templatePosition() const960 MapCoord Template::templatePosition() const
961 {
962 	return MapCoord::fromNative(transform.template_x, transform.template_y);
963 }
964 
965 
setTemplatePosition(const MapCoord & coord)966 void Template::setTemplatePosition(const MapCoord& coord)
967 {
968 	transform.template_x = coord.nativeX();
969 	transform.template_y = coord.nativeY();
970 	updateTransformationMatrices();
971 }
972 
templatePositionOffset() const973 MapCoord Template::templatePositionOffset() const
974 {
975 	return accounted_offset;
976 }
977 
setTemplatePositionOffset(const MapCoord & offset)978 void Template::setTemplatePositionOffset(const MapCoord& offset)
979 {
980 	const auto move = accounted_offset - offset;
981 	if (move != MapCoord{})
982 	{
983 		accounted_offset = move;
984 		applyTemplatePositionOffset();
985 	}
986 	accounted_offset = offset;
987 }
988 
applyTemplatePositionOffset()989 void Template::applyTemplatePositionOffset()
990 {
991 	QTransform t;
992 	t.rotate(-qRadiansToDegrees(getTemplateRotation()));
993 	const double shear = getTemplateShear();
994 	QTransform scaling(getTemplateScaleX(), shear,
995 	                   shear, getTemplateScaleY(),
996 	                   0, 0);
997 	setTemplatePosition(templatePosition() - MapCoord{(scaling*t).map(QPointF{accounted_offset})});
998 }
999 
resetTemplatePositionOffset()1000 void Template::resetTemplatePositionOffset()
1001 {
1002 	accounted_offset = {};
1003 }
1004 
1005 
updateTransformationMatrices()1006 void Template::updateTransformationMatrices()
1007 {
1008 	double shear = getTemplateShear();
1009 	double cosr = cos(-transform.template_rotation);
1010 	double sinr = sin(-transform.template_rotation);
1011 	double scale_x = getTemplateScaleX();
1012 	double scale_y = getTemplateScaleY();
1013 
1014 	template_to_map.setSize(3, 3);
1015 	template_to_map.set(0, 0, scale_x * cosr -   shear * sinr);
1016 	template_to_map.set(0, 1,   shear * cosr - scale_y * sinr);
1017 	template_to_map.set(1, 0, scale_x * sinr +   shear * cosr);
1018 	template_to_map.set(1, 1,   shear * sinr + scale_y * cosr);
1019 	template_to_map.set(0, 2, transform.template_x / 1000.0);
1020 	template_to_map.set(1, 2, transform.template_y / 1000.0);
1021 	template_to_map.set(2, 0, 0);
1022 	template_to_map.set(2, 1, 0);
1023 	template_to_map.set(2, 2, 1);
1024 
1025 	template_to_map.invert(map_to_template);
1026 }
1027 
1028 
1029 }  // namespace OpenOrienteering
1030