pythonpyqtqmlpyqt5qtlocation

cannot add MapCircles to QML Map on MouseClick


I'm trying to create some markers that will be moving dynamically on a QML map. Before that, however, I need to plot them all on the map using the mouse so that I can update them later. I have been using pyqtProperty to send the coordinates I need to QML but when I try to add them to a MapItemView, they are undefined. The following code demonstrates what I am hoping to accomplish. Is the problem that I am passing a pyqtProperty to QML from another python object that was not added with setContextProperty() like in main.py? Or am I using the MapItemView delegation incorrectly?

main.qml

import QtQuick 2.7
import QtQml 2.5
import QtQuick.Controls 1.3
import QtQuick.Controls.Styles 1.3
import QtQuick.Window 2.2
import QtQuick.Layouts 1.2
import QtPositioning 5.9
import QtLocation 5.3
import QtQuick.Dialogs 1.1

ApplicationWindow {
    id: root
    width: 640
    height: 480
    visible: true

    ListModel {
        id: markers
    }

    Plugin {
        id: mapPlugin
        name: "osm" //"mapboxgl" "osm" "esri"
    }

    Map {
        id: map
        anchors.fill: parent
        plugin: mapPlugin
        center: atc.location
        zoomLevel: 14

        MapItemView {
            model: markers
            delegate: MapCircle {
                radius: 50
                color: 'red'
                center: markLocation //issue here? 
            }
        }

        MapCircle {
            id: home
            center: atc.location
            radius: 40
            color: 'white'
        }

        MouseArea {
            id: mousearea
            anchors.fill: map
            acceptedButtons: Qt.LeftButton | Qt.RightButton
            hoverEnabled: true
            property var coord: map.toCoordinate(Qt.point(mouseX, mouseY))

            onDoubleClicked: {
                if (mouse.button === Qt.LeftButton)
                {
                    //Capture information for model here
                    atc.plot_mark(
                        "marker",
                        mousearea.coord.latitude,
                        mousearea.coord.longitude)
                    markers.append({
                        name: "markers",
                        markLocation: atc.get_marker_center("markers")
                    })
                }
            }
        }
    }
}

atc.py

import geocoder
from DC import DC
from PyQt5.QtPositioning import QGeoCoordinate
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot

class ATC(QObject):
    #pyqt Signals
    locationChanged = pyqtSignal(QGeoCoordinate)

    def __init__(self, parent=None):
        super().__init__(parent)
        self._location = QGeoCoordinate()
        self.dcs = {}
        g = geocoder.ip('me')
        self.set_location(QGeoCoordinate(*g.latlng))


    def set_location(self, coordinate):
        if self._location != coordinate:
            self._location = coordinate
            self.locationChanged.emit(self._location)


    def get_location(self):
        return self._location


    #pyqt Property
    location = pyqtProperty(QGeoCoordinate,
        fget=get_location,
        fset=set_location,
        notify=locationChanged)


    @pyqtSlot(str, str, str)
    def plot_mark(self, mark_name, lat, lng):
            dc = DC(mark_name)
            self.dcs[mark_name] = dc
            self.dcs[mark_name].set_location(
                QGeoCoordinate(float(lat), float(lng)))


    @pyqtSlot(str)
    def get_marker_center(self, mark_name):
        return self.dcs[mark_name].location

DC.py

from PyQt5.QtPositioning import QGeoCoordinate
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot

class DC(QObject):
    #pyqt Signals
    locationChanged = pyqtSignal(QGeoCoordinate)

    def __init__(self, name, parent=None):
        super().__init__(parent)
        self.name = name
        self._location = QGeoCoordinate()        


    def set_location(self, coordinate):
        if self._location != coordinate:
            self._location = coordinate
            self.locationChanged.emit(self._location)


    def get_location(self):
        return self._location


    #pyqt Property
    location = pyqtProperty(QGeoCoordinate,
        fget=get_location,
        fset=set_location,
        notify=locationChanged)

main.py

from PyQt5.QtGui import QGuiApplication
from PyQt5.QtQml import QQmlApplicationEngine
from PyQt5.QtCore import QObject, QUrl, pyqtSignal, pyqtProperty
from PyQt5.QtPositioning import QGeoCoordinate

from ATC import ATC

if __name__ == "__main__":
    import os
    import sys

    app = QGuiApplication(sys.argv)

    engine = QQmlApplicationEngine()

    atc = ATC()

    engine.rootContext().setContextProperty("atc", atc)

    qml_path = os.path.join(os.path.dirname(__file__), "main.qml")
    engine.load(QUrl.fromLocalFile(qml_path))

    if not engine.rootObjects():
        sys.exit(-1)

    engine.quit.connect(app.quit)
    sys.exit(app.exec_())

