add support for intermediate levels

This commit is contained in:
Laura Klünder 2016-12-04 01:49:49 +01:00
parent 834d6f0064
commit a1ed7534d9
8 changed files with 213 additions and 67 deletions

View file

@ -54,6 +54,7 @@ class MapitemFormMixin(ModelForm):
if 'levels' in self.fields: if 'levels' in self.fields:
# set field_name # set field_name
self.fields['levels'].to_field_name = 'name' self.fields['levels'].to_field_name = 'name'
self.fields['levels'].queryset = self.fields['levels'].queryset.order_by('altitude')
if 'geometry' in self.fields: if 'geometry' in self.fields:
# hide geometry widget # hide geometry widget
@ -91,7 +92,8 @@ class MapitemFormMixin(ModelForm):
def create_editor_form(mapitemtype): def create_editor_form(mapitemtype):
possible_fields = ['name', 'package', 'level', 'levels', 'geometry', 'height', 'elevator', 'button'] possible_fields = ['name', 'package', 'altitude', 'level', 'intermediate', 'levels', 'geometry',
'height', 'elevator', 'button']
existing_fields = [field for field in possible_fields if hasattr(mapitemtype, field)] existing_fields = [field for field in possible_fields if hasattr(mapitemtype, field)]
class EditorForm(MapitemFormMixin, ModelForm): class EditorForm(MapitemFormMixin, ModelForm):

View file

@ -20,6 +20,12 @@
{% if has_levels %} {% if has_levels %}
<td>{% for level in item.levels.all %}{% if not forloop.first %}, {% endif %}<a href="" data-level-link="{{ level.name }}">{{ level.name }}</a>{% endfor %}</td> <td>{% for level in item.levels.all %}{% if not forloop.first %}, {% endif %}<a href="" data-level-link="{{ level.name }}">{{ level.name }}</a>{% endfor %}</td>
{% endif %} {% endif %}
{% if has_altitude %}
<td>{{ item.altitude }} m</td>
{% endif %}
{% if has_intermediate %}
<td>{% if item.intermediate %}intermediate{% endif %}</td>
{% endif %}
<td><a href="{% url 'editor.mapitems.edit' mapitem_type=mapitem_type name=item.name %}">Edit</a></td> <td><a href="{% url 'editor.mapitems.edit' mapitem_type=mapitem_type name=item.name %}">Edit</a></td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -47,7 +47,7 @@ def list_mapitems(request, mapitem_type, level=None):
elif not has_level and level is not None: elif not has_level and level is not None:
return redirect('editor.mapitems', mapitem_type=mapitem_type) return redirect('editor.mapitems', mapitem_type=mapitem_type)
queryset = mapitemtype.objects.all() queryset = mapitemtype.objects.all().order_by('name')
if level is not None: if level is not None:
if hasattr(mapitemtype, 'level'): if hasattr(mapitemtype, 'level'):
queryset = queryset.filter(level__name=level) queryset = queryset.filter(level__name=level)
@ -60,6 +60,8 @@ def list_mapitems(request, mapitem_type, level=None):
'has_level': level is not None, 'has_level': level is not None,
'has_elevator': hasattr(mapitemtype, 'elevator'), 'has_elevator': hasattr(mapitemtype, 'elevator'),
'has_levels': hasattr(mapitemtype, 'levels'), 'has_levels': hasattr(mapitemtype, 'levels'),
'has_altitude': hasattr(mapitemtype, 'altitude'),
'has_intermediate': hasattr(mapitemtype, 'intermediate'),
'level': level, 'level': level,
'items': filter_queryset_by_package_access(request, queryset), 'items': filter_queryset_by_package_access(request, queryset),
}) })

View file

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2016-12-03 21:39
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('mapdata', '0009_levelconnector'),
]
operations = [
migrations.AddField(
model_name='level',
name='intermediate',
field=models.BooleanField(default=False, verbose_name='intermediate level'),
preserve_default=False,
),
migrations.AlterField(
model_name='elevatorlevel',
name='elevator',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='elevatorlevels', to='mapdata.Elevator'),
),
]

View file

@ -95,7 +95,7 @@ class GeometryMapItemWithLevel(GeometryMapItem):
def get_geojson_properties(self): def get_geojson_properties(self):
result = super().get_geojson_properties() result = super().get_geojson_properties()
result['level'] = float(self.level.name) result['level'] = self.level.name
return result return result
def tofile(self): def tofile(self):
@ -105,6 +105,42 @@ class GeometryMapItemWithLevel(GeometryMapItem):
return result return result
class Building(GeometryMapItemWithLevel):
"""
The outline of a building on a specific level
"""
geomtype = 'polygon'
class Meta:
verbose_name = _('Building')
verbose_name_plural = _('Buildings')
default_related_name = 'buildings'
class Room(GeometryMapItemWithLevel):
"""
An accessible area like a room. Can overlap.
"""
geomtype = 'polygon'
class Meta:
verbose_name = _('Room')
verbose_name_plural = _('Rooms')
default_related_name = 'rooms'
class Outside(GeometryMapItemWithLevel):
"""
An accessible outdoor area like a court. Can overlap.
"""
geomtype = 'polygon'
class Meta:
verbose_name = _('Outside Area')
verbose_name_plural = _('Outside Areas')
default_related_name = 'outsides'
class LevelConnector(GeometryMapItem): class LevelConnector(GeometryMapItem):
""" """
A connector connecting levels A connector connecting levels
@ -144,42 +180,6 @@ class LevelConnector(GeometryMapItem):
return result return result
class Building(GeometryMapItemWithLevel):
"""
The outline of a building on a specific level
"""
geomtype = 'polygon'
class Meta:
verbose_name = _('Building')
verbose_name_plural = _('Buildings')
default_related_name = 'buildings'
class Room(GeometryMapItemWithLevel):
"""
An accessible area like a room. Can overlap.
"""
geomtype = 'polygon'
class Meta:
verbose_name = _('Room')
verbose_name_plural = _('Rooms')
default_related_name = 'rooms'
class Outside(GeometryMapItemWithLevel):
"""
An accessible outdoor area like a court. Can overlap.
"""
geomtype = 'polygon'
class Meta:
verbose_name = _('Outside Area')
verbose_name_plural = _('Outside Areas')
default_related_name = 'outsides'
class Obstacle(GeometryMapItemWithLevel): class Obstacle(GeometryMapItemWithLevel):
""" """
An obstacle An obstacle

View file

@ -14,6 +14,7 @@ class Level(MapItem):
name = models.SlugField(_('level name'), unique=True, max_length=50, name = models.SlugField(_('level name'), unique=True, max_length=50,
help_text=_('Usually just an integer (e.g. -1, 0, 1, 2)')) help_text=_('Usually just an integer (e.g. -1, 0, 1, 2)'))
altitude = models.DecimalField(_('level altitude'), null=True, max_digits=6, decimal_places=2) altitude = models.DecimalField(_('level altitude'), null=True, max_digits=6, decimal_places=2)
intermediate = models.BooleanField(_('intermediate level'))
class Meta: class Meta:
verbose_name = _('Level') verbose_name = _('Level')
@ -22,11 +23,20 @@ class Level(MapItem):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.geometries = LevelGeometries(self)
@cached_property
def geometries(self):
return LevelGeometries.by_level(self)
def tofilename(self): def tofilename(self):
return 'levels/%s.json' % self.name return 'levels/%s.json' % self.name
def lower(self):
return Level.objects.filter(altitude__lt=self.altitude).order_by('altitude')
def higher(self):
return Level.objects.filter(altitude__gt=self.altitude).order_by('altitude')
@classmethod @classmethod
def fromfile(cls, data, file_path): def fromfile(cls, data, file_path):
kwargs = super().fromfile(data, file_path) kwargs = super().fromfile(data, file_path)
@ -51,16 +61,29 @@ class Level(MapItem):
class LevelGeometries(): class LevelGeometries():
by_level_name = {}
@classmethod
def by_level(cls, level):
return cls.by_level_name.setdefault(level.name, cls(level))
def __init__(self, level): def __init__(self, level):
self.level = level self.level = level
@cached_property
def raw_rooms(self):
return cascaded_union([room.geometry for room in self.level.rooms.all()])
@cached_property @cached_property
def buildings(self): def buildings(self):
return cascaded_union([building.geometry for building in self.level.buildings.all()]) result = cascaded_union([building.geometry for building in self.level.buildings.all()])
if self.level.intermediate:
result = cascaded_union([result, self.raw_rooms])
return result
@cached_property @cached_property
def rooms(self): def rooms(self):
return cascaded_union([room.geometry for room in self.level.rooms.all()]).intersection(self.buildings) return self.raw_rooms.intersection(self.buildings)
@cached_property @cached_property
def outsides(self): def outsides(self):
@ -72,7 +95,7 @@ class LevelGeometries():
@cached_property @cached_property
def obstacles(self): def obstacles(self):
return cascaded_union([obstacle.geometry for obstacle in self.level.obstacles.all()]) return cascaded_union([obstacle.geometry for obstacle in self.level.obstacles.all()]).intersection(self.mapped)
@cached_property @cached_property
def raw_doors(self): def raw_doors(self):

View file

@ -1,8 +1,19 @@
from c3nav.mapdata.models import Level from c3nav.mapdata.models import Level
from c3nav.mapdata.render.renderer import LevelRenderer # noqa from c3nav.mapdata.render.renderer import LevelRenderer, get_render_path # noqa
def render_all_levels(): def render_all_levels():
for level in Level.objects.all(): renderers = []
renderer = LevelRenderer(level) for level in Level.objects.all().order_by('altitude'):
renderer.render_png() renderers.append(LevelRenderer(level))
for renderer in renderers:
renderer.render_base()
for renderer in renderers:
if not renderer.level.intermediate:
renderer.render_simple()
for renderer in renderers:
if not renderer.level.intermediate:
renderer.render_full()

View file

@ -5,21 +5,31 @@ import xml.etree.ElementTree as ET
from django.conf import settings from django.conf import settings
from django.db.models import Max, Min from django.db.models import Max, Min
from shapely.affinity import scale from shapely.affinity import scale
from shapely.geometry import JOIN_STYLE, box
from c3nav.mapdata.models import Package from c3nav.mapdata.models import Package
def get_render_path(filename):
return os.path.join(settings.RENDER_ROOT, filename)
def get_dimensions():
aggregate = Package.objects.all().aggregate(Max('right'), Min('left'), Max('top'), Min('bottom'))
return (
float(aggregate['right__max'] - aggregate['left__min']),
float(aggregate['top__max'] - aggregate['bottom__min']),
)
class LevelRenderer(): class LevelRenderer():
def __init__(self, level): def __init__(self, level):
self.level = level self.level = level
@staticmethod @staticmethod
def get_dimensions(): def get_dimensions():
aggregate = Package.objects.all().aggregate(Max('right'), Min('left'), Max('top'), Min('bottom')) width, height = get_dimensions()
return ( return (width * settings.RENDER_SCALE, height * settings.RENDER_SCALE)
float(aggregate['right__max'] - aggregate['left__min']) * settings.RENDER_SCALE,
float(aggregate['top__max'] - aggregate['bottom__min']) * settings.RENDER_SCALE
)
@staticmethod @staticmethod
def polygon_svg(geometry, fill_color=None, fill_opacity=None, stroke_width=0.0, stroke_color=None, filter=None): def polygon_svg(geometry, fill_color=None, fill_opacity=None, stroke_width=0.0, stroke_color=None, filter=None):
@ -51,20 +61,45 @@ class LevelRenderer():
return element return element
def get_svg(self): def create_svg(self):
width, height = self.get_dimensions() width, height = self.get_dimensions()
svg = ET.Element('svg', { svg = ET.Element('svg', {
'width': str(width), 'width': str(width),
'height': str(height), 'height': str(height),
'xmlns:svg': 'http://www.w3.org/2000/svg', 'xmlns:svg': 'http://www.w3.org/2000/svg',
'xmlns': 'http://www.w3.org/2000/svg', 'xmlns': 'http://www.w3.org/2000/svg',
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
}) })
return svg
def add_svg_content(self, svg):
width, height = self.get_dimensions()
contents = ET.Element('g', { contents = ET.Element('g', {
'transform': 'scale(1 -1) translate(0 -%d)' % (height), 'transform': 'scale(1 -1) translate(0 -%d)' % (height),
}) })
svg.append(contents) svg.append(contents)
return contents
def add_svg_image(self, svg, image):
width, height = self.get_dimensions()
contents = ET.Element('image', {
'x': '0',
'y': '0',
'width': str(width),
'height': str(height),
'xlink:href': image
})
svg.append(contents)
def render_base(self, png=True):
svg = self.create_svg()
contents = self.add_svg_content(svg)
if not self.level.intermediate:
width, height = get_dimensions()
holes = self.level.geometries.holes.buffer(0.1, join_style=JOIN_STYLE.mitre)
contents.append(self.polygon_svg(box(0, 0, width, height).difference(holes),
fill_color='#000000'))
contents.append(self.polygon_svg(self.level.geometries.buildings_with_holes, contents.append(self.polygon_svg(self.level.geometries.buildings_with_holes,
fill_color='#D5D5D5')) fill_color='#D5D5D5'))
@ -94,18 +129,58 @@ class LevelRenderer():
stroke_color='#3c3c3c', stroke_color='#3c3c3c',
stroke_width=0.05)) stroke_width=0.05))
return ET.tostring(svg).decode() filename = get_render_path('level-%s.base.svg' % self.level.name)
def _get_render_path(self, filename):
return os.path.join(settings.RENDER_ROOT, filename)
def write_svg(self):
filename = self._get_render_path('level-%s.svg' % self.level.name)
with open(filename, 'w') as f: with open(filename, 'w') as f:
f.write(self.get_svg()) f.write(ET.tostring(svg).decode())
return filename
def render_png(self): if png:
svg_filename = self.write_svg() png_filename = get_render_path('level-%s.base.png' % self.level.name)
filename = self._get_render_path('level-%s.png' % self.level.name) subprocess.call(['rsvg-convert', filename, '-o', png_filename])
subprocess.call(['rsvg-convert', svg_filename, '-o', filename])
def render_simple(self, png=True):
svg = self.create_svg()
lower = []
for level in self.level.lower():
lower.append(level)
if not level.intermediate:
lower = []
lower.append(self.level)
width, height = get_dimensions()
contents = self.add_svg_content(svg)
contents.append(self.polygon_svg(box(0, 0, width, height),
fill_color='#000000'))
for level in lower:
self.add_svg_image(svg, 'file://'+get_render_path('level-%s.base.png' % level.name))
filename = get_render_path('level-%s.simple.svg' % self.level.name)
with open(filename, 'w') as f:
f.write(ET.tostring(svg).decode())
if png:
png_filename = get_render_path('level-%s.simple.png' % self.level.name)
subprocess.call(['rsvg-convert', filename, '-o', png_filename])
def render_full(self, png=True):
svg = self.create_svg()
self.add_svg_image(svg, 'file://' + get_render_path('level-%s.simple.png' % self.level.name))
higher = []
for level in self.level.higher():
if not level.intermediate:
break
higher.append(level)
for level in higher:
self.add_svg_image(svg, 'file://'+get_render_path('level-%s.base.png' % level.name))
filename = get_render_path('level-%s.full.svg' % self.level.name)
with open(filename, 'w') as f:
f.write(ET.tostring(svg).decode())
if png:
png_filename = get_render_path('level-%s.full.png' % self.level.name)
subprocess.call(['rsvg-convert', filename, '-o', png_filename])