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
|
import pickle
|
||||||
|
from collections import namedtuple
|
||||||
|
from functools import reduce
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db import transaction
|
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 shapely.ops import unary_union
|
||||||
|
|
||||||
from c3nav.mapdata.cache import MapHistory
|
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
|
from c3nav.mapdata.utils.mpl import shapely_to_mpl
|
||||||
|
|
||||||
|
|
||||||
class HybridGeometry:
|
def hybrid_union(geoms):
|
||||||
def __init__(self, geom, faces):
|
if not geoms:
|
||||||
self._geom = geom
|
return HybridGeometry(GeometryCollection(), set())
|
||||||
self._faces = faces
|
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
|
@classmethod
|
||||||
def create(cls, geom, face_centers):
|
def create(cls, geom, face_centers):
|
||||||
if isinstance(geom, (LineString, MultiLineString)):
|
if isinstance(geom, (LineString, MultiLineString)):
|
||||||
return HybridGeometry(geom, set())
|
return HybridGeometry(geom, set())
|
||||||
return HybridGeometry(geom, set(np.argwhere(shapely_to_mpl(geom).contains_points(face_centers)).flatten()))
|
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:
|
class AltitudeAreaGeometries:
|
||||||
def __init__(self, altitudearea=None, colors=None):
|
def __init__(self, altitudearea=None, colors=None):
|
||||||
|
@ -37,7 +56,7 @@ class AltitudeAreaGeometries:
|
||||||
self.colors = colors
|
self.colors = colors
|
||||||
|
|
||||||
def get_geometries(self):
|
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):
|
def create_hybrid_geometries(self, face_centers):
|
||||||
self.geometry = HybridGeometry.create(self.geometry, face_centers)
|
self.geometry = HybridGeometry.create(self.geometry, face_centers)
|
||||||
|
@ -271,7 +290,7 @@ class LevelGeometries:
|
||||||
return geoms
|
return geoms
|
||||||
|
|
||||||
def get_geometries(self):
|
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())
|
self.restricted_spaces_indoors.values(), self.restricted_spaces_outdoors.values())
|
||||||
|
|
||||||
def create_hybrid_geometries(self, face_centers):
|
def create_hybrid_geometries(self, face_centers):
|
||||||
|
@ -287,7 +306,7 @@ class LevelGeometries:
|
||||||
def build_mesh(self):
|
def build_mesh(self):
|
||||||
rings = tuple(chain(*(get_rings(geom) for geom in self.get_geometries())))
|
rings = tuple(chain(*(get_rings(geom) for geom in self.get_geometries())))
|
||||||
self.vertices, self.faces = triangulate_rings(rings)
|
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):
|
def get_level_render_data(level):
|
||||||
|
|
|
@ -3,6 +3,7 @@ from abc import ABC, abstractmethod
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from shapely.geometry import JOIN_STYLE, box
|
||||||
from shapely.ops import unary_union
|
from shapely.ops import unary_union
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,6 +33,7 @@ class RenderEngine(ABC):
|
||||||
self.minx = xoff
|
self.minx = xoff
|
||||||
self.miny = yoff
|
self.miny = yoff
|
||||||
self.scale = scale
|
self.scale = scale
|
||||||
|
self.orig_buffer = buffer
|
||||||
self.buffer = int(math.ceil(buffer*self.scale))
|
self.buffer = int(math.ceil(buffer*self.scale))
|
||||||
self.background = background
|
self.background = background
|
||||||
|
|
||||||
|
@ -42,6 +44,7 @@ class RenderEngine(ABC):
|
||||||
self.buffer = int(math.ceil(buffer*self.scale))
|
self.buffer = int(math.ceil(buffer*self.scale))
|
||||||
self.buffered_width = self.width + 2 * self.buffer
|
self.buffered_width = self.width + 2 * self.buffer
|
||||||
self.buffered_height = self.height + 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))
|
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 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_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:
|
if geometry.is_empty:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -101,3 +96,6 @@ class RenderEngine(ABC):
|
||||||
def _add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
|
def _add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
|
||||||
altitude=None, height=None, shape_cache_key=None):
|
altitude=None, height=None, shape_cache_key=None):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def set_mesh_lookup_data(self, vertices, faces):
|
||||||
|
pass
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import io
|
import io
|
||||||
import threading
|
import threading
|
||||||
from collections import deque, namedtuple
|
from collections import namedtuple
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from typing import Optional, Tuple, Union
|
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.geometry import CAP_STYLE, JOIN_STYLE, MultiPolygon, Polygon
|
||||||
from shapely.ops import unary_union
|
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.render.engines.base import FillAttribs, RenderEngine, StrokeAttribs
|
||||||
from c3nav.mapdata.utils.mesh import triangulate_polygon
|
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_scale = np.array((scale_x, -scale_y))
|
||||||
self.np_offset = np.array((-self.minx * scale_x - 1, self.maxy * scale_y - 1))
|
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):
|
# mesh data
|
||||||
triangles = deque()
|
self.vertices_lookup = None
|
||||||
|
self.faces_lookup = None
|
||||||
|
|
||||||
|
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)
|
vertices, faces = triangulate_polygon(geometry)
|
||||||
triangles = vertices[faces.flatten()]
|
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
|
vertices = vertices * self.np_scale + self.np_offset
|
||||||
if append is not None:
|
if append is not None:
|
||||||
append = np.array(append, dtype=np.float32).flatten()
|
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,
|
def _add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
|
||||||
altitude=None, height=None, shape_cache_key=None):
|
altitude=None, height=None, shape_cache_key=None):
|
||||||
if fill is not 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),
|
geometry = geometry.buffer(max(stroke.width, (stroke.min_px or 0) / self.scale),
|
||||||
cap_style=CAP_STYLE.flat, join_style=JOIN_STYLE.mitre)
|
cap_style=CAP_STYLE.flat, join_style=JOIN_STYLE.mitre)
|
||||||
stroke = None
|
stroke = None
|
||||||
self.vertices.append(self._create_geometry(geometry, self.color_to_rgb(fill.color)))
|
self.vertices.append(self._create_geometry(geometry, self.color_to_rgb(fill.color)))
|
||||||
|
|
||||||
if stroke is not None:
|
if stroke is not None:
|
||||||
|
geometry = self.buffered_bbox.intersection(geometry.geom)
|
||||||
lines = tuple(chain(*(
|
lines = tuple(chain(*(
|
||||||
((geom.exterior, *geom.interiors) if isinstance(geom, Polygon) else (geom, ))
|
((geom.exterior, *geom.interiors) if isinstance(geom, Polygon) else (geom, ))
|
||||||
for geom in getattr(geometry, 'geoms', (geometry, ))
|
for geom in getattr(geometry, 'geoms', (geometry, ))
|
||||||
)))
|
)))
|
||||||
|
|
||||||
|
if not lines:
|
||||||
|
return
|
||||||
|
|
||||||
width = max(stroke.width, (stroke.min_px or 0) / self.scale) / 2
|
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
|
# 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:
|
def get_png(self) -> bytes:
|
||||||
return self.worker.render(self.width, self.height, self.samples, self.background_rgb,
|
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()
|
OpenGLEngine.worker.start()
|
||||||
|
|
|
@ -198,6 +198,7 @@ class SVGEngine(RenderEngine):
|
||||||
|
|
||||||
def _add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
|
def _add_geometry(self, geometry, fill: Optional[FillAttribs] = None, stroke: Optional[StrokeAttribs] = None,
|
||||||
altitude=None, height=None, shape_cache_key=None):
|
altitude=None, height=None, shape_cache_key=None):
|
||||||
|
geometry = self.buffered_bbox.intersection(geometry.geom)
|
||||||
|
|
||||||
if fill:
|
if fill:
|
||||||
attribs = ' fill="'+(fill.color)+'"'
|
attribs = ' fill="'+(fill.color)+'"'
|
||||||
|
|
|
@ -2,11 +2,10 @@ from django.core.cache import cache
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from shapely import prepared
|
from shapely import prepared
|
||||||
from shapely.geometry import box
|
from shapely.geometry import box
|
||||||
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.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
|
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
|
# 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])
|
||||||
|
|
||||||
bbox = self.bbox
|
bbox = prepared.prep(self.bbox)
|
||||||
bbox_prep = prepared.prep(bbox)
|
|
||||||
|
|
||||||
for geoms, default_height in self.level_render_data.levels:
|
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
|
continue
|
||||||
|
|
||||||
|
engine.set_mesh_lookup_data(geoms.vertices, geoms.faces)
|
||||||
|
|
||||||
# hide indoor and outdoor rooms if their access restriction was not unlocked
|
# 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()
|
add_walls = hybrid_union(tuple(area for access_restriction, area in geoms.restricted_spaces_indoors.items()
|
||||||
if access_restriction not in unlocked_access_restrictions))
|
if access_restriction not in unlocked_access_restrictions))
|
||||||
crop_areas = unary_union(
|
crop_areas = hybrid_union(
|
||||||
tuple(area for access_restriction, area in geoms.restricted_spaces_outdoors.items()
|
tuple(area for access_restriction, area in geoms.restricted_spaces_outdoors.items()
|
||||||
if access_restriction not in unlocked_access_restrictions)
|
if access_restriction not in unlocked_access_restrictions)
|
||||||
).union(add_walls)
|
).union(add_walls)
|
||||||
|
@ -88,7 +88,7 @@ class MapRenderer:
|
||||||
# render altitude areas in default ground color and add ground colors to each one afterwards
|
# render altitude areas in default ground color and add ground colors to each one afterwards
|
||||||
# shadows are directly calculated and added by the engine
|
# shadows are directly calculated and added by the engine
|
||||||
for altitudearea in geoms.altitudeareas:
|
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'))
|
altitude=altitudearea.altitude, fill=FillAttribs('#eeeeee'))
|
||||||
|
|
||||||
for color, areas in altitudearea.colors.items():
|
for color, areas in altitudearea.colors.items():
|
||||||
|
@ -96,18 +96,18 @@ class MapRenderer:
|
||||||
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:
|
||||||
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,
|
# add walls, stroke_px makes sure that all walls are at least 1px thick on all zoom levels,
|
||||||
walls = None
|
walls = None
|
||||||
if not add_walls.is_empty or not geoms.walls.is_empty:
|
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:
|
if walls is not None:
|
||||||
engine.add_geometry(walls, height=default_height, fill=FillAttribs('#aaaaaa'))
|
engine.add_geometry(walls, height=default_height, fill=FillAttribs('#aaaaaa'))
|
||||||
|
|
||||||
if not geoms.doors.is_empty:
|
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))
|
stroke=StrokeAttribs('#ffffff', 0.05, min_px=0.2))
|
||||||
|
|
||||||
if walls is not None:
|
if walls is not None:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue