first stuff for showing the route on the site

This commit is contained in:
Laura Klünder 2016-12-17 19:25:27 +01:00
parent ad58ddbbcb
commit f6129d621c
14 changed files with 366 additions and 154 deletions

View file

@ -1,11 +1,40 @@
import os
from django.http import HttpResponse from django.http import HttpResponse
from PIL import Image from PIL import Image
from c3nav.mapdata.render.utils import get_render_path from c3nav.mapdata.utils.misc import get_render_path
def get_level_image(request, level): class LevelComposer:
im = Image.open(get_render_path('png', level.name, 'full', True)) images = {}
response = HttpResponse(content_type="image/png") images_mtimes = {}
im.save(response, 'PNG')
return response @classmethod
def _get_image(cls, filename, cached=True):
mtime = None
if cached:
mtime = os.path.getmtime(filename)
if filename in cls.images:
if cls.images_mtimes[filename] == mtime:
return cls.images[filename]
img = Image.open(filename)
if cached:
cls.images[filename] = img
cls.images_mtimes[filename] = mtime
return img
def _get_public_level_image(self, level):
return self._get_image(get_render_path('png', level.name, 'full', True))
def get_level_image(self, request, level):
img = self._get_public_level_image(level)
response = HttpResponse(content_type="image/png")
img.save(response, 'PNG')
return response
composer = LevelComposer()

View file

