preview images for routes

This commit is contained in:
Gwendolyn 2023-12-26 20:03:49 +01:00
parent 7ab1881185
commit d025ffa194
3 changed files with 152 additions and 45 deletions

View file

@ -15,7 +15,7 @@ register_converter(ArchiveFileExtConverter, 'archive_fileext')
urlpatterns = [ urlpatterns = [
path('<int:level>/<sint:zoom>/<sint:x>/<sint:y>.png', tile, name='mapdata.tile'), path('<int:level>/<sint:zoom>/<sint:x>/<sint:y>.png', tile, name='mapdata.tile'),
path('preview/l/<loc:slug>.png', preview_location, name='mapdata.preview.location'), path('preview/l/<loc:slug>.png', preview_location, name='mapdata.preview.location'),
# path('preview/r/<loc:slug>/<loc:slug2>.png', preview_route, name='mapdata.preview.route'), path('preview/r/<loc:slug>/<loc:slug2>.png', preview_route, name='mapdata.preview.route'),
path('<int:level>/<sint:zoom>/<sint:x>/<sint:y>/<a_perms:access_permissions>.png', tile, name='mapdata.tile'), path('<int:level>/<sint:zoom>/<sint:x>/<sint:y>/<a_perms:access_permissions>.png', tile, name='mapdata.tile'),
path('history/<int:level>/<h_mode:mode>.<h_fileext:filetype>', map_history, name='mapdata.map_history'), path('history/<int:level>/<h_mode:mode>.<h_fileext:filetype>', map_history, name='mapdata.map_history'),
path('cache/package.<archive_fileext:filetype>', get_cache_package, name='mapdata.cache_package'), path('cache/package.<archive_fileext:filetype>', get_cache_package, name='mapdata.cache_package'),

View file

@ -12,7 +12,7 @@ from django.shortcuts import get_object_or_404
from django.utils.http import content_disposition_header from django.utils.http import content_disposition_header
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
from django.views.decorators.http import etag from django.views.decorators.http import etag
from shapely import Point, box from shapely import Point, box, LineString, unary_union
from c3nav.mapdata.middleware import no_language from c3nav.mapdata.middleware import no_language
from c3nav.mapdata.models import Level, MapUpdate from c3nav.mapdata.models import Level, MapUpdate
@ -24,7 +24,6 @@ from c3nav.mapdata.utils.cache import CachePackage, MapHistory
from c3nav.mapdata.utils.tiles import (build_access_cache_key, build_base_cache_key, build_tile_access_cookie, from c3nav.mapdata.utils.tiles import (build_access_cache_key, build_base_cache_key, build_tile_access_cookie,
build_tile_etag, get_tile_bounds, parse_tile_access_cookie) build_tile_etag, get_tile_bounds, parse_tile_access_cookie)
PREVIEW_HIGHLIGHT_FILL_COLOR = settings.PRIMARY_COLOR PREVIEW_HIGHLIGHT_FILL_COLOR = settings.PRIMARY_COLOR
PREVIEW_HIGHLIGHT_FILL_OPACITY = 0.1 PREVIEW_HIGHLIGHT_FILL_OPACITY = 0.1
PREVIEW_HIGHLIGHT_STROKE_COLOR = PREVIEW_HIGHLIGHT_FILL_COLOR PREVIEW_HIGHLIGHT_STROKE_COLOR = PREVIEW_HIGHLIGHT_FILL_COLOR
@ -60,35 +59,7 @@ def enforce_tile_secret_auth(request):
raise PermissionDenied raise PermissionDenied
@no_language() def bounds_for_preview(geometry, cache_package):
@cache_page(60 * 5)
def preview_location(request, slug):
from c3nav.site.views import check_location
from c3nav.mapdata.utils.locations import CustomLocation
from c3nav.mapdata.models.geometry.base import GeometryMixin
location = check_location(slug, request)
highlight = True
if location is None:
raise Http404
if isinstance(location, CustomLocation):
geometry = Point(location.x, location.y)
level = location.level.pk
elif isinstance(location, GeometryMixin):
geometry = location.geometry
level = location.level_id
elif isinstance(location, Level):
[minx, miny, maxx, maxy] = location.bounds
geometry = box(minx, miny, maxx, maxy)
level = location.pk
highlight = False
else:
raise NotImplementedError(f'location type {type(location)} is not supported yet')
cache_package = CachePackage.open_cached()
if isinstance(geometry, Point):
geometry = geometry.buffer(1)
bounds = geometry.bounds bounds = geometry.bounds
if not cache_package.bounds_valid(bounds[0], bounds[1], bounds[2], bounds[3]): if not cache_package.bounds_valid(bounds[0], bounds[1], bounds[2], bounds[3]):
@ -111,9 +82,43 @@ def preview_location(request, slug):
maxx = int(bounds[2] + dx / 2) maxx = int(bounds[2] + dx / 2)
miny = int(bounds[1] - dy / 2) miny = int(bounds[1] - dy / 2)
maxy = int(bounds[3] + dy / 2) maxy = int(bounds[3] + dy / 2)
img_scale = PREVIEW_IMG_HEIGHT / height img_scale = PREVIEW_IMG_HEIGHT / height
return minx, miny, maxx, maxy, img_scale
@no_language()
@cache_page(60 * 5)
def preview_location(request, slug):
from c3nav.site.views import check_location
from c3nav.mapdata.utils.locations import CustomLocation
from c3nav.mapdata.models.geometry.base import GeometryMixin
location = check_location(slug, None)
highlight = True
if location is None:
raise Http404
if isinstance(location, CustomLocation):
geometry = Point(location.x, location.y)
level = location.level.pk
elif isinstance(location, GeometryMixin):
geometry = location.geometry
level = location.level_id
elif isinstance(location, Level):
[minx, miny, maxx, maxy] = location.bounds
geometry = box(minx, miny, maxx, maxy)
level = location.pk
highlight = False
else:
raise NotImplementedError(f'location type {type(location)} is not supported yet')
cache_package = CachePackage.open_cached()
if isinstance(geometry, Point):
geometry = geometry.buffer(1)
minx, miny, maxx, maxy, img_scale = bounds_for_preview(geometry, cache_package)
level_data = cache_package.levels.get(level) level_data = cache_package.levels.get(level)
if level_data is None: if level_data is None:
raise Http404 raise Http404
@ -125,13 +130,116 @@ def preview_location(request, slug):
category='highlight') category='highlight')
data = image.render() data = image.render()
response = HttpResponse(data, 'image/png') response = HttpResponse(data, 'image/png')
# TODO use cache key like in tile render function # TODO use cache key like in tile render function, same for the routing option
return response return response
@no_language() @no_language()
@cache_page(60 * 5)
def preview_route(request, slug, slug2): def preview_route(request, slug, slug2):
raise NotImplementedError() from c3nav.routing.router import Router
from c3nav.routing.models import RouteOptions
from c3nav.routing.exceptions import NotYetRoutable
from c3nav.routing.exceptions import LocationUnreachable
from c3nav.routing.exceptions import NoRouteFound
from c3nav.site.views import check_location
from c3nav.mapdata.utils.locations import CustomLocation
from c3nav.mapdata.utils.geometry import unwrap_geom
origin = check_location(slug, None)
destination = check_location(slug2, None)
try:
route = Router.load().get_route(origin=origin,
destination=destination,
permissions=set(),
options=RouteOptions())
except NotYetRoutable:
raise Http404()
except LocationUnreachable:
raise Http404()
except NoRouteFound:
raise Http404()
route_items = [route.router.nodes[x] for x in route.path_nodes]
route_points = [(item.point.x, item.point.y, route.router.spaces[item.space].level_id) for item in route_items]
from c3nav.mapdata.models.geometry.base import GeometryMixin
if isinstance(origin, CustomLocation):
origin_level = origin.level.pk
origin_geometry = Point(origin.x, origin.y)
elif isinstance(origin, GeometryMixin):
origin_level = origin.level_id
origin_geometry = origin.geometry
elif isinstance(origin, Level):
raise Http404 # preview for routes from a level don't really make sense
else:
raise NotImplementedError(f'location type {type(origin)} is not implemented')
if isinstance(origin_geometry, Point):
origin_geometry = origin_geometry.buffer(1)
if isinstance(destination, CustomLocation):
destination_level = destination.level.pk
destination_geometry = Point(destination.x, destination.y)
elif isinstance(destination, GeometryMixin):
destination_level = destination.level_id
destination_geometry = destination.geometry
elif isinstance(destination, Level):
destination_level = destination.pk
destination_geometry = None
else:
destination_level = origin_level # just so that it's set to something
destination_geometry = None
if destination_level != origin_level:
destination_geometry = None
if isinstance(destination_geometry, Point):
origin_geometry = destination_geometry.buffer(1)
lines = []
line = None
for x, y, level in route_points:
if line is None and level == origin_level:
line = [(x, y)]
elif line is not None and level == origin_level:
line.append((x, y))
elif line is not None and level != origin_level:
if len(line) > 1:
lines.append(line)
line = None
if line is not None and len(line) > 1:
lines.append(line)
route_geometries = [LineString(line) for line in lines]
all_geoms = [*route_geometries, unwrap_geom(origin_geometry), unwrap_geom(destination_geometry)]
combined_geometry = unary_union([x for x in all_geoms if x is not None])
cache_package = CachePackage.open_cached()
minx, miny, maxx, maxy, img_scale = bounds_for_preview(combined_geometry, cache_package)
level_data = cache_package.levels.get(origin_level)
if level_data is None:
raise Http404
renderer = MapRenderer(origin_level, minx, miny, maxx, maxy, scale=img_scale, access_permissions=set())
image = renderer.render(ImageRenderEngine)
if origin_geometry is not None:
image.add_geometry(origin_geometry, stroke=StrokeAttribs(PREVIEW_HIGHLIGHT_STROKE_COLOR,
PREVIEW_HIGHLIGHT_STROKE_WIDTH),
category='highlight')
if destination_geometry is not None:
image.add_geometry(destination_geometry, stroke=StrokeAttribs(PREVIEW_HIGHLIGHT_STROKE_COLOR,
PREVIEW_HIGHLIGHT_STROKE_WIDTH),
category='highlight')
for geom in route_geometries:
image.add_geometry(geom, stroke=StrokeAttribs(PREVIEW_HIGHLIGHT_STROKE_COLOR, PREVIEW_HIGHLIGHT_STROKE_WIDTH),
category='route')
data = image.render()
response = HttpResponse(data, 'image/png')
# TODO use cache key like in tile render function
return response
@no_language() @no_language()

View file

@ -135,11 +135,10 @@ def map_index(request, mode=None, slug=None, slug2=None, details=None, options=N
if origin is not None and destination is not None: if origin is not None and destination is not None:
metadata = { metadata = {
'title': _('Route from %s to %s') % (origin.title, destination.title), 'title': _('Route from %s to %s') % (origin.title, destination.title),
# TODO: enable when route image generation is implemented 'preview_img_url': request.build_absolute_uri(reverse('mapdata.preview.route', kwargs={
# 'preview_img_url': request.build_absolute_uri(reverse('mapdata.preview.route', kwargs={ 'slug': slug,
# 'slug': slug, 'slug2': slug2,
# 'slug2': slug2, })),
# })),
'canonical_url': request.build_absolute_uri(reverse('site.index', kwargs={ 'canonical_url': request.build_absolute_uri(reverse('site.index', kwargs={
'mode': 'r', 'mode': 'r',
'slug': slug, 'slug': slug,