diff --git a/.github/workflows/optional_requirements.txt b/.github/workflows/optional_requirements.txt index 2c8219136e1..244a75e2933 100644 --- a/.github/workflows/optional_requirements.txt +++ b/.github/workflows/optional_requirements.txt @@ -1,3 +1,4 @@ folium jupyter PyVirtualDisplay +ipyleaflet diff --git a/.github/workflows/periodic_update.yml b/.github/workflows/periodic_update.yml index 4c8060aaabf..bbcea87948f 100644 --- a/.github/workflows/periodic_update.yml +++ b/.github/workflows/periodic_update.yml @@ -33,7 +33,7 @@ jobs: run: git status --ignored - name: Create Pull Request id: cpr - uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5.0.2 + uses: peter-evans/create-pull-request@b1ddad2c994a25fbc81a28b3ec0e368bb2021c50 # v6.0.0 with: commit-message: "config.guess + config.sub: updated from http://git.savannah.gnu.org/cgit/config.git/plain/" branch: periodic/update-configure diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml index f40f071f0d8..3087f61eff8 100644 --- a/.github/workflows/super-linter.yml +++ b/.github/workflows/super-linter.yml @@ -29,8 +29,12 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + with: + # super-linter needs the full git history to get the + # list of files that changed across commits + fetch-depth: 0 - name: Lint code base - uses: super-linter/super-linter/slim@v5.7.2 + uses: super-linter/super-linter/slim@v6.0.0 env: DEFAULT_BRANCH: main # To report GitHub Actions status checks diff --git a/db/db.connect/db.connect.html b/db/db.connect/db.connect.html index c7020ccbded..48d6dbee02f 100644 --- a/db/db.connect/db.connect.html +++ b/db/db.connect/db.connect.html @@ -4,8 +4,8 @@

DESCRIPTION

These parameters are then taken as default values by modules so that the user does not need to enter the parameters each time.

-The default database backend in GRASS GIS 7 -is SQLite. +The default database backend in GRASS GIS +is SQLite (since version 7).

NOTES

diff --git a/display/d.vect.thematic/legend.c b/display/d.vect.thematic/legend.c index 85b7f57940d..291f9799804 100644 --- a/display/d.vect.thematic/legend.c +++ b/display/d.vect.thematic/legend.c @@ -20,7 +20,7 @@ void write_into_legend_file(const char *legfile, const char *icon, double stats_max, double *breakpoints, int nbreaks, int size, struct color_rgb bcolor, struct color_rgb *colors, int default_width, - int *frequencies, const char *topo) + int *frequencies, int c_type, const char *topo) { FILE *fd; int i; @@ -33,29 +33,34 @@ void write_into_legend_file(const char *legfile, const char *icon, /* Title */ fprintf(fd, "||||||%s\n", title); + // Do not show decimal places for integers. + int n_places = c_type == DB_C_TYPE_INT ? 0 : 2; + /* First line */ if (stats_min > breakpoints[0]) { - fprintf(fd, "< %.2f|", breakpoints[0]); + fprintf(fd, "< %.*f|", n_places, breakpoints[0]); } else { - fprintf(fd, "%.2f - %.2f|", stats_min, breakpoints[0]); + fprintf(fd, "%.*f - %.*f|", n_places, stats_min, n_places, + breakpoints[0]); } fprintf(fd, "%s|%d|ps|%d:%d:%d|%d:%d:%d|%d|%s|%d\n", icon, size, colors[0].r, colors[0].g, colors[0].b, bcolor.r, bcolor.g, bcolor.b, default_width, topo, frequencies[0]); /* Middle lines */ for (i = 1; i < nbreaks; i++) { - fprintf(fd, "%.2f - %.2f|%s|%d|ps|%d:%d:%d|%d:%d:%d|%d|%s|%d\n", - breakpoints[i - 1], breakpoints[i], icon, size, colors[i].r, - colors[i].g, colors[i].b, bcolor.r, bcolor.g, bcolor.b, - default_width, topo, frequencies[i]); + fprintf(fd, "%.*f - %.*f|%s|%d|ps|%d:%d:%d|%d:%d:%d|%d|%s|%d\n", + n_places, breakpoints[i - 1], n_places, breakpoints[i], icon, + size, colors[i].r, colors[i].g, colors[i].b, bcolor.r, bcolor.g, + bcolor.b, default_width, topo, frequencies[i]); } /* Last one */ if (stats_max < breakpoints[nbreaks - 1]) { - fprintf(fd, ">%.2f|", breakpoints[nbreaks - 1]); + fprintf(fd, ">%.*f|", n_places, breakpoints[nbreaks - 1]); } else { - fprintf(fd, "%.2f - %.2f|", breakpoints[nbreaks - 1], stats_max); + fprintf(fd, "%.*f - %.*f|", n_places, breakpoints[nbreaks - 1], + n_places, stats_max); } fprintf(fd, "%s|%d|ps|%d:%d:%d|%d:%d:%d|%d|%s|%d\n", icon, size, colors[nbreaks].r, colors[nbreaks].g, colors[nbreaks].b, bcolor.r, diff --git a/display/d.vect.thematic/local_proto.h b/display/d.vect.thematic/local_proto.h index 0f1377e4db6..3eee24ef112 100644 --- a/display/d.vect.thematic/local_proto.h +++ b/display/d.vect.thematic/local_proto.h @@ -28,4 +28,4 @@ int display_lines(struct Map_info *, struct cat_list *, int, const char *, /* legend.c */ void write_into_legend_file(const char *, const char *, const char *, double, double, double *, int, int, struct color_rgb, - struct color_rgb *, int, int *, const char *); + struct color_rgb *, int, int *, int, const char *); diff --git a/display/d.vect.thematic/main.c b/display/d.vect.thematic/main.c index 08bc0d264a3..bf162df0499 100644 --- a/display/d.vect.thematic/main.c +++ b/display/d.vect.thematic/main.c @@ -533,26 +533,26 @@ int main(int argc, char **argv) while (TRUE) { nfeatures = Vect_get_num_primitives(&Map, GV_POINT); if (nfeatures > 0) { - write_into_legend_file("stdout", icon_opt->answer, title, - stats.min, stats.max, breakpoints, - nbreaks, size, bcolor, colors, - default_width, frequencies, "point"); + write_into_legend_file( + "stdout", icon_opt->answer, title, stats.min, stats.max, + breakpoints, nbreaks, size, bcolor, colors, default_width, + frequencies, ctype, "point"); break; } nfeatures = Vect_get_num_primitives(&Map, GV_LINE); if (nfeatures > 0) { - write_into_legend_file("stdout", icon_line_opt->answer, title, - stats.min, stats.max, breakpoints, - nbreaks, size, bcolor, colors, - default_width, frequencies, "line"); + write_into_legend_file( + "stdout", icon_line_opt->answer, title, stats.min, + stats.max, breakpoints, nbreaks, size, bcolor, colors, + default_width, frequencies, ctype, "line"); break; } nfeatures = Vect_get_num_primitives(&Map, GV_BOUNDARY); if (nfeatures > 0) { - write_into_legend_file("stdout", icon_area_opt->answer, title, - stats.min, stats.max, breakpoints, - nbreaks, size, bcolor, colors, - default_width, frequencies, "area"); + write_into_legend_file( + "stdout", icon_area_opt->answer, title, stats.min, + stats.max, breakpoints, nbreaks, size, bcolor, colors, + default_width, frequencies, ctype, "area"); break; } } @@ -564,26 +564,26 @@ int main(int argc, char **argv) while (TRUE) { nfeatures = Vect_get_num_primitives(&Map, GV_POINT); if (nfeatures > 0) { - write_into_legend_file(leg_file, icon_opt->answer, title, - stats.min, stats.max, breakpoints, - nbreaks, size, bcolor, colors, - default_width, frequencies, "point"); + write_into_legend_file( + leg_file, icon_opt->answer, title, stats.min, stats.max, + breakpoints, nbreaks, size, bcolor, colors, default_width, + frequencies, ctype, "point"); break; } nfeatures = Vect_get_num_primitives(&Map, GV_LINE); if (nfeatures > 0) { - write_into_legend_file(leg_file, icon_line_opt->answer, title, - stats.min, stats.max, breakpoints, - nbreaks, size, bcolor, colors, - default_width, frequencies, "line"); + write_into_legend_file( + leg_file, icon_line_opt->answer, title, stats.min, + stats.max, breakpoints, nbreaks, size, bcolor, colors, + default_width, frequencies, ctype, "line"); break; } nfeatures = Vect_get_num_primitives(&Map, GV_BOUNDARY); if (nfeatures > 0) { - write_into_legend_file(leg_file, icon_area_opt->answer, title, - stats.min, stats.max, breakpoints, - nbreaks, size, bcolor, colors, - default_width, frequencies, "area"); + write_into_legend_file( + leg_file, icon_area_opt->answer, title, stats.min, + stats.max, breakpoints, nbreaks, size, bcolor, colors, + default_width, frequencies, ctype, "area"); break; } } @@ -598,7 +598,7 @@ int main(int argc, char **argv) write_into_legend_file( legend_file_opt->answer, icon_opt->answer, title, stats.min, stats.max, breakpoints, nbreaks, size, bcolor, colors, - default_width, frequencies, "point"); + default_width, frequencies, ctype, "point"); break; } nfeatures = Vect_get_num_primitives(&Map, GV_LINE); @@ -606,7 +606,7 @@ int main(int argc, char **argv) write_into_legend_file( legend_file_opt->answer, icon_line_opt->answer, title, stats.min, stats.max, breakpoints, nbreaks, size, bcolor, - colors, default_width, frequencies, "line"); + colors, default_width, frequencies, ctype, "line"); break; } nfeatures = Vect_get_num_primitives(&Map, GV_BOUNDARY); @@ -614,7 +614,7 @@ int main(int argc, char **argv) write_into_legend_file( legend_file_opt->answer, icon_area_opt->answer, title, stats.min, stats.max, breakpoints, nbreaks, size, bcolor, - colors, default_width, frequencies, "area"); + colors, default_width, frequencies, ctype, "area"); break; } } diff --git a/doc/notebooks/jupyter_tutorial.ipynb b/doc/notebooks/jupyter_tutorial.ipynb index 194ec0e7083..6d33be61f36 100644 --- a/doc/notebooks/jupyter_tutorial.ipynb +++ b/doc/notebooks/jupyter_tutorial.ipynb @@ -10,7 +10,7 @@ "\n", "The _grass.jupyter_ package was initially written as part of [Google Summer of Code in 2021](https://trac.osgeo.org/grass/wiki/GSoC/2021/JupyterAndGRASS) by Caitlin Haedrich and was experimentally included in version 8.0.0. Caitlin further improved it thanks to the [GRASS Mini Grant 2022](https://trac.osgeo.org/grass/wiki/GSoC/2021/JupyterAndGRASS/MiniGrant2022). The package was officially released for the first time as part of version 8.2.0. Credits for mentoring and additional development go to Vaclav Petras, Helena Mitasova, Stefan Blumentrath, and Anna Petrasova as well as to many members of the GRASS community who provided important feedback.\n", "\n", - "In addition to simplifying the launch of *GRASS GIS* with a dedicated _init_ function, _grass.jupyter_ has two main display classes, _Map_ and _InteractiveMap_. Using the GRASS rendering engine in the background, _Map_ creates maps as PNG images. _InteractiveMap_ displays GRASS rasters and vectors with [*folium*](http://python-visualization.github.io/folium/), a [*leaflet*](https://leafletjs.com/) library for Python. The package includes also _Map3D_ and [_TimeSeriesMap_](temporal.ipynb).\n", + "In addition to simplifying the launch of *GRASS GIS* with a dedicated _init_ function, _grass.jupyter_ has two main display classes, _Map_ and _InteractiveMap_. Using the GRASS rendering engine in the background, _Map_ creates maps as PNG images. _InteractiveMap_ displays GRASS rasters and vectors either with [*folium*](http://python-visualization.github.io/folium/), or [*ipyleaflet*](https://ipyleaflet.readthedocs.io/en/latest/), both are [*leaflet*](https://leafletjs.com/)-based libraries for Python. The package includes also _Map3D_ and [_TimeSeriesMap_](temporal.ipynb).\n", "\n", "This interactive notebook is available online thanks to the [Binder](https://mybinder.org) service. To run the select part (called a *cell*), hit `Shift + Enter`." ] @@ -210,7 +210,7 @@ "source": [ "## Interactive Map Display\n", "\n", - "The `InteractiveMap` class displays *GRASS GIS* rasters and vectors with [*folium*](http://python-visualization.github.io/folium/), a [*leaflet*](https://leafletjs.com/) library for *Python*." + "The `InteractiveMap` class displays *GRASS GIS* rasters and vectors with [*folium*](http://python-visualization.github.io/folium/) or [*ipyleaflet*](https://ipyleaflet.readthedocs.io/en/latest/)." ] }, { @@ -220,7 +220,7 @@ "outputs": [], "source": [ "# Create Interactive Map\n", - "raleigh_map = gj.InteractiveMap(width = 600)" + "raleigh_map = gj.InteractiveMap(width=600)" ] }, { @@ -232,7 +232,7 @@ "# Add raster, vector and layer control to map\n", "raleigh_map.add_raster(\"elevation\")\n", "raleigh_map.add_vector(\"roadsmajor\")\n", - "raleigh_map.add_layer_control(position = \"bottomright\")" + "raleigh_map.add_layer_control(position=\"bottomright\")" ] }, { @@ -279,11 +279,9 @@ "source": [ "import folium \n", "\n", - "# Create figure\n", - "fig = folium.Figure(width=600, height=400)\n", "\n", - "# Create a map to add to the figure later\n", - "m = folium.Map(tiles=\"Stamen Terrain\", location=[35.761168,-78.668271], zoom_start=13)\n", + "# Create a map\n", + "m = folium.Map(location=[35.761168,-78.668271], zoom_start=13)\n", "\n", "# Create and add elevation layer to map\n", "gj.Raster(\"elevation\", opacity=0.5).add_to(m)\n", @@ -296,11 +294,45 @@ " [35.781608,-78.675800], popup=\"Point of Interest\", tooltip=tooltip\n", ").add_to(m)\n", "\n", - "# Add the map to the figure\n", - "fig.add_child(m)\n", + "# Display map\n", + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## GRASS-ipyleaflet Integration\n", + "\n", + "We can also pass GRASS rasters and vectors directly to ipyleaflet with the Raster and Vector classes. This provides much more flexibility when creating maps since we can access all of ipyleaflet's capabilities." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ipyleaflet \n", + "\n", + "# Create map\n", + "m = ipyleaflet.Map(center=[35.761168,-78.668271], zoom=13)\n", + "\n", + "# Create and add elevation layer to map\n", + "gj.Raster(\"elevation\", opacity=0.5).add_to(m)\n", + "\n", + "# Do some cool ipyleaflet stuff!\n", + "# Like make a tooltip\n", + "title = \"Click me!\"\n", + "# and add a marker\n", + "marker = ipyleaflet.Marker(name='marker', location=(35.781608,-78.675800), title=title)\n", + "\n", + "# Add the marker to the map\n", + "m.add(marker)\n", "\n", - "# Display figure\n", - "fig" + "control = ipyleaflet.LayersControl(position='topright')\n", + "m.add(control)\n", + "m" ] }, { diff --git a/gui/wxpython/gmodeler/model.py b/gui/wxpython/gmodeler/model.py index 4789ba156a6..4bf278ac43d 100644 --- a/gui/wxpython/gmodeler/model.py +++ b/gui/wxpython/gmodeler/model.py @@ -724,7 +724,7 @@ def Run(self, log, onDone, parent=None): # split condition # TODO: this part needs some better solution condVar, condText = map( - lambda x: x.strip(), re.split("\s* in \s*", cond) + lambda x: x.strip(), re.split(r"\s* in \s*", cond) ) pattern = re.compile("%" + condVar) # for vars()[condVar] in eval(condText): ? @@ -2578,7 +2578,7 @@ def _writeItem(self, item, ignoreBlock=True, variables={}): cond = pattern.sub(value, cond) if isinstance(item, ModelLoop): condVar, condText = map( - lambda x: x.strip(), re.split("\s* in \s*", cond) + lambda x: x.strip(), re.split(r"\s* in \s*", cond) ) cond = "%sfor %s in " % (" " * self.indent, condVar) if condText[0] == "`" and condText[-1] == "`": @@ -3362,7 +3362,7 @@ def _substituteVariable(self, string, variable, data): :return: modified string """ result = "" - ss = re.split("\w*(%" + variable + ")w*", string) + ss = re.split(r"\w*(%" + variable + ")w*", string) if not ss[0] and not ss[-1]: if data: diff --git a/gui/wxpython/modules/mcalc_builder.py b/gui/wxpython/modules/mcalc_builder.py index bbd77b07ff4..47c317fb937 100644 --- a/gui/wxpython/modules/mcalc_builder.py +++ b/gui/wxpython/modules/mcalc_builder.py @@ -581,7 +581,7 @@ def _getCommand(self): if self.overwrite.IsChecked(): overwrite = " --overwrite" seed_flag = seed = "" - if re.search(pattern="rand *\(.+\)", string=expr): + if re.search(pattern=r"rand *\(.+\)", string=expr): if self.randomSeed.IsChecked(): seed_flag = " -s" else: @@ -624,7 +624,7 @@ def _addSomething(self, what): self.text_mcalc.SetValue(newmcalcstr) if len(what) > 0: - match = re.search(pattern="\(.*\)", string=what) + match = re.search(pattern=r"\(.*\)", string=what) if match: position_offset += match.start() + 1 else: @@ -665,7 +665,7 @@ def OnMCalcRun(self, event): return seed_flag = seed = None - if re.search(pattern="rand *\(.+\)", string=expr): + if re.search(pattern=r"rand *\(.+\)", string=expr): if self.randomSeed.IsChecked(): seed_flag = "-s" else: diff --git a/gui/wxpython/web_services/widgets.py b/gui/wxpython/web_services/widgets.py index 43119415aa2..e1cf5e84dd8 100644 --- a/gui/wxpython/web_services/widgets.py +++ b/gui/wxpython/web_services/widgets.py @@ -1000,7 +1000,7 @@ def addlayer(layer, item): # self.ExpandAll(self.GetRootItem()) def GetSelectedLayers(self): - """Get selected layers/styles in LayersList + r"""Get selected layers/styles in LayersList :return: dict with these items: * 'name' : layer name used for request diff --git a/python/grass/gunittest/gmodules.py b/python/grass/gunittest/gmodules.py index be19ff7d4b8..c105fcfa12d 100644 --- a/python/grass/gunittest/gmodules.py +++ b/python/grass/gunittest/gmodules.py @@ -19,7 +19,7 @@ class SimpleModule(Module): - """Simple wrapper around pygrass.modules.Module to make sure that + r"""Simple wrapper around pygrass.modules.Module to make sure that run\_, finish\_, stdout and stderr are set correctly. >>> mapcalc = SimpleModule('r.mapcalc', expression='test_a = 1', diff --git a/python/grass/jupyter/interactivemap.py b/python/grass/jupyter/interactivemap.py index ffd32e222e0..da06582bfba 100644 --- a/python/grass/jupyter/interactivemap.py +++ b/python/grass/jupyter/interactivemap.py @@ -1,27 +1,40 @@ # # AUTHOR(S): Caitlin Haedrich +# Anna Petrasova # # PURPOSE: This module contains functions for interactive visualizations # in Jupyter Notebooks. # -# COPYRIGHT: (C) 2021-2022 Caitlin Haedrich, and by the GRASS Development Team +# COPYRIGHT: (C) 2021-2024 Caitlin Haedrich, and by the GRASS Development Team # # This program is free software under the GNU General Public # License (>=v2). Read the file COPYING that comes with GRASS # for details. -"""Interactive visualizations map with folium""" +"""Interactive visualizations map with folium or ipyleaflet""" +import base64 +import json from .reprojection_renderer import ReprojectionRenderer -class Raster: - """Overlays rasters on folium maps. +def get_backend(interactive_map): + """Identifies if interactive_map is of type folium.Map + or ipyleaflet.Map. Returns "folium" or "ipyleaflet". + """ + try: + import folium # pylint: disable=import-outside-toplevel + except ImportError: + return "ipyleaflet" + isfolium = isinstance(interactive_map, folium.Map) + if isfolium: + return "folium" + return "ipyleaflet" - Basic Usage: - >>> m = folium.Map() - >>> gj.Raster("elevation", opacity=0.5).add_to(m) - >>> m + +class Layer: # pylint: disable=too-few-public-methods + """Base class for overlaing raster or vector layer + on a folium or ipyleaflet map. """ def __init__( @@ -35,19 +48,15 @@ def __init__( ): """Reproject GRASS raster, export to PNG, and compute bounding box. - param str name: raster name - param str title: title of raster to display in layer control legend + param str name: layer name + param str title: title of layer to display in layer control legend param bool use_region: use computational region of current mapset param str saved_region: name of saved computation region param renderer: instance of ReprojectionRenderer - **kwargs: keyword arguments passed to folium.raster_layers.ImageOverlay() + **kwargs: keyword arguments passed to folium/ipyleaflet layer instance """ - import folium # pylint: disable=import-outside-toplevel - - self._folium = folium - self._name = name - self._overlay_kwargs = kwargs + self._layer_kwargs = kwargs self._title = title if not self._title: self._title = self._name @@ -58,36 +67,78 @@ def __init__( ) else: self._renderer = renderer + + +class Raster(Layer): + """Overlays rasters on a folium or ipyleaflet map. + + Basic Usage: + >>> m = folium.Map() + >>> gj.Raster("elevation", opacity=0.5).add_to(m) + >>> m + + >>> m = ipyleaflet.Map() + >>> gj.Raster("elevation", opacity=0.5).add_to(m) + >>> m + """ + + def __init__( + self, + name, + title=None, + use_region=False, + saved_region=None, + renderer=None, + **kwargs, + ): + """Reproject GRASS raster, export to PNG, and compute bounding box.""" + super().__init__(name, title, use_region, saved_region, renderer, **kwargs) # Render overlay # By doing this here instead of in add_to, we avoid rendering # twice if added to multiple maps. This mimics the behavior # folium.raster_layers.ImageOverlay() self._filename, self._bounds = self._renderer.render_raster(name) - def add_to(self, folium_map): - """Add raster to folium map with folium.raster_layers.ImageOverlay() - - A folium map is an instance of folium.Map. - """ - # Overlay image on folium map - img = self._folium.raster_layers.ImageOverlay( - image=self._filename, - bounds=self._bounds, - name=self._title, - **self._overlay_kwargs, - ) - - # Add image to map - img.add_to(folium_map) + def add_to(self, interactive_map): + """Add raster to map object which is an instance of either + folium.Map or ipyleaflet.Map""" + if get_backend(interactive_map) == "folium": + import folium # pylint: disable=import-outside-toplevel + + # Overlay image on folium map + image = folium.raster_layers.ImageOverlay( + image=self._filename, + bounds=self._bounds, + name=self._title, + **self._layer_kwargs, + ) + image.add_to(interactive_map) + else: + import ipyleaflet # pylint: disable=import-outside-toplevel + + # ImageOverlays don't work well with local files, + # they need relative address and behavior differs + # for notebooks and jupyterlab + with open(self._filename, "rb") as file: + data = base64.b64encode(file.read()).decode("ascii") + url = "data:image/png;base64," + data + image = ipyleaflet.ImageOverlay( + url=url, bounds=self._bounds, name=self._title, **self._layer_kwargs + ) + interactive_map.add(image) -class Vector: - """Adds vectors to a folium map. +class Vector(Layer): + """Adds vectors to a folium or ipyleaflet map. Basic Usage: >>> m = folium.Map() >>> gj.Vector("roadsmajor").add_to(m) >>> m + + >>> m = ipyleaflet.Map() + >>> gj.Vector("roadsmajor").add_to(m) + >>> m """ def __init__( @@ -99,43 +150,39 @@ def __init__( renderer=None, **kwargs, ): - """Reproject GRASS vector and export to folium-ready PNG. Also computes bounding - box for PNG overlay in folium map. - - param str name: vector name - param str title: title of vector to display in layer control legend - param bool use_region: use computational region of current mapset - param str saved_region: name of saved computation region - renderer: instance of ReprojectionRenderer - **kwargs: keyword arguments passed to folium.GeoJson() - """ - import folium # pylint: disable=import-outside-toplevel + """Reproject GRASS vector and export to GeoJSON.""" + super().__init__(name, title, use_region, saved_region, renderer, **kwargs) + self._filename = self._renderer.render_vector(name) - self._folium = folium + def add_to(self, interactive_map): + """Add vector to map""" + if get_backend(interactive_map) == "folium": + import folium # pylint: disable=import-outside-toplevel - self._name = name - self._title = title - if not self._title: - self._title = self._name - self._geojson_kwargs = kwargs - - if not renderer: - self._renderer = ReprojectionRenderer( - use_region=use_region, saved_region=saved_region - ) + folium.GeoJson( + str(self._filename), name=self._title, **self._layer_kwargs + ).add_to(interactive_map) else: - self._renderer = renderer - self._filename = self._renderer.render_vector(name) - - def add_to(self, folium_map): - """Add vector to folium map with folium.GeoJson()""" - self._folium.GeoJson( - str(self._filename), name=self._title, **self._geojson_kwargs - ).add_to(folium_map) + import ipyleaflet # pylint: disable=import-outside-toplevel + + with open(self._filename, "r", encoding="utf-8") as file: + data = json.load(file) + # allow using opacity directly to keep interface + # consistent for both backends + if "opacity" in self._layer_kwargs: + opacity = self._layer_kwargs.pop("opacity") + if "style" in self._layer_kwargs: + self._layer_kwargs["style"]["opacity"] = opacity + else: + self._layer_kwargs["style"] = {"opacity": opacity} + geo_json = ipyleaflet.GeoJSON( + data=data, name=self._title, **self._layer_kwargs + ) + interactive_map.add(geo_json) class InteractiveMap: - """This class creates interactive GRASS maps with folium. + """This class creates interactive GRASS maps with folium or ipyleaflet. Basic Usage: @@ -154,16 +201,24 @@ def __init__( API_key=None, # pylint: disable=invalid-name use_region=False, saved_region=None, + map_backend=None, ): - """Creates a blank folium map centered on g.region. + """Creates a blank folium/ipyleaflet map centered on g.region. + + If map_backend is not specified, InteractiveMap tries to import + ipyleaflet first, then folium if it fails. The backend can be + specified explicitely with valid values "folium" and "ipyleaflet" . - tiles parameter is passed directly to folium.Map() which supports several - built-in tilesets (including "OpenStreetMap", "Stamen Toner", "Stamen Terrain", + In case of folium backend, tiles parameter is passed directly + to folium.Map() which supports several built-in tilesets + (including "OpenStreetMap", "Stamen Toner", "Stamen Terrain", "Stamen Watercolor", "Mapbox Bright", "Mapbox Control Room", "CartoDB positron", "CartoDB dark_matter") as well as custom tileset URL (i.e. "http://{s}.yourtiles.com/{z}/{x}/{y}.png"). For more information, visit folium documentation: https://python-visualization.github.io/folium/modules.html + In case of ipyleaflet, only the tileset name and not the URL is + currently supported. Raster and vector data are always reprojected to Pseudo-Mercator. With use_region=True or saved_region=myregion, the region extent @@ -181,22 +236,69 @@ def __init__( :param str API_key: API key for Mapbox or Cloudmade tiles :param bool use_region: use computational region of current mapset :param str saved_region: name of saved computation region + :param str map_backend: "ipyleaflet" or "folium" or None """ - import folium # pylint: disable=import-outside-toplevel + self._ipyleaflet = None + self._folium = None + + def _import_folium(error): + try: + import folium # pylint: disable=import-outside-toplevel + + return folium + except ImportError as err: + if error: + raise err + return None + + def _import_ipyleaflet(error): + try: + import ipyleaflet # pylint: disable=import-outside-toplevel + + return ipyleaflet + except ImportError as err: + if error: + raise err + return None + + if not map_backend: + self._ipyleaflet = _import_ipyleaflet(error=False) + if not self._ipyleaflet: + self._folium = _import_folium(error=False) + if not (self._ipyleaflet or self._folium): + raise ImportError(_("Neither ipyleaflet nor folium found.")) + elif map_backend == "folium": + self._folium = _import_folium(error=True) + + elif map_backend == "ipyleaflet": + self._ipyleaflet = _import_ipyleaflet(error=True) + else: + raise ValueError(_("Invalid map backend, use 'folium' or 'ipyleaflet'")) - self._folium = folium + if self._ipyleaflet: + import ipywidgets as widgets # pylint: disable=import-outside-toplevel + import xyzservices # pylint: disable=import-outside-toplevel # Store height and width self.width = width self.height = height - # Create Folium Map - self.map = self._folium.Map( - width=self.width, - height=self.height, - tiles=tiles, - API_key=API_key, # pylint: disable=invalid-name - ) + if self._ipyleaflet: + basemap = xyzservices.providers.query_name(tiles) + if API_key and basemap.get("accessToken"): + basemap["accessToken"] = API_key + layout = widgets.Layout(width=f"{width}px", height=f"{height}px") + self.map = self._ipyleaflet.Map( + basemap=basemap, layout=layout, scroll_wheel_zoom=True + ) + + else: + self.map = self._folium.Map( + width=self.width, + height=self.height, + tiles=tiles, + API_key=API_key, # pylint: disable=invalid-name + ) # Set LayerControl default self.layer_control = False self.layer_control_object = None @@ -207,18 +309,18 @@ def __init__( def add_vector(self, name, title=None, **kwargs): """Imports vector into temporary WGS84 location, re-formats to a GeoJSON and - adds to folium map. + adds to map. :param str name: name of vector to be added to map; positional-only parameter :param str title: vector name for layer control - :**kwargs: keyword arguments passed to folium.GeoJson() + :**kwargs: keyword arguments passed to GeoJSON overlay """ Vector(name, title=title, renderer=self._renderer, **kwargs).add_to(self.map) def add_raster(self, name, title=None, **kwargs): """Imports raster into temporary WGS84 location, - exports as png and overlays on folium map. + exports as png and overlays on a map. Color table for the raster can be modified with `r.colors` before calling this function. @@ -230,34 +332,40 @@ def add_raster(self, name, title=None, **kwargs): :param str name: name of raster to add to display; positional-only parameter :param str title: raster name for layer control - :**kwargs: keyword arguments passed to folium.raster_layers.ImageOverlay() + :**kwargs: keyword arguments passed to image overlay """ Raster(name, title=title, renderer=self._renderer, **kwargs).add_to(self.map) def add_layer_control(self, **kwargs): """Add layer control to display" - Accepts keyword arguments to be passed to folium.LayerControl()""" + Accepts keyword arguments to be passed to layer control object""" self.layer_control = True - self.layer_control_object = self._folium.LayerControl(**kwargs) + if self._folium: + self.layer_control_object = self._folium.LayerControl(**kwargs) + else: + self.layer_control_object = self._ipyleaflet.LayersControl(**kwargs) def show(self): - """This function returns a folium figure object with a GRASS raster - overlaid on a basemap. + """This function returns a folium figure or ipyleaflet map object + with a GRASS raster and/or vector overlaid on a basemap. If map has layer control enabled, additional layers cannot be added after calling show().""" - if self.layer_control: - self.map.add_child(self.layer_control_object) - # Create Figure - fig = self._folium.Figure(width=self.width, height=self.height) - # Add map to figure - fig.add_child(self.map) self.map.fit_bounds(self._renderer.get_bbox()) - - return fig + if self._folium: + if self.layer_control: + self.map.add_child(self.layer_control_object) + fig = self._folium.Figure(width=self.width, height=self.height) + fig.add_child(self.map) + + return fig + # ipyleaflet + if self.layer_control: + self.map.add(self.layer_control_object) + return self.map def save(self, filename): """Save map as an html map. diff --git a/python/grass/pygrass/modules/interface/module.py b/python/grass/pygrass/modules/interface/module.py index b8f49f5b888..57b4c235302 100644 --- a/python/grass/pygrass/modules/interface/module.py +++ b/python/grass/pygrass/modules/interface/module.py @@ -221,7 +221,7 @@ def __init__(self, nprocs=1): self._finished_modules = [] # Store all processed modules in a list def put(self, module): - """Put the next Module or MultiModule object in the queue + r"""Put the next Module or MultiModule object in the queue To run the Module objects in parallel the run\_ and finish\_ options of the Module must be set to False. diff --git a/python/grass/pygrass/modules/interface/parameter.py b/python/grass/pygrass/modules/interface/parameter.py index 8d58165d3ee..7ab67c186ad 100644 --- a/python/grass/pygrass/modules/interface/parameter.py +++ b/python/grass/pygrass/modules/interface/parameter.py @@ -176,7 +176,8 @@ def __init__(self, xparameter=None, diz=None): try: # Check for integer ranges: "3-30" or float ranges: "0.0-1.0" isrange = re.match( - "(?P-*\d+.*\d*)*-(?P\d+.*\d*)*", diz["values"][0] + r"(?P-?(?:\d*\.)?\d+)?-(?P-?(?:\d*\.)?\d+)?", + diz["values"][0], ) if isrange: mn, mx = isrange.groups() diff --git a/python/grass/pygrass/vector/geometry.py b/python/grass/pygrass/vector/geometry.py index f5d7cd3580e..81d9f3a8d78 100644 --- a/python/grass/pygrass/vector/geometry.py +++ b/python/grass/pygrass/vector/geometry.py @@ -24,8 +24,8 @@ LineDist = namedtuple("LineDist", "point dist spdist sldist") WKT = { - "POINT\((.*)\)": "point", # 'POINT\(\s*([+-]*\d+\.*\d*)+\s*\)' - "LINESTRING\((.*)\)": "line", + r"POINT\((.*)\)": "point", # 'POINT\(\s*([+-]*\d+\.*\d*)+\s*\)' + r"LINESTRING\((.*)\)": "line", } @@ -1135,7 +1135,7 @@ def from_wkt(self, wkt): .. """ - match = re.match("LINESTRING\((.*)\)", wkt) + match = re.match(r"LINESTRING\((.*)\)", wkt) if match: self.reset() for coord in match.groups()[0].strip().split(","): @@ -1682,7 +1682,7 @@ def isles(self, isles=None): @mapinfo_must_be_set def area(self): - """Returns area of area without areas of isles. + r"""Returns area of area without areas of isles. double Vect_get_area_area (const struct Map_info \*Map, int area) """ return libvect.Vect_get_area_area(self.c_mapinfo, self.id) @@ -1763,7 +1763,7 @@ def buffer( @mapinfo_must_be_set def boundaries(self, ilist=False): - """Creates list of boundaries for given area. + r"""Creates list of boundaries for given area. int Vect_get_area_boundaries(const struct Map_info \*Map, int area, struct ilist \*List) @@ -1802,7 +1802,7 @@ def cats(self, cats=None): return cats def get_first_cat(self): - """Find FIRST category of given field and area. + r"""Find FIRST category of given field and area. int Vect_get_area_cat(const struct Map_info \*Map, int area, int field) @@ -1828,7 +1828,7 @@ def contains_point(self, point, bbox=None): @mapinfo_must_be_set def perimeter(self): - """Calculate area perimeter. + r"""Calculate area perimeter. :return: double Vect_area_perimeter (const struct line_pnts \*Points) diff --git a/python/grass/script/task.py b/python/grass/script/task.py index 31102835db4..1c4e29f69a6 100644 --- a/python/grass/script/task.py +++ b/python/grass/script/task.py @@ -446,7 +446,7 @@ def convert_xml_to_utf8(xml_text): # modify: fetch encoding from the interface description text(xml) # e.g. - pattern = re.compile(b'<\?xml[^>]*\Wencoding="([^"]*)"[^>]*\?>') + pattern = re.compile(rb'<\?xml[^>]*\Wencoding="([^"]*)"[^>]*\?>') m = re.match(pattern, xml_text) if m is None: return xml_text.encode("utf-8") if xml_text else None diff --git a/python/grass/temporal/abstract_space_time_dataset.py b/python/grass/temporal/abstract_space_time_dataset.py index a193a1a926d..1289fa8c00d 100644 --- a/python/grass/temporal/abstract_space_time_dataset.py +++ b/python/grass/temporal/abstract_space_time_dataset.py @@ -1630,8 +1630,8 @@ def leading_zero(value): shortcut_identifier = leading_zero(self.semantic_label) if shortcut_identifier: where += ( - "{br} LIKE '{si}\_%' {esc} OR {br} LIKE '%\_{si}' {esc} OR " - "{br} LIKE '{orig}\_%' {esc} OR {br} LIKE '%\_{orig}' {esc}".format( + "{br} LIKE '{si}\\_%' {esc} OR {br} LIKE '%\\_{si}' {esc} OR " + "{br} LIKE '{orig}\\_%' {esc} OR {br} LIKE '%\\_{orig}' {esc}".format( br="semantic_label", si=shortcut_identifier, orig=self.semantic_label.upper(),