Spaces:
Sleeping
Sleeping
Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- data/generated/city_0002/scenarios/accident/config.json +13 -13
- data/generated/city_0002/scenarios/construction/config.json +13 -13
- data/generated/city_0002/scenarios/district_overload/config.json +13 -13
- data/generated/city_0002/scenarios/evening_rush/config.json +13 -13
- data/generated/city_0002/scenarios/event_spike/config.json +13 -13
- data/generated/city_0002/scenarios/morning_rush/config.json +13 -13
- data/generated/city_0002/scenarios/normal/config.json +13 -13
- data/splits/splits/README.md +21 -0
- data/splits/splits/test_cities.txt +15 -0
- data/splits/splits/train_cities.txt +70 -0
- data/splits/splits/val_cities.txt +15 -0
- server/environment.py +168 -168
- server/remote_runner.py +58 -58
- server/roadnet_matcher.py +55 -55
- server/visualizer_app.py +404 -404
- third_party/CityFlow/.gitignore +13 -13
- third_party/CityFlow/.gitmodules +6 -6
- third_party/CityFlow/CMakeLists.txt +76 -76
- third_party/CityFlow/Dockerfile +20 -20
- third_party/CityFlow/LICENSE.txt +200 -200
- third_party/CityFlow/README.rst +51 -51
- third_party/CityFlow/docs/Makefile +19 -19
- third_party/CityFlow/docs/make.bat +36 -36
- third_party/CityFlow/docs/source/conf.py +154 -154
- third_party/CityFlow/docs/source/flow.rst +23 -23
- third_party/CityFlow/docs/source/index.rst +18 -18
- third_party/CityFlow/docs/source/install.rst +66 -66
- third_party/CityFlow/examples/config.json +11 -11
- third_party/CityFlow/examples/flow.json +241 -241
- third_party/CityFlow/frontend/Point.js +56 -56
- third_party/CityFlow/frontend/README.md +6 -6
- third_party/CityFlow/frontend/download_replay.py +18 -18
- third_party/CityFlow/frontend/index.html +136 -136
- third_party/CityFlow/frontend/script.js +1035 -1035
- third_party/CityFlow/frontend/script_multi.js +0 -0
- third_party/CityFlow/frontend/spinner.css +52 -52
- third_party/CityFlow/frontend/style.css +63 -63
- third_party/CityFlow/frontend/style_multi.css +747 -747
- third_party/CityFlow/setup.py +70 -70
- third_party/CityFlow/src/CMakeLists.txt +35 -35
- third_party/CityFlow/src/cityflow.cpp +47 -47
- third_party/CityFlow/tests/CMakeLists.txt +13 -13
- third_party/CityFlow/tools/debug/CMakeLists.txt +8 -8
- third_party/CityFlow/tools/debug/simple_run.cpp +58 -58
- third_party/CityFlow/tools/generator/generate_grid_scenario.py +127 -127
- third_party/CityFlow/tools/generator/generate_json_from_grid.py +428 -428
- third_party/CityFlow/tools/generator/readme.md +41 -41
- training/README.md +38 -38
- training/__init__.py +6 -6
- training/cityflow_dataset.py +202 -202
data/generated/city_0002/scenarios/accident/config.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
-
{
|
| 2 |
-
"interval": 1.0,
|
| 3 |
-
"seed": 11348,
|
| 4 |
-
"dir": "/
|
| 5 |
-
"roadnetFile": "roadnet.json",
|
| 6 |
-
"flowFile": "scenarios/accident/flow.json",
|
| 7 |
-
"rlTrafficLight": true,
|
| 8 |
-
"laneChange": false,
|
| 9 |
-
"saveReplay": false,
|
| 10 |
-
"roadnetLogFile": "scenarios/accident/roadnetLogFile.json",
|
| 11 |
-
"replayLogFile": "scenarios/accident/replay.txt",
|
| 12 |
-
"step": 3600
|
| 13 |
-
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"interval": 1.0,
|
| 3 |
+
"seed": 11348,
|
| 4 |
+
"dir": "C:/Users/toke/Desktop/agentic-traffic/data/generated/city_0002/",
|
| 5 |
+
"roadnetFile": "roadnet.json",
|
| 6 |
+
"flowFile": "scenarios/accident/flow.json",
|
| 7 |
+
"rlTrafficLight": true,
|
| 8 |
+
"laneChange": false,
|
| 9 |
+
"saveReplay": false,
|
| 10 |
+
"roadnetLogFile": "scenarios/accident/roadnetLogFile.json",
|
| 11 |
+
"replayLogFile": "scenarios/accident/replay.txt",
|
| 12 |
+
"step": 3600
|
| 13 |
+
}
|
data/generated/city_0002/scenarios/construction/config.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
-
{
|
| 2 |
-
"interval": 1.0,
|
| 3 |
-
"seed": 11449,
|
| 4 |
-
"dir": "/
|
| 5 |
-
"roadnetFile": "roadnet.json",
|
| 6 |
-
"flowFile": "scenarios/construction/flow.json",
|
| 7 |
-
"rlTrafficLight": true,
|
| 8 |
-
"laneChange": false,
|
| 9 |
-
"saveReplay": false,
|
| 10 |
-
"roadnetLogFile": "scenarios/construction/roadnetLogFile.json",
|
| 11 |
-
"replayLogFile": "scenarios/construction/replay.txt",
|
| 12 |
-
"step": 3600
|
| 13 |
-
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"interval": 1.0,
|
| 3 |
+
"seed": 11449,
|
| 4 |
+
"dir": "C:/Users/toke/Desktop/agentic-traffic/data/generated/city_0002/",
|
| 5 |
+
"roadnetFile": "roadnet.json",
|
| 6 |
+
"flowFile": "scenarios/construction/flow.json",
|
| 7 |
+
"rlTrafficLight": true,
|
| 8 |
+
"laneChange": false,
|
| 9 |
+
"saveReplay": false,
|
| 10 |
+
"roadnetLogFile": "scenarios/construction/roadnetLogFile.json",
|
| 11 |
+
"replayLogFile": "scenarios/construction/replay.txt",
|
| 12 |
+
"step": 3600
|
| 13 |
+
}
|
data/generated/city_0002/scenarios/district_overload/config.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
-
{
|
| 2 |
-
"interval": 1.0,
|
| 3 |
-
"seed": 11651,
|
| 4 |
-
"dir": "/
|
| 5 |
-
"roadnetFile": "roadnet.json",
|
| 6 |
-
"flowFile": "scenarios/district_overload/flow.json",
|
| 7 |
-
"rlTrafficLight": true,
|
| 8 |
-
"laneChange": false,
|
| 9 |
-
"saveReplay": false,
|
| 10 |
-
"roadnetLogFile": "scenarios/district_overload/roadnetLogFile.json",
|
| 11 |
-
"replayLogFile": "scenarios/district_overload/replay.txt",
|
| 12 |
-
"step": 3600
|
| 13 |
-
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"interval": 1.0,
|
| 3 |
+
"seed": 11651,
|
| 4 |
+
"dir": "C:/Users/toke/Desktop/agentic-traffic/data/generated/city_0002/",
|
| 5 |
+
"roadnetFile": "roadnet.json",
|
| 6 |
+
"flowFile": "scenarios/district_overload/flow.json",
|
| 7 |
+
"rlTrafficLight": true,
|
| 8 |
+
"laneChange": false,
|
| 9 |
+
"saveReplay": false,
|
| 10 |
+
"roadnetLogFile": "scenarios/district_overload/roadnetLogFile.json",
|
| 11 |
+
"replayLogFile": "scenarios/district_overload/replay.txt",
|
| 12 |
+
"step": 3600
|
| 13 |
+
}
|
data/generated/city_0002/scenarios/evening_rush/config.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
-
{
|
| 2 |
-
"interval": 1.0,
|
| 3 |
-
"seed": 11247,
|
| 4 |
-
"dir": "/
|
| 5 |
-
"roadnetFile": "roadnet.json",
|
| 6 |
-
"flowFile": "scenarios/evening_rush/flow.json",
|
| 7 |
-
"rlTrafficLight": true,
|
| 8 |
-
"laneChange": false,
|
| 9 |
-
"saveReplay": false,
|
| 10 |
-
"roadnetLogFile": "scenarios/evening_rush/roadnetLogFile.json",
|
| 11 |
-
"replayLogFile": "scenarios/evening_rush/replay.txt",
|
| 12 |
-
"step": 3600
|
| 13 |
-
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"interval": 1.0,
|
| 3 |
+
"seed": 11247,
|
| 4 |
+
"dir": "C:/Users/toke/Desktop/agentic-traffic/data/generated/city_0002/",
|
| 5 |
+
"roadnetFile": "roadnet.json",
|
| 6 |
+
"flowFile": "scenarios/evening_rush/flow.json",
|
| 7 |
+
"rlTrafficLight": true,
|
| 8 |
+
"laneChange": false,
|
| 9 |
+
"saveReplay": false,
|
| 10 |
+
"roadnetLogFile": "scenarios/evening_rush/roadnetLogFile.json",
|
| 11 |
+
"replayLogFile": "scenarios/evening_rush/replay.txt",
|
| 12 |
+
"step": 3600
|
| 13 |
+
}
|
data/generated/city_0002/scenarios/event_spike/config.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
-
{
|
| 2 |
-
"interval": 1.0,
|
| 3 |
-
"seed": 11550,
|
| 4 |
-
"dir": "/
|
| 5 |
-
"roadnetFile": "roadnet.json",
|
| 6 |
-
"flowFile": "scenarios/event_spike/flow.json",
|
| 7 |
-
"rlTrafficLight": true,
|
| 8 |
-
"laneChange": false,
|
| 9 |
-
"saveReplay": false,
|
| 10 |
-
"roadnetLogFile": "scenarios/event_spike/roadnetLogFile.json",
|
| 11 |
-
"replayLogFile": "scenarios/event_spike/replay.txt",
|
| 12 |
-
"step": 3600
|
| 13 |
-
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"interval": 1.0,
|
| 3 |
+
"seed": 11550,
|
| 4 |
+
"dir": "C:/Users/toke/Desktop/agentic-traffic/data/generated/city_0002/",
|
| 5 |
+
"roadnetFile": "roadnet.json",
|
| 6 |
+
"flowFile": "scenarios/event_spike/flow.json",
|
| 7 |
+
"rlTrafficLight": true,
|
| 8 |
+
"laneChange": false,
|
| 9 |
+
"saveReplay": false,
|
| 10 |
+
"roadnetLogFile": "scenarios/event_spike/roadnetLogFile.json",
|
| 11 |
+
"replayLogFile": "scenarios/event_spike/replay.txt",
|
| 12 |
+
"step": 3600
|
| 13 |
+
}
|
data/generated/city_0002/scenarios/morning_rush/config.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
-
{
|
| 2 |
-
"interval": 1.0,
|
| 3 |
-
"seed": 11146,
|
| 4 |
-
"dir": "/
|
| 5 |
-
"roadnetFile": "roadnet.json",
|
| 6 |
-
"flowFile": "scenarios/morning_rush/flow.json",
|
| 7 |
-
"rlTrafficLight": true,
|
| 8 |
-
"laneChange": false,
|
| 9 |
-
"saveReplay": false,
|
| 10 |
-
"roadnetLogFile": "scenarios/morning_rush/roadnetLogFile.json",
|
| 11 |
-
"replayLogFile": "scenarios/morning_rush/replay.txt",
|
| 12 |
-
"step": 3600
|
| 13 |
-
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"interval": 1.0,
|
| 3 |
+
"seed": 11146,
|
| 4 |
+
"dir": "C:/Users/toke/Desktop/agentic-traffic/data/generated/city_0002/",
|
| 5 |
+
"roadnetFile": "roadnet.json",
|
| 6 |
+
"flowFile": "scenarios/morning_rush/flow.json",
|
| 7 |
+
"rlTrafficLight": true,
|
| 8 |
+
"laneChange": false,
|
| 9 |
+
"saveReplay": false,
|
| 10 |
+
"roadnetLogFile": "scenarios/morning_rush/roadnetLogFile.json",
|
| 11 |
+
"replayLogFile": "scenarios/morning_rush/replay.txt",
|
| 12 |
+
"step": 3600
|
| 13 |
+
}
|
data/generated/city_0002/scenarios/normal/config.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
-
{
|
| 2 |
-
"interval": 1.0,
|
| 3 |
-
"seed": 11045,
|
| 4 |
-
"dir": "/
|
| 5 |
-
"roadnetFile": "roadnet.json",
|
| 6 |
-
"flowFile": "scenarios/normal/flow.json",
|
| 7 |
-
"rlTrafficLight": true,
|
| 8 |
-
"laneChange": false,
|
| 9 |
-
"saveReplay": false,
|
| 10 |
-
"roadnetLogFile": "scenarios/normal/roadnetLogFile.json",
|
| 11 |
-
"replayLogFile": "scenarios/normal/replay.txt",
|
| 12 |
-
"step": 3600
|
| 13 |
-
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"interval": 1.0,
|
| 3 |
+
"seed": 11045,
|
| 4 |
+
"dir": "C:/Users/toke/Desktop/agentic-traffic/data/generated/city_0002/",
|
| 5 |
+
"roadnetFile": "roadnet.json",
|
| 6 |
+
"flowFile": "scenarios/normal/flow.json",
|
| 7 |
+
"rlTrafficLight": true,
|
| 8 |
+
"laneChange": false,
|
| 9 |
+
"saveReplay": false,
|
| 10 |
+
"roadnetLogFile": "scenarios/normal/roadnetLogFile.json",
|
| 11 |
+
"replayLogFile": "scenarios/normal/replay.txt",
|
| 12 |
+
"step": 3600
|
| 13 |
+
}
|
data/splits/splits/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# data/splits
|
| 2 |
+
|
| 3 |
+
City-level train/validation/test splits for the generated dataset.
|
| 4 |
+
|
| 5 |
+
## Files
|
| 6 |
+
|
| 7 |
+
- [train_cities.txt](/Users/aditya/Developer/traffic-llm/data/splits/train_cities.txt)
|
| 8 |
+
- [val_cities.txt](/Users/aditya/Developer/traffic-llm/data/splits/val_cities.txt)
|
| 9 |
+
- [test_cities.txt](/Users/aditya/Developer/traffic-llm/data/splits/test_cities.txt)
|
| 10 |
+
|
| 11 |
+
## Important rule
|
| 12 |
+
|
| 13 |
+
Splits are by city only. All scenarios for a given city belong to the same split.
|
| 14 |
+
|
| 15 |
+
## Regeneration
|
| 16 |
+
|
| 17 |
+
Use:
|
| 18 |
+
|
| 19 |
+
`python3 -m training.train_local_policy make-splits`
|
| 20 |
+
|
| 21 |
+
The split logic is implemented in [training/dataset.py](/Users/aditya/Developer/traffic-llm/training/dataset.py).
|
data/splits/splits/test_cities.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
city_0005
|
| 2 |
+
city_0007
|
| 3 |
+
city_0008
|
| 4 |
+
city_0010
|
| 5 |
+
city_0012
|
| 6 |
+
city_0013
|
| 7 |
+
city_0020
|
| 8 |
+
city_0028
|
| 9 |
+
city_0042
|
| 10 |
+
city_0047
|
| 11 |
+
city_0051
|
| 12 |
+
city_0065
|
| 13 |
+
city_0069
|
| 14 |
+
city_0075
|
| 15 |
+
city_0084
|
data/splits/splits/train_cities.txt
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
city_0001
|
| 2 |
+
city_0002
|
| 3 |
+
city_0003
|
| 4 |
+
city_0004
|
| 5 |
+
city_0006
|
| 6 |
+
city_0011
|
| 7 |
+
city_0014
|
| 8 |
+
city_0015
|
| 9 |
+
city_0017
|
| 10 |
+
city_0018
|
| 11 |
+
city_0019
|
| 12 |
+
city_0021
|
| 13 |
+
city_0022
|
| 14 |
+
city_0023
|
| 15 |
+
city_0024
|
| 16 |
+
city_0025
|
| 17 |
+
city_0026
|
| 18 |
+
city_0027
|
| 19 |
+
city_0030
|
| 20 |
+
city_0032
|
| 21 |
+
city_0033
|
| 22 |
+
city_0034
|
| 23 |
+
city_0035
|
| 24 |
+
city_0036
|
| 25 |
+
city_0037
|
| 26 |
+
city_0038
|
| 27 |
+
city_0039
|
| 28 |
+
city_0040
|
| 29 |
+
city_0041
|
| 30 |
+
city_0043
|
| 31 |
+
city_0044
|
| 32 |
+
city_0045
|
| 33 |
+
city_0046
|
| 34 |
+
city_0048
|
| 35 |
+
city_0049
|
| 36 |
+
city_0050
|
| 37 |
+
city_0052
|
| 38 |
+
city_0053
|
| 39 |
+
city_0057
|
| 40 |
+
city_0058
|
| 41 |
+
city_0059
|
| 42 |
+
city_0060
|
| 43 |
+
city_0061
|
| 44 |
+
city_0062
|
| 45 |
+
city_0063
|
| 46 |
+
city_0064
|
| 47 |
+
city_0066
|
| 48 |
+
city_0067
|
| 49 |
+
city_0068
|
| 50 |
+
city_0070
|
| 51 |
+
city_0072
|
| 52 |
+
city_0074
|
| 53 |
+
city_0076
|
| 54 |
+
city_0077
|
| 55 |
+
city_0079
|
| 56 |
+
city_0080
|
| 57 |
+
city_0081
|
| 58 |
+
city_0082
|
| 59 |
+
city_0083
|
| 60 |
+
city_0085
|
| 61 |
+
city_0087
|
| 62 |
+
city_0088
|
| 63 |
+
city_0089
|
| 64 |
+
city_0092
|
| 65 |
+
city_0093
|
| 66 |
+
city_0094
|
| 67 |
+
city_0095
|
| 68 |
+
city_0097
|
| 69 |
+
city_0099
|
| 70 |
+
city_0100
|
data/splits/splits/val_cities.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
city_0009
|
| 2 |
+
city_0016
|
| 3 |
+
city_0029
|
| 4 |
+
city_0031
|
| 5 |
+
city_0054
|
| 6 |
+
city_0055
|
| 7 |
+
city_0056
|
| 8 |
+
city_0071
|
| 9 |
+
city_0073
|
| 10 |
+
city_0078
|
| 11 |
+
city_0086
|
| 12 |
+
city_0090
|
| 13 |
+
city_0091
|
| 14 |
+
city_0096
|
| 15 |
+
city_0098
|
server/environment.py
CHANGED
|
@@ -1,168 +1,168 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import os
|
| 4 |
-
import uuid
|
| 5 |
-
from pathlib import Path
|
| 6 |
-
from typing import Any
|
| 7 |
-
|
| 8 |
-
from district_llm.inference import DistrictLLMInference
|
| 9 |
-
from district_llm.schema import DistrictAction
|
| 10 |
-
from models import (
|
| 11 |
-
AgenticTrafficAction,
|
| 12 |
-
AgenticTrafficObservation,
|
| 13 |
-
AgenticTrafficState,
|
| 14 |
-
)
|
| 15 |
-
from openenv.core.env_server.interfaces import Environment
|
| 16 |
-
from openenv_app.openenv_wrapper import OpenEnvTrafficWrapper
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 20 |
-
DATA_DIR = Path(os.environ.get("DATA_DIR", "") or (REPO_ROOT / "data" / "generated"))
|
| 21 |
-
SPLITS_DIR = Path(os.environ.get("SPLITS_DIR", "") or (REPO_ROOT / "data" / "splits"))
|
| 22 |
-
DISTRICT_LLM_ADAPTER_PATH = Path(
|
| 23 |
-
os.environ.get("DISTRICT_LLM_ADAPTER_PATH", "")
|
| 24 |
-
or (REPO_ROOT / "artifacts" / "district_llm_adapter_v3" / "main_run" / "adapter")
|
| 25 |
-
)
|
| 26 |
-
DISTRICT_LLM_DEVICE = os.environ.get("DISTRICT_LLM_DEVICE")
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
class AgenticTrafficEnvironment(
|
| 30 |
-
Environment[AgenticTrafficAction, AgenticTrafficObservation, AgenticTrafficState]
|
| 31 |
-
):
|
| 32 |
-
"""Minimal OpenEnv-compatible wrapper around the existing district controller stack."""
|
| 33 |
-
|
| 34 |
-
def __init__(self) -> None:
|
| 35 |
-
super().__init__()
|
| 36 |
-
self.wrapper = OpenEnvTrafficWrapper(
|
| 37 |
-
generated_root=DATA_DIR,
|
| 38 |
-
splits_root=SPLITS_DIR,
|
| 39 |
-
)
|
| 40 |
-
self._state = AgenticTrafficState()
|
| 41 |
-
self._llm_inference: DistrictLLMInference | None = None
|
| 42 |
-
self._llm_load_attempted = False
|
| 43 |
-
self._llm_error: str | None = None
|
| 44 |
-
|
| 45 |
-
def reset(
|
| 46 |
-
self,
|
| 47 |
-
seed: int | None = None,
|
| 48 |
-
episode_id: str | None = None,
|
| 49 |
-
**kwargs: Any,
|
| 50 |
-
) -> AgenticTrafficObservation:
|
| 51 |
-
payload = self.wrapper.reset(
|
| 52 |
-
seed=seed,
|
| 53 |
-
city_id=kwargs.get("city_id"),
|
| 54 |
-
scenario_name=kwargs.get("scenario_name"),
|
| 55 |
-
)
|
| 56 |
-
self._state.episode_id = episode_id or str(uuid.uuid4())
|
| 57 |
-
self._state.step_count = 0
|
| 58 |
-
self._sync_state()
|
| 59 |
-
observation = AgenticTrafficObservation.model_validate(payload["observation"])
|
| 60 |
-
observation.reward = None
|
| 61 |
-
observation.done = False
|
| 62 |
-
observation.metadata["llm"] = self._llm_status()
|
| 63 |
-
return observation
|
| 64 |
-
|
| 65 |
-
def step(
|
| 66 |
-
self,
|
| 67 |
-
action: AgenticTrafficAction,
|
| 68 |
-
timeout_s: float | None = None,
|
| 69 |
-
**kwargs: Any,
|
| 70 |
-
) -> AgenticTrafficObservation:
|
| 71 |
-
del timeout_s, kwargs
|
| 72 |
-
payload = self.wrapper.step(action=self._build_step_payload(action))
|
| 73 |
-
self._state.step_count += 1
|
| 74 |
-
self._sync_state()
|
| 75 |
-
observation = AgenticTrafficObservation.model_validate(payload["observation"])
|
| 76 |
-
observation.done = bool(payload.get("done", False))
|
| 77 |
-
observation.reward = float(payload.get("reward", 0.0))
|
| 78 |
-
observation.metadata["llm"] = self._llm_status()
|
| 79 |
-
return observation
|
| 80 |
-
|
| 81 |
-
@property
|
| 82 |
-
def state(self) -> AgenticTrafficState:
|
| 83 |
-
self._sync_state()
|
| 84 |
-
return self._state
|
| 85 |
-
|
| 86 |
-
def _build_step_payload(self, action: AgenticTrafficAction) -> dict[str, Any]:
|
| 87 |
-
district_actions = dict(action.district_actions)
|
| 88 |
-
llm_generated_actions: dict[str, Any] = {}
|
| 89 |
-
|
| 90 |
-
if action.use_llm:
|
| 91 |
-
llm_generated_actions = self._generate_llm_actions(
|
| 92 |
-
existing_actions=district_actions,
|
| 93 |
-
max_new_tokens=action.llm_max_new_tokens,
|
| 94 |
-
)
|
| 95 |
-
for district_id, directive in llm_generated_actions.items():
|
| 96 |
-
district_actions.setdefault(district_id, directive)
|
| 97 |
-
|
| 98 |
-
payload = {"district_actions": district_actions}
|
| 99 |
-
payload["metadata"] = {
|
| 100 |
-
"use_llm": bool(action.use_llm),
|
| 101 |
-
"llm_generated_districts": sorted(llm_generated_actions),
|
| 102 |
-
"llm": self._llm_status(),
|
| 103 |
-
}
|
| 104 |
-
return payload
|
| 105 |
-
|
| 106 |
-
def _generate_llm_actions(
|
| 107 |
-
self,
|
| 108 |
-
existing_actions: dict[str, Any],
|
| 109 |
-
max_new_tokens: int,
|
| 110 |
-
) -> dict[str, Any]:
|
| 111 |
-
if not self.wrapper.last_summaries:
|
| 112 |
-
return {}
|
| 113 |
-
|
| 114 |
-
inference = self._get_llm_inference()
|
| 115 |
-
if inference is None:
|
| 116 |
-
return {}
|
| 117 |
-
|
| 118 |
-
generated_actions: dict[str, Any] = {}
|
| 119 |
-
for district_id, summary in self.wrapper.last_summaries.items():
|
| 120 |
-
if district_id in existing_actions:
|
| 121 |
-
continue
|
| 122 |
-
result = inference.predict_with_result(summary=summary, max_new_tokens=max_new_tokens)
|
| 123 |
-
generated_actions[district_id] = result.action.to_dict()
|
| 124 |
-
return generated_actions
|
| 125 |
-
|
| 126 |
-
def _get_llm_inference(self) -> DistrictLLMInference | None:
|
| 127 |
-
if self._llm_inference is not None:
|
| 128 |
-
return self._llm_inference
|
| 129 |
-
if self._llm_load_attempted:
|
| 130 |
-
return None
|
| 131 |
-
|
| 132 |
-
self._llm_load_attempted = True
|
| 133 |
-
if not DISTRICT_LLM_ADAPTER_PATH.exists():
|
| 134 |
-
self._llm_error = f"Adapter not found at {DISTRICT_LLM_ADAPTER_PATH}"
|
| 135 |
-
return None
|
| 136 |
-
|
| 137 |
-
try:
|
| 138 |
-
self._llm_inference = DistrictLLMInference(
|
| 139 |
-
model_name_or_path=str(DISTRICT_LLM_ADAPTER_PATH),
|
| 140 |
-
device=DISTRICT_LLM_DEVICE,
|
| 141 |
-
fallback_action=DistrictAction.default_hold(
|
| 142 |
-
duration_steps=self.wrapper.district_decision_interval
|
| 143 |
-
),
|
| 144 |
-
)
|
| 145 |
-
except Exception as exc:
|
| 146 |
-
self._llm_error = f"{type(exc).__name__}: {exc}"
|
| 147 |
-
self._llm_inference = None
|
| 148 |
-
return self._llm_inference
|
| 149 |
-
|
| 150 |
-
def _llm_status(self) -> dict[str, Any]:
|
| 151 |
-
return {
|
| 152 |
-
"adapter_path": str(DISTRICT_LLM_ADAPTER_PATH),
|
| 153 |
-
"adapter_present": DISTRICT_LLM_ADAPTER_PATH.exists(),
|
| 154 |
-
"loaded": self._llm_inference is not None,
|
| 155 |
-
"load_attempted": self._llm_load_attempted,
|
| 156 |
-
"error": self._llm_error,
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
def _sync_state(self) -> None:
|
| 160 |
-
payload = self.wrapper.state()["state"]
|
| 161 |
-
self._state = AgenticTrafficState.model_validate(
|
| 162 |
-
{
|
| 163 |
-
**payload,
|
| 164 |
-
"episode_id": self._state.episode_id,
|
| 165 |
-
"step_count": self._state.step_count,
|
| 166 |
-
"llm": self._llm_status(),
|
| 167 |
-
}
|
| 168 |
-
)
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import uuid
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
from district_llm.inference import DistrictLLMInference
|
| 9 |
+
from district_llm.schema import DistrictAction
|
| 10 |
+
from models import (
|
| 11 |
+
AgenticTrafficAction,
|
| 12 |
+
AgenticTrafficObservation,
|
| 13 |
+
AgenticTrafficState,
|
| 14 |
+
)
|
| 15 |
+
from openenv.core.env_server.interfaces import Environment
|
| 16 |
+
from openenv_app.openenv_wrapper import OpenEnvTrafficWrapper
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 20 |
+
DATA_DIR = Path(os.environ.get("DATA_DIR", "") or (REPO_ROOT / "data" / "generated"))
|
| 21 |
+
SPLITS_DIR = Path(os.environ.get("SPLITS_DIR", "") or (REPO_ROOT / "data" / "splits"))
|
| 22 |
+
DISTRICT_LLM_ADAPTER_PATH = Path(
|
| 23 |
+
os.environ.get("DISTRICT_LLM_ADAPTER_PATH", "")
|
| 24 |
+
or (REPO_ROOT / "artifacts" / "district_llm_adapter_v3" / "main_run" / "adapter")
|
| 25 |
+
)
|
| 26 |
+
DISTRICT_LLM_DEVICE = os.environ.get("DISTRICT_LLM_DEVICE")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class AgenticTrafficEnvironment(
|
| 30 |
+
Environment[AgenticTrafficAction, AgenticTrafficObservation, AgenticTrafficState]
|
| 31 |
+
):
|
| 32 |
+
"""Minimal OpenEnv-compatible wrapper around the existing district controller stack."""
|
| 33 |
+
|
| 34 |
+
def __init__(self) -> None:
|
| 35 |
+
super().__init__()
|
| 36 |
+
self.wrapper = OpenEnvTrafficWrapper(
|
| 37 |
+
generated_root=DATA_DIR,
|
| 38 |
+
splits_root=SPLITS_DIR,
|
| 39 |
+
)
|
| 40 |
+
self._state = AgenticTrafficState()
|
| 41 |
+
self._llm_inference: DistrictLLMInference | None = None
|
| 42 |
+
self._llm_load_attempted = False
|
| 43 |
+
self._llm_error: str | None = None
|
| 44 |
+
|
| 45 |
+
def reset(
|
| 46 |
+
self,
|
| 47 |
+
seed: int | None = None,
|
| 48 |
+
episode_id: str | None = None,
|
| 49 |
+
**kwargs: Any,
|
| 50 |
+
) -> AgenticTrafficObservation:
|
| 51 |
+
payload = self.wrapper.reset(
|
| 52 |
+
seed=seed,
|
| 53 |
+
city_id=kwargs.get("city_id"),
|
| 54 |
+
scenario_name=kwargs.get("scenario_name"),
|
| 55 |
+
)
|
| 56 |
+
self._state.episode_id = episode_id or str(uuid.uuid4())
|
| 57 |
+
self._state.step_count = 0
|
| 58 |
+
self._sync_state()
|
| 59 |
+
observation = AgenticTrafficObservation.model_validate(payload["observation"])
|
| 60 |
+
observation.reward = None
|
| 61 |
+
observation.done = False
|
| 62 |
+
observation.metadata["llm"] = self._llm_status()
|
| 63 |
+
return observation
|
| 64 |
+
|
| 65 |
+
def step(
|
| 66 |
+
self,
|
| 67 |
+
action: AgenticTrafficAction,
|
| 68 |
+
timeout_s: float | None = None,
|
| 69 |
+
**kwargs: Any,
|
| 70 |
+
) -> AgenticTrafficObservation:
|
| 71 |
+
del timeout_s, kwargs
|
| 72 |
+
payload = self.wrapper.step(action=self._build_step_payload(action))
|
| 73 |
+
self._state.step_count += 1
|
| 74 |
+
self._sync_state()
|
| 75 |
+
observation = AgenticTrafficObservation.model_validate(payload["observation"])
|
| 76 |
+
observation.done = bool(payload.get("done", False))
|
| 77 |
+
observation.reward = float(payload.get("reward", 0.0))
|
| 78 |
+
observation.metadata["llm"] = self._llm_status()
|
| 79 |
+
return observation
|
| 80 |
+
|
| 81 |
+
@property
|
| 82 |
+
def state(self) -> AgenticTrafficState:
|
| 83 |
+
self._sync_state()
|
| 84 |
+
return self._state
|
| 85 |
+
|
| 86 |
+
def _build_step_payload(self, action: AgenticTrafficAction) -> dict[str, Any]:
|
| 87 |
+
district_actions = dict(action.district_actions)
|
| 88 |
+
llm_generated_actions: dict[str, Any] = {}
|
| 89 |
+
|
| 90 |
+
if action.use_llm:
|
| 91 |
+
llm_generated_actions = self._generate_llm_actions(
|
| 92 |
+
existing_actions=district_actions,
|
| 93 |
+
max_new_tokens=action.llm_max_new_tokens,
|
| 94 |
+
)
|
| 95 |
+
for district_id, directive in llm_generated_actions.items():
|
| 96 |
+
district_actions.setdefault(district_id, directive)
|
| 97 |
+
|
| 98 |
+
payload = {"district_actions": district_actions}
|
| 99 |
+
payload["metadata"] = {
|
| 100 |
+
"use_llm": bool(action.use_llm),
|
| 101 |
+
"llm_generated_districts": sorted(llm_generated_actions),
|
| 102 |
+
"llm": self._llm_status(),
|
| 103 |
+
}
|
| 104 |
+
return payload
|
| 105 |
+
|
| 106 |
+
def _generate_llm_actions(
|
| 107 |
+
self,
|
| 108 |
+
existing_actions: dict[str, Any],
|
| 109 |
+
max_new_tokens: int,
|
| 110 |
+
) -> dict[str, Any]:
|
| 111 |
+
if not self.wrapper.last_summaries:
|
| 112 |
+
return {}
|
| 113 |
+
|
| 114 |
+
inference = self._get_llm_inference()
|
| 115 |
+
if inference is None:
|
| 116 |
+
return {}
|
| 117 |
+
|
| 118 |
+
generated_actions: dict[str, Any] = {}
|
| 119 |
+
for district_id, summary in self.wrapper.last_summaries.items():
|
| 120 |
+
if district_id in existing_actions:
|
| 121 |
+
continue
|
| 122 |
+
result = inference.predict_with_result(summary=summary, max_new_tokens=max_new_tokens)
|
| 123 |
+
generated_actions[district_id] = result.action.to_dict()
|
| 124 |
+
return generated_actions
|
| 125 |
+
|
| 126 |
+
def _get_llm_inference(self) -> DistrictLLMInference | None:
|
| 127 |
+
if self._llm_inference is not None:
|
| 128 |
+
return self._llm_inference
|
| 129 |
+
if self._llm_load_attempted:
|
| 130 |
+
return None
|
| 131 |
+
|
| 132 |
+
self._llm_load_attempted = True
|
| 133 |
+
if not DISTRICT_LLM_ADAPTER_PATH.exists():
|
| 134 |
+
self._llm_error = f"Adapter not found at {DISTRICT_LLM_ADAPTER_PATH}"
|
| 135 |
+
return None
|
| 136 |
+
|
| 137 |
+
try:
|
| 138 |
+
self._llm_inference = DistrictLLMInference(
|
| 139 |
+
model_name_or_path=str(DISTRICT_LLM_ADAPTER_PATH),
|
| 140 |
+
device=DISTRICT_LLM_DEVICE,
|
| 141 |
+
fallback_action=DistrictAction.default_hold(
|
| 142 |
+
duration_steps=self.wrapper.district_decision_interval
|
| 143 |
+
),
|
| 144 |
+
)
|
| 145 |
+
except Exception as exc:
|
| 146 |
+
self._llm_error = f"{type(exc).__name__}: {exc}"
|
| 147 |
+
self._llm_inference = None
|
| 148 |
+
return self._llm_inference
|
| 149 |
+
|
| 150 |
+
def _llm_status(self) -> dict[str, Any]:
|
| 151 |
+
return {
|
| 152 |
+
"adapter_path": str(DISTRICT_LLM_ADAPTER_PATH),
|
| 153 |
+
"adapter_present": DISTRICT_LLM_ADAPTER_PATH.exists(),
|
| 154 |
+
"loaded": self._llm_inference is not None,
|
| 155 |
+
"load_attempted": self._llm_load_attempted,
|
| 156 |
+
"error": self._llm_error,
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
def _sync_state(self) -> None:
|
| 160 |
+
payload = self.wrapper.state()["state"]
|
| 161 |
+
self._state = AgenticTrafficState.model_validate(
|
| 162 |
+
{
|
| 163 |
+
**payload,
|
| 164 |
+
"episode_id": self._state.episode_id,
|
| 165 |
+
"step_count": self._state.step_count,
|
| 166 |
+
"llm": self._llm_status(),
|
| 167 |
+
}
|
| 168 |
+
)
|
server/remote_runner.py
CHANGED
|
@@ -1,58 +1,58 @@
|
|
| 1 |
-
"""HTTP client that delegates simulation runs to a remote OpenEnv API Space."""
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
import json
|
| 5 |
-
import logging
|
| 6 |
-
from pathlib import Path
|
| 7 |
-
from typing import Any
|
| 8 |
-
|
| 9 |
-
import httpx
|
| 10 |
-
|
| 11 |
-
logger = logging.getLogger(__name__)
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
def run_policy_remote(
|
| 15 |
-
city_id: str,
|
| 16 |
-
scenario_name: str,
|
| 17 |
-
policy_name: str,
|
| 18 |
-
openenv_api_url: str,
|
| 19 |
-
output_root: Path,
|
| 20 |
-
timeout: float = 120.0,
|
| 21 |
-
):
|
| 22 |
-
"""Call Space 1's /replay endpoint and write results to output_root."""
|
| 23 |
-
from server.policy_runner import RunResult
|
| 24 |
-
|
| 25 |
-
url = f"{openenv_api_url.rstrip('/')}/replay/{city_id}/{scenario_name}/{policy_name}"
|
| 26 |
-
logger.info("Remote replay request: %s", url)
|
| 27 |
-
|
| 28 |
-
with httpx.Client(timeout=timeout) as client:
|
| 29 |
-
resp = client.get(url)
|
| 30 |
-
resp.raise_for_status()
|
| 31 |
-
|
| 32 |
-
payload: dict[str, Any] = resp.json()
|
| 33 |
-
|
| 34 |
-
output_dir = output_root / city_id / scenario_name / policy_name
|
| 35 |
-
output_dir.mkdir(parents=True, exist_ok=True)
|
| 36 |
-
|
| 37 |
-
replay_path = output_dir / "replay.txt"
|
| 38 |
-
replay_path.write_text(payload["replay_text"], encoding="utf-8")
|
| 39 |
-
|
| 40 |
-
roadnet_log_path = output_dir / "roadnetLogFile.json"
|
| 41 |
-
if payload.get("roadnet_log"):
|
| 42 |
-
roadnet_log_path.write_text(
|
| 43 |
-
json.dumps(payload["roadnet_log"], indent=2), encoding="utf-8"
|
| 44 |
-
)
|
| 45 |
-
|
| 46 |
-
metrics_path = output_dir / "metrics.json"
|
| 47 |
-
metrics_path.write_text(
|
| 48 |
-
json.dumps(payload.get("metrics", {}), indent=2), encoding="utf-8"
|
| 49 |
-
)
|
| 50 |
-
|
| 51 |
-
return RunResult(
|
| 52 |
-
city_id=city_id,
|
| 53 |
-
scenario_name=scenario_name,
|
| 54 |
-
policy_name=policy_name,
|
| 55 |
-
replay_path=replay_path,
|
| 56 |
-
roadnet_log_path=roadnet_log_path,
|
| 57 |
-
metrics=payload.get("metrics", {}),
|
| 58 |
-
)
|
|
|
|
| 1 |
+
"""HTTP client that delegates simulation runs to a remote OpenEnv API Space."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
import logging
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
import httpx
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def run_policy_remote(
|
| 15 |
+
city_id: str,
|
| 16 |
+
scenario_name: str,
|
| 17 |
+
policy_name: str,
|
| 18 |
+
openenv_api_url: str,
|
| 19 |
+
output_root: Path,
|
| 20 |
+
timeout: float = 120.0,
|
| 21 |
+
):
|
| 22 |
+
"""Call Space 1's /replay endpoint and write results to output_root."""
|
| 23 |
+
from server.policy_runner import RunResult
|
| 24 |
+
|
| 25 |
+
url = f"{openenv_api_url.rstrip('/')}/replay/{city_id}/{scenario_name}/{policy_name}"
|
| 26 |
+
logger.info("Remote replay request: %s", url)
|
| 27 |
+
|
| 28 |
+
with httpx.Client(timeout=timeout) as client:
|
| 29 |
+
resp = client.get(url)
|
| 30 |
+
resp.raise_for_status()
|
| 31 |
+
|
| 32 |
+
payload: dict[str, Any] = resp.json()
|
| 33 |
+
|
| 34 |
+
output_dir = output_root / city_id / scenario_name / policy_name
|
| 35 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 36 |
+
|
| 37 |
+
replay_path = output_dir / "replay.txt"
|
| 38 |
+
replay_path.write_text(payload["replay_text"], encoding="utf-8")
|
| 39 |
+
|
| 40 |
+
roadnet_log_path = output_dir / "roadnetLogFile.json"
|
| 41 |
+
if payload.get("roadnet_log"):
|
| 42 |
+
roadnet_log_path.write_text(
|
| 43 |
+
json.dumps(payload["roadnet_log"], indent=2), encoding="utf-8"
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
metrics_path = output_dir / "metrics.json"
|
| 47 |
+
metrics_path.write_text(
|
| 48 |
+
json.dumps(payload.get("metrics", {}), indent=2), encoding="utf-8"
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
return RunResult(
|
| 52 |
+
city_id=city_id,
|
| 53 |
+
scenario_name=scenario_name,
|
| 54 |
+
policy_name=policy_name,
|
| 55 |
+
replay_path=replay_path,
|
| 56 |
+
roadnet_log_path=roadnet_log_path,
|
| 57 |
+
metrics=payload.get("metrics", {}),
|
| 58 |
+
)
|
server/roadnet_matcher.py
CHANGED
|
@@ -1,55 +1,55 @@
|
|
| 1 |
-
"""Match an uploaded roadnet.json to a known city in the dataset by fingerprint."""
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
import json
|
| 5 |
-
from pathlib import Path
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
def match_city_by_roadnet(roadnet_data: dict, generated_root: Path) -> str | None:
|
| 9 |
-
"""Return the city_id whose roadnet.json matches the uploaded data, or None."""
|
| 10 |
-
uploaded_fp = _fingerprint(roadnet_data)
|
| 11 |
-
if not uploaded_fp:
|
| 12 |
-
return None
|
| 13 |
-
|
| 14 |
-
for city_dir in sorted(generated_root.glob("city_*")):
|
| 15 |
-
roadnet_path = city_dir / "roadnet.json"
|
| 16 |
-
if not roadnet_path.exists():
|
| 17 |
-
continue
|
| 18 |
-
try:
|
| 19 |
-
candidate = json.loads(roadnet_path.read_text())
|
| 20 |
-
if _fingerprint(candidate) == uploaded_fp:
|
| 21 |
-
return city_dir.name
|
| 22 |
-
except Exception:
|
| 23 |
-
continue
|
| 24 |
-
|
| 25 |
-
return None
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def list_all_cities(generated_root: Path) -> list[str]:
|
| 29 |
-
return sorted(
|
| 30 |
-
d.name
|
| 31 |
-
for d in generated_root.glob("city_*")
|
| 32 |
-
if d.is_dir() and (d / "roadnet.json").exists()
|
| 33 |
-
)
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def list_scenarios_for_city(city_id: str, generated_root: Path) -> list[str]:
|
| 37 |
-
scenario_root = generated_root / city_id / "scenarios"
|
| 38 |
-
if not scenario_root.exists():
|
| 39 |
-
return []
|
| 40 |
-
return sorted(
|
| 41 |
-
d.name
|
| 42 |
-
for d in scenario_root.iterdir()
|
| 43 |
-
if d.is_dir()
|
| 44 |
-
and (d / "config.json").exists()
|
| 45 |
-
and (d / "flow.json").exists()
|
| 46 |
-
)
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def _fingerprint(roadnet: dict) -> frozenset[str]:
|
| 50 |
-
"""Fingerprint = set of non-virtual intersection IDs."""
|
| 51 |
-
return frozenset(
|
| 52 |
-
item["id"]
|
| 53 |
-
for item in roadnet.get("intersections", [])
|
| 54 |
-
if not item.get("virtual", False) and item.get("id")
|
| 55 |
-
)
|
|
|
|
| 1 |
+
"""Match an uploaded roadnet.json to a known city in the dataset by fingerprint."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def match_city_by_roadnet(roadnet_data: dict, generated_root: Path) -> str | None:
|
| 9 |
+
"""Return the city_id whose roadnet.json matches the uploaded data, or None."""
|
| 10 |
+
uploaded_fp = _fingerprint(roadnet_data)
|
| 11 |
+
if not uploaded_fp:
|
| 12 |
+
return None
|
| 13 |
+
|
| 14 |
+
for city_dir in sorted(generated_root.glob("city_*")):
|
| 15 |
+
roadnet_path = city_dir / "roadnet.json"
|
| 16 |
+
if not roadnet_path.exists():
|
| 17 |
+
continue
|
| 18 |
+
try:
|
| 19 |
+
candidate = json.loads(roadnet_path.read_text())
|
| 20 |
+
if _fingerprint(candidate) == uploaded_fp:
|
| 21 |
+
return city_dir.name
|
| 22 |
+
except Exception:
|
| 23 |
+
continue
|
| 24 |
+
|
| 25 |
+
return None
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def list_all_cities(generated_root: Path) -> list[str]:
|
| 29 |
+
return sorted(
|
| 30 |
+
d.name
|
| 31 |
+
for d in generated_root.glob("city_*")
|
| 32 |
+
if d.is_dir() and (d / "roadnet.json").exists()
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def list_scenarios_for_city(city_id: str, generated_root: Path) -> list[str]:
|
| 37 |
+
scenario_root = generated_root / city_id / "scenarios"
|
| 38 |
+
if not scenario_root.exists():
|
| 39 |
+
return []
|
| 40 |
+
return sorted(
|
| 41 |
+
d.name
|
| 42 |
+
for d in scenario_root.iterdir()
|
| 43 |
+
if d.is_dir()
|
| 44 |
+
and (d / "config.json").exists()
|
| 45 |
+
and (d / "flow.json").exists()
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _fingerprint(roadnet: dict) -> frozenset[str]:
|
| 50 |
+
"""Fingerprint = set of non-virtual intersection IDs."""
|
| 51 |
+
return frozenset(
|
| 52 |
+
item["id"]
|
| 53 |
+
for item in roadnet.get("intersections", [])
|
| 54 |
+
if not item.get("virtual", False) and item.get("id")
|
| 55 |
+
)
|
server/visualizer_app.py
CHANGED
|
@@ -1,404 +1,404 @@
|
|
| 1 |
-
"""FastAPI visualizer server for CityFlow multi-policy comparison dashboard.
|
| 2 |
-
|
| 3 |
-
Deployment modes
|
| 4 |
-
----------------
|
| 5 |
-
Local (no OPENENV_API_URL set):
|
| 6 |
-
Runs CityFlow simulations in-process via ``server.policy_runner``.
|
| 7 |
-
|
| 8 |
-
HF Space 2 (OPENENV_API_URL set to Space 1's URL):
|
| 9 |
-
Delegates all simulation runs to the remote OpenEnv API via
|
| 10 |
-
``server.remote_runner``. No CityFlow or torch needed locally.
|
| 11 |
-
"""
|
| 12 |
-
from __future__ import annotations
|
| 13 |
-
|
| 14 |
-
import json
|
| 15 |
-
import logging
|
| 16 |
-
import os
|
| 17 |
-
import sys
|
| 18 |
-
import time
|
| 19 |
-
from contextlib import asynccontextmanager
|
| 20 |
-
from pathlib import Path
|
| 21 |
-
from typing import Any
|
| 22 |
-
|
| 23 |
-
import httpx
|
| 24 |
-
from fastapi import FastAPI, HTTPException, UploadFile
|
| 25 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 26 |
-
from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse
|
| 27 |
-
from fastapi.staticfiles import StaticFiles
|
| 28 |
-
from joblib import Parallel, delayed
|
| 29 |
-
from pydantic import BaseModel
|
| 30 |
-
|
| 31 |
-
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 32 |
-
if str(REPO_ROOT) not in sys.path:
|
| 33 |
-
sys.path.insert(0, str(REPO_ROOT))
|
| 34 |
-
|
| 35 |
-
logger = logging.getLogger(__name__)
|
| 36 |
-
|
| 37 |
-
# ---------------------------------------------------------------------------
|
| 38 |
-
# Configuration — all overridable via environment variables
|
| 39 |
-
# ---------------------------------------------------------------------------
|
| 40 |
-
|
| 41 |
-
OPENENV_API_URL: str | None = os.environ.get("OPENENV_API_URL") or None
|
| 42 |
-
|
| 43 |
-
GENERATED_ROOT = Path(
|
| 44 |
-
os.environ.get("DATA_DIR", "") or (REPO_ROOT / "data" / "generated")
|
| 45 |
-
)
|
| 46 |
-
REPLAY_OUTPUT_ROOT = Path(
|
| 47 |
-
os.environ.get("REPLAY_ROOT", "") or (REPO_ROOT / "results" / "replays")
|
| 48 |
-
)
|
| 49 |
-
CHECKPOINT_PATH = Path(
|
| 50 |
-
os.environ.get("CHECKPOINT_PATH", "")
|
| 51 |
-
or (REPO_ROOT / "artifacts" / "dqn_shared" / "best_validation.pt")
|
| 52 |
-
)
|
| 53 |
-
FRONTEND_DIR = REPO_ROOT / "third_party" / "CityFlow" / "frontend"
|
| 54 |
-
|
| 55 |
-
# ---------------------------------------------------------------------------
|
| 56 |
-
# Runner selection: local (policy_runner) vs. remote (remote_runner)
|
| 57 |
-
# ---------------------------------------------------------------------------
|
| 58 |
-
|
| 59 |
-
from server.path_validators import validate_path_segment
|
| 60 |
-
from server.policy_runner import ALL_POLICIES, RunResult
|
| 61 |
-
|
| 62 |
-
if OPENENV_API_URL:
|
| 63 |
-
logger.info("Remote mode — OpenEnv API at %s", OPENENV_API_URL)
|
| 64 |
-
from server.remote_runner import run_policy_remote as _run_remote
|
| 65 |
-
|
| 66 |
-
def _run_policy(city_id: str, scenario_name: str, policy_name: str) -> RunResult:
|
| 67 |
-
return _run_remote(
|
| 68 |
-
city_id=city_id,
|
| 69 |
-
scenario_name=scenario_name,
|
| 70 |
-
policy_name=policy_name,
|
| 71 |
-
openenv_api_url=OPENENV_API_URL, # type: ignore[arg-type]
|
| 72 |
-
output_root=REPLAY_OUTPUT_ROOT,
|
| 73 |
-
)
|
| 74 |
-
|
| 75 |
-
else:
|
| 76 |
-
logger.info("Local mode — running CityFlow in-process")
|
| 77 |
-
from server.policy_runner import run_policy_for_city as _run_local
|
| 78 |
-
|
| 79 |
-
def _run_policy(city_id: str, scenario_name: str, policy_name: str) -> RunResult:
|
| 80 |
-
return _run_local(
|
| 81 |
-
city_id=city_id,
|
| 82 |
-
scenario_name=scenario_name,
|
| 83 |
-
policy_name=policy_name,
|
| 84 |
-
generated_root=GENERATED_ROOT,
|
| 85 |
-
output_root=REPLAY_OUTPUT_ROOT,
|
| 86 |
-
)
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
from server.roadnet_matcher import (
|
| 90 |
-
list_all_cities,
|
| 91 |
-
list_scenarios_for_city,
|
| 92 |
-
match_city_by_roadnet,
|
| 93 |
-
)
|
| 94 |
-
|
| 95 |
-
# ---------------------------------------------------------------------------
|
| 96 |
-
# Lifespan
|
| 97 |
-
# ---------------------------------------------------------------------------
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
@asynccontextmanager
|
| 101 |
-
async def lifespan(app: FastAPI):
|
| 102 |
-
REPLAY_OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)
|
| 103 |
-
|
| 104 |
-
if OPENENV_API_URL:
|
| 105 |
-
try:
|
| 106 |
-
with httpx.Client(timeout=10.0) as client:
|
| 107 |
-
resp = client.get(f"{OPENENV_API_URL.rstrip('/')}/health")
|
| 108 |
-
resp.raise_for_status()
|
| 109 |
-
logger.info("OpenEnv API health check passed: %s", OPENENV_API_URL)
|
| 110 |
-
except Exception as exc:
|
| 111 |
-
logger.warning(
|
| 112 |
-
"OpenEnv API at %s did not respond to /health: %s. "
|
| 113 |
-
"Simulation requests will fail until it is reachable.",
|
| 114 |
-
OPENENV_API_URL,
|
| 115 |
-
exc,
|
| 116 |
-
)
|
| 117 |
-
else:
|
| 118 |
-
from server.policy_runner import load_district_llm_inference, load_dqn_checkpoint
|
| 119 |
-
if CHECKPOINT_PATH.exists():
|
| 120 |
-
load_dqn_checkpoint(CHECKPOINT_PATH)
|
| 121 |
-
else:
|
| 122 |
-
logger.warning("Checkpoint not found at %s — 'learned' policy will fail", CHECKPOINT_PATH)
|
| 123 |
-
try:
|
| 124 |
-
load_district_llm_inference()
|
| 125 |
-
except Exception as exc:
|
| 126 |
-
logger.warning(
|
| 127 |
-
"District LLM prewarm failed: %s. "
|
| 128 |
-
"The llm_dqn policy will retry loading lazily on first use.",
|
| 129 |
-
exc,
|
| 130 |
-
)
|
| 131 |
-
|
| 132 |
-
yield
|
| 133 |
-
|
| 134 |
-
if not OPENENV_API_URL:
|
| 135 |
-
from server.policy_runner import unload_district_llm_inference
|
| 136 |
-
|
| 137 |
-
try:
|
| 138 |
-
unload_district_llm_inference()
|
| 139 |
-
except Exception as exc:
|
| 140 |
-
logger.warning("District LLM unload failed during shutdown: %s", exc)
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
# ---------------------------------------------------------------------------
|
| 144 |
-
# App setup
|
| 145 |
-
# ---------------------------------------------------------------------------
|
| 146 |
-
|
| 147 |
-
app = FastAPI(
|
| 148 |
-
title="Traffic Visualizer",
|
| 149 |
-
description="Multi-policy CityFlow replay comparison dashboard.",
|
| 150 |
-
version="1.0.0",
|
| 151 |
-
lifespan=lifespan,
|
| 152 |
-
)
|
| 153 |
-
|
| 154 |
-
app.add_middleware(
|
| 155 |
-
CORSMiddleware,
|
| 156 |
-
allow_origins=["*"],
|
| 157 |
-
allow_methods=["*"],
|
| 158 |
-
allow_headers=["*"],
|
| 159 |
-
)
|
| 160 |
-
|
| 161 |
-
app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="frontend")
|
| 162 |
-
|
| 163 |
-
# ---------------------------------------------------------------------------
|
| 164 |
-
# Request / response models
|
| 165 |
-
# ---------------------------------------------------------------------------
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
class RunSimulationsRequest(BaseModel):
|
| 169 |
-
city_id: str
|
| 170 |
-
scenario_name: str
|
| 171 |
-
policies: list[str] = list(ALL_POLICIES)
|
| 172 |
-
force: bool = False
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
class PolicyMetrics(BaseModel):
|
| 176 |
-
policy_name: str
|
| 177 |
-
metrics: dict[str, Any]
|
| 178 |
-
replay_available: bool
|
| 179 |
-
roadnet_log_available: bool
|
| 180 |
-
elapsed_ms: float | None = None
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
class RunSimulationsResponse(BaseModel):
|
| 184 |
-
city_id: str
|
| 185 |
-
scenario_name: str
|
| 186 |
-
results: list[PolicyMetrics]
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
# ---------------------------------------------------------------------------
|
| 190 |
-
# Endpoints
|
| 191 |
-
# ---------------------------------------------------------------------------
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
@app.get("/")
|
| 195 |
-
def root():
|
| 196 |
-
index_path = FRONTEND_DIR / "index.html"
|
| 197 |
-
if index_path.exists():
|
| 198 |
-
return FileResponse(str(index_path))
|
| 199 |
-
return JSONResponse({"status": "Traffic Visualizer API running"})
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
@app.post("/upload-roadnet")
|
| 203 |
-
async def upload_roadnet(file: UploadFile) -> dict:
|
| 204 |
-
raw = await file.read()
|
| 205 |
-
try:
|
| 206 |
-
roadnet_data = json.loads(raw)
|
| 207 |
-
except json.JSONDecodeError as exc:
|
| 208 |
-
raise HTTPException(status_code=400, detail=f"Invalid JSON: {exc}")
|
| 209 |
-
|
| 210 |
-
city_id = match_city_by_roadnet(roadnet_data, GENERATED_ROOT)
|
| 211 |
-
if city_id is None:
|
| 212 |
-
return {
|
| 213 |
-
"matched": False,
|
| 214 |
-
"city_id": None,
|
| 215 |
-
"scenarios": [],
|
| 216 |
-
"all_cities": list_all_cities(GENERATED_ROOT),
|
| 217 |
-
}
|
| 218 |
-
|
| 219 |
-
scenarios = list_scenarios_for_city(city_id, GENERATED_ROOT)
|
| 220 |
-
return {"matched": True, "city_id": city_id, "scenarios": scenarios, "all_cities": []}
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
@app.get("/cities")
|
| 224 |
-
def get_cities() -> dict:
|
| 225 |
-
return {"cities": list_all_cities(GENERATED_ROOT)}
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
@app.get("/cities/{city_id}/scenarios")
|
| 229 |
-
def get_scenarios(city_id: str) -> dict:
|
| 230 |
-
validate_path_segment(city_id, "city_id")
|
| 231 |
-
scenarios = list_scenarios_for_city(city_id, GENERATED_ROOT)
|
| 232 |
-
if not scenarios:
|
| 233 |
-
raise HTTPException(
|
| 234 |
-
status_code=404,
|
| 235 |
-
detail=f"City '{city_id}' not found or has no scenarios.",
|
| 236 |
-
)
|
| 237 |
-
return {"city_id": city_id, "scenarios": scenarios}
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
@app.get("/cities/{city_id}/district-map")
|
| 241 |
-
def get_district_map(city_id: str) -> JSONResponse:
|
| 242 |
-
validate_path_segment(city_id, "city_id")
|
| 243 |
-
district_map_path = GENERATED_ROOT / city_id / "district_map.json"
|
| 244 |
-
if not district_map_path.exists():
|
| 245 |
-
raise HTTPException(
|
| 246 |
-
status_code=404,
|
| 247 |
-
detail=f"District map not found for city '{city_id}'.",
|
| 248 |
-
)
|
| 249 |
-
return JSONResponse(json.loads(district_map_path.read_text(encoding="utf-8")))
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
@app.post("/run-simulations", response_model=RunSimulationsResponse)
|
| 253 |
-
def run_simulations(request: RunSimulationsRequest) -> RunSimulationsResponse:
|
| 254 |
-
validate_path_segment(request.city_id, "city_id")
|
| 255 |
-
validate_path_segment(request.scenario_name, "scenario_name")
|
| 256 |
-
|
| 257 |
-
valid_policies = set(ALL_POLICIES)
|
| 258 |
-
bad = [p for p in request.policies if p not in valid_policies]
|
| 259 |
-
if bad:
|
| 260 |
-
raise HTTPException(
|
| 261 |
-
status_code=400,
|
| 262 |
-
detail=f"Unknown policies: {bad}. Valid: {list(ALL_POLICIES)}",
|
| 263 |
-
)
|
| 264 |
-
|
| 265 |
-
def _run_one(policy_name: str) -> PolicyMetrics:
|
| 266 |
-
started_at = time.perf_counter()
|
| 267 |
-
output_dir = REPLAY_OUTPUT_ROOT / request.city_id / request.scenario_name / policy_name
|
| 268 |
-
replay_path = output_dir / "replay.txt"
|
| 269 |
-
roadnet_path = output_dir / "roadnetLogFile.json"
|
| 270 |
-
metrics_path = output_dir / "metrics.json"
|
| 271 |
-
|
| 272 |
-
if not request.force and replay_path.exists() and metrics_path.exists():
|
| 273 |
-
return PolicyMetrics(
|
| 274 |
-
policy_name=policy_name,
|
| 275 |
-
metrics=json.loads(metrics_path.read_text(encoding="utf-8")),
|
| 276 |
-
replay_available=True,
|
| 277 |
-
roadnet_log_available=roadnet_path.exists(),
|
| 278 |
-
elapsed_ms=0.0,
|
| 279 |
-
)
|
| 280 |
-
|
| 281 |
-
try:
|
| 282 |
-
result: RunResult = _run_policy(
|
| 283 |
-
city_id=request.city_id,
|
| 284 |
-
scenario_name=request.scenario_name,
|
| 285 |
-
policy_name=policy_name,
|
| 286 |
-
)
|
| 287 |
-
return PolicyMetrics(
|
| 288 |
-
policy_name=policy_name,
|
| 289 |
-
metrics=result.metrics,
|
| 290 |
-
replay_available=result.replay_path.exists(),
|
| 291 |
-
roadnet_log_available=result.roadnet_log_path.exists(),
|
| 292 |
-
elapsed_ms=(time.perf_counter() - started_at) * 1000.0,
|
| 293 |
-
)
|
| 294 |
-
except Exception as exc:
|
| 295 |
-
logger.error("Policy run failed for %s/%s/%s: %s", request.city_id, request.scenario_name, policy_name, exc)
|
| 296 |
-
return PolicyMetrics(
|
| 297 |
-
policy_name=policy_name,
|
| 298 |
-
metrics={"error": "Simulation failed. Check server logs."},
|
| 299 |
-
replay_available=False,
|
| 300 |
-
roadnet_log_available=False,
|
| 301 |
-
elapsed_ms=(time.perf_counter() - started_at) * 1000.0,
|
| 302 |
-
)
|
| 303 |
-
|
| 304 |
-
n_jobs = min(len(request.policies), 4)
|
| 305 |
-
results: list[PolicyMetrics] = Parallel(
|
| 306 |
-
n_jobs=n_jobs, prefer="threads"
|
| 307 |
-
)(delayed(_run_one)(p) for p in request.policies)
|
| 308 |
-
|
| 309 |
-
return RunSimulationsResponse(
|
| 310 |
-
city_id=request.city_id,
|
| 311 |
-
scenario_name=request.scenario_name,
|
| 312 |
-
results=results,
|
| 313 |
-
)
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
@app.get("/replay/{city_id}/{scenario_name}/{policy_name}", response_model=None)
|
| 317 |
-
def get_replay(
|
| 318 |
-
city_id: str,
|
| 319 |
-
scenario_name: str,
|
| 320 |
-
policy_name: str,
|
| 321 |
-
max_steps: int = 0,
|
| 322 |
-
) -> PlainTextResponse | FileResponse:
|
| 323 |
-
validate_path_segment(city_id, "city_id")
|
| 324 |
-
validate_path_segment(scenario_name, "scenario_name")
|
| 325 |
-
validate_path_segment(policy_name, "policy_name")
|
| 326 |
-
|
| 327 |
-
replay_path = REPLAY_OUTPUT_ROOT / city_id / scenario_name / policy_name / "replay.txt"
|
| 328 |
-
if not replay_path.exists():
|
| 329 |
-
raise HTTPException(
|
| 330 |
-
status_code=404,
|
| 331 |
-
detail=f"Replay not found for {city_id}/{scenario_name}/{policy_name}. Run /run-simulations first.",
|
| 332 |
-
)
|
| 333 |
-
if max_steps > 0:
|
| 334 |
-
lines: list[str] = []
|
| 335 |
-
with open(replay_path, encoding="utf-8") as fh:
|
| 336 |
-
for raw in fh:
|
| 337 |
-
if raw.strip():
|
| 338 |
-
lines.append(raw.rstrip("\n"))
|
| 339 |
-
if len(lines) >= max_steps:
|
| 340 |
-
break
|
| 341 |
-
return PlainTextResponse("\n".join(lines))
|
| 342 |
-
return FileResponse(str(replay_path), media_type="text/plain")
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
@app.get("/roadnet-log/{city_id}/{scenario_name}/{policy_name}")
|
| 346 |
-
def get_roadnet_log(city_id: str, scenario_name: str, policy_name: str) -> JSONResponse:
|
| 347 |
-
validate_path_segment(city_id, "city_id")
|
| 348 |
-
validate_path_segment(scenario_name, "scenario_name")
|
| 349 |
-
validate_path_segment(policy_name, "policy_name")
|
| 350 |
-
|
| 351 |
-
path = REPLAY_OUTPUT_ROOT / city_id / scenario_name / policy_name / "roadnetLogFile.json"
|
| 352 |
-
if not path.exists():
|
| 353 |
-
for p in ALL_POLICIES:
|
| 354 |
-
fallback = REPLAY_OUTPUT_ROOT / city_id / scenario_name / p / "roadnetLogFile.json"
|
| 355 |
-
if fallback.exists():
|
| 356 |
-
path = fallback
|
| 357 |
-
break
|
| 358 |
-
if not path.exists():
|
| 359 |
-
raise HTTPException(
|
| 360 |
-
status_code=404,
|
| 361 |
-
detail=f"Roadnet log not found for {city_id}/{scenario_name}.",
|
| 362 |
-
)
|
| 363 |
-
return JSONResponse(json.loads(path.read_text(encoding="utf-8")))
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
@app.get("/metrics/{city_id}/{scenario_name}")
|
| 367 |
-
def get_metrics(city_id: str, scenario_name: str) -> dict:
|
| 368 |
-
validate_path_segment(city_id, "city_id")
|
| 369 |
-
validate_path_segment(scenario_name, "scenario_name")
|
| 370 |
-
|
| 371 |
-
base = REPLAY_OUTPUT_ROOT / city_id / scenario_name
|
| 372 |
-
if not base.exists():
|
| 373 |
-
raise HTTPException(
|
| 374 |
-
status_code=404,
|
| 375 |
-
detail=f"No simulation results found for {city_id}/{scenario_name}.",
|
| 376 |
-
)
|
| 377 |
-
|
| 378 |
-
metrics: dict[str, Any] = {}
|
| 379 |
-
for policy_dir in sorted(base.iterdir()):
|
| 380 |
-
if not policy_dir.is_dir():
|
| 381 |
-
continue
|
| 382 |
-
metrics_path = policy_dir / "metrics.json"
|
| 383 |
-
replay_path = policy_dir / "replay.txt"
|
| 384 |
-
roadnet_log_path = policy_dir / "roadnetLogFile.json"
|
| 385 |
-
payload: dict[str, Any] = {}
|
| 386 |
-
if metrics_path.exists():
|
| 387 |
-
payload.update(json.loads(metrics_path.read_text(encoding="utf-8")))
|
| 388 |
-
if replay_path.exists():
|
| 389 |
-
payload["replay_available"] = True
|
| 390 |
-
if roadnet_log_path.exists():
|
| 391 |
-
payload["roadnet_log_available"] = True
|
| 392 |
-
if payload:
|
| 393 |
-
metrics[policy_dir.name] = payload
|
| 394 |
-
|
| 395 |
-
return {"city_id": city_id, "scenario_name": scenario_name, "metrics": metrics}
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
# ---------------------------------------------------------------------------
|
| 399 |
-
# Entry point
|
| 400 |
-
# ---------------------------------------------------------------------------
|
| 401 |
-
|
| 402 |
-
if __name__ == "__main__":
|
| 403 |
-
import uvicorn
|
| 404 |
-
uvicorn.run("server.visualizer_app:app", host="0.0.0.0", port=8080, reload=False)
|
|
|
|
| 1 |
+
"""FastAPI visualizer server for CityFlow multi-policy comparison dashboard.
|
| 2 |
+
|
| 3 |
+
Deployment modes
|
| 4 |
+
----------------
|
| 5 |
+
Local (no OPENENV_API_URL set):
|
| 6 |
+
Runs CityFlow simulations in-process via ``server.policy_runner``.
|
| 7 |
+
|
| 8 |
+
HF Space 2 (OPENENV_API_URL set to Space 1's URL):
|
| 9 |
+
Delegates all simulation runs to the remote OpenEnv API via
|
| 10 |
+
``server.remote_runner``. No CityFlow or torch needed locally.
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import json
|
| 15 |
+
import logging
|
| 16 |
+
import os
|
| 17 |
+
import sys
|
| 18 |
+
import time
|
| 19 |
+
from contextlib import asynccontextmanager
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
from typing import Any
|
| 22 |
+
|
| 23 |
+
import httpx
|
| 24 |
+
from fastapi import FastAPI, HTTPException, UploadFile
|
| 25 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 26 |
+
from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse
|
| 27 |
+
from fastapi.staticfiles import StaticFiles
|
| 28 |
+
from joblib import Parallel, delayed
|
| 29 |
+
from pydantic import BaseModel
|
| 30 |
+
|
| 31 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 32 |
+
if str(REPO_ROOT) not in sys.path:
|
| 33 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 34 |
+
|
| 35 |
+
logger = logging.getLogger(__name__)
|
| 36 |
+
|
| 37 |
+
# ---------------------------------------------------------------------------
|
| 38 |
+
# Configuration — all overridable via environment variables
|
| 39 |
+
# ---------------------------------------------------------------------------
|
| 40 |
+
|
| 41 |
+
OPENENV_API_URL: str | None = os.environ.get("OPENENV_API_URL") or None
|
| 42 |
+
|
| 43 |
+
GENERATED_ROOT = Path(
|
| 44 |
+
os.environ.get("DATA_DIR", "") or (REPO_ROOT / "data" / "generated")
|
| 45 |
+
)
|
| 46 |
+
REPLAY_OUTPUT_ROOT = Path(
|
| 47 |
+
os.environ.get("REPLAY_ROOT", "") or (REPO_ROOT / "results" / "replays")
|
| 48 |
+
)
|
| 49 |
+
CHECKPOINT_PATH = Path(
|
| 50 |
+
os.environ.get("CHECKPOINT_PATH", "")
|
| 51 |
+
or (REPO_ROOT / "artifacts" / "dqn_shared" / "best_validation.pt")
|
| 52 |
+
)
|
| 53 |
+
FRONTEND_DIR = REPO_ROOT / "third_party" / "CityFlow" / "frontend"
|
| 54 |
+
|
| 55 |
+
# ---------------------------------------------------------------------------
|
| 56 |
+
# Runner selection: local (policy_runner) vs. remote (remote_runner)
|
| 57 |
+
# ---------------------------------------------------------------------------
|
| 58 |
+
|
| 59 |
+
from server.path_validators import validate_path_segment
|
| 60 |
+
from server.policy_runner import ALL_POLICIES, RunResult
|
| 61 |
+
|
| 62 |
+
if OPENENV_API_URL:
|
| 63 |
+
logger.info("Remote mode — OpenEnv API at %s", OPENENV_API_URL)
|
| 64 |
+
from server.remote_runner import run_policy_remote as _run_remote
|
| 65 |
+
|
| 66 |
+
def _run_policy(city_id: str, scenario_name: str, policy_name: str) -> RunResult:
|
| 67 |
+
return _run_remote(
|
| 68 |
+
city_id=city_id,
|
| 69 |
+
scenario_name=scenario_name,
|
| 70 |
+
policy_name=policy_name,
|
| 71 |
+
openenv_api_url=OPENENV_API_URL, # type: ignore[arg-type]
|
| 72 |
+
output_root=REPLAY_OUTPUT_ROOT,
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
else:
|
| 76 |
+
logger.info("Local mode — running CityFlow in-process")
|
| 77 |
+
from server.policy_runner import run_policy_for_city as _run_local
|
| 78 |
+
|
| 79 |
+
def _run_policy(city_id: str, scenario_name: str, policy_name: str) -> RunResult:
|
| 80 |
+
return _run_local(
|
| 81 |
+
city_id=city_id,
|
| 82 |
+
scenario_name=scenario_name,
|
| 83 |
+
policy_name=policy_name,
|
| 84 |
+
generated_root=GENERATED_ROOT,
|
| 85 |
+
output_root=REPLAY_OUTPUT_ROOT,
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
from server.roadnet_matcher import (
|
| 90 |
+
list_all_cities,
|
| 91 |
+
list_scenarios_for_city,
|
| 92 |
+
match_city_by_roadnet,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
# ---------------------------------------------------------------------------
|
| 96 |
+
# Lifespan
|
| 97 |
+
# ---------------------------------------------------------------------------
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@asynccontextmanager
|
| 101 |
+
async def lifespan(app: FastAPI):
|
| 102 |
+
REPLAY_OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)
|
| 103 |
+
|
| 104 |
+
if OPENENV_API_URL:
|
| 105 |
+
try:
|
| 106 |
+
with httpx.Client(timeout=10.0) as client:
|
| 107 |
+
resp = client.get(f"{OPENENV_API_URL.rstrip('/')}/health")
|
| 108 |
+
resp.raise_for_status()
|
| 109 |
+
logger.info("OpenEnv API health check passed: %s", OPENENV_API_URL)
|
| 110 |
+
except Exception as exc:
|
| 111 |
+
logger.warning(
|
| 112 |
+
"OpenEnv API at %s did not respond to /health: %s. "
|
| 113 |
+
"Simulation requests will fail until it is reachable.",
|
| 114 |
+
OPENENV_API_URL,
|
| 115 |
+
exc,
|
| 116 |
+
)
|
| 117 |
+
else:
|
| 118 |
+
from server.policy_runner import load_district_llm_inference, load_dqn_checkpoint
|
| 119 |
+
if CHECKPOINT_PATH.exists():
|
| 120 |
+
load_dqn_checkpoint(CHECKPOINT_PATH)
|
| 121 |
+
else:
|
| 122 |
+
logger.warning("Checkpoint not found at %s — 'learned' policy will fail", CHECKPOINT_PATH)
|
| 123 |
+
try:
|
| 124 |
+
load_district_llm_inference()
|
| 125 |
+
except Exception as exc:
|
| 126 |
+
logger.warning(
|
| 127 |
+
"District LLM prewarm failed: %s. "
|
| 128 |
+
"The llm_dqn policy will retry loading lazily on first use.",
|
| 129 |
+
exc,
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
yield
|
| 133 |
+
|
| 134 |
+
if not OPENENV_API_URL:
|
| 135 |
+
from server.policy_runner import unload_district_llm_inference
|
| 136 |
+
|
| 137 |
+
try:
|
| 138 |
+
unload_district_llm_inference()
|
| 139 |
+
except Exception as exc:
|
| 140 |
+
logger.warning("District LLM unload failed during shutdown: %s", exc)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
# ---------------------------------------------------------------------------
|
| 144 |
+
# App setup
|
| 145 |
+
# ---------------------------------------------------------------------------
|
| 146 |
+
|
| 147 |
+
app = FastAPI(
|
| 148 |
+
title="Traffic Visualizer",
|
| 149 |
+
description="Multi-policy CityFlow replay comparison dashboard.",
|
| 150 |
+
version="1.0.0",
|
| 151 |
+
lifespan=lifespan,
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
app.add_middleware(
|
| 155 |
+
CORSMiddleware,
|
| 156 |
+
allow_origins=["*"],
|
| 157 |
+
allow_methods=["*"],
|
| 158 |
+
allow_headers=["*"],
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="frontend")
|
| 162 |
+
|
| 163 |
+
# ---------------------------------------------------------------------------
|
| 164 |
+
# Request / response models
|
| 165 |
+
# ---------------------------------------------------------------------------
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
class RunSimulationsRequest(BaseModel):
|
| 169 |
+
city_id: str
|
| 170 |
+
scenario_name: str
|
| 171 |
+
policies: list[str] = list(ALL_POLICIES)
|
| 172 |
+
force: bool = False
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
class PolicyMetrics(BaseModel):
|
| 176 |
+
policy_name: str
|
| 177 |
+
metrics: dict[str, Any]
|
| 178 |
+
replay_available: bool
|
| 179 |
+
roadnet_log_available: bool
|
| 180 |
+
elapsed_ms: float | None = None
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
class RunSimulationsResponse(BaseModel):
|
| 184 |
+
city_id: str
|
| 185 |
+
scenario_name: str
|
| 186 |
+
results: list[PolicyMetrics]
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
# ---------------------------------------------------------------------------
|
| 190 |
+
# Endpoints
|
| 191 |
+
# ---------------------------------------------------------------------------
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
@app.get("/")
|
| 195 |
+
def root():
|
| 196 |
+
index_path = FRONTEND_DIR / "index.html"
|
| 197 |
+
if index_path.exists():
|
| 198 |
+
return FileResponse(str(index_path))
|
| 199 |
+
return JSONResponse({"status": "Traffic Visualizer API running"})
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
@app.post("/upload-roadnet")
|
| 203 |
+
async def upload_roadnet(file: UploadFile) -> dict:
|
| 204 |
+
raw = await file.read()
|
| 205 |
+
try:
|
| 206 |
+
roadnet_data = json.loads(raw)
|
| 207 |
+
except json.JSONDecodeError as exc:
|
| 208 |
+
raise HTTPException(status_code=400, detail=f"Invalid JSON: {exc}")
|
| 209 |
+
|
| 210 |
+
city_id = match_city_by_roadnet(roadnet_data, GENERATED_ROOT)
|
| 211 |
+
if city_id is None:
|
| 212 |
+
return {
|
| 213 |
+
"matched": False,
|
| 214 |
+
"city_id": None,
|
| 215 |
+
"scenarios": [],
|
| 216 |
+
"all_cities": list_all_cities(GENERATED_ROOT),
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
scenarios = list_scenarios_for_city(city_id, GENERATED_ROOT)
|
| 220 |
+
return {"matched": True, "city_id": city_id, "scenarios": scenarios, "all_cities": []}
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
@app.get("/cities")
|
| 224 |
+
def get_cities() -> dict:
|
| 225 |
+
return {"cities": list_all_cities(GENERATED_ROOT)}
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
@app.get("/cities/{city_id}/scenarios")
|
| 229 |
+
def get_scenarios(city_id: str) -> dict:
|
| 230 |
+
validate_path_segment(city_id, "city_id")
|
| 231 |
+
scenarios = list_scenarios_for_city(city_id, GENERATED_ROOT)
|
| 232 |
+
if not scenarios:
|
| 233 |
+
raise HTTPException(
|
| 234 |
+
status_code=404,
|
| 235 |
+
detail=f"City '{city_id}' not found or has no scenarios.",
|
| 236 |
+
)
|
| 237 |
+
return {"city_id": city_id, "scenarios": scenarios}
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
@app.get("/cities/{city_id}/district-map")
|
| 241 |
+
def get_district_map(city_id: str) -> JSONResponse:
|
| 242 |
+
validate_path_segment(city_id, "city_id")
|
| 243 |
+
district_map_path = GENERATED_ROOT / city_id / "district_map.json"
|
| 244 |
+
if not district_map_path.exists():
|
| 245 |
+
raise HTTPException(
|
| 246 |
+
status_code=404,
|
| 247 |
+
detail=f"District map not found for city '{city_id}'.",
|
| 248 |
+
)
|
| 249 |
+
return JSONResponse(json.loads(district_map_path.read_text(encoding="utf-8")))
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
@app.post("/run-simulations", response_model=RunSimulationsResponse)
|
| 253 |
+
def run_simulations(request: RunSimulationsRequest) -> RunSimulationsResponse:
|
| 254 |
+
validate_path_segment(request.city_id, "city_id")
|
| 255 |
+
validate_path_segment(request.scenario_name, "scenario_name")
|
| 256 |
+
|
| 257 |
+
valid_policies = set(ALL_POLICIES)
|
| 258 |
+
bad = [p for p in request.policies if p not in valid_policies]
|
| 259 |
+
if bad:
|
| 260 |
+
raise HTTPException(
|
| 261 |
+
status_code=400,
|
| 262 |
+
detail=f"Unknown policies: {bad}. Valid: {list(ALL_POLICIES)}",
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
def _run_one(policy_name: str) -> PolicyMetrics:
|
| 266 |
+
started_at = time.perf_counter()
|
| 267 |
+
output_dir = REPLAY_OUTPUT_ROOT / request.city_id / request.scenario_name / policy_name
|
| 268 |
+
replay_path = output_dir / "replay.txt"
|
| 269 |
+
roadnet_path = output_dir / "roadnetLogFile.json"
|
| 270 |
+
metrics_path = output_dir / "metrics.json"
|
| 271 |
+
|
| 272 |
+
if not request.force and replay_path.exists() and metrics_path.exists():
|
| 273 |
+
return PolicyMetrics(
|
| 274 |
+
policy_name=policy_name,
|
| 275 |
+
metrics=json.loads(metrics_path.read_text(encoding="utf-8")),
|
| 276 |
+
replay_available=True,
|
| 277 |
+
roadnet_log_available=roadnet_path.exists(),
|
| 278 |
+
elapsed_ms=0.0,
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
try:
|
| 282 |
+
result: RunResult = _run_policy(
|
| 283 |
+
city_id=request.city_id,
|
| 284 |
+
scenario_name=request.scenario_name,
|
| 285 |
+
policy_name=policy_name,
|
| 286 |
+
)
|
| 287 |
+
return PolicyMetrics(
|
| 288 |
+
policy_name=policy_name,
|
| 289 |
+
metrics=result.metrics,
|
| 290 |
+
replay_available=result.replay_path.exists(),
|
| 291 |
+
roadnet_log_available=result.roadnet_log_path.exists(),
|
| 292 |
+
elapsed_ms=(time.perf_counter() - started_at) * 1000.0,
|
| 293 |
+
)
|
| 294 |
+
except Exception as exc:
|
| 295 |
+
logger.error("Policy run failed for %s/%s/%s: %s", request.city_id, request.scenario_name, policy_name, exc)
|
| 296 |
+
return PolicyMetrics(
|
| 297 |
+
policy_name=policy_name,
|
| 298 |
+
metrics={"error": "Simulation failed. Check server logs."},
|
| 299 |
+
replay_available=False,
|
| 300 |
+
roadnet_log_available=False,
|
| 301 |
+
elapsed_ms=(time.perf_counter() - started_at) * 1000.0,
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
n_jobs = min(len(request.policies), 4)
|
| 305 |
+
results: list[PolicyMetrics] = Parallel(
|
| 306 |
+
n_jobs=n_jobs, prefer="threads"
|
| 307 |
+
)(delayed(_run_one)(p) for p in request.policies)
|
| 308 |
+
|
| 309 |
+
return RunSimulationsResponse(
|
| 310 |
+
city_id=request.city_id,
|
| 311 |
+
scenario_name=request.scenario_name,
|
| 312 |
+
results=results,
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
@app.get("/replay/{city_id}/{scenario_name}/{policy_name}", response_model=None)
|
| 317 |
+
def get_replay(
|
| 318 |
+
city_id: str,
|
| 319 |
+
scenario_name: str,
|
| 320 |
+
policy_name: str,
|
| 321 |
+
max_steps: int = 0,
|
| 322 |
+
) -> PlainTextResponse | FileResponse:
|
| 323 |
+
validate_path_segment(city_id, "city_id")
|
| 324 |
+
validate_path_segment(scenario_name, "scenario_name")
|
| 325 |
+
validate_path_segment(policy_name, "policy_name")
|
| 326 |
+
|
| 327 |
+
replay_path = REPLAY_OUTPUT_ROOT / city_id / scenario_name / policy_name / "replay.txt"
|
| 328 |
+
if not replay_path.exists():
|
| 329 |
+
raise HTTPException(
|
| 330 |
+
status_code=404,
|
| 331 |
+
detail=f"Replay not found for {city_id}/{scenario_name}/{policy_name}. Run /run-simulations first.",
|
| 332 |
+
)
|
| 333 |
+
if max_steps > 0:
|
| 334 |
+
lines: list[str] = []
|
| 335 |
+
with open(replay_path, encoding="utf-8") as fh:
|
| 336 |
+
for raw in fh:
|
| 337 |
+
if raw.strip():
|
| 338 |
+
lines.append(raw.rstrip("\n"))
|
| 339 |
+
if len(lines) >= max_steps:
|
| 340 |
+
break
|
| 341 |
+
return PlainTextResponse("\n".join(lines))
|
| 342 |
+
return FileResponse(str(replay_path), media_type="text/plain")
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
@app.get("/roadnet-log/{city_id}/{scenario_name}/{policy_name}")
|
| 346 |
+
def get_roadnet_log(city_id: str, scenario_name: str, policy_name: str) -> JSONResponse:
|
| 347 |
+
validate_path_segment(city_id, "city_id")
|
| 348 |
+
validate_path_segment(scenario_name, "scenario_name")
|
| 349 |
+
validate_path_segment(policy_name, "policy_name")
|
| 350 |
+
|
| 351 |
+
path = REPLAY_OUTPUT_ROOT / city_id / scenario_name / policy_name / "roadnetLogFile.json"
|
| 352 |
+
if not path.exists():
|
| 353 |
+
for p in ALL_POLICIES:
|
| 354 |
+
fallback = REPLAY_OUTPUT_ROOT / city_id / scenario_name / p / "roadnetLogFile.json"
|
| 355 |
+
if fallback.exists():
|
| 356 |
+
path = fallback
|
| 357 |
+
break
|
| 358 |
+
if not path.exists():
|
| 359 |
+
raise HTTPException(
|
| 360 |
+
status_code=404,
|
| 361 |
+
detail=f"Roadnet log not found for {city_id}/{scenario_name}.",
|
| 362 |
+
)
|
| 363 |
+
return JSONResponse(json.loads(path.read_text(encoding="utf-8")))
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
@app.get("/metrics/{city_id}/{scenario_name}")
|
| 367 |
+
def get_metrics(city_id: str, scenario_name: str) -> dict:
|
| 368 |
+
validate_path_segment(city_id, "city_id")
|
| 369 |
+
validate_path_segment(scenario_name, "scenario_name")
|
| 370 |
+
|
| 371 |
+
base = REPLAY_OUTPUT_ROOT / city_id / scenario_name
|
| 372 |
+
if not base.exists():
|
| 373 |
+
raise HTTPException(
|
| 374 |
+
status_code=404,
|
| 375 |
+
detail=f"No simulation results found for {city_id}/{scenario_name}.",
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
metrics: dict[str, Any] = {}
|
| 379 |
+
for policy_dir in sorted(base.iterdir()):
|
| 380 |
+
if not policy_dir.is_dir():
|
| 381 |
+
continue
|
| 382 |
+
metrics_path = policy_dir / "metrics.json"
|
| 383 |
+
replay_path = policy_dir / "replay.txt"
|
| 384 |
+
roadnet_log_path = policy_dir / "roadnetLogFile.json"
|
| 385 |
+
payload: dict[str, Any] = {}
|
| 386 |
+
if metrics_path.exists():
|
| 387 |
+
payload.update(json.loads(metrics_path.read_text(encoding="utf-8")))
|
| 388 |
+
if replay_path.exists():
|
| 389 |
+
payload["replay_available"] = True
|
| 390 |
+
if roadnet_log_path.exists():
|
| 391 |
+
payload["roadnet_log_available"] = True
|
| 392 |
+
if payload:
|
| 393 |
+
metrics[policy_dir.name] = payload
|
| 394 |
+
|
| 395 |
+
return {"city_id": city_id, "scenario_name": scenario_name, "metrics": metrics}
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
# ---------------------------------------------------------------------------
|
| 399 |
+
# Entry point
|
| 400 |
+
# ---------------------------------------------------------------------------
|
| 401 |
+
|
| 402 |
+
if __name__ == "__main__":
|
| 403 |
+
import uvicorn
|
| 404 |
+
uvicorn.run("server.visualizer_app:app", host="0.0.0.0", port=8080, reload=False)
|
third_party/CityFlow/.gitignore
CHANGED
|
@@ -1,14 +1,14 @@
|
|
| 1 |
-
data/
|
| 2 |
-
build/
|
| 3 |
-
build-docker/
|
| 4 |
-
local/
|
| 5 |
-
.vs/
|
| 6 |
-
.vscode/
|
| 7 |
-
.idea/
|
| 8 |
-
.DS_Store
|
| 9 |
-
__pycache__
|
| 10 |
-
CMakeSettings.json
|
| 11 |
-
cmake-build-*
|
| 12 |
-
CityFlow.egg-info
|
| 13 |
-
frontend/replay/*
|
| 14 |
results/replays
|
|
|
|
| 1 |
+
data/
|
| 2 |
+
build/
|
| 3 |
+
build-docker/
|
| 4 |
+
local/
|
| 5 |
+
.vs/
|
| 6 |
+
.vscode/
|
| 7 |
+
.idea/
|
| 8 |
+
.DS_Store
|
| 9 |
+
__pycache__
|
| 10 |
+
CMakeSettings.json
|
| 11 |
+
cmake-build-*
|
| 12 |
+
CityFlow.egg-info
|
| 13 |
+
frontend/replay/*
|
| 14 |
results/replays
|
third_party/CityFlow/.gitmodules
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
[submodule "extern/rapidjson"]
|
| 2 |
-
path = extern/rapidjson
|
| 3 |
-
url = https://github.com/cityflow-project/rapidjson.git
|
| 4 |
-
[submodule "extern/pybind11"]
|
| 5 |
-
path = extern/pybind11
|
| 6 |
-
url = https://github.com/cityflow-project/pybind11.git
|
|
|
|
| 1 |
+
[submodule "extern/rapidjson"]
|
| 2 |
+
path = extern/rapidjson
|
| 3 |
+
url = https://github.com/cityflow-project/rapidjson.git
|
| 4 |
+
[submodule "extern/pybind11"]
|
| 5 |
+
path = extern/pybind11
|
| 6 |
+
url = https://github.com/cityflow-project/pybind11.git
|
third_party/CityFlow/CMakeLists.txt
CHANGED
|
@@ -1,76 +1,76 @@
|
|
| 1 |
-
cmake_minimum_required(VERSION 3.5)
|
| 2 |
-
project(cityflow)
|
| 3 |
-
|
| 4 |
-
set(CMAKE_CXX_STANDARD "11" CACHE STRING "")
|
| 5 |
-
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -DRAPIDJSON_HAS_STDSTRING=1")
|
| 6 |
-
set(CMAKE_CXX_FLAGS_RELEASE "-O2")
|
| 7 |
-
set(CMAKE_CXX_FLAGS_DEBUG "-g")
|
| 8 |
-
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
| 9 |
-
|
| 10 |
-
if(POLICY CMP0063)
|
| 11 |
-
cmake_policy(SET CMP0063 NEW)
|
| 12 |
-
endif()
|
| 13 |
-
|
| 14 |
-
if(NOT CMAKE_BUILD_TYPE)
|
| 15 |
-
set(CMAKE_BUILD_TYPE Release)
|
| 16 |
-
endif(NOT CMAKE_BUILD_TYPE)
|
| 17 |
-
|
| 18 |
-
include_directories(extern/milo)
|
| 19 |
-
|
| 20 |
-
set(REQUIRED_SUBMODULES
|
| 21 |
-
"extern/pybind11/CMakeLists.txt"
|
| 22 |
-
"extern/rapidjson/include"
|
| 23 |
-
)
|
| 24 |
-
|
| 25 |
-
foreach(REQUIRED_SUBMODULE ${REQUIRED_SUBMODULES})
|
| 26 |
-
if(NOT EXISTS "${PROJECT_SOURCE_DIR}/${REQUIRED_SUBMODULE}")
|
| 27 |
-
# update submodule
|
| 28 |
-
# https://cliutils.gitlab.io/modern-cmake/chapters/projects/submodule.html
|
| 29 |
-
find_package(Git QUIET)
|
| 30 |
-
if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git")
|
| 31 |
-
# Update submodules as needed
|
| 32 |
-
option(GIT_SUBMODULE "Check submodules during build" ON)
|
| 33 |
-
if(GIT_SUBMODULE)
|
| 34 |
-
message(STATUS "Submodule update, this may take some time...")
|
| 35 |
-
execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive
|
| 36 |
-
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
| 37 |
-
RESULT_VARIABLE GIT_SUBMOD_RESULT)
|
| 38 |
-
if(NOT GIT_SUBMOD_RESULT EQUAL "0")
|
| 39 |
-
message(FATAL_ERROR "git submodule update --init failed with ${GIT_SUBMOD_RESULT}, please checkout submodules")
|
| 40 |
-
endif()
|
| 41 |
-
endif()
|
| 42 |
-
endif()
|
| 43 |
-
break()
|
| 44 |
-
else()
|
| 45 |
-
message(STATUS "Found Submodule: ${REQUIRED_SUBMODULE}")
|
| 46 |
-
endif()
|
| 47 |
-
endforeach()
|
| 48 |
-
|
| 49 |
-
foreach(REQUIRED_SUBMODULE ${REQUIRED_SUBMODULES})
|
| 50 |
-
if(NOT EXISTS "${PROJECT_SOURCE_DIR}/${REQUIRED_SUBMODULE}")
|
| 51 |
-
message(FATAL_ERROR "The submodule ${REQUIRED_SUBMODULE} was not downloaded! GIT_SUBMODULE was turned off or failed. Please update submodules and try again.")
|
| 52 |
-
endif()
|
| 53 |
-
endforeach()
|
| 54 |
-
|
| 55 |
-
add_subdirectory(extern/pybind11)
|
| 56 |
-
include_directories(extern/rapidjson/include)
|
| 57 |
-
|
| 58 |
-
add_subdirectory(src)
|
| 59 |
-
|
| 60 |
-
# Tests
|
| 61 |
-
find_package(GTest)
|
| 62 |
-
if(GTEST_FOUND)
|
| 63 |
-
enable_testing()
|
| 64 |
-
add_subdirectory(tests)
|
| 65 |
-
endif()
|
| 66 |
-
|
| 67 |
-
if (${CMAKE_BUILD_TYPE} STREQUAL Debug)
|
| 68 |
-
add_subdirectory(tools/debug)
|
| 69 |
-
endif()
|
| 70 |
-
|
| 71 |
-
pybind11_add_module(${PROJECT_NAME} MODULE src/cityflow.cpp)
|
| 72 |
-
target_link_libraries(${PROJECT_NAME} PRIVATE ${PROJECT_LIB_NAME})
|
| 73 |
-
if(VERSION)
|
| 74 |
-
target_compile_definitions(${PROJECT_NAME} PRIVATE -DVERSION=${VERSION})
|
| 75 |
-
endif()
|
| 76 |
-
|
|
|
|
| 1 |
+
cmake_minimum_required(VERSION 3.5)
|
| 2 |
+
project(cityflow)
|
| 3 |
+
|
| 4 |
+
set(CMAKE_CXX_STANDARD "11" CACHE STRING "")
|
| 5 |
+
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -DRAPIDJSON_HAS_STDSTRING=1")
|
| 6 |
+
set(CMAKE_CXX_FLAGS_RELEASE "-O2")
|
| 7 |
+
set(CMAKE_CXX_FLAGS_DEBUG "-g")
|
| 8 |
+
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
| 9 |
+
|
| 10 |
+
if(POLICY CMP0063)
|
| 11 |
+
cmake_policy(SET CMP0063 NEW)
|
| 12 |
+
endif()
|
| 13 |
+
|
| 14 |
+
if(NOT CMAKE_BUILD_TYPE)
|
| 15 |
+
set(CMAKE_BUILD_TYPE Release)
|
| 16 |
+
endif(NOT CMAKE_BUILD_TYPE)
|
| 17 |
+
|
| 18 |
+
include_directories(extern/milo)
|
| 19 |
+
|
| 20 |
+
set(REQUIRED_SUBMODULES
|
| 21 |
+
"extern/pybind11/CMakeLists.txt"
|
| 22 |
+
"extern/rapidjson/include"
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
foreach(REQUIRED_SUBMODULE ${REQUIRED_SUBMODULES})
|
| 26 |
+
if(NOT EXISTS "${PROJECT_SOURCE_DIR}/${REQUIRED_SUBMODULE}")
|
| 27 |
+
# update submodule
|
| 28 |
+
# https://cliutils.gitlab.io/modern-cmake/chapters/projects/submodule.html
|
| 29 |
+
find_package(Git QUIET)
|
| 30 |
+
if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git")
|
| 31 |
+
# Update submodules as needed
|
| 32 |
+
option(GIT_SUBMODULE "Check submodules during build" ON)
|
| 33 |
+
if(GIT_SUBMODULE)
|
| 34 |
+
message(STATUS "Submodule update, this may take some time...")
|
| 35 |
+
execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive
|
| 36 |
+
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
| 37 |
+
RESULT_VARIABLE GIT_SUBMOD_RESULT)
|
| 38 |
+
if(NOT GIT_SUBMOD_RESULT EQUAL "0")
|
| 39 |
+
message(FATAL_ERROR "git submodule update --init failed with ${GIT_SUBMOD_RESULT}, please checkout submodules")
|
| 40 |
+
endif()
|
| 41 |
+
endif()
|
| 42 |
+
endif()
|
| 43 |
+
break()
|
| 44 |
+
else()
|
| 45 |
+
message(STATUS "Found Submodule: ${REQUIRED_SUBMODULE}")
|
| 46 |
+
endif()
|
| 47 |
+
endforeach()
|
| 48 |
+
|
| 49 |
+
foreach(REQUIRED_SUBMODULE ${REQUIRED_SUBMODULES})
|
| 50 |
+
if(NOT EXISTS "${PROJECT_SOURCE_DIR}/${REQUIRED_SUBMODULE}")
|
| 51 |
+
message(FATAL_ERROR "The submodule ${REQUIRED_SUBMODULE} was not downloaded! GIT_SUBMODULE was turned off or failed. Please update submodules and try again.")
|
| 52 |
+
endif()
|
| 53 |
+
endforeach()
|
| 54 |
+
|
| 55 |
+
add_subdirectory(extern/pybind11)
|
| 56 |
+
include_directories(extern/rapidjson/include)
|
| 57 |
+
|
| 58 |
+
add_subdirectory(src)
|
| 59 |
+
|
| 60 |
+
# Tests
|
| 61 |
+
find_package(GTest)
|
| 62 |
+
if(GTEST_FOUND)
|
| 63 |
+
enable_testing()
|
| 64 |
+
add_subdirectory(tests)
|
| 65 |
+
endif()
|
| 66 |
+
|
| 67 |
+
if (${CMAKE_BUILD_TYPE} STREQUAL Debug)
|
| 68 |
+
add_subdirectory(tools/debug)
|
| 69 |
+
endif()
|
| 70 |
+
|
| 71 |
+
pybind11_add_module(${PROJECT_NAME} MODULE src/cityflow.cpp)
|
| 72 |
+
target_link_libraries(${PROJECT_NAME} PRIVATE ${PROJECT_LIB_NAME})
|
| 73 |
+
if(VERSION)
|
| 74 |
+
target_compile_definitions(${PROJECT_NAME} PRIVATE -DVERSION=${VERSION})
|
| 75 |
+
endif()
|
| 76 |
+
|
third_party/CityFlow/Dockerfile
CHANGED
|
@@ -1,21 +1,21 @@
|
|
| 1 |
-
FROM ubuntu:16.04
|
| 2 |
-
|
| 3 |
-
# c++ dependencies
|
| 4 |
-
RUN apt update && \
|
| 5 |
-
apt-get install -y build-essential cmake wget git
|
| 6 |
-
|
| 7 |
-
# install Miniconda Python 3.6
|
| 8 |
-
ENV LANG=C.UTF-8 LC_ALL=C.UTF-8
|
| 9 |
-
ENV PATH /opt/conda/bin:$PATH
|
| 10 |
-
|
| 11 |
-
RUN wget -P /tmp/ https://repo.continuum.io/miniconda/Miniconda3-4.5.4-Linux-x86_64.sh && \
|
| 12 |
-
/bin/bash /tmp/Miniconda3-4.5.4-Linux-x86_64.sh -b -p /opt/conda && \
|
| 13 |
-
rm /tmp/Miniconda3-4.5.4-Linux-x86_64.sh && \
|
| 14 |
-
ln -s /opt/conda/etc/profile.d/conda.sh /etc/profile.d/conda.sh && \
|
| 15 |
-
echo ". /opt/conda/etc/profile.d/conda.sh" >> ~/.bashrc
|
| 16 |
-
|
| 17 |
-
# install cityflow
|
| 18 |
-
COPY . /home/cityflow
|
| 19 |
-
RUN pip install flask && \
|
| 20 |
-
cd /home/cityflow && \
|
| 21 |
pip install .
|
|
|
|
| 1 |
+
FROM ubuntu:16.04
|
| 2 |
+
|
| 3 |
+
# c++ dependencies
|
| 4 |
+
RUN apt update && \
|
| 5 |
+
apt-get install -y build-essential cmake wget git
|
| 6 |
+
|
| 7 |
+
# install Miniconda Python 3.6
|
| 8 |
+
ENV LANG=C.UTF-8 LC_ALL=C.UTF-8
|
| 9 |
+
ENV PATH /opt/conda/bin:$PATH
|
| 10 |
+
|
| 11 |
+
RUN wget -P /tmp/ https://repo.continuum.io/miniconda/Miniconda3-4.5.4-Linux-x86_64.sh && \
|
| 12 |
+
/bin/bash /tmp/Miniconda3-4.5.4-Linux-x86_64.sh -b -p /opt/conda && \
|
| 13 |
+
rm /tmp/Miniconda3-4.5.4-Linux-x86_64.sh && \
|
| 14 |
+
ln -s /opt/conda/etc/profile.d/conda.sh /etc/profile.d/conda.sh && \
|
| 15 |
+
echo ". /opt/conda/etc/profile.d/conda.sh" >> ~/.bashrc
|
| 16 |
+
|
| 17 |
+
# install cityflow
|
| 18 |
+
COPY . /home/cityflow
|
| 19 |
+
RUN pip install flask && \
|
| 20 |
+
cd /home/cityflow && \
|
| 21 |
pip install .
|
third_party/CityFlow/LICENSE.txt
CHANGED
|
@@ -1,201 +1,201 @@
|
|
| 1 |
-
Apache License
|
| 2 |
-
Version 2.0, January 2004
|
| 3 |
-
http://www.apache.org/licenses/
|
| 4 |
-
|
| 5 |
-
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
-
|
| 7 |
-
1. Definitions.
|
| 8 |
-
|
| 9 |
-
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
-
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
-
|
| 12 |
-
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
-
the copyright owner that is granting the License.
|
| 14 |
-
|
| 15 |
-
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
-
other entities that control, are controlled by, or are under common
|
| 17 |
-
control with that entity. For the purposes of this definition,
|
| 18 |
-
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
-
direction or management of such entity, whether by contract or
|
| 20 |
-
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
-
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
-
|
| 23 |
-
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
-
exercising permissions granted by this License.
|
| 25 |
-
|
| 26 |
-
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
-
including but not limited to software source code, documentation
|
| 28 |
-
source, and configuration files.
|
| 29 |
-
|
| 30 |
-
"Object" form shall mean any form resulting from mechanical
|
| 31 |
-
transformation or translation of a Source form, including but
|
| 32 |
-
not limited to compiled object code, generated documentation,
|
| 33 |
-
and conversions to other media types.
|
| 34 |
-
|
| 35 |
-
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
-
Object form, made available under the License, as indicated by a
|
| 37 |
-
copyright notice that is included in or attached to the work
|
| 38 |
-
(an example is provided in the Appendix below).
|
| 39 |
-
|
| 40 |
-
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
-
form, that is based on (or derived from) the Work and for which the
|
| 42 |
-
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
-
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
-
of this License, Derivative Works shall not include works that remain
|
| 45 |
-
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
-
the Work and Derivative Works thereof.
|
| 47 |
-
|
| 48 |
-
"Contribution" shall mean any work of authorship, including
|
| 49 |
-
the original version of the Work and any modifications or additions
|
| 50 |
-
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
-
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
-
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
-
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
-
means any form of electronic, verbal, or written communication sent
|
| 55 |
-
to the Licensor or its representatives, including but not limited to
|
| 56 |
-
communication on electronic mailing lists, source code control systems,
|
| 57 |
-
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
-
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
-
excluding communication that is conspicuously marked or otherwise
|
| 60 |
-
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
-
|
| 62 |
-
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
-
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
-
subsequently incorporated within the Work.
|
| 65 |
-
|
| 66 |
-
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
-
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
-
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
-
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
-
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
-
Work and such Derivative Works in Source or Object form.
|
| 72 |
-
|
| 73 |
-
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
-
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
-
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
-
(except as stated in this section) patent license to make, have made,
|
| 77 |
-
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
-
where such license applies only to those patent claims licensable
|
| 79 |
-
by such Contributor that are necessarily infringed by their
|
| 80 |
-
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
-
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
-
institute patent litigation against any entity (including a
|
| 83 |
-
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
-
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
-
or contributory patent infringement, then any patent licenses
|
| 86 |
-
granted to You under this License for that Work shall terminate
|
| 87 |
-
as of the date such litigation is filed.
|
| 88 |
-
|
| 89 |
-
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
-
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
-
modifications, and in Source or Object form, provided that You
|
| 92 |
-
meet the following conditions:
|
| 93 |
-
|
| 94 |
-
(a) You must give any other recipients of the Work or
|
| 95 |
-
Derivative Works a copy of this License; and
|
| 96 |
-
|
| 97 |
-
(b) You must cause any modified files to carry prominent notices
|
| 98 |
-
stating that You changed the files; and
|
| 99 |
-
|
| 100 |
-
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
-
that You distribute, all copyright, patent, trademark, and
|
| 102 |
-
attribution notices from the Source form of the Work,
|
| 103 |
-
excluding those notices that do not pertain to any part of
|
| 104 |
-
the Derivative Works; and
|
| 105 |
-
|
| 106 |
-
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
-
distribution, then any Derivative Works that You distribute must
|
| 108 |
-
include a readable copy of the attribution notices contained
|
| 109 |
-
within such NOTICE file, excluding those notices that do not
|
| 110 |
-
pertain to any part of the Derivative Works, in at least one
|
| 111 |
-
of the following places: within a NOTICE text file distributed
|
| 112 |
-
as part of the Derivative Works; within the Source form or
|
| 113 |
-
documentation, if provided along with the Derivative Works; or,
|
| 114 |
-
within a display generated by the Derivative Works, if and
|
| 115 |
-
wherever such third-party notices normally appear. The contents
|
| 116 |
-
of the NOTICE file are for informational purposes only and
|
| 117 |
-
do not modify the License. You may add Your own attribution
|
| 118 |
-
notices within Derivative Works that You distribute, alongside
|
| 119 |
-
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
-
that such additional attribution notices cannot be construed
|
| 121 |
-
as modifying the License.
|
| 122 |
-
|
| 123 |
-
You may add Your own copyright statement to Your modifications and
|
| 124 |
-
may provide additional or different license terms and conditions
|
| 125 |
-
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
-
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
-
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
-
the conditions stated in this License.
|
| 129 |
-
|
| 130 |
-
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
-
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
-
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
-
this License, without any additional terms or conditions.
|
| 134 |
-
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
-
the terms of any separate license agreement you may have executed
|
| 136 |
-
with Licensor regarding such Contributions.
|
| 137 |
-
|
| 138 |
-
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
-
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
-
except as required for reasonable and customary use in describing the
|
| 141 |
-
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
-
|
| 143 |
-
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
-
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
-
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
-
implied, including, without limitation, any warranties or conditions
|
| 148 |
-
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
-
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
-
appropriateness of using or redistributing the Work and assume any
|
| 151 |
-
risks associated with Your exercise of permissions under this License.
|
| 152 |
-
|
| 153 |
-
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
-
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
-
unless required by applicable law (such as deliberate and grossly
|
| 156 |
-
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
-
liable to You for damages, including any direct, indirect, special,
|
| 158 |
-
incidental, or consequential damages of any character arising as a
|
| 159 |
-
result of this License or out of the use or inability to use the
|
| 160 |
-
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
-
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
-
other commercial damages or losses), even if such Contributor
|
| 163 |
-
has been advised of the possibility of such damages.
|
| 164 |
-
|
| 165 |
-
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
-
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
-
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
-
or other liability obligations and/or rights consistent with this
|
| 169 |
-
License. However, in accepting such obligations, You may act only
|
| 170 |
-
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
-
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
-
defend, and hold each Contributor harmless for any liability
|
| 173 |
-
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
-
of your accepting any such warranty or additional liability.
|
| 175 |
-
|
| 176 |
-
END OF TERMS AND CONDITIONS
|
| 177 |
-
|
| 178 |
-
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
-
|
| 180 |
-
To apply the Apache License to your work, attach the following
|
| 181 |
-
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
-
replaced with your own identifying information. (Don't include
|
| 183 |
-
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
-
comment syntax for the file format. We also recommend that a
|
| 185 |
-
file or class name and description of purpose be included on the
|
| 186 |
-
same "printed page" as the copyright notice for easier
|
| 187 |
-
identification within third-party archives.
|
| 188 |
-
|
| 189 |
-
Copyright [yyyy] [name of copyright owner]
|
| 190 |
-
|
| 191 |
-
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
-
you may not use this file except in compliance with the License.
|
| 193 |
-
You may obtain a copy of the License at
|
| 194 |
-
|
| 195 |
-
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
-
|
| 197 |
-
Unless required by applicable law or agreed to in writing, software
|
| 198 |
-
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
-
See the License for the specific language governing permissions and
|
| 201 |
limitations under the License.
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright [yyyy] [name of copyright owner]
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
limitations under the License.
|
third_party/CityFlow/README.rst
CHANGED
|
@@ -1,51 +1,51 @@
|
|
| 1 |
-
CityFlow
|
| 2 |
-
============
|
| 3 |
-
|
| 4 |
-
.. image:: https://readthedocs.org/projects/cityflow/badge/?version=latest
|
| 5 |
-
:target: https://cityflow.readthedocs.io/en/latest/?badge=latest
|
| 6 |
-
:alt: Documentation Status
|
| 7 |
-
|
| 8 |
-
.. image:: https://dev.azure.com/CityFlow/CityFlow/_apis/build/status/cityflow-project.CityFlow?branchName=master
|
| 9 |
-
:target: https://dev.azure.com/CityFlow/CityFlow/_build/latest?definitionId=2&branchName=master
|
| 10 |
-
:alt: Build Status
|
| 11 |
-
|
| 12 |
-
CityFlow is a multi-agent reinforcement learning environment for large-scale city traffic scenario.
|
| 13 |
-
|
| 14 |
-
Checkout these features!
|
| 15 |
-
|
| 16 |
-
- A microscopic traffic simulator which simulates the behavior of each vehicle, providing highest level detail of traffic evolution.
|
| 17 |
-
- Supports flexible definitions for road network and traffic flow
|
| 18 |
-
- Provides friendly python interface for reinforcement learning
|
| 19 |
-
- **Fast!** Elaborately designed data structure and simulation algorithm with multithreading. Capable of simulating city-wide traffic. See the performance comparison with SUMO [#sumo]_.
|
| 20 |
-
|
| 21 |
-
.. figure:: https://user-images.githubusercontent.com/44251346/54403537-5ce16b00-470b-11e9-928d-76c8ba0ab463.png
|
| 22 |
-
:align: center
|
| 23 |
-
:alt: performance compared with SUMO
|
| 24 |
-
|
| 25 |
-
Performance comparison between CityFlow with different number of threads (1, 2, 4, 8) and SUMO. From small 1x1 grid roadnet to city-level 30x30 roadnet. Even faster when you need to interact with the simulator through python API.
|
| 26 |
-
|
| 27 |
-
Screencast
|
| 28 |
-
----------
|
| 29 |
-
|
| 30 |
-
.. figure:: https://user-images.githubusercontent.com/44251346/62375390-c9e98600-b570-11e9-8808-e13dbe776f1e.gif
|
| 31 |
-
:align: center
|
| 32 |
-
:alt: demo
|
| 33 |
-
|
| 34 |
-
Featured Research and Projects Using CityFlow
|
| 35 |
-
---------------------------------------------
|
| 36 |
-
- `PressLight: Learning Max Pressure Control to Coordinate Traffic Signals in Arterial Network (KDD 2019) <http://personal.psu.edu/hzw77/publications/presslight-kdd19.pdf>`_
|
| 37 |
-
- `CoLight: Learning Network-level Cooperation for Traffic Signal Control <https://arxiv.org/abs/1905.05717>`_
|
| 38 |
-
- `Traffic Signal Control Benchmark <https://traffic-signal-control.github.io/>`_
|
| 39 |
-
- `TSCC2050: A Traffic Signal Control Game by Tianrang Intelligence (in Chinese) <http://game.tscc2050.com/>`_ [#tianrang]_
|
| 40 |
-
|
| 41 |
-
Links
|
| 42 |
-
-----
|
| 43 |
-
|
| 44 |
-
- `WWW 2019 Demo Paper <https://arxiv.org/abs/1905.05217>`_
|
| 45 |
-
- `Home Page <http://cityflow-project.github.io/>`_
|
| 46 |
-
- `Documentation and Quick Start <https://cityflow.readthedocs.io/en/latest/>`_
|
| 47 |
-
- `Docker <https://hub.docker.com/r/cityflowproject/cityflow>`_
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
.. [#sumo] `SUMO home page <https://sumo.dlr.de/index.html>`_
|
| 51 |
-
.. [#tianrang] `Tianrang Intelligence home page <https://www.tianrang.com/>`_
|
|
|
|
| 1 |
+
CityFlow
|
| 2 |
+
============
|
| 3 |
+
|
| 4 |
+
.. image:: https://readthedocs.org/projects/cityflow/badge/?version=latest
|
| 5 |
+
:target: https://cityflow.readthedocs.io/en/latest/?badge=latest
|
| 6 |
+
:alt: Documentation Status
|
| 7 |
+
|
| 8 |
+
.. image:: https://dev.azure.com/CityFlow/CityFlow/_apis/build/status/cityflow-project.CityFlow?branchName=master
|
| 9 |
+
:target: https://dev.azure.com/CityFlow/CityFlow/_build/latest?definitionId=2&branchName=master
|
| 10 |
+
:alt: Build Status
|
| 11 |
+
|
| 12 |
+
CityFlow is a multi-agent reinforcement learning environment for large-scale city traffic scenario.
|
| 13 |
+
|
| 14 |
+
Checkout these features!
|
| 15 |
+
|
| 16 |
+
- A microscopic traffic simulator which simulates the behavior of each vehicle, providing highest level detail of traffic evolution.
|
| 17 |
+
- Supports flexible definitions for road network and traffic flow
|
| 18 |
+
- Provides friendly python interface for reinforcement learning
|
| 19 |
+
- **Fast!** Elaborately designed data structure and simulation algorithm with multithreading. Capable of simulating city-wide traffic. See the performance comparison with SUMO [#sumo]_.
|
| 20 |
+
|
| 21 |
+
.. figure:: https://user-images.githubusercontent.com/44251346/54403537-5ce16b00-470b-11e9-928d-76c8ba0ab463.png
|
| 22 |
+
:align: center
|
| 23 |
+
:alt: performance compared with SUMO
|
| 24 |
+
|
| 25 |
+
Performance comparison between CityFlow with different number of threads (1, 2, 4, 8) and SUMO. From small 1x1 grid roadnet to city-level 30x30 roadnet. Even faster when you need to interact with the simulator through python API.
|
| 26 |
+
|
| 27 |
+
Screencast
|
| 28 |
+
----------
|
| 29 |
+
|
| 30 |
+
.. figure:: https://user-images.githubusercontent.com/44251346/62375390-c9e98600-b570-11e9-8808-e13dbe776f1e.gif
|
| 31 |
+
:align: center
|
| 32 |
+
:alt: demo
|
| 33 |
+
|
| 34 |
+
Featured Research and Projects Using CityFlow
|
| 35 |
+
---------------------------------------------
|
| 36 |
+
- `PressLight: Learning Max Pressure Control to Coordinate Traffic Signals in Arterial Network (KDD 2019) <http://personal.psu.edu/hzw77/publications/presslight-kdd19.pdf>`_
|
| 37 |
+
- `CoLight: Learning Network-level Cooperation for Traffic Signal Control <https://arxiv.org/abs/1905.05717>`_
|
| 38 |
+
- `Traffic Signal Control Benchmark <https://traffic-signal-control.github.io/>`_
|
| 39 |
+
- `TSCC2050: A Traffic Signal Control Game by Tianrang Intelligence (in Chinese) <http://game.tscc2050.com/>`_ [#tianrang]_
|
| 40 |
+
|
| 41 |
+
Links
|
| 42 |
+
-----
|
| 43 |
+
|
| 44 |
+
- `WWW 2019 Demo Paper <https://arxiv.org/abs/1905.05217>`_
|
| 45 |
+
- `Home Page <http://cityflow-project.github.io/>`_
|
| 46 |
+
- `Documentation and Quick Start <https://cityflow.readthedocs.io/en/latest/>`_
|
| 47 |
+
- `Docker <https://hub.docker.com/r/cityflowproject/cityflow>`_
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
.. [#sumo] `SUMO home page <https://sumo.dlr.de/index.html>`_
|
| 51 |
+
.. [#tianrang] `Tianrang Intelligence home page <https://www.tianrang.com/>`_
|
third_party/CityFlow/docs/Makefile
CHANGED
|
@@ -1,20 +1,20 @@
|
|
| 1 |
-
# Minimal makefile for Sphinx documentation
|
| 2 |
-
#
|
| 3 |
-
|
| 4 |
-
# You can set these variables from the command line.
|
| 5 |
-
SPHINXOPTS =
|
| 6 |
-
SPHINXBUILD = sphinx-build
|
| 7 |
-
SPHINXPROJ = CityFlow
|
| 8 |
-
SOURCEDIR = source
|
| 9 |
-
BUILDDIR = build
|
| 10 |
-
|
| 11 |
-
# Put it first so that "make" without argument is like "make help".
|
| 12 |
-
help:
|
| 13 |
-
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
| 14 |
-
|
| 15 |
-
.PHONY: help Makefile
|
| 16 |
-
|
| 17 |
-
# Catch-all target: route all unknown targets to Sphinx using the new
|
| 18 |
-
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
| 19 |
-
%: Makefile
|
| 20 |
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
|
|
| 1 |
+
# Minimal makefile for Sphinx documentation
|
| 2 |
+
#
|
| 3 |
+
|
| 4 |
+
# You can set these variables from the command line.
|
| 5 |
+
SPHINXOPTS =
|
| 6 |
+
SPHINXBUILD = sphinx-build
|
| 7 |
+
SPHINXPROJ = CityFlow
|
| 8 |
+
SOURCEDIR = source
|
| 9 |
+
BUILDDIR = build
|
| 10 |
+
|
| 11 |
+
# Put it first so that "make" without argument is like "make help".
|
| 12 |
+
help:
|
| 13 |
+
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
| 14 |
+
|
| 15 |
+
.PHONY: help Makefile
|
| 16 |
+
|
| 17 |
+
# Catch-all target: route all unknown targets to Sphinx using the new
|
| 18 |
+
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
| 19 |
+
%: Makefile
|
| 20 |
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
third_party/CityFlow/docs/make.bat
CHANGED
|
@@ -1,36 +1,36 @@
|
|
| 1 |
-
@ECHO OFF
|
| 2 |
-
|
| 3 |
-
pushd %~dp0
|
| 4 |
-
|
| 5 |
-
REM Command file for Sphinx documentation
|
| 6 |
-
|
| 7 |
-
if "%SPHINXBUILD%" == "" (
|
| 8 |
-
set SPHINXBUILD=sphinx-build
|
| 9 |
-
)
|
| 10 |
-
set SOURCEDIR=source
|
| 11 |
-
set BUILDDIR=build
|
| 12 |
-
set SPHINXPROJ=CityFlow
|
| 13 |
-
|
| 14 |
-
if "%1" == "" goto help
|
| 15 |
-
|
| 16 |
-
%SPHINXBUILD% >NUL 2>NUL
|
| 17 |
-
if errorlevel 9009 (
|
| 18 |
-
echo.
|
| 19 |
-
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
| 20 |
-
echo.installed, then set the SPHINXBUILD environment variable to point
|
| 21 |
-
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
| 22 |
-
echo.may add the Sphinx directory to PATH.
|
| 23 |
-
echo.
|
| 24 |
-
echo.If you don't have Sphinx installed, grab it from
|
| 25 |
-
echo.http://sphinx-doc.org/
|
| 26 |
-
exit /b 1
|
| 27 |
-
)
|
| 28 |
-
|
| 29 |
-
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
| 30 |
-
goto end
|
| 31 |
-
|
| 32 |
-
:help
|
| 33 |
-
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
| 34 |
-
|
| 35 |
-
:end
|
| 36 |
-
popd
|
|
|
|
| 1 |
+
@ECHO OFF
|
| 2 |
+
|
| 3 |
+
pushd %~dp0
|
| 4 |
+
|
| 5 |
+
REM Command file for Sphinx documentation
|
| 6 |
+
|
| 7 |
+
if "%SPHINXBUILD%" == "" (
|
| 8 |
+
set SPHINXBUILD=sphinx-build
|
| 9 |
+
)
|
| 10 |
+
set SOURCEDIR=source
|
| 11 |
+
set BUILDDIR=build
|
| 12 |
+
set SPHINXPROJ=CityFlow
|
| 13 |
+
|
| 14 |
+
if "%1" == "" goto help
|
| 15 |
+
|
| 16 |
+
%SPHINXBUILD% >NUL 2>NUL
|
| 17 |
+
if errorlevel 9009 (
|
| 18 |
+
echo.
|
| 19 |
+
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
| 20 |
+
echo.installed, then set the SPHINXBUILD environment variable to point
|
| 21 |
+
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
| 22 |
+
echo.may add the Sphinx directory to PATH.
|
| 23 |
+
echo.
|
| 24 |
+
echo.If you don't have Sphinx installed, grab it from
|
| 25 |
+
echo.http://sphinx-doc.org/
|
| 26 |
+
exit /b 1
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
| 30 |
+
goto end
|
| 31 |
+
|
| 32 |
+
:help
|
| 33 |
+
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
| 34 |
+
|
| 35 |
+
:end
|
| 36 |
+
popd
|
third_party/CityFlow/docs/source/conf.py
CHANGED
|
@@ -1,155 +1,155 @@
|
|
| 1 |
-
# -*- coding: utf-8 -*-
|
| 2 |
-
#
|
| 3 |
-
# Configuration file for the Sphinx documentation builder.
|
| 4 |
-
#
|
| 5 |
-
# This file does only contain a selection of the most common options. For a
|
| 6 |
-
# full list see the documentation:
|
| 7 |
-
# http://www.sphinx-doc.org/en/master/config
|
| 8 |
-
|
| 9 |
-
# -- Path setup --------------------------------------------------------------
|
| 10 |
-
|
| 11 |
-
# If extensions (or modules to document with autodoc) are in another directory,
|
| 12 |
-
# add these directories to sys.path here. If the directory is relative to the
|
| 13 |
-
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
| 14 |
-
#
|
| 15 |
-
# import os
|
| 16 |
-
# import sys
|
| 17 |
-
# sys.path.insert(0, os.path.abspath('.'))
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
# -- Project information -----------------------------------------------------
|
| 21 |
-
|
| 22 |
-
project = 'CityFlow'
|
| 23 |
-
copyright = '2019, CityFlow'
|
| 24 |
-
author = 'Huichu Zhang'
|
| 25 |
-
|
| 26 |
-
# The short X.Y version
|
| 27 |
-
version = ''
|
| 28 |
-
# The full version, including alpha/beta/rc tags
|
| 29 |
-
release = '0.1'
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
# -- General configuration ---------------------------------------------------
|
| 33 |
-
|
| 34 |
-
# If your documentation needs a minimal Sphinx version, state it here.
|
| 35 |
-
#
|
| 36 |
-
# needs_sphinx = '1.0'
|
| 37 |
-
|
| 38 |
-
# Add any Sphinx extension module names here, as strings. They can be
|
| 39 |
-
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
| 40 |
-
# ones.
|
| 41 |
-
extensions = [
|
| 42 |
-
]
|
| 43 |
-
|
| 44 |
-
# Add any paths that contain templates here, relative to this directory.
|
| 45 |
-
templates_path = ['_templates']
|
| 46 |
-
|
| 47 |
-
# The suffix(es) of source filenames.
|
| 48 |
-
# You can specify multiple suffix as a list of string:
|
| 49 |
-
#
|
| 50 |
-
# source_suffix = ['.rst', '.md']
|
| 51 |
-
source_suffix = '.rst'
|
| 52 |
-
|
| 53 |
-
# The master toctree document.
|
| 54 |
-
master_doc = 'index'
|
| 55 |
-
|
| 56 |
-
# The language for content autogenerated by Sphinx. Refer to documentation
|
| 57 |
-
# for a list of supported languages.
|
| 58 |
-
#
|
| 59 |
-
# This is also used if you do content translation via gettext catalogs.
|
| 60 |
-
# Usually you set "language" from the command line for these cases.
|
| 61 |
-
language = None
|
| 62 |
-
|
| 63 |
-
# List of patterns, relative to source directory, that match files and
|
| 64 |
-
# directories to ignore when looking for source files.
|
| 65 |
-
# This pattern also affects html_static_path and html_extra_path .
|
| 66 |
-
exclude_patterns = []
|
| 67 |
-
|
| 68 |
-
# The name of the Pygments (syntax highlighting) style to use.
|
| 69 |
-
pygments_style = 'sphinx'
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
# -- Options for HTML output -------------------------------------------------
|
| 73 |
-
|
| 74 |
-
# The theme to use for HTML and HTML Help pages. See the documentation for
|
| 75 |
-
# a list of builtin themes.
|
| 76 |
-
#
|
| 77 |
-
html_theme = 'sphinx_rtd_theme'
|
| 78 |
-
|
| 79 |
-
# Theme options are theme-specific and customize the look and feel of a theme
|
| 80 |
-
# further. For a list of options available for each theme, see the
|
| 81 |
-
# documentation.
|
| 82 |
-
#
|
| 83 |
-
# html_theme_options = {}
|
| 84 |
-
|
| 85 |
-
# Add any paths that contain custom static files (such as style sheets) here,
|
| 86 |
-
# relative to this directory. They are copied after the builtin static files,
|
| 87 |
-
# so a file named "default.css" will overwrite the builtin "default.css".
|
| 88 |
-
html_static_path = ['_static']
|
| 89 |
-
|
| 90 |
-
# Custom sidebar templates, must be a dictionary that maps document names
|
| 91 |
-
# to template names.
|
| 92 |
-
#
|
| 93 |
-
# The default sidebars (for documents that don't match any pattern) are
|
| 94 |
-
# defined by theme itself. Builtin themes are using these templates by
|
| 95 |
-
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
| 96 |
-
# 'searchbox.html']``.
|
| 97 |
-
#
|
| 98 |
-
# html_sidebars = {}
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
# -- Options for HTMLHelp output ---------------------------------------------
|
| 102 |
-
|
| 103 |
-
# Output file base name for HTML help builder.
|
| 104 |
-
htmlhelp_basename = 'CityFlowdoc'
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
# -- Options for LaTeX output ------------------------------------------------
|
| 108 |
-
|
| 109 |
-
latex_elements = {
|
| 110 |
-
# The paper size ('letterpaper' or 'a4paper').
|
| 111 |
-
#
|
| 112 |
-
# 'papersize': 'letterpaper',
|
| 113 |
-
|
| 114 |
-
# The font size ('10pt', '11pt' or '12pt').
|
| 115 |
-
#
|
| 116 |
-
# 'pointsize': '10pt',
|
| 117 |
-
|
| 118 |
-
# Additional stuff for the LaTeX preamble.
|
| 119 |
-
#
|
| 120 |
-
# 'preamble': '',
|
| 121 |
-
|
| 122 |
-
# Latex figure (float) alignment
|
| 123 |
-
#
|
| 124 |
-
# 'figure_align': 'htbp',
|
| 125 |
-
}
|
| 126 |
-
|
| 127 |
-
# Grouping the document tree into LaTeX files. List of tuples
|
| 128 |
-
# (source start file, target name, title,
|
| 129 |
-
# author, documentclass [howto, manual, or own class]).
|
| 130 |
-
latex_documents = [
|
| 131 |
-
(master_doc, 'CityFlow.tex', 'CityFlow Documentation',
|
| 132 |
-
'Huichu Zhang', 'manual'),
|
| 133 |
-
]
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
# -- Options for manual page output ------------------------------------------
|
| 137 |
-
|
| 138 |
-
# One entry per manual page. List of tuples
|
| 139 |
-
# (source start file, name, description, authors, manual section).
|
| 140 |
-
man_pages = [
|
| 141 |
-
(master_doc, 'cityflow', 'CityFlow Documentation',
|
| 142 |
-
[author], 1)
|
| 143 |
-
]
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
# -- Options for Texinfo output ----------------------------------------------
|
| 147 |
-
|
| 148 |
-
# Grouping the document tree into Texinfo files. List of tuples
|
| 149 |
-
# (source start file, target name, title, author,
|
| 150 |
-
# dir menu entry, description, category)
|
| 151 |
-
texinfo_documents = [
|
| 152 |
-
(master_doc, 'CityFlow', 'CityFlow Documentation',
|
| 153 |
-
author, 'CityFlow', 'One line description of project.',
|
| 154 |
-
'Miscellaneous'),
|
| 155 |
]
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
#
|
| 3 |
+
# Configuration file for the Sphinx documentation builder.
|
| 4 |
+
#
|
| 5 |
+
# This file does only contain a selection of the most common options. For a
|
| 6 |
+
# full list see the documentation:
|
| 7 |
+
# http://www.sphinx-doc.org/en/master/config
|
| 8 |
+
|
| 9 |
+
# -- Path setup --------------------------------------------------------------
|
| 10 |
+
|
| 11 |
+
# If extensions (or modules to document with autodoc) are in another directory,
|
| 12 |
+
# add these directories to sys.path here. If the directory is relative to the
|
| 13 |
+
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
| 14 |
+
#
|
| 15 |
+
# import os
|
| 16 |
+
# import sys
|
| 17 |
+
# sys.path.insert(0, os.path.abspath('.'))
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# -- Project information -----------------------------------------------------
|
| 21 |
+
|
| 22 |
+
project = 'CityFlow'
|
| 23 |
+
copyright = '2019, CityFlow'
|
| 24 |
+
author = 'Huichu Zhang'
|
| 25 |
+
|
| 26 |
+
# The short X.Y version
|
| 27 |
+
version = ''
|
| 28 |
+
# The full version, including alpha/beta/rc tags
|
| 29 |
+
release = '0.1'
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# -- General configuration ---------------------------------------------------
|
| 33 |
+
|
| 34 |
+
# If your documentation needs a minimal Sphinx version, state it here.
|
| 35 |
+
#
|
| 36 |
+
# needs_sphinx = '1.0'
|
| 37 |
+
|
| 38 |
+
# Add any Sphinx extension module names here, as strings. They can be
|
| 39 |
+
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
| 40 |
+
# ones.
|
| 41 |
+
extensions = [
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
# Add any paths that contain templates here, relative to this directory.
|
| 45 |
+
templates_path = ['_templates']
|
| 46 |
+
|
| 47 |
+
# The suffix(es) of source filenames.
|
| 48 |
+
# You can specify multiple suffix as a list of string:
|
| 49 |
+
#
|
| 50 |
+
# source_suffix = ['.rst', '.md']
|
| 51 |
+
source_suffix = '.rst'
|
| 52 |
+
|
| 53 |
+
# The master toctree document.
|
| 54 |
+
master_doc = 'index'
|
| 55 |
+
|
| 56 |
+
# The language for content autogenerated by Sphinx. Refer to documentation
|
| 57 |
+
# for a list of supported languages.
|
| 58 |
+
#
|
| 59 |
+
# This is also used if you do content translation via gettext catalogs.
|
| 60 |
+
# Usually you set "language" from the command line for these cases.
|
| 61 |
+
language = None
|
| 62 |
+
|
| 63 |
+
# List of patterns, relative to source directory, that match files and
|
| 64 |
+
# directories to ignore when looking for source files.
|
| 65 |
+
# This pattern also affects html_static_path and html_extra_path .
|
| 66 |
+
exclude_patterns = []
|
| 67 |
+
|
| 68 |
+
# The name of the Pygments (syntax highlighting) style to use.
|
| 69 |
+
pygments_style = 'sphinx'
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# -- Options for HTML output -------------------------------------------------
|
| 73 |
+
|
| 74 |
+
# The theme to use for HTML and HTML Help pages. See the documentation for
|
| 75 |
+
# a list of builtin themes.
|
| 76 |
+
#
|
| 77 |
+
html_theme = 'sphinx_rtd_theme'
|
| 78 |
+
|
| 79 |
+
# Theme options are theme-specific and customize the look and feel of a theme
|
| 80 |
+
# further. For a list of options available for each theme, see the
|
| 81 |
+
# documentation.
|
| 82 |
+
#
|
| 83 |
+
# html_theme_options = {}
|
| 84 |
+
|
| 85 |
+
# Add any paths that contain custom static files (such as style sheets) here,
|
| 86 |
+
# relative to this directory. They are copied after the builtin static files,
|
| 87 |
+
# so a file named "default.css" will overwrite the builtin "default.css".
|
| 88 |
+
html_static_path = ['_static']
|
| 89 |
+
|
| 90 |
+
# Custom sidebar templates, must be a dictionary that maps document names
|
| 91 |
+
# to template names.
|
| 92 |
+
#
|
| 93 |
+
# The default sidebars (for documents that don't match any pattern) are
|
| 94 |
+
# defined by theme itself. Builtin themes are using these templates by
|
| 95 |
+
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
| 96 |
+
# 'searchbox.html']``.
|
| 97 |
+
#
|
| 98 |
+
# html_sidebars = {}
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
# -- Options for HTMLHelp output ---------------------------------------------
|
| 102 |
+
|
| 103 |
+
# Output file base name for HTML help builder.
|
| 104 |
+
htmlhelp_basename = 'CityFlowdoc'
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
# -- Options for LaTeX output ------------------------------------------------
|
| 108 |
+
|
| 109 |
+
latex_elements = {
|
| 110 |
+
# The paper size ('letterpaper' or 'a4paper').
|
| 111 |
+
#
|
| 112 |
+
# 'papersize': 'letterpaper',
|
| 113 |
+
|
| 114 |
+
# The font size ('10pt', '11pt' or '12pt').
|
| 115 |
+
#
|
| 116 |
+
# 'pointsize': '10pt',
|
| 117 |
+
|
| 118 |
+
# Additional stuff for the LaTeX preamble.
|
| 119 |
+
#
|
| 120 |
+
# 'preamble': '',
|
| 121 |
+
|
| 122 |
+
# Latex figure (float) alignment
|
| 123 |
+
#
|
| 124 |
+
# 'figure_align': 'htbp',
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
# Grouping the document tree into LaTeX files. List of tuples
|
| 128 |
+
# (source start file, target name, title,
|
| 129 |
+
# author, documentclass [howto, manual, or own class]).
|
| 130 |
+
latex_documents = [
|
| 131 |
+
(master_doc, 'CityFlow.tex', 'CityFlow Documentation',
|
| 132 |
+
'Huichu Zhang', 'manual'),
|
| 133 |
+
]
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
# -- Options for manual page output ------------------------------------------
|
| 137 |
+
|
| 138 |
+
# One entry per manual page. List of tuples
|
| 139 |
+
# (source start file, name, description, authors, manual section).
|
| 140 |
+
man_pages = [
|
| 141 |
+
(master_doc, 'cityflow', 'CityFlow Documentation',
|
| 142 |
+
[author], 1)
|
| 143 |
+
]
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
# -- Options for Texinfo output ----------------------------------------------
|
| 147 |
+
|
| 148 |
+
# Grouping the document tree into Texinfo files. List of tuples
|
| 149 |
+
# (source start file, target name, title, author,
|
| 150 |
+
# dir menu entry, description, category)
|
| 151 |
+
texinfo_documents = [
|
| 152 |
+
(master_doc, 'CityFlow', 'CityFlow Documentation',
|
| 153 |
+
author, 'CityFlow', 'One line description of project.',
|
| 154 |
+
'Miscellaneous'),
|
| 155 |
]
|
third_party/CityFlow/docs/source/flow.rst
CHANGED
|
@@ -1,23 +1,23 @@
|
|
| 1 |
-
.. _flow:
|
| 2 |
-
|
| 3 |
-
Flow File Format
|
| 4 |
-
===================
|
| 5 |
-
|
| 6 |
-
Flow file defines the traffic flow. Each flow contains following field:
|
| 7 |
-
|
| 8 |
-
- ``vehicle``: defines the parameter of vehicle.
|
| 9 |
-
- length: length of the vehicle
|
| 10 |
-
- width: width of the vehicle
|
| 11 |
-
- maxPosAcc: maximum acceleration (in m/s)
|
| 12 |
-
- maxNegAcc: maximum deceleration (in m/s)
|
| 13 |
-
- usualPosAcc: usual acceleration (in m/s)
|
| 14 |
-
- usualNegAcc: usual deceleration (in m/s)
|
| 15 |
-
- minGap: minimum acceptable gap with leading vehicle (in meter)
|
| 16 |
-
- maxSpeed: maximum cruising speed (in m/s)
|
| 17 |
-
- headwayTime: desired headway time (in seconds) with leading vehicle, keep *current speed \* headwayTime* gap.
|
| 18 |
-
- ``route``: defines the route, all vehicles of this flow will follow the route. Specify the source and the destination, optionally some anchor points and the router will connect them with shortest paths automatically.
|
| 19 |
-
- ``interval``: defines the interval of consecutive vehicles (in seconds). If the interval is too small, vehicles may not be able to enter the road due to blockage, it will be held and let go once there are enough space.
|
| 20 |
-
- ``startTime``, ``endTime``: Flow will generate vehicles between time [startTime, endTime] (in seconds), including ``startTime`` and ``endTime``.
|
| 21 |
-
|
| 22 |
-
.. note::
|
| 23 |
-
Runnable sample flow files can be found in ``examples`` folder.
|
|
|
|
| 1 |
+
.. _flow:
|
| 2 |
+
|
| 3 |
+
Flow File Format
|
| 4 |
+
===================
|
| 5 |
+
|
| 6 |
+
Flow file defines the traffic flow. Each flow contains following field:
|
| 7 |
+
|
| 8 |
+
- ``vehicle``: defines the parameter of vehicle.
|
| 9 |
+
- length: length of the vehicle
|
| 10 |
+
- width: width of the vehicle
|
| 11 |
+
- maxPosAcc: maximum acceleration (in m/s)
|
| 12 |
+
- maxNegAcc: maximum deceleration (in m/s)
|
| 13 |
+
- usualPosAcc: usual acceleration (in m/s)
|
| 14 |
+
- usualNegAcc: usual deceleration (in m/s)
|
| 15 |
+
- minGap: minimum acceptable gap with leading vehicle (in meter)
|
| 16 |
+
- maxSpeed: maximum cruising speed (in m/s)
|
| 17 |
+
- headwayTime: desired headway time (in seconds) with leading vehicle, keep *current speed \* headwayTime* gap.
|
| 18 |
+
- ``route``: defines the route, all vehicles of this flow will follow the route. Specify the source and the destination, optionally some anchor points and the router will connect them with shortest paths automatically.
|
| 19 |
+
- ``interval``: defines the interval of consecutive vehicles (in seconds). If the interval is too small, vehicles may not be able to enter the road due to blockage, it will be held and let go once there are enough space.
|
| 20 |
+
- ``startTime``, ``endTime``: Flow will generate vehicles between time [startTime, endTime] (in seconds), including ``startTime`` and ``endTime``.
|
| 21 |
+
|
| 22 |
+
.. note::
|
| 23 |
+
Runnable sample flow files can be found in ``examples`` folder.
|
third_party/CityFlow/docs/source/index.rst
CHANGED
|
@@ -1,18 +1,18 @@
|
|
| 1 |
-
.. CityFlow documentation master file, created by
|
| 2 |
-
sphinx-quickstart on Wed Mar 13 21:59:27 2019.
|
| 3 |
-
You can adapt this file completely to your liking, but it should at least
|
| 4 |
-
contain the root `toctree` directive.
|
| 5 |
-
|
| 6 |
-
Welcome to CityFlow's documentation!
|
| 7 |
-
====================================
|
| 8 |
-
|
| 9 |
-
.. toctree::
|
| 10 |
-
:maxdepth: 2
|
| 11 |
-
:caption: Contents:
|
| 12 |
-
|
| 13 |
-
introduction
|
| 14 |
-
install
|
| 15 |
-
start
|
| 16 |
-
roadnet
|
| 17 |
-
flow
|
| 18 |
-
replay
|
|
|
|
| 1 |
+
.. CityFlow documentation master file, created by
|
| 2 |
+
sphinx-quickstart on Wed Mar 13 21:59:27 2019.
|
| 3 |
+
You can adapt this file completely to your liking, but it should at least
|
| 4 |
+
contain the root `toctree` directive.
|
| 5 |
+
|
| 6 |
+
Welcome to CityFlow's documentation!
|
| 7 |
+
====================================
|
| 8 |
+
|
| 9 |
+
.. toctree::
|
| 10 |
+
:maxdepth: 2
|
| 11 |
+
:caption: Contents:
|
| 12 |
+
|
| 13 |
+
introduction
|
| 14 |
+
install
|
| 15 |
+
start
|
| 16 |
+
roadnet
|
| 17 |
+
flow
|
| 18 |
+
replay
|
third_party/CityFlow/docs/source/install.rst
CHANGED
|
@@ -1,66 +1,66 @@
|
|
| 1 |
-
.. _install:
|
| 2 |
-
|
| 3 |
-
Installation Guide
|
| 4 |
-
==================
|
| 5 |
-
|
| 6 |
-
Docker
|
| 7 |
-
------
|
| 8 |
-
|
| 9 |
-
The easiest way to use CityFlow is via docker.
|
| 10 |
-
|
| 11 |
-
.. code-block:: shell
|
| 12 |
-
|
| 13 |
-
docker pull cityflowproject/cityflow:latest
|
| 14 |
-
|
| 15 |
-
This will create docker image ``cityflow:latest``.
|
| 16 |
-
|
| 17 |
-
.. code-block:: shell
|
| 18 |
-
|
| 19 |
-
docker run -it cityflowproject/cityflow:latest
|
| 20 |
-
|
| 21 |
-
Create and start a container, CityFlow is out-of-the-box along with miniconda with python3.6.
|
| 22 |
-
|
| 23 |
-
.. code-block:: python
|
| 24 |
-
|
| 25 |
-
import cityflow
|
| 26 |
-
eng = cityflow.Engine
|
| 27 |
-
|
| 28 |
-
Build From Source
|
| 29 |
-
-----------------
|
| 30 |
-
|
| 31 |
-
If you want to get nightly version of CityFlow or running on native system, you can build CityFlow from source. Currently, we only support building on Unix systems. This guide is based on Ubuntu 16.04.
|
| 32 |
-
|
| 33 |
-
CityFlow has little dependencies, so building from source is not scary.
|
| 34 |
-
|
| 35 |
-
1. Check that you have python 3 installed. Other version of python might work, however, we only tested on python with version >= 3.5.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
2. Install cpp dependencies
|
| 39 |
-
|
| 40 |
-
.. code-block:: shell
|
| 41 |
-
|
| 42 |
-
sudo apt update && sudo apt install -y build-essential cmake
|
| 43 |
-
|
| 44 |
-
3. Clone CityFlow project from github.
|
| 45 |
-
|
| 46 |
-
.. code-block:: shell
|
| 47 |
-
|
| 48 |
-
git clone https://github.com/cityflow-project/CityFlow.git
|
| 49 |
-
|
| 50 |
-
4. Go to CityFlow project's root directory and run
|
| 51 |
-
|
| 52 |
-
.. code-block:: shell
|
| 53 |
-
|
| 54 |
-
pip install .
|
| 55 |
-
|
| 56 |
-
5. Wait for installation to complete and CityFlow should be successfully installed.
|
| 57 |
-
|
| 58 |
-
.. code-block:: python
|
| 59 |
-
|
| 60 |
-
import cityflow
|
| 61 |
-
eng = cityflow.Engine
|
| 62 |
-
|
| 63 |
-
For Windows Users
|
| 64 |
-
------------------
|
| 65 |
-
|
| 66 |
-
For Windows users, it is recommended to run CityFlow under Windows Subsystem for Linux (WSL) or use docker.
|
|
|
|
| 1 |
+
.. _install:
|
| 2 |
+
|
| 3 |
+
Installation Guide
|
| 4 |
+
==================
|
| 5 |
+
|
| 6 |
+
Docker
|
| 7 |
+
------
|
| 8 |
+
|
| 9 |
+
The easiest way to use CityFlow is via docker.
|
| 10 |
+
|
| 11 |
+
.. code-block:: shell
|
| 12 |
+
|
| 13 |
+
docker pull cityflowproject/cityflow:latest
|
| 14 |
+
|
| 15 |
+
This will create docker image ``cityflow:latest``.
|
| 16 |
+
|
| 17 |
+
.. code-block:: shell
|
| 18 |
+
|
| 19 |
+
docker run -it cityflowproject/cityflow:latest
|
| 20 |
+
|
| 21 |
+
Create and start a container, CityFlow is out-of-the-box along with miniconda with python3.6.
|
| 22 |
+
|
| 23 |
+
.. code-block:: python
|
| 24 |
+
|
| 25 |
+
import cityflow
|
| 26 |
+
eng = cityflow.Engine
|
| 27 |
+
|
| 28 |
+
Build From Source
|
| 29 |
+
-----------------
|
| 30 |
+
|
| 31 |
+
If you want to get nightly version of CityFlow or running on native system, you can build CityFlow from source. Currently, we only support building on Unix systems. This guide is based on Ubuntu 16.04.
|
| 32 |
+
|
| 33 |
+
CityFlow has little dependencies, so building from source is not scary.
|
| 34 |
+
|
| 35 |
+
1. Check that you have python 3 installed. Other version of python might work, however, we only tested on python with version >= 3.5.
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
2. Install cpp dependencies
|
| 39 |
+
|
| 40 |
+
.. code-block:: shell
|
| 41 |
+
|
| 42 |
+
sudo apt update && sudo apt install -y build-essential cmake
|
| 43 |
+
|
| 44 |
+
3. Clone CityFlow project from github.
|
| 45 |
+
|
| 46 |
+
.. code-block:: shell
|
| 47 |
+
|
| 48 |
+
git clone https://github.com/cityflow-project/CityFlow.git
|
| 49 |
+
|
| 50 |
+
4. Go to CityFlow project's root directory and run
|
| 51 |
+
|
| 52 |
+
.. code-block:: shell
|
| 53 |
+
|
| 54 |
+
pip install .
|
| 55 |
+
|
| 56 |
+
5. Wait for installation to complete and CityFlow should be successfully installed.
|
| 57 |
+
|
| 58 |
+
.. code-block:: python
|
| 59 |
+
|
| 60 |
+
import cityflow
|
| 61 |
+
eng = cityflow.Engine
|
| 62 |
+
|
| 63 |
+
For Windows Users
|
| 64 |
+
------------------
|
| 65 |
+
|
| 66 |
+
For Windows users, it is recommended to run CityFlow under Windows Subsystem for Linux (WSL) or use docker.
|
third_party/CityFlow/examples/config.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
| 1 |
-
{
|
| 2 |
-
"interval": 1.0,
|
| 3 |
-
"seed": 0,
|
| 4 |
-
"dir": "CityFlow/examples/",
|
| 5 |
-
"roadnetFile": "roadnet.json",
|
| 6 |
-
"flowFile": "flow.json",
|
| 7 |
-
"rlTrafficLight": false,
|
| 8 |
-
"laneChange": false,
|
| 9 |
-
"saveReplay": true,
|
| 10 |
-
"roadnetLogFile": "replay_roadnet.json",
|
| 11 |
-
"replayLogFile": "replay.txt"
|
| 12 |
}
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"interval": 1.0,
|
| 3 |
+
"seed": 0,
|
| 4 |
+
"dir": "CityFlow/examples/",
|
| 5 |
+
"roadnetFile": "roadnet.json",
|
| 6 |
+
"flowFile": "flow.json",
|
| 7 |
+
"rlTrafficLight": false,
|
| 8 |
+
"laneChange": false,
|
| 9 |
+
"saveReplay": true,
|
| 10 |
+
"roadnetLogFile": "replay_roadnet.json",
|
| 11 |
+
"replayLogFile": "replay.txt"
|
| 12 |
}
|
third_party/CityFlow/examples/flow.json
CHANGED
|
@@ -1,242 +1,242 @@
|
|
| 1 |
-
[
|
| 2 |
-
{
|
| 3 |
-
"vehicle": {
|
| 4 |
-
"length": 5.0,
|
| 5 |
-
"width": 2.0,
|
| 6 |
-
"maxPosAcc": 2.0,
|
| 7 |
-
"maxNegAcc": 4.5,
|
| 8 |
-
"usualPosAcc": 2.0,
|
| 9 |
-
"usualNegAcc": 4.5,
|
| 10 |
-
"minGap": 2.5,
|
| 11 |
-
"maxSpeed": 16.67,
|
| 12 |
-
"headwayTime": 1.5
|
| 13 |
-
},
|
| 14 |
-
"route": [
|
| 15 |
-
"road_0_1_0",
|
| 16 |
-
"road_1_1_0"
|
| 17 |
-
],
|
| 18 |
-
"interval": 5.0,
|
| 19 |
-
"startTime": 0,
|
| 20 |
-
"endTime": -1
|
| 21 |
-
},
|
| 22 |
-
{
|
| 23 |
-
"vehicle": {
|
| 24 |
-
"length": 5.0,
|
| 25 |
-
"width": 2.0,
|
| 26 |
-
"maxPosAcc": 2.0,
|
| 27 |
-
"maxNegAcc": 4.5,
|
| 28 |
-
"usualPosAcc": 2.0,
|
| 29 |
-
"usualNegAcc": 4.5,
|
| 30 |
-
"minGap": 2.5,
|
| 31 |
-
"maxSpeed": 16.67,
|
| 32 |
-
"headwayTime": 1.5
|
| 33 |
-
},
|
| 34 |
-
"route": [
|
| 35 |
-
"road_2_1_2",
|
| 36 |
-
"road_1_1_2"
|
| 37 |
-
],
|
| 38 |
-
"interval": 5.0,
|
| 39 |
-
"startTime": 0,
|
| 40 |
-
"endTime": -1
|
| 41 |
-
},
|
| 42 |
-
{
|
| 43 |
-
"vehicle": {
|
| 44 |
-
"length": 5.0,
|
| 45 |
-
"width": 2.0,
|
| 46 |
-
"maxPosAcc": 2.0,
|
| 47 |
-
"maxNegAcc": 4.5,
|
| 48 |
-
"usualPosAcc": 2.0,
|
| 49 |
-
"usualNegAcc": 4.5,
|
| 50 |
-
"minGap": 2.5,
|
| 51 |
-
"maxSpeed": 16.67,
|
| 52 |
-
"headwayTime": 1.5
|
| 53 |
-
},
|
| 54 |
-
"route": [
|
| 55 |
-
"road_1_0_1",
|
| 56 |
-
"road_1_1_1"
|
| 57 |
-
],
|
| 58 |
-
"interval": 5.0,
|
| 59 |
-
"startTime": 0,
|
| 60 |
-
"endTime": -1
|
| 61 |
-
},
|
| 62 |
-
{
|
| 63 |
-
"vehicle": {
|
| 64 |
-
"length": 5.0,
|
| 65 |
-
"width": 2.0,
|
| 66 |
-
"maxPosAcc": 2.0,
|
| 67 |
-
"maxNegAcc": 4.5,
|
| 68 |
-
"usualPosAcc": 2.0,
|
| 69 |
-
"usualNegAcc": 4.5,
|
| 70 |
-
"minGap": 2.5,
|
| 71 |
-
"maxSpeed": 16.67,
|
| 72 |
-
"headwayTime": 1.5
|
| 73 |
-
},
|
| 74 |
-
"route": [
|
| 75 |
-
"road_1_2_3",
|
| 76 |
-
"road_1_1_3"
|
| 77 |
-
],
|
| 78 |
-
"interval": 5.0,
|
| 79 |
-
"startTime": 0,
|
| 80 |
-
"endTime": -1
|
| 81 |
-
},
|
| 82 |
-
{
|
| 83 |
-
"vehicle": {
|
| 84 |
-
"length": 5.0,
|
| 85 |
-
"width": 2.0,
|
| 86 |
-
"maxPosAcc": 2.0,
|
| 87 |
-
"maxNegAcc": 4.5,
|
| 88 |
-
"usualPosAcc": 2.0,
|
| 89 |
-
"usualNegAcc": 4.5,
|
| 90 |
-
"minGap": 2.5,
|
| 91 |
-
"maxSpeed": 16.67,
|
| 92 |
-
"headwayTime": 1.5
|
| 93 |
-
},
|
| 94 |
-
"route": [
|
| 95 |
-
"road_1_0_1",
|
| 96 |
-
"road_1_1_0"
|
| 97 |
-
],
|
| 98 |
-
"interval": 5.0,
|
| 99 |
-
"startTime": 0,
|
| 100 |
-
"endTime": -1
|
| 101 |
-
},
|
| 102 |
-
{
|
| 103 |
-
"vehicle": {
|
| 104 |
-
"length": 5.0,
|
| 105 |
-
"width": 2.0,
|
| 106 |
-
"maxPosAcc": 2.0,
|
| 107 |
-
"maxNegAcc": 4.5,
|
| 108 |
-
"usualPosAcc": 2.0,
|
| 109 |
-
"usualNegAcc": 4.5,
|
| 110 |
-
"minGap": 2.5,
|
| 111 |
-
"maxSpeed": 16.67,
|
| 112 |
-
"headwayTime": 1.5
|
| 113 |
-
},
|
| 114 |
-
"route": [
|
| 115 |
-
"road_0_1_0",
|
| 116 |
-
"road_1_1_1"
|
| 117 |
-
],
|
| 118 |
-
"interval": 5.0,
|
| 119 |
-
"startTime": 0,
|
| 120 |
-
"endTime": -1
|
| 121 |
-
},
|
| 122 |
-
{
|
| 123 |
-
"vehicle": {
|
| 124 |
-
"length": 5.0,
|
| 125 |
-
"width": 2.0,
|
| 126 |
-
"maxPosAcc": 2.0,
|
| 127 |
-
"maxNegAcc": 4.5,
|
| 128 |
-
"usualPosAcc": 2.0,
|
| 129 |
-
"usualNegAcc": 4.5,
|
| 130 |
-
"minGap": 2.5,
|
| 131 |
-
"maxSpeed": 16.67,
|
| 132 |
-
"headwayTime": 1.5
|
| 133 |
-
},
|
| 134 |
-
"route": [
|
| 135 |
-
"road_2_1_2",
|
| 136 |
-
"road_1_1_3"
|
| 137 |
-
],
|
| 138 |
-
"interval": 5.0,
|
| 139 |
-
"startTime": 0,
|
| 140 |
-
"endTime": -1
|
| 141 |
-
},
|
| 142 |
-
{
|
| 143 |
-
"vehicle": {
|
| 144 |
-
"length": 5.0,
|
| 145 |
-
"width": 2.0,
|
| 146 |
-
"maxPosAcc": 2.0,
|
| 147 |
-
"maxNegAcc": 4.5,
|
| 148 |
-
"usualPosAcc": 2.0,
|
| 149 |
-
"usualNegAcc": 4.5,
|
| 150 |
-
"minGap": 2.5,
|
| 151 |
-
"maxSpeed": 16.67,
|
| 152 |
-
"headwayTime": 1.5
|
| 153 |
-
},
|
| 154 |
-
"route": [
|
| 155 |
-
"road_1_2_3",
|
| 156 |
-
"road_1_1_2"
|
| 157 |
-
],
|
| 158 |
-
"interval": 5.0,
|
| 159 |
-
"startTime": 0,
|
| 160 |
-
"endTime": -1
|
| 161 |
-
},
|
| 162 |
-
{
|
| 163 |
-
"vehicle": {
|
| 164 |
-
"length": 5.0,
|
| 165 |
-
"width": 2.0,
|
| 166 |
-
"maxPosAcc": 2.0,
|
| 167 |
-
"maxNegAcc": 4.5,
|
| 168 |
-
"usualPosAcc": 2.0,
|
| 169 |
-
"usualNegAcc": 4.5,
|
| 170 |
-
"minGap": 2.5,
|
| 171 |
-
"maxSpeed": 16.67,
|
| 172 |
-
"headwayTime": 1.5
|
| 173 |
-
},
|
| 174 |
-
"route": [
|
| 175 |
-
"road_0_1_0",
|
| 176 |
-
"road_1_1_3"
|
| 177 |
-
],
|
| 178 |
-
"interval": 5.0,
|
| 179 |
-
"startTime": 0,
|
| 180 |
-
"endTime": -1
|
| 181 |
-
},
|
| 182 |
-
{
|
| 183 |
-
"vehicle": {
|
| 184 |
-
"length": 5.0,
|
| 185 |
-
"width": 2.0,
|
| 186 |
-
"maxPosAcc": 2.0,
|
| 187 |
-
"maxNegAcc": 4.5,
|
| 188 |
-
"usualPosAcc": 2.0,
|
| 189 |
-
"usualNegAcc": 4.5,
|
| 190 |
-
"minGap": 2.5,
|
| 191 |
-
"maxSpeed": 16.67,
|
| 192 |
-
"headwayTime": 1.5
|
| 193 |
-
},
|
| 194 |
-
"route": [
|
| 195 |
-
"road_1_2_3",
|
| 196 |
-
"road_1_1_0"
|
| 197 |
-
],
|
| 198 |
-
"interval": 5.0,
|
| 199 |
-
"startTime": 0,
|
| 200 |
-
"endTime": -1
|
| 201 |
-
},
|
| 202 |
-
{
|
| 203 |
-
"vehicle": {
|
| 204 |
-
"length": 5.0,
|
| 205 |
-
"width": 2.0,
|
| 206 |
-
"maxPosAcc": 2.0,
|
| 207 |
-
"maxNegAcc": 4.5,
|
| 208 |
-
"usualPosAcc": 2.0,
|
| 209 |
-
"usualNegAcc": 4.5,
|
| 210 |
-
"minGap": 2.5,
|
| 211 |
-
"maxSpeed": 16.67,
|
| 212 |
-
"headwayTime": 1.5
|
| 213 |
-
},
|
| 214 |
-
"route": [
|
| 215 |
-
"road_2_1_2",
|
| 216 |
-
"road_1_1_1"
|
| 217 |
-
],
|
| 218 |
-
"interval": 5.0,
|
| 219 |
-
"startTime": 0,
|
| 220 |
-
"endTime": -1
|
| 221 |
-
},
|
| 222 |
-
{
|
| 223 |
-
"vehicle": {
|
| 224 |
-
"length": 5.0,
|
| 225 |
-
"width": 2.0,
|
| 226 |
-
"maxPosAcc": 2.0,
|
| 227 |
-
"maxNegAcc": 4.5,
|
| 228 |
-
"usualPosAcc": 2.0,
|
| 229 |
-
"usualNegAcc": 4.5,
|
| 230 |
-
"minGap": 2.5,
|
| 231 |
-
"maxSpeed": 16.67,
|
| 232 |
-
"headwayTime": 1.5
|
| 233 |
-
},
|
| 234 |
-
"route": [
|
| 235 |
-
"road_1_0_1",
|
| 236 |
-
"road_1_1_2"
|
| 237 |
-
],
|
| 238 |
-
"interval": 5.0,
|
| 239 |
-
"startTime": 0,
|
| 240 |
-
"endTime": -1
|
| 241 |
-
}
|
| 242 |
]
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"vehicle": {
|
| 4 |
+
"length": 5.0,
|
| 5 |
+
"width": 2.0,
|
| 6 |
+
"maxPosAcc": 2.0,
|
| 7 |
+
"maxNegAcc": 4.5,
|
| 8 |
+
"usualPosAcc": 2.0,
|
| 9 |
+
"usualNegAcc": 4.5,
|
| 10 |
+
"minGap": 2.5,
|
| 11 |
+
"maxSpeed": 16.67,
|
| 12 |
+
"headwayTime": 1.5
|
| 13 |
+
},
|
| 14 |
+
"route": [
|
| 15 |
+
"road_0_1_0",
|
| 16 |
+
"road_1_1_0"
|
| 17 |
+
],
|
| 18 |
+
"interval": 5.0,
|
| 19 |
+
"startTime": 0,
|
| 20 |
+
"endTime": -1
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"vehicle": {
|
| 24 |
+
"length": 5.0,
|
| 25 |
+
"width": 2.0,
|
| 26 |
+
"maxPosAcc": 2.0,
|
| 27 |
+
"maxNegAcc": 4.5,
|
| 28 |
+
"usualPosAcc": 2.0,
|
| 29 |
+
"usualNegAcc": 4.5,
|
| 30 |
+
"minGap": 2.5,
|
| 31 |
+
"maxSpeed": 16.67,
|
| 32 |
+
"headwayTime": 1.5
|
| 33 |
+
},
|
| 34 |
+
"route": [
|
| 35 |
+
"road_2_1_2",
|
| 36 |
+
"road_1_1_2"
|
| 37 |
+
],
|
| 38 |
+
"interval": 5.0,
|
| 39 |
+
"startTime": 0,
|
| 40 |
+
"endTime": -1
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"vehicle": {
|
| 44 |
+
"length": 5.0,
|
| 45 |
+
"width": 2.0,
|
| 46 |
+
"maxPosAcc": 2.0,
|
| 47 |
+
"maxNegAcc": 4.5,
|
| 48 |
+
"usualPosAcc": 2.0,
|
| 49 |
+
"usualNegAcc": 4.5,
|
| 50 |
+
"minGap": 2.5,
|
| 51 |
+
"maxSpeed": 16.67,
|
| 52 |
+
"headwayTime": 1.5
|
| 53 |
+
},
|
| 54 |
+
"route": [
|
| 55 |
+
"road_1_0_1",
|
| 56 |
+
"road_1_1_1"
|
| 57 |
+
],
|
| 58 |
+
"interval": 5.0,
|
| 59 |
+
"startTime": 0,
|
| 60 |
+
"endTime": -1
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"vehicle": {
|
| 64 |
+
"length": 5.0,
|
| 65 |
+
"width": 2.0,
|
| 66 |
+
"maxPosAcc": 2.0,
|
| 67 |
+
"maxNegAcc": 4.5,
|
| 68 |
+
"usualPosAcc": 2.0,
|
| 69 |
+
"usualNegAcc": 4.5,
|
| 70 |
+
"minGap": 2.5,
|
| 71 |
+
"maxSpeed": 16.67,
|
| 72 |
+
"headwayTime": 1.5
|
| 73 |
+
},
|
| 74 |
+
"route": [
|
| 75 |
+
"road_1_2_3",
|
| 76 |
+
"road_1_1_3"
|
| 77 |
+
],
|
| 78 |
+
"interval": 5.0,
|
| 79 |
+
"startTime": 0,
|
| 80 |
+
"endTime": -1
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"vehicle": {
|
| 84 |
+
"length": 5.0,
|
| 85 |
+
"width": 2.0,
|
| 86 |
+
"maxPosAcc": 2.0,
|
| 87 |
+
"maxNegAcc": 4.5,
|
| 88 |
+
"usualPosAcc": 2.0,
|
| 89 |
+
"usualNegAcc": 4.5,
|
| 90 |
+
"minGap": 2.5,
|
| 91 |
+
"maxSpeed": 16.67,
|
| 92 |
+
"headwayTime": 1.5
|
| 93 |
+
},
|
| 94 |
+
"route": [
|
| 95 |
+
"road_1_0_1",
|
| 96 |
+
"road_1_1_0"
|
| 97 |
+
],
|
| 98 |
+
"interval": 5.0,
|
| 99 |
+
"startTime": 0,
|
| 100 |
+
"endTime": -1
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"vehicle": {
|
| 104 |
+
"length": 5.0,
|
| 105 |
+
"width": 2.0,
|
| 106 |
+
"maxPosAcc": 2.0,
|
| 107 |
+
"maxNegAcc": 4.5,
|
| 108 |
+
"usualPosAcc": 2.0,
|
| 109 |
+
"usualNegAcc": 4.5,
|
| 110 |
+
"minGap": 2.5,
|
| 111 |
+
"maxSpeed": 16.67,
|
| 112 |
+
"headwayTime": 1.5
|
| 113 |
+
},
|
| 114 |
+
"route": [
|
| 115 |
+
"road_0_1_0",
|
| 116 |
+
"road_1_1_1"
|
| 117 |
+
],
|
| 118 |
+
"interval": 5.0,
|
| 119 |
+
"startTime": 0,
|
| 120 |
+
"endTime": -1
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"vehicle": {
|
| 124 |
+
"length": 5.0,
|
| 125 |
+
"width": 2.0,
|
| 126 |
+
"maxPosAcc": 2.0,
|
| 127 |
+
"maxNegAcc": 4.5,
|
| 128 |
+
"usualPosAcc": 2.0,
|
| 129 |
+
"usualNegAcc": 4.5,
|
| 130 |
+
"minGap": 2.5,
|
| 131 |
+
"maxSpeed": 16.67,
|
| 132 |
+
"headwayTime": 1.5
|
| 133 |
+
},
|
| 134 |
+
"route": [
|
| 135 |
+
"road_2_1_2",
|
| 136 |
+
"road_1_1_3"
|
| 137 |
+
],
|
| 138 |
+
"interval": 5.0,
|
| 139 |
+
"startTime": 0,
|
| 140 |
+
"endTime": -1
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"vehicle": {
|
| 144 |
+
"length": 5.0,
|
| 145 |
+
"width": 2.0,
|
| 146 |
+
"maxPosAcc": 2.0,
|
| 147 |
+
"maxNegAcc": 4.5,
|
| 148 |
+
"usualPosAcc": 2.0,
|
| 149 |
+
"usualNegAcc": 4.5,
|
| 150 |
+
"minGap": 2.5,
|
| 151 |
+
"maxSpeed": 16.67,
|
| 152 |
+
"headwayTime": 1.5
|
| 153 |
+
},
|
| 154 |
+
"route": [
|
| 155 |
+
"road_1_2_3",
|
| 156 |
+
"road_1_1_2"
|
| 157 |
+
],
|
| 158 |
+
"interval": 5.0,
|
| 159 |
+
"startTime": 0,
|
| 160 |
+
"endTime": -1
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"vehicle": {
|
| 164 |
+
"length": 5.0,
|
| 165 |
+
"width": 2.0,
|
| 166 |
+
"maxPosAcc": 2.0,
|
| 167 |
+
"maxNegAcc": 4.5,
|
| 168 |
+
"usualPosAcc": 2.0,
|
| 169 |
+
"usualNegAcc": 4.5,
|
| 170 |
+
"minGap": 2.5,
|
| 171 |
+
"maxSpeed": 16.67,
|
| 172 |
+
"headwayTime": 1.5
|
| 173 |
+
},
|
| 174 |
+
"route": [
|
| 175 |
+
"road_0_1_0",
|
| 176 |
+
"road_1_1_3"
|
| 177 |
+
],
|
| 178 |
+
"interval": 5.0,
|
| 179 |
+
"startTime": 0,
|
| 180 |
+
"endTime": -1
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"vehicle": {
|
| 184 |
+
"length": 5.0,
|
| 185 |
+
"width": 2.0,
|
| 186 |
+
"maxPosAcc": 2.0,
|
| 187 |
+
"maxNegAcc": 4.5,
|
| 188 |
+
"usualPosAcc": 2.0,
|
| 189 |
+
"usualNegAcc": 4.5,
|
| 190 |
+
"minGap": 2.5,
|
| 191 |
+
"maxSpeed": 16.67,
|
| 192 |
+
"headwayTime": 1.5
|
| 193 |
+
},
|
| 194 |
+
"route": [
|
| 195 |
+
"road_1_2_3",
|
| 196 |
+
"road_1_1_0"
|
| 197 |
+
],
|
| 198 |
+
"interval": 5.0,
|
| 199 |
+
"startTime": 0,
|
| 200 |
+
"endTime": -1
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"vehicle": {
|
| 204 |
+
"length": 5.0,
|
| 205 |
+
"width": 2.0,
|
| 206 |
+
"maxPosAcc": 2.0,
|
| 207 |
+
"maxNegAcc": 4.5,
|
| 208 |
+
"usualPosAcc": 2.0,
|
| 209 |
+
"usualNegAcc": 4.5,
|
| 210 |
+
"minGap": 2.5,
|
| 211 |
+
"maxSpeed": 16.67,
|
| 212 |
+
"headwayTime": 1.5
|
| 213 |
+
},
|
| 214 |
+
"route": [
|
| 215 |
+
"road_2_1_2",
|
| 216 |
+
"road_1_1_1"
|
| 217 |
+
],
|
| 218 |
+
"interval": 5.0,
|
| 219 |
+
"startTime": 0,
|
| 220 |
+
"endTime": -1
|
| 221 |
+
},
|
| 222 |
+
{
|
| 223 |
+
"vehicle": {
|
| 224 |
+
"length": 5.0,
|
| 225 |
+
"width": 2.0,
|
| 226 |
+
"maxPosAcc": 2.0,
|
| 227 |
+
"maxNegAcc": 4.5,
|
| 228 |
+
"usualPosAcc": 2.0,
|
| 229 |
+
"usualNegAcc": 4.5,
|
| 230 |
+
"minGap": 2.5,
|
| 231 |
+
"maxSpeed": 16.67,
|
| 232 |
+
"headwayTime": 1.5
|
| 233 |
+
},
|
| 234 |
+
"route": [
|
| 235 |
+
"road_1_0_1",
|
| 236 |
+
"road_1_1_2"
|
| 237 |
+
],
|
| 238 |
+
"interval": 5.0,
|
| 239 |
+
"startTime": 0,
|
| 240 |
+
"endTime": -1
|
| 241 |
+
}
|
| 242 |
]
|
third_party/CityFlow/frontend/Point.js
CHANGED
|
@@ -1,57 +1,57 @@
|
|
| 1 |
-
class Point {
|
| 2 |
-
constructor(x, y) {
|
| 3 |
-
if (arguments.length == 1) {
|
| 4 |
-
this.x = x[0];
|
| 5 |
-
this.y = x[1];
|
| 6 |
-
} else if (arguments.length == 2) {
|
| 7 |
-
this.x = x;
|
| 8 |
-
this.y = y;
|
| 9 |
-
}
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
getLength() {
|
| 13 |
-
return Math.sqrt(this.x*this.x + this.y*this.y);
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
getAngle() {
|
| 17 |
-
return this.getAngleInRadians() * 180 / Math.PI;
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
getAngleInRadians() {
|
| 21 |
-
return Math.atan2(this.y, this.x);
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
add(point) {
|
| 25 |
-
return new Point(this.x + point.x, this.y + point.y)
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
directTo(point) {
|
| 29 |
-
let x = point.x - this.x;
|
| 30 |
-
let y = point.y - this.y;
|
| 31 |
-
let length = Math.sqrt(x*x + y*y);
|
| 32 |
-
let scale = length !== 0 ? 1 / length : 0;
|
| 33 |
-
return new Point(x * scale, y * scale);
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
distanceTo(point) {
|
| 37 |
-
let x = point.x - this.x;
|
| 38 |
-
let y = point.y - this.y;
|
| 39 |
-
return Math.sqrt(x*x + y*y);
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
moveAlong(direct, length) {
|
| 43 |
-
return new Point(this.x + direct.x * length, this.y + direct.y * length);
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
moveAlongDirectTo(point, length) {
|
| 47 |
-
let direct = this.directTo(point);
|
| 48 |
-
return new Point(this.x + direct.x * length, this.y + direct.y * length);
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
rotate(angle) {
|
| 52 |
-
angle = angle * Math.PI / 180;
|
| 53 |
-
let sin = Math.sin(angle);
|
| 54 |
-
let cos = Math.cos(angle);
|
| 55 |
-
return new Point(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
|
| 56 |
-
}
|
| 57 |
}
|
|
|
|
| 1 |
+
class Point {
|
| 2 |
+
constructor(x, y) {
|
| 3 |
+
if (arguments.length == 1) {
|
| 4 |
+
this.x = x[0];
|
| 5 |
+
this.y = x[1];
|
| 6 |
+
} else if (arguments.length == 2) {
|
| 7 |
+
this.x = x;
|
| 8 |
+
this.y = y;
|
| 9 |
+
}
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
getLength() {
|
| 13 |
+
return Math.sqrt(this.x*this.x + this.y*this.y);
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
getAngle() {
|
| 17 |
+
return this.getAngleInRadians() * 180 / Math.PI;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
getAngleInRadians() {
|
| 21 |
+
return Math.atan2(this.y, this.x);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
add(point) {
|
| 25 |
+
return new Point(this.x + point.x, this.y + point.y)
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
directTo(point) {
|
| 29 |
+
let x = point.x - this.x;
|
| 30 |
+
let y = point.y - this.y;
|
| 31 |
+
let length = Math.sqrt(x*x + y*y);
|
| 32 |
+
let scale = length !== 0 ? 1 / length : 0;
|
| 33 |
+
return new Point(x * scale, y * scale);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
distanceTo(point) {
|
| 37 |
+
let x = point.x - this.x;
|
| 38 |
+
let y = point.y - this.y;
|
| 39 |
+
return Math.sqrt(x*x + y*y);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
moveAlong(direct, length) {
|
| 43 |
+
return new Point(this.x + direct.x * length, this.y + direct.y * length);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
moveAlongDirectTo(point, length) {
|
| 47 |
+
let direct = this.directTo(point);
|
| 48 |
+
return new Point(this.x + direct.x * length, this.y + direct.y * length);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
rotate(angle) {
|
| 52 |
+
angle = angle * Math.PI / 180;
|
| 53 |
+
let sin = Math.sin(angle);
|
| 54 |
+
let cos = Math.cos(angle);
|
| 55 |
+
return new Point(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
|
| 56 |
+
}
|
| 57 |
}
|
third_party/CityFlow/frontend/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
-
# CityFlow Frontend Tool
|
| 2 |
-
|
| 3 |
-
Open `index.html` to view replay.
|
| 4 |
-
|
| 5 |
-
Run `python download_replay.py` to download example replay files.
|
| 6 |
-
|
| 7 |
Checkout [Document](https://cityflow.readthedocs.io/en/latest/replay.html) for more instructions.
|
|
|
|
| 1 |
+
# CityFlow Frontend Tool
|
| 2 |
+
|
| 3 |
+
Open `index.html` to view replay.
|
| 4 |
+
|
| 5 |
+
Run `python download_replay.py` to download example replay files.
|
| 6 |
+
|
| 7 |
Checkout [Document](https://cityflow.readthedocs.io/en/latest/replay.html) for more instructions.
|
third_party/CityFlow/frontend/download_replay.py
CHANGED
|
@@ -1,19 +1,19 @@
|
|
| 1 |
-
import os
|
| 2 |
-
from urllib.request import urlretrieve
|
| 3 |
-
|
| 4 |
-
files = ["replay.txt", "roadnet.json"]
|
| 5 |
-
repo_url = "https://github.com/cityflow-project/data/raw/master/frontend/replay/"
|
| 6 |
-
|
| 7 |
-
folder = "replay"
|
| 8 |
-
if __name__ == '__main__':
|
| 9 |
-
if not (os.path.exists(folder) and os.path.isdir(folder)):
|
| 10 |
-
os.mkdir(folder)
|
| 11 |
-
print("make %s folder, done!" % folder)
|
| 12 |
-
|
| 13 |
-
for file_name in files:
|
| 14 |
-
if os.path.exists(folder + "/" + file_name):
|
| 15 |
-
print(file_name + " found.")
|
| 16 |
-
else:
|
| 17 |
-
print("retrieving %s from cityflow-project/data/frontend/replay" % file_name)
|
| 18 |
-
urlretrieve(repo_url + file_name, folder + "/" + file_name)
|
| 19 |
print("done!")
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from urllib.request import urlretrieve
|
| 3 |
+
|
| 4 |
+
files = ["replay.txt", "roadnet.json"]
|
| 5 |
+
repo_url = "https://github.com/cityflow-project/data/raw/master/frontend/replay/"
|
| 6 |
+
|
| 7 |
+
folder = "replay"
|
| 8 |
+
if __name__ == '__main__':
|
| 9 |
+
if not (os.path.exists(folder) and os.path.isdir(folder)):
|
| 10 |
+
os.mkdir(folder)
|
| 11 |
+
print("make %s folder, done!" % folder)
|
| 12 |
+
|
| 13 |
+
for file_name in files:
|
| 14 |
+
if os.path.exists(folder + "/" + file_name):
|
| 15 |
+
print(file_name + " found.")
|
| 16 |
+
else:
|
| 17 |
+
print("retrieving %s from cityflow-project/data/frontend/replay" % file_name)
|
| 18 |
+
urlretrieve(repo_url + file_name, folder + "/" + file_name)
|
| 19 |
print("done!")
|
third_party/CityFlow/frontend/index.html
CHANGED
|
@@ -1,136 +1,136 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="utf-8" />
|
| 5 |
-
<title>Traffic Visualizer</title>
|
| 6 |
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.8.2/pixi.min.js"></script>
|
| 7 |
-
<script src="https://cdn.jsdelivr.net/npm/pixi-viewport@4.2.3/dist/viewport.min.js"></script>
|
| 8 |
-
<link rel="stylesheet" href="/static/style_multi.css?v=20260308h">
|
| 9 |
-
<script src="/static/Point.js?v=20260308h"></script>
|
| 10 |
-
<style>
|
| 11 |
-
@keyframes fadeInUp {
|
| 12 |
-
from { opacity: 0; transform: translateY(8px); }
|
| 13 |
-
to { opacity: 1; transform: translateY(0); }
|
| 14 |
-
}
|
| 15 |
-
</style>
|
| 16 |
-
</head>
|
| 17 |
-
<body>
|
| 18 |
-
|
| 19 |
-
<!-- ── Left sidebar ───────────────────────────────────────
|
| 20 |
-
<aside id="sidebar">
|
| 21 |
-
<div>
|
| 22 |
-
<h1>Traffic Visualizer</h1>
|
| 23 |
-
<p class="subtitle">Multi-policy comparison dashboard</p>
|
| 24 |
-
</div>
|
| 25 |
-
|
| 26 |
-
<!-- 1. Roadnet upload -->
|
| 27 |
-
<div class="sidebar-section">
|
| 28 |
-
<h3>1. Upload Roadnet</h3>
|
| 29 |
-
<div id="roadnet-drop-zone">
|
| 30 |
-
<span class="drop-icon">🗺</span>
|
| 31 |
-
<div>Drop <strong>roadnet.json</strong> here</div>
|
| 32 |
-
<div style="font-size:11px; margin-top:4px; color:#555">or click to browse</div>
|
| 33 |
-
<div class="drop-filename"></div>
|
| 34 |
-
</div>
|
| 35 |
-
<input type="file" id="roadnet-file-input" accept=".json" />
|
| 36 |
-
</div>
|
| 37 |
-
|
| 38 |
-
<!-- 2. City / Scenario -->
|
| 39 |
-
<div class="sidebar-section">
|
| 40 |
-
<h3>2. Select Scenario</h3>
|
| 41 |
-
<div class="picker-row">
|
| 42 |
-
<label for="city-select">City</label>
|
| 43 |
-
<select id="city-select">
|
| 44 |
-
<option value="">-- loading --</option>
|
| 45 |
-
</select>
|
| 46 |
-
</div>
|
| 47 |
-
<div class="picker-row" style="margin-top:8px;">
|
| 48 |
-
<label for="scenario-select">Scenario</label>
|
| 49 |
-
<select id="scenario-select" disabled>
|
| 50 |
-
<option value="">-- select city first --</option>
|
| 51 |
-
</select>
|
| 52 |
-
</div>
|
| 53 |
-
</div>
|
| 54 |
-
|
| 55 |
-
<!-- 3. Policies -->
|
| 56 |
-
<div class="sidebar-section">
|
| 57 |
-
<h3>3. Policies</h3>
|
| 58 |
-
<div id="policy-list" class="policy-list"></div>
|
| 59 |
-
</div>
|
| 60 |
-
|
| 61 |
-
<!-- 4. View mode -->
|
| 62 |
-
<div class="sidebar-section">
|
| 63 |
-
<h3>4. View Mode</h3>
|
| 64 |
-
<div class="view-mode-row">
|
| 65 |
-
<button class="view-mode-btn active" data-panels="1">1</button>
|
| 66 |
-
<button class="view-mode-btn" data-panels="2">2</button>
|
| 67 |
-
<button class="view-mode-btn" data-panels="3">3</button>
|
| 68 |
-
<button class="view-mode-btn" data-panels="6">All 6</button>
|
| 69 |
-
</div>
|
| 70 |
-
</div>
|
| 71 |
-
|
| 72 |
-
<!-- Run -->
|
| 73 |
-
<button id="run-btn">Run Simulations</button>
|
| 74 |
-
<label class="force-rerun-row">
|
| 75 |
-
<input type="checkbox" id="force-rerun-checkbox" />
|
| 76 |
-
<span>Force rerun (ignore cache)</span>
|
| 77 |
-
</label>
|
| 78 |
-
|
| 79 |
-
<!-- Progress -->
|
| 80 |
-
<div class="sidebar-section" id="progress-section" style="display:none;"></div>
|
| 81 |
-
|
| 82 |
-
<!-- Debug log -->
|
| 83 |
-
<div class="sidebar-section" id="debug-log-section">
|
| 84 |
-
<h3>Debug Log</h3>
|
| 85 |
-
<div id="debug-log" class="debug-log"></div>
|
| 86 |
-
</div>
|
| 87 |
-
|
| 88 |
-
<!-- Recent runs -->
|
| 89 |
-
<div class="sidebar-section" id="recent-runs-section">
|
| 90 |
-
<h3>Recent Runs</h3>
|
| 91 |
-
<div id="recent-runs-list" class="recent-runs-list">
|
| 92 |
-
<div class="recent-run-empty">No saved runs yet.</div>
|
| 93 |
-
</div>
|
| 94 |
-
</div>
|
| 95 |
-
</aside>
|
| 96 |
-
|
| 97 |
-
<div id="sidebar-resize-handle"></div>
|
| 98 |
-
|
| 99 |
-
<!-- ── Main area ─────────────────────────────────────────── -->
|
| 100 |
-
<div id="main">
|
| 101 |
-
|
| 102 |
-
<!-- Playback toolbar -->
|
| 103 |
-
<div id="playback-bar">
|
| 104 |
-
<button id="pause-btn">Pause</button>
|
| 105 |
-
<button id="speed-down">−</button>
|
| 106 |
-
<input type="range" id="scrubber" min="0" max="0" value="0" />
|
| 107 |
-
<button id="speed-up">+</button>
|
| 108 |
-
<span id="speed-label">0.5x</span>
|
| 109 |
-
<span id="step-display">0 / 0</span>
|
| 110 |
-
</div>
|
| 111 |
-
|
| 112 |
-
<div id="summary-bar">
|
| 113 |
-
<div class="summary-empty">Run a simulation to compare policy metrics.</div>
|
| 114 |
-
</div>
|
| 115 |
-
|
| 116 |
-
<!-- Panels container -->
|
| 117 |
-
<div id="panels-container" class="layout-1" style="position:relative; flex:1; overflow:hidden;">
|
| 118 |
-
|
| 119 |
-
<!-- Welcome state (shown until first run) -->
|
| 120 |
-
<div id="welcome-overlay">
|
| 121 |
-
<h2>Traffic Visualizer</h2>
|
| 122 |
-
<p>
|
| 123 |
-
Upload a <strong>roadnet.json</strong> and select a scenario,
|
| 124 |
-
then click <strong>Run Simulations</strong> to compare policies
|
| 125 |
-
side by side.
|
| 126 |
-
</p>
|
| 127 |
-
</div>
|
| 128 |
-
|
| 129 |
-
</div>
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
</div>
|
| 133 |
-
|
| 134 |
-
<script src="/static/script_multi.js?v=20260308h"></script>
|
| 135 |
-
</body>
|
| 136 |
-
</html>
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<title>Traffic Visualizer</title>
|
| 6 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.8.2/pixi.min.js"></script>
|
| 7 |
+
<script src="https://cdn.jsdelivr.net/npm/pixi-viewport@4.2.3/dist/viewport.min.js"></script>
|
| 8 |
+
<link rel="stylesheet" href="/static/style_multi.css?v=20260308h">
|
| 9 |
+
<script src="/static/Point.js?v=20260308h"></script>
|
| 10 |
+
<style>
|
| 11 |
+
@keyframes fadeInUp {
|
| 12 |
+
from { opacity: 0; transform: translateY(8px); }
|
| 13 |
+
to { opacity: 1; transform: translateY(0); }
|
| 14 |
+
}
|
| 15 |
+
</style>
|
| 16 |
+
</head>
|
| 17 |
+
<body>
|
| 18 |
+
|
| 19 |
+
<!-- ── Left sidebar ──��───────────────────────────────────── -->
|
| 20 |
+
<aside id="sidebar">
|
| 21 |
+
<div>
|
| 22 |
+
<h1>Traffic Visualizer</h1>
|
| 23 |
+
<p class="subtitle">Multi-policy comparison dashboard</p>
|
| 24 |
+
</div>
|
| 25 |
+
|
| 26 |
+
<!-- 1. Roadnet upload -->
|
| 27 |
+
<div class="sidebar-section">
|
| 28 |
+
<h3>1. Upload Roadnet</h3>
|
| 29 |
+
<div id="roadnet-drop-zone">
|
| 30 |
+
<span class="drop-icon">🗺</span>
|
| 31 |
+
<div>Drop <strong>roadnet.json</strong> here</div>
|
| 32 |
+
<div style="font-size:11px; margin-top:4px; color:#555">or click to browse</div>
|
| 33 |
+
<div class="drop-filename"></div>
|
| 34 |
+
</div>
|
| 35 |
+
<input type="file" id="roadnet-file-input" accept=".json" />
|
| 36 |
+
</div>
|
| 37 |
+
|
| 38 |
+
<!-- 2. City / Scenario -->
|
| 39 |
+
<div class="sidebar-section">
|
| 40 |
+
<h3>2. Select Scenario</h3>
|
| 41 |
+
<div class="picker-row">
|
| 42 |
+
<label for="city-select">City</label>
|
| 43 |
+
<select id="city-select">
|
| 44 |
+
<option value="">-- loading --</option>
|
| 45 |
+
</select>
|
| 46 |
+
</div>
|
| 47 |
+
<div class="picker-row" style="margin-top:8px;">
|
| 48 |
+
<label for="scenario-select">Scenario</label>
|
| 49 |
+
<select id="scenario-select" disabled>
|
| 50 |
+
<option value="">-- select city first --</option>
|
| 51 |
+
</select>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
|
| 55 |
+
<!-- 3. Policies -->
|
| 56 |
+
<div class="sidebar-section">
|
| 57 |
+
<h3>3. Policies</h3>
|
| 58 |
+
<div id="policy-list" class="policy-list"></div>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<!-- 4. View mode -->
|
| 62 |
+
<div class="sidebar-section">
|
| 63 |
+
<h3>4. View Mode</h3>
|
| 64 |
+
<div class="view-mode-row">
|
| 65 |
+
<button class="view-mode-btn active" data-panels="1">1</button>
|
| 66 |
+
<button class="view-mode-btn" data-panels="2">2</button>
|
| 67 |
+
<button class="view-mode-btn" data-panels="3">3</button>
|
| 68 |
+
<button class="view-mode-btn" data-panels="6">All 6</button>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
<!-- Run -->
|
| 73 |
+
<button id="run-btn">Run Simulations</button>
|
| 74 |
+
<label class="force-rerun-row">
|
| 75 |
+
<input type="checkbox" id="force-rerun-checkbox" />
|
| 76 |
+
<span>Force rerun (ignore cache)</span>
|
| 77 |
+
</label>
|
| 78 |
+
|
| 79 |
+
<!-- Progress -->
|
| 80 |
+
<div class="sidebar-section" id="progress-section" style="display:none;"></div>
|
| 81 |
+
|
| 82 |
+
<!-- Debug log -->
|
| 83 |
+
<div class="sidebar-section" id="debug-log-section">
|
| 84 |
+
<h3>Debug Log</h3>
|
| 85 |
+
<div id="debug-log" class="debug-log"></div>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
<!-- Recent runs -->
|
| 89 |
+
<div class="sidebar-section" id="recent-runs-section">
|
| 90 |
+
<h3>Recent Runs</h3>
|
| 91 |
+
<div id="recent-runs-list" class="recent-runs-list">
|
| 92 |
+
<div class="recent-run-empty">No saved runs yet.</div>
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
</aside>
|
| 96 |
+
|
| 97 |
+
<div id="sidebar-resize-handle"></div>
|
| 98 |
+
|
| 99 |
+
<!-- ── Main area ─────────────────────────────────────────── -->
|
| 100 |
+
<div id="main">
|
| 101 |
+
|
| 102 |
+
<!-- Playback toolbar -->
|
| 103 |
+
<div id="playback-bar">
|
| 104 |
+
<button id="pause-btn">Pause</button>
|
| 105 |
+
<button id="speed-down">−</button>
|
| 106 |
+
<input type="range" id="scrubber" min="0" max="0" value="0" />
|
| 107 |
+
<button id="speed-up">+</button>
|
| 108 |
+
<span id="speed-label">0.5x</span>
|
| 109 |
+
<span id="step-display">0 / 0</span>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
<div id="summary-bar">
|
| 113 |
+
<div class="summary-empty">Run a simulation to compare policy metrics.</div>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
<!-- Panels container -->
|
| 117 |
+
<div id="panels-container" class="layout-1" style="position:relative; flex:1; overflow:hidden;">
|
| 118 |
+
|
| 119 |
+
<!-- Welcome state (shown until first run) -->
|
| 120 |
+
<div id="welcome-overlay">
|
| 121 |
+
<h2>Traffic Visualizer</h2>
|
| 122 |
+
<p>
|
| 123 |
+
Upload a <strong>roadnet.json</strong> and select a scenario,
|
| 124 |
+
then click <strong>Run Simulations</strong> to compare policies
|
| 125 |
+
side by side.
|
| 126 |
+
</p>
|
| 127 |
+
</div>
|
| 128 |
+
|
| 129 |
+
</div>
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
<script src="/static/script_multi.js?v=20260308h"></script>
|
| 135 |
+
</body>
|
| 136 |
+
</html>
|
third_party/CityFlow/frontend/script.js
CHANGED
|
@@ -1,1035 +1,1035 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Draw Road Network
|
| 3 |
-
*/
|
| 4 |
-
id = Math.random().toString(36).substring(2, 15);
|
| 5 |
-
|
| 6 |
-
BACKGROUND_COLOR = 0xe8ebed;
|
| 7 |
-
LANE_COLOR = 0x586970;
|
| 8 |
-
LANE_BORDER_WIDTH = 1;
|
| 9 |
-
LANE_BORDER_COLOR = 0x82a8ba;
|
| 10 |
-
LANE_INNER_COLOR = 0xbed8e8;
|
| 11 |
-
LANE_DASH = 10;
|
| 12 |
-
LANE_GAP = 12;
|
| 13 |
-
TRAFFIC_LIGHT_WIDTH = 3;
|
| 14 |
-
MAX_TRAFFIC_LIGHT_NUM = 100000;
|
| 15 |
-
ROTATE = 90;
|
| 16 |
-
|
| 17 |
-
CAR_LENGTH = 5;
|
| 18 |
-
CAR_WIDTH = 2;
|
| 19 |
-
CAR_COLOR = 0xe8bed4;
|
| 20 |
-
|
| 21 |
-
CAR_COLORS = [0xf2bfd7, // pink
|
| 22 |
-
0xb7ebe4, // cyan
|
| 23 |
-
0xdbebb7, // blue
|
| 24 |
-
0xf5ddb5,
|
| 25 |
-
0xd4b5f5];
|
| 26 |
-
CAR_COLORS_NUM = CAR_COLORS.length;
|
| 27 |
-
|
| 28 |
-
NUM_CAR_POOL = 150000;
|
| 29 |
-
DISTRICT_BORDER_COLOR = 0x34495e;
|
| 30 |
-
GATEWAY_NODE_COLOR = 0xf39c12;
|
| 31 |
-
GATEWAY_EDGE_COLOR = 0xe67e22;
|
| 32 |
-
DISTRICT_PALETTE = [
|
| 33 |
-
0x1f77b4, 0xff7f0e, 0x2ca02c, 0xd62728, 0x9467bd, 0x8c564b,
|
| 34 |
-
0xe377c2, 0x7f7f7f, 0xbcbd22, 0x17becf, 0x393b79, 0x637939
|
| 35 |
-
];
|
| 36 |
-
|
| 37 |
-
LIGHT_RED = 0xdb635e;
|
| 38 |
-
LIGHT_GREEN = 0x85ee00;
|
| 39 |
-
|
| 40 |
-
TURN_SIGNAL_COLOR = 0xFFFFFF;
|
| 41 |
-
TURN_SIGNAL_WIDTH = 1;
|
| 42 |
-
TURN_SIGNAL_LENGTH = 5;
|
| 43 |
-
|
| 44 |
-
var simulation, roadnet, steps;
|
| 45 |
-
var nodes = {};
|
| 46 |
-
var edges = {};
|
| 47 |
-
var logs;
|
| 48 |
-
var gettingLog = false;
|
| 49 |
-
|
| 50 |
-
let Application = PIXI.Application,
|
| 51 |
-
Sprite = PIXI.Sprite,
|
| 52 |
-
Graphics = PIXI.Graphics,
|
| 53 |
-
Container = PIXI.Container,
|
| 54 |
-
ParticleContainer = PIXI.particles.ParticleContainer,
|
| 55 |
-
Texture = PIXI.Texture,
|
| 56 |
-
Rectangle = PIXI.Rectangle
|
| 57 |
-
;
|
| 58 |
-
|
| 59 |
-
var controls = new function () {
|
| 60 |
-
this.replaySpeedMax = 1;
|
| 61 |
-
this.replaySpeedMin = 0.01;
|
| 62 |
-
this.replaySpeed = 0.5;
|
| 63 |
-
this.paused = false;
|
| 64 |
-
};
|
| 65 |
-
|
| 66 |
-
var trafficLightsG = {};
|
| 67 |
-
|
| 68 |
-
var app, viewport, renderer, simulatorContainer, carContainer, trafficLightContainer;
|
| 69 |
-
var overlayContainer;
|
| 70 |
-
var turnSignalContainer;
|
| 71 |
-
var carPool;
|
| 72 |
-
|
| 73 |
-
var cnt = 0;
|
| 74 |
-
var frameElapsed = 0;
|
| 75 |
-
var totalStep;
|
| 76 |
-
|
| 77 |
-
var nodeCarNum = document.getElementById("car-num");
|
| 78 |
-
var nodeProgressPercentage = document.getElementById("progress-percentage");
|
| 79 |
-
var nodeTotalStep = document.getElementById("total-step-num");
|
| 80 |
-
var nodeCurrentStep = document.getElementById("current-step-num");
|
| 81 |
-
var nodeSelectedEntity = document.getElementById("selected-entity");
|
| 82 |
-
|
| 83 |
-
var SPEED = 3, SCALE_SPEED = 1.01;
|
| 84 |
-
var LEFT = 37, UP = 38, RIGHT = 39, DOWN = 40;
|
| 85 |
-
var MINUS = 189, EQUAL = 187, P = 80;
|
| 86 |
-
var LEFT_BRACKET = 219, RIGHT_BRACKET = 221;
|
| 87 |
-
var ONE = 49, TWO = 50;
|
| 88 |
-
var SPACE = 32;
|
| 89 |
-
|
| 90 |
-
var keyDown = new Set();
|
| 91 |
-
|
| 92 |
-
var turnSignalTextures = [];
|
| 93 |
-
|
| 94 |
-
let pauseButton = document.getElementById("pause");
|
| 95 |
-
let nodeCanvas = document.getElementById("simulator-canvas");
|
| 96 |
-
let replayControlDom = document.getElementById("replay-control");
|
| 97 |
-
let replaySpeedDom = document.getElementById("replay-speed");
|
| 98 |
-
|
| 99 |
-
let loading = false;
|
| 100 |
-
let infoDOM = document.getElementById("info");
|
| 101 |
-
let selectedDOM = document.getElementById("selected-entity");
|
| 102 |
-
|
| 103 |
-
function infoAppend(msg) {
|
| 104 |
-
infoDOM.innerText += "- " + msg + "\n";
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
function infoReset() {
|
| 108 |
-
infoDOM.innerText = "";
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
/**
|
| 112 |
-
* Upload files
|
| 113 |
-
*/
|
| 114 |
-
let ready = false;
|
| 115 |
-
|
| 116 |
-
let roadnetData = [];
|
| 117 |
-
let replayData = [];
|
| 118 |
-
let chartData = [];
|
| 119 |
-
let districtDataRaw = [];
|
| 120 |
-
let districtMap = null;
|
| 121 |
-
let showDistrictOverlay = true;
|
| 122 |
-
let showGatewayOverlay = true;
|
| 123 |
-
|
| 124 |
-
function handleChooseFile(v, label_dom) {
|
| 125 |
-
return function(evt) {
|
| 126 |
-
let file = evt.target.files[0];
|
| 127 |
-
label_dom.innerText = file.name;
|
| 128 |
-
}
|
| 129 |
-
}
|
| 130 |
-
|
| 131 |
-
function uploadFile(v, file, callback) {
|
| 132 |
-
let reader = new FileReader();
|
| 133 |
-
reader.onloadstart = function () {
|
| 134 |
-
infoAppend("Loading " + file.name);
|
| 135 |
-
};
|
| 136 |
-
reader.onerror = function() {
|
| 137 |
-
infoAppend("Loading " + file.name + "failed");
|
| 138 |
-
}
|
| 139 |
-
reader.onload = function (e) {
|
| 140 |
-
infoAppend(file.name + " loaded");
|
| 141 |
-
v[0] = e.target.result;
|
| 142 |
-
callback();
|
| 143 |
-
};
|
| 144 |
-
try {
|
| 145 |
-
reader.readAsText(file);
|
| 146 |
-
} catch (e) {
|
| 147 |
-
infoAppend("Loading failed");
|
| 148 |
-
console.error(e.message);
|
| 149 |
-
}
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
let debugMode = false;
|
| 153 |
-
let chartLog;
|
| 154 |
-
let showChart = false;
|
| 155 |
-
let chartConainterDOM = document.getElementById("chart-container");
|
| 156 |
-
let showDistrictOverlayDom = document.getElementById("show-district-overlay");
|
| 157 |
-
let showGatewayOverlayDom = document.getElementById("show-gateway-overlay");
|
| 158 |
-
|
| 159 |
-
function parseDistrictMap() {
|
| 160 |
-
if (!districtDataRaw[0]) {
|
| 161 |
-
districtMap = null;
|
| 162 |
-
return;
|
| 163 |
-
}
|
| 164 |
-
try {
|
| 165 |
-
districtMap = JSON.parse(districtDataRaw[0]);
|
| 166 |
-
} catch (e) {
|
| 167 |
-
districtMap = null;
|
| 168 |
-
infoAppend("Parsing district map file failed");
|
| 169 |
-
}
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
function uploadOptionalDistrictThenChart(done) {
|
| 173 |
-
if (DistrictFileDom.value) {
|
| 174 |
-
uploadFile(districtDataRaw, DistrictFileDom.files[0], function() {
|
| 175 |
-
parseDistrictMap();
|
| 176 |
-
if (ChartFileDom.value) {
|
| 177 |
-
showChart = true;
|
| 178 |
-
uploadFile(chartData, ChartFileDom.files[0], done);
|
| 179 |
-
} else {
|
| 180 |
-
showChart = false;
|
| 181 |
-
done();
|
| 182 |
-
}
|
| 183 |
-
});
|
| 184 |
-
} else {
|
| 185 |
-
districtMap = null;
|
| 186 |
-
if (ChartFileDom.value) {
|
| 187 |
-
showChart = true;
|
| 188 |
-
uploadFile(chartData, ChartFileDom.files[0], done);
|
| 189 |
-
} else {
|
| 190 |
-
showChart = false;
|
| 191 |
-
done();
|
| 192 |
-
}
|
| 193 |
-
}
|
| 194 |
-
}
|
| 195 |
-
|
| 196 |
-
function start() {
|
| 197 |
-
if (loading) return;
|
| 198 |
-
loading = true;
|
| 199 |
-
infoReset();
|
| 200 |
-
showDistrictOverlay = showDistrictOverlayDom.checked;
|
| 201 |
-
showGatewayOverlay = showGatewayOverlayDom.checked;
|
| 202 |
-
uploadFile(roadnetData, RoadnetFileDom.files[0], function(){
|
| 203 |
-
uploadFile(replayData, ReplayFileDom.files[0], function(){
|
| 204 |
-
let after_update = function() {
|
| 205 |
-
infoAppend("drawing roadnet");
|
| 206 |
-
ready = false;
|
| 207 |
-
document.getElementById("guide").classList.add("d-none");
|
| 208 |
-
hideCanvas();
|
| 209 |
-
try {
|
| 210 |
-
simulation = JSON.parse(roadnetData[0]);
|
| 211 |
-
} catch (e) {
|
| 212 |
-
infoAppend("Parsing roadnet file failed");
|
| 213 |
-
loading = false;
|
| 214 |
-
return;
|
| 215 |
-
}
|
| 216 |
-
try {
|
| 217 |
-
logs = replayData[0].split('\n');
|
| 218 |
-
logs.pop();
|
| 219 |
-
} catch (e) {
|
| 220 |
-
infoAppend("Reading replay file failed");
|
| 221 |
-
loading = false;
|
| 222 |
-
return;
|
| 223 |
-
}
|
| 224 |
-
|
| 225 |
-
totalStep = logs.length;
|
| 226 |
-
if (showChart) {
|
| 227 |
-
chartConainterDOM.classList.remove("d-none");
|
| 228 |
-
let chart_lines = chartData[0].split('\n');
|
| 229 |
-
if (chart_lines.length == 0) {
|
| 230 |
-
infoAppend("Chart file is empty");
|
| 231 |
-
showChart = false;
|
| 232 |
-
}
|
| 233 |
-
chartLog = [];
|
| 234 |
-
for (let i = 0 ; i < totalStep ; ++i) {
|
| 235 |
-
step_data = chart_lines[i + 1].split(/[ \t]+/);
|
| 236 |
-
chartLog.push([]);
|
| 237 |
-
for (let j = 0; j < step_data.length; ++j) {
|
| 238 |
-
chartLog[i].push(parseFloat(step_data[j]));
|
| 239 |
-
}
|
| 240 |
-
}
|
| 241 |
-
chart.init(chart_lines[0], chartLog[0].length, totalStep);
|
| 242 |
-
}else {
|
| 243 |
-
chartConainterDOM.classList.add("d-none");
|
| 244 |
-
}
|
| 245 |
-
|
| 246 |
-
controls.paused = false;
|
| 247 |
-
cnt = 0;
|
| 248 |
-
debugMode = document.getElementById("debug-mode").checked;
|
| 249 |
-
setTimeout(function () {
|
| 250 |
-
try {
|
| 251 |
-
drawRoadnet();
|
| 252 |
-
} catch (e) {
|
| 253 |
-
infoAppend("Drawing roadnet failed");
|
| 254 |
-
console.error(e.message);
|
| 255 |
-
loading = false;
|
| 256 |
-
return;
|
| 257 |
-
}
|
| 258 |
-
ready = true;
|
| 259 |
-
loading = false;
|
| 260 |
-
infoAppend("Start replaying");
|
| 261 |
-
}, 200);
|
| 262 |
-
};
|
| 263 |
-
|
| 264 |
-
uploadOptionalDistrictThenChart(after_update);
|
| 265 |
-
|
| 266 |
-
}); // replay callback
|
| 267 |
-
}); // roadnet callback
|
| 268 |
-
}
|
| 269 |
-
|
| 270 |
-
let RoadnetFileDom = document.getElementById("roadnet-file");
|
| 271 |
-
let ReplayFileDom = document.getElementById("replay-file");
|
| 272 |
-
let ChartFileDom = document.getElementById("chart-file");
|
| 273 |
-
let DistrictFileDom = document.getElementById("district-file");
|
| 274 |
-
|
| 275 |
-
RoadnetFileDom.addEventListener("change",
|
| 276 |
-
handleChooseFile(roadnetData, document.getElementById("roadnet-label")), false);
|
| 277 |
-
ReplayFileDom.addEventListener("change",
|
| 278 |
-
handleChooseFile(replayData, document.getElementById("replay-label")), false);
|
| 279 |
-
ChartFileDom.addEventListener("change",
|
| 280 |
-
handleChooseFile(chartData, document.getElementById("chart-label")), false);
|
| 281 |
-
DistrictFileDom.addEventListener("change",
|
| 282 |
-
handleChooseFile(districtDataRaw, document.getElementById("district-label")), false);
|
| 283 |
-
|
| 284 |
-
showDistrictOverlayDom.addEventListener("change", function(evt) {
|
| 285 |
-
showDistrictOverlay = evt.target.checked;
|
| 286 |
-
if (ready) drawRoadnet();
|
| 287 |
-
});
|
| 288 |
-
showGatewayOverlayDom.addEventListener("change", function(evt) {
|
| 289 |
-
showGatewayOverlay = evt.target.checked;
|
| 290 |
-
if (ready) drawRoadnet();
|
| 291 |
-
});
|
| 292 |
-
|
| 293 |
-
document.getElementById("start-btn").addEventListener("click", start);
|
| 294 |
-
|
| 295 |
-
document.getElementById("slow-btn").addEventListener("click", function() {
|
| 296 |
-
updateReplaySpeed(controls.replaySpeed - 0.1);
|
| 297 |
-
})
|
| 298 |
-
|
| 299 |
-
document.getElementById("fast-btn").addEventListener("click", function() {
|
| 300 |
-
updateReplaySpeed(controls.replaySpeed + 0.1);
|
| 301 |
-
})
|
| 302 |
-
|
| 303 |
-
function updateReplaySpeed(speed){
|
| 304 |
-
speed = Math.min(speed, 1);
|
| 305 |
-
speed = Math.max(speed, 0);
|
| 306 |
-
controls.replaySpeed = speed;
|
| 307 |
-
replayControlDom.value = speed * 100;
|
| 308 |
-
replaySpeedDom.innerHTML = speed.toFixed(2);
|
| 309 |
-
}
|
| 310 |
-
|
| 311 |
-
updateReplaySpeed(0.5);
|
| 312 |
-
|
| 313 |
-
replayControlDom.addEventListener('change', function(e){
|
| 314 |
-
updateReplaySpeed(replayControlDom.value / 100);
|
| 315 |
-
});
|
| 316 |
-
|
| 317 |
-
document.addEventListener('keydown', function(e) {
|
| 318 |
-
if (e.keyCode == P) {
|
| 319 |
-
controls.paused = !controls.paused;
|
| 320 |
-
} else if (e.keyCode == ONE) {
|
| 321 |
-
updateReplaySpeed(Math.max(controls.replaySpeed / 1.5, controls.replaySpeedMin));
|
| 322 |
-
} else if (e.keyCode == TWO ) {
|
| 323 |
-
updateReplaySpeed(Math.min(controls.replaySpeed * 1.5, controls.replaySpeedMax));
|
| 324 |
-
} else if (e.keyCode == LEFT_BRACKET) {
|
| 325 |
-
cnt = (cnt - 1) % totalStep;
|
| 326 |
-
cnt = (cnt + totalStep) % totalStep;
|
| 327 |
-
drawStep(cnt);
|
| 328 |
-
} else if (e.keyCode == RIGHT_BRACKET) {
|
| 329 |
-
cnt = (cnt + 1) % totalStep;
|
| 330 |
-
drawStep(cnt);
|
| 331 |
-
} else {
|
| 332 |
-
keyDown.add(e.keyCode)
|
| 333 |
-
}
|
| 334 |
-
});
|
| 335 |
-
|
| 336 |
-
document.addEventListener('keyup', (e) => keyDown.delete(e.keyCode));
|
| 337 |
-
|
| 338 |
-
nodeCanvas.addEventListener('dblclick', function(e){
|
| 339 |
-
controls.paused = !controls.paused;
|
| 340 |
-
});
|
| 341 |
-
|
| 342 |
-
pauseButton.addEventListener('click', function(e){
|
| 343 |
-
controls.paused = !controls.paused;
|
| 344 |
-
});
|
| 345 |
-
|
| 346 |
-
function initCanvas() {
|
| 347 |
-
app = new Application({
|
| 348 |
-
width: nodeCanvas.offsetWidth,
|
| 349 |
-
height: nodeCanvas.offsetHeight,
|
| 350 |
-
transparent: false,
|
| 351 |
-
backgroundColor: BACKGROUND_COLOR
|
| 352 |
-
});
|
| 353 |
-
|
| 354 |
-
nodeCanvas.appendChild(app.view);
|
| 355 |
-
app.view.classList.add("d-none");
|
| 356 |
-
|
| 357 |
-
renderer = app.renderer;
|
| 358 |
-
renderer.interactive = true;
|
| 359 |
-
renderer.autoResize = true;
|
| 360 |
-
|
| 361 |
-
renderer.resize(nodeCanvas.offsetWidth, nodeCanvas.offsetHeight);
|
| 362 |
-
app.ticker.add(run);
|
| 363 |
-
}
|
| 364 |
-
|
| 365 |
-
function showCanvas() {
|
| 366 |
-
document.getElementById("spinner").classList.add("d-none");
|
| 367 |
-
app.view.classList.remove("d-none");
|
| 368 |
-
}
|
| 369 |
-
|
| 370 |
-
function hideCanvas() {
|
| 371 |
-
document.getElementById("spinner").classList.remove("d-none");
|
| 372 |
-
app.view.classList.add("d-none");
|
| 373 |
-
}
|
| 374 |
-
|
| 375 |
-
function drawRoadnet() {
|
| 376 |
-
if (simulatorContainer) {
|
| 377 |
-
simulatorContainer.destroy(true);
|
| 378 |
-
}
|
| 379 |
-
app.stage.removeChildren();
|
| 380 |
-
viewport = new Viewport.Viewport({
|
| 381 |
-
screenWidth: window.innerWidth,
|
| 382 |
-
screenHeight: window.innerHeight,
|
| 383 |
-
interaction: app.renderer.plugins.interaction
|
| 384 |
-
});
|
| 385 |
-
viewport
|
| 386 |
-
.drag()
|
| 387 |
-
.pinch()
|
| 388 |
-
.wheel()
|
| 389 |
-
.decelerate();
|
| 390 |
-
app.stage.addChild(viewport);
|
| 391 |
-
simulatorContainer = new Container();
|
| 392 |
-
viewport.addChild(simulatorContainer);
|
| 393 |
-
|
| 394 |
-
roadnet = simulation.static;
|
| 395 |
-
nodes = [];
|
| 396 |
-
edges = [];
|
| 397 |
-
trafficLightsG = {};
|
| 398 |
-
|
| 399 |
-
for (let i = 0, len = roadnet.nodes.length;i < len;++i) {
|
| 400 |
-
node = roadnet.nodes[i];
|
| 401 |
-
node.point = new Point(transCoord(node.point));
|
| 402 |
-
nodes[node.id] = node;
|
| 403 |
-
}
|
| 404 |
-
|
| 405 |
-
for (let i = 0, len = roadnet.edges.length;i < len;++i) {
|
| 406 |
-
edge = roadnet.edges[i];
|
| 407 |
-
edge.from = nodes[edge.from];
|
| 408 |
-
edge.to = nodes[edge.to];
|
| 409 |
-
for (let j = 0, len = edge.points.length;j < len;++j) {
|
| 410 |
-
edge.points[j] = new Point(transCoord(edge.points[j]));
|
| 411 |
-
}
|
| 412 |
-
edges[edge.id] = edge;
|
| 413 |
-
}
|
| 414 |
-
|
| 415 |
-
/**
|
| 416 |
-
* Draw Map
|
| 417 |
-
*/
|
| 418 |
-
trafficLightContainer = new ParticleContainer(MAX_TRAFFIC_LIGHT_NUM, {tint: true});
|
| 419 |
-
let mapContainer, mapGraphics;
|
| 420 |
-
if (debugMode) {
|
| 421 |
-
mapContainer = new Container();
|
| 422 |
-
simulatorContainer.addChild(mapContainer);
|
| 423 |
-
}else {
|
| 424 |
-
mapGraphics = new Graphics();
|
| 425 |
-
simulatorContainer.addChild(mapGraphics);
|
| 426 |
-
}
|
| 427 |
-
|
| 428 |
-
for (nodeId in nodes) {
|
| 429 |
-
if (!nodes[nodeId].virtual) {
|
| 430 |
-
let nodeGraphics;
|
| 431 |
-
if (debugMode) {
|
| 432 |
-
nodeGraphics = new Graphics();
|
| 433 |
-
mapContainer.addChild(nodeGraphics);
|
| 434 |
-
} else {
|
| 435 |
-
nodeGraphics = mapGraphics;
|
| 436 |
-
}
|
| 437 |
-
drawNode(nodes[nodeId], nodeGraphics);
|
| 438 |
-
}
|
| 439 |
-
}
|
| 440 |
-
for (edgeId in edges) {
|
| 441 |
-
let edgeGraphics;
|
| 442 |
-
if (debugMode) {
|
| 443 |
-
edgeGraphics = new Graphics();
|
| 444 |
-
mapContainer.addChild(edgeGraphics);
|
| 445 |
-
} else {
|
| 446 |
-
edgeGraphics = mapGraphics;
|
| 447 |
-
}
|
| 448 |
-
drawEdge(edges[edgeId], edgeGraphics);
|
| 449 |
-
}
|
| 450 |
-
drawOverlayLayers();
|
| 451 |
-
let bounds = simulatorContainer.getBounds();
|
| 452 |
-
simulatorContainer.pivot.set(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2);
|
| 453 |
-
simulatorContainer.position.set(renderer.width / 2, renderer.height / 2);
|
| 454 |
-
simulatorContainer.addChild(trafficLightContainer);
|
| 455 |
-
|
| 456 |
-
/**
|
| 457 |
-
* Settings for Cars
|
| 458 |
-
*/
|
| 459 |
-
TURN_SIGNAL_LENGTH = CAR_LENGTH;
|
| 460 |
-
TURN_SIGNAL_WIDTH = CAR_WIDTH / 2;
|
| 461 |
-
|
| 462 |
-
var carG = new Graphics();
|
| 463 |
-
carG.lineStyle(0);
|
| 464 |
-
carG.beginFill(0xFFFFFF, 0.8);
|
| 465 |
-
carG.drawRect(0, 0, CAR_LENGTH, CAR_WIDTH);
|
| 466 |
-
|
| 467 |
-
let carTexture = renderer.generateTexture(carG);
|
| 468 |
-
|
| 469 |
-
let signalG = new Graphics();
|
| 470 |
-
signalG.beginFill(TURN_SIGNAL_COLOR, 0.7).drawRect(0,0, TURN_SIGNAL_LENGTH, TURN_SIGNAL_WIDTH)
|
| 471 |
-
.drawRect(0, 3 * CAR_WIDTH - TURN_SIGNAL_WIDTH, TURN_SIGNAL_LENGTH, TURN_SIGNAL_WIDTH).endFill();
|
| 472 |
-
let turnSignalTexture = renderer.generateTexture(signalG);
|
| 473 |
-
|
| 474 |
-
let signalLeft = new Texture(turnSignalTexture, new Rectangle(0, 0, TURN_SIGNAL_LENGTH, CAR_WIDTH));
|
| 475 |
-
let signalStraight = new Texture(turnSignalTexture, new Rectangle(0, CAR_WIDTH, TURN_SIGNAL_LENGTH, CAR_WIDTH));
|
| 476 |
-
let signalRight = new Texture(turnSignalTexture, new Rectangle(0, CAR_WIDTH * 2, TURN_SIGNAL_LENGTH, CAR_WIDTH));
|
| 477 |
-
turnSignalTextures = [signalLeft, signalStraight, signalRight];
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
carPool = [];
|
| 481 |
-
if (debugMode)
|
| 482 |
-
carContainer = new Container();
|
| 483 |
-
else
|
| 484 |
-
carContainer = new ParticleContainer(NUM_CAR_POOL, {rotation: true, tint: true});
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
turnSignalContainer = new ParticleContainer(NUM_CAR_POOL, {rotation: true, tint: true});
|
| 488 |
-
simulatorContainer.addChild(carContainer);
|
| 489 |
-
simulatorContainer.addChild(turnSignalContainer);
|
| 490 |
-
for (let i = 0, len = NUM_CAR_POOL;i < len;++i) {
|
| 491 |
-
//var car = Sprite.fromImage("images/car.png")
|
| 492 |
-
let car = new Sprite(carTexture);
|
| 493 |
-
let signal = new Sprite(turnSignalTextures[1]);
|
| 494 |
-
car.anchor.set(1, 0.5);
|
| 495 |
-
|
| 496 |
-
if (debugMode) {
|
| 497 |
-
car.interactive = true;
|
| 498 |
-
car.on('mouseover', function () {
|
| 499 |
-
selectedDOM.innerText = car.name;
|
| 500 |
-
car.alpha = 0.8;
|
| 501 |
-
});
|
| 502 |
-
car.on('mouseout', function () {
|
| 503 |
-
// selectedDOM.innerText = "";
|
| 504 |
-
car.alpha = 1;
|
| 505 |
-
});
|
| 506 |
-
}
|
| 507 |
-
signal.anchor.set(1, 0.5);
|
| 508 |
-
carPool.push([car, signal]);
|
| 509 |
-
}
|
| 510 |
-
showCanvas();
|
| 511 |
-
|
| 512 |
-
return true;
|
| 513 |
-
}
|
| 514 |
-
|
| 515 |
-
function appendText(id, text) {
|
| 516 |
-
let p = document.createElement("span");
|
| 517 |
-
p.innerText = text;
|
| 518 |
-
document.getElementById("info").appendChild(p);
|
| 519 |
-
document.getElementById("info").appendChild(document.createElement("br"));
|
| 520 |
-
}
|
| 521 |
-
|
| 522 |
-
var statsFile = "";
|
| 523 |
-
var withRange = false;
|
| 524 |
-
var nodeStats, nodeRange;
|
| 525 |
-
|
| 526 |
-
initCanvas();
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
function transCoord(point) {
|
| 530 |
-
return [point[0], -point[1]];
|
| 531 |
-
}
|
| 532 |
-
|
| 533 |
-
function getDistrictColor(districtId) {
|
| 534 |
-
return DISTRICT_PALETTE[stringHash(districtId) % DISTRICT_PALETTE.length];
|
| 535 |
-
}
|
| 536 |
-
|
| 537 |
-
function cross2D(o, a, b) {
|
| 538 |
-
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
| 539 |
-
}
|
| 540 |
-
|
| 541 |
-
function convexHull(points) {
|
| 542 |
-
if (!points || points.length <= 2) return points ? points.slice() : [];
|
| 543 |
-
let sorted = points
|
| 544 |
-
.slice()
|
| 545 |
-
.sort((p, q) => (p.x === q.x ? p.y - q.y : p.x - q.x));
|
| 546 |
-
let lower = [];
|
| 547 |
-
for (let i = 0; i < sorted.length; ++i) {
|
| 548 |
-
while (lower.length >= 2 && cross2D(lower[lower.length - 2], lower[lower.length - 1], sorted[i]) <= 0) {
|
| 549 |
-
lower.pop();
|
| 550 |
-
}
|
| 551 |
-
lower.push(sorted[i]);
|
| 552 |
-
}
|
| 553 |
-
let upper = [];
|
| 554 |
-
for (let i = sorted.length - 1; i >= 0; --i) {
|
| 555 |
-
while (upper.length >= 2 && cross2D(upper[upper.length - 2], upper[upper.length - 1], sorted[i]) <= 0) {
|
| 556 |
-
upper.pop();
|
| 557 |
-
}
|
| 558 |
-
upper.push(sorted[i]);
|
| 559 |
-
}
|
| 560 |
-
lower.pop();
|
| 561 |
-
upper.pop();
|
| 562 |
-
return lower.concat(upper);
|
| 563 |
-
}
|
| 564 |
-
|
| 565 |
-
function drawDistrictRegionFills(graphics, districtToPoints) {
|
| 566 |
-
for (let districtId in districtToPoints) {
|
| 567 |
-
let pts = districtToPoints[districtId];
|
| 568 |
-
if (!pts || pts.length === 0) continue;
|
| 569 |
-
let color = getDistrictColor(districtId);
|
| 570 |
-
if (pts.length >= 3) {
|
| 571 |
-
let hull = convexHull(pts);
|
| 572 |
-
if (hull.length >= 3) {
|
| 573 |
-
graphics.lineStyle(1.2, color, 0.35);
|
| 574 |
-
graphics.beginFill(color, 0.11);
|
| 575 |
-
graphics.moveTo(hull[0].x, hull[0].y);
|
| 576 |
-
for (let i = 1; i < hull.length; ++i) {
|
| 577 |
-
graphics.lineTo(hull[i].x, hull[i].y);
|
| 578 |
-
}
|
| 579 |
-
graphics.lineTo(hull[0].x, hull[0].y);
|
| 580 |
-
graphics.endFill();
|
| 581 |
-
continue;
|
| 582 |
-
}
|
| 583 |
-
}
|
| 584 |
-
if (pts.length === 2) {
|
| 585 |
-
graphics.lineStyle(18, color, 0.10);
|
| 586 |
-
graphics.drawLine(pts[0], pts[1]);
|
| 587 |
-
continue;
|
| 588 |
-
}
|
| 589 |
-
graphics.lineStyle(0);
|
| 590 |
-
graphics.beginFill(color, 0.12);
|
| 591 |
-
graphics.drawCircle(pts[0].x, pts[0].y, 18);
|
| 592 |
-
graphics.endFill();
|
| 593 |
-
}
|
| 594 |
-
}
|
| 595 |
-
|
| 596 |
-
function drawEdgePolyline(graphics, edge, width, color, alpha) {
|
| 597 |
-
graphics.lineStyle(width, color, alpha);
|
| 598 |
-
for (let i = 1; i < edge.points.length; ++i) {
|
| 599 |
-
graphics.drawLine(edge.points[i - 1], edge.points[i]);
|
| 600 |
-
}
|
| 601 |
-
}
|
| 602 |
-
|
| 603 |
-
function drawOverlayLayers() {
|
| 604 |
-
if (overlayContainer) {
|
| 605 |
-
overlayContainer.destroy(true);
|
| 606 |
-
overlayContainer = null;
|
| 607 |
-
}
|
| 608 |
-
if (!showDistrictOverlay && !showGatewayOverlay) {
|
| 609 |
-
return;
|
| 610 |
-
}
|
| 611 |
-
overlayContainer = new Container();
|
| 612 |
-
simulatorContainer.addChild(overlayContainer);
|
| 613 |
-
|
| 614 |
-
let overlayGraphics = new Graphics();
|
| 615 |
-
overlayContainer.addChild(overlayGraphics);
|
| 616 |
-
|
| 617 |
-
let intersectionToDistrict = {};
|
| 618 |
-
if (districtMap && districtMap.intersection_to_district) {
|
| 619 |
-
intersectionToDistrict = districtMap.intersection_to_district;
|
| 620 |
-
}
|
| 621 |
-
let gatewayNodeIds = new Set(
|
| 622 |
-
districtMap && districtMap.gateway_intersections
|
| 623 |
-
? districtMap.gateway_intersections
|
| 624 |
-
: []
|
| 625 |
-
);
|
| 626 |
-
let gatewayEdgeIds = new Set(
|
| 627 |
-
districtMap && districtMap.gateway_roads
|
| 628 |
-
? districtMap.gateway_roads
|
| 629 |
-
: []
|
| 630 |
-
);
|
| 631 |
-
let nodeDegree = {};
|
| 632 |
-
for (let edgeId in edges) {
|
| 633 |
-
let edge = edges[edgeId];
|
| 634 |
-
nodeDegree[edge.from.id] = (nodeDegree[edge.from.id] || 0) + 1;
|
| 635 |
-
nodeDegree[edge.to.id] = (nodeDegree[edge.to.id] || 0) + 1;
|
| 636 |
-
}
|
| 637 |
-
for (let nodeId in nodes) {
|
| 638 |
-
if ((nodeDegree[nodeId] || 0) <= 1) {
|
| 639 |
-
gatewayNodeIds.add(nodeId);
|
| 640 |
-
}
|
| 641 |
-
}
|
| 642 |
-
|
| 643 |
-
if (showDistrictOverlay && districtMap && districtMap.intersection_to_district) {
|
| 644 |
-
let districtToPoints = {};
|
| 645 |
-
for (let nodeId in nodes) {
|
| 646 |
-
let districtId = intersectionToDistrict[nodeId];
|
| 647 |
-
if (!districtId) continue;
|
| 648 |
-
if (!districtToPoints[districtId]) districtToPoints[districtId] = [];
|
| 649 |
-
districtToPoints[districtId].push(nodes[nodeId].point);
|
| 650 |
-
}
|
| 651 |
-
drawDistrictRegionFills(overlayGraphics, districtToPoints);
|
| 652 |
-
|
| 653 |
-
for (let edgeId in edges) {
|
| 654 |
-
let edge = edges[edgeId];
|
| 655 |
-
let fromDistrict = intersectionToDistrict[edge.from.id];
|
| 656 |
-
let toDistrict = intersectionToDistrict[edge.to.id];
|
| 657 |
-
if (!fromDistrict || !toDistrict) continue;
|
| 658 |
-
if (fromDistrict === toDistrict) {
|
| 659 |
-
drawEdgePolyline(
|
| 660 |
-
overlayGraphics,
|
| 661 |
-
edge,
|
| 662 |
-
1.2,
|
| 663 |
-
getDistrictColor(fromDistrict),
|
| 664 |
-
0.45
|
| 665 |
-
);
|
| 666 |
-
} else {
|
| 667 |
-
drawEdgePolyline(
|
| 668 |
-
overlayGraphics,
|
| 669 |
-
edge,
|
| 670 |
-
1.8,
|
| 671 |
-
DISTRICT_BORDER_COLOR,
|
| 672 |
-
0.8
|
| 673 |
-
);
|
| 674 |
-
}
|
| 675 |
-
}
|
| 676 |
-
for (let nodeId in nodes) {
|
| 677 |
-
let districtId = intersectionToDistrict[nodeId];
|
| 678 |
-
if (!districtId) continue;
|
| 679 |
-
let point = nodes[nodeId].point;
|
| 680 |
-
overlayGraphics.beginFill(getDistrictColor(districtId), 0.55);
|
| 681 |
-
overlayGraphics.drawCircle(point.x, point.y, 2.0);
|
| 682 |
-
overlayGraphics.endFill();
|
| 683 |
-
}
|
| 684 |
-
}
|
| 685 |
-
|
| 686 |
-
if (showGatewayOverlay) {
|
| 687 |
-
for (let edgeId in edges) {
|
| 688 |
-
let edge = edges[edgeId];
|
| 689 |
-
let fromGateway = gatewayNodeIds.has(edge.from.id) || String(edge.from.id).indexOf("g_") === 0;
|
| 690 |
-
let toGateway = gatewayNodeIds.has(edge.to.id) || String(edge.to.id).indexOf("g_") === 0;
|
| 691 |
-
let edgeLooksGateway = edgeId.indexOf("_g_") >= 0 || edgeId.indexOf("r_g_") === 0;
|
| 692 |
-
if (
|
| 693 |
-
gatewayEdgeIds.has(edgeId)
|
| 694 |
-
|| fromGateway
|
| 695 |
-
|| toGateway
|
| 696 |
-
|| edgeLooksGateway
|
| 697 |
-
) {
|
| 698 |
-
drawEdgePolyline(
|
| 699 |
-
overlayGraphics,
|
| 700 |
-
edge,
|
| 701 |
-
5.6,
|
| 702 |
-
GATEWAY_EDGE_COLOR,
|
| 703 |
-
0.82
|
| 704 |
-
);
|
| 705 |
-
}
|
| 706 |
-
}
|
| 707 |
-
for (let nodeId in nodes) {
|
| 708 |
-
if (gatewayNodeIds.has(nodeId) || String(nodeId).indexOf("g_") === 0) {
|
| 709 |
-
let point = nodes[nodeId].point;
|
| 710 |
-
overlayGraphics.lineStyle(0);
|
| 711 |
-
overlayGraphics.beginFill(GATEWAY_NODE_COLOR, 0.28);
|
| 712 |
-
overlayGraphics.drawCircle(point.x, point.y, 34.0);
|
| 713 |
-
overlayGraphics.endFill();
|
| 714 |
-
overlayGraphics.lineStyle(2.2, 0x1f2d3d, 0.98);
|
| 715 |
-
overlayGraphics.beginFill(GATEWAY_NODE_COLOR, 0.96);
|
| 716 |
-
overlayGraphics.drawCircle(point.x, point.y, 13.0);
|
| 717 |
-
overlayGraphics.endFill();
|
| 718 |
-
overlayGraphics.lineStyle(0);
|
| 719 |
-
overlayGraphics.beginFill(0xffffff, 0.92);
|
| 720 |
-
overlayGraphics.drawCircle(point.x, point.y, 5.2);
|
| 721 |
-
overlayGraphics.endFill();
|
| 722 |
-
}
|
| 723 |
-
}
|
| 724 |
-
}
|
| 725 |
-
}
|
| 726 |
-
|
| 727 |
-
PIXI.Graphics.prototype.drawLine = function(pointA, pointB) {
|
| 728 |
-
this.moveTo(pointA.x, pointA.y);
|
| 729 |
-
this.lineTo(pointB.x, pointB.y);
|
| 730 |
-
}
|
| 731 |
-
|
| 732 |
-
PIXI.Graphics.prototype.drawDashLine = function(pointA, pointB, dash = 16, gap = 8) {
|
| 733 |
-
let direct = pointA.directTo(pointB);
|
| 734 |
-
let distance = pointA.distanceTo(pointB);
|
| 735 |
-
|
| 736 |
-
let currentPoint = pointA;
|
| 737 |
-
let currentDistance = 0;
|
| 738 |
-
let length;
|
| 739 |
-
let finish = false;
|
| 740 |
-
while (true) {
|
| 741 |
-
this.moveTo(currentPoint.x, currentPoint.y);
|
| 742 |
-
if (currentDistance + dash >= distance) {
|
| 743 |
-
length = distance - currentDistance;
|
| 744 |
-
finish = true;
|
| 745 |
-
} else {
|
| 746 |
-
length = dash
|
| 747 |
-
}
|
| 748 |
-
currentPoint = currentPoint.moveAlong(direct, length);
|
| 749 |
-
this.lineTo(currentPoint.x, currentPoint.y);
|
| 750 |
-
if (finish) break;
|
| 751 |
-
currentDistance += length;
|
| 752 |
-
|
| 753 |
-
if (currentDistance + gap >= distance) {
|
| 754 |
-
break;
|
| 755 |
-
} else {
|
| 756 |
-
currentPoint = currentPoint.moveAlong(direct, gap);
|
| 757 |
-
currentDistance += gap;
|
| 758 |
-
}
|
| 759 |
-
}
|
| 760 |
-
};
|
| 761 |
-
|
| 762 |
-
function drawNode(node, graphics) {
|
| 763 |
-
graphics.beginFill(LANE_COLOR);
|
| 764 |
-
let outline = node.outline;
|
| 765 |
-
for (let i = 0 ; i < outline.length ; i+=2) {
|
| 766 |
-
outline[i+1] = -outline[i+1];
|
| 767 |
-
if (i == 0)
|
| 768 |
-
graphics.moveTo(outline[i], outline[i+1]);
|
| 769 |
-
else
|
| 770 |
-
graphics.lineTo(outline[i], outline[i+1]);
|
| 771 |
-
}
|
| 772 |
-
graphics.endFill();
|
| 773 |
-
|
| 774 |
-
if (debugMode) {
|
| 775 |
-
graphics.hitArea = new PIXI.Polygon(outline);
|
| 776 |
-
graphics.interactive = true;
|
| 777 |
-
graphics.on("mouseover", function () {
|
| 778 |
-
selectedDOM.innerText = node.id;
|
| 779 |
-
graphics.alpha = 0.5;
|
| 780 |
-
});
|
| 781 |
-
graphics.on("mouseout", function () {
|
| 782 |
-
graphics.alpha = 1;
|
| 783 |
-
});
|
| 784 |
-
}
|
| 785 |
-
|
| 786 |
-
}
|
| 787 |
-
|
| 788 |
-
function drawEdge(edge, graphics) {
|
| 789 |
-
let from = edge.from;
|
| 790 |
-
let to = edge.to;
|
| 791 |
-
let points = edge.points;
|
| 792 |
-
|
| 793 |
-
let pointA, pointAOffset, pointB, pointBOffset;
|
| 794 |
-
let prevPointBOffset = null;
|
| 795 |
-
|
| 796 |
-
let roadWidth = 0;
|
| 797 |
-
edge.laneWidths.forEach(function(l){
|
| 798 |
-
roadWidth += l;
|
| 799 |
-
}, 0);
|
| 800 |
-
|
| 801 |
-
let coords = [], coords1 = [];
|
| 802 |
-
|
| 803 |
-
for (let i = 1;i < points.length;++i) {
|
| 804 |
-
if (i == 1){
|
| 805 |
-
pointA = points[0].moveAlongDirectTo(points[1], from.virtual ? 0 : from.width);
|
| 806 |
-
pointAOffset = points[0].directTo(points[1]).rotate(ROTATE);
|
| 807 |
-
} else {
|
| 808 |
-
pointA = points[i-1];
|
| 809 |
-
pointAOffset = prevPointBOffset;
|
| 810 |
-
}
|
| 811 |
-
if (i == points.length - 1) {
|
| 812 |
-
pointB = points[i].moveAlongDirectTo(points[i-1], to.virtual ? 0 : to.width);
|
| 813 |
-
pointBOffset = points[i-1].directTo(points[i]).rotate(ROTATE);
|
| 814 |
-
} else {
|
| 815 |
-
pointB = points[i];
|
| 816 |
-
pointBOffset = points[i-1].directTo(points[i+1]).rotate(ROTATE);
|
| 817 |
-
}
|
| 818 |
-
prevPointBOffset = pointBOffset;
|
| 819 |
-
|
| 820 |
-
lightG = new Graphics();
|
| 821 |
-
lightG.lineStyle(TRAFFIC_LIGHT_WIDTH, 0xFFFFFF);
|
| 822 |
-
lightG.drawLine(new Point(0, 0), new Point(1, 0));
|
| 823 |
-
lightTexture = renderer.generateTexture(lightG);
|
| 824 |
-
|
| 825 |
-
// Draw Traffic Lights
|
| 826 |
-
if (i == points.length-1 && !to.virtual) {
|
| 827 |
-
edgeTrafficLights = [];
|
| 828 |
-
prevOffset = offset = 0;
|
| 829 |
-
for (lane = 0;lane < edge.nLane;++lane) {
|
| 830 |
-
offset += edge.laneWidths[lane];
|
| 831 |
-
var light = new Sprite(lightTexture);
|
| 832 |
-
light.anchor.set(0, 0.5);
|
| 833 |
-
light.scale.set(offset - prevOffset, 1);
|
| 834 |
-
point_ = pointB.moveAlong(pointBOffset, prevOffset);
|
| 835 |
-
light.position.set(point_.x, point_.y);
|
| 836 |
-
light.rotation = pointBOffset.getAngleInRadians();
|
| 837 |
-
edgeTrafficLights.push(light);
|
| 838 |
-
prevOffset = offset;
|
| 839 |
-
trafficLightContainer.addChild(light);
|
| 840 |
-
}
|
| 841 |
-
trafficLightsG[edge.id] = edgeTrafficLights;
|
| 842 |
-
}
|
| 843 |
-
|
| 844 |
-
// Draw Roads
|
| 845 |
-
graphics.lineStyle(LANE_BORDER_WIDTH, LANE_BORDER_COLOR, 1);
|
| 846 |
-
graphics.drawLine(pointA, pointB);
|
| 847 |
-
|
| 848 |
-
pointA1 = pointA.moveAlong(pointAOffset, roadWidth);
|
| 849 |
-
pointB1 = pointB.moveAlong(pointBOffset, roadWidth);
|
| 850 |
-
|
| 851 |
-
graphics.lineStyle(0);
|
| 852 |
-
graphics.beginFill(LANE_COLOR);
|
| 853 |
-
|
| 854 |
-
coords = coords.concat([pointA.x, pointA.y, pointB.x, pointB.y]);
|
| 855 |
-
coords1 = coords1.concat([pointA1.y, pointA1.x, pointB1.y, pointB1.x]);
|
| 856 |
-
|
| 857 |
-
graphics.drawPolygon([pointA.x, pointA.y, pointB.x, pointB.y, pointB1.x, pointB1.y, pointA1.x, pointA1.y]);
|
| 858 |
-
graphics.endFill();
|
| 859 |
-
|
| 860 |
-
offset = 0;
|
| 861 |
-
for (let lane = 0, len = edge.nLane-1;lane < len;++lane) {
|
| 862 |
-
offset += edge.laneWidths[lane];
|
| 863 |
-
graphics.lineStyle(LANE_BORDER_WIDTH, LANE_INNER_COLOR);
|
| 864 |
-
graphics.drawDashLine(pointA.moveAlong(pointAOffset, offset), pointB.moveAlong(pointBOffset, offset), LANE_DASH, LANE_GAP);
|
| 865 |
-
}
|
| 866 |
-
|
| 867 |
-
offset += edge.laneWidths[edge.nLane-1];
|
| 868 |
-
|
| 869 |
-
// graphics.lineStyle(LANE_BORDER_WIDTH, LANE_BORDER_COLOR);
|
| 870 |
-
// graphics.drawLine(pointA.moveAlong(pointAOffset, offset), pointB.moveAlong(pointBOffset, offset));
|
| 871 |
-
}
|
| 872 |
-
|
| 873 |
-
if (debugMode) {
|
| 874 |
-
coords = coords.concat(coords1.reverse());
|
| 875 |
-
graphics.interactive = true;
|
| 876 |
-
graphics.hitArea = new PIXI.Polygon(coords);
|
| 877 |
-
graphics.on("mouseover", function () {
|
| 878 |
-
graphics.alpha = 0.5;
|
| 879 |
-
selectedDOM.innerText = edge.id;
|
| 880 |
-
});
|
| 881 |
-
|
| 882 |
-
graphics.on("mouseout", function () {
|
| 883 |
-
graphics.alpha = 1;
|
| 884 |
-
});
|
| 885 |
-
}
|
| 886 |
-
}
|
| 887 |
-
|
| 888 |
-
function run(delta) {
|
| 889 |
-
let redraw = false;
|
| 890 |
-
|
| 891 |
-
if (ready && (!controls.paused || redraw)) {
|
| 892 |
-
try {
|
| 893 |
-
drawStep(cnt);
|
| 894 |
-
}catch (e) {
|
| 895 |
-
infoAppend("Error occurred when drawing");
|
| 896 |
-
ready = false;
|
| 897 |
-
}
|
| 898 |
-
if (!controls.paused) {
|
| 899 |
-
frameElapsed += 1;
|
| 900 |
-
if (frameElapsed >= 1 / controls.replaySpeed ** 2) {
|
| 901 |
-
cnt += 1;
|
| 902 |
-
frameElapsed = 0;
|
| 903 |
-
if (cnt == totalStep) cnt = 0;
|
| 904 |
-
}
|
| 905 |
-
}
|
| 906 |
-
}
|
| 907 |
-
}
|
| 908 |
-
|
| 909 |
-
function _statusToColor(status) {
|
| 910 |
-
switch (status) {
|
| 911 |
-
case 'r':
|
| 912 |
-
return LIGHT_RED;
|
| 913 |
-
case 'g':
|
| 914 |
-
return LIGHT_GREEN;
|
| 915 |
-
default:
|
| 916 |
-
return 0x808080;
|
| 917 |
-
}
|
| 918 |
-
}
|
| 919 |
-
|
| 920 |
-
function stringHash(str) {
|
| 921 |
-
let hash = 0;
|
| 922 |
-
let p = 127, p_pow = 1;
|
| 923 |
-
let m = 1e9 + 9;
|
| 924 |
-
for (let i = 0; i < str.length; i++) {
|
| 925 |
-
hash = (hash + str.charCodeAt(i) * p_pow) % m;
|
| 926 |
-
p_pow = (p_pow * p) % m;
|
| 927 |
-
}
|
| 928 |
-
return hash;
|
| 929 |
-
}
|
| 930 |
-
|
| 931 |
-
function drawStep(step) {
|
| 932 |
-
if (showChart && (step > chart.ptr || step == 0)) {
|
| 933 |
-
if (step == 0) {
|
| 934 |
-
chart.clear();
|
| 935 |
-
}
|
| 936 |
-
chart.ptr = step;
|
| 937 |
-
chart.addData(chartLog[step]);
|
| 938 |
-
}
|
| 939 |
-
|
| 940 |
-
let [carLogs, tlLogs] = logs[step].split(';');
|
| 941 |
-
|
| 942 |
-
tlLogs = tlLogs.split(',');
|
| 943 |
-
carLogs = carLogs.split(',');
|
| 944 |
-
|
| 945 |
-
let tlLog, tlEdge, tlStatus;
|
| 946 |
-
for (let i = 0, len = tlLogs.length;i < len;++i) {
|
| 947 |
-
tlLog = tlLogs[i].split(' ');
|
| 948 |
-
tlEdge = tlLog[0];
|
| 949 |
-
tlStatus = tlLog.slice(1);
|
| 950 |
-
for (let j = 0, len = tlStatus.length;j < len;++j) {
|
| 951 |
-
trafficLightsG[tlEdge][j].tint = _statusToColor(tlStatus[j]);
|
| 952 |
-
if (tlStatus[j] == 'i' ) {
|
| 953 |
-
trafficLightsG[tlEdge][j].alpha = 0;
|
| 954 |
-
}else{
|
| 955 |
-
trafficLightsG[tlEdge][j].alpha = 1;
|
| 956 |
-
}
|
| 957 |
-
}
|
| 958 |
-
}
|
| 959 |
-
|
| 960 |
-
carContainer.removeChildren();
|
| 961 |
-
turnSignalContainer.removeChildren();
|
| 962 |
-
let carLog, position, length, width;
|
| 963 |
-
for (let i = 0, len = carLogs.length - 1;i < len;++i) {
|
| 964 |
-
carLog = carLogs[i].split(' ');
|
| 965 |
-
position = transCoord([parseFloat(carLog[0]), parseFloat(carLog[1])]);
|
| 966 |
-
length = parseFloat(carLog[5]);
|
| 967 |
-
width = parseFloat(carLog[6]);
|
| 968 |
-
carPool[i][0].position.set(position[0], position[1]);
|
| 969 |
-
carPool[i][0].rotation = 2*Math.PI - parseFloat(carLog[2]);
|
| 970 |
-
carPool[i][0].name = carLog[3];
|
| 971 |
-
let carColorId = stringHash(carLog[3]) % CAR_COLORS_NUM;
|
| 972 |
-
carPool[i][0].tint = CAR_COLORS[carColorId];
|
| 973 |
-
carPool[i][0].width = length;
|
| 974 |
-
carPool[i][0].height = width;
|
| 975 |
-
carContainer.addChild(carPool[i][0]);
|
| 976 |
-
|
| 977 |
-
let laneChange = parseInt(carLog[4]) + 1;
|
| 978 |
-
carPool[i][1].position.set(position[0], position[1]);
|
| 979 |
-
carPool[i][1].rotation = carPool[i][0].rotation;
|
| 980 |
-
carPool[i][1].texture = turnSignalTextures[laneChange];
|
| 981 |
-
carPool[i][1].width = length;
|
| 982 |
-
carPool[i][1].height = width;
|
| 983 |
-
turnSignalContainer.addChild(carPool[i][1]);
|
| 984 |
-
}
|
| 985 |
-
nodeCarNum.innerText = carLogs.length-1;
|
| 986 |
-
nodeTotalStep.innerText = totalStep;
|
| 987 |
-
nodeCurrentStep.innerText = cnt+1;
|
| 988 |
-
nodeProgressPercentage.innerText = (cnt / totalStep * 100).toFixed(2) + "%";
|
| 989 |
-
if (statsFile != "") {
|
| 990 |
-
if (withRange) nodeRange.value = stats[step][1];
|
| 991 |
-
nodeStats.innerText = stats[step][0].toFixed(2);
|
| 992 |
-
}
|
| 993 |
-
}
|
| 994 |
-
|
| 995 |
-
/*
|
| 996 |
-
Chart
|
| 997 |
-
*/
|
| 998 |
-
let chart = {
|
| 999 |
-
max_steps: 3600,
|
| 1000 |
-
data: {
|
| 1001 |
-
labels: [],
|
| 1002 |
-
series: [[]]
|
| 1003 |
-
},
|
| 1004 |
-
options: {
|
| 1005 |
-
showPoint: false,
|
| 1006 |
-
lineSmooth: false,
|
| 1007 |
-
axisX: {
|
| 1008 |
-
showGrid: false,
|
| 1009 |
-
showLabel: false
|
| 1010 |
-
}
|
| 1011 |
-
},
|
| 1012 |
-
init : function(title, series_cnt, max_step){
|
| 1013 |
-
document.getElementById("chart-title").innerText = title;
|
| 1014 |
-
this.max_steps = max_step;
|
| 1015 |
-
this.data.labels = new Array(this.max_steps);
|
| 1016 |
-
this.data.series = [];
|
| 1017 |
-
for (let i = 0 ; i < series_cnt ; ++i)
|
| 1018 |
-
this.data.series.push([]);
|
| 1019 |
-
this.chart = new Chartist.Line('#chart', this.data, this.options);
|
| 1020 |
-
},
|
| 1021 |
-
addData: function (value) {
|
| 1022 |
-
for (let i = 0 ; i < value.length; ++i) {
|
| 1023 |
-
this.data.series[i].push(value[i]);
|
| 1024 |
-
if (this.data.series[i].length > this.max_steps) {
|
| 1025 |
-
this.data.series[i].shift();
|
| 1026 |
-
}
|
| 1027 |
-
}
|
| 1028 |
-
this.chart.update();
|
| 1029 |
-
},
|
| 1030 |
-
clear: function() {
|
| 1031 |
-
for (let i = 0 ; i < this.data.series.length ; ++i)
|
| 1032 |
-
this.data.series[i] = [];
|
| 1033 |
-
},
|
| 1034 |
-
ptr: 0
|
| 1035 |
-
};
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Draw Road Network
|
| 3 |
+
*/
|
| 4 |
+
id = Math.random().toString(36).substring(2, 15);
|
| 5 |
+
|
| 6 |
+
BACKGROUND_COLOR = 0xe8ebed;
|
| 7 |
+
LANE_COLOR = 0x586970;
|
| 8 |
+
LANE_BORDER_WIDTH = 1;
|
| 9 |
+
LANE_BORDER_COLOR = 0x82a8ba;
|
| 10 |
+
LANE_INNER_COLOR = 0xbed8e8;
|
| 11 |
+
LANE_DASH = 10;
|
| 12 |
+
LANE_GAP = 12;
|
| 13 |
+
TRAFFIC_LIGHT_WIDTH = 3;
|
| 14 |
+
MAX_TRAFFIC_LIGHT_NUM = 100000;
|
| 15 |
+
ROTATE = 90;
|
| 16 |
+
|
| 17 |
+
CAR_LENGTH = 5;
|
| 18 |
+
CAR_WIDTH = 2;
|
| 19 |
+
CAR_COLOR = 0xe8bed4;
|
| 20 |
+
|
| 21 |
+
CAR_COLORS = [0xf2bfd7, // pink
|
| 22 |
+
0xb7ebe4, // cyan
|
| 23 |
+
0xdbebb7, // blue
|
| 24 |
+
0xf5ddb5,
|
| 25 |
+
0xd4b5f5];
|
| 26 |
+
CAR_COLORS_NUM = CAR_COLORS.length;
|
| 27 |
+
|
| 28 |
+
NUM_CAR_POOL = 150000;
|
| 29 |
+
DISTRICT_BORDER_COLOR = 0x34495e;
|
| 30 |
+
GATEWAY_NODE_COLOR = 0xf39c12;
|
| 31 |
+
GATEWAY_EDGE_COLOR = 0xe67e22;
|
| 32 |
+
DISTRICT_PALETTE = [
|
| 33 |
+
0x1f77b4, 0xff7f0e, 0x2ca02c, 0xd62728, 0x9467bd, 0x8c564b,
|
| 34 |
+
0xe377c2, 0x7f7f7f, 0xbcbd22, 0x17becf, 0x393b79, 0x637939
|
| 35 |
+
];
|
| 36 |
+
|
| 37 |
+
LIGHT_RED = 0xdb635e;
|
| 38 |
+
LIGHT_GREEN = 0x85ee00;
|
| 39 |
+
|
| 40 |
+
TURN_SIGNAL_COLOR = 0xFFFFFF;
|
| 41 |
+
TURN_SIGNAL_WIDTH = 1;
|
| 42 |
+
TURN_SIGNAL_LENGTH = 5;
|
| 43 |
+
|
| 44 |
+
var simulation, roadnet, steps;
|
| 45 |
+
var nodes = {};
|
| 46 |
+
var edges = {};
|
| 47 |
+
var logs;
|
| 48 |
+
var gettingLog = false;
|
| 49 |
+
|
| 50 |
+
let Application = PIXI.Application,
|
| 51 |
+
Sprite = PIXI.Sprite,
|
| 52 |
+
Graphics = PIXI.Graphics,
|
| 53 |
+
Container = PIXI.Container,
|
| 54 |
+
ParticleContainer = PIXI.particles.ParticleContainer,
|
| 55 |
+
Texture = PIXI.Texture,
|
| 56 |
+
Rectangle = PIXI.Rectangle
|
| 57 |
+
;
|
| 58 |
+
|
| 59 |
+
var controls = new function () {
|
| 60 |
+
this.replaySpeedMax = 1;
|
| 61 |
+
this.replaySpeedMin = 0.01;
|
| 62 |
+
this.replaySpeed = 0.5;
|
| 63 |
+
this.paused = false;
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
+
var trafficLightsG = {};
|
| 67 |
+
|
| 68 |
+
var app, viewport, renderer, simulatorContainer, carContainer, trafficLightContainer;
|
| 69 |
+
var overlayContainer;
|
| 70 |
+
var turnSignalContainer;
|
| 71 |
+
var carPool;
|
| 72 |
+
|
| 73 |
+
var cnt = 0;
|
| 74 |
+
var frameElapsed = 0;
|
| 75 |
+
var totalStep;
|
| 76 |
+
|
| 77 |
+
var nodeCarNum = document.getElementById("car-num");
|
| 78 |
+
var nodeProgressPercentage = document.getElementById("progress-percentage");
|
| 79 |
+
var nodeTotalStep = document.getElementById("total-step-num");
|
| 80 |
+
var nodeCurrentStep = document.getElementById("current-step-num");
|
| 81 |
+
var nodeSelectedEntity = document.getElementById("selected-entity");
|
| 82 |
+
|
| 83 |
+
var SPEED = 3, SCALE_SPEED = 1.01;
|
| 84 |
+
var LEFT = 37, UP = 38, RIGHT = 39, DOWN = 40;
|
| 85 |
+
var MINUS = 189, EQUAL = 187, P = 80;
|
| 86 |
+
var LEFT_BRACKET = 219, RIGHT_BRACKET = 221;
|
| 87 |
+
var ONE = 49, TWO = 50;
|
| 88 |
+
var SPACE = 32;
|
| 89 |
+
|
| 90 |
+
var keyDown = new Set();
|
| 91 |
+
|
| 92 |
+
var turnSignalTextures = [];
|
| 93 |
+
|
| 94 |
+
let pauseButton = document.getElementById("pause");
|
| 95 |
+
let nodeCanvas = document.getElementById("simulator-canvas");
|
| 96 |
+
let replayControlDom = document.getElementById("replay-control");
|
| 97 |
+
let replaySpeedDom = document.getElementById("replay-speed");
|
| 98 |
+
|
| 99 |
+
let loading = false;
|
| 100 |
+
let infoDOM = document.getElementById("info");
|
| 101 |
+
let selectedDOM = document.getElementById("selected-entity");
|
| 102 |
+
|
| 103 |
+
function infoAppend(msg) {
|
| 104 |
+
infoDOM.innerText += "- " + msg + "\n";
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
function infoReset() {
|
| 108 |
+
infoDOM.innerText = "";
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
/**
|
| 112 |
+
* Upload files
|
| 113 |
+
*/
|
| 114 |
+
let ready = false;
|
| 115 |
+
|
| 116 |
+
let roadnetData = [];
|
| 117 |
+
let replayData = [];
|
| 118 |
+
let chartData = [];
|
| 119 |
+
let districtDataRaw = [];
|
| 120 |
+
let districtMap = null;
|
| 121 |
+
let showDistrictOverlay = true;
|
| 122 |
+
let showGatewayOverlay = true;
|
| 123 |
+
|
| 124 |
+
function handleChooseFile(v, label_dom) {
|
| 125 |
+
return function(evt) {
|
| 126 |
+
let file = evt.target.files[0];
|
| 127 |
+
label_dom.innerText = file.name;
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
function uploadFile(v, file, callback) {
|
| 132 |
+
let reader = new FileReader();
|
| 133 |
+
reader.onloadstart = function () {
|
| 134 |
+
infoAppend("Loading " + file.name);
|
| 135 |
+
};
|
| 136 |
+
reader.onerror = function() {
|
| 137 |
+
infoAppend("Loading " + file.name + "failed");
|
| 138 |
+
}
|
| 139 |
+
reader.onload = function (e) {
|
| 140 |
+
infoAppend(file.name + " loaded");
|
| 141 |
+
v[0] = e.target.result;
|
| 142 |
+
callback();
|
| 143 |
+
};
|
| 144 |
+
try {
|
| 145 |
+
reader.readAsText(file);
|
| 146 |
+
} catch (e) {
|
| 147 |
+
infoAppend("Loading failed");
|
| 148 |
+
console.error(e.message);
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
let debugMode = false;
|
| 153 |
+
let chartLog;
|
| 154 |
+
let showChart = false;
|
| 155 |
+
let chartConainterDOM = document.getElementById("chart-container");
|
| 156 |
+
let showDistrictOverlayDom = document.getElementById("show-district-overlay");
|
| 157 |
+
let showGatewayOverlayDom = document.getElementById("show-gateway-overlay");
|
| 158 |
+
|
| 159 |
+
function parseDistrictMap() {
|
| 160 |
+
if (!districtDataRaw[0]) {
|
| 161 |
+
districtMap = null;
|
| 162 |
+
return;
|
| 163 |
+
}
|
| 164 |
+
try {
|
| 165 |
+
districtMap = JSON.parse(districtDataRaw[0]);
|
| 166 |
+
} catch (e) {
|
| 167 |
+
districtMap = null;
|
| 168 |
+
infoAppend("Parsing district map file failed");
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
function uploadOptionalDistrictThenChart(done) {
|
| 173 |
+
if (DistrictFileDom.value) {
|
| 174 |
+
uploadFile(districtDataRaw, DistrictFileDom.files[0], function() {
|
| 175 |
+
parseDistrictMap();
|
| 176 |
+
if (ChartFileDom.value) {
|
| 177 |
+
showChart = true;
|
| 178 |
+
uploadFile(chartData, ChartFileDom.files[0], done);
|
| 179 |
+
} else {
|
| 180 |
+
showChart = false;
|
| 181 |
+
done();
|
| 182 |
+
}
|
| 183 |
+
});
|
| 184 |
+
} else {
|
| 185 |
+
districtMap = null;
|
| 186 |
+
if (ChartFileDom.value) {
|
| 187 |
+
showChart = true;
|
| 188 |
+
uploadFile(chartData, ChartFileDom.files[0], done);
|
| 189 |
+
} else {
|
| 190 |
+
showChart = false;
|
| 191 |
+
done();
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
function start() {
|
| 197 |
+
if (loading) return;
|
| 198 |
+
loading = true;
|
| 199 |
+
infoReset();
|
| 200 |
+
showDistrictOverlay = showDistrictOverlayDom.checked;
|
| 201 |
+
showGatewayOverlay = showGatewayOverlayDom.checked;
|
| 202 |
+
uploadFile(roadnetData, RoadnetFileDom.files[0], function(){
|
| 203 |
+
uploadFile(replayData, ReplayFileDom.files[0], function(){
|
| 204 |
+
let after_update = function() {
|
| 205 |
+
infoAppend("drawing roadnet");
|
| 206 |
+
ready = false;
|
| 207 |
+
document.getElementById("guide").classList.add("d-none");
|
| 208 |
+
hideCanvas();
|
| 209 |
+
try {
|
| 210 |
+
simulation = JSON.parse(roadnetData[0]);
|
| 211 |
+
} catch (e) {
|
| 212 |
+
infoAppend("Parsing roadnet file failed");
|
| 213 |
+
loading = false;
|
| 214 |
+
return;
|
| 215 |
+
}
|
| 216 |
+
try {
|
| 217 |
+
logs = replayData[0].split('\n');
|
| 218 |
+
logs.pop();
|
| 219 |
+
} catch (e) {
|
| 220 |
+
infoAppend("Reading replay file failed");
|
| 221 |
+
loading = false;
|
| 222 |
+
return;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
totalStep = logs.length;
|
| 226 |
+
if (showChart) {
|
| 227 |
+
chartConainterDOM.classList.remove("d-none");
|
| 228 |
+
let chart_lines = chartData[0].split('\n');
|
| 229 |
+
if (chart_lines.length == 0) {
|
| 230 |
+
infoAppend("Chart file is empty");
|
| 231 |
+
showChart = false;
|
| 232 |
+
}
|
| 233 |
+
chartLog = [];
|
| 234 |
+
for (let i = 0 ; i < totalStep ; ++i) {
|
| 235 |
+
step_data = chart_lines[i + 1].split(/[ \t]+/);
|
| 236 |
+
chartLog.push([]);
|
| 237 |
+
for (let j = 0; j < step_data.length; ++j) {
|
| 238 |
+
chartLog[i].push(parseFloat(step_data[j]));
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
chart.init(chart_lines[0], chartLog[0].length, totalStep);
|
| 242 |
+
}else {
|
| 243 |
+
chartConainterDOM.classList.add("d-none");
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
controls.paused = false;
|
| 247 |
+
cnt = 0;
|
| 248 |
+
debugMode = document.getElementById("debug-mode").checked;
|
| 249 |
+
setTimeout(function () {
|
| 250 |
+
try {
|
| 251 |
+
drawRoadnet();
|
| 252 |
+
} catch (e) {
|
| 253 |
+
infoAppend("Drawing roadnet failed");
|
| 254 |
+
console.error(e.message);
|
| 255 |
+
loading = false;
|
| 256 |
+
return;
|
| 257 |
+
}
|
| 258 |
+
ready = true;
|
| 259 |
+
loading = false;
|
| 260 |
+
infoAppend("Start replaying");
|
| 261 |
+
}, 200);
|
| 262 |
+
};
|
| 263 |
+
|
| 264 |
+
uploadOptionalDistrictThenChart(after_update);
|
| 265 |
+
|
| 266 |
+
}); // replay callback
|
| 267 |
+
}); // roadnet callback
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
let RoadnetFileDom = document.getElementById("roadnet-file");
|
| 271 |
+
let ReplayFileDom = document.getElementById("replay-file");
|
| 272 |
+
let ChartFileDom = document.getElementById("chart-file");
|
| 273 |
+
let DistrictFileDom = document.getElementById("district-file");
|
| 274 |
+
|
| 275 |
+
RoadnetFileDom.addEventListener("change",
|
| 276 |
+
handleChooseFile(roadnetData, document.getElementById("roadnet-label")), false);
|
| 277 |
+
ReplayFileDom.addEventListener("change",
|
| 278 |
+
handleChooseFile(replayData, document.getElementById("replay-label")), false);
|
| 279 |
+
ChartFileDom.addEventListener("change",
|
| 280 |
+
handleChooseFile(chartData, document.getElementById("chart-label")), false);
|
| 281 |
+
DistrictFileDom.addEventListener("change",
|
| 282 |
+
handleChooseFile(districtDataRaw, document.getElementById("district-label")), false);
|
| 283 |
+
|
| 284 |
+
showDistrictOverlayDom.addEventListener("change", function(evt) {
|
| 285 |
+
showDistrictOverlay = evt.target.checked;
|
| 286 |
+
if (ready) drawRoadnet();
|
| 287 |
+
});
|
| 288 |
+
showGatewayOverlayDom.addEventListener("change", function(evt) {
|
| 289 |
+
showGatewayOverlay = evt.target.checked;
|
| 290 |
+
if (ready) drawRoadnet();
|
| 291 |
+
});
|
| 292 |
+
|
| 293 |
+
document.getElementById("start-btn").addEventListener("click", start);
|
| 294 |
+
|
| 295 |
+
document.getElementById("slow-btn").addEventListener("click", function() {
|
| 296 |
+
updateReplaySpeed(controls.replaySpeed - 0.1);
|
| 297 |
+
})
|
| 298 |
+
|
| 299 |
+
document.getElementById("fast-btn").addEventListener("click", function() {
|
| 300 |
+
updateReplaySpeed(controls.replaySpeed + 0.1);
|
| 301 |
+
})
|
| 302 |
+
|
| 303 |
+
function updateReplaySpeed(speed){
|
| 304 |
+
speed = Math.min(speed, 1);
|
| 305 |
+
speed = Math.max(speed, 0);
|
| 306 |
+
controls.replaySpeed = speed;
|
| 307 |
+
replayControlDom.value = speed * 100;
|
| 308 |
+
replaySpeedDom.innerHTML = speed.toFixed(2);
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
updateReplaySpeed(0.5);
|
| 312 |
+
|
| 313 |
+
replayControlDom.addEventListener('change', function(e){
|
| 314 |
+
updateReplaySpeed(replayControlDom.value / 100);
|
| 315 |
+
});
|
| 316 |
+
|
| 317 |
+
document.addEventListener('keydown', function(e) {
|
| 318 |
+
if (e.keyCode == P) {
|
| 319 |
+
controls.paused = !controls.paused;
|
| 320 |
+
} else if (e.keyCode == ONE) {
|
| 321 |
+
updateReplaySpeed(Math.max(controls.replaySpeed / 1.5, controls.replaySpeedMin));
|
| 322 |
+
} else if (e.keyCode == TWO ) {
|
| 323 |
+
updateReplaySpeed(Math.min(controls.replaySpeed * 1.5, controls.replaySpeedMax));
|
| 324 |
+
} else if (e.keyCode == LEFT_BRACKET) {
|
| 325 |
+
cnt = (cnt - 1) % totalStep;
|
| 326 |
+
cnt = (cnt + totalStep) % totalStep;
|
| 327 |
+
drawStep(cnt);
|
| 328 |
+
} else if (e.keyCode == RIGHT_BRACKET) {
|
| 329 |
+
cnt = (cnt + 1) % totalStep;
|
| 330 |
+
drawStep(cnt);
|
| 331 |
+
} else {
|
| 332 |
+
keyDown.add(e.keyCode)
|
| 333 |
+
}
|
| 334 |
+
});
|
| 335 |
+
|
| 336 |
+
document.addEventListener('keyup', (e) => keyDown.delete(e.keyCode));
|
| 337 |
+
|
| 338 |
+
nodeCanvas.addEventListener('dblclick', function(e){
|
| 339 |
+
controls.paused = !controls.paused;
|
| 340 |
+
});
|
| 341 |
+
|
| 342 |
+
pauseButton.addEventListener('click', function(e){
|
| 343 |
+
controls.paused = !controls.paused;
|
| 344 |
+
});
|
| 345 |
+
|
| 346 |
+
function initCanvas() {
|
| 347 |
+
app = new Application({
|
| 348 |
+
width: nodeCanvas.offsetWidth,
|
| 349 |
+
height: nodeCanvas.offsetHeight,
|
| 350 |
+
transparent: false,
|
| 351 |
+
backgroundColor: BACKGROUND_COLOR
|
| 352 |
+
});
|
| 353 |
+
|
| 354 |
+
nodeCanvas.appendChild(app.view);
|
| 355 |
+
app.view.classList.add("d-none");
|
| 356 |
+
|
| 357 |
+
renderer = app.renderer;
|
| 358 |
+
renderer.interactive = true;
|
| 359 |
+
renderer.autoResize = true;
|
| 360 |
+
|
| 361 |
+
renderer.resize(nodeCanvas.offsetWidth, nodeCanvas.offsetHeight);
|
| 362 |
+
app.ticker.add(run);
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
function showCanvas() {
|
| 366 |
+
document.getElementById("spinner").classList.add("d-none");
|
| 367 |
+
app.view.classList.remove("d-none");
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
function hideCanvas() {
|
| 371 |
+
document.getElementById("spinner").classList.remove("d-none");
|
| 372 |
+
app.view.classList.add("d-none");
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
function drawRoadnet() {
|
| 376 |
+
if (simulatorContainer) {
|
| 377 |
+
simulatorContainer.destroy(true);
|
| 378 |
+
}
|
| 379 |
+
app.stage.removeChildren();
|
| 380 |
+
viewport = new Viewport.Viewport({
|
| 381 |
+
screenWidth: window.innerWidth,
|
| 382 |
+
screenHeight: window.innerHeight,
|
| 383 |
+
interaction: app.renderer.plugins.interaction
|
| 384 |
+
});
|
| 385 |
+
viewport
|
| 386 |
+
.drag()
|
| 387 |
+
.pinch()
|
| 388 |
+
.wheel()
|
| 389 |
+
.decelerate();
|
| 390 |
+
app.stage.addChild(viewport);
|
| 391 |
+
simulatorContainer = new Container();
|
| 392 |
+
viewport.addChild(simulatorContainer);
|
| 393 |
+
|
| 394 |
+
roadnet = simulation.static;
|
| 395 |
+
nodes = [];
|
| 396 |
+
edges = [];
|
| 397 |
+
trafficLightsG = {};
|
| 398 |
+
|
| 399 |
+
for (let i = 0, len = roadnet.nodes.length;i < len;++i) {
|
| 400 |
+
node = roadnet.nodes[i];
|
| 401 |
+
node.point = new Point(transCoord(node.point));
|
| 402 |
+
nodes[node.id] = node;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
for (let i = 0, len = roadnet.edges.length;i < len;++i) {
|
| 406 |
+
edge = roadnet.edges[i];
|
| 407 |
+
edge.from = nodes[edge.from];
|
| 408 |
+
edge.to = nodes[edge.to];
|
| 409 |
+
for (let j = 0, len = edge.points.length;j < len;++j) {
|
| 410 |
+
edge.points[j] = new Point(transCoord(edge.points[j]));
|
| 411 |
+
}
|
| 412 |
+
edges[edge.id] = edge;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
/**
|
| 416 |
+
* Draw Map
|
| 417 |
+
*/
|
| 418 |
+
trafficLightContainer = new ParticleContainer(MAX_TRAFFIC_LIGHT_NUM, {tint: true});
|
| 419 |
+
let mapContainer, mapGraphics;
|
| 420 |
+
if (debugMode) {
|
| 421 |
+
mapContainer = new Container();
|
| 422 |
+
simulatorContainer.addChild(mapContainer);
|
| 423 |
+
}else {
|
| 424 |
+
mapGraphics = new Graphics();
|
| 425 |
+
simulatorContainer.addChild(mapGraphics);
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
for (nodeId in nodes) {
|
| 429 |
+
if (!nodes[nodeId].virtual) {
|
| 430 |
+
let nodeGraphics;
|
| 431 |
+
if (debugMode) {
|
| 432 |
+
nodeGraphics = new Graphics();
|
| 433 |
+
mapContainer.addChild(nodeGraphics);
|
| 434 |
+
} else {
|
| 435 |
+
nodeGraphics = mapGraphics;
|
| 436 |
+
}
|
| 437 |
+
drawNode(nodes[nodeId], nodeGraphics);
|
| 438 |
+
}
|
| 439 |
+
}
|
| 440 |
+
for (edgeId in edges) {
|
| 441 |
+
let edgeGraphics;
|
| 442 |
+
if (debugMode) {
|
| 443 |
+
edgeGraphics = new Graphics();
|
| 444 |
+
mapContainer.addChild(edgeGraphics);
|
| 445 |
+
} else {
|
| 446 |
+
edgeGraphics = mapGraphics;
|
| 447 |
+
}
|
| 448 |
+
drawEdge(edges[edgeId], edgeGraphics);
|
| 449 |
+
}
|
| 450 |
+
drawOverlayLayers();
|
| 451 |
+
let bounds = simulatorContainer.getBounds();
|
| 452 |
+
simulatorContainer.pivot.set(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2);
|
| 453 |
+
simulatorContainer.position.set(renderer.width / 2, renderer.height / 2);
|
| 454 |
+
simulatorContainer.addChild(trafficLightContainer);
|
| 455 |
+
|
| 456 |
+
/**
|
| 457 |
+
* Settings for Cars
|
| 458 |
+
*/
|
| 459 |
+
TURN_SIGNAL_LENGTH = CAR_LENGTH;
|
| 460 |
+
TURN_SIGNAL_WIDTH = CAR_WIDTH / 2;
|
| 461 |
+
|
| 462 |
+
var carG = new Graphics();
|
| 463 |
+
carG.lineStyle(0);
|
| 464 |
+
carG.beginFill(0xFFFFFF, 0.8);
|
| 465 |
+
carG.drawRect(0, 0, CAR_LENGTH, CAR_WIDTH);
|
| 466 |
+
|
| 467 |
+
let carTexture = renderer.generateTexture(carG);
|
| 468 |
+
|
| 469 |
+
let signalG = new Graphics();
|
| 470 |
+
signalG.beginFill(TURN_SIGNAL_COLOR, 0.7).drawRect(0,0, TURN_SIGNAL_LENGTH, TURN_SIGNAL_WIDTH)
|
| 471 |
+
.drawRect(0, 3 * CAR_WIDTH - TURN_SIGNAL_WIDTH, TURN_SIGNAL_LENGTH, TURN_SIGNAL_WIDTH).endFill();
|
| 472 |
+
let turnSignalTexture = renderer.generateTexture(signalG);
|
| 473 |
+
|
| 474 |
+
let signalLeft = new Texture(turnSignalTexture, new Rectangle(0, 0, TURN_SIGNAL_LENGTH, CAR_WIDTH));
|
| 475 |
+
let signalStraight = new Texture(turnSignalTexture, new Rectangle(0, CAR_WIDTH, TURN_SIGNAL_LENGTH, CAR_WIDTH));
|
| 476 |
+
let signalRight = new Texture(turnSignalTexture, new Rectangle(0, CAR_WIDTH * 2, TURN_SIGNAL_LENGTH, CAR_WIDTH));
|
| 477 |
+
turnSignalTextures = [signalLeft, signalStraight, signalRight];
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
carPool = [];
|
| 481 |
+
if (debugMode)
|
| 482 |
+
carContainer = new Container();
|
| 483 |
+
else
|
| 484 |
+
carContainer = new ParticleContainer(NUM_CAR_POOL, {rotation: true, tint: true});
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
turnSignalContainer = new ParticleContainer(NUM_CAR_POOL, {rotation: true, tint: true});
|
| 488 |
+
simulatorContainer.addChild(carContainer);
|
| 489 |
+
simulatorContainer.addChild(turnSignalContainer);
|
| 490 |
+
for (let i = 0, len = NUM_CAR_POOL;i < len;++i) {
|
| 491 |
+
//var car = Sprite.fromImage("images/car.png")
|
| 492 |
+
let car = new Sprite(carTexture);
|
| 493 |
+
let signal = new Sprite(turnSignalTextures[1]);
|
| 494 |
+
car.anchor.set(1, 0.5);
|
| 495 |
+
|
| 496 |
+
if (debugMode) {
|
| 497 |
+
car.interactive = true;
|
| 498 |
+
car.on('mouseover', function () {
|
| 499 |
+
selectedDOM.innerText = car.name;
|
| 500 |
+
car.alpha = 0.8;
|
| 501 |
+
});
|
| 502 |
+
car.on('mouseout', function () {
|
| 503 |
+
// selectedDOM.innerText = "";
|
| 504 |
+
car.alpha = 1;
|
| 505 |
+
});
|
| 506 |
+
}
|
| 507 |
+
signal.anchor.set(1, 0.5);
|
| 508 |
+
carPool.push([car, signal]);
|
| 509 |
+
}
|
| 510 |
+
showCanvas();
|
| 511 |
+
|
| 512 |
+
return true;
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
function appendText(id, text) {
|
| 516 |
+
let p = document.createElement("span");
|
| 517 |
+
p.innerText = text;
|
| 518 |
+
document.getElementById("info").appendChild(p);
|
| 519 |
+
document.getElementById("info").appendChild(document.createElement("br"));
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
var statsFile = "";
|
| 523 |
+
var withRange = false;
|
| 524 |
+
var nodeStats, nodeRange;
|
| 525 |
+
|
| 526 |
+
initCanvas();
|
| 527 |
+
|
| 528 |
+
|
| 529 |
+
function transCoord(point) {
|
| 530 |
+
return [point[0], -point[1]];
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
function getDistrictColor(districtId) {
|
| 534 |
+
return DISTRICT_PALETTE[stringHash(districtId) % DISTRICT_PALETTE.length];
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
function cross2D(o, a, b) {
|
| 538 |
+
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
function convexHull(points) {
|
| 542 |
+
if (!points || points.length <= 2) return points ? points.slice() : [];
|
| 543 |
+
let sorted = points
|
| 544 |
+
.slice()
|
| 545 |
+
.sort((p, q) => (p.x === q.x ? p.y - q.y : p.x - q.x));
|
| 546 |
+
let lower = [];
|
| 547 |
+
for (let i = 0; i < sorted.length; ++i) {
|
| 548 |
+
while (lower.length >= 2 && cross2D(lower[lower.length - 2], lower[lower.length - 1], sorted[i]) <= 0) {
|
| 549 |
+
lower.pop();
|
| 550 |
+
}
|
| 551 |
+
lower.push(sorted[i]);
|
| 552 |
+
}
|
| 553 |
+
let upper = [];
|
| 554 |
+
for (let i = sorted.length - 1; i >= 0; --i) {
|
| 555 |
+
while (upper.length >= 2 && cross2D(upper[upper.length - 2], upper[upper.length - 1], sorted[i]) <= 0) {
|
| 556 |
+
upper.pop();
|
| 557 |
+
}
|
| 558 |
+
upper.push(sorted[i]);
|
| 559 |
+
}
|
| 560 |
+
lower.pop();
|
| 561 |
+
upper.pop();
|
| 562 |
+
return lower.concat(upper);
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
function drawDistrictRegionFills(graphics, districtToPoints) {
|
| 566 |
+
for (let districtId in districtToPoints) {
|
| 567 |
+
let pts = districtToPoints[districtId];
|
| 568 |
+
if (!pts || pts.length === 0) continue;
|
| 569 |
+
let color = getDistrictColor(districtId);
|
| 570 |
+
if (pts.length >= 3) {
|
| 571 |
+
let hull = convexHull(pts);
|
| 572 |
+
if (hull.length >= 3) {
|
| 573 |
+
graphics.lineStyle(1.2, color, 0.35);
|
| 574 |
+
graphics.beginFill(color, 0.11);
|
| 575 |
+
graphics.moveTo(hull[0].x, hull[0].y);
|
| 576 |
+
for (let i = 1; i < hull.length; ++i) {
|
| 577 |
+
graphics.lineTo(hull[i].x, hull[i].y);
|
| 578 |
+
}
|
| 579 |
+
graphics.lineTo(hull[0].x, hull[0].y);
|
| 580 |
+
graphics.endFill();
|
| 581 |
+
continue;
|
| 582 |
+
}
|
| 583 |
+
}
|
| 584 |
+
if (pts.length === 2) {
|
| 585 |
+
graphics.lineStyle(18, color, 0.10);
|
| 586 |
+
graphics.drawLine(pts[0], pts[1]);
|
| 587 |
+
continue;
|
| 588 |
+
}
|
| 589 |
+
graphics.lineStyle(0);
|
| 590 |
+
graphics.beginFill(color, 0.12);
|
| 591 |
+
graphics.drawCircle(pts[0].x, pts[0].y, 18);
|
| 592 |
+
graphics.endFill();
|
| 593 |
+
}
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
function drawEdgePolyline(graphics, edge, width, color, alpha) {
|
| 597 |
+
graphics.lineStyle(width, color, alpha);
|
| 598 |
+
for (let i = 1; i < edge.points.length; ++i) {
|
| 599 |
+
graphics.drawLine(edge.points[i - 1], edge.points[i]);
|
| 600 |
+
}
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
function drawOverlayLayers() {
|
| 604 |
+
if (overlayContainer) {
|
| 605 |
+
overlayContainer.destroy(true);
|
| 606 |
+
overlayContainer = null;
|
| 607 |
+
}
|
| 608 |
+
if (!showDistrictOverlay && !showGatewayOverlay) {
|
| 609 |
+
return;
|
| 610 |
+
}
|
| 611 |
+
overlayContainer = new Container();
|
| 612 |
+
simulatorContainer.addChild(overlayContainer);
|
| 613 |
+
|
| 614 |
+
let overlayGraphics = new Graphics();
|
| 615 |
+
overlayContainer.addChild(overlayGraphics);
|
| 616 |
+
|
| 617 |
+
let intersectionToDistrict = {};
|
| 618 |
+
if (districtMap && districtMap.intersection_to_district) {
|
| 619 |
+
intersectionToDistrict = districtMap.intersection_to_district;
|
| 620 |
+
}
|
| 621 |
+
let gatewayNodeIds = new Set(
|
| 622 |
+
districtMap && districtMap.gateway_intersections
|
| 623 |
+
? districtMap.gateway_intersections
|
| 624 |
+
: []
|
| 625 |
+
);
|
| 626 |
+
let gatewayEdgeIds = new Set(
|
| 627 |
+
districtMap && districtMap.gateway_roads
|
| 628 |
+
? districtMap.gateway_roads
|
| 629 |
+
: []
|
| 630 |
+
);
|
| 631 |
+
let nodeDegree = {};
|
| 632 |
+
for (let edgeId in edges) {
|
| 633 |
+
let edge = edges[edgeId];
|
| 634 |
+
nodeDegree[edge.from.id] = (nodeDegree[edge.from.id] || 0) + 1;
|
| 635 |
+
nodeDegree[edge.to.id] = (nodeDegree[edge.to.id] || 0) + 1;
|
| 636 |
+
}
|
| 637 |
+
for (let nodeId in nodes) {
|
| 638 |
+
if ((nodeDegree[nodeId] || 0) <= 1) {
|
| 639 |
+
gatewayNodeIds.add(nodeId);
|
| 640 |
+
}
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
if (showDistrictOverlay && districtMap && districtMap.intersection_to_district) {
|
| 644 |
+
let districtToPoints = {};
|
| 645 |
+
for (let nodeId in nodes) {
|
| 646 |
+
let districtId = intersectionToDistrict[nodeId];
|
| 647 |
+
if (!districtId) continue;
|
| 648 |
+
if (!districtToPoints[districtId]) districtToPoints[districtId] = [];
|
| 649 |
+
districtToPoints[districtId].push(nodes[nodeId].point);
|
| 650 |
+
}
|
| 651 |
+
drawDistrictRegionFills(overlayGraphics, districtToPoints);
|
| 652 |
+
|
| 653 |
+
for (let edgeId in edges) {
|
| 654 |
+
let edge = edges[edgeId];
|
| 655 |
+
let fromDistrict = intersectionToDistrict[edge.from.id];
|
| 656 |
+
let toDistrict = intersectionToDistrict[edge.to.id];
|
| 657 |
+
if (!fromDistrict || !toDistrict) continue;
|
| 658 |
+
if (fromDistrict === toDistrict) {
|
| 659 |
+
drawEdgePolyline(
|
| 660 |
+
overlayGraphics,
|
| 661 |
+
edge,
|
| 662 |
+
1.2,
|
| 663 |
+
getDistrictColor(fromDistrict),
|
| 664 |
+
0.45
|
| 665 |
+
);
|
| 666 |
+
} else {
|
| 667 |
+
drawEdgePolyline(
|
| 668 |
+
overlayGraphics,
|
| 669 |
+
edge,
|
| 670 |
+
1.8,
|
| 671 |
+
DISTRICT_BORDER_COLOR,
|
| 672 |
+
0.8
|
| 673 |
+
);
|
| 674 |
+
}
|
| 675 |
+
}
|
| 676 |
+
for (let nodeId in nodes) {
|
| 677 |
+
let districtId = intersectionToDistrict[nodeId];
|
| 678 |
+
if (!districtId) continue;
|
| 679 |
+
let point = nodes[nodeId].point;
|
| 680 |
+
overlayGraphics.beginFill(getDistrictColor(districtId), 0.55);
|
| 681 |
+
overlayGraphics.drawCircle(point.x, point.y, 2.0);
|
| 682 |
+
overlayGraphics.endFill();
|
| 683 |
+
}
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
if (showGatewayOverlay) {
|
| 687 |
+
for (let edgeId in edges) {
|
| 688 |
+
let edge = edges[edgeId];
|
| 689 |
+
let fromGateway = gatewayNodeIds.has(edge.from.id) || String(edge.from.id).indexOf("g_") === 0;
|
| 690 |
+
let toGateway = gatewayNodeIds.has(edge.to.id) || String(edge.to.id).indexOf("g_") === 0;
|
| 691 |
+
let edgeLooksGateway = edgeId.indexOf("_g_") >= 0 || edgeId.indexOf("r_g_") === 0;
|
| 692 |
+
if (
|
| 693 |
+
gatewayEdgeIds.has(edgeId)
|
| 694 |
+
|| fromGateway
|
| 695 |
+
|| toGateway
|
| 696 |
+
|| edgeLooksGateway
|
| 697 |
+
) {
|
| 698 |
+
drawEdgePolyline(
|
| 699 |
+
overlayGraphics,
|
| 700 |
+
edge,
|
| 701 |
+
5.6,
|
| 702 |
+
GATEWAY_EDGE_COLOR,
|
| 703 |
+
0.82
|
| 704 |
+
);
|
| 705 |
+
}
|
| 706 |
+
}
|
| 707 |
+
for (let nodeId in nodes) {
|
| 708 |
+
if (gatewayNodeIds.has(nodeId) || String(nodeId).indexOf("g_") === 0) {
|
| 709 |
+
let point = nodes[nodeId].point;
|
| 710 |
+
overlayGraphics.lineStyle(0);
|
| 711 |
+
overlayGraphics.beginFill(GATEWAY_NODE_COLOR, 0.28);
|
| 712 |
+
overlayGraphics.drawCircle(point.x, point.y, 34.0);
|
| 713 |
+
overlayGraphics.endFill();
|
| 714 |
+
overlayGraphics.lineStyle(2.2, 0x1f2d3d, 0.98);
|
| 715 |
+
overlayGraphics.beginFill(GATEWAY_NODE_COLOR, 0.96);
|
| 716 |
+
overlayGraphics.drawCircle(point.x, point.y, 13.0);
|
| 717 |
+
overlayGraphics.endFill();
|
| 718 |
+
overlayGraphics.lineStyle(0);
|
| 719 |
+
overlayGraphics.beginFill(0xffffff, 0.92);
|
| 720 |
+
overlayGraphics.drawCircle(point.x, point.y, 5.2);
|
| 721 |
+
overlayGraphics.endFill();
|
| 722 |
+
}
|
| 723 |
+
}
|
| 724 |
+
}
|
| 725 |
+
}
|
| 726 |
+
|
| 727 |
+
PIXI.Graphics.prototype.drawLine = function(pointA, pointB) {
|
| 728 |
+
this.moveTo(pointA.x, pointA.y);
|
| 729 |
+
this.lineTo(pointB.x, pointB.y);
|
| 730 |
+
}
|
| 731 |
+
|
| 732 |
+
PIXI.Graphics.prototype.drawDashLine = function(pointA, pointB, dash = 16, gap = 8) {
|
| 733 |
+
let direct = pointA.directTo(pointB);
|
| 734 |
+
let distance = pointA.distanceTo(pointB);
|
| 735 |
+
|
| 736 |
+
let currentPoint = pointA;
|
| 737 |
+
let currentDistance = 0;
|
| 738 |
+
let length;
|
| 739 |
+
let finish = false;
|
| 740 |
+
while (true) {
|
| 741 |
+
this.moveTo(currentPoint.x, currentPoint.y);
|
| 742 |
+
if (currentDistance + dash >= distance) {
|
| 743 |
+
length = distance - currentDistance;
|
| 744 |
+
finish = true;
|
| 745 |
+
} else {
|
| 746 |
+
length = dash
|
| 747 |
+
}
|
| 748 |
+
currentPoint = currentPoint.moveAlong(direct, length);
|
| 749 |
+
this.lineTo(currentPoint.x, currentPoint.y);
|
| 750 |
+
if (finish) break;
|
| 751 |
+
currentDistance += length;
|
| 752 |
+
|
| 753 |
+
if (currentDistance + gap >= distance) {
|
| 754 |
+
break;
|
| 755 |
+
} else {
|
| 756 |
+
currentPoint = currentPoint.moveAlong(direct, gap);
|
| 757 |
+
currentDistance += gap;
|
| 758 |
+
}
|
| 759 |
+
}
|
| 760 |
+
};
|
| 761 |
+
|
| 762 |
+
function drawNode(node, graphics) {
|
| 763 |
+
graphics.beginFill(LANE_COLOR);
|
| 764 |
+
let outline = node.outline;
|
| 765 |
+
for (let i = 0 ; i < outline.length ; i+=2) {
|
| 766 |
+
outline[i+1] = -outline[i+1];
|
| 767 |
+
if (i == 0)
|
| 768 |
+
graphics.moveTo(outline[i], outline[i+1]);
|
| 769 |
+
else
|
| 770 |
+
graphics.lineTo(outline[i], outline[i+1]);
|
| 771 |
+
}
|
| 772 |
+
graphics.endFill();
|
| 773 |
+
|
| 774 |
+
if (debugMode) {
|
| 775 |
+
graphics.hitArea = new PIXI.Polygon(outline);
|
| 776 |
+
graphics.interactive = true;
|
| 777 |
+
graphics.on("mouseover", function () {
|
| 778 |
+
selectedDOM.innerText = node.id;
|
| 779 |
+
graphics.alpha = 0.5;
|
| 780 |
+
});
|
| 781 |
+
graphics.on("mouseout", function () {
|
| 782 |
+
graphics.alpha = 1;
|
| 783 |
+
});
|
| 784 |
+
}
|
| 785 |
+
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
function drawEdge(edge, graphics) {
|
| 789 |
+
let from = edge.from;
|
| 790 |
+
let to = edge.to;
|
| 791 |
+
let points = edge.points;
|
| 792 |
+
|
| 793 |
+
let pointA, pointAOffset, pointB, pointBOffset;
|
| 794 |
+
let prevPointBOffset = null;
|
| 795 |
+
|
| 796 |
+
let roadWidth = 0;
|
| 797 |
+
edge.laneWidths.forEach(function(l){
|
| 798 |
+
roadWidth += l;
|
| 799 |
+
}, 0);
|
| 800 |
+
|
| 801 |
+
let coords = [], coords1 = [];
|
| 802 |
+
|
| 803 |
+
for (let i = 1;i < points.length;++i) {
|
| 804 |
+
if (i == 1){
|
| 805 |
+
pointA = points[0].moveAlongDirectTo(points[1], from.virtual ? 0 : from.width);
|
| 806 |
+
pointAOffset = points[0].directTo(points[1]).rotate(ROTATE);
|
| 807 |
+
} else {
|
| 808 |
+
pointA = points[i-1];
|
| 809 |
+
pointAOffset = prevPointBOffset;
|
| 810 |
+
}
|
| 811 |
+
if (i == points.length - 1) {
|
| 812 |
+
pointB = points[i].moveAlongDirectTo(points[i-1], to.virtual ? 0 : to.width);
|
| 813 |
+
pointBOffset = points[i-1].directTo(points[i]).rotate(ROTATE);
|
| 814 |
+
} else {
|
| 815 |
+
pointB = points[i];
|
| 816 |
+
pointBOffset = points[i-1].directTo(points[i+1]).rotate(ROTATE);
|
| 817 |
+
}
|
| 818 |
+
prevPointBOffset = pointBOffset;
|
| 819 |
+
|
| 820 |
+
lightG = new Graphics();
|
| 821 |
+
lightG.lineStyle(TRAFFIC_LIGHT_WIDTH, 0xFFFFFF);
|
| 822 |
+
lightG.drawLine(new Point(0, 0), new Point(1, 0));
|
| 823 |
+
lightTexture = renderer.generateTexture(lightG);
|
| 824 |
+
|
| 825 |
+
// Draw Traffic Lights
|
| 826 |
+
if (i == points.length-1 && !to.virtual) {
|
| 827 |
+
edgeTrafficLights = [];
|
| 828 |
+
prevOffset = offset = 0;
|
| 829 |
+
for (lane = 0;lane < edge.nLane;++lane) {
|
| 830 |
+
offset += edge.laneWidths[lane];
|
| 831 |
+
var light = new Sprite(lightTexture);
|
| 832 |
+
light.anchor.set(0, 0.5);
|
| 833 |
+
light.scale.set(offset - prevOffset, 1);
|
| 834 |
+
point_ = pointB.moveAlong(pointBOffset, prevOffset);
|
| 835 |
+
light.position.set(point_.x, point_.y);
|
| 836 |
+
light.rotation = pointBOffset.getAngleInRadians();
|
| 837 |
+
edgeTrafficLights.push(light);
|
| 838 |
+
prevOffset = offset;
|
| 839 |
+
trafficLightContainer.addChild(light);
|
| 840 |
+
}
|
| 841 |
+
trafficLightsG[edge.id] = edgeTrafficLights;
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
// Draw Roads
|
| 845 |
+
graphics.lineStyle(LANE_BORDER_WIDTH, LANE_BORDER_COLOR, 1);
|
| 846 |
+
graphics.drawLine(pointA, pointB);
|
| 847 |
+
|
| 848 |
+
pointA1 = pointA.moveAlong(pointAOffset, roadWidth);
|
| 849 |
+
pointB1 = pointB.moveAlong(pointBOffset, roadWidth);
|
| 850 |
+
|
| 851 |
+
graphics.lineStyle(0);
|
| 852 |
+
graphics.beginFill(LANE_COLOR);
|
| 853 |
+
|
| 854 |
+
coords = coords.concat([pointA.x, pointA.y, pointB.x, pointB.y]);
|
| 855 |
+
coords1 = coords1.concat([pointA1.y, pointA1.x, pointB1.y, pointB1.x]);
|
| 856 |
+
|
| 857 |
+
graphics.drawPolygon([pointA.x, pointA.y, pointB.x, pointB.y, pointB1.x, pointB1.y, pointA1.x, pointA1.y]);
|
| 858 |
+
graphics.endFill();
|
| 859 |
+
|
| 860 |
+
offset = 0;
|
| 861 |
+
for (let lane = 0, len = edge.nLane-1;lane < len;++lane) {
|
| 862 |
+
offset += edge.laneWidths[lane];
|
| 863 |
+
graphics.lineStyle(LANE_BORDER_WIDTH, LANE_INNER_COLOR);
|
| 864 |
+
graphics.drawDashLine(pointA.moveAlong(pointAOffset, offset), pointB.moveAlong(pointBOffset, offset), LANE_DASH, LANE_GAP);
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
offset += edge.laneWidths[edge.nLane-1];
|
| 868 |
+
|
| 869 |
+
// graphics.lineStyle(LANE_BORDER_WIDTH, LANE_BORDER_COLOR);
|
| 870 |
+
// graphics.drawLine(pointA.moveAlong(pointAOffset, offset), pointB.moveAlong(pointBOffset, offset));
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
if (debugMode) {
|
| 874 |
+
coords = coords.concat(coords1.reverse());
|
| 875 |
+
graphics.interactive = true;
|
| 876 |
+
graphics.hitArea = new PIXI.Polygon(coords);
|
| 877 |
+
graphics.on("mouseover", function () {
|
| 878 |
+
graphics.alpha = 0.5;
|
| 879 |
+
selectedDOM.innerText = edge.id;
|
| 880 |
+
});
|
| 881 |
+
|
| 882 |
+
graphics.on("mouseout", function () {
|
| 883 |
+
graphics.alpha = 1;
|
| 884 |
+
});
|
| 885 |
+
}
|
| 886 |
+
}
|
| 887 |
+
|
| 888 |
+
function run(delta) {
|
| 889 |
+
let redraw = false;
|
| 890 |
+
|
| 891 |
+
if (ready && (!controls.paused || redraw)) {
|
| 892 |
+
try {
|
| 893 |
+
drawStep(cnt);
|
| 894 |
+
}catch (e) {
|
| 895 |
+
infoAppend("Error occurred when drawing");
|
| 896 |
+
ready = false;
|
| 897 |
+
}
|
| 898 |
+
if (!controls.paused) {
|
| 899 |
+
frameElapsed += 1;
|
| 900 |
+
if (frameElapsed >= 1 / controls.replaySpeed ** 2) {
|
| 901 |
+
cnt += 1;
|
| 902 |
+
frameElapsed = 0;
|
| 903 |
+
if (cnt == totalStep) cnt = 0;
|
| 904 |
+
}
|
| 905 |
+
}
|
| 906 |
+
}
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
function _statusToColor(status) {
|
| 910 |
+
switch (status) {
|
| 911 |
+
case 'r':
|
| 912 |
+
return LIGHT_RED;
|
| 913 |
+
case 'g':
|
| 914 |
+
return LIGHT_GREEN;
|
| 915 |
+
default:
|
| 916 |
+
return 0x808080;
|
| 917 |
+
}
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
function stringHash(str) {
|
| 921 |
+
let hash = 0;
|
| 922 |
+
let p = 127, p_pow = 1;
|
| 923 |
+
let m = 1e9 + 9;
|
| 924 |
+
for (let i = 0; i < str.length; i++) {
|
| 925 |
+
hash = (hash + str.charCodeAt(i) * p_pow) % m;
|
| 926 |
+
p_pow = (p_pow * p) % m;
|
| 927 |
+
}
|
| 928 |
+
return hash;
|
| 929 |
+
}
|
| 930 |
+
|
| 931 |
+
function drawStep(step) {
|
| 932 |
+
if (showChart && (step > chart.ptr || step == 0)) {
|
| 933 |
+
if (step == 0) {
|
| 934 |
+
chart.clear();
|
| 935 |
+
}
|
| 936 |
+
chart.ptr = step;
|
| 937 |
+
chart.addData(chartLog[step]);
|
| 938 |
+
}
|
| 939 |
+
|
| 940 |
+
let [carLogs, tlLogs] = logs[step].split(';');
|
| 941 |
+
|
| 942 |
+
tlLogs = tlLogs.split(',');
|
| 943 |
+
carLogs = carLogs.split(',');
|
| 944 |
+
|
| 945 |
+
let tlLog, tlEdge, tlStatus;
|
| 946 |
+
for (let i = 0, len = tlLogs.length;i < len;++i) {
|
| 947 |
+
tlLog = tlLogs[i].split(' ');
|
| 948 |
+
tlEdge = tlLog[0];
|
| 949 |
+
tlStatus = tlLog.slice(1);
|
| 950 |
+
for (let j = 0, len = tlStatus.length;j < len;++j) {
|
| 951 |
+
trafficLightsG[tlEdge][j].tint = _statusToColor(tlStatus[j]);
|
| 952 |
+
if (tlStatus[j] == 'i' ) {
|
| 953 |
+
trafficLightsG[tlEdge][j].alpha = 0;
|
| 954 |
+
}else{
|
| 955 |
+
trafficLightsG[tlEdge][j].alpha = 1;
|
| 956 |
+
}
|
| 957 |
+
}
|
| 958 |
+
}
|
| 959 |
+
|
| 960 |
+
carContainer.removeChildren();
|
| 961 |
+
turnSignalContainer.removeChildren();
|
| 962 |
+
let carLog, position, length, width;
|
| 963 |
+
for (let i = 0, len = carLogs.length - 1;i < len;++i) {
|
| 964 |
+
carLog = carLogs[i].split(' ');
|
| 965 |
+
position = transCoord([parseFloat(carLog[0]), parseFloat(carLog[1])]);
|
| 966 |
+
length = parseFloat(carLog[5]);
|
| 967 |
+
width = parseFloat(carLog[6]);
|
| 968 |
+
carPool[i][0].position.set(position[0], position[1]);
|
| 969 |
+
carPool[i][0].rotation = 2*Math.PI - parseFloat(carLog[2]);
|
| 970 |
+
carPool[i][0].name = carLog[3];
|
| 971 |
+
let carColorId = stringHash(carLog[3]) % CAR_COLORS_NUM;
|
| 972 |
+
carPool[i][0].tint = CAR_COLORS[carColorId];
|
| 973 |
+
carPool[i][0].width = length;
|
| 974 |
+
carPool[i][0].height = width;
|
| 975 |
+
carContainer.addChild(carPool[i][0]);
|
| 976 |
+
|
| 977 |
+
let laneChange = parseInt(carLog[4]) + 1;
|
| 978 |
+
carPool[i][1].position.set(position[0], position[1]);
|
| 979 |
+
carPool[i][1].rotation = carPool[i][0].rotation;
|
| 980 |
+
carPool[i][1].texture = turnSignalTextures[laneChange];
|
| 981 |
+
carPool[i][1].width = length;
|
| 982 |
+
carPool[i][1].height = width;
|
| 983 |
+
turnSignalContainer.addChild(carPool[i][1]);
|
| 984 |
+
}
|
| 985 |
+
nodeCarNum.innerText = carLogs.length-1;
|
| 986 |
+
nodeTotalStep.innerText = totalStep;
|
| 987 |
+
nodeCurrentStep.innerText = cnt+1;
|
| 988 |
+
nodeProgressPercentage.innerText = (cnt / totalStep * 100).toFixed(2) + "%";
|
| 989 |
+
if (statsFile != "") {
|
| 990 |
+
if (withRange) nodeRange.value = stats[step][1];
|
| 991 |
+
nodeStats.innerText = stats[step][0].toFixed(2);
|
| 992 |
+
}
|
| 993 |
+
}
|
| 994 |
+
|
| 995 |
+
/*
|
| 996 |
+
Chart
|
| 997 |
+
*/
|
| 998 |
+
let chart = {
|
| 999 |
+
max_steps: 3600,
|
| 1000 |
+
data: {
|
| 1001 |
+
labels: [],
|
| 1002 |
+
series: [[]]
|
| 1003 |
+
},
|
| 1004 |
+
options: {
|
| 1005 |
+
showPoint: false,
|
| 1006 |
+
lineSmooth: false,
|
| 1007 |
+
axisX: {
|
| 1008 |
+
showGrid: false,
|
| 1009 |
+
showLabel: false
|
| 1010 |
+
}
|
| 1011 |
+
},
|
| 1012 |
+
init : function(title, series_cnt, max_step){
|
| 1013 |
+
document.getElementById("chart-title").innerText = title;
|
| 1014 |
+
this.max_steps = max_step;
|
| 1015 |
+
this.data.labels = new Array(this.max_steps);
|
| 1016 |
+
this.data.series = [];
|
| 1017 |
+
for (let i = 0 ; i < series_cnt ; ++i)
|
| 1018 |
+
this.data.series.push([]);
|
| 1019 |
+
this.chart = new Chartist.Line('#chart', this.data, this.options);
|
| 1020 |
+
},
|
| 1021 |
+
addData: function (value) {
|
| 1022 |
+
for (let i = 0 ; i < value.length; ++i) {
|
| 1023 |
+
this.data.series[i].push(value[i]);
|
| 1024 |
+
if (this.data.series[i].length > this.max_steps) {
|
| 1025 |
+
this.data.series[i].shift();
|
| 1026 |
+
}
|
| 1027 |
+
}
|
| 1028 |
+
this.chart.update();
|
| 1029 |
+
},
|
| 1030 |
+
clear: function() {
|
| 1031 |
+
for (let i = 0 ; i < this.data.series.length ; ++i)
|
| 1032 |
+
this.data.series[i] = [];
|
| 1033 |
+
},
|
| 1034 |
+
ptr: 0
|
| 1035 |
+
};
|
third_party/CityFlow/frontend/script_multi.js
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
third_party/CityFlow/frontend/spinner.css
CHANGED
|
@@ -1,53 +1,53 @@
|
|
| 1 |
-
/* https://tobiasahlin.com/spinkit/ */
|
| 2 |
-
.spinner {
|
| 3 |
-
margin: 100px auto;
|
| 4 |
-
width: 50px;
|
| 5 |
-
height: 40px;
|
| 6 |
-
text-align: center;
|
| 7 |
-
font-size: 10px;
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
.spinner > div {
|
| 11 |
-
background-color: #333;
|
| 12 |
-
height: 100%;
|
| 13 |
-
width: 6px;
|
| 14 |
-
display: inline-block;
|
| 15 |
-
|
| 16 |
-
-webkit-animation: sk-stretchdelay 1.2s infinite ease-in-out;
|
| 17 |
-
animation: sk-stretchdelay 1.2s infinite ease-in-out;
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
.spinner .rect2 {
|
| 21 |
-
-webkit-animation-delay: -1.1s;
|
| 22 |
-
animation-delay: -1.1s;
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
.spinner .rect3 {
|
| 26 |
-
-webkit-animation-delay: -1.0s;
|
| 27 |
-
animation-delay: -1.0s;
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
.spinner .rect4 {
|
| 31 |
-
-webkit-animation-delay: -0.9s;
|
| 32 |
-
animation-delay: -0.9s;
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
.spinner .rect5 {
|
| 36 |
-
-webkit-animation-delay: -0.8s;
|
| 37 |
-
animation-delay: -0.8s;
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
@-webkit-keyframes sk-stretchdelay {
|
| 41 |
-
0%, 40%, 100% { -webkit-transform: scaleY(0.4) }
|
| 42 |
-
20% { -webkit-transform: scaleY(1.0) }
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
@keyframes sk-stretchdelay {
|
| 46 |
-
0%, 40%, 100% {
|
| 47 |
-
transform: scaleY(0.4);
|
| 48 |
-
-webkit-transform: scaleY(0.4);
|
| 49 |
-
} 20% {
|
| 50 |
-
transform: scaleY(1.0);
|
| 51 |
-
-webkit-transform: scaleY(1.0);
|
| 52 |
-
}
|
| 53 |
}
|
|
|
|
| 1 |
+
/* https://tobiasahlin.com/spinkit/ */
|
| 2 |
+
.spinner {
|
| 3 |
+
margin: 100px auto;
|
| 4 |
+
width: 50px;
|
| 5 |
+
height: 40px;
|
| 6 |
+
text-align: center;
|
| 7 |
+
font-size: 10px;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
.spinner > div {
|
| 11 |
+
background-color: #333;
|
| 12 |
+
height: 100%;
|
| 13 |
+
width: 6px;
|
| 14 |
+
display: inline-block;
|
| 15 |
+
|
| 16 |
+
-webkit-animation: sk-stretchdelay 1.2s infinite ease-in-out;
|
| 17 |
+
animation: sk-stretchdelay 1.2s infinite ease-in-out;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
.spinner .rect2 {
|
| 21 |
+
-webkit-animation-delay: -1.1s;
|
| 22 |
+
animation-delay: -1.1s;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
.spinner .rect3 {
|
| 26 |
+
-webkit-animation-delay: -1.0s;
|
| 27 |
+
animation-delay: -1.0s;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.spinner .rect4 {
|
| 31 |
+
-webkit-animation-delay: -0.9s;
|
| 32 |
+
animation-delay: -0.9s;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.spinner .rect5 {
|
| 36 |
+
-webkit-animation-delay: -0.8s;
|
| 37 |
+
animation-delay: -0.8s;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
@-webkit-keyframes sk-stretchdelay {
|
| 41 |
+
0%, 40%, 100% { -webkit-transform: scaleY(0.4) }
|
| 42 |
+
20% { -webkit-transform: scaleY(1.0) }
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
@keyframes sk-stretchdelay {
|
| 46 |
+
0%, 40%, 100% {
|
| 47 |
+
transform: scaleY(0.4);
|
| 48 |
+
-webkit-transform: scaleY(0.4);
|
| 49 |
+
} 20% {
|
| 50 |
+
transform: scaleY(1.0);
|
| 51 |
+
-webkit-transform: scaleY(1.0);
|
| 52 |
+
}
|
| 53 |
}
|
third_party/CityFlow/frontend/style.css
CHANGED
|
@@ -1,64 +1,64 @@
|
|
| 1 |
-
html, body {
|
| 2 |
-
margin: 0;
|
| 3 |
-
padding: 0;
|
| 4 |
-
height: 100%;
|
| 5 |
-
width: 100%;
|
| 6 |
-
overflow: hidden;
|
| 7 |
-
}
|
| 8 |
-
|
| 9 |
-
.noselect {
|
| 10 |
-
-webkit-touch-callout: none;
|
| 11 |
-
-webkit-user-select: none;
|
| 12 |
-
-khtml-user-select: none;
|
| 13 |
-
-moz-user-select: none;
|
| 14 |
-
-ms-user-select: none;
|
| 15 |
-
user-select: none;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
#simulator-canvas {
|
| 19 |
-
marigin: 0;
|
| 20 |
-
padding: 0;
|
| 21 |
-
height: 100%;
|
| 22 |
-
width: 100%;
|
| 23 |
-
overflow: hidden;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
.status-panel {
|
| 27 |
-
z-index: 3;
|
| 28 |
-
height: 100vh;
|
| 29 |
-
overflow-y: auto;
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
#info {
|
| 33 |
-
max-height: 100%;
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
#chart {
|
| 37 |
-
height: 100%;
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
#chart .ct-line {
|
| 41 |
-
stroke-width: 1px;
|
| 42 |
-
stroke-opacity: 0.8;
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
#chart .ct-label {
|
| 46 |
-
font-weight: bold;
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
#chart-container {
|
| 50 |
-
background-color: rgba(216, 220, 225, 0.8);
|
| 51 |
-
position: absolute;
|
| 52 |
-
bottom: 0;
|
| 53 |
-
right: 0;
|
| 54 |
-
left: 0;
|
| 55 |
-
box-sizing: border-box;
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
#chart-title {
|
| 59 |
-
font-weight: bold;
|
| 60 |
-
font-size: large;
|
| 61 |
-
color: #6c757d;
|
| 62 |
-
width: 100%;
|
| 63 |
-
text-align: center;
|
| 64 |
}
|
|
|
|
| 1 |
+
html, body {
|
| 2 |
+
margin: 0;
|
| 3 |
+
padding: 0;
|
| 4 |
+
height: 100%;
|
| 5 |
+
width: 100%;
|
| 6 |
+
overflow: hidden;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
.noselect {
|
| 10 |
+
-webkit-touch-callout: none;
|
| 11 |
+
-webkit-user-select: none;
|
| 12 |
+
-khtml-user-select: none;
|
| 13 |
+
-moz-user-select: none;
|
| 14 |
+
-ms-user-select: none;
|
| 15 |
+
user-select: none;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
#simulator-canvas {
|
| 19 |
+
marigin: 0;
|
| 20 |
+
padding: 0;
|
| 21 |
+
height: 100%;
|
| 22 |
+
width: 100%;
|
| 23 |
+
overflow: hidden;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.status-panel {
|
| 27 |
+
z-index: 3;
|
| 28 |
+
height: 100vh;
|
| 29 |
+
overflow-y: auto;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
#info {
|
| 33 |
+
max-height: 100%;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
#chart {
|
| 37 |
+
height: 100%;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
#chart .ct-line {
|
| 41 |
+
stroke-width: 1px;
|
| 42 |
+
stroke-opacity: 0.8;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
#chart .ct-label {
|
| 46 |
+
font-weight: bold;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
#chart-container {
|
| 50 |
+
background-color: rgba(216, 220, 225, 0.8);
|
| 51 |
+
position: absolute;
|
| 52 |
+
bottom: 0;
|
| 53 |
+
right: 0;
|
| 54 |
+
left: 0;
|
| 55 |
+
box-sizing: border-box;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
#chart-title {
|
| 59 |
+
font-weight: bold;
|
| 60 |
+
font-size: large;
|
| 61 |
+
color: #6c757d;
|
| 62 |
+
width: 100%;
|
| 63 |
+
text-align: center;
|
| 64 |
}
|
third_party/CityFlow/frontend/style_multi.css
CHANGED
|
@@ -1,747 +1,747 @@
|
|
| 1 |
-
/* =========================================================
|
| 2 |
-
Traffic Visualizer – Multi-Panel Dashboard Styles
|
| 3 |
-
========================================================= */
|
| 4 |
-
|
| 5 |
-
*, *::before, *::after { box-sizing: border-box; }
|
| 6 |
-
|
| 7 |
-
body {
|
| 8 |
-
margin: 0;
|
| 9 |
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
| 10 |
-
background: #1a1d23;
|
| 11 |
-
color: #d4d8e0;
|
| 12 |
-
height: 100vh;
|
| 13 |
-
overflow: hidden;
|
| 14 |
-
display: flex;
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
/* ── Sidebar ──────────────────────────────────────────── */
|
| 18 |
-
#sidebar {
|
| 19 |
-
width: 270px;
|
| 20 |
-
min-width: 180px;
|
| 21 |
-
background: #22252e;
|
| 22 |
-
border-right: 1px solid #333744;
|
| 23 |
-
display: flex;
|
| 24 |
-
flex-direction: column;
|
| 25 |
-
overflow-y: auto;
|
| 26 |
-
padding: 16px;
|
| 27 |
-
gap: 16px;
|
| 28 |
-
z-index: 10;
|
| 29 |
-
scrollbar-width: thin;
|
| 30 |
-
scrollbar-color: #3a4055 transparent;
|
| 31 |
-
}
|
| 32 |
-
#sidebar::-webkit-scrollbar { width: 5px; }
|
| 33 |
-
#sidebar::-webkit-scrollbar-track { background: transparent; }
|
| 34 |
-
#sidebar::-webkit-scrollbar-thumb { background: #3a4055; border-radius: 3px; }
|
| 35 |
-
#sidebar::-webkit-scrollbar-thumb:hover { background: #5b6cf9; }
|
| 36 |
-
|
| 37 |
-
#sidebar h1 {
|
| 38 |
-
font-size: 18px;
|
| 39 |
-
font-weight: 700;
|
| 40 |
-
color: #fff;
|
| 41 |
-
margin: 0 0 4px 0;
|
| 42 |
-
letter-spacing: 0.5px;
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
#sidebar .subtitle {
|
| 46 |
-
font-size: 11px;
|
| 47 |
-
color: #777e90;
|
| 48 |
-
margin: 0;
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
.sidebar-section {
|
| 52 |
-
background: #1a1d23;
|
| 53 |
-
border-radius: 8px;
|
| 54 |
-
padding: 12px;
|
| 55 |
-
border: 1px solid #2e3240;
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
.sidebar-section h3 {
|
| 59 |
-
font-size: 11px;
|
| 60 |
-
font-weight: 600;
|
| 61 |
-
text-transform: uppercase;
|
| 62 |
-
letter-spacing: 1px;
|
| 63 |
-
color: #777e90;
|
| 64 |
-
margin: 0 0 10px 0;
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
/* File upload zone */
|
| 68 |
-
#roadnet-drop-zone {
|
| 69 |
-
border: 2px dashed #3a4055;
|
| 70 |
-
border-radius: 6px;
|
| 71 |
-
padding: 20px 12px;
|
| 72 |
-
text-align: center;
|
| 73 |
-
cursor: pointer;
|
| 74 |
-
transition: border-color 0.2s, background 0.2s;
|
| 75 |
-
font-size: 13px;
|
| 76 |
-
color: #777e90;
|
| 77 |
-
}
|
| 78 |
-
#roadnet-drop-zone:hover,
|
| 79 |
-
#roadnet-drop-zone.drag-over {
|
| 80 |
-
border-color: #5b6cf9;
|
| 81 |
-
background: rgba(91, 108, 249, 0.06);
|
| 82 |
-
color: #aab0c2;
|
| 83 |
-
}
|
| 84 |
-
#roadnet-drop-zone .drop-icon {
|
| 85 |
-
font-size: 24px;
|
| 86 |
-
margin-bottom: 6px;
|
| 87 |
-
display: block;
|
| 88 |
-
}
|
| 89 |
-
#roadnet-drop-zone .drop-filename {
|
| 90 |
-
font-size: 12px;
|
| 91 |
-
color: #5b6cf9;
|
| 92 |
-
font-weight: 600;
|
| 93 |
-
margin-top: 4px;
|
| 94 |
-
}
|
| 95 |
-
#roadnet-file-input { display: none; }
|
| 96 |
-
|
| 97 |
-
/* City/Scenario pickers */
|
| 98 |
-
.picker-row {
|
| 99 |
-
display: flex;
|
| 100 |
-
flex-direction: column;
|
| 101 |
-
gap: 6px;
|
| 102 |
-
}
|
| 103 |
-
.picker-row label {
|
| 104 |
-
font-size: 11px;
|
| 105 |
-
color: #777e90;
|
| 106 |
-
font-weight: 600;
|
| 107 |
-
text-transform: uppercase;
|
| 108 |
-
letter-spacing: 0.5px;
|
| 109 |
-
}
|
| 110 |
-
.picker-row select {
|
| 111 |
-
width: 100%;
|
| 112 |
-
background: #22252e;
|
| 113 |
-
border: 1px solid #3a4055;
|
| 114 |
-
color: #d4d8e0;
|
| 115 |
-
border-radius: 5px;
|
| 116 |
-
padding: 6px 8px;
|
| 117 |
-
font-size: 13px;
|
| 118 |
-
outline: none;
|
| 119 |
-
cursor: pointer;
|
| 120 |
-
}
|
| 121 |
-
.picker-row select:focus {
|
| 122 |
-
border-color: #5b6cf9;
|
| 123 |
-
}
|
| 124 |
-
.picker-row select:disabled {
|
| 125 |
-
opacity: 0.4;
|
| 126 |
-
cursor: not-allowed;
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
/* Policy checkboxes */
|
| 130 |
-
.policy-list {
|
| 131 |
-
display: flex;
|
| 132 |
-
flex-direction: column;
|
| 133 |
-
gap: 7px;
|
| 134 |
-
}
|
| 135 |
-
.policy-item {
|
| 136 |
-
display: flex;
|
| 137 |
-
align-items: center;
|
| 138 |
-
gap: 8px;
|
| 139 |
-
cursor: pointer;
|
| 140 |
-
user-select: none;
|
| 141 |
-
}
|
| 142 |
-
.policy-item input[type="checkbox"] {
|
| 143 |
-
accent-color: var(--policy-color);
|
| 144 |
-
width: 15px;
|
| 145 |
-
height: 15px;
|
| 146 |
-
cursor: pointer;
|
| 147 |
-
}
|
| 148 |
-
.policy-item.disabled {
|
| 149 |
-
opacity: 0.4;
|
| 150 |
-
cursor: not-allowed;
|
| 151 |
-
}
|
| 152 |
-
.policy-item.disabled input { cursor: not-allowed; }
|
| 153 |
-
|
| 154 |
-
.policy-badge {
|
| 155 |
-
display: inline-block;
|
| 156 |
-
width: 10px;
|
| 157 |
-
height: 10px;
|
| 158 |
-
border-radius: 50%;
|
| 159 |
-
flex-shrink: 0;
|
| 160 |
-
}
|
| 161 |
-
.policy-label {
|
| 162 |
-
font-size: 13px;
|
| 163 |
-
flex: 1;
|
| 164 |
-
}
|
| 165 |
-
.policy-tag {
|
| 166 |
-
font-size: 10px;
|
| 167 |
-
background: rgba(255,255,255,0.07);
|
| 168 |
-
color: #888;
|
| 169 |
-
border-radius: 3px;
|
| 170 |
-
padding: 1px 5px;
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
.recent-runs-list {
|
| 174 |
-
display: flex;
|
| 175 |
-
flex-direction: column;
|
| 176 |
-
gap: 8px;
|
| 177 |
-
}
|
| 178 |
-
.recent-run-empty {
|
| 179 |
-
font-size: 12px;
|
| 180 |
-
color: #777e90;
|
| 181 |
-
}
|
| 182 |
-
.recent-run-card {
|
| 183 |
-
border: 1px solid #323747;
|
| 184 |
-
border-radius: 7px;
|
| 185 |
-
background: #22252e;
|
| 186 |
-
padding: 9px 10px;
|
| 187 |
-
display: flex;
|
| 188 |
-
flex-direction: column;
|
| 189 |
-
gap: 6px;
|
| 190 |
-
}
|
| 191 |
-
.recent-run-top {
|
| 192 |
-
display: flex;
|
| 193 |
-
justify-content: space-between;
|
| 194 |
-
gap: 8px;
|
| 195 |
-
align-items: baseline;
|
| 196 |
-
}
|
| 197 |
-
.recent-run-title {
|
| 198 |
-
font-size: 12px;
|
| 199 |
-
font-weight: 700;
|
| 200 |
-
color: #e7ecf4;
|
| 201 |
-
}
|
| 202 |
-
.recent-run-time {
|
| 203 |
-
font-size: 10px;
|
| 204 |
-
color: #7f8a9d;
|
| 205 |
-
white-space: nowrap;
|
| 206 |
-
}
|
| 207 |
-
.recent-run-meta {
|
| 208 |
-
font-size: 11px;
|
| 209 |
-
color: #aab4c4;
|
| 210 |
-
}
|
| 211 |
-
.recent-run-policies {
|
| 212 |
-
display: flex;
|
| 213 |
-
flex-wrap: wrap;
|
| 214 |
-
gap: 4px;
|
| 215 |
-
}
|
| 216 |
-
.recent-run-chip {
|
| 217 |
-
font-size: 10px;
|
| 218 |
-
color: #dbe2ee;
|
| 219 |
-
background: rgba(255,255,255,0.08);
|
| 220 |
-
border: 1px solid rgba(255,255,255,0.08);
|
| 221 |
-
border-radius: 999px;
|
| 222 |
-
padding: 2px 6px;
|
| 223 |
-
}
|
| 224 |
-
|
| 225 |
-
.debug-log {
|
| 226 |
-
max-height: 180px;
|
| 227 |
-
overflow: auto;
|
| 228 |
-
padding: 10px;
|
| 229 |
-
border-radius: 8px;
|
| 230 |
-
background: #161a22;
|
| 231 |
-
border: 1px solid #2c3442;
|
| 232 |
-
color: #dbe4ff;
|
| 233 |
-
font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
| 234 |
-
white-space: pre-wrap;
|
| 235 |
-
}
|
| 236 |
-
.recent-run-actions {
|
| 237 |
-
display: flex;
|
| 238 |
-
gap: 6px;
|
| 239 |
-
}
|
| 240 |
-
.recent-run-btn {
|
| 241 |
-
flex: 1;
|
| 242 |
-
background: #2b3040;
|
| 243 |
-
border: 1px solid #40475d;
|
| 244 |
-
color: #dce3ef;
|
| 245 |
-
border-radius: 5px;
|
| 246 |
-
padding: 5px 7px;
|
| 247 |
-
font-size: 11px;
|
| 248 |
-
cursor: pointer;
|
| 249 |
-
}
|
| 250 |
-
.recent-run-btn:hover {
|
| 251 |
-
border-color: #5b6cf9;
|
| 252 |
-
}
|
| 253 |
-
|
| 254 |
-
/* View mode selector */
|
| 255 |
-
.view-mode-row {
|
| 256 |
-
display: flex;
|
| 257 |
-
gap: 6px;
|
| 258 |
-
}
|
| 259 |
-
.view-mode-btn {
|
| 260 |
-
flex: 1;
|
| 261 |
-
background: #22252e;
|
| 262 |
-
border: 1px solid #3a4055;
|
| 263 |
-
color: #777e90;
|
| 264 |
-
border-radius: 5px;
|
| 265 |
-
padding: 6px 0;
|
| 266 |
-
font-size: 12px;
|
| 267 |
-
font-weight: 600;
|
| 268 |
-
cursor: pointer;
|
| 269 |
-
transition: all 0.15s;
|
| 270 |
-
text-align: center;
|
| 271 |
-
}
|
| 272 |
-
.view-mode-btn:hover { border-color: #5b6cf9; color: #aab0c2; }
|
| 273 |
-
.view-mode-btn.active {
|
| 274 |
-
background: #5b6cf9;
|
| 275 |
-
border-color: #5b6cf9;
|
| 276 |
-
color: #fff;
|
| 277 |
-
}
|
| 278 |
-
|
| 279 |
-
/* Run button */
|
| 280 |
-
#run-btn {
|
| 281 |
-
width: 100%;
|
| 282 |
-
padding: 10px;
|
| 283 |
-
background: linear-gradient(135deg, #5b6cf9, #7c3aed);
|
| 284 |
-
color: #fff;
|
| 285 |
-
border: none;
|
| 286 |
-
border-radius: 6px;
|
| 287 |
-
font-size: 14px;
|
| 288 |
-
font-weight: 600;
|
| 289 |
-
cursor: pointer;
|
| 290 |
-
transition: opacity 0.2s, transform 0.1s;
|
| 291 |
-
letter-spacing: 0.3px;
|
| 292 |
-
}
|
| 293 |
-
#run-btn:hover { opacity: 0.9; }
|
| 294 |
-
#run-btn:active { transform: scale(0.98); }
|
| 295 |
-
#run-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
| 296 |
-
|
| 297 |
-
/* Force-rerun checkbox */
|
| 298 |
-
.force-rerun-row {
|
| 299 |
-
display: flex;
|
| 300 |
-
align-items: center;
|
| 301 |
-
gap: 7px;
|
| 302 |
-
font-size: 11px;
|
| 303 |
-
color: #777e90;
|
| 304 |
-
cursor: pointer;
|
| 305 |
-
user-select: none;
|
| 306 |
-
margin-top: 2px;
|
| 307 |
-
}
|
| 308 |
-
.force-rerun-row input[type="checkbox"] {
|
| 309 |
-
accent-color: #5b6cf9;
|
| 310 |
-
width: 13px;
|
| 311 |
-
height: 13px;
|
| 312 |
-
cursor: pointer;
|
| 313 |
-
}
|
| 314 |
-
|
| 315 |
-
/* Progress */
|
| 316 |
-
#progress-section { display: none; }
|
| 317 |
-
|
| 318 |
-
.progress-policy-row {
|
| 319 |
-
padding: 6px 0;
|
| 320 |
-
border-bottom: 1px solid #2e3240;
|
| 321 |
-
}
|
| 322 |
-
.progress-policy-row:last-child { border-bottom: none; }
|
| 323 |
-
|
| 324 |
-
.progress-policy-top {
|
| 325 |
-
display: flex;
|
| 326 |
-
align-items: center;
|
| 327 |
-
gap: 8px;
|
| 328 |
-
margin-bottom: 5px;
|
| 329 |
-
}
|
| 330 |
-
.progress-policy-dot {
|
| 331 |
-
width: 8px;
|
| 332 |
-
height: 8px;
|
| 333 |
-
border-radius: 50%;
|
| 334 |
-
flex-shrink: 0;
|
| 335 |
-
}
|
| 336 |
-
.progress-policy-name { flex: 1; font-size: 12px; color: #d4d8e0; }
|
| 337 |
-
.progress-policy-pct {
|
| 338 |
-
font-size: 11px;
|
| 339 |
-
font-variant-numeric: tabular-nums;
|
| 340 |
-
color: #777e90;
|
| 341 |
-
min-width: 36px;
|
| 342 |
-
text-align: right;
|
| 343 |
-
}
|
| 344 |
-
.progress-policy-pct.done { color: #10b981; }
|
| 345 |
-
.progress-policy-pct.error { color: #ef4444; }
|
| 346 |
-
|
| 347 |
-
.progress-policy-bar-track {
|
| 348 |
-
height: 4px;
|
| 349 |
-
background: #2e3240;
|
| 350 |
-
border-radius: 2px;
|
| 351 |
-
overflow: hidden;
|
| 352 |
-
}
|
| 353 |
-
.progress-policy-bar-fill {
|
| 354 |
-
height: 100%;
|
| 355 |
-
border-radius: 2px;
|
| 356 |
-
width: 0%;
|
| 357 |
-
transition: width 0.15s ease;
|
| 358 |
-
}
|
| 359 |
-
|
| 360 |
-
/* Sidebar resize handle */
|
| 361 |
-
#sidebar-resize-handle {
|
| 362 |
-
width: 4px;
|
| 363 |
-
min-width: 4px;
|
| 364 |
-
background: #2e3240;
|
| 365 |
-
cursor: col-resize;
|
| 366 |
-
transition: background 0.15s;
|
| 367 |
-
z-index: 10;
|
| 368 |
-
flex-shrink: 0;
|
| 369 |
-
}
|
| 370 |
-
#sidebar-resize-handle:hover,
|
| 371 |
-
#sidebar-resize-handle.dragging {
|
| 372 |
-
background: #5b6cf9;
|
| 373 |
-
}
|
| 374 |
-
|
| 375 |
-
/* ── Main area ────────────────────────────────────────── */
|
| 376 |
-
#main {
|
| 377 |
-
flex: 1;
|
| 378 |
-
display: flex;
|
| 379 |
-
flex-direction: column;
|
| 380 |
-
overflow: hidden;
|
| 381 |
-
}
|
| 382 |
-
|
| 383 |
-
/* Playback toolbar */
|
| 384 |
-
#playback-bar {
|
| 385 |
-
background: #22252e;
|
| 386 |
-
border-bottom: 1px solid #333744;
|
| 387 |
-
padding: 8px 16px;
|
| 388 |
-
display: flex;
|
| 389 |
-
align-items: center;
|
| 390 |
-
gap: 12px;
|
| 391 |
-
flex-shrink: 0;
|
| 392 |
-
}
|
| 393 |
-
#playback-bar button {
|
| 394 |
-
background: #2e3240;
|
| 395 |
-
border: 1px solid #3a4055;
|
| 396 |
-
color: #d4d8e0;
|
| 397 |
-
border-radius: 5px;
|
| 398 |
-
padding: 5px 14px;
|
| 399 |
-
font-size: 13px;
|
| 400 |
-
cursor: pointer;
|
| 401 |
-
transition: background 0.15s;
|
| 402 |
-
}
|
| 403 |
-
#playback-bar button:hover { background: #3a4055; }
|
| 404 |
-
#playback-bar button:disabled { opacity: 0.4; cursor: not-allowed; }
|
| 405 |
-
|
| 406 |
-
#summary-bar {
|
| 407 |
-
background: #1c2028;
|
| 408 |
-
border-bottom: 1px solid #2c3341;
|
| 409 |
-
padding: 10px 12px;
|
| 410 |
-
display: flex;
|
| 411 |
-
gap: 10px;
|
| 412 |
-
overflow-x: auto;
|
| 413 |
-
flex-shrink: 0;
|
| 414 |
-
}
|
| 415 |
-
|
| 416 |
-
.summary-empty {
|
| 417 |
-
color: #8d97aa;
|
| 418 |
-
font-size: 13px;
|
| 419 |
-
}
|
| 420 |
-
|
| 421 |
-
.summary-card {
|
| 422 |
-
min-width: 245px;
|
| 423 |
-
background: #222733;
|
| 424 |
-
border: 1px solid #313949;
|
| 425 |
-
border-top: 3px solid var(--summary-color, #5b6cf9);
|
| 426 |
-
border-radius: 10px;
|
| 427 |
-
padding: 10px 12px;
|
| 428 |
-
display: flex;
|
| 429 |
-
flex-direction: column;
|
| 430 |
-
gap: 8px;
|
| 431 |
-
}
|
| 432 |
-
|
| 433 |
-
.summary-card-header {
|
| 434 |
-
display: flex;
|
| 435 |
-
justify-content: space-between;
|
| 436 |
-
align-items: baseline;
|
| 437 |
-
gap: 8px;
|
| 438 |
-
}
|
| 439 |
-
|
| 440 |
-
.summary-card-title {
|
| 441 |
-
font-size: 13px;
|
| 442 |
-
font-weight: 700;
|
| 443 |
-
color: #edf2ff;
|
| 444 |
-
}
|
| 445 |
-
|
| 446 |
-
.summary-card-subtitle {
|
| 447 |
-
font-size: 10px;
|
| 448 |
-
color: #9aa4b8;
|
| 449 |
-
text-transform: uppercase;
|
| 450 |
-
letter-spacing: 0.08em;
|
| 451 |
-
}
|
| 452 |
-
|
| 453 |
-
.summary-grid {
|
| 454 |
-
display: grid;
|
| 455 |
-
grid-template-columns: 1fr 1fr;
|
| 456 |
-
gap: 6px 10px;
|
| 457 |
-
}
|
| 458 |
-
|
| 459 |
-
.summary-stat-label {
|
| 460 |
-
font-size: 10px;
|
| 461 |
-
color: #8893a8;
|
| 462 |
-
text-transform: uppercase;
|
| 463 |
-
letter-spacing: 0.08em;
|
| 464 |
-
}
|
| 465 |
-
|
| 466 |
-
.summary-stat-value {
|
| 467 |
-
font-size: 13px;
|
| 468 |
-
color: #f5f7fb;
|
| 469 |
-
font-weight: 600;
|
| 470 |
-
}
|
| 471 |
-
|
| 472 |
-
.summary-delta {
|
| 473 |
-
font-size: 11px;
|
| 474 |
-
font-weight: 600;
|
| 475 |
-
}
|
| 476 |
-
|
| 477 |
-
.summary-delta.good { color: #34d399; }
|
| 478 |
-
.summary-delta.bad { color: #f87171; }
|
| 479 |
-
.summary-delta.neutral { color: #94a3b8; }
|
| 480 |
-
|
| 481 |
-
#scrubber {
|
| 482 |
-
flex: 1;
|
| 483 |
-
accent-color: #5b6cf9;
|
| 484 |
-
cursor: pointer;
|
| 485 |
-
}
|
| 486 |
-
#step-display {
|
| 487 |
-
font-size: 12px;
|
| 488 |
-
color: #777e90;
|
| 489 |
-
font-variant-numeric: tabular-nums;
|
| 490 |
-
min-width: 90px;
|
| 491 |
-
text-align: right;
|
| 492 |
-
}
|
| 493 |
-
#speed-label {
|
| 494 |
-
font-size: 12px;
|
| 495 |
-
color: #777e90;
|
| 496 |
-
min-width: 48px;
|
| 497 |
-
}
|
| 498 |
-
|
| 499 |
-
/* Panel grid */
|
| 500 |
-
#panels-container {
|
| 501 |
-
flex: 1;
|
| 502 |
-
overflow: hidden;
|
| 503 |
-
display: grid;
|
| 504 |
-
gap: 2px;
|
| 505 |
-
background: #111318;
|
| 506 |
-
}
|
| 507 |
-
|
| 508 |
-
/* Grid layouts */
|
| 509 |
-
#panels-container.layout-1 {
|
| 510 |
-
grid-template-columns: 1fr;
|
| 511 |
-
grid-template-rows: 1fr;
|
| 512 |
-
}
|
| 513 |
-
#panels-container.layout-2 {
|
| 514 |
-
grid-template-columns: 1fr 1fr;
|
| 515 |
-
grid-template-rows: 1fr;
|
| 516 |
-
}
|
| 517 |
-
#panels-container.layout-3 {
|
| 518 |
-
grid-template-columns: 1fr 1fr 1fr;
|
| 519 |
-
grid-template-rows: 1fr;
|
| 520 |
-
}
|
| 521 |
-
#panels-container.layout-6 {
|
| 522 |
-
grid-template-columns: 1fr 1fr 1fr;
|
| 523 |
-
grid-template-rows: 1fr 1fr;
|
| 524 |
-
}
|
| 525 |
-
|
| 526 |
-
/* Individual panel */
|
| 527 |
-
.sim-panel {
|
| 528 |
-
position: relative;
|
| 529 |
-
background: #f5f0e8;
|
| 530 |
-
overflow: hidden;
|
| 531 |
-
display: flex;
|
| 532 |
-
flex-direction: column;
|
| 533 |
-
}
|
| 534 |
-
|
| 535 |
-
.panel-header {
|
| 536 |
-
position: absolute;
|
| 537 |
-
top: 8px;
|
| 538 |
-
left: 8px;
|
| 539 |
-
right: 8px;
|
| 540 |
-
display: flex;
|
| 541 |
-
align-items: center;
|
| 542 |
-
gap: 6px;
|
| 543 |
-
z-index: 5;
|
| 544 |
-
pointer-events: none;
|
| 545 |
-
}
|
| 546 |
-
.panel-policy-dot {
|
| 547 |
-
width: 10px;
|
| 548 |
-
height: 10px;
|
| 549 |
-
border-radius: 50%;
|
| 550 |
-
flex-shrink: 0;
|
| 551 |
-
}
|
| 552 |
-
.panel-policy-name {
|
| 553 |
-
font-size: 12px;
|
| 554 |
-
font-weight: 700;
|
| 555 |
-
letter-spacing: 0.5px;
|
| 556 |
-
}
|
| 557 |
-
/* Dark background box wrapping dot + policy name */
|
| 558 |
-
.panel-label-box {
|
| 559 |
-
display: flex;
|
| 560 |
-
align-items: center;
|
| 561 |
-
gap: 6px;
|
| 562 |
-
background: rgba(0, 0, 0, 0.72);
|
| 563 |
-
backdrop-filter: blur(4px);
|
| 564 |
-
border-radius: 6px;
|
| 565 |
-
padding: 4px 8px;
|
| 566 |
-
pointer-events: none;
|
| 567 |
-
}
|
| 568 |
-
/* Per-panel zoom controls */
|
| 569 |
-
.panel-zoom-controls {
|
| 570 |
-
display: flex;
|
| 571 |
-
gap: 3px;
|
| 572 |
-
pointer-events: auto;
|
| 573 |
-
margin-left: auto;
|
| 574 |
-
}
|
| 575 |
-
.panel-zoom-btn {
|
| 576 |
-
background: rgba(0,0,0,0.45);
|
| 577 |
-
backdrop-filter: blur(4px);
|
| 578 |
-
border: 1px solid rgba(255,255,255,0.15);
|
| 579 |
-
color: #e0dbd2;
|
| 580 |
-
border-radius: 4px;
|
| 581 |
-
width: 22px;
|
| 582 |
-
height: 22px;
|
| 583 |
-
font-size: 14px;
|
| 584 |
-
line-height: 1;
|
| 585 |
-
cursor: pointer;
|
| 586 |
-
display: flex;
|
| 587 |
-
align-items: center;
|
| 588 |
-
justify-content: center;
|
| 589 |
-
transition: background 0.15s;
|
| 590 |
-
}
|
| 591 |
-
.panel-zoom-btn:hover { background: rgba(0,0,0,0.65); }
|
| 592 |
-
|
| 593 |
-
/* Per-panel metrics overlay (bottom-left of canvas) */
|
| 594 |
-
.panel-metrics-overlay {
|
| 595 |
-
position: absolute;
|
| 596 |
-
bottom: 8px;
|
| 597 |
-
left: 8px;
|
| 598 |
-
z-index: 5;
|
| 599 |
-
background: rgba(0, 0, 0, 0.72);
|
| 600 |
-
backdrop-filter: blur(4px);
|
| 601 |
-
border-radius: 6px;
|
| 602 |
-
padding: 5px 9px;
|
| 603 |
-
pointer-events: none;
|
| 604 |
-
display: flex;
|
| 605 |
-
flex-direction: column;
|
| 606 |
-
gap: 2px;
|
| 607 |
-
}
|
| 608 |
-
.pmo-row {
|
| 609 |
-
display: flex;
|
| 610 |
-
justify-content: space-between;
|
| 611 |
-
gap: 14px;
|
| 612 |
-
font-size: 10px;
|
| 613 |
-
}
|
| 614 |
-
.pmo-label { color: #9ea8b8; }
|
| 615 |
-
.pmo-value { color: #e2ddd6; font-variant-numeric: tabular-nums; }
|
| 616 |
-
|
| 617 |
-
.panel-map-legend {
|
| 618 |
-
position: absolute;
|
| 619 |
-
right: 8px;
|
| 620 |
-
bottom: 8px;
|
| 621 |
-
z-index: 5;
|
| 622 |
-
background: rgba(0, 0, 0, 0.72);
|
| 623 |
-
backdrop-filter: blur(4px);
|
| 624 |
-
border-radius: 6px;
|
| 625 |
-
padding: 6px 8px;
|
| 626 |
-
pointer-events: none;
|
| 627 |
-
display: flex;
|
| 628 |
-
flex-direction: column;
|
| 629 |
-
gap: 3px;
|
| 630 |
-
min-width: 108px;
|
| 631 |
-
}
|
| 632 |
-
.panel-map-legend.hidden { display: none; }
|
| 633 |
-
.panel-map-legend-title {
|
| 634 |
-
font-size: 9px;
|
| 635 |
-
color: #b8c1cf;
|
| 636 |
-
text-transform: uppercase;
|
| 637 |
-
letter-spacing: 0.08em;
|
| 638 |
-
font-weight: 700;
|
| 639 |
-
margin-bottom: 1px;
|
| 640 |
-
}
|
| 641 |
-
.panel-map-legend-row {
|
| 642 |
-
display: flex;
|
| 643 |
-
align-items: center;
|
| 644 |
-
gap: 6px;
|
| 645 |
-
font-size: 10px;
|
| 646 |
-
color: #e2ddd6;
|
| 647 |
-
}
|
| 648 |
-
.panel-map-swatch {
|
| 649 |
-
display: inline-block;
|
| 650 |
-
width: 18px;
|
| 651 |
-
height: 8px;
|
| 652 |
-
border-radius: 999px;
|
| 653 |
-
flex-shrink: 0;
|
| 654 |
-
}
|
| 655 |
-
.panel-map-swatch.districts {
|
| 656 |
-
background: rgba(95, 172, 248, 0.55);
|
| 657 |
-
border: 1px solid rgba(95, 172, 248, 0.8);
|
| 658 |
-
}
|
| 659 |
-
.panel-map-swatch.entry { background: #22c55e; }
|
| 660 |
-
.panel-map-swatch.exit { background: #ef4444; }
|
| 661 |
-
.panel-map-swatch.gateway { background: #f39c12; }
|
| 662 |
-
|
| 663 |
-
.panel-canvas-wrapper {
|
| 664 |
-
flex: 1;
|
| 665 |
-
position: relative;
|
| 666 |
-
}
|
| 667 |
-
.panel-canvas-wrapper canvas {
|
| 668 |
-
display: block;
|
| 669 |
-
width: 100% !important;
|
| 670 |
-
height: 100% !important;
|
| 671 |
-
}
|
| 672 |
-
|
| 673 |
-
/* Coming-soon overlay for LLM+DQN */
|
| 674 |
-
.coming-soon-overlay {
|
| 675 |
-
position: absolute;
|
| 676 |
-
inset: 0;
|
| 677 |
-
background: rgba(17,19,24,0.88);
|
| 678 |
-
display: flex;
|
| 679 |
-
flex-direction: column;
|
| 680 |
-
align-items: center;
|
| 681 |
-
justify-content: center;
|
| 682 |
-
gap: 8px;
|
| 683 |
-
z-index: 10;
|
| 684 |
-
}
|
| 685 |
-
.coming-soon-overlay .cs-icon { font-size: 36px; }
|
| 686 |
-
.coming-soon-overlay .cs-title {
|
| 687 |
-
font-size: 15px;
|
| 688 |
-
font-weight: 700;
|
| 689 |
-
color: #fff;
|
| 690 |
-
}
|
| 691 |
-
.coming-soon-overlay .cs-sub {
|
| 692 |
-
font-size: 12px;
|
| 693 |
-
color: #777e90;
|
| 694 |
-
}
|
| 695 |
-
|
| 696 |
-
/* Panel loading spinner */
|
| 697 |
-
.panel-spinner {
|
| 698 |
-
position: absolute;
|
| 699 |
-
inset: 0;
|
| 700 |
-
display: flex;
|
| 701 |
-
align-items: center;
|
| 702 |
-
justify-content: center;
|
| 703 |
-
background: #f5f0e8;
|
| 704 |
-
z-index: 8;
|
| 705 |
-
}
|
| 706 |
-
.panel-spinner.hidden { display: none; }
|
| 707 |
-
.spinner-ring {
|
| 708 |
-
width: 36px;
|
| 709 |
-
height: 36px;
|
| 710 |
-
border: 3px solid #d4c9b8;
|
| 711 |
-
border-top-color: #5b6cf9;
|
| 712 |
-
border-radius: 50%;
|
| 713 |
-
animation: spin 0.8s linear infinite;
|
| 714 |
-
}
|
| 715 |
-
@keyframes spin { to { transform: rotate(360deg); } }
|
| 716 |
-
|
| 717 |
-
/* Welcome / idle state */
|
| 718 |
-
#welcome-overlay {
|
| 719 |
-
position: absolute;
|
| 720 |
-
inset: 0;
|
| 721 |
-
display: flex;
|
| 722 |
-
align-items: center;
|
| 723 |
-
justify-content: center;
|
| 724 |
-
background: #f5f0e8;
|
| 725 |
-
z-index: 20;
|
| 726 |
-
flex-direction: column;
|
| 727 |
-
gap: 10px;
|
| 728 |
-
text-align: center;
|
| 729 |
-
}
|
| 730 |
-
#welcome-overlay h2 { font-size: 20px; color: #2e2820; margin: 0; }
|
| 731 |
-
#welcome-overlay p { font-size: 13px; color: #7a6e60; margin: 0; max-width: 320px; }
|
| 732 |
-
#welcome-overlay.hidden { display: none; }
|
| 733 |
-
|
| 734 |
-
/* Policy color variables */
|
| 735 |
-
:root {
|
| 736 |
-
--color-no-intervention: #94a3b8;
|
| 737 |
-
--color-fixed: #f59e0b;
|
| 738 |
-
--color-random: #10b981;
|
| 739 |
-
--color-learned: #5b6cf9;
|
| 740 |
-
--color-llm-dqn: #ec4899;
|
| 741 |
-
--color-dqn-heuristic: #06b6d4;
|
| 742 |
-
}
|
| 743 |
-
|
| 744 |
-
/* Responsive: if viewport too narrow, collapse sidebar */
|
| 745 |
-
@media (max-width: 600px) {
|
| 746 |
-
#sidebar { width: 200px; min-width: 200px; }
|
| 747 |
-
}
|
|
|
|
| 1 |
+
/* =========================================================
|
| 2 |
+
Traffic Visualizer – Multi-Panel Dashboard Styles
|
| 3 |
+
========================================================= */
|
| 4 |
+
|
| 5 |
+
*, *::before, *::after { box-sizing: border-box; }
|
| 6 |
+
|
| 7 |
+
body {
|
| 8 |
+
margin: 0;
|
| 9 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
| 10 |
+
background: #1a1d23;
|
| 11 |
+
color: #d4d8e0;
|
| 12 |
+
height: 100vh;
|
| 13 |
+
overflow: hidden;
|
| 14 |
+
display: flex;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
/* ── Sidebar ──────────────────────────────────────────── */
|
| 18 |
+
#sidebar {
|
| 19 |
+
width: 270px;
|
| 20 |
+
min-width: 180px;
|
| 21 |
+
background: #22252e;
|
| 22 |
+
border-right: 1px solid #333744;
|
| 23 |
+
display: flex;
|
| 24 |
+
flex-direction: column;
|
| 25 |
+
overflow-y: auto;
|
| 26 |
+
padding: 16px;
|
| 27 |
+
gap: 16px;
|
| 28 |
+
z-index: 10;
|
| 29 |
+
scrollbar-width: thin;
|
| 30 |
+
scrollbar-color: #3a4055 transparent;
|
| 31 |
+
}
|
| 32 |
+
#sidebar::-webkit-scrollbar { width: 5px; }
|
| 33 |
+
#sidebar::-webkit-scrollbar-track { background: transparent; }
|
| 34 |
+
#sidebar::-webkit-scrollbar-thumb { background: #3a4055; border-radius: 3px; }
|
| 35 |
+
#sidebar::-webkit-scrollbar-thumb:hover { background: #5b6cf9; }
|
| 36 |
+
|
| 37 |
+
#sidebar h1 {
|
| 38 |
+
font-size: 18px;
|
| 39 |
+
font-weight: 700;
|
| 40 |
+
color: #fff;
|
| 41 |
+
margin: 0 0 4px 0;
|
| 42 |
+
letter-spacing: 0.5px;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
#sidebar .subtitle {
|
| 46 |
+
font-size: 11px;
|
| 47 |
+
color: #777e90;
|
| 48 |
+
margin: 0;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.sidebar-section {
|
| 52 |
+
background: #1a1d23;
|
| 53 |
+
border-radius: 8px;
|
| 54 |
+
padding: 12px;
|
| 55 |
+
border: 1px solid #2e3240;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.sidebar-section h3 {
|
| 59 |
+
font-size: 11px;
|
| 60 |
+
font-weight: 600;
|
| 61 |
+
text-transform: uppercase;
|
| 62 |
+
letter-spacing: 1px;
|
| 63 |
+
color: #777e90;
|
| 64 |
+
margin: 0 0 10px 0;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/* File upload zone */
|
| 68 |
+
#roadnet-drop-zone {
|
| 69 |
+
border: 2px dashed #3a4055;
|
| 70 |
+
border-radius: 6px;
|
| 71 |
+
padding: 20px 12px;
|
| 72 |
+
text-align: center;
|
| 73 |
+
cursor: pointer;
|
| 74 |
+
transition: border-color 0.2s, background 0.2s;
|
| 75 |
+
font-size: 13px;
|
| 76 |
+
color: #777e90;
|
| 77 |
+
}
|
| 78 |
+
#roadnet-drop-zone:hover,
|
| 79 |
+
#roadnet-drop-zone.drag-over {
|
| 80 |
+
border-color: #5b6cf9;
|
| 81 |
+
background: rgba(91, 108, 249, 0.06);
|
| 82 |
+
color: #aab0c2;
|
| 83 |
+
}
|
| 84 |
+
#roadnet-drop-zone .drop-icon {
|
| 85 |
+
font-size: 24px;
|
| 86 |
+
margin-bottom: 6px;
|
| 87 |
+
display: block;
|
| 88 |
+
}
|
| 89 |
+
#roadnet-drop-zone .drop-filename {
|
| 90 |
+
font-size: 12px;
|
| 91 |
+
color: #5b6cf9;
|
| 92 |
+
font-weight: 600;
|
| 93 |
+
margin-top: 4px;
|
| 94 |
+
}
|
| 95 |
+
#roadnet-file-input { display: none; }
|
| 96 |
+
|
| 97 |
+
/* City/Scenario pickers */
|
| 98 |
+
.picker-row {
|
| 99 |
+
display: flex;
|
| 100 |
+
flex-direction: column;
|
| 101 |
+
gap: 6px;
|
| 102 |
+
}
|
| 103 |
+
.picker-row label {
|
| 104 |
+
font-size: 11px;
|
| 105 |
+
color: #777e90;
|
| 106 |
+
font-weight: 600;
|
| 107 |
+
text-transform: uppercase;
|
| 108 |
+
letter-spacing: 0.5px;
|
| 109 |
+
}
|
| 110 |
+
.picker-row select {
|
| 111 |
+
width: 100%;
|
| 112 |
+
background: #22252e;
|
| 113 |
+
border: 1px solid #3a4055;
|
| 114 |
+
color: #d4d8e0;
|
| 115 |
+
border-radius: 5px;
|
| 116 |
+
padding: 6px 8px;
|
| 117 |
+
font-size: 13px;
|
| 118 |
+
outline: none;
|
| 119 |
+
cursor: pointer;
|
| 120 |
+
}
|
| 121 |
+
.picker-row select:focus {
|
| 122 |
+
border-color: #5b6cf9;
|
| 123 |
+
}
|
| 124 |
+
.picker-row select:disabled {
|
| 125 |
+
opacity: 0.4;
|
| 126 |
+
cursor: not-allowed;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
/* Policy checkboxes */
|
| 130 |
+
.policy-list {
|
| 131 |
+
display: flex;
|
| 132 |
+
flex-direction: column;
|
| 133 |
+
gap: 7px;
|
| 134 |
+
}
|
| 135 |
+
.policy-item {
|
| 136 |
+
display: flex;
|
| 137 |
+
align-items: center;
|
| 138 |
+
gap: 8px;
|
| 139 |
+
cursor: pointer;
|
| 140 |
+
user-select: none;
|
| 141 |
+
}
|
| 142 |
+
.policy-item input[type="checkbox"] {
|
| 143 |
+
accent-color: var(--policy-color);
|
| 144 |
+
width: 15px;
|
| 145 |
+
height: 15px;
|
| 146 |
+
cursor: pointer;
|
| 147 |
+
}
|
| 148 |
+
.policy-item.disabled {
|
| 149 |
+
opacity: 0.4;
|
| 150 |
+
cursor: not-allowed;
|
| 151 |
+
}
|
| 152 |
+
.policy-item.disabled input { cursor: not-allowed; }
|
| 153 |
+
|
| 154 |
+
.policy-badge {
|
| 155 |
+
display: inline-block;
|
| 156 |
+
width: 10px;
|
| 157 |
+
height: 10px;
|
| 158 |
+
border-radius: 50%;
|
| 159 |
+
flex-shrink: 0;
|
| 160 |
+
}
|
| 161 |
+
.policy-label {
|
| 162 |
+
font-size: 13px;
|
| 163 |
+
flex: 1;
|
| 164 |
+
}
|
| 165 |
+
.policy-tag {
|
| 166 |
+
font-size: 10px;
|
| 167 |
+
background: rgba(255,255,255,0.07);
|
| 168 |
+
color: #888;
|
| 169 |
+
border-radius: 3px;
|
| 170 |
+
padding: 1px 5px;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.recent-runs-list {
|
| 174 |
+
display: flex;
|
| 175 |
+
flex-direction: column;
|
| 176 |
+
gap: 8px;
|
| 177 |
+
}
|
| 178 |
+
.recent-run-empty {
|
| 179 |
+
font-size: 12px;
|
| 180 |
+
color: #777e90;
|
| 181 |
+
}
|
| 182 |
+
.recent-run-card {
|
| 183 |
+
border: 1px solid #323747;
|
| 184 |
+
border-radius: 7px;
|
| 185 |
+
background: #22252e;
|
| 186 |
+
padding: 9px 10px;
|
| 187 |
+
display: flex;
|
| 188 |
+
flex-direction: column;
|
| 189 |
+
gap: 6px;
|
| 190 |
+
}
|
| 191 |
+
.recent-run-top {
|
| 192 |
+
display: flex;
|
| 193 |
+
justify-content: space-between;
|
| 194 |
+
gap: 8px;
|
| 195 |
+
align-items: baseline;
|
| 196 |
+
}
|
| 197 |
+
.recent-run-title {
|
| 198 |
+
font-size: 12px;
|
| 199 |
+
font-weight: 700;
|
| 200 |
+
color: #e7ecf4;
|
| 201 |
+
}
|
| 202 |
+
.recent-run-time {
|
| 203 |
+
font-size: 10px;
|
| 204 |
+
color: #7f8a9d;
|
| 205 |
+
white-space: nowrap;
|
| 206 |
+
}
|
| 207 |
+
.recent-run-meta {
|
| 208 |
+
font-size: 11px;
|
| 209 |
+
color: #aab4c4;
|
| 210 |
+
}
|
| 211 |
+
.recent-run-policies {
|
| 212 |
+
display: flex;
|
| 213 |
+
flex-wrap: wrap;
|
| 214 |
+
gap: 4px;
|
| 215 |
+
}
|
| 216 |
+
.recent-run-chip {
|
| 217 |
+
font-size: 10px;
|
| 218 |
+
color: #dbe2ee;
|
| 219 |
+
background: rgba(255,255,255,0.08);
|
| 220 |
+
border: 1px solid rgba(255,255,255,0.08);
|
| 221 |
+
border-radius: 999px;
|
| 222 |
+
padding: 2px 6px;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.debug-log {
|
| 226 |
+
max-height: 180px;
|
| 227 |
+
overflow: auto;
|
| 228 |
+
padding: 10px;
|
| 229 |
+
border-radius: 8px;
|
| 230 |
+
background: #161a22;
|
| 231 |
+
border: 1px solid #2c3442;
|
| 232 |
+
color: #dbe4ff;
|
| 233 |
+
font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
| 234 |
+
white-space: pre-wrap;
|
| 235 |
+
}
|
| 236 |
+
.recent-run-actions {
|
| 237 |
+
display: flex;
|
| 238 |
+
gap: 6px;
|
| 239 |
+
}
|
| 240 |
+
.recent-run-btn {
|
| 241 |
+
flex: 1;
|
| 242 |
+
background: #2b3040;
|
| 243 |
+
border: 1px solid #40475d;
|
| 244 |
+
color: #dce3ef;
|
| 245 |
+
border-radius: 5px;
|
| 246 |
+
padding: 5px 7px;
|
| 247 |
+
font-size: 11px;
|
| 248 |
+
cursor: pointer;
|
| 249 |
+
}
|
| 250 |
+
.recent-run-btn:hover {
|
| 251 |
+
border-color: #5b6cf9;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
/* View mode selector */
|
| 255 |
+
.view-mode-row {
|
| 256 |
+
display: flex;
|
| 257 |
+
gap: 6px;
|
| 258 |
+
}
|
| 259 |
+
.view-mode-btn {
|
| 260 |
+
flex: 1;
|
| 261 |
+
background: #22252e;
|
| 262 |
+
border: 1px solid #3a4055;
|
| 263 |
+
color: #777e90;
|
| 264 |
+
border-radius: 5px;
|
| 265 |
+
padding: 6px 0;
|
| 266 |
+
font-size: 12px;
|
| 267 |
+
font-weight: 600;
|
| 268 |
+
cursor: pointer;
|
| 269 |
+
transition: all 0.15s;
|
| 270 |
+
text-align: center;
|
| 271 |
+
}
|
| 272 |
+
.view-mode-btn:hover { border-color: #5b6cf9; color: #aab0c2; }
|
| 273 |
+
.view-mode-btn.active {
|
| 274 |
+
background: #5b6cf9;
|
| 275 |
+
border-color: #5b6cf9;
|
| 276 |
+
color: #fff;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
/* Run button */
|
| 280 |
+
#run-btn {
|
| 281 |
+
width: 100%;
|
| 282 |
+
padding: 10px;
|
| 283 |
+
background: linear-gradient(135deg, #5b6cf9, #7c3aed);
|
| 284 |
+
color: #fff;
|
| 285 |
+
border: none;
|
| 286 |
+
border-radius: 6px;
|
| 287 |
+
font-size: 14px;
|
| 288 |
+
font-weight: 600;
|
| 289 |
+
cursor: pointer;
|
| 290 |
+
transition: opacity 0.2s, transform 0.1s;
|
| 291 |
+
letter-spacing: 0.3px;
|
| 292 |
+
}
|
| 293 |
+
#run-btn:hover { opacity: 0.9; }
|
| 294 |
+
#run-btn:active { transform: scale(0.98); }
|
| 295 |
+
#run-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
| 296 |
+
|
| 297 |
+
/* Force-rerun checkbox */
|
| 298 |
+
.force-rerun-row {
|
| 299 |
+
display: flex;
|
| 300 |
+
align-items: center;
|
| 301 |
+
gap: 7px;
|
| 302 |
+
font-size: 11px;
|
| 303 |
+
color: #777e90;
|
| 304 |
+
cursor: pointer;
|
| 305 |
+
user-select: none;
|
| 306 |
+
margin-top: 2px;
|
| 307 |
+
}
|
| 308 |
+
.force-rerun-row input[type="checkbox"] {
|
| 309 |
+
accent-color: #5b6cf9;
|
| 310 |
+
width: 13px;
|
| 311 |
+
height: 13px;
|
| 312 |
+
cursor: pointer;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
/* Progress */
|
| 316 |
+
#progress-section { display: none; }
|
| 317 |
+
|
| 318 |
+
.progress-policy-row {
|
| 319 |
+
padding: 6px 0;
|
| 320 |
+
border-bottom: 1px solid #2e3240;
|
| 321 |
+
}
|
| 322 |
+
.progress-policy-row:last-child { border-bottom: none; }
|
| 323 |
+
|
| 324 |
+
.progress-policy-top {
|
| 325 |
+
display: flex;
|
| 326 |
+
align-items: center;
|
| 327 |
+
gap: 8px;
|
| 328 |
+
margin-bottom: 5px;
|
| 329 |
+
}
|
| 330 |
+
.progress-policy-dot {
|
| 331 |
+
width: 8px;
|
| 332 |
+
height: 8px;
|
| 333 |
+
border-radius: 50%;
|
| 334 |
+
flex-shrink: 0;
|
| 335 |
+
}
|
| 336 |
+
.progress-policy-name { flex: 1; font-size: 12px; color: #d4d8e0; }
|
| 337 |
+
.progress-policy-pct {
|
| 338 |
+
font-size: 11px;
|
| 339 |
+
font-variant-numeric: tabular-nums;
|
| 340 |
+
color: #777e90;
|
| 341 |
+
min-width: 36px;
|
| 342 |
+
text-align: right;
|
| 343 |
+
}
|
| 344 |
+
.progress-policy-pct.done { color: #10b981; }
|
| 345 |
+
.progress-policy-pct.error { color: #ef4444; }
|
| 346 |
+
|
| 347 |
+
.progress-policy-bar-track {
|
| 348 |
+
height: 4px;
|
| 349 |
+
background: #2e3240;
|
| 350 |
+
border-radius: 2px;
|
| 351 |
+
overflow: hidden;
|
| 352 |
+
}
|
| 353 |
+
.progress-policy-bar-fill {
|
| 354 |
+
height: 100%;
|
| 355 |
+
border-radius: 2px;
|
| 356 |
+
width: 0%;
|
| 357 |
+
transition: width 0.15s ease;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
/* Sidebar resize handle */
|
| 361 |
+
#sidebar-resize-handle {
|
| 362 |
+
width: 4px;
|
| 363 |
+
min-width: 4px;
|
| 364 |
+
background: #2e3240;
|
| 365 |
+
cursor: col-resize;
|
| 366 |
+
transition: background 0.15s;
|
| 367 |
+
z-index: 10;
|
| 368 |
+
flex-shrink: 0;
|
| 369 |
+
}
|
| 370 |
+
#sidebar-resize-handle:hover,
|
| 371 |
+
#sidebar-resize-handle.dragging {
|
| 372 |
+
background: #5b6cf9;
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
/* ── Main area ────────────────────────────────────────── */
|
| 376 |
+
#main {
|
| 377 |
+
flex: 1;
|
| 378 |
+
display: flex;
|
| 379 |
+
flex-direction: column;
|
| 380 |
+
overflow: hidden;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
/* Playback toolbar */
|
| 384 |
+
#playback-bar {
|
| 385 |
+
background: #22252e;
|
| 386 |
+
border-bottom: 1px solid #333744;
|
| 387 |
+
padding: 8px 16px;
|
| 388 |
+
display: flex;
|
| 389 |
+
align-items: center;
|
| 390 |
+
gap: 12px;
|
| 391 |
+
flex-shrink: 0;
|
| 392 |
+
}
|
| 393 |
+
#playback-bar button {
|
| 394 |
+
background: #2e3240;
|
| 395 |
+
border: 1px solid #3a4055;
|
| 396 |
+
color: #d4d8e0;
|
| 397 |
+
border-radius: 5px;
|
| 398 |
+
padding: 5px 14px;
|
| 399 |
+
font-size: 13px;
|
| 400 |
+
cursor: pointer;
|
| 401 |
+
transition: background 0.15s;
|
| 402 |
+
}
|
| 403 |
+
#playback-bar button:hover { background: #3a4055; }
|
| 404 |
+
#playback-bar button:disabled { opacity: 0.4; cursor: not-allowed; }
|
| 405 |
+
|
| 406 |
+
#summary-bar {
|
| 407 |
+
background: #1c2028;
|
| 408 |
+
border-bottom: 1px solid #2c3341;
|
| 409 |
+
padding: 10px 12px;
|
| 410 |
+
display: flex;
|
| 411 |
+
gap: 10px;
|
| 412 |
+
overflow-x: auto;
|
| 413 |
+
flex-shrink: 0;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.summary-empty {
|
| 417 |
+
color: #8d97aa;
|
| 418 |
+
font-size: 13px;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.summary-card {
|
| 422 |
+
min-width: 245px;
|
| 423 |
+
background: #222733;
|
| 424 |
+
border: 1px solid #313949;
|
| 425 |
+
border-top: 3px solid var(--summary-color, #5b6cf9);
|
| 426 |
+
border-radius: 10px;
|
| 427 |
+
padding: 10px 12px;
|
| 428 |
+
display: flex;
|
| 429 |
+
flex-direction: column;
|
| 430 |
+
gap: 8px;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.summary-card-header {
|
| 434 |
+
display: flex;
|
| 435 |
+
justify-content: space-between;
|
| 436 |
+
align-items: baseline;
|
| 437 |
+
gap: 8px;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.summary-card-title {
|
| 441 |
+
font-size: 13px;
|
| 442 |
+
font-weight: 700;
|
| 443 |
+
color: #edf2ff;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
.summary-card-subtitle {
|
| 447 |
+
font-size: 10px;
|
| 448 |
+
color: #9aa4b8;
|
| 449 |
+
text-transform: uppercase;
|
| 450 |
+
letter-spacing: 0.08em;
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
.summary-grid {
|
| 454 |
+
display: grid;
|
| 455 |
+
grid-template-columns: 1fr 1fr;
|
| 456 |
+
gap: 6px 10px;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
.summary-stat-label {
|
| 460 |
+
font-size: 10px;
|
| 461 |
+
color: #8893a8;
|
| 462 |
+
text-transform: uppercase;
|
| 463 |
+
letter-spacing: 0.08em;
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
.summary-stat-value {
|
| 467 |
+
font-size: 13px;
|
| 468 |
+
color: #f5f7fb;
|
| 469 |
+
font-weight: 600;
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
.summary-delta {
|
| 473 |
+
font-size: 11px;
|
| 474 |
+
font-weight: 600;
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
.summary-delta.good { color: #34d399; }
|
| 478 |
+
.summary-delta.bad { color: #f87171; }
|
| 479 |
+
.summary-delta.neutral { color: #94a3b8; }
|
| 480 |
+
|
| 481 |
+
#scrubber {
|
| 482 |
+
flex: 1;
|
| 483 |
+
accent-color: #5b6cf9;
|
| 484 |
+
cursor: pointer;
|
| 485 |
+
}
|
| 486 |
+
#step-display {
|
| 487 |
+
font-size: 12px;
|
| 488 |
+
color: #777e90;
|
| 489 |
+
font-variant-numeric: tabular-nums;
|
| 490 |
+
min-width: 90px;
|
| 491 |
+
text-align: right;
|
| 492 |
+
}
|
| 493 |
+
#speed-label {
|
| 494 |
+
font-size: 12px;
|
| 495 |
+
color: #777e90;
|
| 496 |
+
min-width: 48px;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
/* Panel grid */
|
| 500 |
+
#panels-container {
|
| 501 |
+
flex: 1;
|
| 502 |
+
overflow: hidden;
|
| 503 |
+
display: grid;
|
| 504 |
+
gap: 2px;
|
| 505 |
+
background: #111318;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
/* Grid layouts */
|
| 509 |
+
#panels-container.layout-1 {
|
| 510 |
+
grid-template-columns: 1fr;
|
| 511 |
+
grid-template-rows: 1fr;
|
| 512 |
+
}
|
| 513 |
+
#panels-container.layout-2 {
|
| 514 |
+
grid-template-columns: 1fr 1fr;
|
| 515 |
+
grid-template-rows: 1fr;
|
| 516 |
+
}
|
| 517 |
+
#panels-container.layout-3 {
|
| 518 |
+
grid-template-columns: 1fr 1fr 1fr;
|
| 519 |
+
grid-template-rows: 1fr;
|
| 520 |
+
}
|
| 521 |
+
#panels-container.layout-6 {
|
| 522 |
+
grid-template-columns: 1fr 1fr 1fr;
|
| 523 |
+
grid-template-rows: 1fr 1fr;
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
/* Individual panel */
|
| 527 |
+
.sim-panel {
|
| 528 |
+
position: relative;
|
| 529 |
+
background: #f5f0e8;
|
| 530 |
+
overflow: hidden;
|
| 531 |
+
display: flex;
|
| 532 |
+
flex-direction: column;
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
.panel-header {
|
| 536 |
+
position: absolute;
|
| 537 |
+
top: 8px;
|
| 538 |
+
left: 8px;
|
| 539 |
+
right: 8px;
|
| 540 |
+
display: flex;
|
| 541 |
+
align-items: center;
|
| 542 |
+
gap: 6px;
|
| 543 |
+
z-index: 5;
|
| 544 |
+
pointer-events: none;
|
| 545 |
+
}
|
| 546 |
+
.panel-policy-dot {
|
| 547 |
+
width: 10px;
|
| 548 |
+
height: 10px;
|
| 549 |
+
border-radius: 50%;
|
| 550 |
+
flex-shrink: 0;
|
| 551 |
+
}
|
| 552 |
+
.panel-policy-name {
|
| 553 |
+
font-size: 12px;
|
| 554 |
+
font-weight: 700;
|
| 555 |
+
letter-spacing: 0.5px;
|
| 556 |
+
}
|
| 557 |
+
/* Dark background box wrapping dot + policy name */
|
| 558 |
+
.panel-label-box {
|
| 559 |
+
display: flex;
|
| 560 |
+
align-items: center;
|
| 561 |
+
gap: 6px;
|
| 562 |
+
background: rgba(0, 0, 0, 0.72);
|
| 563 |
+
backdrop-filter: blur(4px);
|
| 564 |
+
border-radius: 6px;
|
| 565 |
+
padding: 4px 8px;
|
| 566 |
+
pointer-events: none;
|
| 567 |
+
}
|
| 568 |
+
/* Per-panel zoom controls */
|
| 569 |
+
.panel-zoom-controls {
|
| 570 |
+
display: flex;
|
| 571 |
+
gap: 3px;
|
| 572 |
+
pointer-events: auto;
|
| 573 |
+
margin-left: auto;
|
| 574 |
+
}
|
| 575 |
+
.panel-zoom-btn {
|
| 576 |
+
background: rgba(0,0,0,0.45);
|
| 577 |
+
backdrop-filter: blur(4px);
|
| 578 |
+
border: 1px solid rgba(255,255,255,0.15);
|
| 579 |
+
color: #e0dbd2;
|
| 580 |
+
border-radius: 4px;
|
| 581 |
+
width: 22px;
|
| 582 |
+
height: 22px;
|
| 583 |
+
font-size: 14px;
|
| 584 |
+
line-height: 1;
|
| 585 |
+
cursor: pointer;
|
| 586 |
+
display: flex;
|
| 587 |
+
align-items: center;
|
| 588 |
+
justify-content: center;
|
| 589 |
+
transition: background 0.15s;
|
| 590 |
+
}
|
| 591 |
+
.panel-zoom-btn:hover { background: rgba(0,0,0,0.65); }
|
| 592 |
+
|
| 593 |
+
/* Per-panel metrics overlay (bottom-left of canvas) */
|
| 594 |
+
.panel-metrics-overlay {
|
| 595 |
+
position: absolute;
|
| 596 |
+
bottom: 8px;
|
| 597 |
+
left: 8px;
|
| 598 |
+
z-index: 5;
|
| 599 |
+
background: rgba(0, 0, 0, 0.72);
|
| 600 |
+
backdrop-filter: blur(4px);
|
| 601 |
+
border-radius: 6px;
|
| 602 |
+
padding: 5px 9px;
|
| 603 |
+
pointer-events: none;
|
| 604 |
+
display: flex;
|
| 605 |
+
flex-direction: column;
|
| 606 |
+
gap: 2px;
|
| 607 |
+
}
|
| 608 |
+
.pmo-row {
|
| 609 |
+
display: flex;
|
| 610 |
+
justify-content: space-between;
|
| 611 |
+
gap: 14px;
|
| 612 |
+
font-size: 10px;
|
| 613 |
+
}
|
| 614 |
+
.pmo-label { color: #9ea8b8; }
|
| 615 |
+
.pmo-value { color: #e2ddd6; font-variant-numeric: tabular-nums; }
|
| 616 |
+
|
| 617 |
+
.panel-map-legend {
|
| 618 |
+
position: absolute;
|
| 619 |
+
right: 8px;
|
| 620 |
+
bottom: 8px;
|
| 621 |
+
z-index: 5;
|
| 622 |
+
background: rgba(0, 0, 0, 0.72);
|
| 623 |
+
backdrop-filter: blur(4px);
|
| 624 |
+
border-radius: 6px;
|
| 625 |
+
padding: 6px 8px;
|
| 626 |
+
pointer-events: none;
|
| 627 |
+
display: flex;
|
| 628 |
+
flex-direction: column;
|
| 629 |
+
gap: 3px;
|
| 630 |
+
min-width: 108px;
|
| 631 |
+
}
|
| 632 |
+
.panel-map-legend.hidden { display: none; }
|
| 633 |
+
.panel-map-legend-title {
|
| 634 |
+
font-size: 9px;
|
| 635 |
+
color: #b8c1cf;
|
| 636 |
+
text-transform: uppercase;
|
| 637 |
+
letter-spacing: 0.08em;
|
| 638 |
+
font-weight: 700;
|
| 639 |
+
margin-bottom: 1px;
|
| 640 |
+
}
|
| 641 |
+
.panel-map-legend-row {
|
| 642 |
+
display: flex;
|
| 643 |
+
align-items: center;
|
| 644 |
+
gap: 6px;
|
| 645 |
+
font-size: 10px;
|
| 646 |
+
color: #e2ddd6;
|
| 647 |
+
}
|
| 648 |
+
.panel-map-swatch {
|
| 649 |
+
display: inline-block;
|
| 650 |
+
width: 18px;
|
| 651 |
+
height: 8px;
|
| 652 |
+
border-radius: 999px;
|
| 653 |
+
flex-shrink: 0;
|
| 654 |
+
}
|
| 655 |
+
.panel-map-swatch.districts {
|
| 656 |
+
background: rgba(95, 172, 248, 0.55);
|
| 657 |
+
border: 1px solid rgba(95, 172, 248, 0.8);
|
| 658 |
+
}
|
| 659 |
+
.panel-map-swatch.entry { background: #22c55e; }
|
| 660 |
+
.panel-map-swatch.exit { background: #ef4444; }
|
| 661 |
+
.panel-map-swatch.gateway { background: #f39c12; }
|
| 662 |
+
|
| 663 |
+
.panel-canvas-wrapper {
|
| 664 |
+
flex: 1;
|
| 665 |
+
position: relative;
|
| 666 |
+
}
|
| 667 |
+
.panel-canvas-wrapper canvas {
|
| 668 |
+
display: block;
|
| 669 |
+
width: 100% !important;
|
| 670 |
+
height: 100% !important;
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
/* Coming-soon overlay for LLM+DQN */
|
| 674 |
+
.coming-soon-overlay {
|
| 675 |
+
position: absolute;
|
| 676 |
+
inset: 0;
|
| 677 |
+
background: rgba(17,19,24,0.88);
|
| 678 |
+
display: flex;
|
| 679 |
+
flex-direction: column;
|
| 680 |
+
align-items: center;
|
| 681 |
+
justify-content: center;
|
| 682 |
+
gap: 8px;
|
| 683 |
+
z-index: 10;
|
| 684 |
+
}
|
| 685 |
+
.coming-soon-overlay .cs-icon { font-size: 36px; }
|
| 686 |
+
.coming-soon-overlay .cs-title {
|
| 687 |
+
font-size: 15px;
|
| 688 |
+
font-weight: 700;
|
| 689 |
+
color: #fff;
|
| 690 |
+
}
|
| 691 |
+
.coming-soon-overlay .cs-sub {
|
| 692 |
+
font-size: 12px;
|
| 693 |
+
color: #777e90;
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
/* Panel loading spinner */
|
| 697 |
+
.panel-spinner {
|
| 698 |
+
position: absolute;
|
| 699 |
+
inset: 0;
|
| 700 |
+
display: flex;
|
| 701 |
+
align-items: center;
|
| 702 |
+
justify-content: center;
|
| 703 |
+
background: #f5f0e8;
|
| 704 |
+
z-index: 8;
|
| 705 |
+
}
|
| 706 |
+
.panel-spinner.hidden { display: none; }
|
| 707 |
+
.spinner-ring {
|
| 708 |
+
width: 36px;
|
| 709 |
+
height: 36px;
|
| 710 |
+
border: 3px solid #d4c9b8;
|
| 711 |
+
border-top-color: #5b6cf9;
|
| 712 |
+
border-radius: 50%;
|
| 713 |
+
animation: spin 0.8s linear infinite;
|
| 714 |
+
}
|
| 715 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 716 |
+
|
| 717 |
+
/* Welcome / idle state */
|
| 718 |
+
#welcome-overlay {
|
| 719 |
+
position: absolute;
|
| 720 |
+
inset: 0;
|
| 721 |
+
display: flex;
|
| 722 |
+
align-items: center;
|
| 723 |
+
justify-content: center;
|
| 724 |
+
background: #f5f0e8;
|
| 725 |
+
z-index: 20;
|
| 726 |
+
flex-direction: column;
|
| 727 |
+
gap: 10px;
|
| 728 |
+
text-align: center;
|
| 729 |
+
}
|
| 730 |
+
#welcome-overlay h2 { font-size: 20px; color: #2e2820; margin: 0; }
|
| 731 |
+
#welcome-overlay p { font-size: 13px; color: #7a6e60; margin: 0; max-width: 320px; }
|
| 732 |
+
#welcome-overlay.hidden { display: none; }
|
| 733 |
+
|
| 734 |
+
/* Policy color variables */
|
| 735 |
+
:root {
|
| 736 |
+
--color-no-intervention: #94a3b8;
|
| 737 |
+
--color-fixed: #f59e0b;
|
| 738 |
+
--color-random: #10b981;
|
| 739 |
+
--color-learned: #5b6cf9;
|
| 740 |
+
--color-llm-dqn: #ec4899;
|
| 741 |
+
--color-dqn-heuristic: #06b6d4;
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
/* Responsive: if viewport too narrow, collapse sidebar */
|
| 745 |
+
@media (max-width: 600px) {
|
| 746 |
+
#sidebar { width: 200px; min-width: 200px; }
|
| 747 |
+
}
|
third_party/CityFlow/setup.py
CHANGED
|
@@ -1,70 +1,70 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import re
|
| 3 |
-
import sys
|
| 4 |
-
import platform
|
| 5 |
-
import subprocess
|
| 6 |
-
|
| 7 |
-
from setuptools import setup, Extension
|
| 8 |
-
from setuptools.command.build_ext import build_ext
|
| 9 |
-
from distutils.version import LooseVersion
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
class CMakeExtension(Extension):
|
| 13 |
-
def __init__(self, name, sourcedir=''):
|
| 14 |
-
Extension.__init__(self, name, sources=[])
|
| 15 |
-
self.sourcedir = os.path.abspath(sourcedir)
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
class CMakeBuild(build_ext):
|
| 19 |
-
def run(self):
|
| 20 |
-
try:
|
| 21 |
-
out = subprocess.check_output(['cmake', '--version'])
|
| 22 |
-
except OSError:
|
| 23 |
-
raise RuntimeError("CMake must be installed to build the following extensions: " +
|
| 24 |
-
", ".join(e.name for e in self.extensions))
|
| 25 |
-
|
| 26 |
-
if platform.system() == "Windows":
|
| 27 |
-
cmake_version = LooseVersion(re.search(r'version\s*([\d.]+)', out.decode()).group(1))
|
| 28 |
-
if cmake_version < '3.1.0':
|
| 29 |
-
raise RuntimeError("CMake >= 3.1.0 is required on Windows")
|
| 30 |
-
|
| 31 |
-
for ext in self.extensions:
|
| 32 |
-
self.build_extension(ext)
|
| 33 |
-
|
| 34 |
-
def build_extension(self, ext):
|
| 35 |
-
extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name)))
|
| 36 |
-
cmake_args = ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=' + extdir,
|
| 37 |
-
'-DPYTHON_EXECUTABLE=' + sys.executable,
|
| 38 |
-
'-DVERSION="' + self.distribution.get_version() + '"']
|
| 39 |
-
|
| 40 |
-
cfg = 'Debug' if self.debug else 'Release'
|
| 41 |
-
build_args = ['--config', cfg]
|
| 42 |
-
|
| 43 |
-
if platform.system() == "Windows":
|
| 44 |
-
cmake_args += ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}'.format(cfg.upper(), extdir)]
|
| 45 |
-
if sys.maxsize > 2**32:
|
| 46 |
-
cmake_args += ['-A', 'x64']
|
| 47 |
-
build_args += ['--', '/m']
|
| 48 |
-
else:
|
| 49 |
-
cmake_args += ['-DCMAKE_BUILD_TYPE=' + cfg]
|
| 50 |
-
build_args += ['--', '-j2']
|
| 51 |
-
|
| 52 |
-
env = os.environ.copy()
|
| 53 |
-
|
| 54 |
-
if not os.path.exists(self.build_temp):
|
| 55 |
-
os.makedirs(self.build_temp)
|
| 56 |
-
subprocess.check_call(['cmake', ext.sourcedir] + cmake_args, cwd=self.build_temp, env=env)
|
| 57 |
-
subprocess.check_call(['cmake', '--build', '.', '--target', 'cityflow'] + build_args, cwd=self.build_temp)
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
setup(
|
| 61 |
-
name='CityFlow',
|
| 62 |
-
version='0.1',
|
| 63 |
-
author='Huichu Zhang',
|
| 64 |
-
author_email='zhc@apex.sjtu.edu.cn',
|
| 65 |
-
description='CityFlow: A Multi-Agent Reinforcement Learning Environment for Large Scale City Traffic Scenario',
|
| 66 |
-
long_description='',
|
| 67 |
-
ext_modules=[CMakeExtension('cityflow')],
|
| 68 |
-
cmdclass=dict(build_ext=CMakeBuild),
|
| 69 |
-
zip_safe=False
|
| 70 |
-
)
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import sys
|
| 4 |
+
import platform
|
| 5 |
+
import subprocess
|
| 6 |
+
|
| 7 |
+
from setuptools import setup, Extension
|
| 8 |
+
from setuptools.command.build_ext import build_ext
|
| 9 |
+
from distutils.version import LooseVersion
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class CMakeExtension(Extension):
|
| 13 |
+
def __init__(self, name, sourcedir=''):
|
| 14 |
+
Extension.__init__(self, name, sources=[])
|
| 15 |
+
self.sourcedir = os.path.abspath(sourcedir)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class CMakeBuild(build_ext):
|
| 19 |
+
def run(self):
|
| 20 |
+
try:
|
| 21 |
+
out = subprocess.check_output(['cmake', '--version'])
|
| 22 |
+
except OSError:
|
| 23 |
+
raise RuntimeError("CMake must be installed to build the following extensions: " +
|
| 24 |
+
", ".join(e.name for e in self.extensions))
|
| 25 |
+
|
| 26 |
+
if platform.system() == "Windows":
|
| 27 |
+
cmake_version = LooseVersion(re.search(r'version\s*([\d.]+)', out.decode()).group(1))
|
| 28 |
+
if cmake_version < '3.1.0':
|
| 29 |
+
raise RuntimeError("CMake >= 3.1.0 is required on Windows")
|
| 30 |
+
|
| 31 |
+
for ext in self.extensions:
|
| 32 |
+
self.build_extension(ext)
|
| 33 |
+
|
| 34 |
+
def build_extension(self, ext):
|
| 35 |
+
extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name)))
|
| 36 |
+
cmake_args = ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=' + extdir,
|
| 37 |
+
'-DPYTHON_EXECUTABLE=' + sys.executable,
|
| 38 |
+
'-DVERSION="' + self.distribution.get_version() + '"']
|
| 39 |
+
|
| 40 |
+
cfg = 'Debug' if self.debug else 'Release'
|
| 41 |
+
build_args = ['--config', cfg]
|
| 42 |
+
|
| 43 |
+
if platform.system() == "Windows":
|
| 44 |
+
cmake_args += ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}'.format(cfg.upper(), extdir)]
|
| 45 |
+
if sys.maxsize > 2**32:
|
| 46 |
+
cmake_args += ['-A', 'x64']
|
| 47 |
+
build_args += ['--', '/m']
|
| 48 |
+
else:
|
| 49 |
+
cmake_args += ['-DCMAKE_BUILD_TYPE=' + cfg]
|
| 50 |
+
build_args += ['--', '-j2']
|
| 51 |
+
|
| 52 |
+
env = os.environ.copy()
|
| 53 |
+
|
| 54 |
+
if not os.path.exists(self.build_temp):
|
| 55 |
+
os.makedirs(self.build_temp)
|
| 56 |
+
subprocess.check_call(['cmake', ext.sourcedir] + cmake_args, cwd=self.build_temp, env=env)
|
| 57 |
+
subprocess.check_call(['cmake', '--build', '.', '--target', 'cityflow'] + build_args, cwd=self.build_temp)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
setup(
|
| 61 |
+
name='CityFlow',
|
| 62 |
+
version='0.1',
|
| 63 |
+
author='Huichu Zhang',
|
| 64 |
+
author_email='zhc@apex.sjtu.edu.cn',
|
| 65 |
+
description='CityFlow: A Multi-Agent Reinforcement Learning Environment for Large Scale City Traffic Scenario',
|
| 66 |
+
long_description='',
|
| 67 |
+
ext_modules=[CMakeExtension('cityflow')],
|
| 68 |
+
cmdclass=dict(build_ext=CMakeBuild),
|
| 69 |
+
zip_safe=False
|
| 70 |
+
)
|
third_party/CityFlow/src/CMakeLists.txt
CHANGED
|
@@ -1,36 +1,36 @@
|
|
| 1 |
-
set(PROJECT_HEADER_FILES
|
| 2 |
-
utility/config.h
|
| 3 |
-
utility/utility.h
|
| 4 |
-
utility/barrier.h
|
| 5 |
-
utility/optionparser.h
|
| 6 |
-
engine/archive.h
|
| 7 |
-
engine/engine.h
|
| 8 |
-
flow/flow.h
|
| 9 |
-
flow/route.h
|
| 10 |
-
roadnet/roadnet.h
|
| 11 |
-
roadnet/trafficlight.h
|
| 12 |
-
vehicle/router.h
|
| 13 |
-
vehicle/vehicle.h
|
| 14 |
-
vehicle/lanechange.h
|
| 15 |
-
)
|
| 16 |
-
|
| 17 |
-
set(PROJECT_SOURCE_FILES
|
| 18 |
-
utility/utility.cpp
|
| 19 |
-
utility/barrier.cpp
|
| 20 |
-
engine/archive.cpp
|
| 21 |
-
engine/engine.cpp
|
| 22 |
-
flow/flow.cpp
|
| 23 |
-
roadnet/roadnet.cpp
|
| 24 |
-
roadnet/trafficlight.cpp
|
| 25 |
-
vehicle/router.cpp
|
| 26 |
-
vehicle/vehicle.cpp
|
| 27 |
-
vehicle/lanechange.cpp)
|
| 28 |
-
|
| 29 |
-
set(PROJECT_LIB_NAME ${PROJECT_NAME}_lib CACHE INTERNAL "")
|
| 30 |
-
|
| 31 |
-
find_package(Threads REQUIRED)
|
| 32 |
-
|
| 33 |
-
add_library(${PROJECT_LIB_NAME} ${PROJECT_HEADER_FILES} ${PROJECT_SOURCE_FILES})
|
| 34 |
-
set_target_properties(${PROJECT_LIB_NAME} PROPERTIES CXX_VISIBILITY_PRESET "hidden")
|
| 35 |
-
target_link_libraries(${PROJECT_LIB_NAME} PRIVATE Threads::Threads)
|
| 36 |
target_include_directories(${PROJECT_LIB_NAME} PUBLIC ${CMAKE_CURRENT_LIST_DIR})
|
|
|
|
| 1 |
+
set(PROJECT_HEADER_FILES
|
| 2 |
+
utility/config.h
|
| 3 |
+
utility/utility.h
|
| 4 |
+
utility/barrier.h
|
| 5 |
+
utility/optionparser.h
|
| 6 |
+
engine/archive.h
|
| 7 |
+
engine/engine.h
|
| 8 |
+
flow/flow.h
|
| 9 |
+
flow/route.h
|
| 10 |
+
roadnet/roadnet.h
|
| 11 |
+
roadnet/trafficlight.h
|
| 12 |
+
vehicle/router.h
|
| 13 |
+
vehicle/vehicle.h
|
| 14 |
+
vehicle/lanechange.h
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
set(PROJECT_SOURCE_FILES
|
| 18 |
+
utility/utility.cpp
|
| 19 |
+
utility/barrier.cpp
|
| 20 |
+
engine/archive.cpp
|
| 21 |
+
engine/engine.cpp
|
| 22 |
+
flow/flow.cpp
|
| 23 |
+
roadnet/roadnet.cpp
|
| 24 |
+
roadnet/trafficlight.cpp
|
| 25 |
+
vehicle/router.cpp
|
| 26 |
+
vehicle/vehicle.cpp
|
| 27 |
+
vehicle/lanechange.cpp)
|
| 28 |
+
|
| 29 |
+
set(PROJECT_LIB_NAME ${PROJECT_NAME}_lib CACHE INTERNAL "")
|
| 30 |
+
|
| 31 |
+
find_package(Threads REQUIRED)
|
| 32 |
+
|
| 33 |
+
add_library(${PROJECT_LIB_NAME} ${PROJECT_HEADER_FILES} ${PROJECT_SOURCE_FILES})
|
| 34 |
+
set_target_properties(${PROJECT_LIB_NAME} PROPERTIES CXX_VISIBILITY_PRESET "hidden")
|
| 35 |
+
target_link_libraries(${PROJECT_LIB_NAME} PRIVATE Threads::Threads)
|
| 36 |
target_include_directories(${PROJECT_LIB_NAME} PUBLIC ${CMAKE_CURRENT_LIST_DIR})
|
third_party/CityFlow/src/cityflow.cpp
CHANGED
|
@@ -1,48 +1,48 @@
|
|
| 1 |
-
#include "engine/engine.h"
|
| 2 |
-
#include "engine/archive.h"
|
| 3 |
-
|
| 4 |
-
#include "pybind11/pybind11.h"
|
| 5 |
-
#include "pybind11/stl.h"
|
| 6 |
-
|
| 7 |
-
namespace py = pybind11;
|
| 8 |
-
using namespace py::literals;
|
| 9 |
-
|
| 10 |
-
PYBIND11_MODULE(cityflow, m) {
|
| 11 |
-
py::class_<CityFlow::Engine>(m, "Engine")
|
| 12 |
-
.def(py::init<const std::string&, int>(),
|
| 13 |
-
"config_file"_a,
|
| 14 |
-
"thread_num"_a=1
|
| 15 |
-
)
|
| 16 |
-
.def("next_step", &CityFlow::Engine::nextStep)
|
| 17 |
-
.def("get_vehicle_count", &CityFlow::Engine::getVehicleCount)
|
| 18 |
-
.def("get_vehicles", &CityFlow::Engine::getVehicles, "include_waiting"_a=false)
|
| 19 |
-
.def("get_lane_vehicle_count", &CityFlow::Engine::getLaneVehicleCount)
|
| 20 |
-
.def("get_lane_waiting_vehicle_count", &CityFlow::Engine::getLaneWaitingVehicleCount)
|
| 21 |
-
.def("get_lane_vehicles", &CityFlow::Engine::getLaneVehicles)
|
| 22 |
-
.def("get_vehicle_speed", &CityFlow::Engine::getVehicleSpeed)
|
| 23 |
-
.def("get_vehicle_info", &CityFlow::Engine::getVehicleInfo, "vehicle_id"_a)
|
| 24 |
-
.def("get_vehicle_distance", &CityFlow::Engine::getVehicleDistance)
|
| 25 |
-
.def("get_leader", &CityFlow::Engine::getLeader, "vehicle_id"_a)
|
| 26 |
-
.def("get_current_time", &CityFlow::Engine::getCurrentTime)
|
| 27 |
-
.def("get_average_travel_time", &CityFlow::Engine::getAverageTravelTime)
|
| 28 |
-
.def("set_tl_phase", &CityFlow::Engine::setTrafficLightPhase, "intersection_id"_a, "phase_id"_a)
|
| 29 |
-
.def("set_vehicle_speed", &CityFlow::Engine::setVehicleSpeed, "vehicle_id"_a, "speed"_a)
|
| 30 |
-
.def("set_replay_file", &CityFlow::Engine::setReplayLogFile, "replay_file"_a)
|
| 31 |
-
.def("set_random_seed", &CityFlow::Engine::setRandomSeed, "seed"_a)
|
| 32 |
-
.def("set_save_replay", &CityFlow::Engine::setSaveReplay, "open"_a)
|
| 33 |
-
.def("push_vehicle", (void (CityFlow::Engine::*)(const std::map<std::string, double>&, const std::vector<std::string>&)) &CityFlow::Engine::pushVehicle)
|
| 34 |
-
.def("reset", &CityFlow::Engine::reset, "seed"_a=false)
|
| 35 |
-
.def("load", &CityFlow::Engine::load, "archive"_a)
|
| 36 |
-
.def("snapshot", &CityFlow::Engine::snapshot)
|
| 37 |
-
.def("load_from_file", &CityFlow::Engine::loadFromFile, "path"_a)
|
| 38 |
-
.def("set_vehicle_route", &CityFlow::Engine::setRoute, "vehicle_id"_a, "route"_a);
|
| 39 |
-
|
| 40 |
-
py::class_<CityFlow::Archive>(m, "Archive")
|
| 41 |
-
.def(py::init<const CityFlow::Engine&>())
|
| 42 |
-
.def("dump", &CityFlow::Archive::dump, "path"_a);
|
| 43 |
-
#ifdef VERSION
|
| 44 |
-
m.attr("__version__") = VERSION;
|
| 45 |
-
#else
|
| 46 |
-
m.attr("__version__") = "dev";
|
| 47 |
-
#endif
|
| 48 |
}
|
|
|
|
| 1 |
+
#include "engine/engine.h"
|
| 2 |
+
#include "engine/archive.h"
|
| 3 |
+
|
| 4 |
+
#include "pybind11/pybind11.h"
|
| 5 |
+
#include "pybind11/stl.h"
|
| 6 |
+
|
| 7 |
+
namespace py = pybind11;
|
| 8 |
+
using namespace py::literals;
|
| 9 |
+
|
| 10 |
+
PYBIND11_MODULE(cityflow, m) {
|
| 11 |
+
py::class_<CityFlow::Engine>(m, "Engine")
|
| 12 |
+
.def(py::init<const std::string&, int>(),
|
| 13 |
+
"config_file"_a,
|
| 14 |
+
"thread_num"_a=1
|
| 15 |
+
)
|
| 16 |
+
.def("next_step", &CityFlow::Engine::nextStep)
|
| 17 |
+
.def("get_vehicle_count", &CityFlow::Engine::getVehicleCount)
|
| 18 |
+
.def("get_vehicles", &CityFlow::Engine::getVehicles, "include_waiting"_a=false)
|
| 19 |
+
.def("get_lane_vehicle_count", &CityFlow::Engine::getLaneVehicleCount)
|
| 20 |
+
.def("get_lane_waiting_vehicle_count", &CityFlow::Engine::getLaneWaitingVehicleCount)
|
| 21 |
+
.def("get_lane_vehicles", &CityFlow::Engine::getLaneVehicles)
|
| 22 |
+
.def("get_vehicle_speed", &CityFlow::Engine::getVehicleSpeed)
|
| 23 |
+
.def("get_vehicle_info", &CityFlow::Engine::getVehicleInfo, "vehicle_id"_a)
|
| 24 |
+
.def("get_vehicle_distance", &CityFlow::Engine::getVehicleDistance)
|
| 25 |
+
.def("get_leader", &CityFlow::Engine::getLeader, "vehicle_id"_a)
|
| 26 |
+
.def("get_current_time", &CityFlow::Engine::getCurrentTime)
|
| 27 |
+
.def("get_average_travel_time", &CityFlow::Engine::getAverageTravelTime)
|
| 28 |
+
.def("set_tl_phase", &CityFlow::Engine::setTrafficLightPhase, "intersection_id"_a, "phase_id"_a)
|
| 29 |
+
.def("set_vehicle_speed", &CityFlow::Engine::setVehicleSpeed, "vehicle_id"_a, "speed"_a)
|
| 30 |
+
.def("set_replay_file", &CityFlow::Engine::setReplayLogFile, "replay_file"_a)
|
| 31 |
+
.def("set_random_seed", &CityFlow::Engine::setRandomSeed, "seed"_a)
|
| 32 |
+
.def("set_save_replay", &CityFlow::Engine::setSaveReplay, "open"_a)
|
| 33 |
+
.def("push_vehicle", (void (CityFlow::Engine::*)(const std::map<std::string, double>&, const std::vector<std::string>&)) &CityFlow::Engine::pushVehicle)
|
| 34 |
+
.def("reset", &CityFlow::Engine::reset, "seed"_a=false)
|
| 35 |
+
.def("load", &CityFlow::Engine::load, "archive"_a)
|
| 36 |
+
.def("snapshot", &CityFlow::Engine::snapshot)
|
| 37 |
+
.def("load_from_file", &CityFlow::Engine::loadFromFile, "path"_a)
|
| 38 |
+
.def("set_vehicle_route", &CityFlow::Engine::setRoute, "vehicle_id"_a, "route"_a);
|
| 39 |
+
|
| 40 |
+
py::class_<CityFlow::Archive>(m, "Archive")
|
| 41 |
+
.def(py::init<const CityFlow::Engine&>())
|
| 42 |
+
.def("dump", &CityFlow::Archive::dump, "path"_a);
|
| 43 |
+
#ifdef VERSION
|
| 44 |
+
m.attr("__version__") = VERSION;
|
| 45 |
+
#else
|
| 46 |
+
m.attr("__version__") = "dev";
|
| 47 |
+
#endif
|
| 48 |
}
|
third_party/CityFlow/tests/CMakeLists.txt
CHANGED
|
@@ -1,14 +1,14 @@
|
|
| 1 |
-
file(GLOB TEST_SRCS cpp/*.cpp)
|
| 2 |
-
set(FLAGS "-fsanitize=address -fno-omit-frame-pointer")
|
| 3 |
-
include_directories(${GTEST_INCLUDE_DIRS})
|
| 4 |
-
foreach(SRC_PATH ${TEST_SRCS})
|
| 5 |
-
get_filename_component(SRC_NAME ${SRC_PATH} NAME)
|
| 6 |
-
string(REPLACE ".cpp" "" EXE_NAME ${SRC_NAME})
|
| 7 |
-
add_executable(${EXE_NAME} ${SRC_PATH})
|
| 8 |
-
set_target_properties(${EXE_NAME} PROPERTIES APPEND_STRING PROPERTY LINK_FLAGS "${FLAGS}")
|
| 9 |
-
set_target_properties(${EXE_NAME} PROPERTIES APPEND_STRING PROPERTY COMPILE_FLAGS "${FLAGS}")
|
| 10 |
-
target_link_libraries(${EXE_NAME} PUBLIC ${PROJECT_LIB_NAME} ${GTEST_BOTH_LIBRARIES})
|
| 11 |
-
add_test(NAME ${EXE_NAME}
|
| 12 |
-
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
|
| 13 |
-
COMMAND ${EXE_NAME})
|
| 14 |
endforeach()
|
|
|
|
| 1 |
+
file(GLOB TEST_SRCS cpp/*.cpp)
|
| 2 |
+
set(FLAGS "-fsanitize=address -fno-omit-frame-pointer")
|
| 3 |
+
include_directories(${GTEST_INCLUDE_DIRS})
|
| 4 |
+
foreach(SRC_PATH ${TEST_SRCS})
|
| 5 |
+
get_filename_component(SRC_NAME ${SRC_PATH} NAME)
|
| 6 |
+
string(REPLACE ".cpp" "" EXE_NAME ${SRC_NAME})
|
| 7 |
+
add_executable(${EXE_NAME} ${SRC_PATH})
|
| 8 |
+
set_target_properties(${EXE_NAME} PROPERTIES APPEND_STRING PROPERTY LINK_FLAGS "${FLAGS}")
|
| 9 |
+
set_target_properties(${EXE_NAME} PROPERTIES APPEND_STRING PROPERTY COMPILE_FLAGS "${FLAGS}")
|
| 10 |
+
target_link_libraries(${EXE_NAME} PUBLIC ${PROJECT_LIB_NAME} ${GTEST_BOTH_LIBRARIES})
|
| 11 |
+
add_test(NAME ${EXE_NAME}
|
| 12 |
+
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
|
| 13 |
+
COMMAND ${EXE_NAME})
|
| 14 |
endforeach()
|
third_party/CityFlow/tools/debug/CMakeLists.txt
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
-
set(SOURCE_FILES simple_run.cpp)
|
| 2 |
-
|
| 3 |
-
foreach(SRC_PATH ${SOURCE_FILES})
|
| 4 |
-
get_filename_component(SRC_NAME ${SRC_PATH} NAME)
|
| 5 |
-
string(REPLACE ".cpp" "" EXE_NAME ${SRC_NAME})
|
| 6 |
-
add_executable(${EXE_NAME} ${SRC_PATH})
|
| 7 |
-
set_target_properties(${EXE_NAME} PROPERTIES CXX_VISIBILITY_PRESET "hidden")
|
| 8 |
-
target_link_libraries(${EXE_NAME} PRIVATE ${PROJECT_LIB_NAME})
|
| 9 |
endforeach()
|
|
|
|
| 1 |
+
set(SOURCE_FILES simple_run.cpp)
|
| 2 |
+
|
| 3 |
+
foreach(SRC_PATH ${SOURCE_FILES})
|
| 4 |
+
get_filename_component(SRC_NAME ${SRC_PATH} NAME)
|
| 5 |
+
string(REPLACE ".cpp" "" EXE_NAME ${SRC_NAME})
|
| 6 |
+
add_executable(${EXE_NAME} ${SRC_PATH})
|
| 7 |
+
set_target_properties(${EXE_NAME} PROPERTIES CXX_VISIBILITY_PRESET "hidden")
|
| 8 |
+
target_link_libraries(${EXE_NAME} PRIVATE ${PROJECT_LIB_NAME})
|
| 9 |
endforeach()
|
third_party/CityFlow/tools/debug/simple_run.cpp
CHANGED
|
@@ -1,59 +1,59 @@
|
|
| 1 |
-
#include "engine/engine.h"
|
| 2 |
-
#include "utility/optionparser.h"
|
| 3 |
-
|
| 4 |
-
#include <string>
|
| 5 |
-
#include <iostream>
|
| 6 |
-
#include <cstdlib>
|
| 7 |
-
#include <ctime>
|
| 8 |
-
|
| 9 |
-
using namespace CityFlow;
|
| 10 |
-
|
| 11 |
-
int main(int argc, char const *argv[]) {
|
| 12 |
-
optionparser::OptionParser parser;
|
| 13 |
-
|
| 14 |
-
parser.add_option("--configFile", "-c")
|
| 15 |
-
.help("config file")
|
| 16 |
-
.mode(optionparser::StorageMode::STORE_VALUE)
|
| 17 |
-
.required(true);
|
| 18 |
-
|
| 19 |
-
parser.add_option("--totalStep", "-s")
|
| 20 |
-
.help("simulation steps")
|
| 21 |
-
.default_value(1000)
|
| 22 |
-
.mode(optionparser::StorageMode::STORE_VALUE);
|
| 23 |
-
|
| 24 |
-
parser.add_option("--threadNum", "-t")
|
| 25 |
-
.help("number of threads")
|
| 26 |
-
.default_value(1)
|
| 27 |
-
.mode(optionparser::StorageMode::STORE_VALUE);
|
| 28 |
-
|
| 29 |
-
parser.add_option("--verbose", "-v")
|
| 30 |
-
.help("be verbose")
|
| 31 |
-
.mode(optionparser::StorageMode::STORE_TRUE);
|
| 32 |
-
|
| 33 |
-
parser.eat_arguments(argc, argv);
|
| 34 |
-
std::string configFile = parser.get_value<std::string>("configFile");
|
| 35 |
-
bool verbose = parser.get_value("verbose");
|
| 36 |
-
size_t totalStep = parser.get_value<int>("totalStep");
|
| 37 |
-
size_t threadNum = parser.get_value<int>("threadNum");
|
| 38 |
-
|
| 39 |
-
std::string dataDir(std::getenv("DATADIR"));
|
| 40 |
-
|
| 41 |
-
Engine engine(dataDir + configFile, (size_t) threadNum);
|
| 42 |
-
time_t startTime, endTime;
|
| 43 |
-
time(&startTime);
|
| 44 |
-
for (int i = 0; i < totalStep; i++) {
|
| 45 |
-
if (verbose) {
|
| 46 |
-
std::cout << i << " " << engine.getVehicleCount() << std::endl;
|
| 47 |
-
}
|
| 48 |
-
engine.nextStep();
|
| 49 |
-
//engine.getVehicleSpeed();
|
| 50 |
-
//engine.getLaneVehicles();
|
| 51 |
-
//engine.getLaneWaitingVehicleCount();
|
| 52 |
-
//engine.getVehicleDistance();
|
| 53 |
-
//engine.getCurrentTime();
|
| 54 |
-
}
|
| 55 |
-
time(&endTime);
|
| 56 |
-
std::cout << "Total Step: " << totalStep << std::endl;
|
| 57 |
-
std::cout << "Total Time: " << (endTime - startTime) << "s" << std::endl;
|
| 58 |
-
return 0;
|
| 59 |
}
|
|
|
|
| 1 |
+
#include "engine/engine.h"
|
| 2 |
+
#include "utility/optionparser.h"
|
| 3 |
+
|
| 4 |
+
#include <string>
|
| 5 |
+
#include <iostream>
|
| 6 |
+
#include <cstdlib>
|
| 7 |
+
#include <ctime>
|
| 8 |
+
|
| 9 |
+
using namespace CityFlow;
|
| 10 |
+
|
| 11 |
+
int main(int argc, char const *argv[]) {
|
| 12 |
+
optionparser::OptionParser parser;
|
| 13 |
+
|
| 14 |
+
parser.add_option("--configFile", "-c")
|
| 15 |
+
.help("config file")
|
| 16 |
+
.mode(optionparser::StorageMode::STORE_VALUE)
|
| 17 |
+
.required(true);
|
| 18 |
+
|
| 19 |
+
parser.add_option("--totalStep", "-s")
|
| 20 |
+
.help("simulation steps")
|
| 21 |
+
.default_value(1000)
|
| 22 |
+
.mode(optionparser::StorageMode::STORE_VALUE);
|
| 23 |
+
|
| 24 |
+
parser.add_option("--threadNum", "-t")
|
| 25 |
+
.help("number of threads")
|
| 26 |
+
.default_value(1)
|
| 27 |
+
.mode(optionparser::StorageMode::STORE_VALUE);
|
| 28 |
+
|
| 29 |
+
parser.add_option("--verbose", "-v")
|
| 30 |
+
.help("be verbose")
|
| 31 |
+
.mode(optionparser::StorageMode::STORE_TRUE);
|
| 32 |
+
|
| 33 |
+
parser.eat_arguments(argc, argv);
|
| 34 |
+
std::string configFile = parser.get_value<std::string>("configFile");
|
| 35 |
+
bool verbose = parser.get_value("verbose");
|
| 36 |
+
size_t totalStep = parser.get_value<int>("totalStep");
|
| 37 |
+
size_t threadNum = parser.get_value<int>("threadNum");
|
| 38 |
+
|
| 39 |
+
std::string dataDir(std::getenv("DATADIR"));
|
| 40 |
+
|
| 41 |
+
Engine engine(dataDir + configFile, (size_t) threadNum);
|
| 42 |
+
time_t startTime, endTime;
|
| 43 |
+
time(&startTime);
|
| 44 |
+
for (int i = 0; i < totalStep; i++) {
|
| 45 |
+
if (verbose) {
|
| 46 |
+
std::cout << i << " " << engine.getVehicleCount() << std::endl;
|
| 47 |
+
}
|
| 48 |
+
engine.nextStep();
|
| 49 |
+
//engine.getVehicleSpeed();
|
| 50 |
+
//engine.getLaneVehicles();
|
| 51 |
+
//engine.getLaneWaitingVehicleCount();
|
| 52 |
+
//engine.getVehicleDistance();
|
| 53 |
+
//engine.getCurrentTime();
|
| 54 |
+
}
|
| 55 |
+
time(&endTime);
|
| 56 |
+
std::cout << "Total Step: " << totalStep << std::endl;
|
| 57 |
+
std::cout << "Total Time: " << (endTime - startTime) << "s" << std::endl;
|
| 58 |
+
return 0;
|
| 59 |
}
|
third_party/CityFlow/tools/generator/generate_grid_scenario.py
CHANGED
|
@@ -1,127 +1,127 @@
|
|
| 1 |
-
import argparse
|
| 2 |
-
import json
|
| 3 |
-
import os
|
| 4 |
-
from generate_json_from_grid import gridToRoadnet
|
| 5 |
-
|
| 6 |
-
def parse_args():
|
| 7 |
-
parser = argparse.ArgumentParser()
|
| 8 |
-
parser.add_argument("rowNum", type=int)
|
| 9 |
-
parser.add_argument("colNum", type=int)
|
| 10 |
-
parser.add_argument("--rowDistance", type=int, default=300)
|
| 11 |
-
parser.add_argument("--columnDistance", type=int, default=300)
|
| 12 |
-
parser.add_argument("--intersectionWidth", type=int, default=30)
|
| 13 |
-
parser.add_argument("--numLeftLanes", type=int, default=1)
|
| 14 |
-
parser.add_argument("--numStraightLanes", type=int, default=1)
|
| 15 |
-
parser.add_argument("--numRightLanes", type=int, default=1)
|
| 16 |
-
parser.add_argument("--laneMaxSpeed", type=float, default=16.67)
|
| 17 |
-
parser.add_argument("--vehLen", type=float, default=5.0)
|
| 18 |
-
parser.add_argument("--vehWidth", type=float, default=2.0)
|
| 19 |
-
parser.add_argument("--vehMaxPosAcc", type=float, default=2.0)
|
| 20 |
-
parser.add_argument("--vehMaxNegAcc", type=float, default=4.5)
|
| 21 |
-
parser.add_argument("--vehUsualPosAcc", type=float, default=2.0)
|
| 22 |
-
parser.add_argument("--vehUsualNegAcc", type=float, default=4.5)
|
| 23 |
-
parser.add_argument("--vehMinGap", type=float, default=2.5)
|
| 24 |
-
parser.add_argument("--vehMaxSpeed", type=float, default=16.67)
|
| 25 |
-
parser.add_argument("--vehHeadwayTime", type=float, default=1.5)
|
| 26 |
-
parser.add_argument("--dir", type=str, default="./")
|
| 27 |
-
parser.add_argument("--roadnetFile", type=str)
|
| 28 |
-
parser.add_argument("--turn", action="store_true")
|
| 29 |
-
parser.add_argument("--tlPlan", action="store_true")
|
| 30 |
-
parser.add_argument("--interval", type=float, default=2.0)
|
| 31 |
-
parser.add_argument("--flowFile", type=str)
|
| 32 |
-
return parser.parse_args()
|
| 33 |
-
|
| 34 |
-
def generate_route(rowNum, colNum, turn=False):
|
| 35 |
-
routes = []
|
| 36 |
-
move = [(1, 0), (0, 1), (-1, 0), (0, -1)]
|
| 37 |
-
|
| 38 |
-
def get_straight_route(start, direction, step):
|
| 39 |
-
x, y = start
|
| 40 |
-
route = []
|
| 41 |
-
for _ in range(step):
|
| 42 |
-
route.append("road_%d_%d_%d" % (x, y, direction))
|
| 43 |
-
x += move[direction][0]
|
| 44 |
-
y += move[direction][1]
|
| 45 |
-
return route
|
| 46 |
-
|
| 47 |
-
for i in range(1, rowNum+1):
|
| 48 |
-
routes.append(get_straight_route((0, i), 0, colNum+1))
|
| 49 |
-
routes.append(get_straight_route((colNum+1, i), 2, colNum+1))
|
| 50 |
-
for i in range(1, colNum+1):
|
| 51 |
-
routes.append(get_straight_route((i, 0), 1, rowNum+1))
|
| 52 |
-
routes.append(get_straight_route((i, rowNum+1), 3, rowNum+1))
|
| 53 |
-
|
| 54 |
-
if turn:
|
| 55 |
-
def get_turn_route(start, direction):
|
| 56 |
-
if direction[0] % 2 == 0:
|
| 57 |
-
step = min(rowNum*2, colNum*2+1)
|
| 58 |
-
else:
|
| 59 |
-
step = min(colNum*2, rowNum*2+1)
|
| 60 |
-
x, y = start
|
| 61 |
-
route = []
|
| 62 |
-
cur = 0
|
| 63 |
-
for _ in range(step):
|
| 64 |
-
route.append("road_%d_%d_%d" % (x, y, direction[cur]))
|
| 65 |
-
x += move[direction[cur]][0]
|
| 66 |
-
y += move[direction[cur]][1]
|
| 67 |
-
cur = 1 - cur
|
| 68 |
-
return route
|
| 69 |
-
|
| 70 |
-
routes.append(get_turn_route((1, 0), (1, 0)))
|
| 71 |
-
routes.append(get_turn_route((0, 1), (0, 1)))
|
| 72 |
-
routes.append(get_turn_route((colNum+1, rowNum), (2, 3)))
|
| 73 |
-
routes.append(get_turn_route((colNum, rowNum+1), (3, 2)))
|
| 74 |
-
routes.append(get_turn_route((0, rowNum), (0, 3)))
|
| 75 |
-
routes.append(get_turn_route((1, rowNum+1), (3, 0)))
|
| 76 |
-
routes.append(get_turn_route((colNum+1, 1), (2, 1)))
|
| 77 |
-
routes.append(get_turn_route((colNum, 0), (1, 2)))
|
| 78 |
-
|
| 79 |
-
return routes
|
| 80 |
-
|
| 81 |
-
if __name__ == '__main__':
|
| 82 |
-
args = parse_args()
|
| 83 |
-
if args.roadnetFile is None:
|
| 84 |
-
args.roadnetFile = "roadnet_%d_%d%s.json" % (args.rowNum, args.colNum, "_turn" if args.turn else "")
|
| 85 |
-
if args.flowFile is None:
|
| 86 |
-
args.flowFile = "flow_%d_%d%s.json" % (args.rowNum, args.colNum, "_turn" if args.turn else "")
|
| 87 |
-
|
| 88 |
-
grid = {
|
| 89 |
-
"rowNumber": args.rowNum,
|
| 90 |
-
"columnNumber": args.colNum,
|
| 91 |
-
"rowDistances": [args.rowDistance] * (args.colNum-1),
|
| 92 |
-
"columnDistances": [args.columnDistance] * (args.rowNum-1),
|
| 93 |
-
"outRowDistance": args.rowDistance,
|
| 94 |
-
"outColumnDistance": args.columnDistance,
|
| 95 |
-
"intersectionWidths": [[args.intersectionWidth] * args.colNum] * args.rowNum,
|
| 96 |
-
"numLeftLanes": args.numLeftLanes,
|
| 97 |
-
"numStraightLanes": args.numStraightLanes,
|
| 98 |
-
"numRightLanes": args.numRightLanes,
|
| 99 |
-
"laneMaxSpeed": args.laneMaxSpeed,
|
| 100 |
-
"tlPlan": args.tlPlan
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
json.dump(gridToRoadnet(**grid), open(os.path.join(args.dir, args.roadnetFile), "w"), indent=2)
|
| 104 |
-
|
| 105 |
-
vehicle_template = {
|
| 106 |
-
"length": args.vehLen,
|
| 107 |
-
"width": args.vehWidth,
|
| 108 |
-
"maxPosAcc": args.vehMaxPosAcc,
|
| 109 |
-
"maxNegAcc": args.vehMaxNegAcc,
|
| 110 |
-
"usualPosAcc": args.vehUsualPosAcc,
|
| 111 |
-
"usualNegAcc": args.vehUsualNegAcc,
|
| 112 |
-
"minGap": args.vehMinGap,
|
| 113 |
-
"maxSpeed": args.vehMaxSpeed,
|
| 114 |
-
"headwayTime": args.vehHeadwayTime
|
| 115 |
-
}
|
| 116 |
-
routes = generate_route(args.rowNum, args.colNum, args.turn)
|
| 117 |
-
flow = []
|
| 118 |
-
for route in routes:
|
| 119 |
-
flow.append({
|
| 120 |
-
"vehicle": vehicle_template,
|
| 121 |
-
"route": route,
|
| 122 |
-
"interval": args.interval,
|
| 123 |
-
"startTime": 0,
|
| 124 |
-
"endTime": -1
|
| 125 |
-
})
|
| 126 |
-
json.dump(flow, open(os.path.join(args.dir, args.flowFile), "w"), indent=2)
|
| 127 |
-
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
from generate_json_from_grid import gridToRoadnet
|
| 5 |
+
|
| 6 |
+
def parse_args():
|
| 7 |
+
parser = argparse.ArgumentParser()
|
| 8 |
+
parser.add_argument("rowNum", type=int)
|
| 9 |
+
parser.add_argument("colNum", type=int)
|
| 10 |
+
parser.add_argument("--rowDistance", type=int, default=300)
|
| 11 |
+
parser.add_argument("--columnDistance", type=int, default=300)
|
| 12 |
+
parser.add_argument("--intersectionWidth", type=int, default=30)
|
| 13 |
+
parser.add_argument("--numLeftLanes", type=int, default=1)
|
| 14 |
+
parser.add_argument("--numStraightLanes", type=int, default=1)
|
| 15 |
+
parser.add_argument("--numRightLanes", type=int, default=1)
|
| 16 |
+
parser.add_argument("--laneMaxSpeed", type=float, default=16.67)
|
| 17 |
+
parser.add_argument("--vehLen", type=float, default=5.0)
|
| 18 |
+
parser.add_argument("--vehWidth", type=float, default=2.0)
|
| 19 |
+
parser.add_argument("--vehMaxPosAcc", type=float, default=2.0)
|
| 20 |
+
parser.add_argument("--vehMaxNegAcc", type=float, default=4.5)
|
| 21 |
+
parser.add_argument("--vehUsualPosAcc", type=float, default=2.0)
|
| 22 |
+
parser.add_argument("--vehUsualNegAcc", type=float, default=4.5)
|
| 23 |
+
parser.add_argument("--vehMinGap", type=float, default=2.5)
|
| 24 |
+
parser.add_argument("--vehMaxSpeed", type=float, default=16.67)
|
| 25 |
+
parser.add_argument("--vehHeadwayTime", type=float, default=1.5)
|
| 26 |
+
parser.add_argument("--dir", type=str, default="./")
|
| 27 |
+
parser.add_argument("--roadnetFile", type=str)
|
| 28 |
+
parser.add_argument("--turn", action="store_true")
|
| 29 |
+
parser.add_argument("--tlPlan", action="store_true")
|
| 30 |
+
parser.add_argument("--interval", type=float, default=2.0)
|
| 31 |
+
parser.add_argument("--flowFile", type=str)
|
| 32 |
+
return parser.parse_args()
|
| 33 |
+
|
| 34 |
+
def generate_route(rowNum, colNum, turn=False):
|
| 35 |
+
routes = []
|
| 36 |
+
move = [(1, 0), (0, 1), (-1, 0), (0, -1)]
|
| 37 |
+
|
| 38 |
+
def get_straight_route(start, direction, step):
|
| 39 |
+
x, y = start
|
| 40 |
+
route = []
|
| 41 |
+
for _ in range(step):
|
| 42 |
+
route.append("road_%d_%d_%d" % (x, y, direction))
|
| 43 |
+
x += move[direction][0]
|
| 44 |
+
y += move[direction][1]
|
| 45 |
+
return route
|
| 46 |
+
|
| 47 |
+
for i in range(1, rowNum+1):
|
| 48 |
+
routes.append(get_straight_route((0, i), 0, colNum+1))
|
| 49 |
+
routes.append(get_straight_route((colNum+1, i), 2, colNum+1))
|
| 50 |
+
for i in range(1, colNum+1):
|
| 51 |
+
routes.append(get_straight_route((i, 0), 1, rowNum+1))
|
| 52 |
+
routes.append(get_straight_route((i, rowNum+1), 3, rowNum+1))
|
| 53 |
+
|
| 54 |
+
if turn:
|
| 55 |
+
def get_turn_route(start, direction):
|
| 56 |
+
if direction[0] % 2 == 0:
|
| 57 |
+
step = min(rowNum*2, colNum*2+1)
|
| 58 |
+
else:
|
| 59 |
+
step = min(colNum*2, rowNum*2+1)
|
| 60 |
+
x, y = start
|
| 61 |
+
route = []
|
| 62 |
+
cur = 0
|
| 63 |
+
for _ in range(step):
|
| 64 |
+
route.append("road_%d_%d_%d" % (x, y, direction[cur]))
|
| 65 |
+
x += move[direction[cur]][0]
|
| 66 |
+
y += move[direction[cur]][1]
|
| 67 |
+
cur = 1 - cur
|
| 68 |
+
return route
|
| 69 |
+
|
| 70 |
+
routes.append(get_turn_route((1, 0), (1, 0)))
|
| 71 |
+
routes.append(get_turn_route((0, 1), (0, 1)))
|
| 72 |
+
routes.append(get_turn_route((colNum+1, rowNum), (2, 3)))
|
| 73 |
+
routes.append(get_turn_route((colNum, rowNum+1), (3, 2)))
|
| 74 |
+
routes.append(get_turn_route((0, rowNum), (0, 3)))
|
| 75 |
+
routes.append(get_turn_route((1, rowNum+1), (3, 0)))
|
| 76 |
+
routes.append(get_turn_route((colNum+1, 1), (2, 1)))
|
| 77 |
+
routes.append(get_turn_route((colNum, 0), (1, 2)))
|
| 78 |
+
|
| 79 |
+
return routes
|
| 80 |
+
|
| 81 |
+
if __name__ == '__main__':
|
| 82 |
+
args = parse_args()
|
| 83 |
+
if args.roadnetFile is None:
|
| 84 |
+
args.roadnetFile = "roadnet_%d_%d%s.json" % (args.rowNum, args.colNum, "_turn" if args.turn else "")
|
| 85 |
+
if args.flowFile is None:
|
| 86 |
+
args.flowFile = "flow_%d_%d%s.json" % (args.rowNum, args.colNum, "_turn" if args.turn else "")
|
| 87 |
+
|
| 88 |
+
grid = {
|
| 89 |
+
"rowNumber": args.rowNum,
|
| 90 |
+
"columnNumber": args.colNum,
|
| 91 |
+
"rowDistances": [args.rowDistance] * (args.colNum-1),
|
| 92 |
+
"columnDistances": [args.columnDistance] * (args.rowNum-1),
|
| 93 |
+
"outRowDistance": args.rowDistance,
|
| 94 |
+
"outColumnDistance": args.columnDistance,
|
| 95 |
+
"intersectionWidths": [[args.intersectionWidth] * args.colNum] * args.rowNum,
|
| 96 |
+
"numLeftLanes": args.numLeftLanes,
|
| 97 |
+
"numStraightLanes": args.numStraightLanes,
|
| 98 |
+
"numRightLanes": args.numRightLanes,
|
| 99 |
+
"laneMaxSpeed": args.laneMaxSpeed,
|
| 100 |
+
"tlPlan": args.tlPlan
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
json.dump(gridToRoadnet(**grid), open(os.path.join(args.dir, args.roadnetFile), "w"), indent=2)
|
| 104 |
+
|
| 105 |
+
vehicle_template = {
|
| 106 |
+
"length": args.vehLen,
|
| 107 |
+
"width": args.vehWidth,
|
| 108 |
+
"maxPosAcc": args.vehMaxPosAcc,
|
| 109 |
+
"maxNegAcc": args.vehMaxNegAcc,
|
| 110 |
+
"usualPosAcc": args.vehUsualPosAcc,
|
| 111 |
+
"usualNegAcc": args.vehUsualNegAcc,
|
| 112 |
+
"minGap": args.vehMinGap,
|
| 113 |
+
"maxSpeed": args.vehMaxSpeed,
|
| 114 |
+
"headwayTime": args.vehHeadwayTime
|
| 115 |
+
}
|
| 116 |
+
routes = generate_route(args.rowNum, args.colNum, args.turn)
|
| 117 |
+
flow = []
|
| 118 |
+
for route in routes:
|
| 119 |
+
flow.append({
|
| 120 |
+
"vehicle": vehicle_template,
|
| 121 |
+
"route": route,
|
| 122 |
+
"interval": args.interval,
|
| 123 |
+
"startTime": 0,
|
| 124 |
+
"endTime": -1
|
| 125 |
+
})
|
| 126 |
+
json.dump(flow, open(os.path.join(args.dir, args.flowFile), "w"), indent=2)
|
| 127 |
+
|
third_party/CityFlow/tools/generator/generate_json_from_grid.py
CHANGED
|
@@ -1,428 +1,428 @@
|
|
| 1 |
-
from __future__ import division, print_function
|
| 2 |
-
import sys
|
| 3 |
-
import argparse
|
| 4 |
-
import json
|
| 5 |
-
import math
|
| 6 |
-
import numpy as np
|
| 7 |
-
|
| 8 |
-
parser = argparse.ArgumentParser(description='Generate roadnet JSON from a grid\
|
| 9 |
-
network JSON file.')
|
| 10 |
-
parser.add_argument('--config', dest='config', help='grid config file', type=str)
|
| 11 |
-
|
| 12 |
-
dx = [1, 0, -1, 0]
|
| 13 |
-
dy = [0, 1, 0, -1]
|
| 14 |
-
|
| 15 |
-
def isHorizontal(road):
|
| 16 |
-
return dx[road["direction"]] != 0
|
| 17 |
-
|
| 18 |
-
def isVertical(k):
|
| 19 |
-
return dy[k] == 0
|
| 20 |
-
|
| 21 |
-
def pointToDict(x, y):
|
| 22 |
-
return {"x": x, "y": y}
|
| 23 |
-
|
| 24 |
-
def pointToDict2(p):
|
| 25 |
-
return {"x": float(p.x), "y": float(p.y)}
|
| 26 |
-
|
| 27 |
-
def pointToDict3(p):
|
| 28 |
-
return {"x": p[0], "y": p[1]}
|
| 29 |
-
|
| 30 |
-
def getLaneShift(road, laneIndex):
|
| 31 |
-
shift = 0.
|
| 32 |
-
for i in range(laneIndex):
|
| 33 |
-
shift += road["lanes"][i]["width"]
|
| 34 |
-
shift += road["lanes"][laneIndex]["width"] * .5
|
| 35 |
-
return shift
|
| 36 |
-
|
| 37 |
-
def getRoadUnitVector(road):
|
| 38 |
-
startPoint = road["points"][0]
|
| 39 |
-
endPoint = road["points"][-1]
|
| 40 |
-
dx = endPoint["x"] - startPoint["x"]
|
| 41 |
-
dy = endPoint["y"] - startPoint["y"]
|
| 42 |
-
length = math.sqrt(dx * dx + dy * dy)
|
| 43 |
-
# print(length, dx, dy)
|
| 44 |
-
return dx / length, dy / length
|
| 45 |
-
|
| 46 |
-
def getOutPoint(road, width, laneIndex):
|
| 47 |
-
dx, dy = getRoadUnitVector(road)
|
| 48 |
-
laneShift = getLaneShift(road, laneIndex)
|
| 49 |
-
point = road["points"][-1]
|
| 50 |
-
x, y = point["x"], point["y"]
|
| 51 |
-
x, y = x - dx * width, y - dy * width
|
| 52 |
-
x, y = x + dy * laneShift, y - dx * laneShift
|
| 53 |
-
return x, y
|
| 54 |
-
|
| 55 |
-
def getOutTurnPoints(road, lim, laneIndex, width):
|
| 56 |
-
dx, dy = getRoadUnitVector(road)
|
| 57 |
-
laneShift = getLaneShift(road, laneIndex)
|
| 58 |
-
# distance = width * .5
|
| 59 |
-
point = road["points"][-1]
|
| 60 |
-
x, y = point["x"], point["y"]
|
| 61 |
-
x, y = x - dx * width, y - dy * width
|
| 62 |
-
x, y = x + dy * laneShift, y - dx * laneShift
|
| 63 |
-
return [pointToDict(x, y),
|
| 64 |
-
pointToDict(x + dx * lim, y + dy * lim)]
|
| 65 |
-
|
| 66 |
-
def getInPoint(road, width, laneIndex):
|
| 67 |
-
dx, dy = getRoadUnitVector(road)
|
| 68 |
-
laneShift = getLaneShift(road, laneIndex)
|
| 69 |
-
point = road["points"][0]
|
| 70 |
-
x, y = point["x"], point["y"]
|
| 71 |
-
x, y = x + dx * width, y + dy * width
|
| 72 |
-
x, y = x + dy * laneShift, y - dx * laneShift
|
| 73 |
-
return x, y
|
| 74 |
-
|
| 75 |
-
def getInTurnPoints(road, lim, laneIndex, width):
|
| 76 |
-
dx, dy = getRoadUnitVector(road)
|
| 77 |
-
laneShift = getLaneShift(road, laneIndex)
|
| 78 |
-
# distance = width * .5
|
| 79 |
-
point = road["points"][0]
|
| 80 |
-
x, y = point["x"], point["y"]
|
| 81 |
-
x, y = x + dx * width, y + dy * width
|
| 82 |
-
x, y = x + dy * laneShift, y - dx * laneShift
|
| 83 |
-
return [pointToDict(x - dx * lim, y - dy * lim),
|
| 84 |
-
pointToDict(x, y),]
|
| 85 |
-
|
| 86 |
-
# Hermite Spline
|
| 87 |
-
def findPath(roada, lanea, roadb, laneb, width, midPoint=10):
|
| 88 |
-
# def scale(k, p):
|
| 89 |
-
# return p.scale(k, k)
|
| 90 |
-
|
| 91 |
-
dxa, dya = getRoadUnitVector(roada)
|
| 92 |
-
dxb, dyb = getRoadUnitVector(roadb)
|
| 93 |
-
pxa, pya = getOutPoint(roada, width, lanea)
|
| 94 |
-
pxb, pyb = getInPoint(roadb, width, laneb)
|
| 95 |
-
|
| 96 |
-
# pa = Point(pxa, pya)
|
| 97 |
-
# pb = Point(pxb, pyb)
|
| 98 |
-
dxa = dxa * width
|
| 99 |
-
dya = dya * width
|
| 100 |
-
dxb = dxb * width
|
| 101 |
-
dyb = dyb * width
|
| 102 |
-
# da = scale(width, Point(dxa, dya))
|
| 103 |
-
# db = scale(width, Point(dxb, dyb))
|
| 104 |
-
|
| 105 |
-
path = []
|
| 106 |
-
for i in range(midPoint + 1):
|
| 107 |
-
t = i / midPoint
|
| 108 |
-
t3 = t * t * t
|
| 109 |
-
t2 = t * t
|
| 110 |
-
|
| 111 |
-
k1 = 2 * t3 - 3 * t2 + 1
|
| 112 |
-
x1 = k1 * pxa
|
| 113 |
-
y1 = k1 * pya
|
| 114 |
-
|
| 115 |
-
k2 = t3 - 2 * t2 + t
|
| 116 |
-
x2 = k2 * dxa
|
| 117 |
-
y2 = k2 * dya
|
| 118 |
-
|
| 119 |
-
k3 = -2 * t3 + 3 * t2
|
| 120 |
-
x3 = k3 * pxb
|
| 121 |
-
y3 = k3 * pyb
|
| 122 |
-
|
| 123 |
-
k4 = t3 - t2
|
| 124 |
-
x4 = k4 * dxb
|
| 125 |
-
y4 = k4 * dyb
|
| 126 |
-
|
| 127 |
-
path.append([x1 + x2 + x3 + x4, y1 + y2 + y3 + y4])
|
| 128 |
-
# path.append((scale(2 * t3 - 3 * t2 + 1, pa) +
|
| 129 |
-
# scale(t3 - 2 * t2 + t, da) +
|
| 130 |
-
# scale(-2 * t3 + 3 * t2, pb) +
|
| 131 |
-
# scale(t3 - t2, db)).evalf())
|
| 132 |
-
# path.append(pb)
|
| 133 |
-
|
| 134 |
-
# print(path)
|
| 135 |
-
|
| 136 |
-
return list(map(pointToDict3, path))
|
| 137 |
-
|
| 138 |
-
def findPathSimple(roada, lanea, roadb, laneb, width):
|
| 139 |
-
dxa, dya = getRoadUnitVector(roada)
|
| 140 |
-
dxb, dyb = getRoadUnitVector(roadb)
|
| 141 |
-
pxa, pya = getOutPoint(roada, width, lanea)
|
| 142 |
-
pxb, pyb = getInPoint(roadb, width, laneb)
|
| 143 |
-
|
| 144 |
-
return [pointToDict(pxa, pya), pointToDict(pxa + dxa * width / 2, pya + dya * width / 2),
|
| 145 |
-
pointToDict(pxb - dxb * width / 2, pyb - dyb * width / 2), pointToDict(pxb, pyb)]
|
| 146 |
-
|
| 147 |
-
def decideType(roada, roadb):
|
| 148 |
-
da = roada["direction"]
|
| 149 |
-
db = roadb["direction"]
|
| 150 |
-
if (da + 1) % 4 == db:
|
| 151 |
-
return "turn_left"
|
| 152 |
-
elif (db + 1) % 4 == da:
|
| 153 |
-
return "turn_right"
|
| 154 |
-
elif da == db:
|
| 155 |
-
return "go_straight"
|
| 156 |
-
else:
|
| 157 |
-
raise ValueError
|
| 158 |
-
|
| 159 |
-
def checkIntersection(interseciontName, isTruelyInside):
|
| 160 |
-
return interseciontName
|
| 161 |
-
# _, i, j = interseciontName.split('_')
|
| 162 |
-
# i, j = int(i), int(j)
|
| 163 |
-
# if isTruelyInside(i, j):
|
| 164 |
-
# return interseciontName
|
| 165 |
-
# else:
|
| 166 |
-
# return None
|
| 167 |
-
|
| 168 |
-
def gridToRoadnet(rowNumber, columnNumber, rowDistances, columnDistances, outRowDistance, outColumnDistance,
|
| 169 |
-
intersectionWidths, laneWidth=4, laneMaxSpeed=20,
|
| 170 |
-
numLeftLanes=1, numStraightLanes=1, numRightLanes=1, tlPlan=False, midPoints=10):
|
| 171 |
-
|
| 172 |
-
rowNumber += 2
|
| 173 |
-
columnNumber += 2
|
| 174 |
-
numLanes = numLeftLanes + numStraightLanes + numRightLanes
|
| 175 |
-
intersectionWidths = [[0] * columnNumber] + intersectionWidths + [[0] * columnNumber]
|
| 176 |
-
for i in range(1, rowNumber - 1):
|
| 177 |
-
intersectionWidths[i] = [0] + intersectionWidths[i] + [0]
|
| 178 |
-
|
| 179 |
-
def isInside(i, j):
|
| 180 |
-
return i >= 0 and j >= 0 and i < rowNumber and j < columnNumber
|
| 181 |
-
|
| 182 |
-
def isTruelyInside(i, j):
|
| 183 |
-
return i > 0 and j > 0 and i < rowNumber - 1 and j < columnNumber - 1
|
| 184 |
-
|
| 185 |
-
def isCorner(i, j):
|
| 186 |
-
return (i == 0 or i == rowNumber - 1) and (j == 0 or j == columnNumber - 1)
|
| 187 |
-
|
| 188 |
-
def isEdge(i, j):
|
| 189 |
-
return isInside(i, j) and not isTruelyInside(i, j)
|
| 190 |
-
|
| 191 |
-
def shouldDraw(road):
|
| 192 |
-
return isTruelyInside(road["fromi"], road["fromj"]) or isTruelyInside(road["toi"], road["toj"])
|
| 193 |
-
|
| 194 |
-
def isLeftLane(index):
|
| 195 |
-
return 0 <= index < numLeftLanes
|
| 196 |
-
|
| 197 |
-
def isStraightLane(index):
|
| 198 |
-
return numLeftLanes <= index < numLeftLanes + numStraightLanes
|
| 199 |
-
|
| 200 |
-
def isRightLane(index):
|
| 201 |
-
return numLeftLanes + numStraightLanes <= index < numLanes
|
| 202 |
-
|
| 203 |
-
x = [[None for _ in range(columnNumber)] for _ in range(rowNumber)]
|
| 204 |
-
y = [[None for _ in range(columnNumber)] for _ in range(rowNumber)]
|
| 205 |
-
|
| 206 |
-
rowDistances = [outRowDistance] + rowDistances + [outRowDistance]
|
| 207 |
-
columnDistances = [outColumnDistance] + columnDistances + [outColumnDistance]
|
| 208 |
-
|
| 209 |
-
for i in range(rowNumber):
|
| 210 |
-
for j in range(columnNumber):
|
| 211 |
-
if j > 0:
|
| 212 |
-
x[i][j] = x[i][j - 1] + rowDistances[i - 1]
|
| 213 |
-
y[i][j] = y[i][j - 1]
|
| 214 |
-
elif i > 0:
|
| 215 |
-
x[i][j] = x[i - 1][j]
|
| 216 |
-
y[i][j] = y[i - 1][j] + columnDistances[j - 1]
|
| 217 |
-
else:
|
| 218 |
-
x[i][j] = -outRowDistance
|
| 219 |
-
y[i][j] = -outColumnDistance
|
| 220 |
-
|
| 221 |
-
roads = [[[None, None, None, None] for _ in range(columnNumber)] for _ in range(rowNumber)]
|
| 222 |
-
for i in range(rowNumber):
|
| 223 |
-
for j in range(columnNumber):
|
| 224 |
-
for k in range(4):
|
| 225 |
-
ni, nj = i + dy[k], j + dx[k]
|
| 226 |
-
if not isInside(ni, nj):
|
| 227 |
-
road = None
|
| 228 |
-
else:
|
| 229 |
-
road = {
|
| 230 |
-
"id": "road_%d_%d_%d" % (j, i, k),
|
| 231 |
-
"direction": k,
|
| 232 |
-
"fromi": i,
|
| 233 |
-
"fromj": j,
|
| 234 |
-
"toi": ni,
|
| 235 |
-
"toj": nj,
|
| 236 |
-
"points": [
|
| 237 |
-
pointToDict(x[i][j], y[i][j]),
|
| 238 |
-
pointToDict(x[ni][nj], y[ni][nj])
|
| 239 |
-
],
|
| 240 |
-
"lanes": [
|
| 241 |
-
{
|
| 242 |
-
"width": laneWidth,
|
| 243 |
-
"maxSpeed": laneMaxSpeed
|
| 244 |
-
}
|
| 245 |
-
] * numLanes,
|
| 246 |
-
"startIntersection": "intersection_%d_%d" % (j, i),
|
| 247 |
-
"endIntersection": "intersection_%d_%d" % (nj, ni)
|
| 248 |
-
}
|
| 249 |
-
roads[i][j][k] = road
|
| 250 |
-
|
| 251 |
-
intersections = [[None for _ in range(columnNumber)] for _ in range(rowNumber)]
|
| 252 |
-
for i in range(rowNumber):
|
| 253 |
-
for j in range(columnNumber):
|
| 254 |
-
width = intersectionWidths[i][j]
|
| 255 |
-
intersection = {
|
| 256 |
-
"id": "intersection_%d_%d" % (j, i),
|
| 257 |
-
"point": pointToDict(x[i][j], y[i][j]),
|
| 258 |
-
"width": width,
|
| 259 |
-
"roads": [],
|
| 260 |
-
"roadLinks": [],
|
| 261 |
-
"trafficLight": {
|
| 262 |
-
"roadLinkIndices": [],
|
| 263 |
-
"lightphases": []
|
| 264 |
-
},
|
| 265 |
-
"virtual": not isTruelyInside(i, j)
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
roadLinks = intersection["roadLinks"]
|
| 269 |
-
roadLinkIndices = intersection["trafficLight"]["roadLinkIndices"]
|
| 270 |
-
|
| 271 |
-
outRoads = list(filter(lambda x: x is not None, roads[i][j]))
|
| 272 |
-
inRoads = list(map(lambda k: roads[i - dy[k]][j - dx[k]][k],
|
| 273 |
-
filter(lambda k: isInside(i - dy[k], j - dx[k]), range(4))))
|
| 274 |
-
outRoads = list(filter(shouldDraw, outRoads))
|
| 275 |
-
inRoads = list(filter(shouldDraw, inRoads))
|
| 276 |
-
|
| 277 |
-
for road in inRoads + outRoads:
|
| 278 |
-
intersection["roads"].append(road["id"])
|
| 279 |
-
|
| 280 |
-
for a in range(len(inRoads)):
|
| 281 |
-
for b in range(len(outRoads)):
|
| 282 |
-
try:
|
| 283 |
-
roada = inRoads[a]
|
| 284 |
-
roadb = outRoads[b]
|
| 285 |
-
roadLink = {
|
| 286 |
-
"type": decideType(roada, roadb),
|
| 287 |
-
"startRoad": roada["id"],
|
| 288 |
-
"endRoad": roadb["id"],
|
| 289 |
-
"direction": roada["direction"],
|
| 290 |
-
"laneLinks": []
|
| 291 |
-
}
|
| 292 |
-
for c in range(len(roada["lanes"])):
|
| 293 |
-
if roadLink["type"] == "turn_left" and not isLeftLane(c):
|
| 294 |
-
continue
|
| 295 |
-
if roadLink["type"] == "go_straight" and not isStraightLane(c):
|
| 296 |
-
continue
|
| 297 |
-
if roadLink["type"] == "turn_right" and not isRightLane(c):
|
| 298 |
-
continue
|
| 299 |
-
for d in range(len(roadb["lanes"])):
|
| 300 |
-
path = {
|
| 301 |
-
"startLaneIndex": c,
|
| 302 |
-
"endLaneIndex": d,
|
| 303 |
-
"points": findPath(roada, c, roadb, d, width, midPoints)
|
| 304 |
-
}
|
| 305 |
-
roadLink["laneLinks"].append(path)
|
| 306 |
-
if roadLink["laneLinks"]:
|
| 307 |
-
roadLinkIndices.append(len(roadLinks))
|
| 308 |
-
roadLinks.append(roadLink)
|
| 309 |
-
except ValueError:
|
| 310 |
-
pass
|
| 311 |
-
|
| 312 |
-
leftLaneLinks = set(filter(lambda x: roadLinks[x]["type"] == "turn_left", roadLinkIndices))
|
| 313 |
-
rightLaneLinks = set(filter(lambda x: roadLinks[x]["type"] == "turn_right", roadLinkIndices))
|
| 314 |
-
straightLaneLinks = set(filter(lambda x: roadLinks[x]["type"] == "go_straight", roadLinkIndices))
|
| 315 |
-
WELaneLinks = set(filter(lambda x: roadLinks[x]["direction"] == 0, roadLinkIndices))
|
| 316 |
-
NSLaneLinks = set(filter(lambda x: roadLinks[x]["direction"] == 1, roadLinkIndices))
|
| 317 |
-
EWLaneLinks = set(filter(lambda x: roadLinks[x]["direction"] == 2, roadLinkIndices))
|
| 318 |
-
SNLaneLinks = set(filter(lambda x: roadLinks[x]["direction"] == 3, roadLinkIndices))
|
| 319 |
-
|
| 320 |
-
tlPhases = intersection["trafficLight"]["lightphases"]
|
| 321 |
-
if not tlPlan:
|
| 322 |
-
tlPhases.append({
|
| 323 |
-
"time": 5,
|
| 324 |
-
"availableRoadLinks": rightLaneLinks
|
| 325 |
-
})
|
| 326 |
-
tlPhases.append({
|
| 327 |
-
"time": 30,
|
| 328 |
-
"availableRoadLinks": ((EWLaneLinks | WELaneLinks) & straightLaneLinks) | (rightLaneLinks)
|
| 329 |
-
})
|
| 330 |
-
tlPhases.append({
|
| 331 |
-
"time": 30,
|
| 332 |
-
"availableRoadLinks": ((NSLaneLinks | SNLaneLinks) & straightLaneLinks) | (rightLaneLinks)
|
| 333 |
-
})
|
| 334 |
-
tlPhases.append({
|
| 335 |
-
"time": 30,
|
| 336 |
-
"availableRoadLinks": ((EWLaneLinks | WELaneLinks) & leftLaneLinks) | (rightLaneLinks)
|
| 337 |
-
})
|
| 338 |
-
tlPhases.append({
|
| 339 |
-
"time": 30,
|
| 340 |
-
"availableRoadLinks": ((SNLaneLinks | NSLaneLinks) & leftLaneLinks) | (rightLaneLinks)
|
| 341 |
-
})
|
| 342 |
-
tlPhases.append({
|
| 343 |
-
"time": 30,
|
| 344 |
-
"availableRoadLinks": (WELaneLinks) | (rightLaneLinks)
|
| 345 |
-
})
|
| 346 |
-
tlPhases.append({
|
| 347 |
-
"time": 30,
|
| 348 |
-
"availableRoadLinks": (EWLaneLinks) | (rightLaneLinks)
|
| 349 |
-
})
|
| 350 |
-
tlPhases.append({
|
| 351 |
-
"time": 30,
|
| 352 |
-
"availableRoadLinks": (NSLaneLinks) | (rightLaneLinks)
|
| 353 |
-
})
|
| 354 |
-
tlPhases.append({
|
| 355 |
-
"time": 30,
|
| 356 |
-
"availableRoadLinks": (SNLaneLinks) | (rightLaneLinks)
|
| 357 |
-
})
|
| 358 |
-
else:
|
| 359 |
-
tlPhases.append({
|
| 360 |
-
"time": 30,
|
| 361 |
-
"availableRoadLinks": ((EWLaneLinks | WELaneLinks) & straightLaneLinks) | (rightLaneLinks)
|
| 362 |
-
})
|
| 363 |
-
tlPhases.append({
|
| 364 |
-
"time": 5,
|
| 365 |
-
"availableRoadLinks": rightLaneLinks
|
| 366 |
-
})
|
| 367 |
-
if numLeftLanes:
|
| 368 |
-
tlPhases.append({
|
| 369 |
-
"time": 30,
|
| 370 |
-
"availableRoadLinks": ((EWLaneLinks | WELaneLinks) & leftLaneLinks) | (rightLaneLinks)
|
| 371 |
-
})
|
| 372 |
-
tlPhases.append({
|
| 373 |
-
"time": 5,
|
| 374 |
-
"availableRoadLinks": rightLaneLinks
|
| 375 |
-
})
|
| 376 |
-
tlPhases.append({
|
| 377 |
-
"time": 30,
|
| 378 |
-
"availableRoadLinks": ((NSLaneLinks | SNLaneLinks) & straightLaneLinks) | (rightLaneLinks)
|
| 379 |
-
})
|
| 380 |
-
tlPhases.append({
|
| 381 |
-
"time": 5,
|
| 382 |
-
"availableRoadLinks": rightLaneLinks
|
| 383 |
-
})
|
| 384 |
-
if numLeftLanes:
|
| 385 |
-
tlPhases.append({
|
| 386 |
-
"time": 30,
|
| 387 |
-
"availableRoadLinks": ((SNLaneLinks | NSLaneLinks) & leftLaneLinks) | (rightLaneLinks)
|
| 388 |
-
})
|
| 389 |
-
tlPhases.append({
|
| 390 |
-
"time": 5,
|
| 391 |
-
"availableRoadLinks": rightLaneLinks
|
| 392 |
-
})
|
| 393 |
-
for tlPhase in tlPhases:
|
| 394 |
-
tlPhase["availableRoadLinks"] = list(tlPhase["availableRoadLinks"])
|
| 395 |
-
intersections[i][j] = intersection
|
| 396 |
-
|
| 397 |
-
final_intersecions = []
|
| 398 |
-
for i in range(rowNumber):
|
| 399 |
-
for j in range(columnNumber):
|
| 400 |
-
if not isCorner(i, j):
|
| 401 |
-
# print(i, j)
|
| 402 |
-
final_intersecions.append(intersections[i][j])
|
| 403 |
-
|
| 404 |
-
final_roads = []
|
| 405 |
-
for i in range(rowNumber):
|
| 406 |
-
for j in range(columnNumber):
|
| 407 |
-
for k in range(4):
|
| 408 |
-
if roads[i][j][k] is not None and shouldDraw(roads[i][j][k]):
|
| 409 |
-
ni, nj = i + dy[k], j + dx[k]
|
| 410 |
-
roads[i][j][k].pop("direction")
|
| 411 |
-
roads[i][j][k].pop("fromi")
|
| 412 |
-
roads[i][j][k].pop("fromj")
|
| 413 |
-
roads[i][j][k].pop("toi")
|
| 414 |
-
roads[i][j][k].pop("toj")
|
| 415 |
-
roads[i][j][k]["startIntersection"] = checkIntersection(roads[i][j][k]["startIntersection"], isTruelyInside)
|
| 416 |
-
roads[i][j][k]["endIntersection"] = checkIntersection(roads[i][j][k]["endIntersection"], isTruelyInside)
|
| 417 |
-
final_roads.append(roads[i][j][k])
|
| 418 |
-
|
| 419 |
-
return {
|
| 420 |
-
"intersections": final_intersecions,
|
| 421 |
-
"roads": final_roads
|
| 422 |
-
}
|
| 423 |
-
|
| 424 |
-
if __name__ == "__main__":
|
| 425 |
-
args = parser.parse_args()
|
| 426 |
-
path = '../data/roadnet/'
|
| 427 |
-
grid_config = json.load(open(path + args.config if args.config is not None else path + 'grid66.json'))
|
| 428 |
-
json.dump(gridToRoadnet(**grid_config), open(path + "roadnet66.json", "w"), indent=2)
|
|
|
|
| 1 |
+
from __future__ import division, print_function
|
| 2 |
+
import sys
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import math
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
parser = argparse.ArgumentParser(description='Generate roadnet JSON from a grid\
|
| 9 |
+
network JSON file.')
|
| 10 |
+
parser.add_argument('--config', dest='config', help='grid config file', type=str)
|
| 11 |
+
|
| 12 |
+
dx = [1, 0, -1, 0]
|
| 13 |
+
dy = [0, 1, 0, -1]
|
| 14 |
+
|
| 15 |
+
def isHorizontal(road):
|
| 16 |
+
return dx[road["direction"]] != 0
|
| 17 |
+
|
| 18 |
+
def isVertical(k):
|
| 19 |
+
return dy[k] == 0
|
| 20 |
+
|
| 21 |
+
def pointToDict(x, y):
|
| 22 |
+
return {"x": x, "y": y}
|
| 23 |
+
|
| 24 |
+
def pointToDict2(p):
|
| 25 |
+
return {"x": float(p.x), "y": float(p.y)}
|
| 26 |
+
|
| 27 |
+
def pointToDict3(p):
|
| 28 |
+
return {"x": p[0], "y": p[1]}
|
| 29 |
+
|
| 30 |
+
def getLaneShift(road, laneIndex):
|
| 31 |
+
shift = 0.
|
| 32 |
+
for i in range(laneIndex):
|
| 33 |
+
shift += road["lanes"][i]["width"]
|
| 34 |
+
shift += road["lanes"][laneIndex]["width"] * .5
|
| 35 |
+
return shift
|
| 36 |
+
|
| 37 |
+
def getRoadUnitVector(road):
|
| 38 |
+
startPoint = road["points"][0]
|
| 39 |
+
endPoint = road["points"][-1]
|
| 40 |
+
dx = endPoint["x"] - startPoint["x"]
|
| 41 |
+
dy = endPoint["y"] - startPoint["y"]
|
| 42 |
+
length = math.sqrt(dx * dx + dy * dy)
|
| 43 |
+
# print(length, dx, dy)
|
| 44 |
+
return dx / length, dy / length
|
| 45 |
+
|
| 46 |
+
def getOutPoint(road, width, laneIndex):
|
| 47 |
+
dx, dy = getRoadUnitVector(road)
|
| 48 |
+
laneShift = getLaneShift(road, laneIndex)
|
| 49 |
+
point = road["points"][-1]
|
| 50 |
+
x, y = point["x"], point["y"]
|
| 51 |
+
x, y = x - dx * width, y - dy * width
|
| 52 |
+
x, y = x + dy * laneShift, y - dx * laneShift
|
| 53 |
+
return x, y
|
| 54 |
+
|
| 55 |
+
def getOutTurnPoints(road, lim, laneIndex, width):
|
| 56 |
+
dx, dy = getRoadUnitVector(road)
|
| 57 |
+
laneShift = getLaneShift(road, laneIndex)
|
| 58 |
+
# distance = width * .5
|
| 59 |
+
point = road["points"][-1]
|
| 60 |
+
x, y = point["x"], point["y"]
|
| 61 |
+
x, y = x - dx * width, y - dy * width
|
| 62 |
+
x, y = x + dy * laneShift, y - dx * laneShift
|
| 63 |
+
return [pointToDict(x, y),
|
| 64 |
+
pointToDict(x + dx * lim, y + dy * lim)]
|
| 65 |
+
|
| 66 |
+
def getInPoint(road, width, laneIndex):
|
| 67 |
+
dx, dy = getRoadUnitVector(road)
|
| 68 |
+
laneShift = getLaneShift(road, laneIndex)
|
| 69 |
+
point = road["points"][0]
|
| 70 |
+
x, y = point["x"], point["y"]
|
| 71 |
+
x, y = x + dx * width, y + dy * width
|
| 72 |
+
x, y = x + dy * laneShift, y - dx * laneShift
|
| 73 |
+
return x, y
|
| 74 |
+
|
| 75 |
+
def getInTurnPoints(road, lim, laneIndex, width):
|
| 76 |
+
dx, dy = getRoadUnitVector(road)
|
| 77 |
+
laneShift = getLaneShift(road, laneIndex)
|
| 78 |
+
# distance = width * .5
|
| 79 |
+
point = road["points"][0]
|
| 80 |
+
x, y = point["x"], point["y"]
|
| 81 |
+
x, y = x + dx * width, y + dy * width
|
| 82 |
+
x, y = x + dy * laneShift, y - dx * laneShift
|
| 83 |
+
return [pointToDict(x - dx * lim, y - dy * lim),
|
| 84 |
+
pointToDict(x, y),]
|
| 85 |
+
|
| 86 |
+
# Hermite Spline
|
| 87 |
+
def findPath(roada, lanea, roadb, laneb, width, midPoint=10):
|
| 88 |
+
# def scale(k, p):
|
| 89 |
+
# return p.scale(k, k)
|
| 90 |
+
|
| 91 |
+
dxa, dya = getRoadUnitVector(roada)
|
| 92 |
+
dxb, dyb = getRoadUnitVector(roadb)
|
| 93 |
+
pxa, pya = getOutPoint(roada, width, lanea)
|
| 94 |
+
pxb, pyb = getInPoint(roadb, width, laneb)
|
| 95 |
+
|
| 96 |
+
# pa = Point(pxa, pya)
|
| 97 |
+
# pb = Point(pxb, pyb)
|
| 98 |
+
dxa = dxa * width
|
| 99 |
+
dya = dya * width
|
| 100 |
+
dxb = dxb * width
|
| 101 |
+
dyb = dyb * width
|
| 102 |
+
# da = scale(width, Point(dxa, dya))
|
| 103 |
+
# db = scale(width, Point(dxb, dyb))
|
| 104 |
+
|
| 105 |
+
path = []
|
| 106 |
+
for i in range(midPoint + 1):
|
| 107 |
+
t = i / midPoint
|
| 108 |
+
t3 = t * t * t
|
| 109 |
+
t2 = t * t
|
| 110 |
+
|
| 111 |
+
k1 = 2 * t3 - 3 * t2 + 1
|
| 112 |
+
x1 = k1 * pxa
|
| 113 |
+
y1 = k1 * pya
|
| 114 |
+
|
| 115 |
+
k2 = t3 - 2 * t2 + t
|
| 116 |
+
x2 = k2 * dxa
|
| 117 |
+
y2 = k2 * dya
|
| 118 |
+
|
| 119 |
+
k3 = -2 * t3 + 3 * t2
|
| 120 |
+
x3 = k3 * pxb
|
| 121 |
+
y3 = k3 * pyb
|
| 122 |
+
|
| 123 |
+
k4 = t3 - t2
|
| 124 |
+
x4 = k4 * dxb
|
| 125 |
+
y4 = k4 * dyb
|
| 126 |
+
|
| 127 |
+
path.append([x1 + x2 + x3 + x4, y1 + y2 + y3 + y4])
|
| 128 |
+
# path.append((scale(2 * t3 - 3 * t2 + 1, pa) +
|
| 129 |
+
# scale(t3 - 2 * t2 + t, da) +
|
| 130 |
+
# scale(-2 * t3 + 3 * t2, pb) +
|
| 131 |
+
# scale(t3 - t2, db)).evalf())
|
| 132 |
+
# path.append(pb)
|
| 133 |
+
|
| 134 |
+
# print(path)
|
| 135 |
+
|
| 136 |
+
return list(map(pointToDict3, path))
|
| 137 |
+
|
| 138 |
+
def findPathSimple(roada, lanea, roadb, laneb, width):
|
| 139 |
+
dxa, dya = getRoadUnitVector(roada)
|
| 140 |
+
dxb, dyb = getRoadUnitVector(roadb)
|
| 141 |
+
pxa, pya = getOutPoint(roada, width, lanea)
|
| 142 |
+
pxb, pyb = getInPoint(roadb, width, laneb)
|
| 143 |
+
|
| 144 |
+
return [pointToDict(pxa, pya), pointToDict(pxa + dxa * width / 2, pya + dya * width / 2),
|
| 145 |
+
pointToDict(pxb - dxb * width / 2, pyb - dyb * width / 2), pointToDict(pxb, pyb)]
|
| 146 |
+
|
| 147 |
+
def decideType(roada, roadb):
|
| 148 |
+
da = roada["direction"]
|
| 149 |
+
db = roadb["direction"]
|
| 150 |
+
if (da + 1) % 4 == db:
|
| 151 |
+
return "turn_left"
|
| 152 |
+
elif (db + 1) % 4 == da:
|
| 153 |
+
return "turn_right"
|
| 154 |
+
elif da == db:
|
| 155 |
+
return "go_straight"
|
| 156 |
+
else:
|
| 157 |
+
raise ValueError
|
| 158 |
+
|
| 159 |
+
def checkIntersection(interseciontName, isTruelyInside):
|
| 160 |
+
return interseciontName
|
| 161 |
+
# _, i, j = interseciontName.split('_')
|
| 162 |
+
# i, j = int(i), int(j)
|
| 163 |
+
# if isTruelyInside(i, j):
|
| 164 |
+
# return interseciontName
|
| 165 |
+
# else:
|
| 166 |
+
# return None
|
| 167 |
+
|
| 168 |
+
def gridToRoadnet(rowNumber, columnNumber, rowDistances, columnDistances, outRowDistance, outColumnDistance,
|
| 169 |
+
intersectionWidths, laneWidth=4, laneMaxSpeed=20,
|
| 170 |
+
numLeftLanes=1, numStraightLanes=1, numRightLanes=1, tlPlan=False, midPoints=10):
|
| 171 |
+
|
| 172 |
+
rowNumber += 2
|
| 173 |
+
columnNumber += 2
|
| 174 |
+
numLanes = numLeftLanes + numStraightLanes + numRightLanes
|
| 175 |
+
intersectionWidths = [[0] * columnNumber] + intersectionWidths + [[0] * columnNumber]
|
| 176 |
+
for i in range(1, rowNumber - 1):
|
| 177 |
+
intersectionWidths[i] = [0] + intersectionWidths[i] + [0]
|
| 178 |
+
|
| 179 |
+
def isInside(i, j):
|
| 180 |
+
return i >= 0 and j >= 0 and i < rowNumber and j < columnNumber
|
| 181 |
+
|
| 182 |
+
def isTruelyInside(i, j):
|
| 183 |
+
return i > 0 and j > 0 and i < rowNumber - 1 and j < columnNumber - 1
|
| 184 |
+
|
| 185 |
+
def isCorner(i, j):
|
| 186 |
+
return (i == 0 or i == rowNumber - 1) and (j == 0 or j == columnNumber - 1)
|
| 187 |
+
|
| 188 |
+
def isEdge(i, j):
|
| 189 |
+
return isInside(i, j) and not isTruelyInside(i, j)
|
| 190 |
+
|
| 191 |
+
def shouldDraw(road):
|
| 192 |
+
return isTruelyInside(road["fromi"], road["fromj"]) or isTruelyInside(road["toi"], road["toj"])
|
| 193 |
+
|
| 194 |
+
def isLeftLane(index):
|
| 195 |
+
return 0 <= index < numLeftLanes
|
| 196 |
+
|
| 197 |
+
def isStraightLane(index):
|
| 198 |
+
return numLeftLanes <= index < numLeftLanes + numStraightLanes
|
| 199 |
+
|
| 200 |
+
def isRightLane(index):
|
| 201 |
+
return numLeftLanes + numStraightLanes <= index < numLanes
|
| 202 |
+
|
| 203 |
+
x = [[None for _ in range(columnNumber)] for _ in range(rowNumber)]
|
| 204 |
+
y = [[None for _ in range(columnNumber)] for _ in range(rowNumber)]
|
| 205 |
+
|
| 206 |
+
rowDistances = [outRowDistance] + rowDistances + [outRowDistance]
|
| 207 |
+
columnDistances = [outColumnDistance] + columnDistances + [outColumnDistance]
|
| 208 |
+
|
| 209 |
+
for i in range(rowNumber):
|
| 210 |
+
for j in range(columnNumber):
|
| 211 |
+
if j > 0:
|
| 212 |
+
x[i][j] = x[i][j - 1] + rowDistances[i - 1]
|
| 213 |
+
y[i][j] = y[i][j - 1]
|
| 214 |
+
elif i > 0:
|
| 215 |
+
x[i][j] = x[i - 1][j]
|
| 216 |
+
y[i][j] = y[i - 1][j] + columnDistances[j - 1]
|
| 217 |
+
else:
|
| 218 |
+
x[i][j] = -outRowDistance
|
| 219 |
+
y[i][j] = -outColumnDistance
|
| 220 |
+
|
| 221 |
+
roads = [[[None, None, None, None] for _ in range(columnNumber)] for _ in range(rowNumber)]
|
| 222 |
+
for i in range(rowNumber):
|
| 223 |
+
for j in range(columnNumber):
|
| 224 |
+
for k in range(4):
|
| 225 |
+
ni, nj = i + dy[k], j + dx[k]
|
| 226 |
+
if not isInside(ni, nj):
|
| 227 |
+
road = None
|
| 228 |
+
else:
|
| 229 |
+
road = {
|
| 230 |
+
"id": "road_%d_%d_%d" % (j, i, k),
|
| 231 |
+
"direction": k,
|
| 232 |
+
"fromi": i,
|
| 233 |
+
"fromj": j,
|
| 234 |
+
"toi": ni,
|
| 235 |
+
"toj": nj,
|
| 236 |
+
"points": [
|
| 237 |
+
pointToDict(x[i][j], y[i][j]),
|
| 238 |
+
pointToDict(x[ni][nj], y[ni][nj])
|
| 239 |
+
],
|
| 240 |
+
"lanes": [
|
| 241 |
+
{
|
| 242 |
+
"width": laneWidth,
|
| 243 |
+
"maxSpeed": laneMaxSpeed
|
| 244 |
+
}
|
| 245 |
+
] * numLanes,
|
| 246 |
+
"startIntersection": "intersection_%d_%d" % (j, i),
|
| 247 |
+
"endIntersection": "intersection_%d_%d" % (nj, ni)
|
| 248 |
+
}
|
| 249 |
+
roads[i][j][k] = road
|
| 250 |
+
|
| 251 |
+
intersections = [[None for _ in range(columnNumber)] for _ in range(rowNumber)]
|
| 252 |
+
for i in range(rowNumber):
|
| 253 |
+
for j in range(columnNumber):
|
| 254 |
+
width = intersectionWidths[i][j]
|
| 255 |
+
intersection = {
|
| 256 |
+
"id": "intersection_%d_%d" % (j, i),
|
| 257 |
+
"point": pointToDict(x[i][j], y[i][j]),
|
| 258 |
+
"width": width,
|
| 259 |
+
"roads": [],
|
| 260 |
+
"roadLinks": [],
|
| 261 |
+
"trafficLight": {
|
| 262 |
+
"roadLinkIndices": [],
|
| 263 |
+
"lightphases": []
|
| 264 |
+
},
|
| 265 |
+
"virtual": not isTruelyInside(i, j)
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
roadLinks = intersection["roadLinks"]
|
| 269 |
+
roadLinkIndices = intersection["trafficLight"]["roadLinkIndices"]
|
| 270 |
+
|
| 271 |
+
outRoads = list(filter(lambda x: x is not None, roads[i][j]))
|
| 272 |
+
inRoads = list(map(lambda k: roads[i - dy[k]][j - dx[k]][k],
|
| 273 |
+
filter(lambda k: isInside(i - dy[k], j - dx[k]), range(4))))
|
| 274 |
+
outRoads = list(filter(shouldDraw, outRoads))
|
| 275 |
+
inRoads = list(filter(shouldDraw, inRoads))
|
| 276 |
+
|
| 277 |
+
for road in inRoads + outRoads:
|
| 278 |
+
intersection["roads"].append(road["id"])
|
| 279 |
+
|
| 280 |
+
for a in range(len(inRoads)):
|
| 281 |
+
for b in range(len(outRoads)):
|
| 282 |
+
try:
|
| 283 |
+
roada = inRoads[a]
|
| 284 |
+
roadb = outRoads[b]
|
| 285 |
+
roadLink = {
|
| 286 |
+
"type": decideType(roada, roadb),
|
| 287 |
+
"startRoad": roada["id"],
|
| 288 |
+
"endRoad": roadb["id"],
|
| 289 |
+
"direction": roada["direction"],
|
| 290 |
+
"laneLinks": []
|
| 291 |
+
}
|
| 292 |
+
for c in range(len(roada["lanes"])):
|
| 293 |
+
if roadLink["type"] == "turn_left" and not isLeftLane(c):
|
| 294 |
+
continue
|
| 295 |
+
if roadLink["type"] == "go_straight" and not isStraightLane(c):
|
| 296 |
+
continue
|
| 297 |
+
if roadLink["type"] == "turn_right" and not isRightLane(c):
|
| 298 |
+
continue
|
| 299 |
+
for d in range(len(roadb["lanes"])):
|
| 300 |
+
path = {
|
| 301 |
+
"startLaneIndex": c,
|
| 302 |
+
"endLaneIndex": d,
|
| 303 |
+
"points": findPath(roada, c, roadb, d, width, midPoints)
|
| 304 |
+
}
|
| 305 |
+
roadLink["laneLinks"].append(path)
|
| 306 |
+
if roadLink["laneLinks"]:
|
| 307 |
+
roadLinkIndices.append(len(roadLinks))
|
| 308 |
+
roadLinks.append(roadLink)
|
| 309 |
+
except ValueError:
|
| 310 |
+
pass
|
| 311 |
+
|
| 312 |
+
leftLaneLinks = set(filter(lambda x: roadLinks[x]["type"] == "turn_left", roadLinkIndices))
|
| 313 |
+
rightLaneLinks = set(filter(lambda x: roadLinks[x]["type"] == "turn_right", roadLinkIndices))
|
| 314 |
+
straightLaneLinks = set(filter(lambda x: roadLinks[x]["type"] == "go_straight", roadLinkIndices))
|
| 315 |
+
WELaneLinks = set(filter(lambda x: roadLinks[x]["direction"] == 0, roadLinkIndices))
|
| 316 |
+
NSLaneLinks = set(filter(lambda x: roadLinks[x]["direction"] == 1, roadLinkIndices))
|
| 317 |
+
EWLaneLinks = set(filter(lambda x: roadLinks[x]["direction"] == 2, roadLinkIndices))
|
| 318 |
+
SNLaneLinks = set(filter(lambda x: roadLinks[x]["direction"] == 3, roadLinkIndices))
|
| 319 |
+
|
| 320 |
+
tlPhases = intersection["trafficLight"]["lightphases"]
|
| 321 |
+
if not tlPlan:
|
| 322 |
+
tlPhases.append({
|
| 323 |
+
"time": 5,
|
| 324 |
+
"availableRoadLinks": rightLaneLinks
|
| 325 |
+
})
|
| 326 |
+
tlPhases.append({
|
| 327 |
+
"time": 30,
|
| 328 |
+
"availableRoadLinks": ((EWLaneLinks | WELaneLinks) & straightLaneLinks) | (rightLaneLinks)
|
| 329 |
+
})
|
| 330 |
+
tlPhases.append({
|
| 331 |
+
"time": 30,
|
| 332 |
+
"availableRoadLinks": ((NSLaneLinks | SNLaneLinks) & straightLaneLinks) | (rightLaneLinks)
|
| 333 |
+
})
|
| 334 |
+
tlPhases.append({
|
| 335 |
+
"time": 30,
|
| 336 |
+
"availableRoadLinks": ((EWLaneLinks | WELaneLinks) & leftLaneLinks) | (rightLaneLinks)
|
| 337 |
+
})
|
| 338 |
+
tlPhases.append({
|
| 339 |
+
"time": 30,
|
| 340 |
+
"availableRoadLinks": ((SNLaneLinks | NSLaneLinks) & leftLaneLinks) | (rightLaneLinks)
|
| 341 |
+
})
|
| 342 |
+
tlPhases.append({
|
| 343 |
+
"time": 30,
|
| 344 |
+
"availableRoadLinks": (WELaneLinks) | (rightLaneLinks)
|
| 345 |
+
})
|
| 346 |
+
tlPhases.append({
|
| 347 |
+
"time": 30,
|
| 348 |
+
"availableRoadLinks": (EWLaneLinks) | (rightLaneLinks)
|
| 349 |
+
})
|
| 350 |
+
tlPhases.append({
|
| 351 |
+
"time": 30,
|
| 352 |
+
"availableRoadLinks": (NSLaneLinks) | (rightLaneLinks)
|
| 353 |
+
})
|
| 354 |
+
tlPhases.append({
|
| 355 |
+
"time": 30,
|
| 356 |
+
"availableRoadLinks": (SNLaneLinks) | (rightLaneLinks)
|
| 357 |
+
})
|
| 358 |
+
else:
|
| 359 |
+
tlPhases.append({
|
| 360 |
+
"time": 30,
|
| 361 |
+
"availableRoadLinks": ((EWLaneLinks | WELaneLinks) & straightLaneLinks) | (rightLaneLinks)
|
| 362 |
+
})
|
| 363 |
+
tlPhases.append({
|
| 364 |
+
"time": 5,
|
| 365 |
+
"availableRoadLinks": rightLaneLinks
|
| 366 |
+
})
|
| 367 |
+
if numLeftLanes:
|
| 368 |
+
tlPhases.append({
|
| 369 |
+
"time": 30,
|
| 370 |
+
"availableRoadLinks": ((EWLaneLinks | WELaneLinks) & leftLaneLinks) | (rightLaneLinks)
|
| 371 |
+
})
|
| 372 |
+
tlPhases.append({
|
| 373 |
+
"time": 5,
|
| 374 |
+
"availableRoadLinks": rightLaneLinks
|
| 375 |
+
})
|
| 376 |
+
tlPhases.append({
|
| 377 |
+
"time": 30,
|
| 378 |
+
"availableRoadLinks": ((NSLaneLinks | SNLaneLinks) & straightLaneLinks) | (rightLaneLinks)
|
| 379 |
+
})
|
| 380 |
+
tlPhases.append({
|
| 381 |
+
"time": 5,
|
| 382 |
+
"availableRoadLinks": rightLaneLinks
|
| 383 |
+
})
|
| 384 |
+
if numLeftLanes:
|
| 385 |
+
tlPhases.append({
|
| 386 |
+
"time": 30,
|
| 387 |
+
"availableRoadLinks": ((SNLaneLinks | NSLaneLinks) & leftLaneLinks) | (rightLaneLinks)
|
| 388 |
+
})
|
| 389 |
+
tlPhases.append({
|
| 390 |
+
"time": 5,
|
| 391 |
+
"availableRoadLinks": rightLaneLinks
|
| 392 |
+
})
|
| 393 |
+
for tlPhase in tlPhases:
|
| 394 |
+
tlPhase["availableRoadLinks"] = list(tlPhase["availableRoadLinks"])
|
| 395 |
+
intersections[i][j] = intersection
|
| 396 |
+
|
| 397 |
+
final_intersecions = []
|
| 398 |
+
for i in range(rowNumber):
|
| 399 |
+
for j in range(columnNumber):
|
| 400 |
+
if not isCorner(i, j):
|
| 401 |
+
# print(i, j)
|
| 402 |
+
final_intersecions.append(intersections[i][j])
|
| 403 |
+
|
| 404 |
+
final_roads = []
|
| 405 |
+
for i in range(rowNumber):
|
| 406 |
+
for j in range(columnNumber):
|
| 407 |
+
for k in range(4):
|
| 408 |
+
if roads[i][j][k] is not None and shouldDraw(roads[i][j][k]):
|
| 409 |
+
ni, nj = i + dy[k], j + dx[k]
|
| 410 |
+
roads[i][j][k].pop("direction")
|
| 411 |
+
roads[i][j][k].pop("fromi")
|
| 412 |
+
roads[i][j][k].pop("fromj")
|
| 413 |
+
roads[i][j][k].pop("toi")
|
| 414 |
+
roads[i][j][k].pop("toj")
|
| 415 |
+
roads[i][j][k]["startIntersection"] = checkIntersection(roads[i][j][k]["startIntersection"], isTruelyInside)
|
| 416 |
+
roads[i][j][k]["endIntersection"] = checkIntersection(roads[i][j][k]["endIntersection"], isTruelyInside)
|
| 417 |
+
final_roads.append(roads[i][j][k])
|
| 418 |
+
|
| 419 |
+
return {
|
| 420 |
+
"intersections": final_intersecions,
|
| 421 |
+
"roads": final_roads
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
if __name__ == "__main__":
|
| 425 |
+
args = parser.parse_args()
|
| 426 |
+
path = '../data/roadnet/'
|
| 427 |
+
grid_config = json.load(open(path + args.config if args.config is not None else path + 'grid66.json'))
|
| 428 |
+
json.dump(gridToRoadnet(**grid_config), open(path + "roadnet66.json", "w"), indent=2)
|
third_party/CityFlow/tools/generator/readme.md
CHANGED
|
@@ -1,41 +1,41 @@
|
|
| 1 |
-
# Generator
|
| 2 |
-
|
| 3 |
-
`generate_grid_scenario.py` can generate NxM grid road network with traffic flows.
|
| 4 |
-
|
| 5 |
-
### Quick Start
|
| 6 |
-
|
| 7 |
-
To generate a 3x4 grid road network
|
| 8 |
-
|
| 9 |
-
```
|
| 10 |
-
python generate_grid_scenario.py 3 4
|
| 11 |
-
```
|
| 12 |
-
|
| 13 |
-
To generate a 3x4 grid road network with 2 straight lanes and with a predefined working traffic light plan
|
| 14 |
-
|
| 15 |
-
```
|
| 16 |
-
python generate_grid_scenario.py 3 4 --numStraightLanes 2 --tlPlan
|
| 17 |
-
```
|
| 18 |
-
|
| 19 |
-
### Other arguments
|
| 20 |
-
- `--rowDistance`: int, default=300, distance between consecutive intersections of each row (East-West Roads
|
| 21 |
-
- `--columnDistance`: int, default=300, distance between consecutive intersections of each column (South-North Roads
|
| 22 |
-
- `--intersectionWidth`: int, default=30
|
| 23 |
-
- `--numLeftLanes`: int, default=1
|
| 24 |
-
- `--numStraightLanes`: int, default=1
|
| 25 |
-
- `--numRightLanes`: int, default=1
|
| 26 |
-
- `--laneMaxSpeed`: int, default=16.67, meters/second
|
| 27 |
-
- `--vehLen`: float, default=5.0, meters
|
| 28 |
-
- `--vehWidth`: float, default=2.0, meters
|
| 29 |
-
- `--vehMaxPosAcc`: float, default=2.0
|
| 30 |
-
- `--vehMaxNegAcc`: float, default=4.5
|
| 31 |
-
- `--vehUsualPosAcc`: float, default=2.0
|
| 32 |
-
- `--vehUsualNegAcc`: float, default=4.5
|
| 33 |
-
- `--vehMinGap`: float, default=2.5
|
| 34 |
-
- `--vehMaxSpeed`: float, default=16.67
|
| 35 |
-
- `--vehHeadwayTime`: float, default=1.5
|
| 36 |
-
- `--dir`: str, default="./"
|
| 37 |
-
- `--roadnetFile`: str, generated road network file
|
| 38 |
-
- `--turn`: if specified, generate turning flows instead of straight flows
|
| 39 |
-
- `--tlPlan`: if specified, generate working predefined traffic signal plan instead of plans with default orders
|
| 40 |
-
- `--interval`: float, default=2.0, time (seconds) between each vehicle for each flow
|
| 41 |
-
- `--flowFile`: str, generated flow file
|
|
|
|
| 1 |
+
# Generator
|
| 2 |
+
|
| 3 |
+
`generate_grid_scenario.py` can generate NxM grid road network with traffic flows.
|
| 4 |
+
|
| 5 |
+
### Quick Start
|
| 6 |
+
|
| 7 |
+
To generate a 3x4 grid road network
|
| 8 |
+
|
| 9 |
+
```
|
| 10 |
+
python generate_grid_scenario.py 3 4
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
To generate a 3x4 grid road network with 2 straight lanes and with a predefined working traffic light plan
|
| 14 |
+
|
| 15 |
+
```
|
| 16 |
+
python generate_grid_scenario.py 3 4 --numStraightLanes 2 --tlPlan
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
### Other arguments
|
| 20 |
+
- `--rowDistance`: int, default=300, distance between consecutive intersections of each row (East-West Roads
|
| 21 |
+
- `--columnDistance`: int, default=300, distance between consecutive intersections of each column (South-North Roads
|
| 22 |
+
- `--intersectionWidth`: int, default=30
|
| 23 |
+
- `--numLeftLanes`: int, default=1
|
| 24 |
+
- `--numStraightLanes`: int, default=1
|
| 25 |
+
- `--numRightLanes`: int, default=1
|
| 26 |
+
- `--laneMaxSpeed`: int, default=16.67, meters/second
|
| 27 |
+
- `--vehLen`: float, default=5.0, meters
|
| 28 |
+
- `--vehWidth`: float, default=2.0, meters
|
| 29 |
+
- `--vehMaxPosAcc`: float, default=2.0
|
| 30 |
+
- `--vehMaxNegAcc`: float, default=4.5
|
| 31 |
+
- `--vehUsualPosAcc`: float, default=2.0
|
| 32 |
+
- `--vehUsualNegAcc`: float, default=4.5
|
| 33 |
+
- `--vehMinGap`: float, default=2.5
|
| 34 |
+
- `--vehMaxSpeed`: float, default=16.67
|
| 35 |
+
- `--vehHeadwayTime`: float, default=1.5
|
| 36 |
+
- `--dir`: str, default="./"
|
| 37 |
+
- `--roadnetFile`: str, generated road network file
|
| 38 |
+
- `--turn`: if specified, generate turning flows instead of straight flows
|
| 39 |
+
- `--tlPlan`: if specified, generate working predefined traffic signal plan instead of plans with default orders
|
| 40 |
+
- `--interval`: float, default=2.0, time (seconds) between each vehicle for each flow
|
| 41 |
+
- `--flowFile`: str, generated flow file
|
training/README.md
CHANGED
|
@@ -1,38 +1,38 @@
|
|
| 1 |
-
# training
|
| 2 |
-
|
| 3 |
-
Training, evaluation, device selection, and dataset utilities for the local multi-agent DQN stack.
|
| 4 |
-
|
| 5 |
-
## Main files
|
| 6 |
-
|
| 7 |
-
- [train_local_policy.py](/Users/aditya/Developer/traffic-llm/training/train_local_policy.py)
|
| 8 |
-
Main CLI for split generation, DQN training, and checkpoint evaluation.
|
| 9 |
-
- [trainer.py](/Users/aditya/Developer/traffic-llm/training/trainer.py)
|
| 10 |
-
Replay buffer, DQN training loop, checkpointing, validation, and aggregate metrics.
|
| 11 |
-
- [rollout.py](/Users/aditya/Developer/traffic-llm/training/rollout.py)
|
| 12 |
-
Evaluation helper shared by learned agents and rule-based baselines.
|
| 13 |
-
- [models.py](/Users/aditya/Developer/traffic-llm/training/models.py)
|
| 14 |
-
Dueling Q-network and running observation normalization.
|
| 15 |
-
- [device.py](/Users/aditya/Developer/traffic-llm/training/device.py)
|
| 16 |
-
Torch device selection for `cuda`, `mps`, or `cpu`.
|
| 17 |
-
- [cityflow_dataset.py](/root/aditya/agentic-traffic/training/cityflow_dataset.py)
|
| 18 |
-
City discovery, split loading, and scenario sampling.
|
| 19 |
-
|
| 20 |
-
## Main entry points
|
| 21 |
-
|
| 22 |
-
- `python3 -m training.train_local_policy make-splits`
|
| 23 |
-
- `python3 -m training.train_local_policy train`
|
| 24 |
-
- `python3 -m training.train_local_policy train --max-val-cities 3 --val-scenarios-per-city 7`
|
| 25 |
-
- `python3 -m training.train_local_policy train --max-train-cities 70 --max-val-cities 3 --val-scenarios-per-city 7 --policy-arch single_head_with_district_feature --reward-variant wait_queue_throughput`
|
| 26 |
-
- `python3 -m training.train_local_policy train --policy-arch single_head_with_district_feature --reward-variant wait_queue_throughput`
|
| 27 |
-
- `python3 -m training.train_local_policy evaluate --checkpoint artifacts/dqn_shared/best_validation.pt --split val`
|
| 28 |
-
- `python3 -m training.train_local_policy evaluate --baseline queue_greedy --split val`
|
| 29 |
-
- `tensorboard --logdir artifacts/dqn_shared/tensorboard`
|
| 30 |
-
|
| 31 |
-
## Training flow
|
| 32 |
-
|
| 33 |
-
1. Load city-level splits from `data/splits/`.
|
| 34 |
-
2. Sample one `(city, scenario)` episode at a time from the train split.
|
| 35 |
-
3. Run one shared Q-network across all controlled intersections in that city.
|
| 36 |
-
4. Collect per-intersection transitions into prioritized replay with n-step returns, using parallel CPU rollout workers by default.
|
| 37 |
-
5. Perform Double DQN updates against a target network.
|
| 38 |
-
6. Periodically evaluate on validation cities and save checkpoints. By default, eval and checkpoint cadence are both 40 updates, and each validation pass also writes an `update_XXXX.pt` checkpoint.
|
|
|
|
| 1 |
+
# training
|
| 2 |
+
|
| 3 |
+
Training, evaluation, device selection, and dataset utilities for the local multi-agent DQN stack.
|
| 4 |
+
|
| 5 |
+
## Main files
|
| 6 |
+
|
| 7 |
+
- [train_local_policy.py](/Users/aditya/Developer/traffic-llm/training/train_local_policy.py)
|
| 8 |
+
Main CLI for split generation, DQN training, and checkpoint evaluation.
|
| 9 |
+
- [trainer.py](/Users/aditya/Developer/traffic-llm/training/trainer.py)
|
| 10 |
+
Replay buffer, DQN training loop, checkpointing, validation, and aggregate metrics.
|
| 11 |
+
- [rollout.py](/Users/aditya/Developer/traffic-llm/training/rollout.py)
|
| 12 |
+
Evaluation helper shared by learned agents and rule-based baselines.
|
| 13 |
+
- [models.py](/Users/aditya/Developer/traffic-llm/training/models.py)
|
| 14 |
+
Dueling Q-network and running observation normalization.
|
| 15 |
+
- [device.py](/Users/aditya/Developer/traffic-llm/training/device.py)
|
| 16 |
+
Torch device selection for `cuda`, `mps`, or `cpu`.
|
| 17 |
+
- [cityflow_dataset.py](/root/aditya/agentic-traffic/training/cityflow_dataset.py)
|
| 18 |
+
City discovery, split loading, and scenario sampling.
|
| 19 |
+
|
| 20 |
+
## Main entry points
|
| 21 |
+
|
| 22 |
+
- `python3 -m training.train_local_policy make-splits`
|
| 23 |
+
- `python3 -m training.train_local_policy train`
|
| 24 |
+
- `python3 -m training.train_local_policy train --max-val-cities 3 --val-scenarios-per-city 7`
|
| 25 |
+
- `python3 -m training.train_local_policy train --max-train-cities 70 --max-val-cities 3 --val-scenarios-per-city 7 --policy-arch single_head_with_district_feature --reward-variant wait_queue_throughput`
|
| 26 |
+
- `python3 -m training.train_local_policy train --policy-arch single_head_with_district_feature --reward-variant wait_queue_throughput`
|
| 27 |
+
- `python3 -m training.train_local_policy evaluate --checkpoint artifacts/dqn_shared/best_validation.pt --split val`
|
| 28 |
+
- `python3 -m training.train_local_policy evaluate --baseline queue_greedy --split val`
|
| 29 |
+
- `tensorboard --logdir artifacts/dqn_shared/tensorboard`
|
| 30 |
+
|
| 31 |
+
## Training flow
|
| 32 |
+
|
| 33 |
+
1. Load city-level splits from `data/splits/`.
|
| 34 |
+
2. Sample one `(city, scenario)` episode at a time from the train split.
|
| 35 |
+
3. Run one shared Q-network across all controlled intersections in that city.
|
| 36 |
+
4. Collect per-intersection transitions into prioritized replay with n-step returns, using parallel CPU rollout workers by default.
|
| 37 |
+
5. Perform Double DQN updates against a target network.
|
| 38 |
+
6. Periodically evaluate on validation cities and save checkpoints. By default, eval and checkpoint cadence are both 40 updates, and each validation pass also writes an `update_XXXX.pt` checkpoint.
|
training/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
from training.cityflow_dataset import CityFlowDataset, ScenarioSpec
|
| 2 |
-
|
| 3 |
-
__all__ = [
|
| 4 |
-
"CityFlowDataset",
|
| 5 |
-
"ScenarioSpec",
|
| 6 |
-
]
|
|
|
|
| 1 |
+
from training.cityflow_dataset import CityFlowDataset, ScenarioSpec
|
| 2 |
+
|
| 3 |
+
__all__ = [
|
| 4 |
+
"CityFlowDataset",
|
| 5 |
+
"ScenarioSpec",
|
| 6 |
+
]
|
training/cityflow_dataset.py
CHANGED
|
@@ -1,202 +1,202 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import random
|
| 4 |
-
from dataclasses import dataclass
|
| 5 |
-
from pathlib import Path
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
DEFAULT_SCENARIOS = (
|
| 9 |
-
"normal",
|
| 10 |
-
"morning_rush",
|
| 11 |
-
"evening_rush",
|
| 12 |
-
"accident",
|
| 13 |
-
"construction",
|
| 14 |
-
"district_overload",
|
| 15 |
-
"event_spike",
|
| 16 |
-
)
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
@dataclass(frozen=True)
|
| 20 |
-
class ScenarioSpec:
|
| 21 |
-
city_id: str
|
| 22 |
-
scenario_name: str
|
| 23 |
-
city_dir: Path
|
| 24 |
-
scenario_dir: Path
|
| 25 |
-
config_path: Path
|
| 26 |
-
roadnet_path: Path
|
| 27 |
-
district_map_path: Path
|
| 28 |
-
metadata_path: Path
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
class CityFlowDataset:
|
| 32 |
-
def __init__(
|
| 33 |
-
self,
|
| 34 |
-
generated_root: str | Path = "data/generated",
|
| 35 |
-
splits_root: str | Path = "data/splits",
|
| 36 |
-
):
|
| 37 |
-
self.generated_root = Path(generated_root)
|
| 38 |
-
self.splits_root = Path(splits_root)
|
| 39 |
-
|
| 40 |
-
def discover_cities(self) -> list[str]:
|
| 41 |
-
return sorted(
|
| 42 |
-
city_dir.name
|
| 43 |
-
for city_dir in self.generated_root.glob("city_*")
|
| 44 |
-
if city_dir.is_dir() and (city_dir / "roadnet.json").exists()
|
| 45 |
-
)
|
| 46 |
-
|
| 47 |
-
def scenarios_for_city(self, city_id: str) -> list[str]:
|
| 48 |
-
scenario_root = self.generated_root / city_id / "scenarios"
|
| 49 |
-
return sorted(
|
| 50 |
-
scenario_dir.name
|
| 51 |
-
for scenario_dir in scenario_root.iterdir()
|
| 52 |
-
if scenario_dir.is_dir()
|
| 53 |
-
and (scenario_dir / "config.json").exists()
|
| 54 |
-
and (scenario_dir / "flow.json").exists()
|
| 55 |
-
)
|
| 56 |
-
|
| 57 |
-
def load_split(self, split_name: str, create_if_missing: bool = True) -> list[str]:
|
| 58 |
-
split_path = self.splits_root / f"{split_name}_cities.txt"
|
| 59 |
-
if not split_path.exists():
|
| 60 |
-
if not create_if_missing:
|
| 61 |
-
raise FileNotFoundError(f"Missing split file: {split_path}")
|
| 62 |
-
self.generate_default_splits()
|
| 63 |
-
|
| 64 |
-
split_cities = [
|
| 65 |
-
line.strip()
|
| 66 |
-
for line in split_path.read_text().splitlines()
|
| 67 |
-
if line.strip()
|
| 68 |
-
]
|
| 69 |
-
available_cities = set(self.discover_cities())
|
| 70 |
-
filtered_cities = [city_id for city_id in split_cities if city_id in available_cities]
|
| 71 |
-
if filtered_cities:
|
| 72 |
-
return filtered_cities
|
| 73 |
-
|
| 74 |
-
if create_if_missing and available_cities:
|
| 75 |
-
splits = self.generate_default_splits(overwrite=True)
|
| 76 |
-
return splits.get(split_name, [])
|
| 77 |
-
|
| 78 |
-
return filtered_cities
|
| 79 |
-
|
| 80 |
-
def generate_default_splits(
|
| 81 |
-
self,
|
| 82 |
-
seed: int = 7,
|
| 83 |
-
train_ratio: float = 0.7,
|
| 84 |
-
val_ratio: float = 0.15,
|
| 85 |
-
overwrite: bool = False,
|
| 86 |
-
) -> dict[str, list[str]]:
|
| 87 |
-
self.splits_root.mkdir(parents=True, exist_ok=True)
|
| 88 |
-
|
| 89 |
-
train_path = self.splits_root / "train_cities.txt"
|
| 90 |
-
val_path = self.splits_root / "val_cities.txt"
|
| 91 |
-
test_path = self.splits_root / "test_cities.txt"
|
| 92 |
-
if not overwrite and train_path.exists() and val_path.exists() and test_path.exists():
|
| 93 |
-
return {
|
| 94 |
-
"train": self.load_split("train", create_if_missing=False),
|
| 95 |
-
"val": self.load_split("val", create_if_missing=False),
|
| 96 |
-
"test": self.load_split("test", create_if_missing=False),
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
city_ids = self.discover_cities()
|
| 100 |
-
rng = random.Random(seed)
|
| 101 |
-
rng.shuffle(city_ids)
|
| 102 |
-
|
| 103 |
-
num_cities = len(city_ids)
|
| 104 |
-
if num_cities == 0:
|
| 105 |
-
splits = {"train": [], "val": [], "test": []}
|
| 106 |
-
elif num_cities == 1:
|
| 107 |
-
splits = {"train": city_ids[:], "val": city_ids[:], "test": city_ids[:]}
|
| 108 |
-
elif num_cities == 2:
|
| 109 |
-
splits = {
|
| 110 |
-
"train": sorted(city_ids[:1]),
|
| 111 |
-
"val": sorted(city_ids[1:2]),
|
| 112 |
-
"test": sorted(city_ids[:1]),
|
| 113 |
-
}
|
| 114 |
-
else:
|
| 115 |
-
train_count = max(1, int(num_cities * train_ratio))
|
| 116 |
-
val_count = int(num_cities * val_ratio)
|
| 117 |
-
if train_count + val_count >= num_cities:
|
| 118 |
-
val_count = max(0, num_cities - train_count - 1)
|
| 119 |
-
test_count = num_cities - train_count - val_count
|
| 120 |
-
if test_count <= 0:
|
| 121 |
-
test_count = 1
|
| 122 |
-
if val_count > 0:
|
| 123 |
-
val_count -= 1
|
| 124 |
-
else:
|
| 125 |
-
train_count = max(1, train_count - 1)
|
| 126 |
-
|
| 127 |
-
splits = {
|
| 128 |
-
"train": sorted(city_ids[:train_count]),
|
| 129 |
-
"val": sorted(city_ids[train_count : train_count + val_count]),
|
| 130 |
-
"test": sorted(city_ids[train_count + val_count :]),
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
for split_name, city_list in splits.items():
|
| 134 |
-
split_path = self.splits_root / f"{split_name}_cities.txt"
|
| 135 |
-
split_path.write_text("\n".join(city_list) + "\n")
|
| 136 |
-
|
| 137 |
-
return splits
|
| 138 |
-
|
| 139 |
-
def build_scenario_spec(self, city_id: str, scenario_name: str) -> ScenarioSpec:
|
| 140 |
-
city_dir = self.generated_root / city_id
|
| 141 |
-
scenario_dir = city_dir / "scenarios" / scenario_name
|
| 142 |
-
return ScenarioSpec(
|
| 143 |
-
city_id=city_id,
|
| 144 |
-
scenario_name=scenario_name,
|
| 145 |
-
city_dir=city_dir,
|
| 146 |
-
scenario_dir=scenario_dir,
|
| 147 |
-
config_path=scenario_dir / "config.json",
|
| 148 |
-
roadnet_path=city_dir / "roadnet.json",
|
| 149 |
-
district_map_path=city_dir / "district_map.json",
|
| 150 |
-
metadata_path=city_dir / "metadata.json",
|
| 151 |
-
)
|
| 152 |
-
|
| 153 |
-
def sample_scenario(
|
| 154 |
-
self,
|
| 155 |
-
split_name: str,
|
| 156 |
-
rng: random.Random,
|
| 157 |
-
city_id: str | None = None,
|
| 158 |
-
scenario_name: str | None = None,
|
| 159 |
-
) -> ScenarioSpec:
|
| 160 |
-
available_cities = self.load_split(split_name)
|
| 161 |
-
if not available_cities:
|
| 162 |
-
available_cities = self.discover_cities()
|
| 163 |
-
if not available_cities:
|
| 164 |
-
raise FileNotFoundError(
|
| 165 |
-
f"No generated cities found under {self.generated_root} for split '{split_name}'."
|
| 166 |
-
)
|
| 167 |
-
selected_city = city_id or rng.choice(available_cities)
|
| 168 |
-
selected_scenario = scenario_name or rng.choice(self.scenarios_for_city(selected_city))
|
| 169 |
-
return self.build_scenario_spec(selected_city, selected_scenario)
|
| 170 |
-
|
| 171 |
-
def iter_scenarios(
|
| 172 |
-
self,
|
| 173 |
-
split_name: str,
|
| 174 |
-
scenarios_per_city: int | None = None,
|
| 175 |
-
max_cities: int | None = None,
|
| 176 |
-
diversify_single_scenario: bool = False,
|
| 177 |
-
) -> list[ScenarioSpec]:
|
| 178 |
-
scenario_specs: list[ScenarioSpec] = []
|
| 179 |
-
city_ids = self.load_split(split_name)
|
| 180 |
-
if max_cities is not None:
|
| 181 |
-
city_ids = city_ids[:max_cities]
|
| 182 |
-
for city_index, city_id in enumerate(city_ids):
|
| 183 |
-
scenario_names = self.scenarios_for_city(city_id)
|
| 184 |
-
if (
|
| 185 |
-
diversify_single_scenario
|
| 186 |
-
and scenarios_per_city == 1
|
| 187 |
-
and scenario_names
|
| 188 |
-
):
|
| 189 |
-
preferred_order = [
|
| 190 |
-
scenario_name
|
| 191 |
-
for scenario_name in DEFAULT_SCENARIOS
|
| 192 |
-
if scenario_name in scenario_names
|
| 193 |
-
]
|
| 194 |
-
if preferred_order:
|
| 195 |
-
scenario_names = [preferred_order[city_index % len(preferred_order)]]
|
| 196 |
-
else:
|
| 197 |
-
scenario_names = [scenario_names[0]]
|
| 198 |
-
elif scenarios_per_city is not None:
|
| 199 |
-
scenario_names = scenario_names[:scenarios_per_city]
|
| 200 |
-
for scenario_name in scenario_names:
|
| 201 |
-
scenario_specs.append(self.build_scenario_spec(city_id, scenario_name))
|
| 202 |
-
return scenario_specs
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import random
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
DEFAULT_SCENARIOS = (
|
| 9 |
+
"normal",
|
| 10 |
+
"morning_rush",
|
| 11 |
+
"evening_rush",
|
| 12 |
+
"accident",
|
| 13 |
+
"construction",
|
| 14 |
+
"district_overload",
|
| 15 |
+
"event_spike",
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@dataclass(frozen=True)
|
| 20 |
+
class ScenarioSpec:
|
| 21 |
+
city_id: str
|
| 22 |
+
scenario_name: str
|
| 23 |
+
city_dir: Path
|
| 24 |
+
scenario_dir: Path
|
| 25 |
+
config_path: Path
|
| 26 |
+
roadnet_path: Path
|
| 27 |
+
district_map_path: Path
|
| 28 |
+
metadata_path: Path
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class CityFlowDataset:
|
| 32 |
+
def __init__(
|
| 33 |
+
self,
|
| 34 |
+
generated_root: str | Path = "data/generated",
|
| 35 |
+
splits_root: str | Path = "data/splits",
|
| 36 |
+
):
|
| 37 |
+
self.generated_root = Path(generated_root)
|
| 38 |
+
self.splits_root = Path(splits_root)
|
| 39 |
+
|
| 40 |
+
def discover_cities(self) -> list[str]:
|
| 41 |
+
return sorted(
|
| 42 |
+
city_dir.name
|
| 43 |
+
for city_dir in self.generated_root.glob("city_*")
|
| 44 |
+
if city_dir.is_dir() and (city_dir / "roadnet.json").exists()
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
def scenarios_for_city(self, city_id: str) -> list[str]:
|
| 48 |
+
scenario_root = self.generated_root / city_id / "scenarios"
|
| 49 |
+
return sorted(
|
| 50 |
+
scenario_dir.name
|
| 51 |
+
for scenario_dir in scenario_root.iterdir()
|
| 52 |
+
if scenario_dir.is_dir()
|
| 53 |
+
and (scenario_dir / "config.json").exists()
|
| 54 |
+
and (scenario_dir / "flow.json").exists()
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
def load_split(self, split_name: str, create_if_missing: bool = True) -> list[str]:
|
| 58 |
+
split_path = self.splits_root / f"{split_name}_cities.txt"
|
| 59 |
+
if not split_path.exists():
|
| 60 |
+
if not create_if_missing:
|
| 61 |
+
raise FileNotFoundError(f"Missing split file: {split_path}")
|
| 62 |
+
self.generate_default_splits()
|
| 63 |
+
|
| 64 |
+
split_cities = [
|
| 65 |
+
line.strip()
|
| 66 |
+
for line in split_path.read_text().splitlines()
|
| 67 |
+
if line.strip()
|
| 68 |
+
]
|
| 69 |
+
available_cities = set(self.discover_cities())
|
| 70 |
+
filtered_cities = [city_id for city_id in split_cities if city_id in available_cities]
|
| 71 |
+
if filtered_cities:
|
| 72 |
+
return filtered_cities
|
| 73 |
+
|
| 74 |
+
if create_if_missing and available_cities:
|
| 75 |
+
splits = self.generate_default_splits(overwrite=True)
|
| 76 |
+
return splits.get(split_name, [])
|
| 77 |
+
|
| 78 |
+
return filtered_cities
|
| 79 |
+
|
| 80 |
+
def generate_default_splits(
|
| 81 |
+
self,
|
| 82 |
+
seed: int = 7,
|
| 83 |
+
train_ratio: float = 0.7,
|
| 84 |
+
val_ratio: float = 0.15,
|
| 85 |
+
overwrite: bool = False,
|
| 86 |
+
) -> dict[str, list[str]]:
|
| 87 |
+
self.splits_root.mkdir(parents=True, exist_ok=True)
|
| 88 |
+
|
| 89 |
+
train_path = self.splits_root / "train_cities.txt"
|
| 90 |
+
val_path = self.splits_root / "val_cities.txt"
|
| 91 |
+
test_path = self.splits_root / "test_cities.txt"
|
| 92 |
+
if not overwrite and train_path.exists() and val_path.exists() and test_path.exists():
|
| 93 |
+
return {
|
| 94 |
+
"train": self.load_split("train", create_if_missing=False),
|
| 95 |
+
"val": self.load_split("val", create_if_missing=False),
|
| 96 |
+
"test": self.load_split("test", create_if_missing=False),
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
city_ids = self.discover_cities()
|
| 100 |
+
rng = random.Random(seed)
|
| 101 |
+
rng.shuffle(city_ids)
|
| 102 |
+
|
| 103 |
+
num_cities = len(city_ids)
|
| 104 |
+
if num_cities == 0:
|
| 105 |
+
splits = {"train": [], "val": [], "test": []}
|
| 106 |
+
elif num_cities == 1:
|
| 107 |
+
splits = {"train": city_ids[:], "val": city_ids[:], "test": city_ids[:]}
|
| 108 |
+
elif num_cities == 2:
|
| 109 |
+
splits = {
|
| 110 |
+
"train": sorted(city_ids[:1]),
|
| 111 |
+
"val": sorted(city_ids[1:2]),
|
| 112 |
+
"test": sorted(city_ids[:1]),
|
| 113 |
+
}
|
| 114 |
+
else:
|
| 115 |
+
train_count = max(1, int(num_cities * train_ratio))
|
| 116 |
+
val_count = int(num_cities * val_ratio)
|
| 117 |
+
if train_count + val_count >= num_cities:
|
| 118 |
+
val_count = max(0, num_cities - train_count - 1)
|
| 119 |
+
test_count = num_cities - train_count - val_count
|
| 120 |
+
if test_count <= 0:
|
| 121 |
+
test_count = 1
|
| 122 |
+
if val_count > 0:
|
| 123 |
+
val_count -= 1
|
| 124 |
+
else:
|
| 125 |
+
train_count = max(1, train_count - 1)
|
| 126 |
+
|
| 127 |
+
splits = {
|
| 128 |
+
"train": sorted(city_ids[:train_count]),
|
| 129 |
+
"val": sorted(city_ids[train_count : train_count + val_count]),
|
| 130 |
+
"test": sorted(city_ids[train_count + val_count :]),
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
for split_name, city_list in splits.items():
|
| 134 |
+
split_path = self.splits_root / f"{split_name}_cities.txt"
|
| 135 |
+
split_path.write_text("\n".join(city_list) + "\n")
|
| 136 |
+
|
| 137 |
+
return splits
|
| 138 |
+
|
| 139 |
+
def build_scenario_spec(self, city_id: str, scenario_name: str) -> ScenarioSpec:
|
| 140 |
+
city_dir = self.generated_root / city_id
|
| 141 |
+
scenario_dir = city_dir / "scenarios" / scenario_name
|
| 142 |
+
return ScenarioSpec(
|
| 143 |
+
city_id=city_id,
|
| 144 |
+
scenario_name=scenario_name,
|
| 145 |
+
city_dir=city_dir,
|
| 146 |
+
scenario_dir=scenario_dir,
|
| 147 |
+
config_path=scenario_dir / "config.json",
|
| 148 |
+
roadnet_path=city_dir / "roadnet.json",
|
| 149 |
+
district_map_path=city_dir / "district_map.json",
|
| 150 |
+
metadata_path=city_dir / "metadata.json",
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
def sample_scenario(
|
| 154 |
+
self,
|
| 155 |
+
split_name: str,
|
| 156 |
+
rng: random.Random,
|
| 157 |
+
city_id: str | None = None,
|
| 158 |
+
scenario_name: str | None = None,
|
| 159 |
+
) -> ScenarioSpec:
|
| 160 |
+
available_cities = self.load_split(split_name)
|
| 161 |
+
if not available_cities:
|
| 162 |
+
available_cities = self.discover_cities()
|
| 163 |
+
if not available_cities:
|
| 164 |
+
raise FileNotFoundError(
|
| 165 |
+
f"No generated cities found under {self.generated_root} for split '{split_name}'."
|
| 166 |
+
)
|
| 167 |
+
selected_city = city_id or rng.choice(available_cities)
|
| 168 |
+
selected_scenario = scenario_name or rng.choice(self.scenarios_for_city(selected_city))
|
| 169 |
+
return self.build_scenario_spec(selected_city, selected_scenario)
|
| 170 |
+
|
| 171 |
+
def iter_scenarios(
|
| 172 |
+
self,
|
| 173 |
+
split_name: str,
|
| 174 |
+
scenarios_per_city: int | None = None,
|
| 175 |
+
max_cities: int | None = None,
|
| 176 |
+
diversify_single_scenario: bool = False,
|
| 177 |
+
) -> list[ScenarioSpec]:
|
| 178 |
+
scenario_specs: list[ScenarioSpec] = []
|
| 179 |
+
city_ids = self.load_split(split_name)
|
| 180 |
+
if max_cities is not None:
|
| 181 |
+
city_ids = city_ids[:max_cities]
|
| 182 |
+
for city_index, city_id in enumerate(city_ids):
|
| 183 |
+
scenario_names = self.scenarios_for_city(city_id)
|
| 184 |
+
if (
|
| 185 |
+
diversify_single_scenario
|
| 186 |
+
and scenarios_per_city == 1
|
| 187 |
+
and scenario_names
|
| 188 |
+
):
|
| 189 |
+
preferred_order = [
|
| 190 |
+
scenario_name
|
| 191 |
+
for scenario_name in DEFAULT_SCENARIOS
|
| 192 |
+
if scenario_name in scenario_names
|
| 193 |
+
]
|
| 194 |
+
if preferred_order:
|
| 195 |
+
scenario_names = [preferred_order[city_index % len(preferred_order)]]
|
| 196 |
+
else:
|
| 197 |
+
scenario_names = [scenario_names[0]]
|
| 198 |
+
elif scenarios_per_city is not None:
|
| 199 |
+
scenario_names = scenario_names[:scenarios_per_city]
|
| 200 |
+
for scenario_name in scenario_names:
|
| 201 |
+
scenario_specs.append(self.build_scenario_spec(city_id, scenario_name))
|
| 202 |
+
return scenario_specs
|