Skip to content
114 changes: 113 additions & 1 deletion python/grass/jupyter/interactivemap.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,118 @@ def add_layer_control(self, **kwargs):
else:
self.layer_control_object = self._ipyleaflet.LayersControl(**kwargs)

def add_legend(self, raster, position="bottomright"):
import subprocess

# Getting all in-built colour scheme for elevations
all_colours = subprocess.check_output(f"r.colors.out map={raster}", shell=True).decode("utf-8").strip()
colors = {}
last_ht = None
for colour in all_colours.split("\n"):
if "nv" in colour or "default" in colour:
continue
elev_col_info = colour.split()
ht = float(elev_col_info[0])
rgb = elev_col_info[1]
label = f"{last_ht:.1f} - {ht:.1f} m" if last_ht else f"≤ {ht:.1f} m"
colors[label] = f"rgb({rgb.replace(':', ',')})"
last_ht = ht

# Position Coordinates
position_map = {
"topright": "top: 5px; right: 5px;",
"topleft": "top: 5px; left: 5px;",
"bottomright": "bottom: 5px; right: 5px;",
"bottomleft": "bottom: 5px; left: 5px;"
}
pos_style = position_map.get(position)

# Creating legend
legend_html = f"""
<div style="
display: flex;
flex-direction: column;
align-items: center;
position: absolute;
{pos_style}
z-index: 9999;
min-width: 130px;
padding: 5px;
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 12px;
color: black;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 12px;
">
<div style="
background: rgba(255, 255, 255, 0.5)
font-size: 14px;
font-weight: bold;
padding-bottom: 6px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
color: black;
">
{raster.capitalize()} Map
</div>
<div style="display: flex; flex-direction: column; gap: 6px;">
"""

for label, color in colors.items():
legend_html += f"""
<div style="display: flex; align-items: center; gap: 10px;">
<div style="
width: 20px;
height: 20px;
background: {color};
border-radius: 4px;
border: 1px solid rgba(255,255,255,0.5);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
"></div>
<span style="color: black;">{label}</span>
</div>
"""

legend_html += """
</div>
</div>
"""

if self._ipyleaflet:
import ipywidgets as widgets
from ipyleaflet import WidgetControl
legend_widget = widgets.HTML(value=legend_html)
control = WidgetControl(widget=legend_widget, position=position)
self.map.add(control)
return self

elif self._folium:
from branca.element import MacroElement
from jinja2 import Template
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wenzeslaus You earlier worried about these dependencies, but both of them are the dependencies of of folium itself See.
Folium has the following dependencies, all of which are installed automatically with the above installation commands:
branca, Jinja2, Numpy ,Requests
However, I avoided branca, as it is not very active but we can use jinja as it is already installed. Folium internally calles Template() method of jinja to create Template object. And this object is essential as it further calls for module attribute associate with this object. So, far it is working fine as expected as per my observation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good. While it is not bulletproof, transitive vs direct dependency makes as difference.


class Legend(MacroElement):
def __init__(self, html):
super().__init__()
self.html = html
self._name = "Legend"

def render(self, **kwargs):
template = Template("""
{% macro html(this, kwargs) %}
{{ this.html|safe }}
{% endmacro %}
""")
return template.module.html(self)

self.map.get_root().add_child(Legend(legend_html))
return self


else:
return self

def setup_drawing_interface(self):
"""Sets up the drawing interface for users
to interactively draw and manage geometries on the map.
Expand Down Expand Up @@ -847,4 +959,4 @@ def clear_popups(self):
"""Clears the popups."""
for item in reversed(list(self.map.layers)):
if isinstance(item, self._ipyleaflet.Popup):
self.map.remove(item)
self.map.remove(item)