Skip to content

Commit 420720a

Browse files
updates
1 parent 64d95e5 commit 420720a

File tree

3 files changed

+112
-161
lines changed

3 files changed

+112
-161
lines changed

python/grass/jupyter/baseseriesmap.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from .utils import get_number_of_cores, save_gif
2929

3030

31+
3132
class BaseSeriesMap:
3233
"""
3334
Base class for SeriesMap and TimeSeriesMap
@@ -56,10 +57,10 @@ def __init__(self, width=None, height=None, env=None):
5657
self._width = width
5758
self._height = height
5859
self._slider_description = ""
60+
self._legend = None # Shared legend settings for all timesteps/frames
5961
self._labels = []
6062
self._indices = []
6163
self.base_file = None
62-
6364
# Create a temporary directory for our PNG images
6465
# Resource managed by weakref.finalize.
6566
self._tmpdir = (
@@ -98,12 +99,29 @@ def wrapper(**kwargs):
9899
self._base_calls.append((grass_module, kwargs))
99100

100101
return wrapper
102+
103+
def d_legend(self, **kwargs):
104+
"""Display legend for all timesteps/frames."""
105+
self._legend = kwargs
106+
self._layers_rendered = False # Force re-render
107+
101108

102109
def _render_baselayers(self, img):
103110
"""Add collected baselayers to Map instance"""
104111
for grass_module, kwargs in self._base_layer_calls:
105112
img.run(grass_module, **kwargs)
106113

114+
def _apply_overlays(self, img):
115+
"""Apply overlay commands (d.vect, d.barscale) to map instance"""
116+
for grass_module, kwargs in self._base_calls:
117+
img.run(grass_module, **kwargs)
118+
119+
def _apply_legend(self, img):
120+
"""Apply legend to a map instance"""
121+
if self._legend:
122+
img.d_legend(**self._legend)
123+
124+
107125
def _render(self, tasks):
108126
"""
109127
Renders the base image for the dataset.
@@ -145,7 +163,7 @@ def _render(self, tasks):
145163
for i, filename in results:
146164
self._base_filename_dict[i] = filename
147165

148-
self._layers_rendered = True
166+
self._layers_rendered = True
149167

150168
def show(self, slider_width=None):
151169
"""Create interactive timeline slider.

python/grass/jupyter/seriesmap.py

+33-60
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from .region import RegionManagerForSeries
2323
from .baseseriesmap import BaseSeriesMap
2424

25-
2625
class SeriesMap(BaseSeriesMap):
2726
"""Creates visualizations from a series of rasters or vectors in Jupyter
2827
Notebooks.
@@ -79,25 +78,16 @@ def add_rasters(self, rasters, **kwargs):
7978
for raster in rasters:
8079
if not map_exists(name=raster, element="raster"):
8180
raise NameError(_("Could not find a raster named {}").format(raster))
82-
# Update region to rasters if not use_region or saved_region
83-
self._region_manager.set_region_from_rasters(rasters)
84-
if self._baseseries_added:
85-
assert self.baseseries == len(rasters), _(
86-
"Number of vectors in series must match number of vectors"
87-
)
88-
for i in range(self.baseseries):
89-
kwargs["map"] = rasters[i]
90-
self._base_calls[i].append(("d.rast", kwargs.copy()))
91-
else:
92-
self.baseseries = len(rasters)
93-
for raster in rasters:
94-
kwargs["map"] = raster
95-
self._base_calls.append([("d.rast", kwargs.copy())])
96-
self._baseseries_added = True
97-
if not self._labels:
98-
self._labels = rasters
81+
# Update region
82+
self._region_manager.set_region_from_rasters(rasters)
83+
# Add overlays via BaseSeriesMap
84+
for raster in rasters:
85+
self.d_rast(map=raster, **kwargs)
86+
87+
# Set labels and indices
88+
self._labels = rasters
89+
self._indices = list(range(len(rasters)))
9990
self._layers_rendered = False
100-
self._indices = list(range(len(self._labels)))
10191

