Do not change the following items!
GROUPING_HINT = 'grouping_hint'
USER_WORKSPACES = 'user'
DEFAULT_WORKSPACES = 'default'
TEMP_WORKSPACES = 'temp'
OTHER_WORKSPACES = 'other' # for flexible adding workspaces of others
TODO: Can this property be moved to mapnik_helper?
ICON_ORIGINALS = pkg_resources.resource_filename('lizard_map', 'icons')
ADAPTER_ENTRY_POINT = 'lizard_map.adapter_class'
SEARCH_ENTRY_POINT = 'lizard_map.search_method'
LOCATION_ENTRY_POINT = 'lizard_map.location_method'
WMS is a special kind of adapter: the client side behaves different.
ADAPTER_CLASS_WMS = 'wms'
logger = logging.getLogger(__name__)
Add introspection rules for ColorField
add_introspection_rules([], ["lizard_map.models.ColorField"])
Interpolates colors between min_value and max_value, calc
def legend_values(min_value, max_value, min_color, max_color, steps):
corresponding colors and gives boundary values for each band.
Makes list of dictionaries: {'color': Color, 'low_value': low value, 'high_value': high value}""" result = [] value_per_step = (max_value - min_value) / steps for step in range(steps):
try: fraction = float(step) / (steps - 1) except ZeroDivisionError: fraction = 0 alpha = (min_color.a * (1 - fraction) + max_color.a * fraction) red = (min_color.r * (1 - fraction) + max_color.r * fraction) green = (min_color.g * (1 - fraction) + max_color.g * fraction) blue = (min_color.b * (1 - fraction) + max_color.b * fraction) color = Color('%02x%02x%02x%02x' % (red, green, blue, alpha))low_value = min_value + step * value_per_step high_value = min_value + (step + 1) * value_per_step result.append({ 'color': color, 'low_value': low_value, 'high_value': high_value, }) return result
class Color(str): Simple color object: r, g, b, a.
The object is in fact a string with class variables.
def init(self, s):
self.r = None self.g = None self.b = None if s is None: return try: self.r = int(s[0:2], 16) except ValueError: self.r = 128 try: self.g = int(s[2:4], 16) except ValueError: self.b = 128 try: self.b = int(s[4:6], 16) except ValueError: self.b = 128 try:def to_tuple(self):Alpha is optional.
self.a = int(s[6:8], 16) except ValueError: self.a = 255
Returns color values in a tuple. Values are 0..1
result = (self.r / 255.0, self.g / 255.0,
self.b / 255.0, self.a / 255.0) return result@property def html(self):
Returns color in html format.
if self.r is not None and self.g is not None and self.b is not None:
return '#%02x%02x%02x' % (self.r, self.g, self.b) else: return '#ff0000' # Red as alarm color
class ColorField(models.CharField): Custom ColorField for use in Django models. It's an extension
of CharField."""
default_error_messages = {
'invalid': _(
u'Enter a valid color code rrggbbaa, '
'where aa is optional.'),
}
description = "Color representation in rgb"
#DIVIDER
__metaclass__ = models.SubfieldBase
#DIVIDER
def adapter_class_names():
#DIVIDER
entrypoints = [(entrypoint.name, entrypoint.name) for entrypoint in
pkg_resources.iter_entry_points(group=ADAPTER_ENTRY_POINT)]
return tuple(entrypoints)
#DIVIDER
class WorkspaceItemError(Exception):
#DIVIDER
pass
#DIVIDER
#DIVIDER
class Workspace(models.Model):
#DIVIDER
Returns workspace extent, using extents from workspace items.
#DIVIDER
Returns a list of wms_layers. Each wms_layer is a dict with keys:
wms_id, name, url, params, options. They are used in wms.html
#DIVIDER
for workspace_item in self.workspace_items.filter(visible=True):
if workspace_item.adapter.is_animatable:
return True
return False
#DIVIDER
class WorkspaceItem(models.Model):
#DIVIDER
for entrypoint in pkg_resources.iter_entry_points(
group=ADAPTER_ENTRY_POINT):
if entrypoint.name == self.adapter_class:
try:
real_adapter = entrypoint.load()
real_adapter = real_adapter(self,
layer_arguments=self.adapter_layer_arguments)
except ImportError, e:
logger.critical("Invalid entry point: %s", e)
raise
except WorkspaceItemError:
logger.warning(
"Deleting problematic WorkspaceItem: %s", self)
#DIVIDER
self.delete()
return real_adapter
raise AdapterClassNotFoundError(
u'Entry point for %r not found' % self.adapter_class)
@property
#DIVIDER
def adapter_layer_arguments(self):
#DIVIDER
layer_json = self.adapter_layer_json
if not layer_json:
return {}
result = {}
for k, v in json.loads(layer_json).items():
result[str(k)] = v
return result
#DIVIDER
def has_adapter(self):
#DIVIDER
Return true if workspace item has a extent that makes sense.
#DIVIDER
When deleting a WorkspaceItem, delete corresponding snippets
#DIVIDER
name = models.CharField(max_length=80,
default='Collage')
workspace = models.ForeignKey(Workspace,
related_name='collages')
#DIVIDER
def locations(self):
#DIVIDER
snippets_in_groups = [snippet_group.snippets.all()
for snippet_group in self.snippet_groups.all()]
#DIVIDER
snippets = list(itertools.chain(*snippets_in_groups))
return [snippet.location for snippet in snippets]
@property
#DIVIDER
def workspace_items(self):
#DIVIDER
Makes snippet in a snippet group. Finds or creates
corresponding snippet group (see below)
#DIVIDER
AGGREGATION_PERIOD_CHOICES = (
(ALL, _('all')),
(YEAR, _('year')),
(QUARTER, _('quarter')),
(MONTH, _('month')),
(WEEK, _('week')),
(DAY, _('day')),
)
workspace_collage = models.ForeignKey(WorkspaceCollage,
related_name='snippet_groups')
index = models.IntegerField(default=1000) # larger = lower in the list
name = models.CharField(max_length=80, blank=True, null=True)
#DIVIDER
boundary_value = models.FloatField(blank=True, null=True)
#DIVIDER
percentile_value = models.FloatField(blank=True, null=True)
#DIVIDER
restrict_to_month = models.IntegerField(blank=True, null=True)
aggregation_period = models.IntegerField(
choices=AGGREGATION_PERIOD_CHOICES, default=ALL)
layout_title = models.CharField(max_length=80, blank=True, null=True)
layout_x_label = models.CharField(max_length=80, blank=True, null=True)
layout_y_label = models.CharField(max_length=80, blank=True, null=True)
layout_y_min = models.FloatField(blank=True, null=True)
layout_y_max = models.FloatField(blank=True, null=True)
#DIVIDER
def statistics(self, start_date, end_date):
#DIVIDER
periods = calc_aggregation_periods(start_date, end_date,
self.aggregation_period)
for period_start_date, period_end_date in periods:
if not self.restrict_to_month or (
self.aggregation_period != MONTH) or (
self.aggregation_period == MONTH and
self.restrict_to_month == period_start_date.month):
#DIVIDER
statistics_row = snippet_adapter.value_aggregate(
snippet.identifier,
{'min': None,
'max': None,
'avg': None,
'count_lt': self.boundary_value,
'count_gte': self.boundary_value,
'percentile': self.percentile_value},
start_date=period_start_date,
end_date=period_end_date)
#DIVIDER
if statistics_row:
statistics_row['name'] = snippet.name
statistics_row['period'] = fancy_period(
period_start_date, period_end_date,
self.aggregation_period)
statistics.append(statistics_row)
if len(statistics) > 1:
#DIVIDER
averages = [row['avg'] for row in statistics if row['avg']]
try:
average = float(sum(averages) / len(averages))
except ZeroDivisionError:
average = None
totals = {
'min': min([row['min'] for row in statistics]),
'max': max([row['max'] for row in statistics]),
'avg': average,
'name': 'Totaal',
}
if statistics[0]['count_lt'] is not None:
totals['count_lt'] = sum(
[row['count_lt'] for row in statistics
if row['count_lt']])
else:
totals['count_lt'] = None
if statistics[0]['count_gte'] is not None:
totals['count_gte'] = sum(
[row['count_gte'] for row in statistics
if row['count_gte']])
else:
totals['count_gte'] = None
#DIVIDER
totals['percentile'] = None
statistics.append(totals)
return statistics
#DIVIDER
def values_table(self, start_date, end_date):
#DIVIDER
values_table.append(['Datum + tijdstip'] +
[snippet.name for snippet in snippets])
#DIVIDER
found_dates = {}
#DIVIDER
snippet_values = {}
for snippet in snippets:
values = snippet.workspace_item.adapter.values(
identifier=snippet.identifier, start_date=start_date,
end_date=end_date)
snippet_values[snippet.id] = {}
#DIVIDER
for row in values:
if not self.restrict_to_month or (
self.aggregation_period != MONTH) or (
self.aggregation_period == MONTH and
row['datetime'].month == self.restrict_to_month):
snippet_values[snippet.id][row['datetime']] = row
found_dates.update(snippet_values[snippet.id])
#DIVIDER
found_dates_sorted = found_dates.keys()
found_dates_sorted.sort()
#DIVIDER
for found_date in found_dates_sorted:
value_row = [found_date, ]
for snippet in snippets:
single_value = snippet_values[snippet.id].get(found_date, {})
value_row.append(single_value.get('value', None))
values_table.append(value_row)
return values_table
#DIVIDER
def layout(self):
#DIVIDER
name = models.CharField(max_length=80,
default='Snippet')
shortname = models.CharField(max_length=80,
default='Snippet',
blank=True,
null=True)
snippet_group = models.ForeignKey(WorkspaceCollageSnippetGroup,
related_name='snippets')
workspace_item = models.ForeignKey(
WorkspaceItem)
identifier_json = models.TextField()
#DIVIDER
#DIVIDER
def save(self, *args, **kwargs):
#DIVIDER
workspace = self.snippet_group.workspace_collage.workspace
if len(workspace.workspace_items.filter(
pk=self.workspace_item.pk)) == 0:
raise "workspace_item of snippet not in workspace of collage"
#DIVIDER
super(WorkspaceCollageSnippet, self).save(*args, **kwargs)
#DIVIDER
def delete(self, *args, **kwargs):
#DIVIDER
start_end_dates: 2-tuple of datetimes
#DIVIDER
self.identifier_json = json.dumps(identifier).replace('"', '%22')
#DIVIDER
class LegendManager(models.Manager):
#DIVIDER
#DIVIDER
def find(self, name):
#DIVIDER
color-max, color < min, color > max, number of steps. Legends can
be found using the descriptor.
Used for mapnik lines and polygons.
#DIVIDER
Required by legend_default."""
delta = abs(self.max_value - self.min_value) / self.steps
if delta < 0.1:
return '%.3f'
if delta < 1:
return '%.2f'
if delta < 10:
return '%.1f'
return '%.0f'
Ensures that to_python is always called.
def legend_values(self):
Return allowed layer method names (from entrypoints)
following format: string rrggbb in hex """
#DIVIDER
def mapnik_style(self, value_field=None):
#DIVIDER
Makes mapnik rule for looks. For lines and polygons.
#DIVIDER
line/polystyle from Legend object"""
return self.mapnik_style(self, value_field=value_field)
in tuple of 2-tuples
class LegendPoint(Legend):
To be raised when a WorkspaceItem is out of date.
def mapnik_style(self, value_field=None):
A WorkspaceItem can represent something that does no longer exist. For example, it may refer to a shape that has been deleted from the database. This error may trigger deletion of such orphans.
Defines background maps.
Global settings.
Available keys with default values:
projection 'EPSG:900913'
display_projection 'EPSG:4326'
googlemaps_api_key
Collection for managing what's visible on a map.
Return value from given key.
If the key does not exist, return None
class Meta:
verbose_name = ("Workspace") verbose_name_plural = ("Workspaces")name = models.CharField(max_length=80,
blank=True, default='My Workspace')owner = models.ForeignKey(User, blank=True, null=True) visible = models.BooleanField(default=False)
def unicode(self): return u'%s' % (self.name)
def get_absolute_url(self): return reverse('lizard_map_workspace', kwargs={'workspace_id': self.id})
def extent(self):
Return {key: value} for given key
north = None south = None east = None west = None for workspace_item in self.workspace_items.all():
wsi_extent = workspace_item.adapter.extent() if wsi_extent['east'] > east or east is None: east = wsi_extent['east'] if wsi_extent['west'] < west or west is None: west = wsi_extent['west'] if wsi_extent['south'] < south or south is None: south = wsi_extent['south'] if wsi_extent['north'] > north or north is None: north = wsi_extent['north'] return {'north': north, 'south': south, 'east': east, 'west': west}def wms_layers(self):
result = [] for workspace_item in self.workspace_items.filter(
adapter_class=ADAPTER_CLASS_WMS, visible=True):
layer_arguments = workspace_item.adapter_layer_arguments layer_arguments.update(
{'wms_id': '%d_%d' % (self.id, workspace_item.id)}) result.append(layer_arguments)result.reverse() return result
@property def is_animatable(self): Determine if any visible workspace_item is animatable.
Can show things on a map based on configuration in a url.
class Meta:
ordering = ['index'] verbose_name = ("Workspace item") verbose_name_plural = ("Workspace items")name = models.CharField(max_length=80,
blank=True) workspace = models.ForeignKey(Workspace, related_name='workspace_items') adapter_class = models.SlugField(blank=True, choices=adapter_class_names()) adapter_layer_json = models.TextField(blank=True)index = models.IntegerField(blank=True, default=0) visible = models.BooleanField(default=True)^^^ Contains json (TODO: add json verification)
def unicode(self): return u'(%d) name=%s ws=%s %s' % (self.id, self.name, self.workspace,
self.adapter_class)@property def adapter(self): Return adapter instance for entrypoint
Trac #2470. Return a NullAdapter instead?
Return dict of parsed adapter_layer_json.
Converts keys to str.
Can I provide a adapter class for i.e. WMS layer?
extent = self.adapter.extent() if None in extent.values():
return False else: return Truedef delete(self, args, *kwargs):
snippets = WorkspaceCollageSnippet.objects.filter(workspace_item=self)
for snippet in snippets:
snippet.delete() super(WorkspaceItem, self).delete(args, *kwargs)
class WorkspaceCollage(models.Model): A collage contains selections/locations from a workspace
locations of all snippets
Flatten snippets in groups:
Return workspace items used by one of our snippets.
# found.add(snippet.workspace_item)def get_or_create_snippet(self, workspace_item, identifier_json,return list(found)
return WorkspaceItem.objects.filter( workspacecollagesnippet__in=self.snippets.all()).distinct()
shortname, name):
found_snippet_group = None identifier = parse_identifier_json(identifier_json) snippet_groups = self.snippet_groups.all()
if GROUPING_HINT in identifier:
for snippet_group in snippet_groups: for snippet in snippet_group.snippets.all(): if snippet.identifier.get( GROUPING_HINT) == identifier[GROUPING_HINT]: found_snippet_group = snippet_group break
if not found_snippet_group: for snippet_group in snippet_groups: if snippet_group.snippets.filter( workspace_item=workspace_item).exists(): found_snippet_group = snippet_group break
if not found_snippet_group: found_snippet_group = self.snippet_groups.create()
snippet, snippet_created = found_snippet_group.snippets.get_or_create( workspace_item=workspace_item, identifier_json=identifier_json, shortname=shortname, name=name) return snippet, snippet_created
class WorkspaceCollageSnippetGroup(models.Model): Contains a group of snippets, belongs to one collage
Boundary value for statistics.
Percentile value for statistics.
Restrict_to_month is used to filter the data.
Calcs standard statistics: min, max, avg, count_lt, count_gte, percentile and return them in a list of dicts
Can be filtered by option restrict_to_month.
Calc periods based on aggregation period setting.
Base statistics for each period.
Add name.
Also show a 'totals' column.
We cannot calculate a meaningful total percentile here.
Calculates a table with each location as column, each row as datetime. First row consist of column names. List of lists.
Can be filtered by option restrict_to_month.
Add snippet names
Collect all data and found_dates.
Snippet_values is a dict of (dicts of dicts {'datetime': ., 'value': .., 'unit': ..}, key: 'datetime').
Translate list into dict with dates.
^^^ The value doesn't matter.
Create each row.
Returns layout properties of this snippet_group. Used in
snippet.identifier['layout']""" result = {} if self.layout_y_label:
result['y_label'] = self.layout_y_label if self.layout_x_label: result['x_label'] = self.layout_x_label if self.layout_y_min is not None: result['y_min'] = self.layout_y_min if self.layout_y_max is not None: result['y_max'] = self.layout_y_max if self.layout_title: result['title'] = self.layout_title if self.restrict_to_month: result['restrict_to_month'] = self.restrict_to_month if self.boundary_value is not None: result['horizontal_lines'] = [{ 'name': _('Boundary value'), 'value': self.boundary_value, 'style': {'linewidth': 3, 'linestyle': '--', 'color': 'green'}, }, ]TODO: implement percentile. Start/end date is not known here.
if self.percentile_value is not None:
calculated_percentile = self.statistics(self.percentile_value,,)
result['horizontal_lines'] = [{
'name': _('Percentile value'),
'value': calculated_percentile,
'style': {'linewidth': 2,
'linestyle': '--',
'color': 'green'},
}, ]
return result
class WorkspaceCollageSnippet(models.Model): One snippet in a collage
^^^ Format depends on workspace_item layer_method
Save model and run an extra check.
Check the constraint that workspace_item is in workspace of owner collage.
Call the "real" save() method.
Delete model, also deletes snippet_group if that's empty
return parse_identifier_json(self.identifier_json)
@property def location(self): return self.workspace_item.adapter.location(**self.identifier)
def image(self, start_end_dates, width=None, height=None): Return image from adapter.
return self.workspace_item.adapter.image([self.identifier],
start_end_dates, width=width, height=height)def set_identifier(self, identifier): sets dictionary identifier to property identifier_json
Implements extra function 'find'
Tries to find matching legend. Second choice legend
'default'. If nothing found, return None"""
try:
found_legend = Legend.objects.get(descriptor__icontains=name) except Legend.DoesNotExist: try: found_legend = Legend.objects.get(descriptor='default') except Legend.DoesNotExist: found_legend = None return found_legend
class Legend(models.Model): Simple lineair interpolated legend with min, max, color-min,
descriptor = models.CharField(max_length=80) min_value = models.FloatField(default=0) max_value = models.FloatField(default=100) steps = models.IntegerField(default=10)
default_color = ColorField() min_color = ColorField() max_color = ColorField() too_low_color = ColorField() too_high_color = ColorField()
objects = LegendManager()
def unicode(self):
return self.descriptor@property def float_format(self): Determine float format for defined legend.
Determines legend steps and values. Required by legend_default.
return legend_values(
self.min_value, self.max_value, self.min_color, self.max_color, self.steps)def update(self, updates): Updates model with updates dict. Color values have the
Return a Mapnik line/polystyle from Legend object
def mapnik_rule(color, mapnik_filter=None):
rule = mapnik.Rule() if mapnik_filter is not None:
rule.filter = mapnik.Filter(mapnik_filter) mapnik_color = mapnik.Color(color.r, color.g, color.b)symb_line = mapnik.LineSymbolizer(mapnik_color, 3) rule.symbols.append(symb_line)
symb_poly = mapnik.PolygonSymbolizer(mapnik_color) symb_poly.fill_opacity = 0.5 rule.symbols.append(symb_poly) return rule
mapnik_style = mapnik.Style() if value_field is None: value_field = "value"
rule = mapnik_rule(self.default_color) mapnik_style.rules.append(rule)
mapnik_filter = "[%s] <= %f" % (value_field, self.min_value) logger.debug('adding mapnik_filter: %s' % mapnik_filter) rule = mapnik_rule(self.too_low_color, mapnik_filter) mapnik_style.rules.append(rule)
for legend_value in self.legend_values(): mapnik_filter = "[%s] > %f and [%s] <= %f" % ( value_field, legend_value['low_value'], value_field, legend_value['high_value']) logger.debug('adding mapnik_filter: %s' % mapnik_filter) rule = mapnik_rule(legend_value['color'], mapnik_filter) mapnik_style.rules.append(rule)
mapnik_filter = "[%s] > %f" % (value_field, self.max_value) logger.debug('adding mapnik_filter: %s' % mapnik_filter) rule = mapnik_rule(self.too_high_color, mapnik_filter) mapnik_style.rules.append(rule)
return mapnik_style
def mapnik_linestyle(self, value_field=None): Deprecated. Use mapnik_style instead. Return a Mapnik
Legend for points.
Return a Mapnik style from Legend object.
mapnik_style = mapnik.Style() if value_field is None:
value_field = "value"
mapnik_rule = point_rule( self.icon, self.mask, self.default_color) mapnik_style.rules.append(mapnik_rule)
mapnik_filter = "[%s] <= %f" % (value_field, self.min_value) logger.debug('adding mapnik_filter: %s' % mapnik_filter) mapnik_rule = point_rule( self.icon, self.mask, self.too_low_color, mapnik_filter=mapnik_filter) mapnik_style.rules.append(mapnik_rule)
for legend_value in self.legend_values(): mapnik_filter = "[%s] > %f and [%s] <= %f" % (
value_field, legend_value['low_value'], value_field, legend_value['high_value']) logger.debug('adding mapnik_filter: %s' % mapnik_filter) mapnik_rule = point_rule( self.icon, self.mask, legend_value['color'], mapnik_filter=mapnik_filter) mapnik_style.rules.append(mapnik_rule)
mapnik_filter = "[%s] > %f" % (value_field, self.max_value) logger.debug('adding mapnik_filter: %s' % mapnik_filter) mapnik_rule = point_rule( self.icon, self.mask, self.too_high_color, mapnik_filter=mapnik_filter) mapnik_style.rules.append(mapnik_rule)
return mapnik_style
class BackgroundMap(models.Model):
LAYER_TYPE_GOOGLE = 1 LAYER_TYPE_OSM = 2 LAYER_TYPE_WMS = 3
LAYER_TYPE_CHOICES = (
(LAYER_TYPE_GOOGLE, 'GOOGLE'), (LAYER_TYPE_OSM, 'OSM'), (LAYER_TYPE_WMS, 'WMS'), )GOOGLE_TYPE_DEFAULT = 1 GOOGLE_TYPE_PHYSICAL = 2 GOOGLE_TYPE_HYBRID = 3 GOOGLE_TYPE_SATELLITE = 4
GOOGLE_TYPE_CHOICES = ( (GOOGLE_TYPE_DEFAULT, 'google default'), (GOOGLE_TYPE_PHYSICAL, 'google physical'), (GOOGLE_TYPE_HYBRID, 'google hybrid'), (GOOGLE_TYPE_SATELLITE, 'google satellite'), )
name = models.CharField(max_length=20) index = models.IntegerField(default=100) default = models.BooleanField(default=False) active = models.BooleanField(default=True)
layer_type = models.IntegerField(choices=LAYER_TYPE_CHOICES) google_type = models.IntegerField( choices=GOOGLE_TYPE_CHOICES, null=True, blank=True, help_text='Choose map type in case of GOOGLE maps.') layer_url = models.CharField( max_length=200, null=True, blank=True, help_text='Tile url for use with OSM or WMS', default='http://tile.openstreetmap.nl/tiles/${z}/${x}/${y}.png') layer_names = models.TextField( null=True, blank=True, help_text='Fill in layer names in case of WMS')
class Meta: ordering = ('index', )
def unicode(self): return '%s' % self.name
class Setting(models.Model):
key = models.CharField(max_length=20, unique=True) value = models.CharField(max_length=200)
def unicode(self):
return '%s = %s' % (self.key, self.value)@classmethod def get(cls, key, default=None):
try:
setting = cls.objects.get(key=key) return setting.value except cls.DoesNotExist: logger.warn('Setting "%s" does not exist, taking default ' 'value "%s"' % (key, default)) return default@classmethod def get_dict(cls, key, default=None):