text: import json
  import math
  
  import numpy
  from colorful.fields import RGBColorField
  
  from django.conf import settings
  from django.contrib.gis.db import models
  from django.contrib.gis.gdal import Envelope, OGRGeometry, SpatialReference
  from django.contrib.postgres.fields import ArrayField
  from django.db.models import Max, Min
  from django.db.models.signals import m2m_changed, post_save, pre_save
  from django.dispatch import receiver
  
  from .const import WEB_MERCATOR_SRID
  from .utils import hex_to_rgba
  from .valuecount import ValueCountMixin
  
  
  class LegendSemantics(models.Model):
      """
      Labels for pixel types (urban, forrest, warm, cold, etc)
      """
      name = models.CharField(max_length=50)
      description = models.TextField(null=True, blank=True)
      keyword = models.TextField(null=True, blank=True, max_length=100)
  
      def __str__(self):
          return self.name
  
  
  class LegendEntry(models.Model):
      """
      One row in a Legend.
      """
      semantics = models.ForeignKey(LegendSemantics)
      expression = models.CharField(max_length=500,
          help_text='Use a number or a valid numpy logical expression where x is the'
                    'pixel value. For instance: "(-3.0 < x) & (x <= 1)" or "x <= 1".')
      color = RGBColorField()
  
      def __str__(self):
          return '{}, {}, {}'.format(self.semantics.name,
                                     self.expression,
                                     self.color)
  
  
  class Legend(models.Model):
      """
      Legend object for Rasters.
      """
      title = models.CharField(max_length=200)
      description = models.TextField(null=True, blank=True)
      entries = models.ManyToManyField(LegendEntry)
      json = models.TextField(null=True, blank=True)
      modified = models.DateTimeField(auto_now=True)
  
      def __str__(self):
          return self.title
  
      def update_json(self):
          data = []
          for val in self.entries.all():
              data.append({
                  'name': val.semantics.name,
                  'expression': val.expression,
                  'color': val.color
              })
          self.json = json.dumps(data)
  
      @property
      def colormap(self):
          legend = json.loads(self.json)
          cmap = {}
          for leg in legend:
              cmap[leg['expression']] = hex_to_rgba(leg['color'])
          return cmap
  
      def save(self, *args, **kwargs):
          if self.id:
              self.update_json()
          super(Legend, self).save(*args, **kwargs)
  
  
  def legend_entries_changed(sender, instance, action, **kwargs):
      """
      Updates style json upon adding or removing legend entries.
      """
      if action in ('post_add', 'post_remove'):
          instance.update_json()
          instance.save()
  
  m2m_changed.connect(legend_entries_changed, sender=Legend.entries.through)
  
  
  @receiver(post_save, sender=LegendEntry)
  def update_dependent_legends_on_entry_change(sender, instance, **kwargs):
      """
      Updates dependent Legends on a change in Legend entries.
      """
      for legend in Legend.objects.filter(entries__id=instance.id):
          legend.update_json()
          legend.save()
  
  
  @receiver(post_save, sender=LegendSemantics)
  def update_dependent_legends_on_semantics_change(sender, instance, **kwargs):
      """
      Updates dependent Legends on a change in Semantics.
      """
      for entry in LegendEntry.objects.filter(semantics_id=instance.id):
          for legend in Legend.objects.filter(entries__id=entry.id):
              legend.update_json()
              legend.save()
  
  
  class RasterLayer(models.Model, ValueCountMixin):
      """
      Source data model for raster layers
      """
      CONTINUOUS = 'co'
      CATEGORICAL = 'ca'
      MASK = 'ma'
      RANK_ORDERED = 'ro'
  
      DATATYPES = (
          (CONTINUOUS, 'Continuous'),
          (CATEGORICAL, 'Categorical'),
          (MASK, 'Mask'),
          (RANK_ORDERED, 'Rank Ordered')
      )
  
      name = models.CharField(max_length=100, blank=True, null=True)
      description = models.TextField(blank=True, null=True)
      datatype = models.CharField(max_length=2, choices=DATATYPES, default='co')
      rasterfile = models.FileField(upload_to='rasters', null=True, blank=True)
      nodata = models.CharField(max_length=100, null=True, blank=True,
          help_text='Leave blank to keep the internal band nodata values. If a nodata'
                    'value is specified here, it will be used for all bands of this raster.')
      legend = models.ForeignKey(Legend, blank=True, null=True)
      modified = models.DateTimeField(auto_now=True)
  
      def __str__(self):
          return '{} {} (type: {})'.format(self.id, self.name, self.datatype)
  
      @property
      def discrete(self):
          """
          Returns true for discrete rasters.
          """
          return self.datatype in (self.CATEGORICAL, self.MASK, self.RANK_ORDERED)
  
      _bbox = None
  
      def extent(self, srid=WEB_MERCATOR_SRID):
          """
          Returns bbox for layer.
          """
          if not self._bbox:
              # Get bbox for raster in original coordinates
              meta = self.metadata
              xmin = meta.uperleftx
              ymax = meta.uperlefty
              xmax = xmin + meta.width * meta.scalex
              ymin = ymax + meta.height * meta.scaley
  
              # Create Polygon box
              geom = OGRGeometry(Envelope((xmin, ymin, xmax, ymax)).wkt)
  
              # Set original srs
              if meta.srs_wkt:
                  geom.srs = SpatialReference(meta.srs_wkt)
              else:
                  geom.srid = meta.srid
  
              # Transform to requested srid
              geom.transform(srid)
  
              # Calculate value range for bbox
              coords = geom.coords[0]
              xvals = [x[0] for x in coords]
              yvals = [x[1] for x in coords]
  
              # Set bbox
              self._bbox = (min(xvals), min(yvals), max(xvals), max(yvals))
          return self._bbox
  
      def index_range(self, zoom):
          """
          Returns the index range for
          """
          return self.rastertile_set.filter(tilez=zoom).aggregate(
              Min('tilex'), Max('tilex'), Min('tiley'), Max('tiley')
          )
  
  
  @receiver(pre_save, sender=RasterLayer)
  def reset_parse_log_if_data_changed(sender, instance, **kwargs):
      try:
          obj = RasterLayer.objects.get(pk=instance.pk)
      except RasterLayer.DoesNotExist:
          pass
      else:
          if obj.rasterfile.name != instance.rasterfile.name:
              instance.parsestatus.log = ''
  
  
  @receiver(post_save, sender=RasterLayer)
  def parse_raster_layer_if_log_is_empty(sender, instance, created, **kwargs):
      if created:
          RasterLayerParseStatus.objects.create(rasterlayer=instance)
          RasterLayerMetadata.objects.create(rasterlayer=instance)
  
      if instance.rasterfile.name and instance.parsestatus.log == '':
          if hasattr(settings, 'RASTER_USE_CELERY') and settings.RASTER_USE_CELERY:
              from raster.tasks import parse_raster_layer_with_celery
              parse_raster_layer_with_celery.delay(instance)
          else:
              from raster.parser import RasterLayerParser
              parser = RasterLayerParser(instance)
              parser.parse_raster_layer()
  
  
  class RasterLayerMetadata(models.Model):
      """
      Stores meta data for a raster layer
      """
      rasterlayer = models.OneToOneField(RasterLayer, related_name='metadata')
      uperleftx = models.FloatField(null=True, blank=True)
      uperlefty = models.FloatField(null=True, blank=True)
      width = models.IntegerField(null=True, blank=True)
      height = models.IntegerField(null=True, blank=True)
      scalex = models.FloatField(null=True, blank=True)
      scaley = models.FloatField(null=True, blank=True)
      skewx = models.FloatField(null=True, blank=True)
      skewy = models.FloatField(null=True, blank=True)
      numbands = models.IntegerField(null=True, blank=True)
      srs_wkt = models.TextField(null=True, blank=True)
      srid = models.PositiveSmallIntegerField(null=True, blank=True)
      max_zoom = models.PositiveSmallIntegerField(null=True, blank=True)
  
      def __str__(self):
          return self.rasterlayer.name
  
  
  class RasterLayerParseStatus(models.Model):
      """
      Tracks the parsing status of the raster layer.
      """
      UNPARSED = 0
      DOWNLOADING_FILE = 1
      REPROJECTING_RASTER = 2
      CREATING_TILES = 3
      DROPPING_EMPTY_TILES = 4
      FINISHED = 5
      FAILED = 6
  
      STATUS_CHOICES = (
          (UNPARSED, 'Layer not yet parsed'),
          (DOWNLOADING_FILE, 'Downloading file'),
          (REPROJECTING_RASTER, 'Reprojecting'),
          (CREATING_TILES, 'Creating tiles'),
          (DROPPING_EMPTY_TILES, 'Dropping empty tiles'),
          (FINISHED, 'Finished parsing'),
          (FAILED, 'Failed parsing'),
      )
      rasterlayer = models.OneToOneField(RasterLayer, related_name='parsestatus')
      status = models.IntegerField(choices=STATUS_CHOICES, default=UNPARSED)
      tile_level = models.IntegerField(null=True, blank=True)
      log = models.TextField(default='', editable=False)
  
      def __str__(self):
          return '{0} - {1}'.format(self.rasterlayer.name, self.get_status_display())
  
  
  class RasterLayerBandMetadata(models.Model):
  
      HISTOGRAM_BINS = 100
  
      rasterlayer = models.ForeignKey(RasterLayer)
      band = models.PositiveIntegerField()
      nodata_value = models.FloatField(null=True)
      max = models.FloatField()
      min = models.FloatField()
      hist_values = ArrayField(models.FloatField(), size=HISTOGRAM_BINS)
      hist_bins = ArrayField(models.FloatField(), size=HISTOGRAM_BINS + 1)
  
      def __str__(self):
          return '{} - Min {} - Max {}'.format(self.rasterlayer.name, self.min, self.max)
  
      def save(self, *args, **kwargs):
          if not self.pk:
              # Construct empty histogram
              hist = numpy.histogram(
                  [],
                  range=(math.floor(self.min), math.ceil(self.max)),
                  bins=self.HISTOGRAM_BINS
              )
              # Set empty histogram values
              self.hist_values = hist[0].tolist()
              self.hist_bins = hist[1].tolist()
  
          super(RasterLayerBandMetadata, self).save(*args, **kwargs)
  
  
  class RasterTile(models.Model):
      """
      Store individual tiles of a raster data source layer.
      """
      ZOOMLEVELS = (
          (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7),
          (8, 8), (9, 9), (10, 10), (11, 11), (12, 12), (13, 13),
          (14, 14), (15, 15), (16, 16), (17, 17), (18, 18)
      )
      rid = models.AutoField(primary_key=True)
      rast = models.RasterField(null=True, blank=True, srid=WEB_MERCATOR_SRID)
      rasterlayer = models.ForeignKey(RasterLayer, null=True, blank=True, db_index=True)
      tilex = models.IntegerField(db_index=True, null=True)
      tiley = models.IntegerField(db_index=True, null=True)
      tilez = models.IntegerField(db_index=True, null=True, choices=ZOOMLEVELS)
  
      def __str__(self):
          return '{0} {1}'.format(self.rid, self.filename)
  