diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index c3644d12..c3ea2767 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -54,6 +54,7 @@ class MapitemFormMixin(ModelForm): if 'levels' in self.fields: # set field_name self.fields['levels'].to_field_name = 'name' + self.fields['levels'].queryset = self.fields['levels'].queryset.order_by('altitude') if 'geometry' in self.fields: # hide geometry widget @@ -91,7 +92,8 @@ class MapitemFormMixin(ModelForm): 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)] class EditorForm(MapitemFormMixin, ModelForm): diff --git a/src/c3nav/editor/templates/editor/mapitems.html b/src/c3nav/editor/templates/editor/mapitems.html index 5b8486fe..1f4d03bb 100644 --- a/src/c3nav/editor/templates/editor/mapitems.html +++ b/src/c3nav/editor/templates/editor/mapitems.html @@ -20,6 +20,12 @@ {% if has_levels %} {% for level in item.levels.all %}{% if not forloop.first %}, {% endif %}{{ level.name }}{% endfor %} {% endif %} + {% if has_altitude %} + {{ item.altitude }} m + {% endif %} + {% if has_intermediate %} + {% if item.intermediate %}intermediate{% endif %} + {% endif %} Edit {% endfor %} diff --git a/src/c3nav/editor/views.py b/src/c3nav/editor/views.py index 745a4d42..f3fcbe80 100644 --- a/src/c3nav/editor/views.py +++ b/src/c3nav/editor/views.py @@ -47,7 +47,7 @@ def list_mapitems(request, mapitem_type, level=None): elif not has_level and level is not None: 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 hasattr(mapitemtype, '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_elevator': hasattr(mapitemtype, 'elevator'), 'has_levels': hasattr(mapitemtype, 'levels'), + 'has_altitude': hasattr(mapitemtype, 'altitude'), + 'has_intermediate': hasattr(mapitemtype, 'intermediate'), 'level': level, 'items': filter_queryset_by_package_access(request, queryset), }) diff --git a/src/c3nav/mapdata/migrations/0010_auto_20161203_2139.py b/src/c3nav/mapdata/migrations/0010_auto_20161203_2139.py new file mode 100644 index 00000000..f803fd8e --- /dev/null +++ b/src/c3nav/mapdata/migrations/0010_auto_20161203_2139.py @@ -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'), + ), + ] diff --git a/src/c3nav/mapdata/models/geometry.py b/src/c3nav/mapdata/models/geometry.py index 612ed757..b2c6baab 100644 --- a/src/c3nav/mapdata/models/geometry.py +++ b/src/c3nav/mapdata/models/geometry.py @@ -95,7 +95,7 @@ class GeometryMapItemWithLevel(GeometryMapItem): def get_geojson_properties(self): result = super().get_geojson_properties() - result['level'] = float(self.level.name) + result['level'] = self.level.name return result def tofile(self): @@ -105,6 +105,42 @@ class GeometryMapItemWithLevel(GeometryMapItem): 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): """ A connector connecting levels @@ -144,42 +180,6 @@ class LevelConnector(GeometryMapItem): 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): """ An obstacle diff --git a/src/c3nav/mapdata/models/level.py b/src/c3nav/mapdata/models/level.py index 8fd2206c..cc88f154 100644 --- a/src/c3nav/mapdata/models/level.py +++ b/src/c3nav/mapdata/models/level.py @@ -14,6 +14,7 @@ class Level(MapItem): name = models.SlugField(_('level name'), unique=True, max_length=50, 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) + intermediate = models.BooleanField(_('intermediate level')) class Meta: verbose_name = _('Level') @@ -22,11 +23,20 @@ class Level(MapItem): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.geometries = LevelGeometries(self) + + @cached_property + def geometries(self): + return LevelGeometries.by_level(self) def tofilename(self): 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 def fromfile(cls, data, file_path): kwargs = super().fromfile(data, file_path) @@ -51,16 +61,29 @@ class Level(MapItem): 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): self.level = level + @cached_property + def raw_rooms(self): + return cascaded_union([room.geometry for room in self.level.rooms.all()]) + @cached_property 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 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 def outsides(self): @@ -72,7 +95,7 @@ class LevelGeometries(): @cached_property 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 def raw_doors(self): diff --git a/src/c3nav/mapdata/render/__init__.py b/src/c3nav/mapdata/render/__init__.py index 4927c7a6..e0cdc89a 100644 --- a/src/c3nav/mapdata/render/__init__.py +++ b/src/c3nav/mapdata/render/__init__.py @@ -1,8 +1,19 @@ 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(): - for level in Level.objects.all(): - renderer = LevelRenderer(level) - renderer.render_png() + renderers = [] + for level in Level.objects.all().order_by('altitude'): + 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() diff --git a/src/c3nav/mapdata/render/renderer.py b/src/c3nav/mapdata/render/renderer.py index ce7e07c3..6cbb85ad 100644 --- a/src/c3nav/mapdata/render/renderer.py +++ b/src/c3nav/mapdata/render/renderer.py @@ -5,21 +5,31 @@ import xml.etree.ElementTree as ET from django.conf import settings from django.db.models import Max, Min from shapely.affinity import scale +from shapely.geometry import JOIN_STYLE, box 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(): def __init__(self, level): self.level = level @staticmethod def get_dimensions(): - aggregate = Package.objects.all().aggregate(Max('right'), Min('left'), Max('top'), Min('bottom')) - return ( - float(aggregate['right__max'] - aggregate['left__min']) * settings.RENDER_SCALE, - float(aggregate['top__max'] - aggregate['bottom__min']) * settings.RENDER_SCALE - ) + width, height = get_dimensions() + return (width * settings.RENDER_SCALE, height * settings.RENDER_SCALE) @staticmethod 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 - def get_svg(self): + def create_svg(self): width, height = self.get_dimensions() - svg = ET.Element('svg', { 'width': str(width), 'height': str(height), 'xmlns:svg': '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', { 'transform': 'scale(1 -1) translate(0 -%d)' % (height), }) 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, fill_color='#D5D5D5')) @@ -94,18 +129,58 @@ class LevelRenderer(): stroke_color='#3c3c3c', stroke_width=0.05)) - return ET.tostring(svg).decode() - - 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) + filename = get_render_path('level-%s.base.svg' % self.level.name) with open(filename, 'w') as f: - f.write(self.get_svg()) - return filename + f.write(ET.tostring(svg).decode()) - def render_png(self): - svg_filename = self.write_svg() - filename = self._get_render_path('level-%s.png' % self.level.name) - subprocess.call(['rsvg-convert', svg_filename, '-o', filename]) + if png: + png_filename = get_render_path('level-%s.base.png' % self.level.name) + subprocess.call(['rsvg-convert', filename, '-o', png_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])