Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New course exporter #2301

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ set(Mapper_Common_SRCS
fileformats/ocd_icon.cpp
fileformats/ocd_parameter_stream_reader.cpp
fileformats/ocd_types.cpp
fileformats/simple_course_export.cpp
fileformats/course_export.cpp
fileformats/xml_file_format.cpp

gui/about_dialog.cpp
Expand All @@ -152,7 +152,7 @@ set(Mapper_Common_SRCS
gui/print_widget.cpp
gui/select_crs_dialog.cpp
gui/settings_dialog.cpp
gui/simple_course_dialog.cpp
gui/course_dialog.cpp
gui/task_dialog.cpp
gui/text_browser_dialog.cpp
gui/touch_cursor.cpp
Expand Down
350 changes: 350 additions & 0 deletions src/fileformats/course_export.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,350 @@
/*
* Copyright 2021 Kai Pastor
* Copyright 2024 Semyon Yakimov
*
* 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 <http://www.gnu.org/licenses/>.
*/

#include "course_export.h"

#include <QDebug>
#include <QRegularExpression>

#include "core/map.h"
#include "core/map_part.h"
#include "core/objects/object.h"
#include "core/objects/text_object.h"


namespace OpenOrienteering {

// static
QString CourseExport::defaultEventName()
{
return tr("Unnamed event");
}

// static
QString CourseExport::defaultCourseName()
{
return tr("Unnamed course");
}

// static
int CourseExport::defaultFirstCode() noexcept
{
return 31;
}

// static
QString CourseExport::defaultStartSymbol()
{
return QStringLiteral("701");
}

// static
QString CourseExport::defaultFinishSymbol()
{
return QStringLiteral("706");
}

bool CourseExport::canExport()
{
std::vector<ControlPoint> controls = getControlsForExport();
if (controls.empty())
{
if (error_string.isEmpty())
{
error_string = tr("No control points or path object selected.");
}
return false;
}
return true;
}

const PathObject* CourseExport::findPathObjectForExport() const
{
const PathObject* path_object = nullptr;
if (map.getNumSelectedObjects() == 1
&& map.getFirstSelectedObject()->getType() == Object::Path)
{
path_object = static_cast<PathObject const*>(map.getFirstSelectedObject());
}
else if (map.getNumParts() == 1
&& map.getPart(0)->getNumObjects() == 1
&& map.getPart(0)->getObject(0)->getType() == Object::Path)
{
path_object = static_cast<PathObject const*>(map.getPart(0)->getObject(0));
}
return (path_object && path_object->parts().size() == 1) ? path_object : nullptr;
}

bool CourseExport::isPathExportMode() const
{
if (findPathObjectForExport())
{
return true;
}
return false;
}

void addControl(std::vector<ControlPoint>& controls, Object* object, const QString& code)
{
if (object)
{
controls.push_back(ControlPoint(code, object->asPoint()->getCoordF()));
}
}

void addControl(std::vector<ControlPoint>& controls, const MapCoord& coord, const QString& code)
{
controls.push_back(ControlPoint(code, MapCoordF(coord)));
}

QString removeTrailingZerosAndDots(QString s)
{
return s.replace(QRegularExpression(QStringLiteral(R"((\.)?0+$)")), QStringLiteral(""));
}

const std::vector<ControlPoint> CourseExport::getControlsForExport()
{
const PathObject* path_object = findPathObjectForExport();
if (path_object)
{
return addControlsByPath(path_object);
}

std::vector<Object*> control_objects;
std::set<Object*> control_code_objects;
if (map.getNumSelectedObjects() < 1)
{
return {};
}
for (auto* object : map.selectedObjects())
{
if (object->getType() == Object::Text)
{
control_code_objects.insert(object);
}
else if (object->getType() == Object::Point)
{
control_objects.push_back(object);
}
}
if (control_objects.empty())
{
error_string = tr("No control points selected.");
return {};
}
if (control_code_objects.empty())
{
error_string = tr("No control codes selected.");
return {};
}
std::size_t control_objects_num = control_objects.size();
std::size_t control_codes_num = control_code_objects.size();
if (control_objects_num < control_codes_num
|| control_objects_num > control_codes_num + 2) // controls + start + finish
{
error_string = tr("Number of control points and control codes does not match.");
return {};
}
qDebug() << "Found control points:" << control_objects.size() << "control codes:" << control_code_objects.size();
auto controls = addControlsWithCodes(control_objects, control_code_objects);
sortControls(controls);
return controls;
}

const std::vector<ControlPoint> CourseExport::addControlsByPath(const PathObject* path_object)
{
auto next = [](auto current) {
return current + (current->isCurveStart() ? 3 : 1);
};

Q_ASSERT(path_object);
std::vector<MapCoord> coords = path_object->getRawCoordinateVector();

std::vector<ControlPoint> controls;
addControl(controls, coords.front(), QStringLiteral("S1"));
auto code_number = firstCode();
for (auto current = next(coords.begin()); current != coords.end() - 1; current = next(current))
{
auto const name = QString::number(code_number);
addControl(controls, *current, name);
++code_number;
}
addControl(controls, coords.back(), QStringLiteral("F1"));
return controls;
}

const std::vector<ControlPoint> CourseExport::addControlsWithCodes(const std::vector<Object*>& control_objects, std::set<Object*>& control_code_objects)
{
std::vector<ControlPoint> controls;
QString start_symbol = removeTrailingZerosAndDots(startSymbol());
QString finish_symbol = removeTrailingZerosAndDots(finishSymbol());
bool start_found = false;
bool finish_found = false;
for (auto* object : control_objects)
{
QString symbol = removeTrailingZerosAndDots(object->getSymbol()->getNumberAsString());
if (symbol == start_symbol)
{
if (start_found)
{
error_string = tr("Select only one start point.");
return {};
}
qDebug() << "Found start:" << object->asPoint()->getCoordF();
addControl(controls, object, QStringLiteral("S1"));
start_found = true;
continue;
}
else if (symbol == finish_symbol)
{
if (finish_found)
{
error_string = tr("Select only one finish point.");
return {};
}
qDebug() << "Found finish:" << object->asPoint()->getCoordF();
addControl(controls, object, QStringLiteral("F1"));
finish_found = true;
continue;
}
auto* control_code_object = getNearestObject(object, control_code_objects);
if (control_code_object)
{
qDebug() << "Found control point:" << object->asPoint()->getCoordF() << "control code:" << control_code_object->asText()->getText();
control_code_objects.erase(control_code_object);
auto code = control_code_object->asText()->getText();
addControl(controls, object, code);
}
}
return controls;
}

// static
Object* CourseExport::getNearestObject(const Object* object, const std::set<Object*>& objects)
{
Object* nearest_object = nullptr;
double nearest_distance = 0;
for (auto* other_object : objects)
{
if (other_object != object)
{
auto distance = getObjectCoord(object).distanceTo(getObjectCoord(other_object));
if (!nearest_object || distance < nearest_distance)
{
nearest_object = other_object;
nearest_distance = distance;
}
}
}
return nearest_object;
}

// static
MapCoordF CourseExport::getObjectCoord(const Object* object)
{
if (object)
{
switch (object->getType())
{
case Object::Point:
return object->asPoint()->getCoordF();
case Object::Text:
return object->asText()->getAnchorCoordF();
default:
break;
}
}
return MapCoordF();
}

// static
void CourseExport::sortControls(std::vector<ControlPoint>& controls)
{
std::sort(controls.begin(), controls.end(), [](const ControlPoint& a, const ControlPoint& b) {
if (a.isStart() || b.isFinish())
{
return true;
}
if (a.isFinish() || b.isStart())
{
return false;
}
return a.code() < b.code();
});
}


QString CourseExport::eventName() const
{
auto name = map.property("course-export-event-name").toString();
if (name.isEmpty())
{
name = defaultEventName();
}
return name;
}

QString CourseExport::courseName() const
{
auto name = map.property("course-export-course-name").toString();
if (name.isEmpty())
{
name = defaultCourseName();
}
return name;
}

QString CourseExport::startSymbol() const
{
auto symbol = map.property("course-export-start-symbol").toString();
if (symbol.isEmpty())
{
symbol = defaultStartSymbol();
}
return symbol;
}

QString CourseExport::finishSymbol() const
{
auto symbol = map.property("course-export-finish-symbol").toString();
if (symbol.isEmpty())
{
symbol = defaultFinishSymbol();
}
return symbol;
}

int CourseExport::firstCode() const
{
auto code = map.property("course-export-first-code").toInt();
return code > 0 ? code : defaultFirstCode();
}

void CourseExport::setProperties(Map& map, const QString& event_name, const QString& course_name, const QString& start_symbol, const QString& finish_symbol, int first_code)
{
map.setProperty("course-export-event-name", event_name);
map.setProperty("course-export-course-name", course_name);
map.setProperty("course-export-start-symbol", start_symbol);
map.setProperty("course-export-finish-symbol", finish_symbol);
map.setProperty("course-export-first-code", first_code);
}


} // namespace OpenOrienteering
Loading