diff --git a/datacube_ows/styles/colormap.py b/datacube_ows/styles/colormap.py index c5a56ebe7..bec3ed791 100644 --- a/datacube_ows/styles/colormap.py +++ b/datacube_ows/styles/colormap.py @@ -8,18 +8,74 @@ from matplotlib import patches as mpatches, pyplot as plt from xarray import Dataset, DataArray, merge +from datacube_ows.config_utils import OWSConfigEntry from datacube_ows.styles.base import StyleDefBase _LOG = logging.getLogger(__name__) +class ValueMapRule(OWSConfigEntry): + def __init__(self, style_def, band, cfg): + super().__init__(self, cfg) + self.style = style_def + self.band = style_def.local_band(band) + + self.title = cfg["title"] + self.abstract = cfg.get("abstract") + if self.title and self.abstract: + self.label = f"{self.title} - {self.abstract}" + elif self.title: + self.label = self.title + elif self.abstract: + self.label = self.abstract + else: + self.label = None + + flags = cfg["flags"] + self.or_flags = False + if "or" in flags: + self.or_flags = True + flags = flags["or"] + elif "and" in flags: + flags = flags["and"] + self.flags = flags + self.color_str = cfg["color"] + self.rgb = Color(self.color_str) + + if cfg.get("mask", False): + self.alpha = 0.0 + else: + self.alpha = cfg.get("alpha", 1.0) + + def create_mask(self, data): + if self.or_flags: + mask = None + for f in self.flags.items(): + f = {f[0]: f[1]} + if mask is None: + mask = make_mask(data, **f) + else: + mask |= make_mask(data, **f) + else: + mask = make_mask(data, **self.flags) + return mask + + @classmethod + def value_map_from_config(cls, style, cfg): + vmap = {} + for band_name, rules in cfg.items(): + band_rules = [cls(style, band_name, rule) for rule in rules] + vmap[band_name] = band_rules + return vmap + + class ColorMapStyleDef(StyleDefBase): auto_legend = True def __init__(self, product, style_cfg): super(ColorMapStyleDef, self).__init__(product, style_cfg) style_cfg = self._raw_cfg - self.value_map = style_cfg["value_map"] + self.value_map = ValueMapRule.value_map_from_config(self, style_cfg["value_map"]) for band in self.value_map.keys(): self.raw_needed_bands.add(band) @@ -42,23 +98,6 @@ def create_colordata(data, rgb, alpha, mask): masked = target.where(mask).where(numpy.isfinite(data)) # remask return masked - @staticmethod - def create_mask(data, flags): - if "or" in flags: - fs = flags["or"] - mask = None - for f in fs.items(): - f = {f[0]: f[1]} - if mask is None: - mask = make_mask(data, **f) - else: - mask |= make_mask(data, **f) - else: - fs = flags if "and" not in flags else flags["and"] - mask = make_mask(data, **fs) - return mask - - def transform_single_date_data(self, data): # pylint: disable=too-many-locals, too-many-branches # extent mask data per band to preseve nodata @@ -71,7 +110,7 @@ def transform_single_date_data(self, data): # data[band] = data[band].where(extent_mask) imgdata = Dataset() - for cfg_band, values in self.value_map.items(): + for cfg_band, rules in self.value_map.items(): # Run through each item band = self.product.band_idx.band(cfg_band) band_data = Dataset() @@ -79,22 +118,11 @@ def transform_single_date_data(self, data): if bdata.dtype.kind == 'f': # Convert back to int for bitmasking bdata = ColorMapStyleDef.reint(bdata) - for value in values: - flags = value["flags"] - rgb = Color(value["color"]) - alpha = value.get("alpha", 1.0) - mask_source_band = value.get("mask", False) - - mask = ColorMapStyleDef.create_mask(bdata, flags) - - if mask_source_band: - # disable checking on the use of ~mask - # pylint: disable=invalid-unary-operand-type - bdata = bdata.where(~mask) - bdata = ColorMapStyleDef.reint(bdata) - else: - masked = ColorMapStyleDef.create_colordata(bdata, rgb, alpha, mask) - band_data = masked if len(band_data.data_vars) == 0 else band_data.combine_first(masked) + for rule in rules: + mask = rule.create_mask(bdata) + + masked = ColorMapStyleDef.create_colordata(bdata, rule.rgb, rule.alpha, mask) + band_data = masked if len(band_data.data_vars) == 0 else band_data.combine_first(masked) imgdata = band_data if len(imgdata.data_vars) == 0 else merge([imgdata, band_data]) @@ -104,13 +132,11 @@ def transform_single_date_data(self, data): def single_date_legend(self, bytesio): patches = [] for band in self.value_map.keys(): - for value in self.value_map[band]: - # only include values that have a title set - if "title" in value and "abstract" in value and "color" in value and value["title"]: - rgb = Color(value["color"]) - label = fill(value["title"] + " - " + value["abstract"], 30) + for rule in reversed(self.value_map[band]): + # only include values that are not transparent (and that have a non-blank title or abstract) + if rule.alpha > 0.001 and rule.label: try: - patch = mpatches.Patch(color=rgb.hex_l, label=label) + patch = mpatches.Patch(color=rule.rgb.hex_l, label=rule.label) # pylint: disable=broad-except except Exception as e: print("Error creating patch?", e) diff --git a/docs/cfg_colourmap_styles.rst b/docs/cfg_colourmap_styles.rst index ffb16d467..8d6248ab7 100644 --- a/docs/cfg_colourmap_styles.rst +++ b/docs/cfg_colourmap_styles.rst @@ -68,8 +68,14 @@ match the rule. The ``color`` entry is in html RGB hex format. The ``alpha`` and ``mask`` entries are optional and allow transparency. ``alpha`` should be a floating point number between 0.0 (fully transparent) and 1.0 (fully opaque) and defaults to 1.0 (i.e. fully transparent). The ``mask`` entry is boolean (default -False). Setting ``mask`` to true is functionally equivalent to setting ``alpha`` to -0.0, but uses a more efficient implementation. +False). Setting ``mask`` to true is the same equivalent to setting ``alpha`` to +0.0. (A third option would be to use the standard style +`pq_masks `_. +Bit-flag Masks (pq_masks) + +syntax.) + +to achieve the same effect ``color`` is still required when mask is True, but is not used in this case. diff --git a/tests/test_styles.py b/tests/test_styles.py index dbaafc9e0..e2ded7384 100644 --- a/tests/test_styles.py +++ b/tests/test_styles.py @@ -466,33 +466,34 @@ def style_cfg_map_mask(): "color": "#FFFFFF", }, { - "title": "Non-Transparent", - "abstract": "A Non-Transparent Value", + "title": "Impossible", + "abstract": "Will already have matched a previous rule", "flags": { "bar": 1, }, - "color": "#111111", + "color": "#54d56f", } ] } } return cfg -def test_RBGAMapped_Masking(product_layer_mask_map, style_cfg_map_mask): +def test_RGBAMapped_Masking(product_layer_mask_map, style_cfg_map_mask): def fake_make_mask(data, **kwargs): val = kwargs["bar"] return data == val + dim = np.array([0, 1, 2, 3, 4, 5]) band = np.array([0, 0, 1, 1, 2, 2]) timarray = [np.datetime64(datetime.date.today())] times = DataArray(timarray, coords=[timarray], dims=["time"], name="time") - da = DataArray(band, name='foo') + da = DataArray(band, name='foo', coords={"dim": dim}, dims=["dim"]) dst = Dataset(data_vars={'foo': da}) ds = concat([dst], times) npmap = np.array([True, True, True, True, True, True]) - damap = DataArray(npmap) + damap = DataArray(npmap, coords={"dim": dim}, dims=["dim"]) with patch('datacube_ows.styles.colormap.make_mask', new_callable=lambda: fake_make_mask) as fmm: style_def = datacube_ows.styles.StyleDef(product_layer_mask_map, style_cfg_map_mask) @@ -502,14 +503,14 @@ def fake_make_mask(data, **kwargs): b = data["blue"] a = data["alpha"] - assert (r[2:3:1] == 0) - assert (g[2:3:1] == 0) - assert (b[2:3:1] == 0) - assert (a[2:3:1] == 0) - assert (r[4:5:1] == 255) - assert (g[4:5:1] == 255) - assert (b[4:5:1] == 255) - assert (a[4:5:1] == 255) + assert (r.values[2] == 17) + assert (g.values[2] == 17) + assert (b.values[2] == 17) + assert (a.values[2] == 0) + assert (r.values[4] == 255) + assert (g.values[4] == 255) + assert (b.values[4] == 255) + assert (a.values[4] == 255) def test_reint():