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