Commit d14b7201 authored by Stefan Schuhbaeck's avatar Stefan Schuhbaeck
Browse files

add stand alone features from VadereManagement module:

* single step mode in GUI:
  Allows the user to step through the simulation one step at a time to
  identify bugs.
* simplify obstacles:
  Merge multiple obstacles based on the convex hull their points create.
  The merge can be undon
* add features to open street map (osm) importer:
  1) import 'open' paths as polygons with a specified width. With this
     it is possible to create walls or subway entrance
  2) add option to include osm ids into each obstacle
parent fd7953ae
Pipeline #125635 passed with stages
in 109 minutes and 1 second
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -55,15 +55,172 @@ from string import Template
import argparse
import utm
import math
import numpy as np
import itertools as iter
from typing import List
vadere_obstacle_string = """{
"shape" : {
"type" : "POLYGON",
"points" : [ $points ]
},
"id" : $id
}"""
class PathToPolygon:
def __init__(self, list_of_tuples, dist):
self.points = [np.array([p[0], p[1]]) for p in list_of_tuples]
self.dist = dist
self.rotPositive = np.array(((0, -1), (1, 0)))
self.rotNegative = np.array(((0, 1), (-1, 0)))
self.polygon = []
self.create_poly_points()
@classmethod
def get_poly_object(cls, list_of_tuples, dist, id):
ptp = cls(list_of_tuples, dist)
return PolyObjectWidthId(id, ptp.create_poly_points())
def create_poly_points(self):
lines_o1 = []
lines_o2 = []
for a, b in self.pairwise(self.points):
lines = self.parallel_lines([a, b], self.dist)
lines_o1.append(lines[0])
lines_o2.append(lines[1])
path_o1 = self.offset_line(lines_o1)
path_o2 = self.offset_line(lines_o2)
self.polygon.extend(path_o1)
path_o2.reverse()
self.polygon.extend(path_o2)
if not self.polygon_closed():
self.polygon.append(self.polygon[0])
return self.polygon
def polygon_closed(self):
if len(self.polygon) < 2:
return False
return np.array_equal(self.polygon[0], self.polygon[-1])
def offset_line(self, lines):
points = [lines[0][0]]
for l1, l2 in self.pairwise(lines):
points.append(self.line_intersection(l1, l2))
points.append(lines[-1][-1]) # last line last point
return points
@staticmethod
def pairwise(iterable):
"""s -> (s0,s1), (s1,s2), (s2, s3), ..."""
a, b = iter.tee(iterable)
next(b, None)
return zip(a, b)
@staticmethod
def line_intersection(line1, line2):
"""
see https://stackoverflow.com/a/20677983
:param line1:
:param line2:
:return:
"""
x_diff = (line1[0][0] - line1[1][0], line2[0][0] - line2[1][0])
y_diff = (line1[0][1] - line1[1][1], line2[0][1] - line2[1][1])
def det(a, b):
return a[0] * b[1] - a[1] * b[0]
div = det(x_diff, y_diff)
if div == 0:
raise Exception('lines do not intersect')
d = (det(*line1), det(*line2))
x = det(d, x_diff) / div
y = det(d, y_diff) / div
return np.array([x, y])
def parallel_lines(self, line, dist):
"""
create parallel lines moved dist amount away in +-90 deg.
:param line: [p1, p2] with p1 = [x1, y1]
:return: (line_pos, line_neg) at dist from line
"""
p1, p2 = line[0], line[1]
v = p2 - p1
v_normalized = v / np.linalg.norm(v)
# +90 and strech by dist
o1 = dist * np.matmul(self.rotPositive, v_normalized)
line_o1 = [o1 + p1, o1 + p2]
# -90 and strech by dist
o2 = dist * np.matmul(self.rotNegative, v_normalized)
line_o2 = [o2 + p1, o2 + p2]
return [line_o1, line_o2]
class PolyObjectWidthId:
"""
Simple wrapper class around a list of points (order is important!) withn an identifier field (id)
"""
def __init__(self, id, utm_points):
self.id = id
self.cartesian_points = utm_points
self.base = None
def shift_points(self, base: list):
shift_in_x = -base[0]
shift_in_y = -base[1]
self.cartesian_points = [(point[0] + shift_in_x, point[1] + shift_in_y) for point in self.cartesian_points]
self.base = base
def str2bool(v):
# see https://stackoverflow.com/a/43357954
if isinstance(v, bool):
return v
if v.lower() in ('yes', 'true', 't', 'y', '1'):
return True
elif v.lower() in ('no', 'false', 'f', 'n', '0'):
return False
else:
raise argparse.ArgumentTypeError('Boolean value expected.')
def parse_command_line_arguments():
parser = argparse.ArgumentParser(description="Convert and OpenStreetMap file to a Vadere topology description.")
parser.add_argument("filename", type=str, nargs="?",
parser.add_argument("osm_file", type=str, nargs="?",
help="An OSM map in XML format.",
default="maps/map_hochschule_klein.osm",
)
parser.add_argument("--use-osm-id", dest='use_osm_id', type=str2bool, const=True, nargs="?",
default=True, help="Set to use osm ids for obstacles")
parser.add_argument("-o", "--output", type=str, nargs="?",
help="Specify filename if you want the output in a file.")
help="Specify filename if you want the output in a file.")
# parser.add_argument("-o", "--output", type=str, nargs="?",
# help="Specify filename if you want the output in a file.")
subparser = parser.add_subparsers(help="sub-command help")
parser_converter = subparser.add_parser('convert', help='convert given osm file to Vadere topography')
parser_converter.set_defaults(main_func=main_convert)
parser_way_to_polygon = subparser.add_parser('wayToPoly', help='convert given way id to a Vadere polygon')
parser_way_to_polygon.set_defaults(main_func=main_way_to_polygon)
parser_way_to_polygon.add_argument("-d", "--path-width", dest="d", type=float, help="Specify filename if you want the output in a file.")
parser_way_to_polygon.add_argument("-w", "--way", type=int, nargs="+")
args = parser.parse_args()
......@@ -71,7 +228,7 @@ def parse_command_line_arguments():
def extract_latitude_and_longitude_for_each_xml_node(xml_tree):
# Select all nodes (not only buildings).
# Select all convertnodes (not only buildings).
nodes = xml_tree.xpath("/osm/node")
nodes_dictionary_with_lat_and_lon = {node.get("id"): (node.get("lat"), node.get("lon")) for node in nodes}
......@@ -86,6 +243,24 @@ def filter_for_buildings(xml_tree):
return buildings
def filter_for_barrier(xml_tree, type='wall'):
barrier = xml_tree.xpath("/osm/way[./tag/@k='barrier']")
return barrier
def get_way(xml_tree, id):
way = xml_tree.xpath(f"/osm/way[@id='{id}']")
return way
def convert_way_to_cartesian(way, lookup_table_latitude_and_longitude, assume_closed_path=True):
node_references = way.xpath("./nd")
return convert_nodes_to_cartesian_points(node_references, lookup_table_latitude_and_longitude, assume_closed_path)
def filter_for_buildings_in_relations(xml_tree):
# Note: A relation describes a shape with "cutting holes".
......@@ -115,11 +290,16 @@ def assert_that_start_and_end_point_are_equal(node_references):
assert node_references[0].get("ref") == node_references[-1].get("ref")
def convert_nodes_to_cartesian_points(nodes, lookup_table_latitude_and_longitude):
def convert_nodes_to_cartesian_points(nodes, lookup_table_latitude_and_longitude, assume_closed_path):
cartesian_points = []
# Omit last node because it should be the same as the first one.
for node in nodes[:len(nodes) - 1]:
# Omit last node becausenodes it should be the same as the first one.
if assume_closed_path:
max_node = len(nodes) - 1
else:
max_node = len(nodes)
for node in nodes[:max_node]:
reference = node.get("ref")
latitude, longitude = lookup_table_latitude_and_longitude[reference]
......@@ -132,23 +312,16 @@ def convert_nodes_to_cartesian_points(nodes, lookup_table_latitude_and_longitude
return cartesian_points
def create_vadere_obstacles_from_points(cartesian_points):
vadere_obstacle_string = """{
"shape" : {
"type" : "POLYGON",
"points" : [ $points ]
},
"id" : -1
}"""
vadere_point_string = '{ "x" : $x, "y" : $y }'
def create_vadere_obstacles_from_building(building: PolyObjectWidthId):
vadere_point_string = ' { "x" : $x, "y" : $y }'
obstacle_string_template = Template(vadere_obstacle_string)
point_string_template = Template(vadere_point_string)
points_as_string = [point_string_template.substitute(x=x, y=y) for x, y in cartesian_points]
points_as_string = [point_string_template.substitute(x=x, y=y) for x, y in building.cartesian_points]
points_as_string_concatenated = ",\n".join(points_as_string)
vadere_obstacle_as_string = obstacle_string_template.substitute(points=points_as_string_concatenated)
vadere_obstacle_as_string = obstacle_string_template.substitute(points=points_as_string_concatenated, id=building.id)
return vadere_obstacle_as_string
......@@ -178,62 +351,63 @@ def print_output(outputfile, output):
print(output, file=text_file)
def find_width_and_height(buildings_cartesian):
def find_width_and_height(buildings: List[PolyObjectWidthId]):
# search for the highest x- and y-coordinates within the points
width = 0
height = 0
for cartesian_points in buildings_cartesian:
for point in cartesian_points:
for cartesian_points in buildings:
for point in cartesian_points.cartesian_points:
width = max(width, point[0])
height = max(height, point[1])
return math.ceil(width), math.ceil(height)
def find_new_basepoint(buildings_cartesian):
def find_new_basepoint(buildings: List[PolyObjectWidthId]):
# "buildings_cartesian" is a list of lists!!!
# The inner list contains the (x,y) tuples!
# search for the lowest x- and y-coordinates within the points
all_points = [point for building in buildings_cartesian for point in building]
all_points = [point for building in buildings for point in building.cartesian_points]
tuple_with_min_x = min(all_points, key=lambda point: point[0])
tuple_with_min_y = min(all_points, key=lambda point: point[1])
return (tuple_with_min_x[0], tuple_with_min_y[1])
return tuple_with_min_x[0], tuple_with_min_y[1]
def shift_points(buildings_utm, shift_in_x_direction, shift_in_y_direction):
new_buildings = []
for cartesian_points in buildings_utm:
shifted_cartesian_points = \
[(point[0] + shift_in_x_direction, point[1] + shift_in_y_direction) for point in cartesian_points]
new_buildings.append(shifted_cartesian_points)
return new_buildings
def shift_way(way, base):
shift_in_x_direction = -base[0]
shift_in_y_direction = -base[1]
shift_cartesian_points = [(point[0] + shift_in_x_direction, point[1] + shift_in_y_direction) for point in way]
return shift_cartesian_points
def convert_buildings_to_cartesian(buildings_as_xml_nodes):
def convert_buildings_to_cartesian(buildings_as_xml_nodes, nodes_dictionary_with_lat_and_lon, use_osm_id: bool):
buildings_in_cartesian = []
for building in buildings_as_xml_nodes:
# Collect nodes that belong to the current building.
node_references = building.xpath("./nd")
assert_that_start_and_end_point_are_equal(node_references)
cartesian_points = convert_nodes_to_cartesian_points(node_references, nodes_dictionary_with_lat_and_lon)
cartesian_points = convert_nodes_to_cartesian_points(node_references, nodes_dictionary_with_lat_and_lon,
assume_closed_path=True)
buildings_in_cartesian.append(cartesian_points)
if use_osm_id:
buildings_in_cartesian.append(PolyObjectWidthId(building.get('id'), cartesian_points))
else:
buildings_in_cartesian.append(PolyObjectWidthId(-1, cartesian_points))
return buildings_in_cartesian
def convert_buildings_as_cartesian_to_buildings_as_vadere_obstacles(buildings_as_cartesian):
def convert_buildings_as_cartesian_to_buildings_as_vadere_obstacles(buildings: List[PolyObjectWidthId]):
list_of_vadere_obstacles_as_strings = []
for cartesian_points in buildings_as_cartesian:
vadere_obstacles_as_strings = create_vadere_obstacles_from_points(cartesian_points)
for building in buildings:
vadere_obstacles_as_strings = create_vadere_obstacles_from_building(building)
list_of_vadere_obstacles_as_strings.append(vadere_obstacles_as_strings)
return list_of_vadere_obstacles_as_strings
if __name__ == "__main__":
args = parse_command_line_arguments()
xml_tree = etree.parse(args.filename)
def init(args):
xml_tree = etree.parse(args.osm_file)
nodes_dictionary_with_lat_and_lon = extract_latitude_and_longitude_for_each_xml_node(xml_tree)
......@@ -241,13 +415,28 @@ if __name__ == "__main__":
complex_buildings = filter_for_buildings_in_relations(xml_tree)
extracted_base_point = extract_base_point(xml_tree)
print_xml_parsing_statistics(args.filename, nodes_dictionary_with_lat_and_lon, simple_buildings, complex_buildings, extracted_base_point)
print_xml_parsing_statistics(args.osm_file, nodes_dictionary_with_lat_and_lon, simple_buildings, complex_buildings, extracted_base_point)
buildings_as_cartesian = convert_buildings_to_cartesian(simple_buildings + complex_buildings)
buildings_as_cartesian = convert_buildings_to_cartesian(simple_buildings + complex_buildings, nodes_dictionary_with_lat_and_lon, args.use_osm_id)
# make sure everything lies within the topography
new_base = find_new_basepoint(buildings_as_cartesian)
buildings_as_cartesian = shift_points(buildings_as_cartesian, -new_base[0], -new_base[1])
for b in buildings_as_cartesian:
b.shift_points(new_base)
return xml_tree, nodes_dictionary_with_lat_and_lon, buildings_as_cartesian, new_base
def main_convert(args):
"""
osm2vadere.pu mf.osm convert -h // for sub command specific help
osm2vadere.py mf.osm convert --output map.json
"""
print(args)
xml_tree, _, buildings_as_cartesian, _ = init(args)
# make sure everything lies within the topography
width_topography, height_topography = find_width_and_height(buildings_as_cartesian)
list_of_vadere_obstacles_as_strings = convert_buildings_as_cartesian_to_buildings_as_vadere_obstacles(buildings_as_cartesian)
......@@ -256,3 +445,32 @@ if __name__ == "__main__":
vadere_topography_output = build_vadere_topography_input_with_obstacles(obstacles_joined, width_topography, height_topography)
print_output(args.output, vadere_topography_output)
def main_way_to_polygon(args):
"""
osm2vadere.py mf.osm wayToPoly -h // for sub command specific help
osm2vadere.py mf.osm wayToPoly --way 116531500 --path-width 0.25
"""
print('way_to_polygon:', args)
xml_tree, node_dict, buildings_as_cartesian, new_base = init(args)
obstacles = []
for w in args.way:
way = get_way(xml_tree, w)
assert len(way) == 1
path_list = convert_way_to_cartesian(way[0], node_dict, assume_closed_path=False)
path_list = shift_way(path_list, new_base)
vadere_obstacle = create_vadere_obstacles_from_building(PathToPolygon.get_poly_object(path_list, args.d, w))
obstacles.append(vadere_obstacle)
print_output(args.output, '\n'.join(obstacles))
return obstacles
if __name__ == "__main__":
args = parse_command_line_arguments()
# map_mf_small.osm wayToPoly -w 258139211 -d 0.5
args.main_func(args)
......@@ -3,6 +3,35 @@ from lxml import etree
import osm2vadere
import unittest
import utm
import os
TEST_DATA_LON_LAT = os.path.join(os.path.dirname(__file__), 'maps/map_for_testing.osm')
TEST_DATA_2 = os.path.join(os.path.dirname(__file__), 'maps/map_mf_small.osm')
TEST_DATA="""{
"shape" : {
"type" : "POLYGON",
"points" : [ { "x" : 143.92115343943564, "y" : 168.69565355275918 },
{ "x" : 143.8295708812843, "y" : 158.46319241869656 },
{ "x" : 147.9524496438901, "y" : 158.453088570111 },
{ "x" : 148.04042161765545, "y" : 168.57734735060853 },
{ "x" : 148.54040274306163, "y" : 168.57300290155786 },
{ "x" : 148.44811333818495, "y" : 157.95187235651085 },
{ "x" : 143.3250868078719, "y" : 157.96442724530158 },
{ "x" : 143.42117346474575, "y" : 168.70012847277175 },
{ "x" : 143.92115343943564, "y" : 168.69565355275918 },
{ "x" : 143.92115343943564, "y" : 168.69565355275918 },
{ "x" : 143.8295708812843, "y" : 158.46319241869656 },
{ "x" : 147.9524496438901, "y" : 158.453088570111 },
{ "x" : 148.04042161765545, "y" : 168.57734735060853 },
{ "x" : 148.54040274306163, "y" : 168.57300290155786 },
{ "x" : 148.44811333818495, "y" : 157.95187235651085 },
{ "x" : 143.3250868078719, "y" : 157.96442724530158 },
{ "x" : 143.42117346474575, "y" : 168.70012847277175 },
{ "x" : 143.92115343943564, "y" : 168.69565355275918 } ]
},
"id" : 258139209
}
"""
class TestOsm2vadere(unittest.TestCase):
......@@ -24,7 +53,7 @@ class TestOsm2vadere(unittest.TestCase):
self.assertTrue(y_distance > 258 and y_distance < 275)
def test_extract_latitude_and_longitude_for_each_xml_node(self):
xml_tree = etree.parse("maps/map_for_testing.osm")
xml_tree = etree.parse(TEST_DATA_LON_LAT)
nodes_dictionary_with_lat_and_lon = osm2vadere.extract_latitude_and_longitude_for_each_xml_node(xml_tree)
self.assertTrue(nodes_dictionary_with_lat_and_lon.get("1")[0] == "1.1")
......@@ -34,12 +63,14 @@ class TestOsm2vadere(unittest.TestCase):
self.assertTrue (nodes_dictionary_with_lat_and_lon.get("3")[0] == "3.1")
self.assertTrue (nodes_dictionary_with_lat_and_lon.get("3")[1] == "3.2")
def test_find_width_and_height(self):
building_normal = [(1, 1), (3, 1), (1, 3), (3, 3)]
building_negative_coordinates = [(-1, 4), (-3, 2), (10, 2)]
building_with_floating_points = [(2.3, 1.4), (-10.5, 7), (9.99, 3), (5, 7.1), (3, 4)]
buildings_cartesian = [building_normal, building_negative_coordinates, building_with_floating_points]
width, height = osm2vadere.find_width_and_height(buildings_cartesian)
building_points = [building_normal, building_negative_coordinates, building_with_floating_points]
buildings = [osm2vadere.PolyObjectWidthId(-1, building) for building in building_points]
width, height = osm2vadere.find_width_and_height(buildings)
self.assertTrue(width == 10)
self.assertTrue(height == 8) # 7.1 is the maximum but the function returns math.ceil
......@@ -48,26 +79,45 @@ class TestOsm2vadere(unittest.TestCase):
building_normal = [(1, 1), (3, 1), (1, 3), (3, 3)]
building_negative_coordinates = [(-1, 4), (-3, 2), (10, 2)]
building_with_floating_points = [(2.3, 1.4), (-10.5, 7), (9.99, 3), (5, 7.1), (3, 4)]
buildings_cartesian = [building_normal, building_negative_coordinates, building_with_floating_points]
new_base_point = osm2vadere.find_new_basepoint(buildings_cartesian)
building_points = [building_normal, building_negative_coordinates, building_with_floating_points]
buildings = [osm2vadere.PolyObjectWidthId(-1, building) for building in building_points]
new_base_point = osm2vadere.find_new_basepoint(buildings)
self.assertTrue(new_base_point == [-10.5, 0])
self.assertTrue(new_base_point == (-10.5, 1))
building_negative_coordinates.append((3, -5))
new_base_point = osm2vadere.find_new_basepoint(buildings_cartesian)
new_base_point = osm2vadere.find_new_basepoint(buildings)
self.assertTrue(new_base_point == [-10.5, -5])
self.assertTrue(new_base_point == (-10.5, -5))
buildings_cartesian_only_positive = [[(1, 3), (1, 2), (2, 2)], [(2, 4), (7, 7), (6, 6)]]
new_base_point = osm2vadere.find_new_basepoint(buildings_cartesian_only_positive)
buildings = [osm2vadere.PolyObjectWidthId(-1, building) for building in buildings_cartesian_only_positive]
new_base_point = osm2vadere.find_new_basepoint(buildings)
self.assertTrue(new_base_point == (1, 2))
def test_get_wall(self):
class Ns():
def __init__(self, d, osm_file, way):
self.d = d
self.osm_file = osm_file
self.way = way
self.output = None
self.use_osm_id = True
o = osm2vadere.main_way_to_polygon(Ns(0.25, TEST_DATA_2, [258139209]))
self.assertEqual(len(o), 1)
self.assertEqual(''.join(TEST_DATA.split()), ''.join(o[0].split()))
self.assertTrue(new_base_point == [0, 0])
def test_shift_points(self):
buildings_cartesian = [[(1, 3), (-1, 2), (2.2, 2)], [(2, 4), (7, 7), (6, 6)]]
buildings_cartesian_shifted_by_one_and_two = osm2vadere.shift_points(buildings_cartesian, 1, 2)
building_points = [[(1, 3), (-1, 2), (2, 2)], [(2, 4), (7, 7), (6, 6)]]
buildings = [osm2vadere.PolyObjectWidthId(-1, points) for points in building_points]
for b in buildings:
b.shift_points([1,2])
self.assertEqual(buildings[0].cartesian_points, [(0, 1), (-2, 0), (1, 0)])
self.assertEqual(buildings[1].cartesian_points, [(1, 2), (6, 5), (5, 4)])
self.assertTrue(buildings_cartesian_shifted_by_one_and_two == [[(2, 5), (0, 4), (3.2, 4)], [(3, 6), (8, 9), (7, 8)]])
if __name__ == "__main__":
unittest.main()
......@@ -80,11 +80,13 @@ ProjectView.btnRunAllTests.text=Run all scenarios
ProjectView.mntmRunSelectetTests.text=Run selected scenario
ProjectView.mntmRunSelectedTests.text=Run selected scenarios
ProjectView.mntmSimulationResult.text=Show Simulation Result Dialog
ProjectView.btnPauseRunningTests.text=Pause running scenarios
ProjectView.btnPauseRunningTests.text=Pause
ProjectView.btnRunSelectedTest.text=Run selected scenario
ProjectView.btnRunSelectedTest.toolTipText=Run selected scenario
ProjectView.btnPauseRunningTests.toolTipText=Pause the scenario run
ProjectView.btnStopRunningTests.text=Stop running scenarios
ProjectView.btnResumeNormalSpeed.text=Resume
ProjectView.btnNextSimulationStep=Step
ProjectView.lblCurrentTest.text=Current scenario\:
ProjectView.lblNewLabel.text=New label
ProjectView.mnFile.text=Project
......@@ -194,6 +196,7 @@ Unavailable.text=Unavailable
Running.text=Running
Paused.text=Paused
Initialized.text=Initialized
Step.text=Singlestep
OutputprocessorsView.columnNames.text=["No dataProcessor chosen"]
OutputprocessorsView.dataProcessor.text={Processor: ""}
......@@ -256,6 +259,7 @@ OnlineVis.msgDialogShowPotentialfield.none=None
TopographyBoundDialog.title=Width x Height
TopographyBoundDialog.tooltip=Set topography bounds
InformationDialogError.title=Internal Error
InformationDialogFileError=Could not load file!
LoadingDialog.title=Loading...
......@@ -309,6 +313,7 @@ TopographyCreator.btnRedo.tooltip=Redo
TopographyCreator.btnCutTopography.tooltip=Cut Scenario
TopographyCreator.btnInsertPedestrian.tooltip=Pedestrian
TopographyCreator.btnTopographyBound.tooltip=Topography Bound
TopographyCreator.btnMergeWithConvexHull.tooltip=Merge With Convex Hull
TopographyCreator.btnTranslation.tooltip=Translate topography
TopographyCreator.btnElementTranslation.tooltip=Translate topography elements
TopographyCreator.btnInsertObstacle.tooltip=Obstacle
......
......@@ -80,11 +80,13 @@ ProjectView.btnRunAllTests.text=Alle Szenarios ausf\u00FChren
ProjectView.mntmRunSelectetTests.text=Ausgew\u00E4hlte Szenarios ausf\u00FChren
ProjectView.mntmRunSelectedTests.text=Ausgew\u00E4hlte Szenarios ausf\u00FChren
ProjectView.mntmSimulationResult.text=Ergebnis Dialog anzeigen