cleanup render code to make room for the opengl rendering engine
This commit is contained in:
parent
f7291ff237
commit
df451f9143
12 changed files with 207 additions and 159 deletions
|
@ -15,7 +15,6 @@ from shapely.ops import cascaded_union
|
||||||
from c3nav.mapdata.models.locations import SpecificLocation
|
from c3nav.mapdata.models.locations import SpecificLocation
|
||||||
from c3nav.mapdata.utils.geometry import assert_multipolygon
|
from c3nav.mapdata.utils.geometry import assert_multipolygon
|
||||||
from c3nav.mapdata.utils.scad import add_indent, polygon_scad
|
from c3nav.mapdata.utils.scad import add_indent, polygon_scad
|
||||||
from c3nav.mapdata.utils.svg import SVGImage
|
|
||||||
|
|
||||||
|
|
||||||
class LevelManager(models.Manager):
|
class LevelManager(models.Manager):
|
||||||
|
@ -125,6 +124,7 @@ class Level(SpecificLocation, models.Model):
|
||||||
svg.add_geometry(obstacle_geometries, fill_color='#999999')
|
svg.add_geometry(obstacle_geometries, fill_color='#999999')
|
||||||
|
|
||||||
def render_svg(self, request, effects=True, draw_spaces=None):
|
def render_svg(self, request, effects=True, draw_spaces=None):
|
||||||
|
from c3nav.mapdata.render.image.engines.svg import SVGImage
|
||||||
from c3nav.mapdata.models import Source, Area, Door, Space
|
from c3nav.mapdata.models import Source, Area, Door, Space
|
||||||
|
|
||||||
bounds = Source.max_bounds()
|
bounds = Source.max_bounds()
|
||||||
|
|
|
@ -69,7 +69,7 @@ class MapUpdate(models.Model):
|
||||||
from c3nav.mapdata.cache import changed_geometries
|
from c3nav.mapdata.cache import changed_geometries
|
||||||
changed_geometries.save(last_map_update, self.to_tuple)
|
changed_geometries.save(last_map_update, self.to_tuple)
|
||||||
|
|
||||||
from c3nav.mapdata.render.base import LevelRenderData
|
from c3nav.mapdata.render.image.data import LevelRenderData
|
||||||
LevelRenderData.rebuild()
|
LevelRenderData.rebuild()
|
||||||
|
|
||||||
cache.set('mapdata:last_update', self.to_tuple, 900)
|
cache.set('mapdata:last_update', self.to_tuple, 900)
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
from c3nav.mapdata.render.svg import SVGRenderer # noqa
|
|
3
src/c3nav/mapdata/render/image/__init__.py
Normal file
3
src/c3nav/mapdata/render/image/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from c3nav.mapdata.render.image.renderer import ImageRenderer # noqa
|
||||||
|
from c3nav.mapdata.render.image.utils import (get_render_level_ids, set_tile_access_cookie, # noqa
|
||||||
|
get_tile_access_cookie) # noqa
|
|
@ -1,64 +1,11 @@
|
||||||
import base64
|
|
||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
import pickle
|
import pickle
|
||||||
import time
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from shapely.ops import unary_union
|
from shapely.ops import unary_union
|
||||||
|
|
||||||
from c3nav.mapdata.cache import MapHistory
|
from c3nav.mapdata.cache import MapHistory
|
||||||
from c3nav.mapdata.models import Level, MapUpdate
|
from c3nav.mapdata.models import Level, MapUpdate
|
||||||
from c3nav.mapdata.models.access import AccessPermission
|
|
||||||
|
|
||||||
|
|
||||||
def get_render_level_ids(cache_key=None):
|
|
||||||
if cache_key is None:
|
|
||||||
cache_key = MapUpdate.current_cache_key()
|
|
||||||
cache_key = 'mapdata:render-level-ids:'+cache_key
|
|
||||||
levels = cache.get(cache_key, None)
|
|
||||||
if levels is None:
|
|
||||||
levels = set(Level.objects.values_list('pk', flat=True))
|
|
||||||
cache.set(cache_key, levels, 300)
|
|
||||||
return levels
|
|
||||||
|
|
||||||
|
|
||||||
def set_tile_access_cookie(request, response):
|
|
||||||
access_permissions = AccessPermission.get_for_request(request)
|
|
||||||
|
|
||||||
if access_permissions:
|
|
||||||
value = '-'.join(str(i) for i in access_permissions)+':'+str(int(time.time())+60)
|
|
||||||
key = hashlib.sha1(settings.SECRET_TILE_KEY.encode()).digest()
|
|
||||||
signed = base64.b64encode(hmac.new(key, msg=value.encode(), digestmod=hashlib.sha256).digest()).decode()
|
|
||||||
response.set_cookie(settings.TILE_ACCESS_COOKIE_NAME, value+':'+signed, max_age=60)
|
|
||||||
else:
|
|
||||||
response.delete_cookie(settings.TILE_ACCESS_COOKIE_NAME)
|
|
||||||
|
|
||||||
|
|
||||||
def get_tile_access_cookie(request):
|
|
||||||
try:
|
|
||||||
cookie = request.COOKIES[settings.TILE_ACCESS_COOKIE_NAME]
|
|
||||||
except KeyError:
|
|
||||||
return set()
|
|
||||||
|
|
||||||
try:
|
|
||||||
access_permissions, expire, signed = cookie.split(':')
|
|
||||||
except ValueError:
|
|
||||||
return set()
|
|
||||||
|
|
||||||
value = access_permissions+':'+expire
|
|
||||||
|
|
||||||
key = hashlib.sha1(settings.SECRET_TILE_KEY.encode()).digest()
|
|
||||||
signed_verify = base64.b64encode(hmac.new(key, msg=value.encode(), digestmod=hashlib.sha256).digest()).decode()
|
|
||||||
if signed != signed_verify:
|
|
||||||
return set()
|
|
||||||
|
|
||||||
if int(expire) < time.time():
|
|
||||||
return set()
|
|
||||||
|
|
||||||
return set(int(i) for i in access_permissions.split('-'))
|
|
||||||
|
|
||||||
|
|
||||||
class AltitudeAreaGeometries:
|
class AltitudeAreaGeometries:
|
0
src/c3nav/mapdata/render/image/engines/__init__.py
Normal file
0
src/c3nav/mapdata/render/image/engines/__init__.py
Normal file
74
src/c3nav/mapdata/render/image/engines/base.py
Normal file
74
src/c3nav/mapdata/render/image/engines/base.py
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import math
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class FillAttribs:
|
||||||
|
__slots__ = ('color', 'opacity')
|
||||||
|
|
||||||
|
def __init__(self, color, opacity=None):
|
||||||
|
self.color = color
|
||||||
|
self.opacity = opacity
|
||||||
|
|
||||||
|
|
||||||
|
class StrokeAttribs:
|
||||||
|
__slots__ = ('color', 'width', 'min_px', 'opacity')
|
||||||
|
|
||||||
|
def __init__(self, color, width, min_px=None, opacity=None):
|
||||||
|
self.color = color
|
||||||
|
self.width = width
|
||||||
|
self.min_px = min_px
|
||||||
|
self.opacity = opacity
|
||||||
|
|
||||||
|
|
||||||
|
class RenderEngine(ABC):
|
||||||
|
# draw an svg image. supports pseudo-3D shadow-rendering
|
||||||
|
def __init__(self, width: int, height: int, xoff=0, yoff=0, scale=1, buffer=0, background='#FFFFFF'):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.minx = xoff
|
||||||
|
self.miny = yoff
|
||||||
|
self.scale = scale
|
||||||
|
self.buffer = int(math.ceil(buffer*self.scale))
|
||||||
|
self.background = background
|
||||||
|
|
||||||
|
self.maxx = self.minx + width / scale
|
||||||
|
self.maxy = self.miny + height / scale
|
||||||
|
|
||||||
|
# how many pixels around the image should be added and later cropped (otherwise rsvg does not blur correctly)
|
||||||
|
self.buffer = int(math.ceil(buffer*self.scale))
|
||||||
|
self.buffered_width = self.width + 2 * self.buffer
|
||||||
|
self.buffered_height = self.height + 2 * self.buffer
|
||||||
|
|
||||||
|
self.background_rgb = tuple(int(background[i:i + 2], 16) for i in range(1, 6, 2))
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_png(self) -> bytes:
|
||||||
|
# render the image to png.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
|
||||||
|
filter=None, clip_path=None, altitude=None, elevation=None, shape_cache_key=None):
|
||||||
|
# draw a shapely geometry with a given style
|
||||||
|
# if altitude is set, the geometry will get a calculated shadow relative to the other geometries
|
||||||
|
# if elevation is set, the geometry will get a shadow with exactly this elevation
|
||||||
|
|
||||||
|
# if fill_color is set, filter out geometries that cannot be filled
|
||||||
|
if fill is not None:
|
||||||
|
try:
|
||||||
|
geometry.geoms
|
||||||
|
except AttributeError:
|
||||||
|
if not hasattr(geometry, 'exterior'):
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
geometry = type(geometry)(tuple(geom for geom in geometry.geoms if hasattr(geom, 'exterior')))
|
||||||
|
if geometry.is_empty:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._add_geometry(geometry=geometry, fill=fill, stroke=stroke, filter=filter, clip_path=clip_path,
|
||||||
|
altitude=altitude, elevation=elevation, shape_cache_key=shape_cache_key)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
|
||||||
|
filter=None, clip_path=None, altitude=None, elevation=None, shape_cache_key=None):
|
||||||
|
pass
|
|
@ -1,9 +1,9 @@
|
||||||
import io
|
import io
|
||||||
import math
|
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import zlib
|
import zlib
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -14,6 +14,8 @@ from shapely.geometry import LineString, Polygon
|
||||||
from shapely.ops import unary_union
|
from shapely.ops import unary_union
|
||||||
|
|
||||||
# import gobject-inspect, cairo and rsvg if the native rsvg SVG_RENDERER should be used
|
# import gobject-inspect, cairo and rsvg if the native rsvg SVG_RENDERER should be used
|
||||||
|
from c3nav.mapdata.render.image.engines.base import FillAttribs, RenderEngine, StrokeAttribs
|
||||||
|
|
||||||
if settings.SVG_RENDERER == 'rsvg':
|
if settings.SVG_RENDERER == 'rsvg':
|
||||||
import pgi
|
import pgi
|
||||||
import cairocffi
|
import cairocffi
|
||||||
|
@ -35,18 +37,10 @@ def check_svg_renderer(app_configs, **kwargs):
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
class SVGImage:
|
class SVGEngine(RenderEngine):
|
||||||
# draw an svg image. supports pseudo-3D shadow-rendering
|
# draw an svg image. supports pseudo-3D shadow-rendering
|
||||||
def __init__(self, bounds, scale: float=1, buffer=0, background_color='#FFFFFF'):
|
def __init__(self, *args, **kwargs):
|
||||||
# get image dimensions.
|
super().__init__(*args, **kwargs)
|
||||||
# note that these values describe the „viewport“ of the image, not its dimensions in pixels.
|
|
||||||
(self.left, self.bottom), (self.right, self.top) = bounds
|
|
||||||
self.width = self.right-self.left
|
|
||||||
self.height = self.top-self.bottom
|
|
||||||
self.scale = scale
|
|
||||||
|
|
||||||
# how many pixels around the image should be added and later cropped (otherwise rsvg does not blur correctly)
|
|
||||||
self.buffer_px = int(math.ceil(buffer*self.scale))
|
|
||||||
|
|
||||||
# create base elements and counter for clip path ids
|
# create base elements and counter for clip path ids
|
||||||
self.g = ''
|
self.g = ''
|
||||||
|
@ -59,28 +53,24 @@ class SVGImage:
|
||||||
|
|
||||||
# for fast numpy operations
|
# for fast numpy operations
|
||||||
self.np_scale = np.array((self.scale, -self.scale))
|
self.np_scale = np.array((self.scale, -self.scale))
|
||||||
self.np_offset = np.array((-self.left*self.scale, self.top*self.scale))
|
self.np_offset = np.array((-self.minx * self.scale, self.maxy * self.scale))
|
||||||
|
|
||||||
# keep track of created blur filters to avoid duplicates
|
# keep track of created blur filters to avoid duplicates
|
||||||
self.blurs = set()
|
self.blurs = set()
|
||||||
|
|
||||||
self.background_color = background_color
|
|
||||||
self.background_color_rgb = tuple(int(background_color[i:i + 2], 16) for i in range(1, 6, 2))
|
|
||||||
|
|
||||||
self._create_geometry_cache = {}
|
self._create_geometry_cache = {}
|
||||||
|
|
||||||
def get_dimensions_px(self, buffer):
|
|
||||||
# get dimensions of the image in pixels, with or without buffer
|
|
||||||
width_px = self.width * self.scale + (self.buffer_px * 2 if buffer else 0)
|
|
||||||
height_px = self.height * self.scale + (self.buffer_px * 2 if buffer else 0)
|
|
||||||
return height_px, width_px
|
|
||||||
|
|
||||||
def get_xml(self, buffer=False):
|
def get_xml(self, buffer=False):
|
||||||
# get the root <svg> element as an ElementTree element, with or without buffer
|
# get the root <svg> element as an ElementTree element, with or without buffer
|
||||||
height_px, width_px = (self._trim_decimals(str(i)) for i in self.get_dimensions_px(buffer))
|
if buffer:
|
||||||
offset_px = self._trim_decimals(str(-self.buffer_px)) if buffer else '0'
|
width_px = self._trim_decimals(str(self.buffered_width))
|
||||||
|
height_px = self._trim_decimals(str(self.buffered_height))
|
||||||
attribs = ' viewBox="'+' '.join((offset_px, offset_px, width_px, height_px))+'"' if buffer else ''
|
offset_px = self._trim_decimals(str(-self.buffer))
|
||||||
|
attribs = ' viewBox="' + ' '.join((offset_px, offset_px, width_px, height_px)) + '"' if buffer else ''
|
||||||
|
else:
|
||||||
|
width_px = self._trim_decimals(str(self.width))
|
||||||
|
height_px = self._trim_decimals(str(self.height))
|
||||||
|
attribs = ''
|
||||||
|
|
||||||
result = ('<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" '
|
result = ('<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" '
|
||||||
'width="'+width_px+'" height="'+height_px+'"'+attribs+'>')
|
'width="'+width_px+'" height="'+height_px+'"'+attribs+'>')
|
||||||
|
@ -91,12 +81,12 @@ class SVGImage:
|
||||||
result += '</svg>'
|
result += '</svg>'
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_png(self, f=None):
|
def get_png(self):
|
||||||
# render the image to png. returns bytes if f is None, otherwise it calls f.write()
|
# render the image to png. returns bytes if f is None, otherwise it calls f.write()
|
||||||
|
|
||||||
if self.get_dimensions_px(buffer=False) == (256, 256) and not self.g:
|
if self.width == 256 and self.height == 256 and not self.g:
|
||||||
# create empty tile png with minimal size, indexed color palette with only one entry
|
# create empty tile png with minimal size, indexed color palette with only one entry
|
||||||
plte = b'PLTE' + bytearray(self.background_color_rgb)
|
plte = b'PLTE' + bytearray(self.background_rgb)
|
||||||
return (b'\x89PNG\r\n\x1a\n' +
|
return (b'\x89PNG\r\n\x1a\n' +
|
||||||
b'\x00\x00\x00\rIHDR\x00\x00\x01\x00\x00\x00\x01\x00\x01\x03\x00\x00\x00f\xbc:%\x00\x00\x00\x03' +
|
b'\x00\x00\x00\rIHDR\x00\x00\x01\x00\x00\x00\x01\x00\x01\x03\x00\x00\x00f\xbc:%\x00\x00\x00\x03' +
|
||||||
plte + zlib.crc32(plte).to_bytes(4, byteorder='big') +
|
plte + zlib.crc32(plte).to_bytes(4, byteorder='big') +
|
||||||
|
@ -105,7 +95,7 @@ class SVGImage:
|
||||||
|
|
||||||
if settings.SVG_RENDERER == 'rsvg':
|
if settings.SVG_RENDERER == 'rsvg':
|
||||||
# create buffered surfaces
|
# create buffered surfaces
|
||||||
buffered_surface = cairocffi.SVGSurface(None, *(int(i) for i in self.get_dimensions_px(buffer=True)))
|
buffered_surface = cairocffi.SVGSurface(None, self.buffered_width, self.buffered_height)
|
||||||
buffered_context = cairocffi.Context(buffered_surface)
|
buffered_context = cairocffi.Context(buffered_surface)
|
||||||
|
|
||||||
# draw svg with rsvg
|
# draw svg with rsvg
|
||||||
|
@ -114,44 +104,39 @@ class SVGImage:
|
||||||
svg.render_cairo(buffered_context)
|
svg.render_cairo(buffered_context)
|
||||||
|
|
||||||
# create cropped image
|
# create cropped image
|
||||||
surface = buffered_surface.create_similar(cairocffi.CONTENT_COLOR,
|
surface = buffered_surface.create_similar(cairocffi.CONTENT_COLOR, self.width, self.height)
|
||||||
*(int(i) for i in self.get_dimensions_px(buffer=False)))
|
|
||||||
context = cairocffi.Context(surface)
|
context = cairocffi.Context(surface)
|
||||||
|
|
||||||
# set background color
|
# set background color
|
||||||
context.set_source(cairocffi.SolidPattern(*(i/255 for i in self.background_color_rgb)))
|
context.set_source(cairocffi.SolidPattern(*(i/255 for i in self.background_rgb)))
|
||||||
context.paint()
|
context.paint()
|
||||||
|
|
||||||
# paste buffered immage with offset
|
# paste buffered immage with offset
|
||||||
context.set_source_surface(buffered_surface, -self.buffer_px, -self.buffer_px)
|
context.set_source_surface(buffered_surface, -self.buffer, -self.buffer)
|
||||||
context.paint()
|
context.paint()
|
||||||
if f is None:
|
|
||||||
return surface.write_to_png()
|
return surface.write_to_png()
|
||||||
f.write(surface.write_to_png())
|
|
||||||
|
|
||||||
elif settings.SVG_RENDERER == 'rsvg-convert':
|
elif settings.SVG_RENDERER == 'rsvg-convert':
|
||||||
p = subprocess.run(('rsvg-convert', '-b', self.background_color, '--format', 'png'),
|
p = subprocess.run(('rsvg-convert', '-b', self.background, '--format', 'png'),
|
||||||
input=self.get_xml(buffer=True).encode(), stdout=subprocess.PIPE, check=True)
|
input=self.get_xml(buffer=True).encode(), stdout=subprocess.PIPE, check=True)
|
||||||
png = io.BytesIO(p.stdout)
|
png = io.BytesIO(p.stdout)
|
||||||
img = Image.open(png)
|
img = Image.open(png)
|
||||||
img = img.crop((self.buffer_px, self.buffer_px,
|
img = img.crop((self.buffer, self.buffer,
|
||||||
self.buffer_px + int(self.width * self.scale),
|
self.buffer + self.width,
|
||||||
self.buffer_px + int(self.height * self.scale)))
|
self.buffer + self.height))
|
||||||
if f is None:
|
|
||||||
f = io.BytesIO()
|
f = io.BytesIO()
|
||||||
img.save(f, 'PNG')
|
|
||||||
f.seek(0)
|
|
||||||
return f.read()
|
|
||||||
img.save(f, 'PNG')
|
img.save(f, 'PNG')
|
||||||
|
f.seek(0)
|
||||||
|
return f.read()
|
||||||
|
|
||||||
elif settings.SVG_RENDERER == 'inkscape':
|
elif settings.SVG_RENDERER == 'inkscape':
|
||||||
p = subprocess.run(('inkscape', '-z', '-b', self.background_color, '-e', '/dev/stderr', '/dev/stdin'),
|
p = subprocess.run(('inkscape', '-z', '-b', self.background, '-e', '/dev/stderr', '/dev/stdin'),
|
||||||
input=self.get_xml().encode(), stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
input=self.get_xml().encode(), stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||||
check=True)
|
check=True)
|
||||||
png = p.stderr[p.stderr.index(b'\x89PNG'):]
|
png = p.stderr[p.stderr.index(b'\x89PNG'):]
|
||||||
if f is None:
|
return png
|
||||||
return png
|
|
||||||
f.write(png)
|
|
||||||
|
|
||||||
def _trim_decimals(self, data):
|
def _trim_decimals(self, data):
|
||||||
# remove trailing zeros from a decimal – yes this is slow, but it greatly speeds up cairo rendering
|
# remove trailing zeros from a decimal – yes this is slow, but it greatly speeds up cairo rendering
|
||||||
|
@ -233,51 +218,30 @@ class SVGImage:
|
||||||
else:
|
else:
|
||||||
self.altitudes[new_altitude] = new_geometry
|
self.altitudes[new_altitude] = new_geometry
|
||||||
|
|
||||||
def add_geometry(self, geometry=None, fill_color=None, fill_opacity=None, opacity=None, filter=None,
|
def _add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
|
||||||
stroke_px=0.0, stroke_width=0.0, stroke_color=None, stroke_opacity=None, stroke_linejoin=None,
|
filter=None, clip_path=None, altitude=None, elevation=None, shape_cache_key=None):
|
||||||
clip_path=None, altitude=None, elevation=None, shape_cache_key=None):
|
|
||||||
# draw a shapely geometry with a given style
|
|
||||||
# if altitude is set, the geometry will get a calculated shadow relative to the other geometries
|
|
||||||
# if elevation is set, the geometry will get a shadow with exactly this elevation
|
|
||||||
|
|
||||||
# if fill_color is set, filter out geometries that cannot be filled
|
if fill:
|
||||||
if fill_color is not None:
|
attribs = ' fill="'+(fill.color)+'"'
|
||||||
try:
|
if fill.opacity:
|
||||||
geometry.geoms
|
attribs += ' fill-opacity="'+str(fill.opacity)[:4]+'"'
|
||||||
except AttributeError:
|
else:
|
||||||
if not hasattr(geometry, 'exterior'):
|
attribs = ' fill="none"'
|
||||||
return
|
|
||||||
else:
|
if stroke:
|
||||||
geometry = type(geometry)(tuple(geom for geom in geometry.geoms if hasattr(geom, 'exterior')))
|
width = stroke.width*self.scale
|
||||||
if geometry.is_empty:
|
if stroke.min_px:
|
||||||
pass
|
width = max(width, stroke.min_px)
|
||||||
|
attribs += ' stroke-width="' + self._trim_decimals(str(width)) + '" stroke="' + stroke.color + '"'
|
||||||
|
if stroke.opacity:
|
||||||
|
attribs += ' stroke-opacity="'+str(stroke.opacity)[:4]+'"'
|
||||||
|
|
||||||
attribs = ' fill="'+(fill_color or 'none')+'"'
|
|
||||||
if fill_opacity:
|
|
||||||
attribs += ' fill-opacity="'+str(fill_opacity)[:4]+'"'
|
|
||||||
if stroke_width:
|
|
||||||
width = stroke_width*self.scale
|
|
||||||
if stroke_px:
|
|
||||||
width = max(width, stroke_px)
|
|
||||||
attribs += ' stroke-width="' + self._trim_decimals(str(width)) + '"'
|
|
||||||
elif stroke_px:
|
|
||||||
attribs += ' stroke-width="'+self._trim_decimals(str(stroke_px))+'"'
|
|
||||||
if stroke_color:
|
|
||||||
attribs += ' stroke="'+stroke_color+'"'
|
|
||||||
if stroke_opacity:
|
|
||||||
attribs += ' stroke-opacity="'+str(stroke_opacity)[:4]+'"'
|
|
||||||
if stroke_linejoin:
|
|
||||||
attribs += ' stroke-linejoin="'+stroke_linejoin+'"'
|
|
||||||
if opacity:
|
|
||||||
attribs += ' opacity="'+str(opacity)[:4]+'"'
|
|
||||||
if filter:
|
if filter:
|
||||||
attribs += ' filter="url(#'+filter+')"'
|
attribs += ' filter="url(#'+filter+')"'
|
||||||
if clip_path:
|
if clip_path:
|
||||||
attribs += ' clip-path="url(#'+clip_path+')"'
|
attribs += ' clip-path="url(#'+clip_path+')"'
|
||||||
|
|
||||||
if geometry is not None:
|
if geometry is not None:
|
||||||
if not geometry:
|
|
||||||
return
|
|
||||||
|
|
||||||
if False:
|
if False:
|
||||||
# old shadow rendering. currently needs too much resources
|
# old shadow rendering. currently needs too much resources
|
|
@ -6,11 +6,12 @@ from shapely.ops import unary_union
|
||||||
|
|
||||||
from c3nav.mapdata.cache import MapHistory
|
from c3nav.mapdata.cache import MapHistory
|
||||||
from c3nav.mapdata.models import MapUpdate
|
from c3nav.mapdata.models import MapUpdate
|
||||||
from c3nav.mapdata.render.base import get_level_render_data
|
from c3nav.mapdata.render.image.data import get_level_render_data
|
||||||
from c3nav.mapdata.utils.svg import SVGImage
|
from c3nav.mapdata.render.image.engines.base import FillAttribs, StrokeAttribs
|
||||||
|
from c3nav.mapdata.render.image.engines.svg import SVGEngine
|
||||||
|
|
||||||
|
|
||||||
class SVGRenderer:
|
class ImageRenderer:
|
||||||
def __init__(self, level, minx, miny, maxx, maxy, scale=1, access_permissions=None):
|
def __init__(self, level, minx, miny, maxx, maxy, scale=1, access_permissions=None):
|
||||||
self.level = level
|
self.level = level
|
||||||
self.minx = minx
|
self.minx = minx
|
||||||
|
@ -20,6 +21,9 @@ class SVGRenderer:
|
||||||
self.scale = scale
|
self.scale = scale
|
||||||
self.access_permissions = access_permissions
|
self.access_permissions = access_permissions
|
||||||
|
|
||||||
|
self.width = int(round((maxx - minx) * scale))
|
||||||
|
self.height = int(round((maxy - miny) * scale))
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def bbox(self):
|
def bbox(self):
|
||||||
return box(self.minx-1, self.miny-1, self.maxx+1, self.maxy+1)
|
return box(self.minx-1, self.miny-1, self.maxx+1, self.maxy+1)
|
||||||
|
@ -61,8 +65,8 @@ class SVGRenderer:
|
||||||
return self.update_cache_key + ':' + self.access_cache_key
|
return self.update_cache_key + ':' + self.access_cache_key
|
||||||
|
|
||||||
def render(self):
|
def render(self):
|
||||||
svg = SVGImage(bounds=((self.minx, self.miny), (self.maxx, self.maxy)),
|
svg = SVGEngine(self.width, self.height, self.minx, self.miny,
|
||||||
scale=self.scale, buffer=1, background_color='#DCDCDC')
|
scale=self.scale, buffer=1, background='#DCDCDC')
|
||||||
|
|
||||||
# add no access restriction to “unlocked“ access restrictions so lookup gets easier
|
# add no access restriction to “unlocked“ access restrictions so lookup gets easier
|
||||||
unlocked_access_restrictions = self.unlocked_access_restrictions | set([None])
|
unlocked_access_restrictions = self.unlocked_access_restrictions | set([None])
|
||||||
|
@ -86,15 +90,15 @@ class SVGRenderer:
|
||||||
# shadows are directly calculated and added by the SVGImage class
|
# shadows are directly calculated and added by the SVGImage class
|
||||||
for altitudearea in geoms.altitudeareas:
|
for altitudearea in geoms.altitudeareas:
|
||||||
svg.add_geometry(bbox.intersection(altitudearea.geometry.difference(crop_areas)),
|
svg.add_geometry(bbox.intersection(altitudearea.geometry.difference(crop_areas)),
|
||||||
fill_color='#eeeeee', altitude=altitudearea.altitude,
|
altitude=altitudearea.altitude, fill=FillAttribs('#eeeeee'),
|
||||||
stroke_width=0.05, stroke_px=0.2, stroke_color='rgba(0, 0, 0, 0.15)')
|
stroke=StrokeAttribs('rgba(0, 0, 0, 0.15)', 0.05, min_px=0.2))
|
||||||
|
|
||||||
for color, areas in altitudearea.colors.items():
|
for color, areas in altitudearea.colors.items():
|
||||||
# only select ground colors if their access restriction is unlocked
|
# only select ground colors if their access restriction is unlocked
|
||||||
areas = tuple(area for access_restriction, area in areas.items()
|
areas = tuple(area for access_restriction, area in areas.items()
|
||||||
if access_restriction in unlocked_access_restrictions)
|
if access_restriction in unlocked_access_restrictions)
|
||||||
if areas:
|
if areas:
|
||||||
svg.add_geometry(bbox.intersection(unary_union(areas)), fill_color=color)
|
svg.add_geometry(bbox.intersection(unary_union(areas)), fill=FillAttribs(color))
|
||||||
|
|
||||||
# add walls, stroke_px makes sure that all walls are at least 1px thick on all zoom levels,
|
# add walls, stroke_px makes sure that all walls are at least 1px thick on all zoom levels,
|
||||||
walls = None
|
walls = None
|
||||||
|
@ -102,13 +106,13 @@ class SVGRenderer:
|
||||||
walls = bbox.intersection(geoms.walls.union(add_walls))
|
walls = bbox.intersection(geoms.walls.union(add_walls))
|
||||||
|
|
||||||
if walls is not None:
|
if walls is not None:
|
||||||
svg.add_geometry(walls, elevation=default_height, fill_color='#aaaaaa')
|
svg.add_geometry(walls, elevation=default_height, fill=FillAttribs('#aaaaaa'))
|
||||||
|
|
||||||
if not geoms.doors.is_empty:
|
if not geoms.doors.is_empty:
|
||||||
svg.add_geometry(bbox.intersection(geoms.doors.difference(add_walls)),
|
svg.add_geometry(bbox.intersection(geoms.doors.difference(add_walls)), fill=FillAttribs('#ffffff'),
|
||||||
fill_color='#ffffff', stroke_width=0.05, stroke_px=0.2, stroke_color='#ffffff')
|
stroke=StrokeAttribs('#ffffff', 0.05, min_px=0.2))
|
||||||
|
|
||||||
if walls is not None:
|
if walls is not None:
|
||||||
svg.add_geometry(walls, stroke_width=0.05, stroke_px=0.2, stroke_color='#666666')
|
svg.add_geometry(walls, stroke=StrokeAttribs('#666666', 0.05, min_px=0.2))
|
||||||
|
|
||||||
return svg
|
return svg
|
57
src/c3nav/mapdata/render/image/utils.py
Normal file
57
src/c3nav/mapdata/render/image/utils.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import time
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
from c3nav.mapdata.models import Level, MapUpdate
|
||||||
|
from c3nav.mapdata.models.access import AccessPermission
|
||||||
|
|
||||||
|
|
||||||
|
def get_render_level_ids(cache_key=None):
|
||||||
|
if cache_key is None:
|
||||||
|
cache_key = MapUpdate.current_cache_key()
|
||||||
|
cache_key = 'mapdata:render-level-ids:'+cache_key
|
||||||
|
levels = cache.get(cache_key, None)
|
||||||
|
if levels is None:
|
||||||
|
levels = set(Level.objects.values_list('pk', flat=True))
|
||||||
|
cache.set(cache_key, levels, 300)
|
||||||
|
return levels
|
||||||
|
|
||||||
|
|
||||||
|
def set_tile_access_cookie(request, response):
|
||||||
|
access_permissions = AccessPermission.get_for_request(request)
|
||||||
|
|
||||||
|
if access_permissions:
|
||||||
|
value = '-'.join(str(i) for i in access_permissions)+':'+str(int(time.time())+60)
|
||||||
|
key = hashlib.sha1(settings.SECRET_TILE_KEY.encode()).digest()
|
||||||
|
signed = base64.b64encode(hmac.new(key, msg=value.encode(), digestmod=hashlib.sha256).digest()).decode()
|
||||||
|
response.set_cookie(settings.TILE_ACCESS_COOKIE_NAME, value+':'+signed, max_age=60)
|
||||||
|
else:
|
||||||
|
response.delete_cookie(settings.TILE_ACCESS_COOKIE_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def get_tile_access_cookie(request):
|
||||||
|
try:
|
||||||
|
cookie = request.COOKIES[settings.TILE_ACCESS_COOKIE_NAME]
|
||||||
|
except KeyError:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
try:
|
||||||
|
access_permissions, expire, signed = cookie.split(':')
|
||||||
|
except ValueError:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
value = access_permissions+':'+expire
|
||||||
|
|
||||||
|
key = hashlib.sha1(settings.SECRET_TILE_KEY.encode()).digest()
|
||||||
|
signed_verify = base64.b64encode(hmac.new(key, msg=value.encode(), digestmod=hashlib.sha256).digest()).decode()
|
||||||
|
if signed != signed_verify:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
if int(expire) < time.time():
|
||||||
|
return set()
|
||||||
|
|
||||||
|
return set(int(i) for i in access_permissions.split('-'))
|
|
@ -14,8 +14,8 @@ from shapely.geometry import box
|
||||||
from c3nav.mapdata.cache import MapHistory
|
from c3nav.mapdata.cache import MapHistory
|
||||||
from c3nav.mapdata.middleware import no_language
|
from c3nav.mapdata.middleware import no_language
|
||||||
from c3nav.mapdata.models import Level, MapUpdate, Source
|
from c3nav.mapdata.models import Level, MapUpdate, Source
|
||||||
from c3nav.mapdata.render.base import get_render_level_ids, get_tile_access_cookie, set_tile_access_cookie
|
from c3nav.mapdata.render.image import (ImageRenderer, get_render_level_ids, get_tile_access_cookie,
|
||||||
from c3nav.mapdata.render.svg import SVGRenderer
|
set_tile_access_cookie)
|
||||||
|
|
||||||
|
|
||||||
@no_language()
|
@no_language()
|
||||||
|
@ -51,7 +51,7 @@ def tile(request, level, zoom, x, y, format):
|
||||||
access_permissions = get_tile_access_cookie(request)
|
access_permissions = get_tile_access_cookie(request)
|
||||||
|
|
||||||
# init renderer
|
# init renderer
|
||||||
renderer = SVGRenderer(level, minx, miny, maxx, maxy, scale=2**zoom, access_permissions=access_permissions)
|
renderer = ImageRenderer(level, minx, miny, maxx, maxy, scale=2**zoom, access_permissions=access_permissions)
|
||||||
tile_cache_key = renderer.cache_key
|
tile_cache_key = renderer.cache_key
|
||||||
update_cache_key = renderer.update_cache_key
|
update_cache_key = renderer.update_cache_key
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ from c3nav.mapdata.models import Location, Source
|
||||||
from c3nav.mapdata.models.access import AccessPermission
|
from c3nav.mapdata.models.access import AccessPermission
|
||||||
from c3nav.mapdata.models.level import Level
|
from c3nav.mapdata.models.level import Level
|
||||||
from c3nav.mapdata.models.locations import LocationRedirect, SpecificLocation
|
from c3nav.mapdata.models.locations import LocationRedirect, SpecificLocation
|
||||||
from c3nav.mapdata.render.base import set_tile_access_cookie
|
from c3nav.mapdata.render.image.utils import set_tile_access_cookie
|
||||||
from c3nav.mapdata.utils.locations import get_location_by_slug_for_request
|
from c3nav.mapdata.utils.locations import get_location_by_slug_for_request
|
||||||
|
|
||||||
ctype_mapping = {
|
ctype_mapping = {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue