File indexing completed on 2025-03-09 03:57:14

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2009-12-01
0007  * Description : Google-Maps-backend for geolocation interface
0008  *
0009  * SPDX-FileCopyrightText: 2010-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0010  * SPDX-FileCopyrightText: 2009-2011 by Michael G. Hansen <mike at mghansen dot de>
0011  * SPDX-FileCopyrightText: 2014      by Justus Schwartz <justus at gmx dot li>
0012  *
0013  * SPDX-License-Identifier: GPL-2.0-or-later
0014  *
0015  * ============================================================ */
0016 
0017 #include "backendgooglemaps.h"
0018 
0019 // Qt includes
0020 
0021 #include <QMenu>
0022 #include <QTimer>
0023 #include <QAction>
0024 #include <QBuffer>
0025 #include <QPointer>
0026 #include <QResizeEvent>
0027 #include <QActionGroup>
0028 #include <QApplication>
0029 #include <QStandardPaths>
0030 
0031 // KDE includes
0032 
0033 #include <kconfiggroup.h>
0034 #include <klocalizedstring.h>
0035 
0036 // Marble includes
0037 
0038 #ifdef HAVE_GEOLOCATION
0039 
0040 #   include "GeoDataLatLonAltBox.h"
0041 
0042 #endif
0043 
0044 // Local includes
0045 
0046 #include "digikam_config.h"
0047 #include "digikam_debug.h"
0048 #include "mapwidget.h"
0049 #include "abstractmarkertiler.h"
0050 #include "geomodelhelper.h"
0051 
0052 #ifdef HAVE_QWEBENGINE
0053 
0054 #   include "htmlwidget_qwebengine.h"
0055 
0056 #else
0057 
0058 #   include "htmlwidget_qwebkit.h"
0059 
0060 #endif
0061 
0062 namespace Digikam
0063 {
0064 
0065 class Q_DECL_HIDDEN GMInternalWidgetInfo
0066 {
0067 public:
0068 
0069     GMInternalWidgetInfo()
0070       : htmlWidget(nullptr)
0071     {
0072     }
0073 
0074     HTMLWidget* htmlWidget;
0075 };
0076 
0077 } // namespace Digikam
0078 
0079 Q_DECLARE_METATYPE(Digikam::GMInternalWidgetInfo)
0080 
0081 namespace Digikam
0082 {
0083 
0084 class Q_DECL_HIDDEN BackendGoogleMaps::Private
0085 {
0086 public:
0087 
0088     explicit Private()
0089       : htmlWidget                  (nullptr),
0090         htmlWidgetWrapper           (nullptr),
0091         isReady                     (false),
0092         mapTypeActionGroup          (nullptr),
0093         floatItemsActionGroup       (nullptr),
0094         showMapTypeControlAction    (nullptr),
0095         showNavigationControlAction (nullptr),
0096         showScaleControlAction      (nullptr),
0097         htmlFileName                (QLatin1String("backend-googlemaps.html")),
0098         cacheMapType                (QLatin1String("ROADMAP")),
0099         cacheShowMapTypeControl     (true),
0100         cacheShowNavigationControl  (true),
0101         cacheShowScaleControl       (true),
0102         cacheZoom                   (8),
0103         cacheMaxZoom                (0),
0104         cacheMinZoom                (0),
0105         cacheCenter                 (52.0, 6.0),
0106         cacheBounds                 (),
0107         activeState                 (false),
0108         widgetIsDocked              (false),
0109         trackChangeTracker          ()
0110     {
0111     }
0112 
0113     QPointer<HTMLWidget>                      htmlWidget;
0114     QPointer<QWidget>                         htmlWidgetWrapper;
0115     bool                                      isReady;
0116     QActionGroup*                             mapTypeActionGroup;
0117     QActionGroup*                             floatItemsActionGroup;
0118     QAction*                                  showMapTypeControlAction;
0119     QAction*                                  showNavigationControlAction;
0120     QAction*                                  showScaleControlAction;
0121     QString                                   htmlFileName;
0122     QString                                   cacheMapType;
0123     bool                                      cacheShowMapTypeControl;
0124     bool                                      cacheShowNavigationControl;
0125     bool                                      cacheShowScaleControl;
0126     int                                       cacheZoom;
0127     int                                       cacheMaxZoom;
0128     int                                       cacheMinZoom;
0129     GeoCoordinates                            cacheCenter;
0130     QPair<GeoCoordinates, GeoCoordinates>     cacheBounds;
0131     bool                                      activeState;
0132     bool                                      widgetIsDocked;
0133     QList<TrackManager::TrackChanges>         trackChangeTracker;
0134 };
0135 
0136 BackendGoogleMaps::BackendGoogleMaps(const QExplicitlySharedDataPointer<GeoIfaceSharedData>& sharedData,
0137                                      QObject* const parent)
0138     : MapBackend(sharedData, parent),
0139       d         (new Private())
0140 {
0141     createActions();
0142 }
0143 
0144 BackendGoogleMaps::~BackendGoogleMaps()
0145 {
0146     /// @todo Should we leave our widget in this list and not destroy it?
0147     ///       Maybe for now this should simply be limited to leaving one
0148     ///       unused widget in the global cache.
0149 
0150     GeoIfaceGlobalObject* const go = GeoIfaceGlobalObject::instance();
0151     go->removeMyInternalWidgetFromPool(this);
0152 
0153     if (d->htmlWidgetWrapper)
0154     {
0155         delete d->htmlWidgetWrapper;
0156     }
0157 
0158     delete d;
0159 }
0160 
0161 void BackendGoogleMaps::createActions()
0162 {
0163     // actions for selecting the map type:
0164 
0165     d->mapTypeActionGroup = new QActionGroup(this);
0166     d->mapTypeActionGroup->setExclusive(true);
0167 
0168     connect(d->mapTypeActionGroup, SIGNAL(triggered(QAction*)),
0169             this, SLOT(slotMapTypeActionTriggered(QAction*)));
0170 
0171     QStringList mapTypes, mapTypesHumanNames;
0172     mapTypes
0173         << QLatin1String("ROADMAP")
0174         << QLatin1String("SATELLITE")
0175         << QLatin1String("HYBRID")
0176         << QLatin1String("TERRAIN");
0177 
0178     mapTypesHumanNames
0179         << i18n("Roadmap")
0180         << i18n("Satellite")
0181         << i18n("Hybrid")
0182         << i18n("Terrain");
0183 
0184     for (int i = 0 ; i < mapTypes.count() ; ++i)
0185     {
0186         QAction* const mapTypeAction = new QAction(d->mapTypeActionGroup);
0187         mapTypeAction->setData(mapTypes.at(i));
0188         mapTypeAction->setText(mapTypesHumanNames.at(i));
0189         mapTypeAction->setCheckable(true);
0190     }
0191 
0192     // float items:
0193 
0194     d->floatItemsActionGroup = new QActionGroup(this);
0195     d->floatItemsActionGroup->setExclusive(false);
0196 
0197     connect(d->floatItemsActionGroup, SIGNAL(triggered(QAction*)),
0198             this, SLOT(slotFloatSettingsTriggered(QAction*)));
0199 
0200     d->showMapTypeControlAction = new QAction(i18n("Show Map Type Control"), d->floatItemsActionGroup);
0201     d->showMapTypeControlAction->setCheckable(true);
0202     d->showMapTypeControlAction->setChecked(d->cacheShowMapTypeControl);
0203     d->showMapTypeControlAction->setData(QLatin1String("showmaptypecontrol"));
0204 
0205     d->showNavigationControlAction = new QAction(i18n("Show Navigation Control"), d->floatItemsActionGroup);
0206     d->showNavigationControlAction->setCheckable(true);
0207     d->showNavigationControlAction->setChecked(d->cacheShowNavigationControl);
0208     d->showNavigationControlAction->setData(QLatin1String("shownavigationcontrol"));
0209 
0210     d->showScaleControlAction = new QAction(i18n("Show Scale Control"), d->floatItemsActionGroup);
0211     d->showScaleControlAction->setCheckable(true);
0212     d->showScaleControlAction->setChecked(d->cacheShowScaleControl);
0213     d->showScaleControlAction->setData(QLatin1String("showscalecontrol"));
0214 }
0215 
0216 QString BackendGoogleMaps::backendName() const
0217 {
0218     return QLatin1String("googlemaps");
0219 }
0220 
0221 QString BackendGoogleMaps::backendHumanName() const
0222 {
0223     return i18n("Google Maps");
0224 }
0225 
0226 QWidget* BackendGoogleMaps::mapWidget()
0227 {
0228     if (!d->htmlWidgetWrapper)
0229     {
0230         GeoIfaceGlobalObject* const go = GeoIfaceGlobalObject::instance();
0231 
0232         GeoIfaceInternalWidgetInfo info;
0233         bool foundReusableWidget       = go->getInternalWidgetFromPool(this, &info);
0234 
0235         if (foundReusableWidget)
0236         {
0237             d->htmlWidgetWrapper               = info.widget;
0238             const GMInternalWidgetInfo intInfo = info.backendData.value<GMInternalWidgetInfo>();
0239             d->htmlWidget                      = intInfo.htmlWidget;
0240         }
0241         else
0242         {
0243             // the widget has not been created yet, create it now:
0244 
0245             d->htmlWidgetWrapper = new QWidget();
0246             d->htmlWidgetWrapper->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
0247             d->htmlWidget        = new HTMLWidget(d->htmlWidgetWrapper);
0248             d->htmlWidgetWrapper->resize(400, 400);
0249         }
0250 
0251         connect(d->htmlWidget, SIGNAL(signalJavaScriptReady()),
0252                 this, SLOT(slotHTMLInitialized()));
0253 
0254         connect(d->htmlWidget, SIGNAL(signalHTMLEvents(QStringList)),
0255                 this, SLOT(slotHTMLEvents(QStringList)));
0256 
0257         connect(d->htmlWidget, SIGNAL(signalMessageEvent(QString)),
0258                 this, SLOT(slotMessageEvent(QString)));
0259 
0260         connect(d->htmlWidget, SIGNAL(selectionHasBeenMade(Digikam::GeoCoordinates::Pair)),
0261                 this, SLOT(slotSelectionHasBeenMade(Digikam::GeoCoordinates::Pair)));
0262 
0263         d->htmlWidget->setSharedGeoIfaceObject(s.data());
0264         d->htmlWidgetWrapper->installEventFilter(this);
0265 
0266         if (foundReusableWidget)
0267         {
0268             slotHTMLInitialized();
0269         }
0270         else
0271         {
0272             reload();
0273         }
0274     }
0275 
0276     return d->htmlWidgetWrapper.data();
0277 }
0278 
0279 void BackendGoogleMaps::reload()
0280 {
0281     if (d->htmlWidget)
0282     {
0283         const QUrl htmlUrl = GeoIfaceGlobalObject::instance()->locateDataFile(d->htmlFileName);
0284         d->htmlWidget->load(htmlUrl);
0285     }
0286 }
0287 
0288 GeoCoordinates BackendGoogleMaps::getCenter() const
0289 {
0290     return d->cacheCenter;
0291 }
0292 
0293 void BackendGoogleMaps::setCenter(const GeoCoordinates& coordinate)
0294 {
0295     d->cacheCenter = coordinate;
0296 
0297     if (isReady())
0298     {
0299         QTimer::singleShot(0, this, SLOT(slotSetCenterTimer()));
0300     }
0301 }
0302 
0303 void BackendGoogleMaps::slotSetCenterTimer()
0304 {
0305     d->htmlWidget->runScript(QString::fromLatin1("kgeomapSetCenter(%1, %2);")
0306                                  .arg(d->cacheCenter.latString())
0307                                  .arg(d->cacheCenter.lonString()));
0308 }
0309 
0310 bool BackendGoogleMaps::isReady() const
0311 {
0312     return d->isReady;
0313 }
0314 
0315 void BackendGoogleMaps::slotHTMLInitialized()
0316 {
0317     d->isReady     = true;
0318     d->htmlWidget->runScript(QString::fromLatin1("kgeomapWidgetResized(%1, %2)")
0319                                  .arg(d->htmlWidgetWrapper->width())
0320                                  .arg(d->htmlWidgetWrapper->height()));
0321 
0322     // TODO: call javascript directly here and update action availability in one shot
0323 
0324     setMapType(d->cacheMapType);
0325     setShowScaleControl(d->cacheShowScaleControl);
0326     setShowMapTypeControl(d->cacheShowMapTypeControl);
0327     setShowNavigationControl(d->cacheShowNavigationControl);
0328 
0329     Q_EMIT signalBackendReadyChanged(backendName());
0330 }
0331 
0332 void BackendGoogleMaps::zoomIn()
0333 {
0334     if (!d->isReady)
0335     {
0336         return;
0337     }
0338 
0339     d->htmlWidget->runScript(QLatin1String("kgeomapZoomIn();"));
0340 }
0341 
0342 void BackendGoogleMaps::zoomOut()
0343 {
0344     if (!d->isReady)
0345     {
0346         return;
0347     }
0348 
0349     d->htmlWidget->runScript(QLatin1String("kgeomapZoomOut();"));
0350 }
0351 
0352 QString BackendGoogleMaps::getMapType() const
0353 {
0354     return d->cacheMapType;
0355 }
0356 
0357 void BackendGoogleMaps::setMapType(const QString& newMapType)
0358 {
0359     d->cacheMapType = newMapType;
0360     qCDebug(DIGIKAM_GEOIFACE_LOG) << newMapType;
0361 
0362     if (isReady())
0363     {
0364         d->htmlWidget->runScript(QString::fromLatin1("kgeomapSetMapType(\"%1\");").arg(newMapType));
0365         updateZoomMinMaxCache();
0366         updateActionAvailability();
0367     }
0368 }
0369 
0370 void BackendGoogleMaps::slotMapTypeActionTriggered(QAction* action)
0371 {
0372     const QString newMapType = action->data().toString();
0373     setMapType(newMapType);
0374 }
0375 
0376 void BackendGoogleMaps::addActionsToConfigurationMenu(QMenu* const configurationMenu)
0377 {
0378     GEOIFACE_ASSERT(configurationMenu!=nullptr);
0379 
0380     if (!d->isReady)
0381     {
0382         return;
0383     }
0384 
0385     configurationMenu->addSeparator();
0386 
0387     // map type actions:
0388 
0389     const QList<QAction*> mapTypeActions = d->mapTypeActionGroup->actions();
0390 
0391     for (int i = 0 ; i < mapTypeActions.count() ; ++i)
0392     {
0393         QAction* const mapTypeAction = mapTypeActions.at(i);
0394         configurationMenu->addAction(mapTypeAction);
0395     }
0396 
0397     configurationMenu->addSeparator();
0398 
0399     // float items visibility:
0400 
0401     QMenu* const floatItemsSubMenu = new QMenu(i18n("Float items"), configurationMenu);
0402     configurationMenu->addMenu(floatItemsSubMenu);
0403 
0404     floatItemsSubMenu->addAction(d->showMapTypeControlAction);
0405     floatItemsSubMenu->addAction(d->showNavigationControlAction);
0406     floatItemsSubMenu->addAction(d->showScaleControlAction);
0407 
0408     configurationMenu->addSeparator();
0409 
0410     addCommonOptions(configurationMenu);
0411 
0412     updateActionAvailability();
0413 }
0414 
0415 void BackendGoogleMaps::saveSettingsToGroup(KConfigGroup* const group)
0416 {
0417     GEOIFACE_ASSERT(group != nullptr);
0418 
0419     if (!group)
0420     {
0421         return;
0422     }
0423 
0424     group->writeEntry("GoogleMaps Map Type",                getMapType());
0425     group->writeEntry("GoogleMaps Show Scale Control",      d->cacheShowScaleControl);
0426     group->writeEntry("GoogleMaps Show Map Type Control",   d->cacheShowMapTypeControl);
0427     group->writeEntry("GoogleMaps Show Navigation Control", d->cacheShowNavigationControl);
0428 }
0429 
0430 void BackendGoogleMaps::readSettingsFromGroup(const KConfigGroup* const group)
0431 {
0432     GEOIFACE_ASSERT(group != nullptr);
0433 
0434     if (!group)
0435     {
0436         return;
0437     }
0438 
0439     setMapType(group->readEntry("GoogleMaps Map Type",                              "ROADMAP"));
0440     setShowScaleControl(group->readEntry("GoogleMaps Show Scale Control",           true));
0441     setShowMapTypeControl(group->readEntry("GoogleMaps Show Map Type Control",      true));
0442     setShowNavigationControl(group->readEntry("GoogleMaps Show Navigation Control", true));
0443 }
0444 
0445 void BackendGoogleMaps::slotUngroupedModelChanged(const int mindex)
0446 {
0447     GEOIFACE_ASSERT(isReady());
0448 
0449     if (!isReady())
0450     {
0451         return;
0452     }
0453 
0454     d->htmlWidget->runScript(QString::fromLatin1("kgeomapClearMarkers(%1);").arg(mindex));
0455 
0456     // this can happen when a model was removed and we are simply asked to remove its markers
0457 
0458     if (mindex > s->ungroupedModels.count())
0459     {
0460         return;
0461     }
0462 
0463     GeoModelHelper* const modelHelper = s->ungroupedModels.at(mindex);
0464 
0465     if (!modelHelper)
0466     {
0467         return;
0468     }
0469 
0470     if (!modelHelper->modelFlags().testFlag(GeoModelHelper::FlagVisible))
0471     {
0472         return;
0473     }
0474 
0475     QAbstractItemModel* const model = modelHelper->model();
0476 
0477     for (int row = 0 ; row < model->rowCount() ; ++row)
0478     {
0479         const QModelIndex currentIndex                = model->index(row, 0);
0480         const GeoModelHelper::PropertyFlags itemFlags = modelHelper->itemFlags(currentIndex);
0481 
0482         // TODO: this is untested! We need to make sure the indices stay correct inside the JavaScript part!
0483 
0484         if (!itemFlags.testFlag(GeoModelHelper::FlagVisible))
0485         {
0486             continue;
0487         }
0488 
0489         GeoCoordinates currentCoordinates;
0490 
0491         if (!modelHelper->itemCoordinates(currentIndex, &currentCoordinates))
0492         {
0493             continue;
0494         }
0495 
0496         // TODO: use the pixmap supplied by the modelHelper
0497 
0498         d->htmlWidget->runScript(QString::fromLatin1("kgeomapAddMarker(%1, %2, %3, %4, %5, %6);")
0499                                      .arg(mindex)
0500                                      .arg(row)
0501                                      .arg(currentCoordinates.latString())
0502                                      .arg(currentCoordinates.lonString())
0503                                      .arg(itemFlags.testFlag(GeoModelHelper::FlagMovable) ? QLatin1String("true")
0504                                                                                           : QLatin1String("false"))
0505                                      .arg(itemFlags.testFlag(GeoModelHelper::FlagSnaps) ? QLatin1String("true")
0506                                                                                         : QLatin1String("false"))
0507                                 );
0508 
0509         QPoint     markerCenterPoint;
0510         QSize      markerSize;
0511         QPixmap    markerPixmap;
0512         QUrl       markerUrl;
0513         const bool markerHasIcon = modelHelper->itemIcon(currentIndex, &markerCenterPoint,
0514                                                          &markerSize, &markerPixmap, &markerUrl);
0515 
0516         if (markerHasIcon)
0517         {
0518             if (!markerUrl.isEmpty())
0519             {
0520                 setMarkerPixmap(mindex, row, markerCenterPoint, markerSize, markerUrl);
0521             }
0522             else
0523             {
0524                 setMarkerPixmap(mindex, row, markerCenterPoint, markerPixmap);
0525             }
0526         }
0527     }
0528 }
0529 void BackendGoogleMaps::updateMarkers()
0530 {
0531     // re-transfer all markers to the javascript-part:
0532 
0533     for (int i = 0 ; i < s->ungroupedModels.count() ; ++i)
0534     {
0535         slotUngroupedModelChanged(i);
0536     }
0537 }
0538 
0539 void BackendGoogleMaps::slotHTMLEvents(const QStringList& events)
0540 {
0541     // for some events, we just note that they appeared and then process them later on:
0542 
0543     bool centerProbablyChanged    = false;
0544     bool mapTypeChanged           = false;
0545     bool zoomProbablyChanged      = false;
0546     bool mapBoundsProbablyChanged = false;
0547     QIntList movedClusters;
0548     QList<QPersistentModelIndex> movedMarkers;
0549     QIntList clickedClusters;
0550 
0551     // TODO: verify that the order of the events is still okay
0552     //       or that the order does not matter
0553 
0554     for (QStringList::const_iterator it = events.constBegin() ; it != events.constEnd() ; ++it)
0555     {
0556         const QString eventCode           = it->left(2);
0557         const QString eventParameter      = it->mid(2);
0558         const QStringList eventParameters = eventParameter.split(QLatin1Char('/'));
0559 
0560         if      (eventCode == QLatin1String("MT"))
0561         {
0562             // map type changed
0563 
0564             mapTypeChanged  = true;
0565             d->cacheMapType = eventParameter;
0566         }
0567         else if (eventCode == QLatin1String("MB"))
0568         {
0569             // NOTE: event currently disabled in javascript part
0570             // map bounds changed
0571 
0572             centerProbablyChanged    = true;
0573             zoomProbablyChanged      = true;
0574             mapBoundsProbablyChanged = true;
0575         }
0576         else if (eventCode == QLatin1String("ZC"))
0577         {
0578             // NOTE: event currently disabled in javascript part
0579             // zoom changed
0580 
0581             zoomProbablyChanged      = true;
0582             mapBoundsProbablyChanged = true;
0583         }
0584         else if (eventCode == QLatin1String("id"))
0585         {
0586             // idle after drastic map changes
0587 
0588             centerProbablyChanged    = true;
0589             zoomProbablyChanged      = true;
0590             mapBoundsProbablyChanged = true;
0591         }
0592         else if (eventCode == QLatin1String("cm"))
0593         {
0594             /// @todo buffer this event type!
0595             // cluster moved
0596 
0597             bool okay              = false;
0598             const int clusterIndex = eventParameter.toInt(&okay);
0599             GEOIFACE_ASSERT(okay);
0600 
0601             if (!okay)
0602             {
0603                 continue;
0604             }
0605 
0606             GEOIFACE_ASSERT(clusterIndex >= 0);
0607             GEOIFACE_ASSERT(clusterIndex < s->clusterList.size());
0608 
0609             if ((clusterIndex < 0) || (clusterIndex > s->clusterList.size()))
0610             {
0611                 continue;
0612             }
0613 
0614             // re-read the marker position:
0615 
0616             GeoCoordinates clusterCoordinates;
0617             const bool isValid = d->htmlWidget->runScript2Coordinates(
0618                     QString::fromLatin1("kgeomapGetClusterPosition(%1);").arg(clusterIndex),
0619                     &clusterCoordinates);
0620 
0621             GEOIFACE_ASSERT(isValid);
0622 
0623             if (!isValid)
0624             {
0625                 continue;
0626             }
0627 
0628             /// @todo this discards the altitude!
0629             /// @todo is this really necessary? clusters should be regenerated anyway...
0630 
0631             s->clusterList[clusterIndex].coordinates = clusterCoordinates;
0632 
0633             movedClusters << clusterIndex;
0634         }
0635         else if (eventCode == QLatin1String("cs"))
0636         {
0637             /// @todo buffer this event type!
0638             // cluster snapped
0639 
0640             bool okay              = false;
0641             const int clusterIndex = eventParameters.first().toInt(&okay);
0642             GEOIFACE_ASSERT(okay);
0643 
0644             if (!okay)
0645             {
0646                 continue;
0647             }
0648 
0649             GEOIFACE_ASSERT(clusterIndex >= 0);
0650             GEOIFACE_ASSERT(clusterIndex < s->clusterList.size());
0651 
0652             if ((clusterIndex < 0) || (clusterIndex > s->clusterList.size()))
0653             {
0654                 continue;
0655             }
0656 
0657             // determine to which marker we snapped:
0658 
0659             okay                  = false;
0660             const int snapModelId = eventParameters.at(1).toInt(&okay);
0661             GEOIFACE_ASSERT(okay);
0662 
0663             if (!okay)
0664             {
0665                 continue;
0666             }
0667 
0668             okay                   = false;
0669             const int snapMarkerId = eventParameters.at(2).toInt(&okay);
0670             GEOIFACE_ASSERT(okay);
0671 
0672             if (!okay)
0673             {
0674                 continue;
0675             }
0676 
0677             /// @todo Q_EMIT signal here or later?
0678 
0679             GeoModelHelper* const modelHelper  = s->ungroupedModels.at(snapModelId);
0680             QAbstractItemModel* const model    = modelHelper->model();
0681             QPair<int, QModelIndex> snapTargetIndex(snapModelId, model->index(snapMarkerId, 0));
0682             Q_EMIT signalClustersMoved(QIntList() << clusterIndex, snapTargetIndex);
0683         }
0684         else if (eventCode == QLatin1String("cc"))
0685         {
0686             /// @todo buffer this event type!
0687             // cluster clicked
0688 
0689             bool okay              = false;
0690             const int clusterIndex = eventParameter.toInt(&okay);
0691             GEOIFACE_ASSERT(okay);
0692 
0693             if (!okay)
0694             {
0695                 continue;
0696             }
0697 
0698             GEOIFACE_ASSERT(clusterIndex >= 0);
0699             GEOIFACE_ASSERT(clusterIndex < s->clusterList.size());
0700 
0701             if ((clusterIndex < 0) || (clusterIndex > s->clusterList.size()))
0702             {
0703                 continue;
0704             }
0705 
0706             clickedClusters << clusterIndex;
0707         }
0708         else if (eventCode == QLatin1String("mm"))
0709         {
0710 /*
0711             // TODO: buffer this event type!
0712             // marker moved
0713 
0714             bool okay           = false;
0715             const int markerRow = eventParameter.toInt(&okay);
0716             GEOIFACE_ASSERT(okay);
0717 
0718             if (!okay)
0719             {
0720                 continue;
0721             }
0722 
0723             GEOIFACE_ASSERT(markerRow >= 0);
0724             GEOIFACE_ASSERT(markerRow<s->specialMarkersModel->rowCount());
0725 
0726             if ((markerRow<0)||(markerRow>=s->specialMarkersModel->rowCount()))
0727             {
0728                 continue;
0729             }
0730 
0731             // re-read the marker position:
0732 
0733             GeoCoordinates markerCoordinates;
0734             const bool isValid = d->htmlWidget->runScript2Coordinates(
0735                     QString::fromLatin1("kgeomapGetMarkerPosition(%1);").arg(markerRow),
0736                     &markerCoordinates
0737                 );
0738 
0739             GEOIFACE_ASSERT(isValid);
0740 
0741             if (!isValid)
0742             {
0743                 continue;
0744             }
0745 
0746             // TODO: this discards the altitude!
0747 
0748             const QModelIndex markerIndex = s->specialMarkersModel->index(markerRow, 0);
0749             s->specialMarkersModel->setData(markerIndex, QVariant::fromValue(markerCoordinates), s->specialMarkersCoordinatesRole);
0750 
0751             movedMarkers << QPersistentModelIndex(markerIndex);
0752 */
0753         }
0754         else if (eventCode == QLatin1String("do"))
0755         {
0756             // debug output:
0757 
0758             qCDebug(DIGIKAM_GEOIFACE_LOG) << QString::fromLatin1("javascript:%1").arg(eventParameter);
0759         }
0760     }
0761 
0762     if (!movedClusters.isEmpty())
0763     {
0764         qCDebug(DIGIKAM_GEOIFACE_LOG) << movedClusters;
0765 
0766         Q_EMIT signalClustersMoved(movedClusters, QPair<int, QModelIndex>(-1, QModelIndex()));
0767     }
0768 
0769     // cppcheck-suppress knownConditionTrueFalse
0770     if (!movedMarkers.isEmpty())
0771     {
0772         qCDebug(DIGIKAM_GEOIFACE_LOG) << movedMarkers;
0773 /*
0774         Q_EMIT signalSpecialMarkersMoved(movedMarkers);
0775 */
0776     }
0777 
0778     if (!clickedClusters.isEmpty())
0779     {
0780         Q_EMIT signalClustersClicked(clickedClusters);
0781     }
0782 
0783     // now process the buffered events:
0784 
0785     if (mapTypeChanged)
0786     {
0787         updateZoomMinMaxCache();
0788     }
0789 
0790     if (zoomProbablyChanged && !mapTypeChanged)
0791     {
0792         d->cacheZoom = d->htmlWidget->runScript(QLatin1String("kgeomapGetZoom();"), false).toInt();
0793 
0794         Q_EMIT signalZoomChanged(QString::fromLatin1("googlemaps:%1").arg(d->cacheZoom));
0795     }
0796 
0797     if (centerProbablyChanged && !mapTypeChanged)
0798     {
0799         // there is nothing we can do if the coordinates are invalid
0800 /*
0801         const bool isValid =
0802 */
0803         d->htmlWidget->runScript2Coordinates(QLatin1String("kgeomapGetCenter();"), &(d->cacheCenter));
0804     }
0805 
0806     // update the actions if necessary:
0807 
0808     if (zoomProbablyChanged || mapTypeChanged || centerProbablyChanged)
0809     {
0810         updateActionAvailability();
0811     }
0812 
0813     if (mapBoundsProbablyChanged)
0814     {
0815         const QString mapBoundsString = d->htmlWidget->runScript(QLatin1String("kgeomapGetBounds();"), false).toString();
0816         const bool isValid            = GeoIfaceHelperParseBoundsString(mapBoundsString, &d->cacheBounds);
0817 
0818         if (!isValid)
0819         {
0820             qCDebug(DIGIKAM_GEOIFACE_LOG) << "Invalid map bounds";
0821         }
0822     }
0823 
0824     if (mapBoundsProbablyChanged || !movedClusters.isEmpty())
0825     {
0826         s->worldMapWidget->markClustersAsDirty();
0827         s->worldMapWidget->updateClusters();
0828     }
0829 }
0830 
0831 void BackendGoogleMaps::updateClusters()
0832 {
0833     qCDebug(DIGIKAM_GEOIFACE_LOG) << "start updateclusters";
0834 
0835     // re-transfer the clusters to the map:
0836 
0837     GEOIFACE_ASSERT(isReady());
0838 
0839     if (!isReady())
0840     {
0841         return;
0842     }
0843 
0844     // TODO: only update clusters that have actually changed!
0845 
0846     // re-transfer all markers to the javascript-part:
0847 
0848     const bool canMoveItems = !s->showThumbnails      &&
0849                               s->modificationsAllowed &&
0850                               s->markerModel->tilerFlags().testFlag(AbstractMarkerTiler::FlagMovable);
0851 
0852     d->htmlWidget->runScript(QLatin1String("kgeomapClearClusters();"));
0853     d->htmlWidget->runScript(QString::fromLatin1("kgeomapSetIsInEditMode(%1);")
0854                                  .arg(s->showThumbnails ? QLatin1String("false")
0855                                                         : QLatin1String("true"))
0856                             );
0857 
0858     for (int currentIndex = 0 ; currentIndex < s->clusterList.size() ; ++currentIndex)
0859     {
0860         const GeoIfaceCluster& currentCluster = s->clusterList.at(currentIndex);
0861 
0862         d->htmlWidget->runScript(QString::fromLatin1("kgeomapAddCluster(%1, %2, %3, %4, %5, %6);")
0863                                      .arg(currentIndex)
0864                                      .arg(currentCluster.coordinates.latString())
0865                                      .arg(currentCluster.coordinates.lonString())
0866                                      .arg(canMoveItems ? QLatin1String("true")
0867                                                        : QLatin1String("false"))
0868                                      .arg(currentCluster.markerCount)
0869                                      .arg(currentCluster.markerSelectedCount)
0870                                 );
0871 
0872         // TODO: for now, only set generated pixmaps when not in edit mode
0873         // this can be changed once we figure out how to appropriately handle
0874         // the selection state changes when a marker is dragged
0875 
0876         if (s->showThumbnails)
0877         {
0878             QPoint clusterCenterPoint;
0879 
0880             // TODO: who calculates the override values?
0881 
0882             const QPixmap clusterPixmap = s->worldMapWidget->getDecoratedPixmapForCluster(currentIndex, nullptr, nullptr, &clusterCenterPoint);
0883 
0884             setClusterPixmap(currentIndex, clusterCenterPoint, clusterPixmap);
0885         }
0886     }
0887 
0888     qCDebug(DIGIKAM_GEOIFACE_LOG) << "end updateclusters";
0889 }
0890 
0891 bool BackendGoogleMaps::screenCoordinates(const GeoCoordinates& coordinates, QPoint* const point)
0892 {
0893     if (!d->isReady)
0894     {
0895         return false;
0896     }
0897 
0898     const QString pointStringResult = d->htmlWidget->runScript(
0899                 QString::fromLatin1("kgeomapLatLngToPixel(%1, %2);")
0900                     .arg(coordinates.latString())
0901                     .arg(coordinates.lonString()),
0902                 false).toString();
0903 
0904     const bool isValid              = GeoIfaceHelperParseXYStringToPoint(pointStringResult, point);
0905 
0906     // TODO: apparently, even points outside the visible area are returned as valid
0907     // check whether they are actually visible
0908 
0909     return isValid;
0910 }
0911 
0912 bool BackendGoogleMaps::geoCoordinates(const QPoint& point, GeoCoordinates* const coordinates) const
0913 {
0914     if (!d->isReady)
0915     {
0916         return false;
0917     }
0918 
0919     const bool isValid = d->htmlWidget->runScript2Coordinates(
0920         QString::fromLatin1("kgeomapPixelToLatLng(%1, %2);")
0921             .arg(point.x())
0922             .arg(point.y()),
0923             coordinates);
0924 
0925     return isValid;
0926 }
0927 
0928 QSize BackendGoogleMaps::mapSize() const
0929 {
0930     GEOIFACE_ASSERT(d->htmlWidgetWrapper != nullptr);
0931 
0932     return d->htmlWidgetWrapper->size();
0933 }
0934 
0935 void BackendGoogleMaps::slotFloatSettingsTriggered(QAction* action)
0936 {
0937     const QString actionIdString = action->data().toString();
0938     const bool actionState       = action->isChecked();
0939 
0940     if      (actionIdString == QLatin1String("showmaptypecontrol"))
0941     {
0942         setShowMapTypeControl(actionState);
0943     }
0944     else if (actionIdString == QLatin1String("shownavigationcontrol"))
0945     {
0946         setShowNavigationControl(actionState);
0947     }
0948     else if (actionIdString == QLatin1String("showscalecontrol"))
0949     {
0950         setShowScaleControl(actionState);
0951     }
0952 }
0953 
0954 void BackendGoogleMaps::setShowScaleControl(const bool state)
0955 {
0956     d->cacheShowScaleControl = state;
0957 
0958     if (d->showScaleControlAction)
0959     {
0960         d->showScaleControlAction->setChecked(state);
0961     }
0962 
0963     if (!isReady())
0964     {
0965         return;
0966     }
0967 
0968     d->htmlWidget->runScript(QString::fromLatin1("kgeomapSetShowScaleControl(%1);")
0969                                  .arg(state ? QLatin1String("true")
0970                                             : QLatin1String("false"))
0971                             );
0972 }
0973 
0974 void BackendGoogleMaps::setShowNavigationControl(const bool state)
0975 {
0976     d->cacheShowNavigationControl = state;
0977 
0978     if (d->showNavigationControlAction)
0979     {
0980         d->showNavigationControlAction->setChecked(state);
0981     }
0982 
0983     if (!isReady())
0984     {
0985         return;
0986     }
0987 
0988     d->htmlWidget->runScript(QString::fromLatin1("kgeomapSetShowNavigationControl(%1);")
0989                                  .arg(state ? QLatin1String("true")
0990                                             : QLatin1String("false"))
0991                             );
0992 }
0993 
0994 void BackendGoogleMaps::setShowMapTypeControl(const bool state)
0995 {
0996     d->cacheShowMapTypeControl = state;
0997 
0998     if (d->showMapTypeControlAction)
0999     {
1000         d->showMapTypeControlAction->setChecked(state);
1001     }
1002 
1003     if (!isReady())
1004     {
1005         return;
1006     }
1007 
1008     d->htmlWidget->runScript(QString::fromLatin1("kgeomapSetShowMapTypeControl(%1);")
1009                                  .arg(state ? QLatin1String("true")
1010                                             : QLatin1String("false"))
1011                             );
1012 }
1013 
1014 void BackendGoogleMaps::slotClustersNeedUpdating()
1015 {
1016     s->worldMapWidget->updateClusters();
1017 }
1018 
1019 void BackendGoogleMaps::setZoom(const QString& newZoom)
1020 {
1021     const QString myZoomString = s->worldMapWidget->convertZoomToBackendZoom(newZoom, QLatin1String("googlemaps"));
1022     GEOIFACE_ASSERT(myZoomString.startsWith(QLatin1String("googlemaps:")));
1023 
1024     const int myZoom           = myZoomString.mid(QString::fromLatin1("googlemaps:").length()).toInt();
1025     d->cacheZoom               = myZoom;
1026 
1027     if (isReady())
1028     {
1029         d->htmlWidget->runScript(QString::fromLatin1("kgeomapSetZoom(%1);").arg(d->cacheZoom));
1030     }
1031 }
1032 
1033 QString BackendGoogleMaps::getZoom() const
1034 {
1035     return QString::fromLatin1("googlemaps:%1").arg(d->cacheZoom);
1036 }
1037 
1038 int BackendGoogleMaps::getMarkerModelLevel()
1039 {
1040     GEOIFACE_ASSERT(isReady());
1041 
1042     if (!isReady())
1043     {
1044         return 0;
1045     }
1046 
1047     // get the current zoom level:
1048 
1049     const int currentZoom = d->cacheZoom;
1050 
1051     int tileLevel = 0;
1052 
1053     if      (currentZoom ==  0) { tileLevel = 1; }
1054     else if (currentZoom ==  1) { tileLevel = 1; }
1055     else if (currentZoom ==  2) { tileLevel = 1; }
1056     else if (currentZoom ==  3) { tileLevel = 2; }
1057     else if (currentZoom ==  4) { tileLevel = 2; }
1058     else if (currentZoom ==  5) { tileLevel = 3; }
1059     else if (currentZoom ==  6) { tileLevel = 3; }
1060     else if (currentZoom ==  7) { tileLevel = 3; }
1061     else if (currentZoom ==  8) { tileLevel = 4; }
1062     else if (currentZoom ==  9) { tileLevel = 4; }
1063     else if (currentZoom == 10) { tileLevel = 4; }
1064     else if (currentZoom == 11) { tileLevel = 4; }
1065     else if (currentZoom == 12) { tileLevel = 4; }
1066     else if (currentZoom == 13) { tileLevel = 4; }
1067     else if (currentZoom == 14) { tileLevel = 5; }
1068     else if (currentZoom == 15) { tileLevel = 5; }
1069     else if (currentZoom == 16) { tileLevel = 6; }
1070     else if (currentZoom == 17) { tileLevel = 7; }
1071     else if (currentZoom == 18) { tileLevel = 7; }
1072     else if (currentZoom == 19) { tileLevel = 8; }
1073     else if (currentZoom == 20) { tileLevel = 9; }
1074     else if (currentZoom == 21) { tileLevel = 9; }
1075     else if (currentZoom == 22) { tileLevel = 9; }
1076     else
1077     {
1078         tileLevel = TileIndex::MaxLevel - 1;
1079     }
1080 
1081     GEOIFACE_ASSERT(tileLevel <= TileIndex::MaxLevel-1);
1082 
1083     return tileLevel;
1084 }
1085 
1086 GeoCoordinates::PairList BackendGoogleMaps::getNormalizedBounds()
1087 {
1088     return GeoIfaceHelperNormalizeBounds(d->cacheBounds);
1089 }
1090 
1091 /*
1092 void BackendGoogleMaps::updateDragDropMarker(const QPoint& pos, const GeoIfaceDragData* const dragData)
1093 {
1094     if (!isReady())
1095     {
1096         return;
1097     }
1098 
1099     if (!dragData)
1100     {
1101         d->htmlWidget->runScript("kgeomapRemoveDragMarker();");
1102     }
1103     else
1104     {
1105         d->htmlWidget->runScript(QLatin1String("kgeomapSetDragMarker(%1, %2, %3, %4);")
1106                 .arg(pos.x())
1107                 .arg(pos.y())
1108                 .arg(dragData->itemCount)
1109                 .arg(dragData->itemCount)
1110             );
1111     }
1112 
1113     // TODO: hide dragged markers on the map
1114 }
1115 
1116 void BackendGoogleMaps::updateDragDropMarkerPosition(const QPoint& pos)
1117 {
1118     // TODO: buffer this!
1119 
1120     if (!isReady())
1121     {
1122         return;
1123     }
1124 
1125     d->htmlWidget->runScript(QLatin1String("kgeomapMoveDragMarker(%1, %2);")
1126             .arg(pos.x())
1127             .arg(pos.y())
1128         );
1129 }
1130 */
1131 
1132 void BackendGoogleMaps::updateActionAvailability()
1133 {
1134     if (!d->activeState || !isReady())
1135     {
1136         return;
1137     }
1138 
1139     const QString currentMapType         = getMapType();
1140     const QList<QAction*> mapTypeActions = d->mapTypeActionGroup->actions();
1141 
1142     for (int i = 0 ; i < mapTypeActions.size() ; ++i)
1143     {
1144         mapTypeActions.at(i)->setChecked(mapTypeActions.at(i)->data().toString()==currentMapType);
1145     }
1146 
1147     s->worldMapWidget->getControlAction(QLatin1String("zoomin"))->setEnabled(true/*d->cacheZoom<d->cacheMaxZoom*/);
1148     s->worldMapWidget->getControlAction(QLatin1String("zoomout"))->setEnabled(true/*d->cacheZoom>d->cacheMinZoom*/);
1149 }
1150 
1151 void BackendGoogleMaps::updateZoomMinMaxCache()
1152 {
1153     // TODO: these functions seem to cause problems, the map is not fully updated after a few calls
1154 /*
1155     d->cacheMaxZoom = d->htmlWidget->runScript("kgeomapGetMaxZoom();", false).toInt();
1156     d->cacheMinZoom = d->htmlWidget->runScript("kgeomapGetMinZoom();", false).toInt();
1157 */
1158 }
1159 
1160 void BackendGoogleMaps::slotThumbnailAvailableForIndex(const QVariant& index, const QPixmap& pixmap)
1161 {
1162     qCDebug(DIGIKAM_GEOIFACE_LOG) << index<<pixmap.size();
1163 
1164     if (pixmap.isNull() || !s->showThumbnails)
1165     {
1166         return;
1167     }
1168 
1169     // TODO: properly reject pixmaps with the wrong size
1170 
1171     const int expectedThumbnailSize = s->worldMapWidget->getUndecoratedThumbnailSize();
1172 
1173     if ((pixmap.size().height() > expectedThumbnailSize) || (pixmap.size().width() > expectedThumbnailSize))
1174     {
1175         return;
1176     }
1177 
1178     // find the cluster which is represented by this index:
1179 
1180     for (int i = 0 ; i < s->clusterList.count() ; ++i)
1181     {
1182         // TODO: use the right sortkey
1183         // TODO: let the representativeChooser handle the index comparison
1184 
1185         const QVariant representativeMarker = s->worldMapWidget->getClusterRepresentativeMarker(i, s->sortKey);
1186 
1187         if (s->markerModel->indicesEqual(index, representativeMarker))
1188         {
1189             QPoint clusterCenterPoint;
1190 
1191             // TODO: who calculates the override values?
1192 
1193             const QPixmap clusterPixmap = s->worldMapWidget->getDecoratedPixmapForCluster(i, nullptr, nullptr, &clusterCenterPoint);
1194 
1195             setClusterPixmap(i, clusterCenterPoint, clusterPixmap);
1196 
1197             break;
1198         }
1199     }
1200 }
1201 
1202 void BackendGoogleMaps::setClusterPixmap(const int clusterId, const QPoint& centerPoint, const QPixmap& clusterPixmap)
1203 {
1204     // decorate the pixmap:
1205 
1206     const QPixmap styledPixmap = clusterPixmap;
1207 
1208     QByteArray bytes;
1209     QBuffer buffer(&bytes);
1210     buffer.open(QIODevice::WriteOnly);
1211     clusterPixmap.save(&buffer, "PNG");
1212     buffer.close();
1213 
1214     // www.faqs.org/rfcs/rfc2397.html
1215 
1216     const QString imageData = QString::fromLatin1("data:image/png;base64,%1").arg(QString::fromLatin1(bytes.toBase64()));
1217     d->htmlWidget->runScript(QString::fromLatin1("kgeomapSetClusterPixmap(%1,%5,%6,%2,%3,'%4');")
1218                                  .arg(clusterId)
1219                                  .arg(centerPoint.x())
1220                                  .arg(centerPoint.y())
1221                                  .arg(imageData)
1222                                  .arg(clusterPixmap.width())
1223                                  .arg(clusterPixmap.height())
1224                             );
1225 }
1226 
1227 void BackendGoogleMaps::setMarkerPixmap(const int modelId, const int markerId,
1228                                         const QPoint& centerPoint, const QPixmap& markerPixmap)
1229 {
1230     QByteArray bytes;
1231     QBuffer buffer(&bytes);
1232     buffer.open(QIODevice::WriteOnly);
1233     markerPixmap.save(&buffer, "PNG");
1234     buffer.close();
1235 
1236     // www.faqs.org/rfcs/rfc2397.html
1237 
1238     const QString imageData = QString::fromLatin1("data:image/png;base64,%1").arg(QString::fromLatin1(bytes.toBase64()));
1239     d->htmlWidget->runScript(QString::fromLatin1("kgeomapSetMarkerPixmap(%7,%1,%5,%6,%2,%3,'%4');")
1240                                  .arg(markerId)
1241                                  .arg(centerPoint.x())
1242                                  .arg(centerPoint.y())
1243                                  .arg(imageData)
1244                                  .arg(markerPixmap.width())
1245                                  .arg(markerPixmap.height())
1246                                  .arg(modelId)
1247                             );
1248 }
1249 
1250 void BackendGoogleMaps::setMarkerPixmap(const int modelId, const int markerId,
1251                                         const QPoint& centerPoint, const QSize& iconSize,
1252                                         const QUrl& iconUrl)
1253 {
1254     /// @todo Sort the parameters
1255 
1256     d->htmlWidget->runScript(QString::fromLatin1("kgeomapSetMarkerPixmap(%7,%1,%5,%6,%2,%3,'%4');")
1257                                  .arg(markerId)
1258                                  .arg(centerPoint.x())
1259                                  .arg(centerPoint.y())
1260                                  .arg(iconUrl.url()) /// @todo Escape characters like apostrophe
1261                                  .arg(iconSize.width())
1262                                  .arg(iconSize.height())
1263                                  .arg(modelId)
1264                             );
1265 }
1266 
1267 bool BackendGoogleMaps::eventFilter(QObject* object, QEvent* event)
1268 {
1269     if (object == d->htmlWidgetWrapper)
1270     {
1271         if (event->type() == QEvent::Resize)
1272         {
1273             QResizeEvent* const resizeEvent = dynamic_cast<QResizeEvent*>(event);
1274 
1275             if (resizeEvent)
1276             {
1277                 // TODO: the map div does not adjust its height properly if height=100%,
1278                 //       therefore we adjust it manually here
1279 
1280                 if (d->isReady)
1281                 {
1282                     d->htmlWidget->runScript(QString::fromLatin1("kgeomapWidgetResized(%1, %2)")
1283                                                  .arg(d->htmlWidgetWrapper->width())
1284                                                  .arg(d->htmlWidgetWrapper->height())
1285                                             );
1286                 }
1287             }
1288         }
1289     }
1290 
1291     return false;
1292 }
1293 
1294 void BackendGoogleMaps::regionSelectionChanged()
1295 {
1296     if (!d->htmlWidget)
1297     {
1298         return;
1299     }
1300 
1301     if (s->hasRegionSelection())
1302     {
1303         d->htmlWidget->setSelectionRectangle(s->selectionRectangle);
1304     }
1305     else
1306     {
1307         d->htmlWidget->removeSelectionRectangle();
1308     }
1309 }
1310 
1311 void BackendGoogleMaps::mouseModeChanged()
1312 {
1313     if (!d->htmlWidget)
1314     {
1315         return;
1316     }
1317 
1318     /// @todo Does htmlwidget read this value from s->currentMouseMode on its own?
1319 
1320     d->htmlWidget->mouseModeChanged(s->currentMouseMode);
1321 }
1322 
1323 void BackendGoogleMaps::slotSelectionHasBeenMade(const Digikam::GeoCoordinates::Pair& searchCoordinates)
1324 {
1325     Q_EMIT signalSelectionHasBeenMade(searchCoordinates);
1326 }
1327 
1328 void BackendGoogleMaps::setActive(const bool state)
1329 {
1330     const bool oldState = d->activeState;
1331     d->activeState      = state;
1332 
1333     if (oldState != state)
1334     {
1335         if (!state && d->htmlWidgetWrapper)
1336         {
1337             // we should share our widget in the list of widgets in the global object
1338 
1339             GeoIfaceInternalWidgetInfo info;
1340             info.deleteFunction = deleteInfoFunction;
1341             info.widget         = d->htmlWidgetWrapper.data();
1342             info.currentOwner   = this;
1343             info.backendName    = backendName();
1344             info.state          = d->widgetIsDocked ? GeoIfaceInternalWidgetInfo::InternalWidgetStillDocked
1345                                                     : GeoIfaceInternalWidgetInfo::InternalWidgetUndocked;
1346 
1347             GMInternalWidgetInfo intInfo;
1348             intInfo.htmlWidget  = d->htmlWidget.data();
1349             info.backendData.setValue(intInfo);
1350 
1351             GeoIfaceGlobalObject* const go = GeoIfaceGlobalObject::instance();
1352             go->addMyInternalWidgetToPool(info);
1353         }
1354 
1355         if (state && d->htmlWidgetWrapper)
1356         {
1357             // we should remove our widget from the list of widgets in the global object
1358 
1359             GeoIfaceGlobalObject* const go = GeoIfaceGlobalObject::instance();
1360             go->removeMyInternalWidgetFromPool(this);
1361 
1362             /// @todo re-cluster, update markers?
1363 
1364             setMapType(d->cacheMapType);
1365             setShowScaleControl(d->cacheShowScaleControl);
1366             setShowMapTypeControl(d->cacheShowMapTypeControl);
1367             setShowNavigationControl(d->cacheShowNavigationControl);
1368 
1369             setCenter(d->cacheCenter);
1370             d->htmlWidget->runScript(QString::fromLatin1("kgeomapSetZoom(%1);").arg(d->cacheZoom));
1371 
1372             /// @TODO update tracks more gently
1373 
1374             slotTracksChanged(d->trackChangeTracker);
1375             d->trackChangeTracker.clear();
1376         }
1377     }
1378 }
1379 
1380 void BackendGoogleMaps::releaseWidget(GeoIfaceInternalWidgetInfo* const info)
1381 {
1382     // clear all tracks
1383 
1384     d->htmlWidget->runScript(QString::fromLatin1("kgeomapClearTracks();"));
1385 
1386     disconnect(d->htmlWidget, SIGNAL(signalJavaScriptReady()),
1387                this, SLOT(slotHTMLInitialized()));
1388 
1389     disconnect(d->htmlWidget, SIGNAL(signalHTMLEvents(QStringList)),
1390                this, SLOT(slotHTMLEvents(QStringList)));
1391 
1392     disconnect(d->htmlWidget, SIGNAL(selectionHasBeenMade(Digikam::GeoCoordinates::Pair)),
1393                this, SLOT(slotSelectionHasBeenMade(Digikam::GeoCoordinates::Pair)));
1394 
1395     d->htmlWidget->setSharedGeoIfaceObject(nullptr);
1396     d->htmlWidgetWrapper->removeEventFilter(this);
1397 
1398     d->htmlWidget        = nullptr;
1399     d->htmlWidgetWrapper = nullptr;
1400     info->currentOwner   = nullptr;
1401     info->state          = GeoIfaceInternalWidgetInfo::InternalWidgetReleased;
1402     d->isReady           = false;
1403 
1404     Q_EMIT signalBackendReadyChanged(backendName());
1405 }
1406 
1407 void BackendGoogleMaps::mapWidgetDocked(const bool state)
1408 {
1409     if (d->widgetIsDocked != state)
1410     {
1411         GeoIfaceGlobalObject* const go = GeoIfaceGlobalObject::instance();
1412         go->updatePooledWidgetState(d->htmlWidgetWrapper, state ? GeoIfaceInternalWidgetInfo::InternalWidgetStillDocked
1413                                                                 : GeoIfaceInternalWidgetInfo::InternalWidgetUndocked);
1414     }
1415 
1416     d->widgetIsDocked = state;
1417 }
1418 
1419 void BackendGoogleMaps::deleteInfoFunction(GeoIfaceInternalWidgetInfo* const info)
1420 {
1421     if (info->currentOwner)
1422     {
1423         qobject_cast<MapBackend*>(info->currentOwner.data())->releaseWidget(info);
1424     }
1425 
1426     const GMInternalWidgetInfo intInfo = info->backendData.value<GMInternalWidgetInfo>();
1427 
1428     delete intInfo.htmlWidget;
1429     delete info->widget.data();
1430 }
1431 
1432 void BackendGoogleMaps::storeTrackChanges(const TrackManager::TrackChanges trackChanges)
1433 {
1434     for (int i = 0 ; i < d->trackChangeTracker.count() ; ++i)
1435     {
1436         if (d->trackChangeTracker.at(i).first == trackChanges.first)
1437         {
1438             d->trackChangeTracker[i].second = TrackManager::ChangeFlag(d->trackChangeTracker.at(i).second | trackChanges.second);
1439 
1440             return;
1441         }
1442     }
1443 
1444     d->trackChangeTracker << trackChanges;
1445 }
1446 
1447 void BackendGoogleMaps::slotTrackManagerChanged()
1448 {
1449     /// @TODO disconnect old track manager
1450     /// @TODO mark all tracks as dirty
1451 
1452     if (s->trackManager)
1453     {
1454         connect(s->trackManager, SIGNAL(signalTracksChanged(QList<TrackManager::TrackChanges>)),
1455                 this, SLOT(slotTracksChanged(QList<TrackManager::TrackChanges>)));
1456 
1457         connect(s->trackManager, SIGNAL(signalVisibilityChanged(bool)),
1458                 this, SLOT(slotTrackVisibilityChanged(bool)));
1459 
1460         // store all tracks which are already in the manager as changed
1461 
1462         const TrackManager::Track::List trackList = s->trackManager->getTrackList();
1463 
1464         Q_FOREACH (const TrackManager::Track& t, trackList)
1465         {
1466             storeTrackChanges(TrackManager::TrackChanges(t.id, TrackManager::ChangeAdd));
1467         }
1468     }
1469 }
1470 
1471 void BackendGoogleMaps::slotTracksChanged(const QList<TrackManager::TrackChanges>& trackChanges)
1472 {
1473     bool needToTrackChanges = !d->activeState;
1474 
1475     if (s->trackManager)
1476     {
1477         needToTrackChanges |= !s->trackManager->getVisibility();
1478     }
1479 
1480     if (needToTrackChanges)
1481     {
1482         Q_FOREACH (const TrackManager::TrackChanges& tc, trackChanges)
1483         {
1484             storeTrackChanges(tc);
1485         }
1486 
1487         return;
1488     }
1489 
1490     /// @TODO We have to re-read the tracks after being inactive.
1491     /// @TODO Tracks have to be cleared in JavaScript every time the
1492     ///       htmlwidget is passed to another mapwidget.
1493     /// @TODO Clearing all tracks and re-adding them takes too long. We
1494     ///       have to see which track changed, and whether coordinates or only properties changed.
1495 
1496     if (!s->trackManager)
1497     {
1498         // no track manager, clear all tracks
1499 
1500         const QVariant successClear = d->htmlWidget->runScript(QString::fromLatin1("kgeomapClearTracks();"), false);
1501 
1502         return;
1503     }
1504 
1505     Q_FOREACH (const TrackManager::TrackChanges& tc, trackChanges)
1506     {
1507         if (tc.second & TrackManager::ChangeRemoved)
1508         {
1509             d->htmlWidget->runScript(QString::fromLatin1("kgeomapRemoveTrack(%1);").arg(tc.first));
1510         }
1511         else
1512         {
1513             /// @TODO For now, remove the track and re-add it.
1514 
1515             d->htmlWidget->runScript(QString::fromLatin1("kgeomapRemoveTrack(%1);").arg(tc.first));
1516 
1517             const TrackManager::Track track = s->trackManager->getTrackById(tc.first);
1518 
1519             if (track.points.count() < 2)
1520             {
1521                 continue;
1522             }
1523 
1524             const QString createTrackScript = QString::fromLatin1("kgeomapCreateTrack(%1,'%2');")
1525                                               .arg(track.id)
1526                                               .arg(track.color.name()); // QColor::name() returns #ff00ff
1527             d->htmlWidget->runScript(createTrackScript);
1528 
1529             QDateTime t1                    = QDateTime::currentDateTime();
1530             const int numPointsToPassAtOnce = 1000;
1531 
1532             for (int coordIdx = 0 ; coordIdx < track.points.count() ; coordIdx += numPointsToPassAtOnce)
1533             {
1534                 /// @TODO Even by passing only a few points each time, we can
1535                 ///       block the UI for a long time. Instead, it may be better
1536                 ///       to call addPointsToTrack via the eventloop repeatedly
1537                 ///       to allow processing of other events.
1538 
1539                 addPointsToTrack(track.id, track.points, coordIdx, numPointsToPassAtOnce);
1540             }
1541 
1542             QDateTime t2 = QDateTime::currentDateTime();
1543             qCDebug(DIGIKAM_GEOIFACE_LOG) << track.url.fileName() << t1.msecsTo(t2);
1544         }
1545     }
1546 }
1547 
1548 void BackendGoogleMaps::addPointsToTrack(const quint64 trackId, TrackManager::TrackPoint::List const& track, const int firstPoint, const int nPoints)
1549 {
1550     QString json;
1551     QTextStream jsonBuilder(&json);
1552     jsonBuilder << '[';
1553     int lastPoint = track.count()-1;
1554 
1555     if (nPoints > 0)
1556     {
1557         lastPoint = qMin(firstPoint + nPoints - 1, track.count()-1);
1558     }
1559 
1560     for (int coordIdx = firstPoint ; coordIdx <= lastPoint ; ++coordIdx)
1561     {
1562         GeoCoordinates const& coordinates = track.at(coordIdx).coordinates;
1563 
1564         if (coordIdx > firstPoint)
1565         {
1566             jsonBuilder << ',';
1567         }
1568 
1569         /// @TODO This looks like a lot of text to parse. Is there a more compact way?
1570 
1571         jsonBuilder << "{\"lat\":" << coordinates.latString() << ","
1572                     << "\"lon\":"  << coordinates.lonString() << "}";
1573     }
1574 
1575     jsonBuilder << ']';
1576     const QString addTrackScript = QString::fromLatin1("kgeomapAddToTrack(%1,'%2');").arg(trackId).arg(json);
1577     d->htmlWidget->runScript(addTrackScript);
1578 }
1579 
1580 void BackendGoogleMaps::slotTrackVisibilityChanged(const bool newState)
1581 {
1582     /// @TODO Now we remove all tracks and re-add them on visibility change.
1583     ///       This is very slow.
1584 
1585     if      (newState)
1586     {
1587         // store all tracks which are already in the manager as changed
1588 
1589         const TrackManager::Track::List trackList = s->trackManager->getTrackList();
1590         QList<TrackManager::TrackChanges> trackChanges;
1591 
1592         Q_FOREACH (const TrackManager::Track& t, trackList)
1593         {
1594             trackChanges << TrackManager::TrackChanges(t.id, TrackManager::ChangeAdd);
1595         }
1596 
1597         slotTracksChanged(trackChanges);
1598     }
1599     else if (d->htmlWidget)
1600     {
1601         const QVariant successClear = d->htmlWidget->runScript(QString::fromLatin1("kgeomapClearTracks();"), false);
1602     }
1603 }
1604 
1605 void BackendGoogleMaps::slotMessageEvent(const QString& /*message*/)
1606 {
1607 /*
1608     qCDebug(DIGIKAM_GEOIFACE_LOG) << "Javascript Message:" << message;
1609 */
1610 }
1611 
1612 #ifdef HAVE_GEOLOCATION
1613 
1614 void BackendGoogleMaps::centerOn(const Marble::GeoDataLatLonBox& latLonBox, const bool useSaneZoomLevel)
1615 {
1616     /// @todo Buffer this call if there is no widget or if inactive!
1617 
1618     if (!d->htmlWidget)
1619     {
1620         return;
1621     }
1622 
1623     const qreal boxWest  = latLonBox.west(Marble::GeoDataCoordinates::Degree);
1624     const qreal boxNorth = latLonBox.north(Marble::GeoDataCoordinates::Degree);
1625     const qreal boxEast  = latLonBox.east(Marble::GeoDataCoordinates::Degree);
1626     const qreal boxSouth = latLonBox.south(Marble::GeoDataCoordinates::Degree);
1627 
1628     d->htmlWidget->centerOn(boxWest, boxNorth, boxEast, boxSouth, useSaneZoomLevel);
1629     qCDebug(DIGIKAM_GEOIFACE_LOG) << getZoom();
1630 }
1631 
1632 #endif
1633 
1634 } // namespace Digikam
1635 
1636 #include "moc_backendgooglemaps.cpp"