social/sharing card with opengraph and twitter, including preview image (no routing preview for now)

This commit is contained in:
Gwendolyn 2023-12-26 17:02:30 +01:00
parent 6c0ed4fd1d
commit 86180c8fb0
6 changed files with 159 additions and 5 deletions

View file

@ -2,8 +2,10 @@ from django.urls import path, register_converter
from c3nav.mapdata.converters import (AccessPermissionsConverter, ArchiveFileExtConverter, HistoryFileExtConverter,
HistoryModeConverter, SignedIntConverter)
from c3nav.mapdata.views import get_cache_package, map_history, tile
from c3nav.mapdata.views import get_cache_package, map_history, tile, preview_location, preview_route
from c3nav.site.converters import LocationConverter
register_converter(LocationConverter, 'loc')
register_converter(SignedIntConverter, 'sint')
register_converter(AccessPermissionsConverter, 'a_perms')
register_converter(HistoryModeConverter, 'h_mode')
@ -12,6 +14,8 @@ register_converter(ArchiveFileExtConverter, 'archive_fileext')
urlpatterns = [
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/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('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'),

View file

@ -11,17 +11,28 @@ from django.http import Http404, HttpResponse, HttpResponseNotModified, Streamin
from django.shortcuts import get_object_or_404
from django.utils.http import content_disposition_header
from django.views.decorators.http import etag
from shapely import Point, Polygon
from c3nav.mapdata.middleware import no_language
from c3nav.mapdata.models import Level, MapUpdate
from c3nav.mapdata.models.access import AccessPermission
from c3nav.mapdata.render.engines import ImageRenderEngine
from c3nav.mapdata.render.engines.base import FillAttribs, StrokeAttribs
from c3nav.mapdata.render.renderer import MapRenderer
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,
build_tile_etag, get_tile_bounds, parse_tile_access_cookie)
PREVIEW_HIGHLIGHT_FILL_COLOR = settings.PRIMARY_COLOR
PREVIEW_HIGHLIGHT_FILL_OPACITY = 0.1
PREVIEW_HIGHLIGHT_STROKE_COLOR = PREVIEW_HIGHLIGHT_FILL_COLOR
PREVIEW_HIGHLIGHT_STROKE_WIDTH = 0.5
PREVIEW_IMG_WIDTH = 1200
PREVIEW_IMG_HEIGHT = 628
PREVIEW_MIN_Y = 100
def set_tile_access_cookie(request, response):
access_permissions = AccessPermission.get_for_request(request)
if access_permissions:
@ -48,6 +59,78 @@ def enforce_tile_secret_auth(request):
raise PermissionDenied
@no_language()
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 = Polygon([(minx, miny), (minx, maxy), (maxx, maxy), (maxx, miny), (minx, miny)])
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
if not cache_package.bounds_valid(bounds[0], bounds[1], bounds[2], bounds[3]):
raise Http404
bounds_width = bounds[2] - bounds[0]
bounds_height = bounds[3] - bounds[1]
height = PREVIEW_MIN_Y
if height < bounds_height:
height = bounds_height + 10
width = height * PREVIEW_IMG_WIDTH / PREVIEW_IMG_HEIGHT
if width < bounds_width:
width = bounds_width + 10
height = width * PREVIEW_IMG_HEIGHT / PREVIEW_IMG_WIDTH
dx = width - bounds_width
dy = height - bounds_height
minx = bounds[0] - dx/2
maxx = bounds[2] + dx/2
miny = bounds[1] - dy/2
maxy = bounds[3] + dy/2
img_scale = PREVIEW_IMG_HEIGHT/height
level_data = cache_package.levels.get(level)
if level_data is None:
raise Http404
renderer = MapRenderer(level, minx, miny, maxx, maxy, scale=img_scale, access_permissions=set())
image = renderer.render(ImageRenderEngine)
if highlight:
image.add_geometry(geometry, fill=FillAttribs(PREVIEW_HIGHLIGHT_FILL_COLOR, PREVIEW_HIGHLIGHT_FILL_OPACITY),
stroke=StrokeAttribs(PREVIEW_HIGHLIGHT_STROKE_COLOR, PREVIEW_HIGHLIGHT_STROKE_WIDTH),
category='highlight')
data = image.render()
response = HttpResponse(data, 'image/png')
return response
@no_language()
def preview_route(request, slug, slug2):
raise NotImplementedError()
@no_language()
def tile(request, level, zoom, x, y, access_permissions: Optional[set] = None):
if access_permissions is not None:

View file

@ -27,6 +27,8 @@
<link href="{% static 'material-symbols/material-symbols.css' %}" rel="stylesheet">
<link href="{% static 'site/css/c3nav.scss' %}" rel="stylesheet" type="text/x-scss">
{% endcompress %}
{% block head %}
{% endblock %}
</head>
<body data-user-data="{{ user_data_json }}">
{% if not embed and not request.mobileclient %}

View file

