diff --git a/autotest/autotest/data/SCL_clipped.tif b/autotest/autotest/data/SCL/SCL_clipped.tif similarity index 100% rename from autotest/autotest/data/SCL_clipped.tif rename to autotest/autotest/data/SCL/SCL_clipped.tif diff --git a/autotest/autotest/data/SCL/fixtures.json b/autotest/autotest/data/SCL/fixtures.json new file mode 100644 index 000000000..92f556dfc --- /dev/null +++ b/autotest/autotest/data/SCL/fixtures.json @@ -0,0 +1,376 @@ +[ +{ + "model": "coverages.fieldtype", + "pk": 1, + "fields": { + "coverage_type": 1, + "index": 0, + "identifier": "scl", + "description": "SCL", + "definition": "http://www.opengis.net/def/property/OGC/0/Radiance", + "unit_of_measure": "nil", + "wavelength": null, + "significant_figures": 2, + "numbits": 8, + "signed": false, + "is_float": false + } +}, +{ + "model": "coverages.allowedvaluerange", + "pk": 1, + "fields": { + "field_type": 1, + "start": 0.0, + "end": 11.0 + } +}, +{ + "model": "coverages.nilvalue", + "pk": 1, + "fields": { + "value": "0", + "reason": "http://www.opengis.net/def/nil/OGC/0/unknown", + "field_types": [ + 1 + ] + } +}, +{ + "model": "coverages.coveragetype", + "pk": 1, + "fields": { + "name": "SCL" + } +}, +{ + "model": "coverages.producttype", + "pk": 1, + "fields": { + "name": "SCL", + "allowed_coverage_types": [ + 1 + ] + } +}, +{ + "model": "coverages.browsetype", + "pk": 1, + "fields": { + "product_type": 1, + "name": "SCL", + "red_or_grey_expression": "scl", + "green_expression": null, + "blue_expression": null, + "alpha_expression": null, + "red_or_grey_nodata_value": null, + "green_nodata_value": null, + "blue_nodata_value": null, + "alpha_nodata_value": null, + "red_or_grey_range_min": null, + "green_range_min": null, + "blue_range_min": null, + "alpha_range_min": null, + "red_or_grey_range_max": null, + "green_range_max": null, + "blue_range_max": null, + "alpha_range_max": null, + "show_out_of_bounds_data": false + } +}, +{ + "model": "coverages.rasterstyle", + "pk": 1, + "fields": { + "name": "SCL", + "type": "values", + "title": null, + "abstract": null + } +}, +{ + "model": "coverages.rasterstyletobrowsetypethrough", + "pk": 1, + "fields": { + "raster_style": 1, + "browse_type": 1, + "style_name": "color" + } +}, +{ + "model": "coverages.rasterstylecolorentry", + "pk": 1, + "fields": { + "raster_style": 1, + "value": 0.0, + "color": "#000000", + "opacity": 1.0, + "label": "NO_DATA" + } +}, +{ + "model": "coverages.rasterstylecolorentry", + "pk": 2, + "fields": { + "raster_style": 1, + "value": 1.0, + "color": "#ff0000", + "opacity": 1.0, + "label": "SATURATED_OR_DEFECTIVE" + } +}, +{ + "model": "coverages.rasterstylecolorentry", + "pk": 3, + "fields": { + "raster_style": 1, + "value": 2.0, + "color": "#2e2e2e", + "opacity": 1.0, + "label": "DARK_AREA_PIXELS" + } +}, +{ + "model": "coverages.rasterstylecolorentry", + "pk": 4, + "fields": { + "raster_style": 1, + "value": 3.0, + "color": "#541800", + "opacity": 1.0, + "label": "CLOUD_SHADOWS" + } +}, +{ + "model": "coverages.rasterstylecolorentry", + "pk": 5, + "fields": { + "raster_style": 1, + "value": 4.0, + "color": "#46e800", + "opacity": 1.0, + "label": "VEGETATION" + } +}, +{ + "model": "coverages.rasterstylecolorentry", + "pk": 6, + "fields": { + "raster_style": 1, + "value": 5.0, + "color": "#ffff00", + "opacity": 1.0, + "label": "NOT_VEGETATED" + } +}, +{ + "model": "coverages.rasterstylecolorentry", + "pk": 7, + "fields": { + "raster_style": 1, + "value": 6.0, + "color": "#0000ff", + "opacity": 1.0, + "label": "WATER" + } +}, +{ + "model": "coverages.rasterstylecolorentry", + "pk": 8, + "fields": { + "raster_style": 1, + "value": 7.0, + "color": "#525252", + "opacity": 1.0, + "label": "UNCLASSIFIED" + } +}, +{ + "model": "coverages.rasterstylecolorentry", + "pk": 9, + "fields": { + "raster_style": 1, + "value": 8.0, + "color": "#787878", + "opacity": 1.0, + "label": "CLOUD_MEDIUM_PROBABILITY" + } +}, +{ + "model": "coverages.rasterstylecolorentry", + "pk": 10, + "fields": { + "raster_style": 1, + "value": 9.0, + "color": "#b5b5b5", + "opacity": 1.0, + "label": "CLOUD_HIGH_PROBABILITY" + } +}, +{ + "model": "coverages.rasterstylecolorentry", + "pk": 11, + "fields": { + "raster_style": 1, + "value": 10.0, + "color": "#00b6bf", + "opacity": 1.0, + "label": "THIN_CIRRUS" + } +}, +{ + "model": "coverages.rasterstylecolorentry", + "pk": 12, + "fields": { + "raster_style": 1, + "value": 11.0, + "color": "#da00f2", + "opacity": 1.0, + "label": "SNOW" + } +}, +{ + "model": "coverages.grid", + "pk": 1, + "fields": { + "name": null, + "coordinate_reference_system": "EPSG:32630", + "axis_1_name": "x", + "axis_2_name": "y", + "axis_3_name": null, + "axis_4_name": null, + "axis_1_type": 0, + "axis_2_type": 0, + "axis_3_type": null, + "axis_4_type": null, + "axis_1_offset": "200.0", + "axis_2_offset": "-200.0", + "axis_3_offset": null, + "axis_4_offset": null, + "resolution": 200 + } +}, +{ + "model": "coverages.eoobject", + "pk": 1, + "fields": { + "identifier": "S2B_30UUG_20221226_0_L2A", + "begin_time": null, + "end_time": null, + "footprint": "SRID=4326;POLYGON ((-6.1994886 55.9041676, -6.1207799 54.9190265, -4.4083987 54.9509423, -4.4439779 55.9372733, -6.1994886 55.9041676))", + "inserted": "2023-09-18T08:57:08.231Z", + "updated": "2023-09-18T11:44:34.789Z" + } +}, +{ + "model": "coverages.eoobject", + "pk": 2, + "fields": { + "identifier": "S2B_30UUG_20221226_0_L2A_scl", + "begin_time": null, + "end_time": null, + "footprint": "SRID=4326;POLYGON ((-6.120779882947349 54.919026538369536, -6.1994885612119175 55.904167624663266, -4.443977851096375 55.937273345275685, -4.40839870205061 54.95094226131747, -6.120779882947349 54.919026538369536))", + "inserted": "2023-09-18T08:57:08.757Z", + "updated": "2023-09-18T08:57:08.765Z" + } +}, +{ + "model": "coverages.product", + "pk": 1, + "fields": { + "product_type": 1, + "package": null, + "collections": [] + } +}, +{ + "model": "coverages.coverage", + "pk": 2, + "fields": { + "grid": 1, + "axis_1_origin": "300000.0", + "axis_2_origin": "6200040.0", + "axis_3_origin": null, + "axis_4_origin": null, + "axis_1_size": 549, + "axis_2_size": 549, + "axis_3_size": null, + "axis_4_size": null, + "coverage_type": 1, + "parent_product": 1, + "collections": [], + "mosaics": [] + } +}, +{ + "model": "coverages.arraydataitem", + "pk": 1, + "fields": { + "storage": null, + "location": "autotest/data/SCL/scl_small.tif", + "format": "image/tiff", + "coverage": 2, + "field_index": 0, + "band_count": 1, + "subdataset_type": null, + "subdataset_locator": null, + "bands_interpretation": 0 + } +}, +{ + "model": "coverages.productmetadata", + "pk": 1, + "fields": { + "product": 1, + "parent_identifier": null, + "production_status": null, + "acquisition_type": null, + "orbit_number": null, + "orbit_direction": null, + "track": null, + "frame": null, + "swath_identifier": null, + "product_version": null, + "product_quality_status": null, + "product_quality_degradation_tag": null, + "processor_name": null, + "processing_center": null, + "creation_date": null, + "modification_date": null, + "processing_date": null, + "sensor_mode": null, + "archiving_center": null, + "processing_mode": null, + "availability_time": null, + "acquisition_station": null, + "acquisition_sub_type": null, + "start_time_from_ascending_node": null, + "completion_time_from_ascending_node": null, + "illumination_azimuth_angle": null, + "illumination_zenith_angle": null, + "illumination_elevation_angle": null, + "polarisation_mode": null, + "polarization_channels": null, + "antenna_look_direction": null, + "minimum_incidence_angle": null, + "maximum_incidence_angle": null, + "across_track_incidence_angle": null, + "along_track_incidence_angle": null, + "doppler_frequency": null, + "incidence_angle_variation": null, + "cloud_cover": null, + "snow_cover": null, + "lowest_location": null, + "highest_location": null + } +}, +{ + "model": "services.servicevisibility", + "pk": 1, + "fields": { + "eo_object": 1, + "service": "wms", + "visibility": true + } +} +] diff --git a/autotest/autotest/data/SCL/scl.sld b/autotest/autotest/data/SCL/scl.sld new file mode 100644 index 000000000..b7e965b64 --- /dev/null +++ b/autotest/autotest/data/SCL/scl.sld @@ -0,0 +1,36 @@ + + + + + + + + S2B_30UUG_20221226_0_L2A_scl + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + diff --git a/autotest/autotest/data/SCL/scl_coverage_type.json b/autotest/autotest/data/SCL/scl_coverage_type.json new file mode 100644 index 000000000..f2882ebe2 --- /dev/null +++ b/autotest/autotest/data/SCL/scl_coverage_type.json @@ -0,0 +1,24 @@ +[{ + "bands": [ + { + "definition": "http://www.opengis.net/def/property/OGC/0/Radiance", + "description": "SCL", + "gdal_interpretation": "gray", + "identifier": "scl", + "name": "scl", + "nil_values": [ + { + "reason": "http://www.opengis.net/def/nil/OGC/0/unknown", + "value": 0 + } + ], + "uom": "nil", + "significant_figures": 2, + "allowed_value_ranges": [ + [0, 11] + ] + } + ], + "data_type": "Byte", + "name": "SCL" +}] diff --git a/autotest/autotest/data/SCL/scl_small.tif b/autotest/autotest/data/SCL/scl_small.tif new file mode 100644 index 000000000..bd349ccfc Binary files /dev/null and b/autotest/autotest/data/SCL/scl_small.tif differ diff --git a/autotest/autotest/data/SCL/setup_data.sh b/autotest/autotest/data/SCL/setup_data.sh new file mode 100755 index 000000000..705467347 --- /dev/null +++ b/autotest/autotest/data/SCL/setup_data.sh @@ -0,0 +1,14 @@ +python3 manage.py coveragetype import autotest/data/SCL/scl_coverage_type.json +python3 manage.py producttype create SCL -c SCL +python3 manage.py browsetype create SCL SCL --grey scl + +python3 manage.py coveragetype import + +python3 manage.py rasterstyle import ./autotest/data/SCL/scl.sld --rename S2B_30UUG_20221226_0_L2A_scl SCL + +python3 manage.py rasterstyle link SCL SCL SCL color + +python3 manage.py product register -i S2B_30UUG_20221226_0_L2A -t SCL --footprint "POLYGON ((-6.1994886 55.9041676, -6.1207799 54.9190265, -4.4083987 54.9509423, -4.4439779 55.9372733, -6.1994886 55.9041676))" --replace +python3 manage.py coverage register -t SCL -r -d autotest/data/SCL/scl_small.tif --footprint-from-extent -i S2B_30UUG_20221226_0_L2A_scl -p S2B_30UUG_20221226_0_L2A + +python3 manage.py visibility S2B_30UUG_20221226_0_L2A --wms diff --git a/autotest/autotest/data/fixtures/scl_cloud_coverages.json b/autotest/autotest/data/fixtures/scl_cloud_coverages.json index e029f161a..66493fa89 100644 --- a/autotest/autotest/data/fixtures/scl_cloud_coverages.json +++ b/autotest/autotest/data/fixtures/scl_cloud_coverages.json @@ -62,7 +62,7 @@ "pk": 20, "fields": { "storage": null, - "location": "autotest/data/SCL_clipped.tif", + "location": "autotest/data/SCL/SCL_clipped.tif", "format": "image/tiff", "coverage": 20, "field_index": 0, diff --git a/autotest/autotest/data/fixtures/scl_styled.json b/autotest/autotest/data/fixtures/scl_styled.json new file mode 120000 index 000000000..a5d97c0f0 --- /dev/null +++ b/autotest/autotest/data/fixtures/scl_styled.json @@ -0,0 +1 @@ +../SCL/fixtures.json \ No newline at end of file diff --git a/autotest/autotest/expected/WMS13GetMapDatasetStyledTestCase.png b/autotest/autotest/expected/WMS13GetMapDatasetStyledTestCase.png new file mode 100644 index 000000000..4da8217a0 Binary files /dev/null and b/autotest/autotest/expected/WMS13GetMapDatasetStyledTestCase.png differ diff --git a/autotest/autotest_services/tests/wms/test_v13.py b/autotest/autotest_services/tests/wms/test_v13.py index 995406385..59b2fc987 100644 --- a/autotest/autotest_services/tests/wms/test_v13.py +++ b/autotest/autotest_services/tests/wms/test_v13.py @@ -340,23 +340,23 @@ class WMS13GetMapCollectionMaskedOutlinesTestCase(wmsbase.WMS13GetMapTestCase): layers = ("MER_FRS_1P_reduced_products_RGB__outlines_masked_clouds",) bbox = (11, 32, 28, 46) -#=============================================================================== +# ============================================================================== # Styled Coverages -#=============================================================================== +# ============================================================================== -# currently disabled because of segfaults in MapServer -''' class WMS13GetMapDatasetStyledTestCase(wmsbase.WMS13GetMapTestCase): """ Test a GetMap request a dataset with an associated style. """ - fixtures = wmsbase.WMS13GetMapTestCase.fixtures + [ - "cryo_range_type.json", "cryo_coverages.json" - ] - - layers = ("FSC_0.0025deg_201303030930_201303031110_MOD_Alps_ENVEOV2.1.00",) - bbox = (6, 44.5, 16, 48) + fixtures = ["scl_styled.json"] + layers = ("S2B_30UUG_20221226_0_L2A__SCL",) + swap_axes = True + bbox = (-6.282089176104, 54.89235272910, -4.3728695585011, 55.962341471504) + crs = "EPSG:4326" width = 200 -''' + height = 200 + styles = ("color",) + frmt = "image/png" + #=============================================================================== # Feature Info #=============================================================================== @@ -420,3 +420,6 @@ def getRequest(self): def getFileExtension(self, file_type): return "png" + + + diff --git a/documentation/users/coverages.rst b/documentation/users/coverages.rst index 661311d4a..de31dc725 100644 --- a/documentation/users/coverages.rst +++ b/documentation/users/coverages.rst @@ -197,6 +197,35 @@ When a collection is linked to a `Collection Type`_ only Products and Coverages whose types are of the set of allowed coverage/product types can be inserted. +.. _RasterStyle Model: + +Raster Style +~~~~~~~~~~~~ + +A raster style is an instruction on ow to colorize a raster at the last step of +a rendering process of single band outputs to generate an RGB(A) image. + +A raster style has a name, title, abstract, type and a number of color entries. +Name, title, abstract are metadata displayed in the service capabilities. +Each color entry maps a value to a color, and has an optional label. The +type defines how the raster style colors are applied. The following types are +possible: + +* "ramp": this is the default. The colors are linearly interpolated for the + values. +* "values": only the colors specified in the color entries are rendered if they + exactly match the value. All other values are not rendered. +* "intervals": all values are mapped to the color of their next lower color + scale entry. + +Raster styles are linked to browse types using a distinct style name, so that +such styles can be re-used in multiple browse types. + +There are a number of default raster styles available, for when no raster +styles are configured. As soon as at least one raster style is configured, it +replaces all default raster styles. + + Command Line Interfaces ----------------------- @@ -876,3 +905,49 @@ stac read the STAC Item from stdin instead from a file. --type TYPE_NAME, --product-type TYPE_NAME, -t TYPE_NAME the name of the new product type. Optional. + + +.. _cmd-rasterstyle: + +rasterstyle + this command allows to manage `Raster Style Model`_ instances and link them + with Browse Types. + + create + this sub-command creates a new raster style from a given set of values. + + name + The raster style name. Mandatory. + + import + this imports a raster style from an SLD file. + + filename + The SLD file name. Mandatory. + + --select + Only select the named styles. Can be specified multiple times. + + --rename + Rename a style from a name to another name + + delete + this sub-command deletes a raster style. + + name + The raster style name. Mandatory. + + link + this sub-command links a raster style to a browse type. + + name + The raster style name. Mandatory. + + product_type_name + The product type name containing the browse type. Mandatory. + + browse_type_name + The browse type name. Mandatory. + + style_name + The assigned style name. Optional. diff --git a/documentation/users/services/wms.rst b/documentation/users/services/wms.rst index 220cdad16..31337bd7d 100644 --- a/documentation/users/services/wms.rst +++ b/documentation/users/services/wms.rst @@ -133,7 +133,7 @@ parameters that are available with GetMap requests. | | The available styles depend on the layer type. Outline | | | | | and mask layers can be rendered in the basic colors. | | | | | Single band output can be styled using a range of | | | - | | color scales. | | | + | | color scales (Raster styles may apply). | | | | | | | | | | The Capabilities document lists the available styles per | | | | | layer. | | | diff --git a/eoxserver/render/browse/defaultstyles.py b/eoxserver/render/browse/defaultstyles.py new file mode 100644 index 000000000..cf91bfb7f --- /dev/null +++ b/eoxserver/render/browse/defaultstyles.py @@ -0,0 +1,25 @@ +from eoxserver.render.colors import COLOR_SCALES, BASE_COLORS +from eoxserver.render.browse.objects import ( + GeometryStyle, + RasterStyle, + RasterStyleColorEntry, +) + +DEFAULT_RASTER_STYLES = {} +DEFAULT_GEOMETRY_STYLES = {} + +for name, entries in COLOR_SCALES.items(): + DEFAULT_RASTER_STYLES[name] = RasterStyle( + name, + "ramp", + name, + name, + [ + RasterStyleColorEntry(i, color) + for i, color in entries + ] + ) + + +for name in BASE_COLORS.keys(): + DEFAULT_GEOMETRY_STYLES[name] = GeometryStyle(name, name, name) diff --git a/eoxserver/render/browse/objects.py b/eoxserver/render/browse/objects.py index ce9589eb9..89afd2b3f 100644 --- a/eoxserver/render/browse/objects.py +++ b/eoxserver/render/browse/objects.py @@ -46,7 +46,8 @@ class Browse(object): - def __init__(self, name, filename, env, size, extent, crs, mode, footprint): + def __init__(self, name, filename, env, size, extent, crs, mode, footprint, + raster_styles): self._name = name self._filename = filename self._env = env @@ -55,6 +56,7 @@ def __init__(self, name, filename, env, size, extent, crs, mode, footprint): self._crs = crs self._mode = mode self._footprint = footprint + self._raster_styles = raster_styles @property def name(self): @@ -101,7 +103,7 @@ def footprint(self): return polygon @classmethod - def from_model(cls, product_model, browse_model): + def from_model(cls, product_model, browse_model, raster_styles=None): filename = get_vsi_path(browse_model) env = get_vsi_env(browse_model.storage) size = (browse_model.width, browse_model.height) @@ -127,11 +129,12 @@ def from_model(cls, product_model, browse_model): return cls( name, filename, env, size, extent, browse_model.coordinate_reference_system, mode, - product_model.footprint + product_model.footprint, + raster_styles if raster_styles is not None else {} ) @classmethod - def from_file(cls, filename, env=None): + def from_file(cls, filename, env=None, raster_styles=None): env = env or {} ds = gdal.Open(filename) size = (ds.RasterXSize, ds.RasterYSize) @@ -140,14 +143,15 @@ def from_file(cls, filename, env=None): return cls( filename, env, filename, size, extent, - ds.GetProjection(), mode, None + ds.GetProjection(), mode, None, + raster_styles if raster_styles is not None else {}, ) class GeneratedBrowse(Browse): def __init__(self, name, band_expressions, ranges, nodata_values, - fields_and_coverages, field_list, footprint, variables, - show_out_of_bounds_data=False, + fields_and_coverages, field_list, footprint, raster_styles, + variables, show_out_of_bounds_data=False, ): self._name = name self._band_expressions = band_expressions @@ -156,6 +160,7 @@ def __init__(self, name, band_expressions, ranges, nodata_values, self._fields_and_coverages = fields_and_coverages self._field_list = field_list self._footprint = footprint + self._raster_styles = raster_styles self._variables = variables self._show_out_of_bounds_data = show_out_of_bounds_data @@ -217,6 +222,10 @@ def field_list(self): def variables(self): return self._variables + @property + def raster_styles(self): + return self._raster_styles + @property def show_out_of_bounds_data(self) -> bool: return self._show_out_of_bounds_data @@ -224,7 +233,8 @@ def show_out_of_bounds_data(self) -> bool: @classmethod def from_coverage_models(cls, band_expressions, ranges, nodata_values, fields_and_coverage_models, - product_model, variables, show_out_of_bounds_data): + product_model, variables, raster_styles, + show_out_of_bounds_data): fields_and_coverages = { field_name: [ @@ -246,6 +256,7 @@ def from_coverage_models(cls, band_expressions, ranges, nodata_values, for field_name in fields_and_coverages.keys() ], product_model.footprint, + raster_styles, variables, show_out_of_bounds_data, ) @@ -320,6 +331,95 @@ def from_models(cls, product_model, browse_model, mask_model, ) +class BaseStyle(object): + def __init__(self, name, title, abstract): + self._name = name + self._title = title or '' + self._abstract = abstract or '' + + @property + def name(self): + return self._name + + @property + def title(self): + return self._title + + @property + def abstract(self): + return self._abstract + + +class GeometryStyle(BaseStyle): + pass + + +class RasterStyle(BaseStyle): + def __init__(self, name, type, title, abstract, entries): + super().__init__(name, title, abstract) + self._type = type + self._entries = entries + + @property + def type(self): + return self._type + + @property + def entries(self): + return self._entries + + @classmethod + def from_model(cls, raster_style_model, name=None): + return cls( + name or raster_style_model.name, + raster_style_model.type, + raster_style_model.title, + raster_style_model.abstract, + [ + RasterStyleColorEntry.from_model(entry_model) + for entry_model in raster_style_model.color_entries.all() + ] + ) + + +def hex_to_rgb(hexa): + hexa = hexa.lstrip("#") + return tuple(int(hexa[i:i + 2], 16) for i in (0, 2, 4)) + + +class RasterStyleColorEntry(object): + def __init__(self, value, color, opacity=1.0, label=None): + self._value = value + self._color = color + self._opacity = opacity + self._label = label + + @property + def value(self): + return self._value + + @property + def color(self): + return self._color + + @property + def opacity(self): + return self._opacity + + @property + def label(self): + return self._label + + @classmethod + def from_model(cls, raster_style_color_entry_model): + return cls( + raster_style_color_entry_model.value, + hex_to_rgb(raster_style_color_entry_model.color), + raster_style_color_entry_model.opacity, + raster_style_color_entry_model.label, + ) + + def _get_ds_mode(ds): first = ds.GetRasterBand(1) diff --git a/eoxserver/render/colors.py b/eoxserver/render/colors.py index e49ab2421..c876cb38b 100644 --- a/eoxserver/render/colors.py +++ b/eoxserver/render/colors.py @@ -440,9 +440,9 @@ def linear(colors): ]), "brylgn" : linear([ - (130,67,0), - (255,200,110), - (255,255,179), + (130, 67, 0), + (255, 200, 110), + (255, 255, 179), (116, 234, 118), (0, 109, 0), ]), diff --git a/eoxserver/render/mapserver/factories.py b/eoxserver/render/mapserver/factories.py index 64449deb4..beca281ca 100644 --- a/eoxserver/render/mapserver/factories.py +++ b/eoxserver/render/mapserver/factories.py @@ -45,6 +45,7 @@ from eoxserver.render.browse.generate import ( generate_browse, FilenameGenerator ) +from eoxserver.render.browse.defaultstyles import DEFAULT_RASTER_STYLES from eoxserver.render.map.objects import ( CoverageLayer, CoveragesLayer, MosaicLayer, OutlinedCoveragesLayer, BrowseLayer, OutlinedBrowseLayer, @@ -213,7 +214,10 @@ def create_coverage_layer(self, map_obj: Map, coverage: Coverage, fields: List[F for layer_obj in layer_objs: _create_raster_style( - style or "blackwhite", layer_obj, range_[0], range_[1], [ + DEFAULT_RASTER_STYLES[style or "blackwhite"], + layer_obj, + range_[0], + range_[1], [ nil_value[0] for nil_value in field.nil_values ] ) @@ -392,8 +396,9 @@ def make_browse_layer_generator(self, map_obj, browses, map_, browse_range = _get_range(field) for layer_obj in layer_objs: + raster_style = browse.raster_styles.get(style or "blackwhite") or DEFAULT_RASTER_STYLES[style or "blackwhite"] _create_raster_style( - style or "blackwhite", layer_obj, + raster_style, layer_obj, browse_range[0], browse_range[1], browse.nodata_values ) @@ -784,10 +789,49 @@ def _build_vrt(size, field_locations): return path -def _create_raster_style(name, layer, minvalue=0, maxvalue=255, +def _create_raster_style(raster_style, layer, minvalue=0, maxvalue=255, nil_values=None): - colors = COLOR_SCALES[name] + if raster_style.type == "ramp": + return _create_raster_style_ramp( + raster_style, layer, minvalue, maxvalue, nil_values + ) + elif raster_style.type == "values": + for entry in raster_style.entries: + value = entry.value + if int(value) == value: + value = int(value) + cls = ms.classObj() + cls.setExpression("([pixel] = %s)" % value) + cls.group = entry.label + + style = ms.styleObj() + style.color = ms.colorObj(*entry.color) + style.opacity = int(entry.opacity * 100) + cls.insertStyle(style) + layer.insertClass(cls) + cls = ms.classObj() + style = ms.styleObj() + style.color = ms.colorObj(0, 0, 0, 0) + style.opacity = 0 + cls.insertStyle(style) + layer.insertClass(cls) + return + + elif raster_style.type == "intervals": + # TODO + return + raise ValueError("Invalid raster style type %r" % raster_style.type) + + +def _create_raster_style_ramp(raster_style, layer, minvalue=0, maxvalue=255, + nil_values=None): + name = raster_style.name + + colors = [ + (entry.value, entry.color) + for entry in raster_style.entries + ] if nil_values and all(v is not None for v in nil_values): nil_values = [float(nil_value) for nil_value in nil_values] else: diff --git a/eoxserver/render/mapserver/map_renderer.py b/eoxserver/render/mapserver/map_renderer.py index 9951e9166..7aed8ae96 100644 --- a/eoxserver/render/mapserver/map_renderer.py +++ b/eoxserver/render/mapserver/map_renderer.py @@ -32,8 +32,12 @@ from eoxserver.contrib import mapserver as ms from eoxserver.contrib import vsi -from eoxserver.render.colors import BASE_COLORS, COLOR_SCALES -from eoxserver.render.mapserver.factories import BaseMapServerLayerFactory, get_layer_factories +from eoxserver.render.browse.defaultstyles import ( + DEFAULT_RASTER_STYLES, DEFAULT_GEOMETRY_STYLES +) +from eoxserver.render.mapserver.factories import ( + BaseMapServerLayerFactory, get_layer_factories +) from eoxserver.render.map.objects import Map, Layer from eoxserver.resources.coverages.formats import getFormatRegistry @@ -56,10 +60,10 @@ class MapserverMapRenderer(object): ] def get_geometry_styles(self): - return BASE_COLORS.keys() + return list(DEFAULT_GEOMETRY_STYLES.values()) def get_raster_styles(self): - return COLOR_SCALES.keys() + return list(DEFAULT_RASTER_STYLES.values()) def get_supported_layer_types(self): layer_types = [] diff --git a/eoxserver/resources/coverages/admin.py b/eoxserver/resources/coverages/admin.py index 2e4cf5391..008d0a1ba 100644 --- a/eoxserver/resources/coverages/admin.py +++ b/eoxserver/resources/coverages/admin.py @@ -1,11 +1,11 @@ -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # # Project: EOxServer # Authors: Fabian Schindler # Stephan Meissl # Stephan Krause # -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ # Copyright (C) 2011 EOX IT Services GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -25,7 +25,7 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------ from django.contrib.gis import admin try: @@ -34,6 +34,7 @@ from django.urls import reverse, NoReverseMatch from django.utils.safestring import mark_safe from django.conf import settings +from django.forms import ModelForm, TextInput from eoxserver.resources.coverages import models @@ -127,7 +128,9 @@ class MetaDataItemInline(admin.StackedInline): def download_link(self, obj): try: return mark_safe('Download'.format( - reverse('metadata', kwargs=dict( + reverse( + 'metadata', + kwargs=dict( identifier=obj.eo_object.identifier, semantic=dict( models.MetaDataItem.SEMANTIC_CHOICES @@ -201,6 +204,7 @@ def default_zoom(self): class CoverageTypeAdmin(admin.ModelAdmin): inlines = [FieldTypeInline] + admin.site.register(models.CoverageType, CoverageTypeAdmin) @@ -208,24 +212,28 @@ class ProductTypeAdmin(admin.ModelAdmin): inlines = [BrowseTypeInline, MaskTypeInline] filter_horizontal = ['allowed_coverage_types'] + admin.site.register(models.ProductType, ProductTypeAdmin) class CollectionTypeAdmin(admin.ModelAdmin): filter_horizontal = ['allowed_product_types', 'allowed_coverage_types'] + admin.site.register(models.CollectionType, CollectionTypeAdmin) class MaskTypeAdmin(admin.ModelAdmin): pass + admin.site.register(models.MaskType, MaskTypeAdmin) class GridAdmin(admin.ModelAdmin): pass + admin.site.register(models.Grid, GridAdmin) # ============================================================================== @@ -236,6 +244,7 @@ class GridAdmin(admin.ModelAdmin): class CoverageAdmin(EOObjectAdmin): inlines = [CoverageMetadataInline, MetaDataItemInline, ArrayDataItemInline] + admin.site.register(models.Coverage, CoverageAdmin) @@ -244,12 +253,14 @@ class ProductAdmin(EOObjectAdmin): MaskInline, BrowseInline, ProductDataItemInline, MetaDataItemInline, ProductMetadataInline ] + admin.site.register(models.Product, ProductAdmin) class MosaicAdmin(EOObjectAdmin): inlines = [] + admin.site.register(models.Mosaic, MosaicAdmin) @@ -279,6 +290,7 @@ class IndexHiddenAdmin(admin.ModelAdmin): def get_model_perms(self, request): return {} + admin.site.register(models.OrbitNumber, IndexHiddenAdmin) admin.site.register(models.Track, IndexHiddenAdmin) admin.site.register(models.Frame, IndexHiddenAdmin) @@ -292,3 +304,35 @@ def get_model_perms(self, request): admin.site.register(models.ProcessingMode, IndexHiddenAdmin) admin.site.register(models.AcquisitionStation, IndexHiddenAdmin) admin.site.register(models.AcquisitionSubType, IndexHiddenAdmin) + + +# ============================================================================== +# Raster Style models +# ============================================================================== + + +class RasterStyleColorEntryForm(ModelForm): + class Meta: + model = models.RasterStyleColorEntry + fields = '__all__' + widgets = { + 'color': TextInput(attrs={'type': 'color'}), + } + + +class RasterStyleColorEntryInline(admin.TabularInline): + model = models.RasterStyleColorEntry + form = RasterStyleColorEntryForm + extra = 0 + + +class RasterStyleToBrowseTypeThroughInline(admin.TabularInline): + model = models.RasterStyleToBrowseTypeThrough + extra = 0 + + +class RasterStyleAdmin(admin.ModelAdmin): + inlines = [RasterStyleToBrowseTypeThroughInline, RasterStyleColorEntryInline] + + +admin.site.register(models.RasterStyle, RasterStyleAdmin) diff --git a/eoxserver/resources/coverages/management/commands/rasterstyle.py b/eoxserver/resources/coverages/management/commands/rasterstyle.py new file mode 100644 index 000000000..5a48d170e --- /dev/null +++ b/eoxserver/resources/coverages/management/commands/rasterstyle.py @@ -0,0 +1,271 @@ +# ------------------------------------------------------------------------------ +# +# Project: EOxServer +# Authors: Fabian Schindler +# +# ------------------------------------------------------------------------------ +# Copyright (C) 2017 EOX IT Services GmbH +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies of this Software or works derived from this Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# ------------------------------------------------------------------------------ + +from django.core.management.base import CommandError, BaseCommand +from django.db import transaction +from lxml import etree + +from eoxserver.resources.coverages import models +from eoxserver.resources.coverages.management.commands import ( + CommandOutputMixIn, SubParserMixIn +) + + +class Command(CommandOutputMixIn, SubParserMixIn, BaseCommand): + """ Command to manage product types. This command uses sub-commands for the + specific tasks: create, delete + """ + def add_arguments(self, parser): + create_parser = self.add_subparser(parser, 'create') + import_parser = self.add_subparser(parser, 'import') + delete_parser = self.add_subparser(parser, 'delete') + list_parser = self.add_subparser(parser, 'list') + link_parser = self.add_subparser(parser, 'link') + + for parser in [create_parser, delete_parser, link_parser]: + parser.add_argument( + 'name', nargs=1, help='The raster style name. Mandatory.' + ) + + create_parser.add_argument( + '--type', '-t', action="store", default="ramp", + choices=["ramp", "values", "intervals"], + help="Specify this raster style type" + ) + create_parser.add_argument( + '--title', action="store", + help="Specify this raster style title" + ) + create_parser.add_argument( + '--abstract', action="store", + help="Specify this raster style abstract" + ) + create_parser.add_argument( + '--color-entry', '-c', + action='append', dest='color_entries', default=[], nargs=4, + help=( + "A color style entry. Must consist of , , " + ",