File indexing completed on 2024-04-21 03:49:42

0001 // SPDX-FileCopyrightText: 2006-2007 Torsten Rahn <tackat@kde.org>
0002 // SPDX-FileCopyrightText: 2007 Inge Wallin <ingwa@kde.org>
0003 // SPDX-FileCopyrightText: 2012 Illya Kovalevskyy <illya.kovalevskyy@gmail.com>
0004 // SPDX-FileCopyrightText: 2013 Yazeed Zoabi <yazeedz.zoabi@gmail.com>
0005 //
0006 // SPDX-License-Identifier: LGPL-2.1-or-later
0007 
0008 #include "MarbleLegendBrowser.h"
0009 
0010 #include <QCoreApplication>
0011 #include <QUrl>
0012 #include <QDesktopServices>
0013 #include <QEvent>
0014 #include <QFile>
0015 #include <QMouseEvent>
0016 #include <QPainter>
0017 #include <QRegExp>
0018 
0019 #ifndef MARBLE_NO_WEBKITWIDGETS
0020 #include <QWebEnginePage>
0021 #include <QWebChannel>
0022 #endif
0023 
0024 #include <QTextDocument>
0025 
0026 #include "GeoSceneDocument.h"
0027 #include "GeoSceneHead.h"
0028 #include "GeoSceneLegend.h"
0029 #include "GeoSceneSection.h"
0030 #include "GeoSceneIcon.h"
0031 #include "GeoSceneItem.h"
0032 #include "GeoSceneProperty.h"
0033 #include "GeoSceneSettings.h"
0034 #include "MarbleModel.h"
0035 #include "MarbleDebug.h"
0036 #include "TemplateDocument.h"
0037 #include "MarbleDirs.h"
0038 
0039 namespace Marble
0040 {
0041 
0042 class MarbleLegendBrowserPrivate
0043 {
0044  public:
0045     MarbleModel            *m_marbleModel;
0046     QMap<QString, bool>     m_checkBoxMap;
0047     QMap<QString, QPixmap>  m_symbolMap;
0048     QString                 m_currentThemeId;
0049     MarbleJsWrapper        *m_jsWrapper;
0050 };
0051 
0052 
0053 // ================================================================
0054 
0055 
0056 MarbleLegendBrowser::MarbleLegendBrowser( QWidget *parent )
0057     : MarbleWebView( parent ),
0058       d( new MarbleLegendBrowserPrivate )
0059 {
0060     d->m_marbleModel = nullptr;
0061     d->m_jsWrapper = new MarbleJsWrapper(this);
0062 }
0063 
0064 MarbleLegendBrowser::~MarbleLegendBrowser()
0065 {
0066     delete d;
0067 }
0068 
0069 void MarbleLegendBrowser::setMarbleModel( MarbleModel *marbleModel )
0070 {
0071     // We need this to be able to get to the MapTheme.
0072     d->m_marbleModel = marbleModel;
0073 
0074     if ( d->m_marbleModel ) {
0075         connect ( d->m_marbleModel, SIGNAL(themeChanged(QString)),
0076                   this, SLOT(initTheme()) );
0077     }
0078 }
0079 
0080 QSize MarbleLegendBrowser::sizeHint() const
0081 {
0082     return QSize( 320, 320 );
0083 }
0084 
0085 void MarbleLegendBrowser::initTheme()
0086 {
0087     // Check for a theme specific legend.html first
0088     if ( d->m_marbleModel != nullptr && d->m_marbleModel->mapTheme() != nullptr )
0089     {
0090         const GeoSceneDocument *currentMapTheme = d->m_marbleModel->mapTheme();
0091 
0092         d->m_checkBoxMap.clear();
0093 
0094         for ( const GeoSceneProperty *property: currentMapTheme->settings()->allProperties() ) {
0095             if ( property->available() ) {
0096                 d->m_checkBoxMap[ property->name() ] = property->value();
0097             }
0098         }
0099 
0100         disconnect ( currentMapTheme, SIGNAL(valueChanged(QString,bool)), nullptr, nullptr );
0101         connect ( currentMapTheme, SIGNAL(valueChanged(QString,bool)),
0102                   this, SLOT(setCheckedProperty(QString,bool)) );
0103     }
0104 
0105     if ( isVisible() ) {
0106         loadLegend();
0107     }
0108 }
0109 
0110 void MarbleLegendBrowser::loadLegend()
0111 {
0112     if (!d->m_marbleModel) {
0113         return;
0114     }
0115 
0116 #ifndef MARBLE_NO_WEBKITWIDGETS
0117     if (d->m_currentThemeId != d->m_marbleModel->mapThemeId()) {
0118         d->m_currentThemeId = d->m_marbleModel->mapThemeId();
0119     } else {
0120         return;
0121     }
0122 
0123     // Read the html string.
0124     QString legendPath;
0125 
0126     // Check for a theme specific legend.html first
0127     if (d->m_marbleModel->mapTheme() != nullptr ) {
0128         const GeoSceneDocument *currentMapTheme = d->m_marbleModel->mapTheme();
0129 
0130         legendPath = MarbleDirs::path(QLatin1String("maps/") +
0131             currentMapTheme->head()->target() + QLatin1Char('/') +
0132             currentMapTheme->head()->theme() + QLatin1String("/legend.html"));
0133     }
0134     if ( legendPath.isEmpty() ) {
0135         legendPath = MarbleDirs::path(QStringLiteral("legend.html"));
0136     }
0137 
0138     QString finalHtml = readHtml( QUrl::fromLocalFile( legendPath ) );
0139 
0140     TemplateDocument doc(finalHtml);
0141     finalHtml = doc.finalText();
0142 
0143     injectWebChannel(finalHtml);
0144     reverseSupportCheckboxes(finalHtml);
0145 
0146     // Generate some parts of the html from the MapTheme <Legend> tag.
0147     const QString sectionsHtml = generateSectionsHtml();
0148 
0149     // And then create the final html from these two parts.
0150     finalHtml.replace( QString( "<!-- ##customLegendEntries:all## -->" ), sectionsHtml );
0151 
0152     translateHtml( finalHtml );
0153 
0154     QUrl baseUrl = QUrl::fromLocalFile( legendPath );
0155 
0156     // Set the html string in the QTextBrowser.
0157     MarbleWebPage * page = new MarbleWebPage(this);
0158     connect( page, SIGNAL(linkClicked(QUrl)), this, SLOT(openLinkExternally(QUrl)) );
0159     page->setHtml(finalHtml, baseUrl);
0160     setPage(page);
0161 
0162     QWebChannel *channel = new QWebChannel(page);
0163     channel->registerObject(QStringLiteral("Marble"), d->m_jsWrapper);
0164     page->setWebChannel(channel);
0165 
0166     if ( d->m_marbleModel ) {
0167         page->toHtml([=]( QString document ) {
0168             d->m_marbleModel->setLegend( new QTextDocument(document) );
0169         });
0170     }
0171 #endif
0172 }
0173 
0174 void MarbleLegendBrowser::openLinkExternally( const QUrl &url )
0175 {
0176     if (url.scheme() == QLatin1String("tour")) {
0177         emit tourLinkClicked(QLatin1String("maps/") + url.host() + url.path());
0178     } else {
0179         QDesktopServices::openUrl( url );
0180     }
0181 }
0182 
0183 bool MarbleLegendBrowser::event( QEvent * event )
0184 {
0185     // "Delayed initialization": legend gets created only 
0186     if ( event->type() == QEvent::Show ) {
0187         loadLegend();
0188     }
0189 
0190     return MarbleWebView::event( event );
0191 }
0192 
0193 QString MarbleLegendBrowser::readHtml( const QUrl & name )
0194 {
0195     QString html;
0196 
0197     QFile data( name.toLocalFile() );
0198     if ( data.open( QFile::ReadOnly ) ) {
0199         QTextStream in( &data );
0200         html = in.readAll();
0201         data.close();
0202     }
0203 
0204     return html;
0205 }
0206 
0207 void MarbleLegendBrowser::translateHtml( QString & html )
0208 {
0209     // must match string extraction in Messages.sh
0210     QString s = html;
0211     QRegExp rx( "</?\\w+((\\s+\\w+(\\s*=\\s*(?:\".*\"|'.*'|[^'\">\\s]+))?)+\\s*|\\s*)/?>" );
0212     rx.setMinimal( true );
0213     s.replace( rx, "\n" );
0214     s.replace( QRegExp( "\\s*\n\\s*" ), "\n" );
0215     const QStringList words = s.split(QLatin1Char('\n'), QString::SkipEmptyParts);
0216 
0217     QStringList::const_iterator i = words.constBegin();
0218     QStringList::const_iterator const end = words.constEnd();
0219     for (; i != end; ++i )
0220         html.replace(*i, QCoreApplication::translate("Legends", (*i).toUtf8().constData()));
0221 }
0222 
0223 void MarbleLegendBrowser::injectWebChannel(QString &html)
0224 {
0225   QString webChannelCode = "<script type=\"text/javascript\" src=\"qrc:///qtwebchannel/qwebchannel.js\"></script>";
0226   webChannelCode += "<script> document.addEventListener(\"DOMContentLoaded\", function() {"
0227                       "new QWebChannel(qt.webChannelTransport, function (channel) {"
0228                       "Marble = channel.objects.Marble;"
0229                       "});"
0230                      "}); </script>"
0231                      "</head>";
0232   html.replace("</head>", webChannelCode);
0233 }
0234 
0235 void MarbleLegendBrowser::reverseSupportCheckboxes(QString &html)
0236 {
0237     const QString old = "<a href=\"checkbox:cities\"/>";
0238 
0239     QString checked;
0240     if (d->m_checkBoxMap["cities"])
0241         checked = "checked";
0242 
0243     const QString repair = QLatin1String(
0244             "<input style=\"position: relative; top: -4px;\" type=\"checkbox\" "
0245             "onchange=\"Marble.setCheckedProperty(this.name, this.checked);\" ") + checked + QLatin1String(" name=\"cities\"/>");
0246 
0247     html.replace(old, repair);
0248 }
0249 
0250 QString MarbleLegendBrowser::generateSectionsHtml()
0251 {
0252     // Generate HTML to include into legend.html here.
0253 
0254     QString customLegendString;
0255 
0256     if ( d->m_marbleModel == nullptr || d->m_marbleModel->mapTheme() == nullptr )
0257         return QString();
0258 
0259     const GeoSceneDocument *currentMapTheme = d->m_marbleModel->mapTheme();
0260 
0261     d->m_symbolMap.clear();
0262 
0263     /* Okay, if you are reading it now, be ready for hell!
0264      * We can't optimize this part of Legend Browser, but we will
0265      * do it, anyway. It's complicated a lot, the most important
0266      * thing is to understand everything.
0267      */
0268     for ( const GeoSceneSection *section: currentMapTheme->legend()->sections() ) {
0269         // Each section is divided into the "well"
0270         // Well is like a block of data with rounded corners
0271         customLegendString += QLatin1String("<div class=\"well well-small well-legend\">");
0272 
0273         const QString heading = QCoreApplication::translate("DGML", section->heading().toUtf8().constData());
0274         QString checkBoxString;
0275         if (section->checkable()) {
0276             // If it's needed to make a checkbox here, we will
0277             QString const checked = d->m_checkBoxMap[section->connectTo()] ? "checked" : "";
0278             /* Important comment:
0279              * We inject Marble object into JavaScript of each legend html file
0280              * This is only one way to handle checkbox changes we see, so
0281              * Marble.setCheckedProperty is a function that does it
0282              */
0283             if(!section->radio().isEmpty()) {
0284                 checkBoxString = QLatin1String(
0285                         "<label class=\"section-head\">"
0286                         "<input style=\"position: relative; top: -4px;\" type=\"radio\" "
0287                         "onchange=\"Marble.setRadioCheckedProperty(this.value, this.name ,this.checked);\" ") +
0288                         checked + QLatin1String(" value=\"") + section->connectTo() + QLatin1String("\" name=\"") + section->radio() + QLatin1String("\" /><span>")
0289                         + heading +
0290                         QLatin1String("</span></label>");
0291 
0292             } else {
0293                 checkBoxString = QLatin1String(
0294                         "<label class=\"section-head\">"
0295                         "<input style=\"position: relative; top: -4px;\" type=\"checkbox\" "
0296                         "onchange=\"Marble.setCheckedProperty(this.name, this.checked);\" ") + checked + QLatin1String(" name=\"") + section->connectTo() + QLatin1String("\" /><span>")
0297                         + heading +
0298                         QLatin1String("</span></label>");
0299 
0300             }
0301             customLegendString += checkBoxString;
0302 
0303         } else {
0304             customLegendString += QLatin1String("<h4 class=\"section-head\">") + heading + QLatin1String("</h4>");
0305         }
0306 
0307         for (const GeoSceneItem *item: section->items()) {
0308 
0309             // checkbox for item
0310             QString checkBoxString;
0311             if (item->checkable()) {
0312                 QString const checked = d->m_checkBoxMap[item->connectTo()] ? "checked" : "";
0313                 checkBoxString = QLatin1String(
0314                         "<input type=\"checkbox\" "
0315                         "onchange=\"Marble.setCheckedProperty(this.name, this.checked);\" ")
0316                         + checked + QLatin1String(" name=\"") + item->connectTo() + QLatin1String("\" />");
0317 
0318             }
0319 
0320             // pixmap and text
0321             QString src;
0322             QString styleDiv;
0323             int pixmapWidth = 24;
0324             int pixmapHeight = 12;
0325             if (!item->icon()->pixmap().isEmpty()) {
0326                 QString path = MarbleDirs::path( item->icon()->pixmap() );
0327                 const QPixmap oncePixmap(path);
0328                 pixmapWidth = oncePixmap.width();
0329                 pixmapHeight = oncePixmap.height();
0330                 src = QUrl::fromLocalFile( path ).toString();
0331                 styleDiv = QLatin1String("width: ") + QString::number(pixmapWidth) + QLatin1String("px; height: ") +
0332                         QString::number(pixmapHeight) + QLatin1String("px;");
0333             } else {
0334               // Workaround for rendered border around empty images in webkit
0335               src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
0336             }
0337             // NOTICE. There are some pixmaps without image, so we should
0338             //         create just a plain rectangle with set color
0339             if (QColor(item->icon()->color()).isValid()) {
0340                 const QColor color = item->icon()->color();
0341                 styleDiv = QLatin1String("width: ") + QString::number(pixmapWidth) + QLatin1String("px; height: ") +
0342                         QString::number(pixmapHeight) + QLatin1String("px; background-color: ") + color.name() + QLatin1Char(';');
0343             }
0344             styleDiv += " position: relative; top: -3px;";
0345             const QString text = QCoreApplication::translate("DGML", item->text().toUtf8().constData());
0346             QString html = QLatin1String(
0347                     "<div class=\"legend-entry\">"
0348                     "  <label>") + checkBoxString + QLatin1String(
0349                     "    <img class=\"image-pic\" src=\"") + src + QLatin1String("\" style=\"") + styleDiv + QLatin1String("\"/>"
0350                     "    <span class=\"kotation\" >") + text + QLatin1String("</span>"
0351                     "  </label>"
0352                     "</div>");
0353             customLegendString += html;
0354         }
0355         customLegendString += QLatin1String("</div>"); // <div class="well">
0356     }
0357 
0358     return customLegendString;
0359 }
0360 
0361 void MarbleLegendBrowser::setCheckedProperty( const QString& name, bool checked )
0362 {
0363     if (checked != d->m_checkBoxMap[name]) {
0364         d->m_checkBoxMap[name] = checked;
0365         emit toggledShowProperty( name, checked );
0366     }
0367 }
0368 
0369 void MarbleLegendBrowser::setRadioCheckedProperty( const QString& value, const QString& name , bool checked )
0370 {
0371   Q_UNUSED(value)
0372   if (checked != d->m_checkBoxMap[name]) {
0373       d->m_checkBoxMap[name] = checked;
0374       emit toggledShowProperty( name, checked );
0375   }
0376 }
0377 
0378 }
0379 
0380 #include "moc_MarbleLegendBrowser.cpp"