10292
def add_vectors(self, vectors, **kwargs):
10393
"""
@@ -106,62 +96,45 @@ def add_vectors(self, vectors, **kwargs):
10696
for vector in vectors:
10797
if not map_exists(name=vector, element="vector"):
10898
raise NameError(_("Could not find a vector named {}").format(vector))
109-
# Update region extent to vectors if not use_region or saved_region
99+
# Update region
110100
self._region_manager.set_region_from_vectors(vectors)
111-
if self._baseseries_added:
112-
assert self.baseseries == len(vectors), _(
113-
"Number of rasters in series must match number of vectors"
114-
)
115-
for i in range(self.baseseries):
116-
kwargs["map"] = vectors[i]
117-
self._base_calls[i].append(("d.vect", kwargs.copy()))
118-
else:
119-
self.baseseries = len(vectors)
120-
for vector in vectors:
121-
kwargs["map"] = vector
122-
self._base_calls.append([("d.vect", kwargs.copy())])
123-
self._baseseries_added = True
124-
if not self._labels:
125-
self._labels = vectors
101+
# Add overlays via BaseSeriesMap
102+
for vector in vectors:
103+
self.d_vect(map=vector, **kwargs)
104+
105+
# Set labels and indices
106+
self._labels = vectors
107+
self._indices = list(range(len(vectors)))
126108
self._layers_rendered = False
127-
self._indices = range(len(self._labels))
128109

129110
def add_names(self, names):
130-
"""Add list of names associated with layers.
131-
Default will be names of first series added."""
132-
assert self.baseseries == len(names), _(
133-
"Number of vectors in series must match number of vectors"
134-
)
111+
"""Add list of names associated with layers."""
112+
assert len(self._labels) == len(names), "Names must match layer count"
135113
self._labels = names
136-
self._indices = list(range(len(self._labels)))
137114

138115
def _render_worker(self, i):
139-
"""Function to render a single layer."""
116+
"""Render a single layer with overlays/legends"""
140117
filename = os.path.join(self._tmpdir.name, f"{i}.png")
141-
shutil.copyfile(self.base_file, filename)
142118
img = Map(
143119
width=self._width,
144120
height=self._height,
145121
filename=filename,
146122
use_region=True,
147123
env=self._env,
148-
read_file=True,
124+
read_file=False,
149125
)
150-
for grass_module, kwargs in self._base_calls[i]:
151-
img.run(grass_module, **kwargs)
152-
return i, filename
126+
img.d_erase()
127+
128+
# Apply overlays and legend via BaseSeriesMap
129+
self._apply_overlays(img)
130+
self._apply_legend(img)
131+
132+
img.save(filename)
133+
return (i, filename)
153134

154135
def render(self):
155-
"""Renders image for each raster in series.
156-
157-
Save PNGs to temporary directory. Must be run before creating a visualization
158-
(i.e. show or save).
159-
"""
136+
"""Renders image for each raster in series."""
160137
if not self._baseseries_added:
161-
msg = (
162-
"Cannot render series since none has been added."
163-
"Use SeriesMap.add_rasters() or SeriesMap.add_vectors()"
164-
)
165-
raise RuntimeError(msg)
166-
tasks = [(i,) for i in range(self.baseseries)]
167-
self._render(tasks)
138+
raise RuntimeError("Add series using add_rasters() or add_vectors()")
139+
tasks = [(i,) for i in range(len(self._labels))]
140+
self._render(tasks)

python/grass/jupyter/timeseriesmap.py

+59-99
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,6 @@ def add_raster_series(self, baseseries, fill_gaps=False):
175175
self.baseseries = baseseries
176176
self._fill_gaps = fill_gaps
177177
self._baseseries_added = True
178-
179-
self._base_calls = []
180-
self._legend = None
181178
# create list of layers to render and date/times
182179
self._layers, self._labels = collect_layers(
183180
self.baseseries, self._element_type, self._fill_gaps
@@ -202,9 +199,6 @@ def add_vector_series(self, baseseries, fill_gaps=False):
202199
self.baseseries = baseseries
203200
self._fill_gaps = fill_gaps
204201
self._baseseries_added = True
205-
# NEW CODE: Reset overlays and legend when adding a new series
206-
self._base_calls = []
207-
self._legend = None
208202
# create list of layers to render and date/times
209203
self._layers, self._labels = collect_layers(
210204
self.baseseries, self._element_type, self._fill_gaps
@@ -217,108 +211,74 @@ def add_vector_series(self, baseseries, fill_gaps=False):
217211
self._indices = self._labels
218212

219213
def d_legend(self, **kwargs):
220-
"""Display legend.
221-
222-
Wraps d.legend and uses same keyword arguments.
214+
"""Display legend for all timesteps.
215+
216+
Wraps `d.legend` and uses the same keyword arguments.
223217
"""
224-
if "raster" in kwargs and not self._baseseries_added:
225-
self._base_layer_calls.append(("d.legend", kwargs))
226-
if "raster" in kwargs and self._baseseries_added:
227-
self._base_calls.append(("d.legend", kwargs))
228-
else:
229-
self._legend = kwargs
230-
# If d_legend has been called, we need to re-render layers
231-
self._layers_rendered = False
232-
233-
def _render_legend(self, img):
234-
"""Add legend to Map instance"""
235-
info = gs.parse_command(
236-
"t.info", input=self.baseseries, flags="g", env=self._env
237-
)
238-
min_min = info["min_min"]
239-
max_max = info["max_max"]
240-
img.d_legend(
241-
raster=self._layers[0],
242-
range=f"{min_min}, {max_max}",
243-
**self._legend,
244-
)
218+
self._legend = kwargs
219+
self._layers_rendered = False
245220

