1 // SPDX-FileCopyrightText: 2006-2007 Torsten Rahn <tackat@kde.org>
2 // SPDX-FileCopyrightText: 2007 Inge Wallin <ingwa@kde.org>
3 // SPDX-FileCopyrightText: 2012 Illya Kovalevskyy <illya.kovalevskyy@gmail.com>
4 // SPDX-FileCopyrightText: 2013 Yazeed Zoabi <yazeedz.zoabi@gmail.com>
5 //
6 // SPDX-License-Identifier: LGPL-2.1-or-later
7 
8 #include "MarbleLegendBrowser.h"
9 
10 #include <QCoreApplication>
11 #include <QUrl>
12 #include <QDesktopServices>
13 #include <QEvent>
14 #include <QFile>
15 #include <QMouseEvent>
16 #include <QPainter>
17 #include <QRegExp>
18 
19 #ifndef MARBLE_NO_WEBKITWIDGETS
20 #include <QWebEnginePage>
21 #include <QWebChannel>
22 #endif
23 
24 #include <QTextDocument>
25 
26 #include "GeoSceneDocument.h"
27 #include "GeoSceneHead.h"
28 #include "GeoSceneLegend.h"
29 #include "GeoSceneSection.h"
30 #include "GeoSceneIcon.h"
31 #include "GeoSceneItem.h"
32 #include "GeoSceneProperty.h"
33 #include "GeoSceneSettings.h"
34 #include "MarbleModel.h"
35 #include "MarbleDebug.h"
36 #include "TemplateDocument.h"
37 #include "MarbleDirs.h"
38 
39 namespace Marble
40 {
41 
42 class MarbleLegendBrowserPrivate
43 {
44  public:
45     MarbleModel            *m_marbleModel;
46     QMap<QString, bool>     m_checkBoxMap;
47     QMap<QString, QPixmap>  m_symbolMap;
48     QString                 m_currentThemeId;
49     MarbleJsWrapper        *m_jsWrapper;
50 };
51 
52 
53 // ================================================================
54 
55 
MarbleLegendBrowser(QWidget * parent)56 MarbleLegendBrowser::MarbleLegendBrowser( QWidget *parent )
57     : MarbleWebView( parent ),
58       d( new MarbleLegendBrowserPrivate )
59 {
60     d->m_marbleModel = nullptr;
61     d->m_jsWrapper = new MarbleJsWrapper(this);
62 }
63 
~MarbleLegendBrowser()64 MarbleLegendBrowser::~MarbleLegendBrowser()
65 {
66     delete d;
67 }
68 
setMarbleModel(MarbleModel * marbleModel)69 void MarbleLegendBrowser::setMarbleModel( MarbleModel *marbleModel )
70 {
71     // We need this to be able to get to the MapTheme.
72     d->m_marbleModel = marbleModel;
73 
74     if ( d->m_marbleModel ) {
75         connect ( d->m_marbleModel, SIGNAL(themeChanged(QString)),
76                   this, SLOT(initTheme()) );
77     }
78 }
79 
sizeHint() const80 QSize MarbleLegendBrowser::sizeHint() const
81 {
82     return QSize( 320, 320 );
83 }
84 
initTheme()85 void MarbleLegendBrowser::initTheme()
86 {
87     // Check for a theme specific legend.html first
88     if ( d->m_marbleModel != nullptr && d->m_marbleModel->mapTheme() != nullptr )
89     {
90         const GeoSceneDocument *currentMapTheme = d->m_marbleModel->mapTheme();
91 
92         d->m_checkBoxMap.clear();
93 
94         for ( const GeoSceneProperty *property: currentMapTheme->settings()->allProperties() ) {
95             if ( property->available() ) {
96                 d->m_checkBoxMap[ property->name() ] = property->value();
97             }
98         }
99 
100         disconnect ( currentMapTheme, SIGNAL(valueChanged(QString,bool)), nullptr, nullptr );
101         connect ( currentMapTheme, SIGNAL(valueChanged(QString,bool)),
102                   this, SLOT(setCheckedProperty(QString,bool)) );
103     }
104 
105     if ( isVisible() ) {
106         loadLegend();
107     }
108 }
109 
loadLegend()110 void MarbleLegendBrowser::loadLegend()
111 {
112     if (!d->m_marbleModel) {
113         return;
114     }
115 
116 #ifndef MARBLE_NO_WEBKITWIDGETS
117     if (d->m_currentThemeId != d->m_marbleModel->mapThemeId()) {
118         d->m_currentThemeId = d->m_marbleModel->mapThemeId();
119     } else {
120         return;
121     }
122 
123     // Read the html string.
124     QString legendPath;
125 
126     // Check for a theme specific legend.html first
127     if (d->m_marbleModel->mapTheme() != nullptr ) {
128         const GeoSceneDocument *currentMapTheme = d->m_marbleModel->mapTheme();
129 
130         legendPath = MarbleDirs::path(QLatin1String("maps/") +
131             currentMapTheme->head()->target() + QLatin1Char('/') +
132             currentMapTheme->head()->theme() + QLatin1String("/legend.html"));
133     }
134     if ( legendPath.isEmpty() ) {
135         legendPath = MarbleDirs::path(QStringLiteral("legend.html"));
136     }
137 
138     QString finalHtml = readHtml( QUrl::fromLocalFile( legendPath ) );
139 
140     TemplateDocument doc(finalHtml);
141     finalHtml = doc.finalText();
142 
143     injectWebChannel(finalHtml);
144     reverseSupportCheckboxes(finalHtml);
145 
146     // Generate some parts of the html from the MapTheme <Legend> tag.
147     const QString sectionsHtml = generateSectionsHtml();
148 
149     // And then create the final html from these two parts.
150     finalHtml.replace( QString( "<!-- ##customLegendEntries:all## -->" ), sectionsHtml );
151 
152     translateHtml( finalHtml );
153 
154     QUrl baseUrl = QUrl::fromLocalFile( legendPath );
155 
156     // Set the html string in the QTextBrowser.
157     MarbleWebPage * page = new MarbleWebPage(this);
158     connect( page, SIGNAL(linkClicked(QUrl)), this, SLOT(openLinkExternally(QUrl)) );
159     page->setHtml(finalHtml, baseUrl);
160     setPage(page);
161 
162     QWebChannel *channel = new QWebChannel(page);
163     channel->registerObject(QStringLiteral("Marble"), d->m_jsWrapper);
164     page->setWebChannel(channel);
165 
166     if ( d->m_marbleModel ) {
167         page->toHtml([=]( QString document ) {
168             d->m_marbleModel->setLegend( new QTextDocument(document) );
169         });
170     }
171 #endif
172 }
173 
openLinkExternally(const QUrl & url)174 void MarbleLegendBrowser::openLinkExternally( const QUrl &url )
175 {
176     if (url.scheme() == QLatin1String("tour")) {
177         emit tourLinkClicked(QLatin1String("maps/") + url.host() + url.path());
178     } else {
179         QDesktopServices::openUrl( url );
180     }
181 }
182 
event(QEvent * event)183 bool MarbleLegendBrowser::event( QEvent * event )
184 {
185     // "Delayed initialization": legend gets created only
186     if ( event->type() == QEvent::Show ) {
187         setVisible(true);
188         loadLegend();
189         return true;
190     }
191 
192     return MarbleWebView::event( event );
193 }
194 
readHtml(const QUrl & name)195 QString MarbleLegendBrowser::readHtml( const QUrl & name )
196 {
197     QString html;
198 
199     QFile data( name.toLocalFile() );
200     if ( data.open( QFile::ReadOnly ) ) {
201         QTextStream in( &data );
202         html = in.readAll();
203         data.close();
204     }
205 
206     return html;
207 }
208 
translateHtml(QString & html)209 void MarbleLegendBrowser::translateHtml( QString & html )
210 {
211     // must match string extraction in Messages.sh
212     QString s = html;
213     QRegExp rx( "</?\\w+((\\s+\\w+(\\s*=\\s*(?:\".*\"|'.*'|[^'\">\\s]+))?)+\\s*|\\s*)/?>" );
214     rx.setMinimal( true );
215     s.replace( rx, "\n" );
216     s.replace( QRegExp( "\\s*\n\\s*" ), "\n" );
217     const QStringList words = s.split(QLatin1Char('\n'), QString::SkipEmptyParts);
218 
219     QStringList::const_iterator i = words.constBegin();
220     QStringList::const_iterator const end = words.constEnd();
221     for (; i != end; ++i )
222         html.replace(*i, QCoreApplication::translate("Legends", (*i).toUtf8().constData()));
223 }
224 
injectWebChannel(QString & html)225 void MarbleLegendBrowser::injectWebChannel(QString &html)
226 {
227   QString webChannelCode = "<script type=\"text/javascript\" src=\"qrc:///qtwebchannel/qwebchannel.js\"></script>";
228   webChannelCode += "<script> document.addEventListener(\"DOMContentLoaded\", function() {"
229                       "new QWebChannel(qt.webChannelTransport, function (channel) {"
230                       "Marble = channel.objects.Marble;"
231                       "});"
232                      "}); </script>"
233                      "</head>";
234   html.replace("</head>", webChannelCode);
235 }
236 
reverseSupportCheckboxes(QString & html)237 void MarbleLegendBrowser::reverseSupportCheckboxes(QString &html)
238 {
239     const QString old = "<a href=\"checkbox:cities\"/>";
240 
241     QString checked;
242     if (d->m_checkBoxMap["cities"])
243         checked = "checked";
244 
245     const QString repair = QLatin1String(
246             "<input style=\"position: relative; top: -4px;\" type=\"checkbox\" "
247             "onchange=\"Marble.setCheckedProperty(this.name, this.checked);\" ") + checked + QLatin1String(" name=\"cities\"/>");
248 
249     html.replace(old, repair);
250 }
251 
generateSectionsHtml()252 QString MarbleLegendBrowser::generateSectionsHtml()
253 {
254     // Generate HTML to include into legend.html here.
255 
256     QString customLegendString;
257 
258     if ( d->m_marbleModel == nullptr || d->m_marbleModel->mapTheme() == nullptr )
259         return QString();
260 
261     const GeoSceneDocument *currentMapTheme = d->m_marbleModel->mapTheme();
262 
263     d->m_symbolMap.clear();
264 
265     /* Okay, if you are reading it now, be ready for hell!
266      * We can't optimize this part of Legend Browser, but we will
267      * do it, anyway. It's complicated a lot, the most important
268      * thing is to understand everything.
269      */
270     for ( const GeoSceneSection *section: currentMapTheme->legend()->sections() ) {
271         // Each section is divided into the "well"
272         // Well is like a block of data with rounded corners
273         customLegendString += QLatin1String("<div class=\"well well-small well-legend\">");
274 
275         const QString heading = QCoreApplication::translate("DGML", section->heading().toUtf8().constData());
276         QString checkBoxString;
277         if (section->checkable()) {
278             // If it's needed to make a checkbox here, we will
279             QString const checked = d->m_checkBoxMap[section->connectTo()] ? "checked" : "";
280             /* Important comment:
281              * We inject Marble object into JavaScript of each legend html file
282              * This is only one way to handle checkbox changes we see, so
283              * Marble.setCheckedProperty is a function that does it
284              */
285             if(!section->radio().isEmpty()) {
286                 checkBoxString = QLatin1String(
287                         "<label class=\"section-head\">"
288                         "<input style=\"position: relative; top: -4px;\" type=\"radio\" "
289                         "onchange=\"Marble.setRadioCheckedProperty(this.value, this.name ,this.checked);\" ") +
290                         checked + QLatin1String(" value=\"") + section->connectTo() + QLatin1String("\" name=\"") + section->radio() + QLatin1String("\" /><span>")
291                         + heading +
292                         QLatin1String("</span></label>");
293 
294             } else {
295                 checkBoxString = QLatin1String(
296                         "<label class=\"section-head\">"
297                         "<input style=\"position: relative; top: -4px;\" type=\"checkbox\" "
298                         "onchange=\"Marble.setCheckedProperty(this.name, this.checked);\" ") + checked + QLatin1String(" name=\"") + section->connectTo() + QLatin1String("\" /><span>")
299                         + heading +
300                         QLatin1String("</span></label>");
301 
302             }
303             customLegendString += checkBoxString;
304 
305         } else {
306             customLegendString += QLatin1String("<h4 class=\"section-head\">") + heading + QLatin1String("</h4>");
307         }
308 
309         for (const GeoSceneItem *item: section->items()) {
310 
311             // checkbox for item
312             QString checkBoxString;
313             if (item->checkable()) {
314                 QString const checked = d->m_checkBoxMap[item->connectTo()] ? "checked" : "";
315                 checkBoxString = QLatin1String(
316                         "<input type=\"checkbox\" "
317                         "onchange=\"Marble.setCheckedProperty(this.name, this.checked);\" ")
318                         + checked + QLatin1String(" name=\"") + item->connectTo() + QLatin1String("\" />");
319 
320             }
321 
322             // pixmap and text
323             QString src;
324             QString styleDiv;
325             int pixmapWidth = 24;
326             int pixmapHeight = 12;
327             if (!item->icon()->pixmap().isEmpty()) {
328                 QString path = MarbleDirs::path( item->icon()->pixmap() );
329                 const QPixmap oncePixmap(path);
330                 pixmapWidth = oncePixmap.width();
331                 pixmapHeight = oncePixmap.height();
332                 src = QUrl::fromLocalFile( path ).toString();
333                 styleDiv = QLatin1String("width: ") + QString::number(pixmapWidth) + QLatin1String("px; height: ") +
334                         QString::number(pixmapHeight) + QLatin1String("px;");
335             } else {
336               // Workaround for rendered border around empty images in webkit
337               src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
338             }
339             // NOTICE. There are some pixmaps without image, so we should
340             //         create just a plain rectangle with set color
341             if (QColor(item->icon()->color()).isValid()) {
342                 const QColor color = item->icon()->color();
343                 styleDiv = QLatin1String("width: ") + QString::number(pixmapWidth) + QLatin1String("px; height: ") +
344                         QString::number(pixmapHeight) + QLatin1String("px; background-color: ") + color.name() + QLatin1Char(';');
345             }
346             styleDiv += " position: relative; top: -3px;";
347             const QString text = QCoreApplication::translate("DGML", item->text().toUtf8().constData());
348             QString html = QLatin1String(
349                     "<div class=\"legend-entry\">"
350                     "  <label>") + checkBoxString + QLatin1String(
351                     "    <img class=\"image-pic\" src=\"") + src + QLatin1String("\" style=\"") + styleDiv + QLatin1String("\"/>"
352                     "    <span class=\"kotation\" >") + text + QLatin1String("</span>"
353                     "  </label>"
354                     "</div>");
355             customLegendString += html;
356         }
357         customLegendString += QLatin1String("</div>"); // <div class="well">
358     }
359 
360     return customLegendString;
361 }
362 
setCheckedProperty(const QString & name,bool checked)363 void MarbleLegendBrowser::setCheckedProperty( const QString& name, bool checked )
364 {
365     if (checked != d->m_checkBoxMap[name]) {
366         d->m_checkBoxMap[name] = checked;
367         emit toggledShowProperty( name, checked );
368     }
369 }
370 
setRadioCheckedProperty(const QString & value,const QString & name,bool checked)371 void MarbleLegendBrowser::setRadioCheckedProperty( const QString& value, const QString& name , bool checked )
372 {
373   Q_UNUSED(value)
374   if (checked != d->m_checkBoxMap[name]) {
375       d->m_checkBoxMap[name] = checked;
376       emit toggledShowProperty( name, checked );
377   }
378 }
379 
380 }
381 
382 #include "moc_MarbleLegendBrowser.cpp"
383