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