from __future__ import absolute_import
from __future__ import print_function
import os
import sys
import stat
import traceback
import webbrowser
import datetime
import json
import threading
import subprocess
import tempfile
import shutil
from zipfile import ZipFile
import base64
import ssl
import collections
from http.server import HTTPServer, SimpleHTTPRequestHandler
import time
import osmGet
import osmBuild
import randomTrips
import ptlines2flows
import tileGet
import sumolib
from webWizard.SimpleWebSocketServer import SimpleWebSocketServer, WebSocket
SUMO_HOME = os.environ.get("SUMO_HOME", os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
DEFAULT_PORT = 8010
try:
basestring
except NameError:
basestring = str
typemapdir = os.path.join("${SUMO_HOME}" if "SUMO_HOME" in os.environ else SUMO_HOME, "data", "typemap")
typemaps = {
"net": os.path.join(typemapdir, "osmNetconvert.typ.xml"),
"poly": os.path.join(typemapdir, "osmPolyconvert.typ.xml"),
"urban": os.path.join(typemapdir, "osmNetconvertUrbanDe.typ.xml"),
"pedestrians": os.path.join(typemapdir, "osmNetconvertPedestrians.typ.xml"),
"ships": os.path.join(typemapdir, "osmNetconvertShips.typ.xml"),
"bicycles": os.path.join(typemapdir, "osmNetconvertBicycle.typ.xml"),
"aerialway": os.path.join(typemapdir, "osmNetconvertAerialway.typ.xml"),
}
CP = ["--trip-attributes", 'departLane="best"',
"--fringe-start-attributes", 'departSpeed="max"',
"--validate", "--remove-loops",
"--via-edge-types", ','.join(["highway.motorway",
"highway.motorway_link",
"highway.trunk_link",
"highway.primary_link",
"highway.secondary_link",
"highway.tertiary_link"])
]
PP = ["--vehicle-class", "pedestrian", "--prefix", "ped", ]
def getParams(vClass, prefix=None):
if prefix is None:
prefix = vClass
return ["--vehicle-class", vClass, "--vclass", vClass, "--prefix", prefix]
vehicleParameters = {
"passenger": CP + getParams("passenger", "veh") + ["--min-distance", "300", "--min-distance.fringe", "10",
"--allow-fringe.min-length", "1000", "--lanes"],
"truck": CP + getParams("truck") + ["--min-distance", "600", "--min-distance.fringe", "10"],
"bus": CP + getParams("bus") + ["--min-distance", "600", "--min-distance.fringe", "10"],
"motorcycle": CP + getParams("motorcycle") + ["--max-distance", "1200"],
"bicycle": CP + getParams("bicycle", "bike") + ["--max-distance", "8000"],
"tram": CP + getParams("tram") + ["--min-distance", "1200", "--min-distance.fringe", "10"],
"rail_urban": CP + getParams("rail_urban") + ["--min-distance", "1800", "--min-distance.fringe", "10"],
"rail": CP + getParams("rail") + ["--min-distance", "2400", "--min-distance.fringe", "10"],
"ship": getParams("ship") + ["--fringe-start-attributes", 'departSpeed="max"', "--validate"],
"pedestrian": PP + ["--pedestrians", "--max-distance", "2000"],
"persontrips": PP + ["--persontrips", "--trip-attributes", 'modes="public"'],
}
vehicleNames = {
"passenger": "Cars",
"truck": "Trucks",
"bus": "Bus",
"motorcycle": "Motorcycles",
"bicycle": "Bicycles",
"pedestrian": "Pedestrians",
"tram": "Trams",
"rail_urban": "Urban Trains",
"rail": "Trains",
"ship": "Ships"
}
BATCH_MODE = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH
BATCH_MODE |= stat.S_IWUSR
BATCH_MODE |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
def quoted_str(s):
if isinstance(s, float):
return "%.6f" % s
elif not isinstance(s, str):
return str(s)
elif '"' in s or ' ' in s:
return '"' + s.replace('"', '\\"') + '"'
else:
return s
class Builder(object):
prefix = "osm"
def __init__(self, data, local):
self.files = {}
self.files_relative = {}
self.data = data
self.tmp = None
if local:
now = data.get("outputDir", datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"))
for base in ['', os.path.expanduser('~/Sumo')]:
try:
self.tmp = os.path.abspath(os.path.join(base, now))
os.makedirs(self.tmp, exist_ok=data.get("outputDirExistOk", False))
break
except Exception:
print("Cannot create directory '%s'." % self.tmp, file=sys.stderr)
self.tmp = None
if self.tmp is None:
self.tmp = tempfile.mkdtemp()
self.origDir = os.getcwd()
print("Building scenario in '%s'." % self.tmp)
def report(self, message):
pass
def filename(self, use, name, usePrefix=True):
prefix = self.prefix if usePrefix else ''
self.files_relative[use] = prefix + name
self.files[use] = os.path.join(self.tmp, prefix + name)
def getRelative(self, options):
result = []
dirname = self.tmp
ld = len(dirname)
for o in options:
if isinstance(o, basestring) and o[:ld] == dirname:
remove = o[:ld+1]
result.append(o.replace(remove, ''))
else:
result.append(o)
return result
def build(self):
self.filename("osm", "_bbox.osm.xml.gz")
self.filename("ogc", ".ogcfg")
self.filename("net", ".net.xml.gz")
if self.data.get("coords") is None:
self.files["osm"] = self.data['osm']
else:
self.report("Downloading map data")
osmArgs = ["-b=" + (",".join(map(str, self.data["coords"]))), "-p", self.prefix, "-d", self.tmp, "-z"]
if self.data["poly"]:
osmArgs.append("--shapes")
if 'osmMirror' in self.data:
osmArgs += ["-u", self.data["osmMirror"]]
if 'roadTypes' in self.data:
osmArgs += ["-r", json.dumps(self.data["roadTypes"])]
subprocess.call([sys.executable, osmGet.__file__] + osmArgs + ['-C', self.files['ogc']])
osmGet.get(osmArgs)
if not os.path.exists(self.files["osm"]):
raise RuntimeError("Download failed")
options = ["-f", self.files["osm"], "-p", self.prefix, "-d", self.tmp, "-z"]
self.additionalFiles = []
self.routenames = []
if self.data["poly"]:
self.filename("poly", ".poly.xml.gz")
options += ["-m", typemaps["poly"]]
self.additionalFiles.append(self.files["poly"])
typefiles = [typemaps["net"]]
netconvertOptions = " " + osmBuild.DEFAULT_NETCONVERT_OPTS
if self.data.get("options"):
netconvertOptions += "," + self.data["options"]
netconvertOptions += ",--tls.default-type,actuated"
if "pedestrian" in self.data["vehicles"]:
netconvertOptions += ",--crossings.guess"
netconvertOptions += ",--osm.sidewalks"
typefiles.append(typemaps["urban"])
typefiles.append(typemaps["pedestrians"])
if ("ship" in self.data["vehicles"] or
(self.data.get("publicTransport")
and self.data.get("ptModes")
and self.data.get("ptModes").get("ferry"))):
typefiles.append(typemaps["ships"])
if "bicycle" in self.data["vehicles"]:
typefiles.append(typemaps["bicycles"])
netconvertOptions += ",--osm.bike-access"
if self.data["publicTransport"]:
self.filename("stops", "_stops.add.xml")
netconvertOptions += ",--ptstop-output,%s" % self.files["stops"]
netconvertOptions += ",--ptline-clean-up"
self.filename("ptlines", "_ptlines.xml")
self.filename("ptroutes", "_pt.rou.xml")
netconvertOptions += ",--ptline-output,%s" % self.files["ptlines"]
self.additionalFiles.append(self.files["stops"])
netconvertOptions += ",--railway.topology.repair"
typefiles.append(typemaps["aerialway"])
if self.data["leftHand"]:
netconvertOptions += ",--lefthand"
if self.data.get("verbose"):
netconvertOptions += ",--verbose"
if self.data["decal"]:
netconvertOptions += ",--proj,+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs"
if self.data["carOnlyNetwork"]:
if self.data["publicTransport"]:
options += ["--vehicle-classes", "publicTransport"]
else:
options += ["--vehicle-classes", "passenger"]
options += ["--netconvert-typemap", ','.join(typefiles)]
options += ["--netconvert-options", netconvertOptions]
options += ["--polyconvert-options", " -v,--osm.keep-full-type,--osm.merge-relations,1"]
self.report("Converting map data")
osmBuild.build(options)
ptOptions = None
hasPTFlows = False
begin = self.data.get("begin", 0)
if self.data["publicTransport"]:
self.report("Generating public transport schedule")
self.filename("pt_stopinfos", "stopinfos.xml", False)
self.filename("pt_vehroutes", "vehroutes.xml", False)
self.filename("pt_trips", "trips.trips.xml", False)
ptOptions = [
"-n", self.files["net"],
"-b", begin,
"-e", begin + self.data["duration"],
"-p", "600",
"--random-begin",
"--seed", "42",
"--ptstops", self.files["stops"],
"--ptlines", self.files["ptlines"],
"-o", self.files["ptroutes"],
"--ignore-errors",
"--vtype-prefix", "pt_",
"--stopinfos-file", self.files["pt_stopinfos"],
"--routes-file", self.files["pt_vehroutes"],
"--trips-file", self.files["pt_trips"],
"--min-stops", "0",
"--extend-to-fringe",
"--verbose",
]
if self.data.get("ptModes") is not None:
types = [t for t, v in self.data.get("ptModes").items() if v]
if types:
ptOptions += ["--types", ','.join(types)]
ptlines2flows.main(ptlines2flows.get_options(ptOptions))
hasPTFlows = True
if self.data["decal"]:
self.report("Downloading background images")
tileOptions = [
"-n", self.files["net"],
"-t", "100",
"-d", "background_images",
"-l", "-300",
"-a", "Mozilla/5.0 (X11; Linux x86_64) osmWebWizard.py/1.0 (+https://github.com/eclipse-sumo/sumo)",
]
try:
os.chdir(self.tmp)
os.mkdir("background_images")
tileGet.get(tileOptions)
self.report("Success.")
self.decalError = False
except Exception as e:
os.chdir(self.tmp)
shutil.rmtree("background_images", ignore_errors=True)
self.report("Error while downloading background images: %s" % e)
self.decalError = True
if self.data["vehicles"] or hasPTFlows:
randomTripsCalls = []
self.edges = sumolib.net.readNet(os.path.join(self.tmp, self.files["net"])).getEdges()
seed = 42
for vehicle in sorted(self.data["vehicles"].keys()):
options = self.data["vehicles"][vehicle]
self.report("Processing %s" % vehicleNames[vehicle])
self.filename("route", ".%s.rou.xml" % vehicle)
self.filename("trips", ".%s.trips.xml" % vehicle)
try:
options = self.parseTripOpts(vehicle, options, self.data["publicTransport"])
except ZeroDivisionError:
continue
if vehicle == "pedestrian" and self.data["publicTransport"]:
addFiles = [self.files["stops"]]
if hasPTFlows:
addFiles.append(self.files["ptroutes"])
options += ["--additional-files", ",".join(addFiles)]
options += ["--persontrip.walk-opposite-factor", "0.8"]
options += ["--duarouter-weights.tls-penalty", "20"]
options += ["--seed", str(seed)]
seed += 1
try:
randomTrips.main(randomTrips.get_options(options))
except ValueError:
print("Could not generate %s traffic" % vehicle, file=sys.stderr)
continue
randomTripsCalls.append(options)
if vehicle == "pedestrian":
self.routenames.append(self.files["route"])
else:
self.routenames.append(self.files["trips"])
os.remove(self.files["route"])
if hasPTFlows:
self.routenames.append(self.files["ptroutes"])
if os.name == "posix":
SUMO_HOME_VAR = "$SUMO_HOME"
else:
SUMO_HOME_VAR = "%SUMO_HOME%"
randomTripsPath = os.path.join(SUMO_HOME_VAR, "tools", "randomTrips.py")
ptlines2flowsPath = os.path.join(SUMO_HOME_VAR, "tools", "ptlines2flows.py")
self.filename("build.bat", "build.bat", False)
batchFile = self.files["build.bat"]
with open(batchFile, 'w') as f:
if os.name == "posix":
f.write("#!/bin/bash\n")
if ptOptions is not None:
f.write('python "%s" %s\n' %
(ptlines2flowsPath, " ".join(map(quoted_str, self.getRelative(ptOptions)))))
for opts in randomTripsCalls:
f.write('python "%s" %s\n' %
(randomTripsPath, " ".join(map(quoted_str, self.getRelative(opts)))))
os.chmod(batchFile, BATCH_MODE)
def parseTripOpts(self, vehicle, options, publicTransport):
"Return an option list for randomTrips.py for a given vehicle"
begin = self.data.get("begin", 0)
opts = ["-n", self.files["net"], "--fringe-factor", options.get("fringeFactor", "1"),
"--insertion-density", options["count"],
"-o", self.files["trips"],
"-r", self.files["route"],
"-b", begin,
"-e", begin + self.data["duration"]]
if vehicle == "pedestrian" and publicTransport:
opts += vehicleParameters["persontrips"]
else:
opts += vehicleParameters[vehicle]
return opts
def makeConfigFile(self):
"Save the configuration for SUMO in a file"
self.report("Generating configuration file")
self.filename("guisettings", ".view.xml")
with open(self.files["guisettings"], 'w') as f:
if self.data["decal"] and not self.decalError:
f.write("""
<viewsettings>
<scheme name="real world"/>
<delay value="20"/>
<include href="background_images/settings.xml"/>
</viewsettings>
""")
else:
f.write("""
<viewsettings>
<scheme name="real world"/>
<delay value="20"/>
</viewsettings>
""")
sumo = sumolib.checkBinary("sumo")
self.filename("config", ".sumocfg")
opts = [sumo, "-n", self.files_relative["net"], "--gui-settings-file", self.files_relative["guisettings"],
"--duration-log.statistics",
"--device.rerouting.adaptation-interval", "10",
"--device.rerouting.adaptation-steps", "18",
"--tls.actuated.jam-threshold", "30",
"-v", "--no-step-log", "--save-configuration", self.files_relative["config"], "--ignore-route-errors"]
if self.routenames:
opts += ["-r", ",".join(self.getRelative(self.routenames))]
opts += ["--tripinfo-output", "tripinfos.xml"]
opts += ["--statistic-output", "stats.xml"]
self.filename("outadd", "output.add.xml", False)
with open(self.files["outadd"], 'w') as fadd:
sumolib.writeXMLHeader(fadd, "$Id$", "additional")
fadd.write(' <edgeData id="wizard_example" period="3600" file="edgeData.xml"/>\n')
fadd.write("</additional>\n")
self.additionalFiles.append(self.files["outadd"])
if self.data["publicTransport"]:
opts += ["--stop-output", "stopinfos.xml"]
if len(self.additionalFiles) > 0:
opts += ["-a", ",".join(self.getRelative(self.additionalFiles))]
subprocess.call(opts, cwd=self.tmp)
def createBatch(self):
"Create a batch / bash file "
self.filename("run.bat", "run.bat", False)
with open(self.files["run.bat"], "w") as batchfile:
batchfile.write("sumo-gui -c " + self.files_relative["config"])
os.chmod(self.files["run.bat"], BATCH_MODE)
def openSUMO(self):
self.report("Calling SUMO")
sumogui = sumolib.checkBinary("sumo-gui")
subprocess.Popen([sumogui, "-c", self.files["config"]], cwd=self.tmp)
def createZip(self):
"Create a zip file with everything inside which SUMO GUI needs, returns it base64 encoded"
self.report("Building zip file")
self.filename("zip", ".zip")
with ZipFile(self.files["zip"], "w") as zipfile:
files = ["net", "guisettings", "config", "run.bat"]
if self.data["vehicles"] or self.data["publicTransport"]:
files += ["build.bat"]
if self.data["poly"]:
files += ["poly"]
files = list(map(lambda name: self.files[name], files))
if self.data["vehicles"]:
files += self.routenames
for name in files:
zipfile.write(name)
with open(self.files["zip"], "rb") as zipfile:
content = zipfile.read()
return base64.b64encode(content)
def finalize(self):
try:
shutil.rmtree(self.tmp)
except Exception:
pass
class OSMImporterWebSocket(WebSocket):
local = False
outputDir = None
def report(self, message):
print(message)
self.sendMessage(u"report " + message)
self.steps -= 1
def handleMessage(self):
data = json.loads(self.data)
thread = threading.Thread(target=self.build, args=(data,))
thread.start()
def build(self, data):
if self.outputDir is not None:
data['outputDir'] = self.outputDir
builder = Builder(data, self.local)
builder.report = self.report
self.steps = len(data["vehicles"]) + 4
self.sendMessage(u"steps %s" % self.steps)
try:
builder.build()
builder.makeConfigFile()
builder.createBatch()
if self.local:
builder.openSUMO()
else:
data = builder.createZip()
builder.finalize()
self.sendMessage(b"zip " + data)
except ssl.CertificateError:
self.report("Error with SSL certificate, try 'pip install -U certifi'.")
except Exception as e:
print(traceback.format_exc())
while self.steps > 0:
self.report(str(e) + ", recovering.")
if os.path.isdir(builder.tmp) and not os.listdir(builder.tmp):
os.rmdir(builder.tmp)
os.chdir(builder.origDir)
class OSMImporterHTTPHandler(SimpleHTTPRequestHandler):
web_root = os.path.join(os.path.dirname(os.path.realpath(__file__)), "webWizard")
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=self.web_root, **kwargs)
def report(self, message):
print(message)
self.server.messages.append(u"report " + message)
self.server.last_seen = time.time()
self.steps -= 1
def build(self, data):
if self.server.output is not None:
data['outputDir'] = self.server.output
builder = Builder(data, self.server.local)
builder.report = self.report
self.steps = len(data["vehicles"]) + 4
self.server.messages.append(u"steps %s" % self.steps)
try:
builder.build()
builder.makeConfigFile()
builder.createBatch()
if self.server.local:
builder.openSUMO()
else:
self.server.messages.append(b"zip " + builder.createZip())
builder.finalize()
except ssl.CertificateError:
self.report("Error with SSL certificate, try 'pip install -U certifi'.")
except Exception as e:
print(traceback.format_exc())
while self.steps > 0:
self.report(str(e) + ", recovering.")
if os.path.isdir(builder.tmp) and not os.listdir(builder.tmp):
os.rmdir(builder.tmp)
os.chdir(builder.origDir)
def log_message(self, format, *args):
pass
def do_POST(self):
if self.path == "/build":
length = int(self.headers.get("Content-Length", 0))
data = self.rfile.read(length)
threading.Thread(target=self.build, args=(json.loads(data),)).start()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"status": "started"}).encode())
else:
self.send_error(404)
def do_GET(self):
if self.path == "/progress":
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"data": self.server.messages[0] if self.server.messages else ""}).encode())
del self.server.messages[:1]
else:
super().do_GET()
def watchdog(server, timeout=3600):
while True:
time.sleep(60)
if time.time() - server.last_seen > timeout:
print("No request, shutting down server.")
server.shutdown()
break
def get_options(args=None):
parser = sumolib.options.ArgumentParser(description="OSM Web Wizard for SUMO")
parser.add_argument("--web-socket", action="store_true", default=False,
help="Create a (legacy) websocket instead of running a http server.")
parser.add_argument("--remote", action="store_true", default=False,
help="In remote mode, SUMO GUI will not be automatically opened instead a zip file " +
"will be generated.")
parser.add_argument("--osm-file", default="osm_bbox.osm.xml", dest="osmFile", help="use input file from path.")
parser.add_argument("--test-output", dest="testOutputDir",
help="Run with pre-defined options on file 'osm_bbox.osm.xml' and " +
"write output to the given directory.")
parser.add_argument("--bbox", help="bounding box to retrieve in geo coordinates west,south,east,north.")
parser.add_argument("-o", "--output", dest="outputDir",
help="Write output to the given folder rather than creating a name based on the timestamp")
parser.add_argument("--address", default="", help="Address for the web socket or server.")
parser.add_argument("--port", type=int,
help="Port for the web socket or server. By default a random port is chosen.")
parser.add_argument("-v", "--verbose", action="store_true", default=False, help="tell me what you are doing")
parser.add_argument("-b", "--begin", default=0, type=sumolib.miscutils.parseTime,
help="Defines the begin time for the scenario.")
parser.add_argument("-e", "--end", default=900, type=sumolib.miscutils.parseTime,
help="Defines the end time for the scenario.")
parser.add_argument("-n", "--netconvert-options", help="additional comma-separated options for netconvert")
parser.add_argument("--demand", default="passenger:6f5,bicycle:2f2,pedestrian:4,ship:1f40",
help="Traffic demand definition for non-interactive mode.")
return parser.parse_args(args)
def main(options):
if options.port is None:
options.port = DEFAULT_PORT if options.remote else 0
if options.testOutputDir is not None:
demand = collections.defaultdict(dict)
for mode in options.demand.split(","):
k, v = mode.split(":")
if "f" in v:
demand[k]['count'], demand[k]['fringeFactor'] = v.split("f")
else:
demand[k]['count'] = v
data = {u'begin': options.begin,
u'duration': options.end - options.begin,
u'vehicles': demand,
u'osm': os.path.abspath(options.osmFile),
u'poly': options.bbox is None,
u'publicTransport': True,
u'ptModes': {'bus': True,
'tram': True,
'train': True,
'subway': True,
'light_rail': True,
'monorail': True,
'trolleybus': True,
'minibus': True,
'share_taxi': True,
'aerialway': True,
'ferry': True},
u'leftHand': False,
u'decal': False,
u'verbose': options.verbose,
u'carOnlyNetwork': False,
u'outputDir': options.testOutputDir,
u'coords': options.bbox.split(",") if options.bbox else None,
u'options': options.netconvert_options
}
builder = Builder(data, True)
builder.build()
builder.makeConfigFile()
builder.createBatch()
if not options.remote:
subprocess.call([sumolib.checkBinary("sumo"), "-c", builder.files["config"]])
elif options.web_socket:
OSMImporterWebSocket.local = options.testOutputDir is not None or not options.remote
OSMImporterWebSocket.outputDir = options.outputDir
port = DEFAULT_PORT
if os.name != "nt":
port = options.port if options.port else sumolib.miscutils.getFreeSocketPort()
server = SimpleWebSocketServer(options.address, port, OSMImporterWebSocket)
if not options.remote:
path = os.path.dirname(os.path.realpath(__file__))
if os.name != "nt" and not path.startswith(os.path.expanduser('~')):
new_path = os.path.expanduser('~/Sumo')
wizard_path = os.path.join(new_path, 'webWizard')
if not os.path.exists(wizard_path):
os.makedirs(new_path, exist_ok=True)
shutil.copytree(os.path.join(path, "webWizard"), wizard_path, dirs_exist_ok=True)
path = new_path
os.chdir(path)
url = "file://" + os.path.join(path, "webWizard", "index.html")
if os.name != "nt":
url += "?port=%s" % port
webbrowser.open(url)
server.serveforever()
else:
port = options.port if options.port else sumolib.miscutils.getFreeSocketPort()
httpd = HTTPServer((options.address, port), OSMImporterHTTPHandler)
httpd.local = options.testOutputDir is not None or not options.remote
httpd.output = options.outputDir
httpd.messages = []
httpd.last_seen = time.time()
if options.remote:
print("Web server listening on http://%s:%s/ in remote mode." % (httpd.server_name, port))
else:
webbrowser.open("http://127.0.0.1:%s/" % port)
threading.Thread(target=watchdog, args=(httpd,), daemon=True).start()
httpd.serve_forever()
if __name__ == "__main__":
main(get_options())