diff --git a/docs/source/cli/xcube_serve.rst b/docs/source/cli/xcube_serve.rst index ff98b3ec5..551e76ab9 100644 --- a/docs/source/cli/xcube_serve.rst +++ b/docs/source/cli/xcube_serve.rst @@ -650,20 +650,12 @@ Both, reversed and alpha blending is possible as well and can be configured by n ColorBar: plasma_r_alpha ValueRange: [0., 24.] -Colormaps may be user-defined within the configuration file. One example is shown below. - - -.. code-block:: yaml - - Styles: - - Identifier: default - ColorMappings: - conc_chl: - CustomColorBar: my_cmap - -The colormap `my_cmap` can then be configured in section -`customcolormaps`_. - +Colormaps may be user-defined within the configuration file, which can be configured +in section `customcolormaps`_. The colormap can be selected by setting +`ColorBar: my_cmap`, where `my_cmap` is the identifier of the custom defined color map. +If the `ValueRange` is given, it overwrites the value range defined in +`CustomColorMaps` if the color map type is continuous or stepwise, and it is ignored +if the color map type is categorical, where a warning is raised. .. _customcolormaps: @@ -739,6 +731,10 @@ For example *CustomColorMaps* can look like this: - [ 1, orange, medium_risk] - [ 2, [1, 0, 0], high_risk] +All colormaps defined in the `CustomColorMaps` section will be available in the xcube +Viewer, even if they haven't been selected in the Styles section for a specific data +variable. + .. _example: Example diff --git a/examples/serve/demo/config.yml b/examples/serve/demo/config.yml index 1e1f7157a..b79edcabc 100644 --- a/examples/serve/demo/config.yml +++ b/examples/serve/demo/config.yml @@ -161,9 +161,10 @@ Styles: - Identifier: default ColorMappings: conc_chl: - CustomColorBar: my_cmap + ColorBar: my_cmap + ValueRange: [0., 20.] # this value range overwrites the range defined in CustomColorMaps chl_category: - CustomColorBar: cmap_bloom_risk + ColorBar: cmap_bloom_risk conc_tsm: ColorFile: cc_tsm.cpd kd489: @@ -220,9 +221,24 @@ CustomColorMaps: - Identifier: cmap_bloom_risk Type: categorical Colors: - - [ 0, [0, 1, 0., 0.5], low_risk] - - [ 1, orange, medium_risk] - - [ 2, [1, 0, 0], high_risk] + - [ 0, [0, 1, 0., 0.5]] + - [ 1, orange] + - [ 2, [1, 0, 0]] + - Identifier: s2_l2_scl + Type: categorical + Colors: + - [ 0, red, no data] + - [ 1, yellow, defective] + - [ 2, black, dark area pixels] + - [ 3, gray, cloud shadows] + - [ 4, green, vegetation] + - [ 5, tan, bare soils] + - [ 6, blue, water] + - [ 7, "#aaaabb", clouds low prob ] + - [ 8, "#bbbbcc", clouds medium prob] + - [ 9, "#ccccdd", clouds high prob] + - [10, "#ddddee", cirrus] + - [11, "#ffffff", snow or ice] ServiceProvider: ProviderName: Brockmann Consult GmbH diff --git a/test/util/test_cmaps.py b/test/util/test_cmaps.py index e3e93aeb1..164718621 100644 --- a/test/util/test_cmaps.py +++ b/test/util/test_cmaps.py @@ -402,7 +402,11 @@ def test_create_colormap_from_config_color_entry_object(self): { "name": "my_cmap", "type": "continuous", - "colors": [[0.0, "red"], [12.0, "#0000FF"], [24.0, [0, 1, 0, 0.3]]], + "colors": [ + [0.0, "red", "low"], + [12.0, "#0000FF", "medium"], + [24.0, [0, 1, 0, 0.3], "high"], + ], }, config_parse, ) @@ -413,7 +417,7 @@ def test_create_colormap_from_config_color_entry_tuple(self): Type="categorical", Colors=[ [0, "red", "low"], - [1, "#0000FF", "medium"], + [1, "#0000FF"], [2, [0, 1, 0], "high"], ], ) @@ -430,7 +434,11 @@ def test_create_colormap_from_config_color_entry_tuple(self): { "name": "my_cmap", "type": "categorical", - "colors": [[0.0, "red"], [1.0, "#0000FF"], [2.0, [0, 1, 0]]], + "colors": [ + [0.0, "red", "low"], + [1.0, "#0000FF"], + [2.0, [0, 1, 0], "high"], + ], }, config_parse, ) diff --git a/test/webapi/datasets/test_context.py b/test/webapi/datasets/test_context.py index 2194f3762..b82afb78f 100644 --- a/test/webapi/datasets/test_context.py +++ b/test/webapi/datasets/test_context.py @@ -153,23 +153,32 @@ def test_config_and_dataset_cache(self): self.assertNotIn("demo2", ctx.dataset_cache) def test_get_color_mappings(self): - ctx = get_datasets_ctx() + with self.assertLogs("xcube", level="WARNING") as cm: + ctx = get_datasets_ctx() + self.assertEqual( + cm.output, + [ + "WARNING:xcube:Custom color map 'cmap_bloom_risk' is categorical. " + "ValueRange is ignored." + ], + ) color_mapping = ctx.get_color_mappings("demo-1w") self.assertEqual( { - "conc_chl": {"ColorBar": "my_cmap", "ValueRange": (0.0, 24.0)}, - "conc_tsm": {"ColorBar": "cmap_cat", "ValueRange": (0.0, 3.0)}, + "conc_chl": {"ColorBar": "my_cmap", "ValueRange": [0.0, 20.0]}, + "conc_tsm": {"ColorBar": "cmap_bloom_risk", "ValueRange": [0.0, 3.0]}, "kd489": {"ColorBar": "jet", "ValueRange": [0.0, 6.0]}, }, color_mapping, ) + self.assertIn("s2_l2_scl", ctx.colormap_registry.colormaps) def test_get_color_mapping(self): ctx = get_datasets_ctx() cm = ctx.get_color_mapping("demo", "conc_chl") - self.assertEqual(("my_cmap", "lin", (0.0, 24.0)), cm) + self.assertEqual(("my_cmap", "lin", (0.0, 20.0)), cm) cm = ctx.get_color_mapping("demo", "conc_tsm") - self.assertEqual(("cmap_cat", "lin", (0.0, 3.0)), cm) + self.assertEqual(("cmap_bloom_risk", "lin", (0.0, 3.0)), cm) cm = ctx.get_color_mapping("demo", "kd489") self.assertEqual(("jet", "lin", (0.0, 6.0)), cm) with self.assertRaises(ApiError.NotFound): diff --git a/test/webapi/res/config.yml b/test/webapi/res/config.yml index 4f249ac12..7c1d84ed0 100644 --- a/test/webapi/res/config.yml +++ b/test/webapi/res/config.yml @@ -49,9 +49,11 @@ Styles: - Identifier: default ColorMappings: conc_chl: - CustomColorBar: my_cmap + ColorBar: my_cmap + ValueRange: [0., 20.] conc_tsm: - CustomColorBar: cmap_cat + ColorBar: cmap_bloom_risk + ValueRange: [0., 1.] kd489: ColorBar: jet ValueRange: [0., 6.] @@ -72,12 +74,28 @@ CustomColorMaps: - Value: 24 Color: [0, 1, 0, 0.3] Label: high - - Identifier: cmap_cat + - Identifier: cmap_bloom_risk Type: categorical Colors: - [ 0, [0, 1, 0., 0.5]] - [ 1, orange] - [ 2, [1, 0, 0]] + - Identifier: s2_l2_scl + Type: categorical + Colors: + - [ 0, red, no data] + - [ 1, yellow, defective] + - [ 2, black, dark area pixels] + - [ 3, gray, cloud shadows] + - [ 4, green, vegetation] + - [ 5, tan, bare soils] + - [ 6, blue, water] + - [ 7, "#aaaabb", clouds low prob ] + - [ 8, "#bbbbcc", clouds medium prob] + - [ 9, "#ccccdd", clouds high prob] + - [10, "#ddddee", cirrus] + - [11, "#ffffff", snow or ice] + - [11, "#ffffff", snow or ice] Viewer: Configuration: diff --git a/xcube/util/cmaps.py b/xcube/util/cmaps.py index 510942fe6..e73bd9808 100644 --- a/xcube/util/cmaps.py +++ b/xcube/util/cmaps.py @@ -491,6 +491,7 @@ def parse_cm_code(cm_code: str) -> tuple[str, Optional[Colormap]]: user_color_map: dict[str, Any] = json.loads(cm_code) cm_name = user_color_map["name"] cm_items = user_color_map["colors"] + cm_items = [item[:2] for item in cm_items] cm_type = user_color_map.get("type", "continuous") cm_base_name, _, _ = parse_cm_name(cm_name) n = len(cm_items) @@ -635,23 +636,24 @@ def get_cmap_png_base64(cmap: matplotlib.colors.Colormap) -> str: def create_colormap_from_config(cmap_config: dict) -> Colormap: registry = ColormapRegistry() colors = [] - labels = [] for color in cmap_config["Colors"]: if isinstance(color, list): - colors.append(color[:2]) - if len(color) == 3: - labels.append(labels) + colors.append(color) else: - colors.append([color["Value"], color["Color"]]) if "Label" in color: - labels.append(color["Label"]) + colors.append([color["Value"], color["Color"], color["Label"]]) + else: + colors.append([color["Value"], color["Color"]]) - for i, [_, color] in enumerate(colors): + for i, item in enumerate(colors): + color = item[1] if isinstance(color, list): if any([val > 1 for val in color]): colors[i][1] = list(np.array(color) / 255) config_parse = dict( - name=cmap_config["Identifier"], type=cmap_config["Type"], colors=colors + name=cmap_config["Identifier"], + type=cmap_config["Type"], + colors=colors, ) _, cmap = registry.get_cmap(json.dumps(config_parse)) return cmap, config_parse diff --git a/xcube/webapi/datasets/config.py b/xcube/webapi/datasets/config.py index f04b5b22e..74a2d4fb9 100644 --- a/xcube/webapi/datasets/config.py +++ b/xcube/webapi/datasets/config.py @@ -120,7 +120,7 @@ COLOR_MAPPING_EXPLICIT_SCHEMA = JsonObjectSchema( properties=dict(ColorBar=STRING_SCHEMA, ValueRange=VALUE_RANGE_SCHEMA), - required=[], + required=["ColorBar"], additional_properties=False, ) @@ -134,21 +134,11 @@ additional_properties=False, ) -COLOR_MAPPING_BY_CUSTOM_CONFIG = JsonObjectSchema( - properties=dict( - CustomColorBar=STRING_SCHEMA, - ), - required=[ - "CustomColorBar", - ], - additional_properties=False, -) COLOR_MAPPING_SCHEMA = JsonComplexSchema( one_of=[ COLOR_MAPPING_EXPLICIT_SCHEMA, COLOR_MAPPING_BY_PATH_SCHEMA, - COLOR_MAPPING_BY_CUSTOM_CONFIG, ] ) diff --git a/xcube/webapi/datasets/context.py b/xcube/webapi/datasets/context.py index 470bb4fca..96838589a 100644 --- a/xcube/webapi/datasets/context.py +++ b/xcube/webapi/datasets/context.py @@ -535,6 +535,12 @@ def get_color_mapping( def _get_cm_styles(self) -> tuple[dict[str, Any], ColormapRegistry]: custom_colormaps = {} cm_styles = {} + + # go through CustomColorMaps + for ctx_cmap in self.config.get("CustomColorMaps", []): + custom_colormap, config_parse = create_colormap_from_config(ctx_cmap) + custom_colormaps[custom_colormap.cm_name] = custom_colormap + # go through Styles for style in self.config.get("Styles", []): style_id = style["Identifier"] color_mappings = dict() @@ -556,23 +562,32 @@ def _get_cm_styles(self) -> tuple[dict[str, Any], ColormapRegistry]: custom_colormap.norm.vmax, ), } - elif "CustomColorBar" in color_mapping: - ctx_cmaps = self.config["CustomColorMaps"] - for ctx_cmap in ctx_cmaps: - if ctx_cmap["Identifier"] == color_mapping["CustomColorBar"]: - break - custom_colormap, config_parse = create_colormap_from_config( - ctx_cmap - ) - custom_colormaps[custom_colormap.cm_name] = custom_colormap - color_mappings[var_name] = { - "ColorBar": custom_colormap.cm_name, - "ValueRange": ( - min(custom_colormap.values), - max(custom_colormap.values), - ), - } else: + if color_mapping.get("ColorBar") in custom_colormaps: + cmap = custom_colormaps[color_mapping["ColorBar"]] + if ( + cmap.cm_type == "categorical" + and "ValueRange" in color_mapping + ): + LOG.warning( + f"Custom color map {color_mapping['ColorBar']!r} is " + "categorical. ValueRange is ignored." + ) + color_mapping = dict( + ColorBar=color_mapping["ColorBar"], + ValueRange=[ + min(cmap.values), + max(cmap.values), + ], + ) + if "ValueRange" not in color_mapping: + color_mapping = dict( + ColorBar=color_mapping["ColorBar"], + ValueRange=[ + min(cmap.values), + max(cmap.values), + ], + ) color_mappings[var_name] = dict(color_mapping) cm_styles[style_id] = color_mappings