Search code examples
pythonpyqtpyqt5qtwebengineqtwebchannel

storing lat long on mouse click from JS to PyQt5


I'm trying to store lat/lng on mouse click to geometry Point. So basically when user clicks on map i need to get those coordinates(lat/lng) and store them to variable for example point = Point(lat, lng)so i can calculate nearest geometry and pull data from PostGIS.

I'm aware i need to establish backend and @pyqtSlot(float,float), but since im new to this i cant get it to work. I have this code that is generated by QtDesigner and you dont have to bother with all the buttons. Here is the HTML/JS part:

maphtml = '''
    <!DOCTYPE HTML>
    <html>
    <head>
    <meta name="robots" content="index, all" />    
    <script src="qrc:///qtwebchannel/qwebchannel.js"></script>
    <title>WebGL Earth API - Side-by-side - Basic Leaflet 
    compatibility</title>
    <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet- 
    0.7.2/leaflet.css" />
    <script src="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.js"></script>
    <script src="http://www.webglearth.com/v2/api.js"></script>
    <script src="scripts/qwebchannel.js"></script>
    <script>
    var backend;
    new QWebChannel(qt.webChannelTransport, function (channel) {
    backend = channel.objects.backend;


    });
    function init() {
    var m = {};

    start_(L, 'L');
    start_(WE, 'WE');

    function start_(API, suffix) {
    var mapDiv = 'map' + suffix;
    var map = API.map(mapDiv, {
    center: [51.505, -0.09],
    zoom: 4,
    dragging: true,
    scrollWheelZoom: true,
    proxyHost: 'http://srtm.webglearth.com/cgi-bin/corsproxy.fcgi?url='
    });
    m[suffix] = map;

    //Add baselayer
    API.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
    attribution: '© OpenStreetMap contributors'
    }).addTo(map);

    //Add TileJSON overlay
    var json = {"profile": "mercator", "name": "Grand Canyon USGS", "format": 
    "png", "bounds": [15.976953506469728, 45.813157465613884], "minzoom": 10, 
    "version": "1.0.0", "maxzoom": 16, "center": [15.976953506469728, 
    45.813157465613884, 16], "type": "overlay", "description": "", 
    "basename": 
    "grandcanyon", "tilejson": "2.0.0", "sheme": "xyz", "tiles": 
    ["http://tileserver.maptiler.com/grandcanyon/{z}/{x}/{y}.png"]};
    if (API.tileLayerJSON) {
    var overlay2 = API.tileLayerJSON(json, map);
    } else {
    //If not able to display the overlay, at least move to the same location
    map.setView([json.center[1], json.center[0]], json.center[2]);
    }



    //Print coordinates of the mouse
    map.on('click', function(e) {

    document.getElementById('coords').innerHTML = e.latlng.lat + ', ' + 
    e.latlng.lng;
    backend.foo(e.latlng.lat,e.latlng.lng)

    });
    }

    //Synchronize view
    m['L'].on('click', function(e) {
    var center = m['L'].getCenter();
    var zoom = m['L'].getZoom();
    m['WE'].setView([center['lat'], center['lng']], zoom);
    });
    }
    </script>
    <style>
    html, body{padding: 0; margin: 0; overflow: hidden;}
    #mapL, #mapWE {position:absolute !important; top: 0; right: 0; bottom: 0; 
    left: 0;
    background-color: #fff; position: absolute !important;}
    #mapL {right: 0%;}
    #mapWE {left: 100%;}
    #coords {position: absolute; bottom: 0;}
    </style>
    </head>
    <body onload="javascript:init()">
    <div id="mapL"></div>
    <div id="mapWE"></div>
    <div id="coords"></div>
    </body>
    </html>
    '''

And here is PyQt code, they are all in same .py btw. At the start i have Backend() that should be doin its job. You can skip to def setupUI and to self.mapa = QtWidgets.QWidget(self.centralwidget) to see where i call Backend():

class Backend(QObject):
    @pyqtSlot(float,float)
    def foo(self, lat,lng):
        global x,y
        x=lng
        y=lat
        print(lat, lng)

class Ui_MainWindow(object):

    def selected(self, text):
        self.selected_text = text
        return self.selected_text

    def connecti(self):
        try:
            engine = 
     create_engine('postgresql://postgres:hrvatina@localhost:5432/Diplomski')
            self.connection = engine.connect()

            return QMessageBox.information(None, "Uspješna konekcija", 
     "Spojeni ste na bazu")
        except:

            return QMessageBox.critical(None, "Pogreška", "Neuspješno 
     spajanje na bazu")


    def odabir(self):

        try:

            broj = self.link.text()

            self.comboBox.activated[str].connect(self.selected)

            value = str(self.comboBox.currentText())
            if self.pon.isChecked():
                radio = str(self.pon.text())
                print(radio)
            elif self.uto.isChecked():
                radio = str(self.uto.text())
                print(radio)
            elif self.sri.isChecked():
                radio = str(self.sri.text())
                print(radio)
            elif self.cet.isChecked():
                radio = str(self.cet.text())
                print(radio)
            elif self.pet.isChecked():
                radio = str(self.pet.text())
                print(radio)
            elif self.sub.isChecked():
                radio = str(self.sub.text())
                print(radio)
            elif self.ned.isChecked():
                radio = str(self.ned.text())
                print(radio)


            query ='SELECT * FROM ' + radio + '_' + value + ' where "IdLink" 
     = %s' % (broj)

            df = pd.read_sql_query(query, con=self.connection, params= 
     {'link': '%' + broj + '%'})
            df = df.transpose()

            hf = df.join(df.iloc[:, 0].str.split(';', 3, 
    expand=True).rename(columns={0: 'mean', 1: 'median', 2: 'std'}))


            g = hf.drop(hf.columns[[0]], axis=1)

            hm = g.drop(g.index[[0]])

            prva1 = hm.apply(pd.to_numeric, errors='coerce')

            #Izračun srednjih vrijednosti
            mean=prva1['mean'].mean()
            self.mean.setText(str(mean))
            median=prva1['median'].mean()
            self.median.setText(str(median))
            std=prva1['std'].mean()
            self.std.setText(str(std))

            upper_bound = go.Scatter(
                name='Gornja granica',
                x=prva1.index.to_native_types(),
                y=prva1['mean'] + prva1['std'],
                mode='lines',
                marker=dict(color="028F1E"),
                line=dict(width=1, color="028F1E"),
                fillcolor='rgba(68, 68, 68, 0.3)',

                fill='tonexty')

            trace = go.Scatter(
                name='Srednja vrijednost',
                x=prva1.index.to_native_types(),
                y=prva1['mean'],
                mode='lines',
                line=dict(color='rgb(31, 119, 180)'),
                fillcolor='rgba(68, 68, 68, 0.3)',
                fill='tonexty')

            lower_bound = go.Scatter(
                name='Donja granica',
                x=prva1.index.to_native_types(),
                y=prva1['mean'] - prva1['std'],
                marker=dict(color="9B111E"),
                line=dict(width=1, color="9B111E"),
                mode='lines')

            data = [lower_bound, trace, upper_bound]

            layout = go.Layout(
                xaxis=dict(
                    title='AXIS TITLE',
                    titlefont=dict(
                        family='Arial, sans-serif',
                        size=18,
                        color='lightgrey'
                    ),
                    showticklabels=True,
                    tickangle=45,
                    tickfont=dict(
                        family='Old Standard TT, serif',
                        size=14,
                        color='black'
                    ),
                    exponentformat='e',
                    showexponent='All'
                ),
                yaxis=dict(title='Brzina (km/h)'),
                title='Minutni interval za ' + radio + ' sa standardnom 
          devijacijom za ' + broj,
            )

            fig = go.Figure(data=data, layout=layout)
            raw_html = '<html><head><meta charset="utf-8" />'
            raw_html += '<script src="https://cdn.plot.ly/plotly- 
    latest.min.js"></script></head>'
            raw_html += '<body>'
            raw_html += plotly.offline.plot(fig, include_plotlyjs=False, 
    output_type='div')
            raw_html += '</body></html>'

            #PLOTLY
            self.graf.setHtml(raw_html)

        except:

            return QMessageBox.critical(None, "Pogreška", "Podaci za odabrane 
            opcije ne postoje!")
    def setupUi(self, MainWindow):

        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(978, 704)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.formLayout = QtWidgets.QFormLayout(self.centralwidget)
        self.formLayout.setObjectName("formLayout")
        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")
        self.gumb_spoji = QtWidgets.QPushButton(self.centralwidget)
        self.gumb_spoji.setObjectName("gumb_spoji")

        # Spoji se na bazu
        self.gumb_spoji.clicked.connect(self.connecti)
        engine = 
        self.verticalLayout.addWidget(self.gumb_spoji)
        self.link = QtWidgets.QLineEdit(self.centralwidget)
        self.link.setObjectName("link")
        self.verticalLayout.addWidget(self.link)
        self.pon = QtWidgets.QRadioButton(self.centralwidget)
        self.pon.setObjectName("pon")
        self.verticalLayout.addWidget(self.pon)
        self.uto = QtWidgets.QRadioButton(self.centralwidget)
        self.uto.setObjectName("uto")
        self.verticalLayout.addWidget(self.uto)
        self.sri = QtWidgets.QRadioButton(self.centralwidget)
        self.sri.setObjectName("sri")
        self.verticalLayout.addWidget(self.sri)
        self.cet = QtWidgets.QRadioButton(self.centralwidget)
        self.cet.setObjectName("cet")
        self.verticalLayout.addWidget(self.cet)
        self.pet = QtWidgets.QRadioButton(self.centralwidget)
        self.pet.setObjectName("pet")
        self.verticalLayout.addWidget(self.pet)
        self.sub = QtWidgets.QRadioButton(self.centralwidget)
        self.sub.setObjectName("sub")
        self.verticalLayout.addWidget(self.sub)
        self.ned = QtWidgets.QRadioButton(self.centralwidget)
        self.ned.setObjectName("ned")
        self.verticalLayout.addWidget(self.ned)
        self.comboBox = QtWidgets.QComboBox(self.centralwidget)
        self.comboBox.setObjectName("comboBox")
        self.comboBox.addItem("")
        self.comboBox.addItem("")
        self.verticalLayout.addWidget(self.comboBox)
        self.gumb_dohvati = QtWidgets.QPushButton(self.centralwidget)
        self.gumb_dohvati.setObjectName("gumb_dohvati")

        # Dohvati podatke
        self.gumb_dohvati.clicked.connect(self.odabir)

        self.verticalLayout.addWidget(self.gumb_dohvati)
        self.label = QtWidgets.QLabel(self.centralwidget)
        self.label.setObjectName("label")
        self.verticalLayout.addWidget(self.label)
        self.mean = QtWidgets.QLineEdit(self.centralwidget)
        self.mean.setObjectName("mean")
        self.verticalLayout.addWidget(self.mean)
        self.label_2 = QtWidgets.QLabel(self.centralwidget)
        self.label_2.setObjectName("label_2")
        self.verticalLayout.addWidget(self.label_2)
        self.median = QtWidgets.QLineEdit(self.centralwidget)
        self.median.setObjectName("median")
        self.verticalLayout.addWidget(self.median)
        self.label_3 = QtWidgets.QLabel(self.centralwidget)
        self.label_3.setObjectName("label_3")
        self.verticalLayout.addWidget(self.label_3)
        self.std = QtWidgets.QLineEdit(self.centralwidget)
        self.std.setObjectName("std")
        self.verticalLayout.addWidget(self.std)
        self.formLayout.setLayout(0, QtWidgets.QFormLayout.LabelRole, 
        self.verticalLayout)

        # I want to pass lat/lng here and after user click show data in 
        Qwidget below, but i will do that on my own i just need those lat/lng 
        after every click
        self.mapa = QtWidgets.QWidget(self.centralwidget)
        self.mapa.setObjectName("mapa")
        backend = Backend()
        channel = QWebChannel()
        channel.registerObject('backend', backend)

        self.mapa = QWebEngineView()
        self.mapa.page().setWebChannel(channel)

        self.mapa.setHtml(maphtml)

        point=Point(y,x)





        #sql = 'select * from geo'
        #df = gpd.GeoDataFrame.from_postgis(sql, self.connection, 
 geom_col='geometry' )
        #def min_dist(point, gpd2):
            #df['Dist'] = df.apply(lambda row:  
 point.distance(row.geometry),axis=1)
            #geoseries = df.iloc[gpd2['Dist'].idxmin()]
            #return geoseries['IdLink']
        #point=Point(self.Backend.lat(),self.Backend.lng())
        #self.brojka=min_dist(point,df)

        self.formLayout_3 = QtWidgets.QFormLayout(self.mapa)
        self.formLayout_3.setObjectName("formLayout_3")
        self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, 
        self.mapa)
        self.graf = QtWidgets.QWidget(self.centralwidget)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, 
        QtWidgets.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(255)

   sizePolicy.setHeightForWidth(self.graf.sizePolicy().hasHeightForWidth())
        self.graf.setSizePolicy(sizePolicy)
        self.graf.setMouseTracking(True)
        self.graf.setObjectName("graf")
        self.graf = QWebEngineView(self.centralwidget)
        self.formLayout_2 = QtWidgets.QFormLayout(self.graf)
        self.formLayout_2.setObjectName("formLayout_2")
        self.formLayout.setWidget(1, QtWidgets.QFormLayout.SpanningRole, 
        self.graf)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 978, 21))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.gumb_spoji.setText(_translate("MainWindow", "Spoji se na bazu"))
        self.pon.setText(_translate("MainWindow", "Ponedjeljak"))
        self.uto.setText(_translate("MainWindow", "Utorak"))
        self.sri.setText(_translate("MainWindow", "Srijeda"))
        self.cet.setText(_translate("MainWindow", "Cetvrtak"))
        self.pet.setText(_translate("MainWindow", "Petak"))
        self.sub.setText(_translate("MainWindow", "Subota"))
        self.ned.setText(_translate("MainWindow", "Nedjelja"))
        self.comboBox.setItemText(0, _translate("MainWindow", "5min"))
        self.comboBox.setItemText(1, _translate("MainWindow", "15min"))
        self.gumb_dohvati.setText(_translate("MainWindow", "Dohvati 
        podatke"))
        self.label.setText(_translate("MainWindow", "Srednja vrijednost"))
        self.label_2.setText(_translate("MainWindow", "Median"))
        self.label_3.setText(_translate("MainWindow", "Standardna 
        devijacija"))


if __name__ == "__main__":
    import sys
    app = QtCore.QCoreApplication.instance()
    if app is None:
        app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()

    sys.exit(app.exec_())

I saw similar questions but couldn't make it work so far... In PyQt4 i have it almost working, but since i am bad at JS, i dont know how to to return lat/lng intead of getCenter().lat from this code:

if(typeof MainWindow != 'undefined') {
    var onMapMove = function(e) { MainWindow.onMapMove(map.getCenter().lat, 
map.getCenter().lng) };
    map.on('click', onMapMove);
    onMapMove();


Solution

  • I can not work much with your code because it is badly formatted and there are missing elements to define so my answer will be based on showing you an example.

    First of all it does not put all the code in a file, that makes it difficult to maintain the code, in this case I have divided into the following files:

    .
    ├── index.html
    ├── main.py
    ├── utils.css
    └── utils.js
    

    window.onload = function() {
        new QWebChannel(qt.webChannelTransport, function (channel) {
            window.backend = channel.objects.backend;
        });
    
        var m = {};
    
        start_(L, 'L');
        start_(WE, 'WE');
    
        function start_(API, suffix) {
            var mapDiv = 'map' + suffix;
            var map = API.map(mapDiv, {
                center: [51.505, -0.09],
                zoom: 4,
                dragging: true,
                scrollWheelZoom: true,
                proxyHost: 'http://srtm.webglearth.com/cgi-bin/corsproxy.fcgi?url='
            });
            m[suffix] = map;
    
            //Add baselayer
            API.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                attribution: '© OpenStreetMap contributors'
            }).addTo(map);
    
            //Add TileJSON overlay
            var json = {
                "profile": "mercator",
                "name": "Grand Canyon USGS",
                "format": "png",
                "bounds": [15.976953506469728, 45.813157465613884],
                "minzoom": 10,
                "version": "1.0.0",
                "maxzoom": 16,
                "center": [15.976953506469728,
                    45.813157465613884, 16
                ],
                "type": "overlay",
                "description": "",
                "basename": "grandcanyon",
                "tilejson": "2.0.0",
                "sheme": "xyz",
                "tiles": ["http://tileserver.maptiler.com/grandcanyon/{z}/{x}/{y}.png"]
            };
            if (API.tileLayerJSON) {
                var overlay2 = API.tileLayerJSON(json, map);
            } else {
                //If not able to display the overlay, at least move to the same location
                map.setView([json.center[1], json.center[0]], json.center[2]);
            }
    
            //Print coordinates of the mouse
            map.on('click', function(e) {
                document.getElementById('coords').innerHTML = e.latlng.lat + ', ' + e.latlng.lng;
                backend.pointClicked(e.latlng.lat, e.latlng.lng);
            });
        }
    
        //Synchronize view
        m['L'].on('click', function(e) {
            var center = m['L'].getCenter();
            var zoom = m['L'].getZoom();
            m['WE'].setView([center['lat'], center['lng']], zoom);
        });
    }
    html, body {
         padding: 0;
         margin: 0;
         overflow: hidden;
    }
    
    #mapL, #mapWE {
         position: absolute !important;
         top: 0;
         right: 0;
         bottom: 0;
         left: 0;
         background-color: #fff;
         position: absolute !important;
    }
     #mapL {
         right: 0%;
    }
     #mapWE {
         left: 100%;
    }
     #coords {
         position: absolute;
         bottom: 0;
    }
    <!DOCTYPE HTML>
    <html>
    
    <head>
        <meta name="robots" content="index, all"/>
        <title>WebGL Earth API - Side-by-side - Basic Leaflet compatibility
        </title>
        <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.css"/>
        <link rel="stylesheet" href="utils.css"/>
    
        <script src="qrc:///qtwebchannel/qwebchannel.js"></script>
        <script type="text/javascript" src="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.js"></script>
        <script type="text/javascript" src="http://www.webglearth.com/v2/api.js"></script>
        <script type="text/javascript" src="utils.js"> </script>
    </head>
    
    <body>
        <div id="mapL"></div>
        <div id="mapWE"></div>
        <div id="coords"></div>
    </body>
    
    </html>

    main.py

    from PyQt5 import QtWebEngineWidgets, QtWidgets, QtCore, QtWebChannel
    
    class Backend(QtCore.QObject):
        pointChanged = QtCore.pyqtSignal(float, float)
    
        @QtCore.pyqtSlot(float,float)
        def pointClicked(self, x, y):
            self.pointChanged.emit(x, y)
    
    
    class MainWindow(QtWidgets.QMainWindow):
        def __init__(self, parent=None):
            super(MainWindow, self).__init__(parent)
    
            map_view = QtWebEngineWidgets.QWebEngineView()
    
            backend = Backend(self)
            backend.pointChanged.connect(self.onPointChanged)
            channel = QtWebChannel.QWebChannel(self)
            channel.registerObject('backend', backend)
            map_view.page().setWebChannel(channel)
    
            file = QtCore.QDir.current().absoluteFilePath("index.html")
            map_view.load(QtCore.QUrl.fromLocalFile(file))
    
            self.setCentralWidget(map_view)
    
        @QtCore.pyqtSlot(float,float)
        def onPointChanged(self, x, y):
            print("new points")
            print(x, y)
    
    
    if __name__ == '__main__':
        import sys
    
        app = QtWidgets.QApplication(sys.argv)
        w = MainWindow()
        w.show()
        sys.exit(app.exec_())