diff --git a/code-check-wrapper.sh b/code-check-wrapper.sh index 42dd46dba..f89d228c3 100755 --- a/code-check-wrapper.sh +++ b/code-check-wrapper.sh @@ -1,6 +1,6 @@ #!/bin/sh -e -# Copyright 2017, 2018 Kai Pastor +# Copyright 2017, 2018, 2024 Kai Pastor # # This file is part of OpenOrienteering. # @@ -71,6 +71,7 @@ for I in \ map_coord.cpp \ map_editor.cpp \ map_find_feature.cpp \ + map_information_dialog.cpp \ map_printer \ map_widget.cpp \ mapper_proxystyle.cpp \ diff --git a/images/map-information.png b/images/map-information.png new file mode 100755 index 000000000..d7d75d800 Binary files /dev/null and b/images/map-information.png differ diff --git a/resources.qrc b/resources.qrc index a3fd91f57..83ef2a9fd 100644 --- a/resources.qrc +++ b/resources.qrc @@ -117,6 +117,7 @@ images/view-zoom-out.png images/window-new.png images/mapper-icon/Mapper-128.png + images/map-information.png doc/tip-of-the-day/tips_en.txt diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index faa764cb0..6a25ffb2d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,6 @@ # # Copyright 2012-2014 Thomas Schöps -# Copyright 2012-2018 Kai Pastor +# Copyright 2012-2024 Kai Pastor # # This file is part of OpenOrienteering. # @@ -163,6 +163,7 @@ set(Mapper_Common_SRCS gui/map/map_editor.cpp gui/map/map_editor_activity.cpp gui/map/map_find_feature.cpp + gui/map/map_information_dialog.cpp gui/map/map_widget.cpp gui/map/rotate_map_dialog.cpp gui/map/stretch_map_dialog.cpp diff --git a/src/gui/map/map_editor.cpp b/src/gui/map/map_editor.cpp index 7d79cf311..bf2646364 100644 --- a/src/gui/map/map_editor.cpp +++ b/src/gui/map/map_editor.cpp @@ -124,6 +124,7 @@ #include "gui/map/map_dialog_scale.h" #include "gui/map/map_editor_activity.h" #include "gui/map/map_find_feature.h" +#include "gui/map/map_information_dialog.h" #include "gui/map/map_widget.h" #include "gui/map/rotate_map_dialog.h" #include "gui/symbols/symbol_replacement.h" @@ -462,6 +463,7 @@ void MapEditorController::setEditingInProgress(bool value) scale_map_act->setEnabled(!editing_in_progress); rotate_map_act->setEnabled(!editing_in_progress); map_notes_act->setEnabled(!editing_in_progress); + map_info_act->setEnabled(!editing_in_progress); // Map menu, continued const int num_parts = map->getNumParts(); @@ -1027,6 +1029,7 @@ void MapEditorController::createActions() scale_map_act = newAction("scalemap", tr("Change map scale..."), this, SLOT(scaleMapClicked()), "tool-scale.png", tr("Change the map scale and adjust map objects and symbol sizes"), "map_menu.html"); rotate_map_act = newAction("rotatemap", tr("Rotate map..."), this, SLOT(rotateMapClicked()), "tool-rotate.png", tr("Rotate the whole map"), "map_menu.html"); map_notes_act = newAction("mapnotes", tr("Map notes..."), this, SLOT(mapNotesClicked()), nullptr, QString{}, "map_menu.html"); + map_info_act = newAction("mapinfo", tr("Map information..."), this, SLOT(mapInfoClicked()), "map-information.png", QString{}, "map_menu.html"); template_window_act = newCheckAction("templatewindow", tr("Template setup window"), this, SLOT(showTemplateWindow(bool)), "templates.png", tr("Show/Hide the template window"), "templates_menu.html"); //QAction* template_config_window_act = newCheckAction("templateconfigwindow", tr("Template configurations window"), this, SLOT(showTemplateConfigurationsWindow(bool)), "window-new", tr("Show/Hide the template configurations window")); @@ -1253,6 +1256,7 @@ void MapEditorController::createMenuAndToolbars() map_menu->addAction(scale_map_act); map_menu->addAction(rotate_map_act); map_menu->addAction(map_notes_act); + map_menu->addAction(map_info_act); map_menu->addSeparator(); updateMapPartsUI(); map_menu->addAction(mappart_add_act); @@ -2282,6 +2286,13 @@ void MapEditorController::mapNotesClicked() } } +void MapEditorController::mapInfoClicked() +{ + MapInformationDialog dialog(window, map); + dialog.setWindowModality(Qt::WindowModal); + dialog.exec(); +} + void MapEditorController::createTemplateWindow() { Q_ASSERT(!template_dock_widget); diff --git a/src/gui/map/map_editor.h b/src/gui/map/map_editor.h index 0e641d449..4cbb8e775 100644 --- a/src/gui/map/map_editor.h +++ b/src/gui/map/map_editor.h @@ -1,6 +1,6 @@ /* * Copyright 2012, 2013, 2014 Thomas Schöps - * Copyright 2013-2021 Kai Pastor + * Copyright 2013-2024 Kai Pastor * * This file is part of OpenOrienteering. * @@ -348,6 +348,8 @@ public slots: void rotateMapClicked(); /** Shows the dialog to enter map notes. */ void mapNotesClicked(); + /** Shows the map information. */ + void mapInfoClicked(); /** Shows or hides the template setup dock widget. */ void showTemplateWindow(bool show); @@ -746,6 +748,7 @@ protected slots: QAction* scale_map_act = {}; QAction* rotate_map_act = {}; QAction* map_notes_act = {}; + QAction* map_info_act = {}; QAction* symbol_set_id_act = {}; std::unique_ptr symbol_report_feature; diff --git a/src/gui/map/map_information_dialog.cpp b/src/gui/map/map_information_dialog.cpp new file mode 100644 index 000000000..9d3c44089 --- /dev/null +++ b/src/gui/map/map_information_dialog.cpp @@ -0,0 +1,395 @@ +/* + * Copyright 2024 Matthias Kühlewein + * + * 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 "map_information_dialog.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/georeferencing.h" +#include "core/map.h" +#include "core/objects/object.h" +#include "core/symbols/symbol.h" +#include "core/symbols/text_symbol.h" +#include "gui/file_dialog.h" +#include "gui/main_window.h" +#include "gui/map/map_editor.h" +#include "undo/undo_manager.h" + + +namespace OpenOrienteering { + +MapInformationDialog::MapInformationDialog(MainWindow* parent, Map* map) + : QDialog(parent, Qt::WindowSystemMenuHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint) + , map(map) + , main_window(parent) + , text_report_indent{3} +{ + setWindowTitle(tr("Map information")); + + auto* save_button = new QPushButton(QIcon(QLatin1String(":/images/save.png")), tr("Save")); + auto* ok_button = new QPushButton(QIcon(QLatin1String(":/images/arrow-right.png")), tr("OK")); + ok_button->setDefault(true); + + auto* buttons_layout = new QHBoxLayout(); + buttons_layout->addWidget(save_button); + buttons_layout->addStretch(1); + buttons_layout->addWidget(ok_button); + + map_info_tree = new QTreeWidget(); + map_info_tree->setColumnCount(2); + map_info_tree->setHeaderHidden(true); + + retrieveInformation(); + buildTree(); + setupTreeWidget(); + + map_info_tree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + + auto* layout = new QVBoxLayout(); + layout->addWidget(map_info_tree); + layout->addLayout(buttons_layout); + setLayout(layout); + resize(650, 600); + + connect(save_button, &QAbstractButton::clicked, this, &MapInformationDialog::save); + connect(ok_button, &QAbstractButton::clicked, this, &QDialog::accept); +} + +MapInformationDialog::~MapInformationDialog() = default; + + +// retrieve and store the information +void MapInformationDialog::retrieveInformation() +{ + map_information.map_scale = int(map->getScaleDenominator()); + + const auto& map_crs = map->getGeoreferencing().getProjectedCRSId(); + map_information.map_crs = map->getGeoreferencing().getProjectedCRSName(); + if (map_crs != QLatin1String("Local") && map_crs != QLatin1String("PROJ.4")) + { + const auto& projected_crs_parameters = map->getGeoreferencing().getProjectedCRSParameters(); + if (!projected_crs_parameters.empty()) + { + QString crs_details = QLatin1String(" (") + (map_crs == QLatin1String("EPSG") ? tr("code") : tr("zone")) + QChar::Space + projected_crs_parameters.front() + QLatin1Char(')'); + map_information.map_crs += crs_details; + } + } + + map_information.map_parts_num = map->getNumParts(); + map_information.map_parts.reserve(map_information.map_parts_num); + map_information.symbols_num = map->getNumSymbols(); + map_information.templates_num = map->getNumTemplates(); + map_information.colors_num = map->getNumColors(); + + for (int i = 0; i < map_information.colors_num; ++i) + { + const auto* map_color = map->getMapColor(i); + if (map_color) + { + ColorUsage color = {}; + color.name = map_color->getName(); + for (int j = 0; j < map_information.symbols_num; ++j) + { + const auto* symbol = map->getSymbol(j); + if (symbol->getType()) + { + if (symbol->containsColor(map_color)) + { + color.symbols.push_back({getFullSymbolName(symbol)}); + } + } + } + map_information.colors.emplace_back(color); + } + } + + const auto& undo_manager = map->undoManager(); + map_information.undo_steps_num = undo_manager.canUndo() ? undo_manager.undoStepCount() : 0; + map_information.redo_steps_num = undo_manager.canRedo() ? undo_manager.redoStepCount() : 0; + + int i = 0; + for (auto& name : {tr("Point symbols"), tr("Line symbols"), tr("Area symbols"), tr("Text symbols"), tr("Combined symbols"), tr("Undefined symbols")}) + map_information.map_objects[i++].category.name = name; + + for (i = 0; i < map_information.map_parts_num; ++i) + { + const auto* map_part = map->getPart(i); + const auto map_part_objects = map_part->getNumObjects(); + map_information.map_parts.push_back({map_part->getName(), map_part_objects}); + map_information.map_objects_num += map_part_objects; + + for (int j = 0; j < map_part_objects; ++j) + { + const auto* symbol = map_part->getObject(j)->getSymbol(); + addSymbol(symbol); + } + } + for (auto& map_object : map_information.map_objects) + { + if (map_object.category.num > 1) + { + auto& objects_vector = map_object.objects; + sort(begin(objects_vector), end(objects_vector), [](const SymbolNum& sym1, const SymbolNum& sym2) { return Symbol::lessByNumber(sym1.symbol, sym2.symbol); }); + } + } + for (i = 0; i < map_information.symbols_num; ++i) + { + const auto* symbol = map->getSymbol(i); + if (symbol->getType() == Symbol::Text) + { + addFont(symbol); + } + } + map_information.fonts_num = static_cast(map_information.font_names.size()); +} + +void MapInformationDialog::addSymbol(const Symbol* symbol) +{ + const auto symbol_type = symbol->getType(); + int symbol_index; + for (symbol_index = 0; symbol_index < 5; ++symbol_index) + { + if ((symbol_type & (1 << symbol_index)) == symbol_type) + break; + } + const bool undefined_object = map->findSymbolIndex(symbol) < 0; + auto& object_category = map_information.map_objects[symbol_index]; + auto& objects_vector = object_category.objects; + auto found = std::find_if(begin(objects_vector), end(objects_vector), [&symbol](const SymbolNum& sym_count) { return sym_count.symbol == symbol; }); + if (found != std::end(objects_vector)) + { + ++found->num; + } + else + { + std::vector colors; + for (int i = 0; i < map_information.colors_num; ++i) + { + const auto* map_color = map->getMapColor(i); + if (map_color && symbol->containsColor(map_color)) + { + colors.push_back({map_color->getName()}); + } + } + objects_vector.push_back({symbol, undefined_object ? tr("") : getFullSymbolName(symbol), 1, colors}); + } + ++object_category.category.num; +} + +void MapInformationDialog::addFont(const Symbol* symbol) +{ + const auto* text_symbol = symbol->asText(); + const auto& font_family = text_symbol->getFontFamily(); + const auto font_family_substituted = QFontInfo(text_symbol->getQFont()).family(); + auto& font_names = map_information.font_names; + auto found = std::find_if(begin(font_names), end(font_names), [&font_family](const FontNum& font_count) { return font_count.name == font_family; }); + if (found != std::end(font_names)) + { + ++found->num; + } + else + { + font_names.push_back({font_family, font_family_substituted, 1}); + } +} + +// create textual information and put it in the tree widget +void MapInformationDialog::buildTree() +{ + tree_item_hierarchy.reserve(4); + max_item_length = 0; + + addTreeItem(0, tr("Map"), tr("%n object(s)", nullptr, map_information.map_objects_num)); + + addTreeItem(1, tr("Scale"), QString::fromLatin1("1:%1").arg(map_information.map_scale)); + + addTreeItem(1, tr("Coordinate reference system"), map_information.map_crs); + + addTreeItem(0, tr("Map parts"), tr("%n part(s)", nullptr, map_information.map_parts_num)); + for (const auto& map_part : map_information.map_parts) + { + addTreeItem(1, map_part.name, tr("%n object(s)", nullptr, map_part.num)); + } + + addTreeItem(0, tr("Symbols"), tr("%n symbol(s)", nullptr, map_information.symbols_num)); + + addTreeItem(0, tr("Templates"), tr("%n template(s)", nullptr, map_information.templates_num)); + + if (map_information.undo_steps_num || map_information.redo_steps_num) + { + QString undo_redo_type = map_information.undo_steps_num ? tr("Undo") : tr("Redo"); + QString undo_redo_num = QString::fromLatin1("%1").arg(map_information.undo_steps_num ? map_information.undo_steps_num : map_information.redo_steps_num); + if (map_information.undo_steps_num && map_information.redo_steps_num) + { + undo_redo_type.append(QLatin1Char('/')).append(tr("Redo")); + undo_redo_num.append(QString::fromLatin1("/%1").arg(map_information.redo_steps_num)); + } + undo_redo_type.append(QChar::Space).append(tr("steps")); + undo_redo_num.append(QChar::Space).append(map_information.undo_steps_num + map_information.redo_steps_num > 1 ? tr("steps") : tr("step")); + addTreeItem(0, undo_redo_type, undo_redo_num); + } + + addTreeItem(0, tr("Colors"), tr("%n color(s)", nullptr, map_information.colors_num)); + if (map_information.colors_num) + { + for (const auto& color : map_information.colors) + { + addTreeItem(1, color.name); + if (color.symbols.size()) + { + for (const auto& symbol : color.symbols) + { + addTreeItem(2, symbol); + } + } + } + } + + addTreeItem(0, tr("Fonts"), tr("%n font(s)", nullptr, map_information.fonts_num)); + for (const auto& font_name : map_information.font_names) + { + addTreeItem(1, (font_name.name == font_name.name_substitute ? font_name.name : font_name.name + tr(" (substituted by ") + font_name.name_substitute + QLatin1Char(')')), + tr("%n symbol(s)", nullptr, font_name.num)); + } + + addTreeItem(0, tr("Objects"), tr("%n object(s)", nullptr, map_information.map_objects_num)); + for (const auto& map_object : map_information.map_objects) + { + if (map_object.category.num) + { + addTreeItem(1, map_object.category.name, tr("%n object(s)", nullptr, map_object.category.num)); + for (const auto& object : map_object.objects) + { + addTreeItem(2, object.name, tr("%n object(s)", nullptr, object.num)); + if (object.colors.size()) + { + for (const auto& color : object.colors) + { + addTreeItem(3, color); + } + } + } + } + } +} + +void MapInformationDialog::addTreeItem(const int level, const QString& item, const QString& value) +{ + Q_ASSERT(level >= 0 && level <= 3); + + tree_items.push_back({level, item, value}); + max_item_length = qMax(max_item_length, level * text_report_indent + item.length()); +} + +void MapInformationDialog::setupTreeWidget() +{ + for (const auto &tree_item : tree_items) + { + auto tree_depth = static_cast(tree_item_hierarchy.size()); + Q_ASSERT(tree_item.level <= tree_depth); + + for (; tree_depth > tree_item.level; --tree_depth) + tree_item_hierarchy.pop_back(); + + QTreeWidgetItem* tree_widget_item; + if (tree_depth) + tree_widget_item = new QTreeWidgetItem(tree_item_hierarchy.back()); + else + tree_widget_item = new QTreeWidgetItem(map_info_tree); + tree_widget_item->setText(0, tree_item.item); + tree_widget_item->setText(1, tree_item.value); + tree_item_hierarchy.emplace_back(tree_widget_item); // always store last tree item + } +} + +QString MapInformationDialog::makeTextReport() const +{ + QString text_report; + for (const auto &tree_item : tree_items) + { + QString item_value; + if (!text_report.isEmpty() && !tree_item.level) // separate items on topmost level + text_report += QChar::LineFeed; + item_value.fill(QChar::Space, tree_item.level * text_report_indent); + item_value.append(tree_item.item); + if (!tree_item.value.isEmpty()) + { + item_value = item_value.leftJustified(max_item_length + 5, QChar::Space); + item_value.append(tree_item.value); + } + text_report += item_value + QChar::LineFeed; + } + return text_report; +} + +// slot +void MapInformationDialog::save() +{ + auto filepath = FileDialog::getSaveFileName( + this, + ::OpenOrienteering::MainWindow::tr("Save file"), + QFileInfo(main_window->currentPath()).canonicalPath(), + tr("Textfiles (*.txt)") ); + + if (filepath.isEmpty()) + return; + if (!filepath.endsWith(QLatin1String(".txt"), Qt::CaseInsensitive)) + filepath.append(QLatin1String(".txt")); + + QSaveFile file(filepath); + if (file.open(QIODevice::WriteOnly | QIODevice::Text)) + { + file.write(makeTextReport().toUtf8()); + if (file.commit()) + return; + } + QMessageBox::warning(this, tr("Error"), + tr("Cannot save file:\n%1\n\n%2") + .arg(filepath, file.errorString()) ); +} + +inline +const QString MapInformationDialog::getFullSymbolName(const Symbol* symbol) const +{ + return (symbol->getNumberAsString() + QStringLiteral(" ") + symbol->getPlainTextName()); +} + +} // namespace OpenOrienteering diff --git a/src/gui/map/map_information_dialog.h b/src/gui/map/map_information_dialog.h new file mode 100644 index 000000000..b917635cf --- /dev/null +++ b/src/gui/map/map_information_dialog.h @@ -0,0 +1,177 @@ +/* + * Copyright 2024 Matthias Kühlewein + * + * 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_MAP_INFORMATION_DIALOG_H +#define OPENORIENTEERING_MAP_INFORMATION_DIALOG_H + +#include +#include + +#include +#include +#include + +class QTreeWidget; +class QTreeWidgetItem; +class QWidget; + +namespace OpenOrienteering { + +class Map; +class MainWindow; +class Symbol; + +/** + * A class for providing information about the current map. + * + * Information is given about the number of objects in total and per map part, + * the number of symbols, templates and undo/redo steps. + * For each color a list of symbols using that color is shown. + * All fonts (and their substitutions) being used by symbols are shown. + * For objects there is a hierarchical view: + * - the number of objects per symbol class (e.g., Point symbols, Line symbols etc.) + * - for each symbol class the symbols in use and the related number of objects + * - for each symbol the colors used by it + */ +class MapInformationDialog : public QDialog +{ +Q_OBJECT +public: + /** + * Creates a new MapInformationDialog object. + */ + MapInformationDialog(MainWindow* parent, Map* map); + + ~MapInformationDialog() override; + +private slots: + void save(); + +private: + struct ItemNum { + QString name; + int num = 0; + }; + + struct FontNum { + QString name; + QString name_substitute; // the substituted font name, can be the same as 'name' + int num = 0; + }; + + struct SymbolNum { + const Symbol* symbol; + QString name; + int num; + std::vector colors; + }; + + struct ObjectCategory { + ItemNum category; + std::vector objects; + }; + + struct ColorUsage { + QString name; + std::vector symbols; + }; + + struct MapInfo { + int map_objects_num = 0; + int map_scale; + QString map_crs; + int map_parts_num; + std::vector map_parts; + int symbols_num; + int undo_steps_num; + int redo_steps_num; + int templates_num; + int colors_num; + std::vector colors; + int fonts_num; + std::vector font_names; + std::array map_objects; + } map_information; + + struct TreeItem { + int level; + QString item; + QString value; + }; + + /** + * Retrieves the map information and stores it in the map_information structure. + */ + void retrieveInformation(); + + /** + * For a given symbol: + * - Retrieves the symbol class and counts its objects. + * - Counts the objects. + * - Retrieves all colors. + */ + void addSymbol(const Symbol* symbol); + + /** + * Retrieves font (and its substitution) for a given text symbol and counts + * the font occurrence. + */ + void addFont(const Symbol* symbol); + + /** + * Creates the textual information from the stored information and + * puts it in the tree widget. + */ + void buildTree(); + + /** + * Adds an item, its level and (in most cases) its value to the tree_items list and + * determines the maximum length of all (indented) items for creating a text report. + */ + void addTreeItem(const int level, const QString& item, const QString& value = QString()); + + /** + * Processes tree_items list to hierarchically setup the tree widget. + */ + void setupTreeWidget(); + + /** + * Processes tree_items list to create a text report. + */ + QString makeTextReport() const; + + /** + * Takes a pointer to a symbol and returns a string consisting of the symbol's number and name. + */ + const QString getFullSymbolName(const Symbol* symbol) const; + + const Map* map; + const MainWindow* main_window; + + std::vector tree_items; + QTreeWidget* map_info_tree; + std::vector tree_item_hierarchy; + + int max_item_length; + const int text_report_indent; +}; + +} // namespace OpenOrienteering + +#endif // OPENORIENTEERING_MAP_INFORMATION_DIALOG_H