1 /*
2  *    Copyright 2012, 2013 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 #include "edit_tool.h"
22 
23 #include <cstddef>
24 #include <limits>
25 #include <map>
26 #include <memory>
27 #include <unordered_set>
28 
29 #include <QtGlobal>
30 #include <QtMath>
31 #include <QCursor>
32 #include <QPainter>
33 #include <QPainterPath>
34 #include <QPen>
35 #include <QPixmap>
36 #include <QString>
37 
38 #include "core/map.h"
39 #include "core/virtual_path.h"
40 #include "core/objects/object.h"
41 #include "core/objects/text_object.h"
42 #include "gui/map/map_widget.h"
43 #include "tools/object_selector.h"
44 #include "tools/tool_helpers.h"
45 #include "undo/object_undo.h"
46 #include "util/util.h"
47 
48 
49 namespace OpenOrienteering {
50 
EditTool(MapEditorController * editor,MapEditorTool::Type type,QAction * tool_action)51 EditTool::EditTool(MapEditorController* editor, MapEditorTool::Type type, QAction* tool_action)
52  : MapEditorToolBase { QCursor(QPixmap(QString::fromLatin1(":/images/cursor-hollow.png")), 1, 1), type, editor, tool_action }
53  , object_selector { new ObjectSelector(map()) }
54 {
55 	; // nothing
56 }
57 
58 
59 EditTool::~EditTool() = default;
60 
61 
62 
deleteSelectedObjects()63 void EditTool::deleteSelectedObjects()
64 {
65 	map()->deleteSelectedObjects();
66 	updateStatusText();
67 }
68 
69 
createReplaceUndoStep(Object * object)70 void EditTool::createReplaceUndoStep(Object* object)
71 {
72 	auto undo_step = new ReplaceObjectsUndoStep(map());
73 	auto undo_duplicate = object->duplicate();
74 	undo_duplicate->setMap(map());
75 	undo_step->addObject(object, undo_duplicate);
76 	map()->push(undo_step);
77 
78 	map()->setObjectsDirty();
79 }
80 
81 
pointOverRectangle(const QPointF & point,const QRectF & rect) const82 bool EditTool::pointOverRectangle(const QPointF& point, const QRectF& rect) const
83 {
84 	auto click_tolerance = clickTolerance();
85 	if (point.x() < rect.left() - click_tolerance) return false;
86 	if (point.y() < rect.top() - click_tolerance) return false;
87 	if (point.x() > rect.right() + click_tolerance) return false;
88 	if (point.y() > rect.bottom() + click_tolerance) return false;
89 	if (point.x() > rect.left() + click_tolerance &&
90 		point.y() > rect.top() + click_tolerance &&
91 		point.x() < rect.right() - click_tolerance &&
92 		point.y() < rect.bottom() - click_tolerance) return false;
93 	return true;
94 }
95 
96 
closestPointOnRect(MapCoordF point,const QRectF & rect)97 MapCoordF EditTool::closestPointOnRect(MapCoordF point, const QRectF& rect)
98 {
99 	if (point.x() < rect.left()) point.setX(rect.left());
100 	if (point.y() < rect.top()) point.setY(rect.top());
101 	if (point.x() > rect.right()) point.setX(rect.right());
102 	if (point.y() > rect.bottom()) point.setY(rect.bottom());
103 	if (rect.height() > 0 && rect.width() > 0)
104 	{
105 		if ((point.x() - rect.left()) / rect.width() > (point.y() - rect.top()) / rect.height())
106 		{
107 			if ((point.x() - rect.left()) / rect.width() > (rect.bottom() - point.y()) / rect.height())
108 				point.setX(rect.right());
109 			else
110 				point.setY(rect.top());
111 		}
112 		else
113 		{
114 			if ((point.x() - rect.left()) / rect.width() > (rect.bottom() - point.y()) / rect.height())
115 				point.setY(rect.bottom());
116 			else
117 				point.setX(rect.left());
118 		}
119 	}
120 	return point;
121 }
122 
123 
setupAngleHelperFromEditedObjects()124 void EditTool::setupAngleHelperFromEditedObjects()
125 {
126 	constexpr auto max_num_primary_directions = std::size_t(5);
127 	constexpr auto angle_window = (2 * M_PI) * 2 / 360.0;
128 	// Amount of all path length which has to be covered by an angle
129 	// to be classified as "primary angle"
130 	constexpr auto path_angle_threshold = qreal(1 / 5.0);
131 
132 	angle_helper->clearAngles();
133 
134 	std::unordered_set<qreal> primary_directions;
135 	for (const Object* object : editedObjects())
136 	{
137 		if (object->getType() == Object::Point)
138 		{
139 			primary_directions.insert(fmod_pos(object->asPoint()->getRotation(), M_PI / 2));
140 		}
141 		else if (object->getType() == Object::Text)
142 		{
143 			primary_directions.insert(fmod_pos(object->asText()->getRotation(), M_PI / 2));
144 		}
145 		else if (object->getType() == Object::Path)
146 		{
147 			const auto* path = object->asPath();
148 			// Maps angles to the path distance covered by them
149 			std::map<qreal, qreal> path_directions;
150 
151 			// Collect segment directions, only looking at the first part
152 			auto& part = path->parts().front();
153 			auto path_length = part.path_coords.back().clen;
154 			for (auto c = part.first_index; c < part.last_index; c = part.nextCoordIndex(c))
155 			{
156 				if (!path->getCoordinate(c).isCurveStart())
157 				{
158 					auto segment = MapCoordF(path->getCoordinate(c + 1) - path->getCoordinate(c));
159 					auto angle = fmod_pos(-segment.angle(), M_PI / 2);
160 					auto length = segment.length();
161 
162 					auto angle_it = path_directions.find(angle);
163 					if (angle_it != path_directions.end())
164 						angle_it->second += length;
165 					else
166 						path_directions.insert({angle, length});
167 				}
168 			}
169 
170 			// Determine primary directions by moving a window over the collected angles
171 			// and determining maxima.
172 			// The iterators are the next angle which crosses the respective window border.
173 			auto angle_start = -angle_window;
174 			auto start_it = path_directions.begin();
175 			auto angle_end = qreal(0.0);
176 			auto end_it = path_directions.begin();
177 
178 			auto length_increasing = true;
179 			auto cur_length = qreal(0.0);
180 			while (start_it != path_directions.end())
181 			{
182 				auto start_dist = start_it->first - angle_start;
183 				auto end_dist = (end_it == path_directions.end()) ?
184 					std::numeric_limits<qreal>::max() :
185 					(end_it->first - angle_end);
186 				if (start_dist > end_dist)
187 				{
188 					// A new angle enters the window (at the end)
189 					cur_length += end_it->second;
190 					length_increasing = true;
191 					++end_it;
192 
193 					angle_start += end_dist;
194 					angle_end += end_dist;
195 				}
196 				else // if (start_dist <= end_dist)
197 				{
198 					// An angle leaves the window (at the start)
199 					// Check if we had a significant maximum.
200 					if (length_increasing &&
201 						cur_length / path_length >= path_angle_threshold)
202 					{
203 						// Find the average angle and insert it
204 						auto angle = qreal(0.0);
205 						auto total_weight = qreal(0.0);
206 						for (auto angle_it = start_it; angle_it != end_it; ++angle_it)
207 						{
208 							angle += angle_it->first * angle_it->second;
209 							total_weight += angle_it->second;
210 						}
211 						primary_directions.insert(angle / total_weight);
212 					}
213 
214 					cur_length -= start_it->second;
215 					length_increasing = false;
216 					++start_it;
217 
218 					angle_start += start_dist;
219 					angle_end += start_dist;
220 				}
221 			}
222 		}
223 
224 		if (primary_directions.size() > max_num_primary_directions)
225 			break;
226 	}
227 
228 	if (primary_directions.size() > max_num_primary_directions ||
229 		primary_directions.empty())
230 	{
231 		angle_helper->addDefaultAnglesDeg(0);
232 	}
233 	else
234 	{
235 		// Add base angles
236 		angle_helper->addAngles(0, M_PI / 2);
237 
238 		// Add object angles
239 		for (auto angle : primary_directions)
240 			angle_helper->addAngles(angle, M_PI / 2);
241 	}
242 }
243 
244 
drawBoundingBox(QPainter * painter,MapWidget * widget,const QRectF & bounding_box,const QRgb & color)245 void EditTool::drawBoundingBox(QPainter* painter, MapWidget* widget, const QRectF& bounding_box, const QRgb& color)
246 {
247 	QPen pen(color);
248 	pen.setStyle(Qt::DashLine);
249 	if (scaleFactor() > 1)
250 		pen.setWidth(scaleFactor());
251 	painter->setPen(pen);
252 	painter->setBrush(Qt::NoBrush);
253 	painter->drawRect(widget->mapToViewport(bounding_box));
254 }
255 
256 
drawBoundingPath(QPainter * painter,MapWidget * widget,const std::vector<QPointF> & bounding_path,const QRgb & color)257 void EditTool::drawBoundingPath(QPainter* painter, MapWidget* widget, const std::vector<QPointF>& bounding_path, const QRgb& color)
258 {
259 	Q_ASSERT(!bounding_path.empty());
260 
261 	QPen pen(color);
262 	pen.setStyle(Qt::DashLine);
263 	if (scaleFactor() > 1)
264 		pen.setWidth(scaleFactor());
265 	painter->setPen(pen);
266 	painter->setBrush(Qt::NoBrush);
267 
268 	QPainterPath painter_path;
269 	painter_path.moveTo(widget->mapToViewport(bounding_path[0]));
270 	for (std::size_t i = 1; i < bounding_path.size(); ++i)
271 		painter_path.lineTo(widget->mapToViewport(bounding_path[i]));
272 	painter_path.closeSubpath();
273 	painter->drawPath(painter_path);
274 }
275 
276 
277 }  // namespace OpenOrienteering
278