@ -3,6 +3,34 @@
{% load compress %}
{% load i18n %}
{% block title %}{% if meta.title %}{{ meta.title }}{% else %}{{ block.super }}{% endif %}{% endblock %}
{% block head %}
{% if meta %}
<meta property="twitter:card" content="summary_large_image"/>
<meta property="og:type" content="website"/>
<meta property="og:site_name" content="c3nav"/>
{% endif %}
{% if meta.title %}
<meta name="title" content="{{ meta.title }}"/>
<meta property="og:title" content="{{ meta.title }}"/>
<meta property="twitter:title" content="{{ meta.title }}"/>
{% endif %}
{% if meta.description %}
<meta name="description" content="{{ meta.description }}"/>
<meta property="og:description" content="{{ meta.description }}"/>
<meta property="twitter:description" content="{{ meta.description }}"/>
{% endif %}
{% if meta.preview_img_url %}
<meta property="og:image" content="{{ meta.preview_img_url }}"/>
<meta property="twitter:image" content="{{ meta.preview_img_url }}"/>
{% endif %}
{% if meta.canonical_url %}
<meta property="og:url" content="{{ meta.canonical_url }}"/>
<meta property="twitter:url" content="{{ meta.canonical_url }}"/>
{% endif %}
{% endblock %}
{% block content %}
<main class="map" data-state="{{ state }}"{% if embed %} data-embed{% endif %} data-last-site-update="{{ last_site_update }}"{% if ssids %} data-ssids="{{ ssids }}"{% endif %} data-primary-color="{{ primary_color }}"{% if random_location_groups %} data-random-location-groups="{{ random_location_groups }}"{% endif %}>
{% if not request.mobileclient %}

View file

@ -8,7 +8,6 @@ from c3nav.site.views import (about_view, access_redeem_view, account_manage, ac
logout_view, map_index, position_create, position_detail, position_list, position_set,
qr_code, register_view, report_create, report_detail, report_list)
register_converter(LocationConverter, 'loc')
register_converter(CoordinatesConverter, 'coords')
register_converter(AtPositionConverter, 'at_pos')
register_converter(IsEmbedConverter, 'is_embed')
@ -19,13 +18,13 @@ pos = '<at_pos:pos>'
def index_paths(pre, suf):
return [
path(f'{pre}l/<loc:slug>/{suf}', map_index, {'mode': 'l'}, name='site.index', ),
path(f'{pre}l/<loc:slug>/{suf}', map_index, {'mode': 'l', 'details': False, 'nearby': False}, name='site.index', ),
path(f'{pre}l/<loc:slug>/details/{suf}', map_index, {'mode': 'l', 'details': True}, name='site.index'),
path(f'{pre}l/<loc:slug>/nearby/{suf}', map_index, {'mode': 'l', 'nearby': True}, name='site.index'),
path(f'{pre}o/<loc:slug>/{suf}', map_index, {'mode': 'o'}, name='site.index'),
path(f'{pre}d/<loc:slug>/{suf}', map_index, {'mode': 'd'}, name='site.index'),
path(f'{pre}r/{suf}', map_index, {'mode': 'r'}, name='site.index'),
path(f'{pre}r/<loc:slug>/<loc:slug2>/{suf}', map_index, {'mode': 'r'}, name='site.index'),
path(f'{pre}r/<loc:slug>/<loc:slug2>/{suf}', map_index, {'mode': 'r', 'details': False, 'options': False}, name='site.index'),
path(f'{pre}r/<loc:slug>/<loc:slug2>/details/{suf}', map_index, {'mode': 'r', 'details': True},
name='site.index'),
path(f'{pre}r/<loc:slug>/<loc:slug2>/options/{suf}', map_index, {'mode': 'r', 'options': True},

View file

@ -61,7 +61,6 @@ def check_location(location: Optional[str], request) -> Optional[SpecificLocatio
def map_index(request, mode=None, slug=None, slug2=None, details=None, options=None, nearby=None, pos=None, embed=None):
# check for access token
access_token = request.GET.get('access')
if access_token:
@ -133,6 +132,44 @@ def map_index(request, mode=None, slug=None, slug2=None, details=None, options=N
if not initial_bounds:
initial_bounds = tuple(chain(*Source.max_bounds()))
if origin is not None and destination is not None:
metadata = {
'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={
# 'slug': slug,
# 'slug2': slug2,
# })),
'canonical_url': request.build_absolute_uri(reverse('site.index', kwargs={
'mode': 'r',
'slug': slug,
'slug2': slug2,
'details': False,
'options': False,
})),
}
elif destination is not None or origin is not None:
metadata = {
'title': destination.title,
'description': destination.subtitle,
'preview_img_url': request.build_absolute_uri(reverse('mapdata.preview.location', kwargs={'slug': slug})),
'canonical_url': request.build_absolute_uri(reverse('site.index', kwargs={
'mode': 'l',
'slug': slug,
'nearby': False,
'details': False,
})),
}
elif mode is None:
metadata = {
'title': 'c3nav',
# 'description': '',
# 'preview_img_url': '',
'canonical_url': request.build_absolute_uri('/'),
}
else:
metadata = None
ctx = {
'bounds': json.dumps(Source.max_bounds(), separators=(',', ':')),
'levels': json.dumps(tuple((level.pk, level.short_label) for level in levels.values()), separators=(',', ':')),
@ -149,6 +186,7 @@ def map_index(request, mode=None, slug=None, slug2=None, details=None, options=N
'editor': can_access_editor(request),
'embed': bool(embed),
'imprint': settings.IMPRINT_LINK,
'meta': metadata,
}
if grid.enabled: