Skip to content

Commit 2da0885

Browse files
committed
SymbolReportFeature: Generate reports for symbol sets
This feature is to help with validation symbol sets with regard to specifications and existing symbol sets. UI: Symbols > Create symbol set report The current implementation create a single-file HTML report which directly embeds all images.
1 parent 844d8cb commit 2da0885

File tree

8 files changed

+519
-0
lines changed

8 files changed

+519
-0
lines changed

code-check-wrapper.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ for I in \
9696
stretch_map_dialog.cpp \
9797
style_t.cpp \
9898
/symbol.cpp \
99+
symbol_report \
99100
symbol_replacement.cpp \
100101
symbol_replacement_dialog.cpp \
101102
symbol_rule_set.cpp \

src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ set(Mapper_Common_SRCS
114114

115115
core/symbols/area_symbol.cpp
116116
core/symbols/combined_symbol.cpp
117+
core/symbols/html_symbol_report.cpp
117118
core/symbols/line_symbol.cpp
118119
core/symbols/point_symbol.cpp
119120
core/symbols/symbol.cpp
@@ -175,6 +176,7 @@ set(Mapper_Common_SRCS
175176
gui/symbols/symbol_properties_widget.cpp
176177
gui/symbols/symbol_replacement.cpp
177178
gui/symbols/symbol_replacement_dialog.cpp
179+
gui/symbols/symbol_report_feature.cpp
178180
gui/symbols/symbol_setting_dialog.cpp
179181
gui/symbols/text_symbol_settings.cpp
180182

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
/*
2+
* Copyright 2024 Kai Pastor
3+
*
4+
* This file is part of OpenOrienteering.
5+
*
6+
* OpenOrienteering is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* OpenOrienteering is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with OpenOrienteering. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
#include "html_symbol_report.h"
21+
22+
#include <QBuffer>
23+
#include <QByteArray>
24+
#include <QChar>
25+
#include <QColor>
26+
#include <QCoreApplication>
27+
#include <QImage>
28+
#include <QImageWriter>
29+
#include <QLatin1String>
30+
#include <QLocale>
31+
#include <QString>
32+
#include <QTextDocumentFragment>
33+
34+
#include "core/map.h"
35+
#include "core/map_color.h"
36+
#include "core/symbols/symbol.h"
37+
38+
namespace OpenOrienteering {
39+
40+
/**
41+
* Generates HTML reports for colors and symbols in a Map.
42+
*
43+
* The public interface of this class is the free-standing function
44+
* makeHTMLSymbolReport(const Map& map).
45+
*/
46+
class HTMLSymbolReportGenerator
47+
{
48+
private:
49+
/**
50+
* Efficiently produce PNG data from QImage.
51+
*
52+
* An object of this class holds a buffer which is reused in multiple calls
53+
* to write(). So the number of actual memory allocations can be kept small.
54+
*/
55+
class PNGImageWriter
56+
{
57+
QByteArray png {"PNG"};
58+
QByteArray png_data;
59+
60+
public:
61+
/**
62+
* Creates PNG data from an image.
63+
*
64+
* @return A reference to a shared buffer. The buffer is invalidated
65+
* when the function is called again (for the same writer).
66+
*/
67+
const QByteArray& write(const QImage& image)
68+
{
69+
png_data.clear();
70+
QBuffer buffer(&png_data);
71+
QImageWriter(&buffer, png).write(image);
72+
return png_data;
73+
}
74+
};
75+
76+
enum class ColorRowType {
77+
Basic, ///< A color row with number, icon and name
78+
Extended ///< A color row with all details
79+
};
80+
81+
const Map& map;
82+
QImage icon_image;
83+
QLocale locale;
84+
PNGImageWriter image_writer;
85+
86+
QString imgForColor(const QColor& c, const QString& alt)
87+
{
88+
icon_image.fill(c);
89+
return QString::fromLatin1("<img alt=\"%1\" src=\"data:image/png;base64,").arg(alt)
90+
+ QString::fromLatin1(image_writer.write(icon_image).toBase64())
91+
+ QString::fromLatin1("\">");
92+
}
93+
94+
QString makeColorRow(const MapColor& c, ColorRowType row_type)
95+
{
96+
icon_image = QImage(16, 16, QImage::Format_ARGB32);
97+
98+
auto details = QString {};
99+
if (row_type == ColorRowType::Extended)
100+
{
101+
auto cmyk = c.getCmyk();
102+
details = QString::fromLatin1(
103+
"<td>%1</td>" // c
104+
"<td>%2</td>" // m
105+
"<td>%3</td>" // y
106+
"<td>%4</td>" // k
107+
"<td style=\"font-family:monospace\">%5</td>" // rgb
108+
"<td>%6</td>" // spot color
109+
"<td>%7</td>" // knockout
110+
).arg(
111+
locale.toString(100*cmyk.c, 'g', 3),
112+
locale.toString(100*cmyk.m, 'g', 3),
113+
locale.toString(100*cmyk.y, 'g', 3),
114+
locale.toString(100*cmyk.k, 'g', 3),
115+
QColor(c.getRgb()).name(),
116+
QString{c.getSpotColorName()}.replace(QString::fromLatin1(", "), QString::fromLatin1(",<br>")),
117+
c.getKnockout() ? QString::fromLatin1("[X]") : QString{} );
118+
}
119+
120+
auto name = QTextDocumentFragment::fromHtml(c.getName()).toPlainText();
121+
return QString::fromLatin1(
122+
"<tr>"
123+
"<td>%1</td>" // level
124+
"<td>%2</td>" // icon
125+
"<td class=\"name\">%3</td>" // name
126+
"%9" // details
127+
"</tr>\n"
128+
).arg(
129+
QString::number(c.getPriority()),
130+
imgForColor(c, name),
131+
name,
132+
details );
133+
}
134+
135+
QString makeColorSection()
136+
{
137+
QString color_data;
138+
map.applyOnAllColors([this, &color_data](const MapColor* c) {
139+
color_data.append(makeColorRow(*c, ColorRowType::Extended));
140+
});
141+
return QString::fromLatin1(
142+
"<h2>%1</h2>\n"
143+
"<table class=\"colors\">\n"
144+
"<thead>\n"
145+
"<tr>"
146+
"<th colspan=\"2\">%2</th>"
147+
"<th class=\"name\">%3</th>"
148+
"<th>C</th>"
149+
"<th>M</th>"
150+
"<th>Y</th>"
151+
"<th>K</th>"
152+
"<th>%4</th>"
153+
"<th>%5</th>"
154+
"<th>%6</th>"
155+
"</tr>\n"
156+
"</thead>\n"
157+
"<tbody>\n"
158+
"%9"
159+
"</tbody>\n"
160+
"</table>\n"
161+
).arg(
162+
QCoreApplication::translate("OpenOrienteering::SymbolReport", "Map Colors"),
163+
QCoreApplication::translate("OpenOrienteering::SymbolReport", "Color"),
164+
QCoreApplication::translate("OpenOrienteering::SymbolReport", "Name"),
165+
QCoreApplication::translate("OpenOrienteering::SymbolReport", "RGB"),
166+
QCoreApplication::translate("OpenOrienteering::SymbolReport", "Spot colors"),
167+
QCoreApplication::translate("OpenOrienteering::SymbolReport", "Knockout"),
168+
color_data );
169+
}
170+
171+
QString imgForSymbol(const Symbol& s, const QString& alt)
172+
{
173+
// For better quality and compression,
174+
// using multiple of display size but no antialiasing
175+
auto const display_size = 48;
176+
icon_image = s.createIcon(map, 4 * display_size, false);
177+
return QString::fromLatin1("<img alt=\"%1\" width=\"%2\" src=\"data:image/png;base64,").arg(alt).arg(display_size)
178+
+ QString::fromLatin1(image_writer.write(icon_image).toBase64())
179+
+ QString::fromLatin1("\">");
180+
}
181+
182+
QString colorsForSymbol(const Symbol& s)
183+
{
184+
QString color_data;
185+
map.applyOnAllColors([this, &s, &color_data](const MapColor* c){
186+
if (s.containsColor(c))
187+
color_data.append(makeColorRow(*c, ColorRowType::Basic));
188+
});
189+
return QString::fromLatin1(
190+
"<table>\n"
191+
"<tbody>\n"
192+
"%1"
193+
"</tbody>\n"
194+
"</table>\n"
195+
).arg(
196+
color_data );
197+
}
198+
199+
QString makeSymbolRow(const Symbol& s)
200+
{
201+
auto label = QString { s.getNumberAsString()
202+
+ QChar::Space
203+
+ QTextDocumentFragment::fromHtml(s.getName()).toPlainText() };
204+
auto extra_text = QString{};
205+
if (s.isRotatable())
206+
extra_text += QCoreApplication::translate("OpenOrienteering::SymbolReport", "[X] %1")
207+
.arg(QCoreApplication::translate("OpenOrienteering::SymbolReport", "Symbol orientation can be changed."))
208+
+ QLatin1String("<br>\n");
209+
if (s.hasRotatableFillPattern())
210+
extra_text += QCoreApplication::translate("OpenOrienteering::SymbolReport", "[X] %1")
211+
.arg(QCoreApplication::translate("OpenOrienteering::SymbolReport", "Pattern orientation can be changed."))
212+
+ QLatin1String("<br>\n");
213+
if (s.isHelperSymbol())
214+
extra_text += QCoreApplication::translate("OpenOrienteering::SymbolReport", "[X] %1")
215+
.arg(QCoreApplication::translate("OpenOrienteering::SymbolReport", "Helper symbol"))
216+
+ QLatin1String("<br>\n");
217+
return QString::fromLatin1(
218+
"<tr>"
219+
"<td style=\"vertical-align:top;\">%1</td>\n" // icon
220+
"<td style=\"vertical-align:middle;\"><b>%2</b></td>" // label
221+
"</tr>\n"
222+
"<tr>"
223+
"<td>&nbsp;</td>\n"
224+
"<td style=\"padding-bottom:18px;\">\n"
225+
"<div>\n%3</div>\n" // description
226+
"<p>%4</p>\n" // configuration properties
227+
"%5</td>" // colors
228+
"</tr>\n"
229+
).arg(
230+
imgForSymbol(s, label),
231+
label,
232+
QString(s.getDescription()).replace(QChar::LineFeed, QLatin1String("<br>\n")),
233+
extra_text,
234+
colorsForSymbol(s) );
235+
}
236+
237+
QString makeSymbolSection()
238+
{
239+
QString symbol_data;
240+
map.applyOnAllSymbols([this, &symbol_data](const Symbol* s){
241+
symbol_data.append(makeSymbolRow(*s));
242+
});
243+
return QString::fromLatin1(
244+
"<h2>%1</h2>\n"
245+
"<table class=\"symbols\">\n"
246+
"<tbody>\n"
247+
"%2"
248+
"</tbody>\n"
249+
"</table>\n"
250+
).arg(
251+
QCoreApplication::translate("OpenOrienteering::SymbolReport", "Symbols"),
252+
symbol_data );
253+
}
254+
255+
public:
256+
explicit HTMLSymbolReportGenerator(const Map& map)
257+
: map(map)
258+
{}
259+
260+
QString generate()
261+
{
262+
return QString::fromLatin1(
263+
"<!DOCTYPE html>\n"
264+
"<html>\n"
265+
"<head>\n"
266+
"<meta charset=\"utf-8\">"
267+
"<title>%0</title>\n"
268+
"<meta name=\"generator\" content=\"OpenOrienteering Mapper\">\n"
269+
"<style>\n"
270+
"th { font-size: 120%; text-align: center; }\n"
271+
"th, td { padding: 4px; }\n"
272+
"table.colors { text-align: center; }\n"
273+
"table.colors td:first-child { text-align: right; }\n"
274+
"table.colors td.name { text-align: left; }\n"
275+
"table.symbols { max-width: 60em; }\n"
276+
"</style>\n"
277+
"</head>\n"
278+
"<body>\n"
279+
"<h1>%0</h1>\n"
280+
"%1"
281+
"%2"
282+
"</body>\n"
283+
"</html>"
284+
).arg(
285+
QCoreApplication::translate("OpenOrienteering::SymbolReport", "Symbol Set Report on '%0'").arg(map.symbolSetId()),
286+
makeColorSection(),
287+
makeSymbolSection() );
288+
}
289+
};
290+
291+
292+
QString makeHTMLSymbolReport(const Map& map)
293+
{
294+
return HTMLSymbolReportGenerator(map).generate();
295+
}
296+
297+
} // namespace OpenOrienteering
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2024 Kai Pastor
3+
*
4+
* This file is part of OpenOrienteering.
5+
*
6+
* OpenOrienteering is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* OpenOrienteering is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with OpenOrienteering. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
#ifndef OPENORIENTEERING_HTML_SYMBOL_REPORT_H
21+
#define OPENORIENTEERING_HTML_SYMBOL_REPORT_H
22+
23+
class QString;
24+
25+
namespace OpenOrienteering {
26+
27+
class Map;
28+
29+
/**
30+
* Generates a symbol set report in HTML format.
31+
*/
32+
QString makeHTMLSymbolReport(const Map& map);
33+
34+
} // namespace OpenOrienteering
35+
36+
#endif // OPENORIENTEERING_HTML_SYMBOL_REPORT_H

src/gui/map/map_editor.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
#include "gui/map/map_widget.h"
128128
#include "gui/map/rotate_map_dialog.h"
129129
#include "gui/symbols/symbol_replacement.h"
130+
#include "gui/symbols/symbol_report_feature.h"
130131
#include "gui/widgets/action_grid_bar.h"
131132
#include "gui/widgets/color_list_widget.h"
132133
#include "gui/widgets/compass_display.h"
@@ -476,6 +477,7 @@ void MapEditorController::setEditingInProgress(bool value)
476477
scale_all_symbols_act->setEnabled(!editing_in_progress);
477478
load_symbols_from_act->setEnabled(!editing_in_progress);
478479
load_crt_act->setEnabled(!editing_in_progress);
480+
symbol_report_feature->setEnabled(!editing_in_progress);
479481

480482
// Templates menu
481483
paint_feature->setEnabled(!editing_in_progress);
@@ -1018,6 +1020,7 @@ void MapEditorController::createActions()
10181020
load_symbols_from_act = newAction("loadsymbols", tr("Replace symbol set..."), this, SLOT(loadSymbolsFromClicked()), nullptr, tr("Replace the symbols with those from another map file"), "symbol_replace_dialog.html");
10191021
load_crt_act = newAction("loadcrt", tr("Load CRT file..."), this, SLOT(loadCrtClicked()), nullptr, tr("Assign new symbols by cross-reference table"), "symbol_replace_dialog.html");
10201022
/*QAction* load_colors_from_act = newAction("loadcolors", tr("Load colors from..."), this, SLOT(loadColorsFromClicked()), nullptr, tr("Replace the colors with those from another map file"));*/
1023+
symbol_report_feature = std::make_unique<SymbolReportFeature>(*this);
10211024

10221025
scale_all_symbols_act = newAction("scaleall", tr("Scale all symbols..."), this, SLOT(scaleAllSymbolsClicked()), nullptr, tr("Scale the whole symbol set"), "map_menu.html");
10231026
georeferencing_act = newAction("georef", tr("Georeferencing..."), this, SLOT(editGeoreferencing()), nullptr, QString{}, "georeferencing.html");
@@ -1270,6 +1273,8 @@ void MapEditorController::createMenuAndToolbars()
12701273
symbols_menu->addAction(load_symbols_from_act);
12711274
symbols_menu->addAction(load_crt_act);
12721275
/*symbols_menu->addAction(load_colors_from_act);*/
1276+
symbols_menu->addSeparator();
1277+
symbols_menu->addAction(symbol_report_feature->showDialogAction());
12731278

12741279
// Templates menu
12751280
QMenu* template_menu = window->menuBar()->addMenu(tr("&Templates"));

0 commit comments

Comments
 (0)