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