Implementation Details¶
This document is an overview of the technical details of pyqtlet. It covers how the module is built, the problems that were faced, and how they were tackled.
Structure¶
The folder and code structure of the leaflet section was designed to mimic that of the Leaflet library. I figured they had a better idea on as to why their code had been structured that way and didn’t want to mess with institutional knowledge like that. So now if someone familiar with the leaflet code wants to mess around with pyqtlet, they’ll know where to find the files they’re looking for.
All the leaflet source code is included in the package so that the package is not dependent on any CDN or network connectivity.
QWebEngine¶
Qt offers a QWebEngine to handle all the web features. This is obviously the core of
the package. The main widget, pyqtlet.mapWidget
is subclassed from QWebEngineView.
To start off, mapWidget
loads the html from file, and loads the leaflet canvas
into itself. To initiate the use of the pyqtlet library, the first pyqtlet object that is
created should be L.map
. This links all further objects created in the module to
the widget, by assigning the widget as a static variable to pyqtlet.core.Evented
,
the base class in the module.
Creating the objects in Leaflet¶
Since QWebEnginePage allows us to run JavaScript code, it becomes fairly straightforward
to start creating objects from python in the JS runtime. Every object (except abstract ones)
in PyQt, during initialisation, are given their respective jsObject code. So for marker,
the code would be L.marker. Evented then has a _createJsObject
that then runs
the required javascript in the JS runtime.
The names of layers, controls etc are all controlled using static variable in the respective
baseclasses, such that layers are named l0
, l1
and so on. The name of the
layer is then stored within the object in the jsName
attribute.
Python -> JavaScript Communication¶
In order for pyqtlet to offer the functionality of a wrapper around a JS library, it is
necessary to establish communication between JS to send commands and data between the
runtimes. To send commands from Python to JS, we use QWebEnginePage.runJavaScript
method. This method can be called with a callback as well. The methods with and without
callback are accessed from pyqtlet.core.Evented.getJsResponse
and
pyqtlet.core.runJavaScript
respectively.
All the methods from pyqtlet objects are mostly just calling a runJavaScript or getJsResponse for the appropriate JS code. So map modifying functions like setView or setMaxZoom are just calling the same code in JS.
Another thing that we need to keep in mind while communicating with JS is passing objects as paramenters in options or otherwise. The object needs to be represented in the JS script string as an object, and not as a string or a python object. Evented has a method for this called _stringifyForJs, which recursively goes through dicts and replaces all the python objects with JS ones and makes it a string.
JavaScript -> Python communication¶
Communication from JavaScript is mostly required only for connecting events to their
respective pyqtSignals. In order to do this, we need to set up a QWebChannel
.
What the web channel allows us to do is to trigger python methods from JS code.
In order to do this, we have to first register our python objects with the web channel.
This happens in the initialisation of the object, in Evented._createJsObject
.
It is important to note that only methods of the registered objects can be called from
within JS, so arbitrary code cannot be run, and we cannot pass lambdas, but only methods.
So for every event that leaflet has access to, we have to create a pyqtSignal and a method
that emits the signal. We also then have to connect the event to the method. For that there
is a method Evented._connectEventToSignal
that does that. It also handles
circular references in the JSON to be returned.
Handling Async¶
Solved¶
The largest async problem that was faced was nothing to actually do with JavaScript at all.
It was rather to do with Qt. Qt has a few methods that are run asynchronously. One of these
is QWebEnginePage.load()
which loads html onto the widget.
The problem with this asynchronicity is that it causes problem with running the next code. Anstantiating the map ran a L.map in the JS runtime before the page (and thus the leaflet library) was loaded, which caused a widget to just be a blank page.
The solution to this came using QEventLoop
. An infinite loop was created that only
quit when the web page emitted a loadFinished
signal. This ensured that the interpreter
waited until the html was loaded and only them progressed.
Unsolved¶
The problem that still has not been solved is how to handle the async running of runJavaScript. If we want the method to return a value from JS, then a similar approach doesn’t seem to work. I have tried numerous combinations of threads, signals, callbacks and channel objects, but none of them seem to be able to emit the signal which would break the infinite loop that is waiting for a response before it can terminate.
The problem that I’m looking to solve is how a method can run some JS code, and then return the response from that same code.
Overall building this package was a great exercise that I learnt a lot out of. I hope it helps you in creating some great apps. In case you have any feedback on how this whole thing can be implemented better, please raise an issue and let me know, or a pull request if you are in the mood for something like that.