pythonpython-3.xpyqtpyqt5ipyleaflet

How to add an Ipyleaflet map to a PyQt5 application?


I've created an app using PyQt5 that (amongst others) displays a map.

For the map widget, I've used pyqtlet, but I'm starting to realize that this package is really limited (I want to show different layers, to use a draggable marker, etc.), so I want to transition to ipyleaflet instead.

Except that I cannot get my map to show in the app!

The original code was like so:

import sys
from PyQt5.QtWidgets import QApplication, QVBoxLayout, QWidget, QLabel
from pyqtlet import L, MapWidget


class MapWindow(QWidget):
    def __init__(self, base_coords):
        self.base_coords = base_coords
        # Setting up the widgets and layout
        super().__init__()
        self.layout = QVBoxLayout()
        self.title = QLabel("<b>This is my title</b>")
        self.layout.addWidget(self.title)
        self.mapWidget = MapWidget()
        self.layout.addWidget(self.mapWidget)
        self.setLayout(self.layout)

        # Working with the maps with pyqtlet
        self.map = L.map(self.mapWidget)
        self.map.setView(self.base_coords, zoom=10)
        L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}'
                    ).addTo(self.map)  # ArcGIS_topo layer
        self.marker = L.marker(self.base_coords)
        self.marker.bindPopup('This is my marker')
        self.map.addLayer(self.marker)
        self.show()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    base_coords = [45.783119, 3.123364]
    widget = MapWindow(base_coords)
    sys.exit(app.exec_())
    

I'm then trying to use this to change to ipyleaflet:

import sys
from PyQt5.QtWidgets import QApplication, QVBoxLayout, QWidget, QLabel
from PyQt5 import QtWebEngineWidgets
from ipyleaflet import Map, Marker, LayersControl, basemaps
from ipywidgets import HTML


class MapWindow(QWidget):
    def __init__(self, base_coords):
        self.base_coords = base_coords
        # Setting up the widgets and layout
        super().__init__()
        self.layout = QVBoxLayout()
        self.title = QLabel("<b>This is my title</b>")
        self.layout.addWidget(self.title)

        # Working with the maps with ipyleaflet
        self.map = Map(center=self.base_coords, basemaps=basemaps.Esri.WorldTopoMap, zoom=10)
        self.layout.addWidget(self.map)

        self.marker = Marker(location=self.base_coords)
        self.marker.popup = HTML(value='This is my marker')
        self.map.add_layer(self.marker)
        self.show()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    base_coords = [45.783119, 3.123364]
    widget = MapWindow(base_coords)
    sys.exit(app.exec_())

But the addition of the map in the layout doesn't work, I get this error message:

Traceback (most recent call last):
  File "G:\venv\lib\site-packages\IPython\core\interactiveshell.py", line 3418, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-2-bd466e04ab02>", line 1, in <module>
    runfile('G:/Application/short_app - Copie.py', wdir='G:/Application')
  File "C:\Program Files\JetBrains\PyCharm 2020.2.3\plugins\python\helpers\pydev\_pydev_bundle\pydev_umd.py", line 197, in runfile
    pydev_imports.execfile(filename, global_vars, local_vars)  # execute the script
  File "C:\Program Files\JetBrains\PyCharm 2020.2.3\plugins\python\helpers\pydev\_pydev_imps\_pydev_execfile.py", line 18, in execfile
    exec(compile(contents+"\n", file, 'exec'), glob, loc)
  File "G:/Application/short_app - Copie.py", line 29, in <module>
    widget = MapWindow(base_coords)
  File "G:/Application/short_app - Copie.py", line 19, in __init__
    self.layout.addWidget(self.map)
TypeError: addWidget(self, QWidget, stretch: int = 0, alignment: Union[Qt.Alignment, Qt.AlignmentFlag] = Qt.Alignment()): argument 1 has unexpected type 'Map'

Does anybody know I can add the ipyleaflet map to my application?


Solution

  • The problem is that you're trying to add self.map to the layout but since it isn't derived from a QWidget, it doesn't work and hence the error. I can see that you've imported QtWebEngineWidgets but it isn't being used. To embed a Jupyter widget, you could generate HTML that contains the ipyleaflet widget and then add the HTML to QtWebEngineWidgets.QWebEngineView. Below is a simple implementation of adding Jupyter widgets to HTML from the documentation:

    import sys
    import json
    from PyQt5.QtWidgets import QApplication, QVBoxLayout, QWidget, QLabel
    from PyQt5 import QtWebEngineWidgets
    
    from ipyleaflet import Map, Marker, LayersControl, basemaps
    from ipywidgets import HTML, IntSlider
    from ipywidgets.embed import embed_data
    
    
    class MapWindow(QWidget):
        def __init__(self, base_coords):
            self.base_coords = base_coords
            # Setting up the widgets and layout
            super().__init__()
            self.layout = QVBoxLayout()
            self.title = QLabel("<b>This is my title</b>")
            self.layout.addWidget(self.title)
    
            # Create QtWebEngineView widget
            self.web = QtWebEngineWidgets.QWebEngineView(self)
    
            # Sliders
            s1 = IntSlider(max=200, value=100)
            s2 = IntSlider(value=40)
    
            # Working with the maps with ipyleaflet
            self.map = Map(center=self.base_coords, basemaps=basemaps.Esri.WorldTopoMap, zoom=10)
    
            self.marker = Marker(location=self.base_coords)
            self.marker.popup = HTML(value='This is my marker')
            self.map.add_layer(self.marker)
    
            data = embed_data(views=[s1, s2, self.map])
    
            html_template = """
            <html>
              <head>
    
                <title>Widget export</title>
    
                <!-- Load RequireJS, used by the IPywidgets for dependency management -->
                <script 
                  src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js" 
                  integrity="sha256-Ae2Vz/4ePdIu6ZyI/5ZGsYnb+m0JlOmKPjt6XZ9JJkA=" 
                  crossorigin="anonymous">
                </script>
    
                <!-- Load IPywidgets bundle for embedding. -->
                <script
                  data-jupyter-widgets-cdn="https://cdn.jsdelivr.net/npm/"
                  src="https://unpkg.com/@jupyter-widgets/html-manager@*/dist/embed-amd.js" 
                  crossorigin="anonymous">
                </script>
    
                <!-- The state of all the widget models on the page -->
                <script type="application/vnd.jupyter.widget-state+json">
                  {manager_state}
                </script>
              </head>
    
              <body>
    
                <h1>Widget export</h1>
    
                <div id="first-slider-widget">
                  <!-- This script tag will be replaced by the view's DOM tree -->
                  <script type="application/vnd.jupyter.widget-view+json">
                    {widget_views[0]}
                  </script>
                </div>
    
                <hrule />
    
                <div id="second-slider-widget">
                  <!-- This script tag will be replaced by the view's DOM tree -->
                  <script type="application/vnd.jupyter.widget-view+json">
                    {widget_views[1]}
                  </script>
                </div>
                
                <!-- The ipyleaflet map -->
                <div id="ipyleaflet-map">
                    <script type="application/vnd.jupyter.widget-view+json">
                        {widget_views[2]}
                    </script>
                </div>
    
              </body>
            </html>
            """
    
            manager_state = json.dumps(data['manager_state'])
            widget_views = [json.dumps(view) for view in data['view_specs']]
            rendered_template = html_template.format(manager_state=manager_state, widget_views=widget_views)
    
            # Set HTML
            self.web.setHtml(rendered_template)
    
            # Add webengine to layout and add layout to widget
            self.layout.addWidget(self.web)
            self.setLayout(self.layout)
    
            self.show()
    
    
    if __name__ == '__main__':
        app = QApplication(sys.argv)
        base_coords = [45.783119, 3.123364]
        widget = MapWindow(base_coords)
        widget.resize(900, 800)
        sys.exit(app.exec_())