2017-11-04 23:21:36 +01:00
|
|
|
import math
|
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
from typing import Optional
|
|
|
|
|
2017-11-06 10:02:34 +01:00
|
|
|
from shapely.ops import unary_union
|
|
|
|
|
2017-11-04 23:21:36 +01:00
|
|
|
|
|
|
|
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))
|
|
|
|
|
2017-11-06 10:02:34 +01:00
|
|
|
# keep track which area of the image has which altitude currently
|
|
|
|
self.altitudes = {}
|
|
|
|
self.last_altitude = None
|
|
|
|
|
2017-11-04 23:21:36 +01:00
|
|
|
@abstractmethod
|
|
|
|
def get_png(self) -> bytes:
|
|
|
|
# render the image to png.
|
|
|
|
pass
|
|
|
|
|
2017-11-06 14:51:59 +01:00
|
|
|
@staticmethod
|
2017-11-07 12:24:31 +01:00
|
|
|
def color_to_rgb(color, alpha=None):
|
|
|
|
if color.startswith('#'):
|
|
|
|
return (*(int(color[i:i + 2], 16) / 255 for i in range(1, 6, 2)), 1 if alpha is None else alpha)
|
|
|
|
if color.startswith('rgba('):
|
|
|
|
color = tuple(float(i.strip()) for i in color.strip()[5:-1].split(','))
|
|
|
|
return (*(i/255 for i in color[:3]), color[3] if alpha is None else alpha)
|
|
|
|
raise ValueError('invalid color string!')
|
2017-11-06 14:51:59 +01:00
|
|
|
|
2017-11-06 10:02:34 +01:00
|
|
|
def clip_altitudes(self, new_geometry, new_altitude=None):
|
|
|
|
# register new geometry with an altitude
|
|
|
|
# a geometry with no altitude will reset the altitude information of its area as if nothing was ever there
|
|
|
|
if self.last_altitude is not None and self.last_altitude > new_altitude:
|
|
|
|
raise ValueError('Altitudes have to be ascending.')
|
|
|
|
|
|
|
|
if new_altitude in self.altitudes:
|
|
|
|
self.altitudes[new_altitude] = unary_union([self.altitudes[new_altitude], new_geometry])
|
|
|
|
else:
|
|
|
|
self.altitudes[new_altitude] = new_geometry
|
|
|
|
|
2017-11-04 23:21:36 +01:00
|
|
|
def add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
|
2017-11-05 13:30:45 +01:00
|
|
|
altitude=None, height=None, shape_cache_key=None):
|
2017-11-04 23:21:36 +01:00
|
|
|
# draw a shapely geometry with a given style
|
2017-11-05 13:24:35 +01:00
|
|
|
# altitude is the absolute altitude of the upper bound of the element
|
|
|
|
# height is the height of the element
|
|
|
|
# if altitude is not set but height is, the altitude will depend on the geometries below
|
2017-11-04 23:21:36 +01:00
|
|
|
|
|
|
|
# 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
|
|
|
|
|
2017-11-05 13:30:45 +01:00
|
|
|
self._add_geometry(geometry=geometry, fill=fill, stroke=stroke,
|
|
|
|
altitude=altitude, height=height, shape_cache_key=shape_cache_key)
|
2017-11-04 23:21:36 +01:00
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def _add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
|
2017-11-05 13:30:45 +01:00
|
|
|
altitude=None, height=None, shape_cache_key=None):
|
2017-11-04 23:21:36 +01:00
|
|
|
pass
|