/* ============================================================
 *
 * This file is a part of digiKam project
 * https://www.digikam.org
 *
 * Date        : 2009-12-01
 * Description : world map widget library
 *
 * Copyright (C) 2010-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
 * Copyright (C) 2009-2011 by Michael G. Hansen <mike at mghansen dot de>
 * Copyright (C)      2014 by Justus Schwartz <justus at gmx dot li>
 *
 * This program is free software; you can redistribute it
 * and/or modify it under the terms of the GNU General
 * Public License as published by the Free Software Foundation;
 * either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * ============================================================ */

#include "mapwidget.h"

// C++ includes

#include <cmath>

// Qt includes

#include <QDragEnterEvent>
#include <QDropEvent>
#include <QItemSelectionModel>
#include <QMenu>
#include <QPainter>
#include <QPointer>
#include <QStackedLayout>
#include <QTimer>
#include <QPushButton>
#include <QToolButton>
#include <QHBoxLayout>
#include <QAction>
#include <QFrame>

// KDE includes

#include <klocalizedstring.h>
#include <kconfiggroup.h>

// Marbel includes

#include <marble/GeoDataLineString.h>
#include <marble/GeoDataLatLonBox.h>
#include <marble/MarbleGlobal.h>

// local includes

#include "geoifacecommon.h"
#include "geodragdrophandler.h"
#include "geomodelhelper.h"
#include "trackmanager.h"
#include "placeholderwidget.h"
#include "tilegrouper.h"
#include "digikam_debug.h"
#include "abstractmarkertiler.h"
#include "backendgooglemaps.h"
#include "backendmarble.h"

namespace Digikam
{

/**
 * @class MapWidget
 * @brief The central map view class of geolocation interface
 *
 * The MapWidget class is the central widget of geolocation interface. It provides a widget which can display maps using
 * either the Marble or Google Maps backend. Using a model, items can be displayed on the map. For
 * models containing only a small number of items, the items can be shown directly, but for models with
 * a larger number of items, the items can also be grouped. Currently, any number of ungrouped models
 * can be shown, but only one grouped model. Item selection models can also be used along with the models,
 * to interact with the selection states of the items on the map. In order to use a model with geolocation interface, however,
 * a model helper has to be implemented, which extracts data from the model that is not provided by the Qt part
 * of a model's API.
 *
 * Now, a brief introduction on how to get geolocation interface working is provided:
 * @li First, an instance of @c MapWidget has to be created.
 * @li Next, @c GeoModelHelper has to be subclassed and at least the pure virtual functions have to be implemented.
 * @li To show the model's data ungrouped, the model helper has to be added to @c MapWidget instance using addUngroupedModel.
 * @li To show the model's data grouped, an instance of @c AbstractMarkerTiler has to be created and the model helper has to be
 *     set to it using setMarkerGeoModelHelper. The @c AbstractMarkerTiler has then to be given to MapWidget using setGroupedModel. If
 *     the items to be displayed do not reside in a model, a subclass of @c AbstractMarkerTiler can be created which returns
 *     just the number of items in a particular area, and picks representative items for thumbnails.
 * @li To handle dropping of items from the host applications UI onto the map, @c DragDropHandler has to be subclassed
 *     as well and added to the model using setDragDropHandler.
 * @li Finally, setActive() has to be called to tell the widget that it should start displaying things.
 */

class Q_DECL_HIDDEN MapWidget::Private
{
public:

    explicit Private()
      : loadedBackends(),
        currentBackend(nullptr),
        currentBackendName(),
        stackedLayout(nullptr),
        cacheCenterCoordinate(52.0,6.0),
        cacheZoom(QLatin1String("marble:900")),
        configurationMenu(nullptr),
        actionGroupBackendSelection(nullptr),
        actionZoomIn(nullptr),
        actionZoomOut(nullptr),
        actionShowThumbnails(nullptr),
        mouseModesHolder(nullptr),
        controlWidget(nullptr),
        actionPreviewSingleItems(nullptr),
        actionPreviewGroupedItems(nullptr),
        actionShowNumbersOnItems(nullptr),
        lazyReclusteringRequested(false),
        dragDropHandler(nullptr),
        sortMenu(nullptr),
        actionIncreaseThumbnailSize(nullptr),
        actionDecreaseThumbnailSize(nullptr),
        hBoxForAdditionalControlWidgetItems(nullptr),
        mouseModeActionGroup(nullptr),
        actionRemoveCurrentRegionSelection(nullptr),
        actionSetRegionSelectionMode(nullptr),
        actionSetPanMode(nullptr),
        actionSetZoomIntoGroupMode(nullptr),
        actionSetRegionSelectionFromIconMode(nullptr),
        actionSetFilterMode(nullptr),
        actionRemoveFilter(nullptr),
        actionSetSelectThumbnailMode(nullptr),
        setPanModeButton(nullptr),
        setSelectionModeButton(nullptr),
        removeCurrentSelectionButton(nullptr),
        setZoomModeButton(nullptr),
        setRegionSelectionFromIconModeButton(nullptr),
        setFilterModeButton(nullptr),
        removeFilterModeButton(nullptr),
        setSelectThumbnailMode(nullptr),
        thumbnailTimer(nullptr),
        thumbnailTimerCount(0),
        thumbnailsHaveBeenLoaded(false),
        availableExtraActions(),
        visibleExtraActions(),
        actionStickyMode(nullptr),
        buttonStickyMode(nullptr),
        placeholderWidget(nullptr)
    {
    }

    QList<MapBackend*>      loadedBackends;
    MapBackend*             currentBackend;
    QString                 currentBackendName;
    QStackedLayout*         stackedLayout;

    /// NOTE: these values are cached in case the backend is not ready:
    GeoCoordinates          cacheCenterCoordinate;
    QString                 cacheZoom;

    /// actions for controlling the widget
    QMenu*                  configurationMenu;
    QActionGroup*           actionGroupBackendSelection;
    QAction*                actionZoomIn;
    QAction*                actionZoomOut;
    QAction*                actionShowThumbnails;
    QWidget*                mouseModesHolder;
    QPointer<QWidget>       controlWidget;
    QAction*                actionPreviewSingleItems;
    QAction*                actionPreviewGroupedItems;
    QAction*                actionShowNumbersOnItems;

    bool                    lazyReclusteringRequested;

    GeoDragDropHandler*     dragDropHandler;

    QMenu*                  sortMenu;
    QAction*                actionIncreaseThumbnailSize;
    QAction*                actionDecreaseThumbnailSize;
    QWidget*                hBoxForAdditionalControlWidgetItems;

    QActionGroup*           mouseModeActionGroup;
    QAction*                actionRemoveCurrentRegionSelection;
    QAction*                actionSetRegionSelectionMode;
    QAction*                actionSetPanMode;
    QAction*                actionSetZoomIntoGroupMode;
    QAction*                actionSetRegionSelectionFromIconMode;
    QAction*                actionSetFilterMode;
    QAction*                actionRemoveFilter;
    QAction*                actionSetSelectThumbnailMode;
    QToolButton*            setPanModeButton;
    QToolButton*            setSelectionModeButton;
    QToolButton*            removeCurrentSelectionButton;
    QToolButton*            setZoomModeButton;
    QToolButton*            setRegionSelectionFromIconModeButton;
    QToolButton*            setFilterModeButton;
    QToolButton*            removeFilterModeButton;
    QToolButton*            setSelectThumbnailMode;

    QTimer*                 thumbnailTimer;
    int                     thumbnailTimerCount;
    bool                    thumbnailsHaveBeenLoaded;

    GeoExtraActions         availableExtraActions;
    GeoExtraActions         visibleExtraActions;
    QAction*                actionStickyMode;
    QToolButton*            buttonStickyMode;

