base for new renderer

This commit is contained in:
Laura Klünder 2017-05-12 23:37:03 +02:00
parent d644ac614e
commit 97ffea6166
6 changed files with 152 additions and 277 deletions

View file

@ -69,6 +69,12 @@ class SectionViewSet(MapdataViewSet):
results.append(door)
return Response([obj.to_geojson() for obj in results])
@detail_route(methods=['get'])
def svg(self, requests, pk=None):
section = self.get_object()
response = HttpResponse(section.render_svg(), 'image/svg+xml')
return response
class BuildingViewSet(MapdataViewSet):
""" Add ?geometry=1 to get geometries, add ?section=<id> to filter by section. """

View file

@ -1,12 +1,13 @@
from django.conf import settings
from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from shapely.geometry import CAP_STYLE, JOIN_STYLE
from shapely.ops import cascaded_union
from c3nav.mapdata.models.base import EditorFormMixin
from c3nav.mapdata.models.locations import SpecificLocation
from c3nav.mapdata.utils.geometry import assert_multilinestring, assert_multipolygon
from c3nav.mapdata.render.svg import SVGImage
from c3nav.mapdata.utils.misc import get_dimensions
class Section(SpecificLocation, EditorFormMixin, models.Model):
@ -45,173 +46,48 @@ class Section(SpecificLocation, EditorFormMixin, models.Model):
result['altitude'] = float(str(self.altitude))
return result
def render_svg(self):
width, height = get_dimensions()
svg = SVGImage(width=width, height=height, scale=settings.RENDER_SCALE)
class SectionGeometries():
by_section_id = {}
building_geometries = cascaded_union(tuple(b.geometry for b in self.buildings.all()))
@classmethod
def by_section(cls, section, only_public=True):
return cls.by_section_id.setdefault((section.id, only_public), cls(section, only_public=only_public))
spaces = self.spaces.all()
space_levels = {
'upper': [],
'lower': [],
'': [],
}
for space in spaces:
space_levels[space.level].append(space)
space_geometries = {
level: cascaded_union(tuple((s.geometry.difference(building_geometries) if s.outside else s.geometry)
for s in level_spaces))
for level, level_spaces in space_levels.items()}
def __init__(self, section, only_public=True):
self.section = section
self.only_public = only_public
hole_geometries = cascaded_union(tuple(h.geometry for h in self.holes.all()))
hole_geometries = hole_geometries.intersection(space_geometries[''])
hole_svg = svg.add_geometry(hole_geometries, 'holes')
hole_mask = svg.add_mask(hole_svg, inverted=True, defid='holes-mask')
def query(self, name):
queryset = getattr(self.section, name)
if not self.only_public:
return queryset.all()
return queryset.filter(public=True)
space_lower_svg = svg.add_geometry(space_geometries['lower'], defid='spaces-lower')
svg.use_geometry(space_lower_svg, fill_color='#d1d1d1')
@cached_property
def raw_rooms(self):
return cascaded_union([room.geometry for room in self.query('rooms')])
space_svg = svg.add_geometry(space_geometries[''], defid='spaces')
space_hole_mask = svg.add_mask(space_svg, hole_svg, inverted=True, defid='spaces_mask')
svg.use_geometry(space_svg, fill_color='#d1d1d1', mask=hole_mask)
@cached_property
def buildings(self):
result = cascaded_union([building.geometry for building in self.query('buildings')])
if self.section.intermediate:
result = cascaded_union([result, self.raw_rooms])
return result
building_svg = svg.add_geometry(building_geometries, 'buildings')
svg.use_geometry(building_svg, fill_color='#929292', mask=space_hole_mask)
@cached_property
def rooms(self):
return self.raw_rooms.intersection(self.buildings)
svg.use_geometry(space_svg, stroke_color='#333333', stroke_width=0.08)
svg.use_geometry(building_svg, stroke_color='#333333', stroke_width=0.10)
@cached_property
def outsides(self):
return cascaded_union([outside.geometry for outside in self.query('outsides')]).difference(self.buildings)
door_geometries = cascaded_union(tuple(d.geometry for d in self.doors.all()))
door_geometries = door_geometries.difference(space_geometries[''])
door_svg = svg.add_geometry(door_geometries, defid='doors')
svg.use_geometry(door_svg, fill_color='#ffffff')
@cached_property
def mapped(self):
return cascaded_union([self.buildings, self.outsides])
@cached_property
def lineobstacles(self):
lineobstacles = []
for obstacle in self.query('lineobstacles'):
lineobstacles.append(obstacle.geometry.buffer(obstacle.width/2,
join_style=JOIN_STYLE.mitre, cap_style=CAP_STYLE.flat))
return cascaded_union(lineobstacles)
@cached_property
def uncropped_obstacles(self):
obstacles = [obstacle.geometry for obstacle in self.query('obstacles').filter(crop_to_level__isnull=True)]
return cascaded_union(obstacles).intersection(self.mapped)
@cached_property
def cropped_obstacles(self):
levels_by_name = {}
obstacles_by_crop_to_level = {}
for obstacle in self.query('obstacles').filter(crop_to_level__isnull=False):
level_name = obstacle.crop_to_level.name
levels_by_name.setdefault(level_name, obstacle.crop_to_level)
obstacles_by_crop_to_level.setdefault(level_name, []).append(obstacle.geometry)
all_obstacles = []
for level_name, obstacles in obstacles_by_crop_to_level.items():
obstacles = cascaded_union(obstacles).intersection(levels_by_name[level_name].geometries.mapped)
all_obstacles.append(obstacles)
all_obstacles.extend(assert_multipolygon(self.lineobstacles))
return cascaded_union(all_obstacles).intersection(self.mapped)
@cached_property
def obstacles(self):
return cascaded_union([self.uncropped_obstacles, self.cropped_obstacles])
@cached_property
def raw_doors(self):
return cascaded_union([door.geometry for door in self.query('doors').all()]).intersection(self.mapped)
@cached_property
def raw_escalators(self):
return cascaded_union([escalator.geometry for escalator in self.query('escalators').all()])
@cached_property
def escalators(self):
return self.raw_escalators.intersection(self.accessible)
@cached_property
def elevatorlevels(self):
return cascaded_union([elevatorlevel.geometry for elevatorlevel in self.query('elevatorlevels').all()])
@cached_property
def areas(self):
return cascaded_union([self.rooms, self.outsides, self.elevatorlevels])
@cached_property
def holes(self):
return cascaded_union([holes.geometry for holes in self.query('holes').all()]).intersection(self.areas)
@cached_property
def accessible(self):
return self.areas.difference(cascaded_union([self.holes, self.obstacles]))
@cached_property
def buildings_with_holes(self):
return self.buildings.difference(self.holes)
@cached_property
def outsides_with_holes(self):
return self.outsides.difference(self.holes)
@cached_property
def areas_and_doors(self):
return cascaded_union([self.areas, self.raw_doors])
@cached_property
def walls(self):
return self.buildings.difference(self.areas_and_doors)
@cached_property
def walls_shadow(self):
return self.walls.buffer(0.2, join_style=JOIN_STYLE.mitre).intersection(self.buildings_with_holes)
@cached_property
def doors(self):
return self.raw_doors.difference(self.areas)
def get_levelconnectors(self, to_level=None):
queryset = self.query('levelconnectors').prefetch_related('levels')
if to_level is not None:
queryset = queryset.filter(levels=to_level)
return cascaded_union([levelconnector.geometry for levelconnector in queryset])
@cached_property
def levelconnectors(self):
return cascaded_union([levelconnector.geometry for levelconnector in self.query('levelconnectors')])
@cached_property
def intermediate_shadows(self):
qs = self.query('levelconnectors').prefetch_related('levels').filter(levels__altitude__lt=self.section.altitude)
connectors = cascaded_union([levelconnector.geometry for levelconnector in qs])
shadows = self.buildings.difference(connectors.buffer(0.4, join_style=JOIN_STYLE.mitre))
shadows = shadows.buffer(0.3)
return shadows
@cached_property
def hole_shadows(self):
holes = self.holes.buffer(0.1, join_style=JOIN_STYLE.mitre)
shadows = holes.difference(self.holes.buffer(-0.3, join_style=JOIN_STYLE.mitre))
qs = self.query('levelconnectors').prefetch_related('levels').filter(levels__altitude__lt=self.section.altitude)
connectors = cascaded_union([levelconnector.geometry for levelconnector in qs])
shadows = shadows.difference(connectors.buffer(1.0, join_style=JOIN_STYLE.mitre))
return shadows
@cached_property
def stairs(self):
return cascaded_union([stair.geometry for stair in self.query('stairs')]).intersection(self.accessible)
@cached_property
def stair_areas(self):
left = []
for stair in assert_multilinestring(self.stairs):
left.append(stair.parallel_offset(0.15, 'right', join_style=JOIN_STYLE.mitre))
return cascaded_union(left).buffer(0.20, join_style=JOIN_STYLE.mitre, cap_style=CAP_STYLE.flat)
@cached_property
def stuffedareas(self):
return cascaded_union([stuffedarea.geometry for stuffedarea in self.query('stuffedareas')])
space_upper_svg = svg.add_geometry(space_geometries['upper'], defid='spaces-upper')
svg.use_geometry(space_upper_svg, fill_color='#d1d1d1')
return svg.get_xml()

