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 , , "
+ ",