1 /*
2  *    Copyright 2012, 2013 Thomas Schöps
3  *    Copyright 2012-2019 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 "point_symbol_editor_widget.h"
23 
24 #include <algorithm>
25 #include <limits>
26 #include <memory>
27 // IWYU pragma: no_include <ext/alloc_traits.h>
28 
29 #include <Qt>
30 #include <QAbstractItemView>
31 #include <QBoxLayout>
32 #include <QCheckBox>
33 #include <QComboBox>
34 #include <QCursor>
35 #include <QDoubleSpinBox>
36 #include <QFlags>
37 #include <QFontMetrics>
38 #include <QGridLayout>
39 #include <QHBoxLayout>
40 #include <QHeaderView>
41 #include <QIcon>
42 #include <QLabel>
43 #include <QListWidget>
44 #include <QListWidgetItem>
45 #include <QLocale>
46 #include <QMenu>
47 #include <QMouseEvent>
48 #include <QPainter>
49 #include <QPen>
50 #include <QPixmap>
51 #include <QPoint>
52 #include <QPointF>
53 #include <QPushButton>
54 #include <QRectF>
55 #include <QSizePolicy>
56 #include <QStackedWidget>
57 #include <QStringList>
58 #include <QTableWidget>
59 #include <QTableWidgetItem>
60 #include <QToolButton>
61 #include <QVBoxLayout>
62 #include <QVariant>
63 
64 #include "core/map.h"
65 #include "core/objects/object.h"
66 #include "core/symbols/area_symbol.h"
67 #include "core/symbols/line_symbol.h"
68 #include "core/symbols/point_symbol.h"
69 #include "core/symbols/symbol.h"
70 #include "gui/modifier_key.h"
71 #include "gui/map/map_editor.h"
72 #include "gui/map/map_widget.h"
73 #include "gui/util_gui.h"
74 #include "gui/widgets/color_dropdown.h"
75 #include "util/backports.h"  // IWYU pragma: keep
76 
77 // IWYU pragma: no_forward_declare QBoxLayout
78 // IWYU pragma: no_forward_declare QGridLayout
79 // IWYU pragma: no_forward_declare QHBoxLayout
80 // IWYU pragma: no_forward_declare QMenu
81 // IWYU pragma: no_forward_declare QTableWidgetItem
82 // IWYU pragma: no_forward_declare QVBoxLayout
83 
84 
85 namespace OpenOrienteering {
86 
PointSymbolEditorWidget(MapEditorController * controller,PointSymbol * symbol,qreal offset_y,bool permanent_preview,QWidget * parent)87 PointSymbolEditorWidget::PointSymbolEditorWidget(MapEditorController* controller, PointSymbol* symbol, qreal offset_y, bool permanent_preview, QWidget* parent)
88 : QWidget(parent)
89 , symbol(symbol)
90 , object_origin_coord(0, offset_y)
91 , offset_y(offset_y)
92 , controller(controller)
93 , permanent_preview(permanent_preview)
94 {
95 	map = controller->getMap();
96 
97 	midpoint_object = nullptr;
98 	if (permanent_preview)
99 	{
100 		midpoint_object = new PointObject(symbol);
101 		midpoint_object->setPosition(object_origin_coord);
102 		map->addObject(midpoint_object);
103 	}
104 
105 	oriented_to_north = new QCheckBox(tr("Always oriented to north (not rotatable)"));
106 	oriented_to_north->setChecked(!symbol->isRotatable());
107 
108 	QLabel* elements_label = Util::Headline::create(tr("Elements"));
109 	element_list = new QListWidget();
110 	initElementList();
111 
112 	delete_element_button = new QPushButton(QIcon(QString::fromLatin1(":/images/minus.png")), QString{});
113 	QToolButton* add_element_button = new QToolButton();
114 	add_element_button->setIcon(QIcon(QString::fromLatin1(":/images/plus.png")));
115 	add_element_button->setToolButtonStyle(Qt::ToolButtonIconOnly);
116 	add_element_button->setPopupMode(QToolButton::InstantPopup);
117 	add_element_button->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed));
118 	add_element_button->setMinimumSize(delete_element_button->sizeHint());
119 	QMenu* add_element_button_menu = new QMenu(add_element_button);
120 	add_element_button_menu->addAction(tr("Point"), this, SLOT(addPointClicked()));
121 	add_element_button_menu->addAction(tr("Line"), this, SLOT(addLineClicked()));
122 	add_element_button_menu->addAction(tr("Area"), this, SLOT(addAreaClicked()));
123 	add_element_button->setMenu(add_element_button_menu);
124 
125 	center_all_elements_button = new QPushButton(tr("Center all elements"));
126 
127 	QLabel* current_element_label = Util::Headline::create(tr("Current element"));
128 
129 	element_properties_widget = new QStackedWidget();
130 
131 	// Point (circle)
132 	point_properties = new QWidget();
133 	QLabel* point_inner_radius_label = new QLabel(tr("Diameter <b>a</b>:"));
134 	point_inner_radius_edit = Util::SpinBox::create(2, 0.0, 99999.9, tr("mm"));
135 
136 	QLabel* point_inner_color_label = new QLabel(tr("Inner color:"));
137 	point_inner_color_edit = new ColorDropDown(map);
138 
139 	QLabel* point_outer_width_label = new QLabel(tr("Outer width <b>b</b>:"));
140 	point_outer_width_edit = Util::SpinBox::create(2, 0.0, 99999.9, tr("mm"));
141 
142 	QLabel* point_outer_color_label = new QLabel(tr("Outer color:"));
143 	point_outer_color_edit = new ColorDropDown(map);
144 
145 	QLabel* explanation_label = new QLabel();
146 	explanation_label->setPixmap(QPixmap(QString::fromLatin1(":/images/symbol_point_explanation.png")));
147 
148 	QGridLayout* point_layout = new QGridLayout();
149 	point_layout->setContentsMargins(0, 0, 0, 0);
150 	point_layout->addWidget(point_inner_radius_label, 0, 0);
151 	point_layout->addWidget(point_inner_radius_edit, 0, 1);
152 	point_layout->addWidget(point_inner_color_label, 1, 0);
153 	point_layout->addWidget(point_inner_color_edit, 1, 1);
154 	point_layout->addWidget(new QWidget(), 2, 0, 1, -1);
155 	point_layout->addWidget(point_outer_width_label, 3, 0);
156 	point_layout->addWidget(point_outer_width_edit, 3, 1);
157 	point_layout->addWidget(point_outer_color_label, 4, 0);
158 	point_layout->addWidget(point_outer_color_edit, 4, 1);
159 	point_layout->addWidget(explanation_label, 0, 2, 5, 1);
160 	point_layout->setRowStretch(6, 1);
161 	point_layout->setColumnStretch(1,1);
162 	point_properties->setLayout(point_layout);
163 	element_properties_widget->addWidget(point_properties);
164 
165 	// Line
166 	line_properties = new QWidget();
167 	QLabel* line_width_label = new QLabel(tr("Line width:"));
168 	line_width_edit = Util::SpinBox::create(3, 0.0, 99999.9, tr("mm"));
169 
170 	QLabel* line_color_label = new QLabel(tr("Line color:"));
171 	line_color_edit = new ColorDropDown(map);
172 
173 	QLabel* line_cap_label = new QLabel(tr("Line cap:"));
174 	line_cap_edit = new QComboBox();
175 	line_cap_edit->addItem(tr("flat"), QVariant(LineSymbol::FlatCap));
176 	line_cap_edit->addItem(tr("round"), QVariant(LineSymbol::RoundCap));
177 	line_cap_edit->addItem(tr("square"), QVariant(LineSymbol::SquareCap));
178 	//line_cap_edit->addItem(tr("pointed"), QVariant(LineSymbol::PointedCap));	// this would require another input field for the cap length
179 
180 	QLabel* line_join_label = new QLabel(tr("Line join:"));
181 	line_join_edit = new QComboBox();
182 	line_join_edit->addItem(tr("miter"), QVariant(LineSymbol::MiterJoin));
183 	line_join_edit->addItem(tr("round"), QVariant(LineSymbol::RoundJoin));
184 	line_join_edit->addItem(tr("bevel"), QVariant(LineSymbol::BevelJoin));
185 
186 	line_closed_check = new QCheckBox(tr("Line closed"));
187 
188 	QGridLayout* line_layout = new QGridLayout();
189 	line_layout->setContentsMargins(0, 0, 0, 0);
190 	line_layout->addWidget(line_width_label, 0, 0);
191 	line_layout->addWidget(line_width_edit, 0, 1);
192 	line_layout->addWidget(line_color_label, 1, 0);
193 	line_layout->addWidget(line_color_edit, 1, 1);
194 	line_layout->addWidget(line_cap_label, 2, 0);
195 	line_layout->addWidget(line_cap_edit, 2, 1);
196 	line_layout->addWidget(line_join_label, 3, 0);
197 	line_layout->addWidget(line_join_edit, 3, 1);
198 	line_layout->addWidget(line_closed_check, 4, 0, 1, 2);
199 	line_layout->setRowStretch(5, 1);
200 	line_layout->setColumnStretch(1,1);
201 	line_properties->setLayout(line_layout);
202 	element_properties_widget->addWidget(line_properties);
203 
204 	// Area
205 	area_properties = new QWidget();
206 	QLabel* area_color_label = new QLabel(tr("Area color:"));
207 	area_color_edit = new ColorDropDown(map);
208 
209 	QGridLayout* area_layout = new QGridLayout();
210 	area_layout->setContentsMargins(0, 0, 0, 0);
211 	area_layout->addWidget(area_color_label, 0, 0);
212 	area_layout->addWidget(area_color_edit, 0, 1);
213 	area_layout->setRowStretch(1, 1);
214 	area_layout->setColumnStretch(1,1);
215 	area_properties->setLayout(area_layout);
216 	element_properties_widget->addWidget(area_properties);
217 
218 	// Coordinates
219 	coords_label = new QLabel(tr("Coordinates:"));
220 
221 	coords_table = new QTableWidget(0, 3);
222 	coords_table->setEditTriggers(QAbstractItemView::AllEditTriggers);
223 	coords_table->setSelectionBehavior(QAbstractItemView::SelectRows);
224 	coords_table->setHorizontalHeaderLabels(QStringList() << tr("X") << tr("Y") << tr("Curve start"));
225 	coords_table->verticalHeader()->setVisible(false);
226 
227 	QHeaderView* header_view = coords_table->horizontalHeader();
228 	header_view->setSectionResizeMode(0, QHeaderView::Interactive);
229 	header_view->setSectionResizeMode(1, QHeaderView::Interactive);
230 	header_view->setSectionResizeMode(2, QHeaderView::ResizeToContents);
231 	header_view->setSectionsClickable(false);
232 
233 	add_coord_button = new QPushButton(QIcon(QString::fromLatin1(":/images/plus.png")), QString{});
234 	delete_coord_button = new QPushButton(QIcon(QString::fromLatin1(":/images/minus.png")), QString{});
235 	center_coords_button = new QPushButton(tr("Center by coordinate average"));
236 
237 	// Layout
238 	QBoxLayout* left_layout = new QVBoxLayout();
239 
240 	left_layout->addWidget(elements_label);
241 	left_layout->addWidget(element_list, 1);
242 	QGridLayout* element_buttons_layout = new QGridLayout();
243 	element_buttons_layout->setColumnStretch(0, 1);
244 	element_buttons_layout->addWidget(add_element_button, 1, 2);
245 	element_buttons_layout->addWidget(delete_element_button, 1, 3);
246 	element_buttons_layout->addWidget(center_all_elements_button, 2, 1, 1, 4);
247 	element_buttons_layout->setColumnStretch(5, 1);
248 	left_layout->addLayout(element_buttons_layout);
249 
250 	QBoxLayout* right_layout = new QVBoxLayout();
251 	right_layout->setMargin(0);
252 
253 	right_layout->addWidget(current_element_label);
254 	right_layout->addWidget(element_properties_widget);
255 	right_layout->addSpacing(16);
256 
257 	right_layout->addWidget(coords_label);
258 	right_layout->addWidget(coords_table);
259 	QHBoxLayout* coords_buttons_layout = new QHBoxLayout();
260 	coords_buttons_layout->addWidget(add_coord_button);
261 	coords_buttons_layout->addWidget(delete_coord_button);
262 	coords_buttons_layout->addStretch(1);
263 	coords_buttons_layout->addWidget(center_coords_button);
264 	right_layout->addLayout(coords_buttons_layout);
265 
266 	QBoxLayout* columns_layout = new QHBoxLayout;
267 	columns_layout->addLayout(left_layout);
268 	columns_layout->addSpacing(16);
269 	columns_layout->addLayout(right_layout);
270 
271 	QVBoxLayout* layout = new QVBoxLayout();
272 	layout->addWidget(oriented_to_north);
273 	layout->addSpacerItem(Util::SpacerItem::create(this));
274 	layout->addLayout(columns_layout);
275 	setLayout(layout);
276 
277 	// Connections
278 	connect(oriented_to_north, &QCheckBox::clicked, this, &PointSymbolEditorWidget::orientedToNorthClicked);
279 
280 	connect(element_list, &QListWidget::currentRowChanged, this, &PointSymbolEditorWidget::changeElement);
281 	connect(delete_element_button, &QPushButton::clicked, this, &PointSymbolEditorWidget::deleteCurrentElement);
282 	connect(center_all_elements_button, &QPushButton::clicked, this, &PointSymbolEditorWidget::centerAllElements);
283 
284 	connect(point_inner_radius_edit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &PointSymbolEditorWidget::pointInnerRadiusChanged);
285 	connect(point_inner_color_edit, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PointSymbolEditorWidget::pointInnerColorChanged);
286 	connect(point_outer_width_edit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &PointSymbolEditorWidget::pointOuterWidthChanged);
287 	connect(point_outer_color_edit, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PointSymbolEditorWidget::pointOuterColorChanged);
288 
289 	connect(line_width_edit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &PointSymbolEditorWidget::lineWidthChanged);
290 	connect(line_color_edit, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PointSymbolEditorWidget::lineColorChanged);
291 	connect(line_cap_edit, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PointSymbolEditorWidget::lineCapChanged);
292 	connect(line_join_edit, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PointSymbolEditorWidget::lineJoinChanged);
293 	connect(line_closed_check, &QCheckBox::clicked, this, &PointSymbolEditorWidget::lineClosedClicked);
294 
295 	connect(area_color_edit, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PointSymbolEditorWidget::areaColorChanged);
296 
297 	connect(coords_table, &QTableWidget::currentCellChanged, this, &PointSymbolEditorWidget::updateDeleteCoordButton);
298 	connect(coords_table, &QTableWidget::cellChanged, this, &PointSymbolEditorWidget::coordinateChanged);
299 	connect(add_coord_button, &QPushButton::clicked, this, &PointSymbolEditorWidget::addCoordClicked);
300 	connect(delete_coord_button, &QPushButton::clicked, this, &PointSymbolEditorWidget::deleteCoordClicked);
301 	connect(center_coords_button, &QPushButton::clicked, this, &PointSymbolEditorWidget::centerCoordsClicked);
302 }
303 
~PointSymbolEditorWidget()304 PointSymbolEditorWidget::~PointSymbolEditorWidget()
305 {
306 	if (isVisible())
307 		setEditorActive(false);
308 	if (permanent_preview)
309 		map->deleteObject(midpoint_object);
310 }
311 
setEditorActive(bool active)312 void PointSymbolEditorWidget::setEditorActive(bool active)
313 {
314 	if (active)
315 	{
316 		if (!permanent_preview && !midpoint_object)
317 		{
318 			midpoint_object = new PointObject(symbol);
319 			midpoint_object->setPosition(object_origin_coord);
320 			map->addObject(midpoint_object);
321 		}
322 		map->updateAllObjectsWithSymbol(symbol);
323 		controller->setTool(new PointSymbolEditorTool(controller, this));
324 		activity = new PointSymbolEditorActivity(map, this);
325 		controller->setEditorActivity(activity);
326 		changeElement(element_list->currentRow());
327 	}
328 	else
329 	{
330 		controller->setTool(nullptr);
331 		controller->setEditorActivity(nullptr);
332 		if (!permanent_preview && midpoint_object)
333 		{
334 			map->deleteObject(midpoint_object);
335 			midpoint_object = nullptr;
336 		}
337 	}
338 }
339 
setVisible(bool visible)340 void PointSymbolEditorWidget::setVisible(bool visible)
341 {
342 	setEditorActive(visible);
343 	QWidget::setVisible(visible);
344 }
345 
changeCurrentCoordinate(const MapCoordF & new_coord)346 bool PointSymbolEditorWidget::changeCurrentCoordinate(const MapCoordF& new_coord)
347 {
348 	Object* object = getCurrentElementObject();
349 	if (object == midpoint_object)
350 		return false;
351 
352 	if (object->getType() == Object::Point)
353 	{
354 		auto point = static_cast<PointObject*>(object);
355 		MapCoordF coord = point->getCoordF();
356 		coord.setX(new_coord.x());
357 		coord.setY(new_coord.y() - offset_y);
358 		point->setPosition(coord);
359 	}
360 	else
361 	{
362 		auto table_row = coords_table->currentRow();
363 		if (table_row < 0)
364 			return false;
365 
366 		PathObject* path = object->asPath();
367 
368 		auto coord_index = MapCoordVector::size_type(table_row);
369 		Q_ASSERT(coord_index < path->getCoordinateCount());
370 
371 		auto coord = path->getCoordinate(coord_index);
372 		coord.setX(new_coord.x());
373 		coord.setY(new_coord.y() - offset_y);
374 		path->setCoordinate(coord_index, coord);
375 	}
376 
377 	updateCoordsTable();
378 	map->updateAllObjectsWithSymbol(symbol);
379 	emit symbolEdited();
380 	return true;
381 }
382 
addCoordinate(const MapCoordF & new_coord)383 bool PointSymbolEditorWidget::addCoordinate(const MapCoordF& new_coord)
384 {
385 	Object* object = getCurrentElementObject();
386 	if (object == midpoint_object)
387 		return false;
388 
389 	if (object->getType() == Object::Point)
390 		return changeCurrentCoordinate(new_coord);
391 	Q_ASSERT(object->getType() == Object::Path);
392 	auto path = static_cast<PathObject*>(object);
393 
394 	auto table_row = coords_table->currentRow();
395 	if (table_row < 0)
396 		table_row = coords_table->rowCount();
397 	else
398 		++table_row;
399 
400 	auto coord_index = MapCoordVector::size_type(table_row);
401 	path->addCoordinate(coord_index, { new_coord.x(), new_coord.y() - offset_y });
402 
403 	if (path->getCoordinateCount() == 1)
404 	{
405 		if (object->getSymbol()->getType() == Symbol::Area)
406 			path->parts().front().setClosed(true, false);
407 	}
408 
409 	updateCoordsTable();
410 	coords_table->setCurrentItem(coords_table->item(table_row, (coords_table->currentColumn() < 0) ? 0 : coords_table->currentColumn()));
411 	map->updateAllObjectsWithSymbol(symbol);
412 	emit symbolEdited();
413 	return true;
414 }
415 
initElementList()416 void PointSymbolEditorWidget::initElementList()
417 {
418 	element_list->clear();
419 	element_list->addItem(tr("[Midpoint]"));	// NOTE: Is that item needed?
420 	for (int i = 0; i < symbol->getNumElements(); ++i)
421 	{
422 		Symbol* element_symbol = symbol->getElementSymbol(i);
423 		element_list->addItem(getLabelForSymbol(element_symbol));
424 	}
425 	element_list->setCurrentRow(0);
426 }
427 
orientedToNorthClicked(bool checked)428 void PointSymbolEditorWidget::orientedToNorthClicked(bool checked)
429 {
430 	symbol->setRotatable(!checked);
431 	emit symbolEdited();
432 }
433 
changeElement(int row)434 void PointSymbolEditorWidget::changeElement(int row)
435 {
436 	delete_element_button->setEnabled(row > 0);	// must not remove first row
437 	center_all_elements_button->setEnabled(symbol->getNumElements() > 0);
438 
439 	if (row >= 0)
440 	{
441 		Symbol* element_symbol = getCurrentElementSymbol();
442 		Object* object = getCurrentElementObject();
443 
444 		if (object->getType() == Object::Path)
445 		{
446 			auto path = static_cast<PathObject*>(object);
447 			if (element_symbol->getType() == Symbol::Line)
448 			{
449 				line_width_edit->blockSignals(true);
450 				line_color_edit->blockSignals(true);
451 				line_cap_edit->blockSignals(true);
452 				line_join_edit->blockSignals(true);
453 				line_closed_check->blockSignals(true);
454 
455 				auto line = static_cast<LineSymbol*>(element_symbol);
456 				line_width_edit->setValue(0.001 * line->getLineWidth());
457 				line_color_edit->setColor(line->getColor());
458 				line_cap_edit->setCurrentIndex(line_cap_edit->findData(QVariant(line->getCapStyle())));
459 				line_join_edit->setCurrentIndex(line_join_edit->findData(QVariant(line->getJoinStyle())));
460 
461 				const auto& parts = path->parts();
462 				line_closed_check->setChecked(!parts.empty() && parts.front().isClosed());
463 				line_closed_check->setEnabled(!parts.empty());
464 
465 				line_width_edit->blockSignals(false);
466 				line_color_edit->blockSignals(false);
467 				line_cap_edit->blockSignals(false);
468 				line_join_edit->blockSignals(false);
469 				line_closed_check->blockSignals(false);
470 
471 				element_properties_widget->setCurrentWidget(line_properties);
472 			}
473 			else if (element_symbol->getType() == Symbol::Area)
474 			{
475 				area_color_edit->blockSignals(true);
476 
477 				auto area = static_cast<AreaSymbol*>(element_symbol);
478 				area_color_edit->setColor(area->getColor());
479 
480 				area_color_edit->blockSignals(false);
481 
482 				element_properties_widget->setCurrentWidget(area_properties);
483 			}
484 
485 			coords_label->setEnabled(true);
486 			coords_table->setEnabled(true);
487 			coords_table->setColumnHidden(2, false);
488 			add_coord_button->setEnabled(true);
489 			center_coords_button->setEnabled(path->getCoordinateCount() > 0);
490 		}
491 		else
492 		{
493 			point_inner_radius_edit->blockSignals(true);
494 			point_inner_color_edit->blockSignals(true);
495 			point_outer_width_edit->blockSignals(true);
496 			point_outer_width_edit->blockSignals(true);
497 
498 			auto point = static_cast<PointSymbol*>(element_symbol);
499 			point_inner_radius_edit->setValue(2 * 0.001 * point->getInnerRadius());
500 			point_inner_color_edit->setColor(point->getInnerColor());
501 			point_outer_width_edit->setValue(0.001 * point->getOuterWidth());
502 			point_outer_color_edit->setColor(point->getOuterColor());
503 
504 			point_inner_radius_edit->blockSignals(false);
505 			point_inner_color_edit->blockSignals(false);
506 			point_outer_width_edit->blockSignals(false);
507 			point_outer_width_edit->blockSignals(false);
508 
509 			element_properties_widget->setCurrentWidget(point_properties);
510 
511 			coords_table->setColumnHidden(2, true);
512 			coords_label->setEnabled(row > 0);
513 			coords_table->setEnabled(row > 0);
514 			add_coord_button->setEnabled(false);
515 			center_coords_button->setEnabled(row > 0);
516 		}
517 
518 		coords_table->clearContents();
519 	}
520 
521 	if (row > 0)
522 		updateCoordsTable();
523 	else
524 		coords_table->setRowCount(0);
525 
526 	delete_coord_button->setEnabled(false);
527 }
528 
addPointClicked()529 void PointSymbolEditorWidget::addPointClicked()
530 {
531 	PointSymbol* new_point = new PointSymbol();
532 	PointObject* new_object = new PointObject(new_point);
533 
534 	insertElement(new_object, new_point);
535 }
536 
addLineClicked()537 void PointSymbolEditorWidget::addLineClicked()
538 {
539 	LineSymbol* new_line = new LineSymbol();
540 	PathObject* new_object = new PathObject(new_line);
541 
542 	insertElement(new_object, new_line);
543 }
544 
addAreaClicked()545 void PointSymbolEditorWidget::addAreaClicked()
546 {
547 	AreaSymbol* new_area = new AreaSymbol();
548 	PathObject* new_object = new PathObject(new_area);
549 
550 	insertElement(new_object, new_area);
551 }
552 
deleteCurrentElement()553 void PointSymbolEditorWidget::deleteCurrentElement()
554 {
555 	int row = element_list->currentRow();
556 	Q_ASSERT(row > 0);
557 	delete element_list->item(row);
558 	symbol->deleteElement(row - 1);
559 	map->updateAllObjectsWithSymbol(symbol);
560 	emit symbolEdited();
561 }
562 
centerAllElements()563 void PointSymbolEditorWidget::centerAllElements()
564 {
565 	bool has_coordinates = false;
566 	auto min_x = std::numeric_limits<qint32>::max();
567 	auto max_x = std::numeric_limits<qint32>::min();
568 	auto min_y = std::numeric_limits<qint32>::max();
569 	auto max_y = std::numeric_limits<qint32>::min();
570 
571 	for (int i = 0; i < symbol->getNumElements(); ++i)
572 	{
573 		Object* object = symbol->getElementObject(i);
574 		for (const auto& coord : object->getRawCoordinateVector())
575 		{
576 			min_x = std::min(min_x, coord.nativeX());
577 			min_y = std::min(min_y, coord.nativeY());
578 			max_x = std::max(max_x, coord.nativeX());
579 			max_y = std::max(max_y, coord.nativeY());
580 			has_coordinates = true;
581 		}
582 	}
583 
584 	if (has_coordinates)
585 	{
586 		auto center_x = (min_x + max_x) / 2;
587 		auto center_y = (min_y + max_y) / 2;
588 
589 		for (int i = 0; i < symbol->getNumElements(); ++i)
590 		{
591 			auto object = symbol->getElementObject(i);
592 			object->move(-center_x, -center_y);
593 			object->update();
594 		}
595 	}
596 
597 	if (element_list->currentRow() > 0)
598 		updateCoordsTable();
599 	emit symbolEdited();
600 }
601 
pointInnerRadiusChanged(double value)602 void PointSymbolEditorWidget::pointInnerRadiusChanged(double value)
603 {
604 	auto symbol = static_cast<PointSymbol*>(getCurrentElementSymbol());
605 	symbol->inner_radius = qRound(1000 * 0.5 * value);
606 	map->updateAllObjectsWithSymbol(symbol);
607 	emit symbolEdited();
608 }
609 
pointInnerColorChanged()610 void PointSymbolEditorWidget::pointInnerColorChanged()
611 {
612 	auto symbol = static_cast<PointSymbol*>(getCurrentElementSymbol());
613 	symbol->inner_color = point_inner_color_edit->color();
614 	map->updateAllObjectsWithSymbol(symbol);
615 	emit symbolEdited();
616 }
617 
pointOuterWidthChanged(double value)618 void PointSymbolEditorWidget::pointOuterWidthChanged(double value)
619 {
620 	auto symbol = static_cast<PointSymbol*>(getCurrentElementSymbol());
621 	symbol->outer_width = qRound(1000 * value);
622 	map->updateAllObjectsWithSymbol(symbol);
623 	emit symbolEdited();
624 }
625 
pointOuterColorChanged()626 void PointSymbolEditorWidget::pointOuterColorChanged()
627 {
628 	auto symbol = static_cast<PointSymbol*>(getCurrentElementSymbol());
629 	symbol->outer_color = point_outer_color_edit->color();
630 	map->updateAllObjectsWithSymbol(symbol);
631 	emit symbolEdited();
632 }
633 
lineWidthChanged(double value)634 void PointSymbolEditorWidget::lineWidthChanged(double value)
635 {
636 	auto symbol = static_cast<LineSymbol*>(getCurrentElementSymbol());
637 	symbol->line_width = qRound(1000 * value);
638 	map->updateAllObjectsWithSymbol(symbol);
639 	emit symbolEdited();
640 }
641 
lineColorChanged()642 void PointSymbolEditorWidget::lineColorChanged()
643 {
644 	auto symbol = static_cast<LineSymbol*>(getCurrentElementSymbol());
645 	symbol->color = line_color_edit->color();
646 	map->updateAllObjectsWithSymbol(symbol);
647 	emit symbolEdited();
648 }
649 
lineCapChanged(int index)650 void PointSymbolEditorWidget::lineCapChanged(int index)
651 {
652 	auto symbol = static_cast<LineSymbol*>(getCurrentElementSymbol());
653 	symbol->cap_style = static_cast<LineSymbol::CapStyle>(line_cap_edit->itemData(index).toInt());
654 	map->updateAllObjectsWithSymbol(symbol);
655 	emit symbolEdited();
656 }
657 
lineJoinChanged(int index)658 void PointSymbolEditorWidget::lineJoinChanged(int index)
659 {
660 	auto symbol = static_cast<LineSymbol*>(getCurrentElementSymbol());
661 	symbol->join_style = static_cast<LineSymbol::JoinStyle>(line_join_edit->itemData(index).toInt());
662 	map->updateAllObjectsWithSymbol(symbol);
663 	emit symbolEdited();
664 }
665 
lineClosedClicked(bool checked)666 void PointSymbolEditorWidget::lineClosedClicked(bool checked)
667 {
668 	Object* object = getCurrentElementObject();
669 	Q_ASSERT(object->getType() == Object::Path);
670 	auto path = static_cast<PathObject*>(object);
671 
672 	if (!checked && path->getCoordinateCount() >= 4 && path->getCoordinate(path->getCoordinateCount() - 4).isCurveStart())
673 		path->getCoordinateRef(path->getCoordinateCount() - 4).setCurveStart(false);
674 
675 	Q_ASSERT(!path->parts().empty());
676 	path->parts().front().setClosed(checked, true);
677 	if (!checked)
678 		path->deleteCoordinate(path->getCoordinateCount() - 1, false);
679 
680 	updateCoordsTable();
681 	map->updateAllObjectsWithSymbol(symbol);
682 	emit symbolEdited();
683 }
684 
areaColorChanged()685 void PointSymbolEditorWidget::areaColorChanged()
686 {
687 	auto symbol = static_cast<AreaSymbol*>(getCurrentElementSymbol());
688 	symbol->color = area_color_edit->color();
689 	map->updateAllObjectsWithSymbol(symbol);
690 	emit symbolEdited();
691 }
692 
coordinateChanged(int row,int column)693 void PointSymbolEditorWidget::coordinateChanged(int row, int column)
694 {
695 	Object* object = getCurrentElementObject();
696 	if (!object || !midpoint_object)
697 		return;
698 
699 	auto coord_index = MapCoordVector::size_type(row);
700 
701 	if (column < 2)
702 	{
703 		auto coord = MapCoord{};
704 		if (object->getType() == Object::Point)
705 		{
706 			auto point = static_cast<PointObject*>(object);
707 			coord = point->getCoord();
708 		}
709 		else if (object->getType() == Object::Path)
710 		{
711 			auto path = static_cast<const PathObject*>(object);
712 			Q_ASSERT(coord_index < path->getCoordinateCount());
713 			coord = path->getCoordinate(coord_index);
714 		}
715 
716 		QLocale locale;
717 		auto ok = false;
718 		auto new_value = qRound(1000 * locale.toDouble(coords_table->item(row, column)->text(), &ok));
719 
720 		if (ok)
721 		{
722 			if (column == 0)
723 				coord.setNativeX(new_value);
724 			else
725 				coord.setNativeY(-new_value);
726 
727 			if (object->getType() == Object::Point)
728 			{
729 				auto point = static_cast<PointObject*>(object);
730 				point->setPosition(coord);
731 			}
732 			else if (object->getType() == Object::Path)
733 			{
734 				auto path = static_cast<PathObject*>(object);
735 				path->setCoordinate(coord_index, coord);
736 			}
737 
738 			map->updateAllObjectsWithSymbol(symbol);
739 			emit symbolEdited();
740 		}
741 
742 		// Update, needed in cases of rounding and error
743 		coords_table->blockSignals(true);
744 		coords_table->item(row, column)->setText(locale.toString((column == 0) ? coord.x() : -coord.y(), 'f', 3));
745 		coords_table->blockSignals(false);
746 	}
747 	else
748 	{
749 		Q_ASSERT(object->getType() == Object::Path);
750 		auto path = static_cast<PathObject*>(object);
751 		Q_ASSERT(coord_index < path->getCoordinateCount());
752 		auto coord = path->getCoordinate(coord_index);
753 		coord.setCurveStart(coords_table->item(row, column)->checkState() == Qt::Checked);
754 		path->setCoordinate(coord_index, coord);
755 
756 		updateCoordsTable();
757 		map->updateAllObjectsWithSymbol(symbol);
758 		emit symbolEdited();
759 	}
760 }
761 
addCoordClicked()762 void PointSymbolEditorWidget::addCoordClicked()
763 {
764 	Object* object = getCurrentElementObject();
765 	Q_ASSERT(object->getType() == Object::Path);
766 	auto path = static_cast<PathObject*>(object);
767 
768 	if (coords_table->currentRow() < 0)
769 		path->addCoordinate(MapCoordVector::size_type(coords_table->rowCount()), MapCoord(0, 0));
770 	else
771 		path->addCoordinate(MapCoordVector::size_type(coords_table->currentRow()) + 1, path->getCoordinate(MapCoordVector::size_type(coords_table->currentRow())));
772 
773 	int row = (coords_table->currentRow() < 0) ? coords_table->rowCount() : (coords_table->currentRow() + 1);
774 	updateCoordsTable();	// NOTE: incremental updates (to the curve start boxes) would be possible but mean some implementation effort
775 	coords_table->setCurrentItem(coords_table->item(row, coords_table->currentColumn()));
776 }
777 
deleteCoordClicked()778 void PointSymbolEditorWidget::deleteCoordClicked()
779 {
780 	Object* object = getCurrentElementObject();
781 	Q_ASSERT(object->getType() == Object::Path);
782 	auto path = static_cast<PathObject*>(object);
783 
784 	int row = coords_table->currentRow();
785 	if (row < 0)
786 		return;
787 
788 	path->deleteCoordinate(MapCoordVector::size_type(row), false);
789 
790 	updateCoordsTable();	// NOTE: incremental updates (to the curve start boxes) would be possible but mean some implementation effort
791 	center_coords_button->setEnabled(path->getCoordinateCount() > 0);
792 	updateDeleteCoordButton();
793 	map->updateAllObjectsWithSymbol(symbol);
794 	emit symbolEdited();
795 }
796 
centerCoordsClicked()797 void PointSymbolEditorWidget::centerCoordsClicked()
798 {
799 	Object* object = getCurrentElementObject();
800 
801 	if (object->getType() == Object::Point)
802 	{
803 		auto point = static_cast<PointObject*>(object);
804 		point->setPosition(0, 0);
805 	}
806 	else
807 	{
808 		Q_ASSERT(object->getType() == Object::Path);
809 		auto path = static_cast<PathObject*>(object);
810 
811 		MapCoordF center = MapCoordF(0, 0);
812 		auto size = path->getCoordinateCount();
813 		auto change_size = (!path->parts().empty() && path->parts().front().isClosed()) ? (size - 1) : size;
814 
815 		Q_ASSERT(change_size > 0);
816 		for (auto i = 0u; i < change_size; ++i)
817 			center = MapCoordF(center.x() + path->getCoordinate(i).x(), center.y() + path->getCoordinate(i).y());
818 		center = MapCoordF(center.x() / change_size, center.y() / change_size);
819 
820 		path->move(MapCoord{-center});
821 		path->update();
822 	}
823 
824 	updateCoordsTable();
825 	map->updateAllObjectsWithSymbol(symbol);
826 	emit symbolEdited();
827 }
828 
updateCoordsTable()829 void PointSymbolEditorWidget::updateCoordsTable()
830 {
831 	Object* object = getCurrentElementObject();
832 	int num_rows;
833 	if (object->getType() == Object::Point)
834 		num_rows = 1;
835 	else
836 	{
837 		auto path = static_cast<PathObject*>(object);
838 		num_rows = int(path->getCoordinateCount());
839 		if (num_rows > 0 && path->parts().front().isClosed())
840 			--num_rows;
841 		if (path->getSymbol()->getType() == Symbol::Line)
842 			line_closed_check->setEnabled(num_rows > 0);
843 	}
844 
845 	coords_table->setRowCount(num_rows);
846 	for (int i = 0; i < num_rows; ++i)
847 		addCoordsRow(i);
848 
849 	center_coords_button->setEnabled(num_rows > 0);
850 }
851 
addCoordsRow(int row)852 void PointSymbolEditorWidget::addCoordsRow(int row)
853 {
854 	coords_table->setRowHeight(row, coords_table->fontMetrics().height() + 2);
855 
856 	coords_table->blockSignals(true);
857 
858 	for (int c = 0; c < 3; ++c)
859 	{
860 		if (!coords_table->item(row, c))
861 		{
862 			QTableWidgetItem* item = new QTableWidgetItem();
863 			if (c < 2)
864 			{
865 				item->setFlags(Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
866 				item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
867 			}
868 			coords_table->setItem(row, c, item);
869 		}
870 	}
871 	updateCoordsRow(row);
872 
873 	coords_table->blockSignals(false);
874 }
875 
updateCoordsRow(int row)876 void PointSymbolEditorWidget::updateCoordsRow(int row)
877 {
878 	Q_ASSERT(element_list->currentRow() > 0);
879 	Object* object = getCurrentElementObject();
880 	auto coord_index = MapCoordVector::size_type(row);
881 
882 	MapCoordF coordF(0, 0);
883 	if (object->getType() == Object::Point)
884 		coordF = static_cast<const PointObject*>(object)->getCoordF();
885 	else if (object->getType() == Object::Path)
886 		coordF = MapCoordF(static_cast<const PathObject*>(object)->getCoordinate(coord_index));
887 
888 	QLocale locale;
889 	coords_table->item(row, 0)->setText(locale.toString(coordF.x(), 'f', 3));
890 	coords_table->item(row, 1)->setText(locale.toString(-coordF.y(), 'f', 3));
891 
892 	if (object->getType() == Object::Path)
893 	{
894 		auto path = static_cast<const PathObject*>(object);
895 		bool has_curve_start_box = coord_index+3 < path->getCoordinateCount()
896 		                           && (!path->getCoordinate(coord_index+1).isCurveStart() && !path->getCoordinate(coord_index+2).isCurveStart())
897 		                           && (row <= 0 || !path->getCoordinate(coord_index-1).isCurveStart())
898 		                           && (row <= 1 || !path->getCoordinate(coord_index-2).isCurveStart());
899 		if (has_curve_start_box)
900 		{
901 			coords_table->item(row, 2)->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable);
902 			coords_table->item(row, 2)->setCheckState(path->getCoordinate(coord_index).isCurveStart() ? Qt::Checked : Qt::Unchecked);
903 			return;
904 		}
905 	}
906 
907 	if (coords_table->item(row, 2)->flags() & Qt::ItemIsUserCheckable)
908 		coords_table->setItem(row, 2, new QTableWidgetItem());	// remove checkbox by replacing the item with a new one - is there a better way?
909 	coords_table->item(row, 2)->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
910 }
911 
updateDeleteCoordButton()912 void PointSymbolEditorWidget::updateDeleteCoordButton()
913 {
914 	const bool has_coords = coords_table->rowCount() > 0;
915 	const bool is_point = getCurrentElementObject()->getType() == Object::Point;
916 	bool part_of_curve = false;
917 
918 	if (!is_point)
919 	{
920 		auto path = static_cast<const PathObject*>(getCurrentElementObject());
921 		for (int i = 1; i < 4; i++)
922 		{
923 			int row = coords_table->currentRow() - i;
924 			if (row >= 0 && path->getCoordinate(MapCoordVector::size_type(row)).isCurveStart())
925 				part_of_curve = true;
926 		}
927 	}
928 
929 	delete_coord_button->setEnabled(has_coords && !is_point && !part_of_curve);
930 }
931 
insertElement(Object * object,Symbol * element_symbol)932 void PointSymbolEditorWidget::insertElement(Object* object, Symbol* element_symbol)
933 {
934 	int row = (element_list->currentRow() < 0) ? element_list->count() : (element_list->currentRow() + 1);
935 	int pos = row - 1;
936 	symbol->addElement(pos, object, element_symbol);
937 	element_list->insertItem(row, getLabelForSymbol(element_symbol));
938 	element_list->setCurrentRow(row);
939 	map->updateAllObjectsWithSymbol(symbol);
940 	emit symbolEdited();
941 }
942 
getLabelForSymbol(const Symbol * symbol) const943 QString PointSymbolEditorWidget::getLabelForSymbol(const Symbol* symbol) const
944 {
945 	if (symbol->getType() == Symbol::Point)
946 		return tr("Point");						// FIXME: This is rather a circle.
947 	else if (symbol->getType() == Symbol::Line)
948 		return tr("Line");
949 	else if (symbol->getType() == Symbol::Area)
950 		return tr("Area");
951 
952 	Q_ASSERT(false);
953 	return tr("Unknown");
954 }
955 
getCurrentElementObject()956 Object* PointSymbolEditorWidget::getCurrentElementObject()
957 {
958 	if (element_list->currentRow() > 0)
959 		return symbol->getElementObject(element_list->currentRow() - 1);
960 	else
961 		return midpoint_object;
962 }
963 
getCurrentElementSymbol()964 Symbol* PointSymbolEditorWidget::getCurrentElementSymbol()
965 {
966 	if (element_list->currentRow() > 0)
967 		return symbol->getElementSymbol(element_list->currentRow() - 1);
968 	else
969 		return symbol;
970 }
971 
972 
973 
974 // ### PointSymbolEditorTool ###
975 
PointSymbolEditorTool(MapEditorController * editor,PointSymbolEditorWidget * symbol_editor)976 PointSymbolEditorTool::PointSymbolEditorTool(MapEditorController* editor, PointSymbolEditorWidget* symbol_editor)
977 : MapEditorTool(editor, Other)
978 , symbol_editor(symbol_editor)
979 {
980 	// nothing
981 }
982 
983 PointSymbolEditorTool::~PointSymbolEditorTool() = default;
984 
init()985 void PointSymbolEditorTool::init()
986 {
987 	setStatusBarText(tr("<b>Click</b>: Add a coordinate. <b>%1+Click</b>: Change the selected coordinate. ").arg(ModifierKey::control()));
988 
989 	MapEditorTool::init();
990 }
991 
mousePressEvent(QMouseEvent * event,const MapCoordF & map_coord,MapWidget * map_widget)992 bool PointSymbolEditorTool::mousePressEvent(QMouseEvent* event, const MapCoordF& map_coord, MapWidget* map_widget)
993 {
994 	Q_UNUSED(map_widget);
995 	if (event->button() == Qt::LeftButton)
996 	{
997 		if (event->modifiers() & Qt::ControlModifier)
998 			symbol_editor->changeCurrentCoordinate(map_coord);
999 		else
1000 			symbol_editor->addCoordinate(map_coord);
1001 		return true;
1002 	}
1003 	return false;
1004 }
1005 
getCursor() const1006 const QCursor& PointSymbolEditorTool::getCursor() const
1007 {
1008 	static auto const cursor = scaledToScreen(QCursor{ QPixmap(QString::fromLatin1(":/images/cursor-crosshair.png")), 11, 11 });
1009 	return cursor;
1010 }
1011 
1012 
1013 
1014 // ### PointSymbolEditorActivity ###
1015 
1016 const int PointSymbolEditorActivity::cross_radius = 5;
1017 
PointSymbolEditorActivity(Map * map,PointSymbolEditorWidget * symbol_editor)1018 PointSymbolEditorActivity::PointSymbolEditorActivity(Map* map, PointSymbolEditorWidget* symbol_editor)
1019 : MapEditorActivity()
1020 , map(map)
1021 , symbol_editor(symbol_editor)
1022 {
1023 	// nothing
1024 }
1025 
1026 PointSymbolEditorActivity::~PointSymbolEditorActivity() = default;
1027 
init()1028 void PointSymbolEditorActivity::init()
1029 {
1030 	update();
1031 }
1032 
update()1033 void PointSymbolEditorActivity::update()
1034 {
1035 	QRectF rect = QRectF(0.0, symbol_editor->offset_y, 0.0, 0.0);
1036 	map->setActivityBoundingBox(rect, cross_radius + 1);
1037 }
1038 
draw(QPainter * painter,MapWidget * map_widget)1039 void PointSymbolEditorActivity::draw(QPainter* painter, MapWidget* map_widget)
1040 {
1041 	QPoint midpoint = map_widget->mapToViewport(symbol_editor->object_origin_coord).toPoint();
1042 
1043 	QPen pen = QPen(Qt::white);
1044 	pen.setWidth(3);
1045 	painter->setPen(pen);
1046 	painter->drawLine(midpoint + QPoint(0, -cross_radius), midpoint + QPoint(0, cross_radius));
1047 	painter->drawLine(midpoint + QPoint(-cross_radius, 0), midpoint + QPoint(cross_radius, 0));
1048 
1049 	pen.setWidth(0);
1050 	pen.setColor(Qt::black);
1051 	painter->setPen(pen);
1052 	painter->drawLine(midpoint + QPoint(0, -cross_radius), midpoint + QPoint(0, cross_radius));
1053 	painter->drawLine(midpoint + QPoint(-cross_radius, 0), midpoint + QPoint(cross_radius, 0));
1054 }
1055 
1056 
1057 }  // namespace OpenOrienteering
1058