From a5a80bfffd1d0d5d0d24274d9f240d4e2cf4229c Mon Sep 17 00:00:00 2001 From: Raphael Date: Mon, 13 Mar 2023 19:14:58 +0100 Subject: [PATCH 1/7] explicitly handle "unmanaged" geo-axes artists and put them on "base" layer --- eomaps/helpers.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/eomaps/helpers.py b/eomaps/helpers.py index e060a39fb..8206ea2e9 100644 --- a/eomaps/helpers.py +++ b/eomaps/helpers.py @@ -1322,6 +1322,9 @@ def __init__(self, m): self._bg_artists = dict() self._bg_layers = dict() + # the name of the layer at which all "unmanaged" artists are drawn + self._unmanaged_artists_layer = "base" + # grab the background on every draw self.cid = self.canvas.mpl_connect("draw_event", self.on_draw) @@ -1617,6 +1620,9 @@ def get_bg_artists(self, layer): layer_artists = set(self._bg_artists.get(l, [])) artists = artists.union(layer_artists) + if l == self._unmanaged_artists_layer: + artists = artists.union(self._get_unmanaged_artists()) + artists = sorted(artists, key=self._bg_artists_sort) return artists @@ -1727,6 +1733,7 @@ def _do_fetch_bg(self, layer, bbox=None): for art in allartists: if art not in self._hidden_artists: art.draw(cv.get_renderer()) + art.stale = False self._bg_layers[layer] = cv.copy_from_bbox(bbox) @@ -1819,6 +1826,15 @@ def on_draw(self, event): self._layers_to_refetch.clear() self._refetch_bg = False else: + + # in case there is a stale (unmanaged) artists and the + # stale-artist layer is attempted to be drawn, re-draw the + # cached background for the unmanaged-artists layer + if self._unmanaged_artists_layer in self._bg_layer.split("|") and any( + a.stale for a in self._get_unmanaged_artists() + ): + self._refetch_layer(self._unmanaged_artists_layer) + # remove all cached backgrounds that were tagged for refetch while len(self._layers_to_refetch) > 0: self._bg_layers.pop(self._layers_to_refetch.pop(), None) @@ -2055,6 +2071,29 @@ def _draw_animated(self, layers=None, artists=None): for a in sorted(allartists, key=self._get_artist_zorder): fig.draw_artist(a) + def _get_unmanaged_artists(self): + # return all artists not explicitly managed by the blit-manager + # (e.g. any artist added via cartopy or matplotlib functions) + managed_artists = set( + chain(*self._bg_artists.values(), *self._artists.values()) + ) + + axes = {m.ax for m in (self._m, *self._m._children)} + + allartists = set() + for ax in axes: + axartists = { + *ax._children, + ax.title, + ax._left_title, + ax._right_title, + *([ax.legend_] if ax.legend_ is not None else []), + } + + allartists.update(axartists) + + return allartists.difference(managed_artists) + def _clear_temp_artists(self, method, forward=True): # clear artists from connected methods if method == "_click_move" and forward: From a1287115495ff3772bd0fa7dff4147624b4ecabc Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 14 Mar 2023 11:42:23 +0100 Subject: [PATCH 2/7] fix issues with using m.savefig(bbox_inches='tight') --- eomaps/helpers.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/eomaps/helpers.py b/eomaps/helpers.py index 7451665e2..99e0324a3 100644 --- a/eomaps/helpers.py +++ b/eomaps/helpers.py @@ -1789,9 +1789,20 @@ def _disconnect_draw(self): def on_draw(self, event): """Callback to register with 'draw_event'.""" - cv = self.canvas + if "RendererBase._draw_disabled" in cv.renderer.draw_image.__qualname__: + # TODO this fixes issues when saving figues with a "tight" bbox, e.g.: + # m.savefig(bbox_inches='tight', pad_inches=0.1) + + # This workaround is necessary but the implementation is suboptimal since + # it relies on the __qualname__ to identify if the + # `matplotlib.backend_bases.RendererBase._draw_disabled()` context is active + # The reason why the "_draw_disabled" context has to be handled explicitly + # is because otherwise empty backgrounds would be fetched (and cached) by + # the draw-event and the export would result in an empty figure. + return + if event is not None: if event.canvas != cv: raise RuntimeError From f01fec6736a17a47e8dc01c09d1f01e0e4639b3e Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 14 Mar 2023 15:45:01 +0100 Subject: [PATCH 3/7] update docs --- docs/api.rst | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 79e068d59..b8d6a2802 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -114,6 +114,34 @@ Once you have created your first ``Maps`` object, you can: m_ocean.add_feature.preset.ocean() # add ocean-coloring to the "ocean" layer m.show_layer("ocean") # show the "ocean" layer (note that it has coastlines as well!) + +.. admonition:: Artists added with methods **outside of EOmaps** + + If you use methods that are **NOT provided by EOmaps**, the corresponding artists will always appear on the ``"base"`` layer by default! + (e.g. ``cartopy`` or ``matplotlib`` methods accessible via ``m.ax.`` or ``m.f.`` like ``m.ax.plot(...)``) + + In most cases this behavior is sufficient... for more complicated use-cases, artists must be explicitly added to the **Blit Manager** (``m.BM``) so that ``EOmaps`` can handle drawing accordingly. + + To put the artists on dedicated layers, use one of the the following options: + + - For artists that are dynamically updated on each event, use ``m.BM.add_artist(artist, layer=...)`` + - For "background" artists that only require updates on pan/zoom/resize, use ``m.BM.add_bg_artist(artist, layer=...)`` + + + .. code-block:: python + + m = Maps() + m.all.add_feature.preset.coastline() # add coastlines to ALL layers of the map + + # draw a red X over the whole axis and put the lines + # as background-artists on the layer "mylayer" + (l1, ) = m.ax.plot([0, 1], [0, 1], lw=5, c="r", transform=m.ax.transAxes) + (l2, ) = m.ax.plot([0, 1], [1, 0], lw=5, c="r", transform=m.ax.transAxes) + + m.BM.add_bg_artist(l1, layer="mylayer") + m.BM.add_bg_artist(l2, layer="mylayer") + m.show_layer("mylayer") + .. _combine_layers: 🗗 Combine & compare multiple layers From f563ef72a817e945b515c830c2d0a5f576df287d Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 14 Mar 2023 19:29:47 +0100 Subject: [PATCH 4/7] add new WebMap services --- eomaps/_webmap_containers.py | 278 +++++++++++++++++++++++++++++- eomaps/qtcompanion/widgets/wms.py | 81 ++++++--- 2 files changed, 337 insertions(+), 22 deletions(-) diff --git a/eomaps/_webmap_containers.py b/eomaps/_webmap_containers.py index d6e1ff9f1..ef8e65f8c 100644 --- a/eomaps/_webmap_containers.py +++ b/eomaps/_webmap_containers.py @@ -387,6 +387,10 @@ def __init__(self, m): self._m = m self.add_layer = self._OSM(self._m) + self.OSM_waymarkedtrails = self._OSM_waymarkedtrails(self._m) + self.OSM_openrailwaymap = self._OSM_openrailwaymap(self._m) + self.OSM_cartodb = self._OSM_cartodb(self._m) + class _OSM: def __init__(self, m): self._m = m @@ -448,7 +452,123 @@ def __init__(self, m): check: https://wiki.openstreetmap.org/wiki/OpenTopoMap """, - self.default_german.__call__.__doc__, + self.OpenTopoMap.__call__.__doc__, + ) + + self.OpenRiverboatMap = _xyz_tile_service( + m=self._m, + url="https://a.tile.openstreetmap.fr/openriverboatmap/{z}/{x}/{y}.png", + maxzoom=16, + name="OSM_OpenRiverboatMap", + ) + self.OpenRiverboatMap.__doc__ = combdoc( + """ + Open Riverboat Map plans to make an open source CartoCSS map style + of navigable waterways, on top of OpenStreetMap project. + + https://github.com/tilery/OpenRiverboatMap + + Note + ---- + **LICENSE-info (without any warranty for correctness!!)** + + check: + + - https://github.com/tilery/OpenRiverboatMap + - https://openstreetmap.fr + - https://operations.osmfoundation.org/policies/tiles/ + + """, + self.OpenRiverboatMap.__call__.__doc__, + ) + + self.CyclOSM = _xyz_tile_service( + m=self._m, + url="https://a.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png", + maxzoom=16, + name="CyclOSM", + ) + self.CyclOSM.__doc__ = combdoc( + """ + CyclOSM is a bicycle-oriented map built on top of OpenStreetMap data. + It aims at providing a beautiful and practical map for cyclists, no + matter their cycling habits or abilities. + + https://www.cyclosm.org/ + + A legend is available here: https://www.cyclosm.org/legend.html + + Note + ---- + **LICENSE-info (without any warranty for correctness!!)** + + check: + + - https://www.cyclosm.org/ + - https://openstreetmap.fr + - https://operations.osmfoundation.org/policies/tiles/ + + """, + self.CyclOSM.__call__.__doc__, + ) + + self.CyclOSM_lite = _xyz_tile_service( + m=self._m, + url="https://a.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png", + maxzoom=16, + name="CyclOSM", + ) + + self.CyclOSM_lite.__doc__ = combdoc( + """ + CyclOSM is a bicycle-oriented map built on top of OpenStreetMap data. + It aims at providing a beautiful and practical map for cyclists, no + matter their cycling habits or abilities. + + https://www.cyclosm.org/ + + A legend is available here: https://www.cyclosm.org/legend.html + + Note + ---- + **LICENSE-info (without any warranty for correctness!!)** + + check: + + - https://www.cyclosm.org/ + - https://openstreetmap.fr + - https://operations.osmfoundation.org/policies/tiles/ + + """, + self.CyclOSM_lite.__call__.__doc__, + ) + + self.OEPNV_public_transport = _xyz_tile_service( + m=self._m, + url="http://tile.memomaps.de/tilegen/{z}/{x}/{y}.png", + maxzoom=16, + name="CyclOSM", + ) + + self.OEPNV_public_transport.__doc__ = combdoc( + """ + We display worldwide public transport facilities on a uniform map, + so that you can forget about browsing individual operators websites. + + https://www.öpnvkarte.de + + Note + ---- + **LICENSE-info (without any warranty for correctness!!)** + + check: + + - https://www.öpnvkarte.de + - https://memomaps.de/ + - https://operations.osmfoundation.org/policies/tiles/ + + """, + self.OEPNV_public_transport.__call__.__doc__, ) self.stamen_toner = _xyz_tile_service( @@ -578,6 +698,138 @@ def __init__(self, m): self.stamen_watercolor.__doc__ = stamen_watercolor_doc + class _OSM_waymarkedtrails: + def __init__(self, m): + self._m = m + + self.add_layer = self._add_layer(m) + + class _add_layer: + def __init__(self, m): + for v in [ + "hiking", + "cycling", + "mtb", + "slopes", + "riding", + "skating", + ]: + + srv = _xyz_tile_service( + m, + ( + "https://tile.waymarkedtrails.org/" + + v + + "/{z}/{x}/{y}.png" + ), + name=f"OSM_WaymarkedTrails_{v}", + ) + + setattr(self, v, srv) + + getattr(self, v).__doc__ = combdoc( + ( + f"WaymarkedTrails {v} layer\n" + "\n" + "Note\n" + "----\n" + "**LICENSE-info (without any warranty for correctness!!)**\n" + "\n" + f"check: https://{v}.waymarkedtrails.org/#help-legal\n" + ), + getattr(self, v).__call__.__doc__, + ) + + class _OSM_openrailwaymap: + def __init__(self, m): + self._m = m + + self.add_layer = self._add_layer(m) + + class _add_layer: + def __init__(self, m): + for v in [ + "standard", + "maxspeed", + "signals", + "electrification", + "gauge", + ]: + + srv = _xyz_tile_service( + m, + ( + "https://a.tiles.openrailwaymap.org/" + + v + + "/{z}/{x}/{y}.png" + ), + name=f"OSM_OpenRailwayMap_{v}", + ) + + setattr(self, v, srv) + + getattr(self, v).__doc__ = combdoc( + ( + f"OpenRailwayMap {v} layer\n" + "\n" + "Note\n" + "----\n" + "**LICENSE-info (without any warranty for correctness!!)**\n" + "\n" + f"check: https://wiki.openstreetmap.org/wiki/OpenRailwayMap/API\n" + ), + getattr(self, v).__call__.__doc__, + ) + + class _OSM_cartodb: + def __init__(self, m): + self._m = m + + self.add_layer = self._add_layer(m) + + class _add_layer: + def __init__(self, m): + for v in [ + "light_all", + "dark_all", + "light_nolabels", + "light_only_labels", + "dark_nolabels", + "dark_only_labels", + "base-antique", + "rastertiles/voyager", + "rastertiles/voyager_nolabels", + "rastertiles/voyager_only_labels", + "rastertiles/voyager_labels_under", + ]: + + srv = _xyz_tile_service( + m, + ( + "https://cartodb-basemaps-a.global.ssl.fastly.net/" + + v + + "/{z}/{x}/{y}.png" + ), + name=f"OSM_CartoDB_{v}", + ) + + name = v.replace(r"/", "_").replace("-", "_") + + setattr(self, name, srv) + + getattr(self, name).__doc__ = combdoc( + ( + f"CartoDB basemap {v} layer\n" + "\n" + "Note\n" + "----\n" + "**LICENSE-info (without any warranty for correctness!!)**\n" + "\n" + f"check: https://github.com/CartoDB/basemap-styles\n" + ), + getattr(self, name).__call__.__doc__, + ) + @property @lru_cache() def OSM_terrestis(self): @@ -620,6 +872,30 @@ def OSM_mundialis(self): ) return WMS + @property + @lru_cache() + def OSM_wheregroup(self): + WMS = _WebServiec_collection( + m=self._m, + service_type="wms", + url="https://osm-demo.wheregroup.com/service?REQUEST=GetCapabilities", + ) + WMS.__doc__ = combdoc( + type(self).__doc__, + """ + Note + ---- + **LICENSE-info (without any warranty for correctness!!)** + + ... this service is hosted by WhereGroup... + + It is ONLY allowed for private use and testing! For more details, check: + + https://wheregroup.com/kontakt/ + """, + ) + return WMS + @property @lru_cache() def OSM_wms(self): diff --git a/eomaps/qtcompanion/widgets/wms.py b/eomaps/qtcompanion/widgets/wms.py index 11dc2d8fa..cf837718f 100644 --- a/eomaps/qtcompanion/widgets/wms.py +++ b/eomaps/qtcompanion/widgets/wms.py @@ -227,34 +227,73 @@ def __init__(self, m=None): self._OSM_wms = [] print("Problem while fetching wmslayers for OSM_wms", ex) + try: + self._OSM_wheregroup = [ + "WhereGroup__" + i + for i in m.add_wms.OpenStreetMap.OSM_wheregroup.add_layer.__dict__ + ] + except Exception as ex: + self._OSM_wheregroup = [] + print("Problem while fetching wmslayers for OSM_wheregroup", ex) + + try: + self._OSM_waymarkedtrails = [ + "WaymarkedTrails__" + i + for i in m.add_wms.OpenStreetMap.OSM_waymarkedtrails.add_layer.__dict__ + ] + except Exception as ex: + self._OSM_waymarkedtrails = [] + print("Problem while fetching wmslayers for OSM_waymarkedtrails", ex) + + try: + self._OSM_openrailwaymap = [ + "OpenRailwayMap__" + i + for i in m.add_wms.OpenStreetMap.OSM_openrailwaymap.add_layer.__dict__ + ] + except Exception as ex: + self._OSM_openrailwaymap = [] + print("Problem while fetching wmslayers for OSM_openrailwaymap", ex) + + try: + self._OSM_cartodb = [ + "CartoDB__" + i + for i in m.add_wms.OpenStreetMap.OSM_cartodb.add_layer.__dict__ + ] + except Exception as ex: + self._OSM_cartodb = [] + print("Problem while fetching wmslayers for OSM_cartodb", ex) + self.wmslayers += self._terrestis self.wmslayers += self._mundialis self.wmslayers += self._OSM_landuse self.wmslayers += self._OSM_wms + self.wmslayers += self._OSM_wheregroup + self.wmslayers += self._OSM_waymarkedtrails + self.wmslayers += self._OSM_openrailwaymap + self.wmslayers += self._OSM_cartodb def do_add_layer(self, wmslayer, layer): - if wmslayer in self._OSM_wms: - wms = getattr( - self.m.add_wms.OpenStreetMap.OSM_wms.add_layer, - remove_prefix(wmslayer, "OSM_wms__"), - ) - elif wmslayer in self._OSM_landuse: - wms = getattr( - self.m.add_wms.OpenStreetMap.OSM_landuse.add_layer, - remove_prefix(wmslayer, "OSM_landuse__"), - ) - elif wmslayer in self._mundialis: - wms = getattr( - self.m.add_wms.OpenStreetMap.OSM_mundialis.add_layer, - remove_prefix(wmslayer, "Mundialis__"), - ) - elif wmslayer in self._terrestis: - wms = getattr( - self.m.add_wms.OpenStreetMap.OSM_terrestis.add_layer, - remove_prefix(wmslayer, "Terrestis__"), - ) - else: + wms = None + + # check if we need to remove a prefix (e.g. from the dropdown-names) + for layers, servicename, prefix in ( + (self._OSM_wms, "OSM_wms", "OSM_wms__"), + (self._OSM_landuse, "OSM_landuse", "OSM_landuse__"), + (self._mundialis, "OSM_mundialis", "Mundialis__"), + (self._terrestis, "OSM_terrestis", "Terrestis__"), + (self._OSM_wheregroup, "OSM_wheregroup", "WhereGroup__"), + (self._OSM_waymarkedtrails, "OSM_waymarkedtrails", "WaymarkedTrails__"), + (self._OSM_openrailwaymap, "OSM_openrailwaymap", "OpenRailwayMap__"), + (self._OSM_cartodb, "OSM_cartodb", "CartoDB__"), + ): + + if wmslayer in layers: + service = getattr(self.m.add_wms.OpenStreetMap, servicename, None) + wms = getattr(service.add_layer, remove_prefix(wmslayer, prefix)) + break + + if wms is None: wms = getattr(self.m.add_wms.OpenStreetMap.add_layer, wmslayer) wms(layer=layer, transparent=True) From c7dedadfd53b3137cfc11ef36808d81343324b51 Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 14 Mar 2023 19:43:30 +0100 Subject: [PATCH 5/7] update docstrings --- eomaps/_webmap_containers.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/eomaps/_webmap_containers.py b/eomaps/_webmap_containers.py index ef8e65f8c..72f8a9d88 100644 --- a/eomaps/_webmap_containers.py +++ b/eomaps/_webmap_containers.py @@ -341,7 +341,11 @@ def OpenStreetMap(self): - default: standard OSM layer - default_german: standard OSM layer in german - - standard: standard OSM layer + - CyclOSM: a bycicle oriented style + - OEPNV_public_transport: a layer indicating global public transportation + - OpenRiverboatMap: a style to navigate waterways + - OpenTopoMap: SRTM + OSM for nice topography + - - stamen_toner: Black and white style by stamen - stamen_toner_lines - stamen_toner_background @@ -355,6 +359,10 @@ def OpenStreetMap(self): - stamen_terrain_background - OSM_terrestis: Styles hosted as free WMS service by Terrestis - OSM_mundialis: Styles hosted as free WMS service by Mundialis + - OSM_cartodb: Styles hosted as free WMS service by CartoDB + - OSM_wheregroup: Styles hosted as free WMS service by WhereGroup + - OSM_openrailwaymap: layers provided by OSM-OpenRailwayMap + - OSM_waymarkedtrails: layers provided by OSM-WayMarkedTrails - OSM_wms and OSM_landuse: WMS hosted by Heidelberg Institute for Geoinformation Technology @@ -369,8 +377,15 @@ def OpenStreetMap(self): - for OSM_terrestis: https://www.terrestris.de/en/openstreetmap-wms/ - for OSM_mundialis: https://www.mundialis.de/en/ows-mundialis/ + - for OSM_cartodb: https://github.com/CartoDB/basemap-styles + - for OSM_WhereGroup: https://wheregroup.com/kontakt/ - for OSM_wms and OSM_landuse : https://heigit.org - + - for CyclOSM: https://www.cyclosm.org + - for OEPNV: http://öpnvkarte.de + - for Stamen: http://maps.stamen.com + - for OpenRailwayMap: https://wiki.openstreetmap.org/wiki/OpenRailwayMap + - for OSM_WaymarkedTrails: https://waymarkedtrails.org + - for OpenTopoMap: https://wiki.openstreetmap.org/wiki/OpenTopoMap """ WMS = self._OpenStreetMap(self._m) From 1257b3ee175ce03084f7edad2dc2f1f79f31afb8 Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 14 Mar 2023 19:51:09 +0100 Subject: [PATCH 6/7] add more docstrings for new webmaps --- eomaps/_webmap_containers.py | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/eomaps/_webmap_containers.py b/eomaps/_webmap_containers.py index 72f8a9d88..85553a229 100644 --- a/eomaps/_webmap_containers.py +++ b/eomaps/_webmap_containers.py @@ -714,10 +714,25 @@ def __init__(self, m): self.stamen_watercolor.__doc__ = stamen_watercolor_doc class _OSM_waymarkedtrails: + """ + OSM layers from WaymarkedTrails. + + Note + ---- + **LICENSE-info (withowayut any warranty for correctness!!)** + + check: + + - https://waymarkedtrails.org + - https://hiking.waymarkedtrails.org/#help-legal + + """ + def __init__(self, m): self._m = m self.add_layer = self._add_layer(m) + self.layers = list(self.add_layer.__dict__) class _add_layer: def __init__(self, m): @@ -756,10 +771,24 @@ def __init__(self, m): ) class _OSM_openrailwaymap: + """ + OSM layers from OpenRailwayMap. + + Note + ---- + **LICENSE-info (withowayut any warranty for correctness!!)** + + check: + + - https://wiki.openstreetmap.org/wiki/OpenRailwayMap/API + + """ + def __init__(self, m): self._m = m self.add_layer = self._add_layer(m) + self.layers = list(self.add_layer.__dict__) class _add_layer: def __init__(self, m): @@ -797,10 +826,25 @@ def __init__(self, m): ) class _OSM_cartodb: + """ + OSM basemap styles hosted by CartoDB. + + Note + ---- + **LICENSE-info (without any warranty for correctness!!)** + + check: + + - https://github.com/CartoDB/basemap-styles + - https://carto.com + + """ + def __init__(self, m): self._m = m self.add_layer = self._add_layer(m) + self.layers = list(self.add_layer.__dict__) class _add_layer: def __init__(self, m): From e009c13aa6d000b4ae93c5e042e253e39d9a2a15 Mon Sep 17 00:00:00 2001 From: Raphael Quast Date: Tue, 14 Mar 2023 22:21:45 +0100 Subject: [PATCH 7/7] update version to v6.2 --- eomaps/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eomaps/_version.py b/eomaps/_version.py index 86fe47d5b..063175c61 100644 --- a/eomaps/_version.py +++ b/eomaps/_version.py @@ -1 +1 @@ -__version__ = "6.1.3" +__version__ = "6.2"