diff --git a/pyqtlet2/__init__.py b/pyqtlet2/__init__.py index 2ebd50f..d17b70a 100644 --- a/pyqtlet2/__init__.py +++ b/pyqtlet2/__init__.py @@ -3,7 +3,7 @@ """ __author__ = 'Leon Friedmann ' -__version__ = '0.8.3' +__version__ = '0.9.0' from .mapwidget import MapWidget from .leaflet import L diff --git a/pyqtlet2/leaflet/core/evented.py b/pyqtlet2/leaflet/core/evented.py index 4e3f9d5..b3dfd2c 100644 --- a/pyqtlet2/leaflet/core/evented.py +++ b/pyqtlet2/leaflet/core/evented.py @@ -1,5 +1,4 @@ import logging -import time from ... import mapwidget @@ -11,9 +10,9 @@ class Evented(QObject): Base class for all pyqtlet2 objects. Handles initiation, as well as all python<->js communication ''' - mapWidget = None + mapWidgets = [] - def __init__(self, mapWidget=None): + def __init__(self, mapWidget=None, mapWidgetIndex=None): ''' Base class for all pyqtlet2 objects Handles initiation, as well as python-Js communication @@ -28,22 +27,31 @@ def __init__(self, mapWidget=None): super().__init__() self._logger = logging.getLogger(__name__) self.response = None - if Evented.mapWidget: + + if isinstance(mapWidgetIndex, type(None)): return + if mapWidget is None: raise RuntimeError('L.map must be initialised before other pyqtlet2 objects') if not issubclass(type(mapWidget), mapwidget.MapWidget): raise TypeError(('Expected mapWidget of type pyqtlet2.MapWidget, ' 'received {type_}'.format(type_=type(mapWidget)))) - Evented.mapWidget = mapWidget + self.mapWidgets.append(mapWidget) js = ('var channelObjects = null;' 'new QWebChannel(qt.webChannelTransport, function(channel) {' ' channelObjects = channel.objects;' '});') - self.runJavaScript(js) - self.mapWidget.page.titleChanged.connect(lambda: print('title changed')) + self.runJavaScript(js, mapWidgetIndex) + if mapWidget := self.getMapWidgetAtIndex(mapWidgetIndex): + mapWidget.page.titleChanged.connect(lambda: print('title changed')) + + def getMapWidgetAtIndex(self, mapWidgetIndex): + if len(self.mapWidgets) > mapWidgetIndex: + return self.mapWidgets[mapWidgetIndex] + self._logger.error("No") + return None - def getJsResponse(self, js, callback): + def getJsResponse(self, js, mapWidgetIndex, callback): ''' Runs javascript code in the mapWidget and triggers callback. @@ -63,9 +71,12 @@ def getJsResponse(self, js, callback): ''' self._logger.debug('Running JS with callback: {js}=>{callback}'.format( js=js, callback=callback.__name__)) - self.mapWidget.page.runJavaScript(js, callback) + if mapWidget := self.getMapWidgetAtIndex(mapWidgetIndex): + mapWidget.page.runJavaScript(js, callback) + else: + self._logger.error(f"Can't find mapWidget at index: {mapWidgetIndex}") - def runJavaScript(self, js): + def runJavaScript(self, js, mapWidgetIndex: int): ''' Runs javascript code in the mapWidget. @@ -75,9 +86,12 @@ def runJavaScript(self, js): :param str js: The javascript code ''' self._logger.debug('Running JS: {js}'.format(js=js)) - self.mapWidget.page.runJavaScript(js) + if mapWidget := self.getMapWidgetAtIndex(mapWidgetIndex): + mapWidget.page.runJavaScript(js) + else: + self._logger.error(f"Can't find mapWidget at index: {mapWidgetIndex}") - def _createJsObject(self, leafletJsObject): + def _createJsObject(self, leafletJsObject, mapWidgetIndex): ''' Function to create variables/objects in leaflet in the javascript "engine", and registers the object so that it can @@ -89,12 +103,15 @@ def _createJsObject(self, leafletJsObject): # Creates the js object on the mapWidget page js = 'var {name} = {jsObject}'.format(name=self.jsName, jsObject=leafletJsObject) - self.runJavaScript(js) + self.runJavaScript(js, mapWidgetIndex) # register the object in the channel - self.mapWidget.channel.registerObject( - '{name}Object'.format(name=self.jsName), self) + if mapWidget := self.getMapWidgetAtIndex(mapWidgetIndex): + mapWidget.channel.registerObject( + '{name}Object'.format(name=self.jsName), self) + else: + self._logger.error(f"Can't find mapWidget at index: {mapWidgetIndex}") - def _connectEventToSignal(self, event, signalEmitter): + def _connectEventToSignal(self, event, signalEmitter, mapWidgetIndex): # We need to delete some keys as they are causing circular structures js = '{name}.on("{event}", function(e) {{\ delete e.target;\ @@ -102,7 +119,7 @@ def _connectEventToSignal(self, event, signalEmitter): e = copyWithoutCircularReferences([e], e);\ channelObjects.{name}Object.{signalEmitter}(e)}})'.format( name=self.jsName, event=event, signalEmitter=signalEmitter) - self.runJavaScript(js) + self.runJavaScript(js, mapWidgetIndex) def _stringifyForJs(self, object_): # When passing options to JS, sometimes we need to pass in objects diff --git a/pyqtlet2/leaflet/layer/featuregroup.py b/pyqtlet2/leaflet/layer/featuregroup.py index a2b2b88..6f016a1 100644 --- a/pyqtlet2/leaflet/layer/featuregroup.py +++ b/pyqtlet2/leaflet/layer/featuregroup.py @@ -8,7 +8,7 @@ class FeatureGroup(LayerGroup): def _initJs(self): leafletJsObject = 'new L.featureGroup()' - self._createJsObject(leafletJsObject) + self._createJsObject(leafletJsObject, self._map.mapWidgetIndex) def createAndAddDrawnLayer(self, drawnLayer, options=None): layerType = drawnLayer['layerType'] diff --git a/pyqtlet2/leaflet/layer/icon/icon.py b/pyqtlet2/leaflet/layer/icon/icon.py index 102d986..bf63007 100644 --- a/pyqtlet2/leaflet/layer/icon/icon.py +++ b/pyqtlet2/leaflet/layer/icon/icon.py @@ -12,7 +12,8 @@ def __init__(self, iconUrl: str, options=None): self.icon_found = False self.options = options self._check_icon_url() - self._initJs() + if self._map: + self._initJs() def _check_icon_url(self): if "http" in self.iconUrl: @@ -26,4 +27,4 @@ def _check_icon_url(self): def _initJs(self): leafletJsObject = 'L.icon({options});'.format(options=Parser.dict_for_js({"iconUrl": self.iconUrl, **self.options})) - self._createJsObject(leafletJsObject) \ No newline at end of file + self._createJsObject(leafletJsObject, self._map.mapWidgetIndex) \ No newline at end of file diff --git a/pyqtlet2/leaflet/layer/imageoverlay.py b/pyqtlet2/leaflet/layer/imageoverlay.py index 2735486..b6b9e1c 100644 --- a/pyqtlet2/leaflet/layer/imageoverlay.py +++ b/pyqtlet2/leaflet/layer/imageoverlay.py @@ -6,12 +6,13 @@ def __init__(self, imageURL, bounds, options=None): self.imageURL = imageURL self.bounds = bounds self.options = options - self._initJs() + if self._map: + self._initJs() def _initJs(self): leafletJsObject = 'L.imageOverlay("{imageURL}",{bounds}'.format(imageURL=self.imageURL,bounds=self.bounds) if self.options: leafletJsObject += ', {options}'.format(options=self.options) leafletJsObject += ')' - self._createJsObject(leafletJsObject) + self._createJsObject(leafletJsObject, self._map.mapWidgetIndex) diff --git a/pyqtlet2/leaflet/layer/layer.py b/pyqtlet2/leaflet/layer/layer.py index a9e9429..7585520 100644 --- a/pyqtlet2/leaflet/layer/layer.py +++ b/pyqtlet2/leaflet/layer/layer.py @@ -1,5 +1,6 @@ from ..core import Evented import logging +from abc import abstractmethod class Layer(Evented): @@ -27,6 +28,13 @@ def map(self): def map(self, map_): self._map = map_ + @abstractmethod + def _initJs(self): + raise NotImplemented + + def runJavaScriptForMapIndex(self, js): + self.runJavaScript(js, self._map.mapWidgetIndex) + def __init__(self): super().__init__() self._map = None @@ -52,12 +60,12 @@ def bindPopup(self, content, options=None): if options: js += ', {options}'.format(options=self._stringifyForJs(options)) js += ')' - self.runJavaScript(js) + self.runJavaScriptForMapIndex(js) return self def unbindPopup(self): js = '{layerName}.unbindPopup()'.format(layerName=self._layerName) - self.runJavaScript(js) + self.runJavaScriptForMapIndex(js) return self def bindTooltip(self, content, options=None): @@ -66,12 +74,12 @@ def bindTooltip(self, content, options=None): if options: js += ', {options}'.format(options=self._stringifyForJs(options)) js += ')' - self.runJavaScript(js) + self.runJavaScriptForMapIndex(js) return self def unbindTooltip(self): js = '{layerName}.unbindTooltip()'.format(layerName=self._layerName) - self.runJavaScript(js) + self.runJavaScriptForMapIndex(js) return self diff --git a/pyqtlet2/leaflet/layer/layergroup.py b/pyqtlet2/leaflet/layer/layergroup.py index 80abf1b..1e6ef78 100644 --- a/pyqtlet2/leaflet/layer/layergroup.py +++ b/pyqtlet2/leaflet/layer/layergroup.py @@ -12,17 +12,18 @@ def layers(self): def __init__(self): super().__init__() self._layers = [] - self._initJs() + if self._map: + self._initJs() def _initJs(self): leafletJsObject = 'new L.layerGroup()' - self._createJsObject(leafletJsObject) + self._createJsObject(leafletJsObject, self._map.mapWidgetIndex) def addLayer(self, layer): self._layers.append(layer) js = '{layerGroup}.addLayer({layerName})'.format(layerGroup=self._layerName, layerName=layer._layerName) - self.runJavaScript(js) + self.runJavaScriptForMapIndex(js) def removeLayer(self, layer): if not layer in self._layers: @@ -31,11 +32,11 @@ def removeLayer(self, layer): self._layers.remove(layer) js = '{layerGroup}.removeLayer({layerName})'.format(layerGroup=self._layerName, layerName=layer._layerName) - self.runJavaScript(js) + self.runJavaScriptForMapIndex(js) def clearLayers(self): js = '{layerGroup}.clearLayers()'.format(layerGroup=self._layerName) - self.runJavaScript(js) + self.runJavaScriptForMapIndex(js) def toGeoJSON(self, callback): self.getJsResponse('{layer}.toGeoJSON()'.format(layer=self.jsName), callback) diff --git a/pyqtlet2/leaflet/layer/marker/marker.py b/pyqtlet2/leaflet/layer/marker/marker.py index 966f68a..71e062f 100644 --- a/pyqtlet2/leaflet/layer/marker/marker.py +++ b/pyqtlet2/leaflet/layer/marker/marker.py @@ -20,10 +20,8 @@ def __init__(self, latLng: List[float], options=None): self.options = options self.opacity = options.get('opacity', 1) self.draggable = options.get('draggable', False) - self._initJs() - self._connectEventToSignal('move', '_onMove') - self._connectEventToSignal('moveend', '_onMoveend') - self._connectEventToSignal('click', '_click') + if self._map: + self._initJs() @Slot(QJsonValue) def _onMove(self, event): @@ -51,40 +49,43 @@ def _initJs(self): if self.options: leafletJsObject += ', {options}'.format(options=Parser.dict_for_js(self.options)) leafletJsObject += ')' - self._createJsObject(leafletJsObject) + self._createJsObject(leafletJsObject, self._map.mapWidgetIndex) + self._connectEventToSignal('move', '_onMove', self._map.mapWidgetIndex) + self._connectEventToSignal('moveend', '_onMoveend', self._map.mapWidgetIndex) + self._connectEventToSignal('click', '_click', self._map.mapWidgetIndex) def setLatLng(self, latLng): self.latLng = latLng js = '{layerName}.setLatLng({latLng})'.format( layerName=self._layerName, latLng=latLng) - self.runJavaScript(js) + self.runJavaScriptForMapIndex(js) return self def setOpacity(self, opacity): self.opacity = opacity js = '{layerName}.setOpacity({opacity})'.format( layerName=self._layerName, opacity=self.opacity) - self.runJavaScript(js) + self.runJavaScriptForMapIndex(js) return self def setDragging(self, draggable): self.draggable = draggable option = 'enable' if self.draggable else 'disable' js = '{layerName}.dragging.{option}();'.format(layerName=self._layerName, option=option) - self.runJavaScript(js) + self.runJavaScriptForMapIndex(js) return self def setIcon(self, icon: Icon): js = '{layerName}.setIcon({markerIcon});'.format(layerName=self._layerName, markerIcon=icon._layerName) - self.runJavaScript(js) + self.runJavaScriptForMapIndex(js) return self def setRotationAngle(self, angle_deg: float): js = '{layerName}.setRotationAngle({angle_deg});'.format(layerName=self._layerName, angle_deg=angle_deg) - self.runJavaScript(js) + self.runJavaScriptForMapIndex(js) return self def setRotationOrigin(self, origin: str): js = '{layerName}.setRotationOrigin({origin});'.format(layerName=self._layerName, origin=origin) - self.runJavaScript(js) + self.runJavaScriptForMapIndex(js) return self diff --git a/pyqtlet2/leaflet/layer/tile/tilelayer.py b/pyqtlet2/leaflet/layer/tile/tilelayer.py index cfc41cb..109be7b 100644 --- a/pyqtlet2/leaflet/layer/tile/tilelayer.py +++ b/pyqtlet2/leaflet/layer/tile/tilelayer.py @@ -5,12 +5,12 @@ def __init__(self, urlTemplate, options=None): super().__init__() self.urlTemplate = urlTemplate self.options = options - self._initJs() + if self._map: + self._initJs() def _initJs(self): leafletJsObject = 'L.tileLayer("{urlTemplate}"'.format(urlTemplate=self.urlTemplate) if self.options: leafletJsObject += ', {options}'.format(options=self.options) leafletJsObject += ')' - self._createJsObject(leafletJsObject) - + self._createJsObject(leafletJsObject, self._map.mapWidgetIndex) \ No newline at end of file diff --git a/pyqtlet2/leaflet/layer/vector/circle.py b/pyqtlet2/leaflet/layer/vector/circle.py index 2c25671..c0118fa 100644 --- a/pyqtlet2/leaflet/layer/vector/circle.py +++ b/pyqtlet2/leaflet/layer/vector/circle.py @@ -12,5 +12,4 @@ def _initJs(self): if self.options: leafletJsObject += ', {options}'.format(options=self.options) leafletJsObject += ')' - self._createJsObject(leafletJsObject) - + self._createJsObject(leafletJsObject, self) diff --git a/pyqtlet2/leaflet/layer/vector/circlemarker.py b/pyqtlet2/leaflet/layer/vector/circlemarker.py index 13a75a6..1feb06c 100644 --- a/pyqtlet2/leaflet/layer/vector/circlemarker.py +++ b/pyqtlet2/leaflet/layer/vector/circlemarker.py @@ -6,12 +6,13 @@ def __init__(self, latLng, options=None): super().__init__() self.latLng = latLng self.options = options - self._initJs() + if self._map: + self._initJs() def _initJs(self): leafletJsObject = 'L.circleMarker({latLng}'.format(latLng=self.latLng) if self.options: leafletJsObject += ', {options}'.format(options=self.options) leafletJsObject += ')' - self._createJsObject(leafletJsObject) + self._createJsObject(leafletJsObject, self._map.mapWidgetIndex) diff --git a/pyqtlet2/leaflet/layer/vector/polygon.py b/pyqtlet2/leaflet/layer/vector/polygon.py index 6913027..60839f4 100644 --- a/pyqtlet2/leaflet/layer/vector/polygon.py +++ b/pyqtlet2/leaflet/layer/vector/polygon.py @@ -10,5 +10,5 @@ def _initJs(self): if self.options: leafletJsObject += ', {options}'.format(options=self.options) leafletJsObject += ')' - self._createJsObject(leafletJsObject) + self._createJsObject(leafletJsObject, self._map.mapWidgetIndex) diff --git a/pyqtlet2/leaflet/layer/vector/polyline.py b/pyqtlet2/leaflet/layer/vector/polyline.py index f336abb..e7c2fff 100644 --- a/pyqtlet2/leaflet/layer/vector/polyline.py +++ b/pyqtlet2/leaflet/layer/vector/polyline.py @@ -6,12 +6,13 @@ def __init__(self, latLngs, options=None): super().__init__() self.latLngs = latLngs self.options = options - self._initJs() + if self._map: + self._initJs() def _initJs(self): leafletJsObject = 'L.polyline({latLngs}'.format(latLngs=self.latLngs) if self.options: leafletJsObject += ', {options}'.format(options=self.options) leafletJsObject += ')' - self._createJsObject(leafletJsObject) + self._createJsObject(leafletJsObject, self._map.mapWidgetIndex) diff --git a/pyqtlet2/leaflet/layer/vector/rectangle.py b/pyqtlet2/leaflet/layer/vector/rectangle.py index e140876..12ac90d 100644 --- a/pyqtlet2/leaflet/layer/vector/rectangle.py +++ b/pyqtlet2/leaflet/layer/vector/rectangle.py @@ -10,5 +10,5 @@ def _initJs(self): if self.options: leafletJsObject += ', {options}'.format(options=self.options) leafletJsObject += ')' - self._createJsObject(leafletJsObject) + self._createJsObject(leafletJsObject, self._map.mapWidgetIndex) diff --git a/pyqtlet2/leaflet/map/map.py b/pyqtlet2/leaflet/map/map.py index 974ece8..9b229a3 100644 --- a/pyqtlet2/leaflet/map/map.py +++ b/pyqtlet2/leaflet/map/map.py @@ -7,6 +7,7 @@ from ... import mapwidget from ..core import Evented +from ..layer import Layer class Map(Evented): @@ -31,6 +32,7 @@ class Map(Evented): zoom = Signal(dict) drawCreated = Signal(dict) right_mouse_clicked = Signal(dict) + mapWidgetCounter = 0 @property def layers(self): @@ -82,24 +84,26 @@ def __init__(self, mapWidget, options=None): Further documentation can be found at the official leaflet API. ''' - super().__init__(mapWidget) + super().__init__(mapWidget, Map.mapWidgetCounter) + self.mapWidgetIndex = Map.mapWidgetCounter + Map.mapWidgetCounter += 1 self._logger = logging.getLogger(__name__) self.options = options self._layers = [] self._controls = [] self._jsName = 'map' self._initJs() - self._connectEventToSignal('click', '_onClick') - self._connectEventToSignal('contextmenu', '_onRightClick') - self._connectEventToSignal('zoom', '_onZoom') - self._connectEventToSignal('draw:created', '_onDrawCreated') + self._connectEventToSignal('click', '_onClick', self.mapWidgetIndex) + self._connectEventToSignal('contextmenu', '_onRightClick', self.mapWidgetIndex) + self._connectEventToSignal('zoom', '_onZoom', self.mapWidgetIndex) + self._connectEventToSignal('draw:created', '_onDrawCreated', self.mapWidgetIndex) def _initJs(self): jsObject = 'L.map("map"' if self.options: jsObject += ', {options}'.format(options=self._stringifyForJs(self.options)) jsObject += ')' - self._createJsObject(jsObject) + self._createJsObject(jsObject, self.mapWidgetIndex) def setView(self, latLng, zoom=None, options=None): js = 'map.setView({latLng}'.format(latLng=latLng) @@ -108,14 +112,18 @@ def setView(self, latLng, zoom=None, options=None): if options: js += ', {options}'.format(options=options) js += ');' - self.runJavaScript(js) + self.runJavaScript(js, self.mapWidgetIndex) return self - def addLayer(self, layer): + def runJavaScriptForMap(self, js): + self.runJavaScript(js, self.mapWidgetIndex) + + def addLayer(self, layer: Layer): self._layers.append(layer) layer.map = self + layer._initJs() js = 'map.addLayer({layerName})'.format(layerName=layer.layerName) - self.runJavaScript(js) + self.runJavaScriptForMap(js) return self def removeLayer(self, layer): @@ -125,14 +133,14 @@ def removeLayer(self, layer): self._layers.remove(layer) layer.map = None js = 'map.removeLayer({layerName})'.format(layerName=layer.layerName) - self.runJavaScript(js) + self.runJavaScriptForMap(js) return self def addControl(self, control): self._controls.append(control) control.map = self js = 'map.addControl({controlName})'.format(controlName=control.controlName) - self.runJavaScript(js) + self.runJavaScriptForMap(js) return self def removeControl(self, control): @@ -142,20 +150,23 @@ def removeControl(self, control): self._controls.remove(control) control.map = None js = 'map.removeControl({controlName})'.format(controlName=control.controlName) - self.runJavaScript(js) + self.runJavaScriptForMap(js) return self + def getJsresponseForMap(self, js, callback): + return self.getJsResponse(js, self.mapWidgetIndex, callback) + def getBounds(self, callback): - return self.getJsResponse('map.getBounds()', callback) + return self.getJsresponseForMap('map.getBounds()', callback) def getCenter(self, callback): - return self.getJsResponse('map.getCenter()', callback) + return self.getJsresponseForMap('map.getCenter()', callback) def getZoom(self, callback): - return self.getJsResponse('map.getZoom()', callback) + return self.getJsresponseForMap('map.getZoom()', callback) def getState(self, callback): - return self.getJsResponse('getMapState()', callback) + return self.getJsresponseForMap('getMapState()', callback) def hasLayer(self, layer): return layer in self._layers @@ -165,27 +176,27 @@ def setZoom(self, zoom, options=None): if options: js += ', {options}'.format(options=options) js += ');' - self.runJavaScript(js) + self.runJavaScriptForMap(js) return self def setMaxBounds(self, bounds): js = 'map.setMaxBounds({bounds})'.format(bounds=bounds) - self.runJavaScript(js) + self.runJavaScriptForMap(js) return self def fitBounds(self, bounds): js = 'map.fitBounds({bounds})'.format(bounds=bounds) - self.runJavaScript(js) + self.runJavaScriptForMap(js) return self def setMaxZoom(self, zoom): js = 'map.setMaxZoom({zoom})'.format(zoom=zoom) - self.runJavaScript(js) + self.runJavaScriptForMap(js) return self def setMinZoom(self, zoom): js = 'map.setMinZoom({zoom})'.format(zoom=zoom) - self.runJavaScript(js) + self.runJavaScriptForMap(js) return self def panTo(self, latLng, options=None): @@ -193,7 +204,7 @@ def panTo(self, latLng, options=None): if options: js += ', {options}'.format(options=options) js += ');' - self.runJavaScript(js) + self.runJavaScriptForMap(js) return self def flyTo(self, latLng, zoom=None, options=None): @@ -203,5 +214,5 @@ def flyTo(self, latLng, zoom=None, options=None): if options: js += ', {options}'.format(options=options) js += ');' - self.runJavaScript(js) + self.runJavaScriptForMap(js) return self diff --git a/pyqtlet2/mapwidget.py b/pyqtlet2/mapwidget.py index 57a2a17..41fb356 100644 --- a/pyqtlet2/mapwidget.py +++ b/pyqtlet2/mapwidget.py @@ -1,9 +1,8 @@ import os -import time -from qtpy.QtCore import QEventLoop, QObject, Qt, QUrl, Signal +from qtpy.QtCore import QEventLoop, Qt, QUrl from qtpy.QtWebChannel import QWebChannel -from qtpy.QtWebEngineWidgets import QWebEngineView, QWebEnginePage, QWebEngineSettings +from qtpy.QtWebEngineWidgets import QWebEngineView, QWebEnginePage class MapWidget(QWebEngineView): diff --git a/setup.py b/setup.py index 0a0043a..4139c34 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='pyqtlet2', - version='0.8.3', + version='0.9.0', description='Bringing leaflet maps to Python Qt bindings', long_description=long_description, long_description_content_type="text/markdown",