1 /*
2 * Copyright 2012-2014 Thomas Schöps
3 * Copyright 2013-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 "draw_path_tool.h"
23
24 #include <cmath>
25 #include <memory>
26 #include <vector>
27
28 #include <Qt>
29 #include <QtMath>
30 #include <QColor>
31 #include <QCursor>
32 #include <QFlags>
33 #include <QLatin1String>
34 #include <QKeyEvent>
35 #include <QLineF>
36 #include <QLocale>
37 #include <QMouseEvent>
38 #include <QPainter>
39 #include <QPen>
40 #include <QPixmap>
41 #include <QPointF>
42 #include <QRectF>
43 #include <QRgb>
44 #include <QSizeF>
45 #include <QString>
46 #include <QToolButton>
47 #include <QVarLengthArray>
48
49 #include "core/map.h"
50 #include "core/path_coord.h"
51 #include "core/virtual_coord_vector.h"
52 #include "core/virtual_path.h"
53 #include "core/symbols/line_symbol.h"
54 #include "core/symbols/symbol.h"
55 #include "core/objects/object.h"
56 #include "core/renderables/renderable.h"
57 #include "gui/modifier_key.h"
58 #include "gui/map/map_editor.h"
59 #include "gui/map/map_widget.h"
60 #include "gui/widgets/key_button_bar.h"
61 #include "tools/tool.h"
62 #include "tools/tool_helpers.h"
63 #include "util/util.h"
64 #include "undo/object_undo.h"
65
66
67 namespace OpenOrienteering {
68
DrawPathTool(MapEditorController * editor,QAction * tool_action,bool is_helper_tool,bool allow_closing_paths)69 DrawPathTool::DrawPathTool(MapEditorController* editor, QAction* tool_action, bool is_helper_tool, bool allow_closing_paths)
70 : DrawLineAndAreaTool(editor, DrawPath, tool_action, is_helper_tool)
71 , cur_map_widget(mapWidget())
72 , angle_helper(new ConstrainAngleToolHelper())
73 , azimuth_helper(new AzimuthInfoHelper(cur_map_widget, active_color))
74 , snap_helper(new SnappingToolHelper(this))
75 , follow_helper(new FollowPathToolHelper())
76 , allow_closing_paths(allow_closing_paths)
77 {
78 angle_helper->setActive(false);
79 connect(angle_helper.get(), &ConstrainAngleToolHelper::displayChanged, this, &DrawPathTool::updateDirtyRect);
80
81 updateSnapHelper();
82 connect(snap_helper.get(), &SnappingToolHelper::displayChanged, this, &DrawPathTool::updateDirtyRect);
83
84 connect(map(), &Map::objectSelectionChanged, this, &DrawPathTool::objectSelectionChanged);
85 }
86
~DrawPathTool()87 DrawPathTool::~DrawPathTool()
88 {
89 if (key_button_bar)
90 editor->deletePopupWidget(key_button_bar);
91 }
92
init()93 void DrawPathTool::init()
94 {
95 updateDashPointDrawing();
96 updateStatusText();
97
98 if (editor->isInMobileMode())
99 {
100 // Create key replacement bar
101 key_button_bar = new KeyButtonBar(editor->getMainWidget());
102 key_button_bar->addKeyButton(Qt::Key_Return, Qt::ControlModifier, tr("Finish"));
103 key_button_bar->addKeyButton(Qt::Key_Return, tr("Close"));
104 key_button_bar->addModifierButton(Qt::ShiftModifier, tr("Snap", "Snap to existing objects"));
105 key_button_bar->addModifierButton(Qt::ControlModifier, tr("Angle", "Using constrained angles"));
106 azimuth_button = key_button_bar->addKeyButton(Qt::Key_Space, Qt::ControlModifier, tr("Info", "Show segment azimuth and length"));
107 azimuth_button->setCheckable(true);
108 azimuth_button->setChecked(azimuth_helper->isActive());
109 dash_points_button = key_button_bar->addKeyButton(Qt::Key_Space, tr("Dash", "Drawing dash points"));
110 dash_points_button->setCheckable(true);
111 dash_points_button->setChecked(draw_dash_points);
112 key_button_bar->addKeyButton(Qt::Key_Backspace, tr("Undo"));
113 key_button_bar->addKeyButton(Qt::Key_Escape, tr("Abort"));
114 editor->showPopupWidget(key_button_bar, QString{});
115 }
116
117 MapEditorTool::init();
118 }
119
getCursor() const120 const QCursor& DrawPathTool::getCursor() const
121 {
122 static auto const cursor = scaledToScreen(QCursor{ QPixmap(QString::fromLatin1(":/images/cursor-draw-path.png")), 11, 11 });
123 return cursor;
124 }
125
mousePressEvent(QMouseEvent * event,const MapCoordF & map_coord,MapWidget * widget)126 bool DrawPathTool::mousePressEvent(QMouseEvent* event, const MapCoordF& map_coord, MapWidget* widget)
127 {
128 cur_map_widget = widget;
129 created_point_at_last_mouse_press = false;
130
131 if (editingInProgress() &&
132 ((event->button() == Qt::RightButton) &&
133 !drawOnRightClickEnabled()))
134 {
135 finishDrawing();
136 return true;
137 }
138 else if (editingInProgress() &&
139 ((event->button() == Qt::RightButton && event->buttons() & Qt::LeftButton) ||
140 (event->button() == Qt::LeftButton && event->buttons() & Qt::RightButton)))
141 {
142 if (!previous_point_is_curve_point)
143 undoLastPoint();
144 if (editingInProgress())
145 finishDrawing();
146 return true;
147 }
148 else if (isDrawingButton(event->button()))
149 {
150 dragging = false;
151 bool start_appending = false;
152 if (shift_pressed)
153 {
154 SnappingToolHelperSnapInfo snap_info;
155 MapCoord snap_coord = snap_helper->snapToObject(map_coord, widget, &snap_info);
156 click_pos_map = MapCoordF(snap_coord);
157 cur_pos_map = click_pos_map;
158 click_pos = widget->mapToViewport(click_pos_map).toPoint();
159
160 // Check for following and appending
161 if (!is_helper_tool)
162 {
163 if (!editingInProgress())
164 {
165 if (snap_info.type == SnappingToolHelper::ObjectCorners &&
166 (snap_info.coord_index == 0 || snap_info.coord_index == snap_info.object->asPath()->getCoordinateCount() - 1) &&
167 snap_info.object->getSymbol() == editor->activeSymbol())
168 {
169 // Appending to another path
170 start_appending = true;
171 startAppending(snap_info);
172 }
173
174 // Setup angle helper
175 if (snap_helper->snapToDirection(map_coord, widget, angle_helper.get()))
176 picked_angle = true;
177 }
178 else if (editingInProgress() &&
179 (snap_info.type == SnappingToolHelper::ObjectCorners || snap_info.type == SnappingToolHelper::ObjectPaths) &&
180 snap_info.object->getType() == Object::Path)
181 {
182 // Start following another path
183 picked_angle = false;
184 startFollowing(snap_info, snap_coord);
185 return true;
186 }
187 }
188 }
189 else if (!editingInProgress() && ctrl_pressed)
190 {
191 // Start picking direction of an existing object
192 picking_angle = true;
193 pickAngle(map_coord, widget);
194 return true;
195 }
196 else
197 {
198 click_pos = event->pos();
199 click_pos_map = map_coord;
200 cur_pos_map = map_coord;
201 }
202
203 if (!editingInProgress())
204 {
205 // Start a new path
206 startDrawing();
207 angle_helper->setCenter(click_pos_map);
208 updateSnapHelper();
209
210 path_has_preview_point = false;
211 previous_point_is_curve_point = false;
212 appending = start_appending;
213 }
214 else
215 {
216 if (!shift_pressed)
217 {
218 if (previous_point_is_curve_point
219 && (cur_pos - widget->mapToViewport(previous_drag_map)).manhattanLength() < startDragDistance())
220 {
221 click_pos_map = previous_drag_map;
222 }
223 else
224 {
225 angle_helper->getConstrainedCursorPosMap(click_pos_map, click_pos_map);
226 }
227 }
228 cur_pos_map = click_pos_map;
229 }
230
231 // Set path point
232 auto coord = MapCoord { click_pos_map };
233 if (draw_dash_points)
234 coord.setDashPoint(true);
235
236 if (preview_path->getCoordinateCount() > 0 && picked_angle)
237 picked_angle = false;
238 if (previous_point_is_curve_point)
239 {
240 // Do nothing yet, wait until the user drags or releases the mouse button
241 }
242 else if (path_has_preview_point)
243 {
244 preview_path->setCoordinate(preview_path->getCoordinateCount() - 1, coord);
245 updateAngleHelper();
246 created_point_at_last_mouse_press = true;
247 }
248 else
249 {
250 if (preview_path->getCoordinateCount() == 0 || !preview_path->getCoordinate(preview_path->getCoordinateCount() - 1).isPositionEqualTo(coord))
251 {
252 preview_path->addCoordinate(coord);
253 updatePreviewPath();
254 if (!start_appending)
255 updateAngleHelper();
256 created_point_at_last_mouse_press = true;
257 }
258 }
259
260 path_has_preview_point = false;
261
262 create_segment = true;
263 updateDirtyRect();
264 updateStatusText();
265 return true;
266 }
267
268 return false;
269 }
270
mouseMoveEvent(QMouseEvent * event,const MapCoordF & map_coord,MapWidget * widget)271 bool DrawPathTool::mouseMoveEvent(QMouseEvent* event, const MapCoordF& map_coord, MapWidget* widget)
272 {
273 cur_pos = event->pos();
274 cur_pos_map = map_coord;
275
276 if (!containsDrawingButtons(event->buttons()))
277 {
278 updateHover();
279 }
280 else if (!editingInProgress())
281 {
282 left_mouse_down = true;
283 if (picking_angle)
284 pickAngle(map_coord, widget);
285 else
286 return false;
287 }
288 else if (following)
289 {
290 updateFollowing();
291 }
292 else
293 {
294 if ((event->pos() - click_pos).manhattanLength() >= startDragDistance())
295 {
296 // Giving a direction by dragging
297 dragging = true;
298 create_spline_corner = false;
299 create_segment = true;
300
301 if (previous_point_is_curve_point)
302 angle_helper->setCenter(click_pos_map);
303
304 QPointF constrained_pos;
305 angle_helper->getConstrainedCursorPositions(map_coord, constrained_pos_map, constrained_pos, widget);
306
307 if (previous_point_is_curve_point)
308 {
309 hidePreviewPoints();
310 auto drag_direction = calculateRotation(constrained_pos.toPoint(), constrained_pos_map);
311
312 // Add a new node or convert the last node into a corner?
313 if ((widget->mapToViewport(previous_pos_map) - click_pos).manhattanLength() >= startDragDistance())
314 {
315 createPreviewCurve(MapCoord(click_pos_map), drag_direction);
316 }
317 else
318 {
319 create_spline_corner = true;
320 // This hides the old direction indicator
321 previous_drag_map = previous_pos_map;
322 }
323 }
324
325 updateDirtyRect();
326 }
327 }
328
329 return true;
330 }
331
mouseReleaseEvent(QMouseEvent * event,const MapCoordF & map_coord,MapWidget * widget)332 bool DrawPathTool::mouseReleaseEvent(QMouseEvent* event, const MapCoordF& map_coord, MapWidget* widget)
333 {
334 if (!isDrawingButton(event->button()))
335 return false;
336
337 left_mouse_down = false;
338
339 if (picking_angle)
340 {
341 picking_angle = false;
342 picked_angle = pickAngle(map_coord, widget);
343 return true;
344 }
345 else if (!editingInProgress())
346 {
347 return false;
348 }
349
350 if (following)
351 {
352 finishFollowing();
353 if ((event->button() == Qt::RightButton) && drawOnRightClickEnabled())
354 finishDrawing();
355 return true;
356 }
357
358 if (!create_segment)
359 return true;
360
361 if (previous_point_is_curve_point && !dragging && !create_spline_corner)
362 {
363 // The new point has not been added yet
364 MapCoord coord;
365 if (shift_pressed)
366 {
367 coord = snap_helper->snapToObject(map_coord, widget);
368 }
369 if (previous_point_is_curve_point
370 && (cur_pos - widget->mapToViewport(click_pos_map)).manhattanLength() < startDragDistance())
371 {
372 coord = MapCoord{click_pos_map};
373 }
374 else if (angle_helper->isActive())
375 {
376 QPointF constrained_pos;
377 angle_helper->getConstrainedCursorPositions(map_coord, constrained_pos_map, constrained_pos, widget);
378 coord = MapCoord(constrained_pos_map);
379 }
380 else
381 {
382 coord = MapCoord(map_coord);
383 }
384
385 if (draw_dash_points)
386 coord.setDashPoint(true);
387 preview_path->addCoordinate(coord);
388 updatePreviewPath();
389 }
390
391 previous_point_is_curve_point = dragging;
392 if (previous_point_is_curve_point)
393 {
394 QPointF constrained_pos;
395 angle_helper->getConstrainedCursorPositions(map_coord, constrained_pos_map, constrained_pos, widget);
396
397 previous_pos_map = click_pos_map;
398 previous_drag_map = constrained_pos_map;
399 previous_point_direction = calculateRotation(constrained_pos.toPoint(), constrained_pos_map);
400 }
401
402 updateAngleHelper();
403 updateDirtyRect();
404
405 create_spline_corner = false;
406 path_has_preview_point = false;
407 dragging = false;
408
409 if ((event->button() == Qt::RightButton) && drawOnRightClickEnabled())
410 finishDrawing();
411 return true;
412 }
413
mouseDoubleClickEvent(QMouseEvent * event,const MapCoordF &,MapWidget *)414 bool DrawPathTool::mouseDoubleClickEvent(QMouseEvent* event, const MapCoordF& /*map_coord*/, MapWidget* /*widget*/)
415 {
416 if (event->button() != Qt::LeftButton)
417 return false;
418
419 if (editingInProgress())
420 {
421 if (created_point_at_last_mouse_press)
422 undoLastPoint();
423 if (editingInProgress())
424 finishDrawing();
425 }
426 return true;
427 }
428
keyPressEvent(QKeyEvent * event)429 bool DrawPathTool::keyPressEvent(QKeyEvent* event)
430 {
431 switch (event->key())
432 {
433 case Qt::Key_Escape:
434 if (editingInProgress())
435 {
436 abortDrawing();
437 return true;
438 }
439 break;
440
441 case Qt::Key_Backspace:
442 if (editingInProgress())
443 {
444 undoLastPoint();
445 return true;
446 }
447 else if (finished_path_is_selected)
448 {
449 if (removeLastPointFromSelectedPath())
450 return true;
451 }
452 break;
453
454 case Qt::Key_Return:
455 if (editingInProgress())
456 {
457 if (allow_closing_paths && !(event->modifiers() & Qt::ControlModifier))
458 closeDrawing();
459 finishDrawing();
460 return true;
461 }
462 break;
463
464 case Qt::Key_Tab:
465 deactivate();
466 return true;
467
468 case Qt::Key_Space:
469 if (event->modifiers() & Qt::ControlModifier)
470 {
471 updateDirtyRect();
472 azimuth_helper->setActive(!azimuth_helper->isActive());
473 if (azimuth_button)
474 azimuth_button->setChecked(azimuth_helper->isActive());
475 updateDirtyRect();
476 }
477 else
478 {
479 draw_dash_points = !draw_dash_points;
480 if (dash_points_button)
481 dash_points_button->setChecked(draw_dash_points);
482 updateStatusText();
483 }
484 return true;
485
486 case Qt::Key_Control:
487 ctrl_pressed = true;
488 angle_helper->setActive(true);
489 if (editingInProgress() && !dragging)
490 updateDrawHover();
491 picked_angle = false;
492 updateStatusText();
493 return false; // not consuming Ctrl
494
495 case Qt::Key_Shift:
496 shift_pressed = true;
497 if (!dragging)
498 {
499 updateHover();
500 updateDirtyRect();
501 }
502 updateStatusText();
503 return false; // not consuming Shift
504
505 }
506 return false;
507 }
508
keyReleaseEvent(QKeyEvent * event)509 bool DrawPathTool::keyReleaseEvent(QKeyEvent* event)
510 {
511 switch (event->key())
512 {
513 case Qt::Key_Control:
514 ctrl_pressed = false;
515 if (!picked_angle)
516 angle_helper->setActive(false);
517 if (editingInProgress() && !dragging)
518 updateDrawHover();
519 updateStatusText();
520 return false; // not consuming Ctrl
521
522 case Qt::Key_Shift:
523 shift_pressed = false;
524 if (!dragging && !following)
525 {
526 updateHover();
527 updateDirtyRect();
528 }
529 updateStatusText();
530 return false; // not consuming Shift
531
532 }
533 return false;
534 }
535
draw(QPainter * painter,MapWidget * widget)536 void DrawPathTool::draw(QPainter* painter, MapWidget* widget)
537 {
538 drawPreviewObjects(painter, widget);
539
540 if (editingInProgress())
541 {
542 painter->setRenderHint(QPainter::Antialiasing);
543
544 auto azimuth_info_pending = azimuth_helper->isActive();
545
546 if (dragging && (cur_pos - click_pos).manhattanLength() >= startDragDistance())
547 {
548 auto line = QLineF{ widget->mapToViewport(click_pos_map),
549 widget->mapToViewport(constrained_pos_map) };
550 QPen pen(qRgb(255, 255, 255));
551 pen.setWidth(3);
552 painter->setPen(pen);
553 painter->drawLine(line);
554 painter->setPen(active_color);
555 painter->drawLine(line);
556 if (azimuth_info_pending)
557 {
558 azimuth_helper->draw(painter, widget, map(), click_pos_map, constrained_pos_map);
559 azimuth_info_pending = false;
560 }
561 }
562
563 if (previous_point_is_curve_point)
564 {
565 auto line = QLineF{ widget->mapToViewport(previous_pos_map),
566 widget->mapToViewport(previous_drag_map) };
567 QPen pen(qRgb(255, 255, 255));
568 pen.setWidth(3);
569 painter->setPen(pen);
570 painter->drawLine(line);
571 painter->setPen(active_color);
572 painter->drawLine(line);
573 if (azimuth_info_pending)
574 {
575 if ((cur_pos - line.p2()).manhattanLength() < startDragDistance())
576 {
577 azimuth_helper->draw(painter, widget, map(), previous_pos_map, previous_drag_map);
578 QPen pen(qRgb(255, 0, 0));
579 painter->setPen(pen);
580 painter->drawRect(QRectF{line.p2(), QSizeF{8.0, 8.0}}.translated(-4.0, -4.0));
581 }
582 else
583 {
584 azimuth_helper->draw(painter, widget, map(), click_pos_map, constrained_pos_map);
585 }
586 azimuth_info_pending = false;
587 }
588 }
589
590 if (azimuth_info_pending && preview_path && preview_path->getCoordinateCount() >= 2)
591 {
592 auto start_pos = MapCoordF{preview_path->getCoordinate(preview_path->getCoordinateCount() - 2)};
593 auto end_pos = MapCoordF{preview_path->getCoordinate(preview_path->getCoordinateCount() - 1)};
594 azimuth_helper->draw(painter, widget, map(), start_pos, end_pos);
595 // unused: azimuth_info_pending = false;
596 }
597
598 if (!shift_pressed)
599 angle_helper->draw(painter, widget);
600 }
601
602 if (shift_pressed && !dragging)
603 {
604 snap_helper->draw(painter, widget);
605 }
606 else if (!editingInProgress() && (picking_angle || picked_angle))
607 {
608 if (picking_angle)
609 snap_helper->draw(painter, widget);
610 angle_helper->draw(painter, widget);
611 }
612 }
613
updatePreviewPath()614 void DrawPathTool::updatePreviewPath()
615 {
616 DrawLineAndAreaTool::updatePreviewPath();
617 updateStatusText();
618 }
619
updateHover()620 void DrawPathTool::updateHover()
621 {
622 if (shift_pressed)
623 constrained_pos_map = MapCoordF(snap_helper->snapToObject(cur_pos_map, cur_map_widget));
624 else
625 constrained_pos_map = cur_pos_map;
626
627 if (!editingInProgress())
628 {
629 // Show preview objects at this position
630 setPreviewPointsPosition(constrained_pos_map);
631 if (picked_angle)
632 angle_helper->setCenter(constrained_pos_map);
633 updateDirtyRect();
634 }
635 else // if (draw_in_progress)
636 {
637 updateDrawHover();
638 }
639 }
640
updateDrawHover()641 void DrawPathTool::updateDrawHover()
642 {
643 if (!shift_pressed)
644 angle_helper->getConstrainedCursorPosMap(cur_pos_map, constrained_pos_map);
645 if (!previous_point_is_curve_point && !left_mouse_down && editingInProgress())
646 {
647 // Show a line to the cursor position as preview
648 hidePreviewPoints();
649
650 if (!path_has_preview_point)
651 {
652 preview_path->addCoordinate(MapCoord(constrained_pos_map));
653 path_has_preview_point = true;
654 }
655 preview_path->setCoordinate(preview_path->getCoordinateCount() - 1, MapCoord(constrained_pos_map));
656
657 updatePreviewPath();
658 updateDirtyRect(); // TODO: Possible optimization: mark only the last segment as dirty
659 }
660 else if (previous_point_is_curve_point && !left_mouse_down && editingInProgress())
661 {
662 setPreviewPointsPosition(constrained_pos_map, 1);
663 updateDirtyRect();
664 }
665 }
666
createPreviewCurve(MapCoord position,qreal direction)667 void DrawPathTool::createPreviewCurve(MapCoord position, qreal direction)
668 {
669 if (!path_has_preview_point)
670 {
671 auto last = preview_path->getCoordinateCount() - 1;
672 (preview_path->getCoordinateRef(last)).setCurveStart(true);
673
674 preview_path->addCoordinate(MapCoord(0, 0));
675 preview_path->addCoordinate(MapCoord(0, 0));
676 if (draw_dash_points)
677 position.setDashPoint(true);
678 position.setCurveStart(false);
679 preview_path->addCoordinate(position);
680
681 path_has_preview_point = true;
682 }
683
684 // Adjust the preview curve
685 // preview_path is going to be modified. Non-const getCoordinate is fine.
686 auto last = preview_path->getCoordinateCount() - 1;
687 const MapCoord previous_point = preview_path->getCoordinate(last - 3);
688 const MapCoord last_point = preview_path->getCoordinate(last);
689
690 double bezier_handle_distance = BEZIER_HANDLE_DISTANCE * previous_point.distanceTo(last_point);
691
692 preview_path->setCoordinate(last - 2, MapCoord(previous_point.x() - bezier_handle_distance * sin(previous_point_direction),
693 previous_point.y() - bezier_handle_distance * cos(previous_point_direction)));
694 preview_path->setCoordinate(last - 1, MapCoord(last_point.x() + bezier_handle_distance * sin(direction),
695 last_point.y() + bezier_handle_distance * cos(direction)));
696 updatePreviewPath();
697 }
698
undoLastPoint()699 void DrawPathTool::undoLastPoint()
700 {
701 Q_ASSERT(editingInProgress());
702
703 if (preview_path->getCoordinateCount() <= (preview_path->parts().front().isClosed() ? 3 : (path_has_preview_point ? 2 : 1)))
704 {
705 abortDrawing();
706 return;
707 }
708
709 auto& part = preview_path->parts().back();
710 auto last_index = part.last_index;
711 // preview_path is going to be modified. Non-const getCoordinate is fine.
712 auto prev_coord_index = part.prevCoordIndex(part.last_index);
713 auto prev_coord = preview_path->getCoordinate(prev_coord_index);
714
715 // Pre-undo preparation
716 if (path_has_preview_point)
717 {
718 if (prev_coord.isCurveStart())
719 {
720 // Undo just the preview point
721 path_has_preview_point = false;
722 }
723 else
724 {
725 // Remove the preview point from a straight edge, preparing for re-adding.
726 Q_ASSERT(!previous_point_is_curve_point);
727
728 preview_path->deleteCoordinate(last_index, false);
729 last_index = prev_coord_index;
730 prev_coord_index = part.prevCoordIndex(part.last_index);
731 prev_coord = preview_path->getCoordinate(prev_coord_index);
732
733 path_has_preview_point = !prev_coord.isCurveStart();
734 }
735 }
736
737 if (prev_coord.isCurveStart())
738 {
739 // Removing last point of a curve, no re-adding of preview point.
740 const MapCoord prev_drag = preview_path->getCoordinate(prev_coord_index+1);
741 previous_point_direction = -atan2(prev_drag.x() - prev_coord.x(), prev_coord.y() - prev_drag.y());
742 previous_pos_map = MapCoordF(prev_coord);
743 previous_drag_map = MapCoordF((prev_coord.x() + prev_drag.x()) / 2, (prev_coord.y() + prev_drag.y()) / 2);
744 previous_point_is_curve_point = true;
745 path_has_preview_point = false;
746
747 click_pos_map = previous_pos_map; // for azimuth info
748 }
749 else if (!path_has_preview_point)
750 {
751 // Removing last point from a straight edge, no re-adding of preview point.
752 previous_point_is_curve_point = false;
753 }
754
755 // Actually delete the last point of the edge.
756 preview_path->deleteCoordinate(last_index, false);
757 if (preview_path->getRawCoordinateVector().empty())
758 {
759 // Re-add first point.
760 prev_coord.setCurveStart(false);
761 preview_path->addCoordinate(prev_coord);
762 }
763
764 // Post-undo
765 if (path_has_preview_point)
766 {
767 // Re-add preview point.
768 preview_path->addCoordinate(MapCoord(cur_pos_map));
769 }
770 else if (previous_point_is_curve_point && dragging)
771 {
772 cur_pos = click_pos;
773 cur_pos_map = click_pos_map;
774 }
775
776 dragging = false;
777
778 updateHover();
779 updatePreviewPath();
780 updateAngleHelper();
781 updateDirtyRect();
782 }
783
removeLastPointFromSelectedPath()784 bool DrawPathTool::removeLastPointFromSelectedPath()
785 {
786 if (editingInProgress() || map()->getNumSelectedObjects() != 1)
787 {
788 return false;
789 }
790
791 Object* object = map()->getFirstSelectedObject();
792 if (object->getType() != Object::Path)
793 {
794 return false;
795 }
796
797 PathObject* path = object->asPath();
798 if (path->parts().size() != 1)
799 {
800 return false;
801 }
802
803 int points_on_path = 0;
804 auto num_coords = path->getCoordinateCount();
805 for (MapCoordVector::size_type i = 0; i < num_coords && points_on_path < 3; ++i)
806 {
807 ++points_on_path;
808 if (path->getCoordinate(i).isCurveStart())
809 {
810 i += 2; // Skip the control points.
811 }
812 }
813
814 if (points_on_path < 3)
815 {
816 // Too few points after deleting the last: delete the whole object.
817 map()->deleteSelectedObjects();
818 return true;
819 }
820
821 auto undo_step = new ReplaceObjectsUndoStep(map());
822 auto undo_duplicate = object->duplicate();
823 undo_duplicate->setMap(map());
824 undo_step->addObject(object, undo_duplicate);
825 map()->push(undo_step);
826 updateDirtyRect();
827
828 path->parts().front().setClosed(false);
829 path->deleteCoordinate(num_coords - 1, false);
830
831 path->update();
832 map()->setObjectsDirty();
833 map()->emitSelectionEdited();
834 return true;
835 }
836
closeDrawing()837 void DrawPathTool::closeDrawing()
838 {
839 Q_ASSERT(editingInProgress());
840
841 if (preview_path->getCoordinateCount() <= 1)
842 return;
843
844 // preview_path is going to be modified. Non-const getCoordinate is fine.
845 if (previous_point_is_curve_point && preview_path->getCoordinate(0).isCurveStart())
846 {
847 // Finish with a curve
848 path_has_preview_point = false;
849
850 if (dragging)
851 previous_point_direction = -atan2(cur_pos_map.x() - click_pos_map.x(), click_pos_map.y() - cur_pos_map.y());
852
853 const MapCoord first = preview_path->getCoordinate(0);
854 const MapCoord second = preview_path->getCoordinate(1);
855 createPreviewCurve(first, -atan2(second.x() - first.x(), first.y() - second.y()));
856 path_has_preview_point = false;
857 }
858
859 if (!preview_path->parts().empty())
860 preview_path->parts().front().setClosed(true, true);
861 }
862
finishDrawing()863 void DrawPathTool::finishDrawing()
864 {
865 Q_ASSERT(editingInProgress());
866
867 // Does the symbols contain only areas? If so, auto-close the path if not done yet
868 bool contains_only_areas = !is_helper_tool && (drawing_symbol->getContainedTypes() & ~(Symbol::Area | Symbol::Combined)) == 0 && (drawing_symbol->getContainedTypes() & Symbol::Area);
869 if (contains_only_areas && !preview_path->parts().empty())
870 preview_path->parts().front().setClosed(true, true);
871
872 // Remove last point if closed and first and last points are equal, or if the last point was just a preview
873 if (path_has_preview_point && !dragging)
874 preview_path->deleteCoordinate(preview_path->getCoordinateCount() - (preview_path->parts().front().isClosed() ? 2 : 1), false);
875
876 if (preview_path->getCoordinateCount() < (contains_only_areas ? 3 : 2))
877 {
878 renderables->removeRenderablesOfObject(preview_path, false);
879 delete preview_path;
880 preview_path = nullptr;
881 }
882
883 dragging = false;
884 following = false;
885 setEditingInProgress(false);
886 if (!ctrl_pressed)
887 angle_helper->setActive(false);
888 updateSnapHelper();
889 updateStatusText();
890 hidePreviewPoints();
891
892 DrawLineAndAreaTool::finishDrawing(appending ? append_to_object : nullptr);
893
894 finished_path_is_selected = true;
895 }
896
abortDrawing()897 void DrawPathTool::abortDrawing()
898 {
899 dragging = false;
900 following = false;
901 setEditingInProgress(false);
902 if (!ctrl_pressed)
903 angle_helper->setActive(false);
904 updateSnapHelper();
905 updateStatusText();
906 hidePreviewPoints();
907
908 DrawLineAndAreaTool::abortDrawing();
909 }
910
updateDirtyRect()911 void DrawPathTool::updateDirtyRect()
912 {
913 QRectF rect;
914
915 if (azimuth_helper->isActive() && editingInProgress())
916 {
917 rectIncludeSafe(rect, azimuth_helper->dirtyRect(mapWidget(), constrained_pos_map));
918 }
919
920 if (dragging)
921 {
922 rectIncludeSafe(rect, click_pos_map);
923 rectInclude(rect, cur_pos_map);
924 }
925 if (editingInProgress() && previous_point_is_curve_point)
926 {
927 rectIncludeSafe(rect, previous_pos_map);
928 rectInclude(rect, previous_drag_map);
929 }
930 if ((editingInProgress() && !dragging) ||
931 (!editingInProgress() && !shift_pressed && ctrl_pressed) ||
932 (!editingInProgress() && (picking_angle || picked_angle)))
933 {
934 angle_helper->includeDirtyRect(rect);
935 }
936 if (shift_pressed || (!editingInProgress() && ctrl_pressed))
937 snap_helper->includeDirtyRect(rect);
938 includePreviewRects(rect);
939
940 if (is_helper_tool)
941 emit dirtyRectChanged(rect);
942 else
943 {
944 if (rect.isValid())
945 map()->setDrawingBoundingBox(rect, qMax(qMax(dragging ? 1 : 0, angle_helper->getDisplayRadius()), snap_helper->getDisplayRadius()), true);
946 else
947 map()->clearDrawingBoundingBox();
948 }
949 }
950
setDrawingSymbol(const Symbol * symbol)951 void DrawPathTool::setDrawingSymbol(const Symbol* symbol)
952 {
953 if (is_helper_tool)
954 return;
955 DrawLineAndAreaTool::setDrawingSymbol(symbol);
956
957 updateDashPointDrawing();
958 }
959
objectSelectionChanged()960 void DrawPathTool::objectSelectionChanged()
961 {
962 finished_path_is_selected = false;
963 }
964
updateAngleHelper()965 void DrawPathTool::updateAngleHelper()
966 {
967 if (picked_angle)
968 return;
969
970 if (!preview_path
971 || (static_cast<void>(updatePreviewPath()), preview_path->parts().empty()))
972 {
973 angle_helper->clearAngles();
974 angle_helper->addDefaultAnglesDeg(0);
975 return;
976 }
977
978 const auto& part = preview_path->parts().back();
979
980 bool rectangular_stepping = true;
981 double angle;
982 if (part.size() >= 2)
983 {
984 bool ok = false;
985 MapCoordF tangent = part.calculateTangent(part.size()-1, true, ok);
986 if (!ok)
987 tangent = MapCoordF(1, 0);
988 angle = -tangent.angle();
989 }
990 else if (previous_point_is_curve_point)
991 {
992 angle = previous_point_direction;
993 }
994 else
995 {
996 angle = 0;
997 rectangular_stepping = false;
998 }
999
1000 if (!part.empty())
1001 angle_helper->setCenter(part.coords.back());
1002 angle_helper->clearAngles();
1003 if (rectangular_stepping)
1004 angle_helper->addAnglesDeg(angle * 180 / M_PI, 45);
1005 else
1006 angle_helper->addDefaultAnglesDeg(angle * 180 / M_PI);
1007 }
1008
pickAngle(const MapCoordF & coord,MapWidget * widget)1009 bool DrawPathTool::pickAngle(const MapCoordF& coord, MapWidget* widget)
1010 {
1011 MapCoord snap_position;
1012 bool picked = snap_helper->snapToDirection(coord, widget, angle_helper.get(), &snap_position);
1013 if (picked)
1014 {
1015 angle_helper->setCenter(MapCoordF(snap_position));
1016 }
1017 else
1018 {
1019 updateAngleHelper();
1020 angle_helper->setCenter(constrained_pos_map);
1021 }
1022 hidePreviewPoints();
1023 updateDirtyRect();
1024 return picked;
1025 }
1026
updateSnapHelper()1027 void DrawPathTool::updateSnapHelper()
1028 {
1029 if (editingInProgress())
1030 snap_helper->setFilter(SnappingToolHelper::AllTypes);
1031 else
1032 {
1033 //snap_helper.setFilter((SnappingToolHelper::SnapObjects)(SnappingToolHelper::GridCorners | SnappingToolHelper::ObjectCorners));
1034 snap_helper->setFilter(SnappingToolHelper::AllTypes);
1035 }
1036 }
1037
startAppending(SnappingToolHelperSnapInfo & snap_info)1038 void DrawPathTool::startAppending(SnappingToolHelperSnapInfo& snap_info)
1039 {
1040 append_to_object = snap_info.object->asPath();
1041 }
1042
startFollowing(SnappingToolHelperSnapInfo & snap_info,const MapCoord & snap_coord)1043 void DrawPathTool::startFollowing(SnappingToolHelperSnapInfo& snap_info, const MapCoord& snap_coord)
1044 {
1045 following = true;
1046 auto followed_object = snap_info.object->asPath();
1047 create_segment = false;
1048
1049 if (snap_info.type == SnappingToolHelper::ObjectCorners)
1050 follow_helper->startFollowingFromCoord(followed_object, snap_info.coord_index);
1051 else // if (snap_info.type == SnappingToolHelper::ObjectPaths)
1052 follow_helper->startFollowingFromPathCoord(followed_object, snap_info.path_coord);
1053
1054 if (path_has_preview_point)
1055 preview_path->setCoordinate(preview_path->getCoordinateCount() - 1, snap_coord);
1056 else
1057 preview_path->addCoordinate(snap_coord);
1058 path_has_preview_point = false;
1059 previous_point_is_curve_point = false;
1060 updatePreviewPath();
1061 follow_start_index = preview_path->getCoordinateCount() - 1;
1062 }
1063
updateFollowing()1064 void DrawPathTool::updateFollowing()
1065 {
1066 PathCoord path_coord;
1067 float distance_sq;
1068 const auto* followed_object = follow_helper->followed_object();
1069 const auto& part = followed_object->parts()[follow_helper->partIndex()];
1070 followed_object->calcClosestPointOnPath(cur_pos_map, distance_sq, path_coord, part.first_index, part.last_index);
1071 auto followed_path = follow_helper->updateFollowing(path_coord);
1072
1073 // Append the temporary object to the preview object at follow_start_index
1074 // 1. Delete everything appended, except for the point where following started
1075 // (thus avoiding deletion of the whole part).
1076 // \todo Implement as truncate()
1077 for (auto i = preview_path->getCoordinateCount() - 1;
1078 i > follow_start_index + 1;
1079 i = preview_path->getCoordinateCount() - 1)
1080 {
1081 preview_path->deleteCoordinate(i, false);
1082 }
1083 // 2. Merge segments at the point where following started.
1084 if (followed_path)
1085 {
1086 preview_path->connectPathParts(preview_path->findPartIndexForIndex(follow_start_index),
1087 followed_path.get(), 0, false, true);
1088 }
1089 updatePreviewPath();
1090 hidePreviewPoints();
1091 updateDirtyRect();
1092 }
1093
finishFollowing()1094 void DrawPathTool::finishFollowing()
1095 {
1096 following = false;
1097
1098 auto last = preview_path->getCoordinateCount() - 1;
1099
1100 previous_point_is_curve_point = (last >= 3 && preview_path->getCoordinate(last - 3).isCurveStart());
1101 if (previous_point_is_curve_point)
1102 {
1103 const MapCoord first = preview_path->getCoordinate(last - 1);
1104 const MapCoord second = preview_path->getCoordinate(last);
1105
1106 previous_point_direction = -atan2(second.x() - first.x(), first.y() - second.y());
1107 previous_pos_map = MapCoordF(second);
1108 previous_drag_map = MapCoordF(2*second.x() - first.x(), 2*second.y() + - first.y());
1109 }
1110
1111 updateAngleHelper();
1112 }
1113
calculateRotation(const QPoint & mouse_pos,const MapCoordF & mouse_pos_map) const1114 qreal DrawPathTool::calculateRotation(const QPoint& mouse_pos, const MapCoordF& mouse_pos_map) const
1115 {
1116 if (dragging && (mouse_pos - click_pos).manhattanLength() >= startDragDistance())
1117 return -atan2(mouse_pos_map.x() - click_pos_map.x(), click_pos_map.y() - mouse_pos_map.y());
1118 else
1119 return 0;
1120 }
1121
updateDashPointDrawing()1122 void DrawPathTool::updateDashPointDrawing()
1123 {
1124 if (is_helper_tool)
1125 return;
1126
1127 Symbol* symbol = editor->activeSymbol();
1128 if (symbol && symbol->getType() == Symbol::Line)
1129 {
1130 // Auto-activate dash points depending on if the selected symbol has a dash symbol.
1131 // TODO: instead of just looking if it is a line symbol with dash points,
1132 // could also check for combined symbols containing lines with dash points
1133 draw_dash_points = (symbol->asLine()->getDashSymbol());
1134
1135 updateStatusText();
1136 }
1137 else if (symbol &&
1138 (symbol->getType() == Symbol::Area ||
1139 symbol->getType() == Symbol::Combined))
1140 {
1141 draw_dash_points = false;
1142 }
1143
1144 if (dash_points_button)
1145 dash_points_button->setChecked(draw_dash_points);
1146 }
1147
updateStatusText()1148 void DrawPathTool::updateStatusText()
1149 {
1150 QString text;
1151 if (editingInProgress() && preview_path && preview_path->getCoordinateCount() >= 2)
1152 {
1153 //Q_ASSERT(!preview_path->isDirty());
1154 float length = map()->getScaleDenominator() * preview_path->parts().front().path_coords.back().clen * 0.001f;
1155 text += tr("<b>Length:</b> %1 m ").arg(QLocale().toString(length, 'f', 1)) + QLatin1String("| ");
1156 }
1157
1158 if (draw_dash_points && !is_helper_tool)
1159 {
1160 text += ::OpenOrienteering::DrawLineAndAreaTool::tr("<b>Dash points on.</b> ") + QLatin1String("| ");
1161 }
1162
1163 QVarLengthArray<QString, 3> modifier_keys;
1164 if (!editingInProgress())
1165 {
1166 if (shift_pressed)
1167 {
1168 text += ::OpenOrienteering::DrawLineAndAreaTool::tr("<b>%1+Click</b>: Snap or append to existing objects. ").arg(ModifierKey::shift());
1169 }
1170 else
1171 {
1172 modifier_keys.append(ModifierKey::shift());
1173
1174 if (ctrl_pressed)
1175 {
1176 text += ::OpenOrienteering::DrawLineAndAreaTool::tr("<b>%1+Click</b>: Pick direction from existing objects. ").arg(ModifierKey::control());
1177 text += ::OpenOrienteering::DrawLineAndAreaTool::tr("<b>%1+%2</b>: Segment azimuth and length. ").arg(ModifierKey::control(), ModifierKey::space());
1178 }
1179 else
1180 {
1181 modifier_keys.append(ModifierKey::control());
1182
1183 text += tr("<b>Click</b>: Start a straight line. <b>Drag</b>: Start a curve. ");
1184
1185 // text += ::OpenOrienteering::DrawLineAndAreaTool::tr(draw_dash_points ? "<b>%1</b> Disable dash points. " : "<b>%1</b>: Enable dash points. ").arg(ModifierKey::space());
1186 }
1187 }
1188 }
1189 else
1190 {
1191 if (shift_pressed)
1192 {
1193 text += ::OpenOrienteering::DrawLineAndAreaTool::tr("<b>%1+Click</b>: Snap to existing objects. ").arg(ModifierKey::shift())
1194 + tr("<b>%1+Drag</b>: Follow existing objects. ").arg(ModifierKey::shift());
1195 }
1196 else
1197 {
1198 modifier_keys.append(ModifierKey::shift());
1199
1200 if (ctrl_pressed)
1201 {
1202 if (angle_helper->isActive())
1203 text += ::OpenOrienteering::DrawLineAndAreaTool::tr("<b>%1</b>: Fixed angles. ").arg(ModifierKey::control());
1204 text += ::OpenOrienteering::DrawLineAndAreaTool::tr("<b>%1+%2</b>: Segment azimuth and length. ").arg(ModifierKey::control(), ModifierKey::space());
1205 }
1206 else
1207 {
1208 modifier_keys.append(ModifierKey::control());
1209
1210 text += tr("<b>Click</b>: Draw a straight line. <b>Drag</b>: Draw a curve. "
1211 "<b>Right or double click</b>: Finish the path. "
1212 "<b>%1</b>: Close the path. ").arg(ModifierKey::return_key())
1213 + ::OpenOrienteering::DrawLineAndAreaTool::tr("<b>%1</b>: Undo last point. ").arg(ModifierKey::backspace())
1214 + ::OpenOrienteering::MapEditorTool::tr("<b>%1</b>: Abort. ").arg(ModifierKey::escape());
1215 }
1216 }
1217 }
1218
1219 if (!is_helper_tool && !ctrl_pressed)
1220 {
1221 modifier_keys.append(ModifierKey::space());
1222 }
1223
1224 if (!modifier_keys.isEmpty())
1225 {
1226 QString text_more;
1227 switch (modifier_keys.length())
1228 {
1229 case 1:
1230 text_more = ::OpenOrienteering::MapEditorTool::tr("More: %1").arg(modifier_keys[0]);
1231 break;
1232 case 2:
1233 text_more = ::OpenOrienteering::MapEditorTool::tr("More: %1, %2").arg(modifier_keys[0], modifier_keys[1]);
1234 break;
1235 case 3:
1236 text_more = ::OpenOrienteering::MapEditorTool::tr("More: %1, %2, %3").arg(modifier_keys[0], modifier_keys[1], modifier_keys[2]);
1237 break;
1238 default:
1239 Q_UNREACHABLE();
1240 }
1241 text += QLatin1String("| ") + text_more;
1242 }
1243
1244 setStatusBarText(text);
1245 }
1246
1247
1248 } // namespace OpenOrienteering
1249