add support for intermediate levels
This commit is contained in:
parent
834d6f0064
commit
a1ed7534d9
8 changed files with 213 additions and 67 deletions
|
@ -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):
|
||||
|
|
|
@ -20,6 +20,12 @@
|
|||
{% 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>
|
||||
{% 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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
|
|
27
src/c3nav/mapdata/migrations/0010_auto_20161203_2139.py
Normal file
27
src/c3nav/mapdata/migrations/0010_auto_20161203_2139.py
Normal 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'),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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])
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue