1#!/usr/local/bin/python3.8
2# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.org/sumo
3# Copyright (C) 2012-2019 German Aerospace Center (DLR) and others.
4# This program and the accompanying materials
5# are made available under the terms of the Eclipse Public License v2.0
6# which accompanies this distribution, and is available at
7# http://www.eclipse.org/legal/epl-v20.html
8# SPDX-License-Identifier: EPL-2.0
9
10# @file    route2poly.py
11# @author  Jakob Erdmann
12# @author  Michael Behrisch
13# @date    2012-11-15
14# @version $Id$
15
16"""
17From a sumo network and a route file, this script generates a polygon (polyline) for every route
18which can be loaded with sumo-gui for visualization
19"""
20from __future__ import absolute_import
21import sys
22import os
23import itertools
24import random
25from collections import defaultdict
26from optparse import OptionParser
27sys.path.append(os.path.join(os.environ['SUMO_HOME'], 'tools'))
28from sumolib.output import parse  # noqa
29from sumolib.net import readNet  # noqa
30from sumolib.miscutils import Colorgen  # noqa
31from sumolib import geomhelper  # noqa
32
33
34def parse_args(args):
35    USAGE = "Usage: " + sys.argv[0] + " <netfile> <routefile> [options]"
36    optParser = OptionParser()
37    optParser.add_option("-o", "--outfile", help="name of output file")
38    optParser.add_option("-u", "--hue", default="random",
39                         help="hue for polygons (float from [0,1] or 'random')")
40    optParser.add_option("-s", "--saturation", default=1,
41                         help="saturation for polygons (float from [0,1] or 'random')")
42    optParser.add_option("-b", "--brightness", default=1,
43                         help="brightness for polygons (float from [0,1] or 'random')")
44    optParser.add_option("-l", "--layer", default=100, help="layer for generated polygons")
45    optParser.add_option("--geo", action="store_true",
46                         default=False, help="write polgyons with geo-coordinates")
47    optParser.add_option("--internal", action="store_true",
48                         default=False, help="include internal edges in generated shapes")
49    optParser.add_option("--spread", type="float", help="spread polygons laterally to avoid overlap")
50    optParser.add_option("--blur", type="float",
51                         default=0, help="maximum random disturbance to route geometry")
52    optParser.add_option("--scale-width", type="float", dest="scaleWidth",
53                         help="group similar routes and scale width by " +
54                              "group size multiplied with the given factor (in m)")
55    optParser.add_option("--standalone", action="store_true", default=False,
56                         help="Parse stand-alone routes that are not define as child-element of a vehicle")
57    optParser.add_option("--filter-output.file", dest="filterOutputFile",
58                         help="only write output for edges in the given selection file")
59    optParser.add_option("--seed", type="int", help="random seed")
60    options, args = optParser.parse_args(args=args)
61    if options.seed:
62        random.seed(options.seed)
63    if len(args) < 2:
64        sys.exit(USAGE)
65    try:
66        options.net = args[0]
67        options.routefiles = args[1:]
68        options.colorgen = Colorgen(
69            (options.hue, options.saturation, options.brightness))
70    except Exception:
71        sys.exit(USAGE)
72    if options.outfile is None:
73        options.outfile = options.routefiles[0] + ".poly.xml"
74
75    return options
76
77
78def randomize_pos(pos, blur):
79    return tuple([val + random.uniform(-blur, blur) for val in pos])
80
81
82MISSING_EDGES = set()
83SPREAD = defaultdict(set)
84SPREAD_MAX = [0]
85
86
87def getSpread(lanes):
88    """find the smalles spread value that is available for all lanes"""
89    cands = [0]
90    for i in range(1, SPREAD_MAX[0] + 2):
91        cands += [i, -i]
92    for i in cands:
93        if all([i not in SPREAD[l] for l in lanes]):
94            SPREAD_MAX[0] = max(SPREAD_MAX[0], i)
95            [SPREAD[l].add(i) for l in lanes]
96            return i
97        else:
98            pass
99            # print(i, [l.getID() for l in lanes])
100    assert(False)
101
102
103def generate_poly(options, net, id, color, edges, outf, type="route", lineWidth=None, params={}):
104    lanes = []
105    spread = 0
106    for e in edges:
107        if net.hasEdge(e):
108            lanes.append(net.getEdge(e).getLane(0))
109        else:
110            if e not in MISSING_EDGES:
111                sys.stderr.write("Warning: unknown edge '%s'\n" % e)
112                MISSING_EDGES.add(e)
113    if not lanes:
114        return
115    if options.internal and len(lanes) > 1:
116        lanes2 = []
117        preferedCon = -1
118        for i, lane in enumerate(lanes):
119            edge = lane.getEdge()
120            if i == 0:
121                cons = edge.getConnections(lanes[i + 1].getEdge())
122                if cons:
123                    lanes2.append(cons[preferedCon].getFromLane())
124                else:
125                    lanes2.append(lane)
126            else:
127                cons = lanes[i - 1].getEdge().getConnections(edge)
128                if cons:
129                    viaID = cons[preferedCon].getViaLaneID()
130                    if viaID:
131                        via = net.getLane(viaID)
132                        lanes2.append(via)
133                        cons2 = via.getEdge().getConnections(edge)
134                        if cons2:
135                            viaID2 = cons2[preferedCon].getViaLaneID()
136                            if viaID2:
137                                via2 = net.getLane(viaID2)
138                                lanes2.append(via2)
139                        lanes2.append(cons[preferedCon].getToLane())
140                else:
141                    lanes2.append(lane)
142        lanes = lanes2
143
144    shape = list(itertools.chain(*list(l.getShape() for l in lanes)))
145    if options.spread:
146        spread = getSpread(lanes)
147        if spread:
148            shape = geomhelper.move2side(shape, options.spread * spread)
149            params["spread"] = str(spread)
150    if options.blur > 0:
151        shape = [randomize_pos(pos, options.blur) for pos in shape]
152
153    geoFlag = ""
154    lineWidth = '' if lineWidth is None else ' lineWidth="%s"' % lineWidth
155    if options.geo:
156        shape = [net.convertXY2LonLat(*pos) for pos in shape]
157        geoFlag = ' geo="true"'
158    shapeString = ' '.join('%s,%s' % (x, y) for x, y in shape)
159    close = '/'
160    if params:
161        close = ''
162    outf.write('<poly id="%s" color="%s" layer="%s" type="%s" shape="%s"%s%s%s>\n' % (
163        id, color, options.layer, type, shapeString, geoFlag, lineWidth, close))
164    if params:
165        for key, value in params.items():
166            outf.write('    <param key="%s" value="%s"/>\n' % (key, value))
167        outf.write('</poly>\n')
168
169
170def filterEdges(edges, keep):
171    if keep is None:
172        return edges
173    else:
174        return [e for e in edges if e in keep]
175
176
177def parseRoutes(options):
178    known_ids = set()
179
180    def unique_id(cand, index=0):
181        cand2 = cand
182        if index > 0:
183            cand2 = "%s#%s" % (cand, index)
184        if cand2 in known_ids:
185            return unique_id(cand, index + 1)
186        else:
187            known_ids.add(cand2)
188            return cand2
189
190    keep = None
191    if options.filterOutputFile is not None:
192        keep = set()
193        for line in open(options.filterOutputFile):
194            if line.startswith('edge:'):
195                keep.add(line.replace('edge:', '').strip())
196
197    for routefile in options.routefiles:
198        print("parsing %s" % routefile)
199        if options.standalone:
200            for route in parse(routefile, 'route'):
201                # print("found veh", vehicle.id)
202                yield unique_id(route.id), filterEdges(route.edges.split(), keep)
203        else:
204            for vehicle in parse(routefile, 'vehicle'):
205                # print("found veh", vehicle.id)
206                yield unique_id(vehicle.id), filterEdges(vehicle.route[0].edges.split(), keep)
207
208
209def main(args):
210    options = parse_args(args)
211    net = readNet(options.net, withInternal=options.internal)
212
213    with open(options.outfile, 'w') as outf:
214        outf.write('<polygons>\n')
215        if options.scaleWidth is None:
216            for route_id, edges in parseRoutes(options):
217                generate_poly(options, net, route_id, options.colorgen(), edges, outf)
218        else:
219            count = {}
220            for route_id, edges in parseRoutes(options):
221                edges = tuple(edges)
222                if edges in count:
223                    count[edges][0] += 1
224                else:
225                    count[edges] = [1, route_id]
226            for edges, (n, route_id) in count.items():
227                width = options.scaleWidth * n
228                params = {'count': str(n)}
229                generate_poly(options, net, route_id, options.colorgen(), edges, outf, lineWidth=width, params=params)
230
231        outf.write('</polygons>\n')
232
233
234if __name__ == "__main__":
235    main(sys.argv[1:])
236