1 /*
2  *    Copyright 2013 Thomas Schöps
3  *    Copyright 2014-2017 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 "fill_tool.h"
23 
24 #include <algorithm>
25 #include <cstddef>
26 #include <iterator>
27 #include <limits>
28 #include <memory>
29 #include <type_traits>
30 
31 #include <QtGlobal>
32 #include <QCursor>
33 #include <QDir>  // IWYU pragma: keep
34 #include <QFlags>
35 #include <QMessageBox>
36 #include <QPainter>
37 #include <QPixmap>
38 #include <QPoint>
39 #include <QPointF>
40 #include <QRect>
41 #include <QRgb>
42 #include <QSize>
43 #include <QString>
44 
45 #include "core/map.h"
46 #include "core/map_color.h"
47 #include "core/map_coord.h"
48 #include "core/map_part.h"
49 #include "core/map_view.h"
50 #include "core/path_coord.h"
51 #include "core/objects/object.h"
52 #include "core/renderables/renderable.h"
53 #include "core/symbols/area_symbol.h"
54 #include "core/symbols/symbol.h"
55 #include "gui/map/map_editor.h"
56 #include "gui/map/map_widget.h"
57 #include "tools/tool.h"
58 #include "tools/tool_base.h"
59 #include "undo/object_undo.h"
60 
61 
62 // Uncomment this to generate an image file of the rasterized map
63 //#define FILLTOOL_DEBUG_IMAGE "FillTool.png"
64 
65 
66 // Make sure that we can use the object ID in place of a qRgb
67 // directly and in a sufficiently large domain.
68 Q_STATIC_ASSERT((std::is_same<QRgb, unsigned int>::value));
69 Q_STATIC_ASSERT(RGB_MASK == 0x00ffffff);
70 
71 
72 namespace OpenOrienteering {
73 
74 namespace {
75 
76 /**
77  * Helper structure used to represent a section of a traced path
78  * while constructing the fill object.
79  */
80 struct PathSection
81 {
82 	PathObject* object;
83 	PathPartVector::size_type part;
84 	PathCoord::length_type start_clen;
85 	PathCoord::length_type end_clen;
86 };
87 
88 constexpr auto background = QRgb(0xffffffffu);
89 
90 }  // namespace
91 
92 
93 
FillTool(MapEditorController * editor,QAction * tool_action)94 FillTool::FillTool(MapEditorController* editor, QAction* tool_action)
95 : MapEditorToolBase(QCursor(QPixmap(QString::fromLatin1(":/images/cursor-fill.png")), 11, 11), Other, editor, tool_action)
96 {
97 	drawing_symbol = editor->activeSymbol();
98 	setDrawingSymbol(editor->activeSymbol());
99 
100 	connect(editor, &MapEditorController::activeSymbolChanged, this, &FillTool::setDrawingSymbol);
101 }
102 
103 FillTool::~FillTool() = default;
104 
105 
106 
107 // TODO: create a way for tools to specify which symbols / selections they support and deactivate them automatically if these conditions are not satisfied anymore!
setDrawingSymbol(const Symbol * symbol)108 void FillTool::setDrawingSymbol(const Symbol* symbol)
109 {
110 	// Avoid using deleted symbol
111 	if (map()->findSymbolIndex(drawing_symbol) == -1)
112 		symbol = nullptr;
113 
114 	if (!symbol)
115 		deactivate();
116 	else if (symbol->isHidden())
117 		deactivate();
118 	else if ((symbol->getType() & (Symbol::Line | Symbol::Area | Symbol::Combined)) == 0)
119 		switchToDefaultDrawTool(symbol);
120 	else
121 		drawing_symbol = symbol;
122 }
123 
clickPress()124 void FillTool::clickPress()
125 {
126 	// First try to apply with current viewport only as extent (for speed)
127 	auto widget = editor->getMainWidget();
128 	QRectF viewport_extent = widget->getMapView()->calculateViewedRect(widget->viewportToView(widget->geometry()));
129 	int result = fill(viewport_extent);
130 	if (result == -1 || result == 1)
131 		return;
132 
133 	// If not successful, try again with rasterizing the whole map part
134 	QRectF map_part_extent = map()->getCurrentPart()->calculateExtent(true);
135 	if (viewport_extent.united(map_part_extent) != viewport_extent)
136 		result = fill(map_part_extent);
137 	if (result == -1 || result == 1)
138 		return;
139 
140 	QMessageBox::warning(
141 		window(),
142 		tr("Error"),
143 		tr("The clicked area is not bounded by lines or areas, cannot fill this area.")
144 	);
145 }
146 
fill(const QRectF & extent)147 int FillTool::fill(const QRectF& extent)
148 {
149 	constexpr auto extent_area_warning_threshold = qreal(600 * 600); // 60 cm x 60 cm
150 
151 	// Warn if desired extent is large
152 	if (extent.width() * extent.height() > extent_area_warning_threshold)
153 	{
154 		if (QMessageBox::question(
155 			window(),
156 			tr("Warning"),
157 			tr("The map area is large. Use of the fill tool may be very slow. Do you want to use it anyway?"),
158 			QMessageBox::No | QMessageBox::Yes) == QMessageBox::No)
159 		{
160 			return -1;
161 		}
162 	}
163 
164 	// Rasterize map into image
165 	QTransform transform;
166 	QImage image = rasterizeMap(extent, transform);
167 
168 	// Calculate click position in image and check if it is inside the map area and free
169 	QPoint clicked_pixel = transform.map(cur_map_widget->viewportToMapF(click_pos)).toPoint();
170 	if (!image.rect().contains(clicked_pixel, true))
171 		return 0;
172 	if (image.pixel(clicked_pixel) != background)
173 	{
174 		QMessageBox::warning(
175 			window(),
176 			tr("Error"),
177 			tr("The clicked position is not free, cannot use the fill tool there.")
178 		);
179 		return -1;
180 	}
181 
182 	// Go to the right and find collisions with objects.
183 	// For every collision, trace the boundary of the collision object
184 	// and check whether the click position is inside the boundary.
185 	// If it is, the correct outline was found which is then filled.
186 	for (QPoint free_pixel = clicked_pixel; free_pixel.x() < image.width() - 1; free_pixel += QPoint(1, 0))
187 	{
188 		// Check if there is a collision to the right
189 		QPoint boundary_pixel = free_pixel + QPoint(1, 0);
190 		if (image.pixel(boundary_pixel) == background)
191 			continue;
192 
193 		// Found a collision, trace outline of hit object
194 		// and check whether the outline contains start_pixel
195 		std::vector<QPoint> boundary;
196 		int trace_result = traceBoundary(image, free_pixel, boundary_pixel, boundary);
197 		if (trace_result == -1)
198 			return 0;
199 		else if (trace_result == 0)
200 		{
201 			// The outline does not contain start_pixel.
202 			// Jump to the rightmost pixel of the boundary with same y as the start.
203 			for (const auto& point : boundary)
204 			{
205 				if (point.y() == free_pixel.y() && point.x() > free_pixel.x())
206 					free_pixel = point;
207 			}
208 
209 			// Skip over the rest of the floating object.
210 			free_pixel += QPoint(1, 0);
211 			while (free_pixel.x() < image.width() - 1
212 				&& image.pixel(free_pixel) != background)
213 				free_pixel += QPoint(1, 0);
214 			free_pixel -= QPoint(1, 0);
215 			continue;
216 		}
217 
218 		// Don't let the boundary start in the midde of an object
219 		const auto id = image.pixel(boundary.front());
220 		auto new_object = std::find_if(begin(boundary), end(boundary), [&image, id](auto pos) {
221 			return image.pixel(pos) != id;
222 		});
223 		std::rotate(begin(boundary), new_object, end(boundary));
224 
225 		// Create fill object
226 		if (!fillBoundary(image, boundary, transform.inverted()))
227 		{
228 			QMessageBox::warning(
229 				window(),
230 				tr("Error"),
231 				tr("Failed to create the fill object.")
232 			);
233 			return -1;
234 		}
235 		return 1;
236 	}
237 	return 0;
238 }
239 
updateStatusText()240 void FillTool::updateStatusText()
241 {
242 	setStatusBarText(tr("<b>Click</b>: Fill area with active symbol. The area to be filled must be bounded by lines or areas, other symbols are not taken into account. "));
243 }
244 
objectSelectionChangedImpl()245 void FillTool::objectSelectionChangedImpl()
246 {
247 }
248 
rasterizeMap(const QRectF & extent,QTransform & out_transform)249 QImage FillTool::rasterizeMap(const QRectF& extent, QTransform& out_transform)
250 {
251 	// Draw map into a QImage with the following settings:
252 	// - specific zoom factor (resolution)
253 	// - no antialiasing
254 	// - encode object ids in object colors
255 	// - draw baselines in advance to normal rendering
256 	//   This makes it possible to fill areas bounded by e.g. dashed paths.
257 
258 	constexpr auto zoom_level = qreal(4);
259 
260 	// Create map view centered on the extent
261 	MapView view{ map() };
262 	view.setCenter(MapCoord{ extent.center() });
263 	view.setZoom(zoom_level);
264 
265 	// Allocate the image
266 	auto image_size = view.calculateViewBoundingBox(extent).toAlignedRect().size();
267 	QImage image = QImage(image_size, QImage::Format_RGB32);
268 	image.fill(background);
269 
270 	// Draw map
271 	RenderConfig::Options options = RenderConfig::DisableAntialiasing | RenderConfig::ForceMinSize;
272 	RenderConfig config = { *map(), extent, view.calculateFinalZoomFactor(), options, 1.0 };
273 
274 	QPainter painter;
275 	painter.begin(&image);
276 	painter.translate(image_size.width() / 2.0, image_size.height() / 2.0);
277 	painter.setWorldTransform(view.worldTransform(), true);
278 
279 	auto original_area_hatching = map()->isAreaHatchingEnabled();
280 	if (original_area_hatching)
281 	{
282 		map()->setAreaHatchingEnabled(false);
283 	}
284 
285 	if (!map()->isBaselineViewEnabled())
286 	{
287 		// Temporarily enable baseline view and draw map once.
288 		map()->setBaselineViewEnabled(true);
289 		map()->getCurrentPart()->applyOnAllObjects(&Object::forceUpdate);
290 		drawObjectIDs(map(), &painter, config);
291 		map()->setBaselineViewEnabled(false);
292 		map()->getCurrentPart()->applyOnAllObjects(&Object::forceUpdate);
293 	}
294 	else if (original_area_hatching)
295 	{
296 		map()->getCurrentPart()->applyOnAllObjects(&Object::forceUpdate);
297 	}
298 
299 	// Draw the map in original mode (but without area hatching)
300 	drawObjectIDs(map(), &painter, config);
301 
302 	if (original_area_hatching)
303 	{
304 		map()->setAreaHatchingEnabled(original_area_hatching);
305 		map()->getCurrentPart()->applyOnAllObjects(&Object::forceUpdate);
306 	}
307 
308 	out_transform = painter.combinedTransform();
309 	painter.end();
310 
311 #ifdef FILLTOOL_DEBUG_IMAGE
312 	image.save(QDir::temp().absoluteFilePath(QString::fromLatin1(FILLTOOL_DEBUG_IMAGE)));
313 #endif
314 
315 	return image;
316 }
317 
drawObjectIDs(Map * map,QPainter * painter,const RenderConfig & config)318 void FillTool::drawObjectIDs(Map* map, QPainter* painter, const RenderConfig &config)
319 {
320 	Q_STATIC_ASSERT(MapColor::Reserved == -1);
321 	Q_ASSERT(!map->isAreaHatchingEnabled());
322 
323 	auto part = map->getCurrentPart();
324 	auto num_objects = qMin(part->getNumObjects(), int(RGB_MASK));
325 	auto num_colors = map->getNumColors();
326 	for (auto c = num_colors-1; c >= MapColor::Reserved; --c)
327 	{
328 		auto map_color = map->getColor(c);
329 		for (int o = 0; o < num_objects; ++o)
330 		{
331 			auto object = part->getObject(o);
332 			if (object->getType() != Object::Path)
333 				continue;
334 			if (auto symbol = object->getSymbol())
335 			{
336 				if (symbol->isHidden())
337 					continue;
338 				if (symbol->getType() == Symbol::Area
339 				    && static_cast<const AreaSymbol*>(symbol)->getColor() != map_color)
340 					continue;
341 			}
342 
343 			object->update();
344 			object->renderables().draw(c, QRgb(o) | ~RGB_MASK, painter, config);
345 		}
346 	}
347 }
348 
traceBoundary(const QImage & image,const QPoint & free_pixel,const QPoint & boundary_pixel,std::vector<QPoint> & out_boundary)349 int FillTool::traceBoundary(const QImage& image, const QPoint& free_pixel, const QPoint& boundary_pixel, std::vector<QPoint>& out_boundary)
350 {
351 	Q_ASSERT(image.pixel(free_pixel) == background);
352 	Q_ASSERT(image.pixel(boundary_pixel) != background);
353 
354 #ifdef FILLTOOL_DEBUG_IMAGE
355 	{
356 		auto debugImage = image.copy();
357 		debugImage.setPixel(free_pixel, qRgb(255, 0, 0));
358 		debugImage.save(QDir::temp().absoluteFilePath(QString::fromLatin1(FILLTOOL_DEBUG_IMAGE)));
359 	}
360 #endif
361 
362 	out_boundary.clear();
363 	out_boundary.reserve(4096);
364 	out_boundary.push_back(boundary_pixel);
365 
366 	// Go along obstructed pixels with a "right(?) hand on the wall" method.
367 	// Iteration keeps the following variables as state:
368 	// cur_free_pixel:     current free position next to boundary
369 	// cur_boundary_pixel: current position on boundary
370 	auto cur_free_pixel     = free_pixel;
371 	auto cur_boundary_pixel = boundary_pixel;
372 	do
373 	{
374 		const auto fwd_vector = cur_free_pixel - cur_boundary_pixel;
375 		const auto right_vector = QPoint(fwd_vector.y(), -fwd_vector.x());
376 		const auto right_pixel = cur_free_pixel + right_vector;
377 		if (!image.rect().contains(right_pixel, true))
378 			return -1;
379 
380 		// Now analysing a 2x2 pixel block:
381 		// | cur_free_pixel | cur_boundary_pixel |
382 		// |  right_pixel   |     diag_pixel     |
383 		const auto diag_pixel = cur_boundary_pixel + right_vector;
384 		if (image.pixel(right_pixel) == image.pixel(cur_boundary_pixel))
385 		{
386 			out_boundary.push_back(right_pixel);
387 		}
388 		else if (image.pixel(diag_pixel) != background)
389 		{
390 			out_boundary.push_back(diag_pixel);
391 			if (image.pixel(right_pixel) == background)
392 				cur_free_pixel = right_pixel;
393 			else
394 				out_boundary.push_back(right_pixel);
395 		}
396 		else if (image.pixel(right_pixel) != background)
397 		{
398 			out_boundary.push_back(right_pixel);
399 		}
400 		else
401 		{
402 			cur_free_pixel = diag_pixel;
403 		}
404 		cur_boundary_pixel = out_boundary.back();
405 	}
406 	while (cur_boundary_pixel != boundary_pixel || cur_free_pixel != free_pixel);
407 
408 	bool inside = false;
409 	auto size = out_boundary.size();
410 	for (std::size_t i = 0, j = size - 1; i < size; j = i++)
411 	{
412 		if ( ((out_boundary[i].y() > free_pixel.y()) != (out_boundary[j].y() > free_pixel.y()))
413 		     &&	(free_pixel.x() < (out_boundary[j].x() - out_boundary[i].x())
414 		                           * (free_pixel.y() - out_boundary[i].y())
415 		                           / float(out_boundary[j].y() - out_boundary[i].y()) + out_boundary[i].x()) )
416 			inside = !inside;
417 	}
418 	return inside ? 1 : 0;
419 }
420 
fillBoundary(const QImage & image,const std::vector<QPoint> & boundary,const QTransform & image_to_map)421 bool FillTool::fillBoundary(const QImage& image, const std::vector<QPoint>& boundary, const QTransform& image_to_map)
422 {
423 	auto path = new PathObject(drawing_symbol);
424 	auto append_section = [path](const PathSection& section)
425 	{
426 		if (!section.object)
427 			return;
428 
429 		const auto& part = section.object->parts()[section.part];
430 		if (section.end_clen == section.start_clen)
431 		{
432 			path->addCoordinate(MapCoord(SplitPathCoord::at(section.start_clen, SplitPathCoord::begin(part.path_coords)).pos));
433 			return;
434 		}
435 
436 		PathObject part_copy { part };
437 		if (section.end_clen < section.start_clen)
438 		{
439 			part_copy.changePathBounds(0, section.end_clen, section.start_clen);
440 			part_copy.reverse();
441 		}
442 		else
443 		{
444 			part_copy.changePathBounds(0, section.start_clen, section.end_clen);
445 		}
446 
447 		if (path->getCoordinateCount() == 0)
448 			path->appendPath(&part_copy);
449 		else
450 			path->connectPathParts(0, &part_copy, 0, false, false);
451 	};
452 
453 	auto last_pixel = background; // no object
454 	const auto pixel_length = PathCoord::length_type((image_to_map.map(QPointF(0, 0)) - image_to_map.map(QPointF(1, 1))).manhattanLength());
455 	auto threshold = std::numeric_limits<PathCoord::length_type>::max();
456 	auto section = PathSection{ nullptr, 0, 0, 0 };
457 	for (const auto& point : boundary)
458 	{
459 		auto pixel = image.pixel(point);
460 		if (pixel == background)
461 			continue;
462 
463 		MapCoordF map_pos = MapCoordF(image_to_map.map(QPointF(point)));
464 		PathCoord path_coord;
465 		float distance_sq;
466 
467 		if (pixel != last_pixel)
468 		{
469 			// Change of object
470 			append_section(section);
471 
472 			section.object = map()->getCurrentPart()->getObject(int(pixel & RGB_MASK))->asPath();
473 			section.object->calcClosestPointOnPath(map_pos, distance_sq, path_coord);
474 			section.part = section.object->findPartIndexForIndex(path_coord.index);
475 			section.start_clen = path_coord.clen;
476 			section.end_clen = path_coord.clen;
477 			last_pixel = pixel;
478 			threshold = section.object->parts()[section.part].length() - 5*pixel_length;
479 			continue;
480 		}
481 
482 		section.object->calcClosestPointOnPath(map_pos, distance_sq, path_coord);
483 		auto part = section.object->findPartIndexForIndex(path_coord.index);
484 		if (Q_UNLIKELY(part != section.part))
485 		{
486 			// Change of path part
487 			append_section(section);
488 
489 			section.part = part;
490 			section.start_clen = path_coord.clen;
491 			section.end_clen = path_coord.clen;
492 			threshold = section.object->parts()[section.part].length() - 4*pixel_length;
493 			continue;
494 		}
495 
496 		if (section.end_clen - path_coord.clen >= threshold)
497 		{
498 			// Forward over closing point
499 			section.end_clen = section.object->parts()[section.part].length();
500 			append_section(section);
501 			section.start_clen = 0;
502 		}
503 		else if (path_coord.clen - section.end_clen >= threshold)
504 		{
505 			// Backward over closing point
506 			section.end_clen = 0;
507 			append_section(section);
508 			section.start_clen = section.object->parts()[section.part].length();
509 		}
510 		section.end_clen = path_coord.clen;
511 	}
512 	// Final section
513 	append_section(section);
514 
515 	if (path->getCoordinateCount() < 2)
516 	{
517 		delete path;
518 		return false;
519 	}
520 
521 	path->closeAllParts();
522 
523 	// Obsolete: The resulting path is as simple as the bounding objects,
524 	// so better avoid the loss in precision from PathObject::simplify.
525 	//   const auto simplify_epsilon = 1e-2;
526 	//   path->simplify(nullptr, simplify_epsilon);
527 
528 	int index = map()->addObject(path);
529 	map()->clearObjectSelection(false);
530 	map()->addObjectToSelection(path, true);
531 
532 	auto undo_step = new DeleteObjectsUndoStep(map());
533 	undo_step->addObject(index);
534 	map()->push(undo_step);
535 
536 	map()->setObjectsDirty();
537 	updateDirtyRect();
538 
539 	return true;
540 }
541 
542 
543 }  // namespace OpenOrienteering
544