1 /*
2  * Copyright (c) 2005-2019 Libor Pecháček.
3  * Copyright 2020 Kai Pastor
4  *
5  * This file is part of CoVe
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Lesser 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  * This program 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 Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public License
18  * along with this program. If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "mainform.h"
22 #include "ui_mainform.h"
23 
24 // IWYU pragma: no_include <algorithm>
25 #include <cstdlib>
26 #include <ctime>
27 #include <iosfwd>
28 #include <iterator>
29 #include <utility>
30 
31 #include <QtGlobal>
32 #include <QByteArray>
33 #include <QColor>
34 #include <QComboBox>
35 #include <QDir>
36 #include <QException>
37 #include <QFileInfo>
38 #include <QFileDialog>
39 #include <QGridLayout>
40 #include <QGroupBox>
41 #include <QIcon>
42 #include <QImage>
43 #include <QImageReader>
44 #include <QLabel>
45 #include <QList>
46 #include <QMessageBox>
47 #include <QPainter>
48 #include <QPixmap>
49 #include <QPoint>
50 #include <QPointF>
51 #include <QPushButton>
52 #include <QSpinBox>
53 #include <QTabWidget>
54 #include <QUndoStack>
55 
56 #include "core/map.h"
57 #include "core/map_coord.h"
58 #include "core/map_part.h"
59 #include "core/objects/object.h"
60 #include "core/symbols/line_symbol.h"
61 #include "core/symbols/symbol.h"
62 #include "gui/widgets/symbol_dropdown.h"
63 #include "templates/template.h"
64 #include "templates/template_image.h"
65 #include "undo/object_undo.h"
66 
67 #include "libvectorizer/Concurrency.h"
68 #include "libvectorizer/FIRFilter.h"
69 #include "libvectorizer/Polygons.h"
70 #include "libvectorizer/Vectorizer.h"
71 
72 #include "ImageView.h"
73 #include "PolygonsView.h"
74 #include "Settings.h"
75 #include "UIProgressDialog.h"
76 #include "classificationconfigform.h"
77 #include "colorseditform.h"
78 #include "vectorizationconfigform.h"
79 
80 using namespace std;
81 namespace cove {
82 
83 class ProgressObserver;
84 
85 //@{
86 /*! \defgroup gui GUI classes */
87 
88 /*! \class mainForm
89   \brief Main form that is used as centralWidget() of QMainWindow.
90   */
91 
92 /*! \var Ui::mainForm mainForm::ui
93   User interface as created by Qt Designer.
94   */
95 
96 /*! \var UIProgressDialog* mainForm::progressDialog
97   Progress dialog pointer. Is 0 when there is no progress dialog currently
98   displayed.
99 */
100 
101 /*! \var Vectorizer* mainForm::vectorizerApp
102   Vectorizer application.
103   \sa Vectorizer
104 */
105 
106 /*! \var QString mainForm::imageFileName
107   Filename of the currently loaded image.
108 */
109 
110 /*! \var QImage mainForm::imageBitmap
111   The currently loaded image.
112 */
113 
114 /*! \var QImage mainForm::classifiedBitmap
115   The classified image on the colors tab.
116 */
117 
118 /*! \var QImage mainForm::bwBitmap
119   The BW image on the thinning tab.
120 */
121 
122 /*! \var bool mainForm::rollbackHistory
123   Boolean indicating whether the history was committed.
124   \sa prepareBWImageHistory, commitBWImageHistory
125 */
126 
127 /*! \var QList<QImage> mainForm::bwBitmapHistory
128   Container for all the images in the history.
129 */
130 
131 /*! \var QList<QImage>::iterator mainForm::bwBitmapHistoryIterator
132   Pointer to \a bwBitmapHistory.
133 */
134 
135 /*! \var int mainForm::numberOfColorButtons
136   Number of color buttons in the ColorButtonsGroup on the Colors tab.
137   \sa colorButtons
138 */
139 
140 /*! \var QPushButton** mainForm::colorButtons
141   Array of pointers to color buttons in the ColorButtonsGroup on the Colors tab.
142   \sa numberOfColorButtons
143 */
144 
145 /*! \var Settings mainForm::settings
146   Current status data (configuration variables).
147 */
148 
149 /*! \var QDoubleValidator mainForm::positiveDoubleValid
150   Input validator for LineEdit.
151 */
152 
153 //! Constructor initializing the menu, actions and connecting signals from
154 //! actions
mainForm(QWidget * parent,OpenOrienteering::Map * map,OpenOrienteering::TemplateImage * templ,Qt::WindowFlags flags)155 mainForm::mainForm(QWidget* parent, OpenOrienteering::Map* map,
156 				   OpenOrienteering::TemplateImage* templ,
157 				   Qt::WindowFlags flags)
158 	: QDialog(parent, flags)
159 	, ooMap(map)
160 	, ooTempl(templ)
161 {
162 	ui.setupUi(this);
163 
164 	vectorizerApp = nullptr;
165 
166 	setTabEnabled(ui.imageTab, true);
167 	setTabEnabled(ui.thinningTab, false);
168 	setTabEnabled(ui.colorsTab, false);
169 
170 	ui.howManyColorsSpinBox->setValue(settings.getInt("nColors"));
171 
172 	bwBitmapUndo = new QUndoStack(this);
173 	connect(bwBitmapUndo, SIGNAL(canUndoChanged(bool)), ui.bwImageHistoryBack, SLOT(setEnabled(bool)));
174 	connect(bwBitmapUndo, SIGNAL(canRedoChanged(bool)), ui.bwImageHistoryForward, SLOT(setEnabled(bool)));
175 
176 	loadImage(templ->getImage(), templ->getTemplateFilename());
177 
178 	ui.symbolComboBox->init(map, OpenOrienteering::Symbol::Line);
179 	ui.symbolComboBox->setCurrentIndex(std::min(ui.symbolComboBox->count() - 1, 1));
180 }
181 
~mainForm()182 mainForm::~mainForm()
183 {
184 	clearColorButtonsGroup();
185 }
186 
187 //! Clears the Thinning tab, i.e. removes displayed image and polygons
clearBWImageTab()188 void mainForm::clearBWImageTab()
189 {
190 	bwBitmapUndo->clear();
191 	bwBitmapVectorizable = false;
192 	ui.bwImageView->setPolygons(PolygonList());
193 	ui.saveVectorsButton->setEnabled(false);
194 	ui.bwImageView->setImage(nullptr);
195 }
196 
197 //! Clears the Colors tab, i.e. removes displayed image and color buttons
clearColorsTab()198 void mainForm::clearColorsTab()
199 {
200 	if (!imageBitmap.isNull())
201 	{
202 		ui.classifiedColorsView->setImage(&imageBitmap);
203 	}
204 	else
205 	{
206 		ui.classifiedColorsView->setImage(nullptr);
207 	}
208 	clearColorButtonsGroup();
209 }
210 
211 //! Loads image into form.
loadImage(const QImage & imageToLoad,const QString & imageName)212 void mainForm::loadImage(const QImage& imageToLoad, const QString& imageName)
213 {
214 	imageFileName = imageName;
215 
216 	// no alpha channel desired in the vectorized image
217 	// we also want to have white background to mimic Mapper look and feel
218 	imageBitmap = QImage(imageToLoad.size(), QImage::Format_RGB32);
219 	imageBitmap.fill(Qt::white);
220 	QPainter painter(&imageBitmap);
221 	painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
222 	painter.drawImage(0, 0, imageToLoad);
223 	painter.end();
224 
225 	afterLoadImage();
226 }
227 
228 //! Performs form modifications after image load.
afterLoadImage()229 void mainForm::afterLoadImage()
230 {
231 	ui.mainTabWidget->setCurrentIndex(ui.mainTabWidget->indexOf(ui.imageTab));
232 	setTabEnabled(ui.thinningTab, false);
233 	clearBWImageTab();
234 	setTabEnabled(ui.colorsTab, false);
235 	clearColorsTab();
236 	ui.imageView->setImage(&imageBitmap);
237 	ui.imageView->reset();
238 	ui.classifiedColorsView->setImage(&imageBitmap);
239 	ui.classifiedColorsView->reset();
240 
241 	if (!imageBitmap.isNull())
242 	{
243 		if (imageBitmap.format() == QImage::Format_Mono ||
244 			imageBitmap.format() == QImage::Format_MonoLSB)
245 		{
246 			setTabEnabled(ui.thinningTab, true);
247 		}
248 		setTabEnabled(ui.colorsTab, true);
249 	}
250 }
251 
252 //! Simple About dialog.
aboutDialog()253 void mainForm::aboutDialog()
254 {
255 	QMessageBox::about(this, tr("About"),
256 					   trUtf8("<h3>COntour VEctorizer</h3><br />"
257 							  "Author: Libor Pecháček<br />"
258 							  "License: GPL"));
259 }
260 
261 /*! Return color randomly created from image pixels.  It takes 5 scanlines and
262  * makes an average color of its pixels. */
getColorFromImage(const QImage & image)263 QRgb mainForm::getColorFromImage(const QImage& image)
264 {
265 	srand(time(nullptr));  // NOLINT
266 	unsigned long r, g, b, divisor;
267 	r = g = b = divisor = 0;
268 	for (int a = 0; a < 5; a++)
269 	{
270 		int line = rand() % image.height();  // NOLINT
271 		int w = image.width();
272 		for (int x = 0; x < w; x++)
273 		{
274 			QRgb c = image.pixel(x, line);
275 			r += qRed(c);
276 			g += qGreen(c);
277 			b += qBlue(c);
278 			divisor++;
279 		}
280 	}
281 	return qRgb(r / divisor, g / divisor, b / divisor);
282 }
283 
284 /*! Performs actions requested by clicking 'Now' button on Colors tab.
285   \sa classificationFinished() */
on_runClassificationButton_clicked()286 void mainForm::on_runClassificationButton_clicked()
287 {
288 	ui.runClassificationButton->setEnabled(false);
289 	vectorizerApp = std::make_unique<Vectorizer>(imageBitmap);
290 	UIProgressDialog progressDialog(tr("Colors classification in progress"),
291 	                                tr("Cancel"), this);
292 	auto totalProgress = Concurrency::TransformedProgress{progressDialog, 0.5};
293 	switch (settings.getInt("learnMethod"))
294 	{
295 	case 0:
296 		vectorizerApp->setClassificationMethod(Vectorizer::KOHONEN_CLASSIC);
297 		break;
298 	case 1:
299 		vectorizerApp->setClassificationMethod(Vectorizer::KOHONEN_BATCH);
300 		break;
301 	}
302 	vectorizerApp->setP(settings.getDouble("p"));
303 	switch (settings.getInt("colorSpace"))
304 	{
305 	case 0:
306 		vectorizerApp->setColorSpace(Vectorizer::COLSPC_RGB);
307 		break;
308 	case 1:
309 		vectorizerApp->setColorSpace(Vectorizer::COLSPC_HSV);
310 		break;
311 	}
312 	switch (settings.getInt("alphaStrategy"))
313 	{
314 	case 0:
315 		vectorizerApp->setAlphaStrategy(Vectorizer::ALPHA_CLASSIC);
316 		break;
317 	case 1:
318 		vectorizerApp->setAlphaStrategy(Vectorizer::ALPHA_NEUQUANT);
319 		break;
320 	}
321 	switch (settings.getInt("patternStrategy"))
322 	{
323 	case 0:
324 		vectorizerApp->setPatternStrategy(Vectorizer::PATTERN_RANDOM);
325 		break;
326 	case 1:
327 		vectorizerApp->setPatternStrategy(Vectorizer::PATTERN_NEUQUANT);
328 		break;
329 	}
330 	vectorizerApp->setInitAlpha(settings.getDouble("initAlpha"));
331 	vectorizerApp->setMinAlpha(settings.getDouble("minAlpha"));
332 	vectorizerApp->setQ(settings.getDouble("q"));
333 	vectorizerApp->setE(settings.getInt("E"));
334 	int nColors = settings.getInt("nColors");
335 	vectorizerApp->setNumberOfColors(nColors);
336 	std::vector<QRgb> colors;
337 	switch (settings.getInt("initColorsSource"))
338 	{
339 	case ColorsEditForm::Random:
340 		break;
341 	case ColorsEditForm::RandomFromImage:
342 		colors.resize(nColors);
343 		for (auto& c : colors)
344 			c = getColorFromImage(imageBitmap);
345 		break;
346 	case ColorsEditForm::Predefined:
347 		colors = settings.getInitColors();
348 		break;
349 	}
350 	vectorizerApp->setInitColors(colors);
351 	auto classification = [](Vectorizer* v, ProgressObserver& o) -> bool {
352 		return v->performClassification(&o);
353 	};
354 	Concurrency::process<bool>(&totalProgress, classification, vectorizerApp.get());
355 
356 	auto colorsFound = vectorizerApp->getClassifiedColors();
357 	setColorButtonsGroup(colorsFound);
358 
359 	// in case there are some colors (learning was not aborted)
360 	if (colorsFound.size())
361 	{
362 		double quality;
363 		totalProgress.offset = 50.0;
364 		QImage newClassifiedBitmap =
365 			vectorizerApp->getClassifiedImage(&quality, &totalProgress);
366 		if (!newClassifiedBitmap.isNull())
367 		{
368 			classifiedBitmap = newClassifiedBitmap;
369 			ui.classifiedColorsView->setImage(&classifiedBitmap);
370 			ui.learnQualityLabel->setText(QString("LQ: %1").arg(quality));
371 		}
372 		else
373 		{
374 			clearColorButtonsGroup();
375 		}
376 	}
377 	else
378 	{
379 		ui.learnQualityLabel->setText(QString("LQ: -"));
380 	}
381 	ui.runClassificationButton->setEnabled(true);
382 }
383 
384 /*! Removes all color buttons from the frame on Colors tab.
385   \sa setColorButtonsGroup(QRgb* colors, int nColors) */
clearColorButtonsGroup()386 void mainForm::clearColorButtonsGroup()
387 {
388 	setTabEnabled(ui.thinningTab, false);
389 	for (auto button : colorButtons)
390 	{
391 		disconnect(button);
392 		ui.gridLayout->removeWidget(button);
393 		button->deleteLater();
394 	}
395 	colorButtons.clear();
396 }
397 
398 /*! Creates color buttons in the frame on Colors tab.
399   \sa clearColorButtonsGroup() */
setColorButtonsGroup(std::vector<QRgb> colors)400 void mainForm::setColorButtonsGroup(std::vector<QRgb> colors)
401 {
402 	clearColorButtonsGroup();
403 	auto nColors = colors.size();
404 	if (!nColors) return;
405 	colorButtons.resize(nColors + 1);
406 	unsigned i;
407 	for (i = 0; i < nColors; i++)
408 	{
409 		QPixmap buttonFace(20, 20);
410 		buttonFace.fill(QColor(colors[i]));
411 		QPushButton* button =
412 			new QPushButton(buttonFace, QString(), ui.colorButtonsGroup);
413 		ui.gridLayout->addWidget(button, i % 2, i / 2);
414 		colorButtons[i] = button;
415 		button->setCheckable(true);
416 		button->setMaximumSize(30, 30);
417 		button->setMinimumSize(10, 10);
418 		button->show();
419 		connect(button, SIGNAL(toggled(bool)), this,
420 				SLOT(colorButtonToggled(bool)));
421 	}
422 	QPushButton* button = new QPushButton(ui.colorButtonsGroup);
423 	ui.gridLayout->addWidget(button, i % 2, i / 2);
424 	colorButtons[i] = button;
425 	button->setMaximumSize(30, 30);
426 	button->setMinimumSize(10, 10);
427 	button->setToolTip(tr("Set these colors as initial."));
428 	button->setText(tr("IC"));
429 	button->show();
430 	connect(button, SIGNAL(clicked(bool)), this, SLOT(setInitialColors(bool)));
431 	ui.colorButtonsGroup->adjustSize();
432 }
433 
434 //! Performs action requested by clicking IC button in the color buttons frame,
435 // i.e. sets current colors as initial.
setInitialColors(bool)436 void mainForm::setInitialColors(bool /*on*/)
437 {
438 	auto colorsFound = vectorizerApp->getClassifiedColors();
439 	auto n = colorsFound.size();
440 	if (!n) return;
441 	auto comments = std::vector<QString>(n);
442 	settings.setInitColors(colorsFound, comments);
443 }
444 
445 //! Invoked by clicking a color button in the color buttons frame, changes the
446 // classified bitmap palette so that pixels of current selected colors appear in
447 // dark orange.
colorButtonToggled(bool)448 void mainForm::colorButtonToggled(bool /*on*/)
449 {
450 	int sels = 0;
451 	auto colorsFound = vectorizerApp->getClassifiedColors();
452 	auto selectedColors = getSelectedColors();
453 	for (std::size_t i = 0; i < colorButtons.size(); i++)
454 	{
455 		classifiedBitmap.setColor(
456 			i, selectedColors[i] ? (sels++, qRgb(255, 27, 0)) : colorsFound[i]);
457 	}
458 	setTabEnabled(ui.thinningTab, sels != 0);
459 	ui.classifiedColorsView->setImage(&classifiedBitmap);
460 	ui.classifiedColorsView->update();
461 }
462 
463 //! Returns bool array indicating which colors are selected by color buttons.
getSelectedColors()464 std::vector<bool> mainForm::getSelectedColors()
465 {
466 	auto n = vectorizerApp->getClassifiedColors().size();
467 	// do not allow different number of colors and buttons
468 	Q_ASSERT(colorButtons.size() >= n);
469 
470 	auto retval = std::vector<bool>(n);
471 	// copy states of the buttons into array
472 	for (std::size_t i = 0; i < colorButtons.size(); i++)
473 		retval[i] = colorButtons[i]->isChecked();
474 
475 	return retval;
476 }
477 
478 //! Performs generating the BW image on change to Thinning tab.
on_mainTabWidget_currentChanged(int tabindex)479 void mainForm::on_mainTabWidget_currentChanged(int tabindex)
480 {
481 	if (ui.mainTabWidget->widget(tabindex) != ui.thinningTab) return;
482 
483 	if ((imageBitmap.format() == QImage::Format_Mono ||
484 		 imageBitmap.format() == QImage::Format_MonoLSB) &&
485 		colorButtons.empty())
486 	{
487 		clearBWImageTab();
488 		bwBitmap = imageBitmap;
489 		ui.bwImageView->setImage(&bwBitmap);
490 		ui.bwImageView->reset();
491 		return;
492 	}
493 
494 	auto selectedColors = getSelectedColors();
495 	UIProgressDialog progressDialog(tr("Creating B/W image"), tr("Cancel"), this);
496 	QImage newBWBitmap =
497 		vectorizerApp->getBWImage(selectedColors, &progressDialog);
498 	if (newBWBitmap.isNull())
499 	{
500 		// no BW image
501 		clearBWImageTab();
502 		setTabEnabled(ui.thinningTab, false);
503 		return;
504 	}
505 
506 	const auto* undoCommand = static_cast<const BwBitmapUndoStep*>
507 	                      (bwBitmapUndo->command(0));
508 	if (!undoCommand
509 	    || undoCommand->image.constBits() != newBWBitmap.constBits())
510 	{
511 		// new BW image
512 		clearBWImageTab();
513 		bwBitmap = newBWBitmap;
514 		ui.bwImageView->setImage(&bwBitmap);
515 		ui.bwImageView->reset();
516 	}
517 	setTabEnabled(ui.thinningTab, true);
518 }
519 
520 //! Performs one morphological operation on the bwImage.
performMorphologicalOperation(Vectorizer::MorphologicalOperation mo)521 bool mainForm::performMorphologicalOperation(
522 	Vectorizer::MorphologicalOperation mo)
523 {
524 	QString text;
525 	bool transVectorizable = false;
526 
527 	switch (mo)
528 	{
529 	case Vectorizer::THINNING_ROSENFELD:
530 		text = tr("Thinning B/W image");
531 		transVectorizable = true;
532 		break;
533 	case Vectorizer::PRUNING:
534 		text = tr("Pruning B/W image");
535 		transVectorizable = bwBitmapVectorizable;
536 		break;
537 	case Vectorizer::EROSION:
538 		text = tr("Eroding B/W image");
539 		transVectorizable = bwBitmapVectorizable; // formally, does not change the status
540 		break;
541 	case Vectorizer::DILATION:
542 		text = tr("Dilating B/W image");
543 		transVectorizable = false;
544 		break;
545 	}
546 	UIProgressDialog progressDialog(text, tr("Cancel"), this);
547 	QImage transBitmap =
548 		Vectorizer::getTransformedImage(bwBitmap, mo, &progressDialog);
549 
550 	if (transBitmap.isNull())
551 		return false;
552 
553 	// store the current image onto undo stack
554 	auto* command = new BwBitmapUndoStep(*this, bwBitmap, bwBitmapVectorizable);
555 	bwBitmapUndo->push(command);
556 
557 	bwBitmap = transBitmap;
558 	bwBitmapVectorizable = transVectorizable;
559 	ui.bwImageView->setImage(&bwBitmap);
560 	return true;
561 }
562 
563 //! Returns one image back in the history.
on_bwImageHistoryBack_clicked()564 void mainForm::on_bwImageHistoryBack_clicked()
565 {
566 	bwBitmapUndo->undo();
567 }
568 
569 //! Advances one image forward in the history.
on_bwImageHistoryForward_clicked()570 void mainForm::on_bwImageHistoryForward_clicked()
571 {
572 	bwBitmapUndo->redo();
573 }
574 
575 /*! Runs the thinning.
576   \sa performMorphologicalOperation */
on_runThinningButton_clicked()577 void mainForm::on_runThinningButton_clicked()
578 {
579 	performMorphologicalOperation(Vectorizer::THINNING_ROSENFELD);
580 }
581 
582 /*! Runs the pruning.
583   \sa performMorphologicalOperation */
on_runPruningButton_clicked()584 void mainForm::on_runPruningButton_clicked()
585 {
586 	performMorphologicalOperation(Vectorizer::PRUNING);
587 }
588 
589 /*! Erodes the image.
590   \sa performMorphologicalOperation */
on_runErosionButton_clicked()591 void mainForm::on_runErosionButton_clicked()
592 {
593 	performMorphologicalOperation(Vectorizer::EROSION);
594 }
595 
596 /*! Dilates the image.
597   \sa performMorphologicalOperation */
on_runDilationButton_clicked()598 void mainForm::on_runDilationButton_clicked()
599 {
600 	performMorphologicalOperation(Vectorizer::DILATION);
601 }
602 
603 /*! Spawns dialog on classification config options.
604   \sa ClassificationConfigForm */
on_classificationOptionsButton_clicked()605 void mainForm::on_classificationOptionsButton_clicked()
606 {
607 	ClassificationConfigForm* optionsDialog =
608 		new ClassificationConfigForm(this);
609 	optionsDialog->learnMethod = settings.getInt("learnMethod");
610 	optionsDialog->colorSpace = settings.getInt("colorSpace");
611 	optionsDialog->p = settings.getDouble("p");
612 	optionsDialog->E = settings.getInt("E");
613 	optionsDialog->Q = settings.getDouble("q");
614 	optionsDialog->initAlpha = settings.getDouble("initAlpha");
615 	optionsDialog->minAlpha = settings.getDouble("minAlpha");
616 	optionsDialog->setValues();
617 	optionsDialog->exec();
618 	if (optionsDialog->result() == ClassificationConfigForm::Accepted)
619 	{
620 		settings.setInt("E", optionsDialog->E);
621 		settings.setDouble("q", optionsDialog->Q);
622 		settings.setDouble("initAlpha", optionsDialog->initAlpha);
623 		settings.setDouble("minAlpha", optionsDialog->minAlpha);
624 		settings.setDouble("learnMethod", optionsDialog->learnMethod);
625 		settings.setDouble("colorSpace", optionsDialog->colorSpace);
626 		settings.setDouble("p", optionsDialog->p);
627 	}
628 	delete optionsDialog;
629 }
630 
631 /*! Spawns initial colors selection dialog.
632   \sa ColorsEditForm */
on_initialColorsButton_clicked()633 void mainForm::on_initialColorsButton_clicked()
634 {
635 	ColorsEditForm* optionsDialog = new ColorsEditForm(this);
636 	int settingsNColors = settings.getInt("nColors");
637 	std::vector<QString> comments;
638 	auto colors = settings.getInitColors(comments);
639 	auto nColors = colors.size();
640 
641 	if (colors.empty())
642 	{
643 		// There are no settings in the store
644 		nColors = settingsNColors;
645 		colors = std::vector<QRgb>(nColors);
646 		comments = std::vector<QString>(nColors);
647 		for (std::size_t i = 0; i < nColors; i++)
648 		{
649 			colors[i] = qRgb(127, 127, 127);
650 			comments[i] = QString(tr("new color"));
651 		}
652 	}
653 	else if (nColors < settingsNColors)
654 	{
655 		// Existing settings only partially cover the higher number of colors
656 		colors.resize(settingsNColors, qRgb(255, 255, 255));
657 		comments.resize(settingsNColors, QString(tr("new color")));
658 	}
659 
660 	optionsDialog->setColors(colors, comments);
661 	optionsDialog->setColorsSource(static_cast<ColorsEditForm::ColorsSource>(
662 		settings.getInt("initColorsSource")));
663 	optionsDialog->exec();
664 	if (optionsDialog->result() == ColorsEditForm::Accepted)
665 	{
666 		settings.setInt("initColorsSource", optionsDialog->getColorsSource());
667 		settings.setInitColors(optionsDialog->getColors(),
668 							   optionsDialog->getComments());
669 	}
670 	delete optionsDialog;
671 }
672 
673 //! Stores the value of number of colors spinbox located at colors tab.
on_howManyColorsSpinBox_valueChanged(int n)674 void mainForm::on_howManyColorsSpinBox_valueChanged(int n)
675 {
676 	settings.setInt("nColors", n);
677 }
678 
679 //! Spawns dialog on vectorization options.
on_setVectorizationOptionsButton_clicked()680 void mainForm::on_setVectorizationOptionsButton_clicked()
681 {
682 	VectorizationConfigForm optionsDialog(this);
683 	optionsDialog.speckleSize = settings.getDouble("speckleSize");
684 	optionsDialog.doConnections = settings.getInt("doConnections");
685 	optionsDialog.joinDistance = settings.getDouble("joinDistance");
686 	optionsDialog.simpleConnectionsOnly =
687 		settings.getInt("simpleConnectionsOnly");
688 	optionsDialog.distDirBalance = settings.getDouble("distDirBalance");
689 	optionsDialog.cornerMin = settings.getDouble("cornerMin");
690 	optionsDialog.optTolerance = settings.getDouble("optTolerance");
691 	optionsDialog.exec();
692 	if (optionsDialog.result() == ClassificationConfigForm::Accepted)
693 	{
694 		settings.setDouble("speckleSize", optionsDialog.speckleSize);
695 		settings.setDouble("joinDistance", optionsDialog.joinDistance);
696 		settings.setInt("doConnections", optionsDialog.doConnections);
697 		settings.setInt("simpleConnectionsOnly",
698 						optionsDialog.simpleConnectionsOnly);
699 		settings.setDouble("distDirBalance", optionsDialog.distDirBalance);
700 		settings.setDouble("cornerMin", optionsDialog.cornerMin);
701 		settings.setDouble("optTolerance", optionsDialog.optTolerance);
702 	}
703 }
704 
705 //! Creates polygons from current bwImage.
on_createVectorsButton_clicked()706 void mainForm::on_createVectorsButton_clicked()
707 {
708 	if (!bwBitmapVectorizable)
709 		QMessageBox::warning(this, tr("Perform thinning before vectorization"),
710 		                     tr("It seems that thinning was not performed on"
711 		                       " this B/W image. Vectorization can process"
712 		                       " only one-pixel wide lines. Thicker lines"
713 		                       " will not be converted to vectors."));
714 
715 	Polygons p;
716 	p.setSpeckleSize(settings.getInt("speckleSize"));
717 	p.setMaxDistance(settings.getInt("doConnections")
718 						 ? settings.getDouble("joinDistance")
719 						 : 0);
720 	p.setSimpleOnly(settings.getInt("simpleConnectionsOnly"));
721 	p.setDistDirRatio(settings.getDouble("distDirBalance"));
722 	UIProgressDialog progressDialog (tr("Vectorizing"), tr("Cancel"), this);
723 	auto q = p.createPolygonsFromImage(bwBitmap, &progressDialog);
724 	ui.saveVectorsButton->setEnabled(!q.empty());
725 	ui.bwImageView->setPolygons(std::move(q));
726 }
727 
728 //! Transfers traced polygons back to the map.
on_saveVectorsButton_clicked()729 void mainForm::on_saveVectorsButton_clicked()
730 {
731 	const PolygonList& polys = ui.bwImageView->polygons();
732 	if (polys.empty())
733 		return;
734 
735 	ooMap->clearObjectSelection(false);
736 
737 	auto* symbol = ui.symbolComboBox->symbol();
738 	if (!symbol)
739 		symbol = ooMap->getUndefinedLine();
740 
741 	// transform from template coordinates to map coordinates
742 	auto const offset = QPointF { -0.5 * (ui.bwImageView->image()->width() - 1),
743 	                              -0.5 * (ui.bwImageView->image()->height() - 1) };
744 	auto const transform = [this, offset](const QPointF& point) -> OpenOrienteering::MapCoord {
745 		return OpenOrienteering::MapCoord(ooTempl->templateToMap(point + offset));
746 	};
747 
748 	std::vector<OpenOrienteering::Object*> result;
749 	result.reserve(polys.size());
750 
751 	for (const auto& polygon : polys)
752 	{
753 		auto coords = OpenOrienteering::MapCoordVector {};
754 		coords.reserve(polygon.size() + 1);  // One extra slot, for some closed paths.
755 		std::transform(begin(polygon), end(polygon), std::back_inserter(coords), transform);
756 
757 		auto* newOOPolygon = new OpenOrienteering::PathObject(symbol, std::move(coords));
758 		if (polygon.isClosed())
759 			newOOPolygon->closeAllParts();
760 		newOOPolygon->setTag(QStringLiteral("generator"), QStringLiteral("cove")); /// \todo Configuration of tag
761 		ooMap->addObject(newOOPolygon);
762 		ooMap->addObjectToSelection(newOOPolygon, false);
763 		result.push_back(newOOPolygon);
764 	}
765 
766 	auto const* part = ooMap->getCurrentPart();
767 	auto* undo_step = new OpenOrienteering::DeleteObjectsUndoStep(ooMap);
768 	for (auto const* object : result)
769 		undo_step->addObject(part->findObjectIndex(object));
770 	ooMap->push(undo_step);
771 
772 	ooMap->setObjectsDirty();
773 	ooMap->emitSelectionChanged();
774 }
775 
776 //! Saves current bwImage to an image file.
on_bwImageSaveButton_clicked()777 void mainForm::on_bwImageSaveButton_clicked()
778 {
779 	QString filter;
780 	QList<QByteArray> extensions = QImageReader::supportedImageFormats();
781 	QList<QByteArray>::iterator it;
782 
783 	for (it = extensions.begin(); it != extensions.end(); ++it)
784 	{
785 		filter.append(QString(*it).toUpper());
786 		filter.append(QString(*it).prepend(" (*.").append(");;"));
787 	}
788 
789 	filter.append(tr("All files"));
790 	filter.append("(*)");
791 
792 	QString selectedFilter("PNG (*.png)");
793 	QString newfilename = QFileDialog::getSaveFileName(
794 		this, QString::null, QFileInfo(imageFileName).dir().path(), filter,
795 		&selectedFilter);
796 
797 	if (!newfilename.isEmpty())
798 	{
799 		QString suffix = selectedFilter.section(" ", 1, 1);
800 		suffix = suffix.mid(2, suffix.size() - 3);
801 		if (!newfilename.endsWith(suffix, Qt::CaseInsensitive))
802 			newfilename.append(suffix);
803 		bwBitmap.save(newfilename,
804 					  selectedFilter.section(" ", 0, 0).toLatin1().constData());
805 	}
806 }
807 
808 //! Convenience method for enabling/disabling an UI tab.
setTabEnabled(QWidget * tab,bool state)809 void mainForm::setTabEnabled(QWidget* tab, bool state)
810 {
811 	int tabindex = ui.mainTabWidget->indexOf(tab);
812 	ui.mainTabWidget->setTabEnabled(tabindex, state);
813 }
814 
815 /*! Applies FIR filter onto \a imageBitmap.
816   \sa FIRFilter */
on_applyFIRFilterPushButton_clicked()817 void mainForm::on_applyFIRFilterPushButton_clicked()
818 {
819 	if (imageBitmap.isNull()) return;
820 	FIRFilter f;
821 	switch (ui.firFilterComboBox->currentIndex())
822 	{
823 	case 0:
824 		f = FIRFilter(2).binomic();
825 		break;
826 	case 1:
827 		f = FIRFilter(3).binomic();
828 		break;
829 	case 2:
830 		f = FIRFilter(2).box();
831 		break;
832 	case 3:
833 		f = FIRFilter(3).box();
834 		break;
835 	}
836 
837 	UIProgressDialog progressDialog(tr("Applying FIR Filter on image"),
838 	                                tr("Cancel"), this);
839 	auto functor = [&f](const QImage& source_image, ProgressObserver& progress_observer) -> QImage {
840 		return f.apply(source_image, qRgb(127, 127, 127), &progress_observer);
841 	};
842 	QImage newImageBitmap = Concurrency::process<QImage>(&progressDialog, functor, imageBitmap);
843 	if (!newImageBitmap.isNull()) imageBitmap = newImageBitmap;
844 	ui.imageView->setImage(&imageBitmap);
845 }
846 
847 /* \class mainForm::BwBitmapUndoStep
848  * \brief Embedded struct holding undo data.
849  */
850 
BwBitmapUndoStep(mainForm & form,QImage image,bool vectorizable)851 mainForm::BwBitmapUndoStep::BwBitmapUndoStep(mainForm& form, QImage image, bool vectorizable)
852     : form { form }
853     , image { image }
854     , suitableForVectorization { vectorizable }
855 {
856 	// nothing
857 }
858 
redo()859 void mainForm::BwBitmapUndoStep::redo()
860 {
861 	undo(); // the same data swap
862 }
863 
undo()864 void mainForm::BwBitmapUndoStep::undo()
865 {
866 	using std::swap;
867 	swap(form.bwBitmap, image);
868 	swap(form.bwBitmapVectorizable, suitableForVectorization);
869 	form.ui.bwImageView->setImage(&form.bwBitmap);
870 }
871 } // cove
872 
873 //@}
874