第四章 路网生成(三):用XML定义复杂交叉口
上一章我们聊了怎么用netedit画路网,说实话,画个简单的十字路口确实挺快的。但真实项目里,交叉口往往复杂得多——多相位信号灯、让行规则、甚至还有环岛。这时候纯靠鼠标点,效率就太低了。
我个人习惯是:用XML手写交叉口定义。你想想看,一段几百行的XML,就能精确控制每个车道的转向、每组信号灯的时序、每条支路的优先级。而且可复用、可版本管理,这才是工程化的做法。
4.1 交叉口的核心结构
在SUMO里,交叉口其实就是一个junction节点。但真正决定交叉口行为的,是它连接的edge和lane,以及我们附加的trafficLight和rightOfWay规则。
先看一个最简单的四路交叉口XML骨架:
<junction id="J1" x="1000" y="1000" type="traffic_light">
<request index="0" response="0001" foes="0001" cont="0"/>
<request index="1" response="0010" foes="0010" cont="0"/>
<request index="2" response="0100" foes="0100" cont="0"/>
<request index="3" response="1000" foes="1000" cont="0"/>
</junction>
这里type="traffic_light"告诉SUMO:这个交叉口由信号灯控制。而request元素定义了每个进口道的冲突关系。嗯,这里要注意——response和foes的值是二进制掩码,每一位代表一个流向是否冲突。我第一次看这个的时候也懵了,后来发现其实有规律:0001表示只有第0个流向是冲突的。
核心要点:交叉口的行为由junction的type属性和内部的request列表共同决定。type决定了控制方式,request决定了谁让谁。
4.2 信号灯交叉口定义
信号灯交叉口,说白了就是给交叉口配一套红绿灯方案。SUMO里用tlLogic元素来定义。
我曾经在做一个城市主干道项目时,需要模拟一个五相位信号灯。当时用netedit配了半天,总是对不齐相位时间。后来直接写XML,十分钟搞定。
<tlLogic id="J1" type="static" programID="default" offset="0">
<phase duration="30" state="GGGgrrrrrGGGgrrrrr"/>
<phase duration="3" state="yyygrrrrryyygrrrrr"/>
<phase duration="2" state="rrrgrrrrrrrrgrrrrr"/>
<phase duration="30" state="rrrrGGGgrrrrGGGg"/>
<phase duration="3" state="rrrryyygrrrrryyyg"/>
<phase duration="2" state="rrrrrrrgrrrrrrrrg"/>
</tlLogic>
这里state字符串的每个字符代表一个信号灯组:
| 字符 | 含义 |
|---|---|
| G | 绿灯(直行或右转) |
| g | 绿灯(仅左转) |
| y | 黄灯 |
| r | 红灯 |
你想想看,这个字符串的顺序必须和交叉口连接的车道顺序一致。我刚开始做的时候,顺序搞反了,结果左转绿灯亮的时候直行也在走,差点出事故——当然,仿真里的事故就是数据异常。
我的小技巧:写信号灯方案前,先在纸上画出每个相位的流向图。然后按SUMO要求的顺序(通常是顺时针从北开始)排列state字符串。这样不容易乱。
4.3 优先规则(让行/停让)
不是所有交叉口都有信号灯。很多次干路交叉口用的是让行规则。SUMO里用rightOfWay和priority来控制。
举个例子,一个T型路口,支路需要让行主路:
<junction id="J2" x="2000" y="1500" type="priority">
<request index="0" response="0011" foes="0011" cont="0"/>
<request index="1" response="0011" foes="0011" cont="0"/>
<request index="2" response="1100" foes="1100" cont="0"/>
<request index="3" response="1100" foes="1100" cont="0"/>
</junction>
这里type="priority"表示优先规则控制。注意看response的值:主路方向(index 0和1)的response是0011,表示它们不需要让行支路(因为支路的掩码位是1和0?不对,这里要仔细看)。
其实SUMO的优先规则逻辑是这样的:
- response:当前流向需要让行的其他流向掩码
- foes:与当前流向冲突的所有流向掩码
如果response全为0,表示这个流向是主路,不需要让任何人。如果response非0,表示它需要让那些掩码对应的流向。
我曾经踩过的坑:在定义让行规则时,忘记设置cont="0"。结果车辆在交叉口内犹豫不决,一直停在原地。后来查文档才发现,cont表示是否允许连续通过,0表示必须完全停车观察。
4.4 复杂交叉口实战案例
好了,理论说完了,我们来看一个真实项目中的案例。这是一个五岔路口,包含信号灯和让行规则的混合控制。
先画个结构图:
对应的XML定义如下:
<!-- 五岔路口定义 -->
<junction id="J1" x="3000" y="2000" type="traffic_light">
<!-- 北向和南向是信号灯控制的主路 -->
<request index="0" response="0000" foes="1110" cont="0"/>
<request index="1" response="0000" foes="1101" cont="0"/>
<!-- 东向和西向是让行支路 -->
<request index="2" response="0011" foes="1011" cont="0"/>
<request index="3" response="0011" foes="0111" cont="0"/>
<!-- 西北向是停让支路 -->
<request index="4" response="1111" foes="1111" cont="0"/>
</junction>
<!-- 信号灯方案 -->
<tlLogic id="J1" type="static" programID="phase1" offset="0">
<phase duration="40" state="GGGgrrrrrrrrrrr"/>
<phase duration="3" state="yyygrrrrrrrrrrr"/>
<phase duration="2" state="rrrrrrrrrrrrrrr"/>
<phase duration="40" state="rrrrGGGgrrrrrrr"/>
<phase duration="3" state="rrrryyygrrrrrrr"/>
<phase duration="2" state="rrrrrrrrrrrrrrr"/>
</tlLogic>
这里要注意:信号灯只控制北向和南向(前两个流向)。东向和西向虽然也经过交叉口,但它们受让行规则控制,信号灯state里对应的字符是r(红灯)——实际上它们不看信号灯,只看让行标志。
关键理解:在SUMO里,信号灯和让行规则可以共存。信号灯控制的是它管辖的流向,其他流向由request中的response决定优先级。这种混合控制在实际项目中非常常见。
4.5 验证与调试
写完了XML,怎么验证对不对?我一般用三步:
- 语法检查:用
sumo -c your.sumocfg --save-configuration看有没有报错 - 可视化检查:用
sumo-gui打开,观察车辆是否按预期走 - 数据检查:看
tripinfo.xml里的等待时间、停车次数是否合理
我曾经遇到一个情况:车辆在交叉口内全部堵死,怎么调参数都没用。后来用--junction-output选项导出交叉口状态,才发现有一个request的foes写错了,导致两个流向互相死锁。
调试小工具:在sumo-gui里,右键点击交叉口,选择"Show Phase Definition",可以实时看到当前信号灯状态和每个流向的优先级。这个功能我几乎天天用。
4.6 性能优化建议
当路网规模变大,交叉口数量上千时,XML文件的解析效率就成了问题。我个人建议:
- 使用
<include>标签拆分文件,比如信号灯方案单独放一个文件 - 避免在
request中使用过于复杂的掩码,能用简单规则就用简单规则 - 对于规则相同的交叉口,使用
<type>模板复用
嗯,说到模板复用,SUMO支持定义交叉口类型:
<type id="standard_cross" priority="1" numLanes="2" speed="13.89"/>
<junction id="J10" x="5000" y="3000" type="standard_cross"/>
<junction id="J11" x="5100" y="3000" type="standard_cross"/>
这样当你要修改所有标准交叉口的参数时,只需要改一处。我在做城市级路网时,这个特性帮我省了不少时间。
好了,关于用XML定义复杂交叉口,核心就是这些。记住:信号灯管时序,让行管优先级,request管冲突。这三样搞明白了,再复杂的交叉口你也能用XML写出来。
最后提醒一句:XML定义交叉口虽然强大,但不要过度设计。能用简单规则解决的,就别搞复杂的信号灯方案。仿真不是炫技,是解决问题。