246-
def _render_overlays(self, img):
247-
"""Add collected overlays to Map instance"""
248-
for grass_module, kwargs in self._base_calls:
249-
img.run(grass_module, **kwargs)
250-
251-
def _render_blank_layer(self, filename):
252-
"""Write blank image for gaps in time series.
253-
254-
Adds overlays and legend to base map.
255-
"""
256-
img = Map(
257-
width=self._width,
258-
height=self._height,
259-
filename=filename,
260-
use_region=True,
261-
env=self._env,
262-
read_file=True,
263-
)
264-
# Add overlays
265-
self._render_overlays(img)
266-
# Add legend if needed
221+
def _apply_legend(self, img):
222+
"""Override BaseSeriesMap to set dynamic legend range for time series."""
267223
if self._legend:
268-
self._render_legend(img)
269-
270-
def _render_layer(self, layer, filename):
271-
"""Render layer to file with overlays and legend"""
272-
img = Map(
273-
width=self._width,
274-
height=self._height,
275-
filename=filename,
276-
use_region=True,
277-
env=self._env,
278-
read_file=True,
279-
)
280-
281-
282-
img.run("d.erase", flags="f")
224+
info = gs.parse_command(
225+
"t.info", input=self.baseseries, flags="g", env=self._env
226+
)
227+
min_min = info["min_min"]
228+
max_max = info["max_max"]
229+
img.d_legend(
230+
raster=self._layers[0], # Use first layer for range
231+
range=f"{min_min}, {max_max}",
232+
**self._legend,
233+
)
283234

235+
def _render_worker(self, date, layer, filename):
236+
"""Render a timestep with overlays/legends"""
237+
img = Map(
238+
width=self._width,
239+
height=self._height,
240+
filename=filename,
241+
use_region=True,
242+
env=self._env,
243+
read_file=False,
244+
)
245+
img.d_erase()
246+
247+
# Render base layer only if not "None"
248+
if layer != "None":
284249
if self._element_type == "strds":
285250
img.d_rast(map=layer)
286251
elif self._element_type == "stvds":
287252
img.d_vect(map=layer)
288-
# Add overlays
289-
self._render_overlays(img)
290-
# Add legend if needed
291-
if self._legend:
292-
self._render_legend(img)
293253

294-
def _render_worker(self, date, layer, filename):
295-
"""Function to render a single layer."""
296-
shutil.copyfile(self.base_file, filename)
297-
if layer == "None":
298-
self._render_blank_layer(filename)
299-
else:
300-
self._render_layer(layer, filename)
301-
return date, filename
254+
#Apply overlays/legend to ALL layers (including "None")
255+
self._apply_overlays(img) # Fixed indentation
256+
self._apply_legend(img) # Fixed indentation
257+
258+
img.save(filename)
259+
return (date, filename)
302260

303261
def render(self):
304-
"""Renders image for each time-step in space-time dataset."""
305-
if not self._baseseries_added:
306-
msg = (
307-
"Cannot render space time dataset since none has been added."
308-
" Use TimeSeriesMap.add_raster_series() or "
309-
"TimeSeriesMap.add_vector_series() to add dataset"
262+
"""Renders image for each time-step in space-time dataset."""
263+
if not self._baseseries_added:
264+
msg = (
265+
"Cannot render space time dataset since none has been added."
266+
" Use TimeSeriesMap.add_raster_series() or "
267+
"TimeSeriesMap.add_vector_series() to add dataset"
268+
)
269+
raise RuntimeError(msg)
270+
271+
# Prepare tasks with tuples
272+
tasks = []
273+
for date, layer in self._date_layer_dict.items():
274+
if layer == "None":
275+
# Generate unique filename for each "None" layer
276+
filename = os.path.join(
277+
self._tmpdir.name,
278+
f"none_{gs.append_random('', 8)}.png" # Unique random name
310279
)
311-
raise RuntimeError(msg)
312-
313-
# Create name for empty layers
314-
random_name_none = gs.append_random("none", 8) + ".png"
315-
316-
# Prepare tasks with tuples
317-
tasks = []
318-
for date, layer in self._date_layer_dict.items():
319-
if layer == "None":
320-
filename = os.path.join(self._tmpdir.name, random_name_none)
321-
else:
322-
filename = os.path.join(self._tmpdir.name, f"{layer}.png")
323-
tasks.append((date, layer, filename))
324-
self._render(tasks)
280+
else:
281+
filename = os.path.join(self._tmpdir.name, f"{layer}.png")
282+
283+
tasks.append((date, layer, filename))
284+
self._render(tasks)

0 commit comments

Comments
 (0)