    /// NOTE: to be sorted later
    PlaceholderWidget*      placeholderWidget;
};

MapWidget::MapWidget(QWidget* const parent)
    : QWidget(parent),
      s(new GeoIfaceSharedData),
      d(new Private)
{
    createActions();

    s->worldMapWidget = this;
    s->tileGrouper    = new TileGrouper(s, this);

    d->stackedLayout  = new QStackedLayout(this);
    setLayout(d->stackedLayout);

    d->placeholderWidget = new PlaceholderWidget();
    d->stackedLayout->addWidget(d->placeholderWidget);

    d->loadedBackends.append(new BackendGoogleMaps(s, this));
    d->loadedBackends.append(new BackendMarble(s, this));
/*
    d->loadedBackends.append(new BackendOSM(s, this));
*/
    createActionsForBackendSelection();

    setAcceptDrops(true);
}

void MapWidget::createActions()
{
    d->actionZoomIn = new QAction(this);
    d->actionZoomIn->setIcon(QIcon::fromTheme( QLatin1String("zoom-in") ));
    d->actionZoomIn->setToolTip(i18n("Zoom in"));

    connect(d->actionZoomIn, &QAction::triggered,
            this, &MapWidget::slotZoomIn);

    d->actionZoomOut = new QAction(this);
    d->actionZoomOut->setIcon(QIcon::fromTheme( QLatin1String("zoom-out") ));
    d->actionZoomOut->setToolTip(i18n("Zoom out"));

    connect(d->actionZoomOut, &QAction::triggered,
            this, &MapWidget::slotZoomOut);

    d->actionShowThumbnails = new QAction(this);
    d->actionShowThumbnails->setToolTip(i18n("Switch between markers and thumbnails."));
    d->actionShowThumbnails->setCheckable(true);
    d->actionShowThumbnails->setChecked(true);

    connect(d->actionShowThumbnails, &QAction::triggered,
            this, &MapWidget::slotShowThumbnailsChanged);

    // create backend selection entries:

    d->actionGroupBackendSelection = new QActionGroup(this);
    d->actionGroupBackendSelection->setExclusive(true);

    connect(d->actionGroupBackendSelection, &QActionGroup::triggered,
            this, &MapWidget::slotChangeBackend);

    createActionsForBackendSelection();

    d->configurationMenu         = new QMenu(this);
    d->actionPreviewSingleItems  = new QAction(i18n("Preview single items"), this);
    d->actionPreviewSingleItems->setCheckable(true);
    d->actionPreviewSingleItems->setChecked(true);
    d->actionPreviewGroupedItems = new QAction(i18n("Preview grouped items"), this);
    d->actionPreviewGroupedItems->setCheckable(true);
    d->actionPreviewGroupedItems->setChecked(true);
    d->actionShowNumbersOnItems  = new QAction(i18n("Show numbers"), this);
    d->actionShowNumbersOnItems->setCheckable(true);
    d->actionShowNumbersOnItems->setChecked(true);

    d->actionIncreaseThumbnailSize = new QAction(i18n("T+"), this);
    d->actionIncreaseThumbnailSize->setToolTip(i18n("Increase the thumbnail size on the map"));
    d->actionDecreaseThumbnailSize = new QAction(i18n("T-"), this);
    d->actionDecreaseThumbnailSize->setToolTip(i18n("Decrease the thumbnail size on the map"));

    d->actionRemoveCurrentRegionSelection = new QAction(this);
/*
    d->actionRemoveCurrentRegionSelection->setEnabled(false);
*/
    d->actionRemoveCurrentRegionSelection->setIcon(QIcon::fromTheme( QLatin1String("edit-clear") ));
    d->actionRemoveCurrentRegionSelection->setToolTip(i18n("Remove the current region selection"));

    d->mouseModeActionGroup = new QActionGroup(this);
    d->mouseModeActionGroup->setExclusive(true);

    d->actionSetRegionSelectionMode = new QAction(d->mouseModeActionGroup);
    d->actionSetRegionSelectionMode->setCheckable(true);
    d->actionSetRegionSelectionMode->setIcon(QIcon::fromTheme( QLatin1String("select-rectangular") ));
    d->actionSetRegionSelectionMode->setToolTip(i18n("Select images by drawing a rectangle"));
    d->actionSetRegionSelectionMode->setData(QVariant::fromValue<Digikam::GeoMouseModes>(MouseModeRegionSelection));

    d->actionSetPanMode = new QAction(d->mouseModeActionGroup);
    d->actionSetPanMode->setCheckable(true);
    d->actionSetPanMode->setToolTip(i18n("Pan mode"));
    d->actionSetPanMode->setIcon(QIcon::fromTheme( QLatin1String("transform-move") ));
    d->actionSetPanMode->setChecked(true);
    d->actionSetPanMode->setData(QVariant::fromValue<Digikam::GeoMouseModes>(MouseModePan));

    d->actionSetZoomIntoGroupMode = new QAction(d->mouseModeActionGroup);
    d->actionSetZoomIntoGroupMode->setCheckable(true);
    d->actionSetZoomIntoGroupMode->setToolTip(i18n("Zoom into a group"));
    d->actionSetZoomIntoGroupMode->setIcon(QIcon::fromTheme( QLatin1String("zoom-fit-best") ));
    d->actionSetZoomIntoGroupMode->setData(QVariant::fromValue<Digikam::GeoMouseModes>(MouseModeZoomIntoGroup));

    d->actionSetRegionSelectionFromIconMode = new QAction(d->mouseModeActionGroup);
    d->actionSetRegionSelectionFromIconMode->setCheckable(true);
    d->actionSetRegionSelectionFromIconMode->setToolTip(i18n("Create a region selection from a thumbnail"));
    d->actionSetRegionSelectionFromIconMode->setIcon(QIcon::fromTheme( QLatin1String("edit-node") ));
    d->actionSetRegionSelectionFromIconMode->setData(QVariant::fromValue<Digikam::GeoMouseModes>(MouseModeRegionSelectionFromIcon));

    d->actionSetFilterMode = new QAction(d->mouseModeActionGroup);
    d->actionSetFilterMode->setCheckable(true);
    d->actionSetFilterMode->setToolTip(i18n("Filter images"));
    d->actionSetFilterMode->setIcon(QIcon::fromTheme( QLatin1String("view-filter") ));
    d->actionSetFilterMode->setData(QVariant::fromValue<Digikam::GeoMouseModes>(MouseModeFilter));

    d->actionRemoveFilter = new QAction(this);
    d->actionRemoveFilter->setToolTip(i18n("Remove the current filter"));
    d->actionRemoveFilter->setIcon(QIcon::fromTheme( QLatin1String("window-close") ));

    d->actionSetSelectThumbnailMode = new QAction(d->mouseModeActionGroup);
    d->actionSetSelectThumbnailMode->setCheckable(true);
    d->actionSetSelectThumbnailMode->setToolTip(i18n("Select images"));
    d->actionSetSelectThumbnailMode->setIcon(QIcon::fromTheme( QLatin1String("edit-select") ));
    d->actionSetSelectThumbnailMode->setData(QVariant::fromValue<Digikam::GeoMouseModes>(MouseModeSelectThumbnail));

    d->actionStickyMode = new QAction(this);
    d->actionStickyMode->setCheckable(true);
    d->actionStickyMode->setToolTip(i18n("Lock the map position"));

    connect(d->actionStickyMode, &QAction::triggered,
            this, &MapWidget::slotStickyModeChanged);

    connect(d->actionIncreaseThumbnailSize, &QAction::triggered,
            this, &MapWidget::slotIncreaseThumbnailSize);

    connect(d->actionDecreaseThumbnailSize, &QAction::triggered,
            this, &MapWidget::slotDecreaseThumbnailSize);

    connect(d->actionPreviewSingleItems, &QAction::changed,
            this, &MapWidget::slotItemDisplaySettingsChanged);

    connect(d->actionPreviewGroupedItems, &QAction::changed,
            this, &MapWidget::slotItemDisplaySettingsChanged);

    connect(d->actionShowNumbersOnItems, &QAction::changed,
            this, &MapWidget::slotItemDisplaySettingsChanged);

    connect(d->mouseModeActionGroup, &QActionGroup::triggered,
            this, &MapWidget::slotMouseModeChanged);

    connect(d->actionRemoveFilter, &QAction::triggered,
            this, &MapWidget::signalRemoveCurrentFilter);

    connect(d->actionRemoveCurrentRegionSelection, &QAction::triggered,
            this, &MapWidget::slotRemoveCurrentRegionSelection);
}

void MapWidget::createActionsForBackendSelection()
{
    // delete the existing actions:

    qDeleteAll(d->actionGroupBackendSelection->actions());

    // create actions for all backends:

    for (int i = 0 ; i < d->loadedBackends.size() ; ++i)
    {
        const QString backendName    = d->loadedBackends.at(i)->backendName();
        QAction* const backendAction = new QAction(d->actionGroupBackendSelection);
        backendAction->setData(backendName);
        backendAction->setText(d->loadedBackends.at(i)->backendHumanName());
        backendAction->setCheckable(true);
    }
}

MapWidget::~MapWidget()
{
    // release all widgets:

    for (int i = 0 ; i < d->stackedLayout->count() ; ++i)
    {
        d->stackedLayout->removeWidget(d->stackedLayout->widget(i));
    }

    qDeleteAll(d->loadedBackends);
    d->currentBackend = nullptr;
    d->loadedBackends.clear();
    delete d;

    /// @todo delete s, but make sure it is not accessed by any other objects any more!
}

QStringList MapWidget::availableBackends() const
{
    QStringList result;

    foreach (MapBackend* const backend, d->loadedBackends)
    {
        result.append(backend->backendName());
    }

    return result;
}

bool MapWidget::setBackend(const QString& backendName)
{
    if (backendName == d->currentBackendName)
    {
        return true;
    }

    saveBackendToCache();

    // switch to the placeholder widget:

    setShowPlaceholderWidget(true);
    removeMapWidgetFromFrame();

    // disconnect signals from old backend:

    if (d->currentBackend)
    {
        d->currentBackend->setActive(false);

        disconnect(d->currentBackend, SIGNAL(signalBackendReadyChanged(QString)),
                   this, SLOT(slotBackendReadyChanged(QString)));

        disconnect(d->currentBackend, SIGNAL(signalZoomChanged(QString)),
                   this, SLOT(slotBackendZoomChanged(QString)));

        disconnect(d->currentBackend, SIGNAL(signalClustersMoved(QIntList,QPair<int,QModelIndex>)),
                   this, SLOT(slotClustersMoved(QIntList,QPair<int,QModelIndex>)));

        disconnect(d->currentBackend, SIGNAL(signalClustersClicked(QIntList)),
                   this, SLOT(slotClustersClicked(QIntList)));

        disconnect(this, SIGNAL(signalUngroupedModelChanged(int)),
                   d->currentBackend, SLOT(slotUngroupedModelChanged(int)));

        if (s->markerModel)
        {
            disconnect(s->markerModel, SIGNAL(signalThumbnailAvailableForIndex(QVariant,QPixmap)),
                       d->currentBackend, SLOT(slotThumbnailAvailableForIndex(QVariant,QPixmap)));
        }

        disconnect(d->currentBackend, SIGNAL(signalSelectionHasBeenMade(Digikam::GeoCoordinates::Pair)),
                   this, SLOT(slotNewSelectionFromMap(Digikam::GeoCoordinates::Pair)));

    }

    foreach (MapBackend* const backend, d->loadedBackends)
    {
        if (backend->backendName() == backendName)
        {
            qCDebug(DIGIKAM_GEOIFACE_LOG) << QString::fromLatin1("setting backend %1").arg(backendName);
            d->currentBackend     = backend;
            d->currentBackendName = backendName;

            connect(d->currentBackend, &MapBackend::signalBackendReadyChanged,
                    this, &MapWidget::slotBackendReadyChanged);

            connect(d->currentBackend, &MapBackend::signalZoomChanged,
                    this, &MapWidget::slotBackendZoomChanged);

            connect(d->currentBackend, &MapBackend::signalClustersMoved,
                    this, &MapWidget::slotClustersMoved);

            connect(d->currentBackend, &MapBackend::signalClustersClicked,
                    this, &MapWidget::slotClustersClicked);

            /**
             * @todo This connection is queued because otherwise QAbstractItemModel::itemSelected does not
             *       reflect the true state. Maybe monitor another signal instead?
             */
            connect(this, SIGNAL(signalUngroupedModelChanged(int)),
                    d->currentBackend, SLOT(slotUngroupedModelChanged(int)), Qt::QueuedConnection);

            if (s->markerModel)
            {
                connect(s->markerModel, SIGNAL(signalThumbnailAvailableForIndex(QVariant,QPixmap)),
                        d->currentBackend, SLOT(slotThumbnailAvailableForIndex(QVariant,QPixmap)));
            }

            connect(d->currentBackend, &MapBackend::signalSelectionHasBeenMade,
                    this, &MapWidget::slotNewSelectionFromMap);

            if (s->activeState)
            {
                setMapWidgetInFrame(d->currentBackend->mapWidget());

                // call this slot manually in case the backend was ready right away:

                if (d->currentBackend->isReady())
                {
                    slotBackendReadyChanged(d->currentBackendName);
                }
                else
                {
                    rebuildConfigurationMenu();
                }
            }

            d->currentBackend->setActive(s->activeState);

            return true;
        }
    }

    return false;
}

void MapWidget::applyCacheToBackend()
{
    if ((!currentBackendReady()) || (!s->activeState))
    {
        return;
    }

    /// @todo Only do this if the zoom was changed!

    qCDebug(DIGIKAM_GEOIFACE_LOG) << d->cacheZoom;

    setZoom(d->cacheZoom);
    setCenter(d->cacheCenterCoordinate);
    d->currentBackend->mouseModeChanged();
    d->currentBackend->regionSelectionChanged();
}

void MapWidget::saveBackendToCache()
{
    if (!currentBackendReady())
    {
        return;
    }

    d->cacheCenterCoordinate = getCenter();
    d->cacheZoom             = getZoom();
}

GeoCoordinates MapWidget::getCenter() const
{
    if (!currentBackendReady())
    {
        return d->cacheCenterCoordinate;
    }

    return d->currentBackend->getCenter();
}

void MapWidget::setCenter(const GeoCoordinates& coordinate)
{
    d->cacheCenterCoordinate = coordinate;

    if (!currentBackendReady())
    {
        return;
    }

    d->currentBackend->setCenter(coordinate);
}

void MapWidget::slotBackendReadyChanged(const QString& backendName)
{
    qCDebug(DIGIKAM_GEOIFACE_LOG) << QString::fromLatin1("backend %1 is ready!").arg(backendName);

    if (backendName != d->currentBackendName)
    {
        return;
    }

    if (!currentBackendReady())
    {
        return;
    }

    applyCacheToBackend();

    setShowPlaceholderWidget(false);

    if (!d->thumbnailsHaveBeenLoaded)
    {
        d->thumbnailTimer      = new QTimer(this);
        d->thumbnailTimerCount = 0;

        connect(d->thumbnailTimer, &QTimer::timeout,
                this, &MapWidget::stopThumbnailTimer);

        d->thumbnailTimer->start(2000);
    }

    updateMarkers();
    markClustersAsDirty();
    rebuildConfigurationMenu();
}

void MapWidget::stopThumbnailTimer()
{
    d->currentBackend->updateMarkers();
    d->thumbnailTimerCount++;

    if (d->thumbnailTimerCount == 10)
    {
        d->thumbnailTimer->stop();
        d->thumbnailsHaveBeenLoaded = true;
    }
}

void MapWidget::saveSettingsToGroup(KConfigGroup* const group)
{
    GEOIFACE_ASSERT(group != nullptr);

    if (!group)
    {
        return;
    }

    if (!d->currentBackendName.isEmpty())
    {
        group->writeEntry("Backend", d->currentBackendName);
    }

    group->writeEntry("Center",                    getCenter().geoUrl());
    group->writeEntry("Zoom",                      getZoom());
    group->writeEntry("Preview Single Items",      s->previewSingleItems);
    group->writeEntry("Preview Grouped Items",     s->previewGroupedItems);
    group->writeEntry("Show numbers on items",     s->showNumbersOnItems);
    group->writeEntry("Thumbnail Size",            s->thumbnailSize);
    group->writeEntry("Thumbnail Grouping Radius", s->thumbnailGroupingRadius);
    group->writeEntry("Marker Grouping Radius",    s->markerGroupingRadius);
    group->writeEntry("Show Thumbnails",           s->showThumbnails);
    group->writeEntry("Mouse Mode",                int(s->currentMouseMode));

    if (d->visibleExtraActions.testFlag(ExtraActionSticky))
    {
        group->writeEntry("Sticky Mode State", d->actionStickyMode->isChecked());
    }

    for (int i = 0 ; i < d->loadedBackends.size() ; ++i)
    {
        d->loadedBackends.at(i)->saveSettingsToGroup(group);
    }
}

void MapWidget::readSettingsFromGroup(const KConfigGroup* const group)
{
    GEOIFACE_ASSERT(group != nullptr);

    if (!group)
    {
        return;
    }

    setBackend(group->readEntry("Backend", "marble"));

    // Options concerning the display of markers

    d->actionPreviewSingleItems->setChecked(group->readEntry("Preview Single Items",   true));
    d->actionPreviewGroupedItems->setChecked(group->readEntry("Preview Grouped Items", true));
    d->actionShowNumbersOnItems->setChecked(group->readEntry("Show numbers on items",  true));

    setThumnailSize(group->readEntry("Thumbnail Size",                       2*GeoIfaceMinThumbnailSize));
    setThumbnailGroupingRadius(group->readEntry("Thumbnail Grouping Radius", 2*GeoIfaceMinThumbnailGroupingRadius));
    setMarkerGroupingRadius(group->readEntry("Edit Grouping Radius",         GeoIfaceMinMarkerGroupingRadius));
    s->showThumbnails = group->readEntry("Show Thumbnails",                  s->showThumbnails);
    d->actionShowThumbnails->setChecked(s->showThumbnails);
    d->actionStickyMode->setChecked(group->readEntry("Sticky Mode State",    d->actionStickyMode->isChecked()));

    // let the backends load their settings

    for (int i = 0 ; i < d->loadedBackends.size() ; ++i)
    {
        d->loadedBackends.at(i)->readSettingsFromGroup(group);
    }

    // current map state

    const GeoCoordinates centerDefault    = GeoCoordinates(52.0, 6.0);
    const QString centerGeoUrl            = group->readEntry("Center", centerDefault.geoUrl());
    bool centerGeoUrlValid                = false;
    const GeoCoordinates centerCoordinate = GeoCoordinates::fromGeoUrl(centerGeoUrl, &centerGeoUrlValid);
    d->cacheCenterCoordinate              = centerGeoUrlValid ? centerCoordinate : centerDefault;
    d->cacheZoom                          = group->readEntry("Zoom", d->cacheZoom);
    s->currentMouseMode                   = GeoMouseModes(group->readEntry("Mouse Mode", int(s->currentMouseMode)));

    // propagate the loaded values to the map, if appropriate

    applyCacheToBackend();
    slotUpdateActionsEnabled();
}

void MapWidget::rebuildConfigurationMenu()
{
    d->configurationMenu->clear();
    const QList<QAction*> backendSelectionActions = d->actionGroupBackendSelection->actions();

    for (int i = 0 ; i < backendSelectionActions.count() ; ++i)
    {
        QAction* const backendAction = backendSelectionActions.at(i);

        if (backendAction->data().toString() == d->currentBackendName)
        {
            backendAction->setChecked(true);
        }

        d->configurationMenu->addAction(backendAction);
    }

    if (currentBackendReady())
    {
        d->currentBackend->addActionsToConfigurationMenu(d->configurationMenu);
    }

    if (s->showThumbnails)
    {
        d->configurationMenu->addSeparator();

        if (d->sortMenu)
        {
            d->configurationMenu->addMenu(d->sortMenu);
        }

        d->configurationMenu->addAction(d->actionPreviewSingleItems);
        d->configurationMenu->addAction(d->actionPreviewGroupedItems);
        d->configurationMenu->addAction(d->actionShowNumbersOnItems);
    }

    slotUpdateActionsEnabled();
}

QAction* MapWidget::getControlAction(const QString& actionName)
{
    if      (actionName == QLatin1String("zoomin"))
    {
        return d->actionZoomIn;
    }
    else if (actionName == QLatin1String("zoomout"))
    {
        return d->actionZoomOut;
    }
    else if (actionName == QLatin1String("mousemode-regionselectionmode"))
    {
        return d->actionSetRegionSelectionMode;
    }
    else if (actionName == QLatin1String("mousemode-removecurrentregionselection"))
    {
        return d->actionRemoveCurrentRegionSelection;
    }
    else if (actionName == QLatin1String("mousemode-regionselectionfromiconmode"))
    {
        return d->actionSetRegionSelectionFromIconMode;
    }
    else if (actionName == QLatin1String("mousemode-removefilter"))
    {
        return d->actionRemoveFilter;
    }

    return nullptr;
}

/**
 * @brief Returns the control widget.
 */
QWidget* MapWidget::getControlWidget()
{
    if (!d->controlWidget)
    {
        d->controlWidget                           = new QWidget(this);
        QHBoxLayout* const controlWidgetHBoxLayout = new QHBoxLayout(d->controlWidget);
        controlWidgetHBoxLayout->setContentsMargins(QMargins());

        QPushButton* const configurationButton = new QPushButton(d->controlWidget);
        configurationButton->setIcon(QIcon::fromTheme(QLatin1String("globe")));
        controlWidgetHBoxLayout->addWidget(configurationButton);
        configurationButton->setToolTip(i18n("Map settings"));
        configurationButton->setMenu(d->configurationMenu);

        QToolButton* const zoomInButton = new QToolButton(d->controlWidget);
        controlWidgetHBoxLayout->addWidget(zoomInButton);
        zoomInButton->setDefaultAction(d->actionZoomIn);

        QToolButton* const zoomOutButton = new QToolButton(d->controlWidget);
        controlWidgetHBoxLayout->addWidget(zoomOutButton);
        zoomOutButton->setDefaultAction(d->actionZoomOut);

        QToolButton* const showThumbnailsButton = new QToolButton(d->controlWidget);
        controlWidgetHBoxLayout->addWidget(showThumbnailsButton);
        showThumbnailsButton->setDefaultAction(d->actionShowThumbnails);

        QFrame* const vline1 = new QFrame(d->controlWidget);
        vline1->setLineWidth(1);
        vline1->setMidLineWidth(0);
        vline1->setFrameShape(QFrame::VLine);
        vline1->setFrameShadow(QFrame::Sunken);
        vline1->setMinimumSize(2, 0);
        vline1->updateGeometry();
        controlWidgetHBoxLayout->addWidget(vline1);

        QToolButton* const increaseThumbnailSizeButton = new QToolButton(d->controlWidget);
        controlWidgetHBoxLayout->addWidget(increaseThumbnailSizeButton);
        increaseThumbnailSizeButton->setDefaultAction(d->actionIncreaseThumbnailSize);

        QToolButton* const decreaseThumbnailSizeButton = new QToolButton(d->controlWidget);
        controlWidgetHBoxLayout->addWidget(decreaseThumbnailSizeButton);
        decreaseThumbnailSizeButton->setDefaultAction(d->actionDecreaseThumbnailSize);

        /* --- --- --- */

        d->mouseModesHolder                           = new QWidget(d->controlWidget);
        QHBoxLayout* const mouseModesHolderHBoxLayout = new QHBoxLayout(d->mouseModesHolder);
        mouseModesHolderHBoxLayout->setContentsMargins(QMargins());
        controlWidgetHBoxLayout->addWidget(d->mouseModesHolder);

        QFrame* const vline2 = new QFrame(d->mouseModesHolder);
        vline2->setLineWidth(1);
        vline2->setMidLineWidth(0);
        vline2->setFrameShape(QFrame::VLine);
        vline2->setFrameShadow(QFrame::Sunken);
        vline2->setMinimumSize(2, 0);
        vline2->updateGeometry();
        mouseModesHolderHBoxLayout->addWidget(vline2);

        d->setPanModeButton = new QToolButton(d->mouseModesHolder);
        mouseModesHolderHBoxLayout->addWidget(d->setPanModeButton);
        d->setPanModeButton->setDefaultAction(d->actionSetPanMode);

        d->setSelectionModeButton = new QToolButton(d->mouseModesHolder);
        mouseModesHolderHBoxLayout->addWidget(d->setSelectionModeButton);
        d->setSelectionModeButton->setDefaultAction(d->actionSetRegionSelectionMode);

        d->setRegionSelectionFromIconModeButton = new QToolButton(d->mouseModesHolder);
        mouseModesHolderHBoxLayout->addWidget(d->setRegionSelectionFromIconModeButton);
        d->setRegionSelectionFromIconModeButton->setDefaultAction(d->actionSetRegionSelectionFromIconMode);

        d->removeCurrentSelectionButton = new QToolButton(d->mouseModesHolder);
        mouseModesHolderHBoxLayout->addWidget(d->removeCurrentSelectionButton);
        d->removeCurrentSelectionButton->setDefaultAction(d->actionRemoveCurrentRegionSelection);

        d->setZoomModeButton = new QToolButton(d->mouseModesHolder);
        mouseModesHolderHBoxLayout->addWidget(d->setZoomModeButton);
        d->setZoomModeButton->setDefaultAction(d->actionSetZoomIntoGroupMode);

        d->setFilterModeButton = new QToolButton(d->mouseModesHolder);
        mouseModesHolderHBoxLayout->addWidget(d->setFilterModeButton);
        d->setFilterModeButton->setDefaultAction(d->actionSetFilterMode);

        d->removeFilterModeButton = new QToolButton(d->mouseModesHolder);
        mouseModesHolderHBoxLayout->addWidget(d->removeFilterModeButton);
        d->removeFilterModeButton->setDefaultAction(d->actionRemoveFilter);

        d->setSelectThumbnailMode = new QToolButton(d->mouseModesHolder);
        mouseModesHolderHBoxLayout->addWidget(d->setSelectThumbnailMode);
        d->setSelectThumbnailMode->setDefaultAction(d->actionSetSelectThumbnailMode);

        d->buttonStickyMode = new QToolButton(d->controlWidget);
        controlWidgetHBoxLayout->addWidget(d->buttonStickyMode);
        d->buttonStickyMode->setDefaultAction(d->actionStickyMode);

        d->hBoxForAdditionalControlWidgetItems = new QWidget(d->controlWidget);
        QHBoxLayout *hBoxForAdditionalControlWidgetItemsHBoxLayout = new QHBoxLayout(d->hBoxForAdditionalControlWidgetItems);
        hBoxForAdditionalControlWidgetItemsHBoxLayout->setContentsMargins(QMargins());
        controlWidgetHBoxLayout->addWidget(d->hBoxForAdditionalControlWidgetItems);

        setVisibleMouseModes(s->visibleMouseModes);
        setVisibleExtraActions(d->visibleExtraActions);

        // add stretch after the controls:

        QHBoxLayout* const hBoxLayout = reinterpret_cast<QHBoxLayout*>(d->controlWidget->layout());

        if (hBoxLayout)
        {
            hBoxLayout->addStretch();
        }
    }

    // make sure the menu exists, even if no backend has been set:

    rebuildConfigurationMenu();

    return d->controlWidget;
}

void MapWidget::slotZoomIn()
{
    if (!currentBackendReady())
    {
        return;
    }

    d->currentBackend->zoomIn();
}

void MapWidget::slotZoomOut()
{
    if (!currentBackendReady())
    {
        return;
    }

    d->currentBackend->zoomOut();
}

void MapWidget::slotUpdateActionsEnabled()
{
    if (!s->activeState)
    {
        // this widget is not active, no need to update the action availability
        return;
    }

    d->actionDecreaseThumbnailSize->setEnabled((s->showThumbnails)&&(s->thumbnailSize>GeoIfaceMinThumbnailSize));

    /// @todo Define an upper limit for the thumbnail size!

    d->actionIncreaseThumbnailSize->setEnabled(s->showThumbnails);

    d->actionSetRegionSelectionMode->setEnabled(s->availableMouseModes.testFlag(MouseModeRegionSelection));

    d->actionSetPanMode->setEnabled(s->availableMouseModes.testFlag(MouseModePan));
    d->actionSetZoomIntoGroupMode->setEnabled(s->availableMouseModes.testFlag(MouseModeZoomIntoGroup));
    d->actionSetRegionSelectionFromIconMode->setEnabled(s->availableMouseModes.testFlag(MouseModeRegionSelectionFromIcon));
    d->actionSetFilterMode->setEnabled(s->availableMouseModes.testFlag(MouseModeFilter));
    d->actionSetSelectThumbnailMode->setEnabled(s->availableMouseModes.testFlag(MouseModeSelectThumbnail));

    // the 'Remove X' actions are only available if the corresponding X is actually there:

    bool clearRegionSelectionAvailable = s->availableMouseModes.testFlag(MouseModeRegionSelection);

    if (clearRegionSelectionAvailable && s->markerModel)
    {
        clearRegionSelectionAvailable = s->markerModel->getGlobalGroupState() & RegionSelectedMask;
    }

    d->actionRemoveCurrentRegionSelection->setEnabled(clearRegionSelectionAvailable);
    bool clearFilterAvailable = s->availableMouseModes.testFlag(MouseModeRegionSelectionFromIcon);

    if (clearFilterAvailable && s->markerModel)
    {
        clearFilterAvailable = s->markerModel->getGlobalGroupState() & FilteredPositiveMask;
    }

    d->actionRemoveFilter->setEnabled(clearFilterAvailable);

    d->actionStickyMode->setEnabled(d->availableExtraActions.testFlag(ExtraActionSticky));

    /// @todo Only set the icons if they have to be changed!

    d->actionStickyMode->setIcon(QIcon::fromTheme(QLatin1String(d->actionStickyMode->isChecked()
                                                                ? "document-encrypted"
                                                                : "document-decrypt")));
    d->actionShowThumbnails->setIcon(d->actionShowThumbnails->isChecked()
                                     ? QIcon::fromTheme(QLatin1String("folder-pictures"))
                                     : GeoIfaceGlobalObject::instance()->getMarkerPixmap(QLatin1String("marker-icon-16x16")));

    // make sure the action for the current mouse mode is checked

    const QList<QAction*> mouseModeActions = d->mouseModeActionGroup->actions();

    foreach (QAction* const action, mouseModeActions)
    {
        if (action->data().value<GeoMouseModes>() == s->currentMouseMode)
        {
            action->setChecked(true);
            break;
        }
    }
}

void MapWidget::slotChangeBackend(QAction* action)
{
    GEOIFACE_ASSERT(action != nullptr);

    if (!action)
    {
        return;
    }

    const QString newBackendName = action->data().toString();
    setBackend(newBackendName);
}

void MapWidget::updateMarkers()
{
    if (!currentBackendReady())
    {
        return;
    }

    // tell the backend to update the markers

    d->currentBackend->updateMarkers();
}

void MapWidget::updateClusters()
{
    /// @todo Find a better way to tell the TileGrouper about the backend

    s->tileGrouper->setCurrentBackend(d->currentBackend);
    s->tileGrouper->updateClusters();
}

void MapWidget::slotClustersNeedUpdating()
{
    if (currentBackendReady())
    {
        d->currentBackend->slotClustersNeedUpdating();
    }
}

/**
 * @brief Return color and style information for rendering the cluster
 * @param clusterIndex Index of the cluster
 * @param fillColor Color used to fill the circle
 * @param strokeColor Color used for the stroke around the circle
 * @param strokeStyle Style used to draw the stroke around the circle
 * @param labelText Text for the label
 * @param labelColor Color for the label text
 * @param overrideSelection Get the colors for a different selection state
 * @param overrideCount Get the colors for a different amount of markers
 */
void MapWidget::getColorInfos(const int clusterIndex,
                              QColor* fillColor,
                              QColor* strokeColor,
                              Qt::PenStyle* strokeStyle,
                              QString* labelText,
                              QColor* labelColor,
                              const GeoGroupState* const overrideSelection,
                              const int* const overrideCount) const
{
    /// @todo Call the new getColorInfos function!

    const GeoIfaceCluster& cluster = s->clusterList.at(clusterIndex);

    /// @todo Check that this number is already valid!

    const int nMarkers             = overrideCount ? *overrideCount : cluster.markerCount;

    getColorInfos(overrideSelection ? *overrideSelection : cluster.groupState,
                  nMarkers,
                  fillColor, strokeColor, strokeStyle, labelText, labelColor);
}

void MapWidget::getColorInfos(const GeoGroupState groupState,
                              const int nMarkers,
                              QColor* fillColor, QColor* strokeColor,
                              Qt::PenStyle* strokeStyle, QString* labelText,
                              QColor* labelColor) const
{
    if      (nMarkers < 1000)
    {
        *labelText = QString::number(nMarkers);
    }
    else if ((nMarkers >= 1000) && (nMarkers <= 1950))
    {
        *labelText = QString::fromLatin1("%L1k").arg(qreal(nMarkers)/1000.0, 0, 'f', 1);
    }
    else if ((nMarkers >= 1951) && (nMarkers < 19500))
    {
        *labelText = QString::fromLatin1("%L1k").arg(qreal(nMarkers)/1000.0, 0, 'f', 0);
    }
    else
    {
        // convert to "1E5" notation for numbers >=20k:

        qreal exponent           = floor(log((qreal)nMarkers)/log((qreal)10));
        qreal nMarkersFirstDigit = round(qreal(nMarkers)/pow(10,exponent));

        if (nMarkersFirstDigit >= 10)
        {
            nMarkersFirstDigit=round(nMarkersFirstDigit/10.0);
            exponent++;
        }

        *labelText = QString::fromLatin1("%1E%2").arg(int(nMarkersFirstDigit)).arg(int(exponent));
    }

    *labelColor  = QColor(Qt::black);
    *strokeStyle = Qt::NoPen;

    /// @todo On my system, digikam uses QColor(67, 172, 232) as the selection color. Or should we just use blue?

    switch (groupState & SelectedMask)
    {
        case SelectedNone:
            *strokeStyle = Qt::SolidLine;
            *strokeColor = QColor(Qt::black);
            break;

        case SelectedSome:
            *strokeStyle = Qt::DotLine;
            *strokeColor = QColor(Qt::blue);//67, 172, 232);
            break;

        case SelectedAll:
            *strokeStyle = Qt::SolidLine;
            *strokeColor = QColor(Qt::blue);//67, 172, 232);
            break;
    }

    /**
     * @todo These are the fill colors for the circles, for cases in which only some or all of the images
     * are positively filtered. Filtering is implemented in GeoIface, but the code here has not been adapted yet.
     */
    QColor fillAll, fillSome, fillNone;

    if      (nMarkers >= 100)
    {
        fillAll  = QColor(255, 0,   0);
        fillSome = QColor(255, 188, 125);
        fillNone = QColor(255, 185, 185);
    }
    else if (nMarkers >= 50)
    {
        fillAll  = QColor(255, 127, 0);
        fillSome = QColor(255, 190, 125);
        fillNone = QColor(255, 220, 185);
    }
    else if (nMarkers >= 10)
    {
        fillAll  = QColor(255, 255, 0);
        fillSome = QColor(255, 255, 105);
        fillNone = QColor(255, 255, 185);
    }
    else if (nMarkers >= 2)
    {
        fillAll  = QColor(0,   255, 0);
        fillSome = QColor(125, 255, 125);
        fillNone = QColor(185, 255, 255);
    }
    else
    {
        fillAll  = QColor(0,   255, 255);
        fillSome = QColor(125, 255, 255);
        fillNone = QColor(185, 255, 255);
    }

    *fillColor = fillAll;
/*
    switch (groupState)
    {
        case PartialAll:
            *fillColor = fillAll;
            break;

        case PartialSome:
            *fillColor = fillSome;
            break;

        case PartialNone:

            if (haveAnySolo)
            {
                *fillColor = fillNone;
            }
            else
            {
                *fillColor = fillAll;
            }

            break;
    }
*/
}

QString MapWidget::convertZoomToBackendZoom(const QString& someZoom,
                                            const QString& targetBackend) const
{
    const QStringList zoomParts = someZoom.split(QLatin1Char( ':' ));
    GEOIFACE_ASSERT(zoomParts.count() == 2);
    const QString sourceBackend = zoomParts.first();

    if (sourceBackend == targetBackend)
    {
        return someZoom;
    }

    const int sourceZoom = zoomParts.last().toInt();
    int targetZoom       = -1;

    // all of these values were found experimentally!

    if (targetBackend == QLatin1String("marble" ))
    {
        if      (sourceZoom == 0) { targetZoom =  900; }
        else if (sourceZoom == 1) { targetZoom =  970; }
        else if (sourceZoom == 2) { targetZoom = 1108; }
        else if (sourceZoom == 3) { targetZoom = 1250; }
        else if (sourceZoom == 4) { targetZoom = 1384; }
        else if (sourceZoom == 5) { targetZoom = 1520; }
        else if (sourceZoom == 6) { targetZoom = 1665; }
        else if (sourceZoom == 7) { targetZoom = 1800; }
        else if (sourceZoom == 8) { targetZoom = 1940; }
        else if (sourceZoom == 9) { targetZoom = 2070; }
        else if (sourceZoom ==10) { targetZoom = 2220; }
        else if (sourceZoom ==11) { targetZoom = 2357; }
        else if (sourceZoom ==12) { targetZoom = 2510; }
        else if (sourceZoom ==13) { targetZoom = 2635; }
        else if (sourceZoom ==14) { targetZoom = 2775; }
        else if (sourceZoom ==15) { targetZoom = 2900; }
        else if (sourceZoom ==16) { targetZoom = 3051; }
        else if (sourceZoom ==17) { targetZoom = 3180; }
        else if (sourceZoom ==18) { targetZoom = 3295; }
        else if (sourceZoom ==19) { targetZoom = 3450; }
        else                      { targetZoom = 3500; } /// @todo Find values for level 20 and up
    }

    if (targetBackend == QLatin1String("googlemaps" ))
    {
        if      (sourceZoom <= 900) { targetZoom =  0; }
        else if (sourceZoom <= 970) { targetZoom =  1; }
        else if (sourceZoom <=1108) { targetZoom =  2; }
        else if (sourceZoom <=1250) { targetZoom =  3; }
        else if (sourceZoom <=1384) { targetZoom =  4; }
        else if (sourceZoom <=1520) { targetZoom =  5; }
        else if (sourceZoom <=1665) { targetZoom =  6; }
        else if (sourceZoom <=1800) { targetZoom =  7; }
        else if (sourceZoom <=1940) { targetZoom =  8; }
        else if (sourceZoom <=2070) { targetZoom =  9; }
        else if (sourceZoom <=2220) { targetZoom = 10; }
        else if (sourceZoom <=2357) { targetZoom = 11; }
        else if (sourceZoom <=2510) { targetZoom = 12; }
        else if (sourceZoom <=2635) { targetZoom = 13; }
        else if (sourceZoom <=2775) { targetZoom = 14; }
        else if (sourceZoom <=2900) { targetZoom = 15; }
        else if (sourceZoom <=3051) { targetZoom = 16; }
        else if (sourceZoom <=3180) { targetZoom = 17; }
        else if (sourceZoom <=3295) { targetZoom = 18; }
        else if (sourceZoom <=3450) { targetZoom = 19; }
        else                        { targetZoom = 20; } /// @todo Find values for level 20 and up
    }

    GEOIFACE_ASSERT(targetZoom >= 0);

    return QString::fromLatin1("%1:%2").arg(targetBackend).arg(targetZoom);
}

void MapWidget::slotBackendZoomChanged(const QString& newZoom)
{
    d->cacheZoom = newZoom;
}

void MapWidget::setZoom(const QString& newZoom)
{
    d->cacheZoom = newZoom;

    if (currentBackendReady())
    {
        d->currentBackend->setZoom(d->cacheZoom);
    }
}

QString MapWidget::getZoom()
{
    if (currentBackendReady())
    {
        d->cacheZoom = d->currentBackend->getZoom();
    }

    return d->cacheZoom;
}

GeoCoordinates::Pair MapWidget::getRegionSelection()
{
    return s->selectionRectangle;
}

void MapWidget::slotClustersMoved(const QIntList& clusterIndices,
                                  const QPair<int, QModelIndex>& snapTarget)
{
    qCDebug(DIGIKAM_GEOIFACE_LOG) << clusterIndices;

    /// @todo We actually expect only one clusterindex

    int             clusterIndex      = clusterIndices.first();
    GeoCoordinates  targetCoordinates = s->clusterList.at(clusterIndex).coordinates;
    TileIndex::List movedTileIndices;

    if (s->clusterList.at(clusterIndex).groupState == SelectedNone)
    {
        // a not-selected marker was moved. update all of its items:

        const GeoIfaceCluster& cluster = s->clusterList.at(clusterIndex);

        for (int i = 0 ; i < cluster.tileIndicesList.count() ; ++i)
        {
            const TileIndex tileIndex = cluster.tileIndicesList.at(i);
            movedTileIndices << tileIndex;
        }
    }
    else
    {
        // selected items were moved. The model helper should know which tiles are selected,
        // therefore we give him an empty list
    }

    s->markerModel->onIndicesMoved(movedTileIndices, targetCoordinates, snapTarget.second);

    /**
     * @todo Clusters are marked as dirty by slotClustersNeedUpdating
     * which is called while we update the model
     */
}

void MapWidget::addUngroupedModel(GeoModelHelper* const modelHelper)
{
    s->ungroupedModels << modelHelper;

    /// @todo monitor all model signals!

    connect(modelHelper->model(), SIGNAL(dataChanged(QModelIndex,QModelIndex)),
            this, SLOT(slotUngroupedModelChanged()));

    connect(modelHelper->model(), SIGNAL(rowsInserted(QModelIndex,int,int)),
            this, SLOT(slotUngroupedModelChanged()));

    connect(modelHelper->model(), SIGNAL(modelReset()),
            this, SLOT(slotUngroupedModelChanged()));

    connect(modelHelper, SIGNAL(signalVisibilityChanged()),
            this, SLOT(slotUngroupedModelChanged()));

    if (modelHelper->selectionModel())
    {
        connect(modelHelper->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)),
                this, SLOT(slotUngroupedModelChanged()));
    }

    emit signalUngroupedModelChanged(s->ungroupedModels.count() - 1);
}

void MapWidget::removeUngroupedModel(GeoModelHelper* const modelHelper)
{
    if (!modelHelper)
    {
        return;
    }

    const int modelIndex = s->ungroupedModels.indexOf(modelHelper);

    if (modelIndex < 0)
    {
        return;
    }

    /// @todo monitor all model signals!

    disconnect(modelHelper->model(), SIGNAL(dataChanged(QModelIndex,QModelIndex)),
               this, SLOT(slotUngroupedModelChanged()));

    disconnect(modelHelper->model(), SIGNAL(rowsInserted(QModelIndex,int,int)),
               this, SLOT(slotUngroupedModelChanged()));

    disconnect(modelHelper->model(), SIGNAL(modelReset()),
               this, SLOT(slotUngroupedModelChanged()));

    disconnect(modelHelper, SIGNAL(signalVisibilityChanged()),
               this, SLOT(slotUngroupedModelChanged()));

    if (modelHelper->selectionModel())
    {
        disconnect(modelHelper->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)),
                   this, SLOT(slotUngroupedModelChanged()));
    }

    s->ungroupedModels.removeAt(modelIndex);

    // the indices changed, therefore send out notifications
    // sending out a signal with i=s->ungroupedModel.count()
    // will cause the backends to see that the last model is missing

    for (int i = modelIndex ; i <= s->ungroupedModels.count() ; ++i)
    {
        emit signalUngroupedModelChanged(i);
    }
}

void MapWidget::setGroupedModel(AbstractMarkerTiler* const markerModel)
{
    s->markerModel = markerModel;

    if (s->markerModel)
    {
        s->markerModel->setActive(s->activeState);

        /// @todo this needs some buffering for the google maps backend

        connect(s->markerModel, SIGNAL(signalTilesOrSelectionChanged()),
                this, SLOT(slotRequestLazyReclustering()));

        if (d->currentBackend)
        {
            connect(s->markerModel, SIGNAL(signalThumbnailAvailableForIndex(QVariant,QPixmap)),
                    d->currentBackend, SLOT(slotThumbnailAvailableForIndex(QVariant,QPixmap)));
        }
    }

    slotRequestLazyReclustering();
}

void MapWidget::setShowThumbnails(const bool state)
{
    s->showThumbnails = state;

    rebuildConfigurationMenu();
    slotUpdateActionsEnabled();
    slotRequestLazyReclustering();
}

void MapWidget::slotShowThumbnailsChanged()
{
    setShowThumbnails(d->actionShowThumbnails->isChecked());
}

/**
 * @brief Request reclustering, repeated calls should generate only one actual update of the clusters
 */
void MapWidget::slotRequestLazyReclustering()
{
    if (d->lazyReclusteringRequested)
    {
        return;
    }

    s->tileGrouper->setClustersDirty();

    if (s->activeState)
    {
        d->lazyReclusteringRequested = true;
        QTimer::singleShot(0, this, SLOT(slotLazyReclusteringRequestCallBack()));
    }
}

