Search code examples
pythonpyqt5foliumpython-3.9qwebengineview

How add circle markers to a rendered folium map embedded in a QWebEngineView?


I want to develop a desktop application that involves receiving a geographic coordinate every second from a serial port and adding to a map in real-time. The map should look like the below link:

enter image description here

I have written a piece of code to test Folium's ability to perform this task.

import sys
import io
import folium
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QMainWindow, QPushButton
from PyQt5.QtWebEngineWidgets import QWebEngineView


class Window(QMainWindow):
    def __init__(self):
        super().__init__()

        coordinate = (41.8828, 12.4761)

        self.m = folium.Map(
            zoom_start = 18,
            location = coordinate,
            control_scale=True,
            tiles = None
        )

        folium.raster_layers.TileLayer(
            tiles='http://mt1.google.com/vt/lyrs=m&h1=p1Z&x={x}&y={y}&z={z}',
            name='Standard Roadmap',
            attr = 'Google Map',
        ).add_to(self.m)

        folium.raster_layers.TileLayer(
            tiles='http://mt1.google.com/vt/lyrs=s&h1=p1Z&x={x}&y={y}&z={z}',
            name='Satellite Only',
            attr = 'Google Map',
        ).add_to(self.m)

        folium.raster_layers.TileLayer(
            tiles='http://mt1.google.com/vt/lyrs=y&h1=p1Z&x={x}&y={y}&z={z}',
            name='Hybrid',
            attr = 'Google Map',
        ).add_to(self.m)      

        folium.LayerControl().add_to(self.m)

        folium.Marker(coordinate).add_to(self.m)
                                
        self.data = io.BytesIO()
        self.m.save(self.data, close_file=False)

        widget=QWidget()
        vbox = QVBoxLayout()

        buttun1 = QPushButton("Insert Marker")
        buttun1.clicked.connect(self.insert_marker)

        self.webView = QWebEngineView()
        self.webView.setHtml(self.data.getvalue().decode())
        self.webView.setContextMenuPolicy(Qt.NoContextMenu)
    
        vbox.addWidget(buttun1)
        vbox.addWidget(self.webView)
        widget.setLayout(vbox)
        
        self.setCentralWidget(widget)
        
        self.setWindowTitle("App")
        
        self.setMinimumSize(1000, 600)
        self.showMaximized()

    def insert_marker(self):
        folium.CircleMarker([41.8829, 12.4766],
            radius=2,
            weight=5,
        ).add_to(self.m)  
        self.m.save(self.data, close_file=False)
        self.webView.setHtml(self.data.getvalue().decode())
       

App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())

In this code, the folium map is embedded in a PyQt5 QWebEngineView, and a circle marker is expected to be added to the map after clicking the button above. However, after click, the map will only be rerendered.

Despite some discussions about the impossibility of seamless updating of a generated folium map, it seems that using ClickForMarker() you can add markers to a rendered map.

Is there a way to get the same functionality without clicking?


Solution

  • You can add the markers by executing javascript through the runJavaScript() method:

    import io
    import sys
    
    from jinja2 import Template
    
    import folium
    
    from PyQt5.QtCore import pyqtSignal, QObject, QTimer
    from PyQt5.QtWidgets import QApplication, QMainWindow
    from PyQt5.QtWebEngineWidgets import QWebEngineView
    
    
    class CoordinateProvider(QObject):
        coordinate_changed = pyqtSignal(float, float)
    
        def __init__(self, parent=None):
            super().__init__(parent)
            self._timer = QTimer(interval=1000)
            self._timer.timeout.connect(self.generate_coordinate)
    
        def start(self):
            self._timer.start()
    
        def stop(self):
            self._timer.stop()
    
        def generate_coordinate(self):
            import random
    
            center_lat, center_lng = 41.8828, 12.4761
            x, y = (random.uniform(-0.001, 0.001) for _ in range(2))
            latitude = center_lat + x
            longitude = center_lng + y
            self.coordinate_changed.emit(latitude, longitude)
    
    
    class Window(QMainWindow):
        def __init__(self):
            super().__init__()
            coordinate = (41.8828, 12.4761)
            self.map = folium.Map(
                zoom_start=18, location=coordinate, control_scale=True, tiles=None
            )
            folium.raster_layers.TileLayer(
                tiles="http://mt1.google.com/vt/lyrs=m&h1=p1Z&x={x}&y={y}&z={z}",
                name="Standard Roadmap",
                attr="Google Map",
            ).add_to(self.map)
            folium.raster_layers.TileLayer(
                tiles="http://mt1.google.com/vt/lyrs=s&h1=p1Z&x={x}&y={y}&z={z}",
                name="Satellite Only",
                attr="Google Map",
            ).add_to(self.map)
            folium.raster_layers.TileLayer(
                tiles="http://mt1.google.com/vt/lyrs=y&h1=p1Z&x={x}&y={y}&z={z}",
                name="Hybrid",
                attr="Google Map",
            ).add_to(self.map)
            folium.LayerControl().add_to(self.map)
            folium.Marker(coordinate).add_to(self.map)
    
            data = io.BytesIO()
            self.map.save(data, close_file=False)
    
            self.map_view = QWebEngineView()
            self.map_view.setHtml(data.getvalue().decode())
    
            self.setCentralWidget(self.map_view)
    
        def add_marker(self, latitude, longitude):
            js = Template(
                """
            L.marker([{{latitude}}, {{longitude}}] )
                .addTo({{map}});
            L.circleMarker(
                [{{latitude}}, {{longitude}}], {
                    "bubblingMouseEvents": true,
                    "color": "#3388ff",
                    "dashArray": null,
                    "dashOffset": null,
                    "fill": false,
                    "fillColor": "#3388ff",
                    "fillOpacity": 0.2,
                    "fillRule": "evenodd",
                    "lineCap": "round",
                    "lineJoin": "round",
                    "opacity": 1.0,
                    "radius": 2,
                    "stroke": true,
                    "weight": 5
                }
            ).addTo({{map}});
            """
            ).render(map=self.map.get_name(), latitude=latitude, longitude=longitude)
            self.map_view.page().runJavaScript(js)
    
    
    def main():
        app = QApplication(sys.argv)
    
        window = Window()
        window.showMaximized()
    
        provider = CoordinateProvider()
        provider.coordinate_changed.connect(window.add_marker)
        provider.start()
    
        sys.exit(app.exec())
    
    
    if __name__ == "__main__":
        main()
    

    enter image description here