opengl faces cache
This commit is contained in:
parent
3e41ac9e14
commit
ef29f48873
5 changed files with 70 additions and 36 deletions
|
@ -1,10 +1,14 @@
|
|||
import operator
|
||||
import pickle
|
||||
from collections import namedtuple
|
||||
from functools import reduce
|
||||
from itertools import chain
|
||||
|
||||
import numpy as np
|
||||
from django.core.cache import cache
|
||||
from django.db import transaction
|
||||
from shapely.geometry import LineString, MultiLineString
|
||||
from django.utils.functional import cached_property
|
||||
from shapely.geometry import GeometryCollection, LineString, MultiLineString
|
||||
from shapely.ops import unary_union
|
||||
|
||||
from c3nav.mapdata.cache import MapHistory
|
||||
|
@ -14,17 +18,32 @@ from c3nav.mapdata.utils.mesh import triangulate_rings
|
|||
from c3nav.mapdata.utils.mpl import shapely_to_mpl
|
||||
|
||||
|
||||
class HybridGeometry:
|
||||
def __init__(self, geom, faces):
|
||||
self._geom = geom
|
||||
self._faces = faces
|
||||
def hybrid_union(geoms):
|
||||
if not geoms:
|
||||
return HybridGeometry(GeometryCollection(), set())
|
||||
if len(geoms) == 1:
|
||||
return geoms[0]
|
||||
return HybridGeometry(unary_union(tuple(geom.geom for geom in geoms)),
|
||||
reduce(operator.or_, (geom.faces for geom in geoms), set()))
|
||||
|
||||
|
||||
class HybridGeometry(namedtuple('HybridGeometry', ('geom', 'faces'))):
|
||||
@classmethod
|
||||
def create(cls, geom, face_centers):
|
||||
if isinstance(geom, (LineString, MultiLineString)):
|
||||
return HybridGeometry(geom, set())
|
||||
return HybridGeometry(geom, set(np.argwhere(shapely_to_mpl(geom).contains_points(face_centers)).flatten()))
|
||||
|
||||
def union(self, geom):
|
||||
return HybridGeometry(self.geom.union(geom.geom), self.faces | geom.faces)
|
||||
|
||||
def difference(self, geom):
|
||||
return HybridGeometry(self.geom.difference(geom.geom), self.faces - geom.faces)
|
||||
|
||||
@cached_property
|
||||
def is_empty(self):
|
||||
return not self.faces
|
||||
|
||||
|
||||
class AltitudeAreaGeometries:
|
||||
def __init__(self, altitudearea=None, colors=None):
|
||||
|
@ -37,7 +56,7 @@ class AltitudeAreaGeometries:
|
|||
self.colors = colors
|
||||
|
||||
def get_geometries(self):
|
||||
return chain((self.geometry, ), chain(*(areas.values() for areas in self.colors.values())))
|
||||
return chain((self.geometry,), chain(*(areas.values() for areas in self.colors.values())))
|
||||
|
||||
def create_hybrid_geometries(self, face_centers):
|
||||
self.geometry = HybridGeometry.create(self.geometry, face_centers)
|
||||
|
@ -271,7 +290,7 @@ class LevelGeometries:
|
|||
return geoms
|
||||
|
||||
def get_geometries(self):
|
||||
return chain(chain(*(area.get_geometries() for area in self.altitudeareas)), (self.walls, self.doors, ),
|
||||
return chain(chain(*(area.get_geometries() for area in self.altitudeareas)), (self.walls, self.doors,),
|
||||
self.restricted_spaces_indoors.values(), self.restricted_spaces_outdoors.values())
|
||||
|
||||
def create_hybrid_geometries(self, face_centers):
|
||||
|
@ -287,7 +306,7 @@ class LevelGeometries:
|
|||
def build_mesh(self):
|
||||
rings = tuple(chain(*(get_rings(geom) for geom in self.get_geometries())))
|
||||
self.vertices, self.faces = triangulate_rings(rings)
|
||||
self.create_hybrid_geometries(face_centers=self.vertices[self.faces].sum(axis=1)/3)
|
||||
self.create_hybrid_geometries(face_centers=self.vertices[self.faces].sum(axis=1) / 3)
|
||||
|
||||
|
||||
def get_level_render_data(level):
|
||||
|
|
|
@ -3,6 +3,7 @@ from abc import ABC, abstractmethod
|
|||
from functools import lru_cache
|
||||
from typing import Optional
|
||||
|
||||
from shapely.geometry import JOIN_STYLE, box
|
||||
from shapely.ops import unary_union
|
||||
|
||||
|
||||
|
@ -32,6 +33,7 @@ class RenderEngine(ABC):
|
|||
self.minx = xoff
|
||||
self.miny = yoff
|
||||
self.scale = scale
|
||||
self.orig_buffer = buffer
|
||||
self.buffer = int(math.ceil(buffer*self.scale))
|
||||
self.background = background
|
||||
|
||||
|
@ -42,6 +44,7 @@ class RenderEngine(ABC):
|
|||
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.buffered_bbox = box(self.minx, self.miny, self.maxx, self.maxy).buffer(buffer, join_style=JOIN_STYLE.mitre)
|
||||
|
||||
self.background_rgb = tuple(int(background[i:i + 2], 16)/255 for i in range(1, 6, 2))
|
||||
|
||||
|
@ -83,14 +86,6 @@ class RenderEngine(ABC):
|
|||
# if altitude is not set but height is, the altitude will depend on the geometries below
|
||||
|
||||
# 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
|
||||
|
||||
|
@ -101,3 +96,6 @@ class RenderEngine(ABC):
|
|||
def _add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
|
||||
altitude=None, height=None, shape_cache_key=None):
|
||||
pass
|
||||
|
||||
def set_mesh_lookup_data(self, vertices, faces):
|
||||
pass
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import io
|
||||
import threading
|
||||
from collections import deque, namedtuple
|
||||
from collections import namedtuple
|
||||
from itertools import chain
|
||||
from queue import Queue
|
||||
from typing import Optional, Tuple, Union
|
||||
|
@ -11,6 +11,7 @@ from PIL import Image
|
|||
from shapely.geometry import CAP_STYLE, JOIN_STYLE, MultiPolygon, Polygon
|
||||
from shapely.ops import unary_union
|
||||
|
||||
from c3nav.mapdata.render.data import HybridGeometry
|
||||
from c3nav.mapdata.render.engines.base import FillAttribs, RenderEngine, StrokeAttribs
|
||||
from c3nav.mapdata.utils.mesh import triangulate_polygon
|
||||
|
||||
|
@ -143,13 +144,24 @@ class OpenGLEngine(RenderEngine):
|
|||
self.np_scale = np.array((scale_x, -scale_y))
|
||||
self.np_offset = np.array((-self.minx * scale_x - 1, self.maxy * scale_y - 1))
|
||||
|
||||
def _create_geometry(self, geometry: Union[Polygon, MultiPolygon], append=None):
|
||||
triangles = deque()
|
||||
# mesh data
|
||||
self.vertices_lookup = None
|
||||
self.faces_lookup = None
|
||||
|
||||
vertices, faces = triangulate_polygon(geometry)
|
||||
triangles = vertices[faces.flatten()]
|
||||
def set_mesh_lookup_data(self, vertices, faces):
|
||||
self.vertices_lookup = vertices
|
||||
self.faces_lookup = faces
|
||||
|
||||
def _create_geometry(self, geometry: Union[Polygon, MultiPolygon, HybridGeometry], append=None):
|
||||
if isinstance(geometry, HybridGeometry):
|
||||
vertices = self.vertices_lookup[
|
||||
self.faces_lookup[np.array(tuple(geometry.faces))].flatten()
|
||||
].astype(np.float32)
|
||||
else:
|
||||
vertices, faces = triangulate_polygon(geometry)
|
||||
triangles = vertices[faces.flatten()]
|
||||
vertices = np.vstack(triangles).astype(np.float32)
|
||||
|
||||
vertices = np.vstack(triangles).astype(np.float32)
|
||||
vertices = vertices * self.np_scale + self.np_offset
|
||||
if append is not None:
|
||||
append = np.array(append, dtype=np.float32).flatten()
|
||||
|
@ -162,18 +174,22 @@ class OpenGLEngine(RenderEngine):
|
|||
def _add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
|
||||
altitude=None, height=None, shape_cache_key=None):
|
||||
if fill is not None:
|
||||
if stroke is not None and fill.color == stroke.color:
|
||||
if stroke is not None and fill.color == stroke.color and 0:
|
||||
geometry = geometry.buffer(max(stroke.width, (stroke.min_px or 0) / self.scale),
|
||||
cap_style=CAP_STYLE.flat, join_style=JOIN_STYLE.mitre)
|
||||
stroke = None
|
||||
self.vertices.append(self._create_geometry(geometry, self.color_to_rgb(fill.color)))
|
||||
|
||||
if stroke is not None:
|
||||
geometry = self.buffered_bbox.intersection(geometry.geom)
|
||||
lines = tuple(chain(*(
|
||||
((geom.exterior, *geom.interiors) if isinstance(geom, Polygon) else (geom, ))
|
||||
for geom in getattr(geometry, 'geoms', (geometry, ))
|
||||
)))
|
||||
|
||||
if not lines:
|
||||
return
|
||||
|
||||
width = max(stroke.width, (stroke.min_px or 0) / self.scale) / 2
|
||||
|
||||
# if width would be <1px in the upsampled rendering, emulate it thorugh opacity on a 1px width
|
||||
|
@ -193,7 +209,7 @@ class OpenGLEngine(RenderEngine):
|
|||
|
||||
def get_png(self) -> bytes:
|
||||
return self.worker.render(self.width, self.height, self.samples, self.background_rgb,
|
||||
np.hstack(self.vertices).astype(np.float32).tobytes())
|
||||
np.hstack(self.vertices).astype(np.float32).tobytes() if self.vertices else b'')
|
||||
|
||||
|
||||
OpenGLEngine.worker.start()
|
||||
|
|
|
@ -198,6 +198,7 @@ class SVGEngine(RenderEngine):
|
|||
|
||||
def _add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
|
||||
altitude=None, height=None, shape_cache_key=None):
|
||||
geometry = self.buffered_bbox.intersection(geometry.geom)
|
||||
|
||||
if fill:
|
||||
attribs = ' fill="'+(fill.color)+'"'
|
||||
|
|
|
@ -2,11 +2,10 @@ from django.core.cache import cache
|
|||
from django.utils.functional import cached_property
|
||||
from shapely import prepared
|
||||
from shapely.geometry import box
|
||||
from shapely.ops import unary_union
|
||||
|
||||
from c3nav.mapdata.cache import MapHistory
|
||||
from c3nav.mapdata.models import MapUpdate
|
||||
from c3nav.mapdata.render.data import get_level_render_data
|
||||
from c3nav.mapdata.render.data import get_level_render_data, hybrid_union
|
||||
from c3nav.mapdata.render.engines.base import FillAttribs, StrokeAttribs
|
||||
|
||||
|
||||
|
@ -70,17 +69,18 @@ class MapRenderer:
|
|||
# add no access restriction to “unlocked“ access restrictions so lookup gets easier
|
||||
unlocked_access_restrictions = self.unlocked_access_restrictions | set([None])
|
||||
|
||||
bbox = self.bbox
|
||||
bbox_prep = prepared.prep(bbox)
|
||||
bbox = prepared.prep(self.bbox)
|
||||
|
||||
for geoms, default_height in self.level_render_data.levels:
|
||||
if not bbox_prep.intersects(geoms.affected_area):
|
||||
if not bbox.intersects(geoms.affected_area):
|
||||
continue
|
||||
|
||||
engine.set_mesh_lookup_data(geoms.vertices, geoms.faces)
|
||||
|
||||
# hide indoor and outdoor rooms if their access restriction was not unlocked
|
||||
add_walls = unary_union(tuple(area for access_restriction, area in geoms.restricted_spaces_indoors.items()
|
||||
if access_restriction not in unlocked_access_restrictions))
|
||||
crop_areas = unary_union(
|
||||
add_walls = hybrid_union(tuple(area for access_restriction, area in geoms.restricted_spaces_indoors.items()
|
||||
if access_restriction not in unlocked_access_restrictions))
|
||||
crop_areas = hybrid_union(
|
||||
tuple(area for access_restriction, area in geoms.restricted_spaces_outdoors.items()
|
||||
if access_restriction not in unlocked_access_restrictions)
|
||||
).union(add_walls)
|
||||
|
@ -88,7 +88,7 @@ class MapRenderer:
|
|||
# render altitude areas in default ground color and add ground colors to each one afterwards
|
||||
# shadows are directly calculated and added by the engine
|
||||
for altitudearea in geoms.altitudeareas:
|
||||
engine.add_geometry(bbox.intersection(altitudearea.geometry.difference(crop_areas)),
|
||||
engine.add_geometry(altitudearea.geometry.difference(crop_areas),
|
||||
altitude=altitudearea.altitude, fill=FillAttribs('#eeeeee'))
|
||||
|
||||
for color, areas in altitudearea.colors.items():
|
||||
|
@ -96,18 +96,18 @@ class MapRenderer:
|
|||
areas = tuple(area for access_restriction, area in areas.items()
|
||||
if access_restriction in unlocked_access_restrictions)
|
||||
if areas:
|
||||
engine.add_geometry(bbox.intersection(unary_union(areas)), fill=FillAttribs(color))
|
||||
engine.add_geometry(hybrid_union(areas), fill=FillAttribs(color))
|
||||
|
||||
# add walls, stroke_px makes sure that all walls are at least 1px thick on all zoom levels,
|
||||
walls = None
|
||||
if not add_walls.is_empty or not geoms.walls.is_empty:
|
||||
walls = bbox.intersection(geoms.walls.union(add_walls))
|
||||
walls = geoms.walls.union(add_walls)
|
||||
|
||||
if walls is not None:
|
||||
engine.add_geometry(walls, height=default_height, fill=FillAttribs('#aaaaaa'))
|
||||
|
||||
if not geoms.doors.is_empty:
|
||||
engine.add_geometry(bbox.intersection(geoms.doors.difference(add_walls)), fill=FillAttribs('#ffffff'),
|
||||
engine.add_geometry(geoms.doors.difference(add_walls), fill=FillAttribs('#ffffff'),
|
||||
stroke=StrokeAttribs('#ffffff', 0.05, min_px=0.2))
|
||||
|
||||
if walls is not None:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue