Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/district/stationDistricts.py
169674 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 stationDistricts.py
15
# @author Jakob Erdmann
16
# @date 2023-02-22
17
18
"""
19
Segment a (railway) network in districts based on nearby stations
20
"""
21
from __future__ import absolute_import
22
from __future__ import print_function
23
import os
24
import sys
25
import random
26
from heapq import heappush, heappop
27
from collections import defaultdict
28
from itertools import chain
29
SUMO_HOME = os.environ.get('SUMO_HOME',
30
os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..'))
31
sys.path.append(os.path.join(SUMO_HOME, 'tools'))
32
import sumolib # noqa
33
from sumolib.options import ArgumentParser # noqa
34
from sumolib.miscutils import Colorgen, openz # noqa
35
import sumolib.geomhelper as gh # noqa
36
37
38
def get_options():
39
ap = ArgumentParser()
40
ap.add_option("-v", "--verbose", action="store_true", default=False,
41
help="tell me what you are doing")
42
ap.add_option("-n", "--net-file", dest="netfile", required=True, category="input", type=ap.net_file,
43
help="the network to read lane and edge permissions")
44
ap.add_option("-s", "--stop-file", dest="stopfile", required=True, category="input", type=ap.additional_file,
45
help="the additional file with stops")
46
ap.add_option("-o", "--output", required=True, category="output", type=ap.additional_file,
47
help="output taz file")
48
ap.add_option("--split-output", dest="splitOutput", category="output", type=ap.file,
49
help="generate splits for edges assigned to multiple stations")
50
ap.add_option("--poi-output", dest="poiOutput", category="output", type=ap.file,
51
help="generate a point of interest for every station")
52
ap.add_option("--vclasses", default="rail,rail_urban,subway", type=str,
53
help="Include only edges allowing VCLASS")
54
ap.add_option("--parallel-radius", type=float, default=100, dest="parallelRadius",
55
help="search radius for finding parallel edges")
56
ap.add_option("--merge", action="store_true", default=False,
57
help="merge stations that have a common edge")
58
ap.add_option("--hue", default="random", type=str,
59
help="hue for taz (float from [0,1] or 'random')")
60
ap.add_option("--saturation", default=1, type=str,
61
help="saturation for taz (float from [0,1] or 'random')")
62
ap.add_option("--brightness", default=1, type=str,
63
help="brightness for taz (float from [0,1] or 'random')")
64
ap.add_option("--seed", type=int, default=42, help="random seed")
65
options = ap.parse_args()
66
67
if not options.netfile:
68
ap.print_help()
69
ap.exit("Error! setting net-file is mandatory")
70
if not options.stopfile:
71
ap.print_help()
72
ap.exit("Error! setting stop-file is mandatory")
73
if not options.output:
74
ap.print_help()
75
ap.exit("Error! setting output is mandatory")
76
77
options.vclasses = options.vclasses.split(',')
78
options.colorgen = Colorgen((options.hue, options.saturation, options.brightness))
79
return options
80
81
82
class Station:
83
def __init__(self):
84
self.edges = set()
85
self.name = None
86
self.platforms = []
87
self.coord = None
88
89
def write(self, outf, outf_poi, index, color):
90
outf.write(u' <taz id="%s" name="%s" color="%s" edges="%s">\n' %
91
(index, self.name, color, ' '.join(sorted([e.getID() for e in self.edges]))))
92
if self.coord:
93
outf.write(u' <param key="coord" value="%s"/>\n' % ' '.join(map(str, self.coord)))
94
outf.write(u' </taz>\n')
95
96
if self.coord and outf_poi:
97
outf_poi.write(u' <poi id="%s" name="%s" x="%s" y="%s"/>\n' %
98
(index, self.name, self.coord[0], self.coord[1]))
99
100
101
def allowsAny(edge, vclasses):
102
for vclass in vclasses:
103
if edge.allows(vclass):
104
return True
105
return False
106
107
108
def initStations(options, net):
109
stations = defaultdict(Station)
110
111
numStops = 0
112
numIgnoredStops = 0
113
for stop in sumolib.xml.parse(options.stopfile, ['busStop', 'trainStop']):
114
name = stop.getAttributeSecure("attr_name", stop.id)
115
edgeID = stop.lane.rsplit('_', 1)[0]
116
if not net.hasEdge(edgeID):
117
sys.stderr.write("Unknown edge '%s' in stop '%s'" % (edgeID, stop.id))
118
continue
119
edge = net.getEdge(edgeID)
120
if not allowsAny(edge, options.vclasses):
121
numIgnoredStops += 1
122
continue
123
station = stations[name]
124
station.edges.add(edge)
125
station.name = name
126
begCoord = sumolib.geomhelper.positionAtShapeOffset(edge.getShape(), float(stop.startPos))
127
endCoord = sumolib.geomhelper.positionAtShapeOffset(edge.getShape(), float(stop.endPos))
128
station.platforms.append([begCoord, endCoord])
129
numStops += 1
130
131
if options.verbose:
132
print("Read %s stops and %s stations" % (numStops, len(stations)))
133
if numIgnoredStops:
134
print("Ignored %s stops because they did not allow any of the vclasses '%s'" % (
135
numIgnoredStops, ','.join(options.vclasses)))
136
137
return stations
138
139
140
def findParallel(options, net, stations):
141
for station in stations.values():
142
coords = sum(station.platforms, [])
143
station.coord = (
144
round(sum([c[0] for c in coords]) / len(coords), 3),
145
round(sum([c[1] for c in coords]) / len(coords), 3))
146
147
for edge, dist in net.getNeighboringEdges(station.coord[0], station.coord[1], options.parallelRadius):
148
station.edges.add(edge)
149
150
151
def findGroup(mergedStations, station):
152
for group in mergedStations:
153
if station in group:
154
return group
155
assert False
156
157
158
def mergeGroups(stations, mergedStations, group1, group2):
159
if group1 is None or group1 == group2:
160
return group2
161
162
name1 = '|'.join(sorted(group1))
163
name2 = '|'.join(sorted(group2))
164
mergedGroup = group1
165
mergedGroup.update(group2)
166
mergedName = '|'.join(sorted(group1))
167
mergedStations.remove(group2)
168
169
station1 = stations[name1]
170
station2 = stations[name2]
171
mergedStation = Station()
172
mergedStation.name = mergedName
173
mergedStation.edges.update(station1.edges)
174
mergedStation.edges.update(station2.edges)
175
mergedStation.platforms = station1.platforms + station2.platforms
176
del stations[name1]
177
del stations[name2]
178
stations[mergedName] = mergedStation
179
return mergedGroup
180
181
182
def mergeStations(stations, verbose=False):
183
"""merge stations with shared edges"""
184
185
initialStations = len(stations)
186
edgeStation = defaultdict(set)
187
for station in stations.values():
188
for edge in station.edges:
189
edgeStation[edge].add(station.name)
190
191
mergedStations = [set([name]) for name in stations.keys()]
192
193
for edge, stationNames in edgeStation.items():
194
mergedGroup = None
195
for station in stationNames:
196
mergedGroup = mergeGroups(stations, mergedStations, mergedGroup, findGroup(mergedStations, station))
197
198
finalStations = len(stations)
199
if verbose and finalStations != initialStations:
200
print("Merged %s stations" % (initialStations - finalStations))
201
202
203
def splitStations(options, stations):
204
edgeStation = defaultdict(set)
205
for station in stations.values():
206
for edge in station.edges:
207
edgeStation[edge].add(station)
208
209
bidiSplits = defaultdict(list)
210
with openz(options.splitOutput, 'w') as outf:
211
sumolib.writeXMLHeader(outf, "$Id$", "edges", schemaPath="edgediff_file.xsd", options=options)
212
for edge, stations in sorted(edgeStation.items(), key=lambda e: e[0].getID()):
213
if len(stations) == 1:
214
continue
215
shape = edge.getShape(True)
216
stationOffsets = []
217
for station in stations:
218
offset = gh.polygonOffsetWithMinimumDistanceToPoint(station.coord, shape, perpendicular=False)
219
stationOffsets.append((offset, station.name))
220
stationOffsets.sort()
221
222
splits = []
223
for (o1, n1), (o2, n2) in zip(stationOffsets[:-1], stationOffsets[1:]):
224
if o1 != o2:
225
pos = (o1 + o2) / 2
226
splits.append((pos, "%s.%s" % (edge.getID(), int(pos))))
227
else:
228
sys.stderr.write("Cannot split edge '%s' between stations '%s' and '%s'\n" % (
229
edge.getID(), n1, n2))
230
231
if edge.getBidi():
232
if edge.getBidi() in bidiSplits:
233
bidiLength = edge.getBidi().getLength()
234
splits = [(bidiLength - p, n) for p, n in reversed(bidiSplits[edge.getBidi()])]
235
else:
236
bidiSplits[edge] = splits
237
238
outf.write(u' <edge id="%s">\n' % edge.getID())
239
for pos, nodeID in splits:
240
outf.write(u' <split pos="%s" id="%s"/>\n' % (pos, nodeID))
241
outf.write(u' </edge>\n')
242
243
outf.write(u"</edges>\n")
244
245
246
def assignByDistance(options, net, stations):
247
"""assign edges to closest station"""
248
edgeStation = dict()
249
for station in stations.values():
250
for edge in station.edges:
251
assert edge not in edgeStation or not options.merge
252
edgeStation[edge] = station.name
253
254
remaining = set()
255
for edge in net.getEdges():
256
if edge not in edgeStation and allowsAny(edge, options.vclasses):
257
remaining.add(edge)
258
259
seen = set(edgeStation.keys())
260
heap = []
261
for edge, station in edgeStation.items():
262
for neigh in chain(edge.getOutgoing().keys(), edge.getIncoming().keys()):
263
if neigh not in seen:
264
heappush(heap, (neigh.getLength(), neigh.getID(), station))
265
266
while heap:
267
dist, candID, station = heappop(heap)
268
cand = net.getEdge(candID)
269
seen.add(cand)
270
if cand in remaining:
271
stations[station].edges.add(cand)
272
remaining.remove(cand)
273
for neigh in chain(cand.getOutgoing().keys(), cand.getIncoming().keys()):
274
if neigh not in seen:
275
heappush(heap, (neigh.getLength() + dist, neigh.getID(), station))
276
277
278
def main(options):
279
random.seed(options.seed)
280
281
if options.verbose:
282
print("Reading net")
283
net = sumolib.net.readNet(options.netfile)
284
285
stations = initStations(options, net)
286
findParallel(options, net, stations)
287
if options.merge:
288
mergeStations(stations, options.verbose)
289
elif options.splitOutput:
290
splitStations(options, stations)
291
assignByDistance(options, net, stations)
292
293
outf_poi = None
294
if options.poiOutput:
295
outf_poi = openz(options.poiOutput, 'w')
296
sumolib.writeXMLHeader(outf_poi, "$Id$", "additional", options=options)
297
298
with openz(options.output, 'w') as outf:
299
sumolib.writeXMLHeader(outf, "$Id$", "additional", options=options)
300
for i, name in enumerate(sorted(stations.keys())):
301
stations[name].write(outf, outf_poi, i, options.colorgen())
302
outf.write(u"</additional>\n")
303
304
if outf_poi:
305
outf_poi.close()
306
307
308
if __name__ == "__main__":
309
main(get_options())
310
311