View file

@ -1,8 +1,6 @@
from c3nav.mapdata.models.section import Section
from c3nav.mapdata.render.renderer import LevelRenderer # noqa
def render_all_levels(show_accessibles=False):
from c3nav.mapdata.models.section import Section
from c3nav.mapdata.render.renderer import LevelRenderer # noqa
renderers = []
for level in Section.objects.all():

View file

@ -2,14 +2,13 @@ import os
import subprocess
import xml.etree.ElementTree as ET
from django.conf import settings
from shapely.affinity import scale
from shapely.geometry import JOIN_STYLE, box
from c3nav.mapdata.inclusion import get_maybe_invisible_areas
from c3nav.mapdata.utils.misc import get_dimensions, get_public_private_area, get_render_dimensions, get_render_path
class LevelRenderer():
def __init__(self, level, only_public):
self.level = level
@ -23,41 +22,6 @@ class LevelRenderer():
def get_filename(self, mode, filetype, level=None):
return get_render_path(filetype, self.level.name if level is None else level, mode, self.only_public)
@staticmethod
def polygon_svg(geometry, fill_color=None, fill_opacity=None,
stroke_width=0.0, stroke_color=None, stroke_opacity=None):
scaled = scale(geometry, xfact=settings.RENDER_SCALE, yfact=settings.RENDER_SCALE, origin=(0, 0))
element = ET.fromstring(scaled.svg(0, fill_color or '#FFFFFF'))
if element.tag != 'g':
new_element = ET.Element('g')
new_element.append(element)
element = new_element
paths = element.findall('polyline')
if len(paths) == 0:
paths = element.findall('path')
for path in paths:
path.attrib.pop('opacity')
path.set('stroke-width', str(stroke_width * settings.RENDER_SCALE))
if fill_color is None and 'fill' in path.attrib:
path.attrib.pop('fill')
path.set('fill-opacity', '0')
if fill_opacity is not None:
path.set('fill-opacity', str(fill_opacity))
if stroke_color is not None:
path.set('stroke', stroke_color)
elif 'stroke' in path.attrib:
path.attrib.pop('stroke')
if stroke_opacity is not None:
path.set('stroke-opacity', str(stroke_opacity))
return element
def create_svg(self):
width, height = get_render_dimensions()
svg = ET.Element('svg', {

View file

@ -12,12 +12,15 @@ class SVGGroup:
self.height = height
self.scale = scale
self.g = ET.Element('g', {
'transform': 'scale(1 -1) translate(0 -%d)' % (self.height),
'transform': 'scale(1 -1) translate(0 -%d)' % (self.height*scale),
})
def get_xml(self):
def get_element(self):
return self.g
def get_xml(self):
return ET.tostring(self.get_element()).decode()
def add_group(self):
group = SVGGroup(self.width, self.height, self.scale)
self.g.append(group)
@ -27,22 +30,36 @@ class SVGGroup:
class SVGImage(SVGGroup):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.defs = ET.Element('defs')
self.def_i = 0
def get_xml(self):
# blur_filter = ET.Element('filter', {'id': 'wallblur'})
# blur_filter.append(ET.Element('feGaussianBlur', {'in': 'SourceGraphic', 'stdDeviation': str(5*self.scale)}))
# self.defs.append(blur_filter)
def get_element(self):
root = ET.Element('svg', {
'width': str(self.width),
'height': str(self.height),
'width': str(self.width*self.scale),
'height': str(self.height*self.scale),
'xmlns:svg': 'http://www.w3.org/2000/svg',
'xmlns': 'http://www.w3.org/2000/svg',
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
})
root.append(self.defs)
root.append(self.g)
return root
def add_geometry(self, geometry, fill_color=None, fill_opacity=None,
stroke_width=0.0, stroke_color=None, stroke_opacity=None):
def new_defid(self):
defid = 's'+str(self.def_i)
self.def_i += 1
return defid
def add_geometry(self, geometry, defid=None, comment=None):
if defid is None:
defid = self.new_defid()
scaled = scale(geometry, xfact=self.scale, yfact=self.scale, origin=(0, 0))
element = ET.fromstring(scaled.svg(0, fill_color or '#FFFFFF'))
element = ET.fromstring(scaled.svg(0, '#FFFFFF'))
if element.tag != 'g':
new_element = ET.Element('g')
new_element.append(element)
@ -53,74 +70,88 @@ class SVGImage(SVGGroup):
paths = element.findall('path')
for path in paths:
path.attrib.pop('opacity')
path.set('stroke-width', str(stroke_width * self.scale))
path.attrib.pop('opacity', None)
path.attrib.pop('fill', None)
path.attrib.pop('fill-rule', None)
path.attrib.pop('stroke', None)
path.attrib.pop('stroke-width', None)
if fill_color is None and 'fill' in path.attrib:
path.attrib.pop('fill')
path.set('fill-opacity', '0')
element.set('id', defid)
self.defs.append(element)
return defid
if fill_opacity is not None:
path.set('fill-opacity', str(fill_opacity))
def add_mask(self, *geometries, inverted=False, defid=None):
if defid is None:
defid = self.new_defid()
if stroke_color is not None:
path.set('stroke', stroke_color)
elif 'stroke' in path.attrib:
path.attrib.pop('stroke')
mask = ET.Element('mask', {'id': defid})
mask.append(ET.Element('rect', {'width': '100%', 'height': '100%', 'fill': 'white' if inverted else 'black'}))
for geometry in geometries:
mask.append(ET.Element('use', {'xlink:href': '#'+geometry, 'fill': 'black' if inverted else 'white'}))
self.defs.append(mask)
return defid
if stroke_opacity is not None:
path.set('stroke-opacity', str(stroke_opacity))
def add_union(self, *geometries, wall_shadow=False, defid=None):
if defid is None:
defid = self.new_defid()
element = ET.Element('g', {'id': defid})
for geometry in geometries:
newelem = ET.Element('use', {'xlink:href': '#'+geometry})
if wall_shadow:
newelem.set('filter', 'url(#wallshadow)')
element.append(newelem)
self.defs.append(element)
return defid
def add_intersection(self, geometry1, geometry2, defid=None):
if defid is None:
defid = self.new_defid()
mask = ET.Element('mask', {'id': defid+'-mask'})
mask.append(ET.Element('rect', {'width': '100%', 'height': '100%', 'fill': 'black'}))
mask.append(ET.Element('use', {'xlink:href': '#'+geometry2, 'fill': 'white'}))
self.defs.append(mask)
element = ET.Element('g', {'id': defid, 'mask': 'url(#'+defid+'-mask)'})
element.append(ET.Element('use', {'xlink:href': '#'+geometry1}))
self.defs.append(element)
return defid
def add_difference(self, geometry1, geometry2, defid=None):
if defid is None:
defid = self.new_defid()
mask = ET.Element('mask', {'id': defid+'-mask'})
mask.append(ET.Element('rect', {'width': '100%', 'height': '100%', 'fill': 'white'}))
mask.append(ET.Element('use', {'xlink:href': '#'+geometry2, 'fill': 'black'}))
self.defs.append(mask)
element = ET.Element('g', {'id': defid, 'mask': 'url(#'+defid+'-mask)'})
element.append(ET.Element('use', {'xlink:href': '#' + geometry1}))
self.defs.append(element)
return defid
def use_geometry(self, geometry, fill_color=None, fill_opacity=None, opacity=None, mask=None, filter=None,
stroke_width=0.0, stroke_color=None, stroke_opacity=None, stroke_linejoin=None):
element = ET.Element('use', {'xlink:href': '#'+geometry})
element.set('fill', fill_color or 'none')
if fill_opacity:
element.set('fill-opacity', str(fill_opacity))
if stroke_width:
element.set('stroke-width', str(stroke_width * self.scale))
if stroke_color:
element.set('stroke', stroke_color)
if stroke_opacity:
element.set('stroke-opacity', str(stroke_opacity))
if stroke_linejoin:
element.set('stroke-linejoin', str(stroke_linejoin))
if opacity:
element.set('opacity', str(opacity))
if mask:
element.set('mask', 'url(#'+mask+')')
if filter:
element.set('filter', 'url(#'+filter+')')
self.g.append(element)
return element
class MapRenderer(ABC):
def __init__(self):
pass
def create_svg(self):
width, height = get_render_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
@staticmethod
def polygon_svg(geometry, fill_color=None, fill_opacity=None,
stroke_width=0.0, stroke_color=None, stroke_opacity=None):
scaled = scale(geometry, xfact=settings.RENDER_SCALE, yfact=settings.RENDER_SCALE, origin=(0, 0))
element = ET.fromstring(scaled.svg(0, fill_color or '#FFFFFF'))
if element.tag != 'g':
new_element = ET.Element('g')
new_element.append(element)
element = new_element
paths = element.findall('polyline')
if len(paths) == 0:
paths = element.findall('path')
for path in paths:
path.attrib.pop('opacity')
path.set('stroke-width', str(stroke_width * settings.RENDER_SCALE))
if fill_color is None and 'fill' in path.attrib:
path.attrib.pop('fill')
path.set('fill-opacity', '0')
if fill_opacity is not None:
path.set('fill-opacity', str(fill_opacity))
if stroke_color is not None:
path.set('stroke', stroke_color)
elif 'stroke' in path.attrib:
path.attrib.pop('stroke')
if stroke_opacity is not None:
path.set('stroke-opacity', str(stroke_opacity))
return element

View file

@ -50,7 +50,7 @@ else:
debug_fallback = "runserver" in sys.argv
DEBUG = config.getboolean('django', 'debug', fallback=debug_fallback)
DIRECT_EDITING = config.getboolean('c3nav', 'direct_editing', fallback=DEBUG)
RENDER_SCALE = float(config.get('c3nav', 'render_scale', fallback=12.5))
RENDER_SCALE = float(config.get('c3nav', 'render_scale', fallback=20.0))
db_backend = config.get('database', 'backend', fallback='sqlite3')
DATABASES = {