@ -5,7 +5,7 @@ from django.conf import settings
from shapely.affinity import scale from shapely.affinity import scale
from shapely.geometry import JOIN_STYLE, box from shapely.geometry import JOIN_STYLE, box
from c3nav.mapdata.render.utils import get_dimensions, get_render_path from c3nav.mapdata.utils.misc import get_dimensions, get_render_dimensions, get_render_path
class LevelRenderer(): class LevelRenderer():
@ -21,11 +21,6 @@ class LevelRenderer():
def get_filename(self, mode, filetype, level=None): def get_filename(self, mode, filetype, level=None):
return get_render_path(filetype, self.level.name if level is None else level, mode, self.only_public) return get_render_path(filetype, self.level.name if level is None else level, mode, self.only_public)
@staticmethod
def get_dimensions():
width, height = get_dimensions()
return (width * settings.RENDER_SCALE, height * settings.RENDER_SCALE)
@staticmethod @staticmethod
def polygon_svg(geometry, fill_color=None, fill_opacity=None, def polygon_svg(geometry, fill_color=None, fill_opacity=None,
stroke_width=0.0, stroke_color=None, stroke_opacity=None): stroke_width=0.0, stroke_color=None, stroke_opacity=None):
@ -62,7 +57,7 @@ class LevelRenderer():
return element return element
def create_svg(self): def create_svg(self):
width, height = self.get_dimensions() width, height = get_render_dimensions()
svg = ET.Element('svg', { svg = ET.Element('svg', {
'width': str(width), 'width': str(width),
'height': str(height), 'height': str(height),
@ -73,7 +68,7 @@ class LevelRenderer():
return svg return svg
def add_svg_content(self, svg): def add_svg_content(self, svg):
width, height = self.get_dimensions() width, height = get_render_dimensions()
contents = ET.Element('g', { contents = ET.Element('g', {
'transform': 'scale(1 -1) translate(0 -%d)' % (height), 'transform': 'scale(1 -1) translate(0 -%d)' % (height),
}) })
@ -81,7 +76,7 @@ class LevelRenderer():
return contents return contents
def add_svg_image(self, svg, image): def add_svg_image(self, svg, image):
width, height = self.get_dimensions() width, height = get_render_dimensions()
contents = ET.Element('image', { contents = ET.Element('image', {
'x': '0', 'x': '0',
'y': '0', 'y': '0',

View file

@ -4,8 +4,10 @@ from django.conf import settings
from django.db.models import Max, Min from django.db.models import Max, Min
from c3nav.mapdata.models import Package from c3nav.mapdata.models import Package
from c3nav.mapdata.utils.cache import cache_result
@cache_result('c3nav__mapdata__dimensions')
def get_dimensions(): def get_dimensions():
aggregate = Package.objects.all().aggregate(Max('right'), Min('left'), Max('top'), Min('bottom')) aggregate = Package.objects.all().aggregate(Max('right'), Min('left'), Max('top'), Min('bottom'))
return ( return (
@ -14,6 +16,12 @@ def get_dimensions():
) )
@cache_result('c3nav__mapdata__render_dimensions')
def get_render_dimensions():
width, height = get_dimensions()
return (width * settings.RENDER_SCALE, height * settings.RENDER_SCALE)
def get_render_path(filetype, level, mode, public): def get_render_path(filetype, level, mode, public):
return os.path.join(settings.RENDER_ROOT, return os.path.join(settings.RENDER_ROOT,
'%s%s-level-%s.%s' % (('public-' if public else ''), mode, level, filetype)) '%s%s-level-%s.%s' % (('public-' if public else ''), mode, level, filetype))

View file

@ -6,3 +6,6 @@ class GraphConnection():
self.from_point = from_point self.from_point = from_point
self.to_point = to_point self.to_point = to_point
self.distance = distance if distance is not None else abs(np.linalg.norm(from_point.xy - to_point.xy)) self.distance = distance if distance is not None else abs(np.linalg.norm(from_point.xy - to_point.xy))
def __repr__(self):
return '<GraphConnection %r %r distance=%f>' % (self.from_point, self.to_point, self.distance)

View file

@ -12,7 +12,8 @@ from c3nav.mapdata.models.geometry import LevelConnector
from c3nav.mapdata.models.locations import AreaLocation, Location, LocationGroup, PointLocation from c3nav.mapdata.models.locations import AreaLocation, Location, LocationGroup, PointLocation
from c3nav.routing.level import GraphLevel from c3nav.routing.level import GraphLevel
from c3nav.routing.point import GraphPoint from c3nav.routing.point import GraphPoint
from c3nav.routing.route import GraphRouteSegment, LevelRouteSegment, NoRoute, RoomRouteSegment, SegmentRoute from c3nav.routing.route import NoRoute
from c3nav.routing.routesegments import GraphRouteSegment, LevelRouteSegment, RoomRouteSegment, SegmentRoute
class Graph: class Graph:

View file

@ -15,6 +15,10 @@ class GraphPoint():
self.connections = {} self.connections = {}
self.connections_in = {} self.connections_in = {}
@cached_property
def level(self):
return self.room and self.room.level
def serialize(self): def serialize(self):
return ( return (
self.x, self.x,
@ -32,3 +36,6 @@ class GraphPoint():
connection = GraphConnection(self, other_point) connection = GraphConnection(self, other_point)
self.connections[other_point] = connection self.connections[other_point] = connection
other_point.connections_in[self] = connection other_point.connections_in[self] = connection
def __repr__(self):
return '<GraphPoint x=%f y=%f room=%s>' % (self.x, self.y, (id(self.room) if self.room else None))

View file

@ -1,139 +1,7 @@
from abc import ABC, abstractmethod
import numpy as np import numpy as np
from django.utils.functional import cached_property from django.utils.functional import cached_property
from c3nav.mapdata.utils.misc import get_dimensions
class RouteSegment(ABC):
def __init__(self, routers, router, from_point, to_point):
"""
:param router: a Router (RoomRouter, GraphRouter, )
:param from_point: in-router index of first point
:param to_point: in-router index of last point
"""
self.routers = routers
self.router = router
self.from_point = int(from_point)
self.to_point = int(to_point)
def as_route(self):
return SegmentRoute([self])
def _get_points(self):
points = [self.to_point]
first = self.from_point
current = self.to_point
while current != first:
current = self.router.predecessors[first, current]
points.append(current)
return tuple(reversed(points))
@abstractmethod
def get_connections(self):
pass
@cached_property
def distance(self):
return self.router.shortest_paths[self.from_point, self.to_point]
class RoomRouteSegment(RouteSegment):
def __init__(self, room, routers, from_point, to_point):
"""
Route segment within a Room
:param room: GraphRoom
"""
super().__init__(routers, routers[room], from_point, to_point)
self.room = room
self.global_from_point = room.points[from_point]
self.global_to_point = room.points[to_point]
def get_connections(self):
points = self._get_points()
return tuple(self.room.get_connection(from_point, to_point)
for from_point, to_point in zip(points[:-1], points[1:]))
def __repr__(self):
return ('<RoomRouteSegment in %r from points %d to %d with distance %f>' %
(self.room, self.from_point, self.to_point, self.distance))
class LevelRouteSegment(RouteSegment):
def __init__(self, level, routers, from_point, to_point):
"""
Route segment within a Level (from room transfer point to room transfer point)
:param level: GraphLevel
"""
super().__init__(routers, routers[level], from_point, to_point)
self.level = level
self.global_from_point = level.room_transfer_points[from_point]
self.global_to_point = level.room_transfer_points[to_point]
def split(self):
segments = []
points = self._get_points()
for from_point, to_point in zip(points[:-1], points[1:]):
room = self.level.rooms[self.router.room_transfers[from_point, to_point]]
global_from_point = self.level.room_transfer_points[from_point]
global_to_point = self.level.room_transfer_points[to_point]
segments.append(RoomRouteSegment(room, self.routers,
from_point=room.points.index(global_from_point),
to_point=room.points.index(global_to_point)))
return tuple(segments)
def get_connections(self):
return sum((segment.get_connections() for segment in self.split()), ())
def __repr__(self):
return ('<LevelRouteSegment in %r from points %d to %d with distance %f>' %
(self.level, self.from_point, self.to_point, self.distance))
class GraphRouteSegment(RouteSegment):
def __init__(self, graph, routers, from_point, to_point):
"""
Route segment within a Graph (from level transfer point to level transfer point)
:param graph: Graph
"""
super().__init__(routers, routers[graph], from_point, to_point)
self.graph = graph
self.global_from_point = graph.level_transfer_points[from_point]
self.global_to_point = graph.level_transfer_points[to_point]
def split(self):
segments = []
points = self._get_points()
for from_point, to_point in zip(points[:-1], points[1:]):
level = self.graph.levels[self.router.level_transfers[from_point, to_point]]
global_from_point = self.graph.level_transfer_points[from_point]
global_to_point = self.graph.level_transfer_points[to_point]
segments.append(LevelRouteSegment(level, self.routers,
from_point=level.room_transfer_points.index(global_from_point),
to_point=level.room_transfer_points.index(global_to_point)))
return tuple(segments)
def get_connections(self):
return sum((segment.get_connections() for segment in self.split()), ())
def __repr__(self):
return ('<GraphRouteSegment in %r from points %d to %d with distance %f>' %
(self.graph, self.from_point, self.to_point, self.distance))
class SegmentRoute:
def __init__(self, segments, distance=None):
self.segments = sum(((item.segments if isinstance(item, SegmentRoute) else (item,))
for item in segments if item.from_point != item.to_point), ())
self.distance = sum(segment.distance for segment in self.segments)
self.from_point = segments[0].global_from_point
self.to_point = segments[-1].global_to_point
def __repr__(self):
return ('<SegmentedRoute (\n %s\n) distance=%f>' %
('\n '.join(repr(segment) for segment in self.segments), self.distance))
def split(self):
return Route(sum((segment.get_connections() for segment in self.segments), ()))
class Route: class Route:
@ -147,6 +15,73 @@ class Route:
return ('<Route (\n %s\n) distance=%f>' % return ('<Route (\n %s\n) distance=%f>' %
('\n '.join(repr(connection) for connection in self.connections), self.distance)) ('\n '.join(repr(connection) for connection in self.connections), self.distance))
@cached_property
def routeparts(self):
routeparts = []
connections = []
level = self.connections[0].from_point.level
for connection in self.connections:
connections.append(connection)
point = connection.to_point
if point.level and point.level != level:
routeparts.append(RoutePart(level, connections))
level = point.level
connections = []
if connections:
routeparts.append(RoutePart(level, connections))
print(routeparts)
return tuple(routeparts)
class RoutePart:
def __init__(self, level, connections):
self.level = level
self.level_name = level.level.name
self.connections = connections
width, height = get_dimensions()
points = (connections[0].from_point, ) + tuple(connection.to_point for connection in connections)
for point in points:
point.svg_x = point.x * 6
point.svg_y = (height - point.y) * 6
x, y = zip(*((point.svg_x, point.svg_y) for point in points if point.level == level))
self.distance = sum(connection.distance for connection in connections)
# bounds for rendering
self.min_x = min(x) - 20
self.max_x = max(x) + 20
self.min_y = min(y) - 20
self.max_y = max(y) + 20
width = self.max_x - self.min_x
height = self.max_y - self.min_y
if width < 150:
self.min_x -= (10 - width) / 2
self.max_x += (10 - width) / 2
if height < 150:
self.min_y -= (10 - height) / 2
self.max_y += (10 - height) / 2
self.width = self.max_x - self.min_x
self.height = self.max_y - self.min_y
def __str__(self):
return repr(self.__dict__)
class RouteLine:
def __init__(self, from_point, to_point, distance):
self.from_point = from_point
self.to_point = to_point
self.distance = distance
class NoRoute: class NoRoute:
distance = np.inf distance = np.inf

View file

@ -0,0 +1,137 @@
from abc import ABC, abstractmethod
from django.utils.functional import cached_property
from c3nav.routing.route import Route
class RouteSegment(ABC):
def __init__(self, routers, router, from_point, to_point):
"""
:param router: a Router (RoomRouter, GraphRouter, )
:param from_point: in-router index of first point
:param to_point: in-router index of last point
"""
self.routers = routers
self.router = router
self.from_point = int(from_point)
self.to_point = int(to_point)
def as_route(self):
return SegmentRoute([self])
def _get_points(self):
points = [self.to_point]
first = self.from_point
current = self.to_point
while current != first:
current = self.router.predecessors[first, current]
points.append(current)
return tuple(reversed(points))
@abstractmethod
def get_connections(self):
pass
@cached_property
def distance(self):
return self.router.shortest_paths[self.from_point, self.to_point]
class RoomRouteSegment(RouteSegment):
def __init__(self, room, routers, from_point, to_point):
"""
Route segment within a Room
:param room: GraphRoom
"""
super().__init__(routers, routers[room], from_point, to_point)
self.room = room
self.global_from_point = room.points[from_point]
self.global_to_point = room.points[to_point]
def get_connections(self):
points = self._get_points()
return tuple(self.room.get_connection(from_point, to_point)
for from_point, to_point in zip(points[:-1], points[1:]))
def __repr__(self):
return ('<RoomRouteSegment in %r from points %d to %d with distance %f>' %
(self.room, self.from_point, self.to_point, self.distance))
class LevelRouteSegment(RouteSegment):
def __init__(self, level, routers, from_point, to_point):
"""
Route segment within a Level (from room transfer point to room transfer point)
:param level: GraphLevel
"""
super().__init__(routers, routers[level], from_point, to_point)
self.level = level
self.global_from_point = level.room_transfer_points[from_point]
self.global_to_point = level.room_transfer_points[to_point]
def split(self):
segments = []
points = self._get_points()
for from_point, to_point in zip(points[:-1], points[1:]):
room = self.level.rooms[self.router.room_transfers[from_point, to_point]]
global_from_point = self.level.room_transfer_points[from_point]
global_to_point = self.level.room_transfer_points[to_point]
segments.append(RoomRouteSegment(room, self.routers,
from_point=room.points.index(global_from_point),
to_point=room.points.index(global_to_point)))
return tuple(segments)
def get_connections(self):
return sum((segment.get_connections() for segment in self.split()), ())
def __repr__(self):
return ('<LevelRouteSegment in %r from points %d to %d with distance %f>' %
(self.level, self.from_point, self.to_point, self.distance))
class GraphRouteSegment(RouteSegment):
def __init__(self, graph, routers, from_point, to_point):
"""
Route segment within a Graph (from level transfer point to level transfer point)
:param graph: Graph
"""
super().__init__(routers, routers[graph], from_point, to_point)
self.graph = graph
self.global_from_point = graph.level_transfer_points[from_point]
self.global_to_point = graph.level_transfer_points[to_point]
def split(self):
segments = []
points = self._get_points()
for from_point, to_point in zip(points[:-1], points[1:]):
level = self.graph.levels[self.router.level_transfers[from_point, to_point]]
global_from_point = self.graph.level_transfer_points[from_point]
global_to_point = self.graph.level_transfer_points[to_point]
segments.append(LevelRouteSegment(level, self.routers,
from_point=level.room_transfer_points.index(global_from_point),
to_point=level.room_transfer_points.index(global_to_point)))
return tuple(segments)
def get_connections(self):
return sum((segment.get_connections() for segment in self.split()), ())
def __repr__(self):
return ('<GraphRouteSegment in %r from points %d to %d with distance %f>' %
(self.graph, self.from_point, self.to_point, self.distance))
class SegmentRoute:
def __init__(self, segments, distance=None):
self.segments = sum(((item.segments if isinstance(item, SegmentRoute) else (item,))
for item in segments if item.from_point != item.to_point), ())
self.distance = sum(segment.distance for segment in self.segments)
self.from_point = segments[0].global_from_point
self.to_point = segments[-1].global_to_point
def __repr__(self):
return ('<SegmentedRoute (\n %s\n) distance=%f>' %
('\n '.join(repr(segment) for segment in self.segments), self.distance))
def split(self):
return Route(sum((segment.get_connections() for segment in self.segments), ()))

View file

@ -90,3 +90,22 @@ body {
.tt-suggestion p { .tt-suggestion p {
margin: 0; margin: 0;
} }
svg {
width:100%;
height:auto;
vertical-align:bottom;
}
line {
stroke:#FF0000;
stroke-width:2.5px;
}
marker path {
fill: #FF0000;
stroke: 0;
}
circle.pos {
fill:#3399FF;
stroke-width:10px;
stroke:rgba(51, 153, 255, 0.2);
}

View file

@ -2,6 +2,7 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% load route_render %}
{% block content %} {% block content %}
<form> <form>
@ -12,6 +13,34 @@
{% trans "Destination" as heading %} {% trans "Destination" as heading %}
{% include 'site/fragment_location.html' with name='destination' location=destination heading=heading %} {% include 'site/fragment_location.html' with name='destination' location=destination heading=heading %}
</div> </div>
</form> </form>
{% if route %}
<h2>Your Route</h2>
<div class="routeparts">
{% for routepart in route.routeparts %}
<svg xmlns="http://www.w3.org/2000/svg" class="map" data-level="{{ routepart.level_name }}"
viewBox="0 0 {{ routepart.width }} {{ routepart.height }}"
style="max-height:{% if routepart.height > 300 %}300{% else %}{{ routepart.height }}{% endif %}px">
<defs>
<marker id="arrow-{{ forloop.counter0 }}" markerWidth="4" markerHeight="4" refX="2.5" refY="2" orient="auto">
<path d="M0,0 L3,2 L0,4 L0,0" style="fill: #FF0000; stroke: 0;"></path>
</marker>
</defs>
<image width="{{ svg_width }}" height="{{ svg_height }}" x="{{ routepart.min_x | negate }}" y="{{ routepart.min_y | negate }}" xlink:href="/map/{{ routepart.level_name }}.png"></image>
<g class="connections">
{% for c in routepart.connections %}
<line x1="{{ c.from_point.svg_x | subtract:routepart.min_x }}"
y1="{{ c.from_point.svg_y | subtract:routepart.min_y }}"
x2="{{ c.to_point.svg_x | subtract:routepart.min_x }}"
y2="{{ c.to_point.svg_y | subtract:routepart.min_y }}"
marker-end="url(#arrow-{{ forloop.parentloop.counter0 }})"></line>
{% endfor %}
</g>
</svg>
{% endfor %}
</div>
{% endif %}
{% endblock %} {% endblock %}

View file

View file

@ -0,0 +1,13 @@
from django import template
register = template.Library()
@register.filter
def negate(value):
return -value
@register.filter
def subtract(value, arg):
return value - arg

View file

@ -1,8 +1,9 @@
from django.conf.urls import url from django.conf.urls import url
from c3nav.site.views import main from c3nav.site.views import level_image, main
urlpatterns = [ urlpatterns = [
url(r'^map/(?P<level>[a-z0-9-_:]+).png$', level_image, name='site.level_image'),
url(r'^(?P<origin>[a-z0-9-_:]+)/$', main, name='site.main'), url(r'^(?P<origin>[a-z0-9-_:]+)/$', main, name='site.main'),
url(r'^_/(?P<destination>[a-z0-9-_:]+)/$', main, name='site.main'), url(r'^_/(?P<destination>[a-z0-9-_:]+)/$', main, name='site.main'),
url(r'^(?P<origin>[a-z0-9-_:]+)/(?P<destination>[a-z0-9-_:]+)/$', main, name='site.main'), url(r'^(?P<origin>[a-z0-9-_:]+)/(?P<destination>[a-z0-9-_:]+)/$', main, name='site.main'),

View file

@ -1,7 +1,16 @@
from django.shortcuts import redirect, render import os
from django.conf import settings
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from PIL import Image, ImageDraw
from c3nav.mapdata.models import Level
from c3nav.mapdata.models.locations import get_location from c3nav.mapdata.models.locations import get_location
from c3nav.mapdata.render.compose import composer
from c3nav.mapdata.utils.misc import get_dimensions
from c3nav.routing.graph import Graph from c3nav.routing.graph import Graph
from c3nav.routing.utils.draw import _line_coords
def main(request, origin=None, destination=None): def main(request, origin=None, destination=None):
@ -30,13 +39,39 @@ def main(request, origin=None, destination=None):
redirect(new_url) redirect(new_url)
route = None
if origin and destination: if origin and destination:
graph = Graph.load() graph = Graph.load()
route = graph.get_route(origin, destination) route = graph.get_route(origin, destination)
route = route.split()
print(route)
print(route) if False:
filename = os.path.join(settings.RENDER_ROOT, 'base-level-0.png')
im = Image.open(filename)
height = im.size[1]
draw = ImageDraw.Draw(im)
for connection in route.connections:
draw.line(_line_coords(connection.from_point, connection.to_point, height), fill=(255, 100, 100))
response = HttpResponse(content_type="image/png")
im.save(response, "PNG")
return response
width, height = get_dimensions()
return render(request, 'site/main.html', { return render(request, 'site/main.html', {
'origin': origin, 'origin': origin,
'destination': destination 'destination': destination,
'route': route,
'width': width,
'height': height,
'svg_width': width*6,
'svg_height': height*6,
}) })
def level_image(request, level):
level = get_object_or_404(Level, name=level, intermediate=False)
return composer.get_level_image(request, level)