/**
 * @brief Helper function to buffer reclustering
 */
void MapWidget::slotLazyReclusteringRequestCallBack()
{
    if (!d->lazyReclusteringRequested)
    {
        return;
    }

    d->lazyReclusteringRequested = false;
    slotClustersNeedUpdating();
}

/**
 * @todo Clicking on several clusters at once is not actually possible
 */
void MapWidget::slotClustersClicked(const QIntList& clusterIndices)
{
    qCDebug(DIGIKAM_GEOIFACE_LOG) << clusterIndices;

    if ((s->currentMouseMode == MouseModeZoomIntoGroup) ||
        (s->currentMouseMode == MouseModeRegionSelectionFromIcon) )
    {
        int maxTileLevel = 0;

        Marble::GeoDataLineString tileString;

        for (int i = 0 ; i < clusterIndices.count() ; ++i)
        {
            const int clusterIndex               = clusterIndices.at(i);
            const GeoIfaceCluster currentCluster = s->clusterList.at(clusterIndex);

            for (int j = 0 ; j < currentCluster.tileIndicesList.count() ; ++j)
            {
                const TileIndex& currentTileIndex = currentCluster.tileIndicesList.at(j);

                for (int corner = 1 ; corner <= 4 ; ++corner)
                {
                    GeoCoordinates currentTileCoordinate;

                    if      (corner == 1)
                    {
                        currentTileCoordinate = currentTileIndex.toCoordinates(TileIndex::CornerNW);
                    }
                    else if (corner == 2)
                    {
                        currentTileCoordinate = currentTileIndex.toCoordinates(TileIndex::CornerSW);
                    }
                    else if (corner == 3)
                    {
                        currentTileCoordinate = currentTileIndex.toCoordinates(TileIndex::CornerNE);
                    }
                    else if (corner == 4)
                    {
                        currentTileCoordinate = currentTileIndex.toCoordinates(TileIndex::CornerSE);
                    }

                    const Marble::GeoDataCoordinates tileCoordinate(currentTileCoordinate.lon(),
                                                                    currentTileCoordinate.lat(),
                                                                    0,
                                                                    Marble::GeoDataCoordinates::Degree);

                    if (maxTileLevel < currentTileIndex.level())
                    {
                        maxTileLevel = currentTileIndex.level();
                    }

                    tileString.append(tileCoordinate);
                }
            }
        }

        Marble::GeoDataLatLonBox latLonBox = Marble::GeoDataLatLonBox::fromLineString(tileString);

        /// @todo Review this section
/*
        if (maxTileLevel != 0)
        {
            //increase the selection boundaries with 0.1 degrees because some thumbnails aren't caught by selection
            latLonBox.setWest((latLonBox.west(Marble::GeoDataCoordinates::Degree)-(0.1/maxTileLevel)), Marble::GeoDataCoordinates::Degree);
            latLonBox.setNorth((latLonBox.north(Marble::GeoDataCoordinates::Degree)+(0.1/maxTileLevel)), Marble::GeoDataCoordinates::Degree);
            latLonBox.setEast((latLonBox.east(Marble::GeoDataCoordinates::Degree)+(0.1/maxTileLevel)), Marble::GeoDataCoordinates::Degree);
            latLonBox.setSouth((latLonBox.south(Marble::GeoDataCoordinates::Degree)-(0.1/maxTileLevel)), Marble::GeoDataCoordinates::Degree);
        }
        else
        {
*/
            latLonBox.setWest((latLonBox.west(Marble::GeoDataCoordinates::Degree)-0.0001), Marble::GeoDataCoordinates::Degree);
            latLonBox.setNorth((latLonBox.north(Marble::GeoDataCoordinates::Degree)+0.0001), Marble::GeoDataCoordinates::Degree);
            latLonBox.setEast((latLonBox.east(Marble::GeoDataCoordinates::Degree)+0.0001), Marble::GeoDataCoordinates::Degree);
            latLonBox.setSouth((latLonBox.south(Marble::GeoDataCoordinates::Degree)-0.0001), Marble::GeoDataCoordinates::Degree);
/*
        }
*/
        if (s->currentMouseMode == MouseModeZoomIntoGroup)
        {
            /// @todo Very small latLonBoxes can crash Marble

            d->currentBackend->centerOn(latLonBox);
        }
        else
        {
            const GeoCoordinates::Pair newSelection(
                    GeoCoordinates(latLonBox.north(Marble::GeoDataCoordinates::Degree),
                                latLonBox.west(Marble::GeoDataCoordinates::Degree)),
                    GeoCoordinates(latLonBox.south(Marble::GeoDataCoordinates::Degree),
                                latLonBox.east(Marble::GeoDataCoordinates::Degree))
                );

            s->selectionRectangle = newSelection;
            d->currentBackend->regionSelectionChanged();
            emit signalRegionSelectionChanged();
        }
    }
    else if ((s->currentMouseMode == MouseModeFilter && s->selectionRectangle.first.hasCoordinates()) ||
             (s->currentMouseMode == MouseModeSelectThumbnail) )
    {
        // update the selection and filtering state of the clusters

        for (int i = 0 ; i < clusterIndices.count() ; ++i)
        {
            const int clusterIndex               = clusterIndices.at(i);
            const GeoIfaceCluster currentCluster = s->clusterList.at(clusterIndex);
            const TileIndex::List tileIndices    = currentCluster.tileIndicesList;

            /// @todo Isn't this cached in the cluster?

            const QVariant representativeIndex   = getClusterRepresentativeMarker(clusterIndex, s->sortKey);

            AbstractMarkerTiler::ClickInfo clickInfo;
            clickInfo.tileIndicesList            = tileIndices;
            clickInfo.representativeIndex        = representativeIndex;
            clickInfo.groupSelectionState        = currentCluster.groupState;
            clickInfo.currentMouseMode           = s->currentMouseMode;
            s->markerModel->onIndicesClicked(clickInfo);
        }
    }
}

