diff --git a/src/c3nav/mapdata/render/compose.py b/src/c3nav/mapdata/render/compose.py index 59cb6ae4..3fec3d8e 100644 --- a/src/c3nav/mapdata/render/compose.py +++ b/src/c3nav/mapdata/render/compose.py @@ -1,11 +1,40 @@ +import os + from django.http import HttpResponse 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): - im = Image.open(get_render_path('png', level.name, 'full', True)) - response = HttpResponse(content_type="image/png") - im.save(response, 'PNG') - return response +class LevelComposer: + images = {} + images_mtimes = {} + + @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() diff --git a/src/c3nav/mapdata/render/renderer.py b/src/c3nav/mapdata/render/renderer.py index 5e6d5c24..54234bdd 100644 --- a/src/c3nav/mapdata/render/renderer.py +++ b/src/c3nav/mapdata/render/renderer.py @@ -5,7 +5,7 @@ from django.conf import settings from shapely.affinity import scale 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(): @@ -21,11 +21,6 @@ class LevelRenderer(): 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) - @staticmethod - def get_dimensions(): - width, height = get_dimensions() - return (width * settings.RENDER_SCALE, height * settings.RENDER_SCALE) - @staticmethod def polygon_svg(geometry, fill_color=None, fill_opacity=None, stroke_width=0.0, stroke_color=None, stroke_opacity=None): @@ -62,7 +57,7 @@ class LevelRenderer(): return element def create_svg(self): - width, height = self.get_dimensions() + width, height = get_render_dimensions() svg = ET.Element('svg', { 'width': str(width), 'height': str(height), @@ -73,7 +68,7 @@ class LevelRenderer(): return svg def add_svg_content(self, svg): - width, height = self.get_dimensions() + width, height = get_render_dimensions() contents = ET.Element('g', { 'transform': 'scale(1 -1) translate(0 -%d)' % (height), }) @@ -81,7 +76,7 @@ class LevelRenderer(): return contents def add_svg_image(self, svg, image): - width, height = self.get_dimensions() + width, height = get_render_dimensions() contents = ET.Element('image', { 'x': '0', 'y': '0', diff --git a/src/c3nav/mapdata/render/utils.py b/src/c3nav/mapdata/utils/misc.py similarity index 67% rename from src/c3nav/mapdata/render/utils.py rename to src/c3nav/mapdata/utils/misc.py index 7b3f84ed..a1668b80 100644 --- a/src/c3nav/mapdata/render/utils.py +++ b/src/c3nav/mapdata/utils/misc.py @@ -4,8 +4,10 @@ from django.conf import settings from django.db.models import Max, Min from c3nav.mapdata.models import Package +from c3nav.mapdata.utils.cache import cache_result +@cache_result('c3nav__mapdata__dimensions') def get_dimensions(): aggregate = Package.objects.all().aggregate(Max('right'), Min('left'), Max('top'), Min('bottom')) 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): return os.path.join(settings.RENDER_ROOT, '%s%s-level-%s.%s' % (('public-' if public else ''), mode, level, filetype)) diff --git a/src/c3nav/routing/connection.py b/src/c3nav/routing/connection.py index a0c4410b..8528978c 100644 --- a/src/c3nav/routing/connection.py +++ b/src/c3nav/routing/connection.py @@ -6,3 +6,6 @@ class GraphConnection(): self.from_point = from_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)) + + def __repr__(self): + return '' % (self.from_point, self.to_point, self.distance) diff --git a/src/c3nav/routing/graph.py b/src/c3nav/routing/graph.py index 7dba9e2e..a7494fba 100644 --- a/src/c3nav/routing/graph.py +++ b/src/c3nav/routing/graph.py @@ -12,7 +12,8 @@ from c3nav.mapdata.models.geometry import LevelConnector from c3nav.mapdata.models.locations import AreaLocation, Location, LocationGroup, PointLocation from c3nav.routing.level import GraphLevel 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: diff --git a/src/c3nav/routing/point.py b/src/c3nav/routing/point.py index 32273ee9..d42ee3e2 100644 --- a/src/c3nav/routing/point.py +++ b/src/c3nav/routing/point.py @@ -15,6 +15,10 @@ class GraphPoint(): self.connections = {} self.connections_in = {} + @cached_property + def level(self): + return self.room and self.room.level + def serialize(self): return ( self.x, @@ -32,3 +36,6 @@ class GraphPoint(): connection = GraphConnection(self, other_point) self.connections[other_point] = connection other_point.connections_in[self] = connection + + def __repr__(self): + return '' % (self.x, self.y, (id(self.room) if self.room else None)) diff --git a/src/c3nav/routing/route.py b/src/c3nav/routing/route.py index 8d765152..21a7c326 100644 --- a/src/c3nav/routing/route.py +++ b/src/c3nav/routing/route.py @@ -1,139 +1,7 @@ -from abc import ABC, abstractmethod - import numpy as np from django.utils.functional import cached_property - -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 ('' % - (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 ('' % - (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 ('' % - (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 ('' % - ('\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), ())) +from c3nav.mapdata.utils.misc import get_dimensions class Route: @@ -147,6 +15,73 @@ class Route: return ('' % ('\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: distance = np.inf diff --git a/src/c3nav/routing/routesegments.py b/src/c3nav/routing/routesegments.py new file mode 100644 index 00000000..d2901099 --- /dev/null +++ b/src/c3nav/routing/routesegments.py @@ -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 ('' % + (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 ('' % + (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 ('' % + (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 ('' % + ('\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), ())) diff --git a/src/c3nav/site/static/site/css/c3nav.css b/src/c3nav/site/static/site/css/c3nav.css index d271d255..5b507f37 100644 --- a/src/c3nav/site/static/site/css/c3nav.css +++ b/src/c3nav/site/static/site/css/c3nav.css @@ -90,3 +90,22 @@ body { .tt-suggestion p { 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); +} diff --git a/src/c3nav/site/templates/site/main.html b/src/c3nav/site/templates/site/main.html index 9f6f1a33..2585b605 100644 --- a/src/c3nav/site/templates/site/main.html +++ b/src/c3nav/site/templates/site/main.html @@ -2,6 +2,7 @@ {% load static %} {% load i18n %} +{% load route_render %} {% block content %}
@@ -12,6 +13,34 @@ {% trans "Destination" as heading %} {% include 'site/fragment_location.html' with name='destination' location=destination heading=heading %} -
+ + {% if route %} +

Your Route

+
+ {% for routepart in route.routeparts %} + + + + + + + + + + + {% for c in routepart.connections %} + + {% endfor %} + + + {% endfor %} +
+ {% endif %} {% endblock %} diff --git a/src/c3nav/site/templatetags/__init__.py b/src/c3nav/site/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/c3nav/site/templatetags/route_render.py b/src/c3nav/site/templatetags/route_render.py new file mode 100644 index 00000000..48eca2b3 --- /dev/null +++ b/src/c3nav/site/templatetags/route_render.py @@ -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 diff --git a/src/c3nav/site/urls.py b/src/c3nav/site/urls.py index 3938d67c..70c40201 100644 --- a/src/c3nav/site/urls.py +++ b/src/c3nav/site/urls.py @@ -1,8 +1,9 @@ from django.conf.urls import url -from c3nav.site.views import main +from c3nav.site.views import level_image, main urlpatterns = [ + url(r'^map/(?P[a-z0-9-_:]+).png$', level_image, name='site.level_image'), url(r'^(?P[a-z0-9-_:]+)/$', main, name='site.main'), url(r'^_/(?P[a-z0-9-_:]+)/$', main, name='site.main'), url(r'^(?P[a-z0-9-_:]+)/(?P[a-z0-9-_:]+)/$', main, name='site.main'), diff --git a/src/c3nav/site/views.py b/src/c3nav/site/views.py index 342effde..c6cc2142 100644 --- a/src/c3nav/site/views.py +++ b/src/c3nav/site/views.py @@ -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.render.compose import composer +from c3nav.mapdata.utils.misc import get_dimensions from c3nav.routing.graph import Graph +from c3nav.routing.utils.draw import _line_coords def main(request, origin=None, destination=None): @@ -30,13 +39,39 @@ def main(request, origin=None, destination=None): redirect(new_url) + route = None if origin and destination: graph = Graph.load() 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', { '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)