diff --git a/images/cursor-move-parallel.png b/images/cursor-move-parallel.png
new file mode 100644
index 0000000000..cb18f43ed7
Binary files /dev/null and b/images/cursor-move-parallel.png differ
diff --git a/images/svg/cursor-move-parallel.svg b/images/svg/cursor-move-parallel.svg
new file mode 100644
index 0000000000..3367f33601
--- /dev/null
+++ b/images/svg/cursor-move-parallel.svg
@@ -0,0 +1,181 @@
+
+
diff --git a/images/svg/tool-move-parallel.svg b/images/svg/tool-move-parallel.svg
new file mode 100644
index 0000000000..7bd9c1c9f4
--- /dev/null
+++ b/images/svg/tool-move-parallel.svg
@@ -0,0 +1,120 @@
+
+
diff --git a/images/tool-move-parallel.png b/images/tool-move-parallel.png
new file mode 100644
index 0000000000..e88742313f
Binary files /dev/null and b/images/tool-move-parallel.png differ
diff --git a/resources.qrc b/resources.qrc
index a3fd91f570..4b55c51a5f 100644
--- a/resources.qrc
+++ b/resources.qrc
@@ -26,6 +26,7 @@
images/cursor-georeferencing-move.png
images/cursor-hollow.png
images/cursor-invisible.png
+ images/cursor-move-parallel.png
images/cursor-rotate.png
images/cursor-scale.png
images/cut.png
@@ -102,6 +103,7 @@
images/tool-fill-border.png
images/tool-gps-display.png
images/tool-measure.png
+ images/tool-move-parallel.png
images/tool-rotate.png
images/tool-rotate-pattern.png
images/tool-scale.png
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index a4b7d1454d..59cc2f44e3 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -240,6 +240,7 @@ set(Mapper_Common_SRCS
tools/object_selector.cpp
tools/pan_tool.cpp
tools/point_handles.cpp
+ tools/move_parallel_tool.cpp
tools/rotate_tool.cpp
tools/rotate_pattern_tool.cpp
tools/scale_tool.cpp
diff --git a/src/core/objects/object.cpp b/src/core/objects/object.cpp
index 7e0804c7f0..be6f6d3114 100644
--- a/src/core/objects/object.cpp
+++ b/src/core/objects/object.cpp
@@ -1160,7 +1160,7 @@ ClosestPathCoord PathObject::findClosestPointTo(
using distance_type = decltype(ClosestPathCoord::distance_squared);
return std::accumulate(begin(path_parts), end(path_parts),
- ClosestPathCoord { {}, std::numeric_limits::max() },
+ ClosestPathCoord { {}, {}, std::numeric_limits::max() },
op);
}
diff --git a/src/core/path_coord.h b/src/core/path_coord.h
index 8d093ff942..bea204ed80 100644
--- a/src/core/path_coord.h
+++ b/src/core/path_coord.h
@@ -158,11 +158,17 @@ class PathCoord
/**
- * The structure returned when looking for closest coordinates on a path.
+ * The structure returned when looking for the closest coordinates on a path.
+ * The tangent element is defined even on corners and line ends. Following the
+ * intuitive view, the tangent line touches the corner, dividing the plane into
+ * the part with the corner and the part without it. At the line ends, the
+ * "tangent" is the direction of the neighboring segment. The "pseudo tangent"
+ * is generated to define the direction for mouse drag actions.
*/
struct ClosestPathCoord
{
PathCoord path_coord;
+ MapCoordF tangent;
double distance_squared;
};
diff --git a/src/core/virtual_path.cpp b/src/core/virtual_path.cpp
index cc9933d563..5b1f289d11 100644
--- a/src/core/virtual_path.cpp
+++ b/src/core/virtual_path.cpp
@@ -347,9 +347,11 @@ ClosestPathCoord VirtualPath::findClosestPointTo(
size_type start_index,
size_type end_index ) const
{
- Q_ASSERT(!path_coords.empty());
+ Q_ASSERT(path_coords.size() > 1);
- auto result = ClosestPathCoord { path_coords.front(), distance_bound_squared };
+ auto result = ClosestPathCoord { path_coords.front(),
+ path_coords[1].pos - path_coords[0].pos,
+ distance_bound_squared };
// Find upper bound for distance.
for (const auto& path_coord : path_coords)
@@ -367,6 +369,7 @@ ClosestPathCoord VirtualPath::findClosestPointTo(
result.path_coord = path_coord;
}
}
+ result.tangent = calculateTangent(result.path_coord.index);
// Check between this coord and the next one.
auto last = end(path_coords)-1;
@@ -391,6 +394,7 @@ ClosestPathCoord VirtualPath::findClosestPointTo(
if (to_coord.lengthSquared() < result.distance_squared)
{
result.distance_squared = to_coord.lengthSquared();
+ result.tangent = tangent;
result.path_coord = *pc;
}
continue;
@@ -402,6 +406,7 @@ ClosestPathCoord VirtualPath::findClosestPointTo(
if (coord.distanceSquaredTo(next_pos) < result.distance_squared)
{
result.distance_squared = coord.distanceSquaredTo(next_pos);
+ result.tangent = tangent;
result.path_coord = *next_pc;
}
continue;
@@ -425,13 +430,16 @@ ClosestPathCoord VirtualPath::findClosestPointTo(
if (coords.flags[result.path_coord.index].isCurveStart())
{
MapCoordF unused;
+ MapCoordF curve2_control_point1;
PathCoord::splitBezierCurve(MapCoordF(coords.flags[result.path_coord.index]), MapCoordF(coords.flags[result.path_coord.index+1]),
MapCoordF(coords.flags[result.path_coord.index+2]), MapCoordF(coords.flags[result.path_coord.index+3]),
- result.path_coord.param, unused, unused, result.path_coord.pos, unused, unused);
+ result.path_coord.param, unused, unused, result.path_coord.pos, curve2_control_point1, unused);
+ result.tangent = curve2_control_point1 - result.path_coord.pos;
}
else
{
- result.path_coord.pos = pos + (next_pos - pos) * double(factor);
+ result.tangent = next_pos - pos;
+ result.path_coord.pos = pos + result.tangent * double(factor);
}
}
}
diff --git a/src/gui/map/map_editor.cpp b/src/gui/map/map_editor.cpp
index 42af0eb846..18ad4f909a 100644
--- a/src/gui/map/map_editor.cpp
+++ b/src/gui/map/map_editor.cpp
@@ -158,6 +158,7 @@
#include "tools/edit_line_tool.h"
#include "tools/fill_tool.h"
#include "tools/pan_tool.h"
+#include "tools/move_parallel_tool.h"
#include "tools/rotate_pattern_tool.h"
#include "tools/rotate_tool.h"
#include "tools/scale_tool.h"
@@ -936,6 +937,7 @@ void MapEditorController::assignKeyboardShortcuts()
findAction("scaleobjects")->setShortcut(QKeySequence(tr("Z")));
findAction("cutobject")->setShortcut(QKeySequence(tr("K")));
findAction("cuthole")->setShortcut(QKeySequence(tr("H")));
+ findAction("moveparallel")->setShortcut(QKeySequence(tr("Ctrl+Shift+M")));
findAction("measure")->setShortcut(QKeySequence(tr("M")));
findAction("booleanunion")->setShortcut(QKeySequence(tr("U")));
findAction("converttocurves")->setShortcut(QKeySequence(tr("N")));
@@ -1055,6 +1057,7 @@ void MapEditorController::createActions()
cut_hole_menu->addAction(cut_hole_circle_act);
cut_hole_menu->addAction(cut_hole_rectangle_act);
+ move_parallel_act = newToolAction("moveparallel", tr("Move parallel"), this, SLOT(moveParallelClicked()), "tool-move-parallel.png", QString{}, "toolbars.html#tool_move_parallel");
rotate_act = newToolAction("rotateobjects", tr("Rotate objects"), this, SLOT(rotateClicked()), "tool-rotate.png", QString{}, "toolbars.html#rotate");
rotate_pattern_act = newToolAction("rotatepatterns", tr("Rotate pattern"), this, SLOT(rotatePatternClicked()), "tool-rotate-pattern.png", QString{}, "toolbars.html#tool_rotate_pattern");
scale_act = newToolAction("scaleobjects", tr("Scale objects"), this, SLOT(scaleClicked()), "tool-scale.png", QString{}, "toolbars.html#scale");
@@ -1226,6 +1229,7 @@ void MapEditorController::createMenuAndToolbars()
tools_menu->addAction(boolean_merge_holes_act);
tools_menu->addAction(cut_tool_act);
tools_menu->addMenu(cut_hole_menu);
+ tools_menu->addAction(move_parallel_act);
tools_menu->addAction(rotate_act);
tools_menu->addAction(rotate_pattern_act);
tools_menu->addAction(scale_act);
@@ -1363,6 +1367,7 @@ void MapEditorController::createMenuAndToolbars()
cut_hole_button->setMenu(cut_hole_menu);
toolbar_editing->addWidget(cut_hole_button);
+ toolbar_editing->addAction(move_parallel_act);
toolbar_editing->addAction(rotate_act);
toolbar_editing->addAction(rotate_pattern_act);
toolbar_editing->addAction(scale_act);
@@ -2616,6 +2621,8 @@ void MapEditorController::updateObjectDependentActions()
cut_tool_act->setStatusTip(tr("Cut the selected objects into smaller parts.") + (cut_tool_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select at least one line or area object to activate this tool."))));
convert_to_curves_act->setEnabled(have_area || have_line);
convert_to_curves_act->setStatusTip(tr("Turn paths made of straight segments into smooth bezier splines.") + (convert_to_curves_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select a path object to activate this tool."))));
+ move_parallel_act->setEnabled(have_area || have_line);
+ move_parallel_act->setStatusTip(tr("Move lines and area borders in and out.") + (move_parallel_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select at least one line or area object to activate this tool."))));
simplify_path_act->setEnabled(have_area || have_line);
simplify_path_act->setStatusTip(tr("Reduce the number of points in path objects while trying to retain their shape.") + (simplify_path_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select a path object to activate this tool."))));
@@ -2719,6 +2726,11 @@ void MapEditorController::editLineToolClicked()
setTool(new EditLineTool(this, edit_line_tool_act));
}
+void MapEditorController::moveParallelClicked()
+{
+ setTool(new MoveParallelTool(this, move_parallel_act));
+}
+
void MapEditorController::drawPointClicked()
{
setTool(new DrawPointTool(this, draw_point_act));
diff --git a/src/gui/map/map_editor.h b/src/gui/map/map_editor.h
index 9c72a2b43a..925fd49d0a 100644
--- a/src/gui/map/map_editor.h
+++ b/src/gui/map/map_editor.h
@@ -395,6 +395,8 @@ public slots:
void editToolClicked();
/** Activates the line edit tool. */
void editLineToolClicked();
+ /** Move parallel tool activation */
+ void moveParallelClicked();
/** Activates the draw point tool. */
void drawPointClicked();
/** Activates the draw path tool. */
@@ -767,6 +769,7 @@ protected slots:
QAction* edit_tool_act = {};
QAction* edit_line_tool_act = {};
+ QAction* move_parallel_act = {};
QAction* draw_point_act = {};
QAction* draw_path_act = {};
QAction* draw_circle_act = {};
diff --git a/src/tools/move_parallel_tool.cpp b/src/tools/move_parallel_tool.cpp
new file mode 100644
index 0000000000..aba4e81ccd
--- /dev/null
+++ b/src/tools/move_parallel_tool.cpp
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2021 Libor Pecháček
+ *
+ * This file is part of OpenOrienteering.
+ *
+ * OpenOrienteering is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * OpenOrienteering is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with OpenOrienteering. If not, see .
+ */
+
+
+#include "move_parallel_tool.h"
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "core/map.h"
+#include "core/map_view.h"
+#include "core/objects/object.h"
+#include "core/path_coord.h"
+#include "core/renderables/renderable.h"
+#include "core/symbols/combined_symbol.h"
+#include "core/symbols/line_symbol.h"
+#include "core/symbols/symbol.h"
+#include "gui/map/map_widget.h"
+#include "tools/tool.h"
+#include "tools/tool_base.h"
+
+
+class QPainter;
+
+
+namespace OpenOrienteering {
+
+
+MoveParallelTool::MoveParallelTool(MapEditorController* editor, QAction* tool_action)
+: MapEditorToolBase(scaledToScreen(QCursor{ QPixmap(QString::fromLatin1(":/images/cursor-move-parallel.png")), 1, 1 }),
+ Other, editor, tool_action),
+ highlight_renderables(new MapRenderables(map()))
+{
+ // nothing
+}
+
+
+MoveParallelTool::~MoveParallelTool()
+{
+ // nothing
+}
+
+
+void MoveParallelTool::updateStatusText()
+{
+ if (editingInProgress())
+ {
+ setStatusBarText(tr("Move distance: %1 mm")
+ .arg(QLocale().toString(qFabs(move_distance), 'f', 1)));
+ }
+ else
+ {
+ setStatusBarText({});
+ }
+}
+
+
+void MoveParallelTool::objectSelectionChangedImpl()
+{
+ if (map()->getNumSelectedObjects() == 0)
+ deactivate();
+ else
+ updateHoverState();
+}
+
+
+/** \todo - Converge the updateHoverState() implementation across the
+ * different tools (cut, edit line, edit point).
+ */
+void MoveParallelTool::updateHoverState()
+{
+ auto new_hover_state = HoverState { HoverFlag::OverNothing };
+ const PathObject* new_hover_object = {};
+
+ if (!map()->selectedObjects().empty())
+ {
+ auto const click_tolerance_sq = qPow(0.001 * cur_map_widget->getMapView()->pixelToLength(clickTolerance()), 2);
+ auto best_distance_sq = std::numeric_limits::max();
+
+ for (auto* object : map()->selectedObjects())
+ {
+ if (object->getType() == Object::Path)
+ {
+ auto* path = object->asPath();
+ auto closest = path->findClosestPointTo(cur_pos_map);
+
+ if (closest.distance_squared >= +0.0 &&
+ closest.distance_squared < best_distance_sq &&
+ closest.distance_squared < qMax(click_tolerance_sq, qPow(path->getSymbol()->calculateLargestLineExtent(), 2)))
+ {
+ new_hover_state = HoverFlag::OverPathEdge;
+ new_hover_object = path;
+ best_distance_sq = closest.distance_squared;
+ path_drag_point = closest.path_coord.pos;
+ path_normal_vector = closest.tangent.normalVector();
+ path_normal_vector.normalize();
+ }
+ }
+ }
+ }
+
+ // Apply possible changes
+ if (new_hover_state != hover_state ||
+ new_hover_object != hover_object)
+ {
+ highlight_renderables->removeRenderablesOfObject(highlight_object.get(), false);
+
+ hover_state = new_hover_state;
+ hover_object = const_cast(new_hover_object);
+
+ if (hover_state == HoverFlag::OverPathEdge && hover_object)
+ {
+ // Extract hover line
+ highlight_object = std::unique_ptr(hover_object->duplicate());
+ highlight_object->setSymbol(map()->getCoveringCombinedLine(), true);
+ highlight_object->update();
+ highlight_renderables->insertRenderablesOfObject(highlight_object.get());
+ }
+
+ effective_start_drag_distance = (hover_state == HoverFlag::OverNothing) ? startDragDistance() : 0;
+ updateDirtyRect();
+ }
+}
+
+
+void MoveParallelTool::mouseMove()
+{
+ updateHoverState();
+}
+
+
+void MoveParallelTool::dragStart()
+{
+ // Drag movement can start without prior mouse move event on the object.
+ // E.g. position cursor on above an object border, select the tool and
+ // immediately start a drag. This is why we update hover state here.
+ updateHoverState();
+
+ if (!hover_object)
+ return;
+
+ orig_hover_object = std::unique_ptr(hover_object->duplicate());
+ startEditing(hover_object);
+ updateStatusText();
+}
+
+
+void MoveParallelTool::dragMove()
+{
+ if (!hover_object)
+ return;
+
+ // taken from PathObject::findClosestPointOnBorder
+ MapCoordVector border_flags;
+ MapCoordVectorF border_coords;
+ move_distance = MapCoordF::dotProduct(path_normal_vector, cur_pos_map - path_drag_point);
+
+ hover_object->clearCoordinates();
+ highlight_renderables->removeRenderablesOfObject(highlight_object.get(), false);
+ highlight_object->clearCoordinates(); // TODO - can we share the path part vectors with hover object?
+ for (auto const& part : orig_hover_object->parts())
+ {
+ LineSymbol::shiftCoordinates(part, -move_distance, 0,
+ LineSymbol::MiterJoin, border_flags, border_coords);
+
+ auto start_new_part = true;
+ std::transform(begin(border_flags), end(border_flags),
+ begin(border_coords), begin(border_flags),
+ [this, &start_new_part](auto coord, auto const& coord_f) {
+ coord.setX(coord_f.x());
+ coord.setY(coord_f.y());
+ hover_object->addCoordinate(coord, start_new_part);
+ highlight_object->addCoordinate(coord, start_new_part);
+ start_new_part = false;
+ return coord;
+ });
+ }
+
+ highlight_object->update();
+ highlight_renderables->insertRenderablesOfObject(highlight_object.get());
+ updatePreviewObjectsAsynchronously();
+ updateDirtyRect();
+ updateStatusText();
+}
+
+
+void MoveParallelTool::dragFinish()
+{
+ if (!hover_object)
+ return;
+
+ finishEditing();
+ updateStatusText();
+}
+
+
+void MoveParallelTool::drawImpl(QPainter* painter, MapWidget* widget)
+{
+ auto num_selected_objects = map()->selectedObjects().size();
+ if (num_selected_objects > 0)
+ {
+ drawSelectionOrPreviewObjects(painter, widget);
+
+ if (!highlight_renderables->empty())
+ map()->drawSelection(painter, true, widget, highlight_renderables.get(), true);
+ }
+}
+
+
+} // namespace OpenOrienteering
diff --git a/src/tools/move_parallel_tool.h b/src/tools/move_parallel_tool.h
new file mode 100644
index 0000000000..a96162a7e2
--- /dev/null
+++ b/src/tools/move_parallel_tool.h
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2021 Libor Pecháček
+ *
+ * This file is part of OpenOrienteering.
+ *
+ * OpenOrienteering is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * OpenOrienteering is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with OpenOrienteering. If not, see .
+ */
+
+
+#ifndef OPENORIENTEERING_MOVE_PARALLEL_TOOL_H
+#define OPENORIENTEERING_MOVE_PARALLEL_TOOL_H
+
+#include
+#include
+#include
+
+#include
+
+#include "core/map_coord.h"
+#include "tools/edit_tool.h"
+#include "tools/tool_base.h"
+
+class QAction;
+class QPainter;
+
+
+namespace OpenOrienteering {
+
+class MapEditorController;
+class MapRenderables;
+class MapWidget;
+class PathObject;
+
+/**
+ * A tool to edit lines of PathObjects.
+ */
+class MoveParallelTool : public MapEditorToolBase
+{
+ using HoverFlag = EditTool::HoverFlag;
+ using HoverState = EditTool::HoverState;
+
+Q_OBJECT
+public:
+ MoveParallelTool(MapEditorController* editor, QAction* tool_action);
+ ~MoveParallelTool();
+
+protected:
+ void mouseMove() override;
+ void dragStart() override;
+ void dragMove() override;
+ void dragFinish() override;
+ void drawImpl(QPainter* painter, MapWidget* widget) override;
+
+ void updateHoverState();
+
+ void updateStatusText() override;
+ void objectSelectionChangedImpl() override;
+
+private:
+ /**
+ * An object created for the current hover_line.
+ */
+ std::unique_ptr highlight_object;
+ std::unique_ptr highlight_renderables;
+
+ /**
+ * Provides general information on what is hovered over.
+ */
+ HoverState hover_state = HoverFlag::OverNothing;
+
+ /**
+ * Object which is hovered over (if any).
+ */
+ PathObject* hover_object = {};
+
+ /**
+ * A copy of the original object which we shift. The new object is
+ * always a direct modification of the initial copy, so that we
+ * don't accumulate errors.
+ */
+ std::unique_ptr orig_hover_object;
+
+ /**
+ * Closest point on the dragged path. We use this point to calculate
+ * the object shift distance.
+ */
+ MapCoordF path_drag_point;
+
+ /**
+ * Normal vector at the closest point of the highlighted path. We use the
+ * normal vector for distance calsulation in dragMove().
+ */
+ MapCoordF path_normal_vector;
+
+ /**
+ * The move distance is exposed from dragMove() for the purpose of
+ * status text updates.
+ */
+ qreal move_distance = {};
+};
+
+
+} // namespace OpenOrienteering
+
+#endif