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) result = super().serialize(**kwargs)
return result return result
def _serialize(self, **kwargs): def _serialize(self, detailed=True, **kwargs):
result = super()._serialize(**kwargs) result = super()._serialize(detailed=detailed, **kwargs)
result['titles'] = self.titles if detailed:
result['titles'] = self.titles
result['title'] = self.title result['title'] = self.title
return result return result

View file

@ -1,308 +1,48 @@
# flake8: noqa # flake8: noqa
import copy import copy
from collections import OrderedDict from collections import OrderedDict, deque
import numpy as np import numpy as np
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
class Route: class Route:
def __init__(self, connections, distance=None): def __init__(self, router, origin, destination, distance, path_nodes):
self.connections = tuple(connections) self.router = router
self.distance = sum(connection.distance for connection in self.connections) self.origin = origin
self.from_point = connections[0].from_point self.destination = destination
self.to_point = connections[-1].to_point self.distance = distance
self.path_nodes = path_nodes
self.ctypes_exception = None
self.routeparts = None
def serialize(self): 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(( return OrderedDict((
('distance', float(self.distance)), ('items', tuple(item.serialize() for item in items)),
('routeparts', [routepart.serialize() for routepart in self.routeparts]),
)) ))
def __repr__(self):
return ('<Route (\n %s\n) distance=%f>' %
('\n '.join(repr(connection) for connection in self.connections), self.distance))
def split(self): class RouteItem:
return self def __init__(self, route, node, edge, last_item):
self.route = route
def create_routeparts(self): self.node = node
routeparts = [] self.edge = edge
connections = [] self.last_item = last_item
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
def serialize(self): def serialize(self):
return OrderedDict(( return OrderedDict((
('level', self.level.name), ('id', self.node.pk),
('lines', [line.serialize() for line in self.lines]), ('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)),
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),
)) ))

View file

@ -1,25 +1,30 @@
import operator
import os import os
import pickle import pickle
from collections import deque from collections import deque
from functools import reduce
import numpy as np import numpy as np
from django.conf import settings from django.conf import settings
from django.utils.functional import cached_property from django.utils.functional import cached_property
from scipy.sparse.csgraph._shortest_path import shortest_path
from shapely import prepared from shapely import prepared
from shapely.geometry import Point from shapely.geometry import Point
from shapely.ops import unary_union 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: class Router:
filename = os.path.join(settings.CACHE_ROOT, '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.levels = levels
self.spaces = spaces self.spaces = spaces
self.areas = areas self.areas = areas
self.nodes = nodes self.nodes = nodes
self.edges = edges
self.waytypes = waytypes self.waytypes = waytypes
self.graph = graph self.graph = graph
@ -56,7 +61,8 @@ class Router:
) )
# todo: do something with this, then remove #noqa # 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)): for i, node in enumerate(space_nodes, start=len(nodes)):
node.i = i node.i = i
nodes.extend(space_nodes) nodes.extend(space_nodes)
@ -113,12 +119,12 @@ class Router:
edges = tuple(RouterEdge(from_node=nodes[nodes_lookup[edge.from_node_id]], edges = tuple(RouterEdge(from_node=nodes[nodes_lookup[edge.from_node_id]],
to_node=nodes[nodes_lookup[edge.to_node_id]], to_node=nodes[nodes_lookup[edge.to_node_id]],
waytype=waytypes_lookup[edge.waytype_id]) for edge in GraphEdge.objects.all()) 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 # todo: remove #noqa when we're ready
# build graph matrix # build graph matrix
graph = np.full(shape=(len(nodes), len(nodes)), fill_value=np.inf, dtype=np.float32) 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) index = (edge.from_node.i, edge.to_node.i)
graph[index] = edge.distance graph[index] = edge.distance
waytype = waytypes[edge.waytype] 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.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)) 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')) pickle.dump(router, open(cls.filename, 'wb'))
return router return router
@ -137,6 +143,41 @@ class Router:
def load(cls): def load(cls):
return pickle.load(open(cls.filename, 'rb')) 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: class BaseRouterProxy:
def __init__(self, src): def __init__(self, src):
@ -197,7 +238,8 @@ class RouterAltitudeArea:
class RouterNode: 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.pk = pk
self.x = x self.x = x
self.y = y self.y = y
@ -206,8 +248,8 @@ class RouterNode:
self.areas = areas if areas else set() self.areas = areas if areas else set()
@classmethod @classmethod
def from_graph_node(cls, node): def from_graph_node(cls, node, i):
return cls(node.pk, node.geometry.x, node.geometry.y, node.space_id) return cls(i, node.pk, node.geometry.x, node.geometry.y, node.space_id)
@cached_property @cached_property
def point(self): def point(self):
@ -229,6 +271,26 @@ class RouterEdge:
class RouterWayType: class RouterWayType:
def __init__(self, waytype): def __init__(self, waytype):
self.waytype = waytype self.src = waytype
self.upwards_indices = deque() self.upwards_indices = deque()
self.nonupwards_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