void MapWidget::dragEnterEvent(QDragEnterEvent* event)
{
    /// @todo ignore drops if no marker tiler or model can accept them

    if (!d->dragDropHandler)
    {
        event->ignore();
        return;
    }

    if (d->dragDropHandler->accepts(event) == Qt::IgnoreAction)
    {
        event->ignore();
        return;
    }

    /// @todo need data about the dragged object: #markers, selected, icon, ...

    event->accept();
}

void MapWidget::dragMoveEvent(QDragMoveEvent* event)
{
    if (!d->dragDropHandler)
    {
        event->ignore();
        return;
    }

    event->accept();
}

void MapWidget::dropEvent(QDropEvent* event)
{
    // remove the drag marker:
/*
    d->currentBackend->updateDragDropMarker(QPoint(), 0);
*/
    if (!d->dragDropHandler)
    {
        event->ignore();
        return;
    }

    GeoCoordinates dropCoordinates;

    if (!d->currentBackend->geoCoordinates(event->pos(), &dropCoordinates))
    {
        return;
    }

    // the drag and drop handler handled the drop if it returned true here

    if (d->dragDropHandler->dropEvent(event, dropCoordinates))
    {
        event->acceptProposedAction();
    }
}

void MapWidget::dragLeaveEvent(QDragLeaveEvent* event)
{
    Q_UNUSED(event);

    // remove the marker:
/*
    d->currentBackend->updateDragDropMarker(QPoint(), 0);
*/
}

void MapWidget::markClustersAsDirty()
{
    s->tileGrouper->setClustersDirty();
}

void MapWidget::setDragDropHandler(GeoDragDropHandler* const dragDropHandler)
{
    d->dragDropHandler = dragDropHandler;
}

QVariant MapWidget::getClusterRepresentativeMarker(const int clusterIndex, const int sortKey)
{
    if (!s->markerModel)
    {
        return QVariant();
    }

    const GeoIfaceCluster cluster          = s->clusterList.at(clusterIndex);
    QMap<int, QVariant>::const_iterator it = cluster.representativeMarkers.find(sortKey);

    if (it != cluster.representativeMarkers.end())
    {
        return *it;
    }

    QList<QVariant> repIndices;

    for (int i = 0 ; i < cluster.tileIndicesList.count() ; ++i)
    {
        repIndices << s->markerModel->getTileRepresentativeMarker(cluster.tileIndicesList.at(i), sortKey);
    }

    const QVariant clusterRepresentative                        = s->markerModel->bestRepresentativeIndexFromList(repIndices, sortKey);
    s->clusterList[clusterIndex].representativeMarkers[sortKey] = clusterRepresentative;

    return clusterRepresentative;
}

void MapWidget::slotItemDisplaySettingsChanged()
{
    s->previewSingleItems  = d->actionPreviewSingleItems->isChecked();
    s->previewGroupedItems = d->actionPreviewGroupedItems->isChecked();
    s->showNumbersOnItems  = d->actionShowNumbersOnItems->isChecked();

    /// @todo Update action availability?

    /// @todo We just need to update the display, no need to recluster?

    slotRequestLazyReclustering();
}

void MapWidget::setSortOptionsMenu(QMenu* const sortMenu)
{
    d->sortMenu = sortMenu;

    rebuildConfigurationMenu();
}

void MapWidget::setSortKey(const int sortKey)
{
    s->sortKey = sortKey;

    // this is probably faster than writing a function that changes all the clusters icons...
    /// @todo We just need to update the display, no need to recluster?

    slotRequestLazyReclustering();
}

