1

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_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Your question is not very descriptive, could you give more detail of what you want? Do you want to add a marker when you double click on the map? – eyllanesc Mar 21 '19 at 22:46
  • What is the purpose of plot_mark?, What is self.dcs for? – eyllanesc Mar 21 '19 at 22:48
  • Hi. Yes, when I double click the map, I want to display a MapCircle on the Map. Later on, I am hoping to be able to move the MapCircles I have plotted around in real time. ATC.py contains a number of DC.py points (self.dcs) I wish to plot. `plot_mark()` sends the QML info to DC.py and I can add the new MapCircle using DC.py data – reverse_engineer Mar 21 '19 at 22:52
  • you want to add the points with double click in QML, and then you want to be able to move it with python code? – eyllanesc Mar 21 '19 at 22:57
  • Yes, I add the points on a double-click, and later I invoke some python code that can move the points. – reverse_engineer Mar 21 '19 at 22:59
  • Well, you're on the way since you've only passed the coordinates of the markers but with that information you can not move it afterwards from python. One last query: what is mark_name? Is it necessary? – eyllanesc Mar 21 '19 at 23:02
  • No it's not neccessary. Why will I be unable to move the marks I've created? – reverse_engineer Mar 21 '19 at 23:03
  • How do you plan to move it with code? – eyllanesc Mar 21 '19 at 23:05
  • I mean if you can move the markers from python but the method is different, I'm working on it. – eyllanesc Mar 21 '19 at 23:06
  • Something like this. `import time start = { 'lat': 52.8116017577209, 'lng': -6.96515296327718 } end = { 'lat': 52.8227543792629, 'lng': -6.94086287843584} n = 100 i = n my_coords = start.copy() are_we_there_yet = False while are_we_there_yet != True: i -= 1 my_coords['lat'] = start['lat'] * i / n + end['lat'] * (n - i) / n my_coords['lng'] = start['lng'] * i / n + end['lng'] * (n - i) / n print(my_coords) time.sleep(1) if (i == 0): are_we_there_yet = True` Apologies about the formatting. – reverse_engineer Mar 21 '19 at 23:08
  • And what does self.dcs have to do with that? Also, you should never use time.sleep in a GUI. – eyllanesc Mar 21 '19 at 23:10
  • ATC.py will move a marker along a list of coordinates e.g [(lat, lng), (lat, lng), (lat, lng)]. Each DC will have a list to traverse. The above code was only a proof of concept for myself and time.sleep(1) was to simulate work being done. – reverse_engineer Mar 21 '19 at 23:19

1 Answers1

1

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)))
        }
    }
}
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • The markers are added OK but do not move. I am using QML 5.9 and can only use `QtQuick.Controls 2.2` would this cause the animation to not fire? – reverse_engineer Mar 22 '19 at 18:42
  • @reverse_engineer What do you mean the markers do not move? Do you get any message in the console?, execute the code from the CMD / terminal to get all the error messages – eyllanesc Mar 22 '19 at 18:46
  • @reverse_engineer Are you using my code without modifying ?, if so, check if the console prints an error, if not first try my initial code, understand it and just modify it. – eyllanesc Mar 22 '19 at 18:50
  • @reverse_engineer QtQuick.Controls 2.2 or QtQuick.Controls 2.3 is indifferent – eyllanesc Mar 22 '19 at 18:50
  • @reverse_engineer Finally I recommend you always use the latest version (currently it is 5.12.1) or at least indicate which version of PyQt5 you are using. Your version of Qt (if you have it installed) will not necessarily coincide with the version of PyQt5 and among them the dependencies. – eyllanesc Mar 22 '19 at 18:52
  • I'm running the code as it is shown here, when I run there are no console log statements. I'm looking through the code right now. – reverse_engineer Mar 22 '19 at 18:57
  • @reverse_engineer I have updated my code, now that code will generate a log.log file, execute your code, have a test and send me that log please – eyllanesc Mar 22 '19 at 19:18
  • it's ok I managed to make them move. Just one last question before I mark this as the accepted answer; can animations be drawn in real time or do I have to rely on a constant updating event (such as the mouse click) to redraw the events? – reverse_engineer Mar 22 '19 at 19:49
  • You can add marker using `manager.model.appendMarker(QtPositioning.QGeoCoordinate(lat, lng))` – eyllanesc Mar 22 '19 at 19:51
  • @reverse_engineer If you provide a code, you could point out the correct solution – eyllanesc Mar 22 '19 at 19:52
  • I'm working on something at the minute, if it works I'll post it here – reverse_engineer Mar 22 '19 at 19:57
  • @reverse_engineer How did you manage to move the marker? Maybe other users have the same problem – eyllanesc Mar 22 '19 at 19:58
  • I changed the for clause in `on_markersInserted()` in main.py to `for marker in manager.model._markers` as the markers weren't passed as a argument to the function – reverse_engineer Mar 22 '19 at 20:02
  • @reverse_engineer It looks like a bug, what version of PyQt5 are you using? – eyllanesc Mar 22 '19 at 20:04
  • Qt version: 5.9.6 SIP version: 4.19.8 PyQt version: 5.9.2 – reverse_engineer Mar 22 '19 at 20:06