getting a route

This commit is contained in:
Laura Klünder 2017-11-27 15:01:58 +01:00
parent 4c611995b8
commit 5f2ca60432
3 changed files with 104 additions and 301 deletions

View file

@ -60,9 +60,10 @@ class TitledMixin(SerializableMixin, models.Model):
result = super().serialize(**kwargs)
return result
def _serialize(self, **kwargs):
result = super()._serialize(**kwargs)
result['titles'] = self.titles
def _serialize(self, detailed=True, **kwargs):
result = super()._serialize(detailed=detailed, **kwargs)
if detailed:
result['titles'] = self.titles
result['title'] = self.title
return result

View file

@ -1,308 +1,48 @@
# flake8: noqa
import copy
from collections import OrderedDict
from collections import OrderedDict, deque
import numpy as np
from django.utils.translation import ugettext_lazy as _
class Route:
def __init__(self, connections, distance=None):
self.connections = tuple(connections)
self.distance = sum(connection.distance for connection in self.connections)
self.from_point = connections[0].from_point
self.to_point = connections[-1].to_point
self.ctypes_exception = None
self.routeparts = None
def __init__(self, router, origin, destination, distance, path_nodes):
self.router = router
self.origin = origin
self.destination = destination
self.distance = distance
self.path_nodes = path_nodes
def serialize(self):
items = deque()
last_node = None
last_item = None
for i, node in enumerate(self.path_nodes):
item = RouteItem(self, self.router.nodes[node],
self.router.edges[last_node, node] if last_node else None,
last_item)
items.append(item)
last_item = item
last_node = node
return OrderedDict((
('distance', float(self.distance)),
('routeparts', [routepart.serialize() for routepart in self.routeparts]),
('items', tuple(item.serialize() for item in items)),
))
def __repr__(self):
return ('<Route (\n %s\n) distance=%f>' %
('\n '.join(repr(connection) for connection in self.connections), self.distance))
def split(self):
return self
def create_routeparts(self):
routeparts = []
connections = []
add_connections = []
level = self.connections[0].from_point.level
for connection in self.connections:
connections.append(RouteLine(connection))
point = connection.to_point
if point.level and point.level != level:
if routeparts:
routeparts[-1].lines.extend(connections[:1])
routeparts.append(RoutePart(level, add_connections+connections))
level = point.level
add_connections = [copy.copy(line) for line in connections[-3:]]
connections = []
if connections or add_connections:
if routeparts:
routeparts[-1].lines.extend(connections[:1])
routeparts.append(RoutePart(level, add_connections+connections))
routeparts = [routepart for routepart in routeparts if not routepart.level.intermediate]
for routepart in routeparts:
routepart.render_svg_coordinates()
self.routeparts = routeparts
@staticmethod
def describe_point(point):
locations = sorted(AreaLocation.objects.filter(location_type__in=('room', 'level', 'area'),
name__in=point.arealocations, can_describe=True),
key=AreaLocation.get_sort_key, reverse=True)
if not locations:
return _('Unknown Location'), _('Unknown Location')
elif locations[0].location_type == 'level':
return _('Unknown Location'), locations[0].title
else:
return locations[0].title, locations[0].subtitle
def describe(self, allowed_ctypes):
self.create_routeparts()
for i, routepart in enumerate(self.routeparts):
for j, line in enumerate(routepart.lines):
from_room = line.from_point.room
to_room = line.to_point.room
if i and not j:
line.ignore = True
line.turning = ''
if j:
line.angle_change = (line.angle - routepart.lines[j - 1].angle + 180) % 360 - 180
if 20 < line.angle_change <= 75:
line.turning = 'light_right'
elif -75 <= line.angle_change < -20:
line.turning = 'light_left'
elif 75 < line.angle_change:
line.turning = 'right'
elif line.angle_change < -75:
line.turning = 'left'
line.icon = line.ctype or line.turning
distance = line.distance
if from_room is None:
line.arrow = True
if j+1 < len(routepart.lines) and routepart.lines[j+1].ctype_main == 'elevator':
line.ignore = True
elif j > 0 and (routepart.lines[j-1].ignore or routepart.lines[j-1].ctype_main == 'elevator'):
line.ignore = True
else:
line.icon = 'location'
line.title, line.description = self.describe_point(line.to_point)
elif line.ctype_main in ('stairs', 'escalator', 'elevator'):
line.description = {
'stairs_up': _('Go up the stairs.'),
'stairs_down': _('Go down the stairs.'),
'escalator_up': _('Take the escalator upwards.'),
'escalator_down': _('Take the escalator downwards.'),
'elevator_up': _('Take the elevator upwards.'),
'elevator_down': _('Take the elevator downwards.')
}.get(line.ctype)
if line.ctype_main == 'elevator':
if from_room is None or (to_room is None and from_room.level.level != routepart.level):
line.ignore = True
line.arrow = False
elif to_room is None:
if from_room is not None and from_room.level.level.intermediate:
line.ignore = True
if j > 0:
if routepart.lines[j-1].ctype_main == 'elevator':
line.arrow = False
line.ignore = True
if j+1 < len(routepart.lines):
if routepart.lines[j+1].to_point.room.level.level.intermediate:
line.ignore = True
if j+2 < len(routepart.lines):
if routepart.lines[j+2].ctype_main == 'elevator':
line.ignore = True
line.description = {
'left': _('Go through the door on the left.'),
'right': _('Go through the door on the right.'),
}.get(line.turning.split('_')[-1], _('Go through the door.'))
line.arrow = False
else:
if j > 0:
last = routepart.lines[j-1]
if last.can_merge_to_next:
if last.turning == '' and (line.turning == '' or last.desc_distance < 1):
last.ignore = True
last.arrow = False
distance += last.desc_distance
elif last.turning.endswith('right') and line.turning.endswith('right'):
last.ignore = True
last.arrow = False
line.turning = 'right'
distance += last.desc_distance
elif last.turning.endswith('left') and line.turning.endswith('left'):
last.ignore = True
last.arrow = False
line.turning = 'left'
distance += last.desc_distance
elif line.turning == '':
last.ignore = True
last.arrow = False
line.turning = last.turning
distance += last.desc_distance
line.description = {
'light_left': _('Turn light to the left and continue for %(d).1f meters.') % {'d': distance},
'light_right': _('Turn light to the right and continue for %(d).1f meters.') % {'d': distance},
'left': _('Turn left and continue for %(d).1f meters.') % {'d': distance},
'right': _('Turn right and continue for %(d).1f meters.') % {'d': distance}
}.get(line.turning, _('Continue for %(d).1f meters.') % {'d': distance})
if distance < 0.2:
line.ignore = True
line.can_merge_to_next = True
line.desc_distance = distance
# line.ignore = False
if line.ignore:
line.icon = None
line.description = None
line.desc_distance = None
line.can_merge_to_next = False
if line.arrow is None:
line.arrow = not line.ignore
last_lines = [line for line in routepart.lines if line.ctype_main != 'elevator']
if len(last_lines) > 1:
last_lines[-1].arrow = True
unignored_lines = [i for i, line in enumerate(routepart.lines) if line.description]
if unignored_lines:
first_unignored_i = unignored_lines[0]
if first_unignored_i > 0:
first_unignored = routepart.lines[first_unignored_i]
point = first_unignored.from_point if first_unignored.from_point.room else first_unignored.to_point
line = routepart.lines[first_unignored_i-1]
line.ignore = False
line.icon = 'location'
line.title, line.description = self.describe_point(point)
last_line = self.routeparts[-1].lines[-1]
if last_line.icon == 'location':
last_line.ignore = True
# check allowed ctypes
allowed_ctypes = set(allowed_ctypes)
self.ctypes_exception = False
for connection in self.connections:
if connection.ctype not in allowed_ctypes:
self.ctypes_exception = True
break
class RoutePart:
def __init__(self, graphlevel, lines):
self.graphlevel = graphlevel
self.level = graphlevel.level
self.lines = lines
class RouteItem:
def __init__(self, route, node, edge, last_item):
self.route = route
self.node = node
self.edge = edge
self.last_item = last_item
def serialize(self):
return OrderedDict((
('level', self.level.name),
('lines', [line.serialize() for line in self.lines]),
))
def render_svg_coordinates(self):
svg_width, svg_height = get_dimensions()
points = (self.lines[0].from_point,) + tuple(connection.to_point for connection in self.lines)
for point in points:
point.svg_x = point.x * 6
point.svg_y = (svg_height - point.y) * 6
x, y = zip(*((point.svg_x, point.svg_y) for point in points if point.level == self.graphlevel))
self.distance = sum(connection.distance for connection in self.lines)
# bounds for rendering
self.svg_min_x = min(x) - 20
self.svg_max_x = max(x) + 20
self.svg_min_y = min(y) - 20
self.svg_max_y = max(y) + 20
svg_width = self.svg_max_x - self.svg_min_x
svg_height = self.svg_max_y - self.svg_min_y
if svg_width < 150:
self.svg_min_x -= (10 - svg_width) / 2
self.svg_max_x += (10 - svg_width) / 2
if svg_height < 150:
self.svg_min_y += (10 - svg_height) / 2
self.svg_max_y -= (10 - svg_height) / 2
self.svg_width = self.svg_max_x - self.svg_min_x
self.svg_height = self.svg_max_y - self.svg_min_y
def __str__(self):
return repr(self.__dict__)
class RouteLine:
def __init__(self, connection):
self.from_point = connection.from_point
self.to_point = connection.to_point
self.distance = connection.distance
self.ctype = connection.ctype
self.angle = connection.angle
self.ctype_main = self.ctype.split('_')[0]
self.ctype_direction = self.ctype.split('_')[-1]
self.ignore = False
self.arrow = None
self.angle_change = None
self.can_merge_to_next = False
self.icon = None
self.title = None
self.description = None
def serialize(self):
return OrderedDict((
('from_point', tuple(self.from_point.xy)),
('to_point', tuple(self.to_point.xy)),
('distance', float(self.distance)),
('icon', self.icon),
('ignore', self.ignore),
('title', self.title),
('description', self.description),
('id', self.node.pk),
('coords', (self.node.x, self.node.y, self.node.altitude)),
('waytype', (self.route.router.waytypes[self.edge.waytype].serialize(detailed=False)
if self.edge and self.edge.waytype else None)),
))

