Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/shapes/poly2edgedata.py
193745 views
1
#!/usr/bin/env python
2
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3
# Copyright (C) 2010-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 poly2edgedata.py
15
# @author Jakob Erdmann
16
# @date 2025-01-23
17
18
"""
19
Transform polygons with params into edgedata with attributes
20
For each polygon a unique edge is selected that gives the best geometrical match
21
22
The following syntax is supported in the patch file (one patch per line):
23
24
# lines starting with '#' are ignored as comments
25
# rev overrides the reverse edge of EDGEID to be REVEDGEID
26
rev EDGEID REVEDGEID
27
# edg overrides the edge to assign for POLYID to be EDGEID
28
edg POLYID EDGEID
29
# dat overrrides the data attribute ATTR for POLYID to take on value VALUE
30
dat POLYID ATTR VALUE
31
32
any ID or VALUE may bet set to 'None' to signify that
33
- a reverse edge should not be assigned
34
- a polygon should not be mapped
35
- data should be ignored
36
"""
37
38
from __future__ import print_function
39
from __future__ import absolute_import
40
import os
41
import sys
42
from math import fabs, degrees
43
from collections import defaultdict
44
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
45
sys.path.append(os.path.join(THIS_DIR, '..'))
46
import sumolib # noqa
47
import sumolib.geomhelper as gh # noqa
48
49
PATCH_NONE = 'None'
50
51
52
def get_options(args=None):
53
op = sumolib.options.ArgumentParser(description="Transform polygons with params into edgedata with attributes")
54
55
op.add_option("polyfiles", nargs='+', category="input", type=op.file_list,
56
help="List of polygon files to convert")
57
op.add_option("-n", "--netfile", category="input", required=True, type=op.net_file,
58
help="Network file on which to map the polygons")
59
op.add_option("-p", "--patchfile", category="input", type=op.file,
60
help="Load a file with patches to apply during import")
61
op.add_option("-r", "--radius", type=float, default=20,
62
help="radius for finding edges near polygons")
63
op.add_option("--shapecut", type=float, default=40,
64
help="Shorten polygon and edge shapes to FLOAT to increase robustness of angular comparison")
65
op.add_option("--angle-tolerance", type=float, default=20, dest="atol",
66
help="Match polygons and edges if their angle differs by no more than DEGRESS")
67
op.add_option("--min-length", type=float, default=10, dest="minLength",
68
help="minimum edge length that may be mapped to")
69
op.add_option("-s", "--split-attributes", dest="splitAttrs",
70
help="If a reverse edge is found, split the values of the given attribute list among edge and reverse edge") # noqa
71
op.add_option("-S", "--nosplit-attributes", dest="noSplitAttrs",
72
help="If a reverse edge is found, split the values of all attributes except the given attribute list among edge and reverse edge") # noqa
73
op.add_option("-f", "--filter", dest="filter",
74
help="Read a list of triplets ATTR,MIN,MAX and only keep polygons where value ATTR is within [MIN,MAX]") # noqa
75
op.add_option("-b", "--begin", default=0, type=op.time,
76
help="edgedata interval begin time")
77
op.add_option("-e", "--end", default="1:0:0:0", type=op.time,
78
help="edgedata interval end time)")
79
op.add_option("-o", "--output-file", category="output", dest="outfile", required=True, type=op.file,
80
help="output file")
81
82
try:
83
options = op.parse_args()
84
except (NotImplementedError, ValueError) as e:
85
print(e, file=sys.stderr)
86
sys.exit(1)
87
88
options.splitAttrs = set(options.splitAttrs.split(',')) if options.splitAttrs else []
89
options.noSplitAttrs = set(options.noSplitAttrs.split(',')) if options.noSplitAttrs else []
90
tuples = options.filter.split(',') if options.filter else []
91
options.filter = {} # attr -> (min, max)
92
for i in range(0, len(tuples), 3):
93
options.filter[tuples[i]] = (float(tuples[i + 1]), float(tuples[i + 2]))
94
return options
95
96
97
def hasReverse(edge):
98
for cand in edge.getToNode().getOutgoing():
99
if cand.getToNode() == edge.getFromNode():
100
return True
101
return False
102
103
104
def readPatches(net, pfile):
105
patchEdg = {} # polyID->edge
106
patchRev = {} # forwardEdge->reverseEdge
107
patchDat = defaultdict(lambda: {}) # polyID->attr->data (-1 ignores)
108
if pfile is not None:
109
with open(pfile) as pf:
110
for line in pf:
111
items = line.split()
112
patchtype = items[0]
113
if patchtype == "rev":
114
edgeID, reverseID = items[1:]
115
patchRev[edgeID] = reverseID
116
net.getEdge(edgeID) # provoke error if edgeID does not exist in net
117
elif patchtype == "edg":
118
polyID, edgeID = items[1:]
119
patchEdg[polyID] = edgeID
120
elif patchtype == "dat":
121
polyID, attrName, value = items[1:]
122
patchDat[polyID][attrName] = value
123
elif patchtype == "#":
124
# comment
125
continue
126
else:
127
print("unknown patchtype '%s'" % patchtype)
128
129
return patchEdg, patchRev, patchDat
130
131
132
def main(options):
133
net = sumolib.net.readNet(options.netfile)
134
patchEdg, patchRev, patchDat = readPatches(net, options.patchfile)
135
usedEdges = set() # do not assign different polygons/counts to the same edge
136
scut = options.shapecut
137
138
with open(options.outfile, 'w') as foutobj:
139
foutobj.write('<meandata>\n')
140
foutobj.write(' <interval begin="%s" end="%s" id="%s">\n' % (
141
options.begin, options.end, options.polyfiles[0]))
142
for fname in options.polyfiles:
143
for poly in sumolib.xml.parse(fname, 'poly'):
144
145
shape = []
146
for lonlat in poly.shape.split():
147
lon, lat = lonlat.split(',')
148
shape.append(net.convertLonLat2XY(float(lon), float(lat)))
149
shapelen = gh.polyLength(shape)
150
cx, cy = gh.positionAtShapeOffset(shape, shapelen / 2)
151
edges = net.getNeighboringEdges(cx, cy, options.radius)
152
if not edges:
153
print("No edges near %.2f,%.2f (poly %s)" % (cx, cy, poly.id), file=sys.stderr)
154
continue
155
edges = [(e, d) for e, d in edges if e.allows("passenger")]
156
if not edges:
157
print("No car edges near %.2f,%.2f (poly %s)" % (cx, cy, poly.id), file=sys.stderr)
158
continue
159
edges = [(e, d) for e, d in edges if e.getLength() >= options.minLength]
160
if not edges:
161
print("No long edges near %.2f,%.2f (poly %s)" % (cx, cy, poly.id), file=sys.stderr)
162
continue
163
164
if shapelen < scut:
165
polyAngle = gh.angleTo2D(shape[0], shape[-1])
166
else:
167
polyAngle = gh.angleTo2D(gh.positionAtShapeOffset(shape, shapelen / 2 - scut / 2),
168
gh.positionAtShapeOffset(shape, shapelen / 2 + scut / 2))
169
170
cands = []
171
for e, d in edges:
172
if e.getLength() < scut:
173
angle = gh.angleTo2D(e.getFromNode().getCoord(), e.getToNode().getCoord())
174
revAngle = gh.angleTo2D(e.getToNode().getCoord(), e.getFromNode().getCoord())
175
else:
176
eShape = e.getShape()
177
offset = gh.polygonOffsetWithMinimumDistanceToPoint((cx, cy), eShape)
178
offset1 = max(0, offset - scut / 2)
179
offset2 = min(gh.polyLength(eShape), offset + scut / 2)
180
angle = gh.angleTo2D(gh.positionAtShapeOffset(eShape, offset1),
181
gh.positionAtShapeOffset(eShape, offset2))
182
revAngle = gh.angleTo2D(gh.positionAtShapeOffset(eShape, offset2),
183
gh.positionAtShapeOffset(eShape, offset1))
184
if ((degrees(fabs(polyAngle - angle)) < options.atol or
185
degrees(fabs(polyAngle - revAngle)) < options.atol)):
186
cands.append(e)
187
edges = cands
188
if not edges:
189
print("No edges with angle %.2f found near %.2f,%.2f (poly %s)" % (
190
degrees(polyAngle), cx, cy, poly.id), file=sys.stderr)
191
continue
192
193
bestDist = 1e10
194
bestEdge = None
195
bestReverse = None
196
for e in edges:
197
if shapelen > e.getLength():
198
maxDist = max([gh.distancePointToPolygon(xy, shape) for xy in e.getShape()])
199
else:
200
maxDist = max([gh.distancePointToPolygon(xy, e.getShape()) for xy in shape])
201
if maxDist < bestDist:
202
bestDist = maxDist
203
bestEdge = e
204
# apply edge patch
205
if poly.id in patchEdg:
206
if patchEdg[poly.id] == PATCH_NONE:
207
continue
208
bestEdge = net.getEdge(patchEdg[poly.id])
209
210
if bestEdge in usedEdges:
211
patchInfo = " (was patched)" if poly.id in patchEdg else ""
212
print("Duplicate assignment to edge %s from poly %s%s" % (
213
bestEdge.getID(), poly.id, patchInfo), file=sys.stderr)
214
continue
215
# find opposite direction for undivided road
216
for e in edges:
217
if e.getFromNode() == bestEdge.getToNode() and e.getToNode() == bestEdge.getFromNode():
218
bestReverse = e
219
break
220
# apply revers edge patch
221
if bestEdge.getID() in patchRev:
222
revID = patchRev.get(bestEdge.getID())
223
if revID != PATCH_NONE:
224
bestReverse = net.getEdge(revID)
225
else:
226
bestReverse = None
227
# find opposite direction for divided road
228
elif bestReverse is None:
229
bestDist = 1e10
230
bestAngle = gh.angleTo2D(bestEdge.getFromNode().getCoord(), bestEdge.getToNode().getCoord())
231
for e in edges:
232
if hasReverse(e):
233
continue
234
reverseAngle = gh.angleTo2D(e.getToNode().getCoord(), e.getFromNode().getCoord())
235
if degrees(fabs(bestAngle - reverseAngle)) < 20:
236
maxDist = max([gh.distancePointToPolygon(xy, bestEdge.getShape()) for xy in e.getShape()])
237
if maxDist < bestDist:
238
bestDist = maxDist
239
bestReverse = e
240
241
if bestReverse in usedEdges:
242
patchInfo = " (was patched)" if bestEdge.getID() in patchRev else ""
243
print("Duplicate assignment to reverse edge %s from poly %s%s" % (
244
bestReverse.getID(), poly.id, patchInfo), file=sys.stderr)
245
continue
246
247
attrs = 'polyID="%s"' % poly.id
248
skip = False
249
if poly.param:
250
for param in poly.param:
251
value = param.value
252
if poly.id in patchDat:
253
if param.key in patchDat[poly.id]:
254
value = patchDat[poly.id][param.key]
255
# print("patched %s to %s" % (param.value, value))
256
if value == PATCH_NONE:
257
continue
258
if param.key in options.filter:
259
if ((float(value) < options.filter[param.key][0] or
260
float(value) > options.filter[param.key][1])):
261
skip = True
262
if (bestReverse is not None
263
and (param.key in options.splitAttrs
264
or options.noSplitAttrs and param.key not in options.noSplitAttrs)):
265
try:
266
value = float(value) / 2
267
except ValueError:
268
pass
269
attrs += ' %s="%s"' % (param.key, value)
270
271
if skip:
272
continue
273
comment = ''
274
if bestReverse is not None:
275
usedEdges.add(bestReverse)
276
comment = ' <!-- reverse: %s -->' % bestEdge.getID()
277
foutobj.write(' <edge id="%s" %s/> %s\n' % (bestReverse.getID(), attrs, comment))
278
comment = ' <!-- reverse: %s -->' % bestReverse.getID()
279
usedEdges.add(bestEdge)
280
foutobj.write(' <edge id="%s" %s/> %s\n' % (bestEdge.getID(), attrs, comment))
281
foutobj.write(' </interval>\n')
282
foutobj.write('</meandata>\n')
283
284
285
if __name__ == "__main__":
286
main(get_options())
287
288