Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/net/net2jpsgeometry.py
169673 views
1
#!/usr/bin/env python
2
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3
# Copyright (C) 2007-2025 German Aerospace Center (DLR) and others.
4
# This program and the accompanying materials are made available under the
5
# terms of the Eclipse Public License 2.0 which is available at
6
# https://www.eclipse.org/legal/epl-2.0/
7
# This Source Code may also be made available under the following Secondary
8
# Licenses when the conditions for such availability set forth in the Eclipse
9
# Public License 2.0 are satisfied: GNU General Public License, version 2
10
# or later which is available at
11
# https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
12
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
13
14
# @file net2jpsgeometry.py
15
# @author Jakob Erdmann
16
# @author Matthias Schwamborn
17
# @date 2020-11-13
18
19
"""
20
This script converts a sumo network to a JuPedSim geometry
21
"""
22
from __future__ import absolute_import
23
from __future__ import print_function
24
import os
25
import sys
26
import ezdxf
27
28
if 'SUMO_HOME' in os.environ:
29
sys.path.append(os.path.join(os.environ['SUMO_HOME'], 'tools'))
30
import sumolib # noqa
31
32
33
# defines
34
KEY_JPS_TYPE = "jps-type"
35
KEY_TRANSITION_ID = "transition-id"
36
KEY_FROM_ID = "from-object-id"
37
KEY_TO_ID = "to-object-id"
38
KEY_SUMO_ID = "sumo-object-id"
39
40
JPS_BOUNDARY_TYPE_ID = "boundary"
41
JPS_BOUNDARY_ID_SUFFIX = "_boundary"
42
JPS_BOUNDARY_SLICE_DELIMITER = "_slice_"
43
JPS_BOUNDARY_COLOR = sumolib.color.RGBAColor(255, 0, 0)
44
JPS_BOUNDARY_LAYER = 10
45
46
JPS_DOOR_TYPE_ID = "door"
47
JPS_DOOR_ID_DELIMITER = "_door_"
48
JPS_DOOR_COLOR = sumolib.color.RGBAColor(0, 0, 255)
49
JPS_DOOR_LAYER = 11
50
51
DXF_LAYER_NAME_BOUNDARY = "SUMONetBoundary"
52
DXF_LAYER_NAME_DOOR = "SUMONetDoors"
53
54
DEBUG = False
55
56
57
class DoorInfo:
58
def __init__(self,
59
id=None,
60
shape=None,
61
parentPolygon=None,
62
atLengthsOfParent=None):
63
assert len(shape) == 2, "expected two positions for door (got %d instead)" % len(shape)
64
self._id = id
65
self._shape = shape
66
self._width = sumolib.geomhelper.polyLength(shape)
67
self._parentPolygon = parentPolygon
68
self._atLengthsOfParent = atLengthsOfParent
69
70
_id = None
71
_shape = None
72
_width = 0
73
_parentPolygon = None
74
_atLengthsOfParent = None
75
76
77
def parse_args():
78
USAGE = "Usage: " + sys.argv[0] + " -n <net-file> -s <selected-objects-file> <options>"
79
argParser = sumolib.options.ArgumentParser(usage=USAGE)
80
argParser.add_argument("-n", "--net-file", dest="netFile", help="The .net.xml file to convert")
81
# see https://sumo.dlr.de/docs/sumo-gui.html#selecting_objects
82
argParser.add_argument("-s", "--selected-objects-file", dest="selectedObjectsFile",
83
help="The txt file including the line-by-line list of objects to select")
84
argParser.add_argument("-o", "--output-file", dest="outFile",
85
help="The JuPedSim dxf output file name")
86
argParser.add_argument("-a", "--write-additional-file", action="store_true",
87
help="Export boundaries and doors to XML additional file")
88
# see https://sumo.dlr.de/docs/Simulation/Pedestrians.html#non-exclusive_sidewalks
89
argParser.add_argument("--no-exclusive-sidewalks", action="store_true",
90
help="Choose all lanes allowing pedestrians in case of ambiguities (experimental)")
91
argParser.add_argument("--allow-junctions", action="store_true",
92
help="Allow junctions as feasible pedestrian areas for JuPedSim (experimental)")
93
argParser.add_argument("--fixed-metadata", action="store_true",
94
help="Write fixed metadata (creation date etc.) for stable tests")
95
96
options = argParser.parse_args()
97
if not options.netFile or not options.selectedObjectsFile:
98
print("Missing arguments")
99
argParser.print_help()
100
exit()
101
return options
102
103
104
def isDuplicate(polygon, polygons):
105
for p in polygons:
106
if p.id == polygon.id:
107
return True
108
return False
109
110
111
def calculateBoundingPolygon(shape, width):
112
shiftPlus = sumolib.geomhelper.move2side(shape, width / 2.0)
113
shiftMinus = sumolib.geomhelper.move2side(shape, -width / 2.0)
114
polyShape = []
115
for x, y in shiftPlus:
116
polyShape.append((x, y))
117
for x, y in reversed(shiftMinus):
118
polyShape.append((x, y))
119
polyShape.append(polyShape[0])
120
return polyShape
121
122
123
def addInOutLaneToDoorList(polygon, inOutLane, net, doorInfoList, direction='in'):
124
assert direction == 'in' or direction == 'out'
125
lane = net.getLane(polygon.attributes[KEY_SUMO_ID])
126
if DEBUG:
127
print("DEBUG: lane (%s) \'%s\' for current lane \'%s\'" % (direction, inOutLane.getID(), lane.getID()))
128
shape = inOutLane.getShape()
129
# handle special edges
130
if inOutLane.getEdge().isSpecial():
131
edgeFunc = inOutLane.getEdge().getFunction()
132
if DEBUG:
133
print("DEBUG: edge (%s) \'%s\' has special function \'%s\'" % (
134
direction, inOutLane.getEdge().getID(), edgeFunc))
135
if edgeFunc == "internal":
136
if direction == 'in':
137
shape = inOutLane.getConnection(lane).getJunction().getShape()
138
else:
139
shape = lane.getConnection(inOutLane).getJunction().getShape()
140
if DEBUG:
141
inOutPolygon = sumolib.shapes.polygon.Polygon(id=inOutLane.getID(),
142
color=JPS_BOUNDARY_COLOR,
143
layer=JPS_BOUNDARY_LAYER,
144
shape=shape)
145
print(inOutPolygon.toXML())
146
lengths = sumolib.geomhelper.intersectsAtLengths2D(polygon.shape, shape)
147
lengths.sort()
148
positions = [sumolib.geomhelper.positionAtShapeOffset(polygon.shape, offset) for offset in lengths]
149
doorId = polygon.attributes[KEY_SUMO_ID] + JPS_DOOR_ID_DELIMITER + inOutLane.getID()
150
doorInfoList.append(DoorInfo(doorId, positions, polygon, lengths))
151
if DEBUG:
152
print("DEBUG: calculateDoors() p.shape: \'%s\'" % polygon.shape)
153
print("DEBUG: calculateDoors() shape: \'%s\'" % shape)
154
print("DEBUG: calculateDoors() lengths: \'%s\'" % lengths)
155
print("DEBUG: calculateDoors() positions: \'%s\'" % positions)
156
157
158
def addIncidentEdgeToDoorList(polygon, edge, net, doorInfoList):
159
assert net.hasEdge(edge.getID())
160
if DEBUG:
161
print("DEBUG: incident edge \'%s\' for current node \'%s\'" % (edge.getID(), polygon.attributes[KEY_SUMO_ID]))
162
lane = getExclusiveSidewalkLane(edge)
163
if lane is None:
164
return
165
shape = calculateBoundingPolygon(lane.getShape(includeJunctions=False), lane.getWidth())
166
lengths = sumolib.geomhelper.intersectsAtLengths2D(polygon.shape, shape)
167
lengths.sort()
168
positions = [sumolib.geomhelper.positionAtShapeOffset(polygon.shape, offset) for offset in lengths]
169
doorId = polygon.attributes[KEY_SUMO_ID] + JPS_DOOR_ID_DELIMITER + lane.getID()
170
doorInfoList.append(DoorInfo(doorId, positions, polygon, lengths))
171
172
173
def subtractDoorsFromPolygon(polygon, doorInfoList):
174
result = []
175
lengths = []
176
for doorInfo in doorInfoList:
177
if DEBUG:
178
print("DEBUG: doorInfo._atLengthsOfParent:", doorInfo._atLengthsOfParent)
179
len1 = doorInfo._atLengthsOfParent[0]
180
len2 = doorInfo._atLengthsOfParent[-1]
181
assert len1 < len2, "len1 should be smaller than len2 (len1=%d, len2=%d)" % (len1, len2)
182
if len2 - len1 > doorInfo._width:
183
# corner case with inversely oriented door and closed parent polygon
184
len1 = len2
185
len2 = sumolib.geomhelper.polyLength(polygon.shape)
186
if len1 in lengths and len2 in lengths:
187
# ignore duplicates
188
continue
189
elif len1 not in lengths and len2 not in lengths:
190
lengths.append(len1)
191
lengths.append(len2)
192
else:
193
print("ERROR: only one of (len1, len2) found in length list, aborting...")
194
sys.exit(1)
195
if DEBUG:
196
print("DEBUG: lengths:", lengths)
197
lengths.sort()
198
if DEBUG:
199
print("DEBUG: lengths.sorted:", lengths)
200
polySlices = sumolib.geomhelper.splitPolygonAtLengths2D(polygon.shape, lengths)
201
# start at second slice if first door is at beginning of polygon
202
startSliceIdx = 1 if lengths[0] == 0.0 else 0
203
# stop at penultimate slice if last door is at end of polygon
204
endSliceIdx = len(polySlices) - 1 if lengths[-1] == sumolib.geomhelper.polyLength(polygon.shape) else len(polySlices) # noqa
205
# intersectsAtLengths2D() filters out duplicate lengths -> no two doors are directly adjacent to each other
206
for i in range(startSliceIdx, endSliceIdx, 2):
207
p = sumolib.shapes.polygon.Polygon(id=polygon.id + JPS_BOUNDARY_SLICE_DELIMITER + str(i),
208
color=JPS_BOUNDARY_COLOR,
209
layer=JPS_BOUNDARY_LAYER,
210
shape=polySlices[i])
211
p.attributes[KEY_JPS_TYPE] = polygon.attributes[KEY_JPS_TYPE]
212
p.attributes[KEY_SUMO_ID] = polygon.attributes[KEY_SUMO_ID]
213
result.append(p)
214
return result
215
216
217
def calculateDoors(polygons, net):
218
result = []
219
myPolygonSlices = []
220
myLastTransitionId = 0
221
for p in polygons:
222
isLane = net.hasEdge(p.attributes[KEY_SUMO_ID][:-2])
223
isNode = net.hasNode(p.attributes[KEY_SUMO_ID])
224
if not isLane and not isNode:
225
print("ERROR: objID \'%s\' not found as lane or node in network, aborting..." %
226
p.attributes[KEY_SUMO_ID])
227
sys.exit(1)
228
doorInfoList = []
229
if isLane:
230
lane = net.getLane(p.attributes[KEY_SUMO_ID])
231
if DEBUG:
232
print("DEBUG: calculateDoors() lane \'%s\'" % lane.getID())
233
for inLane in lane.getIncoming(onlyDirect=True):
234
addInOutLaneToDoorList(p, inLane, net, doorInfoList, direction='in')
235
for outCon in lane.getOutgoing():
236
addInOutLaneToDoorList(p, outCon.getToLane(), net, doorInfoList, direction='out')
237
elif isNode:
238
node = net.getNode(p.attributes[KEY_SUMO_ID])
239
if DEBUG:
240
print("DEBUG: calculateDoors() node \'%s\'" % node.getID())
241
for inc in node.getIncoming():
242
if inc.getID()[0] == ":":
243
continue
244
addIncidentEdgeToDoorList(p, inc, net, doorInfoList)
245
for out in node.getOutgoing():
246
if out.getID()[0] == ":":
247
continue
248
addIncidentEdgeToDoorList(p, out, net, doorInfoList)
249
for doorInfo in doorInfoList:
250
door = sumolib.shapes.polygon.Polygon(id=doorInfo._id,
251
color=JPS_DOOR_COLOR,
252
layer=JPS_DOOR_LAYER,
253
shape=doorInfo._shape)
254
door.attributes[KEY_JPS_TYPE] = JPS_DOOR_TYPE_ID
255
door.attributes[KEY_FROM_ID] = door.id.split(JPS_DOOR_ID_DELIMITER)[0]
256
door.attributes[KEY_TO_ID] = door.id.split(JPS_DOOR_ID_DELIMITER)[1]
257
door.attributes[KEY_TRANSITION_ID] = myLastTransitionId
258
if not isDuplicate(door, result):
259
result.append(door)
260
myLastTransitionId += 1
261
myPolygonSlices += subtractDoorsFromPolygon(p, doorInfoList)
262
return result, myPolygonSlices
263
264
265
def addLaneToPolygons(lane, polygons):
266
if not lane.allows("pedestrian"):
267
print("WARNING: lane \'%s\' does not allow pedestrians, skipping..." % (lane.getID()))
268
return
269
polyShape = calculateBoundingPolygon(lane.getShape(includeJunctions=False), lane.getWidth())
270
polygon = sumolib.shapes.polygon.Polygon(id=lane.getID() + JPS_BOUNDARY_ID_SUFFIX,
271
color=JPS_BOUNDARY_COLOR,
272
layer=JPS_BOUNDARY_LAYER,
273
shape=polyShape)
274
polygon.attributes[KEY_JPS_TYPE] = JPS_BOUNDARY_TYPE_ID
275
polygon.attributes[KEY_SUMO_ID] = lane.getID()
276
if not isDuplicate(polygon, polygons):
277
polygons.append(polygon)
278
return
279
280
281
def addNodeToPolygons(node, polygons):
282
polyShape = node.getShape()
283
polyShape.append(polyShape[0])
284
polygon = sumolib.shapes.polygon.Polygon(id=node.getID() + JPS_BOUNDARY_ID_SUFFIX,
285
color=JPS_BOUNDARY_COLOR,
286
layer=JPS_BOUNDARY_LAYER,
287
shape=polyShape)
288
polygon.attributes[KEY_JPS_TYPE] = JPS_BOUNDARY_TYPE_ID
289
polygon.attributes[KEY_SUMO_ID] = node.getID()
290
if not isDuplicate(polygon, polygons):
291
polygons.append(polygon)
292
return
293
294
295
def getExclusiveSidewalkLane(edge):
296
for i in range(edge.getLaneNumber()):
297
lane = edge.getLanes()[i]
298
if lane.allows("pedestrian"):
299
if DEBUG:
300
print("DEBUG: exclusive sidewalk of edge \'%s\' is lane \'%s\'..." % (edge.getID(), lane.getID()))
301
return lane
302
print("WARNING: edge \'%s\' has no exclusive sidewalk lane..." % (edge.getID()))
303
return None
304
305
306
def writeToDxf(polygons, doors, options):
307
# write DXF file
308
doc = ezdxf.new(dxfversion='R2000')
309
msp = doc.modelspace() # add new entities to the modelspace
310
doc.layers.new(name=DXF_LAYER_NAME_BOUNDARY, dxfattribs={'linetype': 'SOLID', 'color': 7})
311
for p in polygons:
312
msp.add_lwpolyline(p.shape, dxfattribs={'layer': DXF_LAYER_NAME_BOUNDARY})
313
doc.layers.new(name=DXF_LAYER_NAME_DOOR, dxfattribs={'linetype': 'DASHED'})
314
for d in doors:
315
# use color attribute to encode transition id
316
msp.add_lwpolyline(d.shape, dxfattribs={'layer': DXF_LAYER_NAME_DOOR, 'color': d.attributes[KEY_TRANSITION_ID]})
317
doc.saveas(options.outFile)
318
319
320
if __name__ == "__main__":
321
options = parse_args()
322
if options.fixed_metadata:
323
ezdxf.options.set("core", "write_fixed_meta_data_for_testing", "true")
324
net = sumolib.net.readNet(options.netFile, withInternal=True, withPedestrianConnections=True)
325
selectedObjects = sumolib.files.selection.read(options.selectedObjectsFile, lanes2edges=False)
326
327
polygons = []
328
if "edge" in selectedObjects.keys():
329
for edgeId in sorted(selectedObjects["edge"]):
330
try:
331
edge = net.getEdge(edgeId)
332
except KeyError:
333
print("WARNING: edge \'%s\' does not exist in the network, skipping..." % (edgeId))
334
continue
335
if not edge.allows("pedestrian"):
336
print("WARNING: edge \'%s\' does not allow pedestrians, skipping..." % (edgeId))
337
continue
338
if not options.no_exclusive_sidewalks:
339
addLaneToPolygons(getExclusiveSidewalkLane(edge), polygons)
340
else:
341
for lane in edge.getLanes():
342
addLaneToPolygons(lane, polygons)
343
if "lane" in selectedObjects.keys():
344
for laneId in sorted(selectedObjects["lane"]):
345
try:
346
lane = net.getLane(laneId)
347
except KeyError:
348
print("WARNING: lane \'%s\' does not exist in the network, skipping..." % (laneId))
349
continue
350
except IndexError:
351
print("WARNING: lane \'%s\' does not exist in the network (please check lane index), skipping..." % (
352
laneId))
353
continue
354
if not options.no_exclusive_sidewalks:
355
sidewalkLane = getExclusiveSidewalkLane(lane.getEdge())
356
if laneId != sidewalkLane.getID():
357
print("WARNING: lane \'%s\' is not the exclusive sidewalk lane, skipping..." % (laneId))
358
continue
359
addLaneToPolygons(lane, polygons)
360
if "junction" in selectedObjects.keys():
361
for nodeId in sorted(selectedObjects["junction"]):
362
if options.allow_junctions:
363
try:
364
node = net.getNode(nodeId)
365
except KeyError:
366
print("WARNING: node \'%s\' does not exist in the network, skipping..." % (nodeId))
367
continue
368
addNodeToPolygons(node, polygons)
369
else:
370
print("WARNING: junctions not allowed (\'%s\'), try \'--allow-junctions\'" % (nodeId))
371
372
doors, polygonSlices = calculateDoors(polygons, net)
373
if not options.outFile:
374
options.outFile = options.netFile[:options.netFile.rfind(".net.xml")] + ".dxf"
375
if options.write_additional_file:
376
additionalFile = options.outFile[:options.outFile.rfind(".dxf")] + ".poly.xml"
377
sumolib.files.additional.write(additionalFile, doors + polygonSlices)
378
writeToDxf(polygonSlices, doors, options)
379
380