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