Solution

  • Instead of creating a model in QML you must create it in python to be able to handle it directly for it you must inherit from QAbstractListModel. For a smooth movement you should use QxxxAnimation as QPropertyAnimation.

    In the following example, each time a marker is inserted, the on_markersInserted function will be called, which will move the marker towards the center of the window.

    main.py

    from functools import partial
    from PyQt5 import QtCore, QtGui, QtQml, QtPositioning
    import geocoder
    
    class Marker(QtCore.QObject):
        locationChanged = QtCore.pyqtSignal(QtPositioning.QGeoCoordinate)
    
        def __init__(self, location=QtPositioning.QGeoCoordinate(), parent=None):
            super().__init__(parent)
            self._location = location
    
        def set_location(self, coordinate):
            if self._location != coordinate:
                self._location = coordinate
                self.locationChanged.emit(self._location)
    
        def get_location(self):
            return self._location
    
        location = QtCore.pyqtProperty(QtPositioning.QGeoCoordinate,
            fget=get_location,
            fset=set_location,
            notify=locationChanged)
    
        def move(self, location, duration=1000):
            animation = QtCore.QPropertyAnimation(
                targetObject=self, 
                propertyName=b'location',
                startValue=self.get_location(),
                endValue=location,
                duration=duration,
                parent=self
            )
            animation.start(QtCore.QAbstractAnimation.DeleteWhenStopped)
    
        def moveFromTo(self, start, end, duration=1000):
            self.set_location(start)
            self.move(end, duration)
    
    class MarkerModel(QtCore.QAbstractListModel):
        markersInserted = QtCore.pyqtSignal(list)
    
        PositionRole = QtCore.Qt.UserRole + 1000
    
        def __init__(self, parent=None):
            super().__init__(parent)
            self._markers = []
            self.rowsInserted.connect(self.on_rowsInserted)
    
        def rowCount(self, parent=QtCore.QModelIndex()):
            return 0 if parent.isValid() else len(self._markers)
    
        def data(self, index, role=QtCore.Qt.DisplayRole):
            if index.isValid() and 0 <= index.row() < self.rowCount():
                if role == MarkerModel.PositionRole:
                    return self._markers[index.row()].get_location()
            return QtCore.QVariant()
    
        def roleNames(self):
            roles = {}
            roles[MarkerModel.PositionRole] = b'position'
            return roles
    
        @QtCore.pyqtSlot(QtPositioning.QGeoCoordinate)
        def appendMarker(self, coordinate):
            self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
            marker = Marker(coordinate)
            self._markers.append(marker)
            self.endInsertRows()
            marker.locationChanged.connect(self.update_model)
    
        def update_model(self):
            marker = self.sender()
            try:
                row = self._markers.index(marker)
                ix = self.index(row)
                self.dataChanged.emit(ix, ix, (MarkerModel.PositionRole,))
            except ValueError as e:
                pass
    
        @QtCore.pyqtSlot(QtCore.QModelIndex, int, int)
        def on_rowsInserted(self, parent, first, end):
            markers = []
            for row in range(first, end+1):
                markers.append(self.get_marker(row))
            self.markersInserted.emit(markers)
    
    
        def get_marker(self, row):
            if 0 <= row < self.rowCount():
                return self._markers[row]
    
    class ManagerMarkers(QtCore.QObject):
        locationChanged = QtCore.pyqtSignal(QtPositioning.QGeoCoordinate)
    
        def __init__(self, parent=None):
            super().__init__(parent)
            self._center= Marker(parent=self)
            self._model = MarkerModel(self)  
            g = geocoder.ip('me')
            self.moveCenter(QtPositioning.QGeoCoordinate(*g.latlng)) 
    
        def model(self):
            return self._model     
    
        def center(self):
            return self._center
    
        def moveCenter(self, position):
            self._center.set_location(position)
    
        center = QtCore.pyqtProperty(QtCore.QObject, fget=center, constant=True)
        model = QtCore.pyqtProperty(QtCore.QObject, fget=model, constant=True)
    
    
    # testing
    # When a marker is added
    # it will begin to move toward
    # the center of the window
    def on_markersInserted(manager, markers):
        end = manager.center.get_location()
        for marker in markers:
            marker.move(end, 5*1000)
    
    if __name__ == "__main__":
        import os
        import sys
    
        app = QtGui.QGuiApplication(sys.argv)
        manager = ManagerMarkers()
        engine = QtQml.QQmlApplicationEngine()
        engine.rootContext().setContextProperty("manager", manager)
        qml_path = os.path.join(os.path.dirname(__file__), "main.qml")
        engine.load(QtCore.QUrl.fromLocalFile(qml_path))
        if not engine.rootObjects():
            sys.exit(-1)
        manager.model.markersInserted.connect(partial(on_markersInserted, manager))
        engine.quit.connect(app.quit)
        sys.exit(app.exec_())
    

    main.qml

    import QtQuick 2.7
    import QtQuick.Controls 2.2
    import QtPositioning 5.9
    import QtLocation 5.3
    
    ApplicationWindow {
        id: root
        width: 640
        height: 480
        visible: true
    
        Plugin {
            id: mapPlugin
            name: "osm" // "mapboxgl" "osm" "esri"
        }
    
        Map {
            id: map
            anchors.fill: parent
            plugin: mapPlugin
            center: manager.center.location
            zoomLevel: 14
    
            MapCircle {
                id: home
                center: manager.center.location
                radius: 40
                color: 'white'
            }
    
            MapItemView {
                model: manager.model
                delegate: MapCircle {
                    radius: 50
                    color: 'red'
                    center: model.position
                }
            }
    
            MouseArea {
                id: mousearea
                anchors.fill: map
                acceptedButtons: Qt.LeftButton | Qt.RightButton
                onDoubleClicked: if (mouse.button === Qt.LeftButton)  
                                    manager.model.appendMarker(map.toCoordinate(Qt.point(mouseX, mouseY)))
            }
        }
    }