|
| 1 | +/* SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | + * |
| 3 | + * Copyright 2024 Kai Pastor (OpenOrienteering) |
| 4 | + * Copyright 2024 Matthias Kühlewein (OpenOrienteering) |
| 5 | + * |
| 6 | + * This file is part of LibreMapper. |
| 7 | + */ |
| 8 | + |
| 9 | +#include "map_information.h" |
| 10 | + |
| 11 | +#include <algorithm> |
| 12 | +#include <iterator> |
| 13 | +// IWYU pragma: no_include <memory> |
| 14 | +#include <numeric> |
| 15 | +#include <vector> |
| 16 | + |
| 17 | +#include <QtGlobal> |
| 18 | +#include <QChar> |
| 19 | +#include <QCoreApplication> |
| 20 | +#include <QFontInfo> |
| 21 | +#include <QLatin1Char> |
| 22 | +#include <QLatin1String> |
| 23 | + |
| 24 | +#include "core/georeferencing.h" |
| 25 | +#include "core/map.h" |
| 26 | +#include "core/map_color.h" // IWYU pragma: keep |
| 27 | +#include "core/map_part.h" |
| 28 | +#include "core/objects/object.h" |
| 29 | +#include "core/symbols/symbol.h" |
| 30 | +#include "core/symbols/text_symbol.h" |
| 31 | +#include "undo/undo_manager.h" |
| 32 | + |
| 33 | + |
| 34 | +namespace LibreMapper { |
| 35 | + |
| 36 | +class MapInformationBuilder |
| 37 | +{ |
| 38 | +public: |
| 39 | + explicit MapInformationBuilder(const Map& map); |
| 40 | + |
| 41 | + /** |
| 42 | + * Creates the textual information from the stored information and |
| 43 | + * puts it in the tree widget. |
| 44 | + */ |
| 45 | + void buildTree(std::vector<MapInformation::TreeItem>& tree_items) const; |
| 46 | + |
| 47 | +private: |
| 48 | + struct MapPartUsage { |
| 49 | + QString name; |
| 50 | + int object_count = 0; |
| 51 | + }; |
| 52 | + |
| 53 | + struct FontUsage { |
| 54 | + QString name; |
| 55 | + QString name_substitute; // the substituted font name, can be equal to 'name' |
| 56 | + int symbol_count = 0; |
| 57 | + }; |
| 58 | + |
| 59 | + struct SymbolUsage { |
| 60 | + const Symbol* symbol; |
| 61 | + QString name; |
| 62 | + int object_count = 0; |
| 63 | + std::vector<QString> colors = {}; |
| 64 | + }; |
| 65 | + |
| 66 | + struct SymbolTypeUsage { |
| 67 | + QString name; |
| 68 | + int object_count = 0; |
| 69 | + std::vector<SymbolUsage> symbols = {}; |
| 70 | + }; |
| 71 | + |
| 72 | + struct ColorUsage { |
| 73 | + QString name; |
| 74 | + std::vector<QString> symbols = {}; |
| 75 | + }; |
| 76 | + |
| 77 | + QString crs; |
| 78 | + int scale; |
| 79 | + int undo_steps_count; |
| 80 | + int redo_steps_count; |
| 81 | + int templates_count; |
| 82 | + int symbols_count; |
| 83 | + int objects_count; |
| 84 | + int fonts_count; |
| 85 | + |
| 86 | + std::vector<MapPartUsage> map_parts; |
| 87 | + |
| 88 | + SymbolTypeUsage symbol_types[6]; |
| 89 | + |
| 90 | + std::vector<ColorUsage> colors; |
| 91 | + |
| 92 | + std::vector<FontUsage> fonts; |
| 93 | + |
| 94 | + /** |
| 95 | + * Returns the usage record for a given symbol type. |
| 96 | + */ |
| 97 | + SymbolTypeUsage& getSymbolTypeUsage(Symbol::Type type); |
| 98 | +}; |
| 99 | + |
| 100 | + |
| 101 | + |
| 102 | +MapInformationBuilder::SymbolTypeUsage& MapInformationBuilder::getSymbolTypeUsage(Symbol::Type type) |
| 103 | +{ |
| 104 | + switch (type) |
| 105 | + { |
| 106 | + case Symbol::Point: |
| 107 | + return symbol_types[0]; |
| 108 | + case Symbol::Line: |
| 109 | + return symbol_types[1]; |
| 110 | + case Symbol::Area: |
| 111 | + return symbol_types[2]; |
| 112 | + case Symbol::Combined: |
| 113 | + return symbol_types[3]; |
| 114 | + case Symbol::Text: |
| 115 | + return symbol_types[4]; |
| 116 | + default: |
| 117 | + return symbol_types[5]; |
| 118 | + } |
| 119 | +} |
| 120 | + |
| 121 | +// retrieve and store the information |
| 122 | +MapInformationBuilder::MapInformationBuilder(const Map& map) |
| 123 | +{ |
| 124 | + scale = int(map.getScaleDenominator()); |
| 125 | + |
| 126 | + const auto& map_crs = map.getGeoreferencing().getProjectedCRSId(); |
| 127 | + crs = map.getGeoreferencing().getProjectedCRSName(); |
| 128 | + if (map_crs.isEmpty() && map.getGeoreferencing().getState() == Georeferencing::State::Geospatial) |
| 129 | + { |
| 130 | + crs = ::LibreMapper::Georeferencing::tr("Custom PROJ.4"); |
| 131 | + } |
| 132 | + else if (map_crs != QLatin1String("Local") && map_crs != QLatin1String("PROJ.4")) |
| 133 | + { |
| 134 | + const auto& projected_crs_parameters = map.getGeoreferencing().getProjectedCRSParameters(); |
| 135 | + if (!projected_crs_parameters.empty()) |
| 136 | + { |
| 137 | + QString crs_details = QLatin1String(" (") + (map_crs == QLatin1String("EPSG") ? QCoreApplication::translate("LibreMapper::MapInformation", "code") : QCoreApplication::translate("LibreMapper::MapInformation", "zone")) + QChar::Space + projected_crs_parameters.front() + QLatin1Char(')'); |
| 138 | + crs += crs_details; |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + templates_count = map.getNumTemplates(); |
| 143 | + |
| 144 | + colors.reserve(map.getNumColorPrios()); |
| 145 | + map.applyOnAllColors([this, &map](const auto* color){ |
| 146 | + colors.push_back({color->getName()}); |
| 147 | + |
| 148 | + map.applyOnMatchingSymbols( |
| 149 | + [this](const Symbol* symbol) { colors.back().symbols.push_back(symbol->getNumberAndPlainTextName()); }, |
| 150 | + [color](const Symbol* symbol) { return symbol->containsColor(color); } |
| 151 | + ); |
| 152 | + }); |
| 153 | + |
| 154 | + getSymbolTypeUsage(Symbol::Point).name = QCoreApplication::translate("LibreMapper::MapInformation", "Point symbols"); |
| 155 | + getSymbolTypeUsage(Symbol::Line).name = QCoreApplication::translate("LibreMapper::MapInformation", "Line symbols"); |
| 156 | + getSymbolTypeUsage(Symbol::Area).name = QCoreApplication::translate("LibreMapper::MapInformation", "Area symbols"); |
| 157 | + getSymbolTypeUsage(Symbol::Combined).name = QCoreApplication::translate("LibreMapper::MapInformation", "Combined symbols"); |
| 158 | + getSymbolTypeUsage(Symbol::Text).name = QCoreApplication::translate("LibreMapper::MapInformation", "Text symbols"); |
| 159 | + getSymbolTypeUsage(Symbol::NoSymbol).name = QCoreApplication::translate("LibreMapper::MapInformation", "Undefined symbols"); |
| 160 | + |
| 161 | + symbols_count = map.getNumSymbols(); |
| 162 | + map.applyOnAllSymbols([this, &map](const Symbol* symbol){ |
| 163 | + auto& category = getSymbolTypeUsage(symbol->getType()); |
| 164 | + category.symbols.push_back({symbol, symbol->getNumberAndPlainTextName()}); |
| 165 | + |
| 166 | + auto& colors = category.symbols.back().colors; |
| 167 | + colors.reserve(4); |
| 168 | + map.applyOnMatchingColors( |
| 169 | + [&colors](const MapColor* color) { colors.push_back(color->getName()); }, |
| 170 | + [symbol](const MapColor* color) { return symbol->containsColor(color); } |
| 171 | + ); |
| 172 | + }); |
| 173 | + |
| 174 | + auto category_text = getSymbolTypeUsage(Symbol::Text); |
| 175 | + fonts.reserve(category_text.symbols.size()); |
| 176 | + for (const auto& item : category_text.symbols) |
| 177 | + { |
| 178 | + Q_ASSERT(item.symbol->getType() == Symbol::Text); |
| 179 | + const auto* text_symbol = item.symbol->asText(); |
| 180 | + const auto& font_family = text_symbol->getFontFamily(); |
| 181 | + auto font_family_substituted = QFontInfo(text_symbol->getQFont()).family(); |
| 182 | + auto& font_names = fonts; |
| 183 | + auto found = std::find_if(begin(font_names), end(font_names), [&font_family](const FontUsage& font_count) { return font_count.name == font_family; }); |
| 184 | + if (found == std::end(font_names)) |
| 185 | + font_names.push_back({font_family, font_family_substituted, 1}); |
| 186 | + else |
| 187 | + ++found->symbol_count; |
| 188 | + } |
| 189 | + fonts_count = static_cast<int>(fonts.size()); |
| 190 | + |
| 191 | + map_parts.reserve(map.getNumParts()); |
| 192 | + objects_count = 0; |
| 193 | + for (int i = 0; i < map.getNumParts(); ++i) |
| 194 | + { |
| 195 | + const auto* map_part = map.getPart(i); |
| 196 | + const auto map_part_objects = map_part->getNumObjects(); |
| 197 | + objects_count += map_part_objects; |
| 198 | + map_parts.push_back({map_part->getName(), map_part_objects}); |
| 199 | + |
| 200 | + map_part->applyOnAllObjects([this](const Object* object) { |
| 201 | + const auto* const symbol = object->getSymbol(); |
| 202 | + auto& object_category = getSymbolTypeUsage(symbol ? symbol->getType() : Symbol::NoSymbol); |
| 203 | + object_category.object_count++; |
| 204 | + auto s = std::find_if(object_category.symbols.begin(), object_category.symbols.end(), [symbol](const auto& s) { return symbol == s.symbol; } ); |
| 205 | + if (s == object_category.symbols.end()) |
| 206 | + s = object_category.symbols.insert(object_category.symbols.end(), {symbol, QCoreApplication::translate("LibreMapper::MapInformation", "<undefined>")}); |
| 207 | + s->object_count++; |
| 208 | + }); |
| 209 | + } |
| 210 | + |
| 211 | + const auto& undo_manager = map.undoManager(); |
| 212 | + undo_steps_count = undo_manager.canUndo() ? undo_manager.undoStepCount() : 0; |
| 213 | + redo_steps_count = undo_manager.canRedo() ? undo_manager.redoStepCount() : 0; |
| 214 | +} |
| 215 | + |
| 216 | +void MapInformationBuilder::buildTree(std::vector<MapInformation::TreeItem>& tree_items) const |
| 217 | +{ |
| 218 | + tree_items.clear(); |
| 219 | + |
| 220 | + tree_items.push_back({0, QCoreApplication::translate("LibreMapper::MapInformation", "Map"), QCoreApplication::translate("LibreMapper::MapInformation", "%n object(s)", nullptr, objects_count)}); |
| 221 | + { |
| 222 | + tree_items.push_back({1, QCoreApplication::translate("LibreMapper::MapInformation", "Scale"), QString::fromLatin1("1:%1").arg(scale)}); |
| 223 | + tree_items.push_back({1, QCoreApplication::translate("LibreMapper::MapInformation", "Coordinate reference system"), crs}); |
| 224 | + if (undo_steps_count) |
| 225 | + tree_items.push_back({1, QCoreApplication::translate("LibreMapper::MapInformation", "Undo steps"), QCoreApplication::translate("LibreMapper::MapInformation", "%n step(s)", nullptr, undo_steps_count)}); |
| 226 | + if (redo_steps_count) |
| 227 | + tree_items.push_back({1, QCoreApplication::translate("LibreMapper::MapInformation", "Redo steps"), QCoreApplication::translate("LibreMapper::MapInformation", "%n step(s)", nullptr, redo_steps_count)}); |
| 228 | + } |
| 229 | + |
| 230 | + tree_items.push_back({0, QCoreApplication::translate("LibreMapper::MapInformation", "Templates"), QCoreApplication::translate("LibreMapper::MapInformation", "%n template(s)", nullptr, templates_count)}); |
| 231 | + |
| 232 | + tree_items.push_back({0, QCoreApplication::translate("LibreMapper::MapInformation", "Map parts"), QCoreApplication::translate("LibreMapper::MapInformation", "%n part(s)", nullptr, map_parts.size())}); |
| 233 | + for (const auto& map_part : map_parts) |
| 234 | + { |
| 235 | + tree_items.push_back({1, map_part.name, QCoreApplication::translate("LibreMapper::MapInformation", "%n object(s)", nullptr, map_part.object_count)}); |
| 236 | + } |
| 237 | + |
| 238 | + tree_items.push_back({0, QCoreApplication::translate("LibreMapper::MapInformation", "Symbols"), QCoreApplication::translate("LibreMapper::MapInformation", "%n symbol(s)", nullptr, symbols_count)}); |
| 239 | + for (const auto& map_object : symbol_types) |
| 240 | + { |
| 241 | + if (map_object.object_count == 0 && map_object.name == QCoreApplication::translate("LibreMapper::MapInformation", "Undefined symbols")) |
| 242 | + continue; |
| 243 | + tree_items.push_back({1, map_object.name, QCoreApplication::translate("LibreMapper::MapInformation", "%n object(s)", nullptr, map_object.object_count)}); |
| 244 | + for (const auto& symbol : map_object.symbols) |
| 245 | + { |
| 246 | + tree_items.push_back({2, symbol.name, QCoreApplication::translate("LibreMapper::MapInformation", "%n object(s)", nullptr, symbol.object_count)}); |
| 247 | + for (const auto& color : symbol.colors) |
| 248 | + { |
| 249 | + tree_items.push_back({3, color}); |
| 250 | + } |
| 251 | + } |
| 252 | + } |
| 253 | + |
| 254 | + tree_items.push_back({0, QCoreApplication::translate("LibreMapper::MapInformation", "Colors"), QCoreApplication::translate("LibreMapper::MapInformation", "%n color(s)", nullptr, colors.size())}); |
| 255 | + for (const auto& color : colors) |
| 256 | + { |
| 257 | + tree_items.push_back({1, color.name}); |
| 258 | + for (const auto& symbol : color.symbols) |
| 259 | + tree_items.push_back({2, symbol}); |
| 260 | + } |
| 261 | + |
| 262 | + tree_items.push_back({0, QCoreApplication::translate("LibreMapper::MapInformation", "Fonts"), QCoreApplication::translate("LibreMapper::MapInformation", "%n font(s)", nullptr, fonts_count)}); |
| 263 | + for (const auto& font_name : fonts) |
| 264 | + { |
| 265 | + auto name = font_name.name; |
| 266 | + if (name != font_name.name_substitute) |
| 267 | + name = QCoreApplication::translate("LibreMapper::MapInformation", "%1 (substituted by %2)").arg(name, font_name.name_substitute); |
| 268 | + tree_items.push_back({1, name, QCoreApplication::translate("LibreMapper::MapInformation", "%n symbol(s)", nullptr, font_name.symbol_count)}); |
| 269 | + } |
| 270 | +} |
| 271 | + |
| 272 | + |
| 273 | +MapInformation::MapInformation(const Map* map) |
| 274 | +{ |
| 275 | + if (map) |
| 276 | + MapInformationBuilder(*map).buildTree(tree_items); |
| 277 | +} |
| 278 | + |
| 279 | +QString MapInformation::makeTextReport(int indent) const |
| 280 | +{ |
| 281 | + QString text_report; |
| 282 | + |
| 283 | + auto actual_indent = [indent](int level) { |
| 284 | + return level * indent; |
| 285 | + }; |
| 286 | + auto max_item_length = std::accumulate(tree_items.begin(), tree_items.end(), 0, [actual_indent](auto acc, const auto& item) { |
| 287 | + return std::max<int>(acc, actual_indent(item.level) + item.label.length()); |
| 288 | + }); |
| 289 | + |
| 290 | + for (const auto &tree_item : tree_items) |
| 291 | + { |
| 292 | + if (!text_report.isEmpty() && !tree_item.level) // separate items on topmost level |
| 293 | + text_report += QChar::LineFeed; |
| 294 | + auto item_value = QString(actual_indent(tree_item.level), QChar::Space); |
| 295 | + item_value.append(tree_item.label); |
| 296 | + if (!tree_item.value.isEmpty()) |
| 297 | + { |
| 298 | + item_value = item_value.leftJustified(max_item_length + 5, QChar::Space); |
| 299 | + item_value.append(tree_item.value); |
| 300 | + } |
| 301 | + text_report += item_value + QChar::LineFeed; |
| 302 | + } |
| 303 | + return text_report; |
| 304 | +} |
| 305 | + |
| 306 | + |
| 307 | +} // namespace LibreMapper |
0 commit comments