• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geo-engine / geoengine-python / 16367912334

18 Jul 2025 10:06AM UTC coverage: 76.934% (+0.1%) from 76.806%
16367912334

push

github

web-flow
ci: use Ruff as new formatter and linter (#233)

* wip

* pycodestyle

* update dependencies

* skl2onnx

* use ruff

* apply formatter

* apply lint auto fixes

* manually apply lints

* change check

* ruff ci from branch

2805 of 3646 relevant lines covered (76.93%)

0.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.82
geoengine/colorizer.py
1
"""This module is used to generate geoengine compatible color map definitions as a json string."""
2

3
from __future__ import annotations
1✔
4

5
import json
1✔
6
import warnings
1✔
7
from abc import abstractmethod
1✔
8
from dataclasses import dataclass
1✔
9
from typing import cast
1✔
10

11
import geoengine_openapi_client
1✔
12
import numpy as np
1✔
13
from matplotlib.cm import ScalarMappable
1✔
14
from matplotlib.colors import Colormap
1✔
15

16
Rgba = tuple[int, int, int, int]
1✔
17

18

19
@dataclass
1✔
20
class ColorBreakpoint:
1✔
21
    """This class is used to generate geoengine compatible color breakpoint definitions."""
22

23
    value: float
1✔
24
    color: Rgba
1✔
25

26
    def to_api_dict(self) -> geoengine_openapi_client.Breakpoint:
1✔
27
        """Return the color breakpoint as a dictionary."""
28
        return geoengine_openapi_client.Breakpoint(value=self.value, color=self.color)
1✔
29

30
    @staticmethod
1✔
31
    def from_response(response: geoengine_openapi_client.Breakpoint) -> ColorBreakpoint:
1✔
32
        """Parse a http response to a `ColorBreakpoint`."""
33
        return ColorBreakpoint(cast(float, response.value), cast(Rgba, tuple(cast(list[int], response.color))))
1✔
34

35

36
@dataclass
1✔
37
class Colorizer:
1✔
38
    """This class is used to generate geoengine compatible color map definitions as a json string."""
39

40
    no_data_color: Rgba
1✔
41

42
    @staticmethod
1✔
43
    def linear_with_mpl_cmap(
1✔
44
        color_map: str | Colormap,
45
        min_max: tuple[float, float],
46
        n_steps: int = 10,
47
        over_color: Rgba = (0, 0, 0, 0),
48
        under_color: Rgba = (0, 0, 0, 0),
49
        no_data_color: Rgba = (0, 0, 0, 0),
50
    ) -> LinearGradientColorizer:
51
        """Initialize the colorizer."""
52
        # pylint: disable=too-many-arguments,too-many-positional-arguments
53

54
        if n_steps < 2:
1✔
55
            raise ValueError(f"n_steps must be greater than or equal to 2, got {n_steps} instead.")
×
56
        if min_max[1] <= min_max[0]:
1✔
57
            raise ValueError(f"min_max[1] must be greater than min_max[0], got {min_max[1]} and {min_max[0]}.")
1✔
58
        if len(over_color) != 4:
1✔
59
            raise ValueError(f"overColor must be a tuple of length 4, got {len(over_color)} instead.")
×
60
        if len(under_color) != 4:
1✔
61
            raise ValueError(f"underColor must be a tuple of length 4, got {len(under_color)} instead.")
×
62
        if len(no_data_color) != 4:
1✔
63
            raise ValueError(f"noDataColor must be a tuple of length 4, got {len(no_data_color)} instead.")
×
64
        if not all(0 <= elem < 256 for elem in no_data_color):
1✔
65
            raise ValueError(f"noDataColor must be a RGBA color specification, got {no_data_color} instead.")
1✔
66
        if not all(0 <= elem < 256 for elem in over_color):
1✔
67
            raise ValueError(f"overColor must be a RGBA color specification, got {over_color} instead.")
1✔
68
        if not all(0 <= elem < 256 for elem in under_color):
1✔
69
            raise ValueError(f"underColor must be a RGBA color specification, got {under_color} instead.")
1✔
70

71
        # get the map, and transform it to a list of (uint8) rgba values
72
        list_of_rgba_colors = ScalarMappable(cmap=color_map).to_rgba(
1✔
73
            np.linspace(min_max[0], min_max[1], n_steps), bytes=True
74
        )
75

76
        # if you want to remap the colors, you can do it here (e.g. cutting of the most extreme colors)
77
        values_of_breakpoints: list[float] = np.linspace(min_max[0], min_max[1], n_steps).tolist()
1✔
78

79
        # generate color map steps for geoengine
80
        breakpoints: list[ColorBreakpoint] = [
1✔
81
            ColorBreakpoint(color=(int(color[0]), int(color[1]), int(color[2]), int(color[3])), value=value)
82
            for (value, color) in zip(values_of_breakpoints, list_of_rgba_colors, strict=False)
83
        ]
84

85
        return LinearGradientColorizer(
1✔
86
            breakpoints=breakpoints, no_data_color=no_data_color, over_color=over_color, under_color=under_color
87
        )
88

89
    @staticmethod
1✔
90
    def logarithmic_with_mpl_cmap(
1✔
91
        color_map: str | Colormap,
92
        min_max: tuple[float, float],
93
        n_steps: int = 10,
94
        over_color: Rgba = (0, 0, 0, 0),
95
        under_color: Rgba = (0, 0, 0, 0),
96
        no_data_color: Rgba = (0, 0, 0, 0),
97
    ) -> LogarithmicGradientColorizer:
98
        """Initialize the colorizer."""
99
        # pylint: disable=too-many-arguments, too-many-positional-arguments
100

101
        if n_steps < 2:
1✔
102
            raise ValueError(f"n_steps must be greater than or equal to 2, got {n_steps} instead.")
×
103
        if min_max[0] <= 0:
1✔
104
            raise ValueError(f"min_max[0] must be greater than 0 for a logarithmic gradient, got {min_max[0]}.")
1✔
105
        if min_max[1] <= min_max[0]:
1✔
106
            raise ValueError(f"min_max[1] must be greater than min_max[0], got {min_max[1]} and {min_max[0]}.")
×
107
        if len(over_color) != 4:
1✔
108
            raise ValueError(f"overColor must be a tuple of length 4, got {len(over_color)} instead.")
×
109
        if len(under_color) != 4:
1✔
110
            raise ValueError(f"underColor must be a tuple of length 4, got {len(under_color)} instead.")
×
111
        if len(no_data_color) != 4:
1✔
112
            raise ValueError(f"noDataColor must be a tuple of length 4, got {len(no_data_color)} instead.")
×
113
        if not all(0 <= elem < 256 for elem in no_data_color):
1✔
114
            raise ValueError(f"noDataColor must be a RGBA color specification, got {no_data_color} instead.")
×
115
        if not all(0 <= elem < 256 for elem in over_color):
1✔
116
            raise ValueError(f"overColor must be a RGBA color specification, got {over_color} instead.")
×
117
        if not all(0 <= elem < 256 for elem in under_color):
1✔
118
            raise ValueError(f"underColor must be a RGBA color specification, got {under_color} instead.")
×
119

120
        # get the map, and transform it to a list of (uint8) rgba values
121
        list_of_rgba_colors = ScalarMappable(cmap=color_map).to_rgba(
1✔
122
            np.linspace(min_max[0], min_max[1], n_steps), bytes=True
123
        )
124

125
        # if you want to remap the colors, you can do it here (e.g. cutting of the most extreme colors)
126
        values_of_breakpoints: list[float] = np.logspace(np.log10(min_max[0]), np.log10(min_max[1]), n_steps).tolist()
1✔
127

128
        # generate color map steps for geoengine
129
        breakpoints: list[ColorBreakpoint] = [
1✔
130
            ColorBreakpoint(color=(int(color[0]), int(color[1]), int(color[2]), int(color[3])), value=value)
131
            for (value, color) in zip(values_of_breakpoints, list_of_rgba_colors, strict=False)
132
        ]
133

134
        return LogarithmicGradientColorizer(
1✔
135
            breakpoints=breakpoints, no_data_color=no_data_color, over_color=over_color, under_color=under_color
136
        )
137

138
    @staticmethod
1✔
139
    def palette(
1✔
140
        color_mapping: dict[float, Rgba],
141
        default_color: Rgba = (0, 0, 0, 0),
142
        no_data_color: Rgba = (0, 0, 0, 0),
143
    ) -> PaletteColorizer:
144
        """Initialize the colorizer."""
145

146
        if len(no_data_color) != 4:
1✔
147
            raise ValueError(f"noDataColor must be a tuple of length 4, got {len(no_data_color)} instead.")
×
148
        if len(default_color) != 4:
1✔
149
            raise ValueError(f"defaultColor must be a tuple of length 4, got {len(default_color)} instead.")
×
150
        if not all(0 <= elem < 256 for elem in no_data_color):
1✔
151
            raise ValueError(f"noDataColor must be a RGBA color specification, got {no_data_color} instead.")
×
152
        if not all(0 <= elem < 256 for elem in default_color):
1✔
153
            raise ValueError(f"defaultColor must be a RGBA color specification, got {default_color} instead.")
×
154

155
        return PaletteColorizer(
1✔
156
            colors=color_mapping,
157
            no_data_color=no_data_color,
158
            default_color=default_color,
159
        )
160

161
    @staticmethod
1✔
162
    def palette_with_colormap(
1✔
163
        values: list[float],
164
        color_map: str | Colormap,
165
        default_color: Rgba = (0, 0, 0, 0),
166
        no_data_color: Rgba = (0, 0, 0, 0),
167
    ) -> PaletteColorizer:
168
        """This method generates a palette colorizer from a given list of values.
169
        A colormap can be given as an object or by name only."""
170

171
        if len(no_data_color) != 4:
1✔
172
            raise ValueError(f"noDataColor must be a tuple of length 4, got {len(no_data_color)} instead.")
×
173
        if len(default_color) != 4:
1✔
174
            raise ValueError(f"defaultColor must be a tuple of length 4, got {len(default_color)} instead.")
×
175
        if not all(0 <= elem < 256 for elem in no_data_color):
1✔
176
            raise ValueError(f"noDataColor must be a RGBA color specification, got {no_data_color} instead.")
×
177
        if not all(0 <= elem < 256 for elem in default_color):
1✔
178
            raise ValueError(f"defaultColor must be a RGBA color specification, got {default_color} instead.")
×
179

180
        n_colors_of_cmap: int = ScalarMappable(cmap=color_map).get_cmap().N
1✔
181

182
        if n_colors_of_cmap < len(values):
1✔
183
            warnings.warn(
1✔
184
                UserWarning(
185
                    f"Warning!\nYour colormap does not have enough colors "
186
                    "to display all unique values of the palette!"
187
                    f"\nNumber of values given: {len(values)} vs. "
188
                    f"Number of available colors: {n_colors_of_cmap}",
189
                ),
190
                stacklevel=2,
191
            )
192

193
        # we only need to generate enough different colors for all values specified in the colors parameter
194
        list_of_rgba_colors = ScalarMappable(cmap=color_map).to_rgba(
1✔
195
            np.linspace(0, len(values), len(values)), bytes=True
196
        )
197

198
        # generate the dict with value: color mapping
199
        color_mapping: dict[float, Rgba] = dict(
1✔
200
            zip(
201
                values,
202
                [(int(color[0]), int(color[1]), int(color[2]), int(color[3])) for color in list_of_rgba_colors],
203
                strict=False,
204
            )
205
        )
206

207
        return PaletteColorizer(
1✔
208
            colors=color_mapping,
209
            no_data_color=no_data_color,
210
            default_color=default_color,
211
        )
212

213
    @abstractmethod
1✔
214
    def to_api_dict(self) -> geoengine_openapi_client.Colorizer:
1✔
215
        pass
×
216

217
    def to_json(self) -> str:
1✔
218
        """Return the colorizer as a JSON string."""
219
        return json.dumps(self.to_api_dict())
×
220

221
    @staticmethod
1✔
222
    def from_response(response: geoengine_openapi_client.Colorizer) -> Colorizer:
1✔
223
        """Create a colorizer from a response."""
224
        inner = response.actual_instance
1✔
225

226
        if isinstance(inner, geoengine_openapi_client.LinearGradient):
1✔
227
            return LinearGradientColorizer.from_response_linear(inner)
1✔
228
        if isinstance(inner, geoengine_openapi_client.PaletteColorizer):
1✔
229
            return PaletteColorizer.from_response_palette(inner)
1✔
230
        if isinstance(inner, geoengine_openapi_client.LogarithmicGradient):
×
231
            return LogarithmicGradientColorizer.from_response_logarithmic(inner)
×
232

233
        raise TypeError("Unknown colorizer type")
×
234

235

236
def rgba_from_list(values: list[int]) -> Rgba:
1✔
237
    """Convert a list of integers to an RGBA tuple."""
238
    if len(values) != 4:
1✔
239
        raise ValueError(f"Expected a list of 4 integers, got {len(values)} instead.")
×
240
    return (values[0], values[1], values[2], values[3])
1✔
241

242

243
@dataclass
1✔
244
class LinearGradientColorizer(Colorizer):
1✔
245
    """A linear gradient colorizer."""
246

247
    breakpoints: list[ColorBreakpoint]
1✔
248
    over_color: Rgba
1✔
249
    under_color: Rgba
1✔
250

251
    @staticmethod
1✔
252
    def from_response_linear(response: geoengine_openapi_client.LinearGradient) -> LinearGradientColorizer:
1✔
253
        """Create a colorizer from a response."""
254
        breakpoints = [ColorBreakpoint.from_response(breakpoint) for breakpoint in response.breakpoints]
1✔
255
        return LinearGradientColorizer(
1✔
256
            no_data_color=rgba_from_list(response.no_data_color),
257
            breakpoints=breakpoints,
258
            over_color=rgba_from_list(response.over_color),
259
            under_color=rgba_from_list(response.under_color),
260
        )
261

262
    def to_api_dict(self) -> geoengine_openapi_client.Colorizer:
1✔
263
        """Return the colorizer as a dictionary."""
264
        return geoengine_openapi_client.Colorizer(
1✔
265
            geoengine_openapi_client.LinearGradient(
266
                type="linearGradient",
267
                breakpoints=[breakpoint.to_api_dict() for breakpoint in self.breakpoints],
268
                no_data_color=self.no_data_color,
269
                over_color=self.over_color,
270
                under_color=self.under_color,
271
            )
272
        )
273

274

275
@dataclass
1✔
276
class LogarithmicGradientColorizer(Colorizer):
1✔
277
    """A logarithmic gradient colorizer."""
278

279
    breakpoints: list[ColorBreakpoint]
1✔
280
    over_color: Rgba
1✔
281
    under_color: Rgba
1✔
282

283
    @staticmethod
1✔
284
    def from_response_logarithmic(
1✔
285
        response: geoengine_openapi_client.LogarithmicGradient,
286
    ) -> LogarithmicGradientColorizer:
287
        """Create a colorizer from a response."""
288
        breakpoints = [ColorBreakpoint.from_response(breakpoint) for breakpoint in response.breakpoints]
×
289
        return LogarithmicGradientColorizer(
×
290
            breakpoints=breakpoints,
291
            no_data_color=rgba_from_list(response.no_data_color),
292
            over_color=rgba_from_list(response.over_color),
293
            under_color=rgba_from_list(response.under_color),
294
        )
295

296
    def to_api_dict(self) -> geoengine_openapi_client.Colorizer:
1✔
297
        """Return the colorizer as a dictionary."""
298
        return geoengine_openapi_client.Colorizer(
1✔
299
            geoengine_openapi_client.LogarithmicGradient(
300
                type="logarithmicGradient",
301
                breakpoints=[breakpoint.to_api_dict() for breakpoint in self.breakpoints],
302
                no_data_color=self.no_data_color,
303
                over_color=self.over_color,
304
                under_color=self.under_color,
305
            )
306
        )
307

308

309
@dataclass
1✔
310
class PaletteColorizer(Colorizer):
1✔
311
    """A palette colorizer."""
312

313
    colors: dict[float, Rgba]
1✔
314
    default_color: Rgba
1✔
315

316
    @staticmethod
1✔
317
    def from_response_palette(response: geoengine_openapi_client.PaletteColorizer) -> PaletteColorizer:
1✔
318
        """Create a colorizer from a response."""
319

320
        return PaletteColorizer(
1✔
321
            colors={float(k): rgba_from_list(v) for k, v in response.colors.items()},
322
            no_data_color=rgba_from_list(response.no_data_color),
323
            default_color=rgba_from_list(response.default_color),
324
        )
325

326
    def to_api_dict(self) -> geoengine_openapi_client.Colorizer:
1✔
327
        """Return the colorizer as a dictionary."""
328
        return geoengine_openapi_client.Colorizer(
1✔
329
            geoengine_openapi_client.PaletteColorizer(
330
                type="palette",
331
                colors={str(k): v for k, v in self.colors.items()},
332
                default_color=self.default_color,
333
                no_data_color=self.no_data_color,
334
            )
335
        )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc