Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/net/remap_additionals.py
194057 views
1
#!/usr/bin/env python
2
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3
# Copyright (C) 2009-2026 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 remap_additionals.py
15
# @author Jakob Erdmann
16
# @date 2025-02-19
17
18
from __future__ import print_function
19
from __future__ import absolute_import
20
import os
21
import sys
22
from math import fabs, degrees
23
from statistics import median
24
25
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
26
import sumolib # noqa
27
from sumolib.geomhelper import distance # noqa
28
from sumolib.xml import parse # noqa
29
from sumolib.net import lane2edge # noqa
30
from sumolib.miscutils import openz # noqa
31
import sumolib.geomhelper as gh # noqa
32
33
34
def get_options(args=None):
35
ap = sumolib.options.ArgumentParser(description="Remap infrastructure from one network to another")
36
ap.add_argument("--orig-net", dest="origNet", required=True, category="input", type=ap.net_file,
37
help="SUMO network for loading infrastructure", metavar="FILE")
38
ap.add_argument("--target-net", dest="targetNet", required=True, category="input", type=ap.net_file,
39
help="SUMO network for writing infrastructure", metavar="FILE")
40
ap.add_argument("-a", "--additional-file", dest="additional", required=True, category="input",
41
type=ap.additional_file,
42
help="File for reading infrastructure", metavar="FILE")
43
ap.add_argument("-o", "--output-file", dest="output", required=True, category="output", type=ap.additional_file,
44
help="File for writing infrastructure", metavar="FILE")
45
ap.add_argument("--radius", type=float, default=20,
46
help="radius for finding candidate edges")
47
ap.add_argument("--shapecut", type=float, default=40,
48
help="Shorten polygon and edge shapes to FLOAT to increase robustness of angular comparison")
49
ap.add_argument("--angle-tolerance", type=float, default=20, dest="atol",
50
help="Match polygons and edges if their angle differs by no more than DEGRESS")
51
ap.add_argument("-v", "--verbose", action="store_true", dest="verbose",
52
default=False, help="tell me what you are doing")
53
options = ap.parse_args()
54
return options
55
56
57
def make_consecutive(net, laneIDs):
58
lanes = [net.getLane(laneID) for laneID in laneIDs]
59
lanes2 = []
60
for i, lane in enumerate(lanes[:-1]):
61
lanes2.append(lane)
62
lane2 = lanes[i + 1]
63
nTo = lane.getEdge().getToNode()
64
nFrom = lane2.getEdge().getFromNode()
65
if nTo != nFrom:
66
path, cost = net.getShortestPath(lane.getEdge(), lane2.getEdge())
67
if path:
68
for edge in path[1:-1]:
69
index = min(edge.getLaneNumber() - 1, lane.getIndex())
70
lanes2.append(edge.getLanes()[index])
71
lanes2.append(lanes[-1])
72
73
return [lane.getID() for lane in lanes2]
74
75
76
def remap_lanes(options, obj, laneIDs, pos=None, size=None):
77
laneIDs_positions2 = [remap_lane(options, obj, laneID, pos, size) for laneID in laneIDs.split()]
78
laneIDs2 = [la for la, pos in laneIDs_positions2]
79
if obj.name in ["e2Detector", "laneAreaDetector"]:
80
laneIDs2 = make_consecutive(options.net2, laneIDs2)
81
pos2 = laneIDs_positions2[0][1]
82
83
return ' '.join(laneIDs2), pos2
84
85
86
def remap_edges(options, obj, edgeIDs, pos=None, size=None):
87
edgeIDs_positions2 = [remap_edge(options, obj, edgeID, pos, size) for edgeID in edgeIDs.split()]
88
edgeIDs2 = [e for e, pos in edgeIDs_positions2]
89
pos2 = edgeIDs_positions2[0][1]
90
return ' '.join(edgeIDs2), pos2
91
92
93
def remap_lane(options, obj, laneID, pos=None, size=None):
94
lane = options.net.getLane(laneID)
95
lane2 = None
96
edge = lane.getEdge()
97
edge2, pos2 = remap_edge(options, obj, edge.getID(), pos, size)
98
if edge2:
99
cands = [c for c in edge.getLanes() if c.getPermissions() == lane.getPermissions()]
100
candIndex = cands.index(lane)
101
102
cands2 = [c for c in edge2.getLanes() if c.getPermissions() == lane.getPermissions()]
103
if not cands2 and lane.allows("passenger"):
104
cands2 = [c for c in edge2.getLanes() if c.allows("passenger")]
105
if not cands2 and lane.allows("bus"):
106
cands2 = [c for c in edge2.getLanes() if c.allows("bus")]
107
if not cands2 and lane.allows("taxi"):
108
cands2 = [c for c in edge2.getLanes() if c.allows("taxi")]
109
if not cands2 and lane.allows("bicycle"):
110
cands2 = [c for c in edge2.getLanes() if c.allows("bicycle")]
111
if not cands2:
112
cands2 = edge2.getLanes()
113
lane2 = cands2[min(candIndex, len(cands2) - 1)].getID()
114
return lane2, pos2
115
116
117
def remap_edge(options, obj, edgeID, pos=None, size=None):
118
edge = options.net.getEdge(edgeID)
119
permissions = set(edge.getPermissions())
120
shape = edge.getShape()
121
shapelen = gh.polyLength(shape)
122
relpos = pos / edge.getLength() if pos else 0.5
123
if relpos < 0:
124
relpos += 1
125
shapePos = relpos * shapelen
126
x, y = gh.positionAtShapeOffset(shape, shapelen * relpos)
127
x2, y2 = options.remap_xy((x, y))
128
129
edges = options.net2.getNeighboringEdges(x2, y2, options.radius)
130
if not edges:
131
print("No edges near %.2f,%.2f (origEdge %s)" % (x, y, edgeID), file=sys.stderr)
132
return None, None
133
134
scut = size / 2 if size is not None else options.shapecut
135
cutFront = 0
136
end = []
137
cutEnd = 0
138
front = []
139
if shapePos + scut < shapelen:
140
# cut off at shape rear
141
cutEnd = shapelen - (shapePos + scut)
142
shape, end = gh.splitPolygonAtLengths2D(shape, [shapePos + scut])
143
shapelen -= cutEnd
144
if shapePos - scut > 0:
145
# cut off at shape front
146
cutFront = shapePos - scut
147
shapelen -= cutFront
148
front, shape = gh.splitPolygonAtLengths2D(shape, [shapePos - scut])
149
150
origAngle = gh.angleTo2D(shape[0], shape[-1])
151
152
cands = []
153
for e, d in edges:
154
if e.getLength() < scut:
155
angle = gh.angleTo2D(e.getFromNode().getCoord(), e.getToNode().getCoord())
156
else:
157
eShape = e.getShape()
158
offset = gh.polygonOffsetWithMinimumDistanceToPoint((x2, y2), eShape)
159
offset1 = max(0, offset - scut / 2)
160
offset2 = min(gh.polyLength(eShape), offset + scut / 2)
161
angle = gh.angleTo2D(gh.positionAtShapeOffset(eShape, offset1),
162
gh.positionAtShapeOffset(eShape, offset2))
163
if degrees(fabs(origAngle - angle)) < options.atol:
164
cands.append(e)
165
edges = cands
166
cands2 = [e for e in edges if permissions.issubset(e.getPermissions())]
167
if not cands2:
168
# relax permission requirements a minimum
169
for svc in ["passenger", "bus", "bicycle" "tram", "rail", "pedestrian"]:
170
if svc in permissions:
171
permissions = set([svc])
172
break
173
cands2 = [e for e in edges if permissions.issubset(e.getPermissions())]
174
if not cands2:
175
print("No edges that allow '%s' found near %.2f,%.2f (origEdge %s)" % (
176
" ".join(permissions), x2, y2, edgeID), file=sys.stderr)
177
return None, None
178
edges = cands2
179
180
if not edges:
181
print("No edges with angle %.2f found near %.2f,%.2f (origEdge %s)" % (
182
degrees(origAngle), x2, y2, edgeID), file=sys.stderr)
183
return None, None
184
185
shape2 = list(map(options.remap_xy, shape))
186
edge2 = None
187
bestDist = 1e10
188
for e in edges:
189
if shapelen > e.getLength():
190
maxDist = median([gh.distancePointToPolygon(xy, shape2) for xy in e.getShape()])
191
else:
192
maxDist = median([gh.distancePointToPolygon(xy, e.getShape()) for xy in shape2])
193
if maxDist < bestDist:
194
bestDist = maxDist
195
edge2 = e
196
197
if pos is not None:
198
edge2shapelen = gh.polyLength(edge2.getShape())
199
pos2 = gh.polygonOffsetWithMinimumDistanceToPoint((x2, y2), edge2.getShape())
200
pos2 = pos2 / edge2shapelen * edge2.getLength()
201
202
return edge2, pos2
203
204
205
POSITIONS = ['startPos', 'endPos', 'pos']
206
207
208
def getPosAttrs(obj):
209
result = []
210
for attr in POSITIONS:
211
if obj.hasAttribute(attr):
212
result.append((attr, float(getattr(obj, attr))))
213
return result
214
215
216
IDS = {
217
'edge': remap_edge,
218
'lane': remap_lane,
219
'edges': remap_edges,
220
'lanes': remap_lanes,
221
}
222
223
224
def remap(options, obj, level=1):
225
success = True
226
for attr, mapper in IDS.items():
227
if obj.hasAttribute(attr):
228
posAttrs = getPosAttrs(obj)
229
size = None
230
if len(posAttrs) == 0:
231
pos = None
232
elif len(posAttrs) == 1:
233
pos = posAttrs[0][1]
234
obj.setAttribute("friendlyPos", True)
235
else:
236
positions = [av[1] for av in posAttrs]
237
pos = sum(positions) / len(positions)
238
size = max(positions) - min(positions)
239
obj.setAttribute("friendlyPos", True)
240
id2, pos2 = mapper(options, obj, getattr(obj, attr), pos, size)
241
if id2:
242
obj.setAttribute(attr, id2)
243
for posAttr, posOrig in posAttrs:
244
obj.setAttribute(posAttr, "%.2f" % (pos2 + posOrig - pos))
245
else:
246
print("Could not map %s on %s '%s'" % (
247
obj.name, attr, getattr(obj, attr)),
248
file=sys.stderr)
249
if level == 1:
250
success = False
251
else:
252
obj.setCommented()
253
for child in obj.getChildList():
254
success &= remap(options, child, level + 1)
255
patchSpecialCases(options, obj, level)
256
return success
257
258
259
def patchSpecialCases(options, obj, level):
260
if level == 1:
261
accessEdges = set()
262
for child in obj.getChildList():
263
if child.name == "access" and not child.isCommented():
264
edge = lane2edge(child.lane)
265
if edge in accessEdges:
266
print("Disabling duplicate access on edge %s" % edge, file=sys.stderr)
267
child.setCommented()
268
else:
269
accessEdges.add(edge)
270
lane = options.net2.getLane(child.lane)
271
if not lane.allows("pedestrian"):
272
found = False
273
for cand in lane.getEdge().getLanes():
274
if cand.allows("pedestrian"):
275
child.lane = cand.getID()
276
found = True
277
break
278
if not found:
279
print("Disabling access on non-pedestrian lane %s" % child.lane, file=sys.stderr)
280
child.setCommented()
281
282
283
def main(options):
284
if options.verbose:
285
print("Reading orig-net '%s'" % options.origNet)
286
options.net = sumolib.net.readNet(options.origNet)
287
if options.verbose:
288
print("Reading target-net '%s'" % options.targetNet)
289
options.net2 = sumolib.net.readNet(options.targetNet)
290
291
if options.net.hasGeoProj() and options.net2.hasGeoProj():
292
def remap_xy(xy):
293
lon, lat = options.net.convertXY2LonLat(*xy)
294
return options.net2.convertLonLat2XY(lon, lat)
295
options.remap_xy = remap_xy
296
else:
297
options.remap_xy = lambda x: x
298
299
with openz(options.output, 'w') as fout:
300
sumolib.writeXMLHeader(fout, "$Id$", "additional", options=options)
301
for obj in parse(options.additional):
302
if not remap(options, obj):
303
obj.setCommented()
304
fout.write(obj.toXML(initialIndent=" " * 4))
305
fout.write("</additional>\n")
306
307
308
if __name__ == "__main__":
309
main(get_options())
310
311