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:
|
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):
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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),
|
||||||
})
|
})
|
||||||
|
|
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):
|
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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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])
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue