diff --git a/geomanager/migrations/0046_colorvalue_show_on_legend_rasterstyle_legend_type.py b/geomanager/migrations/0046_colorvalue_show_on_legend_rasterstyle_legend_type.py new file mode 100644 index 0000000..73e6f06 --- /dev/null +++ b/geomanager/migrations/0046_colorvalue_show_on_legend_rasterstyle_legend_type.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.10 on 2024-02-29 07:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('geomanager', '0045_additionalmapboundarydata_active_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='colorvalue', + name='show_on_legend', + field=models.BooleanField(default=True, verbose_name='Show label on Legend'), + ), + migrations.AddField( + model_name='rasterstyle', + name='legend_type', + field=models.CharField(choices=[('basic', 'Basic'), ('choropleth', 'Choropleth'), ('gradient', 'Gradient')], default='choropleth', max_length=100, verbose_name='Legend Type'), + ), + ] diff --git a/geomanager/models/__init__.py b/geomanager/models/__init__.py index 9b0324b..a177e4d 100644 --- a/geomanager/models/__init__.py +++ b/geomanager/models/__init__.py @@ -14,6 +14,7 @@ from .vector_file import * from .vector_tile import * from .wms import * +from .raster_style import * logger = logging.getLogger(__name__) diff --git a/geomanager/models/raster_file.py b/geomanager/models/raster_file.py index 2cb9db8..233d461 100644 --- a/geomanager/models/raster_file.py +++ b/geomanager/models/raster_file.py @@ -1,37 +1,30 @@ import os from django.core.exceptions import ValidationError -from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django_extensions.db.models import TimeStampedModel -from modelcluster.fields import ParentalKey -from modelcluster.models import ClusterableModel -from wagtail.admin.panels import FieldPanel, FieldRowPanel, MultiFieldPanel, InlinePanel +from wagtail.admin.panels import FieldPanel, MultiFieldPanel from wagtail.api.v2.utils import get_full_url from wagtail.fields import StreamField from wagtail.images.blocks import ImageChooserBlock from wagtail.images.models import Image -from wagtail.models import Orderable -from wagtail_color_panel.edit_handlers import NativeColorPanel -from wagtail_color_panel.fields import ColorField from wagtail_modeladmin.helpers import AdminURLHelper from geomanager.blocks import ( FileLayerPointAnalysisBlock, FileLayerAreaAnalysisBlock, InlineLegendBlock ) -from geomanager.forms import RasterStyleModelForm from geomanager.helpers import get_raster_layer_files_url +from geomanager.models.raster_style import RasterStyle from geomanager.models.core import Dataset, BaseLayer from geomanager.settings import geomanager_settings from geomanager.storage import OverwriteStorage from geomanager.utils import DATE_FORMAT_CHOICES from geomanager.validators import validate_directory_name -from geomanager.widgets import RasterStyleWidget class RasterFileLayer(TimeStampedModel, BaseLayer): @@ -374,193 +367,3 @@ def __str__(self): return f"{self.dataset} - {self.created}" -class RasterStyle(TimeStampedModel, ClusterableModel): - base_form_class = RasterStyleModelForm - - name = models.CharField(max_length=256, verbose_name=_("name"), - help_text=_("Style name for identification")) - unit = models.CharField(max_length=100, blank=True, null=True, verbose_name=_("data unit"), - help_text=_("Data unit")) - min = models.IntegerField(default=0, verbose_name=_("minimum value"), help_text=_("minimum value")) - max = models.IntegerField(default=100, verbose_name=_("maximum value"), help_text=_("maximum value")) - steps = models.IntegerField(default=5, validators=[MinValueValidator(3), MaxValueValidator(20), ], null=True, - blank=True, verbose_name=_("steps"), help_text=_("Number of steps")) - use_custom_colors = models.BooleanField(default=False, verbose_name=_("Use Custom Colors")) - palette = models.TextField(blank=True, null=True, verbose_name=_("Color Palette")) - interpolate = models.BooleanField(default=False, verbose_name=_("interpolate"), help_text="Interpolate colorscale") - custom_color_for_rest = ColorField(blank=True, null=True, default="#ff0000", - verbose_name=_("Color for the rest of values"), - help_text=_( - "Color for values greater than the values defined above, " - "as well as values greater than the maximum defined value")) - - class Meta: - verbose_name = _("Raster Style") - verbose_name_plural = _("Raster Styles") - - def __str__(self): - return self.name - - panels = [ - FieldPanel("name"), - FieldPanel("unit"), - FieldRowPanel( - [ - FieldPanel("min"), - FieldPanel("max"), - ], _("Data values") - ), - FieldPanel("steps"), - FieldPanel("palette", widget=RasterStyleWidget), - FieldPanel("use_custom_colors"), - MultiFieldPanel([ - InlinePanel("color_values", heading=_("Color Values"), label=_("Color Value")), - NativeColorPanel("custom_color_for_rest"), - ], _("Custom Color Values")), - - # FieldPanel("interpolate") - ] - - def get_palette_list(self): - if not self.use_custom_colors: - return self.palette.split(",") - return self.get_custom_palette() - - @property - def min_value(self): - return self.min - - @property - def max_value(self): - max_value = self.max - if self.min == max_value: - max_value += 0.1 - return max_value - - @property - def scale_value(self): - return 254 / (self.max_value - self.min_value) - - @property - def offset_value(self): - return -self.min_value - - @property - def clip_value(self): - return self.max_value + self.offset_value - - def get_custom_color_values(self): - values = [] - color_values = self.color_values.order_by('threshold') - - for i, c_value in enumerate(color_values): - value = c_value.value - # if not the first one, add prev value for later comparison - if i == 0: - value["min_value"] = None - else: - value["min_value"] = color_values[i - 1].threshold - value["max_value"] = value["threshold"] - values.append(value) - return values - - def get_custom_palette(self): - colors = [] - for i in range(256): - color = self.get_color_for_index(i) - colors.append(color) - - return colors - - def get_color_for_index(self, index_value): - values = self.get_custom_color_values() - - for value in values: - max_value = value["max_value"] + self.offset_value - - if max_value > self.clip_value: - max_value = self.clip_value - - if max_value < 0: - max_value = 0 - - max_value = self.scale_value * max_value - - if value["min_value"] is None: - if index_value <= max_value: - return values[0]["color"] - - if value["min_value"] is not None: - min_value = value["min_value"] + self.offset_value - - if min_value > self.clip_value: - min_value = self.clip_value - - if min_value < 0: - min_value = 0 - - min_value = self.scale_value * min_value - - if min_value < index_value <= max_value: - return value["color"] - - return self.custom_color_for_rest - - def get_style_as_json(self): - palette = self.get_palette_list() - style = { - "bands": [ - { - "band": 1, - "min": self.min, - "max": self.max, - "palette": palette, - "scheme": "discrete", - } - ] - } - return style - - def get_legend_config(self): - items = [] - if self.use_custom_colors: - values = self.get_custom_color_values() - count = len(values) - - if count > 1: - for value in values: - item = { - "name": value['label'] if value.get('label') else value['threshold'], - "color": value['color'] - } - items.append(item) - rest_item = {"name": "", "color": self.custom_color_for_rest} - items.append(rest_item) - - return {"type": "choropleth", "items": items} - - -class ColorValue(TimeStampedModel, Orderable): - layer = ParentalKey(RasterStyle, related_name='color_values') - threshold = models.FloatField(verbose_name=_("Threshold value"), help_text=_( - "Values less than or equal to the input value, will be assigned the chosen color")) - color = ColorField(default="#ff0000", verbose_name=_("color")) - label = models.CharField(max_length=100, blank=True, null=True, verbose_name=_('Optional Label')) - - class Meta: - verbose_name = _("Color Value") - verbose_name_plural = _("Color Values") - - panels = [ - FieldPanel("threshold"), - NativeColorPanel("color"), - FieldPanel("label") - ] - - @property - def value(self): - return { - "threshold": self.threshold, - "color": self.color, - "label": self.label - } diff --git a/geomanager/models/raster_style.py b/geomanager/models/raster_style.py new file mode 100644 index 0000000..fcabe07 --- /dev/null +++ b/geomanager/models/raster_style.py @@ -0,0 +1,222 @@ +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django_extensions.db.models import TimeStampedModel +from modelcluster.fields import ParentalKey +from modelcluster.models import ClusterableModel +from wagtail.admin.panels import FieldPanel, FieldRowPanel, MultiFieldPanel, InlinePanel +from wagtail.models import Orderable +from wagtail_color_panel.edit_handlers import NativeColorPanel +from wagtail_color_panel.fields import ColorField + +from geomanager.forms import RasterStyleModelForm +from geomanager.widgets import RasterStyleWidget + + +class RasterStyle(TimeStampedModel, ClusterableModel): + LEGEND_TYPE_CHOICES = ( + ("basic", _("Basic")), + ("choropleth", _("Choropleth")), + ("gradient", _("Gradient")), + ) + + base_form_class = RasterStyleModelForm + + name = models.CharField(max_length=256, verbose_name=_("name"), + help_text=_("Style name for identification")) + unit = models.CharField(max_length=100, blank=True, null=True, verbose_name=_("data unit"), + help_text=_("Data unit")) + min = models.IntegerField(default=0, verbose_name=_("minimum value"), help_text=_("minimum value")) + max = models.IntegerField(default=100, verbose_name=_("maximum value"), help_text=_("maximum value")) + steps = models.IntegerField(default=5, validators=[MinValueValidator(3), MaxValueValidator(20), ], null=True, + blank=True, verbose_name=_("steps"), help_text=_("Number of steps")) + use_custom_colors = models.BooleanField(default=False, verbose_name=_("Use Custom Colors")) + palette = models.TextField(blank=True, null=True, verbose_name=_("Color Palette")) + interpolate = models.BooleanField(default=False, verbose_name=_("interpolate"), help_text="Interpolate colorscale") + legend_type = models.CharField(max_length=100, choices=LEGEND_TYPE_CHOICES, default="choropleth", + verbose_name=_("Legend Type")) + custom_color_for_rest = ColorField(blank=True, null=True, default="#ff0000", + verbose_name=_("Color for the rest of values"), + help_text=_( + "Color for values greater than the values defined above, " + "as well as values greater than the maximum defined value")) + + class Meta: + verbose_name = _("Raster Style") + verbose_name_plural = _("Raster Styles") + + def __str__(self): + return self.name + + panels = [ + FieldPanel("name"), + FieldPanel("unit"), + FieldRowPanel( + [ + FieldPanel("min"), + FieldPanel("max"), + ], _("Data values") + ), + FieldPanel("steps"), + FieldPanel("palette", widget=RasterStyleWidget), + FieldPanel("legend_type"), + FieldPanel("use_custom_colors"), + MultiFieldPanel([ + InlinePanel("color_values", heading=_("Color Values"), label=_("Color Value")), + NativeColorPanel("custom_color_for_rest"), + ], _("Custom Color Values")), + + # FieldPanel("interpolate") + ] + + def get_palette_list(self): + if not self.use_custom_colors: + return self.palette.split(",") + return self.get_custom_palette() + + @property + def min_value(self): + return self.min + + @property + def max_value(self): + max_value = self.max + if self.min == max_value: + max_value += 0.1 + return max_value + + @property + def scale_value(self): + return 254 / (self.max_value - self.min_value) + + @property + def offset_value(self): + return -self.min_value + + @property + def clip_value(self): + return self.max_value + self.offset_value + + def get_custom_color_values(self): + values = [] + color_values = self.color_values.order_by('threshold') + + for i, c_value in enumerate(color_values): + value = c_value.value + # if not the first one, add prev value for later comparison + if i == 0: + value["min_value"] = None + else: + value["min_value"] = color_values[i - 1].threshold + value["max_value"] = value["threshold"] + values.append(value) + return values + + def get_custom_palette(self): + colors = [] + for i in range(256): + color = self.get_color_for_index(i) + colors.append(color) + + return colors + + def get_color_for_index(self, index_value): + values = self.get_custom_color_values() + + for value in values: + max_value = value["max_value"] + self.offset_value + + if max_value > self.clip_value: + max_value = self.clip_value + + if max_value < 0: + max_value = 0 + + max_value = self.scale_value * max_value + + if value["min_value"] is None: + if index_value <= max_value: + return values[0]["color"] + + if value["min_value"] is not None: + min_value = value["min_value"] + self.offset_value + + if min_value > self.clip_value: + min_value = self.clip_value + + if min_value < 0: + min_value = 0 + + min_value = self.scale_value * min_value + + if min_value < index_value <= max_value: + return value["color"] + + return self.custom_color_for_rest + + def get_style_as_json(self): + palette = self.get_palette_list() + style = { + "bands": [ + { + "band": 1, + "min": self.min, + "max": self.max, + "palette": palette, + "scheme": "discrete", + } + ] + } + return style + + def get_legend_config(self): + items = [] + if self.use_custom_colors: + values = self.get_custom_color_values() + count = len(values) + + if count > 1: + for value in values: + item = { + "name": "", + "color": value['color'] + } + if value.get("show_on_legend"): + item.update({ + "name": value['label'] if value.get('label') else value['threshold'], + }) + + items.append(item) + rest_item = {"name": "", "color": self.custom_color_for_rest} + items.append(rest_item) + + return {"type": self.legend_type, "items": items} + + +class ColorValue(TimeStampedModel, Orderable): + layer = ParentalKey(RasterStyle, related_name='color_values') + threshold = models.FloatField(verbose_name=_("Threshold value"), help_text=_( + "Values less than or equal to the input value, will be assigned the chosen color")) + color = ColorField(default="#ff0000", verbose_name=_("color")) + show_on_legend = models.BooleanField(default=True, verbose_name=_("Show label on Legend")) + label = models.CharField(max_length=100, blank=True, null=True, verbose_name=_('Optional Label')) + + class Meta: + verbose_name = _("Color Value") + verbose_name_plural = _("Color Values") + + panels = [ + FieldPanel("threshold"), + NativeColorPanel("color"), + FieldPanel("show_on_legend"), + FieldPanel("label") + ] + + @property + def value(self): + return { + "threshold": self.threshold, + "color": self.color, + "label": self.label, + "show_on_legend": self.show_on_legend + } diff --git a/setup.cfg b/setup.cfg index aa49902..c6dd552 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = geomanager -version = 0.4.5 +version = 0.4.6 description = Wagtail based Geospatial Data Manager long_description = file:README.md long_description_content_type = text/markdown