QPixmap MapWidget::getDecoratedPixmapForCluster(const int clusterId,
                                                const GeoGroupState* const selectedStateOverride,
                                                const int* const countOverride,
                                                QPoint* const centerPoint)
{
    GeoIfaceCluster& cluster = s->clusterList[clusterId];
    int markerCount          = cluster.markerCount;
    GeoGroupState groupState = cluster.groupState;

    if (selectedStateOverride)
    {
        groupState  = *selectedStateOverride;
        markerCount = *countOverride;
    }

    const GeoGroupState selectedState = groupState & SelectedMask;

    // first determine all the color and style values

    QColor       fillColor;
    QColor       strokeColor;
    Qt::PenStyle strokeStyle;
    QColor       labelColor;
    QString      labelText;
    getColorInfos(clusterId, &fillColor, &strokeColor,
                  &strokeStyle, &labelText, &labelColor,
                  &selectedState,
                  &markerCount);

    // determine whether we should use a pixmap or a placeholder

    if (!s->showThumbnails)
    {
        /// @todo Handle positive filtering and region selection!

        QString pixmapName = fillColor.name().mid(1);

        if (selectedState == SelectedAll)
        {
            pixmapName += QLatin1String("-selected");
        }

        if (selectedState == SelectedSome)
        {
            pixmapName += QLatin1String("-someselected");
        }

        const QPixmap& markerPixmap = GeoIfaceGlobalObject::instance()->getMarkerPixmap(pixmapName);

        // update the display information stored in the cluster:

        cluster.pixmapType          = GeoIfaceCluster::PixmapMarker;
        cluster.pixmapOffset        = QPoint(markerPixmap.width()/2, markerPixmap.height()-1);
        cluster.pixmapSize          = markerPixmap.size();

        if (centerPoint)
        {
            *centerPoint = cluster.pixmapOffset;
        }

        return markerPixmap;
    }

    /// @todo This check is strange, there can be no clusters without a markerModel?

    bool displayThumbnail = (s->markerModel != nullptr);

    if (displayThumbnail)
    {
        if (markerCount==1)
        {
            displayThumbnail = s->previewSingleItems;
        }
        else
        {
            displayThumbnail = s->previewGroupedItems;
        }
    }

    if (displayThumbnail)
    {
        const QVariant representativeMarker = getClusterRepresentativeMarker(clusterId, s->sortKey);
        const int undecoratedThumbnailSize  = getUndecoratedThumbnailSize();
        QPixmap clusterPixmap               = s->markerModel->pixmapFromRepresentativeIndex(representativeMarker,
                                                                                            QSize(undecoratedThumbnailSize,
                                                                                                  undecoratedThumbnailSize));

        if (!clusterPixmap.isNull())
        {
            QPixmap resultPixmap(clusterPixmap.size() + QSize(2, 2));

            // we may draw with partially transparent pixmaps later, so make sure we have a defined
            // background color

            resultPixmap.fill(QColor::fromRgb(0xff, 0xff, 0xff));
            QPainter painter(&resultPixmap);
/*
             painter.setRenderHint(QPainter::Antialiasing);
*/
            const int borderWidth = (groupState&SelectedSome) ? 2 : 1;
            QPen borderPen;
            borderPen.setWidth(borderWidth);
            borderPen.setJoinStyle(Qt::MiterJoin);

            GeoGroupState globalState = s->markerModel->getGlobalGroupState();

            /// @todo What about partially in the region or positively filtered?

            const bool clusterIsNotInRegionSelection  = (globalState & RegionSelectedMask) &&
                                                        ((groupState & RegionSelectedMask) == RegionSelectedNone);
            const bool clusterIsNotPositivelyFiltered = (globalState & FilteredPositiveMask) &&
                                                        ((groupState & FilteredPositiveMask) == FilteredPositiveNone);

            const bool shouldGrayOut                  = clusterIsNotInRegionSelection || clusterIsNotPositivelyFiltered;
            const bool shouldCrossOut                 = clusterIsNotInRegionSelection;

            if (shouldGrayOut)
            {
                /// @todo Cache the alphaPixmap!

                QPixmap alphaPixmap(clusterPixmap.size());
                alphaPixmap.fill(QColor::fromRgb(0x80, 0x80, 0x80));
/*
                NOTE : old Qt4 code ported to Qt5 due to deprecated QPixmap::setAlphaChannel()
                clusterPixmap.setAlphaChannel(alphaPixmap);
*/
                QPainter p(&clusterPixmap);
                p.setOpacity(0.2);
                p.drawPixmap(0, 0, alphaPixmap);
                p.end();
            }

            painter.drawPixmap(QPoint(1, 1), clusterPixmap);

            if (shouldGrayOut || shouldCrossOut)
            {
                // draw a red cross above the pixmap

                QPen crossPen(Qt::red);

                if (!shouldCrossOut)
                {
                    /// @todo Maybe we should also do a cross for not positively filtered images?

                    crossPen.setColor(Qt::blue);
                }

                crossPen.setWidth(2);
                painter.setPen(crossPen);

                const int width  = resultPixmap.size().width();
                const int height = resultPixmap.size().height();
                painter.drawLine(0, 0, width-1, height-1);
                painter.drawLine(width-1, 0, 0, height-1);
            }

            if (strokeStyle != Qt::SolidLine)
            {
                // paint a white border around the image

                borderPen.setColor(Qt::white);
                painter.setPen(borderPen);
                painter.drawRect(borderWidth-1, borderWidth-1,
                                 resultPixmap.size().width()-borderWidth,
                                 resultPixmap.size().height()-borderWidth);
            }

            // now draw the selection border

            borderPen.setColor(strokeColor);
            borderPen.setStyle(strokeStyle);
            painter.setPen(borderPen);
            painter.drawRect(borderWidth-1, borderWidth-1,
                             resultPixmap.size().width()-borderWidth,
                             resultPixmap.size().height()-borderWidth);

            if (s->showNumbersOnItems)
            {
                QPen labelPen(labelColor);

                // note: the pen has to be set, otherwise the bounding rect is 0 x 0!!!

                painter.setPen(labelPen);
                const QRect textRect(0, 0, resultPixmap.width(), resultPixmap.height());
                QRect textBoundingRect = painter.boundingRect(textRect,
                                                              Qt::AlignHCenter | Qt::AlignVCenter,
                                                              labelText);
                textBoundingRect.adjust(-1, -1, 1, 1);

                // fill the bounding rect:

                painter.setPen(Qt::NoPen);
                painter.setBrush(QColor::fromRgb(0xff, 0xff, 0xff, 0x80));
                painter.drawRect(textBoundingRect);

                // draw the text:

                painter.setPen(labelPen);
                painter.setBrush(Qt::NoBrush);
                painter.drawText(textRect, Qt::AlignHCenter|Qt::AlignVCenter, labelText);
            }

            // update the display information stored in the cluster:

            cluster.pixmapType   = GeoIfaceCluster::PixmapImage;
            cluster.pixmapOffset = QPoint(resultPixmap.width()/2, resultPixmap.height()/2);
            cluster.pixmapSize   = resultPixmap.size();

            if (centerPoint)
            {
                *centerPoint = cluster.pixmapOffset;
            }

            return resultPixmap;
        }
    }

    // we do not have a thumbnail, draw the circle instead:

    const int circleRadius = s->thumbnailSize/2;
    QPen circlePen;
    circlePen.setColor(strokeColor);
    circlePen.setStyle(strokeStyle);
    circlePen.setWidth(2);
    QBrush circleBrush(fillColor);
    QPen labelPen;
    labelPen.setColor(labelColor);
    const QRect circleRect(0, 0, 2*circleRadius, 2*circleRadius);

    const int pixmapDiameter = 2*(circleRadius+1);
    QPixmap circlePixmap(pixmapDiameter, pixmapDiameter);

    /// @todo cache this somehow

    circlePixmap.fill(QColor(0, 0, 0, 0));

    QPainter circlePainter(&circlePixmap);
    circlePainter.setPen(circlePen);
    circlePainter.setBrush(circleBrush);
    circlePainter.drawEllipse(circleRect);

    circlePainter.setPen(labelPen);
    circlePainter.setBrush(Qt::NoBrush);
    circlePainter.drawText(circleRect, Qt::AlignHCenter|Qt::AlignVCenter, labelText);

    // update the display information stored in the cluster:

    cluster.pixmapType   = GeoIfaceCluster::PixmapCircle;
    cluster.pixmapOffset = QPoint(circlePixmap.width()/2, circlePixmap.height()/2);
    cluster.pixmapSize   = circlePixmap.size();

    if (centerPoint)
    {
        *centerPoint = QPoint(circlePixmap.width()/2, circlePixmap.height()/2);
    }

    return circlePixmap;
}

void MapWidget::setThumnailSize(const int newThumbnailSize)
{
    s->thumbnailSize = qMax(GeoIfaceMinThumbnailSize, newThumbnailSize);

    // make sure the grouping radius is larger than the thumbnail size

    if (2*s->thumbnailGroupingRadius < newThumbnailSize)
    {
        /// @todo more straightforward way for this?

        s->thumbnailGroupingRadius = newThumbnailSize/2 + newThumbnailSize%2;
    }

    if (s->showThumbnails)
    {
        slotRequestLazyReclustering();
    }

    slotUpdateActionsEnabled();
}

void MapWidget::setThumbnailGroupingRadius(const int newGroupingRadius)
{
    s->thumbnailGroupingRadius = qMax(GeoIfaceMinThumbnailGroupingRadius, newGroupingRadius);

    // make sure the thumbnails are smaller than the grouping radius

    if (2*s->thumbnailGroupingRadius < s->thumbnailSize)
    {
        s->thumbnailSize = 2*newGroupingRadius;
    }

    if (s->showThumbnails)
    {
        slotRequestLazyReclustering();
    }

    slotUpdateActionsEnabled();
}

void MapWidget::setMarkerGroupingRadius(const int newGroupingRadius)
{
    s->markerGroupingRadius = qMax(GeoIfaceMinMarkerGroupingRadius, newGroupingRadius);

    if (!s->showThumbnails)
    {
        slotRequestLazyReclustering();
    }

    slotUpdateActionsEnabled();
}

void MapWidget::slotDecreaseThumbnailSize()
{
    if (!s->showThumbnails)
    {
        return;
    }

    if (s->thumbnailSize>GeoIfaceMinThumbnailSize)
    {
        const int newThumbnailSize = qMax(GeoIfaceMinThumbnailSize, s->thumbnailSize-5);

        // make sure the grouping radius is also decreased
        // this will automatically decrease the thumbnail size as well

        setThumbnailGroupingRadius(newThumbnailSize/2);
    }
}

void MapWidget::slotIncreaseThumbnailSize()
{
    if (!s->showThumbnails)
    {
        return;
    }

    setThumnailSize(s->thumbnailSize+5);
}

int MapWidget::getThumbnailSize() const
{
    return s->thumbnailSize;
}

int MapWidget::getUndecoratedThumbnailSize() const
{
    return s->thumbnailSize-2;
}

void MapWidget::setRegionSelection(const GeoCoordinates::Pair& region)
{
    s->selectionRectangle = region;

    d->currentBackend->regionSelectionChanged();

    slotUpdateActionsEnabled();
}

void MapWidget::clearRegionSelection()
{
    s->selectionRectangle.first.clear();

    d->currentBackend->regionSelectionChanged();

    slotUpdateActionsEnabled();
}

void MapWidget::slotNewSelectionFromMap(const Digikam::GeoCoordinates::Pair& sel)
{
    /// @todo Should the backend update s on its own?

    s->selectionRectangle = sel;

    slotUpdateActionsEnabled();

    emit signalRegionSelectionChanged();
}

void MapWidget::slotRemoveCurrentRegionSelection()
{
    clearRegionSelection();
    d->currentBackend->regionSelectionChanged();

    slotUpdateActionsEnabled();

    emit signalRegionSelectionChanged();
}

void MapWidget::slotUngroupedModelChanged()
{
    // determine the index under which we handle this model

    QObject* const senderObject           = sender();
    QAbstractItemModel* const senderModel = qobject_cast<QAbstractItemModel*>(senderObject);

    if (senderModel)
    {
        for (int i = 0 ; i < s->ungroupedModels.count() ; ++i)
        {
            if (s->ungroupedModels.at(i)->model() == senderModel)
            {
                emit signalUngroupedModelChanged(i);

                break;
            }
        }
        return;
    }

    GeoModelHelper* const senderHelper = qobject_cast<GeoModelHelper*>(senderObject);

    if (senderHelper)
    {
        for (int i = 0 ; i < s->ungroupedModels.count() ; ++i)
        {
            if (s->ungroupedModels.at(i) == senderHelper)
            {
                emit signalUngroupedModelChanged(i);

                break;
            }
        }
    }

    QItemSelectionModel* const senderSelectionModel = qobject_cast<QItemSelectionModel*>(senderObject);

    if (senderSelectionModel)
    {
        for (int i = 0 ; i < s->ungroupedModels.count() ; ++i)
        {
            if (s->ungroupedModels.at(i)->selectionModel()==senderSelectionModel)
            {
                emit signalUngroupedModelChanged(i);

                break;
            }
        }

        return;
    }
}

void MapWidget::addWidgetToControlWidget(QWidget* const newWidget)
{
    // make sure the control widget exists

    if (!d->controlWidget)
    {
        getControlWidget();
    }

    QHBoxLayout* const hBoxLayout = reinterpret_cast<QHBoxLayout*>(d->hBoxForAdditionalControlWidgetItems->layout());

    if (hBoxLayout)
    {
        hBoxLayout->addWidget(newWidget);
    }
}

// Static methods ---------------------------------------------------------

QString MapWidget::MarbleWidgetVersion()
{
    return QString(Marble::MARBLE_VERSION_STRING).section(QLatin1Char(' '), 0, 0);
}

void MapWidget::setActive(const bool state)
{
    const bool oldState = s->activeState;
    s->activeState      = state;

    if (d->currentBackend)
    {
        d->currentBackend->setActive(state);
    }

    if (s->markerModel)
    {
        s->markerModel->setActive(state);
    }

    if (state)
    {
        // do we have a map widget shown?

        if ((d->stackedLayout->count() == 1) && d->currentBackend)
        {
            setMapWidgetInFrame(d->currentBackend->mapWidget());

            // call this slot manually in case the backend was ready right away:

            if (d->currentBackend->isReady())
            {
                slotBackendReadyChanged(d->currentBackendName);
            }
            else
            {
                rebuildConfigurationMenu();
            }
        }
    }

    if (state && !oldState && s->tileGrouper->getClustersDirty())
    {
        slotRequestLazyReclustering();
    }
}

bool MapWidget::getActiveState()
{
    return s->activeState;
}

void MapWidget::setVisibleMouseModes(const GeoMouseModes mouseModes)
{
    s->visibleMouseModes = mouseModes;

    if (d->mouseModesHolder)
    {
        d->mouseModesHolder->setVisible(s->visibleMouseModes);
        d->setSelectionModeButton->setVisible(s->visibleMouseModes.testFlag(MouseModeRegionSelection));
        d->removeCurrentSelectionButton->setVisible(s->visibleMouseModes.testFlag(MouseModeRegionSelection));
        d->setPanModeButton->setVisible(s->visibleMouseModes.testFlag(MouseModePan));
        d->setZoomModeButton->setVisible(s->visibleMouseModes.testFlag(MouseModeZoomIntoGroup));
        d->setRegionSelectionFromIconModeButton->setVisible(s->visibleMouseModes.testFlag(MouseModeRegionSelectionFromIcon));
        d->setFilterModeButton->setVisible(s->visibleMouseModes.testFlag(MouseModeFilter));
        d->removeFilterModeButton->setVisible(s->visibleMouseModes.testFlag(MouseModeFilter));
        d->setSelectThumbnailMode->setVisible(s->visibleMouseModes.testFlag(MouseModeSelectThumbnail));
    }
}

void MapWidget::setAvailableMouseModes(const GeoMouseModes mouseModes)
{
    s->availableMouseModes = mouseModes;
}

bool MapWidget::getStickyModeState() const
{
    return d->actionStickyMode->isChecked();
}

void MapWidget::setStickyModeState(const bool state)
{
    d->actionStickyMode->setChecked(state);

    slotUpdateActionsEnabled();
}

void MapWidget::setVisibleExtraActions(const GeoExtraActions actions)
{
    d->visibleExtraActions = actions;

    if (d->buttonStickyMode)
    {
        d->buttonStickyMode->setVisible(actions.testFlag(ExtraActionSticky));
    }

    slotUpdateActionsEnabled();
}

void MapWidget::setEnabledExtraActions(const GeoExtraActions actions)
{
    d->availableExtraActions = actions;

    slotUpdateActionsEnabled();
}

void MapWidget::slotStickyModeChanged()
{
    slotUpdateActionsEnabled();

    emit signalStickyModeChanged();
}

void MapWidget::setAllowModifications(const bool state)
{
    s->modificationsAllowed = state;

    slotUpdateActionsEnabled();
    slotRequestLazyReclustering();
}

/**
 * @brief Adjusts the visible map area such that all grouped markers are visible.
 *
 * Note that a call to this function currently has no effect if the widget has been
 * set inactive via setActive() or the backend is not yet ready.
 *
 * @param useSaneZoomLevel Stop zooming at a sane level, if markers are too close together.
 */
void MapWidget::adjustBoundariesToGroupedMarkers(const bool useSaneZoomLevel)
{
    if ((!s->activeState) || (!s->markerModel) || (!currentBackendReady()))
    {
        return;
    }

    Marble::GeoDataLineString tileString;

    /// @todo not sure that this is the best way to find the bounding box of all items

    for (AbstractMarkerTiler::NonEmptyIterator tileIterator(s->markerModel, TileIndex::MaxLevel);
         !tileIterator.atEnd();
         tileIterator.nextIndex())
    {
        const TileIndex tileIndex = tileIterator.currentIndex();

        for (int corner = 1 ; corner <= 4 ; ++corner)
        {
            const GeoCoordinates currentTileCoordinate = tileIndex.toCoordinates(TileIndex::CornerPosition(corner));

            const Marble::GeoDataCoordinates tileCoordinate(currentTileCoordinate.lon(),
                                                            currentTileCoordinate.lat(),
                                                            0,
                                                            Marble::GeoDataCoordinates::Degree);

            tileString.append(tileCoordinate);
        }
    }

    const Marble::GeoDataLatLonBox latLonBox = Marble::GeoDataLatLonBox::fromLineString(tileString);

    /// @todo use a sane zoom level

    d->currentBackend->centerOn(latLonBox, useSaneZoomLevel);
}

void MapWidget::refreshMap()
{
    slotRequestLazyReclustering();
}

void MapWidget::setShowPlaceholderWidget(const bool state)
{
    if (state)
    {
        d->stackedLayout->setCurrentIndex(0);
    }
    else
    {
        if (d->stackedLayout->count() > 1)
        {
            d->stackedLayout->setCurrentIndex(1);
        }
    }
}

/**
 * @brief Set @p widgetForFrame as the widget in the frame, but does not show it.
 */
void MapWidget::setMapWidgetInFrame(QWidget* const widgetForFrame)
{
    if (d->stackedLayout->count() > 1)
    {
        // widget 0 is the status widget, widget 1 is the map widget

        if (d->stackedLayout->widget(1) == widgetForFrame)
        {
            return;
        }

        // there is some other widget at the target position.
        // remove it and add our widget instead

        d->stackedLayout->removeWidget(d->stackedLayout->widget(1));
    }

    d->stackedLayout->addWidget(widgetForFrame);
}

void MapWidget::removeMapWidgetFromFrame()
{
    if (d->stackedLayout->count() > 1)
    {
        d->stackedLayout->removeWidget(d->stackedLayout->widget(1));
    }

    d->stackedLayout->setCurrentIndex(0);
}

void MapWidget::slotMouseModeChanged(QAction* triggeredAction)
{
    // determine the new mouse mode:

    const QVariant triggeredActionData = triggeredAction->data();
    const GeoMouseModes newMouseMode   = triggeredActionData.value<Digikam::GeoMouseModes>();

    if (newMouseMode == s->currentMouseMode)
    {
        return;
    }

    // store the new mouse mode:

    s->currentMouseMode = newMouseMode;

    if (d->currentBackend)
    {
        d->currentBackend->mouseModeChanged();
    }

    emit signalMouseModeChanged(s->currentMouseMode);

    /// @todo Update action availability?
}

bool MapWidget::currentBackendReady() const
{
    if (!d->currentBackend)
    {
        return false;
    }

    return d->currentBackend->isReady();
}

void MapWidget::setMouseMode(const GeoMouseModes mouseMode)
{
    s->currentMouseMode = mouseMode;

    if (currentBackendReady())
    {
        d->currentBackend->mouseModeChanged();
    }

    slotUpdateActionsEnabled();
}

void MapWidget::setTrackManager(TrackManager* const trackManager)
{
    s->trackManager = trackManager;

    // Some backends track the track manager activity even when not active
    // therefore they have to be notified.

    foreach (MapBackend* const backend, d->loadedBackends)
    {
        backend->slotTrackManagerChanged();
    }
}

} // namespace Digikam
