1 /*
2  *    Copyright 2014 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 "draw_freehand_tool.h"
23 
24 #include <Qt>
25 #include <QCursor>
26 #include <QKeyEvent>
27 #include <QMouseEvent>
28 #include <QPixmap>
29 #include <QRectF>
30 #include <QString>
31 
32 #include "core/map.h"
33 #include "core/map_view.h"
34 #include "core/objects/object.h"
35 #include "gui/modifier_key.h"
36 #include "gui/map/map_editor.h"
37 #include "gui/map/map_widget.h"
38 #include "tools/tool.h"
39 
40 
41 namespace OpenOrienteering {
42 
DrawFreehandTool(MapEditorController * editor,QAction * tool_action,bool is_helper_tool)43 DrawFreehandTool::DrawFreehandTool(MapEditorController* editor, QAction* tool_action, bool is_helper_tool)
44 : DrawLineAndAreaTool(editor, DrawFreehand, tool_action, is_helper_tool)
45 {
46 	// nothing else
47 }
48 
49 
50 DrawFreehandTool::~DrawFreehandTool() = default;
51 
52 
init()53 void DrawFreehandTool::init()
54 {
55 	updateStatusText();
56 
57 	MapEditorTool::init();
58 }
59 
60 
getCursor() const61 const QCursor& DrawFreehandTool::getCursor() const
62 {
63 	static auto const cursor = scaledToScreen(QCursor{ QPixmap(QString::fromLatin1(":/images/cursor-draw-path.png")), 11, 11 });
64 	return cursor;
65 }
66 
67 
68 
mousePressEvent(QMouseEvent * event,const MapCoordF & map_coord,MapWidget * widget)69 bool DrawFreehandTool::mousePressEvent(QMouseEvent* event, const MapCoordF& map_coord, MapWidget* widget)
70 {
71 	Q_UNUSED(widget);
72 
73 	if (event->button() == Qt::LeftButton && !editingInProgress())
74 	{
75 		last_pos = cur_pos = event->pos();
76 		cur_pos_map = map_coord;
77 
78 		startDrawing();
79 		preview_path->addCoordinate(MapCoord(cur_pos_map));
80 		hidePreviewPoints();
81 		return true;
82 	}
83 
84 	return false;
85 }
86 
87 
mouseMoveEvent(QMouseEvent * event,const MapCoordF & map_coord,MapWidget * widget)88 bool DrawFreehandTool::mouseMoveEvent(QMouseEvent* event, const MapCoordF& map_coord, MapWidget* widget)
89 {
90 	Q_UNUSED(widget);
91 
92 	if (editingInProgress())
93 	{
94 		cur_pos = event->pos();
95 		cur_pos_map = map_coord;
96 		updatePath();
97 		return true;
98 	}
99 
100 	setPreviewPointsPosition(map_coord);
101 	setDirtyRect();
102 	return false;
103 }
104 
105 
mouseReleaseEvent(QMouseEvent * event,const MapCoordF & map_coord,MapWidget * widget)106 bool DrawFreehandTool::mouseReleaseEvent(QMouseEvent* event, const MapCoordF& map_coord, MapWidget* widget)
107 {
108 	Q_UNUSED(widget);
109 
110 	if (event->button() == Qt::LeftButton && editingInProgress())
111 	{
112 		if (preview_path->getCoordinateCount() < 2)
113 		{
114 			abortDrawing();
115 			return true;
116 		}
117 
118 		cur_pos = event->pos();
119 		cur_pos_map = map_coord;
120 		updatePath();
121 		finishDrawing();
122 		return true;
123 	}
124 
125 	return false;
126 }
127 
128 
keyPressEvent(QKeyEvent * event)129 bool DrawFreehandTool::keyPressEvent(QKeyEvent* event)
130 {
131 	switch (event->key())
132 	{
133 	case Qt::Key_Escape:
134 		abortDrawing();
135 		return true;
136 
137 	case Qt::Key_Tab:
138 		deactivate();
139 		return true;
140 
141 	}
142 	return false;
143 }
144 
145 
146 
draw(QPainter * painter,MapWidget * widget)147 void DrawFreehandTool::draw(QPainter* painter, MapWidget* widget)
148 {
149 	drawPreviewObjects(painter, widget);
150 }
151 
152 
153 
finishDrawing()154 void DrawFreehandTool::finishDrawing()
155 {
156 	updateStatusText();
157 
158 	// Clean up path: remove superfluous points
159 	if (preview_path->getCoordinateCount() > 2)
160 	{
161 		// Use 999 instead of 1000, and save the rounding.
162 		split_distance_sq = editor->getMainWidget()->getMapView()->pixelToLength(1) / 999;
163 		split_distance_sq *= split_distance_sq;
164 
165 		point_mask.assign(preview_path->getCoordinateCount(), false);
166 		point_mask.front() = true;
167 		point_mask.back() = true;
168 		checkLineSegment(0, point_mask.size() - 1);
169 
170 		for (auto i = point_mask.size() - 1; i != 0; )
171 		{
172 			--i;
173 			if (!point_mask[i])
174 				preview_path->deleteCoordinate(i, false);
175 		}
176 	}
177 
178 	DrawLineAndAreaTool::finishDrawing();
179 	// Do not add stuff here as the tool might get deleted in DrawLineAndAreaTool::finishDrawing()!
180 }
181 
182 
abortDrawing()183 void DrawFreehandTool::abortDrawing()
184 {
185 	updateStatusText();
186 
187 	DrawLineAndAreaTool::abortDrawing();
188 }
189 
190 
191 
checkLineSegment(std::size_t first,std::size_t last)192 void DrawFreehandTool::checkLineSegment(std::size_t first, std::size_t last)
193 {
194 	if (last <= first + 1)
195 		return;
196 
197 	// 'best_index' indicates the point on the path between first and last
198 	// with the highest distance from the direct line between first and last.
199 	auto best_index = first;
200 
201 	// This block ensures the stack is cleaned up before going into recursion.
202 	{
203 		auto max_distance_sq = qreal(0);
204 		const auto start_coord = MapCoordF(preview_path->getRawCoordinateVector()[first]);
205 		const auto end_coord = MapCoordF(preview_path->getRawCoordinateVector()[last]);
206 		auto tangent = end_coord - start_coord;
207 		tangent.normalize();
208 
209 		for (auto i = first + 1; i < last; ++i)
210 		{
211 			const auto coord = MapCoordF(preview_path->getRawCoordinateVector()[i]);
212 			const auto to_coord = coord - start_coord;
213 
214 			qreal distance_sq;
215 
216 			auto dist_along_line = MapCoordF::dotProduct(to_coord, tangent);
217 			if (dist_along_line <= 0)
218 			{
219 				distance_sq = to_coord.lengthSquared();
220 			}
221 			else if (dist_along_line >= end_coord.distanceTo(start_coord))
222 			{
223 				distance_sq = coord.distanceSquaredTo(end_coord);
224 			}
225 			else
226 			{
227 				auto right = tangent.perpRight();
228 				distance_sq = qAbs(MapCoordF::dotProduct(right, to_coord));
229 				distance_sq = distance_sq*distance_sq;
230 			}
231 
232 			if (distance_sq > max_distance_sq)
233 			{
234 				max_distance_sq = distance_sq;
235 				best_index = i;
236 			}
237 		}
238 
239 		// Make new segment?
240 		if (max_distance_sq < split_distance_sq)
241 			return;
242 	}
243 
244 	point_mask[best_index] = true;
245 	checkLineSegment(first, best_index);
246 	checkLineSegment(best_index, last);
247 }
248 
249 
250 
updatePath()251 void DrawFreehandTool::updatePath()
252 {
253 	if ((last_pos - cur_pos).manhattanLength() <= 2)
254 		return;
255 
256 	preview_path->addCoordinate(MapCoord(cur_pos_map));
257 	last_pos = cur_pos;
258 
259 	updatePreviewPath();
260 	setDirtyRect();
261 }
262 
263 
264 
setDirtyRect()265 void DrawFreehandTool::setDirtyRect()
266 {
267 	QRectF rect;
268 	includePreviewRects(rect);
269 
270 	if (is_helper_tool)
271 	{
272 		emit dirtyRectChanged(rect);
273 	}
274 	else if (rect.isValid())
275 	{
276 		map()->setDrawingBoundingBox(rect, 0, true);
277 	}
278 	else
279 	{
280 		map()->clearDrawingBoundingBox();
281 	}
282 }
283 
284 
285 
updateStatusText()286 void DrawFreehandTool::updateStatusText()
287 {
288 	QString text;
289 	text = tr("<b>Drag</b>: Draw a path. ") +
290 			OpenOrienteering::MapEditorTool::tr("<b>%1</b>: Abort. ").arg(ModifierKey::escape());
291 	setStatusBarText(text);
292 }
293 
294 
295 }  // namespace OpenOrienteering
296