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 
22 #include "georeferencing_dialog.h"
23 
24 #include <cmath>
25 #include <vector>
26 
27 #include <Qt>
28 #include <QtGlobal>
29 #include <QAbstractButton>
30 #include <QCheckBox>
31 #include <QCursor>
32 #include <QDate>
33 #include <QDebug>
34 #include <QDesktopServices>  // IWYU pragma: keep
35 #include <QDialogButtonBox>
36 #include <QDoubleSpinBox>
37 #include <QFlags>
38 #include <QFormLayout>
39 #include <QHBoxLayout>
40 #include <QLabel>
41 #include <QLatin1Char>
42 #include <QLatin1String>
43 #include <QLocale>
44 #include <QMessageBox>
45 #include <QMouseEvent>
46 #include <QPixmap>
47 #include <QPointF>
48 #include <QPushButton>
49 #include <QRadioButton>
50 #include <QSignalBlocker>
51 #include <QSize>
52 #include <QSpacerItem>
53 #include <QStringRef>
54 #include <QTimer>
55 #include <QUrl>
56 #include <QUrlQuery>
57 #include <QVariant>
58 #include <QVBoxLayout>
59 #include <QWidget>
60 #include <QXmlStreamReader>
61 // IWYU pragma: no_include <qxmlstream.h>
62 
63 #if defined(QT_NETWORK_LIB)
64 #include <QNetworkAccessManager>
65 #include <QNetworkReply>
66 #include <QNetworkRequest>
67 #endif
68 
69 #include "settings.h"
70 #include "core/crs_template.h"
71 #include "core/georeferencing.h"
72 #include "core/latlon.h"
73 #include "core/map.h"
74 #include "gui/main_window.h"
75 #include "gui/map/map_dialog_rotate.h"
76 #include "gui/map/map_dialog_stretch.h"
77 #include "gui/map/map_editor.h"
78 #include "gui/widgets/crs_selector.h"
79 #include "gui/util_gui.h"
80 #include "util/backports.h"  // IWYU pragma: keep
81 #include "util/scoped_signals_blocker.h"
82 
83 
84 #ifdef __clang_analyzer__
85 #define singleShot(A, B, C) singleShot(A, B, #C) // NOLINT
86 #endif
87 
88 
89 namespace OpenOrienteering {
90 
91 Q_STATIC_ASSERT(Georeferencing::declinationPrecision() == Util::InputProperties<Util::RotationalDegrees>::decimals());
92 
93 
94 namespace  {
95 
setValueIfChanged(QDoubleSpinBox * field,qreal value)96 void setValueIfChanged(QDoubleSpinBox* field, qreal value) {
97 	if (!qFuzzyCompare(field->value(), value))
98 		field->setValue(value);
99 }
100 
101 }  // namespace
102 
103 
104 
105 // ### GeoreferencingDialog ###
106 
GeoreferencingDialog(MapEditorController * controller,const Georeferencing * initial,bool allow_no_georeferencing)107 GeoreferencingDialog::GeoreferencingDialog(MapEditorController* controller, const Georeferencing* initial, bool allow_no_georeferencing)
108  : GeoreferencingDialog(controller->getWindow(), controller, controller->getMap(), initial, allow_no_georeferencing)
109 {
110 	// nothing else
111 }
112 
GeoreferencingDialog(QWidget * parent,Map * map,const Georeferencing * initial,bool allow_no_georeferencing)113 GeoreferencingDialog::GeoreferencingDialog(QWidget* parent, Map* map, const Georeferencing* initial, bool allow_no_georeferencing)
114  : GeoreferencingDialog(parent, nullptr, map, initial, allow_no_georeferencing)
115 {
116 	// nothing else
117 }
118 
GeoreferencingDialog(QWidget * parent,MapEditorController * controller,Map * map,const Georeferencing * initial,bool allow_no_georeferencing)119 GeoreferencingDialog::GeoreferencingDialog(
120         QWidget* parent,
121         MapEditorController* controller,
122         Map* map,
123         const Georeferencing* initial,
124         bool allow_no_georeferencing )
125  : QDialog(parent, Qt::WindowSystemMenuHint | Qt::WindowTitleHint)
126  , controller(controller)
127  , map(map)
128  , initial_georef(initial ? initial : &map->getGeoreferencing())
129  , georef(new Georeferencing(*initial_georef))
130  , allow_no_georeferencing(allow_no_georeferencing)
131  , tool_active(false)
132  , declination_query_in_progress(false)
133  , grivation_locked(!initial_georef->isValid() || initial_georef->getState() != Georeferencing::Normal)
134  , scale_factor_locked(grivation_locked)
135 {
136 	setWindowTitle(tr("Map Georeferencing"));
137 	setWindowModality(Qt::WindowModal);
138 
139 	// Create widgets
140 	auto map_crs_label = Util::Headline::create(tr("Map coordinate reference system"));
141 
142 	crs_selector = new CRSSelector(*georef, nullptr);
143 	crs_selector->addCustomItem(tr("- local -"), Georeferencing::Local);
144 
145 	status_label = new QLabel(tr("Status:"));
146 	status_field = new QLabel();
147 
148 	auto reference_point_label = Util::Headline::create(tr("Reference point"));
149 
150 	ref_point_button = new QPushButton(tr("&Pick on map"));
151 	int ref_point_button_width = ref_point_button->sizeHint().width();
152 	auto geographic_datum_label = new QLabel(tr("(Datum: WGS84)"));
153 	int geographic_datum_label_width = geographic_datum_label->sizeHint().width();
154 
155 	map_x_edit = Util::SpinBox::create<MapCoordF>(tr("mm"));
156 	map_y_edit = Util::SpinBox::create<MapCoordF>(tr("mm"));
157 	ref_point_button->setEnabled(controller);
158 	auto map_ref_layout = new QHBoxLayout();
159 	map_ref_layout->addWidget(map_x_edit, 1);
160 	map_ref_layout->addWidget(new QLabel(tr("X", "x coordinate")), 0);
161 	map_ref_layout->addWidget(map_y_edit, 1);
162 	map_ref_layout->addWidget(new QLabel(tr("Y", "y coordinate")), 0);
163 	if (ref_point_button_width < geographic_datum_label_width)
164 		map_ref_layout->addSpacing(geographic_datum_label_width - ref_point_button_width);
165 	map_ref_layout->addWidget(ref_point_button, 0);
166 
167 	easting_edit = Util::SpinBox::create<Util::RealMeters>(tr("m"));
168 	northing_edit = Util::SpinBox::create<Util::RealMeters>(tr("m"));
169 	auto projected_ref_layout = new QHBoxLayout();
170 	projected_ref_layout->addWidget(easting_edit, 1);
171 	projected_ref_layout->addWidget(new QLabel(tr("E", "west / east")), 0);
172 	projected_ref_layout->addWidget(northing_edit, 1);
173 	projected_ref_layout->addWidget(new QLabel(tr("N", "north / south")), 0);
174 	projected_ref_layout->addSpacing(qMax(ref_point_button_width, geographic_datum_label_width));
175 
176 	projected_ref_label = new QLabel();
177 	lat_edit = Util::SpinBox::create(8, -90.0, +90.0, Util::InputProperties<Util::RotationalDegrees>::unit());
178 	lon_edit = Util::SpinBox::create(8, -180.0, +180.0, Util::InputProperties<Util::RotationalDegrees>::unit());
179 	lon_edit->setWrapping(true);
180 	auto geographic_ref_layout = new QHBoxLayout();
181 	geographic_ref_layout->addWidget(lat_edit, 1);
182 	geographic_ref_layout->addWidget(new QLabel(tr("N", "north")), 0);
183 	geographic_ref_layout->addWidget(lon_edit, 1);
184 	geographic_ref_layout->addWidget(new QLabel(tr("E", "east")), 0);
185 	if (geographic_datum_label_width < ref_point_button_width)
186 		geographic_ref_layout->addSpacing(ref_point_button_width - geographic_datum_label_width);
187 	geographic_ref_layout->addWidget(geographic_datum_label, 0);
188 
189 	show_refpoint_label = new QLabel(tr("Show reference point in:"));
190 	link_label = new QLabel();
191 	link_label->setOpenExternalLinks(true);
192 
193 	keep_projected_radio = new QRadioButton(tr("Projected coordinates"));
194 	keep_geographic_radio = new QRadioButton(tr("Geographic coordinates"));
195 	if (georef->getState() == Georeferencing::Normal && georef->isValid())
196 	{
197 		keep_geographic_radio->setChecked(true);
198 	}
199 	else
200 	{
201 		keep_geographic_radio->setEnabled(false);
202 		keep_projected_radio->setCheckable(true);
203 	}
204 
205 	auto map_north_label = Util::Headline::create(tr("Map north"));
206 
207 	declination_edit = Util::SpinBox::create<Util::RotationalDegrees>();
208 	declination_button = new QPushButton(tr("Lookup..."));
209 	auto declination_layout = new QHBoxLayout();
210 	declination_layout->addWidget(declination_edit, 1);
211 	declination_layout->addWidget(declination_button, 0);
212 
213 	grivation_label = new QLabel();
214 
215 	show_scale_check = new QCheckBox(tr("Show scale factors"));
216 	auto scale_compensation_label = Util::Headline::create(tr("Scale compensation"));
217 
218 	/*: The combined scale factor is the ratio between a length on the ground
219 	    and the corresponding length on the curved earth model. It is applied
220 	    as a factor to ground distances to get grid plane distances. */
221 	auto combined_factor_label = new QLabel(tr("Combined scale factor:"));
222 	combined_factor_display = new QLabel();
223 
224 	/*: The auxiliary scale factor is the ratio between a length in the curved
225 	    earth model and the corresponding length on the ground. It is applied
226 	    as a factor to ground distances to get curved earth model distances. */
227 	auto auxiliary_factor_label = new QLabel(tr("Auxiliary scale factor:"));
228 	scale_factor_edit = Util::SpinBox::create(Georeferencing::scaleFactorPrecision(), 0.001, 1000.0);
229 	scale_widget_list = {
230 		scale_compensation_label,
231 		auxiliary_factor_label, scale_factor_edit,
232 		combined_factor_label, combined_factor_display
233 	};
234 
235 	buttons_box = new QDialogButtonBox(
236 	  QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Reset | QDialogButtonBox::Help,
237 	  Qt::Horizontal);
238 	reset_button = buttons_box->button(QDialogButtonBox::Reset);
239 	reset_button->setEnabled(initial);
240 	auto help_button = buttons_box->button(QDialogButtonBox::Help);
241 
242 	auto edit_layout = new QFormLayout();
243 
244 	edit_layout->addRow(map_crs_label);
245 	edit_layout->addRow(tr("&Coordinate reference system:"), crs_selector);
246 	crs_selector->setDialogLayout(edit_layout);
247 	edit_layout->addRow(status_label, status_field);
248 	edit_layout->addItem(Util::SpacerItem::create(this));
249 
250 	edit_layout->addRow(reference_point_label);
251 	edit_layout->addRow(tr("Map coordinates:"), map_ref_layout);
252 	edit_layout->addRow(projected_ref_label, projected_ref_layout);
253 	edit_layout->addRow(tr("Geographic coordinates:"), geographic_ref_layout);
254 	edit_layout->addRow(show_refpoint_label, link_label);
255 	edit_layout->addRow(show_refpoint_label, link_label);
256 	edit_layout->addRow(tr("On CRS changes, keep:"), keep_projected_radio);
257 	edit_layout->addRow({}, keep_geographic_radio);
258 	edit_layout->addItem(Util::SpacerItem::create(this));
259 
260 	edit_layout->addRow(map_north_label);
261 	edit_layout->addRow(tr("Declination:"), declination_layout);
262 	edit_layout->addRow(tr("Grivation:"), grivation_label);
263 
264 	bool control_scale_factor = Settings::getInstance().getSetting(Settings::MapGeoreferencing_ControlScaleFactor).toBool();
265 	edit_layout->addItem(Util::SpacerItem::create(this));
266 	edit_layout->addRow(show_scale_check);
267 	edit_layout->addRow(scale_compensation_label);
268 	edit_layout->addRow(auxiliary_factor_label, scale_factor_edit);
269 	edit_layout->addRow(combined_factor_label, combined_factor_display);
270 	show_scale_check->setChecked(control_scale_factor);
271 	for (auto scale_widget: scale_widget_list)
272 		scale_widget->setVisible(control_scale_factor);
273 
274 	auto layout = new QVBoxLayout();
275 	layout->addLayout(edit_layout);
276 	layout->addStretch();
277 	layout->addSpacing(16);
278 	layout->addWidget(buttons_box);
279 
280 	setLayout(layout);
281 
282 	connect(crs_selector, &CRSSelector::crsChanged, this, &GeoreferencingDialog::crsEdited);
283 
284 	connect(show_scale_check, &QAbstractButton::clicked, this, &GeoreferencingDialog::showScaleChanged);
285 	connect(scale_factor_edit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &GeoreferencingDialog::auxiliaryFactorEdited);
286 
287 	connect(map_x_edit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &GeoreferencingDialog::mapRefChanged);
288 	connect(map_y_edit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &GeoreferencingDialog::mapRefChanged);
289 	connect(ref_point_button, &QPushButton::clicked, this, &GeoreferencingDialog::selectMapRefPoint);
290 
291 	connect(easting_edit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &GeoreferencingDialog::eastingNorthingEdited);
292 	connect(northing_edit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &GeoreferencingDialog::eastingNorthingEdited);
293 
294 	connect(lat_edit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &GeoreferencingDialog::latLonEdited);
295 	connect(lon_edit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &GeoreferencingDialog::latLonEdited);
296 	connect(keep_geographic_radio, &QRadioButton::toggled, this, &GeoreferencingDialog::keepCoordsChanged);
297 
298 	connect(declination_edit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &GeoreferencingDialog::declinationEdited);
299 	connect(declination_button, &QPushButton::clicked, this, &GeoreferencingDialog::requestDeclination);
300 
301 	connect(buttons_box, &QDialogButtonBox::accepted, this, &GeoreferencingDialog::accept);
302 	connect(buttons_box, &QDialogButtonBox::rejected, this, &GeoreferencingDialog::reject);
303 	connect(reset_button, &QPushButton::clicked, this, &GeoreferencingDialog::reset);
304 	connect(help_button, &QPushButton::clicked, this, &GeoreferencingDialog::showHelp);
305 
306 	connect(georef.data(), &Georeferencing::stateChanged, this, &GeoreferencingDialog::georefStateChanged);
307 	connect(georef.data(), &Georeferencing::transformationChanged, this, &GeoreferencingDialog::transformationChanged);
308 	connect(georef.data(), &Georeferencing::projectionChanged, this, &GeoreferencingDialog::projectionChanged);
309 	connect(georef.data(), &Georeferencing::declinationChanged, this, &GeoreferencingDialog::declinationChanged);
310 	connect(georef.data(), &Georeferencing::auxiliaryScaleFactorChanged, this, &GeoreferencingDialog::auxiliaryFactorChanged);
311 
312 	transformationChanged();
313 	georefStateChanged();
314 	declinationChanged();
315 	auxiliaryFactorChanged();
316 }
317 
~GeoreferencingDialog()318 GeoreferencingDialog::~GeoreferencingDialog()
319 {
320 	if (tool_active)
321 		controller->setOverrideTool(nullptr);
322 }
323 
324 // slot
georefStateChanged()325 void GeoreferencingDialog::georefStateChanged()
326 {
327 	const QSignalBlocker block(crs_selector);
328 
329 	switch (georef->getState())
330 	{
331 	case Georeferencing::Local:
332 		crs_selector->setCurrentItem(Georeferencing::Local);
333 		keep_geographic_radio->setEnabled(false);
334 		keep_projected_radio->setChecked(true);
335 		break;
336 	default:
337 		qDebug() << "Unhandled georeferencing state:" << georef->getState();
338 		Q_FALLTHROUGH();
339 	case Georeferencing::Normal:
340 		projectionChanged();
341 		keep_geographic_radio->setEnabled(true);
342 	}
343 
344 	updateWidgets();
345 }
346 
347 // slot
transformationChanged()348 void GeoreferencingDialog::transformationChanged()
349 {
350 	ScopedMultiSignalsBlocker block(
351 	            map_x_edit, map_y_edit,
352 	            easting_edit, northing_edit,
353 	            scale_factor_edit
354 	);
355 
356 	setValueIfChanged(map_x_edit, georef->getMapRefPoint().x());
357 	setValueIfChanged(map_y_edit, -georef->getMapRefPoint().y());
358 
359 	setValueIfChanged(easting_edit, georef->getProjectedRefPoint().x());
360 	setValueIfChanged(northing_edit, georef->getProjectedRefPoint().y());
361 
362 	setValueIfChanged(scale_factor_edit, georef->getAuxiliaryScaleFactor());
363 
364 	updateGrivation();
365 	updateCombinedFactor();
366 }
367 
368 // slot
projectionChanged()369 void GeoreferencingDialog::projectionChanged()
370 {
371 	ScopedMultiSignalsBlocker block(
372 	            crs_selector,
373 	            lat_edit, lon_edit
374 	);
375 
376 	if (georef->getState() == Georeferencing::Normal)
377 	{
378 		const std::vector< QString >& parameters = georef->getProjectedCRSParameters();
379 		auto temp = CRSTemplateRegistry().find(georef->getProjectedCRSId());
380 		if (!temp || temp->parameters().size() != parameters.size())
381 		{
382 			// The CRS id is not there anymore or the number of parameters has changed.
383 			// Enter as custom spec.
384 			crs_selector->setCurrentCRS(CRSTemplateRegistry().find(QString::fromLatin1("PROJ.4")), { georef->getProjectedCRSSpec() });
385 		}
386 		else
387 		{
388 			crs_selector->setCurrentCRS(temp, parameters);
389 		}
390 	}
391 
392 	LatLon latlon = georef->getGeographicRefPoint();
393 	double latitude  = latlon.latitude();
394 	double longitude = latlon.longitude();
395 	setValueIfChanged(lat_edit, latitude);
396 	setValueIfChanged(lon_edit, longitude);
397 	QString osm_link =
398 	  QString::fromLatin1("http://www.openstreetmap.org/?lat=%1&lon=%2&zoom=18&layers=M").
399 	  arg(latitude).arg(longitude);
400 	QString worldofo_link =
401 	  QString::fromLatin1("http://maps.worldofo.com/?zoom=15&lat=%1&lng=%2").
402 	  arg(latitude).arg(longitude);
403 	link_label->setText(
404 	  tr("<a href=\"%1\">OpenStreetMap</a> | <a href=\"%2\">World of O Maps</a>").
405 	  arg(osm_link, worldofo_link)
406 	);
407 
408 	QString error = georef->getErrorText();
409 	if (error.length() == 0)
410 		status_field->setText(tr("valid"));
411 	else
412 		status_field->setText(QLatin1String("<b style=\"color:red\">") + error + QLatin1String("</b>"));
413 }
414 
415 // slot
declinationChanged()416 void GeoreferencingDialog::declinationChanged()
417 {
418 	const QSignalBlocker block(declination_edit);
419 	setValueIfChanged(declination_edit, georef->getDeclination());
420 }
421 
422 // slot
auxiliaryFactorChanged()423 void GeoreferencingDialog::auxiliaryFactorChanged()
424 {
425 	const QSignalBlocker block(scale_factor_edit);
426 	setValueIfChanged(scale_factor_edit, georef->getAuxiliaryScaleFactor());
427 	updateCombinedFactor();
428 }
429 
requestDeclination(bool no_confirm)430 void GeoreferencingDialog::requestDeclination(bool no_confirm)
431 {
432 	if (georef->isLocal())
433 		return;
434 
435 	/// \todo Move URL (template) to settings.
436 	QString user_url(QString::fromLatin1("https://www.ngdc.noaa.gov/geomag-web/"));
437 	QUrl service_url(user_url + QLatin1String("calculators/calculateDeclination"));
438 	LatLon latlon(georef->getGeographicRefPoint());
439 
440 	if (!no_confirm)
441 	{
442 		int result = QMessageBox::question(this, tr("Online declination lookup"),
443 		  trUtf8("The magnetic declination for the reference point %1° %2° will now be retrieved from <a href=\"%3\">%3</a>. Do you want to continue?").
444 		    arg(latlon.latitude()).arg(latlon.longitude()).arg(user_url),
445 		  QMessageBox::Yes | QMessageBox::No,
446 		  QMessageBox::Yes );
447 		if (result != QMessageBox::Yes)
448 			return;
449 	}
450 
451 	QUrlQuery query;
452 	QDate today = QDate::currentDate();
453 	query.addQueryItem(QString::fromLatin1("lat1"), QString::number(latlon.latitude()));
454 	query.addQueryItem(QString::fromLatin1("lon1"), QString::number(latlon.longitude()));
455 	query.addQueryItem(QString::fromLatin1("startYear"), QString::number(today.year()));
456 	query.addQueryItem(QString::fromLatin1("startMonth"), QString::number(today.month()));
457 	query.addQueryItem(QString::fromLatin1("startDay"), QString::number(today.day()));
458 
459 #if defined(Q_OS_WIN) || defined(Q_OS_MACOS) || defined(Q_OS_ANDROID) || !defined(QT_NETWORK_LIB)
460 	// No QtNetwork or no OpenSSL: open result in system browser.
461 	query.addQueryItem(QString::fromLatin1("resultFormat"), QString::fromLatin1("html"));
462 	service_url.setQuery(query);
463 	QDesktopServices::openUrl(service_url);
464 #else
465 	// Use result directly
466 	query.addQueryItem(QString::fromLatin1("resultFormat"), QString::fromLatin1("xml"));
467 	service_url.setQuery(query);
468 
469 	declination_query_in_progress = true;
470 	updateDeclinationButton();
471 
472 	auto network = new QNetworkAccessManager(this);
473 	connect(network, &QNetworkAccessManager::finished, this, &GeoreferencingDialog::declinationReplyFinished);
474 	network->get(QNetworkRequest(service_url));
475 #endif
476 }
477 
setMapRefPoint(const MapCoord & coords)478 void GeoreferencingDialog::setMapRefPoint(const MapCoord& coords)
479 {
480 	georef->setMapRefPoint(coords);
481 	reset_button->setEnabled(true);
482 }
483 
setKeepProjectedRefCoords()484 void GeoreferencingDialog::setKeepProjectedRefCoords()
485 {
486 	keep_projected_radio->setChecked(true);
487 	reset_button->setEnabled(true);
488 }
489 
setKeepGeographicRefCoords()490 void GeoreferencingDialog::setKeepGeographicRefCoords()
491 {
492 	keep_geographic_radio->setChecked(true);
493 	reset_button->setEnabled(true);
494 }
495 
toolDeleted()496 void GeoreferencingDialog::toolDeleted()
497 {
498 	tool_active = false;
499 }
500 
showHelp()501 void GeoreferencingDialog::showHelp()
502 {
503 	Util::showHelp(parentWidget(), "georeferencing.html");
504 }
505 
reset()506 void GeoreferencingDialog::reset()
507 {
508 	scale_factor_locked = grivation_locked = ( !initial_georef->isValid() || initial_georef->getState() != Georeferencing::Normal );
509 	*georef.data() = *initial_georef;
510 	reset_button->setEnabled(false);
511 }
512 
accept()513 void GeoreferencingDialog::accept()
514 {
515 	auto const declination_change_degrees = georef->getDeclination() - initial_georef->getDeclination();
516 	auto const scale_factor_change = georef->getAuxiliaryScaleFactor() / initial_georef->getAuxiliaryScaleFactor();
517 	if (grivation_locked)
518 	{
519 		georef->updateGrivation();
520 	}
521 	else if (!qIsNull(declination_change_degrees)
522 	         && (map->getNumObjects() > 0 || map->getNumTemplates() > 0))
523 	{
524 		int result = QMessageBox::question(this, tr("Declination change"), tr("The declination has been changed. Do you want to rotate the map content accordingly, too?"), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
525 		if (result == QMessageBox::Cancel)
526 		{
527 			return;
528 		}
529 		else if (result == QMessageBox::Yes)
530 		{
531 			RotateMapDialog dialog(this, map);
532 			dialog.setWindowModality(Qt::WindowModal);
533 			dialog.setRotationDegrees(declination_change_degrees);
534 			dialog.setRotateAroundGeorefRefPoint();
535 			dialog.setAdjustDeclination(false);
536 			dialog.showAdjustDeclination(false);
537 			int result = dialog.exec();
538 			if (result == QDialog::Rejected)
539 				return;
540 		}
541 	}
542 	if (scale_factor_locked)
543 	{
544 		georef->updateCombinedScaleFactor();
545 	}
546 	else if (!qIsNull(std::log(scale_factor_change))
547 	         && (map->getNumObjects() > 0 || map->getNumTemplates() > 0))
548 	{
549 		int result = QMessageBox::question(this, tr("Scale factor change"), tr("The scale factor has been changed. Do you want to stretch/shrink the map content accordingly, too?"), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
550 		if (result == QMessageBox::Cancel)
551 		{
552 			return;
553 		}
554 		else if (result == QMessageBox::Yes)
555 		{
556 			StretchMapDialog dialog(this, map, 1.0/scale_factor_change);
557 			dialog.setWindowModality(Qt::WindowModal);
558 			int result = dialog.exec();
559 			if (result == QDialog::Rejected)
560 				return;
561 		}
562 	}
563 
564 	map->setGeoreferencing(*georef);
565 	QDialog::accept();
566 }
567 
updateWidgets()568 void GeoreferencingDialog::updateWidgets()
569 {
570 	ref_point_button->setEnabled(controller);
571 
572 	if (crs_selector->currentCRSTemplate())
573 		projected_ref_label->setText(crs_selector->currentCRSTemplate()->coordinatesName(crs_selector->parameters()) + QLatin1Char(':'));
574 	else
575 		projected_ref_label->setText(tr("Local coordinates:"));
576 
577 	bool geographic_coords_enabled = crs_selector->currentCustomItem() != Georeferencing::Local;
578 	status_label->setVisible(geographic_coords_enabled);
579 	status_field->setVisible(geographic_coords_enabled);
580 	lat_edit->setEnabled(geographic_coords_enabled);
581 	lon_edit->setEnabled(geographic_coords_enabled);
582 	link_label->setEnabled(geographic_coords_enabled);
583 	//keep_geographic_radio->setEnabled(geographic_coords_enabled);
584 
585 	updateDeclinationButton();
586 
587 	buttons_box->button(QDialogButtonBox::Ok)->setEnabled(georef->isValid());
588 }
589 
updateDeclinationButton()590 void GeoreferencingDialog::updateDeclinationButton()
591 {
592 	/*
593 	bool dialog_enabled = crs_edit->getSelectedCustomItemId() != 0;
594 	bool proj_spec_visible = crs_edit->getSelectedCustomItemId() == 1;
595 	bool geographic_coords_enabled =
596 		dialog_enabled &&
597 		(proj_spec_visible ||
598 		 crs_edit->getSelectedCustomItemId() == -1);
599 	*/
600 	bool enabled = lat_edit->isEnabled() && !declination_query_in_progress;
601 	declination_button->setEnabled(enabled);
602 	declination_button->setText(declination_query_in_progress ? tr("Loading...") : tr("Lookup..."));
603 }
604 
updateCombinedFactor()605 void GeoreferencingDialog::updateCombinedFactor()
606 {
607 	QString text = trUtf8("%1", "scale factor value").arg(QLocale().toString(georef->getCombinedScaleFactor(), 'f', Georeferencing::scaleFactorPrecision()));
608 	if (scale_factor_locked)
609 		text.append(QString::fromLatin1(" (%1)").arg(tr("locked")));
610 	combined_factor_display->setText(text);
611 }
612 
updateGrivation()613 void GeoreferencingDialog::updateGrivation()
614 {
615 	QString text = trUtf8("%1 °", "degree value").arg(QLocale().toString(georef->getGrivation(), 'f', Georeferencing::declinationPrecision()));
616 	if (grivation_locked)
617 		text.append(QString::fromLatin1(" (%1)").arg(tr("locked")));
618 	grivation_label->setText(text);
619 }
620 
crsEdited()621 void GeoreferencingDialog::crsEdited()
622 {
623 	Georeferencing georef_copy = *georef;
624 
625 	auto crs_template = crs_selector->currentCRSTemplate();
626 	auto spec = crs_selector->currentCRSSpec();
627 
628 	auto selected_item_id = crs_selector->currentCustomItem();
629 	switch (selected_item_id)
630 	{
631 	default:
632 		qWarning("Unsupported CRS item id");
633 		Q_FALLTHROUGH();
634 	case Georeferencing::Local:
635 		// Local
636 		georef_copy.setState(Georeferencing::Local);
637 		grivation_locked = true;
638 		updateGrivation();
639 		scale_factor_locked = true;
640 		updateCombinedFactor();
641 		break;
642 	case -1:
643 		// CRS from list
644 		Q_ASSERT(crs_template);
645 		georef_copy.setProjectedCRS(crs_template->id(), spec, crs_selector->parameters());
646 		georef_copy.setState(Georeferencing::Normal); // Allow invalid spec
647 		if (keep_geographic_radio->isChecked())
648 			georef_copy.setGeographicRefPoint(georef->getGeographicRefPoint(), !grivation_locked, !scale_factor_locked);
649 		else
650 			georef_copy.setProjectedRefPoint(georef->getProjectedRefPoint(), !grivation_locked, !scale_factor_locked);
651 		break;
652 	}
653 
654 	// Apply all changes at once
655 	*georef = georef_copy;
656 	reset_button->setEnabled(true);
657 }
658 
showScaleChanged(bool checked)659 void GeoreferencingDialog::showScaleChanged(bool checked)
660 {
661 	Settings::getInstance().setSetting(Settings::MapGeoreferencing_ControlScaleFactor, checked);
662 	for (auto scale_widget: scale_widget_list)
663 		scale_widget->setVisible(checked);
664 }
665 
auxiliaryFactorEdited(double value)666 void GeoreferencingDialog::auxiliaryFactorEdited(double value)
667 {
668 	if (scale_factor_locked)
669 	{
670 		scale_factor_locked = false;
671 		updateCombinedFactor();
672 	}
673 	georef->setAuxiliaryScaleFactor(value);
674 	reset_button->setEnabled(true);
675 }
676 
selectMapRefPoint()677 void GeoreferencingDialog::selectMapRefPoint()
678 {
679 	if (controller)
680 	{
681 		controller->setOverrideTool(new GeoreferencingTool(this, controller));
682 		tool_active = true;
683 		hide();
684 	}
685 }
686 
mapRefChanged()687 void GeoreferencingDialog::mapRefChanged()
688 {
689 	MapCoord coord(map_x_edit->value(), -1 * map_y_edit->value());
690 	setMapRefPoint(coord);
691 }
692 
eastingNorthingEdited()693 void GeoreferencingDialog::eastingNorthingEdited()
694 {
695 	const QSignalBlocker block1(keep_geographic_radio), block2(keep_projected_radio);
696 	double easting   = easting_edit->value();
697 	double northing  = northing_edit->value();
698 	georef->setProjectedRefPoint(QPointF(easting, northing), !grivation_locked, !scale_factor_locked);
699 	keep_projected_radio->setChecked(true);
700 	reset_button->setEnabled(true);
701 }
702 
latLonEdited()703 void GeoreferencingDialog::latLonEdited()
704 {
705 	const QSignalBlocker block1(keep_geographic_radio), block2(keep_projected_radio);
706 	double latitude  = lat_edit->value();
707 	double longitude = lon_edit->value();
708 	georef->setGeographicRefPoint(LatLon(latitude, longitude), !grivation_locked, !scale_factor_locked);
709 	keep_geographic_radio->setChecked(true);
710 	reset_button->setEnabled(true);
711 }
712 
keepCoordsChanged()713 void GeoreferencingDialog::keepCoordsChanged()
714 {
715 	if (keep_geographic_radio->isChecked())
716 	{
717 		if (grivation_locked)
718 		{
719 			grivation_locked = false;
720 			updateGrivation();
721 			georef->updateGrivation();
722 		}
723 		if (scale_factor_locked)
724 		{
725 			scale_factor_locked = false;
726 			updateCombinedFactor();
727 			georef->updateCombinedScaleFactor();
728 		}
729 	}
730 	reset_button->setEnabled(true);
731 }
732 
declinationEdited(double value)733 void GeoreferencingDialog::declinationEdited(double value)
734 {
735 	if (grivation_locked)
736 	{
737 		grivation_locked = false;
738 		updateGrivation();
739 	}
740 	georef->setDeclination(value);
741 	reset_button->setEnabled(true);
742 }
743 
declinationReplyFinished(QNetworkReply * reply)744 void GeoreferencingDialog::declinationReplyFinished(QNetworkReply* reply)
745 {
746 #if defined(QT_NETWORK_LIB)
747 	declination_query_in_progress = false;
748 	updateDeclinationButton();
749 
750 	QString error_string;
751 	if (reply->error() != QNetworkReply::NoError)
752 	{
753 		error_string = reply->errorString();
754 	}
755 	else
756 	{
757 		QXmlStreamReader xml(reply);
758 		while (xml.readNextStartElement())
759 		{
760 			if (xml.name() == QLatin1String("maggridresult"))
761 			{
762 				while(xml.readNextStartElement())
763 				{
764 					if (xml.name() == QLatin1String("result"))
765 					{
766 						while (xml.readNextStartElement())
767 						{
768 							if (xml.name() == QLatin1String("declination"))
769 							{
770 								QString text = xml.readElementText(QXmlStreamReader::IncludeChildElements);
771 								bool ok;
772 								double declination = text.toDouble(&ok);
773 								if (ok)
774 								{
775 									setValueIfChanged(declination_edit, Georeferencing::roundDeclination(declination));
776 									return;
777 								}
778 								else
779 								{
780 									error_string = tr("Could not parse data.") + QLatin1Char(' ');
781 								}
782 							}
783 
784 							xml.skipCurrentElement(); // child of result
785 						}
786 					}
787 
788 					xml.skipCurrentElement(); // child of mapgridresult
789 				}
790 			}
791 			else if (xml.name() == QLatin1String("errors"))
792 			{
793 				error_string.append(xml.readElementText(QXmlStreamReader::IncludeChildElements) + QLatin1Char(' '));
794 			}
795 
796 			xml.skipCurrentElement(); // child of root
797 		}
798 
799 		if (xml.error() != QXmlStreamReader::NoError)
800 		{
801 			error_string.append(xml.errorString());
802 		}
803 		else if (error_string.isEmpty())
804 		{
805 			error_string = tr("Declination value not found.");
806 		}
807 	}
808 
809 	int result = QMessageBox::critical(this, tr("Online declination lookup"),
810 		tr("The online declination lookup failed:\n%1").arg(error_string),
811 		QMessageBox::Retry | QMessageBox::Close,
812 		QMessageBox::Close );
813 	if (result == QMessageBox::Retry)
814 		requestDeclination(true);
815 #else
816 	Q_UNUSED(reply)
817 #endif
818 }
819 
820 
821 
822 // ### GeoreferencingTool ###
823 
GeoreferencingTool(GeoreferencingDialog * dialog,MapEditorController * controller,QAction * action)824 GeoreferencingTool::GeoreferencingTool(GeoreferencingDialog* dialog, MapEditorController* controller, QAction* action)
825  : MapEditorTool(controller, Other, action)
826  , dialog(dialog)
827 {
828 	// nothing
829 }
830 
~GeoreferencingTool()831 GeoreferencingTool::~GeoreferencingTool()
832 {
833 	dialog->toolDeleted();
834 }
835 
init()836 void GeoreferencingTool::init()
837 {
838 	setStatusBarText(tr("<b>Click</b>: Set the reference point. <b>Right click</b>: Cancel."));
839 	MapEditorTool::init();
840 }
841 
mousePressEvent(QMouseEvent * event,const MapCoordF &,MapWidget *)842 bool GeoreferencingTool::mousePressEvent(QMouseEvent* event, const MapCoordF& /*map_coord*/, MapWidget* /*widget*/)
843 {
844 	bool handled = false;
845 	switch (event->button())
846 	{
847 	case Qt::LeftButton:
848 	case Qt::RightButton:
849 		handled = true;
850 		break;
851 	default:
852 		; // nothing
853 	}
854 
855 	return handled;
856 }
857 
mouseReleaseEvent(QMouseEvent * event,const MapCoordF & map_coord,MapWidget *)858 bool GeoreferencingTool::mouseReleaseEvent(QMouseEvent* event, const MapCoordF& map_coord, MapWidget* /*widget*/)
859 {
860 	bool handled = false;
861 	switch (event->button())
862 	{
863 	case Qt::LeftButton:
864 		dialog->setMapRefPoint(MapCoord(map_coord));
865 		Q_FALLTHROUGH();
866 	case Qt::RightButton:
867 		QTimer::singleShot(0, dialog, &QDialog::exec);
868 		handled = true;
869 		break;
870 	default:
871 		; // nothing
872 	}
873 
874 	return handled;
875 }
876 
getCursor() const877 const QCursor& GeoreferencingTool::getCursor() const
878 {
879 	static auto const cursor = scaledToScreen(QCursor{ QPixmap{ QString::fromLatin1(":/images/cursor-crosshair.png") }, 11, 11 });
880 	return cursor;
881 }
882 
883 
884 }  // namespace OpenOrienteering
885