refactor c3nav.routing
This commit is contained in:
parent
575af6c06e
commit
834d6f0064
13 changed files with 381 additions and 364 deletions
|
@ -1,236 +0,0 @@
|
||||||
import os
|
|
||||||
from itertools import combinations, permutations
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.functional import cached_property
|
|
||||||
from matplotlib.path import Path
|
|
||||||
from PIL import Image, ImageDraw
|
|
||||||
from shapely.geometry import JOIN_STYLE, LineString, Polygon
|
|
||||||
|
|
||||||
from c3nav.mapdata.models import Level
|
|
||||||
from c3nav.routing.utils import assert_multipolygon, get_coords_angles, get_nearest_point, polygon_to_mpl_paths
|
|
||||||
|
|
||||||
|
|
||||||
class GraphLevel():
|
|
||||||
def __init__(self, graph, level):
|
|
||||||
self.graph = graph
|
|
||||||
self.level = level
|
|
||||||
self.rooms = []
|
|
||||||
|
|
||||||
def build(self):
|
|
||||||
self.collect_rooms()
|
|
||||||
self.create_points()
|
|
||||||
|
|
||||||
def collect_rooms(self):
|
|
||||||
accessibles = self.level.geometries.accessible
|
|
||||||
accessibles = [accessibles] if isinstance(accessibles, Polygon) else accessibles.geoms
|
|
||||||
for geometry in accessibles:
|
|
||||||
room = GraphRoom(self, geometry)
|
|
||||||
if not room.empty:
|
|
||||||
self.rooms.append(room)
|
|
||||||
|
|
||||||
def create_points(self):
|
|
||||||
for room in self.rooms:
|
|
||||||
room.create_points()
|
|
||||||
|
|
||||||
doors = self.level.geometries.doors
|
|
||||||
doors = assert_multipolygon(doors)
|
|
||||||
for door in doors:
|
|
||||||
polygon = door.buffer(0.01, join_style=JOIN_STYLE.mitre)
|
|
||||||
center = door.centroid
|
|
||||||
points = []
|
|
||||||
for room in self.rooms:
|
|
||||||
if polygon.intersects(room.geometry):
|
|
||||||
nearest_point = get_nearest_point(room.clear_geometry, center)
|
|
||||||
point = GraphPoint(room, *nearest_point.coords[0])
|
|
||||||
points.append(point)
|
|
||||||
room.points.append(point)
|
|
||||||
|
|
||||||
if len(points) < 2:
|
|
||||||
print('door with <2 rooms (%d) detected!' % len(points))
|
|
||||||
|
|
||||||
for from_point, to_point in permutations(points, 2):
|
|
||||||
from_point.connect_to(to_point)
|
|
||||||
|
|
||||||
for room in self.rooms:
|
|
||||||
room.connect_points()
|
|
||||||
|
|
||||||
def _ellipse_bbox(self, x, y, height):
|
|
||||||
x *= settings.RENDER_SCALE
|
|
||||||
y *= settings.RENDER_SCALE
|
|
||||||
y = height-y
|
|
||||||
return ((x - 2, y - 2), (x + 2, y + 2))
|
|
||||||
|
|
||||||
def _line_coords(self, from_point, to_point, height):
|
|
||||||
return (from_point.x * settings.RENDER_SCALE, height - (from_point.y * settings.RENDER_SCALE),
|
|
||||||
to_point.x * settings.RENDER_SCALE, height - (to_point.y * settings.RENDER_SCALE))
|
|
||||||
|
|
||||||
def draw_png(self):
|
|
||||||
filename = os.path.join(settings.RENDER_ROOT, 'level-%s.png' % self.level.name)
|
|
||||||
graph_filename = os.path.join(settings.RENDER_ROOT, 'level-%s-graph.png' % self.level.name)
|
|
||||||
|
|
||||||
im = Image.open(filename)
|
|
||||||
height = im.size[1]
|
|
||||||
draw = ImageDraw.Draw(im)
|
|
||||||
i = 0
|
|
||||||
for room in self.rooms:
|
|
||||||
for point in room.points:
|
|
||||||
for otherpoint, connection in point.connections.items():
|
|
||||||
draw.line(self._line_coords(point, otherpoint, height), fill=(255, 100, 100))
|
|
||||||
|
|
||||||
for point in room.points:
|
|
||||||
i += 1
|
|
||||||
draw.ellipse(self._ellipse_bbox(point.x, point.y, height), (200, 0, 0))
|
|
||||||
|
|
||||||
print(i, 'points')
|
|
||||||
|
|
||||||
im.save(graph_filename)
|
|
||||||
|
|
||||||
|
|
||||||
class GraphRoom():
|
|
||||||
def __init__(self, level, geometry):
|
|
||||||
self.level = level
|
|
||||||
self.geometry = geometry
|
|
||||||
self.points = []
|
|
||||||
|
|
||||||
self.clear_geometry = geometry.buffer(-0.3, join_style=JOIN_STYLE.mitre)
|
|
||||||
self.empty = self.clear_geometry.is_empty
|
|
||||||
|
|
||||||
if not self.empty:
|
|
||||||
self.mpl_paths = polygon_to_mpl_paths(self.clear_geometry.buffer(0.01, join_style=JOIN_STYLE.mitre))
|
|
||||||
|
|
||||||
def create_points(self):
|
|
||||||
original_geometry = self.geometry
|
|
||||||
geometry = original_geometry.buffer(-0.6, join_style=JOIN_STYLE.mitre)
|
|
||||||
|
|
||||||
if geometry.is_empty:
|
|
||||||
return
|
|
||||||
|
|
||||||
# points with 60cm distance to borders
|
|
||||||
polygons = assert_multipolygon(geometry)
|
|
||||||
for polygon in polygons:
|
|
||||||
self._add_ring(polygon.exterior, want_left=False)
|
|
||||||
|
|
||||||
for interior in polygon.interiors:
|
|
||||||
self._add_ring(interior, want_left=True)
|
|
||||||
|
|
||||||
# now fill in missing doorways or similar
|
|
||||||
missing_geometry = self.clear_geometry.difference(geometry.buffer(0.61, join_style=JOIN_STYLE.mitre))
|
|
||||||
polygons = assert_multipolygon(missing_geometry)
|
|
||||||
for polygon in polygons:
|
|
||||||
overlaps = polygon.buffer(0.62).intersection(geometry)
|
|
||||||
if overlaps.is_empty:
|
|
||||||
continue
|
|
||||||
|
|
||||||
points = []
|
|
||||||
|
|
||||||
# overlaps to non-missing areas
|
|
||||||
overlaps = assert_multipolygon(overlaps)
|
|
||||||
for overlap in overlaps:
|
|
||||||
points.append(self.add_point(overlap.centroid.coords[0]))
|
|
||||||
|
|
||||||
points += self._add_ring(polygon.exterior, want_left=False)
|
|
||||||
|
|
||||||
for interior in polygon.interiors:
|
|
||||||
points += self._add_ring(interior, want_left=True)
|
|
||||||
|
|
||||||
for from_point, to_point in permutations(points, 2):
|
|
||||||
from_point.connect_to(to_point)
|
|
||||||
|
|
||||||
def _add_ring(self, geom, want_left):
|
|
||||||
"""
|
|
||||||
add the points of a ring, but only those that have a specific direction change.
|
|
||||||
additionally removes unneeded points if the neighbors can be connected in self.clear_geometry
|
|
||||||
:param geom: LinearRing
|
|
||||||
:param want_left: True if the direction has to be left, False if it has to be right
|
|
||||||
"""
|
|
||||||
coords = []
|
|
||||||
skipped = False
|
|
||||||
can_delete_last = False
|
|
||||||
for coord, is_left in get_coords_angles(geom):
|
|
||||||
if is_left != want_left:
|
|
||||||
skipped = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not skipped and can_delete_last and len(coords) >= 2:
|
|
||||||
if LineString((coords[-2], coord)).within(self.clear_geometry):
|
|
||||||
coords[-1] = coord
|
|
||||||
continue
|
|
||||||
|
|
||||||
coords.append(coord)
|
|
||||||
can_delete_last = not skipped
|
|
||||||
skipped = False
|
|
||||||
|
|
||||||
if not skipped and can_delete_last and len(coords) >= 3:
|
|
||||||
if LineString((coords[-2], coords[0])).within(self.clear_geometry):
|
|
||||||
coords.pop()
|
|
||||||
|
|
||||||
points = []
|
|
||||||
for coord in coords:
|
|
||||||
points.append(self.add_point(coord))
|
|
||||||
|
|
||||||
return points
|
|
||||||
|
|
||||||
def add_point(self, coord):
|
|
||||||
point = GraphPoint(self, *coord)
|
|
||||||
self.points.append(point)
|
|
||||||
return point
|
|
||||||
|
|
||||||
def connect_points(self):
|
|
||||||
room_paths = self.mpl_paths
|
|
||||||
for point1, point2 in combinations(self.points, 2):
|
|
||||||
path = Path(np.vstack((point1.xy, point2.xy)))
|
|
||||||
for room_path in room_paths:
|
|
||||||
if room_path.intersects_path(path, False):
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
point1.connect_to(point2)
|
|
||||||
point2.connect_to(point1)
|
|
||||||
|
|
||||||
|
|
||||||
class GraphPoint():
|
|
||||||
def __init__(self, room, x, y):
|
|
||||||
self.room = room
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.xy = (x, y)
|
|
||||||
self.connections = {}
|
|
||||||
self.connections_in = {}
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def ellipse_bbox(self):
|
|
||||||
x = self.x * settings.RENDER_SCALE
|
|
||||||
y = self.y * settings.RENDER_SCALE
|
|
||||||
return ((x-5, y-5), (x+5, y+5))
|
|
||||||
|
|
||||||
def connect_to(self, to_point):
|
|
||||||
self.room.level.graph.add_connection(self, to_point)
|
|
||||||
|
|
||||||
|
|
||||||
class GraphConnection():
|
|
||||||
def __init__(self, graph, from_point, to_point):
|
|
||||||
self.graph = graph
|
|
||||||
|
|
||||||
if to_point in from_point.connections:
|
|
||||||
self.graph.connections.remove(from_point.connections[to_point])
|
|
||||||
|
|
||||||
from_point.connections[to_point] = self
|
|
||||||
to_point.connections_in[from_point] = self
|
|
||||||
|
|
||||||
|
|
||||||
class Graph():
|
|
||||||
def __init__(self):
|
|
||||||
self.levels = {}
|
|
||||||
self.connections = []
|
|
||||||
|
|
||||||
def build(self):
|
|
||||||
for level in Level.objects.all():
|
|
||||||
self.levels[level.name] = GraphLevel(self, level)
|
|
||||||
|
|
||||||
for level in self.levels.values():
|
|
||||||
level.build()
|
|
||||||
level.draw_png()
|
|
||||||
|
|
||||||
def add_connection(self, from_point, to_point):
|
|
||||||
self.connections.append(GraphConnection(self, from_point, to_point))
|
|
1
src/c3nav/routing/graph/__init__.py
Normal file
1
src/c3nav/routing/graph/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from c3nav.routing.graph.graph import Graph # noqa
|
9
src/c3nav/routing/graph/connection.py
Normal file
9
src/c3nav/routing/graph/connection.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
class GraphConnection():
|
||||||
|
def __init__(self, graph, from_point, to_point):
|
||||||
|
self.graph = graph
|
||||||
|
|
||||||
|
if to_point in from_point.connections:
|
||||||
|
self.graph.connections.remove(from_point.connections[to_point])
|
||||||
|
|
||||||
|
from_point.connections[to_point] = self
|
||||||
|
to_point.connections_in[from_point] = self
|
20
src/c3nav/routing/graph/graph.py
Normal file
20
src/c3nav/routing/graph/graph.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from c3nav.mapdata.models import Level
|
||||||
|
from c3nav.routing.graph.connection import GraphConnection
|
||||||
|
from c3nav.routing.graph.level import GraphLevel
|
||||||
|
|
||||||
|
|
||||||
|
class Graph():
|
||||||
|
def __init__(self):
|
||||||
|
self.levels = {}
|
||||||
|
self.connections = []
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
for level in Level.objects.all():
|
||||||
|
self.levels[level.name] = GraphLevel(self, level)
|
||||||
|
|
||||||
|
for level in self.levels.values():
|
||||||
|
level.build()
|
||||||
|
level.draw_png()
|
||||||
|
|
||||||
|
def add_connection(self, from_point, to_point):
|
||||||
|
self.connections.append(GraphConnection(self, from_point, to_point))
|
77
src/c3nav/routing/graph/level.py
Normal file
77
src/c3nav/routing/graph/level.py
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import os
|
||||||
|
from itertools import permutations
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
from shapely.geometry import JOIN_STYLE, Polygon
|
||||||
|
|
||||||
|
from c3nav.routing.graph.point import GraphPoint
|
||||||
|
from c3nav.routing.graph.room import GraphRoom
|
||||||
|
from c3nav.routing.utils.base import assert_multipolygon, get_nearest_point
|
||||||
|
from c3nav.routing.utils.draw import _ellipse_bbox, _line_coords
|
||||||
|
|
||||||
|
|
||||||
|
class GraphLevel():
|
||||||
|
def __init__(self, graph, level):
|
||||||
|
self.graph = graph
|
||||||
|
self.level = level
|
||||||
|
self.rooms = []
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
self.collect_rooms()
|
||||||
|
self.create_points()
|
||||||
|
|
||||||
|
def collect_rooms(self):
|
||||||
|
accessibles = self.level.geometries.accessible
|
||||||
|
accessibles = [accessibles] if isinstance(accessibles, Polygon) else accessibles.geoms
|
||||||
|
for geometry in accessibles:
|
||||||
|
room = GraphRoom(self, geometry)
|
||||||
|
if not room.empty:
|
||||||
|
self.rooms.append(room)
|
||||||
|
|
||||||
|
def create_points(self):
|
||||||
|
for room in self.rooms:
|
||||||
|
room.create_points()
|
||||||
|
|
||||||
|
doors = self.level.geometries.doors
|
||||||
|
doors = assert_multipolygon(doors)
|
||||||
|
for door in doors:
|
||||||
|
polygon = door.buffer(0.01, join_style=JOIN_STYLE.mitre)
|
||||||
|
center = door.centroid
|
||||||
|
points = []
|
||||||
|
for room in self.rooms:
|
||||||
|
if polygon.intersects(room.geometry):
|
||||||
|
nearest_point = get_nearest_point(room.clear_geometry, center)
|
||||||
|
point = GraphPoint(room, *nearest_point.coords[0])
|
||||||
|
points.append(point)
|
||||||
|
room.points.append(point)
|
||||||
|
|
||||||
|
if len(points) < 2:
|
||||||
|
print('door with <2 rooms (%d) detected!' % len(points))
|
||||||
|
|
||||||
|
for from_point, to_point in permutations(points, 2):
|
||||||
|
from_point.connect_to(to_point)
|
||||||
|
|
||||||
|
for room in self.rooms:
|
||||||
|
room.connect_points()
|
||||||
|
|
||||||
|
def draw_png(self):
|
||||||
|
filename = os.path.join(settings.RENDER_ROOT, 'level-%s.png' % self.level.name)
|
||||||
|
graph_filename = os.path.join(settings.RENDER_ROOT, 'level-%s-graph.png' % self.level.name)
|
||||||
|
|
||||||
|
im = Image.open(filename)
|
||||||
|
height = im.size[1]
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
i = 0
|
||||||
|
for room in self.rooms:
|
||||||
|
for point in room.points:
|
||||||
|
for otherpoint, connection in point.connections.items():
|
||||||
|
draw.line(_line_coords(point, otherpoint, height), fill=(255, 100, 100))
|
||||||
|
|
||||||
|
for point in room.points:
|
||||||
|
i += 1
|
||||||
|
draw.ellipse(_ellipse_bbox(point.x, point.y, height), (200, 0, 0))
|
||||||
|
|
||||||
|
print(i, 'points')
|
||||||
|
|
||||||
|
im.save(graph_filename)
|
21
src/c3nav/routing/graph/point.py
Normal file
21
src/c3nav/routing/graph/point.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
|
||||||
|
class GraphPoint():
|
||||||
|
def __init__(self, room, x, y):
|
||||||
|
self.room = room
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.xy = (x, y)
|
||||||
|
self.connections = {}
|
||||||
|
self.connections_in = {}
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def ellipse_bbox(self):
|
||||||
|
x = self.x * settings.RENDER_SCALE
|
||||||
|
y = self.y * settings.RENDER_SCALE
|
||||||
|
return ((x-5, y-5), (x+5, y+5))
|
||||||
|
|
||||||
|
def connect_to(self, to_point):
|
||||||
|
self.room.level.graph.add_connection(self, to_point)
|
111
src/c3nav/routing/graph/room.py
Normal file
111
src/c3nav/routing/graph/room.py
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
from itertools import combinations, permutations
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from matplotlib.path import Path
|
||||||
|
from shapely.geometry import JOIN_STYLE, LineString
|
||||||
|
|
||||||
|
from c3nav.routing.graph.point import GraphPoint
|
||||||
|
from c3nav.routing.utils.base import assert_multipolygon
|
||||||
|
from c3nav.routing.utils.coords import get_coords_angles
|
||||||
|
from c3nav.routing.utils.mpl import polygon_to_mpl_paths
|
||||||
|
|
||||||
|
|
||||||
|
class GraphRoom():
|
||||||
|
def __init__(self, level, geometry):
|
||||||
|
self.level = level
|
||||||
|
self.geometry = geometry
|
||||||
|
self.points = []
|
||||||
|
|
||||||
|
self.clear_geometry = geometry.buffer(-0.3, join_style=JOIN_STYLE.mitre)
|
||||||
|
self.empty = self.clear_geometry.is_empty
|
||||||
|
|
||||||
|
if not self.empty:
|
||||||
|
self.mpl_paths = polygon_to_mpl_paths(self.clear_geometry.buffer(0.01, join_style=JOIN_STYLE.mitre))
|
||||||
|
|
||||||
|
def create_points(self):
|
||||||
|
original_geometry = self.geometry
|
||||||
|
geometry = original_geometry.buffer(-0.6, join_style=JOIN_STYLE.mitre)
|
||||||
|
|
||||||
|
if geometry.is_empty:
|
||||||
|
return
|
||||||
|
|
||||||
|
# points with 60cm distance to borders
|
||||||
|
polygons = assert_multipolygon(geometry)
|
||||||
|
for polygon in polygons:
|
||||||
|
self._add_ring(polygon.exterior, want_left=False)
|
||||||
|
|
||||||
|
for interior in polygon.interiors:
|
||||||
|
self._add_ring(interior, want_left=True)
|
||||||
|
|
||||||
|
# now fill in missing doorways or similar
|
||||||
|
missing_geometry = self.clear_geometry.difference(geometry.buffer(0.61, join_style=JOIN_STYLE.mitre))
|
||||||
|
polygons = assert_multipolygon(missing_geometry)
|
||||||
|
for polygon in polygons:
|
||||||
|
overlaps = polygon.buffer(0.62).intersection(geometry)
|
||||||
|
if overlaps.is_empty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
points = []
|
||||||
|
|
||||||
|
# overlaps to non-missing areas
|
||||||
|
overlaps = assert_multipolygon(overlaps)
|
||||||
|
for overlap in overlaps:
|
||||||
|
points.append(self.add_point(overlap.centroid.coords[0]))
|
||||||
|
|
||||||
|
points += self._add_ring(polygon.exterior, want_left=False)
|
||||||
|
|
||||||
|
for interior in polygon.interiors:
|
||||||
|
points += self._add_ring(interior, want_left=True)
|
||||||
|
|
||||||
|
for from_point, to_point in permutations(points, 2):
|
||||||
|
from_point.connect_to(to_point)
|
||||||
|
|
||||||
|
def _add_ring(self, geom, want_left):
|
||||||
|
"""
|
||||||
|
add the points of a ring, but only those that have a specific direction change.
|
||||||
|
additionally removes unneeded points if the neighbors can be connected in self.clear_geometry
|
||||||
|
:param geom: LinearRing
|
||||||
|
:param want_left: True if the direction has to be left, False if it has to be right
|
||||||
|
"""
|
||||||
|
coords = []
|
||||||
|
skipped = False
|
||||||
|
can_delete_last = False
|
||||||
|
for coord, is_left in get_coords_angles(geom):
|
||||||
|
if is_left != want_left:
|
||||||
|
skipped = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not skipped and can_delete_last and len(coords) >= 2:
|
||||||
|
if LineString((coords[-2], coord)).within(self.clear_geometry):
|
||||||
|
coords[-1] = coord
|
||||||
|
continue
|
||||||
|
|
||||||
|
coords.append(coord)
|
||||||
|
can_delete_last = not skipped
|
||||||
|
skipped = False
|
||||||
|
|
||||||
|
if not skipped and can_delete_last and len(coords) >= 3:
|
||||||
|
if LineString((coords[-2], coords[0])).within(self.clear_geometry):
|
||||||
|
coords.pop()
|
||||||
|
|
||||||
|
points = []
|
||||||
|
for coord in coords:
|
||||||
|
points.append(self.add_point(coord))
|
||||||
|
|
||||||
|
return points
|
||||||
|
|
||||||
|
def add_point(self, coord):
|
||||||
|
point = GraphPoint(self, *coord)
|
||||||
|
self.points.append(point)
|
||||||
|
return point
|
||||||
|
|
||||||
|
def connect_points(self):
|
||||||
|
room_paths = self.mpl_paths
|
||||||
|
for point1, point2 in combinations(self.points, 2):
|
||||||
|
path = Path(np.vstack((point1.xy, point2.xy)))
|
||||||
|
for room_path in room_paths:
|
||||||
|
if room_path.intersects_path(path, False):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
point1.connect_to(point2)
|
||||||
|
point2.connect_to(point1)
|
|
@ -1,128 +0,0 @@
|
||||||
from math import atan2, degrees
|
|
||||||
|
|
||||||
from matplotlib.path import Path
|
|
||||||
from shapely.geometry import Polygon
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup_coords(coords):
|
|
||||||
"""
|
|
||||||
remove coordinates that are closer than 0.01 (1cm)
|
|
||||||
:param coords: list of (x, y) coordinates
|
|
||||||
:return: list of (x, y) coordinates
|
|
||||||
"""
|
|
||||||
result = []
|
|
||||||
last_coord = coords[-1]
|
|
||||||
for coord in coords:
|
|
||||||
if ((coord[0] - last_coord[0]) ** 2 + (coord[1] - last_coord[1]) ** 2) ** 0.5 >= 0.01:
|
|
||||||
result.append(coord)
|
|
||||||
last_coord = coord
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def coord_angle(coord1, coord2):
|
|
||||||
"""
|
|
||||||
calculate angle in degrees from coord1 to coord2
|
|
||||||
:param coord1: (x, y) coordinate
|
|
||||||
:param coord2: (x, y) coordinate
|
|
||||||
:return: angle in degrees
|
|
||||||
"""
|
|
||||||
return degrees(atan2(-(coord2[1] - coord1[1]), coord2[0] - coord1[0])) % 360
|
|
||||||
|
|
||||||
|
|
||||||
def get_coords_angles(geom):
|
|
||||||
"""
|
|
||||||
inspects all coordinates of a LinearRing counterclockwise and checks if they are a left or a right turn.
|
|
||||||
:param geom: LinearRing
|
|
||||||
:rtype: a list of ((x, y), is_left) tuples
|
|
||||||
"""
|
|
||||||
coords = list(cleanup_coords(geom.coords))
|
|
||||||
last_coords = coords[-2:]
|
|
||||||
last_angle = coord_angle(last_coords[-2], last_coords[-1])
|
|
||||||
result = []
|
|
||||||
|
|
||||||
invert = not geom.is_ccw
|
|
||||||
|
|
||||||
for coord in coords:
|
|
||||||
angle = coord_angle(last_coords[-1], coord)
|
|
||||||
angle_diff = (last_angle-angle) % 360
|
|
||||||
result.append((last_coords[-1], (angle_diff < 180) ^ invert))
|
|
||||||
last_coords.append(coord)
|
|
||||||
last_angle = angle
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def polygon_to_mpl_paths(polygon):
|
|
||||||
"""
|
|
||||||
convert a shapely Polygon or Multipolygon to a matplotlib Path
|
|
||||||
:param polygon: shapely Polygon or Multipolygon
|
|
||||||
:return: matplotlib Path
|
|
||||||
"""
|
|
||||||
paths = []
|
|
||||||
for polygon in assert_multipolygon(polygon):
|
|
||||||
paths.append(linearring_to_mpl_path(polygon.exterior))
|
|
||||||
for interior in polygon.interiors:
|
|
||||||
paths.append(linearring_to_mpl_path(interior))
|
|
||||||
return paths
|
|
||||||
|
|
||||||
|
|
||||||
def linearring_to_mpl_path(linearring):
|
|
||||||
vertices = []
|
|
||||||
codes = []
|
|
||||||
coords = list(linearring.coords)
|
|
||||||
vertices.extend(coords)
|
|
||||||
vertices.append(coords[0])
|
|
||||||
codes.append(Path.MOVETO)
|
|
||||||
codes.extend([Path.LINETO] * (len(coords)-1))
|
|
||||||
codes.append(Path.CLOSEPOLY)
|
|
||||||
return Path(vertices, codes, readonly=True)
|
|
||||||
|
|
||||||
|
|
||||||
def assert_multipolygon(geometry):
|
|
||||||
"""
|
|
||||||
given a Polygon or a MultiPolygon, return a list of Polygons
|
|
||||||
:param geometry: a Polygon or a MultiPolygon
|
|
||||||
:return: a list of Polygons
|
|
||||||
"""
|
|
||||||
if isinstance(geometry, Polygon):
|
|
||||||
polygons = [geometry]
|
|
||||||
else:
|
|
||||||
polygons = geometry.geoms
|
|
||||||
return polygons
|
|
||||||
|
|
||||||
|
|
||||||
def get_nearest_point(polygon, point):
|
|
||||||
"""
|
|
||||||
calculate the nearest point on of a polygon to another point that lies outside
|
|
||||||
:param polygon: a Polygon or a MultiPolygon
|
|
||||||
:param point: something that shapely understands as a point
|
|
||||||
:return: a Point
|
|
||||||
"""
|
|
||||||
polygons = assert_multipolygon(polygon)
|
|
||||||
nearest_distance = float('inf')
|
|
||||||
nearest_point = None
|
|
||||||
for polygon in polygons:
|
|
||||||
if point.within(Polygon(polygon.exterior.coords)):
|
|
||||||
for interior in polygon.interiors:
|
|
||||||
if point.within(Polygon(interior.coords)):
|
|
||||||
point_ = _nearest_point_ring(interior, point)
|
|
||||||
distance = point_.distance(point)
|
|
||||||
if distance and distance < nearest_distance:
|
|
||||||
nearest_distance = distance
|
|
||||||
nearest_point = point_
|
|
||||||
break # in a valid polygon a point can not be within multiple interiors
|
|
||||||
break # in a valid multipolygon a point can not be within multiple polygons
|
|
||||||
else:
|
|
||||||
point_ = _nearest_point_ring(polygon.exterior, point)
|
|
||||||
distance = point_.distance(point)
|
|
||||||
if distance and distance < nearest_distance:
|
|
||||||
nearest_distance = distance
|
|
||||||
nearest_point = point_
|
|
||||||
|
|
||||||
if nearest_point is None:
|
|
||||||
raise ValueError('Point inside polygon.')
|
|
||||||
return nearest_point
|
|
||||||
|
|
||||||
|
|
||||||
def _nearest_point_ring(ring, point):
|
|
||||||
return ring.interpolate(ring.project(point))
|
|
0
src/c3nav/routing/utils/__init__.py
Normal file
0
src/c3nav/routing/utils/__init__.py
Normal file
51
src/c3nav/routing/utils/base.py
Normal file
51
src/c3nav/routing/utils/base.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
|
|
||||||
|
def assert_multipolygon(geometry):
|
||||||
|
"""
|
||||||
|
given a Polygon or a MultiPolygon, return a list of Polygons
|
||||||
|
:param geometry: a Polygon or a MultiPolygon
|
||||||
|
:return: a list of Polygons
|
||||||
|
"""
|
||||||
|
if isinstance(geometry, Polygon):
|
||||||
|
polygons = [geometry]
|
||||||
|
else:
|
||||||
|
polygons = geometry.geoms
|
||||||
|
return polygons
|
||||||
|
|
||||||
|
|
||||||
|
def get_nearest_point(polygon, point):
|
||||||
|
"""
|
||||||
|
calculate the nearest point on of a polygon to another point that lies outside
|
||||||
|
:param polygon: a Polygon or a MultiPolygon
|
||||||
|
:param point: something that shapely understands as a point
|
||||||
|
:return: a Point
|
||||||
|
"""
|
||||||
|
polygons = assert_multipolygon(polygon)
|
||||||
|
nearest_distance = float('inf')
|
||||||
|
nearest_point = None
|
||||||
|
for polygon in polygons:
|
||||||
|
if point.within(Polygon(polygon.exterior.coords)):
|
||||||
|
for interior in polygon.interiors:
|
||||||
|
if point.within(Polygon(interior.coords)):
|
||||||
|
point_ = _nearest_point_ring(interior, point)
|
||||||
|
distance = point_.distance(point)
|
||||||
|
if distance and distance < nearest_distance:
|
||||||
|
nearest_distance = distance
|
||||||
|
nearest_point = point_
|
||||||
|
break # in a valid polygon a point can not be within multiple interiors
|
||||||
|
break # in a valid multipolygon a point can not be within multiple polygons
|
||||||
|
else:
|
||||||
|
point_ = _nearest_point_ring(polygon.exterior, point)
|
||||||
|
distance = point_.distance(point)
|
||||||
|
if distance and distance < nearest_distance:
|
||||||
|
nearest_distance = distance
|
||||||
|
nearest_point = point_
|
||||||
|
|
||||||
|
if nearest_point is None:
|
||||||
|
raise ValueError('Point inside polygon.')
|
||||||
|
return nearest_point
|
||||||
|
|
||||||
|
|
||||||
|
def _nearest_point_ring(ring, point):
|
||||||
|
return ring.interpolate(ring.project(point))
|
49
src/c3nav/routing/utils/coords.py
Normal file
49
src/c3nav/routing/utils/coords.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
from math import atan2, degrees
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_coords(coords):
|
||||||
|
"""
|
||||||
|
remove coordinates that are closer than 0.01 (1cm)
|
||||||
|
:param coords: list of (x, y) coordinates
|
||||||
|
:return: list of (x, y) coordinates
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
last_coord = coords[-1]
|
||||||
|
for coord in coords:
|
||||||
|
if ((coord[0] - last_coord[0]) ** 2 + (coord[1] - last_coord[1]) ** 2) ** 0.5 >= 0.01:
|
||||||
|
result.append(coord)
|
||||||
|
last_coord = coord
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def coord_angle(coord1, coord2):
|
||||||
|
"""
|
||||||
|
calculate angle in degrees from coord1 to coord2
|
||||||
|
:param coord1: (x, y) coordinate
|
||||||
|
:param coord2: (x, y) coordinate
|
||||||
|
:return: angle in degrees
|
||||||
|
"""
|
||||||
|
return degrees(atan2(-(coord2[1] - coord1[1]), coord2[0] - coord1[0])) % 360
|
||||||
|
|
||||||
|
|
||||||
|
def get_coords_angles(geom):
|
||||||
|
"""
|
||||||
|
inspects all coordinates of a LinearRing counterclockwise and checks if they are a left or a right turn.
|
||||||
|
:param geom: LinearRing
|
||||||
|
:rtype: a list of ((x, y), is_left) tuples
|
||||||
|
"""
|
||||||
|
coords = list(cleanup_coords(geom.coords))
|
||||||
|
last_coords = coords[-2:]
|
||||||
|
last_angle = coord_angle(last_coords[-2], last_coords[-1])
|
||||||
|
result = []
|
||||||
|
|
||||||
|
invert = not geom.is_ccw
|
||||||
|
|
||||||
|
for coord in coords:
|
||||||
|
angle = coord_angle(last_coords[-1], coord)
|
||||||
|
angle_diff = (last_angle-angle) % 360
|
||||||
|
result.append((last_coords[-1], (angle_diff < 180) ^ invert))
|
||||||
|
last_coords.append(coord)
|
||||||
|
last_angle = angle
|
||||||
|
|
||||||
|
return result
|
13
src/c3nav/routing/utils/draw.py
Normal file
13
src/c3nav/routing/utils/draw.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def _ellipse_bbox(x, y, height):
|
||||||
|
x *= settings.RENDER_SCALE
|
||||||
|
y *= settings.RENDER_SCALE
|
||||||
|
y = height-y
|
||||||
|
return ((x - 2, y - 2), (x + 2, y + 2))
|
||||||
|
|
||||||
|
|
||||||
|
def _line_coords(from_point, to_point, height):
|
||||||
|
return (from_point.x * settings.RENDER_SCALE, height - (from_point.y * settings.RENDER_SCALE),
|
||||||
|
to_point.x * settings.RENDER_SCALE, height - (to_point.y * settings.RENDER_SCALE))
|
29
src/c3nav/routing/utils/mpl.py
Normal file
29
src/c3nav/routing/utils/mpl.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
from matplotlib.path import Path
|
||||||
|
|
||||||
|
from c3nav.routing.utils.base import assert_multipolygon
|
||||||
|
|
||||||
|
|
||||||
|
def polygon_to_mpl_paths(polygon):
|
||||||
|
"""
|
||||||
|
convert a shapely Polygon or Multipolygon to a matplotlib Path
|
||||||
|
:param polygon: shapely Polygon or Multipolygon
|
||||||
|
:return: matplotlib Path
|
||||||
|
"""
|
||||||
|
paths = []
|
||||||
|
for polygon in assert_multipolygon(polygon):
|
||||||
|
paths.append(linearring_to_mpl_path(polygon.exterior))
|
||||||
|
for interior in polygon.interiors:
|
||||||
|
paths.append(linearring_to_mpl_path(interior))
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
def linearring_to_mpl_path(linearring):
|
||||||
|
vertices = []
|
||||||
|
codes = []
|
||||||
|
coords = list(linearring.coords)
|
||||||
|
vertices.extend(coords)
|
||||||
|
vertices.append(coords[0])
|
||||||
|
codes.append(Path.MOVETO)
|
||||||
|
codes.extend([Path.LINETO] * (len(coords)-1))
|
||||||
|
codes.append(Path.CLOSEPOLY)
|
||||||
|
return Path(vertices, codes, readonly=True)
|
Loading…
Add table
Add a link
Reference in a new issue