View file

@ -1,25 +1,30 @@
import operator
import os
import pickle
from collections import deque
from functools import reduce
import numpy as np
from django.conf import settings
from django.utils.functional import cached_property
from scipy.sparse.csgraph._shortest_path import shortest_path
from shapely import prepared
from shapely.geometry import Point
from shapely.ops import unary_union
from c3nav.mapdata.models import AltitudeArea, GraphEdge, Level, WayType
from c3nav.mapdata.models import AltitudeArea, GraphEdge, Level, Space, WayType
from c3nav.routing.route import Route
class Router:
filename = os.path.join(settings.CACHE_ROOT, 'router')
def __init__(self, levels, spaces, areas, nodes, waytypes, graph):
def __init__(self, levels, spaces, areas, nodes, edges, waytypes, graph):
self.levels = levels
self.spaces = spaces
self.areas = areas
self.nodes = nodes
self.edges = edges
self.waytypes = waytypes
self.graph = graph
@ -56,7 +61,8 @@ class Router:
)
# todo: do something with this, then remove #noqa
space_nodes = tuple(RouterNode.from_graph_node(node) for node in space.graphnodes.all())
space_nodes = tuple(RouterNode.from_graph_node(node, i)
for i, node in enumerate(space.graphnodes.all()))
for i, node in enumerate(space_nodes, start=len(nodes)):
node.i = i
nodes.extend(space_nodes)
@ -113,12 +119,12 @@ class Router:
edges = tuple(RouterEdge(from_node=nodes[nodes_lookup[edge.from_node_id]],
to_node=nodes[nodes_lookup[edge.to_node_id]],
waytype=waytypes_lookup[edge.waytype_id]) for edge in GraphEdge.objects.all())
edges_lookup = {(edge.from_node.i, edge.to_node.i): edge for edge in edges} # noqa
edges = {(edge.from_node.i, edge.to_node.i): edge for edge in edges} # noqa
# todo: remove #noqa when we're ready
# build graph matrix
graph = np.full(shape=(len(nodes), len(nodes)), fill_value=np.inf, dtype=np.float32)
for edge in edges:
for edge in edges.values():
index = (edge.from_node.i, edge.to_node.i)
graph[index] = edge.distance
waytype = waytypes[edge.waytype]
@ -129,7 +135,7 @@ class Router:
waytype.upwards_indices = np.array(waytype.upwards_indices, dtype=np.uint32).reshape((-1, 2))
waytype.nonupwards_indices = np.array(waytype.nonupwards_indices, dtype=np.uint32).reshape((-1, 2))
router = cls(levels, spaces, areas, nodes, waytypes, graph)
router = cls(levels, spaces, areas, nodes, edges, waytypes, graph)
pickle.dump(router, open(cls.filename, 'wb'))
return router
@ -137,6 +143,41 @@ class Router:
def load(cls):
return pickle.load(open(cls.filename, 'rb'))
def get_locations(self, location):
if isinstance(location, Space):
return RouterLocation((self.spaces[location.pk], ))
return RouterLocation()
def get_route(self, origin, destination):
# get possible origins and destinations
origins = self.get_locations(origin)
destinations = self.get_locations(destination)
# calculate shortest path matrix
distances, predecessors = shortest_path(self.graph, directed=True, return_predecessors=True)
# find shortest path for our origins and destinations
origin_nodes = np.array(tuple(origins.nodes))
destination_nodes = np.array(tuple(destinations.nodes))
origin_node, destination_node = np.unravel_index(distances[origin_nodes, destination_nodes].argmin(),
(len(origin_nodes), len(destination_nodes)))
origin_node = origin_nodes[origin_node]
destination_node = destination_nodes[destination_node]
# get best origin and destination
origin = origins.get_location_for_node(origin_node)
destination = destinations.get_location_for_node(destination_node)
# recreate path
path_nodes = deque((destination_node, ))
last_node = destination_node
while last_node != origin_node:
last_node = predecessors[origin_node, last_node]
path_nodes.appendleft(last_node)
path_nodes = tuple(path_nodes)
return Route(self, origin, destination, distances[origin_node, destination_node], path_nodes)
class BaseRouterProxy:
def __init__(self, src):
@ -197,7 +238,8 @@ class RouterAltitudeArea:
class RouterNode:
def __init__(self, pk, x, y, space, altitude=None, areas=None):
def __init__(self, i, pk, x, y, space, altitude=None, areas=None):
self.i = i
self.pk = pk
self.x = x
self.y = y
@ -206,8 +248,8 @@ class RouterNode:
self.areas = areas if areas else set()
@classmethod
def from_graph_node(cls, node):
return cls(node.pk, node.geometry.x, node.geometry.y, node.space_id)
def from_graph_node(cls, node, i):
return cls(i, node.pk, node.geometry.x, node.geometry.y, node.space_id)
@cached_property
def point(self):
@ -229,6 +271,26 @@ class RouterEdge:
class RouterWayType:
def __init__(self, waytype):
self.waytype = waytype
self.src = waytype
self.upwards_indices = deque()
self.nonupwards_indices = deque()
def __getattr__(self, name):
if name == '__setstate__':
raise AttributeError
return getattr(self.src, name)
class RouterLocation:
def __init__(self, locations=()):
self.locations = locations
@cached_property
def nodes(self):
return reduce(operator.or_, (location.nodes for location in self.locations), frozenset())
def get_location_for_node(self, node):
for location in self.locations:
if node in location.nodes:
return location
return None